|
50 | 50 | from azure.cli.core.azclierror import (InvalidArgumentValueError, MutuallyExclusiveArgumentError, ResourceNotFoundError, |
51 | 51 | RequiredArgumentMissingError, ValidationError, CLIInternalError, |
52 | 52 | UnclassifiedUserFault, AzureResponseError, AzureInternalError, |
53 | | - ArgumentUsageError, FileOperationError) |
| 53 | + ArgumentUsageError, FileOperationError, AzureConnectionError) |
54 | 54 |
|
55 | 55 | from .tunnel import TunnelServer |
56 | 56 |
|
@@ -12883,234 +12883,3 @@ def _compute_checksum(input_bytes): |
12883 | 12883 | logger.info("Computing the checksum of the file failed with exception:'%s'", ex) |
12884 | 12884 |
|
12885 | 12885 | return file_hash |
12886 | | - |
12887 | | - |
12888 | | -def _start_shell_session(scm_url, headers, cookies=None): |
12889 | | - import sys |
12890 | | - import ssl |
12891 | | - import platform |
12892 | | - import threading |
12893 | | - import time |
12894 | | - import websocket |
12895 | | - from azure.cli.core.util import should_disable_connection_verify |
12896 | | - |
12897 | | - ws_url = scm_url.replace('https://', 'wss://') + '/exec/shell' |
12898 | | - |
12899 | | - cookie_str = '; '.join(f'{k}={v}' for k, v in cookies.items()) if cookies else None |
12900 | | - # Respect user's SSL verification settings (e.g., self-signed certs on private stamps) |
12901 | | - sslopt = {'cert_reqs': ssl.CERT_NONE} if should_disable_connection_verify() else {} |
12902 | | - |
12903 | | - ws = websocket.create_connection( |
12904 | | - ws_url, |
12905 | | - header=headers, |
12906 | | - cookie=cookie_str, |
12907 | | - sslopt=sslopt, |
12908 | | - timeout=30 |
12909 | | - ) |
12910 | | - |
12911 | | - logger.info("Connected to %s", ws_url) |
12912 | | - print("Connected! Type commands. Ctrl+C twice to quit.\n") |
12913 | | - |
12914 | | - closed = threading.Event() |
12915 | | - |
12916 | | - # WebSocket → stdout |
12917 | | - def recv_loop(): |
12918 | | - try: |
12919 | | - while not closed.is_set(): |
12920 | | - opcode, data = ws.recv_data() |
12921 | | - if not data: |
12922 | | - break |
12923 | | - text = data.decode('utf-8', errors='replace') |
12924 | | - sys.stdout.write(text) |
12925 | | - sys.stdout.flush() |
12926 | | - except (websocket.WebSocketConnectionClosedException, OSError): |
12927 | | - pass |
12928 | | - finally: |
12929 | | - closed.set() |
12930 | | - |
12931 | | - recv_thread = threading.Thread(target=recv_loop, daemon=True) |
12932 | | - recv_thread.start() |
12933 | | - |
12934 | | - if platform.system() == 'Windows': |
12935 | | - _shell_input_loop_windows(ws, closed) |
12936 | | - else: |
12937 | | - _shell_input_loop_unix(ws, closed) |
12938 | | - |
12939 | | - closed.set() |
12940 | | - try: |
12941 | | - ws.close() |
12942 | | - except Exception: # pylint: disable=broad-except |
12943 | | - pass |
12944 | | - |
12945 | | - |
12946 | | -def _shell_input_loop_windows(ws, closed): |
12947 | | - import time |
12948 | | - import msvcrt |
12949 | | - import websocket as ws_module |
12950 | | - |
12951 | | - # Windows special key codes → ANSI escape sequences |
12952 | | - # Tab, Ctrl+D, Ctrl+L etc. work via the regular else branch (sent as-is) |
12953 | | - # TODO: F1-F12, Insert, PgUp/PgDown not yet mapped — silently dropped |
12954 | | - _WINDOWS_KEY_MAP = { |
12955 | | - 72: b'\x1b[A', # Up |
12956 | | - 80: b'\x1b[B', # Down |
12957 | | - 77: b'\x1b[C', # Right |
12958 | | - 75: b'\x1b[D', # Left |
12959 | | - 71: b'\x1b[H', # Home |
12960 | | - 79: b'\x1b[F', # End |
12961 | | - 83: b'\x1b[3~', # Delete |
12962 | | - } |
12963 | | - |
12964 | | - last_ctrl_c = 0 |
12965 | | - try: |
12966 | | - while not closed.is_set(): |
12967 | | - if not msvcrt.kbhit(): |
12968 | | - time.sleep(0.05) |
12969 | | - continue |
12970 | | - |
12971 | | - ch = msvcrt.getwch() |
12972 | | - if ch == '\x03': # Ctrl+C |
12973 | | - now = time.time() |
12974 | | - if now - last_ctrl_c < 2: |
12975 | | - break |
12976 | | - last_ctrl_c = now |
12977 | | - ws.send(b'\x03', opcode=ws_module.ABNF.OPCODE_BINARY) |
12978 | | - elif ch == '\r': # Enter |
12979 | | - ws.send(b'\n', opcode=ws_module.ABNF.OPCODE_BINARY) |
12980 | | - elif ch == '\x08': # Backspace |
12981 | | - ws.send(b'\x7f', opcode=ws_module.ABNF.OPCODE_BINARY) |
12982 | | - elif ch in ('\x00', '\xe0'): # Special key prefix |
12983 | | - code = ord(msvcrt.getwch()) |
12984 | | - escape = _WINDOWS_KEY_MAP.get(code) |
12985 | | - if escape: |
12986 | | - ws.send(escape, opcode=ws_module.ABNF.OPCODE_BINARY) |
12987 | | - else: |
12988 | | - ws.send(ch.encode('utf-8'), opcode=ws_module.ABNF.OPCODE_BINARY) |
12989 | | - except (ws_module.WebSocketConnectionClosedException, OSError): |
12990 | | - pass |
12991 | | - |
12992 | | - |
12993 | | -def _shell_input_loop_unix(ws, closed): |
12994 | | - import sys |
12995 | | - import os |
12996 | | - import tty |
12997 | | - import termios |
12998 | | - import time |
12999 | | - import websocket as ws_module |
13000 | | - |
13001 | | - fd = sys.stdin.fileno() |
13002 | | - old_settings = termios.tcgetattr(fd) |
13003 | | - last_ctrl_c = 0 |
13004 | | - try: |
13005 | | - tty.setraw(fd) |
13006 | | - while not closed.is_set(): |
13007 | | - ch = os.read(fd, 1) |
13008 | | - if not ch: |
13009 | | - break |
13010 | | - if ch == b'\x03': # Ctrl+C |
13011 | | - now = time.time() |
13012 | | - if now - last_ctrl_c < 2: |
13013 | | - break |
13014 | | - last_ctrl_c = now |
13015 | | - # Pass through everything (arrow keys already come as escape sequences) |
13016 | | - ws.send(ch, opcode=ws_module.ABNF.OPCODE_BINARY) |
13017 | | - except (ws_module.WebSocketConnectionClosedException, OSError): |
13018 | | - pass |
13019 | | - finally: |
13020 | | - termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) |
13021 | | - |
13022 | | - |
13023 | | -def _execute_command_on_instance(scm_url, headers, cookies, command, args=None, working_directory=None): |
13024 | | - from azure.cli.core.util import should_disable_connection_verify |
13025 | | - import requests |
13026 | | - |
13027 | | - exec_url = f"{scm_url}/exec/execute" |
13028 | | - |
13029 | | - body = {"Command": command} |
13030 | | - if args: |
13031 | | - body["Args"] = args |
13032 | | - if working_directory: |
13033 | | - body["WorkingDirectory"] = working_directory |
13034 | | - |
13035 | | - response = requests.post( |
13036 | | - exec_url, |
13037 | | - json=body, |
13038 | | - headers=headers, |
13039 | | - cookies=cookies, |
13040 | | - verify=not should_disable_connection_verify() |
13041 | | - ) |
13042 | | - |
13043 | | - if response.status_code == 202: |
13044 | | - return None |
13045 | | - else: |
13046 | | - raise CLIError(f"Command execution failed with status code {response.status_code}: {response.text}") |
13047 | | - |
13048 | | - |
13049 | | -def webapp_exec(cmd, resource_group_name, name, command=None, args=None, mode='execute', working_directory=None, instance=None, slot=None): |
13050 | | - # Validate Linux App |
13051 | | - webapp = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'get', slot) |
13052 | | - if not webapp: |
13053 | | - raise ResourceNotFoundError("Unable to find resource '{}' in ResourceGroup '{}'.".format(name, resource_group_name)) |
13054 | | - |
13055 | | - is_linux = webapp.reserved |
13056 | | - if not is_linux: |
13057 | | - raise ValidationError("Only Linux App Service Plans supported.") |
13058 | | - |
13059 | | - if mode.lower() == 'execute': |
13060 | | - if not command: |
13061 | | - raise ValidationError("Command is required for 'execute' mode.") |
13062 | | - elif mode.lower() == 'shell': |
13063 | | - if instance and instance.lower() == 'all': |
13064 | | - raise ValidationError("Cannot open shell on all instances. Specify a single instance or omit for a random one.") |
13065 | | - else: |
13066 | | - raise ValidationError("Invalid mode '{}'. Supported modes: execute, shell.".format(mode)) |
13067 | | - |
13068 | | - # Get scm site and authorization |
13069 | | - scm_url = _get_scm_url(cmd, resource_group_name, name, slot) |
13070 | | - headers = get_scm_site_headers(cmd.cli_ctx, name, resource_group_name, slot) |
13071 | | - |
13072 | | - # Shell mode — single interactive session |
13073 | | - if mode.lower() == 'shell': |
13074 | | - cookies = {} |
13075 | | - if instance: |
13076 | | - instances = list_instances(cmd, resource_group_name, name, slot=slot) |
13077 | | - instance_names = set(i.name for i in instances) |
13078 | | - if instance not in instance_names: |
13079 | | - raise ValidationError("Instance '{}' is not valid. Valid instances: {}".format( |
13080 | | - instance, ', '.join(sorted(instance_names)))) |
13081 | | - cookies['ARRAffinity'] = instance |
13082 | | - _start_shell_session(scm_url, headers, cookies) |
13083 | | - return None |
13084 | | - |
13085 | | - # Resolve target instances |
13086 | | - target_instances = [None] # default: no affinity cookie, random instance |
13087 | | - if instance is not None: |
13088 | | - if instance.lower() == "all": |
13089 | | - instances = list_instances(cmd, resource_group_name, name, slot=slot) |
13090 | | - target_instances = [i.name for i in instances] |
13091 | | - if not target_instances: |
13092 | | - raise ValidationError("No instances found for this web app.") |
13093 | | - else: |
13094 | | - instances = list_instances(cmd, resource_group_name, name, slot=slot) |
13095 | | - requested_instances = [i.strip() for i in instance.split(',')] |
13096 | | - instance_names = set(i.name for i in instances) |
13097 | | - invalid_instances = [inst for inst in requested_instances if inst not in instance_names] |
13098 | | - if invalid_instances: |
13099 | | - raise ValidationError("The following instances are not valid for this webapp: {}. Valid instances: {}".format( |
13100 | | - ', '.join(invalid_instances), ', '.join(sorted(instance_names)))) |
13101 | | - target_instances = requested_instances |
13102 | | - |
13103 | | - # Execute command on target instances |
13104 | | - results = [] |
13105 | | - for target in target_instances: |
13106 | | - cookies = {} |
13107 | | - if target is not None: |
13108 | | - cookies['ARRAffinity'] = target |
13109 | | - |
13110 | | - try: |
13111 | | - result = _execute_command_on_instance(scm_url, headers, cookies, command, args, working_directory) |
13112 | | - results.append({'instance': target or 'default', 'status': 'success', 'result': result}) |
13113 | | - except CLIError as e: |
13114 | | - results.append({'instance': target or 'default', 'status': 'failed', 'error': str(e)}) |
13115 | | - |
13116 | | - return results if len(results) > 1 else results[0] if results else None |
0 commit comments