Skip to content

Commit 8a7c84d

Browse files
committed
tentative changes testing
1 parent 097fd88 commit 8a7c84d

4 files changed

Lines changed: 126 additions & 0 deletions

File tree

src/azure-cli/azure/cli/command_modules/appservice/_help.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1917,6 +1917,28 @@
19171917
text: az webapp create-remote-connection --name MyWebApp --resource-group MyResourceGroup
19181918
"""
19191919

1920+
helps['webapp exec'] = """
1921+
type: command
1922+
short-summary: Execute a command in a web app container.
1923+
long-summary: >
1924+
Execute commands remotely in your web app container. The command runs asynchronously
1925+
and returns immediately without command output. Requires SCM Basic Auth Publishing
1926+
Credentials to be enabled.
1927+
examples:
1928+
- name: Execute a simple command on a web app
1929+
text: >
1930+
az webapp exec -g MyResourceGroup -n MyWebapp --cmd "ls -la /home"
1931+
- name: Execute a command in a specific working directory on a specific instance
1932+
text: >
1933+
az webapp exec -g MyResourceGroup -n MyWebapp --cmd "npm install" --cwd /home/site/wwwroot --instance MyInstanceId
1934+
- name: Execute a command on all instances
1935+
text: >
1936+
az webapp exec -g MyResourceGroup -n MyWebapp --cmd "systemctl restart nginx" --instance all
1937+
- name: Execute a command on a deployment slot
1938+
text: >
1939+
az webapp exec -g MyResourceGroup -n MyWebapp -s staging --cmd "python manage.py migrate"
1940+
"""
1941+
19201942
helps['webapp delete'] = """
19211943
type: command
19221944
short-summary: Delete a web app.

src/azure-cli/azure/cli/command_modules/appservice/_params.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1502,3 +1502,15 @@ def load_arguments(self, _):
15021502
c.argument('environment_name', help="Name of the environment of static site")
15031503
with self.argument_context('staticwebapp enterprise-edge') as c:
15041504
c.argument("no_register", help="Don't try to register the Microsoft.CDN provider. Registration can be done manually with: az provider register --wait --namespace Microsoft.CDN. For more details, please review the documentation available at https://go.microsoft.com/fwlink/?linkid=2184995 .", default=False)
1505+
with self.argument_context('webapp exec') as c:
1506+
c.argument('name', arg_type=webapp_name_arg_type, id_part=None)
1507+
c.argument('command', options_list=['--command', '--cmd'], help='The command to execute in the container.')
1508+
c.argument('mode',
1509+
help='Execution mode. \'execute\': Starts command execution and returns immediately without returning command output.',
1510+
arg_type=get_enum_type(['execute']), default='execute')
1511+
c.argument('working_directory', options_list=['--working-directory', '--cwd'],
1512+
help='Working directory for command execution')
1513+
c.argument('instance', options_list=['--instance', '-i'],
1514+
help='Webapp instance(s) to target. Specify a comma-separated list of instance IDs (use "az webapp list-instances" to get IDs) or "all" for all instances. Defaults to a random instance.')
1515+
c.argument('slot', options_list=['--slot', '-s'],
1516+
help='Name of the web app slot. Default to the production slot if not specified.')

src/azure-cli/azure/cli/command_modules/appservice/commands.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ def load_command_table(self, _):
125125
g.custom_command('create', 'create_webapp', exception_handler=ex_handler_factory(), validator=validate_vnet_integration)
126126
g.custom_command('up', 'webapp_up', exception_handler=ex_handler_factory(), validator=validate_webapp_up)
127127
g.custom_command('ssh', 'ssh_webapp', exception_handler=ex_handler_factory(), is_preview=True)
128+
g.custom_command('exec', 'webapp_exec', exception_handler=ex_handler_factory(), is_preview=True)
128129
g.custom_command('list', 'list_webapp', table_transformer=transform_web_list_output)
129130
g.custom_show_command('show', 'show_app', table_transformer=transform_web_output)
130131
g.custom_command('delete', 'delete_webapp')

