Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Authentication fails only when debugging a pytest. #38094

Closed
lovettchris opened this issue Oct 25, 2024 · 11 comments
Closed

Authentication fails only when debugging a pytest. #38094

lovettchris opened this issue Oct 25, 2024 · 11 comments
Assignees
Labels
Azure.Identity Client This issue points to a problem in the data-plane of the library. customer-reported Issues that are reported by GitHub users external to the Azure organization. issue-addressed Workflow: The Azure SDK team believes it to be addressed and ready to close. question The issue doesn't require a change to the product in order to be resolved. Most issues start as that

Comments

@lovettchris
Copy link

  • Package Name: see below
  • Package Version: see below
  • Operating System: Windows 11
  • Python Version: 3.10.15
  • VSCode Version: 1.94.2

azure-common 1.1.28
azure-core 1.31.0
azure-data-tables 12.5.0
azure-identity 1.19.0
azure-keyvault-keys 4.9.0
azure-keyvault-secrets 4.8.0
azure-mgmt-compute 33.0.0
azure-mgmt-core 1.4.0
azure-storage-blob 12.23.1

Describe the bug
I have a simple Azure blob that I'm trying to connect to using DefaultCredentials in a pytest and it works fine when I run pytest from the command line, but when I try and debug the pytest in VSCode it fails with strange errors.

To Reproduce
Steps to reproduce the behavior:

  1. debug a pytest that connects to Azure blob store.

Expected behavior
should work the same

Screenshots

Additional context

Here's the debug log:

d:\git\athens\SmartReplay\tests\agents\test_agent_utils.py::test_setup_model failed: command = 'az account get-access-token --output json --resource https://storage.azure.com'
timeout = 10

    def _run_command(command: str, timeout: int) -> str:
        # Ensure executable exists in PATH first. This avoids a subprocess call that would fail anyway.
        if shutil.which(EXECUTABLE_NAME) is None:
            raise CredentialUnavailableError(message=CLI_NOT_FOUND)
    
        if sys.platform.startswith("win"):
            args = ["cmd", "/c", command]
        else:
            args = ["/bin/sh", "-c", command]
        try:
            working_directory = get_safe_working_dir()
    
            kwargs: Dict[str, Any] = {
                "stderr": subprocess.PIPE,
                "stdin": subprocess.DEVNULL,
                "cwd": working_directory,
                "universal_newlines": True,
                "timeout": timeout,
                "env": dict(os.environ, AZURE_CORE_NO_COLOR="true"),
            }
>           return subprocess.check_output(args, **kwargs)

D:\Anaconda3\envs\sr\lib\site-packages\azure\identity\_credentials\azure_cli.py:234: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

timeout = 10
popenargs = (['cmd', '/c', 'az account get-access-token --output json --resource https://storage.azure.com'],)
kwargs = {'cwd': 'C:\\WINDOWS', 'env': {'ADA_GIT_REPO': 'https://github.com/microsoft/ada', 'ADA_STORAGE_CONNECTION_STRING': 'D...EEwpKDuE2QeqC1BUfTdn/9o8MQY=;Version=1.0;', 'ALLUSERSPROFILE': 'C:\\ProgramData', ...}, 'stderr': -1, 'stdin': -3, ...}

    def check_output(*popenargs, timeout=None, **kwargs):
        r"""Run command with arguments and return its output.
    
        If the exit code was non-zero it raises a CalledProcessError.  The
        CalledProcessError object will have the return code in the returncode
        attribute and output in the output attribute.
    
        The arguments are the same as for the Popen constructor.  Example:
    
        >>> check_output(["ls", "-l", "/dev/null"])
        b'crw-rw-rw- 1 root root 1, 3 Oct 18  2007 /dev/null\n'
    
        The stdout argument is not allowed as it is used internally.
        To capture standard error in the result, use stderr=STDOUT.
    
        >>> check_output(["/bin/sh", "-c",
        ...               "ls -l non_existent_file ; exit 0"],
        ...              stderr=STDOUT)
        b'ls: non_existent_file: No such file or directory\n'
    
        There is an additional optional argument, "input", allowing you to
        pass a string to the subprocess's stdin.  If you use this argument
        you may not also use the Popen constructor's "stdin" argument, as
        it too will be used internally.  Example:
    
        >>> check_output(["sed", "-e", "s/foo/bar/"],
        ...              input=b"when in the course of fooman events\n")
        b'when in the course of barman events\n'
    
        By default, all communication is in bytes, and therefore any "input"
        should be bytes, and the return value will be bytes.  If in text mode,
        any "input" should be a string, and the return value will be a string
        decoded according to locale encoding, or by "encoding" if set. Text mode
        is triggered by setting any of text, encoding, errors or universal_newlines.
        """
        if 'stdout' in kwargs:
            raise ValueError('stdout argument not allowed, it will be overridden.')
    
        if 'input' in kwargs and kwargs['input'] is None:
            # Explicitly passing input=None was previously equivalent to passing an
            # empty string. That is maintained here for backwards compatibility.
            if kwargs.get('universal_newlines') or kwargs.get('text') or kwargs.get('encoding') \
                    or kwargs.get('errors'):
                empty = ''
            else:
                empty = b''
            kwargs['input'] = empty
    
