1# -------------------------------------------------------------------------------------------- 2# Copyright (c) Microsoft Corporation. All rights reserved. 3# Licensed under the MIT License. See License.txt in the project root for license information. 4# -------------------------------------------------------------------------------------------- 5 6# pylint: disable=too-few-public-methods,no-self-use,too-many-locals,line-too-long,unused-argument 7 8import errno 9try: 10 import msvcrt 11 from ._vt_helper import enable_vt_mode 12except ImportError: 13 # Not supported for Linux machines. 14 pass 15import os 16import platform 17import shlex 18import signal 19import sys 20import threading 21import time 22try: 23 import termios 24 import tty 25except ImportError: 26 # Not supported for Windows machines. 27 pass 28import websocket 29import yaml 30from knack.log import get_logger 31from knack.prompting import prompt_pass, prompt, NoTTYException 32from knack.util import CLIError 33from azure.mgmt.containerinstance.models import (AzureFileVolume, Container, ContainerGroup, ContainerGroupNetworkProtocol, 34 ContainerPort, ImageRegistryCredential, IpAddress, Port, ResourceRequests, 35 ResourceRequirements, Volume, VolumeMount, ContainerExecRequest, ContainerExecRequestTerminalSize, 36 GitRepoVolume, LogAnalytics, ContainerGroupDiagnostics, ContainerGroupSubnetId, 37 ContainerGroupIpAddressType, ResourceIdentityType, ContainerGroupIdentity) 38from azure.cli.core.util import sdk_no_wait 39from azure.cli.core.azclierror import RequiredArgumentMissingError 40from ._client_factory import (cf_container_groups, cf_container, cf_log_analytics_workspace, 41 cf_log_analytics_workspace_shared_keys, cf_resource, cf_network, cf_msi) 42 43logger = get_logger(__name__) 44WINDOWS_NAME = 'Windows' 45SERVER_DELIMITER = '.' 46ACR_SERVER_DELIMITER = '.azurecr.io' 47AZURE_FILE_VOLUME_NAME = 'azurefile' 48SECRETS_VOLUME_NAME = 'secrets' 49GITREPO_VOLUME_NAME = 'gitrepo' 50MSI_LOCAL_ID = '[system]' 51 52 53def list_containers(client, resource_group_name=None): 54 """List all container groups in a resource group. """ 55 if resource_group_name is None: 56 return client.list() 57 return client.list_by_resource_group(resource_group_name) 58 59 60def get_container(client, resource_group_name, name): 61 """Show details of a container group. """ 62 return client.get(resource_group_name, name) 63 64 65def delete_container(client, resource_group_name, name, **kwargs): 66 """Delete a container group. """ 67 return client.begin_delete(resource_group_name, name) 68 69 70# pylint: disable=too-many-statements 71def create_container(cmd, 72 resource_group_name, 73 name=None, 74 image=None, 75 location=None, 76 cpu=1, 77 memory=1.5, 78 restart_policy='Always', 79 ports=None, 80 protocol=None, 81 os_type='Linux', 82 ip_address=None, 83 dns_name_label=None, 84 command_line=None, 85 environment_variables=None, 86 secure_environment_variables=None, 87 registry_login_server=None, 88 registry_username=None, 89 registry_password=None, 90 azure_file_volume_share_name=None, 91 azure_file_volume_account_name=None, 92 azure_file_volume_account_key=None, 93 azure_file_volume_mount_path=None, 94 log_analytics_workspace=None, 95 log_analytics_workspace_key=None, 96 vnet=None, 97 vnet_name=None, 98 vnet_address_prefix='10.0.0.0/16', 99 subnet=None, 100 subnet_address_prefix='10.0.0.0/24', 101 gitrepo_url=None, 102 gitrepo_dir='.', 103 gitrepo_revision=None, 104 gitrepo_mount_path=None, 105 secrets=None, 106 secrets_mount_path=None, 107 file=None, 108 assign_identity=None, 109 identity_scope=None, 110 identity_role='Contributor', 111 no_wait=False, 112 acr_identity=None): 113 """Create a container group. """ 114 if file: 115 return _create_update_from_file(cmd.cli_ctx, resource_group_name, name, location, file, no_wait) 116 117 if not name: 118 raise CLIError("error: the --name/-n argument is required unless specified with a passed in file.") 119 120 if not image: 121 raise CLIError("error: the --image argument is required unless specified with a passed in file.") 122 123 ports = ports or [80] 124 protocol = protocol or ContainerGroupNetworkProtocol.tcp 125 126 container_resource_requirements = _create_resource_requirements(cpu=cpu, memory=memory) 127 128 image_registry_credentials = _create_image_registry_credentials(cmd=cmd, 129 resource_group_name=resource_group_name, 130 registry_login_server=registry_login_server, 131 registry_username=registry_username, 132 registry_password=registry_password, 133 image=image, 134 identity=acr_identity) 135 136 command = shlex.split(command_line) if command_line else None 137 138 volumes = [] 139 mounts = [] 140 141 azure_file_volume = _create_azure_file_volume(azure_file_volume_share_name=azure_file_volume_share_name, 142 azure_file_volume_account_name=azure_file_volume_account_name, 143 azure_file_volume_account_key=azure_file_volume_account_key) 144 azure_file_volume_mount = _create_azure_file_volume_mount(azure_file_volume=azure_file_volume, 145 azure_file_volume_mount_path=azure_file_volume_mount_path) 146 147 if azure_file_volume: 148 volumes.append(azure_file_volume) 149 mounts.append(azure_file_volume_mount) 150 151 secrets_volume = _create_secrets_volume(secrets) 152 secrets_volume_mount = _create_secrets_volume_mount(secrets_volume=secrets_volume, 153 secrets_mount_path=secrets_mount_path) 154 155 if secrets_volume: 156 volumes.append(secrets_volume) 157 mounts.append(secrets_volume_mount) 158 159 diagnostics = None 160 tags = {} 161 if log_analytics_workspace and log_analytics_workspace_key: 162 log_analytics = LogAnalytics( 163 workspace_id=log_analytics_workspace, workspace_key=log_analytics_workspace_key) 164 165 diagnostics = ContainerGroupDiagnostics( 166 log_analytics=log_analytics 167 ) 168 elif log_analytics_workspace and not log_analytics_workspace_key: 169 diagnostics, tags = _get_diagnostics_from_workspace( 170 cmd.cli_ctx, log_analytics_workspace) 171 if not diagnostics: 172 raise CLIError('Log Analytics workspace "' + log_analytics_workspace + '" not found.') 173 elif not log_analytics_workspace and log_analytics_workspace_key: 174 raise CLIError('"--log-analytics-workspace-key" requires "--log-analytics-workspace".') 175 176 gitrepo_volume = _create_gitrepo_volume(gitrepo_url=gitrepo_url, gitrepo_dir=gitrepo_dir, gitrepo_revision=gitrepo_revision) 177 gitrepo_volume_mount = _create_gitrepo_volume_mount(gitrepo_volume=gitrepo_volume, gitrepo_mount_path=gitrepo_mount_path) 178 179 if gitrepo_volume: 180 volumes.append(gitrepo_volume) 181 mounts.append(gitrepo_volume_mount) 182 183 # Concatenate secure and standard environment variables 184 if environment_variables and secure_environment_variables: 185 environment_variables = environment_variables + secure_environment_variables 186 else: 187 environment_variables = environment_variables or secure_environment_variables 188 189 identity = None 190 if assign_identity is not None: 191 identity = _build_identities_info(assign_identity) 192 193 # Set up VNET and subnet if needed 194 subnet_id = None 195 cgroup_subnet = None 196 if subnet: 197 subnet_id = _get_subnet_id(cmd, location, resource_group_name, vnet, vnet_address_prefix, subnet, subnet_address_prefix) 198 cgroup_subnet = [ContainerGroupSubnetId(id=subnet_id)] 199 200 cgroup_ip_address = _create_ip_address(ip_address, ports, protocol, dns_name_label, subnet_id) 201 202 container = Container(name=name, 203 image=image, 204 resources=container_resource_requirements, 205 command=command, 206 ports=[ContainerPort( 207 port=p, protocol=protocol) for p in ports] if cgroup_ip_address else None, 208 environment_variables=environment_variables, 209 volume_mounts=mounts or None) 210 211 cgroup = ContainerGroup(location=location, 212 identity=identity, 213 containers=[container], 214 os_type=os_type, 215 restart_policy=restart_policy, 216 ip_address=cgroup_ip_address, 217 image_registry_credentials=image_registry_credentials, 218 volumes=volumes or None, 219 subnet_ids=cgroup_subnet, 220 diagnostics=diagnostics, 221 tags=tags) 222 223 container_group_client = cf_container_groups(cmd.cli_ctx) 224 225 lro = sdk_no_wait(no_wait, container_group_client.begin_create_or_update, resource_group_name, 226 name, cgroup) 227 228 if assign_identity is not None and identity_scope: 229 from azure.cli.core.commands.arm import assign_identity 230 cg = container_group_client.get(resource_group_name, name) 231 assign_identity(cmd.cli_ctx, lambda: cg, lambda cg: cg, identity_role, identity_scope) 232 233 return lro 234 235 236def _build_identities_info(identities): 237 identities = identities or [] 238 identity_type = ResourceIdentityType.none 239 if not identities or MSI_LOCAL_ID in identities: 240 identity_type = ResourceIdentityType.system_assigned 241 external_identities = [x for x in identities if x != MSI_LOCAL_ID] 242 if external_identities and identity_type == ResourceIdentityType.system_assigned: 243 identity_type = ResourceIdentityType.system_assigned_user_assigned 244 elif external_identities: 245 identity_type = ResourceIdentityType.user_assigned 246 identity = ContainerGroupIdentity(type=identity_type) 247 if external_identities: 248 identity.user_assigned_identities = {e: {} for e in external_identities} 249 return identity 250 251 252def _get_resource(client, resource_group_name, *subresources): 253 from azure.core.exceptions import HttpResponseError 254 try: 255 resource = client.get(resource_group_name, *subresources) 256 return resource 257 except HttpResponseError as ex: 258 if ex.error.code == "NotFound" or ex.error.code == "ResourceNotFound": 259 return None 260 raise 261 262 263def _get_subnet_id(cmd, location, resource_group_name, vnet, vnet_address_prefix, subnet, subnet_address_prefix): 264 from azure.cli.core.profiles import ResourceType 265 from msrestazure.tools import parse_resource_id, is_valid_resource_id 266 267 aci_delegation_service_name = "Microsoft.ContainerInstance/containerGroups" 268 Delegation = cmd.get_models('Delegation', resource_type=ResourceType.MGMT_NETWORK) 269 aci_delegation = Delegation( 270 name=aci_delegation_service_name, 271 service_name=aci_delegation_service_name 272 ) 273 274 ncf = cf_network(cmd.cli_ctx) 275 276 vnet_name = vnet 277 subnet_name = subnet 278 if is_valid_resource_id(subnet): 279 parsed_subnet_id = parse_resource_id(subnet) 280 subnet_name = parsed_subnet_id['resource_name'] 281 vnet_name = parsed_subnet_id['name'] 282 resource_group_name = parsed_subnet_id['resource_group'] 283 elif is_valid_resource_id(vnet): 284 parsed_vnet_id = parse_resource_id(vnet) 285 vnet_name = parsed_vnet_id['resource_name'] 286 resource_group_name = parsed_vnet_id['resource_group'] 287 288 subnet = _get_resource(ncf.subnets, resource_group_name, vnet_name, subnet_name) 289 # For an existing subnet, validate and add delegation if needed 290 if subnet: 291 logger.info('Using existing subnet "%s" in resource group "%s"', subnet.name, resource_group_name) 292 for sal in (subnet.service_association_links or []): 293 if sal.linked_resource_type != aci_delegation_service_name: 294 raise CLIError("Can not use subnet with existing service association links other than {}.".format(aci_delegation_service_name)) 295 296 if not subnet.delegations: 297 logger.info('Adding ACI delegation to the existing subnet.') 298 subnet.delegations = [aci_delegation] 299 subnet = ncf.subnets.begin_create_or_update(resource_group_name, vnet_name, subnet_name, subnet).result() 300 else: 301 for delegation in subnet.delegations: 302 if delegation.service_name != aci_delegation_service_name: 303 raise CLIError("Can not use subnet with existing delegations other than {}".format(aci_delegation_service_name)) 304 305 # Create new subnet and Vnet if not exists 306 else: 307 Subnet, VirtualNetwork, AddressSpace = cmd.get_models('Subnet', 'VirtualNetwork', 308 'AddressSpace', resource_type=ResourceType.MGMT_NETWORK) 309 310 vnet = _get_resource(ncf.virtual_networks, resource_group_name, vnet_name) 311 if not vnet: 312 logger.info('Creating new vnet "%s" in resource group "%s"', vnet_name, resource_group_name) 313 ncf.virtual_networks.begin_create_or_update(resource_group_name, 314 vnet_name, 315 VirtualNetwork(name=vnet_name, 316 location=location, 317 address_space=AddressSpace(address_prefixes=[vnet_address_prefix]))) 318 subnet = Subnet( 319 name=subnet_name, 320 location=location, 321 address_prefix=subnet_address_prefix, 322 delegations=[aci_delegation]) 323 324 logger.info('Creating new subnet "%s" in resource group "%s"', subnet_name, resource_group_name) 325 subnet = ncf.subnets.begin_create_or_update(resource_group_name, vnet_name, subnet_name, subnet).result() 326 327 return subnet.id 328 329 330def _get_diagnostics_from_workspace(cli_ctx, log_analytics_workspace): 331 from msrestazure.tools import parse_resource_id 332 log_analytics_workspace_client = cf_log_analytics_workspace(cli_ctx) 333 log_analytics_workspace_shared_keys_client = cf_log_analytics_workspace_shared_keys(cli_ctx) 334 335 for workspace in log_analytics_workspace_client.list(): 336 if log_analytics_workspace in (workspace.name, workspace.customer_id): 337 keys = log_analytics_workspace_shared_keys_client.get_shared_keys( 338 parse_resource_id(workspace.id)['resource_group'], workspace.name) 339 340 log_analytics = LogAnalytics( 341 workspace_id=workspace.customer_id, workspace_key=keys.primary_shared_key) 342 343 diagnostics = ContainerGroupDiagnostics( 344 log_analytics=log_analytics) 345 346 return (diagnostics, {'oms-resource-link': workspace.id}) 347 348 return None, {} 349 350 351# pylint: disable=unsupported-assignment-operation,protected-access 352def _create_update_from_file(cli_ctx, resource_group_name, name, location, file, no_wait): 353 resource_client = cf_resource(cli_ctx) 354 container_group_client = cf_container_groups(cli_ctx) 355 cg_defintion = None 356 357 try: 358 with open(file, 'r') as f: 359 cg_defintion = yaml.safe_load(f) 360 except OSError: # FileNotFoundError introduced in Python 3 361 raise CLIError("No such file or directory: " + file) 362 except yaml.YAMLError as e: 363 raise CLIError("Error while parsing yaml file:\n\n" + str(e)) 364 365 # Validate names match if both are provided 366 if name and cg_defintion.get('name', None): 367 if name != cg_defintion.get('name', None): 368 raise CLIError("The name parameter and name from yaml definition must match.") 369 else: 370 # Validate at least one name is provided 371 name = name or cg_defintion.get('name', None) 372 if cg_defintion.get('name', None) is None and not name: 373 raise CLIError("The name of the container group is required") 374 375 cg_defintion['name'] = name 376 377 if cg_defintion.get('location'): 378 location = cg_defintion.get('location') 379 cg_defintion['location'] = location 380 381 api_version = cg_defintion.get('apiVersion', None) or container_group_client._config.api_version 382 383 return sdk_no_wait(no_wait, 384 resource_client.resources.begin_create_or_update, 385 resource_group_name, 386 "Microsoft.ContainerInstance", 387 '', 388 "containerGroups", 389 name, 390 api_version, 391 cg_defintion) 392 393 394# pylint: disable=inconsistent-return-statements 395def _create_resource_requirements(cpu, memory): 396 """Create resource requirements. """ 397 if cpu or memory: 398 container_resource_requests = ResourceRequests(memory_in_gb=memory, cpu=cpu) 399 return ResourceRequirements(requests=container_resource_requests) 400 401 402def _create_image_registry_credentials(cmd, resource_group_name, registry_login_server, registry_username, registry_password, image, identity): 403 from msrestazure.tools import is_valid_resource_id 404 image_registry_credentials = None 405 if registry_login_server: 406 if not registry_username: 407 raise RequiredArgumentMissingError('Please specify --registry-username in order to use custom image registry.') 408 if not registry_password: 409 try: 410 registry_password = prompt_pass(msg='Image registry password: ') 411 except NoTTYException: 412 raise RequiredArgumentMissingError('Please specify --registry-password in order to use custom image registry.') 413 image_registry_credentials = [ImageRegistryCredential(server=registry_login_server, 414 username=registry_username, 415 password=registry_password)] 416 elif ACR_SERVER_DELIMITER in image.split("/")[0]: 417 acr_server = image.split("/")[0] if image.split("/") else None 418 if identity: 419 if not is_valid_resource_id(identity): 420 msi_client = cf_msi(cmd.cli_ctx) 421 identity = msi_client.user_assigned_identities.get(resource_group_name=resource_group_name, 422 resource_name=identity).id 423 if acr_server: 424 image_registry_credentials = [ImageRegistryCredential(server=acr_server, 425 username=registry_username, 426 password=registry_password, 427 identity=identity)] 428 else: 429 if not registry_username: 430 try: 431 registry_username = prompt(msg='Image registry username: ') 432 except NoTTYException: 433 raise RequiredArgumentMissingError('Please specify --registry-username in order to use Azure Container Registry.') 434 435 if not registry_password: 436 try: 437 registry_password = prompt_pass(msg='Image registry password: ') 438 except NoTTYException: 439 raise RequiredArgumentMissingError('Please specify --registry-password in order to use Azure Container Registry.') 440 if acr_server: 441 image_registry_credentials = [ImageRegistryCredential(server=acr_server, 442 username=registry_username, 443 password=registry_password)] 444 elif registry_username and registry_password and SERVER_DELIMITER in image.split("/")[0]: 445 login_server = image.split("/")[0] if image.split("/") else None 446 if login_server: 447 image_registry_credentials = [ImageRegistryCredential(server=login_server, 448 username=registry_username, 449 password=registry_password)] 450 else: 451 raise RequiredArgumentMissingError('Failed to parse login server from image name; please explicitly specify --registry-server.') 452 453 return image_registry_credentials 454 455 456def _create_azure_file_volume(azure_file_volume_share_name, azure_file_volume_account_name, azure_file_volume_account_key): 457 """Create Azure File volume. """ 458 azure_file_volume = None 459 if azure_file_volume_share_name: 460 if not azure_file_volume_account_name: 461 raise CLIError('Please specify --azure-file-volume-account-name in order to use Azure File volume.') 462 if not azure_file_volume_account_key: 463 try: 464 azure_file_volume_account_key = prompt_pass(msg='Azure File storage account key: ') 465 except NoTTYException: 466 raise CLIError('Please specify --azure-file-volume-account-key in order to use Azure File volume.') 467 468 azure_file_volume = AzureFileVolume(share_name=azure_file_volume_share_name, 469 storage_account_name=azure_file_volume_account_name, 470 storage_account_key=azure_file_volume_account_key) 471 472 return Volume(name=AZURE_FILE_VOLUME_NAME, azure_file=azure_file_volume) if azure_file_volume else None 473 474 475def _create_secrets_volume(secrets): 476 """Create secrets volume. """ 477 return Volume(name=SECRETS_VOLUME_NAME, secret=secrets) if secrets else None 478 479 480def _create_gitrepo_volume(gitrepo_url, gitrepo_dir, gitrepo_revision): 481 """Create Git Repo volume. """ 482 gitrepo_volume = GitRepoVolume(repository=gitrepo_url, directory=gitrepo_dir, revision=gitrepo_revision) 483 484 return Volume(name=GITREPO_VOLUME_NAME, git_repo=gitrepo_volume) if gitrepo_url else None 485 486 487# pylint: disable=inconsistent-return-statements 488def _create_azure_file_volume_mount(azure_file_volume, azure_file_volume_mount_path): 489 """Create Azure File volume mount. """ 490 if azure_file_volume_mount_path: 491 if not azure_file_volume: 492 raise CLIError('Please specify --azure-file-volume-share-name --azure-file-volume-account-name --azure-file-volume-account-key ' 493 'to enable Azure File volume mount.') 494 return VolumeMount(name=AZURE_FILE_VOLUME_NAME, mount_path=azure_file_volume_mount_path) 495 496 497def _create_secrets_volume_mount(secrets_volume, secrets_mount_path): 498 """Create secrets volume mount. """ 499 if secrets_volume: 500 if not secrets_mount_path: 501 raise CLIError('Please specify --secrets --secrets-mount-path ' 502 'to enable secrets volume mount.') 503 return VolumeMount(name=SECRETS_VOLUME_NAME, mount_path=secrets_mount_path) 504 505 506def _create_gitrepo_volume_mount(gitrepo_volume, gitrepo_mount_path): 507 """Create Git Repo volume mount. """ 508 if gitrepo_mount_path: 509 if not gitrepo_volume: 510 raise CLIError('Please specify --gitrepo-url (--gitrepo-dir --gitrepo-revision) ' 511 'to enable Git Repo volume mount.') 512 return VolumeMount(name=GITREPO_VOLUME_NAME, mount_path=gitrepo_mount_path) 513 514 515# pylint: disable=inconsistent-return-statements 516def _create_ip_address(ip_address, ports, protocol, dns_name_label, subnet_id): 517 """Create IP address. """ 518 if (ip_address and ip_address.lower() == 'public') or dns_name_label: 519 return IpAddress(ports=[Port(protocol=protocol, port=p) for p in ports], 520 dns_name_label=dns_name_label, type=ContainerGroupIpAddressType.public) 521 if subnet_id: 522 return IpAddress(ports=[Port(protocol=protocol, port=p) for p in ports], 523 type=ContainerGroupIpAddressType.private) 524 525 526# pylint: disable=inconsistent-return-statements 527def container_logs(cmd, resource_group_name, name, container_name=None, follow=False): 528 """Tail a container instance log. """ 529 container_client = cf_container(cmd.cli_ctx) 530 container_group_client = cf_container_groups(cmd.cli_ctx) 531 container_group = container_group_client.get(resource_group_name, name) 532 533 # If container name is not present, use the first container. 534 if container_name is None: 535 container_name = container_group.containers[0].name 536 537 if not follow: 538 log = container_client.list_logs(resource_group_name, name, container_name) 539 print(log.content) 540 else: 541 _start_streaming( 542 terminate_condition=_is_container_terminated, 543 terminate_condition_args=(container_group_client, resource_group_name, name, container_name), 544 shupdown_grace_period=5, 545 stream_target=_stream_logs, 546 stream_args=(container_client, resource_group_name, name, container_name, container_group.restart_policy)) 547 548 549# pylint: disable=protected-access 550def container_export(cmd, resource_group_name, name, file): 551 resource_client = cf_resource(cmd.cli_ctx) 552 container_group_client = cf_container_groups(cmd.cli_ctx) 553 resource = resource_client.resources.get(resource_group_name, 554 "Microsoft.ContainerInstance", 555 '', 556 "containerGroups", 557 name, 558 container_group_client._config.api_version).__dict__ 559 560 # Remove unwanted properites 561 resource['properties'].pop('instanceView', None) 562 resource.pop('sku', None) 563 resource.pop('id', None) 564 resource.pop('plan', None) 565 resource.pop('kind', None) 566 resource.pop('managed_by', None) 567 resource['properties'].pop('provisioningState', None) 568 569 # Correctly export the identity 570 try: 571 identity = resource['identity'].type 572 if identity != ResourceIdentityType.none: 573 resource['identity'] = resource['identity'].__dict__ 574 identity_entry = {'type': resource['identity']['type'].value} 575 if resource['identity']['user_assigned_identities']: 576 identity_entry['user_assigned_identities'] = {k: {} for k in resource['identity']['user_assigned_identities']} 577 resource['identity'] = identity_entry 578 except (KeyError, AttributeError): 579 resource.pop('indentity', None) 580 581 # Remove container instance views 582 for i in range(len(resource['properties']['containers'])): 583 resource['properties']['containers'][i]['properties'].pop('instanceView', None) 584 585 # Add the api version 586 resource['apiVersion'] = container_group_client._config.api_version 587 588 with open(file, 'w+') as f: 589 yaml.safe_dump(resource, f, default_flow_style=False) 590 591 592def container_exec(cmd, resource_group_name, name, exec_command, container_name=None): 593 """Start exec for a container. """ 594 595 container_client = cf_container(cmd.cli_ctx) 596 container_group_client = cf_container_groups(cmd.cli_ctx) 597 container_group = container_group_client.get(resource_group_name, name) 598 599 if container_name or container_name is None and len(container_group.containers) == 1: 600 # If only one container in container group, use that container. 601 if container_name is None: 602 container_name = container_group.containers[0].name 603 604 try: 605 terminalsize = os.get_terminal_size() 606 except OSError: 607 terminalsize = os.terminal_size((80, 24)) 608 terminal_size = ContainerExecRequestTerminalSize(rows=terminalsize.lines, cols=terminalsize.columns) 609 exec_request = ContainerExecRequest(command=exec_command, terminal_size=terminal_size) 610 611 execContainerResponse = container_client.execute_command(resource_group_name, name, container_name, exec_request) 612 613 if platform.system() is WINDOWS_NAME: 614 _start_exec_pipe_windows(execContainerResponse.web_socket_uri, execContainerResponse.password) 615 else: 616 _start_exec_pipe_linux(execContainerResponse.web_socket_uri, execContainerResponse.password) 617 618 else: 619 raise CLIError('--container-name required when container group has more than one container.') 620 621 622def _start_exec_pipe_windows(web_socket_uri, password): 623 import colorama 624 colorama.deinit() 625 enable_vt_mode() 626 buff = bytearray() 627 lock = threading.Lock() 628 629 def _on_ws_open_windows(ws): 630 ws.send(password) 631 readKeyboard = threading.Thread(target=_capture_stdin, args=[_getch_windows, buff, lock], daemon=True) 632 readKeyboard.start() 633 flushKeyboard = threading.Thread(target=_flush_stdin, args=[ws, buff, lock], daemon=True) 634 flushKeyboard.start() 635 ws = websocket.WebSocketApp(web_socket_uri, on_open=_on_ws_open_windows, on_message=_on_ws_msg) 636 # in windows, msvcrt.getch doesn't give us ctrl+C so we have to manually catch it with kb interrupt and send it over the socket 637 websocketRun = threading.Thread(target=ws.run_forever) 638 websocketRun.start() 639 while websocketRun.is_alive(): 640 try: 641 time.sleep(0.01) 642 except KeyboardInterrupt: 643 try: 644 ws.send(b'\x03') # CTRL-C character (ETX character) 645 finally: 646 pass 647 colorama.reinit() 648 649 650def _start_exec_pipe_linux(web_socket_uri, password): 651 stdin_fd = sys.stdin.fileno() 652 old_tty = termios.tcgetattr(stdin_fd) 653 old_winch_handler = signal.getsignal(signal.SIGWINCH) 654 tty.setraw(stdin_fd) 655 tty.setcbreak(stdin_fd) 656 buff = bytearray() 657 lock = threading.Lock() 658 659 def _on_ws_open_linux(ws): 660 ws.send(password) 661 readKeyboard = threading.Thread(target=_capture_stdin, args=[_getch_linux, buff, lock], daemon=True) 662 readKeyboard.start() 663 flushKeyboard = threading.Thread(target=_flush_stdin, args=[ws, buff, lock], daemon=True) 664 flushKeyboard.start() 665 ws = websocket.WebSocketApp(web_socket_uri, on_open=_on_ws_open_linux, on_message=_on_ws_msg) 666 ws.run_forever() 667 termios.tcsetattr(stdin_fd, termios.TCSADRAIN, old_tty) 668 signal.signal(signal.SIGWINCH, old_winch_handler) 669 670 671def _on_ws_msg(ws, msg): 672 if isinstance(msg, str): 673 msg = msg.encode() 674 sys.stdout.buffer.write(msg) 675 sys.stdout.flush() 676 677 678def _capture_stdin(getch_func, buff, lock): 679 # this method will fill up the buffer from one thread (using the lock) 680 while True: 681 try: 682 x = getch_func() 683 lock.acquire() 684 buff.extend(x) 685 lock.release() 686 finally: 687 if lock.locked(): 688 lock.release() 689 690 691def _flush_stdin(ws, buff, lock): 692 # this method will flush the buffer out to the websocket (using the lock) 693 while True: 694 time.sleep(0.01) 695 try: 696 if not buff: 697 continue 698 lock.acquire() 699 x = bytes(buff) 700 buff.clear() 701 lock.release() 702 ws.send(x, opcode=0x2) # OPCODE_BINARY = 0x2 703 except (OSError, IOError, websocket.WebSocketConnectionClosedException) as e: 704 if isinstance(e, websocket.WebSocketConnectionClosedException): 705 pass 706 elif e.errno == 9: # [Errno 9] Bad file descriptor 707 pass 708 elif e.args and e.args[0] == errno.EINTR: 709 pass 710 else: 711 raise 712 finally: 713 if lock.locked(): 714 lock.release() 715 716 717def _getch_windows(): 718 while not msvcrt.kbhit(): 719 time.sleep(0.01) 720 return msvcrt.getch() 721 722 723def _getch_linux(): 724 ch = sys.stdin.read(1) 725 return ch.encode() 726 727 728def attach_to_container(cmd, resource_group_name, name, container_name=None): 729 """Attach to a container. """ 730 container_client = cf_container(cmd.cli_ctx) 731 container_group_client = cf_container_groups(cmd.cli_ctx) 732 container_group = container_group_client.get(resource_group_name, name) 733 734 # If container name is not present, use the first container. 735 if container_name is None: 736 container_name = container_group.containers[0].name 737 738 _start_streaming( 739 terminate_condition=_is_container_terminated, 740 terminate_condition_args=(container_group_client, resource_group_name, name, container_name), 741 shupdown_grace_period=5, 742 stream_target=_stream_container_events_and_logs, 743 stream_args=(container_group_client, container_client, resource_group_name, name, container_name)) 744 745 746def _start_streaming(terminate_condition, terminate_condition_args, shupdown_grace_period, stream_target, stream_args): 747 """Start streaming for the stream target. """ 748 import colorama 749 colorama.init() 750 751 try: 752 t = threading.Thread(target=stream_target, args=stream_args) 753 t.daemon = True 754 t.start() 755 756 while not terminate_condition(*terminate_condition_args) and t.is_alive(): 757 time.sleep(10) 758 759 time.sleep(shupdown_grace_period) 760 761 finally: 762 colorama.deinit() 763 764 765def _stream_logs(client, resource_group_name, name, container_name, restart_policy): 766 """Stream logs for a container. """ 767 lastOutputLines = 0 768 while True: 769 log = client.list_logs(resource_group_name, name, container_name) 770 lines = log.content.split('\n') 771 currentOutputLines = len(lines) 772 773 # Should only happen when the container restarts. 774 if currentOutputLines < lastOutputLines and restart_policy != 'Never': 775 print("Warning: you're having '--restart-policy={}'; the container '{}' was just restarted; the tail of the current log might be missing. Exiting...".format(restart_policy, container_name)) 776 break 777 778 _move_console_cursor_up(lastOutputLines) 779 print(log.content) 780 781 lastOutputLines = currentOutputLines 782 time.sleep(2) 783 784 785def _stream_container_events_and_logs(container_group_client, container_client, resource_group_name, name, container_name): 786 """Stream container events and logs. """ 787 lastOutputLines = 0 788 lastContainerState = None 789 790 while True: 791 container_group, container = _find_container(container_group_client, resource_group_name, name, container_name) 792 793 container_state = 'Unknown' 794 if container.instance_view and container.instance_view.current_state and container.instance_view.current_state.state: 795 container_state = container.instance_view.current_state.state 796 797 _move_console_cursor_up(lastOutputLines) 798 if container_state != lastContainerState: 799 print("Container '{}' is in state '{}'...".format(container_name, container_state)) 800 801 currentOutputLines = 0 802 if container.instance_view and container.instance_view.events: 803 for event in sorted(container.instance_view.events, key=lambda e: e.last_timestamp): 804 print('(count: {}) (last timestamp: {}) {}'.format(event.count, event.last_timestamp, event.message)) 805 currentOutputLines += 1 806 807 lastOutputLines = currentOutputLines 808 lastContainerState = container_state 809 810 if container_state == 'Running': 811 print('\nStart streaming logs:') 812 break 813 814 time.sleep(2) 815 816 _stream_logs(container_client, resource_group_name, name, container_name, container_group.restart_policy) 817 818 819def _is_container_terminated(client, resource_group_name, name, container_name): 820 """Check if a container should be considered terminated. """ 821 container_group, container = _find_container(client, resource_group_name, name, container_name) 822 823 # If a container group is terminated, assume the container is also terminated. 824 if container_group.instance_view and container_group.instance_view.state: 825 if container_group.instance_view.state == 'Succeeded' or container_group.instance_view.state == 'Failed': 826 return True 827 828 # If the restart policy is Always, assume the container will be restarted. 829 if container_group.restart_policy: 830 if container_group.restart_policy == 'Always': 831 return False 832 833 # Only assume the container is terminated if its state is Terminated. 834 if container.instance_view and container.instance_view.current_state and container.instance_view.current_state.state == 'Terminated': 835 return True 836 837 return False 838 839 840def _find_container(client, resource_group_name, name, container_name): 841 """Find a container in a container group. """ 842 container_group = client.get(resource_group_name, name) 843 containers = [c for c in container_group.containers if c.name == container_name] 844 845 if len(containers) != 1: 846 raise CLIError("Found 0 or more than 1 container with name '{}'".format(container_name)) 847 848 return container_group, containers[0] 849 850 851def _move_console_cursor_up(lines): 852 """Move console cursor up. """ 853 if lines > 0: 854 # Use stdout.write to support Python 2 855 sys.stdout.write('\033[{}A\033[K\033[J'.format(lines)) 856 857 858def _gen_guid(): 859 import uuid 860 return uuid.uuid4() 861