src/azure-cli/azure/cli/command_modules/appservice/custom.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10947,3 +10947,94 @@ def _compute_checksum(input_bytes):
1094710947
logger.info("Computing the checksum of the file failed with exception:'%s'", ex)
1094810948

1094910949
return file_hash
10950+
10951+
10952+
def _execute_command_on_instance(scm_url, headers, cookies, command, working_directory=None):
10953+
from azure.cli.core.util import should_disable_connection_verify
10954+
import requests
10955+
10956+
# Build the exec endpoint URL
10957+
exec_url = f"{scm_url}/exec/execute"
10958+
10959+
# Build request body
10960+
body = {"Command": command}
10961+
if working_directory:
10962+
body["WorkingDirectory"] = working_directory
10963+
10964+
# Make the request
10965+
response = requests.post(
10966+
exec_url,
10967+
json=body,
10968+
headers=headers,
10969+
cookies=cookies,
10970+
verify=not should_disable_connection_verify()
10971+
)
10972+
10973+
# Handle response
10974+
if response.status_code == 202:
10975+
# Success - command accepted (no response body expected)
10976+
return None
10977+
else:
10978+
# Error - response contains JSON with ErrorMessage
10979+
raise CLIError(f"Command execution failed with status code {response.status_code}: {response.text}")
10980+
10981+
10982+
def webapp_exec(cmd, resource_group_name, name, command, mode='execute', working_directory=None, instance=None, slot=None):
10983+
# Validate Linux App
10984+
webapp = _generic_site_operation(cmd.cli_ctx, resource_group_name, name, 'get', slot)
10985+
if not webapp:
10986+
raise ResourceNotFoundError("Unable to find resource '{}' in ResourceGroup '{}'.".format(name, resource_group_name))
10987+
10988+
is_linux = webapp.reserved
10989+
if not is_linux:
10990+
raise ValidationError("Only Linux App Service Plans supported.")
10991+
10992+
# Allow mode 'execute' only
10993+
if mode.lower() != 'execute':
10994+
raise ValidationError("Only 'execute' mode is supported currently.")
10995+
10996+
# Validate command is provided if mode is 'execute'
10997+
if mode.lower() == 'execute' and not command:
10998+
raise ValidationError("Command is required.")
10999+
11000+
# Get scm site and authorization
11001+
scm_url = _get_scm_url(cmd, resource_group_name, name, slot)
11002+
headers = get_scm_site_headers(cmd.cli_ctx, name, resource_group_name, slot)
11003+
11004+
# Get target instances if specified
11005+
target_instances = []
11006+
if instance is not None:
11007+
# If command execution on all instances
11008+
if instance.lower() == "all":
11009+
instances = list_instances(cmd, resource_group_name, name, slot=slot)
11010+
target_instances = [i.name for i in instances]
11011+
if not target_instances:
11012+
raise ValidationError("No instances found for this web app.")
11013+
else:
11014+
# Validate listed instances
11015+
instances = list_instances(cmd, resource_group_name, name, slot=slot)
11016+
requested_instances = [i.strip() for i in instance.split(',')]
11017+
instance_names = set(i.name for i in instances)
11018+
invalid_instances = [inst for inst in requested_instances if inst not in instance_names]
11019+
if invalid_instances:
11020+
raise ValidationError("The following instances are not valid for this webapp: {}. Valid instances: {}".format(
11021+
', '.join(invalid_instances), ', '.join(sorted(instance_names))))
11022+
target_instances = requested_instances
11023+
11024+
# Execute command on target instances
11025+
results = []
11026+
for target in target_instances if target_instances else [None]:
11027+
cookies = {}
11028+
if target is not None:
11029+
cookies['ARRAffinity'] = target
11030+
11031+
try:
11032+
result = _execute_command_on_instance(scm_url, headers, cookies, command, working_directory)
11033+
results.append({'instance': target or 'default', 'status': 'success', 'result': result})
11034+
except CLIError as e:
11035+
results.append({'instance': target or 'default', 'status': 'failed', 'error': str(e)})
11036+
11037+
return results if len(results) > 1 else results[0] if results else None
11038+
11039+
11040+

0 commit comments

Comments
 (0)