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