pax_global_header00006660000000000000000000000064136764257360014535gustar00rootroot0000000000000052 comment=7986cdcc41d4131e1d6d866e8c80925921e25969 msrestazure-for-python-0.6.4/000077500000000000000000000000001367642573600162335ustar00rootroot00000000000000msrestazure-for-python-0.6.4/.gitignore000066400000000000000000000002261367642573600202230ustar00rootroot00000000000000__pycache__ *.pyc .venv msrestazure.egg-info .tox tests/.coverage autorest .pytest_cache .cache .coverage coverage.xml Pipfile.lock build dist .vscodemsrestazure-for-python-0.6.4/.travis.yml000066400000000000000000000027121367642573600203460ustar00rootroot00000000000000dist: xenial sudo: required language: python cache: pip _autorest_install: &_autorest_install before_install: - git clone --recursive https://github.com/Azure/autorest.python.git - sudo apt-get install libunwind8-dev - nvm install 8 - pushd autorest.python - npm install # Install test server pre-requisites - popd matrix: include: - python: 2.7 env: TOXENV=py27 - python: 3.5 env: TOXENV=py35 - python: 3.6 env: TOXENV=py36 - python: 3.7 env: TOXENV=py37 - python: 3.8 env: TOXENV=py38 - python: 2.7 env: TOXENV=py27-autorest <<: *_autorest_install - python: 3.5 env: TOXENV=py35-autorest <<: *_autorest_install - python: 3.6 env: TOXENV=py36-autorest <<: *_autorest_install - python: 3.7 env: TOXENV=py37-autorest <<: *_autorest_install - python: 3.8 env: TOXENV=py38-autorest <<: *_autorest_install allow_failures: - env: TOXENV=py27-autorest - env: TOXENV=py35-autorest - env: TOXENV=py36-autorest - env: TOXENV=py37-autorest - env: TOXENV=py38-autorest install: - pip install tox script: - tox after_success: - bash <(curl -s https://codecov.io/bash) -e TOXENV -f $TRAVIS_BUILD_DIR/test/coverage.xml deploy: provider: pypi user: Laurent.Mazuel skip_upload_docs: true skip_cleanup: true # password: use $PYPI_PASSWORD distributions: "sdist bdist_wheel" on: tags: true python: '3.6' msrestazure-for-python-0.6.4/LICENSE.md000066400000000000000000000020601367642573600176350ustar00rootroot00000000000000MIT License Copyright (c) 2016 Microsoft Azure Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. msrestazure-for-python-0.6.4/MANIFEST.in000066400000000000000000000000531367642573600177670ustar00rootroot00000000000000include *.rst recursive-include tests *.py msrestazure-for-python-0.6.4/README.rst000066400000000000000000000342611367642573600177300ustar00rootroot00000000000000AutoRest: Python Client Runtime - Azure Module =============================================== .. image:: https://travis-ci.org/Azure/msrestazure-for-python.svg?branch=master :target: https://travis-ci.org/Azure/msrestazure-for-python .. image:: https://codecov.io/gh/azure/msrestazure-for-python/branch/master/graph/badge.svg :target: https://codecov.io/gh/azure/msrestazure-for-python Installation ------------ To install: .. code-block:: bash $ pip install msrestazure Release History --------------- 2020-06-29 Version 0.6.4 ++++++++++++++++++++++++ **Bugfix** - Unable to raise exception if JSON body contains UTF-8 characters on Python 2 #150 2020-03-17 Version 0.6.3 ++++++++++++++++++++++++ **Bugfix** - Unable to raise exception if JSON body contains UTF-8 characters #144 - Prepare old poller implementation to Python 3.9 #138 **Features** - Add Microsoft Graph to Cloud environment #142 Thanks to @psignoret and @tirkarthi for his contribution 2019-09-16 Version 0.6.2 ++++++++++++++++++++++++ **Bugfix** - Fix ARM error parsing if Type info is used #135 2019-06-10 Version 0.6.1 ++++++++++++++++++++++++ **Features** - Add User Assigned identity support for WebApp/Functions #124 - Add timeout parameter for MSI token, is used from a VM #131 Thanks to @noelbundick for his contribution 2018-12-17 Version 0.6.0 ++++++++++++++++++++++++ **Features** - Implementation of LRO async, based on msrest 0.6.x series (*experimental*) **Disclaimer** - This version contains no direct breaking changes, but is bumped to 0.6.x since it requires a breaking change version of msrest. Thanks to @gison93 for his documentation contribution 2018-11-01 Version 0.5.1 ++++++++++++++++++++++++ **Bugfixes** - Fix CloudError if response and error message are provided at the same time #114 - Fix LRO polling if last call is an empty Location (Autorest.Python 3.x only) #120 **Features** - Altered resource id parsing logic to allow for resource group IDs #117 2018-08-02 Version 0.5.0 ++++++++++++++++++++++++ **Features** - Implementation is now using ADAL and not request-oauthlib. This allows more AD scenarios (like federated) #94 - Add additionalInfo parsing for CloudError #102 **Breaking changes** These breaking changes applies to ServicePrincipalCredentials, UserPassCredentials, AADTokenCredentials - Remove "auth_uri" attribute and parameter. This was unused. - Remove "state" attribute. This was unused. - Remove "client" attribute. This was exposed by mistake and should have been internal. No replacement is possible. - Remove "token_uri" attribute and parameter. Use "cloud_environment" and "tenant" to impact the login url now. - Remove token caching based on "keyring". Token caching should be implemented using ADAL now. This implies: - Remove the "keyring" parameter - Remove the "clear_cached_token" method - Remove the "retrieve_session" method 2018-07-03 Version 0.4.35 +++++++++++++++++++++++++ **Bugfixes** - MSIAuthentication regression for KeyVault since IMDS support #109 2018-07-02 Version 0.4.34 +++++++++++++++++++++++++ **Bugfixes** - MSIAuthentication should initialize the token attribute on creation #106 2018-06-21 Version 0.4.33 +++++++++++++++++++++++++ **Bugfixes** - Fixes refreshToken in UserPassCredentials and AADTokenCredentials #103 - Fix US government cloud definition #104 Thanks to mjcaley for his contribution 2018-06-13 Version 0.4.32 +++++++++++++++++++++++++ **Features** - Implement new LRO options of Autorest #101 **Bug fixes** - Reduce max MSI polling time for VM #100 2018-05-17 Version 0.4.31 +++++++++++++++++++++++++ **Features** - Improve MSI for VM token polling algorithm 2018-05-16 Version 0.4.30 +++++++++++++++++++++++++ **Features** - Allow ADAL 0.5.0 to 2.0.0 excluded as valid ADAL dependency 2018-04-30 Version 0.4.29 +++++++++++++++++++++++++ **Bugfixes** - Fix refresh Token on `AADTokenCredentials` (was broken in 0.4.27) - Now `UserPasswordCredentials` correctly use the refreshToken, and not user/password to refresh the session (was broken in 0.4.27) - Bring back `keyring`, with minimal dependency 12.0.2 that fixes the installation problem on old Python 2018-04-23 Version 0.4.28 +++++++++++++++++++++++++ **Disclaimer** Do to some stability issues with "keyring" dependency that highly change from one system to another, this package is no longer a dependency of "msrestazure". If you were using the secured token cache of `ServicePrincipalCredentials` and `UserPassCredentials`, the feature is still available, but you need to install manually "keyring". The functionnality will activate automatically. 2018-04-18 Version 0.4.27 +++++++++++++++++++++++++ **Features** - Implements new features of msrest 0.4.28 on session improvement. See msrest ChangeLog for details. Update msrest dependency to 0.4.28 2018-04-17 Version 0.4.26 +++++++++++++++++++++++++ **Bugfixes** - IMDS/MSI: Retry on more error codes (#87) - IMDS/MSI: fix a boundary case on timeout (#86) 2018-03-29 Version 0.4.25 +++++++++++++++++++++++++ **Features** - MSIAuthentication now uses IMDS endpoint if available - MSIAuthentication can be used in any environment that defines MSI_ENDPOINT env variable 2018-03-26 Version 0.4.24 +++++++++++++++++++++++++ **Bugfix** - Fix parse_resource_id() tool to be case-insensitive to keywords when matching #81 - Add missing baseclass init call for AdalAuthentication #82 2018-03-19 Version 0.4.23 +++++++++++++++++++++++++ **Bugfix** - Fix LRO result if POST uses AsyncOperation header (Autorest.Python 3.0 only) #79 2018-02-27 Version 0.4.22 +++++++++++++++++++++++++ **Bugfix** - Remove a possible infinite loop with MSIAuthentication #77 **Disclaimer** From this version, MSIAuthentication will fail instantly if you try to get MSI token from a VM where the extension is not installed, or not yet ready. You need to do your own retry mechanism if you think the extension is provisioning and the call might succeed later. This behavior is consistent with other Azure SDK implementation of MSI scenarios. 2018-01-26 Version 0.4.21 +++++++++++++++++++++++++ - Update allowed ADAL dependency to 0.5.x 2018-01-08 Version 0.4.20 +++++++++++++++++++++++++ **Features** - CloudError now includes the "innererror" attribute to match OData v4 #73 - Introduces ARMPolling implementation of Azure Resource Management LRO. Requires msrest 0.4.25 (new dependency). This is used by code generated with Autorest.Python 3.0, and is not used by code generated by previous Autorest version. - Change msrest dependency to ">=0.4.25,<2.0.0" to allow (future) msrest 1.0.0 as compatible dependency. Thank you to demyanenko for his contribution. 2017-12-14 Version 0.4.19 +++++++++++++++++++++++++ **Feature** * Improve MSIAuthentication to support User Assigned Identity #70 **Bugfixes** * Fix session obj for cloudmetadata endpoint #67 * Fix authentication resource node for AzureSatck #65 * Better detection of AppService with MSIAuthentication #70 2017-12-01 Version 0.4.18 +++++++++++++++++++++++++ **Bugfixes** - get_cloud_from_metadata_endpoint incorrect on AzureStack #62 - get_cloud_from_metadata_endpoint certificate issue #61 2017-11-22 Version 0.4.17 +++++++++++++++++++++++++ **Bugfixes** - Fix AttributeError if error JSON from ARM does not follow ODatav4 (as it should) 2017-10-31 Version 0.4.16 +++++++++++++++++++++++++ **Bugfixes** - Fix AttributeError if input JSON is not a dict (#54) 2017-10-13 Version 0.4.15 +++++++++++++++++++++++++ **Features** - Add support for WebApp/Functions in MSIAuthentication classes - Add parse_resource_id(), resource_id(), validate_resource_id() to parse ARM ids - Retry strategy now n reach 24 seconds (instead of 12 seconds) 2017-09-11 Version 0.4.14 +++++++++++++++++++++++++ **Features** - Add Managed Service Integrated (MSI) authentication **Bug fix** - Fix AdalError handling in some scenarios (#44) Thank you to Hexadite-Omer for his contribution 2017-08-24 Version 0.4.13 +++++++++++++++++++++++++ **Features** - "keyring" is now completely optional 2017-08-23 Version 0.4.12 +++++++++++++++++++++++++ **Features** - add "timeout" to ServicePrincipalCredentials and UserPasswordCredentials - Threads created by AzureOperationPoller have now a name prefixed by "AzureOperationPoller" to help identify them **Bugfixes** - Do not fail if keyring is badly installed - Update Azure Gov login endpoint - Update metadata ARM endpoint parser **Breaking changes** - Remove InteractiveCredentials. This class was deprecated and unusable. Use ADAL device code instead. 2017-06-29 Version 0.4.11 +++++++++++++++++++++++++ **Features** - Add cloud definitions for public Azure, German Azure, China Azure and Azure Gov - Add get_cloud_from_metadata_endpoint to automatically create a Cloud object from an ARM endpoint - Add `cloud_environment` to all Credentials objects (except AdalAuthentication) **Note** - This deprecates "china=True", to be replaced by "cloud_environment=AZURE_CHINA_CLOUD" Example: .. code:: python from msrestazure.azure_cloud import AZURE_CHINA_CLOUD from msrestazure.azure_active_directory import UserPassCredentials credentials = UserPassCredentials( login, password, cloud_environment=AZURE_CHINA_CLOUD ) `base_url` of SDK client can be pointed to "cloud_environment.endpoints.resource_manager" for basic scenario: Example: .. code:: python from msrestazure.azure_cloud import AZURE_CHINA_CLOUD from msrestazure.azure_active_directory import UserPassCredentials from azure.mgmt.resource import ResourceManagementClient credentials = UserPassCredentials( login, password, cloud_environment=AZURE_CHINA_CLOUD ) client = ResourceManagementClient( credentials, subscription_id, base_url=AZURE_CHINA_CLOUD.endpoints.resource_manager ) Azure Stack connection can be done: .. code:: python from msrestazure.azure_cloud import get_cloud_from_metadata_endpoint from msrestazure.azure_active_directory import UserPassCredentials from azure.mgmt.resource import ResourceManagementClient mystack_cloud = get_cloud_from_metadata_endpoint("https://myazurestack-arm-endpoint.com") credentials = UserPassCredentials( login, password, cloud_environment=mystack_cloud ) client = ResourceManagementClient( credentials, subscription_id, base_url=mystack_cloud.endpoints.resource_manager ) 2017-06-27 Version 0.4.10 +++++++++++++++++++++++++ **Bugfixes** - Accept PATCH/201 as LRO valid state - Close token session on exit (ServicePrincipal and UserPassword credentials) 2017-06-19 Version 0.4.9 ++++++++++++++++++++++++ **Features** - Add proxies parameters to ServicePrincipal and UserPassword credentials class #29 - Add automatic Azure provider registration if needed (requires msrest 0.4.10) #28 Thank you to likel for his contribution 2017-05-31 Version 0.4.8 ++++++++++++++++++++++++ **Bugfixes** - Fix LRO if first call never returns 200, but ends on 201 (#26) - FiX LRO AttributeError if timeout is short (#21) **Features** - Expose a "status()" method in AzureOperationPoller (#18) 2017-01-23 Version 0.4.7 ++++++++++++++++++++++++ **Bugfixes** - Adding `accept_language` and `generate_client_request_id` default values 2016-12-12 Version 0.4.6 ++++++++++++++++++++++++ **Bugfixes** Refactor Long Running Operation algorithm. - There is no breaking changes, however you might need to record again your offline HTTP records if you use unittests with VCRpy. - Fix a couple of latent bugs 2016-11-30 Version 0.4.5 ++++++++++++++++++++++++ **New features** - Add AdalAuthentification class to wrap ADAL library (https://github.com/Azure/msrestazure-for-python/pull/8) 2016-10-17 Version 0.4.4 ++++++++++++++++++++++++ **Bugfixes** - More informative and well-formed CloudError exceptions (https://github.com/Azure/autorest/issues/1460) - Raise CustomException is defined in Swagger (https://github.com/Azure/autorest/issues/1404) 2016-09-14 Version 0.4.3 ++++++++++++++++++++++++ **Bugfixes** - Make AzureOperationPoller thread as daemon (do not block anymore a Ctrl+C) (https://github.com/Azure/autorest/pull/1379) 2016-09-01 Version 0.4.2 ++++++++++++++++++++++++ **Bugfixes** - Better exception message (https://github.com/Azure/autorest/pull/1300) This version needs msrest >= 0.4.3 2016-06-08 Version 0.4.1 ++++++++++++++++++++++++ **Bugfixes** - Fix for LRO PUT operation https://github.com/Azure/autorest/issues/1133 2016-05-25 Version 0.4.0 ++++++++++++++++++++++++ Update msrest dependency to 0.4.0 **Bugfixes** - Fix for several AAD issues https://github.com/Azure/autorest/issues/1055 - Fix for LRO PATCH bug and refactor https://github.com/Azure/autorest/issues/993 **Behaviour changes** - Needs Autorest > 0.17.0 Nightly 20160525 2016-04-26 Version 0.3.0 ++++++++++++++++++++++++ Update msrest dependency to 0.3.0 **Bugfixes** - Read only values are no longer in __init__ or sent to the server (https://github.com/Azure/autorest/pull/959) - Useless kwarg removed **Behaviour changes** - Needs Autorest > 0.16.0 Nightly 20160426 2016-03-31 Version 0.2.1 ++++++++++++++++++++++++ **Bugfixes** - Fix AzurePollerOperation if Swagger defines provisioning status as enum type (https://github.com/Azure/autorest/pull/892) 2016-03-25 Version 0.2.0 ++++++++++++++++++++++++ Update msrest dependency to 0.2.0 **Behaviour change** - async methods called with raw=True don't return anymore AzureOperationPoller but ClientRawResponse - Needs Autorest > 0.16.0 Nightly 20160324 2016-03-21 Version 0.1.2 ++++++++++++++++++++++++ Update msrest dependency to 0.1.3 **Bugfixes** - AzureOperationPoller.wait() failed to raise exception if query error (https://github.com/Azure/autorest/pull/856) 2016-03-04 Version 0.1.1 ++++++++++++++++++++++++ **Bugfixes** - Source package corrupted in Pypi (https://github.com/Azure/autorest/issues/799) 2016-03-04 Version 0.1.0 ++++++++++++++++++++++++ **Behaviour change** - Replaced _required attribute in CloudErrorData class with _validation dict. 2016-02-29 Version 0.0.2 ++++++++++++++++++++++++ **Bugfixes** - Fixed AAD bug to include connection verification in UserPassCredentials. (https://github.com/Azure/autorest/pull/725) - Source package corrupted in Pypi (https://github.com/Azure/autorest/issues/718) 2016-02-19 Version 0.0.1 ++++++++++++++++++++++++ - Initial release. msrestazure-for-python-0.6.4/dev_requirements.txt000066400000000000000000000001671367642573600223610ustar00rootroot00000000000000-e . mock;python_version<="2.7" httpretty coverage<5.0.0 pytest pytest-cov pytest-asyncio;python_full_version>="3.5.2" msrestazure-for-python-0.6.4/doc/000077500000000000000000000000001367642573600170005ustar00rootroot00000000000000msrestazure-for-python-0.6.4/doc/conf.py000066400000000000000000000172521367642573600203060ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # azure-sdk-for-python documentation build configuration file, created by # sphinx-quickstart on Fri Jun 27 15:42:45 2014. # # 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. import sys import os import pip # 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 = ['sphinx.ext.autodoc', 'sphinx.ext.autosummary', 'sphinx.ext.doctest', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx'] intersphinx_mapping = { 'python': ('https://docs.python.org/3.5', None), 'msrest': ('http://msrest.readthedocs.org/en/latest/', None), 'requests': ('http://docs.python-requests.org/en/master/', None) } # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] source_parsers = { '.md': 'recommonmark.parser.CommonMarkParser', } # The suffix of source filenames. source_suffix = ['.rst', '.md'] # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'msrestazure' copyright = u'2016-2018, Microsoft' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0.5.1' # The full version, including alpha/beta/rc tags. release = '0.5.1' # 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 = ['_build'] # 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 = 'sphinx' # 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 extensions ---------------------------------------------------- autoclass_content = 'both' # -- 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 = 'default' #html_theme_options = {'collapsiblesidebar': True} # Activate the theme. #pip.main(['install', 'sphinx_bootstrap_theme']) #import sphinx_bootstrap_theme #html_theme = 'bootstrap' #html_theme_path = sphinx_bootstrap_theme.get_html_theme_path() # 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 not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # 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 = 'msrestazure-doc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # 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', 'msrestazure.tex', u'msrest Documentation', u'Microsoft', '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 msrestazure-for-python-0.6.4/doc/index.md000066400000000000000000000004441367642573600204330ustar00rootroot00000000000000 msrestazure's documentation has moved from ReadTheDocs to docs.microsoft.com. msrestazure-for-python-0.6.4/doc/make.bat000066400000000000000000000145071367642573600204140ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pydocumentdb.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pydocumentdb.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end msrestazure-for-python-0.6.4/doc/requirements.txt000066400000000000000000000000441367642573600222620ustar00rootroot00000000000000sphinx sphinx_rtd_theme recommonmarkmsrestazure-for-python-0.6.4/msrestazure.pyproj000066400000000000000000000057431367642573600220750ustar00rootroot00000000000000 Debug 2.0 {b80e5ecc-dcdc-4d31-b3be-8c32dea8e864} msrestazure\__init__.py ..\msrest\;. . . clientruntime client_runtime true false true false Code Code Code Code Code Code Code Code 10.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Python Tools\Microsoft.PythonTools.targets msrestazure-for-python-0.6.4/msrestazure/000077500000000000000000000000001367642573600206175ustar00rootroot00000000000000msrestazure-for-python-0.6.4/msrestazure/__init__.py000066400000000000000000000026761367642573600227430ustar00rootroot00000000000000# -------------------------------------------------------------------------- # # Copyright (c) Microsoft Corporation. All rights reserved. # # The MIT License (MIT) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the ""Software""), to # deal in the Software without restriction, including without limitation the # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or # sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # # -------------------------------------------------------------------------- from .azure_configuration import AzureConfiguration from .version import msrestazure_version __all__ = ["AzureConfiguration"] __version__ = msrestazure_version msrestazure-for-python-0.6.4/msrestazure/azure_active_directory.py000066400000000000000000000716541367642573600257530ustar00rootroot00000000000000# -------------------------------------------------------------------------- # # Copyright (c) Microsoft Corporation. All rights reserved. # # The MIT License (MIT) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the ""Software""), to # deal in the Software without restriction, including without limitation the # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or # sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # # -------------------------------------------------------------------------- import ast import os import logging import re import time import warnings try: from urlparse import urlparse, parse_qs except ImportError: from urllib.parse import urlparse, parse_qs import adal from requests import RequestException, ConnectionError, HTTPError import requests from msrest.authentication import OAuthTokenAuthentication, Authentication, BasicTokenAuthentication from msrest.exceptions import TokenExpiredError as Expired from msrest.exceptions import AuthenticationError, raise_with_traceback from msrestazure.azure_cloud import AZURE_CHINA_CLOUD, AZURE_PUBLIC_CLOUD from msrestazure.azure_configuration import AzureConfiguration from msrestazure.azure_exceptions import MSIAuthenticationTimeoutError _LOGGER = logging.getLogger(__name__) class AADMixin(OAuthTokenAuthentication): """Mixin for Authentication object. Provides some AAD functionality: - Token caching and retrieval - Default AAD configuration """ _case = re.compile('([a-z0-9])([A-Z])') def _configure(self, **kwargs): """Configure authentication endpoint. Optional kwargs may include: - cloud_environment (msrestazure.azure_cloud.Cloud): A targeted cloud environment - china (bool): Configure auth for China-based service, default is 'False'. - tenant (str): Alternative tenant, default is 'common'. - resource (str): Alternative authentication resource, default is 'https://management.core.windows.net/'. - verify (bool): Verify secure connection, default is 'True'. - timeout (int): Timeout of the request in seconds. - proxies (dict): Dictionary mapping protocol or protocol and hostname to the URL of the proxy. - cache (adal.TokenCache): A adal.TokenCache, see ADAL configuration for details. This parameter is not used here and directly passed to ADAL. """ if kwargs.get('china'): err_msg = ("china parameter is deprecated, " "please use " "cloud_environment=msrestazure.azure_cloud.AZURE_CHINA_CLOUD") warnings.warn(err_msg, DeprecationWarning) self._cloud_environment = AZURE_CHINA_CLOUD else: self._cloud_environment = AZURE_PUBLIC_CLOUD self._cloud_environment = kwargs.get('cloud_environment', self._cloud_environment) auth_endpoint = self._cloud_environment.endpoints.active_directory resource = self._cloud_environment.endpoints.active_directory_resource_id self._tenant = kwargs.get('tenant', "common") self._verify = kwargs.get('verify') # 'None' will honor ADAL_PYTHON_SSL_NO_VERIFY self.resource = kwargs.get('resource', resource) self._proxies = kwargs.get('proxies') self._timeout = kwargs.get('timeout') self._cache = kwargs.get('cache') self.store_key = "{}_{}".format( auth_endpoint.strip('/'), self.store_key) self.secret = None self._context = None # Future ADAL context def _create_adal_context(self): authority_url = self.cloud_environment.endpoints.active_directory is_adfs = bool(re.match('.+(/adfs|/adfs/)$', authority_url, re.I)) if is_adfs: authority_url = authority_url.rstrip('/') # workaround: ADAL is known to reject auth urls with trailing / else: authority_url = authority_url + '/' + self._tenant self._context = adal.AuthenticationContext( authority_url, timeout=self._timeout, verify_ssl=self._verify, proxies=self._proxies, validate_authority=not is_adfs, cache=self._cache, api_version=None ) def _destroy_adal_context(self): self._context = None @property def verify(self): return self._verify @verify.setter def verify(self, value): self._verify = value self._destroy_adal_context() @property def proxies(self): return self._proxies @proxies.setter def proxies(self, value): self._proxies = value self._destroy_adal_context() @property def timeout(self): return self._timeout @timeout.setter def timeout(self, value): self._timeout = value self._destroy_adal_context() @property def cloud_environment(self): return self._cloud_environment @cloud_environment.setter def cloud_environment(self, value): self._cloud_environment = value self._destroy_adal_context() def _convert_token(self, token): """Convert token fields from camel case. :param dict token: An authentication token. :rtype: dict """ # Beware that ADAL returns a pointer to its own dict, do # NOT change it in place token = token.copy() # If it's from ADAL, expiresOn will be in ISO form. # Bring it back to float, using expiresIn if "expiresOn" in token and "expiresIn" in token: token["expiresOn"] = token['expiresIn'] + time.time() return {self._case.sub(r'\1_\2', k).lower(): v for k, v in token.items()} def _parse_token(self): # AD answers 'expires_on', and Python oauthlib expects 'expires_at' if 'expires_on' in self.token and 'expires_at' not in self.token: self.token['expires_at'] = self.token['expires_on'] if self.token.get('expires_at'): countdown = float(self.token['expires_at']) - time.time() self.token['expires_in'] = countdown def set_token(self): if not self._context: self._create_adal_context() def signed_session(self, session=None): """Create token-friendly Requests session, using auto-refresh. Used internally when a request is made. If a session object is provided, configure it directly. Otherwise, create a new session and return it. :param session: The session to configure for authentication :type session: requests.Session """ self.set_token() # Adal does the caching. self._parse_token() return super(AADMixin, self).signed_session(session) def refresh_session(self, session=None): """Return updated session if token has expired, attempts to refresh using newly acquired token. If a session object is provided, configure it directly. Otherwise, create a new session and return it. :param session: The session to configure for authentication :type session: requests.Session :rtype: requests.Session. """ if 'refresh_token' in self.token: try: token = self._context.acquire_token_with_refresh_token( self.token['refresh_token'], self.id, self.resource, self.secret # This is needed when using Confidential Client ) self.token = self._convert_token(token) except adal.AdalError as err: raise_with_traceback(AuthenticationError, "", err) return self.signed_session(session) class AADTokenCredentials(AADMixin): """ Credentials objects for AAD token retrieved through external process e.g. Python ADAL lib. If you just provide "token", refresh will be done on Public Azure with default public Azure "resource". You can set "cloud_environment", "tenant", "resource" and "client_id" to change that behavior. Optional kwargs may include: - cloud_environment (msrestazure.azure_cloud.Cloud): A targeted cloud environment - china (bool): Configure auth for China-based service, default is 'False'. - tenant (str): Alternative tenant, default is 'common'. - resource (str): Alternative authentication resource, default is 'https://management.core.windows.net/'. - verify (bool): Verify secure connection, default is 'True'. - cache (adal.TokenCache): A adal.TokenCache, see ADAL configuration for details. This parameter is not used here and directly passed to ADAL. :param dict token: Authentication token. :param str client_id: Client ID, if not set, Xplat Client ID will be used. """ def __init__(self, token, client_id=None, **kwargs): if not client_id: # Default to Xplat Client ID. client_id = '04b07795-8ddb-461a-bbee-02f9e1bf7b46' super(AADTokenCredentials, self).__init__(client_id, None) self._configure(**kwargs) self.client = None self.token = self._convert_token(token) class UserPassCredentials(AADMixin): """Credentials object for Headless Authentication, i.e. AAD authentication via username and password. Headless Auth requires an AAD login (no a Live ID) that already has permission to access the resource e.g. an organization account, and that 2-factor auth be disabled. Optional kwargs may include: - cloud_environment (msrestazure.azure_cloud.Cloud): A targeted cloud environment - china (bool): Configure auth for China-based service, default is 'False'. - tenant (str): Alternative tenant, default is 'common'. - resource (str): Alternative authentication resource, default is 'https://management.core.windows.net/'. - verify (bool): Verify secure connection, default is 'True'. - timeout (int): Timeout of the request in seconds. - proxies (dict): Dictionary mapping protocol or protocol and hostname to the URL of the proxy. - cache (adal.TokenCache): A adal.TokenCache, see ADAL configuration for details. This parameter is not used here and directly passed to ADAL. :param str username: Account username. :param str password: Account password. :param str client_id: Client ID, if not set, Xplat Client ID will be used. :param str secret: Client secret, only if required by server. """ def __init__(self, username, password, client_id=None, secret=None, **kwargs): if not client_id: # Default to Xplat Client ID. client_id = '04b07795-8ddb-461a-bbee-02f9e1bf7b46' super(UserPassCredentials, self).__init__(client_id, None) self._configure(**kwargs) self.store_key += "_{}".format(username) self.username = username self.password = password self.secret = secret self.set_token() def set_token(self): """Get token using Username/Password credentials. :raises: AuthenticationError if credentials invalid, or call fails. """ super(UserPassCredentials, self).set_token() try: token = self._context.acquire_token_with_username_password( self.resource, self.username, self.password, self.id ) self.token = self._convert_token(token) except adal.AdalError as err: raise_with_traceback(AuthenticationError, "", err) class ServicePrincipalCredentials(AADMixin): """Credentials object for Service Principle Authentication. Authenticates via a Client ID and Secret. Optional kwargs may include: - cloud_environment (msrestazure.azure_cloud.Cloud): A targeted cloud environment - china (bool): Configure auth for China-based service, default is 'False'. - tenant (str): Alternative tenant, default is 'common'. - resource (str): Alternative authentication resource, default is 'https://management.core.windows.net/'. - verify (bool): Verify secure connection, default is 'True'. - timeout (int): Timeout of the request in seconds. - proxies (dict): Dictionary mapping protocol or protocol and hostname to the URL of the proxy. - cache (adal.TokenCache): A adal.TokenCache, see ADAL configuration for details. This parameter is not used here and directly passed to ADAL. :param str client_id: Client ID. :param str secret: Client secret. """ def __init__(self, client_id, secret, **kwargs): super(ServicePrincipalCredentials, self).__init__(client_id, None) self._configure(**kwargs) self.secret = secret self.set_token() def set_token(self): """Get token using Client ID/Secret credentials. :raises: AuthenticationError if credentials invalid, or call fails. """ super(ServicePrincipalCredentials, self).set_token() try: token = self._context.acquire_token_with_client_credentials( self.resource, self.id, self.secret ) self.token = self._convert_token(token) except adal.AdalError as err: raise_with_traceback(AuthenticationError, "", err) # For backward compatibility of import, but I doubt someone uses that... class InteractiveCredentials(object): """This class has been removed and using it will raise a NotImplementedError error. """ def __init__(self, *args, **kwargs): raise NotImplementedError("InteractiveCredentials was not functionning and was removed. Please use ADAL and device code instead.") class AdalAuthentication(Authentication): # pylint: disable=too-few-public-methods """A wrapper to use ADAL for Python easily to authenticate on Azure. .. versionadded:: 0.4.5 Take an ADAL `acquire_token` method and its parameters. :Example: .. code:: python context = adal.AuthenticationContext('https://login.microsoftonline.com/ABCDEFGH-1234-1234-1234-ABCDEFGHIJKL') RESOURCE = '00000002-0000-0000-c000-000000000000' #AAD graph resource token = context.acquire_token_with_client_credentials( RESOURCE, "http://PythonSDK", "Key-Configured-In-Portal") can be written here: .. code:: python context = adal.AuthenticationContext('https://login.microsoftonline.com/ABCDEFGH-1234-1234-1234-ABCDEFGHIJKL') RESOURCE = '00000002-0000-0000-c000-000000000000' #AAD graph resource credentials = AdalAuthentication( context.acquire_token_with_client_credentials, RESOURCE, "http://PythonSDK", "Key-Configured-In-Portal") or using a lambda if you prefer: .. code:: python context = adal.AuthenticationContext('https://login.microsoftonline.com/ABCDEFGH-1234-1234-1234-ABCDEFGHIJKL') RESOURCE = '00000002-0000-0000-c000-000000000000' #AAD graph resource credentials = AdalAuthentication( lambda: context.acquire_token_with_client_credentials( RESOURCE, "http://PythonSDK", "Key-Configured-In-Portal" ) ) :param callable adal_method: A lambda with no args, or `acquire_token` method with args using args/kwargs :param args: Optional positional args for the method :param kwargs: Optional kwargs for the method """ def __init__(self, adal_method, *args, **kwargs): super(AdalAuthentication, self).__init__() self._adal_method = adal_method self._args = args self._kwargs = kwargs def signed_session(self, session=None): """Create requests session with any required auth headers applied. If a session object is provided, configure it directly. Otherwise, create a new session and return it. :param session: The session to configure for authentication :type session: requests.Session :rtype: requests.Session """ session = super(AdalAuthentication, self).signed_session(session) try: raw_token = self._adal_method(*self._args, **self._kwargs) except adal.AdalError as err: # pylint: disable=no-member if 'AADSTS70008:' in ((getattr(err, 'error_response', None) or {}).get('error_description') or ''): raise Expired("Credentials have expired due to inactivity.") else: raise AuthenticationError(err) except ConnectionError as err: raise AuthenticationError('Please ensure you have network connection. Error detail: ' + str(err)) scheme, token = raw_token['tokenType'], raw_token['accessToken'] header = "{} {}".format(scheme, token) session.headers['Authorization'] = header return session def get_msi_token(resource, port=50342, msi_conf=None): """Get MSI token if MSI_ENDPOINT is set. IF MSI_ENDPOINT is not set, will try legacy access through 'http://localhost:{}/oauth2/token'.format(port). If msi_conf is used, must be a dict of one key in ["client_id", "object_id", "msi_res_id"] :param str resource: The resource where the token would be use. :param int port: The port if not the default 50342 is used. Ignored if MSI_ENDPOINT is set. :param dict[str,str] msi_conf: msi_conf if to request a token through a User Assigned Identity (if not specified, assume System Assigned) """ request_uri = os.environ.get("MSI_ENDPOINT", 'http://localhost:{}/oauth2/token'.format(port)) payload = { 'resource': resource } if msi_conf: if len(msi_conf) > 1: raise ValueError("{} are mutually exclusive".format(list(msi_conf.keys()))) payload.update(msi_conf) try: result = requests.post(request_uri, data=payload, headers={'Metadata': 'true'}) _LOGGER.debug("MSI: Retrieving a token from %s, with payload %s", request_uri, payload) result.raise_for_status() except Exception as ex: # pylint: disable=broad-except _LOGGER.warning("MSI: Failed to retrieve a token from '%s' with an error of '%s'. This could be caused " "by the MSI extension not yet fully provisioned.", request_uri, ex) raise token_entry = result.json() return token_entry['token_type'], token_entry['access_token'], token_entry def get_msi_token_webapp(resource, msi_conf=None): """Get a MSI token from inside a webapp or functions. Env variable will look like: - MSI_ENDPOINT = http://127.0.0.1:41741/MSI/token/ - MSI_SECRET = 69418689F1E342DD946CB82994CDA3CB :param str resource: The resource where the token would be use. :param dict[str,str] msi_conf: msi_conf if to request a token through a User Assigned Identity (if not specified, assume System Assigned) """ try: msi_endpoint = os.environ['MSI_ENDPOINT'] msi_secret = os.environ['MSI_SECRET'] except KeyError as err: err_msg = "{} required env variable was not found. You might need to restart your app/function.".format(err) _LOGGER.critical(err_msg) raise RuntimeError(err_msg) clientid_param = '' if msi_conf: if len(msi_conf) > 1: raise ValueError("{} are mutually exclusive".format(list(msi_conf.keys()))) elif 'client_id' not in msi_conf.keys(): raise ValueError('"client_id" is the only supported explicit identity option on WebApp') else: clientid_param = '&clientid={}'.format(msi_conf['client_id']) request_uri = '{}/?resource={}&api-version=2017-09-01{}'.format(msi_endpoint, resource, clientid_param) headers = { 'secret': msi_secret } err = None try: result = requests.get(request_uri, headers=headers) _LOGGER.debug("MSI: Retrieving a token from %s", request_uri) if result.status_code != 200: err = result.text # Workaround since not all failures are != 200 if 'ExceptionMessage' in result.text: err = result.text except Exception as ex: # pylint: disable=broad-except err = str(ex) if err: err_msg = "MSI: Failed to retrieve a token from '{}' with an error of '{}'.".format( request_uri, err ) _LOGGER.critical(err_msg) raise RuntimeError(err_msg) _LOGGER.debug('MSI: token retrieved') token_entry = result.json() return token_entry['token_type'], token_entry['access_token'], token_entry def _is_app_service(): # Might be discussed if we think it's not robust enough return 'APPSETTING_WEBSITE_SITE_NAME' in os.environ class MSIAuthentication(BasicTokenAuthentication): """Credentials object for MSI authentication,. Optional kwargs may include: - timeout: If provided, must be in seconds and indicates the maximum time we'll try to get a token before raising MSIAuthenticationTimeout - client_id: Identifies, by Azure AD client id, a specific explicit identity to use when authenticating to Azure AD. Mutually exclusive with object_id and msi_res_id. - object_id: Identifies, by Azure AD object id, a specific explicit identity to use when authenticating to Azure AD. Mutually exclusive with client_id and msi_res_id. - msi_res_id: Identifies, by ARM resource id, a specific explicit identity to use when authenticating to Azure AD. Mutually exclusive with client_id and object_id. - cloud_environment (msrestazure.azure_cloud.Cloud): A targeted cloud environment - resource (str): Alternative authentication resource, default is 'https://management.core.windows.net/'. .. versionadded:: 0.4.14 """ def __init__(self, port=50342, **kwargs): super(MSIAuthentication, self).__init__(None) if port != 50342: warnings.warn("The 'port' argument is no longer used, and will be removed in a future release", DeprecationWarning) self.port = port self.msi_conf = {k:v for k,v in kwargs.items() if k in ["client_id", "object_id", "msi_res_id"]} self.cloud_environment = kwargs.get('cloud_environment', AZURE_PUBLIC_CLOUD) self.resource = kwargs.get('resource', self.cloud_environment.endpoints.active_directory_resource_id) if not _is_app_service() and "MSI_ENDPOINT" not in os.environ: # Use IMDS if no MSI_ENDPOINT self._vm_msi = _ImdsTokenProvider( self.msi_conf, timeout=kwargs.get("timeout") ) # Follow the same convention as all Credentials class to check for the token at creation time #106 self.set_token() def set_token(self): if _is_app_service(): self.scheme, _, self.token = get_msi_token_webapp(self.resource, self.msi_conf) elif "MSI_ENDPOINT" in os.environ: self.scheme, _, self.token = get_msi_token(self.resource, self.port, self.msi_conf) else: token_entry = self._vm_msi.get_token(self.resource) self.scheme, self.token = token_entry['token_type'], token_entry def signed_session(self, session=None): """Create requests session with any required auth headers applied. If a session object is provided, configure it directly. Otherwise, create a new session and return it. :param session: The session to configure for authentication :type session: requests.Session :rtype: requests.Session """ # Token cache is handled by the VM extension, call each time to avoid expiration self.set_token() return super(MSIAuthentication, self).signed_session(session) class _ImdsTokenProvider(object): """A help class handling token acquisitions through Azure IMDS plugin. """ def __init__(self, msi_conf=None, timeout=None): self._user_agent = AzureConfiguration(None).user_agent self.identity_type, self.identity_id = None, None if msi_conf: if len(msi_conf.keys()) > 1: raise ValueError('"client_id", "object_id", "msi_res_id" are mutually exclusive') elif len(msi_conf.keys()) == 1: self.identity_type, self.identity_id = next(iter(msi_conf.items())) # default to system assigned identity on an empty configuration object self.cache = {} self.timeout = timeout def get_token(self, resource): import datetime # let us hit the cache first token_entry = self.cache.get(resource, None) if token_entry: expires_on = int(token_entry['expires_on']) expires_on_datetime = datetime.datetime.fromtimestamp(expires_on) expiration_margin = 5 # in minutes if datetime.datetime.now() + datetime.timedelta(minutes=expiration_margin) <= expires_on_datetime: _LOGGER.info("MSI: token is found in cache.") return token_entry _LOGGER.info("MSI: cache is found but expired within %s minutes, so getting a new one.", expiration_margin) self.cache.pop(resource) token_entry = self._retrieve_token_from_imds_with_retry(resource) self.cache[resource] = token_entry return token_entry def _sleep(self, time_to_wait, start_time): """Sleep for time_to_wait or time remaining until timeout reached. :param float time: Time to sleep in seconds :param float start_time: Absolute time where polling started :rtype: bool :returns: True if timeout was used """ if self.timeout is not None: # 0 is acceptable value, so we really want to test None time_to_sleep = max(0, min(time_to_wait, start_time + self.timeout - time.time())) else: time_to_sleep = time_to_wait time.sleep(time_to_sleep) return time_to_sleep != time_to_wait def _retrieve_token_from_imds_with_retry(self, resource): import random import json # 169.254.169.254 is a well known ip address hosting the web service that provides the Azure IMDS metadata request_uri = 'http://169.254.169.254/metadata/identity/oauth2/token' payload = { 'resource': resource, 'api-version': '2018-02-01' } if self.identity_id: payload[self.identity_type] = self.identity_id retry, max_retry, start_time = 1, 12, time.time() # simplified version of https://en.wikipedia.org/wiki/Exponential_backoff slots = [100 * ((2 << x) - 1) / 1000 for x in range(max_retry)] has_timed_out = self.timeout == 0 # Assume a 0 timeout means "no more than one try" while True: result = requests.get(request_uri, params=payload, headers={'Metadata': 'true', 'User-Agent':self._user_agent}) _LOGGER.debug("MSI: Retrieving a token from %s, with payload %s", request_uri, payload) if result.status_code in [404, 410, 429] or (499 < result.status_code < 600): if has_timed_out: # It was the last try, and we still don't get a good status code, die raise MSIAuthenticationTimeoutError('MSI: Failed to acquired tokens before timeout {}'.format(self.timeout)) elif retry <= max_retry: wait = random.choice(slots[:retry]) _LOGGER.warning("MSI: wait: %ss and retry: %s", wait, retry) has_timed_out = self._sleep(wait, start_time) retry += 1 else: if result.status_code == 410: # For IMDS upgrading, we wait up to 70s gap = 70 - (time.time() - start_time) if gap > 0: _LOGGER.warning("MSI: wait till 70 seconds when IMDS is upgrading") has_timed_out = self._sleep(gap, start_time) continue break elif result.status_code != 200: raise HTTPError(request=result.request, response=result.raw) else: break if result.status_code != 200: raise MSIAuthenticationTimeoutError('MSI: Failed to acquire tokens after {} times'.format(max_retry)) _LOGGER.debug('MSI: Token retrieved') token_entry = json.loads(result.content.decode()) return token_entry msrestazure-for-python-0.6.4/msrestazure/azure_cloud.py000066400000000000000000000270751367642573600235200ustar00rootroot00000000000000# -------------------------------------------------------------------------- # # Copyright (c) Microsoft Corporation. All rights reserved. # # The MIT License (MIT) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the ""Software""), to # deal in the Software without restriction, including without limitation the # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or # sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # # -------------------------------------------------------------------------- import os import logging from pprint import pformat _LOGGER = logging.getLogger(__name__) # The exact API version doesn't matter too much right now. It just has to be YYYY-MM-DD format. METADATA_ENDPOINT_SUFFIX = '/metadata/endpoints?api-version=2015-01-01' class CloudEndpointNotSetException(Exception): pass class CloudSuffixNotSetException(Exception): pass class MetadataEndpointError(Exception): pass class CloudEndpoints(object): # pylint: disable=too-few-public-methods,too-many-instance-attributes def __init__(self, management=None, resource_manager=None, sql_management=None, batch_resource_id=None, gallery=None, active_directory=None, active_directory_resource_id=None, active_directory_graph_resource_id=None, microsoft_graph_resource_id=None): # Attribute names are significant. They are used when storing/retrieving clouds from config self.management = management self.resource_manager = resource_manager self.sql_management = sql_management self.batch_resource_id = batch_resource_id self.gallery = gallery self.active_directory = active_directory self.active_directory_resource_id = active_directory_resource_id self.active_directory_graph_resource_id = active_directory_graph_resource_id self.microsoft_graph_resource_id = microsoft_graph_resource_id def has_endpoint_set(self, endpoint_name): try: # Can't simply use hasattr here as we override __getattribute__ below. # Python 3 hasattr() only returns False if an AttributeError is raised but we raise # CloudEndpointNotSetException. This exception is not a subclass of AttributeError. getattr(self, endpoint_name) return True except Exception: # pylint: disable=broad-except return False def __getattribute__(self, name): val = object.__getattribute__(self, name) if val is None: raise CloudEndpointNotSetException("The endpoint '{}' for this cloud " "is not set but is used.".format(name)) return val class CloudSuffixes(object): # pylint: disable=too-few-public-methods def __init__(self, storage_endpoint=None, keyvault_dns=None, sql_server_hostname=None, azure_datalake_store_file_system_endpoint=None, azure_datalake_analytics_catalog_and_job_endpoint=None): # Attribute names are significant. They are used when storing/retrieving clouds from config self.storage_endpoint = storage_endpoint self.keyvault_dns = keyvault_dns self.sql_server_hostname = sql_server_hostname self.azure_datalake_store_file_system_endpoint = azure_datalake_store_file_system_endpoint self.azure_datalake_analytics_catalog_and_job_endpoint = azure_datalake_analytics_catalog_and_job_endpoint # pylint: disable=line-too-long def __getattribute__(self, name): val = object.__getattribute__(self, name) if val is None: raise CloudSuffixNotSetException("The suffix '{}' for this cloud " "is not set but is used.".format(name)) return val class Cloud(object): # pylint: disable=too-few-public-methods """ Represents an Azure Cloud instance """ def __init__(self, name, endpoints=None, suffixes=None): self.name = name self.endpoints = endpoints or CloudEndpoints() self.suffixes = suffixes or CloudSuffixes() def __str__(self): o = { 'name': self.name, 'endpoints': vars(self.endpoints), 'suffixes': vars(self.suffixes), } return pformat(o) AZURE_PUBLIC_CLOUD = Cloud( 'AzureCloud', endpoints=CloudEndpoints( management='https://management.core.windows.net/', resource_manager='https://management.azure.com/', sql_management='https://management.core.windows.net:8443/', batch_resource_id='https://batch.core.windows.net/', gallery='https://gallery.azure.com/', active_directory='https://login.microsoftonline.com', active_directory_resource_id='https://management.core.windows.net/', active_directory_graph_resource_id='https://graph.windows.net/', microsoft_graph_resource_id='https://graph.microsoft.com/'), suffixes=CloudSuffixes( storage_endpoint='core.windows.net', keyvault_dns='.vault.azure.net', sql_server_hostname='.database.windows.net', azure_datalake_store_file_system_endpoint='azuredatalakestore.net', azure_datalake_analytics_catalog_and_job_endpoint='azuredatalakeanalytics.net')) AZURE_CHINA_CLOUD = Cloud( 'AzureChinaCloud', endpoints=CloudEndpoints( management='https://management.core.chinacloudapi.cn/', resource_manager='https://management.chinacloudapi.cn', sql_management='https://management.core.chinacloudapi.cn:8443/', batch_resource_id='https://batch.chinacloudapi.cn/', gallery='https://gallery.chinacloudapi.cn/', active_directory='https://login.chinacloudapi.cn', active_directory_resource_id='https://management.core.chinacloudapi.cn/', active_directory_graph_resource_id='https://graph.chinacloudapi.cn/', microsoft_graph_resource_id='https://microsoftgraph.chinacloudapi.cn/'), suffixes=CloudSuffixes( storage_endpoint='core.chinacloudapi.cn', keyvault_dns='.vault.azure.cn', sql_server_hostname='.database.chinacloudapi.cn')) AZURE_US_GOV_CLOUD = Cloud( 'AzureUSGovernment', endpoints=CloudEndpoints( management='https://management.core.usgovcloudapi.net/', resource_manager='https://management.usgovcloudapi.net/', sql_management='https://management.core.usgovcloudapi.net:8443/', batch_resource_id='https://batch.core.usgovcloudapi.net/', gallery='https://gallery.usgovcloudapi.net/', active_directory='https://login.microsoftonline.us', active_directory_resource_id='https://management.core.usgovcloudapi.net/', active_directory_graph_resource_id='https://graph.windows.net/', microsoft_graph_resource_id='https://graph.microsoft.us/'), suffixes=CloudSuffixes( storage_endpoint='core.usgovcloudapi.net', keyvault_dns='.vault.usgovcloudapi.net', sql_server_hostname='.database.usgovcloudapi.net')) AZURE_GERMAN_CLOUD = Cloud( 'AzureGermanCloud', endpoints=CloudEndpoints( management='https://management.core.cloudapi.de/', resource_manager='https://management.microsoftazure.de', sql_management='https://management.core.cloudapi.de:8443/', batch_resource_id='https://batch.cloudapi.de/', gallery='https://gallery.cloudapi.de/', active_directory='https://login.microsoftonline.de', active_directory_resource_id='https://management.core.cloudapi.de/', active_directory_graph_resource_id='https://graph.cloudapi.de/', microsoft_graph_resource_id='https://graph.microsoft.de/'), suffixes=CloudSuffixes( storage_endpoint='core.cloudapi.de', keyvault_dns='.vault.microsoftazure.de', sql_server_hostname='.database.cloudapi.de')) def _populate_from_metadata_endpoint(cloud, arm_endpoint, session=None): endpoints_in_metadata = ['active_directory_graph_resource_id', 'active_directory_resource_id', 'active_directory'] if not arm_endpoint or all([cloud.endpoints.has_endpoint_set(n) for n in endpoints_in_metadata]): return try: error_msg_fmt = "Unable to get endpoints from the cloud.\n{}" import requests session = requests.Session() if session is None else session metadata_endpoint = arm_endpoint + METADATA_ENDPOINT_SUFFIX response = session.get(metadata_endpoint) if response.status_code == 200: metadata = response.json() if not cloud.endpoints.has_endpoint_set('gallery'): setattr(cloud.endpoints, 'gallery', metadata.get('galleryEndpoint')) if not cloud.endpoints.has_endpoint_set('active_directory_graph_resource_id'): setattr(cloud.endpoints, 'active_directory_graph_resource_id', metadata.get('graphEndpoint')) if not cloud.endpoints.has_endpoint_set('active_directory'): setattr(cloud.endpoints, 'active_directory', metadata['authentication'].get('loginEndpoint')) if not cloud.endpoints.has_endpoint_set('active_directory_resource_id'): setattr(cloud.endpoints, 'active_directory_resource_id', metadata['authentication']['audiences'][0]) else: msg = 'Server returned status code {} for {}'.format(response.status_code, metadata_endpoint) raise MetadataEndpointError(error_msg_fmt.format(msg)) except (requests.exceptions.ConnectionError, requests.exceptions.HTTPError) as err: msg = 'Please ensure you have network connection. Error detail: {}'.format(str(err)) raise MetadataEndpointError(error_msg_fmt.format(msg)) except ValueError as err: msg = 'Response body does not contain valid json. Error detail: {}'.format(str(err)) raise MetadataEndpointError(error_msg_fmt.format(msg)) def get_cloud_from_metadata_endpoint(arm_endpoint, name=None, session=None): """Get a Cloud object from an ARM endpoint. .. versionadded:: 0.4.11 :Example: .. code:: python get_cloud_from_metadata_endpoint(https://management.azure.com/, "Public Azure") :param str arm_endpoint: The ARM management endpoint :param str name: An optional name for the Cloud object. Otherwise it's the ARM endpoint :params requests.Session session: A requests session object if you need to configure proxy, cert, etc. :rtype Cloud: :returns: a Cloud object :raises: MetadataEndpointError if unable to build the Cloud object """ cloud = Cloud(name or arm_endpoint) cloud.endpoints.management = arm_endpoint cloud.endpoints.resource_manager = arm_endpoint _populate_from_metadata_endpoint(cloud, arm_endpoint, session) return cloud msrestazure-for-python-0.6.4/msrestazure/azure_configuration.py000066400000000000000000000074431367642573600252560ustar00rootroot00000000000000# -------------------------------------------------------------------------- # # Copyright (c) Microsoft Corporation. All rights reserved. # # The MIT License (MIT) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the ""Software""), to # deal in the Software without restriction, including without limitation the # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or # sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # # -------------------------------------------------------------------------- try: from configparser import NoOptionError except ImportError: from ConfigParser import NoOptionError import logging from msrest import Configuration from msrest.exceptions import raise_with_traceback from .version import msrestazure_version from .tools import register_rp_hook _LOGGER = logging.getLogger(__name__) class AzureConfiguration(Configuration): """Azure specific client configuration. :param str base_url: REST Service base URL. :param str filepath: Path to an existing config file (optional). """ def __init__(self, base_url, filepath=None): super(AzureConfiguration, self).__init__(base_url) self.long_running_operation_timeout = 30 self.accept_language = 'en-US' self.generate_client_request_id = True self.add_user_agent("msrest_azure/{}".format(msrestazure_version)) # ARM requires 20seconds at least. Putting 4 here is 24seconds self.retry_policy.retries = 4 if filepath: self.load(filepath) # Check if "hasattr", just in case msrest is older than msrestazure if hasattr(self, 'hooks'): self.hooks.append(register_rp_hook) else: _LOGGER.warning(("Your 'msrest' version is too old to activate all the " "features of 'msrestazure'. Please update using" "'pip install -U msrest'")) def save(self, filepath): """Save current configuration to file. :param str filepath: Path to save file to. :raises: ValueError if supplied filepath cannot be written to. """ self._config.add_section("Azure") self._config.set("Azure", "long_running_operation_timeout", self.long_running_operation_timeout) return super(AzureConfiguration, self).save(filepath) def load(self, filepath): """Load configuration from existing file. :param str filepath: Path to existing config file. :raises: ValueError if supplied config file is invalid. """ try: self._config.read(filepath) self.long_running_operation_timeout = self._config.getint( "Azure", "long_running_operation_timeout") except (ValueError, EnvironmentError, NoOptionError): msg = "Supplied config file incompatible" raise_with_traceback(ValueError, msg) finally: self._clear_config() return super(AzureConfiguration, self).load(filepath) msrestazure-for-python-0.6.4/msrestazure/azure_exceptions.py000066400000000000000000000242731367642573600245700ustar00rootroot00000000000000# -------------------------------------------------------------------------- # # Copyright (c) Microsoft Corporation. All rights reserved. # # The MIT License (MIT) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the ""Software""), to # deal in the Software without restriction, including without limitation the # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or # sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # # -------------------------------------------------------------------------- import json import six from requests import RequestException from msrest.exceptions import ClientException from msrest.serialization import Deserializer from msrest.exceptions import DeserializationError # TimeoutError for backward compat since it was used by former MSI code. # but this never worked on Python 2.7, so Python 2.7 users get the correct one now try: class MSIAuthenticationTimeoutError(TimeoutError, ClientException): """If the MSI authentication reached the timeout without getting a token. """ pass except NameError: class MSIAuthenticationTimeoutError(ClientException): """If the MSI authentication reached the timeout without getting a token. """ pass class CloudErrorRoot(object): """Just match the "error" key at the root of a OdataV4 JSON. """ _validation = {} _attribute_map = { 'error': {'key': 'error', 'type': 'CloudErrorData'}, } def __init__(self, error): self.error = error def _unicode_or_str(obj): try: return unicode(obj) except NameError: return str(obj) @six.python_2_unicode_compatible class CloudErrorData(object): """Cloud Error Data object, deserialized from error data returned during a failed REST API call. """ _validation = {} _attribute_map = { 'error': {'key': 'code', 'type': 'str'}, 'message': {'key': 'message', 'type': 'str'}, 'target': {'key': 'target', 'type': 'str'}, 'details': {'key': 'details', 'type': '[CloudErrorData]'}, 'innererror': {'key': 'innererror', 'type': 'object'}, 'additionalInfo': {'key': 'additionalInfo', 'type': '[TypedErrorInfo]'}, 'data': {'key': 'values', 'type': '{str}'} } def __init__(self, *args, **kwargs): self.error = kwargs.get('error') self.message = kwargs.get('message') self.request_id = None self.error_time = None self.target = kwargs.get('target') self.details = kwargs.get('details') self.innererror = kwargs.get('innererror') self.additionalInfo = kwargs.get('additionalInfo') self.data = kwargs.get('data') super(CloudErrorData, self).__init__(*args) def __str__(self): """Cloud error message.""" error_str = u"Azure Error: {}".format(self.error) error_str += u"\nMessage: {}".format(self._message) if self.target: error_str += u"\nTarget: {}".format(self.target) if self.request_id: error_str += u"\nRequest ID: {}".format(self.request_id) if self.error_time: error_str += u"\nError Time: {}".format(self.error_time) if self.data: error_str += u"\nAdditional Data:" for key, value in self.data.items(): error_str += u"\n\t{} : {}".format(key, value) if self.details: error_str += "\nException Details:" for error_obj in self.details: error_str += u"\n\tError Code: {}".format(error_obj.error) error_str += u"\n\tMessage: {}".format(error_obj.message) if error_obj.target: error_str += u"\n\tTarget: {}".format(error_obj.target) if error_obj.innererror: error_str += u"\nInner error: {}".format(json.dumps(error_obj.innererror, indent=4, ensure_ascii=False)) if error_obj.additionalInfo: error_str += u"\n\tAdditional Information:" for error_info in error_obj.additionalInfo: error_str += "\n\t\t{}".format(_unicode_or_str(error_info).replace("\n", "\n\t\t")) if self.innererror: error_str += u"\nInner error: {}".format(json.dumps(self.innererror, indent=4, ensure_ascii=False)) if self.additionalInfo: error_str += "\nAdditional Information:" for error_info in self.additionalInfo: error_str += u"\n\t{}".format(_unicode_or_str(error_info).replace("\n", "\n\t")) return error_str @classmethod def _get_subtype_map(cls): return {} @property def message(self): """Cloud error message.""" return self._message @message.setter def message(self, value): """Attempt to deconstruct error message to retrieve further error data. """ try: import ast value = ast.literal_eval(value) except (SyntaxError, TypeError, ValueError): pass try: value = value.get('value', value) msg_data = value.split('\n') self._message = msg_data[0] except AttributeError: self._message = value return try: self.request_id = msg_data[1].partition(':')[2] time_str = msg_data[2].partition(':') self.error_time = Deserializer.deserialize_iso( "".join(time_str[2:])) except (IndexError, DeserializationError): pass @six.python_2_unicode_compatible class CloudError(ClientException): """ClientError, exception raised for failed Azure REST call. Will attempt to deserialize response into meaningful error data. :param requests.Response response: Response object. :param str error: Optional error message. """ def __init__(self, response, error=None, *args, **kwargs): self.deserializer = Deserializer({ 'CloudErrorRoot': CloudErrorRoot, 'CloudErrorData': CloudErrorData, 'TypedErrorInfo': TypedErrorInfo }) self.error = None self.message = None self.response = response self.status_code = self.response.status_code self.request_id = None if error: self.message = error self.error = response else: self._build_error_data(response) if not self.error or not self.message: self._build_error_message(response) super(CloudError, self).__init__( self.message, self.error, *args, **kwargs) def __str__(self): """Cloud error message""" if self.error: return _unicode_or_str(self.error) return _unicode_or_str(self.message) def _build_error_data(self, response): try: self.error = self.deserializer('CloudErrorRoot', response).error except DeserializationError: self.error = None except AttributeError: # So far seen on Autorest test server only. self.error = None else: if self.error: if not self.error.error or not self.error.message: self.error = None else: self.message = self.error.message def _get_state(self, content): state = content.get("status") if not state: resource_content = content.get('properties', content) state = resource_content.get("provisioningState") return "Resource state {}".format(state) if state else "none" def _build_error_message(self, response): # Assume ClientResponse has "body", and otherwise it's a requests.Response content = response.text() if hasattr(response, "body") else response.text try: data = json.loads(content) except ValueError: message = "none" else: try: message = data.get("message", self._get_state(data)) except AttributeError: # data is not a dict, but is a requests.Response parsable as JSON message = str(content) try: response.raise_for_status() except RequestException as err: if not self.error: self.error = err if not self.message: if message == "none": message = str(err) msg = "Operation failed with status: {!r}. Details: {}" self.message = msg.format(response.reason, message) else: if not self.error: self.error = response if not self.message: msg = "Operation failed with status: {!r}. Details: {}" self.message = msg.format( response.status_code, message) @six.python_2_unicode_compatible class TypedErrorInfo(object): """Typed Error Info object, deserialized from error data returned during a failed REST API call. Contains additional error information """ _validation = {} _attribute_map = { 'type': {'key': 'type', 'type': 'str'}, 'info': {'key': 'info', 'type': 'object'} } def __init__(self, type, info): self.type = type self.info = info def __str__(self): """Cloud error message.""" error_str = u"Type: {}".format(self.type) error_str += u"\nInfo: {}".format(json.dumps(self.info, indent=4, ensure_ascii=False)) return error_str msrestazure-for-python-0.6.4/msrestazure/azure_operation.py000066400000000000000000000454471367642573600244150ustar00rootroot00000000000000# -------------------------------------------------------------------------- # # Copyright (c) Microsoft Corporation. All rights reserved. # # The MIT License (MIT) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the ""Software""), to # deal in the Software without restriction, including without limitation the # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or # sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # # -------------------------------------------------------------------------- import re import threading import time import uuid try: from urlparse import urlparse except ImportError: from urllib.parse import urlparse from msrest.exceptions import DeserializationError, ClientException from msrestazure.azure_exceptions import CloudError FINISHED = frozenset(['succeeded', 'canceled', 'failed']) FAILED = frozenset(['canceled', 'failed']) SUCCEEDED = frozenset(['succeeded']) def finished(status): if hasattr(status, 'value'): status = status.value return str(status).lower() in FINISHED def failed(status): if hasattr(status, 'value'): status = status.value return str(status).lower() in FAILED def succeeded(status): if hasattr(status, 'value'): status = status.value return str(status).lower() in SUCCEEDED def _validate(url): """Validate a url. :param str url: Polling URL extracted from response header. :raises: ValueError if URL has no scheme or host. """ if url is None: return parsed = urlparse(url) if not parsed.scheme or not parsed.netloc: raise ValueError("Invalid URL header") def _get_header_url(response, header_name): """Get a URL from a header requests. :param requests.Response response: REST call response. :param str header_name: Header name. :returns: URL if not None AND valid, None otherwise """ url = response.headers.get(header_name) try: _validate(url) except ValueError: return None else: return url class BadStatus(Exception): pass class BadResponse(Exception): pass class OperationFailed(Exception): pass class SimpleResource: """An implementation of Python 3 SimpleNamespace. Used to deserialize resource objects from response bodies where no particular object type has been specified. """ def __init__(self, **kwargs): self.__dict__.update(kwargs) def __repr__(self): keys = sorted(self.__dict__) items = ("{}={!r}".format(k, self.__dict__[k]) for k in keys) return "{}({})".format(type(self).__name__, ", ".join(items)) def __eq__(self, other): return self.__dict__ == other.__dict__ class LongRunningOperation(object): """LongRunningOperation Provides default logic for interpreting operation responses and status updates. """ _convert = re.compile('([a-z0-9])([A-Z])') def __init__(self, response, outputs): self.method = response.request.method self.status = "" self.resource = None self.get_outputs = outputs self.async_url = None self.location_url = None self.initial_status_code = None def _raise_if_bad_http_status_and_method(self, response): """Check response status code is valid for a Put or Patch request. Must be 200, 201, 202, or 204. :raises: BadStatus if invalid status. """ code = response.status_code if code in {200, 202} or \ (code == 201 and self.method in {'PUT', 'PATCH'}) or \ (code == 204 and self.method in {'DELETE', 'POST'}): return raise BadStatus( "Invalid return status for {!r} operation".format(self.method)) def _is_empty(self, response): """Check if response body contains meaningful content. :rtype: bool :raises: DeserializationError if response body contains invalid json data. """ if not response.content: return True try: body = response.json() return not body except ValueError: raise DeserializationError( "Error occurred in deserializing the response body.") def _deserialize(self, response): """Attempt to deserialize resource from response. :param requests.Response response: latest REST call response. """ # Hacking response with initial status_code previous_status = response.status_code response.status_code = self.initial_status_code resource = self.get_outputs(response) response.status_code = previous_status # Hack for Storage or SQL, to workaround the bug in the Python generator if resource is None: previous_status = response.status_code for status_code_to_test in [200, 201]: try: response.status_code = status_code_to_test resource = self.get_outputs(response) except ClientException: pass else: return resource finally: response.status_code = previous_status return resource def _get_async_status(self, response): """Attempt to find status info in response body. :param requests.Response response: latest REST call response. :rtype: str :returns: Status if found, else 'None'. """ if self._is_empty(response): return None body = response.json() return body.get('status') def _get_provisioning_state(self, response): """ Attempt to get provisioning state from resource. :param requests.Response response: latest REST call response. :returns: Status if found, else 'None'. """ if self._is_empty(response): return None body = response.json() return body.get("properties", {}).get("provisioningState") def should_do_final_get(self): """Check whether the polling should end doing a final GET. :param requests.Response response: latest REST call response. :rtype: bool """ return (self.async_url or not self.resource) and \ self.method in {'PUT', 'PATCH'} def set_initial_status(self, response): """Process first response after initiating long running operation and set self.status attribute. :param requests.Response response: initial REST call response. """ self._raise_if_bad_http_status_and_method(response) if self._is_empty(response): self.resource = None else: try: self.resource = self.get_outputs(response) except DeserializationError: self.resource = None self.set_async_url_if_present(response) if response.status_code in {200, 201, 202, 204}: self.initial_status_code = response.status_code if self.async_url or self.location_url or response.status_code == 202: self.status = 'InProgress' elif response.status_code == 201: status = self._get_provisioning_state(response) self.status = status or 'InProgress' elif response.status_code == 200: status = self._get_provisioning_state(response) self.status = status or 'Succeeded' elif response.status_code == 204: self.status = 'Succeeded' self.resource = None else: raise OperationFailed("Invalid status found") return raise OperationFailed("Operation failed or cancelled") def get_status_from_location(self, response): """Process the latest status update retrieved from a 'location' header. :param requests.Response response: latest REST call response. :raises: BadResponse if response has no body and not status 202. """ self._raise_if_bad_http_status_and_method(response) code = response.status_code if code == 202: self.status = "InProgress" else: self.status = 'Succeeded' if self._is_empty(response): self.resource = None else: self.resource = self._deserialize(response) def get_status_from_resource(self, response): """Process the latest status update retrieved from the same URL as the previous request. :param requests.Response response: latest REST call response. :raises: BadResponse if status not 200 or 204. """ self._raise_if_bad_http_status_and_method(response) if self._is_empty(response): raise BadResponse('The response from long running operation ' 'does not contain a body.') status = self._get_provisioning_state(response) self.status = status or 'Succeeded' self.resource = self._deserialize(response) def get_status_from_async(self, response): """Process the latest status update retrieved from a 'azure-asyncoperation' header. :param requests.Response response: latest REST call response. :raises: BadResponse if response has no body, or body does not contain status. """ self._raise_if_bad_http_status_and_method(response) if self._is_empty(response): raise BadResponse('The response from long running operation ' 'does not contain a body.') self.status = self._get_async_status(response) if not self.status: raise BadResponse("No status found in body") # Status can contains information, see ARM spec: # https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/Addendum.md#operation-resource-format # "properties": { # /\* The resource provider can choose the values here, but it should only be # returned on a successful operation (status being "Succeeded"). \*/ #}, # So try to parse it try: self.resource = self.get_outputs(response) except Exception: self.resource = None def set_async_url_if_present(self, response): async_url = _get_header_url(response, 'azure-asyncoperation') if async_url: self.async_url = async_url location_url = _get_header_url(response, 'location') if location_url: self.location_url = location_url class AzureOperationPoller(object): """Initiates long running operation and polls status in separate thread. :param callable send_cmd: The API request to initiate the operation. :param callable update_cmd: The API reuqest to check the status of the operation. :param callable output_cmd: The function to deserialize the resource of the operation. :param int timeout: Time in seconds to wait between status calls, default is 30. """ def __init__(self, send_cmd, output_cmd, update_cmd, timeout=30): self._timeout = timeout self._callbacks = [] try: self._response = send_cmd() self._operation = LongRunningOperation(self._response, output_cmd) self._operation.set_initial_status(self._response) except BadStatus: self._operation.status = 'Failed' raise CloudError(self._response) except BadResponse as err: self._operation.status = 'Failed' raise CloudError(self._response, str(err)) except OperationFailed: raise CloudError(self._response) self._thread = None self._done = None self._exception = None if not finished(self.status()): self._done = threading.Event() self._thread = threading.Thread( target=self._start, name="AzureOperationPoller({})".format(uuid.uuid4()), args=(update_cmd,)) self._thread.daemon = True self._thread.start() def _start(self, update_cmd): """Start the long running operation. On completion, runs any callbacks. :param callable update_cmd: The API reuqest to check the status of the operation. """ try: self._poll(update_cmd) except BadStatus: self._operation.status = 'Failed' self._exception = CloudError(self._response) except BadResponse as err: self._operation.status = 'Failed' self._exception = CloudError(self._response, str(err)) except OperationFailed: self._exception = CloudError(self._response) except Exception as err: self._exception = err finally: self._done.set() callbacks, self._callbacks = self._callbacks, [] while callbacks: for call in callbacks: call(self._operation) callbacks, self._callbacks = self._callbacks, [] def _delay(self): """Check for a 'retry-after' header to set timeout, otherwise use configured timeout. """ if self._response is None: return if self._response.headers.get('retry-after'): time.sleep(int(self._response.headers['retry-after'])) else: time.sleep(self._timeout) def _polling_cookie(self): """Collect retry cookie - we only want to do this for the test server at this point, unless we implement a proper cookie policy. :returns: Dictionary containing a cookie header if required, otherwise an empty dictionary. """ parsed_url = urlparse(self._response.request.url) host = parsed_url.hostname.strip('.') if host == 'localhost': return {'cookie': self._response.headers.get('set-cookie', '')} return {} def _poll(self, update_cmd): """Poll status of operation so long as operation is incomplete and we have an endpoint to query. :param callable update_cmd: The function to call to retrieve the latest status of the long running operation. :raises: OperationFailed if operation status 'Failed' or 'Cancelled'. :raises: BadStatus if response status invalid. :raises: BadResponse if response invalid. """ initial_url = self._response.request.url while not finished(self.status()): self._delay() headers = self._polling_cookie() if self._operation.async_url: self._response = update_cmd( self._operation.async_url, headers) self._operation.set_async_url_if_present(self._response) self._operation.get_status_from_async( self._response) elif self._operation.location_url: self._response = update_cmd( self._operation.location_url, headers) self._operation.set_async_url_if_present(self._response) self._operation.get_status_from_location( self._response) elif self._operation.method == "PUT": self._response = update_cmd(initial_url, headers) self._operation.set_async_url_if_present(self._response) self._operation.get_status_from_resource( self._response) else: raise BadResponse( 'Location header is missing from long running operation.') if failed(self._operation.status): raise OperationFailed("Operation failed or cancelled") elif self._operation.should_do_final_get(): self._response = update_cmd(initial_url) self._operation.get_status_from_resource( self._response) def status(self): """Returns the current status string. :returns: The current status string :rtype: str """ return self._operation.status def result(self, timeout=None): """Return the result of the long running operation, or the result available after the specified timeout. :returns: The deserialized resource of the long running operation, if one is available. :raises CloudError: Server problem with the query. """ self.wait(timeout) return self._operation.resource def wait(self, timeout=None): """Wait on the long running operation for a specified length of time. :param int timeout: Perion of time to wait for the long running operation to complete. :raises CloudError: Server problem with the query. """ if self._thread is None: return self._thread.join(timeout=timeout) try: raise self._exception except TypeError: pass def done(self): """Check status of the long running operation. :returns: 'True' if the process has completed, else 'False'. """ return self._thread is None or not self._thread.is_alive() def add_done_callback(self, func): """Add callback function to be run once the long running operation has completed - regardless of the status of the operation. :param callable func: Callback function that takes at least one argument, a completed LongRunningOperation. :raises: ValueError if the long running operation has already completed. """ if self._done is None or self._done.is_set(): raise ValueError("Process is complete.") self._callbacks.append(func) def remove_done_callback(self, func): """Remove a callback from the long running operation. :param callable func: The function to be removed from the callbacks. :raises: ValueError if the long running operation has already completed. """ if self._done is None or self._done.is_set(): raise ValueError("Process is complete.") self._callbacks = [c for c in self._callbacks if c != func] msrestazure-for-python-0.6.4/msrestazure/polling/000077500000000000000000000000001367642573600222635ustar00rootroot00000000000000msrestazure-for-python-0.6.4/msrestazure/polling/__init__.py000066400000000000000000000024271367642573600244010ustar00rootroot00000000000000# -------------------------------------------------------------------------- # # Copyright (c) Microsoft Corporation. All rights reserved. # # The MIT License (MIT) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the ""Software""), to # deal in the Software without restriction, including without limitation the # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or # sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # # -------------------------------------------------------------------------- msrestazure-for-python-0.6.4/msrestazure/polling/arm_polling.py000066400000000000000000000420731367642573600251460ustar00rootroot00000000000000# -------------------------------------------------------------------------- # # Copyright (c) Microsoft Corporation. All rights reserved. # # The MIT License (MIT) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the ""Software""), to # deal in the Software without restriction, including without limitation the # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or # sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # # -------------------------------------------------------------------------- import json import time try: from urlparse import urlparse except ImportError: from urllib.parse import urlparse from msrest.exceptions import DeserializationError from msrest.polling import PollingMethod from ..azure_exceptions import CloudError FINISHED = frozenset(['succeeded', 'canceled', 'failed']) FAILED = frozenset(['canceled', 'failed']) SUCCEEDED = frozenset(['succeeded']) _AZURE_ASYNC_OPERATION_FINAL_STATE = "azure-async-operation" _LOCATION_FINAL_STATE = "location" def finished(status): if hasattr(status, 'value'): status = status.value return str(status).lower() in FINISHED def failed(status): if hasattr(status, 'value'): status = status.value return str(status).lower() in FAILED def succeeded(status): if hasattr(status, 'value'): status = status.value return str(status).lower() in SUCCEEDED class BadStatus(Exception): pass class BadResponse(Exception): pass class OperationFailed(Exception): pass def _validate(url): """Validate a url. :param str url: Polling URL extracted from response header. :raises: ValueError if URL has no scheme or host. """ if url is None: return parsed = urlparse(url) if not parsed.scheme or not parsed.netloc: raise ValueError("Invalid URL header") def get_header_url(response, header_name): """Get a URL from a header requests. :param requests.Response response: REST call response. :param str header_name: Header name. :returns: URL if not None AND valid, None otherwise """ url = response.headers.get(header_name) try: _validate(url) except ValueError: return None else: return url class LongRunningOperation(object): """LongRunningOperation Provides default logic for interpreting operation responses and status updates. :param requests.Response response: The initial response. :param callable deserialization_callback: The deserialization callaback. :param dict lro_options: LRO options. :param kwargs: Unused for now """ def __init__(self, response, deserialization_callback, lro_options=None, **kwargs): self.method = response.request.method self.initial_response = response self.status = "" self.resource = None self.deserialization_callback = deserialization_callback self.async_url = None self.location_url = None if lro_options is None: lro_options = { 'final-state-via': _AZURE_ASYNC_OPERATION_FINAL_STATE } self.lro_options = lro_options def _raise_if_bad_http_status_and_method(self, response): """Check response status code is valid for a Put or Patch request. Must be 200, 201, 202, or 204. :raises: BadStatus if invalid status. """ code = response.status_code if code in {200, 202} or \ (code == 201 and self.method in {'PUT', 'PATCH'}) or \ (code == 204 and self.method in {'DELETE', 'POST'}): return raise BadStatus( "Invalid return status for {!r} operation".format(self.method)) def _is_empty(self, response): """Check if response body contains meaningful content. :rtype: bool :raises: DeserializationError if response body contains invalid json data. """ # Assume ClientResponse has "body", and otherwise it's a requests.Response content = response.text() if hasattr(response, "body") else response.text if not content: return True try: return not json.loads(content) except ValueError: raise DeserializationError( "Error occurred in deserializing the response body.") def _as_json(self, response): """Assuming this is not empty, return the content as JSON. Result/exceptions is not determined if you call this method without testing _is_empty. :raises: DeserializationError if response body contains invalid json data. """ # Assume ClientResponse has "body", and otherwise it's a requests.Response content = response.text() if hasattr(response, "body") else response.text try: return json.loads(content) except ValueError: raise DeserializationError( "Error occurred in deserializing the response body.") def _deserialize(self, response): """Attempt to deserialize resource from response. :param requests.Response response: latest REST call response. """ return self.deserialization_callback(response) def _get_async_status(self, response): """Attempt to find status info in response body. :param requests.Response response: latest REST call response. :rtype: str :returns: Status if found, else 'None'. """ if self._is_empty(response): return None body = self._as_json(response) return body.get('status') def _get_provisioning_state(self, response): """ Attempt to get provisioning state from resource. :param requests.Response response: latest REST call response. :returns: Status if found, else 'None'. """ if self._is_empty(response): return None body = self._as_json(response) return body.get("properties", {}).get("provisioningState") def should_do_final_get(self): """Check whether the polling should end doing a final GET. :param requests.Response response: latest REST call response. :rtype: bool """ return ((self.async_url or not self.resource) and self.method in {'PUT', 'PATCH'}) \ or (self.lro_options['final-state-via'] == _LOCATION_FINAL_STATE and self.location_url and self.async_url and self.method == 'POST') def set_initial_status(self, response): """Process first response after initiating long running operation and set self.status attribute. :param requests.Response response: initial REST call response. """ self._raise_if_bad_http_status_and_method(response) if self._is_empty(response): self.resource = None else: try: self.resource = self._deserialize(response) except DeserializationError: self.resource = None self.set_async_url_if_present(response) if response.status_code in {200, 201, 202, 204}: if self.async_url or self.location_url or response.status_code == 202: self.status = 'InProgress' elif response.status_code == 201: status = self._get_provisioning_state(response) self.status = status or 'InProgress' elif response.status_code == 200: status = self._get_provisioning_state(response) self.status = status or 'Succeeded' elif response.status_code == 204: self.status = 'Succeeded' self.resource = None else: raise OperationFailed("Invalid status found") return raise OperationFailed("Operation failed or cancelled") def get_status_from_location(self, response): """Process the latest status update retrieved from a 'location' header. :param requests.Response response: latest REST call response. :raises: BadResponse if response has no body and not status 202. """ self._raise_if_bad_http_status_and_method(response) code = response.status_code if code == 202: self.status = "InProgress" else: self.status = 'Succeeded' if self._is_empty(response): self.resource = None else: self.resource = self._deserialize(response) def get_status_from_resource(self, response): """Process the latest status update retrieved from the same URL as the previous request. :param requests.Response response: latest REST call response. :raises: BadResponse if status not 200 or 204. """ self._raise_if_bad_http_status_and_method(response) if self._is_empty(response): raise BadResponse('The response from long running operation ' 'does not contain a body.') status = self._get_provisioning_state(response) self.status = status or 'Succeeded' self.parse_resource(response) def parse_resource(self, response): """Assuming this response is a resource, use the deserialization callback to parse it. If body is empty, assuming no resource to return. """ self._raise_if_bad_http_status_and_method(response) if not self._is_empty(response): self.resource = self._deserialize(response) else: self.resource = None def get_status_from_async(self, response): """Process the latest status update retrieved from a 'azure-asyncoperation' header. :param requests.Response response: latest REST call response. :raises: BadResponse if response has no body, or body does not contain status. """ self._raise_if_bad_http_status_and_method(response) if self._is_empty(response): raise BadResponse('The response from long running operation ' 'does not contain a body.') self.status = self._get_async_status(response) if not self.status: raise BadResponse("No status found in body") # Status can contains information, see ARM spec: # https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/Addendum.md#operation-resource-format # "properties": { # /\* The resource provider can choose the values here, but it should only be # returned on a successful operation (status being "Succeeded"). \*/ #}, # So try to parse it try: self.resource = self._deserialize(response) except Exception: self.resource = None def set_async_url_if_present(self, response): async_url = get_header_url(response, 'azure-asyncoperation') if async_url: self.async_url = async_url location_url = get_header_url(response, 'location') if location_url: self.location_url = location_url def get_status_link(self): if self.async_url: return self.async_url elif self.location_url: return self.location_url elif self.method == "PUT": return self.initial_response.request.url else: raise BadResponse("Unable to find a valid status link for polling") class ARMPolling(PollingMethod): def __init__(self, timeout=30, lro_options=None, **operation_config): self._timeout = timeout self._operation = None # Will hold an instance of LongRunningOperation self._response = None # Will hold latest received response self._operation_config = operation_config self._lro_options = lro_options def status(self): """Return the current status as a string. :rtype: str """ if not self._operation: raise ValueError("set_initial_status was never called. Did you give this instance to a poller?") return self._operation.status def finished(self): """Is this polling finished? :rtype: bool """ return finished(self.status()) def resource(self): """Return the built resource. """ return self._operation.resource def initialize(self, client, initial_response, deserialization_callback): """Set the initial status of this LRO. :param initial_response: The initial response of the poller :raises: CloudError if initial status is incorrect LRO state """ self._client = client self._response = initial_response self._operation = LongRunningOperation(initial_response, deserialization_callback, self._lro_options) try: self._operation.set_initial_status(initial_response) except BadStatus: self._operation.status = 'Failed' raise CloudError(initial_response) except BadResponse as err: self._operation.status = 'Failed' raise CloudError(initial_response, str(err)) except OperationFailed: raise CloudError(initial_response) def run(self): try: self._poll() except BadStatus: self._operation.status = 'Failed' raise CloudError(self._response) except BadResponse as err: self._operation.status = 'Failed' raise CloudError(self._response, str(err)) except OperationFailed: raise CloudError(self._response) def _poll(self): """Poll status of operation so long as operation is incomplete and we have an endpoint to query. :param callable update_cmd: The function to call to retrieve the latest status of the long running operation. :raises: OperationFailed if operation status 'Failed' or 'Cancelled'. :raises: BadStatus if response status invalid. :raises: BadResponse if response invalid. """ while not self.finished(): self._delay() self.update_status() if failed(self._operation.status): raise OperationFailed("Operation failed or cancelled") elif self._operation.should_do_final_get(): if self._operation.method == 'POST' and self._operation.location_url: final_get_url = self._operation.location_url else: final_get_url = self._operation.initial_response.request.url self._response = self.request_status(final_get_url) self._operation.parse_resource(self._response) def _delay(self): """Check for a 'retry-after' header to set timeout, otherwise use configured timeout. """ if self._response is None: return if self._response.headers.get('retry-after'): time.sleep(int(self._response.headers['retry-after'])) else: time.sleep(self._timeout) def update_status(self): """Update the current status of the LRO. """ if self._operation.async_url: self._response = self.request_status(self._operation.async_url) self._operation.set_async_url_if_present(self._response) self._operation.get_status_from_async(self._response) elif self._operation.location_url: self._response = self.request_status(self._operation.location_url) self._operation.set_async_url_if_present(self._response) self._operation.get_status_from_location(self._response) elif self._operation.method == "PUT": initial_url = self._operation.initial_response.request.url self._response = self.request_status(initial_url) self._operation.set_async_url_if_present(self._response) self._operation.get_status_from_resource(self._response) else: raise BadResponse("Unable to find status link for polling.") def request_status(self, status_link): """Do a simple GET to this status link. This method re-inject 'x-ms-client-request-id'. :rtype: requests.Response """ request = self._client.get(status_link) # ARM requires to re-inject 'x-ms-client-request-id' while polling header_parameters = { 'x-ms-client-request-id': self._operation.initial_response.request.headers['x-ms-client-request-id'] } return self._client.send(request, header_parameters, stream=False, **self._operation_config) msrestazure-for-python-0.6.4/msrestazure/polling/async_arm_polling.py000066400000000000000000000120401367642573600263320ustar00rootroot00000000000000# -------------------------------------------------------------------------- # # Copyright (c) Microsoft Corporation. All rights reserved. # # The MIT License (MIT) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the ""Software""), to # deal in the Software without restriction, including without limitation the # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or # sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # # -------------------------------------------------------------------------- import asyncio from ..azure_exceptions import CloudError from .arm_polling import ( failed, BadStatus, BadResponse, OperationFailed, ARMPolling ) __all__ = ["AsyncARMPolling"] class AsyncARMPolling(ARMPolling): """A subclass or ARMPolling that redefine "run" as async. """ async def run(self): try: await self._poll() except BadStatus: self._operation.status = 'Failed' raise CloudError(self._response) except BadResponse as err: self._operation.status = 'Failed' raise CloudError(self._response, str(err)) except OperationFailed: raise CloudError(self._response) async def _poll(self): """Poll status of operation so long as operation is incomplete and we have an endpoint to query. :param callable update_cmd: The function to call to retrieve the latest status of the long running operation. :raises: OperationFailed if operation status 'Failed' or 'Cancelled'. :raises: BadStatus if response status invalid. :raises: BadResponse if response invalid. """ while not self.finished(): await self._delay() await self.update_status() if failed(self._operation.status): raise OperationFailed("Operation failed or cancelled") elif self._operation.should_do_final_get(): if self._operation.method == 'POST' and self._operation.location_url: final_get_url = self._operation.location_url else: final_get_url = self._operation.initial_response.request.url self._response = await self.request_status(final_get_url) self._operation.get_status_from_resource(self._response) async def _delay(self): """Check for a 'retry-after' header to set timeout, otherwise use configured timeout. """ if self._response is None: await asyncio.sleep(0) if self._response.headers.get('retry-after'): await asyncio.sleep(int(self._response.headers['retry-after'])) else: await asyncio.sleep(self._timeout) async def update_status(self): """Update the current status of the LRO. """ if self._operation.async_url: self._response = await self.request_status(self._operation.async_url) self._operation.set_async_url_if_present(self._response) self._operation.get_status_from_async(self._response) elif self._operation.location_url: self._response = await self.request_status(self._operation.location_url) self._operation.set_async_url_if_present(self._response) self._operation.get_status_from_location(self._response) elif self._operation.method == "PUT": initial_url = self._operation.initial_response.request.url self._response = await self.request_status(initial_url) self._operation.set_async_url_if_present(self._response) self._operation.get_status_from_resource(self._response) else: raise BadResponse("Unable to find status link for polling.") async def request_status(self, status_link): """Do a simple GET to this status link. This method re-inject 'x-ms-client-request-id'. :rtype: requests.Response """ # ARM requires to re-inject 'x-ms-client-request-id' while polling header_parameters = { 'x-ms-client-request-id': self._operation.initial_response.request.headers['x-ms-client-request-id'] } request = self._client.get(status_link, headers=header_parameters) return await self._client.async_send(request, stream=False, **self._operation_config) msrestazure-for-python-0.6.4/msrestazure/tools.py000066400000000000000000000263301367642573600223350ustar00rootroot00000000000000# -------------------------------------------------------------------------- # # Copyright (c) Microsoft Corporation. All rights reserved. # # The MIT License (MIT) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the ""Software""), to # deal in the Software without restriction, including without limitation the # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or # sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # # -------------------------------------------------------------------------- import json import re import logging import time import uuid _LOGGER = logging.getLogger(__name__) _ARMID_RE = re.compile( '(?i)/subscriptions/(?P[^/]*)(/resourceGroups/(?P[^/]*))?' '(/providers/(?P[^/]*)/(?P[^/]*)/(?P[^/]*)(?P.*))?') _CHILDREN_RE = re.compile('(?i)(/providers/(?P[^/]*))?/' '(?P[^/]*)/(?P[^/]*)') _ARMNAME_RE = re.compile('^[^<>%&:\\?/]{1,260}$') def register_rp_hook(r, *args, **kwargs): """This is a requests hook to register RP automatically. You should not use this command manually, this is added automatically by the SDK. See requests documentation for details of the signature of this function. http://docs.python-requests.org/en/master/user/advanced/#event-hooks """ if r.status_code == 409 and 'msrest' in kwargs: rp_name = _check_rp_not_registered_err(r) if rp_name: session = kwargs['msrest']['session'] url_prefix = _extract_subscription_url(r.request.url) if not _register_rp(session, url_prefix, rp_name): return req = r.request # Change the 'x-ms-client-request-id' otherwise the Azure endpoint # just returns the same 409 payload without looking at the actual query if 'x-ms-client-request-id' in req.headers: req.headers['x-ms-client-request-id'] = str(uuid.uuid1()) return session.send(req) def _check_rp_not_registered_err(response): try: response = json.loads(response.content.decode()) if response['error']['code'] == 'MissingSubscriptionRegistration': match = re.match(r".*'(.*)'", response['error']['message']) return match.group(1) except Exception: # pylint: disable=broad-except pass return None def _extract_subscription_url(url): """Extract the first part of the URL, just after subscription: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/ """ match = re.match(r".*/subscriptions/[a-f0-9-]+/", url, re.IGNORECASE) if not match: raise ValueError("Unable to extract subscription ID from URL") return match.group(0) def _register_rp(session, url_prefix, rp_name): """Synchronously register the RP is paremeter. Return False if we have a reason to believe this didn't work """ post_url = "{}providers/{}/register?api-version=2016-02-01".format(url_prefix, rp_name) get_url = "{}providers/{}?api-version=2016-02-01".format(url_prefix, rp_name) _LOGGER.warning("Resource provider '%s' used by this operation is not " "registered. We are registering for you.", rp_name) post_response = session.post(post_url) if post_response.status_code != 200: _LOGGER.warning("Registration failed. Please register manually.") return False while True: time.sleep(10) rp_info = session.get(get_url).json() if rp_info['registrationState'] == 'Registered': _LOGGER.warning("Registration succeeded.") return True def parse_resource_id(rid): """Parses a resource_id into its various parts. Returns a dictionary with a single key-value pair, 'name': rid, if invalid resource id. :param rid: The resource id being parsed :type rid: str :returns: A dictionary with with following key/value pairs (if found): - subscription: Subscription id - resource_group: Name of resource group - namespace: Namespace for the resource provider (i.e. Microsoft.Compute) - type: Type of the root resource (i.e. virtualMachines) - name: Name of the root resource - child_namespace_{level}: Namespace for the child resoure of that level - child_type_{level}: Type of the child resource of that level - child_name_{level}: Name of the child resource of that level - last_child_num: Level of the last child - resource_parent: Computed parent in the following pattern: providers/{namespace}\ /{parent}/{type}/{name} - resource_namespace: Same as namespace. Note that this may be different than the \ target resource's namespace. - resource_type: Type of the target resource (not the parent) - resource_name: Name of the target resource (not the parent) :rtype: dict[str,str] """ if not rid: return {} match = _ARMID_RE.match(rid) if match: result = match.groupdict() children = _CHILDREN_RE.finditer(result['children'] or '') count = None for count, child in enumerate(children): result.update({ key + '_%d' % (count + 1): group for key, group in child.groupdict().items()}) result['last_child_num'] = count + 1 if isinstance(count, int) else None result = _populate_alternate_kwargs(result) else: result = dict(name=rid) return {key: value for key, value in result.items() if value is not None} def _populate_alternate_kwargs(kwargs): """ Translates the parsed arguments into a format used by generic ARM commands such as the resource and lock commands. """ resource_namespace = kwargs['namespace'] resource_type = kwargs.get('child_type_{}'.format(kwargs['last_child_num'])) or kwargs['type'] resource_name = kwargs.get('child_name_{}'.format(kwargs['last_child_num'])) or kwargs['name'] _get_parents_from_parts(kwargs) kwargs['resource_namespace'] = resource_namespace kwargs['resource_type'] = resource_type kwargs['resource_name'] = resource_name return kwargs def _get_parents_from_parts(kwargs): """ Get the parents given all the children parameters. """ parent_builder = [] if kwargs['last_child_num'] is not None: parent_builder.append('{type}/{name}/'.format(**kwargs)) for index in range(1, kwargs['last_child_num']): child_namespace = kwargs.get('child_namespace_{}'.format(index)) if child_namespace is not None: parent_builder.append('providers/{}/'.format(child_namespace)) kwargs['child_parent_{}'.format(index)] = ''.join(parent_builder) parent_builder.append( '{{child_type_{0}}}/{{child_name_{0}}}/' .format(index).format(**kwargs)) child_namespace = kwargs.get('child_namespace_{}'.format(kwargs['last_child_num'])) if child_namespace is not None: parent_builder.append('providers/{}/'.format(child_namespace)) kwargs['child_parent_{}'.format(kwargs['last_child_num'])] = ''.join(parent_builder) kwargs['resource_parent'] = ''.join(parent_builder) if kwargs['name'] else None return kwargs def resource_id(**kwargs): """Create a valid resource id string from the given parts. This method builds the resource id from the left until the next required id parameter to be appended is not found. It then returns the built up id. :param dict kwargs: The keyword arguments that will make up the id. The method accepts the following keyword arguments: - subscription (required): Subscription id - resource_group: Name of resource group - namespace: Namespace for the resource provider (i.e. Microsoft.Compute) - type: Type of the resource (i.e. virtualMachines) - name: Name of the resource (or parent if child_name is also \ specified) - child_namespace_{level}: Namespace for the child resoure of that level (optional) - child_type_{level}: Type of the child resource of that level - child_name_{level}: Name of the child resource of that level :returns: A resource id built from the given arguments. :rtype: str """ kwargs = {k: v for k, v in kwargs.items() if v is not None} rid_builder = ['/subscriptions/{subscription}'.format(**kwargs)] try: try: rid_builder.append('resourceGroups/{resource_group}'.format(**kwargs)) except KeyError: pass rid_builder.append('providers/{namespace}'.format(**kwargs)) rid_builder.append('{type}/{name}'.format(**kwargs)) count = 1 while True: try: rid_builder.append('providers/{{child_namespace_{}}}' .format(count).format(**kwargs)) except KeyError: pass rid_builder.append('{{child_type_{0}}}/{{child_name_{0}}}' .format(count).format(**kwargs)) count += 1 except KeyError: pass return '/'.join(rid_builder) def is_valid_resource_id(rid, exception_type=None): """Validates the given resource id. :param rid: The resource id being validated. :type rid: str :param exception_type: Raises this Exception if invalid. :type exception_type: :class:`Exception` :returns: A boolean describing whether the id is valid. :rtype: bool """ is_valid = False try: is_valid = rid and resource_id(**parse_resource_id(rid)).lower() == rid.lower() except KeyError: pass if not is_valid and exception_type: raise exception_type() return is_valid def is_valid_resource_name(rname, exception_type=None): """Validates the given resource name to ARM guidelines, individual services may be more restrictive. :param rname: The resource name being validated. :type rname: str :param exception_type: Raises this Exception if invalid. :type exception_type: :class:`Exception` :returns: A boolean describing whether the name is valid. :rtype: bool """ match = _ARMNAME_RE.match(rname) if match: return True if exception_type: raise exception_type() return False msrestazure-for-python-0.6.4/msrestazure/version.py000066400000000000000000000025711367642573600226630ustar00rootroot00000000000000# -------------------------------------------------------------------------- # # Copyright (c) Microsoft Corporation. All rights reserved. # # The MIT License (MIT) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the ""Software""), to # deal in the Software without restriction, including without limitation the # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or # sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # # -------------------------------------------------------------------------- #: version of the package. Use msrestazure.__version__ instead. msrestazure_version = "0.6.4" msrestazure-for-python-0.6.4/setup.cfg000066400000000000000000000000311367642573600200460ustar00rootroot00000000000000[bdist_wheel] universal=1msrestazure-for-python-0.6.4/setup.py000066400000000000000000000046021367642573600177470ustar00rootroot00000000000000# -------------------------------------------------------------------------- # # Copyright (c) Microsoft Corporation. All rights reserved. # # The MIT License (MIT) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the ""Software""), to # deal in the Software without restriction, including without limitation the # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or # sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # # -------------------------------------------------------------------------- from setuptools import setup, find_packages setup( name='msrestazure', version='0.6.4', author='Microsoft Corporation', author_email='azpysdkhelp@microsoft.com', packages=find_packages(exclude=["tests", "tests.*"]), url='https://github.com/Azure/msrestazure-for-python', license='MIT License', description=('AutoRest swagger generator Python client runtime. ' 'Azure-specific module.'), long_description=open('README.rst').read(), classifiers=[ 'Development Status :: 4 - Beta', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'License :: OSI Approved :: MIT License', 'Topic :: Software Development'], install_requires=[ "msrest>=0.6.0,<2.0.0", "adal>=0.6.0,<2.0.0", "six", ], ) msrestazure-for-python-0.6.4/tests/000077500000000000000000000000001367642573600173755ustar00rootroot00000000000000msrestazure-for-python-0.6.4/tests/.gitignore000066400000000000000000000000201367642573600213550ustar00rootroot00000000000000credentials.jsonmsrestazure-for-python-0.6.4/tests/__init__.py000066400000000000000000000030771367642573600215150ustar00rootroot00000000000000# -------------------------------------------------------------------------- # # Copyright (c) Microsoft Corporation. All rights reserved. # # The MIT License (MIT) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the ""Software""), to # deal in the Software without restriction, including without limitation the # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or # sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # # -------------------------------------------------------------------------- import os from unittest import TestLoader, TextTestRunner if __name__ == '__main__': runner = TextTestRunner(verbosity=2) test_dir = os.path.dirname(__file__) test_loader = TestLoader() suite = test_loader.discover(test_dir, pattern="unittest_*.py") runner.run(suite) msrestazure-for-python-0.6.4/tests/asynctests/000077500000000000000000000000001367642573600215755ustar00rootroot00000000000000msrestazure-for-python-0.6.4/tests/asynctests/test_async_arm_polling.py000066400000000000000000000356501367642573600267170ustar00rootroot00000000000000#-------------------------------------------------------------------------- # # Copyright (c) Microsoft Corporation. All rights reserved. # # The MIT License (MIT) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the ""Software""), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # #-------------------------------------------------------------------------- import json import re import types import unittest try: from unittest import mock except ImportError: import mock import pytest from requests import Request, Response from msrest import Deserializer, Configuration from msrest.async_client import ServiceClientAsync from msrest.exceptions import DeserializationError from msrest.polling import async_poller from msrestazure.azure_exceptions import CloudError from msrestazure.polling.async_arm_polling import ( AsyncARMPolling, ) from msrestazure.polling.arm_polling import ( LongRunningOperation, BadStatus ) class SimpleResource: """An implementation of Python 3 SimpleNamespace. Used to deserialize resource objects from response bodies where no particular object type has been specified. """ def __init__(self, **kwargs): self.__dict__.update(kwargs) def __repr__(self): keys = sorted(self.__dict__) items = ("{}={!r}".format(k, self.__dict__[k]) for k in keys) return "{}({})".format(type(self).__name__, ", ".join(items)) def __eq__(self, other): return self.__dict__ == other.__dict__ class BadEndpointError(Exception): pass TEST_NAME = 'foo' RESPONSE_BODY = {'properties':{'provisioningState': 'InProgress'}} ASYNC_BODY = json.dumps({ 'status': 'Succeeded' }) ASYNC_URL = 'http://dummyurlFromAzureAsyncOPHeader_Return200' LOCATION_BODY = json.dumps({ 'name': TEST_NAME }) LOCATION_URL = 'http://dummyurlurlFromLocationHeader_Return200' RESOURCE_BODY = json.dumps({ 'name': TEST_NAME }) RESOURCE_URL = 'http://subscriptions/sub1/resourcegroups/g1/resourcetype1/resource1' ERROR = 'http://dummyurl_ReturnError' POLLING_STATUS = 200 CLIENT = ServiceClientAsync(Configuration("http://example.org")) async def mock_send(client_self, request, *, stream): return TestArmPolling.mock_update(request.url) CLIENT.async_send = types.MethodType(mock_send, CLIENT) class TestArmPolling(object): convert = re.compile('([a-z0-9])([A-Z])') @staticmethod def mock_send(method, status, headers, body=None): response = mock.create_autospec(Response) response.request = mock.create_autospec(Request) response.request.method = method response.request.url = RESOURCE_URL response.request.headers = { 'x-ms-client-request-id': '67f4dd4e-6262-45e1-8bed-5c45cf23b6d9' } response.status_code = status response.headers = headers response.headers.update({"content-type": "application/json; charset=utf8"}) content = body if body is not None else RESPONSE_BODY response.text = json.dumps(content) response.json = lambda: json.loads(response.text) return response @staticmethod def mock_update(url, headers=None): response = mock.create_autospec(Response) response.request = mock.create_autospec(Request) response.request.method = 'GET' response.headers = headers or {} response.headers.update({"content-type": "application/json; charset=utf8"}) if url == ASYNC_URL: response.request.url = url response.status_code = POLLING_STATUS response.text = ASYNC_BODY response.randomFieldFromPollAsyncOpHeader = None elif url == LOCATION_URL: response.request.url = url response.status_code = POLLING_STATUS response.text = LOCATION_BODY response.randomFieldFromPollLocationHeader = None elif url == ERROR: raise BadEndpointError("boom") elif url == RESOURCE_URL: response.request.url = url response.status_code = POLLING_STATUS response.text = RESOURCE_BODY else: raise Exception('URL does not match') response.json = lambda: json.loads(response.text) return response @staticmethod def mock_outputs(response): body = response.json() body = {TestArmPolling.convert.sub(r'\1_\2', k).lower(): v for k, v in body.items()} properties = body.setdefault('properties', {}) if 'name' in body: properties['name'] = body['name'] if properties: properties = {TestArmPolling.convert.sub(r'\1_\2', k).lower(): v for k, v in properties.items()} del body['properties'] body.update(properties) resource = SimpleResource(**body) else: raise DeserializationError("Impossible to deserialize") resource = SimpleResource(**body) return resource @pytest.mark.asyncio async def test_long_running_put(): #TODO: Test custom header field # Test throw on non LRO related status code response = TestArmPolling.mock_send('PUT', 1000, {}) op = LongRunningOperation(response, lambda x:None) with pytest.raises(BadStatus): op.set_initial_status(response) with pytest.raises(CloudError): await async_poller(CLIENT, response, TestArmPolling.mock_outputs, AsyncARMPolling(0)) # Test with no polling necessary response_body = { 'properties':{'provisioningState': 'Succeeded'}, 'name': TEST_NAME } response = TestArmPolling.mock_send( 'PUT', 201, {}, response_body ) def no_update_allowed(url, headers=None): raise ValueError("Should not try to update") polling_method = AsyncARMPolling(0) poll = await async_poller(CLIENT, response, TestArmPolling.mock_outputs, polling_method ) assert poll.name == TEST_NAME assert not hasattr(polling_method._response, 'randomFieldFromPollAsyncOpHeader') # Test polling from azure-asyncoperation header response = TestArmPolling.mock_send( 'PUT', 201, {'azure-asyncoperation': ASYNC_URL}) polling_method = AsyncARMPolling(0) poll = await async_poller(CLIENT, response, TestArmPolling.mock_outputs, polling_method) assert poll.name == TEST_NAME assert not hasattr(polling_method._response, 'randomFieldFromPollAsyncOpHeader') # Test polling location header response = TestArmPolling.mock_send( 'PUT', 201, {'location': LOCATION_URL}) polling_method = AsyncARMPolling(0) poll = await async_poller(CLIENT, response, TestArmPolling.mock_outputs, polling_method) assert poll.name == TEST_NAME assert polling_method._response.randomFieldFromPollLocationHeader is None # Test polling initial payload invalid (SQLDb) response_body = {} # Empty will raise response = TestArmPolling.mock_send( 'PUT', 201, {'location': LOCATION_URL}, response_body) polling_method = AsyncARMPolling(0) poll = await async_poller(CLIENT, response, TestArmPolling.mock_outputs, polling_method) assert poll.name == TEST_NAME assert polling_method._response.randomFieldFromPollLocationHeader is None # Test fail to poll from azure-asyncoperation header response = TestArmPolling.mock_send( 'PUT', 201, {'azure-asyncoperation': ERROR}) with pytest.raises(BadEndpointError): poll = await async_poller(CLIENT, response, TestArmPolling.mock_outputs, AsyncARMPolling(0)) # Test fail to poll from location header response = TestArmPolling.mock_send( 'PUT', 201, {'location': ERROR}) with pytest.raises(BadEndpointError): poll = await async_poller(CLIENT, response, TestArmPolling.mock_outputs, AsyncARMPolling(0)) @pytest.mark.asyncio async def test_long_running_patch(): # Test polling from location header response = TestArmPolling.mock_send( 'PATCH', 202, {'location': LOCATION_URL}, body={'properties':{'provisioningState': 'Succeeded'}}) polling_method = AsyncARMPolling(0) poll = await async_poller(CLIENT, response, TestArmPolling.mock_outputs, polling_method) assert poll.name == TEST_NAME assert polling_method._response.randomFieldFromPollLocationHeader is None # Test polling from azure-asyncoperation header response = TestArmPolling.mock_send( 'PATCH', 202, {'azure-asyncoperation': ASYNC_URL}, body={'properties':{'provisioningState': 'Succeeded'}}) polling_method = AsyncARMPolling(0) poll = await async_poller(CLIENT, response, TestArmPolling.mock_outputs, polling_method) assert poll.name == TEST_NAME assert not hasattr(polling_method._response, 'randomFieldFromPollAsyncOpHeader') # Test polling from location header response = TestArmPolling.mock_send( 'PATCH', 200, {'location': LOCATION_URL}, body={'properties':{'provisioningState': 'Succeeded'}}) polling_method = AsyncARMPolling(0) poll = await async_poller(CLIENT, response, TestArmPolling.mock_outputs, polling_method) assert poll.name == TEST_NAME assert polling_method._response.randomFieldFromPollLocationHeader is None # Test polling from azure-asyncoperation header response = TestArmPolling.mock_send( 'PATCH', 200, {'azure-asyncoperation': ASYNC_URL}, body={'properties':{'provisioningState': 'Succeeded'}}) polling_method = AsyncARMPolling(0) poll = await async_poller(CLIENT, response, TestArmPolling.mock_outputs, polling_method) assert poll.name == TEST_NAME assert not hasattr(polling_method._response, 'randomFieldFromPollAsyncOpHeader') # Test fail to poll from azure-asyncoperation header response = TestArmPolling.mock_send( 'PATCH', 202, {'azure-asyncoperation': ERROR}) with pytest.raises(BadEndpointError): poll = await async_poller(CLIENT, response, TestArmPolling.mock_outputs, AsyncARMPolling(0)) # Test fail to poll from location header response = TestArmPolling.mock_send( 'PATCH', 202, {'location': ERROR}) with pytest.raises(BadEndpointError): poll = await async_poller(CLIENT, response, TestArmPolling.mock_outputs, AsyncARMPolling(0)) @pytest.mark.asyncio async def test_long_running_delete(): # Test polling from azure-asyncoperation header response = TestArmPolling.mock_send( 'DELETE', 202, {'azure-asyncoperation': ASYNC_URL}, body="" ) polling_method = AsyncARMPolling(0) poll = await async_poller(CLIENT, response, TestArmPolling.mock_outputs, polling_method) assert poll is None assert polling_method._response.randomFieldFromPollAsyncOpHeader is None @pytest.mark.asyncio async def test_long_running_post(): # Test throw on non LRO related status code response = TestArmPolling.mock_send('POST', 201, {}) op = LongRunningOperation(response, lambda x:None) with pytest.raises(BadStatus): op.set_initial_status(response) with pytest.raises(CloudError): await async_poller(CLIENT, response, TestArmPolling.mock_outputs, AsyncARMPolling(0)) # Test polling from azure-asyncoperation header response = TestArmPolling.mock_send( 'POST', 202, {'azure-asyncoperation': ASYNC_URL}, body={'properties':{'provisioningState': 'Succeeded'}}) polling_method = AsyncARMPolling(0) poll = await async_poller(CLIENT, response, TestArmPolling.mock_outputs, polling_method) #self.assertIsNone(poll) assert polling_method._response.randomFieldFromPollAsyncOpHeader is None # Test polling from location header response = TestArmPolling.mock_send( 'POST', 202, {'location': LOCATION_URL}, body={'properties':{'provisioningState': 'Succeeded'}}) polling_method = AsyncARMPolling(0) poll = await async_poller(CLIENT, response, TestArmPolling.mock_outputs, polling_method) assert poll.name == TEST_NAME assert polling_method._response.randomFieldFromPollLocationHeader is None # Test fail to poll from azure-asyncoperation header response = TestArmPolling.mock_send( 'POST', 202, {'azure-asyncoperation': ERROR}) with pytest.raises(BadEndpointError): await async_poller(CLIENT, response, TestArmPolling.mock_outputs, AsyncARMPolling(0)) # Test fail to poll from location header response = TestArmPolling.mock_send( 'POST', 202, {'location': ERROR}) with pytest.raises(BadEndpointError): await async_poller(CLIENT, response, TestArmPolling.mock_outputs, AsyncARMPolling(0)) @pytest.mark.asyncio async def test_long_running_negative(): global LOCATION_BODY global POLLING_STATUS # Test LRO PUT throws for invalid json LOCATION_BODY = '{' response = TestArmPolling.mock_send( 'POST', 202, {'location': LOCATION_URL}) poll = async_poller( CLIENT, response, TestArmPolling.mock_outputs, AsyncARMPolling(0) ) with pytest.raises(DeserializationError): await poll LOCATION_BODY = '{\'"}' response = TestArmPolling.mock_send( 'POST', 202, {'location': LOCATION_URL}) poll = async_poller(CLIENT, response, TestArmPolling.mock_outputs, AsyncARMPolling(0)) with pytest.raises(DeserializationError): await poll LOCATION_BODY = '{' POLLING_STATUS = 203 response = TestArmPolling.mock_send( 'POST', 202, {'location': LOCATION_URL}) poll = async_poller(CLIENT, response, TestArmPolling.mock_outputs, AsyncARMPolling(0)) with pytest.raises(CloudError): # TODO: Node.js raises on deserialization await poll LOCATION_BODY = json.dumps({ 'name': TEST_NAME }) POLLING_STATUS = 200 msrestazure-for-python-0.6.4/tests/conftest.py000066400000000000000000000045441367642573600216030ustar00rootroot00000000000000#-------------------------------------------------------------------------- # # Copyright (c) Microsoft Corporation. All rights reserved. # # The MIT License (MIT) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the ""Software""), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # #-------------------------------------------------------------------------- import json import os.path import sys import pytest CWD = os.path.dirname(__file__) # Ignore collection of async tests for Python 2 collect_ignore = [] if sys.version_info < (3, 5): collect_ignore.append("asynctests") def pytest_addoption(parser): parser.addoption("--runslow", action="store_true", default=False, help="run slow tests") def pytest_collection_modifyitems(config, items): if config.getoption("--runslow"): # --runslow given in cli: do not skip slow tests return skip_slow = pytest.mark.skip(reason="need --runslow option to run") for item in items: if "slow" in item.keywords: item.add_marker(skip_slow) @pytest.fixture def user_password(): filepath = os.path.join(CWD, "credentials.json") if os.path.exists(filepath): with open(filepath, "r") as fd: userpass = json.load(fd)["userpass"] return userpass["user"], userpass["password"] raise ValueError("Create a {} file with a 'userpass' key and two keys 'user' and 'password'".format( filepath )) msrestazure-for-python-0.6.4/tests/test_arm_polling.py000066400000000000000000000456031367642573600233210ustar00rootroot00000000000000#-------------------------------------------------------------------------- # # Copyright (c) Microsoft Corporation. All rights reserved. # # The MIT License (MIT) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the ""Software""), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # #-------------------------------------------------------------------------- import json import re import types import unittest try: from unittest import mock except ImportError: import mock import httpretty import pytest from requests import Request, Response from msrest import Deserializer, Configuration from msrest.service_client import ServiceClient from msrest.exceptions import DeserializationError from msrest.polling import LROPoller from msrestazure.azure_exceptions import CloudError from msrestazure.polling.arm_polling import ( LongRunningOperation, ARMPolling, BadStatus ) class SimpleResource: """An implementation of Python 3 SimpleNamespace. Used to deserialize resource objects from response bodies where no particular object type has been specified. """ def __init__(self, **kwargs): self.__dict__.update(kwargs) def __repr__(self): keys = sorted(self.__dict__) items = ("{}={!r}".format(k, self.__dict__[k]) for k in keys) return "{}({})".format(type(self).__name__, ", ".join(items)) def __eq__(self, other): return self.__dict__ == other.__dict__ class BadEndpointError(Exception): pass TEST_NAME = 'foo' RESPONSE_BODY = {'properties':{'provisioningState': 'InProgress'}} ASYNC_BODY = json.dumps({ 'status': 'Succeeded' }) ASYNC_URL = 'http://dummyurlFromAzureAsyncOPHeader_Return200' LOCATION_BODY = json.dumps({ 'name': TEST_NAME }) LOCATION_URL = 'http://dummyurlurlFromLocationHeader_Return200' RESOURCE_BODY = json.dumps({ 'name': TEST_NAME }) RESOURCE_URL = 'http://subscriptions/sub1/resourcegroups/g1/resourcetype1/resource1' ERROR = 'http://dummyurl_ReturnError' POLLING_STATUS = 200 CLIENT = ServiceClient(None, Configuration("http://example.org")) def mock_send(client_self, request, header_parameters, stream): return TestArmPolling.mock_update(request.url, header_parameters) CLIENT.send = types.MethodType(mock_send, CLIENT) class TestArmPolling(object): convert = re.compile('([a-z0-9])([A-Z])') @staticmethod def mock_send(method, status, headers=None, body=None): if headers is None: headers = {} response = mock.create_autospec(Response) response.request = mock.create_autospec(Request) response.request.method = method response.request.url = RESOURCE_URL response.request.headers = { 'x-ms-client-request-id': '67f4dd4e-6262-45e1-8bed-5c45cf23b6d9' } response.status_code = status response.headers = headers response.headers.update({"content-type": "application/json; charset=utf8"}) content = body if body is not None else RESPONSE_BODY response.text = json.dumps(content) response.json = lambda: json.loads(response.text) return response @staticmethod def mock_update(url, headers=None): response = mock.create_autospec(Response) response.request = mock.create_autospec(Request) response.request.method = 'GET' response.headers = headers or {} response.headers.update({"content-type": "application/json; charset=utf8"}) if url == ASYNC_URL: response.request.url = url response.status_code = POLLING_STATUS response.text = ASYNC_BODY response.randomFieldFromPollAsyncOpHeader = None elif url == LOCATION_URL: response.request.url = url response.status_code = POLLING_STATUS response.text = LOCATION_BODY response.randomFieldFromPollLocationHeader = None elif url == ERROR: raise BadEndpointError("boom") elif url == RESOURCE_URL: response.request.url = url response.status_code = POLLING_STATUS response.text = RESOURCE_BODY else: raise Exception('URL does not match') response.json = lambda: json.loads(response.text) return response @staticmethod def mock_outputs(response): body = response.json() body = {TestArmPolling.convert.sub(r'\1_\2', k).lower(): v for k, v in body.items()} properties = body.setdefault('properties', {}) if 'name' in body: properties['name'] = body['name'] if properties: properties = {TestArmPolling.convert.sub(r'\1_\2', k).lower(): v for k, v in properties.items()} del body['properties'] body.update(properties) resource = SimpleResource(**body) else: raise DeserializationError("Impossible to deserialize") resource = SimpleResource(**body) return resource def test_long_running_put(self): #TODO: Test custom header field # Test throw on non LRO related status code response = TestArmPolling.mock_send('PUT', 1000, {}) op = LongRunningOperation(response, lambda x:None) with pytest.raises(BadStatus): op.set_initial_status(response) with pytest.raises(CloudError): LROPoller(CLIENT, response, TestArmPolling.mock_outputs, ARMPolling(0)).result() # Test with no polling necessary response_body = { 'properties':{'provisioningState': 'Succeeded'}, 'name': TEST_NAME } response = TestArmPolling.mock_send( 'PUT', 201, {}, response_body ) def no_update_allowed(url, headers=None): raise ValueError("Should not try to update") poll = LROPoller(CLIENT, response, TestArmPolling.mock_outputs, ARMPolling(0) ) assert poll.result().name == TEST_NAME assert not hasattr(poll._polling_method._response, 'randomFieldFromPollAsyncOpHeader') # Test polling from azure-asyncoperation header response = TestArmPolling.mock_send( 'PUT', 201, {'azure-asyncoperation': ASYNC_URL}) poll = LROPoller(CLIENT, response, TestArmPolling.mock_outputs, ARMPolling(0)) assert poll.result().name == TEST_NAME assert not hasattr(poll._polling_method._response, 'randomFieldFromPollAsyncOpHeader') # Test polling location header response = TestArmPolling.mock_send( 'PUT', 201, {'location': LOCATION_URL}) poll = LROPoller(CLIENT, response, TestArmPolling.mock_outputs, ARMPolling(0)) assert poll.result().name == TEST_NAME assert poll._polling_method._response.randomFieldFromPollLocationHeader is None # Test polling initial payload invalid (SQLDb) response_body = {} # Empty will raise response = TestArmPolling.mock_send( 'PUT', 201, {'location': LOCATION_URL}, response_body) poll = LROPoller(CLIENT, response, TestArmPolling.mock_outputs, ARMPolling(0)) assert poll.result().name == TEST_NAME assert poll._polling_method._response.randomFieldFromPollLocationHeader is None # Test fail to poll from azure-asyncoperation header response = TestArmPolling.mock_send( 'PUT', 201, {'azure-asyncoperation': ERROR}) with pytest.raises(BadEndpointError): poll = LROPoller(CLIENT, response, TestArmPolling.mock_outputs, ARMPolling(0)).result() # Test fail to poll from location header response = TestArmPolling.mock_send( 'PUT', 201, {'location': ERROR}) with pytest.raises(BadEndpointError): poll = LROPoller(CLIENT, response, TestArmPolling.mock_outputs, ARMPolling(0)).result() def test_long_running_patch(self): # Test polling from location header response = TestArmPolling.mock_send( 'PATCH', 202, {'location': LOCATION_URL}, body={'properties':{'provisioningState': 'Succeeded'}}) poll = LROPoller(CLIENT, response, TestArmPolling.mock_outputs, ARMPolling(0)) assert poll.result().name == TEST_NAME assert poll._polling_method._response.randomFieldFromPollLocationHeader is None # Test polling from azure-asyncoperation header response = TestArmPolling.mock_send( 'PATCH', 202, {'azure-asyncoperation': ASYNC_URL}, body={'properties':{'provisioningState': 'Succeeded'}}) poll = LROPoller(CLIENT, response, TestArmPolling.mock_outputs, ARMPolling(0)) assert poll.result().name == TEST_NAME assert not hasattr(poll._polling_method._response, 'randomFieldFromPollAsyncOpHeader') # Test polling from location header response = TestArmPolling.mock_send( 'PATCH', 200, {'location': LOCATION_URL}, body={'properties':{'provisioningState': 'Succeeded'}}) poll = LROPoller(CLIENT, response, TestArmPolling.mock_outputs, ARMPolling(0)) assert poll.result().name == TEST_NAME assert poll._polling_method._response.randomFieldFromPollLocationHeader is None # Test polling from azure-asyncoperation header response = TestArmPolling.mock_send( 'PATCH', 200, {'azure-asyncoperation': ASYNC_URL}, body={'properties':{'provisioningState': 'Succeeded'}}) poll = LROPoller(CLIENT, response, TestArmPolling.mock_outputs, ARMPolling(0)) assert poll.result().name == TEST_NAME assert not hasattr(poll._polling_method._response, 'randomFieldFromPollAsyncOpHeader') # Test fail to poll from azure-asyncoperation header response = TestArmPolling.mock_send( 'PATCH', 202, {'azure-asyncoperation': ERROR}) with pytest.raises(BadEndpointError): poll = LROPoller(CLIENT, response, TestArmPolling.mock_outputs, ARMPolling(0)).result() # Test fail to poll from location header response = TestArmPolling.mock_send( 'PATCH', 202, {'location': ERROR}) with pytest.raises(BadEndpointError): poll = LROPoller(CLIENT, response, TestArmPolling.mock_outputs, ARMPolling(0)).result() def test_long_running_delete(self): # Test polling from azure-asyncoperation header response = TestArmPolling.mock_send( 'DELETE', 202, {'azure-asyncoperation': ASYNC_URL}, body="" ) poll = LROPoller(CLIENT, response, TestArmPolling.mock_outputs, ARMPolling(0)) poll.wait() assert poll.result() is None assert poll._polling_method._response.randomFieldFromPollAsyncOpHeader is None @httpretty.activate def test_long_running_post(self): # Test POST LRO with both Location and Azure-AsyncOperation # The initial response contains both Location and Azure-AsyncOperation, a 202 and no Body response = TestArmPolling.mock_send( 'POST', 202, { 'location': 'http://example.org/location', 'azure-asyncoperation': 'http://example.org/async_monitor', }, '' ) class TestServiceClient(ServiceClient): def __init__(self): ServiceClient.__init__(self, None, Configuration("http://example.org")) def send(self, request, headers=None, content=None, **config): assert request.method == 'GET' if request.url == 'http://example.org/location': return TestArmPolling.mock_send( 'GET', 200, body={'location_result': True} ) elif request.url == 'http://example.org/async_monitor': return TestArmPolling.mock_send( 'GET', 200, body={'status': 'Succeeded'} ) else: pytest.fail("No other query allowed") def deserialization_cb(response): return response.json() # Test 1, LRO options with Location final state poll = LROPoller( TestServiceClient(), response, deserialization_cb, ARMPolling(0, lro_options={"final-state-via": "location"})) result = poll.result() assert result['location_result'] == True # Test 2, LRO options with Azure-AsyncOperation final state poll = LROPoller( TestServiceClient(), response, deserialization_cb, ARMPolling(0, lro_options={"final-state-via": "azure-async-operation"})) result = poll.result() assert result['status'] == 'Succeeded' # Test 3, backward compat (no options, means "azure-async-operation") poll = LROPoller( TestServiceClient(), response, deserialization_cb, ARMPolling(0)) result = poll.result() assert result['status'] == 'Succeeded' # Test 4, location has no body class TestServiceClientNoBody(ServiceClient): def __init__(self): ServiceClient.__init__(self, None, Configuration("http://example.org")) def send(self, request, headers=None, content=None, **config): assert request.method == 'GET' if request.url == 'http://example.org/location': return TestArmPolling.mock_send( 'GET', 200, body="" ) elif request.url == 'http://example.org/async_monitor': return TestArmPolling.mock_send( 'GET', 200, body={'status': 'Succeeded'} ) else: pytest.fail("No other query allowed") poll = LROPoller( TestServiceClientNoBody(), response, deserialization_cb, ARMPolling(0, lro_options={"final-state-via": "location"})) result = poll.result() assert result is None # Former oooooold tests to refactor one day to something more readble # Test throw on non LRO related status code response = TestArmPolling.mock_send('POST', 201, {}) op = LongRunningOperation(response, lambda x:None) with pytest.raises(BadStatus): op.set_initial_status(response) with pytest.raises(CloudError): LROPoller(CLIENT, response, TestArmPolling.mock_outputs, ARMPolling(0)).result() # Test polling from azure-asyncoperation header response = TestArmPolling.mock_send( 'POST', 202, {'azure-asyncoperation': ASYNC_URL}, body={'properties':{'provisioningState': 'Succeeded'}}) poll = LROPoller(CLIENT, response, TestArmPolling.mock_outputs, ARMPolling(0)) poll.wait() #self.assertIsNone(poll.result()) assert poll._polling_method._response.randomFieldFromPollAsyncOpHeader is None # Test polling from location header response = TestArmPolling.mock_send( 'POST', 202, {'location': LOCATION_URL}, body={'properties':{'provisioningState': 'Succeeded'}}) poll = LROPoller(CLIENT, response, TestArmPolling.mock_outputs, ARMPolling(0)) assert poll.result().name == TEST_NAME assert poll._polling_method._response.randomFieldFromPollLocationHeader is None # Test fail to poll from azure-asyncoperation header response = TestArmPolling.mock_send( 'POST', 202, {'azure-asyncoperation': ERROR}) with pytest.raises(BadEndpointError): poll = LROPoller(CLIENT, response, TestArmPolling.mock_outputs, ARMPolling(0)).result() # Test fail to poll from location header response = TestArmPolling.mock_send( 'POST', 202, {'location': ERROR}) with pytest.raises(BadEndpointError): poll = LROPoller(CLIENT, response, TestArmPolling.mock_outputs, ARMPolling(0)).result() def test_long_running_negative(self): global LOCATION_BODY global POLLING_STATUS # Test LRO PUT throws for invalid json LOCATION_BODY = '{' response = TestArmPolling.mock_send( 'POST', 202, {'location': LOCATION_URL}) poll = LROPoller( CLIENT, response, TestArmPolling.mock_outputs, ARMPolling(0) ) with pytest.raises(DeserializationError): poll.wait() LOCATION_BODY = '{\'"}' response = TestArmPolling.mock_send( 'POST', 202, {'location': LOCATION_URL}) poll = LROPoller(CLIENT, response, TestArmPolling.mock_outputs, ARMPolling(0)) with pytest.raises(DeserializationError): poll.wait() LOCATION_BODY = '{' POLLING_STATUS = 203 response = TestArmPolling.mock_send( 'POST', 202, {'location': LOCATION_URL}) poll = LROPoller(CLIENT, response, TestArmPolling.mock_outputs, ARMPolling(0)) with pytest.raises(CloudError): # TODO: Node.js raises on deserialization poll.wait() LOCATION_BODY = json.dumps({ 'name': TEST_NAME }) POLLING_STATUS = 200 msrestazure-for-python-0.6.4/tests/test_auth.py000066400000000000000000000661351367642573600217620ustar00rootroot00000000000000#-------------------------------------------------------------------------- # # Copyright (c) Microsoft Corporation. All rights reserved. # # The MIT License (MIT) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the ""Software""), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # #-------------------------------------------------------------------------- import json import sys import time import unittest try: from unittest import mock except ImportError: import mock from requests import HTTPError, Session, ConnectionError import oauthlib import adal import httpretty from msrestazure.azure_active_directory import ( AADMixin, ServicePrincipalCredentials, UserPassCredentials, AADTokenCredentials, AdalAuthentication, MSIAuthentication, get_msi_token, get_msi_token_webapp ) from msrestazure.azure_cloud import AZURE_CHINA_CLOUD from msrestazure.azure_exceptions import MSIAuthenticationTimeoutError from msrest.exceptions import TokenExpiredError, AuthenticationError import pytest class TestServicePrincipalCredentials(unittest.TestCase): def test_convert_token(self): mix = AADMixin(None, None) token = {'access_token':'abc', 'expires_on':123, 'refresh_token':'asd'} self.assertEqual(mix._convert_token(token), token) caps = {'accessToken':'abc', 'expiresOn':123, 'refreshToken':'asd'} self.assertEqual(mix._convert_token(caps), token) caps = {'ACCessToken':'abc', 'Expires_On':123, 'REFRESH_TOKEN':'asd'} self.assertEqual(mix._convert_token(caps), token) @mock.patch("adal.AuthenticationContext") def test_property(self, adal_context): adal_context.acquire_token_with_client_credentials = mock.Mock() creds = ServicePrincipalCredentials( 123, 'secret', tenant="private" ) # Implicit set_token call adal_context.assert_called_with( "https://login.microsoftonline.com/private", timeout=None, verify_ssl=None, proxies=None, validate_authority=True, cache=None, api_version=None ) creds.timeout = 12 assert creds._context is None creds.set_token() adal_context.assert_called_with( "https://login.microsoftonline.com/private", timeout=12, verify_ssl=None, proxies=None, validate_authority=True, cache=None, api_version=None ) creds.verify = True assert creds._context is None creds.set_token() adal_context.assert_called_with( "https://login.microsoftonline.com/private", timeout=12, verify_ssl=True, proxies=None, validate_authority=True, cache=None, api_version=None ) creds.proxies = {} assert creds._context is None creds.set_token() adal_context.assert_called_with( "https://login.microsoftonline.com/private", timeout=12, verify_ssl=True, proxies={}, validate_authority=True, cache=None, api_version=None ) creds.cloud_environment = AZURE_CHINA_CLOUD assert creds._context is None creds.set_token() adal_context.assert_called_with( "https://login.chinacloudapi.cn/private", timeout=12, verify_ssl=True, proxies={}, validate_authority=True, cache=None, api_version=None ) @mock.patch("adal.AuthenticationContext") def test_service_principal(self, adal_context): adal_context.acquire_token_with_client_credentials = mock.Mock() # Basic with parameters mock_proxies = { 'http': 'http://myproxy:8080', 'https': 'https://myproxy:8080', } creds = ServicePrincipalCredentials( 123, 'secret', resource="resource", timeout=12, verify=True, proxies=mock_proxies ) adal_context.assert_called_with( "https://login.microsoftonline.com/common", timeout=12, verify_ssl=True, proxies=mock_proxies, validate_authority=True, cache=None, api_version=None ) creds.set_token() creds._context.acquire_token_with_client_credentials.assert_called_with( "resource", 123, "secret" ) # Using default creds = ServicePrincipalCredentials( 123, 'secret', tenant="private" ) adal_context.assert_called_with( "https://login.microsoftonline.com/private", timeout=None, verify_ssl=None, proxies=None, validate_authority=True, cache=None, api_version=None ) creds.set_token() creds._context.acquire_token_with_client_credentials.assert_called_with( "https://management.core.windows.net/", 123, "secret" ) # Testing cloud_environment creds = ServicePrincipalCredentials( 123, 'secret', cloud_environment=AZURE_CHINA_CLOUD ) adal_context.assert_called_with( "https://login.chinacloudapi.cn/common", timeout=None, verify_ssl=None, proxies=None, validate_authority=True, cache=None, api_version=None ) creds.set_token() creds._context.acquire_token_with_client_credentials.assert_called_with( "https://management.core.chinacloudapi.cn/", 123, "secret" ) # Testing china=True creds = ServicePrincipalCredentials( 123, 'secret', china=True ) adal_context.assert_called_with( "https://login.chinacloudapi.cn/common", timeout=None, verify_ssl=None, proxies=None, validate_authority=True, cache=None, api_version=None ) creds.set_token() creds._context.acquire_token_with_client_credentials.assert_called_with( "https://management.core.chinacloudapi.cn/", 123, "secret" ) # ADAL boom creds._context.acquire_token_with_client_credentials.side_effect = adal.AdalError("Boom") with self.assertRaises(AuthenticationError): creds.set_token() @mock.patch("adal.AuthenticationContext") def test_user_pass_credentials(self, adal_context): adal_context.acquire_token_with_username_password = mock.Mock() # Basic with parameters mock_proxies = { 'http': 'http://myproxy:8080', 'https': 'https://myproxy:8080', } creds = UserPassCredentials( 'user', 'pass', 'id', resource="resource", timeout=12, verify=True, validate_authority=True, cache=None, proxies=mock_proxies ) adal_context.assert_called_with( "https://login.microsoftonline.com/common", timeout=12, verify_ssl=True, proxies=mock_proxies, validate_authority=True, cache=None, api_version=None ) creds.set_token() creds._context.acquire_token_with_username_password.assert_called_with( "resource", "user", "pass", "id" ) # Using default creds = UserPassCredentials( 'user', 'pass', ) adal_context.assert_called_with( "https://login.microsoftonline.com/common", timeout=None, verify_ssl=None, proxies=None, validate_authority=True, cache=None, api_version=None ) creds.set_token() creds._context.acquire_token_with_username_password.assert_called_with( "https://management.core.windows.net/", "user", "pass", "04b07795-8ddb-461a-bbee-02f9e1bf7b46" ) # Testing cloud_environment creds = UserPassCredentials( 'user', 'pass', cloud_environment=AZURE_CHINA_CLOUD ) adal_context.assert_called_with( "https://login.chinacloudapi.cn/common", timeout=None, verify_ssl=None, proxies=None, validate_authority=True, cache=None, api_version=None ) creds.set_token() creds._context.acquire_token_with_username_password.assert_called_with( "https://management.core.chinacloudapi.cn/", "user", "pass", "04b07795-8ddb-461a-bbee-02f9e1bf7b46" ) # Testing china=True creds = UserPassCredentials( 'user', 'pass', china=True ) adal_context.assert_called_with( "https://login.chinacloudapi.cn/common", timeout=None, verify_ssl=None, proxies=None, validate_authority=True, cache=None, api_version=None ) creds.set_token() creds._context.acquire_token_with_username_password.assert_called_with( "https://management.core.chinacloudapi.cn/", "user", "pass", "04b07795-8ddb-461a-bbee-02f9e1bf7b46" ) # ADAL boom creds._context.acquire_token_with_username_password.side_effect = adal.AdalError("Boom") with self.assertRaises(AuthenticationError): creds.set_token() def test_adal_authentication(self): def success_auth(): return { 'tokenType': 'https', 'accessToken': 'cryptictoken' } credentials = AdalAuthentication(success_auth) session = credentials.signed_session() self.assertEqual(session.headers['Authorization'], 'https cryptictoken') def error(): raise adal.AdalError("You hacker", {}) credentials = AdalAuthentication(error) with self.assertRaises(AuthenticationError) as cm: session = credentials.signed_session() def expired(): raise adal.AdalError("Too late", {'error_description': "AADSTS70008: Expired"}) credentials = AdalAuthentication(expired) with self.assertRaises(TokenExpiredError) as cm: session = credentials.signed_session() def connection_error(): raise ConnectionError("Plug the network") credentials = AdalAuthentication(connection_error) with self.assertRaises(AuthenticationError) as cm: session = credentials.signed_session() @httpretty.activate def test_msi_vm(self): # Test legacy MSI, with no MSI_ENDPOINT json_payload = { 'token_type': "TokenType", "access_token": "AccessToken" } httpretty.register_uri(httpretty.POST, 'http://localhost:666/oauth2/token', body=json.dumps(json_payload), content_type="application/json") token_type, access_token, token_entry = get_msi_token("whatever", port=666) assert token_type == "TokenType" assert access_token == "AccessToken" assert token_entry == json_payload httpretty.register_uri(httpretty.POST, 'http://localhost:42/oauth2/token', status=503, content_type="application/json") with self.assertRaises(HTTPError): get_msi_token("whatever", port=42) # Test MSI_ENDPOINT json_payload = { 'token_type': "TokenType", "access_token": "AccessToken" } httpretty.register_uri(httpretty.POST, 'http://random.org/yadadada', body=json.dumps(json_payload), content_type="application/json") with mock.patch('os.environ', {'MSI_ENDPOINT': 'http://random.org/yadadada'}): token_type, access_token, token_entry = get_msi_token("whatever") assert token_type == "TokenType" assert access_token == "AccessToken" assert token_entry == json_payload # Test MSIAuthentication with no MSI_ENDPOINT and no APPSETTING_WEBSITE_SITE_NAME is IMDS json_payload = { 'token_type': "TokenTypeIMDS", "access_token": "AccessToken" } httpretty.register_uri(httpretty.GET, 'http://169.254.169.254/metadata/identity/oauth2/token', body=json.dumps(json_payload), content_type="application/json") credentials = MSIAuthentication() assert credentials.scheme == "TokenTypeIMDS" assert credentials.token == json_payload # Test MSIAuthentication with MSI_ENDPOINT and no APPSETTING_WEBSITE_SITE_NAME is MSI_ENDPOINT json_payload = { 'token_type': "TokenTypeMSI_ENDPOINT", "access_token": "AccessToken" } httpretty.register_uri(httpretty.POST, 'http://random.org/yadadada', body=json.dumps(json_payload), content_type="application/json") with mock.patch('os.environ', {'MSI_ENDPOINT': 'http://random.org/yadadada'}): credentials = MSIAuthentication() assert credentials.scheme == "TokenTypeMSI_ENDPOINT" assert credentials.token == json_payload # WebApp json_payload = { 'token_type': "TokenTypeWebApp", "access_token": "AccessToken" } httpretty.register_uri(httpretty.GET, 'http://127.0.0.1:41741/MSI/token/?resource=foo&api-version=2017-09-01', body=json.dumps(json_payload), content_type="application/json", match_querystring=True) app_service_env = { 'APPSETTING_WEBSITE_SITE_NAME': 'Website name', 'MSI_ENDPOINT': 'http://127.0.0.1:41741/MSI/token', 'MSI_SECRET': '69418689F1E342DD946CB82994CDA3CB' } with mock.patch.dict('os.environ', app_service_env): credentials = MSIAuthentication(resource="foo") assert credentials.scheme == "TokenTypeWebApp" assert credentials.token == json_payload # WebApp with User Assigned Identity json_payload = { 'token_type': "TokenTypeWebApp", "access_token": "AccessToken" } httpretty.register_uri(httpretty.GET, 'http://127.0.0.1:41741/MSI/token/?resource=foo&api-version=2017-09-01&clientid=bar', body=json.dumps(json_payload), content_type="application/json", match_querystring=True) app_service_env = { 'APPSETTING_WEBSITE_SITE_NAME': 'Website name', 'MSI_ENDPOINT': 'http://127.0.0.1:41741/MSI/token', 'MSI_SECRET': '69418689F1E342DD946CB82994CDA3CB' } with mock.patch.dict('os.environ', app_service_env): credentials = MSIAuthentication(resource="foo", client_id="bar") assert credentials.scheme == "TokenTypeWebApp" assert credentials.token == json_payload @httpretty.activate def test_msi_vm_imds_retry(self): json_payload = { 'token_type': "TokenTypeIMDS", "access_token": "AccessToken" } httpretty.register_uri(httpretty.GET, 'http://169.254.169.254/metadata/identity/oauth2/token', responses=[ httpretty.Response('', status=404), httpretty.Response('', status=429), httpretty.Response('', status=599), httpretty.Response(body=json.dumps(json_payload)), ], content_type="application/json") credentials = MSIAuthentication() assert credentials.scheme == "TokenTypeIMDS" assert credentials.token == json_payload # Assert four requests made only assert len(httpretty.httpretty.latest_requests) == 4 @httpretty.activate def test_msi_vm_imds_no_retry_on_bad_error(self): """Check that 499 throws immediatly.""" httpretty.register_uri(httpretty.GET, 'http://169.254.169.254/metadata/identity/oauth2/token', status=499) with self.assertRaises(HTTPError): MSIAuthentication() # Assert one request made only assert len(httpretty.httpretty.latest_requests) == 1 @httpretty.activate def test_msi_vm_imds_timeout_not_used(self): """Check that using timeout still allows a successfull scenario to pass.""" json_payload = { 'token_type': "TokenTypeIMDS", "access_token": "AccessToken" } httpretty.register_uri(httpretty.GET, 'http://169.254.169.254/metadata/identity/oauth2/token', body=json.dumps(json_payload), content_type="application/json") credentials = MSIAuthentication(timeout=15) assert credentials.scheme == "TokenTypeIMDS" assert credentials.token == json_payload @httpretty.activate def test_msi_vm_imds_timeout_used(self): """Will loop on 410 until timeout is reached.""" httpretty.register_uri(httpretty.GET, 'http://169.254.169.254/metadata/identity/oauth2/token', status=410) start_time = time.time() with self.assertRaises(MSIAuthenticationTimeoutError): MSIAuthentication(timeout=1) # Test should take 1 second, but testing against 2 in case machine busy assert time.time() - start_time < 2 # Assert at least two requests have been made assert len(httpretty.httpretty.latest_requests) >= 2 @httpretty.activate def test_msi_vm_imds_timeout_zero_used(self): """If zero timeout, should do a try and fail immediatly.""" httpretty.register_uri(httpretty.GET, 'http://169.254.169.254/metadata/identity/oauth2/token', status=410) with self.assertRaises(MSIAuthenticationTimeoutError): MSIAuthentication(timeout=0) # Assert one request made only assert len(httpretty.httpretty.latest_requests) == 1 @unittest.skipIf(sys.version_info != (2,7), "TimeoutError doesn't exist in Py 2.7") @httpretty.activate def test_msi_vm_imds_timeout_used_timeouterror(self): """Will loop on 410 until timeout is reached.""" httpretty.register_uri(httpretty.GET, 'http://169.254.169.254/metadata/identity/oauth2/token', status=410) # Verify that I can catch TimeoutError as well with self.assertRaises(TimeoutError): MSIAuthentication(timeout=1) # Assert at two requests made only assert len(httpretty.httpretty.latest_requests) >= 2 @pytest.mark.slow def test_refresh_userpassword_no_common_session(user_password): user, password = user_password creds = UserPassCredentials(user, password) # Basic scenarion, I recreate the session each time session = creds.signed_session() response = session.get("https://management.azure.com/subscriptions?api-version=2016-06-01") response.raise_for_status() # Should never raise try: session = creds.signed_session() # Hacking the token time session.auth._client.token['expires_in'] = session.auth._client.expires_in = -10 session.auth._client.token['expires_on'] = session.auth._client.expires_on = time.time() -10 session.auth._client.token['expires_at'] = session.auth._client.expires_at = session.auth._client._expires_at = session.auth._client.expires_on response = session.get("https://management.azure.com/subscriptions?api-version=2016-06-01") pytest.fail("Requests should have failed") except oauthlib.oauth2.rfc6749.errors.TokenExpiredError: session = creds.refresh_session() response = session.get("https://management.azure.com/subscriptions?api-version=2016-06-01") response.raise_for_status() # Should never raise @pytest.mark.slow def test_refresh_userpassword_common_session(user_password): user, password = user_password creds = UserPassCredentials(user, password) root_session = Session() # Basic scenarion, I recreate the session each time session = creds.signed_session(root_session) response = session.get("https://management.azure.com/subscriptions?api-version=2016-06-01") response.raise_for_status() # Should never raise try: session = creds.signed_session(root_session) # Hacking the token time session.auth._client.token['expires_in'] = session.auth._client.expires_in = -10 session.auth._client.token['expires_on'] = session.auth._client.expires_on = time.time() -10 session.auth._client.token['expires_at'] = session.auth._client.expires_at = session.auth._client._expires_at = session.auth._client.expires_on response = session.get("https://management.azure.com/subscriptions?api-version=2016-06-01") pytest.fail("Requests should have failed") except oauthlib.oauth2.rfc6749.errors.TokenExpiredError: session = creds.refresh_session(root_session) response = session.get("https://management.azure.com/subscriptions?api-version=2016-06-01") response.raise_for_status() # Should never raise @pytest.mark.slow def test_refresh_aadtokencredentials_no_common_session(user_password): user, password = user_password context = adal.AuthenticationContext('https://login.microsoftonline.com/common') token = context.acquire_token_with_username_password( 'https://management.core.windows.net/', user, password, '04b07795-8ddb-461a-bbee-02f9e1bf7b46' ) creds = AADTokenCredentials(token) # Basic scenarion, I recreate the session each time session = creds.signed_session() response = session.get("https://management.azure.com/subscriptions?api-version=2016-06-01") response.raise_for_status() # Should never raise # Hacking the token time creds.token['expires_on'] = time.time() - 10 creds.token['expires_at'] = creds.token['expires_on'] try: session = creds.signed_session() response = session.get("https://management.azure.com/subscriptions?api-version=2016-06-01") pytest.fail("Requests should have failed") except oauthlib.oauth2.rfc6749.errors.TokenExpiredError: session = creds.refresh_session() response = session.get("https://management.azure.com/subscriptions?api-version=2016-06-01") response.raise_for_status() # Should never raise @pytest.mark.slow def test_refresh_aadtokencredentials_common_session(user_password): user, password = user_password context = adal.AuthenticationContext('https://login.microsoftonline.com/common') token = context.acquire_token_with_username_password( 'https://management.core.windows.net/', user, password, '04b07795-8ddb-461a-bbee-02f9e1bf7b46' ) creds = AADTokenCredentials(token) root_session = Session() # Basic scenarion, I recreate the session each time session = creds.signed_session(root_session) response = session.get("https://management.azure.com/subscriptions?api-version=2016-06-01") response.raise_for_status() # Should never raise # Hacking the token time creds.token['expires_on'] = time.time() - 10 creds.token['expires_at'] = creds.token['expires_on'] try: session = creds.signed_session(root_session) response = session.get("https://management.azure.com/subscriptions?api-version=2016-06-01") pytest.fail("Requests should have failed") except oauthlib.oauth2.rfc6749.errors.TokenExpiredError: session = creds.refresh_session(root_session) response = session.get("https://management.azure.com/subscriptions?api-version=2016-06-01") response.raise_for_status() # Should never raise @pytest.mark.slow def test_refresh_aadtokencredentials_existing_session(user_password): user, password = user_password context = adal.AuthenticationContext('https://login.microsoftonline.com/common') token = context.acquire_token_with_username_password( 'https://management.core.windows.net/', user, password, '04b07795-8ddb-461a-bbee-02f9e1bf7b46' ) creds = AADTokenCredentials(token) root_session = Session() creds.signed_session(root_session) response = root_session.get("https://management.azure.com/subscriptions?api-version=2016-06-01") response.raise_for_status() # Should never raise # Hacking the token time creds.token['expires_on'] = time.time() - 10 creds.token['expires_at'] = creds.token['expires_on'] try: creds.signed_session(root_session) response = root_session.get("https://management.azure.com/subscriptions?api-version=2016-06-01") pytest.fail("Requests should have failed") except oauthlib.oauth2.rfc6749.errors.TokenExpiredError: creds.refresh_session(root_session) response = root_session.get("https://management.azure.com/subscriptions?api-version=2016-06-01") response.raise_for_status() # Should never raise if __name__ == '__main__': unittest.main() msrestazure-for-python-0.6.4/tests/test_cloud.py000066400000000000000000000074701367642573600221240ustar00rootroot00000000000000#-------------------------------------------------------------------------- # # Copyright (c) Microsoft Corporation. All rights reserved. # # The MIT License (MIT) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the ""Software""), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # #-------------------------------------------------------------------------- import unittest import json import httpretty import requests from msrestazure import azure_cloud class TestCloud(unittest.TestCase): @httpretty.activate def test_get_cloud_from_endpoint(self): public_azure_dict = { "galleryEndpoint": "https://gallery.azure.com", "graphEndpoint": "https://graph.windows.net/", "portalEndpoint": "https://portal.azure.com", "authentication": { "loginEndpoint": "https://login.windows.net", "audiences": ["https://management.core.windows.net/", "https://management.azure.com/"] } } httpretty.register_uri(httpretty.GET, "https://management.azure.com/metadata/endpoints?api-version=1.0", body=json.dumps(public_azure_dict), content_type="application/json") cloud = azure_cloud.get_cloud_from_metadata_endpoint("https://management.azure.com") self.assertEqual("https://management.azure.com", cloud.name) self.assertEqual("https://management.azure.com", cloud.endpoints.management) self.assertEqual("https://management.azure.com", cloud.endpoints.resource_manager) self.assertEqual("https://gallery.azure.com", cloud.endpoints.gallery) self.assertEqual("https://graph.windows.net/", cloud.endpoints.active_directory_graph_resource_id) self.assertEqual("https://login.windows.net", cloud.endpoints.active_directory) session = requests.Session() cloud = azure_cloud.get_cloud_from_metadata_endpoint("https://management.azure.com", "Public Azure", session) self.assertEqual("Public Azure", cloud.name) self.assertEqual("https://management.azure.com", cloud.endpoints.management) self.assertEqual("https://management.azure.com", cloud.endpoints.resource_manager) self.assertEqual("https://gallery.azure.com", cloud.endpoints.gallery) self.assertEqual("https://graph.windows.net/", cloud.endpoints.active_directory_graph_resource_id) self.assertEqual("https://login.windows.net", cloud.endpoints.active_directory) with self.assertRaises(azure_cloud.MetadataEndpointError): azure_cloud.get_cloud_from_metadata_endpoint("https://something.azure.com") with self.assertRaises(azure_cloud.CloudEndpointNotSetException): cloud.endpoints.batch_resource_id with self.assertRaises(azure_cloud.CloudSuffixNotSetException): cloud.suffixes.sql_server_hostname self.assertIsNotNone(str(cloud)) msrestazure-for-python-0.6.4/tests/test_configuration.py000066400000000000000000000031721367642573600236600ustar00rootroot00000000000000#-------------------------------------------------------------------------- # # Copyright (c) Microsoft Corporation. All rights reserved. # # The MIT License (MIT) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the ""Software""), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # #-------------------------------------------------------------------------- import unittest from msrestazure.azure_configuration import AzureConfiguration class TestAzConfiguration(unittest.TestCase): def test_config_basic(self): # Do not raise is already something, we don't really need strong tests here. AzureConfiguration('http://management.something.com') if __name__ == '__main__': unittest.main() msrestazure-for-python-0.6.4/tests/test_exceptions.py000066400000000000000000000345431367642573600232000ustar00rootroot00000000000000# -*- coding: utf-8 -*- #-------------------------------------------------------------------------- # # Copyright (c) Microsoft Corporation. All rights reserved. # # The MIT License (MIT) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the ""Software""), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # #-------------------------------------------------------------------------- import json import unittest from requests import Response, RequestException from msrest import Deserializer, Configuration from msrestazure.azure_exceptions import CloudErrorData, CloudError, TypedErrorInfo class TestCloudException(unittest.TestCase): def setUp(self): self.cfg = Configuration("https://my_endpoint.com") self._d = Deserializer() self._d.dependencies = { 'CloudErrorData': CloudErrorData, 'TypedErrorInfo': TypedErrorInfo } return super(TestCloudException, self).setUp() def test_cloud_exception(self): message = { 'code': '500', 'message': 'Bad Request', 'values': {'invalid_attribute':'data'} } cloud_exp = self._d(CloudErrorData(), message) self.assertEqual(cloud_exp.message, 'Bad Request') self.assertEqual(cloud_exp.error, '500') self.assertEqual(cloud_exp.data['invalid_attribute'], 'data') message = { 'code': '500', 'message': {'value': 'Bad Request\nRequest:34875\nTime:1999-12-31T23:59:59-23:59'}, 'values': {'invalid_attribute':'data'} } cloud_exp = self._d(CloudErrorData(), message) self.assertEqual(cloud_exp.message, 'Bad Request') self.assertEqual(cloud_exp.error, '500') self.assertEqual(cloud_exp.data['invalid_attribute'], 'data') message = { 'code': '500', 'message': {'value': 'Bad Request\nRequest:34875'}, 'values': {'invalid_attribute':'data'} } cloud_exp = self._d(CloudErrorData(), message) self.assertEqual(cloud_exp.message, 'Bad Request') self.assertEqual(cloud_exp.request_id, '34875') self.assertEqual(cloud_exp.error, '500') self.assertEqual(cloud_exp.data['invalid_attribute'], 'data') message = {} cloud_exp = self._d(CloudErrorData(), message) self.assertEqual(cloud_exp.message, None) self.assertEqual(cloud_exp.error, None) message = ('{\r\n "odata.metadata":"https://account.region.batch.azure.com/$metadata#' 'Microsoft.Azure.Batch.Protocol.Entities.Container.errors/@Element","code":' '"InvalidHeaderValue","message":{\r\n "lang":"en-US","value":"The value ' 'for one of the HTTP headers is not in the correct format.\\nRequestId:5f4c1f05-' '603a-4495-8e80-01f776310bbd\\nTime:2016-01-04T22:12:33.9245931Z"\r\n },' '"values":[\r\n {\r\n "key":"HeaderName","value":"Content-Type"\r\n }' ',{\r\n "key":"HeaderValue","value":"application/json; odata=minimalmetadata;' ' charset=utf-8"\r\n }\r\n ]\r\n}') message = json.loads(message) cloud_exp = self._d(CloudErrorData(), message) self.assertEqual( cloud_exp.message, "The value for one of the HTTP headers is not in the correct format.") message = { "code": "BadArgument", "message": "The provided database 'foo' has an invalid username.", "target": "query", "details": [ { "code": "301", "target": "$search", "message": "$search query option not supported", } ], "innererror": { "customKey": "customValue" }, "additionalInfo": [ { "type": "SomeErrorType", "info": { "customKey": "customValue" } } ] } cloud_exp = self._d(CloudErrorData(), message) self.assertEqual(cloud_exp.target, 'query') self.assertEqual(cloud_exp.details[0].target, '$search') self.assertEqual(cloud_exp.innererror['customKey'], 'customValue') self.assertEqual(cloud_exp.additionalInfo[0].type, 'SomeErrorType') self.assertEqual(cloud_exp.additionalInfo[0].info['customKey'], 'customValue') self.assertIn('customValue', str(cloud_exp)) message = { "code": "BadArgument", "message": "The provided database 'foo' has an invalid username.", "target": "query", "details": [ { "code": "301", "target": "$search", "message": "$search query option not supported", "additionalInfo": [ { "type": "PolicyViolation", "info": { "policyDefinitionDisplayName": "Allowed locations", "policyAssignmentParameters": { "listOfAllowedLocations": { "value": [ "westus" ] } } } } ] } ], "additionalInfo": [ { "type": "SomeErrorType", "info": { "customKey": "customValue" } } ] } cloud_exp = self._d(CloudErrorData(), message) self.assertEqual(cloud_exp.target, 'query') self.assertEqual(cloud_exp.details[0].target, '$search') self.assertEqual(cloud_exp.additionalInfo[0].type, 'SomeErrorType') self.assertEqual(cloud_exp.additionalInfo[0].info['customKey'], 'customValue') self.assertEqual(cloud_exp.details[0].additionalInfo[0].type, 'PolicyViolation') self.assertEqual(cloud_exp.details[0].additionalInfo[0].info['policyDefinitionDisplayName'], 'Allowed locations') self.assertEqual(cloud_exp.details[0].additionalInfo[0].info['policyAssignmentParameters']['listOfAllowedLocations']['value'][0], 'westus') self.assertIn('customValue', str(cloud_exp)) def test_cloud_error(self): response = Response() response._content = br'{"real": true}' # Has to be valid bytes JSON response._content_consumed = True response.status_code = 400 response.headers = {"content-type": "application/json; charset=utf8"} response.reason = 'BadRequest' message = { 'error': { 'code': '500', 'message': {'value': 'Bad Request\nRequest:34875\nTime:1999-12-31T23:59:59-23:59'}, 'values': {'invalid_attribute':'data'} }} response._content = json.dumps(message).encode("utf-8") error = CloudError(response) self.assertEqual(error.message, 'Bad Request') self.assertEqual(error.status_code, 400) self.assertIsInstance(error.response, Response) self.assertIsInstance(error.error, CloudErrorData) error = CloudError(response, "Request failed with bad status") self.assertEqual(error.message, "Request failed with bad status") self.assertEqual(error.status_code, 400) self.assertIsInstance(error.error, Response) message = { 'error': { 'code': '500', 'message': u"ééééé", }} response._content = json.dumps(message).encode("utf-8") error = CloudError(response) try: # Python 2 assert u"ééééé" in unicode(error) assert u"ééééé".encode("utf-8") in str(error) except NameError: # Python 3 assert "ééééé" in str(error) response._content = b"{{" error = CloudError(response) self.assertIn("None", error.message) response._content = json.dumps({'message':'server error'}).encode("utf-8") error = CloudError(response) self.assertTrue("server error" in error.message) self.assertEqual(error.status_code, 400) response._content = b"{{" response.reason = "FAILED!" error = CloudError(response) self.assertTrue("FAILED!" in error.message) self.assertIsInstance(error.error, RequestException) response.reason = 'BadRequest' response._content = b'{\r\n "odata.metadata":"https://account.region.batch.azure.com/$metadata#Microsoft.Azure.Batch.Protocol.Entities.Container.errors/@Element","code":"InvalidHeaderValue","message":{\r\n "lang":"en-US","value":"The value for one of the HTTP headers is not in the correct format.\\nRequestId:5f4c1f05-603a-4495-8e80-01f776310bbd\\nTime:2016-01-04T22:12:33.9245931Z"\r\n },"values":[\r\n {\r\n "key":"HeaderName","value":"Content-Type"\r\n },{\r\n "key":"HeaderValue","value":"application/json; odata=minimalmetadata; charset=utf-8"\r\n }\r\n ]\r\n}' error = CloudError(response) self.assertIn("The value for one of the HTTP headers is not in the correct format", error.message) response._content = b'{"error":{"code":"Conflict","message":"The maximum number of Free ServerFarms allowed in a Subscription is 10.","target":null,"details":[{"message":"The maximum number of Free ServerFarms allowed in a Subscription is 10."},{"code":"Conflict"},{"errorentity":{"code":"Conflict","message":"The maximum number of Free ServerFarms allowed in a Subscription is 10.","extendedCode":"59301","messageTemplate":"The maximum number of {0} ServerFarms allowed in a Subscription is {1}.","parameters":["Free","10"],"innerErrors":null}}],"innererror":null}}' error = CloudError(response) self.assertIsInstance(error.error, CloudErrorData) self.assertEqual(error.error.error, "Conflict") response._content = json.dumps({ "error": { "code": "BadArgument", "message": "The provided database 'foo' has an invalid username.", "target": "query", "details": [ { "code": "301", "target": "$search", "message": "$search query option not supported", } ] }}).encode('utf-8') error = CloudError(response) self.assertIsInstance(error.error, CloudErrorData) self.assertEqual(error.error.error, "BadArgument") # See https://github.com/Azure/msrestazure-for-python/issues/54 response._content = b'"{\\"error\\": {\\"code\\": \\"ResourceGroupNotFound\\", \\"message\\": \\"Resource group \'res_grp\' could not be found.\\"}}"' error = CloudError(response) self.assertIn(response.text, error.message) response._content = json.dumps({ "error": { "code": "InvalidTemplateDeployment", "message": "The template deployment failed because of policy violation. Please see details for more information.", "details": [{ "code": "RequestDisallowedByPolicy", "target": "vm1", "message": "Resource 'vm1' was disallowed by policy. Policy identifiers: '[{\"policyAssignment\":{\"name\":\"Allowed virtual machine SKUs\",\"id\":\"/subscriptions/0b1f6471-1bf0-4dda-aec3-cb9272f09590/resourceGroups/fytest/providers/Microsoft.Authorization/policyAssignments/9c95e7fe8227466b82f48228\"},\"policyDefinition\":{\"name\":\"Allowed virtual machine SKUs\",\"id\":\"/providers/Microsoft.Authorization/policyDefinitions/cccc23c7-8427-4f53-ad12-b6a63eb452b3\"}}]'.", "additionalInfo": [{ "type": "PolicyViolation", "info": { "policyDefinitionDisplayName": "Allowed virtual machine SKUs", "evaluationDetails": { "evaluatedExpressions": [{ "result": "True", "expression": "type", "path": "type", "expressionValue": "Microsoft.Compute/virtualMachines", "targetValue": "Microsoft.Compute/virtualMachines", "operator": "Equals" }, { "result": "False", "expression": "Microsoft.Compute/virtualMachines/sku.name", "path": "properties.hardwareProfile.vmSize", "expressionValue": "Standard_DS1_v2", "targetValue": ["Basic_A0"], "operator": "In" }] }, "policyDefinitionId": "/providers/Microsoft.Authorization/policyDefinitions/cccc23c7-8427-4f53-ad12-b6a63eb452b3", "policyDefinitionName": "cccc23c7-8427-4f53-ad12-b6a63eb452b3", "policyDefinitionEffect": "Deny", "policyAssignmentId": "/subscriptions/0b1f6471-1bf0-4dda-aec3-cb9272f09590/resourceGroups/fytest/providers/Microsoft.Authorization/policyAssignments/9c95e7fe8227466b82f48228", "policyAssignmentName": "9c95e7fe8227466b82f48228", "policyAssignmentDisplayName": "Allowed virtual machine SKUs", "policyAssignmentScope": "/subscriptions/0b1f6471-1bf0-4dda-aec3-cb9272f09590/resourceGroups/fytest", "policyAssignmentParameters": { "listOfAllowedSKUs": { "value": ["Basic_A0"] } } } }] }] } }).encode('utf-8') error = CloudError(response) assert error.message == "The template deployment failed because of policy violation. Please see details for more information." if __name__ == '__main__': unittest.main()msrestazure-for-python-0.6.4/tests/test_operation.py000066400000000000000000000365561367642573600230250ustar00rootroot00000000000000#-------------------------------------------------------------------------- # # Copyright (c) Microsoft Corporation. All rights reserved. # # The MIT License (MIT) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the ""Software""), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # #-------------------------------------------------------------------------- import json import re import unittest try: from unittest import mock except ImportError: import mock from requests import Request, Response from msrest import Deserializer from msrest.exceptions import DeserializationError from msrestazure.azure_exceptions import CloudError from msrestazure.azure_operation import ( LongRunningOperation, AzureOperationPoller, BadStatus, SimpleResource) class BadEndpointError(Exception): pass TEST_NAME = 'foo' RESPONSE_BODY = {'properties':{'provisioningState': 'InProgress'}} ASYNC_BODY = json.dumps({ 'status': 'Succeeded' }) ASYNC_URL = 'http://dummyurlFromAzureAsyncOPHeader_Return200' LOCATION_BODY = json.dumps({ 'name': TEST_NAME }) LOCATION_URL = 'http://dummyurlurlFromLocationHeader_Return200' RESOURCE_BODY = json.dumps({ 'name': TEST_NAME }) RESOURCE_URL = 'http://subscriptions/sub1/resourcegroups/g1/resourcetype1/resource1' ERROR = 'http://dummyurl_ReturnError' POLLING_STATUS = 200 class TestLongRunningOperation(unittest.TestCase): convert = re.compile('([a-z0-9])([A-Z])') @staticmethod def mock_send(method, status, headers, body=None): response = mock.create_autospec(Response) response.request = mock.create_autospec(Request) response.request.method = method response.request.url = RESOURCE_URL response.status_code = status response.headers = headers response.headers.update({"content-type": "application/json; charset=utf8"}) content = body if body else RESPONSE_BODY response.text = json.dumps(content) response.json = lambda: json.loads(response.text) return lambda: response @staticmethod def mock_update(url, headers=None): response = mock.create_autospec(Response) response.request = mock.create_autospec(Request) response.request.method = 'GET' response.headers = headers or {} response.headers.update({"content-type": "application/json; charset=utf8"}) if url == ASYNC_URL: response.request.url = url response.status_code = POLLING_STATUS response.text = ASYNC_BODY response.randomFieldFromPollAsyncOpHeader = None elif url == LOCATION_URL: response.request.url = url response.status_code = POLLING_STATUS response.text = LOCATION_BODY response.randomFieldFromPollLocationHeader = None elif url == ERROR: raise BadEndpointError("boom") elif url == RESOURCE_URL: response.request.url = url response.status_code = POLLING_STATUS response.text = RESOURCE_BODY else: raise Exception('URL does not match') response.json = lambda: json.loads(response.text) return response @staticmethod def mock_outputs(response): body = response.json() body = {TestLongRunningOperation.convert.sub(r'\1_\2', k).lower(): v for k, v in body.items()} properties = body.setdefault('properties', {}) if 'name' in body: properties['name'] = body['name'] if properties: properties = {TestLongRunningOperation.convert.sub(r'\1_\2', k).lower(): v for k, v in properties.items()} del body['properties'] body.update(properties) resource = SimpleResource(**body) else: raise DeserializationError("Impossible to deserialize") resource = SimpleResource(**body) return resource def test_long_running_put(self): #TODO: Test custom header field # Test throw on non LRO related status code response = TestLongRunningOperation.mock_send('PUT', 1000, {}) op = LongRunningOperation(response(), lambda x:None) with self.assertRaises(BadStatus): op.set_initial_status(response()) with self.assertRaises(CloudError): AzureOperationPoller(response, TestLongRunningOperation.mock_outputs, TestLongRunningOperation.mock_update, 0).result() # Test with no polling necessary response_body = { 'properties':{'provisioningState': 'Succeeded'}, 'name': TEST_NAME } response = TestLongRunningOperation.mock_send( 'PUT', 201, {}, response_body ) def no_update_allowed(url, headers=None): raise ValueError("Should not try to update") poll = AzureOperationPoller(response, TestLongRunningOperation.mock_outputs, no_update_allowed, 0 ) self.assertEqual(poll.result().name, TEST_NAME) self.assertFalse(hasattr(poll._response, 'randomFieldFromPollAsyncOpHeader')) # Test polling from azure-asyncoperation header response = TestLongRunningOperation.mock_send( 'PUT', 201, {'azure-asyncoperation': ASYNC_URL}) poll = AzureOperationPoller(response, TestLongRunningOperation.mock_outputs, TestLongRunningOperation.mock_update, 0) self.assertEqual(poll.result().name, TEST_NAME) self.assertFalse(hasattr(poll._response, 'randomFieldFromPollAsyncOpHeader')) # Test polling location header response = TestLongRunningOperation.mock_send( 'PUT', 201, {'location': LOCATION_URL}) poll = AzureOperationPoller(response, TestLongRunningOperation.mock_outputs, TestLongRunningOperation.mock_update, 0) self.assertEqual(poll.result().name, TEST_NAME) self.assertIsNone(poll._response.randomFieldFromPollLocationHeader) # Test polling initial payload invalid (SQLDb) response_body = {} # Empty will raise response = TestLongRunningOperation.mock_send( 'PUT', 201, {'location': LOCATION_URL}, response_body) poll = AzureOperationPoller(response, TestLongRunningOperation.mock_outputs, TestLongRunningOperation.mock_update, 0) self.assertEqual(poll.result().name, TEST_NAME) self.assertIsNone(poll._response.randomFieldFromPollLocationHeader) # Test fail to poll from azure-asyncoperation header response = TestLongRunningOperation.mock_send( 'PUT', 201, {'azure-asyncoperation': ERROR}) with self.assertRaises(BadEndpointError): poll = AzureOperationPoller(response, TestLongRunningOperation.mock_outputs, TestLongRunningOperation.mock_update, 0).result() # Test fail to poll from location header response = TestLongRunningOperation.mock_send( 'PUT', 201, {'location': ERROR}) with self.assertRaises(BadEndpointError): poll = AzureOperationPoller(response, TestLongRunningOperation.mock_outputs, TestLongRunningOperation.mock_update, 0).result() def test_long_running_patch(self): # Test polling from location header response = TestLongRunningOperation.mock_send( 'PATCH', 202, {'location': LOCATION_URL}, body={'properties':{'provisioningState': 'Succeeded'}}) poll = AzureOperationPoller(response, TestLongRunningOperation.mock_outputs, TestLongRunningOperation.mock_update, 0) self.assertEqual(poll.result().name, TEST_NAME) self.assertIsNone(poll._response.randomFieldFromPollLocationHeader) # Test polling from azure-asyncoperation header response = TestLongRunningOperation.mock_send( 'PATCH', 202, {'azure-asyncoperation': ASYNC_URL}, body={'properties':{'provisioningState': 'Succeeded'}}) poll = AzureOperationPoller(response, TestLongRunningOperation.mock_outputs, TestLongRunningOperation.mock_update, 0) self.assertEqual(poll.result().name, TEST_NAME) self.assertFalse(hasattr(poll._response, 'randomFieldFromPollAsyncOpHeader')) # Test polling from location header response = TestLongRunningOperation.mock_send( 'PATCH', 200, {'location': LOCATION_URL}, body={'properties':{'provisioningState': 'Succeeded'}}) poll = AzureOperationPoller(response, TestLongRunningOperation.mock_outputs, TestLongRunningOperation.mock_update, 0) self.assertEqual(poll.result().name, TEST_NAME) self.assertIsNone(poll._response.randomFieldFromPollLocationHeader) # Test polling from azure-asyncoperation header response = TestLongRunningOperation.mock_send( 'PATCH', 200, {'azure-asyncoperation': ASYNC_URL}, body={'properties':{'provisioningState': 'Succeeded'}}) poll = AzureOperationPoller(response, TestLongRunningOperation.mock_outputs, TestLongRunningOperation.mock_update, 0) self.assertEqual(poll.result().name, TEST_NAME) self.assertFalse(hasattr(poll._response, 'randomFieldFromPollAsyncOpHeader')) # Test fail to poll from azure-asyncoperation header response = TestLongRunningOperation.mock_send( 'PATCH', 202, {'azure-asyncoperation': ERROR}) with self.assertRaises(BadEndpointError): poll = AzureOperationPoller(response, TestLongRunningOperation.mock_outputs, TestLongRunningOperation.mock_update, 0).result() # Test fail to poll from location header response = TestLongRunningOperation.mock_send( 'PATCH', 202, {'location': ERROR}) with self.assertRaises(BadEndpointError): poll = AzureOperationPoller(response, TestLongRunningOperation.mock_outputs, TestLongRunningOperation.mock_update, 0).result() def test_long_running_delete(self): # Test polling from azure-asyncoperation header response = TestLongRunningOperation.mock_send( 'DELETE', 202, {'azure-asyncoperation': ASYNC_URL}) poll = AzureOperationPoller(response, TestLongRunningOperation.mock_outputs, TestLongRunningOperation.mock_update, 0) poll.wait() self.assertIsNone(poll.result()) self.assertIsNone(poll._response.randomFieldFromPollAsyncOpHeader) def test_long_running_post(self): # Test throw on non LRO related status code response = TestLongRunningOperation.mock_send('POST', 201, {}) op = LongRunningOperation(response(), lambda x:None) with self.assertRaises(BadStatus): op.set_initial_status(response()) with self.assertRaises(CloudError): AzureOperationPoller(response, TestLongRunningOperation.mock_outputs, TestLongRunningOperation.mock_update, 0).result() # Test polling from azure-asyncoperation header response = TestLongRunningOperation.mock_send( 'POST', 202, {'azure-asyncoperation': ASYNC_URL}, body={'properties':{'provisioningState': 'Succeeded'}}) poll = AzureOperationPoller(response, TestLongRunningOperation.mock_outputs, TestLongRunningOperation.mock_update, 0) poll.wait() #self.assertIsNone(poll.result()) self.assertIsNone(poll._response.randomFieldFromPollAsyncOpHeader) # Test polling from location header response = TestLongRunningOperation.mock_send( 'POST', 202, {'location': LOCATION_URL}, body={'properties':{'provisioningState': 'Succeeded'}}) poll = AzureOperationPoller(response, TestLongRunningOperation.mock_outputs, TestLongRunningOperation.mock_update, 0) self.assertEqual(poll.result().name, TEST_NAME) self.assertIsNone(poll._response.randomFieldFromPollLocationHeader) # Test fail to poll from azure-asyncoperation header response = TestLongRunningOperation.mock_send( 'POST', 202, {'azure-asyncoperation': ERROR}) with self.assertRaises(BadEndpointError): poll = AzureOperationPoller(response, TestLongRunningOperation.mock_outputs, TestLongRunningOperation.mock_update, 0).result() # Test fail to poll from location header response = TestLongRunningOperation.mock_send( 'POST', 202, {'location': ERROR}) with self.assertRaises(BadEndpointError): poll = AzureOperationPoller(response, TestLongRunningOperation.mock_outputs, TestLongRunningOperation.mock_update, 0).result() def test_long_running_negative(self): global LOCATION_BODY global POLLING_STATUS # Test LRO PUT throws for invalid json LOCATION_BODY = '{' response = TestLongRunningOperation.mock_send( 'POST', 202, {'location': LOCATION_URL}) poll = AzureOperationPoller(response, TestLongRunningOperation.mock_outputs, TestLongRunningOperation.mock_update, 0) with self.assertRaises(DeserializationError): poll.wait() LOCATION_BODY = '{\'"}' response = TestLongRunningOperation.mock_send( 'POST', 202, {'location': LOCATION_URL}) poll = AzureOperationPoller(response, TestLongRunningOperation.mock_outputs, TestLongRunningOperation.mock_update, 0) with self.assertRaises(DeserializationError): poll.wait() LOCATION_BODY = '{' POLLING_STATUS = 203 response = TestLongRunningOperation.mock_send( 'POST', 202, {'location': LOCATION_URL}) poll = AzureOperationPoller(response, TestLongRunningOperation.mock_outputs, TestLongRunningOperation.mock_update, 0) with self.assertRaises(CloudError): # TODO: Node.js raises on deserialization poll.wait() LOCATION_BODY = json.dumps({ 'name': TEST_NAME }) POLLING_STATUS = 200 if __name__ == '__main__': unittest.main() msrestazure-for-python-0.6.4/tests/test_tools.py000066400000000000000000000541001367642573600221460ustar00rootroot00000000000000#-------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # # The MIT License (MIT) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the ""Software""), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # #-------------------------------------------------------------------------- import unittest import json try: from unittest import mock except ImportError: import mock import requests import httpretty from msrestazure.azure_configuration import AzureConfiguration from msrestazure.tools import parse_resource_id, is_valid_resource_id, resource_id, is_valid_resource_name class TestTools(unittest.TestCase): @httpretty.activate @mock.patch('time.sleep', return_value=None) def test_register_rp_hook(self, time_sleep): """Protocol: - We call the provider and get a 409 provider error - Now we POST register provider and get "Registering" - Now we GET register provider and get "Registered" - We call again the first endpoint and this time this succeed """ provider_url = ("https://management.azure.com/" "subscriptions/12345678-9abc-def0-0000-000000000000/" "resourceGroups/clitest.rg000001/" "providers/Microsoft.Sql/servers/ygserver123?api-version=2014-04-01") provider_error = ('{"error":{"code":"MissingSubscriptionRegistration", ' '"message":"The subscription registration is in \'Unregistered\' state. ' 'The subscription must be registered to use namespace \'Microsoft.Sql\'. ' 'See https://aka.ms/rps-not-found for how to register subscriptions."}}') provider_success = '{"success": true}' httpretty.register_uri(httpretty.PUT, provider_url, responses=[ httpretty.Response(body=provider_error, status=409), httpretty.Response(body=provider_success), ], content_type="application/json") register_post_url = ("https://management.azure.com/" "subscriptions/12345678-9abc-def0-0000-000000000000/" "providers/Microsoft.Sql/register?api-version=2016-02-01") register_post_result = { "registrationState":"Registering" } register_get_url = ("https://management.azure.com/" "subscriptions/12345678-9abc-def0-0000-000000000000/" "providers/Microsoft.Sql?api-version=2016-02-01") register_get_result = { "registrationState":"Registered" } httpretty.register_uri(httpretty.POST, register_post_url, body=json.dumps(register_post_result), content_type="application/json") httpretty.register_uri(httpretty.GET, register_get_url, body=json.dumps(register_get_result), content_type="application/json") configuration = AzureConfiguration(None) register_rp_hook = configuration.hooks[0] session = requests.Session() def rp_cb(r, *args, **kwargs): kwargs.setdefault("msrest", {})["session"] = session return register_rp_hook(r, *args, **kwargs) session.hooks['response'].append(rp_cb) response = session.put(provider_url) self.assertTrue(response.json()['success']) @httpretty.activate @mock.patch('time.sleep', return_value=None) def test_register_failed(self, time_sleep): """Protocol: - We call the provider and get a 409 provider error - Now we POST register provider and get "Registering" - This POST failed """ provider_url = ("https://management.azure.com/" "subscriptions/12345678-9abc-def0-0000-000000000000/" "resourceGroups/clitest.rg000001/" "providers/Microsoft.Sql/servers/ygserver123?api-version=2014-04-01") provider_error = ('{"error":{"code":"MissingSubscriptionRegistration", ' '"message":"The subscription registration is in \'Unregistered\' state. ' 'The subscription must be registered to use namespace \'Microsoft.Sql\'. ' 'See https://aka.ms/rps-not-found for how to register subscriptions."}}') provider_success = '{"success": true}' httpretty.register_uri(httpretty.PUT, provider_url, responses=[ httpretty.Response(body=provider_error, status=409), httpretty.Response(body=provider_success), ], content_type="application/json") register_post_url = ("https://management.azure.com/" "subscriptions/12345678-9abc-def0-0000-000000000000/" "providers/Microsoft.Sql/register?api-version=2016-02-01") httpretty.register_uri(httpretty.POST, register_post_url, status=409, content_type="application/json") configuration = AzureConfiguration(None) register_rp_hook = configuration.hooks[0] session = requests.Session() def rp_cb(r, *args, **kwargs): kwargs.setdefault("msrest", {})["session"] = session return register_rp_hook(r, *args, **kwargs) session.hooks['response'].append(rp_cb) response = session.put(provider_url) self.assertEqual(409, response.status_code) def test_resource_parse(self): """ Tests resource id parsing, reforming, and validation. """ tests = [ { 'resource_id': '/subscriptions/fakesub/resourcegroups/testgroup/providers' '/Microsoft.Storage/storageAccounts/foo/providers' '/Microsoft.Authorization/locks/bar', 'expected': { 'name': 'foo', 'type': 'storageAccounts', 'namespace': 'Microsoft.Storage', 'child_name_1': 'bar', 'child_namespace_1': 'Microsoft.Authorization', 'child_type_1': 'locks', 'child_parent_1': 'storageAccounts/foo/providers/Microsoft.Authorization/', 'resource_group': 'testgroup', 'subscription': 'fakesub', } }, { 'resource_id': '/subscriptions/fakesub/resourcegroups/testgroup/providers' '/Microsoft.Storage/storageAccounts/foo' '/locks/bar', 'expected': { 'name': 'foo', 'type': 'storageAccounts', 'namespace': 'Microsoft.Storage', 'child_name_1': 'bar', 'child_type_1': 'locks', 'child_parent_1': 'storageAccounts/foo/', 'resource_group': 'testgroup', 'subscription': 'fakesub', } }, { 'resource_id': '/subscriptions/fakesub/resourcegroups/testgroup/providers' '/Microsoft.Storage/storageAccounts/foo/providers' '/Microsoft.Authorization/locks/bar/providers/Microsoft.Network/' 'nets/gc', 'expected': { 'name': 'foo', 'type': 'storageAccounts', 'namespace': 'Microsoft.Storage', 'child_name_1': 'bar', 'child_namespace_1': 'Microsoft.Authorization', 'child_type_1': 'locks', 'child_parent_1': 'storageAccounts/foo/providers/Microsoft.Authorization/', 'child_name_2': 'gc', 'child_namespace_2': 'Microsoft.Network', 'child_type_2': 'nets', 'child_parent_2': 'storageAccounts/foo/providers/Microsoft.Authorization/' 'locks/bar/providers/Microsoft.Network/', 'resource_group': 'testgroup', 'subscription': 'fakesub', } }, { 'resource_id': '/subscriptions/fakesub/resourcegroups/testgroup/providers' '/Microsoft.Storage/storageAccounts/foo' '/locks/bar/nets/gc', 'expected': { 'name': 'foo', 'type': 'storageAccounts', 'namespace': 'Microsoft.Storage', 'child_name_1': 'bar', 'child_type_1': 'locks', 'child_parent_1': 'storageAccounts/foo/', 'child_name_2': 'gc', 'child_type_2': 'nets', 'child_parent_2': 'storageAccounts/foo/locks/bar/', 'resource_group': 'testgroup', 'subscription': 'fakesub', } }, { 'resource_id': '/subscriptions/mySub/resourceGroups/myRg/providers/' 'Microsoft.Provider1/resourceType1/name1', 'expected': { 'subscription': 'mySub', 'resource_group': 'myRg', 'namespace': 'Microsoft.Provider1', 'type': 'resourceType1', 'name': 'name1', 'resource_parent': '', 'resource_namespace': 'Microsoft.Provider1', 'resource_type': 'resourceType1', 'resource_name': 'name1' } }, { 'resource_id': '/subscriptions/mySub/resourceGroups/myRg/providers/' 'Microsoft.Provider1/resourceType1/name1/resourceType2/name2', 'expected': { 'subscription': 'mySub', 'resource_group': 'myRg', 'namespace': 'Microsoft.Provider1', 'type': 'resourceType1', 'name': 'name1', 'child_namespace_1': None, 'child_type_1': 'resourceType2', 'child_name_1': 'name2', 'child_parent_1': 'resourceType1/name1/', 'resource_parent': 'resourceType1/name1/', 'resource_namespace': 'Microsoft.Provider1', 'resource_type': 'resourceType2', 'resource_name': 'name2' } }, { 'resource_id': '/subscriptions/00000/resourceGroups/myRg/providers/' 'Microsoft.RecoveryServices/vaults/vault_name/backupFabrics/' 'fabric_name/protectionContainers/container_name/' 'protectedItems/item_name/recoveryPoint/recovery_point_guid', 'expected': { 'subscription': '00000', 'resource_group': 'myRg', 'namespace': 'Microsoft.RecoveryServices', 'type': 'vaults', 'name': 'vault_name', 'child_type_1': 'backupFabrics', 'child_name_1': 'fabric_name', 'child_parent_1': 'vaults/vault_name/', 'child_type_2': 'protectionContainers', 'child_name_2': 'container_name', 'child_parent_2': 'vaults/vault_name/backupFabrics/fabric_name/', 'child_type_3': 'protectedItems', 'child_name_3': 'item_name', 'child_parent_3': 'vaults/vault_name/backupFabrics/fabric_name/' 'protectionContainers/container_name/', 'child_type_4': 'recoveryPoint', 'child_name_4': 'recovery_point_guid', 'child_parent_4': 'vaults/vault_name/backupFabrics/fabric_name/' 'protectionContainers/container_name/protectedItems/' 'item_name/', 'resource_parent': 'vaults/vault_name/backupFabrics/fabric_name/' 'protectionContainers/container_name/protectedItems/' 'item_name/', 'resource_namespace': 'Microsoft.RecoveryServices', 'resource_type': 'recoveryPoint', 'resource_name': 'recovery_point_guid' } }, { 'resource_id': '/subscriptions/mySub/resourceGroups/myRg/providers/' 'Microsoft.Provider1/resourceType1/name1/resourceType2/name2/' 'providers/Microsoft.Provider3/resourceType3/name3', 'expected': { 'subscription': 'mySub', 'resource_group': 'myRg', 'namespace': 'Microsoft.Provider1', 'type': 'resourceType1', 'name': 'name1', 'child_namespace_1': None, 'child_type_1': 'resourceType2', 'child_name_1': 'name2', 'child_parent_1' : 'resourceType1/name1/', 'child_namespace_2': 'Microsoft.Provider3', 'child_type_2': 'resourceType3', 'child_name_2': 'name3', 'child_parent_2': 'resourceType1/name1/resourceType2/name2/' 'providers/Microsoft.Provider3/', 'resource_parent': 'resourceType1/name1/resourceType2/name2/' 'providers/Microsoft.Provider3/', 'resource_namespace': 'Microsoft.Provider1', 'resource_type': 'resourceType3', 'resource_name': 'name3' } }, { 'resource_id': '/subscriptions/fakesub/providers/Microsoft.Authorization' '/locks/foo', 'expected': { 'name': 'foo', 'type': 'locks', 'namespace': 'Microsoft.Authorization', 'subscription': 'fakesub', } }, { 'resource_id': '/Subscriptions/fakesub/providers/Microsoft.Authorization' '/locks/foo', 'expected': { 'name': 'foo', 'type': 'locks', 'namespace': 'Microsoft.Authorization', 'subscription': 'fakesub', } }, { 'resource_id': '/subscriptions/mySub/resourceGroups/myRg', 'expected': { 'subscription': 'mySub', 'resource_group': 'myRg' } } ] for test in tests: self.assertTrue(is_valid_resource_id(test['resource_id'])) kwargs = parse_resource_id(test['resource_id']) for key in test['expected']: try: self.assertEqual(kwargs[key], test['expected'][key]) except KeyError: self.assertTrue(key not in kwargs and test['expected'][key] is None) invalid_ids = [ '/subscriptions/fakesub/resourceGroups/myRg/type1/name1', '/subscriptions/fakesub/resourceGroups/myRg/providers/Microsoft.Provider/foo', '/subscriptions/fakesub/resourceGroups/myRg/providers/namespace/type/name/type1' ] for invalid_id in invalid_ids: self.assertFalse(is_valid_resource_id(invalid_id)) tests = [ { 'resource_id': '/subscriptions/fakesub/resourcegroups/testgroup/providers' '/Microsoft.Storage/storageAccounts/foo/providers' '/Microsoft.Authorization/locks/bar', 'id_args': { 'name': 'foo', 'type': 'storageAccounts', 'namespace': 'Microsoft.Storage', 'child_name_1': 'bar', 'child_namespace_1': 'Microsoft.Authorization', 'child_type_1': 'locks', 'resource_group': 'testgroup', 'subscription': 'fakesub', } }, { 'resource_id': '/subscriptions/fakesub/resourcegroups/testgroup/providers' '/Microsoft.Storage/storageAccounts/foo' '/locks/bar', 'id_args': { 'name': 'foo', 'type': 'storageAccounts', 'namespace': 'Microsoft.Storage', 'child_name_1': 'bar', 'child_type_1': 'locks', 'resource_group': 'testgroup', 'subscription': 'fakesub', } }, { 'resource_id': '/subscriptions/mySub/resourceGroups/myRg/providers/' 'Microsoft.Provider1/resourceType1/name1/resourceType2/name2/' 'providers/Microsoft.Provider3/resourceType3/name3', 'id_args': { 'subscription': 'mySub', 'resource_group': 'myRg', 'namespace': 'Microsoft.Provider1', 'type': 'resourceType1', 'name': 'name1', 'child_type_1': 'resourceType2', 'child_name_1': 'name2', 'child_namespace_2': 'Microsoft.Provider3', 'child_type_2': 'resourceType3', 'child_name_2': 'name3' } }, { 'resource_id': '/subscriptions/mySub/resourceGroups/myRg/' 'providers/Microsoft.Provider1', 'id_args': { 'subscription': 'mySub', 'resource_group': 'myRg', 'namespace': 'Microsoft.Provider1' } }, { 'resource_id': '/subscriptions/mySub/resourceGroups/myRg', 'id_args': { 'subscription': 'mySub', 'resource_group': 'myRg' } }, { 'resource_id': '/subscriptions/mySub/resourceGroups/myRg/' 'providers/Microsoft.Provider1/resourceType1/name1/resourceType2/' 'name2/providers/Microsoft.Provider3', 'id_args': { 'subscription': 'mySub', 'resource_group': 'myRg', 'namespace': 'Microsoft.Provider1', 'type': 'resourceType1', 'name': 'name1', 'child_type_1': 'resourceType2', 'child_name_1': 'name2', 'child_namespace_2': 'Microsoft.Provider3' } }, { 'resource_id': '/subscriptions/mySub/resourceGroups/myRg/' 'providers/Microsoft.Provider1/resourceType1/name1', 'id_args': { 'subscription': 'mySub', 'resource_group': 'myRg', 'namespace': 'Microsoft.Provider1', 'type': 'resourceType1', 'name': 'name1', 'child_type_1': None, 'child_name_1': 'name2', 'child_namespace_2': 'Microsoft.Provider3' } }, { 'resource_id': '/subscriptions/mySub/resourceGroups/myRg', 'id_args': { 'subscription': 'mySub', 'resource_group': 'myRg' } } ] for test in tests: rsrc_id = resource_id(**test['id_args']) self.assertEqual(rsrc_id.lower(), test['resource_id'].lower()) def test_is_resource_name(self): invalid_names = [ '', 'knights/ni', 'spam&eggs', 'i<3you', 'a' * 261, ] for test in invalid_names: assert not is_valid_resource_name(test) valid_names = [ 'abc-123', ' ', # no one said it had to be a good resource name. 'a' * 260, ] for test in valid_names: assert is_valid_resource_name(test) if __name__ == "__main__": unittest.main() msrestazure-for-python-0.6.4/tox.ini000066400000000000000000000007051367642573600175500ustar00rootroot00000000000000[tox] envlist=py27, py35 skipsdist=True [testenv] setenv = PYTHONPATH = {toxinidir}:{toxinidir}/msrestazure PythonLogLevel=30 deps= -rdev_requirements.txt autorest: requests>=2.14.0 commands= pytest --cov=msrestazure tests/ autorest: pytest --cov=msrestazure --cov-append autorest.python/test/azure/ coverage report --fail-under=40 coverage xml --ignore-errors # At this point, don't fail for "async" keyword in 2.7/3.4