>       return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
                   **kwargs).stdout

D:\Anaconda3\envs\sr\lib\subprocess.py:421: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

input = None, capture_output = False, timeout = 10, check = True
popenargs = (['cmd', '/c', 'az account get-access-token --output json --resource https://storage.azure.com'],)
kwargs = {'cwd': 'C:\\WINDOWS', 'env': {'ADA_GIT_REPO': 'https://github.com/microsoft/ada', 'ADA_STORAGE_CONNECTION_STRING': 'D...EEwpKDuE2QeqC1BUfTdn/9o8MQY=;Version=1.0;', 'ALLUSERSPROFILE': 'C:\\ProgramData', ...}, 'stderr': -1, 'stdin': -3, ...}
process = <Popen: returncode: 1 args: ['cmd', '/c', 'az account get-access-token --out...>
stdout = ''
stderr = "'az' is not recognized as an internal or external command,\noperable program or batch file.\n"
retcode = 1

    def run(*popenargs,
            input=None, capture_output=False, timeout=None, check=False, **kwargs):
        """Run command with arguments and return a CompletedProcess instance.
    
        The returned instance will have attributes args, returncode, stdout and
        stderr. By default, stdout and stderr are not captured, and those attributes
        will be None. Pass stdout=PIPE and/or stderr=PIPE in order to capture them,
        or pass capture_output=True to capture both.
    
        If check is True and the exit code was non-zero, it raises a
        CalledProcessError. The CalledProcessError object will have the return code
        in the returncode attribute, and output & stderr attributes if those streams
        were captured.
    
        If timeout is given, and the process takes too long, a TimeoutExpired
        exception will be raised.
    
        There is an optional argument "input", allowing you to
        pass bytes or a string to the subprocess's stdin.  If you use this argument
        you may not also use the Popen constructor's "stdin" argument, as
        it will be used internally.
    
        By default, all communication is in bytes, and therefore any "input" should
        be bytes, and the stdout and stderr will be bytes. If in text mode, any
        "input" should be a string, and stdout and stderr will be strings decoded
        according to locale encoding, or by "encoding" if set. Text mode is
        triggered by setting any of text, encoding, errors or universal_newlines.
    
        The other arguments are the same as for the Popen constructor.
        """
        if input is not None:
            if kwargs.get('stdin') is not None:
                raise ValueError('stdin and input arguments may not both be used.')
            kwargs['stdin'] = PIPE
    
        if capture_output:
            if kwargs.get('stdout') is not None or kwargs.get('stderr') is not None:
                raise ValueError('stdout and stderr arguments may not be used '
                                 'with capture_output.')
            kwargs['stdout'] = PIPE
            kwargs['stderr'] = PIPE
    
        with Popen(*popenargs, **kwargs) as process:
            try:
                stdout, stderr = process.communicate(input, timeout=timeout)
            except TimeoutExpired as exc:
                process.kill()
                if _mswindows:
                    # Windows accumulates the output in a single blocking
                    # read() call run on child threads, with the timeout
                    # being done in a join() on those threads.  communicate()
                    # _after_ kill() is required to collect that and add it
                    # to the exception.
                    exc.stdout, exc.stderr = process.communicate()
                else:
                    # POSIX _communicate already populated the output so
                    # far into the TimeoutExpired exception.
                    process.wait()
                raise
            except:  # Including KeyboardInterrupt, communicate handled that.
                process.kill()
                # We don't call process.wait() as .__exit__ does that for us.
                raise
            retcode = process.poll()
            if check and retcode:
>               raise CalledProcessError(retcode, process.args,
                                         output=stdout, stderr=stderr)
E               subprocess.CalledProcessError: Command '['cmd', '/c', 'az account get-access-token --output json --resource https://storage.azure.com']' returned non-zero exit status 1.

D:\Anaconda3\envs\sr\lib\subprocess.py:526: CalledProcessError

The above exception was the direct cause of the following exception:

    def test_setup_model():
    
>       store = get_store(blob_container_name=BLOB_CONTAINER_NAME, table_name=TABLE_NAME)

tests\agents\test_agent_utils.py:18: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
smart_replay\store\store.py:36: in get_store
    return AzureStore(account_id, blob_container_name=blob_container_name, table_name=table_name)
smart_replay\store\store.py:104: in __init__
    self.container_handler = AzureContainerHandler(storage_account_id, self.blob_container_name)
smart_replay\store\storage_handler.py:122: in __init__
    self.container_client = self._create_container_client()
smart_replay\store\storage_handler.py:138: in _create_container_client
    if not client.exists():
D:\Anaconda3\envs\sr\lib\site-packages\azure\core\tracing\decorator.py:94: in wrapper_use_tracer
    return func(*args, **kwargs)
D:\Anaconda3\envs\sr\lib\site-packages\azure\storage\blob\_container_client.py:554: in exists
    process_storage_error(error)
D:\Anaconda3\envs\sr\lib\site-packages\azure\storage\blob\_shared\response_handlers.py:92: in process_storage_error
    raise storage_error
D:\Anaconda3\envs\sr\lib\site-packages\azure\storage\blob\_container_client.py:550: in exists
    self._client.container.get_properties(**kwargs)
D:\Anaconda3\envs\sr\lib\site-packages\azure\core\tracing\decorator.py:94: in wrapper_use_tracer
    return func(*args, **kwargs)
D:\Anaconda3\envs\sr\lib\site-packages\azure\storage\blob\_generated\operations\_container_operations.py:1063: in get_properties
    pipeline_response: PipelineResponse = self._client._pipeline.run(  # pylint: disable=protected-access
D:\Anaconda3\envs\sr\lib\site-packages\azure\core\pipeline\_base.py:229: in run
    return first_node.send(pipeline_request)
D:\Anaconda3\envs\sr\lib\site-packages\azure\core\pipeline\_base.py:86: in send
    response = self.next.send(request)
D:\Anaconda3\envs\sr\lib\site-packages\azure\core\pipeline\_base.py:86: in send
    response = self.next.send(request)
D:\Anaconda3\envs\sr\lib\site-packages\azure\core\pipeline\_base.py:86: in send
    response = self.next.send(request)
D:\Anaconda3\envs\sr\lib\site-packages\azure\core\pipeline\_base.py:86: in send
    response = self.next.send(request)
D:\Anaconda3\envs\sr\lib\site-packages\azure\core\pipeline\_base.py:86: in send
    response = self.next.send(request)
D:\Anaconda3\envs\sr\lib\site-packages\azure\core\pipeline\policies\_redirect.py:197: in send
    response = self.next.send(request)
D:\Anaconda3\envs\sr\lib\site-packages\azure\core\pipeline\_base.py:86: in send
    response = self.next.send(request)
D:\Anaconda3\envs\sr\lib\site-packages\azure\storage\blob\_shared\policies.py:556: in send
    raise err
D:\Anaconda3\envs\sr\lib\site-packages\azure\storage\blob\_shared\policies.py:528: in send
    response = self.next.send(request)
D:\Anaconda3\envs\sr\lib\site-packages\azure\core\pipeline\_base.py:86: in send
    response = self.next.send(request)
D:\Anaconda3\envs\sr\lib\site-packages\azure\core\pipeline\_base.py:86: in send
    response = self.next.send(request)
D:\Anaconda3\envs\sr\lib\site-packages\azure\core\pipeline\policies\_authentication.py:145: in send
    self.on_request(request)
D:\Anaconda3\envs\sr\lib\site-packages\azure\core\pipeline\policies\_authentication.py:120: in on_request
    self._request_token(*self._scopes)
D:\Anaconda3\envs\sr\lib\site-packages\azure\core\pipeline\policies\_authentication.py:94: in _request_token
    self._token = cast(SupportsTokenInfo, self._credential).get_token_info(*scopes, options=options)
D:\Anaconda3\envs\sr\lib\site-packages\azure\identity\_internal\decorators.py:23: in wrapper
    token = fn(*args, **kwargs)
D:\Anaconda3\envs\sr\lib\site-packages\azure\identity\_credentials\azure_cli.py:125: in get_token_info
    return self._get_token_base(*scopes, options=options)
D:\Anaconda3\envs\sr\lib\site-packages\azure\identity\_credentials\azure_cli.py:147: in _get_token_base
    output = _run_command(command, self._process_timeout)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

command = 'az account get-access-token --output json --resource https://storage.azure.com'
timeout = 10

    def _run_command(command: str, timeout: int) -> str:
        # Ensure executable exists in PATH first. This avoids a subprocess call that would fail anyway.
        if shutil.which(EXECUTABLE_NAME) is None:
            raise CredentialUnavailableError(message=CLI_NOT_FOUND)
    
        if sys.platform.startswith("win"):
            args = ["cmd", "/c", command]
        else:
            args = ["/bin/sh", "-c", command]
        try:
            working_directory = get_safe_working_dir()
    
            kwargs: Dict[str, Any] = {
                "stderr": subprocess.PIPE,
                "stdin": subprocess.DEVNULL,
                "cwd": working_directory,
                "universal_newlines": True,
                "timeout": timeout,
                "env": dict(os.environ, AZURE_CORE_NO_COLOR="true"),
            }
            return subprocess.check_output(args, **kwargs)
        except subprocess.CalledProcessError as ex:
            # non-zero return from shell
            # Fallback check in case the executable is not found while executing subprocess.
            if ex.returncode == 127 or ex.stderr.startswith("'az' is not recognized"):
>               raise CredentialUnavailableError(message=CLI_NOT_FOUND) from ex
E               azure.identity._exceptions.CredentialUnavailableError: Azure CLI not found on path

D:\Anaconda3\envs\sr\lib\site-packages\azure\identity\_credentials\azure_cli.py:239: CredentialUnavailableError

@github-actions github-actions bot added customer-reported Issues that are reported by GitHub users external to the Azure organization. needs-triage Workflow: This is a new issue that needs to be triaged to the appropriate team. question The issue doesn't require a change to the product in order to be resolved. Most issues start as that labels Oct 25, 2024
@xiangyan99 xiangyan99 added Client This issue points to a problem in the data-plane of the library. Azure.Identity and removed needs-triage Workflow: This is a new issue that needs to be triaged to the appropriate team. labels Oct 25, 2024
@github-actions github-actions bot added the needs-team-attention Workflow: This issue needs attention from Azure service team or SDK team label Oct 25, 2024
@pvaneck
Copy link
Member

pvaneck commented Oct 25, 2024

From the error message, the Azure CLI executable az can't be found in the path. Perhaps when you run or debug tests in VS Code, it might be using a different environment that doesn't have the Azure CLI in its PATH.

What terminal does your VS Code use to run tests, and is it the same as the normal terminal you used to run the tests manually?

If you print os.environ.get("PATH") in your test, you can also check to see how it differs in VS Code versus normally. Ultimately, I think this is a VS Code configuration issue and some adjustments might be needed.

@pvaneck pvaneck added needs-author-feedback Workflow: More information is needed from author to address the issue. and removed needs-team-attention Workflow: This issue needs attention from Azure service team or SDK team labels Oct 25, 2024
Copy link

Hi @lovettchris. Thank you for opening this issue and giving us the opportunity to assist. To help our team better understand your issue and the details of your scenario please provide a response to the question asked above or the information requested above. This will help us more accurately address your issue.

@lovettchris
Copy link
Author

Ok, I made sure az is in my PATH for the USER and for SYSTEM and not just for the command line environment, and I get the same error. Here's a small repro:

from azure.identity import AzureCliCredential
from azure.storage.blob import BlobServiceClient


def test_azure():
    credentials = AzureCliCredential()
    storage_account = "srexperiments"
    blob_container_name = "unittests"
    account_url = f"https://{storage_account}.blob.core.windows.net/"
    blob_service_client = BlobServiceClient(account_url, credential=credentials, logging_enable=False)
    container_client = blob_service_client.get_container_client(container=blob_container_name)
    assert container_client.exists()

Steps

  1. Create storage account "srexperiments" and blob container "unittests" and make sure your SC-ALT account has these permissions:

    • "Reader",
    • "Storage Blob Data Reader",
    • "Storage Blob Data Contributor",
    • "Storage Queue Data Reader",
    • "Storage Queue Data Contributor",
    • "Storage Table Data Reader",
    • "Storage Table Data Contributor",
    • "Storage File Data Privileged Reader",
  2. Install azcopy and az cli and make sure az is in your user PATH and system PATH.

  3. Use azcopy login and login to your Azure SC-ALT account using web browser.

  4. Use az login to also login to the same account (should be quick because of cached credentials.

  5. run pytest from command line (works fine)

  6. debug the pytest in VS code, fails with the following errors:

Running pytest with args: ['-p', 'vscode_pytest', '--rootdir=d:\\temp\\test', '--capture=no', 'd:\\temp\\test\\test_azure.py::test_azure']
============================= test session starts =============================
platform win32 -- Python 3.10.15, pytest-7.3.1, pluggy-1.5.0
rootdir: d:\temp\test
plugins: anyio-4.6.2.post1
collected 1 item

test_azure.py F

================================== FAILURES ===================================
_________________________________ test_azure __________________________________
command = 'az account get-access-token --output json --resource https://storage.azure.com'
timeout = 10
    def _run_command(command: str, timeout: int) -> str:
        # Ensure executable exists in PATH first. This avoids a subprocess call that would fail anyway.
        if shutil.which(EXECUTABLE_NAME) is None:
            raise CredentialUnavailableError(message=CLI_NOT_FOUND)
    
        if sys.platform.startswith("win"):
            args = ["cmd", "/c", command]
        else:
            args = ["/bin/sh", "-c", command]
        try:
            working_directory = get_safe_working_dir()
    
            kwargs: Dict[str, Any] = {
                "stderr": subprocess.PIPE,
                "stdin": subprocess.DEVNULL,
                "cwd": working_directory,
                "universal_newlines": True,
                "timeout": timeout,
                "env": dict(os.environ, AZURE_CORE_NO_COLOR="true"),
            }
>           return subprocess.check_output(args, **kwargs)

D:\Anaconda3\envs\sr\lib\site-packages\azure\identity\_credentials\azure_cli.py:234:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
timeout = 10
popenargs = (['cmd', '/c', 'az account get-access-token --output json --resource https://storage.azure.com'],)
kwargs = {'cwd': 'C:\\WINDOWS', 'env': {'ADA_GIT_REPO': 'https://github.com/microsoft/ada', 'ADA_STORAGE_CONNECTION_STRING': 'D...EEwpKDuE2QeqC1BUfTdn/9o8MQY=;Version=1.0;', 'ALLUSERSPROFILE': 'C:\\ProgramData', ...}, 'stderr': -1, 'stdin': -3, ...}
    def check_output(*popenargs, timeout=None, **kwargs):
        r"""Run command with arguments and return its output.
    
        If the exit code was non-zero it raises a CalledProcessError.  The
        CalledProcessError object will have the return code in the returncode
        attribute and output in the output attribute.
    
        The arguments are the same as for the Popen constructor.  Example:
    
        >>> check_output(["ls", "-l", "/dev/null"])
        b'crw-rw-rw- 1 root root 1, 3 Oct 18  2007 /dev/null\n'
    
        The stdout argument is not allowed as it is used internally.
        To capture standard error in the result, use stderr=STDOUT.
    
        >>> check_output(["/bin/sh", "-c",
        ...               "ls -l non_existent_file ; exit 0"],
        ...              stderr=STDOUT)
        b'ls: non_existent_file: No such file or directory\n'
    
        There is an additional optional argument, "input", allowing you to
        pass a string to the subprocess's stdin.  If you use this argument
        you may not also use the Popen constructor's "stdin" argument, as
        it too will be used internally.  Example:
        >>> check_output(["sed", "-e", "s/foo/bar/"],
        ...              input=b"when in the course of fooman events\n")
        b'when in the course of barman events\n'
    
        By default, all communication is in bytes, and therefore any "input"
        should be bytes, and the return value will be bytes.  If in text mode,
        any "input" should be a string, and the return value will be a string
        decoded according to locale encoding, or by "encoding" if set. Text mode
        is triggered by setting any of text, encoding, errors or universal_newlines.
        """
        if 'stdout' in kwargs:
            raise ValueError('stdout argument not allowed, it will be overridden.')
    
        if 'input' in kwargs and kwargs['input'] is None:
            # Explicitly passing input=None was previously equivalent to passing an
            # empty string. That is maintained here for backwards compatibility.
            if kwargs.get('universal_newlines') or kwargs.get('text') or kwargs.get('encoding') \
                    or kwargs.get('errors'):
                empty = ''
            else:
                empty = b''
            kwargs['input'] = empty
    
>       return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
                   **kwargs).stdout
D:\Anaconda3\envs\sr\lib\subprocess.py:421: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

input = None, capture_output = False, timeout = 10, check = True
popenargs = (['cmd', '/c', 'az account get-access-token --output json --resource https://storage.azure.com'],)
kwargs = {'cwd': 'C:\\WINDOWS', 'env': {'ADA_GIT_REPO': 'https://github.com/microsoft/ada', 'ADA_STORAGE_CONNECTION_STRING': 'D...EEwpKDuE2QeqC1BUfTdn/9o8MQY=;Version=1.0;', 'ALLUSERSPROFILE': 'C:\\ProgramData', ...}, 'stderr': -1, 'stdin': -3, ...}
process = <Popen: returncode: 1 args: ['cmd', '/c', 'az account get-access-token --out...>
stdout = ''
stderr = "'az' is not recognized as an internal or external command,\noperable program or batch file.\n"
retcode = 1
    def run(*popenargs,
            input=None, capture_output=False, timeout=None, check=False, **kwargs):
        """Run command with arguments and return a CompletedProcess instance.
    
        The returned instance will have attributes args, returncode, stdout and
        stderr. By default, stdout and stderr are not captured, and those attributes
        will be None. Pass stdout=PIPE and/or stderr=PIPE in order to capture them,
        or pass capture_output=True to capture both.
    
        If check is True and the exit code was non-zero, it raises a
        CalledProcessError. The CalledProcessError object will have the return code
        in the returncode attribute, and output & stderr attributes if those streams
        were captured.
    
        If timeout is given, and the process takes too long, a TimeoutExpired
        exception will be raised.
    
        There is an optional argument "input", allowing you to
        pass bytes or a string to the subprocess's stdin.  If you use this argument
        you may not also use the Popen constructor's "stdin" argument, as
        it will be used internally.
    
        By default, all communication is in bytes, and therefore any "input" should
        be bytes, and the stdout and stderr will be bytes. If in text mode, any
        "input" should be a string, and stdout and stderr will be strings decoded
        according to locale encoding, or by "encoding" if set. Text mode is
        triggered by setting any of text, encoding, errors or universal_newlines.
    
        The other arguments are the same as for the Popen constructor.
        """
        if input is not None:
            if kwargs.get('stdin') is not None:
                raise ValueError('stdin and input arguments may not both be used.')
            kwargs['stdin'] = PIPE
    
        if capture_output:
            if kwargs.get('stdout') is not None or kwargs.get('stderr') is not None:
                raise ValueError('stdout and stderr arguments may not be used '
                                 'with capture_output.')
            kwargs['stdout'] = PIPE
            kwargs['stderr'] = PIPE
    
        with Popen(*popenargs, **kwargs) as process:
            try:
                stdout, stderr = process.communicate(input, timeout=timeout)
            except TimeoutExpired as exc:
                process.kill()
                if _mswindows:
                    # Windows accumulates the output in a single blocking
                    # read() call run on child threads, with the timeout
                    # being done in a join() on those threads.  communicate()
                    # _after_ kill() is required to collect that and add it
                    # to the exception.
                    exc.stdout, exc.stderr = process.communicate()
                else:
                    # POSIX _communicate already populated the output so
                    # far into the TimeoutExpired exception.
                    process.wait()
                raise
            except:  # Including KeyboardInterrupt, communicate handled that.
                process.kill()
                # We don't call process.wait() as .__exit__ does that for us.
                raise
            retcode = process.poll()
            if check and retcode:
>               raise CalledProcessError(retcode, process.args,
                                         output=stdout, stderr=stderr)
E               subprocess.CalledProcessError: Command '['cmd', '/c', 'az account get-access-token --output json --resource https://storage.azure.com']' returned non-zero exit status 1.
D:\Anaconda3\envs\sr\lib\subprocess.py:526: CalledProcessError

The above exception was the direct cause of the following exception:

    def test_azure():
        credentials = AzureCliCredential()
        storage_account = "srexperiments"
        blob_container_name = "unittests"
        account_url = f"https://{storage_account}.blob.core.windows.net/"
        blob_service_client = BlobServiceClient(account_url, credential=credentials, logging_enable=False)
        container_client = blob_service_client.get_container_client(container=blob_container_name)
>       assert container_client.exists()

test_azure.py:12:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
D:\Anaconda3\envs\sr\lib\site-packages\azure\core\tracing\decorator.py:94: in wrapper_use_tracer
    return func(*args, **kwargs)
D:\Anaconda3\envs\sr\lib\site-packages\azure\storage\blob\_container_client.py:554: in exists
    process_storage_error(error)
D:\Anaconda3\envs\sr\lib\site-packages\azure\storage\blob\_shared\response_handlers.py:92: in process_storage_error
    raise storage_error
D:\Anaconda3\envs\sr\lib\site-packages\azure\storage\blob\_container_client.py:550: in exists
    self._client.container.get_properties(**kwargs)
D:\Anaconda3\envs\sr\lib\site-packages\azure\core\tracing\decorator.py:94: in wrapper_use_tracer
    return func(*args, **kwargs)
D:\Anaconda3\envs\sr\lib\site-packages\azure\storage\blob\_generated\operations\_container_operations.py:1063: in get_properties
    pipeline_response: PipelineResponse = self._client._pipeline.run(  # pylint: disable=protected-access
D:\Anaconda3\envs\sr\lib\site-packages\azure\core\pipeline\_base.py:229: in run
    return first_node.send(pipeline_request)
D:\Anaconda3\envs\sr\lib\site-packages\azure\core\pipeline\_base.py:86: in send
    response = self.next.send(request)
D:\Anaconda3\envs\sr\lib\site-packages\azure\core\pipeline\_base.py:86: in send
    response = self.next.send(request)
D:\Anaconda3\envs\sr\lib\site-packages\azure\core\pipeline\_base.py:86: in send
    response = self.next.send(request)
D:\Anaconda3\envs\sr\lib\site-packages\azure\core\pipeline\_base.py:86: in send
    response = self.next.send(request)
D:\Anaconda3\envs\sr\lib\site-packages\azure\core\pipeline\_base.py:86: in send
    response = self.next.send(request)
D:\Anaconda3\envs\sr\lib\site-packages\azure\core\pipeline\policies\_redirect.py:197: in send
    response = self.next.send(request)
D:\Anaconda3\envs\sr\lib\site-packages\azure\core\pipeline\_base.py:86: in send
    response = self.next.send(request)
D:\Anaconda3\envs\sr\lib\site-packages\azure\storage\blob\_shared\policies.py:556: in send
    raise err
D:\Anaconda3\envs\sr\lib\site-packages\azure\storage\blob\_shared\policies.py:528: in send
    response = self.next.send(request)
D:\Anaconda3\envs\sr\lib\site-packages\azure\core\pipeline\_base.py:86: in send
    response = self.next.send(request)
D:\Anaconda3\envs\sr\lib\site-packages\azure\core\pipeline\_base.py:86: in send
    response = self.next.send(request)
D:\Anaconda3\envs\sr\lib\site-packages\azure\core\pipeline\policies\_authentication.py:145: in send
    self.on_request(request)
D:\Anaconda3\envs\sr\lib\site-packages\azure\core\pipeline\policies\_authentication.py:120: in on_request
    self._request_token(*self._scopes)
D:\Anaconda3\envs\sr\lib\site-packages\azure\core\pipeline\policies\_authentication.py:94: in _request_token
    self._token = cast(SupportsTokenInfo, self._credential).get_token_info(*scopes, options=options)
D:\Anaconda3\envs\sr\lib\site-packages\azure\identity\_internal\decorators.py:23: in wrapper
    token = fn(*args, **kwargs)
D:\Anaconda3\envs\sr\lib\site-packages\azure\identity\_credentials\azure_cli.py:125: in get_token_info
    return self._get_token_base(*scopes, options=options)
D:\Anaconda3\envs\sr\lib\site-packages\azure\identity\_credentials\azure_cli.py:147: in _get_token_base
    output = _run_command(command, self._process_timeout)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

command = 'az account get-access-token --output json --resource https://storage.azure.com'
timeout = 10

    def _run_command(command: str, timeout: int) -> str:
        # Ensure executable exists in PATH first. This avoids a subprocess call that would fail anyway.
        if shutil.which(EXECUTABLE_NAME) is None:
            raise CredentialUnavailableError(message=CLI_NOT_FOUND)
    
        if sys.platform.startswith("win"):
            args = ["cmd", "/c", command]
        else:
            args = ["/bin/sh", "-c", command]
        try:
            working_directory = get_safe_working_dir()
    
            kwargs: Dict[str, Any] = {
                "stderr": subprocess.PIPE,
                "stdin": subprocess.DEVNULL,
                "cwd": working_directory,
                "universal_newlines": True,
                "timeout": timeout,
                "env": dict(os.environ, AZURE_CORE_NO_COLOR="true"),
            }
            return subprocess.check_output(args, **kwargs)
        except subprocess.CalledProcessError as ex:
            # non-zero return from shell
            # Fallback check in case the executable is not found while executing subprocess.
            if ex.returncode == 127 or ex.stderr.startswith("'az' is not recognized"):
>               raise CredentialUnavailableError(message=CLI_NOT_FOUND) from ex
E               azure.identity._exceptions.CredentialUnavailableError: Azure CLI not found on path

D:\Anaconda3\envs\sr\lib\site-packages\azure\identity\_credentials\azure_cli.py:239: CredentialUnavailableError
------------------------------ Captured log call ------------------------------
WARNING  azure.identity._internal.decorators:decorators.py:43 AzureCliCredential.get_token_info failed: Azure CLI not found on path
WARNING  azure.identity._internal.decorators:decorators.py:43 AzureCliCredential.get_token_info failed: Azure CLI not found on path
WARNING  azure.identity._internal.decorators:decorators.py:43 AzureCliCredential.get_token_info failed: Azure CLI not found on path
WARNING  azure.identity._internal.decorators:decorators.py:43 AzureCliCredential.get_token_info failed: Azure CLI not found on path
=========================== short test summary info ===========================
FAILED test_azure.py::test_azure - azure.identity._exceptions.CredentialUnava...
======================== 1 failed in 88.11s (0:01:28) =========================

@github-actions github-actions bot added needs-team-attention Workflow: This issue needs attention from Azure service team or SDK team and removed needs-author-feedback Workflow: More information is needed from author to address the issue. labels Oct 28, 2024
@pvaneck
Copy link
Member

pvaneck commented Oct 31, 2024

If you were to have the following code in a separate python script, then use the "Run and Debug" feature in VSCode, does it yield the same error?

import subprocess
import os

print(os.environ['PATH'])

args = ["cmd", "/c", "az account --help"]
kwargs = {
    "universal_newlines": True,
    "env": dict(os.environ, AZURE_CORE_NO_COLOR="true"),
}
try:
    output = subprocess.check_output(args, **kwargs)
    print(output)
except subprocess.CalledProcessError as e:
    print(e)

If so, does removing the "cmd" and "/c" entries in the args list help at all? Would also be curious to see how the output from print(os.environ['PATH']) differs at runtime.

I'm not super familiar with all the VS Code mechanisms like the Debug Console vs Integrated Terminal, but seems like creating/updating a launch.json file could also be something to look into. Perhaps something like this might work:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python Debugger: Current File",
            "type": "debugpy",
            "request": "launch",
            "program": "${file}",
            "console": "integratedTerminal",
            "purpose": [ "debug-test" ],
            "env": {
                "PATH": "${env:PATH};C:\\path\\to\\azure\\cli"
            }
        }
    ]
}

@xiangyan99 xiangyan99 added the needs-author-feedback Workflow: More information is needed from author to address the issue. label Nov 4, 2024
@github-actions github-actions bot removed the needs-team-attention Workflow: This issue needs attention from Azure service team or SDK team label Nov 4, 2024
Copy link

github-actions bot commented Nov 4, 2024

Hi @lovettchris. Thank you for opening this issue and giving us the opportunity to assist. To help our team better understand your issue and the details of your scenario please provide a response to the question asked above or the information requested above. This will help us more accurately address your issue.

@lovettchris
Copy link
Author

Your sample code above works fine in the debugger - the cmd runs with the correct path and az command completes. I used "az account show" to be sure it fetches something from azure that requires authentication. --help is not enough.

I also tested this in the debugger and it runs fine:

import os
import subprocess


def find_az_command():
    path = os.environ['PATH']
    for p in path.split(os.pathsep):
        if os.path.exists(os.path.join(p, 'az.cmd')):
            return os.path.join(p, 'az.cmd')
        if os.path.exists(os.path.join(p, 'az')):
            return os.path.join(p, 'az')
    print("ERROR: az command not found in PATH")
    return None


def get_account_details():
    az = find_az_command()
    args = [az, "account", "show"]
    kwargs = {
        "universal_newlines": True,
        "env": dict(os.environ, AZURE_CORE_NO_COLOR="true"),
    }
    try:
        output = subprocess.check_output(args, **kwargs)
        return output
    except Exception as e:
        return str(e)


def test_account_details():
    details = get_account_details()
    print(details)

And debugging this as a pytest in the debugger also succeeds, so not sure why my original test_azure function fails in the debugger during pytest.

@github-actions github-actions bot added needs-team-attention Workflow: This issue needs attention from Azure service team or SDK team and removed needs-author-feedback Workflow: More information is needed from author to address the issue. labels Nov 6, 2024
@pvaneck
Copy link
Member

pvaneck commented Nov 19, 2024

Sorry for the delay in response. I'm still uncertain of what environment conditions cause this. However, I just noticed in the traceback, that this line passes:

       if shutil.which(EXECUTABLE_NAME) is None:
            raise CredentialUnavailableError(message=CLI_NOT_FOUND) 

This means shutil.which is able to find and produce the path for the az executable, but the subprocess which is supposed to have the same PATH is not able to find it. I am testing out just using the full path of the executable found from shutil.which, and just calling that directly. I think this may resolve the issue you are encountering here.

@xiangyan99 xiangyan99 added the needs-author-feedback Workflow: More information is needed from author to address the issue. label Jan 15, 2025
@github-actions github-actions bot removed the needs-team-attention Workflow: This issue needs attention from Azure service team or SDK team label Jan 15, 2025
Copy link

Hi @lovettchris. Thank you for opening this issue and giving us the opportunity to assist. To help our team better understand your issue and the details of your scenario please provide a response to the question asked above or the information requested above. This will help us more accurately address your issue.

@lovettchris
Copy link
Author

Thanks I'll give that fix a try when it's in, or if there's an easy way to try this fix let me know.

@github-actions github-actions bot added needs-team-attention Workflow: This issue needs attention from Azure service team or SDK team and removed needs-author-feedback Workflow: More information is needed from author to address the issue. labels Jan 16, 2025
@pvaneck
Copy link
Member

pvaneck commented Feb 12, 2025

I believe this should be resolved with the latest release of azure0identity which includes the aforementioned change: #38606

@pvaneck pvaneck added the issue-addressed Workflow: The Azure SDK team believes it to be addressed and ready to close. label Feb 12, 2025
@github-actions github-actions bot removed the needs-team-attention Workflow: This issue needs attention from Azure service team or SDK team label Feb 12, 2025
Copy link

Hi @lovettchris. Thank you for opening this issue and giving us the opportunity to assist. We believe that this has been addressed. If you feel that further discussion is needed, please add a comment with the text "/unresolve" to remove the "issue-addressed" label and continue the conversation.

@pvaneck pvaneck moved this from Untriaged to Done in Azure Identity SDK Improvements Feb 14, 2025
@pvaneck pvaneck closed this as completed by moving to Done in Azure Identity SDK Improvements Feb 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Azure.Identity Client This issue points to a problem in the data-plane of the library. customer-reported Issues that are reported by GitHub users external to the Azure organization. issue-addressed Workflow: The Azure SDK team believes it to be addressed and ready to close. question The issue doesn't require a change to the product in order to be resolved. Most issues start as that
Projects
Development

No branches or pull requests

3 participants