2016-10-10 01:22:15 +03:00
|
|
|
# Copyright (c) Microsoft Corporation
|
|
|
|
#
|
|
|
|
# All rights reserved.
|
|
|
|
#
|
|
|
|
# MIT License
|
|
|
|
#
|
|
|
|
# Permission is hereby granted, free of charge, to any person obtaining a
|
|
|
|
# copy of this software and associated documentation files (the "Software"),
|
|
|
|
# to deal in the Software without restriction, including without limitation
|
|
|
|
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
|
|
# and/or sell copies of the Software, and to permit persons to whom the
|
|
|
|
# Software is furnished to do so, subject to the following conditions:
|
|
|
|
#
|
|
|
|
# The above copyright notice and this permission notice shall be included in
|
|
|
|
# all copies or substantial portions of the Software.
|
|
|
|
#
|
|
|
|
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
|
|
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
|
|
# DEALINGS IN THE SOFTWARE.
|
|
|
|
|
2016-10-31 18:45:05 +03:00
|
|
|
# compat imports
|
|
|
|
from __future__ import (
|
|
|
|
absolute_import, division, print_function, unicode_literals
|
|
|
|
)
|
2016-11-03 18:03:55 +03:00
|
|
|
from builtins import ( # noqa
|
|
|
|
bytes, dict, int, list, object, range, str, ascii, chr, hex, input,
|
|
|
|
next, oct, open, pow, round, super, filter, map, zip)
|
2016-10-10 01:22:15 +03:00
|
|
|
# stdlib imports
|
2017-07-01 09:50:21 +03:00
|
|
|
import collections
|
2016-10-10 01:22:15 +03:00
|
|
|
import datetime
|
2016-10-16 00:30:44 +03:00
|
|
|
import fnmatch
|
2016-10-20 07:10:40 +03:00
|
|
|
import getpass
|
2016-10-10 01:22:15 +03:00
|
|
|
import logging
|
|
|
|
try:
|
|
|
|
import pathlib2 as pathlib
|
2016-11-05 01:00:11 +03:00
|
|
|
except ImportError:
|
|
|
|
import pathlib
|
2016-10-10 01:22:15 +03:00
|
|
|
import os
|
2017-07-01 09:50:21 +03:00
|
|
|
import ssl
|
2016-10-10 07:01:11 +03:00
|
|
|
import tempfile
|
2016-10-10 01:22:15 +03:00
|
|
|
import time
|
|
|
|
# non-stdlib imports
|
|
|
|
import azure.batch.models as batchmodels
|
|
|
|
# local imports
|
2017-07-20 01:34:46 +03:00
|
|
|
from . import autoscale
|
2016-11-01 06:49:24 +03:00
|
|
|
from . import crypto
|
|
|
|
from . import data
|
2017-01-19 21:15:32 +03:00
|
|
|
from . import keyvault
|
2016-11-12 02:11:13 +03:00
|
|
|
from . import settings
|
2016-11-01 06:49:24 +03:00
|
|
|
from . import storage
|
|
|
|
from . import util
|
2017-06-26 23:20:49 +03:00
|
|
|
from .version import __version__
|
2016-10-10 01:22:15 +03:00
|
|
|
|
|
|
|
# create logger
|
|
|
|
logger = logging.getLogger(__name__)
|
2016-11-01 06:49:24 +03:00
|
|
|
util.setup_logger(logger)
|
2016-10-10 01:22:15 +03:00
|
|
|
# global defines
|
|
|
|
_MAX_REBOOT_RETRIES = 5
|
|
|
|
_SSH_TUNNEL_SCRIPT = 'ssh_docker_tunnel_shipyard.sh'
|
2016-10-10 07:01:11 +03:00
|
|
|
_GENERIC_DOCKER_TASK_PREFIX = 'dockertask-'
|
2017-03-01 23:45:32 +03:00
|
|
|
_RUN_ELEVATED = batchmodels.UserIdentity(
|
|
|
|
auto_user=batchmodels.AutoUserSpecification(
|
|
|
|
scope=batchmodels.AutoUserScope.pool,
|
|
|
|
elevation_level=batchmodels.ElevationLevel.admin,
|
|
|
|
)
|
|
|
|
)
|
2017-07-01 09:50:21 +03:00
|
|
|
NodeStateCountCollection = collections.namedtuple(
|
|
|
|
'NodeStateCountCollection', [
|
|
|
|
'creating',
|
|
|
|
'idle',
|
|
|
|
'leaving_pool',
|
|
|
|
'offline',
|
|
|
|
'preempted',
|
|
|
|
'rebooting',
|
|
|
|
'reimaging',
|
|
|
|
'running',
|
|
|
|
'start_task_failed',
|
|
|
|
'starting',
|
|
|
|
'unknown',
|
|
|
|
'unusable',
|
|
|
|
'waiting_for_start_task',
|
|
|
|
]
|
|
|
|
)
|
2016-10-10 07:01:11 +03:00
|
|
|
|
|
|
|
|
2017-03-10 01:38:16 +03:00
|
|
|
def get_batch_account(batch_mgmt_client, config):
|
|
|
|
# type: (azure.mgmt.batch.BatchManagementClient, dict) ->
|
|
|
|
# azure.mgmt.batch.models.BatchAccount
|
|
|
|
"""Get Batch account properties from ARM
|
|
|
|
:param azure.mgmt.batch.BatchManagementClient batch_mgmt_client:
|
|
|
|
batch management client
|
|
|
|
:param dict config: configuration dict
|
|
|
|
:rtype: azure.mgmt.batch.models.BatchAccount
|
|
|
|
:return: Batch account
|
|
|
|
"""
|
|
|
|
if batch_mgmt_client is None:
|
|
|
|
raise RuntimeError(
|
|
|
|
'Batch management client is invalid, please specify management '
|
|
|
|
'aad credentials')
|
|
|
|
bc = settings.credentials_batch(config)
|
|
|
|
return batch_mgmt_client.batch_account.get(
|
|
|
|
resource_group_name=bc.resource_group,
|
|
|
|
account_name=bc.account,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2016-11-30 01:58:52 +03:00
|
|
|
def list_node_agent_skus(batch_client):
|
|
|
|
# type: (batch.BatchServiceClient) -> None
|
|
|
|
"""List all node agent skus
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
"""
|
|
|
|
node_agent_skus = batch_client.account.list_node_agent_skus()
|
|
|
|
for sku in node_agent_skus:
|
|
|
|
for img in sku.verified_image_references:
|
|
|
|
logger.info(
|
|
|
|
'os_type={} publisher={} offer={} sku={} node_agent={}'.format(
|
|
|
|
sku.os_type, img.publisher, img.offer, img.sku, sku.id))
|
|
|
|
|
|
|
|
|
2016-10-20 07:10:40 +03:00
|
|
|
def add_certificate_to_account(batch_client, config, rm_pfxfile=False):
|
2016-11-30 01:58:52 +03:00
|
|
|
# type: (batch.BatchServiceClient, dict, bool) -> None
|
2016-10-20 07:10:40 +03:00
|
|
|
"""Adds a certificate to a Batch account
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
:param dict config: configuration dict
|
|
|
|
:param str sha1_cert_tp: sha1 thumbprint of pfx
|
|
|
|
:param bool rm_pfxfile: remove PFX file from local disk
|
|
|
|
"""
|
2016-11-11 20:19:04 +03:00
|
|
|
pfx = crypto.get_encryption_pfx_settings(config)
|
2016-10-20 07:10:40 +03:00
|
|
|
# first check if this cert exists
|
|
|
|
certs = batch_client.certificate.list()
|
|
|
|
for cert in certs:
|
2016-11-11 20:19:04 +03:00
|
|
|
if cert.thumbprint.lower() == pfx.sha1:
|
2016-10-20 07:10:40 +03:00
|
|
|
logger.error(
|
|
|
|
'cert with thumbprint {} already exists for account'.format(
|
2016-11-11 20:19:04 +03:00
|
|
|
pfx.sha1))
|
2016-10-20 07:10:40 +03:00
|
|
|
# remove pfxfile
|
|
|
|
if rm_pfxfile:
|
2016-11-11 20:19:04 +03:00
|
|
|
os.unlink(pfx.filename)
|
2016-10-20 07:10:40 +03:00
|
|
|
return
|
|
|
|
# add cert to account
|
2016-11-11 20:19:04 +03:00
|
|
|
if pfx.passphrase is None:
|
|
|
|
pfx.passphrase = getpass.getpass('Enter password for PFX: ')
|
2016-10-20 07:10:40 +03:00
|
|
|
logger.debug('adding pfx cert with thumbprint {} to account'.format(
|
2016-11-11 20:19:04 +03:00
|
|
|
pfx.sha1))
|
2017-01-05 21:20:13 +03:00
|
|
|
data = util.base64_encode_string(open(pfx.filename, 'rb').read())
|
2016-10-20 07:10:40 +03:00
|
|
|
batch_client.certificate.add(
|
|
|
|
certificate=batchmodels.CertificateAddParameter(
|
2016-11-11 20:19:04 +03:00
|
|
|
pfx.sha1, 'sha1', data,
|
2016-10-20 07:10:40 +03:00
|
|
|
certificate_format=batchmodels.CertificateFormat.pfx,
|
2016-11-11 20:19:04 +03:00
|
|
|
password=pfx.passphrase)
|
2016-10-20 07:10:40 +03:00
|
|
|
)
|
|
|
|
# remove pfxfile
|
|
|
|
if rm_pfxfile:
|
2016-11-11 20:19:04 +03:00
|
|
|
os.unlink(pfx.filename)
|
2016-10-20 07:10:40 +03:00
|
|
|
|
|
|
|
|
2016-10-27 07:39:00 +03:00
|
|
|
def list_certificates_in_account(batch_client):
|
2016-11-30 01:58:52 +03:00
|
|
|
# type: (batch.BatchServiceClient) -> None
|
2016-10-27 07:39:00 +03:00
|
|
|
"""List all certificates in a Batch account
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
"""
|
|
|
|
i = 0
|
|
|
|
certs = batch_client.certificate.list()
|
|
|
|
for cert in certs:
|
|
|
|
if cert.delete_certificate_error is not None:
|
|
|
|
ce = 'delete_error=(code={} msg={})'.format(
|
|
|
|
cert.delete_certificate_error.code,
|
|
|
|
cert.delete_certificate_error.message)
|
|
|
|
else:
|
|
|
|
ce = ''
|
|
|
|
logger.info('{}={} [state={}{}]'.format(
|
|
|
|
cert.thumbprint_algorithm, cert.thumbprint, cert.state, ce))
|
|
|
|
i += 1
|
|
|
|
if i == 0:
|
|
|
|
logger.error('no certificates found')
|
|
|
|
|
|
|
|
|
2016-10-20 07:10:40 +03:00
|
|
|
def del_certificate_from_account(batch_client, config):
|
2016-11-30 01:58:52 +03:00
|
|
|
# type: (batch.BatchServiceClient, dict) -> None
|
2016-10-20 07:10:40 +03:00
|
|
|
"""Delete a certificate from a Batch account
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
:param dict config: configuration dict
|
|
|
|
"""
|
2016-11-11 20:19:04 +03:00
|
|
|
pfx = crypto.get_encryption_pfx_settings(config)
|
|
|
|
batch_client.certificate.delete('sha1', pfx.sha1)
|
2016-10-20 07:10:40 +03:00
|
|
|
|
|
|
|
|
2016-10-10 01:22:15 +03:00
|
|
|
def _reboot_node(batch_client, pool_id, node_id, wait):
|
|
|
|
# type: (batch.BatchServiceClient, str, str, bool) -> None
|
|
|
|
"""Reboot a node in a pool
|
|
|
|
:param batch_client: The batch client to use.
|
2016-10-14 00:11:52 +03:00
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
2016-10-10 01:22:15 +03:00
|
|
|
:param str pool_id: pool id of node
|
|
|
|
:param str node_id: node id to delete
|
|
|
|
:param bool wait: wait for node to enter rebooting state
|
|
|
|
"""
|
2017-03-01 06:34:22 +03:00
|
|
|
logger.info('Rebooting node {} in pool {}'.format(node_id, pool_id))
|
2016-10-10 01:22:15 +03:00
|
|
|
batch_client.compute_node.reboot(
|
|
|
|
pool_id=pool_id,
|
|
|
|
node_id=node_id,
|
|
|
|
)
|
|
|
|
if wait:
|
|
|
|
logger.debug('waiting for node {} to enter rebooting state'.format(
|
|
|
|
node_id))
|
|
|
|
while True:
|
|
|
|
node = batch_client.compute_node.get(pool_id, node_id)
|
|
|
|
if node.state == batchmodels.ComputeNodeState.rebooting:
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
time.sleep(1)
|
|
|
|
|
|
|
|
|
2016-10-29 21:00:31 +03:00
|
|
|
def _retrieve_outputs_from_failed_nodes(batch_client, config, nodeid=None):
|
|
|
|
# type: (batch.BatchServiceClient, dict) -> None
|
|
|
|
"""Retrieve stdout/stderr from failed nodes
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
:param dict config: configuration dict
|
|
|
|
"""
|
2016-11-13 09:13:55 +03:00
|
|
|
pool_id = settings.pool_id(config)
|
2016-10-29 21:00:31 +03:00
|
|
|
if nodeid is None:
|
|
|
|
nodes = batch_client.compute_node.list(pool_id)
|
|
|
|
else:
|
|
|
|
nodes = [batch_client.compute_node.get(pool_id, nodeid)]
|
|
|
|
# for any node in state start task failed, retrieve the stdout and stderr
|
|
|
|
for node in nodes:
|
2017-03-01 23:45:32 +03:00
|
|
|
if node.state == batchmodels.ComputeNodeState.start_task_failed:
|
2016-11-13 09:13:55 +03:00
|
|
|
settings.set_auto_confirm(config, True)
|
2016-10-29 21:00:31 +03:00
|
|
|
get_all_files_via_node(
|
|
|
|
batch_client, config,
|
|
|
|
filespec='{},{}'.format(node.id, 'startup/std*.txt'))
|
2017-06-27 19:27:17 +03:00
|
|
|
try:
|
|
|
|
get_all_files_via_node(
|
|
|
|
batch_client, config,
|
|
|
|
filespec='{},{}'.format(node.id, 'startup/wd/cascade.log'))
|
|
|
|
except batchmodels.BatchErrorException:
|
|
|
|
pass
|
2016-10-29 21:00:31 +03:00
|
|
|
|
|
|
|
|
2016-10-24 19:12:24 +03:00
|
|
|
def _block_for_nodes_ready(
|
2016-10-30 11:33:15 +03:00
|
|
|
batch_client, config, stopping_states, end_states, pool_id,
|
|
|
|
reboot_on_failed):
|
2016-10-29 21:00:31 +03:00
|
|
|
# type: (batch.BatchServiceClient, dict,
|
2016-10-30 11:33:15 +03:00
|
|
|
# List[batchmodels.ComputeNodeState],
|
2016-10-29 21:00:31 +03:00
|
|
|
# List[batchmodels.ComputeNodeState], str,
|
|
|
|
# bool) -> List[batchmodels.ComputeNode]
|
2016-10-30 11:33:15 +03:00
|
|
|
"""Wait for pool to enter steady state and all nodes to enter stopping
|
|
|
|
states
|
2016-10-10 01:22:15 +03:00
|
|
|
:param batch_client: The batch client to use.
|
2016-10-14 00:11:52 +03:00
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
2016-10-29 21:00:31 +03:00
|
|
|
:param dict config: configuration dict
|
2016-10-30 11:33:15 +03:00
|
|
|
:param list stopping_states: list of node states to stop polling
|
|
|
|
:param list end_states: list of acceptable end states
|
2016-10-10 01:22:15 +03:00
|
|
|
:param str pool_id: pool id
|
|
|
|
:param bool reboot_on_failed: reboot node on failed start state
|
|
|
|
:rtype: list
|
|
|
|
:return: list of nodes
|
|
|
|
"""
|
|
|
|
logger.info(
|
|
|
|
'waiting for all nodes in pool {} to reach one of: {!r}'.format(
|
2016-10-30 11:33:15 +03:00
|
|
|
pool_id, stopping_states))
|
2016-10-10 01:22:15 +03:00
|
|
|
i = 0
|
|
|
|
reboot_map = {}
|
|
|
|
while True:
|
2017-05-13 00:40:06 +03:00
|
|
|
# refresh pool to ensure that there is no dedicated resize error
|
2016-10-10 01:22:15 +03:00
|
|
|
pool = batch_client.pool.get(pool_id)
|
2017-05-13 00:40:06 +03:00
|
|
|
if util.is_not_empty(pool.resize_errors):
|
2017-06-06 18:29:44 +03:00
|
|
|
fatal_resize_error = False
|
2017-05-13 00:40:06 +03:00
|
|
|
errors = []
|
|
|
|
for err in pool.resize_errors:
|
|
|
|
errors.append('code={} msg={}'.format(err.code, err.message))
|
2017-06-06 18:29:44 +03:00
|
|
|
if (err.code == 'AccountCoreQuotaReached' or
|
|
|
|
(err.code == 'AccountLowPriorityCoreQuotaReached' and
|
|
|
|
pool.target_dedicated_nodes == 0)):
|
|
|
|
fatal_resize_error = True
|
|
|
|
if fatal_resize_error:
|
2017-07-01 09:50:21 +03:00
|
|
|
list_nodes(batch_client, config)
|
2017-05-13 00:40:06 +03:00
|
|
|
raise RuntimeError(
|
|
|
|
'Fatal resize errors encountered for pool {}: {}'.format(
|
|
|
|
pool.id, os.linesep.join(errors)))
|
|
|
|
else:
|
|
|
|
logger.error(
|
|
|
|
'Resize errors encountered for pool {}: {}'.format(
|
|
|
|
pool.id, os.linesep.join(errors)))
|
2017-07-01 09:50:21 +03:00
|
|
|
# check pool allocation state
|
|
|
|
if pool.allocation_state == batchmodels.AllocationState.resizing:
|
|
|
|
nodes = []
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
nodes = list(batch_client.compute_node.list(pool.id))
|
|
|
|
except ssl.SSLError:
|
|
|
|
# SSL error happens sometimes on paging... this is probably
|
|
|
|
# a bug in the underlying msrest/msrestazure library that
|
|
|
|
# is reusing the SSL connection improperly
|
|
|
|
nodes = []
|
2016-10-29 21:00:31 +03:00
|
|
|
# check if any nodes are in start task failed state
|
2017-03-01 23:45:32 +03:00
|
|
|
if (any(node.state == batchmodels.ComputeNodeState.start_task_failed
|
2016-10-29 21:00:31 +03:00
|
|
|
for node in nodes)):
|
|
|
|
# attempt reboot if enabled for potentially transient errors
|
|
|
|
if reboot_on_failed:
|
|
|
|
for node in nodes:
|
|
|
|
if (node.state !=
|
2017-03-01 23:45:32 +03:00
|
|
|
batchmodels.ComputeNodeState.start_task_failed):
|
2016-10-29 21:00:31 +03:00
|
|
|
continue
|
2016-10-10 01:22:15 +03:00
|
|
|
if node.id not in reboot_map:
|
|
|
|
reboot_map[node.id] = 0
|
2016-10-29 21:00:31 +03:00
|
|
|
logger.error(
|
2016-11-07 02:07:33 +03:00
|
|
|
('Detected start task failure, attempting to '
|
|
|
|
'retrieve stdout/stderr for error diagnosis '
|
|
|
|
'from node {}').format(node.id))
|
2016-10-29 21:00:31 +03:00
|
|
|
_retrieve_outputs_from_failed_nodes(
|
|
|
|
batch_client, config, nodeid=node.id)
|
2016-10-10 01:22:15 +03:00
|
|
|
if reboot_map[node.id] > _MAX_REBOOT_RETRIES:
|
2016-10-29 21:00:31 +03:00
|
|
|
list_nodes(batch_client, config)
|
2016-10-10 01:22:15 +03:00
|
|
|
raise RuntimeError(
|
2016-10-29 21:00:31 +03:00
|
|
|
('Ran out of reboot retries for recovery. '
|
2016-11-07 02:07:33 +03:00
|
|
|
'Please inspect both the node status above and '
|
|
|
|
'stdout.txt/stderr.txt files within the '
|
|
|
|
'{}/{}/startup directory in the current working '
|
|
|
|
'directory if available. If this error '
|
2016-10-29 21:00:31 +03:00
|
|
|
'appears non-transient, please submit an '
|
|
|
|
'issue on GitHub').format(
|
|
|
|
pool.id, node.id))
|
2016-10-10 01:22:15 +03:00
|
|
|
_reboot_node(batch_client, pool.id, node.id, True)
|
|
|
|
reboot_map[node.id] += 1
|
2016-10-29 21:00:31 +03:00
|
|
|
# refresh node list to reflect rebooting states
|
2017-07-01 09:50:21 +03:00
|
|
|
try:
|
|
|
|
nodes = list(batch_client.compute_node.list(pool.id))
|
|
|
|
except ssl.SSLError:
|
|
|
|
nodes = []
|
2016-10-29 21:00:31 +03:00
|
|
|
else:
|
|
|
|
# fast path check for start task failures in non-reboot mode
|
|
|
|
logger.error(
|
2016-11-07 02:07:33 +03:00
|
|
|
'Detected start task failure, attempting to retrieve '
|
|
|
|
'stdout/stderr for error diagnosis from nodes')
|
2016-10-29 21:00:31 +03:00
|
|
|
_retrieve_outputs_from_failed_nodes(batch_client, config)
|
|
|
|
list_nodes(batch_client, config)
|
2016-10-24 19:12:24 +03:00
|
|
|
raise RuntimeError(
|
2016-11-07 02:07:33 +03:00
|
|
|
('Please inspect both the node status above and '
|
|
|
|
'stdout.txt/stderr.txt files within the '
|
|
|
|
'{}/<nodes>/startup directory in the current working '
|
|
|
|
'directory if available. If this error appears '
|
2016-10-29 21:00:31 +03:00
|
|
|
'non-transient, please submit an issue on '
|
|
|
|
'GitHub.').format(pool.id))
|
2017-05-13 00:40:06 +03:00
|
|
|
if (len(nodes) ==
|
|
|
|
(pool.target_dedicated_nodes +
|
|
|
|
pool.target_low_priority_nodes) and
|
2016-10-30 11:33:15 +03:00
|
|
|
all(node.state in stopping_states for node in nodes)):
|
|
|
|
if any(node.state not in end_states for node in nodes):
|
2016-10-29 21:00:31 +03:00
|
|
|
list_nodes(batch_client, config)
|
2016-10-10 01:22:15 +03:00
|
|
|
raise RuntimeError(
|
2016-11-07 02:07:33 +03:00
|
|
|
('Node(s) of pool {} not in {} state. Please inspect the '
|
|
|
|
'state of nodes in the pool above. If this appears to '
|
|
|
|
'be a transient error, please retry pool creation by '
|
|
|
|
'deleting and recreating the pool.').format(
|
2016-10-30 11:33:15 +03:00
|
|
|
pool.id, end_states))
|
2016-10-10 01:22:15 +03:00
|
|
|
else:
|
|
|
|
return nodes
|
|
|
|
i += 1
|
|
|
|
if i % 3 == 0:
|
|
|
|
i = 0
|
2017-05-13 00:40:06 +03:00
|
|
|
logger.debug(
|
|
|
|
('waiting for {} dedicated nodes and {} low priority nodes '
|
2017-07-14 19:58:16 +03:00
|
|
|
'of size {} to reach desired state in pool {} '
|
|
|
|
'[resize_timeout={} allocation_state={} '
|
|
|
|
'allocation_state_transition_time={}]').format(
|
2017-05-13 00:40:06 +03:00
|
|
|
pool.target_dedicated_nodes,
|
2017-07-01 09:50:21 +03:00
|
|
|
pool.target_low_priority_nodes,
|
2017-07-14 19:58:16 +03:00
|
|
|
pool.vm_size,
|
2017-07-01 09:50:21 +03:00
|
|
|
pool.id,
|
2017-07-14 19:58:16 +03:00
|
|
|
pool.resize_timeout,
|
|
|
|
pool.allocation_state,
|
|
|
|
pool.allocation_state_transition_time,
|
|
|
|
))
|
|
|
|
if len(nodes) <= 5:
|
2017-07-01 09:50:21 +03:00
|
|
|
for node in nodes:
|
|
|
|
logger.debug('{}: {}'.format(node.id, node.state))
|
|
|
|
else:
|
|
|
|
logger.debug(_node_state_counts(nodes))
|
2016-10-10 01:22:15 +03:00
|
|
|
time.sleep(10)
|
|
|
|
|
|
|
|
|
2017-07-01 09:50:21 +03:00
|
|
|
def _node_state_counts(nodes):
|
|
|
|
# type: (List[batchmodels.ComputeNode]) -> NodeStateCountCollection
|
|
|
|
"""Collate counts of various nodes
|
|
|
|
:param list nodes: list of nodes
|
|
|
|
:rtype: NodeStateCountCollection
|
|
|
|
:return: node state count collection
|
|
|
|
"""
|
|
|
|
node_states = [node.state for node in nodes]
|
|
|
|
return NodeStateCountCollection(
|
|
|
|
creating=node_states.count(batchmodels.ComputeNodeState.creating),
|
|
|
|
idle=node_states.count(batchmodels.ComputeNodeState.idle),
|
|
|
|
leaving_pool=node_states.count(
|
|
|
|
batchmodels.ComputeNodeState.leaving_pool),
|
|
|
|
offline=node_states.count(batchmodels.ComputeNodeState.offline),
|
|
|
|
preempted=node_states.count(batchmodels.ComputeNodeState.preempted),
|
|
|
|
rebooting=node_states.count(batchmodels.ComputeNodeState.rebooting),
|
|
|
|
reimaging=node_states.count(batchmodels.ComputeNodeState.reimaging),
|
|
|
|
running=node_states.count(batchmodels.ComputeNodeState.running),
|
|
|
|
start_task_failed=node_states.count(
|
|
|
|
batchmodels.ComputeNodeState.start_task_failed),
|
|
|
|
starting=node_states.count(batchmodels.ComputeNodeState.starting),
|
|
|
|
unknown=node_states.count(batchmodels.ComputeNodeState.unknown),
|
|
|
|
unusable=node_states.count(batchmodels.ComputeNodeState.unusable),
|
|
|
|
waiting_for_start_task=node_states.count(
|
|
|
|
batchmodels.ComputeNodeState.waiting_for_start_task),
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2016-10-30 11:33:15 +03:00
|
|
|
def wait_for_pool_ready(batch_client, config, pool_id, addl_end_states=None):
|
|
|
|
# type: (batch.BatchServiceClient, dict, str,
|
|
|
|
# List[batchmodels.ComputeNode]) -> List[batchmodels.ComputeNode]
|
|
|
|
"""Wait for pool to enter steady state and all nodes in end states
|
2016-10-24 19:12:24 +03:00
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
:param dict config: configuration dict
|
|
|
|
:param str pool_id: pool id
|
2016-10-30 11:33:15 +03:00
|
|
|
:param list addl_end_states: additional end states
|
2016-10-24 19:12:24 +03:00
|
|
|
:rtype: list
|
|
|
|
:return: list of nodes
|
|
|
|
"""
|
2016-10-30 11:33:15 +03:00
|
|
|
base_stopping_states = [
|
2017-03-01 23:45:32 +03:00
|
|
|
batchmodels.ComputeNodeState.start_task_failed,
|
2016-10-30 11:33:15 +03:00
|
|
|
batchmodels.ComputeNodeState.unusable,
|
2017-05-15 18:08:30 +03:00
|
|
|
batchmodels.ComputeNodeState.preempted,
|
|
|
|
batchmodels.ComputeNodeState.idle,
|
|
|
|
]
|
|
|
|
base_end_states = [
|
|
|
|
batchmodels.ComputeNodeState.preempted,
|
|
|
|
batchmodels.ComputeNodeState.idle,
|
2016-10-30 11:33:15 +03:00
|
|
|
]
|
|
|
|
if addl_end_states is not None and len(addl_end_states) > 0:
|
|
|
|
base_stopping_states.extend(addl_end_states)
|
|
|
|
base_end_states.extend(addl_end_states)
|
|
|
|
stopping_states = frozenset(base_stopping_states)
|
|
|
|
end_states = frozenset(base_end_states)
|
2016-10-24 19:12:24 +03:00
|
|
|
nodes = _block_for_nodes_ready(
|
2016-10-30 11:33:15 +03:00
|
|
|
batch_client, config, stopping_states, end_states, pool_id,
|
2016-11-13 09:13:55 +03:00
|
|
|
settings.pool_settings(config).reboot_on_start_task_failed)
|
2016-10-30 11:33:15 +03:00
|
|
|
list_nodes(batch_client, config, nodes=nodes)
|
2016-10-24 19:12:24 +03:00
|
|
|
return nodes
|
|
|
|
|
|
|
|
|
2016-10-14 19:36:38 +03:00
|
|
|
def check_pool_nodes_runnable(batch_client, config):
|
|
|
|
# type: (batch.BatchServiceClient, dict) -> bool
|
|
|
|
"""Check that all pool nodes in idle/running state
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
:param dict config: configuration dict
|
|
|
|
:rtype: bool
|
|
|
|
:return: all pool nodes are runnable
|
|
|
|
"""
|
2016-11-13 09:13:55 +03:00
|
|
|
pool_id = settings.pool_id(config)
|
2016-10-14 19:36:38 +03:00
|
|
|
node_state = frozenset(
|
|
|
|
(batchmodels.ComputeNodeState.idle,
|
|
|
|
batchmodels.ComputeNodeState.running)
|
|
|
|
)
|
|
|
|
pool = batch_client.pool.get(pool_id)
|
|
|
|
nodes = list(batch_client.compute_node.list(pool_id))
|
2017-05-13 00:40:06 +03:00
|
|
|
if (len(nodes) >=
|
|
|
|
(pool.target_dedicated_nodes + pool.target_low_priority_nodes) and
|
2016-10-14 19:36:38 +03:00
|
|
|
all(node.state in node_state for node in nodes)):
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2016-10-10 01:22:15 +03:00
|
|
|
def create_pool(batch_client, config, pool):
|
|
|
|
# type: (batch.BatchServiceClient, dict, batchmodels.PoolAddParameter) ->
|
|
|
|
# List[batchmodels.ComputeNode]
|
|
|
|
"""Create pool if not exists
|
|
|
|
:param batch_client: The batch client to use.
|
2016-10-14 00:11:52 +03:00
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
2016-10-10 01:22:15 +03:00
|
|
|
:param dict config: configuration dict
|
|
|
|
:param batchmodels.PoolAddParameter pool: pool addparameter object
|
|
|
|
:rtype: list
|
|
|
|
:return: list of nodes
|
|
|
|
"""
|
|
|
|
# create pool if not exists
|
|
|
|
try:
|
|
|
|
logger.info('Attempting to create pool: {}'.format(pool.id))
|
2016-11-13 09:13:55 +03:00
|
|
|
if settings.verbose(config):
|
2016-10-23 05:27:48 +03:00
|
|
|
logger.debug('node prep commandline: {}'.format(
|
|
|
|
pool.start_task.command_line))
|
2016-10-10 01:22:15 +03:00
|
|
|
batch_client.pool.add(pool)
|
|
|
|
logger.info('Created pool: {}'.format(pool.id))
|
|
|
|
except batchmodels.BatchErrorException as e:
|
|
|
|
if e.error.code != 'PoolExists':
|
|
|
|
raise
|
|
|
|
else:
|
|
|
|
logger.error('Pool {!r} already exists'.format(pool.id))
|
|
|
|
# wait for pool idle
|
2016-10-24 19:12:24 +03:00
|
|
|
return wait_for_pool_ready(batch_client, config, pool.id)
|
2016-10-10 01:22:15 +03:00
|
|
|
|
|
|
|
|
|
|
|
def _add_admin_user_to_compute_node(
|
2017-04-13 19:31:35 +03:00
|
|
|
batch_client, config, node, username, ssh_public_key_data):
|
2016-10-10 01:22:15 +03:00
|
|
|
# type: (batch.BatchServiceClient, dict, str, batchmodels.ComputeNode,
|
|
|
|
# str) -> None
|
|
|
|
"""Adds an administrative user to the Batch Compute Node with a default
|
|
|
|
expiry time of 7 days if not specified.
|
|
|
|
:param batch_client: The batch client to use.
|
2016-10-14 00:11:52 +03:00
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
2016-10-10 01:22:15 +03:00
|
|
|
:param dict config: configuration dict
|
|
|
|
:param node: The compute node.
|
2016-10-14 00:11:52 +03:00
|
|
|
:type node: `azure.batch.batch_service_client.models.ComputeNode`
|
2016-10-10 01:22:15 +03:00
|
|
|
:param str username: user name
|
2017-04-13 19:31:35 +03:00
|
|
|
:param str ssh_public_key_data: ssh rsa public key data
|
2016-10-10 01:22:15 +03:00
|
|
|
"""
|
2016-11-13 09:13:55 +03:00
|
|
|
pool = settings.pool_settings(config)
|
|
|
|
expiry = datetime.datetime.utcnow() + datetime.timedelta(
|
|
|
|
pool.ssh.expiry_days)
|
2016-10-10 01:22:15 +03:00
|
|
|
logger.info('adding user {} to node {} in pool {}, expiry={}'.format(
|
2016-11-13 09:13:55 +03:00
|
|
|
username, node.id, pool.id, expiry))
|
2016-10-10 01:22:15 +03:00
|
|
|
try:
|
|
|
|
batch_client.compute_node.add_user(
|
2016-11-13 09:13:55 +03:00
|
|
|
pool.id,
|
2016-10-10 01:22:15 +03:00
|
|
|
node.id,
|
|
|
|
batchmodels.ComputeNodeUser(
|
|
|
|
username,
|
|
|
|
is_admin=True,
|
|
|
|
expiry_time=expiry,
|
|
|
|
password=None,
|
2017-04-13 19:31:35 +03:00
|
|
|
ssh_public_key=ssh_public_key_data,
|
2016-10-10 01:22:15 +03:00
|
|
|
)
|
|
|
|
)
|
|
|
|
except batchmodels.batch_error.BatchErrorException as ex:
|
2016-10-28 06:51:36 +03:00
|
|
|
if 'The specified node user already exists' in ex.message.value:
|
|
|
|
logger.warning('user {} already exists on node {}'.format(
|
|
|
|
username, node.id))
|
|
|
|
else:
|
2017-05-13 00:40:06 +03:00
|
|
|
# log as error instead of raising the exception in case
|
|
|
|
# of low-priority removal
|
|
|
|
logger.error(ex.message.value)
|
2016-10-10 01:22:15 +03:00
|
|
|
|
|
|
|
|
|
|
|
def add_ssh_user(batch_client, config, nodes=None):
|
|
|
|
# type: (batch.BatchServiceClient, dict,
|
|
|
|
# List[batchmodels.ComputeNode]) -> None
|
2016-10-28 06:51:36 +03:00
|
|
|
"""Add an SSH user to all nodes of a pool and optionally generate a
|
|
|
|
SSH tunneling script
|
2016-10-10 01:22:15 +03:00
|
|
|
:param batch_client: The batch client to use.
|
2016-10-14 00:11:52 +03:00
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
2016-10-10 01:22:15 +03:00
|
|
|
:param dict config: configuration dict
|
|
|
|
:param list nodes: list of nodes
|
|
|
|
"""
|
2016-11-13 09:13:55 +03:00
|
|
|
pool = settings.pool_settings(config)
|
|
|
|
if util.is_none_or_empty(pool.ssh.username):
|
|
|
|
logger.info('not creating ssh user on pool {}'.format(pool.id))
|
|
|
|
return
|
2017-04-13 19:31:35 +03:00
|
|
|
# read public key data from settings if available
|
|
|
|
if util.is_not_empty(pool.ssh.ssh_public_key_data):
|
|
|
|
ssh_pub_key_data = pool.ssh.ssh_public_key_data
|
2017-04-13 23:13:06 +03:00
|
|
|
ssh_priv_key = pool.ssh.ssh_private_key
|
2016-10-10 01:22:15 +03:00
|
|
|
else:
|
2017-04-13 19:31:35 +03:00
|
|
|
# generate ssh key pair if not specified
|
2017-04-14 22:54:43 +03:00
|
|
|
if pool.ssh.ssh_public_key is None:
|
2017-04-13 19:31:35 +03:00
|
|
|
ssh_priv_key, ssh_pub_key = crypto.generate_ssh_keypair(
|
|
|
|
pool.ssh.generated_file_export_path)
|
|
|
|
else:
|
|
|
|
ssh_pub_key = pool.ssh.ssh_public_key
|
2017-04-13 23:13:06 +03:00
|
|
|
ssh_priv_key = pool.ssh.ssh_private_key
|
2017-04-13 19:31:35 +03:00
|
|
|
# read public key data
|
2017-04-13 23:13:06 +03:00
|
|
|
with ssh_pub_key.open('rb') as fd:
|
2017-04-13 19:31:35 +03:00
|
|
|
ssh_pub_key_data = fd.read().decode('utf8')
|
2016-11-13 09:13:55 +03:00
|
|
|
# get node list if not provided
|
|
|
|
if nodes is None:
|
|
|
|
nodes = batch_client.compute_node.list(pool.id)
|
|
|
|
for node in nodes:
|
|
|
|
_add_admin_user_to_compute_node(
|
2017-04-13 19:31:35 +03:00
|
|
|
batch_client, config, node, pool.ssh.username, ssh_pub_key_data)
|
2016-11-13 09:13:55 +03:00
|
|
|
# generate tunnel script if requested
|
2016-12-15 06:01:50 +03:00
|
|
|
generate_ssh_tunnel_script(batch_client, pool, ssh_priv_key, nodes)
|
|
|
|
|
|
|
|
|
|
|
|
def generate_ssh_tunnel_script(batch_client, pool, ssh_priv_key, nodes):
|
|
|
|
# type: (batch.BatchServiceClient, PoolSettings, str,
|
|
|
|
# List[batchmodels.ComputeNode]) -> None
|
|
|
|
"""Generate SSH tunneling script
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
:param PoolSettings pool: pool settings
|
|
|
|
:param str ssh_priv_key: path to ssh private key
|
|
|
|
:param list nodes: list of nodes
|
|
|
|
"""
|
2016-11-13 09:13:55 +03:00
|
|
|
if pool.ssh.generate_docker_tunnel_script:
|
2016-12-15 06:01:50 +03:00
|
|
|
if nodes is None or len(list(nodes)) != pool.vm_count:
|
|
|
|
nodes = batch_client.compute_node.list(pool.id)
|
|
|
|
if ssh_priv_key is None:
|
|
|
|
ssh_priv_key = pathlib.Path(
|
2017-03-12 06:15:14 +03:00
|
|
|
pool.ssh.generated_file_export_path,
|
|
|
|
crypto.get_ssh_key_prefix())
|
2017-04-13 19:31:35 +03:00
|
|
|
if not ssh_priv_key.exists():
|
|
|
|
logger.warning(
|
|
|
|
('cannot generate tunnel script with non-existant RSA '
|
|
|
|
'private key: {}').format(ssh_priv_key))
|
|
|
|
return
|
2016-12-15 06:01:50 +03:00
|
|
|
ssh_args = [
|
|
|
|
'ssh', '-o', 'StrictHostKeyChecking=no',
|
2017-03-11 20:21:33 +03:00
|
|
|
'-o', 'UserKnownHostsFile={}'.format(os.devnull),
|
2016-12-15 06:01:50 +03:00
|
|
|
'-i', str(ssh_priv_key), '-p', '$port', '-N',
|
2016-12-15 22:04:33 +03:00
|
|
|
'-L', '2375:localhost:2375', '-L', '3476:localhost:3476',
|
|
|
|
'{}@$ip'.format(pool.ssh.username)
|
2016-12-15 06:01:50 +03:00
|
|
|
]
|
2016-11-13 09:13:55 +03:00
|
|
|
tunnelscript = pathlib.Path(
|
|
|
|
pool.ssh.generated_file_export_path, _SSH_TUNNEL_SCRIPT)
|
|
|
|
with tunnelscript.open('w') as fd:
|
|
|
|
fd.write('#!/usr/bin/env bash\n')
|
|
|
|
fd.write('set -e\n')
|
2016-12-15 06:01:50 +03:00
|
|
|
# populate node arrays
|
|
|
|
fd.write('declare -A nodes\n')
|
|
|
|
fd.write('declare -A ips\n')
|
|
|
|
fd.write('declare -A ports\n')
|
|
|
|
i = 0
|
|
|
|
for node in nodes:
|
|
|
|
rls = batch_client.compute_node.get_remote_login_settings(
|
|
|
|
pool.id, node.id)
|
|
|
|
fd.write('nodes[{}]={}\n'.format(i, node.id))
|
|
|
|
fd.write('ips[{}]={}\n'.format(i, rls.remote_login_ip_address))
|
|
|
|
fd.write('ports[{}]={}\n'.format(i, rls.remote_login_port))
|
|
|
|
i += 1
|
|
|
|
fd.write(
|
|
|
|
'if [ -z $1 ]; then echo must specify node cardinal; exit 1; '
|
|
|
|
'fi\n')
|
|
|
|
fd.write('node=${nodes[$1]}\n')
|
|
|
|
fd.write('ip=${ips[$1]}\n')
|
|
|
|
fd.write('port=${ports[$1]}\n')
|
|
|
|
fd.write(
|
|
|
|
'echo tunneling to docker daemon on $node at '
|
|
|
|
'$ip:$port\n')
|
2016-11-13 09:13:55 +03:00
|
|
|
fd.write(' '.join(ssh_args))
|
2017-03-11 20:21:33 +03:00
|
|
|
fd.write(' >{} 2>&1 &\n'.format(os.devnull))
|
2016-12-15 06:01:50 +03:00
|
|
|
fd.write('pid=$!\n')
|
|
|
|
fd.write('echo ssh tunnel pid is $pid\n')
|
|
|
|
fd.write(
|
2016-12-15 22:04:33 +03:00
|
|
|
'echo execute docker commands with DOCKER_HOST=: or with '
|
|
|
|
'option: -H :\n')
|
2016-11-13 09:13:55 +03:00
|
|
|
os.chmod(str(tunnelscript), 0o755)
|
|
|
|
logger.info('ssh tunnel script generated: {}'.format(tunnelscript))
|
2016-10-10 01:22:15 +03:00
|
|
|
|
|
|
|
|
2016-10-28 06:51:36 +03:00
|
|
|
def del_ssh_user(batch_client, config, nodes=None):
|
|
|
|
# type: (batch.BatchServiceClient, dict,
|
|
|
|
# List[batchmodels.ComputeNode]) -> None
|
|
|
|
"""Delete an SSH user on all nodes of a pool
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
:param dict config: configuration dict
|
|
|
|
:param list nodes: list of nodes
|
|
|
|
"""
|
2016-11-13 09:13:55 +03:00
|
|
|
pool = settings.pool_settings(config)
|
|
|
|
if util.is_none_or_empty(pool.ssh.username):
|
2016-10-28 06:51:36 +03:00
|
|
|
logger.error('not deleting unspecified ssh user on pool {}'.format(
|
2016-11-13 09:13:55 +03:00
|
|
|
pool.id))
|
|
|
|
return
|
|
|
|
if not util.confirm_action(
|
|
|
|
config, 'delete user {} from pool {}'.format(
|
|
|
|
pool.ssh.username, pool.id)):
|
|
|
|
return
|
|
|
|
# get node list if not provided
|
|
|
|
if nodes is None:
|
|
|
|
nodes = batch_client.compute_node.list(pool.id)
|
|
|
|
for node in nodes:
|
|
|
|
try:
|
|
|
|
batch_client.compute_node.delete_user(
|
|
|
|
pool.id, node.id, pool.ssh.username)
|
|
|
|
logger.debug('deleted user {} from node {}'.format(
|
|
|
|
pool.ssh.username, node.id))
|
|
|
|
except batchmodels.batch_error.BatchErrorException as ex:
|
|
|
|
if 'The node user does not exist' not in ex.message.value:
|
|
|
|
raise
|
2016-10-28 06:51:36 +03:00
|
|
|
|
|
|
|
|
2016-10-27 07:39:00 +03:00
|
|
|
def list_pools(batch_client):
|
|
|
|
# type: (azure.batch.batch_service_client.BatchServiceClient) -> None
|
|
|
|
"""List pools
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
"""
|
|
|
|
i = 0
|
|
|
|
pools = batch_client.pool.list()
|
|
|
|
for pool in pools:
|
2017-05-13 00:40:06 +03:00
|
|
|
if util.is_not_empty(pool.resize_errors):
|
|
|
|
errors = []
|
|
|
|
for err in pool.resize_errors:
|
|
|
|
errors.append('code={} msg={}'.format(err.code, err.message))
|
|
|
|
errors = ' resize_error=({})'.format(' '.join(errors))
|
2016-10-27 07:39:00 +03:00
|
|
|
else:
|
2017-05-13 00:40:06 +03:00
|
|
|
errors = ''
|
2016-10-27 07:39:00 +03:00
|
|
|
logger.info(
|
2017-06-06 18:29:44 +03:00
|
|
|
('pool_id={} [state={} allocation_state={}{} vm_size={} '
|
|
|
|
'node_agent={} vm_dedicated_count={} '
|
|
|
|
'target_vm_dedicated_count={} vm_low_priority_count={} '
|
2017-05-13 00:40:06 +03:00
|
|
|
'target_vm_low_priority_count={}]'.format(
|
|
|
|
pool.id, pool.state, pool.allocation_state, errors,
|
2017-06-06 18:29:44 +03:00
|
|
|
pool.vm_size,
|
|
|
|
pool.virtual_machine_configuration.node_agent_sku_id,
|
|
|
|
pool.current_dedicated_nodes,
|
2017-05-13 00:40:06 +03:00
|
|
|
pool.target_dedicated_nodes, pool.current_low_priority_nodes,
|
|
|
|
pool.target_low_priority_nodes)))
|
2016-10-27 07:39:00 +03:00
|
|
|
i += 1
|
|
|
|
if i == 0:
|
|
|
|
logger.error('no pools found')
|
|
|
|
|
|
|
|
|
2017-07-20 01:34:46 +03:00
|
|
|
def _check_metadata_mismatch(mdtype, metadata, req_ge=None):
|
|
|
|
# type: (str, List[batchmodels.MetadataItem], str) -> None
|
|
|
|
"""Check for metadata mismatch
|
|
|
|
:param str mdtype: metadata type (e.g., pool, job)
|
|
|
|
:param list metadata: list of metadata items
|
|
|
|
:param str req_ge: required greater than or equal to
|
|
|
|
"""
|
|
|
|
if util.is_none_or_empty(metadata):
|
|
|
|
if req_ge is not None:
|
|
|
|
raise RuntimeError(
|
|
|
|
('{} version metadata not present but version {} is '
|
|
|
|
'required').format(mdtype, req_ge))
|
|
|
|
else:
|
|
|
|
logger.warning('{} version metadata not present'.format(mdtype))
|
|
|
|
else:
|
|
|
|
for md in metadata:
|
|
|
|
if md.name == settings.get_metadata_version_name():
|
|
|
|
if md.value != __version__:
|
|
|
|
logger.warning(
|
|
|
|
'{} version metadata mismatch: {}={} cli={}'.format(
|
|
|
|
mdtype, mdtype, md.value, __version__))
|
|
|
|
if req_ge is not None:
|
|
|
|
# split version into tuple
|
|
|
|
mdt = md.value.split('.')
|
|
|
|
mdt = tuple((int(mdt[0]), int(mdt[1]), mdt[2]))
|
|
|
|
rv = req_ge.split('.')
|
|
|
|
rv = tuple((int(rv[0]), int(rv[1]), rv[2]))
|
|
|
|
if mdt < rv:
|
|
|
|
raise RuntimeError(
|
|
|
|
('{} version of {} does not meet the version '
|
|
|
|
'requirement of at least {}').format(
|
|
|
|
mdtype, md.value, req_ge))
|
|
|
|
break
|
|
|
|
|
|
|
|
|
2016-10-30 11:33:15 +03:00
|
|
|
def resize_pool(batch_client, config, wait=False):
|
|
|
|
# type: (azure.batch.batch_service_client.BatchServiceClient, dict,
|
|
|
|
# bool) -> list
|
2016-10-10 01:22:15 +03:00
|
|
|
"""Resize a pool
|
|
|
|
:param batch_client: The batch client to use.
|
2016-10-14 00:11:52 +03:00
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
2016-10-10 01:22:15 +03:00
|
|
|
:param dict config: configuration dict
|
2016-10-30 11:33:15 +03:00
|
|
|
:param bool wait: wait for operation to complete
|
|
|
|
:rtype: list or None
|
|
|
|
:return: list of nodes if wait or None
|
2016-10-10 01:22:15 +03:00
|
|
|
"""
|
2017-05-13 00:40:06 +03:00
|
|
|
pool = settings.pool_settings(config)
|
2017-05-16 19:49:01 +03:00
|
|
|
_pool = batch_client.pool.get(pool.id)
|
2017-06-26 23:20:49 +03:00
|
|
|
# check pool metadata version
|
2017-07-20 01:34:46 +03:00
|
|
|
_check_metadata_mismatch('pool', _pool.metadata)
|
2017-05-16 19:49:01 +03:00
|
|
|
logger.info(
|
|
|
|
('Resizing pool {} to {} compute nodes [current_dedicated_nodes={} '
|
|
|
|
'current_low_priority_nodes={}]').format(
|
|
|
|
pool.id, pool.vm_count, _pool.current_dedicated_nodes,
|
|
|
|
_pool.current_low_priority_nodes))
|
2017-05-24 00:14:29 +03:00
|
|
|
total_vm_count = (
|
|
|
|
_pool.current_dedicated_nodes + _pool.current_low_priority_nodes
|
|
|
|
)
|
2016-10-10 01:22:15 +03:00
|
|
|
batch_client.pool.resize(
|
2017-05-13 00:40:06 +03:00
|
|
|
pool_id=pool.id,
|
2016-10-10 01:22:15 +03:00
|
|
|
pool_resize_parameter=batchmodels.PoolResizeParameter(
|
2017-05-13 00:40:06 +03:00
|
|
|
target_dedicated_nodes=pool.vm_count.dedicated,
|
|
|
|
target_low_priority_nodes=pool.vm_count.low_priority,
|
|
|
|
resize_timeout=pool.resize_timeout,
|
2016-10-10 01:22:15 +03:00
|
|
|
)
|
|
|
|
)
|
2016-10-30 11:33:15 +03:00
|
|
|
if wait:
|
2017-05-16 19:49:01 +03:00
|
|
|
# wait until at least one node has entered leaving_pool state first
|
2017-05-24 00:14:29 +03:00
|
|
|
# if this pool is being resized down
|
|
|
|
diff_vm_count = (
|
|
|
|
pool.vm_count.dedicated + pool.vm_count.low_priority -
|
|
|
|
total_vm_count
|
2017-05-16 19:49:01 +03:00
|
|
|
)
|
2017-05-24 00:14:29 +03:00
|
|
|
if diff_vm_count < 0:
|
2017-05-16 19:49:01 +03:00
|
|
|
logger.debug(
|
|
|
|
'waiting for resize to start on pool: {}'.format(pool.id))
|
|
|
|
while True:
|
|
|
|
nodes = list(batch_client.compute_node.list(pool.id))
|
2017-05-24 00:14:29 +03:00
|
|
|
if (len(nodes) != total_vm_count or any(
|
|
|
|
node.state == batchmodels.ComputeNodeState.leaving_pool
|
|
|
|
for node in nodes)):
|
2017-05-16 19:49:01 +03:00
|
|
|
break
|
|
|
|
else:
|
|
|
|
time.sleep(1)
|
2016-10-30 11:33:15 +03:00
|
|
|
return wait_for_pool_ready(
|
2017-05-13 00:40:06 +03:00
|
|
|
batch_client, config, pool.id,
|
2016-10-30 11:33:15 +03:00
|
|
|
addl_end_states=[batchmodels.ComputeNodeState.running])
|
2016-10-10 01:22:15 +03:00
|
|
|
|
|
|
|
|
2017-05-24 19:54:09 +03:00
|
|
|
def del_pool(batch_client, config, pool_id=None):
|
|
|
|
# type: (azure.batch.batch_service_client.BatchServiceClient, dict,
|
|
|
|
# str) -> bool
|
2016-10-10 01:22:15 +03:00
|
|
|
"""Delete a pool
|
|
|
|
:param batch_client: The batch client to use.
|
2016-10-14 00:11:52 +03:00
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
2016-10-10 01:22:15 +03:00
|
|
|
:param dict config: configuration dict
|
2017-05-24 19:54:09 +03:00
|
|
|
:param str pool_id: pool id
|
2016-10-27 07:39:00 +03:00
|
|
|
:rtype: bool
|
|
|
|
:return: if pool was deleted
|
2016-10-10 01:22:15 +03:00
|
|
|
"""
|
2017-05-24 19:54:09 +03:00
|
|
|
if util.is_none_or_empty(pool_id):
|
|
|
|
pool_id = settings.pool_id(config)
|
2016-11-01 06:49:24 +03:00
|
|
|
if not util.confirm_action(
|
2016-10-10 01:22:15 +03:00
|
|
|
config, 'delete {} pool'.format(pool_id)):
|
2016-10-27 07:39:00 +03:00
|
|
|
return False
|
2016-10-10 01:22:15 +03:00
|
|
|
logger.info('Deleting pool: {}'.format(pool_id))
|
|
|
|
batch_client.pool.delete(pool_id)
|
2016-10-27 07:39:00 +03:00
|
|
|
return True
|
2016-10-10 01:22:15 +03:00
|
|
|
|
|
|
|
|
2017-07-20 01:34:46 +03:00
|
|
|
def pool_autoscale_disable(batch_client, config):
|
|
|
|
# type: (batch.BatchServiceClient, dict) -> None
|
|
|
|
"""Enable autoscale formula
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
:param dict config: configuration dict
|
|
|
|
"""
|
|
|
|
pool_id = settings.pool_id(config)
|
|
|
|
batch_client.pool.disable_auto_scale(pool_id=pool_id)
|
|
|
|
logger.info('autoscale disabled for pool {}'.format(pool_id))
|
|
|
|
|
|
|
|
|
|
|
|
def pool_autoscale_enable(batch_client, config):
|
|
|
|
# type: (batch.BatchServiceClient, dict) -> None
|
|
|
|
"""Enable autoscale formula
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
:param dict config: configuration dict
|
|
|
|
"""
|
|
|
|
pool = settings.pool_settings(config)
|
|
|
|
_pool = batch_client.pool.get(pool.id)
|
|
|
|
# check pool metadata
|
|
|
|
# TODO fix req version to current release version until 2.9.0
|
|
|
|
_check_metadata_mismatch('pool', _pool.metadata, req_ge='2.8.0')
|
|
|
|
asformula = None
|
|
|
|
asei = None
|
|
|
|
if not _pool.enable_auto_scale:
|
|
|
|
# check if an autoscale formula exists in config
|
|
|
|
if not settings.is_pool_autoscale_enabled(config, pas=pool.autoscale):
|
|
|
|
if not util.confirm_action(
|
|
|
|
config,
|
|
|
|
('enable dummy formula for pool {} as no autoscale '
|
|
|
|
'formula exists').format(pool.id)):
|
|
|
|
logger.error('not enabling autoscale for pool {}'.format(
|
|
|
|
pool.id))
|
|
|
|
return
|
|
|
|
# set dummy formula
|
|
|
|
asformula = (
|
|
|
|
'$TargetDedicatedNodes = {}; '
|
|
|
|
'$TargetLowPriorityNodes = {};'
|
|
|
|
).format(
|
|
|
|
_pool.target_dedicated_nodes, _pool.target_low_priority_nodes)
|
|
|
|
if asformula is None:
|
|
|
|
asformula = autoscale.get_formula(pool)
|
|
|
|
asei = pool.autoscale.evaluation_interval
|
|
|
|
# enable autoscale
|
|
|
|
batch_client.pool.enable_auto_scale(
|
|
|
|
pool_id=pool.id,
|
|
|
|
auto_scale_formula=asformula,
|
|
|
|
auto_scale_evaluation_interval=asei,
|
|
|
|
)
|
|
|
|
logger.info('autoscale enabled/updated for pool {}'.format(pool.id))
|
|
|
|
|
|
|
|
|
|
|
|
def _output_autoscale_result(result):
|
|
|
|
# type: (batchmodels.AutoScaleRun) -> None
|
|
|
|
"""Output autoscale evalute or last exec results
|
|
|
|
:param batchmodels.AutoScaleRun result: result
|
|
|
|
"""
|
|
|
|
if result is None:
|
|
|
|
logger.error(
|
|
|
|
'autoscale result is invalid, ensure autoscale is enabled')
|
|
|
|
return
|
|
|
|
if result.error is not None:
|
|
|
|
logger.error('autoscale evaluate error: code={} message={}'.format(
|
|
|
|
result.error.code, result.error.message))
|
|
|
|
else:
|
|
|
|
logger.info('autoscale result: {}'.format(result.results))
|
|
|
|
logger.info('last autoscale evaluation: {}'.format(result.timestamp))
|
|
|
|
|
|
|
|
|
|
|
|
def pool_autoscale_evaluate(batch_client, config):
|
|
|
|
# type: (batch.BatchServiceClient, dict) -> None
|
|
|
|
"""Evaluate autoscale formula
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
:param dict config: configuration dict
|
|
|
|
"""
|
|
|
|
pool = settings.pool_settings(config)
|
|
|
|
if not settings.is_pool_autoscale_enabled(config, pas=pool.autoscale):
|
|
|
|
logger.error(
|
|
|
|
('cannot evaluate autoscale for pool {}, not enabled or '
|
|
|
|
'no formula').format(pool.id))
|
|
|
|
return
|
|
|
|
result = batch_client.pool.evaluate_auto_scale(
|
|
|
|
pool_id=pool.id,
|
|
|
|
auto_scale_formula=autoscale.get_formula(pool),
|
|
|
|
)
|
|
|
|
_output_autoscale_result(result)
|
|
|
|
|
|
|
|
|
|
|
|
def pool_autoscale_lastexec(batch_client, config):
|
|
|
|
# type: (batch.BatchServiceClient, dict) -> None
|
|
|
|
"""Get last execution of the autoscale formula
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
:param dict config: configuration dict
|
|
|
|
"""
|
|
|
|
pool_id = settings.pool_id(config)
|
|
|
|
pool = batch_client.pool.get(pool_id)
|
|
|
|
if not pool.enable_auto_scale:
|
|
|
|
logger.error(
|
|
|
|
('last execution information not available for autoscale '
|
|
|
|
'disabled pool {}').format(pool_id))
|
|
|
|
return
|
|
|
|
_output_autoscale_result(pool.auto_scale_run)
|
|
|
|
|
|
|
|
|
2017-03-01 06:34:22 +03:00
|
|
|
def reboot_nodes(batch_client, config, all_start_task_failed, node_id):
|
|
|
|
# type: (batch.BatchServiceClient, dict, bool, str) -> None
|
|
|
|
"""Reboot nodes in a pool
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
:param dict config: configuration dict
|
|
|
|
:param bool all_start_task_failed: reboot all start task failed nodes
|
|
|
|
:param str node_id: node id to delete
|
|
|
|
"""
|
|
|
|
pool_id = settings.pool_id(config)
|
|
|
|
if all_start_task_failed:
|
|
|
|
nodes = list(
|
|
|
|
batch_client.compute_node.list(
|
|
|
|
pool_id=pool_id,
|
|
|
|
compute_node_list_options=batchmodels.ComputeNodeListOptions(
|
|
|
|
filter='state eq \'starttaskfailed\'',
|
|
|
|
),
|
|
|
|
))
|
|
|
|
for node in nodes:
|
|
|
|
if not util.confirm_action(
|
|
|
|
config, 'reboot node {} from {} pool'.format(
|
|
|
|
node.id, pool_id)):
|
|
|
|
continue
|
|
|
|
_reboot_node(batch_client, pool_id, node.id, False)
|
|
|
|
else:
|
|
|
|
_reboot_node(batch_client, pool_id, node_id, False)
|
|
|
|
|
|
|
|
|
2017-07-01 09:50:21 +03:00
|
|
|
def del_node(batch_client, config, all_start_task_failed, node_id):
|
|
|
|
# type: (batch.BatchServiceClient, dict, bool, str) -> None
|
2016-10-10 01:22:15 +03:00
|
|
|
"""Delete a node in a pool
|
|
|
|
:param batch_client: The batch client to use.
|
2016-10-14 00:11:52 +03:00
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
2016-10-10 01:22:15 +03:00
|
|
|
:param dict config: configuration dict
|
2017-07-01 11:25:30 +03:00
|
|
|
:param bool all_start_task_failed: delete all start task failed nodes
|
2016-10-10 01:22:15 +03:00
|
|
|
:param str node_id: node id to delete
|
|
|
|
"""
|
2017-07-01 09:50:21 +03:00
|
|
|
node_ids = []
|
2016-11-13 09:13:55 +03:00
|
|
|
pool_id = settings.pool_id(config)
|
2017-07-01 09:50:21 +03:00
|
|
|
if all_start_task_failed:
|
|
|
|
nodes = list(
|
|
|
|
batch_client.compute_node.list(
|
|
|
|
pool_id=pool_id,
|
|
|
|
compute_node_list_options=batchmodels.ComputeNodeListOptions(
|
|
|
|
filter='state eq \'starttaskfailed\'',
|
|
|
|
),
|
|
|
|
))
|
|
|
|
for node in nodes:
|
|
|
|
if util.confirm_action(
|
|
|
|
config, 'delete node {} from {} pool'.format(
|
|
|
|
node.id, pool_id)):
|
|
|
|
node_ids.append(node.id)
|
|
|
|
else:
|
|
|
|
if util.is_none_or_empty(node_id):
|
|
|
|
raise ValueError('node id is invalid')
|
|
|
|
if util.confirm_action(
|
|
|
|
config, 'delete node {} from {} pool'.format(
|
|
|
|
node_id, pool_id)):
|
|
|
|
node_ids.append(node_id)
|
|
|
|
if util.is_none_or_empty(node_ids):
|
|
|
|
logger.warning('no nodes to delete from pool: {}'.format(pool_id))
|
2016-10-10 01:22:15 +03:00
|
|
|
return
|
2017-07-01 09:50:21 +03:00
|
|
|
logger.info('Deleting nodes {} from pool {}'.format(node_ids, pool_id))
|
2016-10-10 01:22:15 +03:00
|
|
|
batch_client.pool.remove_nodes(
|
|
|
|
pool_id=pool_id,
|
|
|
|
node_remove_parameter=batchmodels.NodeRemoveParameter(
|
2017-07-01 09:50:21 +03:00
|
|
|
node_list=node_ids,
|
2016-10-10 01:22:15 +03:00
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2017-07-20 21:40:06 +03:00
|
|
|
def check_pool_for_job_migration(
|
|
|
|
batch_client, config, jobid=None, poolid=None):
|
|
|
|
# type: (azure.batch.batch_service_client.BatchServiceClient, dict,
|
|
|
|
# str, str) -> None
|
|
|
|
"""Check pool for job migration eligibility
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
:param dict config: configuration dict
|
|
|
|
:param str jobid: job id to migrate
|
|
|
|
:param str poolid: pool id to update to
|
|
|
|
"""
|
|
|
|
if poolid is None:
|
|
|
|
poolid = settings.pool_id(config)
|
|
|
|
if jobid is None:
|
|
|
|
jobs = settings.job_specifications(config)
|
|
|
|
else:
|
|
|
|
jobs = [{'id': jobid}]
|
|
|
|
for _job in jobs:
|
|
|
|
job_id = settings.job_id(_job)
|
|
|
|
job = batch_client.job.get(job_id=job_id)
|
|
|
|
if (job.state == batchmodels.JobState.completed or
|
|
|
|
job.state == batchmodels.JobState.deleting or
|
|
|
|
job.state == batchmodels.JobState.terminating):
|
|
|
|
raise RuntimeError(
|
|
|
|
'cannot migrate job {} in state {}'.format(job_id, job.state))
|
|
|
|
if job.pool_info.auto_pool_specification is not None:
|
|
|
|
raise RuntimeError(
|
|
|
|
'cannot migrate job {} with an autopool specification'.format(
|
|
|
|
job_id))
|
|
|
|
if job.pool_info.pool_id == poolid:
|
|
|
|
raise RuntimeError(
|
|
|
|
'cannot migrate job {} to the same pool {}'.format(
|
|
|
|
job_id, poolid))
|
|
|
|
|
|
|
|
|
|
|
|
def update_job_with_pool(batch_client, config, jobid=None, poolid=None):
|
|
|
|
# type: (azure.batch.batch_service_client.BatchServiceClient, dict,
|
|
|
|
# str, str) -> None
|
|
|
|
"""Update job with different pool id
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
:param dict config: configuration dict
|
|
|
|
:param str jobid: job id to update
|
|
|
|
:param str poolid: pool id to update to
|
|
|
|
"""
|
|
|
|
if poolid is None:
|
|
|
|
poolid = settings.pool_id(config)
|
|
|
|
if jobid is None:
|
|
|
|
jobs = settings.job_specifications(config)
|
|
|
|
else:
|
|
|
|
jobs = [{'id': jobid}]
|
|
|
|
for _job in jobs:
|
|
|
|
job_id = settings.job_id(_job)
|
|
|
|
batch_client.job.patch(
|
|
|
|
job_id=job_id,
|
|
|
|
job_patch_parameter=batchmodels.JobPatchParameter(
|
|
|
|
pool_info=batchmodels.PoolInformation(
|
|
|
|
pool_id=poolid)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
logger.info('updated job {} to target pool {}'.format(
|
|
|
|
job_id, poolid))
|
|
|
|
|
|
|
|
|
|
|
|
def disable_jobs(
|
|
|
|
batch_client, config, disable_tasks_action, jobid=None,
|
|
|
|
disabling_state_ok=False, terminate_tasks=False):
|
|
|
|
# type: (azure.batch.batch_service_client.BatchServiceClient, dict,
|
|
|
|
# str, str, bool, bool) -> None
|
|
|
|
"""Disable jobs
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
:param dict config: configuration dict
|
|
|
|
:param str disable_tasks_action: disable tasks action
|
|
|
|
:param str jobid: job id to disable
|
|
|
|
:param bool disabling_state_ok: disabling state is ok to proceed
|
|
|
|
:param bool terminate_tasks: terminate tasks after disable
|
|
|
|
"""
|
|
|
|
if jobid is None:
|
|
|
|
jobs = settings.job_specifications(config)
|
|
|
|
else:
|
|
|
|
jobs = [{'id': jobid}]
|
|
|
|
for job in jobs:
|
|
|
|
job_id = settings.job_id(job)
|
|
|
|
try:
|
|
|
|
batch_client.job.disable(
|
|
|
|
job_id=job_id,
|
|
|
|
disable_tasks=batchmodels.DisableJobOption(
|
|
|
|
disable_tasks_action),
|
|
|
|
)
|
|
|
|
except batchmodels.batch_error.BatchErrorException as ex:
|
|
|
|
if ('The specified job is already in a completed state' in
|
|
|
|
ex.message.value):
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
# wait for job to enter disabled/completed/deleting state
|
|
|
|
while True:
|
|
|
|
_job = batch_client.job.get(
|
|
|
|
job_id=job_id,
|
|
|
|
job_get_options=batchmodels.JobGetOptions(
|
|
|
|
select='id,state')
|
|
|
|
)
|
|
|
|
if ((disabling_state_ok and
|
|
|
|
_job.state == batchmodels.JobState.disabling) or
|
|
|
|
_job.state == batchmodels.JobState.disabled or
|
|
|
|
_job.state == batchmodels.JobState.completed or
|
|
|
|
_job.state == batchmodels.JobState.deleting):
|
|
|
|
break
|
|
|
|
time.sleep(1)
|
|
|
|
logger.info('job {} disabled'.format(job_id))
|
|
|
|
if terminate_tasks:
|
|
|
|
terminate_tasks(
|
|
|
|
batch_client, config, jobid=job_id, wait=True)
|
|
|
|
|
|
|
|
|
|
|
|
def enable_jobs(batch_client, config, jobid=None):
|
|
|
|
# type: (azure.batch.batch_service_client.BatchServiceClient, dict,
|
|
|
|
# str) -> None
|
|
|
|
"""Enable jobs
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
:param dict config: configuration dict
|
|
|
|
:param str jobid: job id to enable
|
|
|
|
"""
|
|
|
|
if jobid is None:
|
|
|
|
jobs = settings.job_specifications(config)
|
|
|
|
else:
|
|
|
|
jobs = [{'id': jobid}]
|
|
|
|
for job in jobs:
|
|
|
|
job_id = settings.job_id(job)
|
|
|
|
try:
|
|
|
|
batch_client.job.enable(job_id=job_id)
|
|
|
|
except batchmodels.batch_error.BatchErrorException as ex:
|
|
|
|
if ('The specified job is already in a completed state' in
|
|
|
|
ex.message.value):
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
logger.info('job {} enabled'.format(job_id))
|
|
|
|
|
|
|
|
|
2017-02-28 20:41:05 +03:00
|
|
|
def del_jobs(batch_client, config, jobid=None, termtasks=False, wait=False):
|
2016-10-23 05:27:48 +03:00
|
|
|
# type: (azure.batch.batch_service_client.BatchServiceClient, dict,
|
2017-02-28 20:41:05 +03:00
|
|
|
# str, bool, bool) -> None
|
2016-10-10 01:22:15 +03:00
|
|
|
"""Delete jobs
|
|
|
|
:param batch_client: The batch client to use.
|
2016-10-14 00:11:52 +03:00
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
2016-10-10 01:22:15 +03:00
|
|
|
:param dict config: configuration dict
|
2016-10-28 20:08:59 +03:00
|
|
|
:param str jobid: job id to delete
|
2017-02-28 20:41:05 +03:00
|
|
|
:param bool termtasks: terminate tasks manually prior
|
2016-10-23 05:27:48 +03:00
|
|
|
:param bool wait: wait for jobs to delete
|
2016-10-10 01:22:15 +03:00
|
|
|
"""
|
2016-10-28 20:08:59 +03:00
|
|
|
if jobid is None:
|
2016-11-13 09:13:55 +03:00
|
|
|
jobs = settings.job_specifications(config)
|
2016-10-28 20:08:59 +03:00
|
|
|
else:
|
|
|
|
jobs = [{'id': jobid}]
|
2016-10-23 05:27:48 +03:00
|
|
|
nocheck = set()
|
2016-10-28 20:08:59 +03:00
|
|
|
for job in jobs:
|
2016-11-13 09:13:55 +03:00
|
|
|
job_id = settings.job_id(job)
|
2016-11-01 06:49:24 +03:00
|
|
|
if not util.confirm_action(
|
2016-10-10 01:22:15 +03:00
|
|
|
config, 'delete {} job'.format(job_id)):
|
2016-10-23 05:27:48 +03:00
|
|
|
nocheck.add(job_id)
|
2016-10-10 01:22:15 +03:00
|
|
|
continue
|
|
|
|
logger.info('Deleting job: {}'.format(job_id))
|
2017-02-23 07:25:17 +03:00
|
|
|
try:
|
2017-05-01 00:46:38 +03:00
|
|
|
if termtasks:
|
|
|
|
# disable job first to prevent active tasks from
|
|
|
|
# getting processed
|
|
|
|
logger.debug(
|
|
|
|
'disabling job {} first due to task termination'.format(
|
|
|
|
job_id))
|
2017-07-20 21:40:06 +03:00
|
|
|
disable_jobs(
|
|
|
|
batch_client, config, 'wait', jobid=job_id,
|
|
|
|
disabling_state_ok=True, terminate_tasks=True)
|
2017-05-01 00:46:38 +03:00
|
|
|
# delete job
|
2017-02-23 07:25:17 +03:00
|
|
|
batch_client.job.delete(job_id)
|
|
|
|
except batchmodels.batch_error.BatchErrorException as ex:
|
|
|
|
if 'The specified job does not exist' in ex.message.value:
|
|
|
|
logger.error('{} job does not exist'.format(job_id))
|
|
|
|
nocheck.add(job_id)
|
|
|
|
continue
|
2017-05-01 00:46:38 +03:00
|
|
|
else:
|
|
|
|
raise
|
2016-10-23 05:27:48 +03:00
|
|
|
if wait:
|
2016-10-28 20:08:59 +03:00
|
|
|
for job in jobs:
|
2016-11-13 09:13:55 +03:00
|
|
|
job_id = settings.job_id(job)
|
2016-10-23 05:27:48 +03:00
|
|
|
if job_id in nocheck:
|
|
|
|
continue
|
|
|
|
try:
|
|
|
|
logger.debug('waiting for job {} to delete'.format(job_id))
|
|
|
|
while True:
|
2017-05-01 00:46:38 +03:00
|
|
|
batch_client.job.get(
|
|
|
|
job_id,
|
|
|
|
job_get_options=batchmodels.JobGetOptions(select='id')
|
|
|
|
)
|
2016-10-23 05:27:48 +03:00
|
|
|
time.sleep(1)
|
|
|
|
except batchmodels.batch_error.BatchErrorException as ex:
|
|
|
|
if 'The specified job does not exist' in ex.message.value:
|
2017-05-01 00:46:38 +03:00
|
|
|
logger.info('job {} does not exist'.format(job_id))
|
2016-10-23 05:27:48 +03:00
|
|
|
continue
|
2017-05-01 00:46:38 +03:00
|
|
|
else:
|
|
|
|
raise
|
2016-10-10 01:22:15 +03:00
|
|
|
|
|
|
|
|
2017-02-28 20:41:05 +03:00
|
|
|
def del_all_jobs(batch_client, config, termtasks=False, wait=False):
|
2016-10-27 07:39:00 +03:00
|
|
|
# type: (azure.batch.batch_service_client.BatchServiceClient, dict,
|
2017-02-28 20:41:05 +03:00
|
|
|
# bool, bool) -> None
|
2016-10-27 07:39:00 +03:00
|
|
|
"""Delete all jobs
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
:param dict config: configuration dict
|
2017-02-28 20:41:05 +03:00
|
|
|
:param bool termtasks: terminate tasks prior
|
2016-10-27 07:39:00 +03:00
|
|
|
:param bool wait: wait for jobs to delete
|
|
|
|
"""
|
|
|
|
check = set()
|
|
|
|
logger.debug('Getting list of all jobs...')
|
|
|
|
jobs = batch_client.job.list()
|
|
|
|
for job in jobs:
|
2016-11-01 06:49:24 +03:00
|
|
|
if not util.confirm_action(
|
2016-10-27 07:39:00 +03:00
|
|
|
config, 'delete {} job'.format(job.id)):
|
|
|
|
continue
|
2017-02-28 20:41:05 +03:00
|
|
|
if termtasks:
|
|
|
|
terminate_tasks(batch_client, config, jobid=job.id, wait=True)
|
2016-10-27 07:39:00 +03:00
|
|
|
logger.info('Deleting job: {}'.format(job.id))
|
|
|
|
batch_client.job.delete(job.id)
|
|
|
|
check.add(job.id)
|
|
|
|
if wait:
|
|
|
|
for job_id in check:
|
|
|
|
try:
|
|
|
|
logger.debug('waiting for job {} to delete'.format(job_id))
|
|
|
|
while True:
|
|
|
|
batch_client.job.get(job_id)
|
|
|
|
time.sleep(1)
|
|
|
|
except batchmodels.batch_error.BatchErrorException as ex:
|
2017-05-01 00:46:38 +03:00
|
|
|
if 'The specified job does not exist' not in ex.message.value:
|
|
|
|
raise
|
2016-10-27 07:39:00 +03:00
|
|
|
|
|
|
|
|
2016-10-28 20:08:59 +03:00
|
|
|
def del_tasks(batch_client, config, jobid=None, taskid=None, wait=False):
|
|
|
|
# type: (azure.batch.batch_service_client.BatchServiceClient, dict,
|
|
|
|
# str, str, bool) -> None
|
|
|
|
"""Delete tasks
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
:param dict config: configuration dict
|
|
|
|
:param str jobid: job id of task to terminate
|
|
|
|
:param str taskid: task id to terminate
|
|
|
|
:param bool wait: wait for task to terminate
|
|
|
|
"""
|
|
|
|
# first terminate tasks, force wait for completion
|
|
|
|
terminate_tasks(
|
|
|
|
batch_client, config, jobid=jobid, taskid=taskid, wait=True)
|
|
|
|
# proceed with deletion
|
|
|
|
if jobid is None:
|
2016-11-13 09:13:55 +03:00
|
|
|
jobs = settings.job_specifications(config)
|
2016-10-28 20:08:59 +03:00
|
|
|
else:
|
|
|
|
jobs = [{'id': jobid}]
|
|
|
|
nocheck = {}
|
|
|
|
for job in jobs:
|
2016-11-13 09:13:55 +03:00
|
|
|
job_id = settings.job_id(job)
|
2016-10-28 20:08:59 +03:00
|
|
|
nocheck[job_id] = set()
|
|
|
|
if taskid is None:
|
2017-05-02 08:16:48 +03:00
|
|
|
tasks = [
|
|
|
|
x.id for x in batch_client.task.list(
|
|
|
|
job_id,
|
|
|
|
task_list_options=batchmodels.TaskListOptions(select='id')
|
|
|
|
)
|
|
|
|
]
|
2016-10-28 20:08:59 +03:00
|
|
|
else:
|
|
|
|
tasks = [taskid]
|
|
|
|
for task in tasks:
|
2016-11-01 06:49:24 +03:00
|
|
|
if not util.confirm_action(
|
2016-10-28 20:08:59 +03:00
|
|
|
config, 'delete {} task in job {}'.format(
|
|
|
|
task, job_id)):
|
|
|
|
nocheck[job_id].add(task)
|
|
|
|
continue
|
|
|
|
logger.info('Deleting task: {}'.format(task))
|
|
|
|
batch_client.task.delete(job_id, task)
|
|
|
|
if wait:
|
|
|
|
for job in jobs:
|
2016-11-13 09:13:55 +03:00
|
|
|
job_id = settings.job_id(job)
|
2016-10-28 20:08:59 +03:00
|
|
|
if taskid is None:
|
2017-05-02 08:16:48 +03:00
|
|
|
tasks = [
|
|
|
|
x.id for x in batch_client.task.list(
|
|
|
|
job_id,
|
|
|
|
task_list_options=batchmodels.TaskListOptions(
|
|
|
|
select='id')
|
|
|
|
)
|
|
|
|
]
|
2016-10-28 20:08:59 +03:00
|
|
|
else:
|
|
|
|
tasks = [taskid]
|
|
|
|
for task in tasks:
|
|
|
|
try:
|
|
|
|
if task in nocheck[job_id]:
|
|
|
|
continue
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
try:
|
|
|
|
logger.debug(
|
|
|
|
'waiting for task {} in job {} to terminate'.format(
|
|
|
|
task, job_id))
|
|
|
|
while True:
|
2017-05-02 08:16:48 +03:00
|
|
|
batch_client.task.get(
|
|
|
|
job_id, task,
|
|
|
|
task_get_options=batchmodels.TaskGetOptions(
|
|
|
|
select='id')
|
|
|
|
)
|
2016-10-28 20:08:59 +03:00
|
|
|
time.sleep(1)
|
|
|
|
except batchmodels.batch_error.BatchErrorException as ex:
|
|
|
|
if 'The specified task does not exist' in ex.message.value:
|
2017-05-01 00:46:38 +03:00
|
|
|
logger.info('task {} in job {} does not exist'.format(
|
|
|
|
task, job_id))
|
2016-10-28 20:08:59 +03:00
|
|
|
continue
|
2017-05-01 00:46:38 +03:00
|
|
|
else:
|
|
|
|
raise
|
2016-10-28 20:08:59 +03:00
|
|
|
|
|
|
|
|
2016-10-10 01:22:15 +03:00
|
|
|
def clean_mi_jobs(batch_client, config):
|
|
|
|
# type: (azure.batch.batch_service_client.BatchServiceClient, dict) -> None
|
|
|
|
"""Clean up multi-instance jobs
|
|
|
|
:param batch_client: The batch client to use.
|
2016-10-14 00:11:52 +03:00
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
2016-10-10 01:22:15 +03:00
|
|
|
:param dict config: configuration dict
|
|
|
|
"""
|
2016-11-13 09:13:55 +03:00
|
|
|
for job in settings.job_specifications(config):
|
|
|
|
job_id = settings.job_id(job)
|
2017-05-24 19:54:09 +03:00
|
|
|
if not util.confirm_action(
|
|
|
|
config, 'cleanup {} job'.format(job_id)):
|
|
|
|
continue
|
2016-10-10 01:22:15 +03:00
|
|
|
cleanup_job_id = 'shipyardcleanup-' + job_id
|
|
|
|
cleanup_job = batchmodels.JobAddParameter(
|
|
|
|
id=cleanup_job_id,
|
|
|
|
pool_info=batchmodels.PoolInformation(
|
2016-11-13 09:13:55 +03:00
|
|
|
pool_id=settings.pool_id(config)),
|
2016-10-10 01:22:15 +03:00
|
|
|
)
|
|
|
|
try:
|
|
|
|
batch_client.job.add(cleanup_job)
|
|
|
|
logger.info('Added cleanup job: {}'.format(cleanup_job.id))
|
|
|
|
except batchmodels.batch_error.BatchErrorException as ex:
|
|
|
|
if 'The specified job already exists' not in ex.message.value:
|
|
|
|
raise
|
|
|
|
# get all cleanup tasks
|
2017-05-02 08:16:48 +03:00
|
|
|
cleanup_tasks = [
|
|
|
|
x.id for x in batch_client.task.list(
|
|
|
|
cleanup_job_id,
|
|
|
|
task_list_options=batchmodels.TaskListOptions(select='id')
|
|
|
|
)
|
|
|
|
]
|
2016-10-10 01:22:15 +03:00
|
|
|
# list all tasks in job
|
|
|
|
tasks = batch_client.task.list(job_id)
|
|
|
|
for task in tasks:
|
|
|
|
if (task.id in cleanup_tasks or
|
|
|
|
task.multi_instance_settings is None):
|
|
|
|
continue
|
|
|
|
# check if task is complete
|
|
|
|
if task.state == batchmodels.TaskState.completed:
|
|
|
|
name = task.multi_instance_settings.coordination_command_line.\
|
|
|
|
split('--name')[-1].split()[0]
|
|
|
|
# create cleanup task
|
|
|
|
batchtask = batchmodels.TaskAddParameter(
|
|
|
|
id=task.id,
|
|
|
|
multi_instance_settings=batchmodels.MultiInstanceSettings(
|
|
|
|
number_of_instances=task.
|
|
|
|
multi_instance_settings.number_of_instances,
|
2016-11-01 06:49:24 +03:00
|
|
|
coordination_command_line=util.
|
2016-10-10 01:22:15 +03:00
|
|
|
wrap_commands_in_shell([
|
|
|
|
'docker stop {}'.format(name),
|
|
|
|
'docker rm -v {}'.format(name),
|
|
|
|
'exit 0',
|
|
|
|
], wait=False),
|
|
|
|
),
|
|
|
|
command_line='/bin/sh -c "exit 0"',
|
2017-03-01 23:45:32 +03:00
|
|
|
user_identity=_RUN_ELEVATED,
|
2016-10-10 01:22:15 +03:00
|
|
|
)
|
|
|
|
batch_client.task.add(job_id=cleanup_job_id, task=batchtask)
|
|
|
|
logger.debug(
|
|
|
|
('Waiting for docker multi-instance clean up task {} '
|
|
|
|
'for job {} to complete').format(batchtask.id, job_id))
|
|
|
|
# wait for cleanup task to complete before adding another
|
|
|
|
while True:
|
|
|
|
batchtask = batch_client.task.get(
|
2017-05-02 08:16:48 +03:00
|
|
|
cleanup_job_id, batchtask.id,
|
|
|
|
task_get_options=batchmodels.TaskGetOptions(
|
|
|
|
select='id,state')
|
|
|
|
)
|
2016-10-10 01:22:15 +03:00
|
|
|
if batchtask.state == batchmodels.TaskState.completed:
|
|
|
|
break
|
|
|
|
time.sleep(1)
|
|
|
|
logger.info(
|
|
|
|
('Docker multi-instance clean up task {} for job {} '
|
|
|
|
'completed').format(batchtask.id, job_id))
|
|
|
|
|
|
|
|
|
|
|
|
def del_clean_mi_jobs(batch_client, config):
|
|
|
|
# type: (azure.batch.batch_service_client.BatchServiceClient, dict) -> None
|
|
|
|
"""Delete clean up multi-instance jobs
|
|
|
|
:param batch_client: The batch client to use.
|
2016-10-14 00:11:52 +03:00
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
2016-10-10 01:22:15 +03:00
|
|
|
:param dict config: configuration dict
|
|
|
|
"""
|
2016-11-13 09:13:55 +03:00
|
|
|
for job in settings.job_specifications(config):
|
|
|
|
job_id = settings.job_id(job)
|
2016-10-10 01:22:15 +03:00
|
|
|
cleanup_job_id = 'shipyardcleanup-' + job_id
|
|
|
|
logger.info('deleting job: {}'.format(cleanup_job_id))
|
|
|
|
try:
|
|
|
|
batch_client.job.delete(cleanup_job_id)
|
|
|
|
except batchmodels.batch_error.BatchErrorException:
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2017-02-28 20:41:05 +03:00
|
|
|
def terminate_jobs(
|
|
|
|
batch_client, config, jobid=None, termtasks=False, wait=False):
|
2016-10-27 07:39:00 +03:00
|
|
|
# type: (azure.batch.batch_service_client.BatchServiceClient, dict,
|
2017-02-28 20:41:05 +03:00
|
|
|
# str, bool, bool) -> None
|
2016-10-10 01:22:15 +03:00
|
|
|
"""Terminate jobs
|
|
|
|
:param batch_client: The batch client to use.
|
2016-10-14 00:11:52 +03:00
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
2016-10-10 01:22:15 +03:00
|
|
|
:param dict config: configuration dict
|
2016-10-28 20:08:59 +03:00
|
|
|
:param str jobid: job id to terminate
|
2017-02-28 20:41:05 +03:00
|
|
|
:param bool termtasks: terminate tasks manually prior
|
2016-10-27 07:39:00 +03:00
|
|
|
:param bool wait: wait for job to terminate
|
2016-10-10 01:22:15 +03:00
|
|
|
"""
|
2017-02-28 20:41:05 +03:00
|
|
|
if termtasks:
|
|
|
|
terminate_tasks(batch_client, config, jobid=jobid, wait=True)
|
2016-10-28 20:08:59 +03:00
|
|
|
if jobid is None:
|
2016-11-13 09:13:55 +03:00
|
|
|
jobs = settings.job_specifications(config)
|
2016-10-28 20:08:59 +03:00
|
|
|
else:
|
|
|
|
jobs = [{'id': jobid}]
|
2016-10-27 07:39:00 +03:00
|
|
|
nocheck = set()
|
2016-10-28 20:08:59 +03:00
|
|
|
for job in jobs:
|
2016-11-13 09:13:55 +03:00
|
|
|
job_id = settings.job_id(job)
|
2016-11-01 06:49:24 +03:00
|
|
|
if not util.confirm_action(
|
2016-10-10 01:22:15 +03:00
|
|
|
config, 'terminate {} job'.format(job_id)):
|
2016-10-27 07:39:00 +03:00
|
|
|
nocheck.add(job_id)
|
2016-10-10 01:22:15 +03:00
|
|
|
continue
|
|
|
|
logger.info('Terminating job: {}'.format(job_id))
|
|
|
|
batch_client.job.terminate(job_id)
|
2016-10-27 07:39:00 +03:00
|
|
|
if wait:
|
2016-10-28 20:08:59 +03:00
|
|
|
for job in jobs:
|
2016-11-13 09:13:55 +03:00
|
|
|
job_id = settings.job_id(job)
|
2016-10-27 07:39:00 +03:00
|
|
|
if job_id in nocheck:
|
|
|
|
continue
|
|
|
|
try:
|
|
|
|
logger.debug('waiting for job {} to terminate'.format(job_id))
|
|
|
|
while True:
|
|
|
|
_job = batch_client.job.get(job_id)
|
|
|
|
if _job.state == batchmodels.JobState.completed:
|
|
|
|
break
|
|
|
|
time.sleep(1)
|
|
|
|
except batchmodels.batch_error.BatchErrorException as ex:
|
2017-05-01 00:46:38 +03:00
|
|
|
if 'The specified job does not exist' not in ex.message.value:
|
|
|
|
raise
|
2016-10-10 01:22:15 +03:00
|
|
|
|
|
|
|
|
2017-02-28 20:41:05 +03:00
|
|
|
def terminate_all_jobs(batch_client, config, termtasks=False, wait=False):
|
2016-10-27 07:39:00 +03:00
|
|
|
# type: (azure.batch.batch_service_client.BatchServiceClient, dict,
|
2017-02-28 20:41:05 +03:00
|
|
|
# bool, bool) -> None
|
2016-10-27 07:39:00 +03:00
|
|
|
"""Terminate all jobs
|
2016-10-10 01:22:15 +03:00
|
|
|
:param batch_client: The batch client to use.
|
2016-10-14 00:11:52 +03:00
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
2016-10-10 01:22:15 +03:00
|
|
|
:param dict config: configuration dict
|
2017-02-28 20:41:05 +03:00
|
|
|
:param bool termtasks: terminate tasks prior
|
2016-10-27 07:39:00 +03:00
|
|
|
:param bool wait: wait for jobs to terminate
|
2016-10-10 01:22:15 +03:00
|
|
|
"""
|
2016-10-27 07:39:00 +03:00
|
|
|
check = set()
|
2016-10-10 01:22:15 +03:00
|
|
|
logger.debug('Getting list of all jobs...')
|
|
|
|
jobs = batch_client.job.list()
|
|
|
|
for job in jobs:
|
2016-11-01 06:49:24 +03:00
|
|
|
if not util.confirm_action(
|
2016-10-27 07:39:00 +03:00
|
|
|
config, 'terminate {} job'.format(job.id)):
|
2016-10-10 01:22:15 +03:00
|
|
|
continue
|
2017-02-28 20:41:05 +03:00
|
|
|
if termtasks:
|
|
|
|
terminate_tasks(batch_client, config, jobid=job.id, wait=True)
|
2016-10-27 07:39:00 +03:00
|
|
|
logger.info('Terminating job: {}'.format(job.id))
|
|
|
|
batch_client.job.terminate(job.id)
|
|
|
|
check.add(job.id)
|
|
|
|
if wait:
|
|
|
|
for job_id in check:
|
|
|
|
try:
|
2016-11-21 01:55:36 +03:00
|
|
|
logger.debug('waiting for job {} to terminate'.format(job_id))
|
2016-10-27 07:39:00 +03:00
|
|
|
while True:
|
|
|
|
_job = batch_client.job.get(job_id)
|
|
|
|
if _job.state == batchmodels.JobState.completed:
|
|
|
|
break
|
|
|
|
time.sleep(1)
|
|
|
|
except batchmodels.batch_error.BatchErrorException as ex:
|
2017-05-01 00:46:38 +03:00
|
|
|
if 'The specified job does not exist' not in ex.message.value:
|
|
|
|
raise
|
2016-10-10 01:22:15 +03:00
|
|
|
|
|
|
|
|
2016-10-28 20:08:59 +03:00
|
|
|
def _send_docker_kill_signal(
|
2016-11-22 10:56:07 +03:00
|
|
|
batch_client, config, username, ssh_private_key, pool_id, node_id,
|
|
|
|
job_id, task_id, task_is_mi):
|
|
|
|
# type: (azure.batch.batch_service_client.BatchServiceClient, dict, str,
|
2016-10-28 20:08:59 +03:00
|
|
|
# pathlib.Path, str, str, str, str, bool) -> None
|
|
|
|
"""Send docker kill signal via SSH
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
2016-11-22 10:56:07 +03:00
|
|
|
:param dict config: configuration dict
|
2016-10-28 20:08:59 +03:00
|
|
|
:param str username: SSH username
|
|
|
|
:param pathlib.Path ssh_private_key: SSH private key
|
|
|
|
:param str pool_id: pool_id of node
|
|
|
|
:param str node_id: node_id of node
|
|
|
|
:param str job_id: job id of task id to kill
|
|
|
|
:param str task_id: task id to kill
|
|
|
|
:param bool task_is_mi: task is multi-instance
|
|
|
|
"""
|
|
|
|
targets = [(pool_id, node_id)]
|
2016-11-23 01:27:33 +03:00
|
|
|
task_name = None
|
2016-10-28 20:08:59 +03:00
|
|
|
# if this task is multi-instance, get all subtasks
|
|
|
|
if task_is_mi:
|
|
|
|
subtasks = batch_client.task.list_subtasks(job_id, task_id)
|
|
|
|
for subtask in subtasks.value:
|
|
|
|
targets.append(
|
|
|
|
(subtask.node_info.pool_id, subtask.node_info.node_id))
|
2016-11-22 10:56:07 +03:00
|
|
|
# fetch container name
|
|
|
|
try:
|
|
|
|
jobs = settings.job_specifications(config)
|
|
|
|
for job in jobs:
|
|
|
|
if job_id == settings.job_id(job):
|
|
|
|
tasks = settings.job_tasks(job)
|
|
|
|
task_name = settings.task_name(tasks[0])
|
|
|
|
break
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
# TODO get task names for non-mi tasks?
|
|
|
|
if task_name is None:
|
|
|
|
task_name = '{}-{}'.format(job_id, task_id)
|
2016-10-28 20:08:59 +03:00
|
|
|
# for each task node target, issue docker kill
|
|
|
|
for target in targets:
|
|
|
|
rls = batch_client.compute_node.get_remote_login_settings(
|
|
|
|
target[0], target[1])
|
|
|
|
ssh_args = [
|
2017-03-11 20:21:33 +03:00
|
|
|
'ssh', '-o', 'StrictHostKeyChecking=no',
|
|
|
|
'-o', 'UserKnownHostsFile={}'.format(os.devnull),
|
|
|
|
'-i', str(ssh_private_key), '-p', str(rls.remote_login_port),
|
|
|
|
'-t', '{}@{}'.format(username, rls.remote_login_ip_address),
|
2016-11-22 10:56:07 +03:00
|
|
|
('sudo /bin/bash -c "docker kill {tn}; '
|
|
|
|
'docker ps -qa -f name={tn} | '
|
|
|
|
'xargs --no-run-if-empty docker rm -v"').format(tn=task_name)
|
2016-10-28 20:08:59 +03:00
|
|
|
]
|
2016-11-01 06:49:24 +03:00
|
|
|
rc = util.subprocess_with_output(ssh_args, shell=False)
|
2016-10-28 20:08:59 +03:00
|
|
|
if rc != 0:
|
|
|
|
logger.error('docker kill failed with return code: {}'.format(rc))
|
|
|
|
|
|
|
|
|
2016-11-01 09:12:14 +03:00
|
|
|
def terminate_tasks(
|
|
|
|
batch_client, config, jobid=None, taskid=None, wait=False,
|
|
|
|
force=False):
|
2016-10-28 20:08:59 +03:00
|
|
|
# type: (azure.batch.batch_service_client.BatchServiceClient, dict,
|
2016-11-01 09:12:14 +03:00
|
|
|
# str, str, bool, bool) -> None
|
2016-10-28 20:08:59 +03:00
|
|
|
"""Terminate tasks
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
:param dict config: configuration dict
|
|
|
|
:param str jobid: job id of task to terminate
|
|
|
|
:param str taskid: task id to terminate
|
|
|
|
:param bool wait: wait for task to terminate
|
2016-11-01 09:12:14 +03:00
|
|
|
:param bool force: force task docker kill signal regardless of state
|
2016-10-28 20:08:59 +03:00
|
|
|
"""
|
|
|
|
# get ssh login settings
|
2016-11-13 09:13:55 +03:00
|
|
|
pool = settings.pool_settings(config)
|
|
|
|
if util.is_none_or_empty(pool.ssh.username):
|
2016-10-28 20:08:59 +03:00
|
|
|
raise ValueError(
|
2016-11-13 09:13:55 +03:00
|
|
|
'cannot terminate docker container without an SSH username')
|
2017-04-13 23:13:06 +03:00
|
|
|
ssh_private_key = pool.ssh.ssh_private_key
|
|
|
|
if ssh_private_key is None:
|
|
|
|
ssh_private_key = pathlib.Path(
|
|
|
|
pool.ssh.generated_file_export_path, crypto.get_ssh_key_prefix())
|
2016-10-28 20:08:59 +03:00
|
|
|
if not ssh_private_key.exists():
|
2017-04-13 23:13:06 +03:00
|
|
|
raise RuntimeError('SSH private key file not found at: {}'.format(
|
2016-10-28 20:08:59 +03:00
|
|
|
ssh_private_key))
|
|
|
|
if jobid is None:
|
2016-11-13 09:13:55 +03:00
|
|
|
jobs = settings.job_specifications(config)
|
2016-10-28 20:08:59 +03:00
|
|
|
else:
|
|
|
|
jobs = [{'id': jobid}]
|
|
|
|
nocheck = {}
|
|
|
|
for job in jobs:
|
2016-11-13 09:13:55 +03:00
|
|
|
job_id = settings.job_id(job)
|
2016-10-28 20:08:59 +03:00
|
|
|
nocheck[job_id] = set()
|
|
|
|
if taskid is None:
|
2017-02-23 07:25:17 +03:00
|
|
|
tasks = [
|
|
|
|
x.id for x in batch_client.task.list(
|
|
|
|
job_id,
|
2017-05-02 08:16:48 +03:00
|
|
|
task_list_options=batchmodels.TaskListOptions(select='id')
|
|
|
|
)
|
|
|
|
]
|
2016-10-28 20:08:59 +03:00
|
|
|
else:
|
|
|
|
tasks = [taskid]
|
|
|
|
for task in tasks:
|
|
|
|
_task = batch_client.task.get(job_id, task)
|
|
|
|
# if completed, skip
|
2016-11-01 09:12:14 +03:00
|
|
|
if _task.state == batchmodels.TaskState.completed and not force:
|
2016-10-28 20:08:59 +03:00
|
|
|
logger.debug(
|
|
|
|
'Skipping termination of completed task {} on '
|
|
|
|
'job {}'.format(task, job_id))
|
|
|
|
nocheck[job_id].add(task)
|
|
|
|
continue
|
2016-11-01 06:49:24 +03:00
|
|
|
if not util.confirm_action(
|
2016-10-28 20:08:59 +03:00
|
|
|
config, 'terminate {} task in job {}'.format(
|
|
|
|
task, job_id)):
|
|
|
|
nocheck[job_id].add(task)
|
|
|
|
continue
|
|
|
|
logger.info('Terminating task: {}'.format(task))
|
|
|
|
# directly send docker kill signal if running
|
2016-11-01 09:12:14 +03:00
|
|
|
if _task.state == batchmodels.TaskState.running or force:
|
2016-10-28 20:08:59 +03:00
|
|
|
if (_task.multi_instance_settings is not None and
|
|
|
|
_task.multi_instance_settings.number_of_instances > 1):
|
|
|
|
task_is_mi = True
|
|
|
|
else:
|
|
|
|
task_is_mi = False
|
|
|
|
_send_docker_kill_signal(
|
2016-11-22 10:56:07 +03:00
|
|
|
batch_client, config, pool.ssh.username, ssh_private_key,
|
2016-10-28 20:08:59 +03:00
|
|
|
_task.node_info.pool_id, _task.node_info.node_id,
|
|
|
|
job_id, task, task_is_mi)
|
|
|
|
else:
|
|
|
|
batch_client.task.terminate(job_id, task)
|
|
|
|
if wait:
|
|
|
|
for job in jobs:
|
2016-11-13 09:13:55 +03:00
|
|
|
job_id = settings.job_id(job)
|
2016-10-28 20:08:59 +03:00
|
|
|
if taskid is None:
|
2017-02-23 07:25:17 +03:00
|
|
|
tasks = [
|
|
|
|
x.id for x in batch_client.task.list(
|
|
|
|
job_id,
|
|
|
|
task_list_options=batchmodels.TaskListOptions(
|
|
|
|
select='id'
|
|
|
|
)
|
2017-05-02 08:16:48 +03:00
|
|
|
)
|
|
|
|
]
|
2016-10-28 20:08:59 +03:00
|
|
|
else:
|
|
|
|
tasks = [taskid]
|
|
|
|
for task in tasks:
|
|
|
|
try:
|
|
|
|
if task in nocheck[job_id]:
|
|
|
|
continue
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
try:
|
|
|
|
logger.debug(
|
|
|
|
'waiting for task {} in job {} to terminate'.format(
|
|
|
|
task, job_id))
|
|
|
|
while True:
|
2017-05-02 08:16:48 +03:00
|
|
|
_task = batch_client.task.get(
|
|
|
|
job_id, task,
|
|
|
|
task_get_options=batchmodels.TaskGetOptions(
|
|
|
|
select='state')
|
|
|
|
)
|
2016-10-28 20:08:59 +03:00
|
|
|
if _task.state == batchmodels.TaskState.completed:
|
|
|
|
break
|
|
|
|
time.sleep(1)
|
|
|
|
except batchmodels.batch_error.BatchErrorException as ex:
|
2017-05-01 00:46:38 +03:00
|
|
|
if ('The specified task does not exist'
|
|
|
|
not in ex.message.value):
|
|
|
|
raise
|
2016-10-28 20:08:59 +03:00
|
|
|
|
|
|
|
|
2016-10-30 11:33:15 +03:00
|
|
|
def list_nodes(batch_client, config, nodes=None):
|
|
|
|
# type: (batch.BatchServiceClient, dict, list) -> None
|
2016-10-28 06:51:36 +03:00
|
|
|
"""Get a list of nodes
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
:param dict config: configuration dict
|
2016-10-30 11:33:15 +03:00
|
|
|
:param lsit nodes: list of nodes
|
2016-10-28 06:51:36 +03:00
|
|
|
"""
|
2016-11-13 09:13:55 +03:00
|
|
|
pool_id = settings.pool_id(config)
|
2016-10-28 06:51:36 +03:00
|
|
|
logger.debug('listing nodes for pool {}'.format(pool_id))
|
2016-10-30 11:33:15 +03:00
|
|
|
if nodes is None:
|
|
|
|
nodes = batch_client.compute_node.list(pool_id)
|
2016-10-28 06:51:36 +03:00
|
|
|
for node in nodes:
|
2016-11-07 02:07:33 +03:00
|
|
|
if node.errors is not None:
|
|
|
|
info = ' error=(code={} message={})'.format(
|
|
|
|
node.errors.code, node.errors.message)
|
|
|
|
else:
|
|
|
|
info = ''
|
|
|
|
if node.start_task_info is not None:
|
2017-05-13 00:40:06 +03:00
|
|
|
if node.start_task_info.failure_info is not None:
|
|
|
|
info += (' start_task_failure_info=(category={} code={} '
|
2016-11-07 02:07:33 +03:00
|
|
|
'message={})').format(
|
2017-05-13 00:40:06 +03:00
|
|
|
node.start_task_info.failure_info.category,
|
|
|
|
node.start_task_info.failure_info.code,
|
|
|
|
node.start_task_info.failure_info.message)
|
2016-11-07 02:07:33 +03:00
|
|
|
else:
|
|
|
|
info += ' start_task_exit_code={}'.format(
|
|
|
|
node.start_task_info.exit_code)
|
2016-10-28 06:51:36 +03:00
|
|
|
logger.info(
|
2016-11-07 02:07:33 +03:00
|
|
|
('node_id={} [state={}{} scheduling_state={} ip_address={} '
|
2017-05-13 05:23:29 +03:00
|
|
|
'vm_size={} dedicated={} total_tasks_run={} '
|
|
|
|
'running_tasks_count={} total_tasks_succeeded={}]').format(
|
2016-11-07 02:07:33 +03:00
|
|
|
node.id, node.state, info, node.scheduling_state,
|
2017-05-13 05:23:29 +03:00
|
|
|
node.ip_address, node.vm_size, node.is_dedicated,
|
|
|
|
node.total_tasks_run, node.running_tasks_count,
|
|
|
|
node.total_tasks_succeeded))
|
2016-10-28 06:51:36 +03:00
|
|
|
|
|
|
|
|
2016-10-10 01:22:15 +03:00
|
|
|
def get_remote_login_settings(batch_client, config, nodes=None):
|
2016-10-28 06:51:36 +03:00
|
|
|
# type: (batch.BatchServiceClient, dict, List[str]) -> dict
|
2016-10-10 01:22:15 +03:00
|
|
|
"""Get remote login settings
|
|
|
|
:param batch_client: The batch client to use.
|
2016-10-14 00:11:52 +03:00
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
2016-10-10 01:22:15 +03:00
|
|
|
:param dict config: configuration dict
|
|
|
|
:param list nodes: list of nodes
|
|
|
|
:rtype: dict
|
|
|
|
:return: dict of node id -> remote login settings
|
|
|
|
"""
|
2016-11-13 09:13:55 +03:00
|
|
|
pool_id = settings.pool_id(config)
|
2016-10-10 01:22:15 +03:00
|
|
|
if nodes is None:
|
|
|
|
nodes = batch_client.compute_node.list(pool_id)
|
|
|
|
ret = {}
|
|
|
|
for node in nodes:
|
|
|
|
rls = batch_client.compute_node.get_remote_login_settings(
|
|
|
|
pool_id, node.id)
|
|
|
|
logger.info('node {}: ip {} port {}'.format(
|
|
|
|
node.id, rls.remote_login_ip_address, rls.remote_login_port))
|
|
|
|
ret[node.id] = rls
|
|
|
|
return ret
|
|
|
|
|
|
|
|
|
2016-12-15 06:01:50 +03:00
|
|
|
def get_remote_login_setting_for_node(batch_client, config, cardinal, node_id):
|
|
|
|
# type: (batch.BatchServiceClient, dict, int, str) -> dict
|
|
|
|
"""Get remote login setting for a node
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
:param dict config: configuration dict
|
|
|
|
:param int cardinal: node cardinal number
|
|
|
|
:param str node_id: node id
|
|
|
|
:rtype: tuple
|
|
|
|
:return: ip, port
|
|
|
|
"""
|
|
|
|
pool_id = settings.pool_id(config)
|
|
|
|
if node_id is None:
|
|
|
|
if cardinal is None:
|
|
|
|
raise ValueError('cardinal is invalid with no node_id specified')
|
|
|
|
nodes = list(batch_client.compute_node.list(pool_id))
|
|
|
|
if cardinal >= len(nodes):
|
|
|
|
raise ValueError(
|
|
|
|
('cardinal value {} invalid for number of nodes {} in '
|
|
|
|
'pool {}').format(cardinal, len(nodes), pool_id))
|
|
|
|
node_id = nodes[cardinal].id
|
|
|
|
rls = batch_client.compute_node.get_remote_login_settings(
|
|
|
|
pool_id, node_id)
|
|
|
|
return rls.remote_login_ip_address, rls.remote_login_port
|
|
|
|
|
|
|
|
|
2016-11-04 00:27:43 +03:00
|
|
|
def stream_file_and_wait_for_task(
|
|
|
|
batch_client, config, filespec=None, disk=False):
|
|
|
|
# type: (batch.BatchServiceClient, dict, str, bool) -> None
|
2016-10-10 01:22:15 +03:00
|
|
|
"""Stream a file and wait for task to complete
|
|
|
|
:param batch_client: The batch client to use.
|
2016-10-14 00:11:52 +03:00
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
2016-11-04 00:27:43 +03:00
|
|
|
:param dict config: configuration dict
|
2016-10-28 06:51:36 +03:00
|
|
|
:param str filespec: filespec (jobid,taskid,filename)
|
2016-11-04 00:27:43 +03:00
|
|
|
:param bool disk: write to disk instead
|
2016-10-10 01:22:15 +03:00
|
|
|
"""
|
|
|
|
if filespec is None:
|
|
|
|
job_id = None
|
|
|
|
task_id = None
|
|
|
|
file = None
|
|
|
|
else:
|
2016-10-28 06:51:36 +03:00
|
|
|
job_id, task_id, file = filespec.split(',')
|
2016-10-10 01:22:15 +03:00
|
|
|
if job_id is None:
|
2016-11-01 06:49:24 +03:00
|
|
|
job_id = util.get_input('Enter job id: ')
|
2016-10-10 01:22:15 +03:00
|
|
|
if task_id is None:
|
2016-11-01 06:49:24 +03:00
|
|
|
task_id = util.get_input('Enter task id: ')
|
2016-10-10 01:22:15 +03:00
|
|
|
if file is None:
|
2016-11-01 06:49:24 +03:00
|
|
|
file = util.get_input(
|
2016-10-10 01:22:15 +03:00
|
|
|
'Enter task-relative file path to stream [stdout.txt]: ')
|
|
|
|
if file == '' or file is None:
|
|
|
|
file = 'stdout.txt'
|
|
|
|
# get first running task if specified
|
|
|
|
if task_id == '@FIRSTRUNNING':
|
|
|
|
logger.debug('attempting to get first running task in job {}'.format(
|
|
|
|
job_id))
|
|
|
|
while True:
|
|
|
|
tasks = batch_client.task.list(
|
|
|
|
job_id,
|
|
|
|
task_list_options=batchmodels.TaskListOptions(
|
|
|
|
filter='state eq \'running\'',
|
2017-05-02 08:16:48 +03:00
|
|
|
select='id,state',
|
2016-10-10 01:22:15 +03:00
|
|
|
),
|
|
|
|
)
|
|
|
|
for task in tasks:
|
|
|
|
task_id = task.id
|
|
|
|
break
|
|
|
|
if task_id == '@FIRSTRUNNING':
|
|
|
|
time.sleep(1)
|
|
|
|
else:
|
|
|
|
break
|
|
|
|
logger.debug('attempting to stream file {} from job={} task={}'.format(
|
|
|
|
file, job_id, task_id))
|
|
|
|
curr = 0
|
|
|
|
completed = False
|
2016-11-01 09:12:14 +03:00
|
|
|
notfound = 0
|
2016-11-04 00:27:43 +03:00
|
|
|
try:
|
|
|
|
fd = None
|
|
|
|
if disk:
|
|
|
|
fp = pathlib.Path(job_id, task_id, file)
|
|
|
|
if (fp.exists() and not util.confirm_action(
|
|
|
|
config, 'overwrite {}'.format(fp))):
|
|
|
|
return
|
|
|
|
fp.parent.mkdir(mode=0o750, parents=True, exist_ok=True)
|
|
|
|
logger.info('writing streamed data to disk: {}'.format(fp))
|
|
|
|
fd = fp.open('wb', buffering=0)
|
|
|
|
while True:
|
|
|
|
# get task file properties
|
|
|
|
try:
|
2017-03-01 23:45:32 +03:00
|
|
|
tfp = batch_client.file.get_properties_from_task(
|
2016-11-04 00:27:43 +03:00
|
|
|
job_id, task_id, file, raw=True)
|
|
|
|
except batchmodels.BatchErrorException as ex:
|
|
|
|
if ('The specified operation is not valid for the current '
|
|
|
|
'state of the resource.' in ex.message):
|
|
|
|
time.sleep(1)
|
|
|
|
continue
|
|
|
|
elif 'The specified file does not exist.' in ex.message:
|
|
|
|
notfound += 1
|
|
|
|
if notfound > 10:
|
|
|
|
raise
|
|
|
|
time.sleep(1)
|
|
|
|
continue
|
|
|
|
else:
|
2016-11-01 09:12:14 +03:00
|
|
|
raise
|
2016-11-04 00:27:43 +03:00
|
|
|
size = int(tfp.response.headers['Content-Length'])
|
2017-05-02 04:47:41 +03:00
|
|
|
if curr < size:
|
2016-11-04 00:27:43 +03:00
|
|
|
frag = batch_client.file.get_from_task(
|
|
|
|
job_id, task_id, file,
|
|
|
|
batchmodels.FileGetFromTaskOptions(
|
2017-05-02 04:47:41 +03:00
|
|
|
ocp_range='bytes={}-{}'.format(curr, size))
|
2016-11-04 00:27:43 +03:00
|
|
|
)
|
|
|
|
for f in frag:
|
|
|
|
if fd is not None:
|
|
|
|
fd.write(f)
|
|
|
|
else:
|
|
|
|
print(f.decode('utf8'), end='')
|
2017-05-02 04:47:41 +03:00
|
|
|
curr = size
|
2016-11-04 00:27:43 +03:00
|
|
|
elif completed:
|
|
|
|
if not disk:
|
|
|
|
print()
|
|
|
|
break
|
2017-05-02 08:16:48 +03:00
|
|
|
if not completed and curr == size:
|
|
|
|
task = batch_client.task.get(
|
|
|
|
job_id, task_id,
|
|
|
|
task_get_options=batchmodels.TaskGetOptions(
|
|
|
|
select='state')
|
|
|
|
)
|
2016-11-04 00:27:43 +03:00
|
|
|
if task.state == batchmodels.TaskState.completed:
|
|
|
|
completed = True
|
|
|
|
time.sleep(1)
|
|
|
|
finally:
|
|
|
|
if fd is not None:
|
|
|
|
fd.close()
|
2016-10-10 01:22:15 +03:00
|
|
|
|
|
|
|
|
|
|
|
def get_file_via_task(batch_client, config, filespec=None):
|
|
|
|
# type: (batch.BatchServiceClient, dict, str) -> None
|
|
|
|
"""Get a file task style
|
|
|
|
:param batch_client: The batch client to use.
|
2016-10-14 00:11:52 +03:00
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
2016-10-10 01:22:15 +03:00
|
|
|
:param dict config: configuration dict
|
2016-10-28 06:51:36 +03:00
|
|
|
:param str filespec: filespec (jobid,taskid,filename)
|
2016-10-10 01:22:15 +03:00
|
|
|
"""
|
|
|
|
if filespec is None:
|
|
|
|
job_id = None
|
|
|
|
task_id = None
|
|
|
|
file = None
|
|
|
|
else:
|
2016-10-28 06:51:36 +03:00
|
|
|
job_id, task_id, file = filespec.split(',')
|
2016-10-10 01:22:15 +03:00
|
|
|
if job_id is None:
|
2016-11-01 06:49:24 +03:00
|
|
|
job_id = util.get_input('Enter job id: ')
|
2016-10-10 01:22:15 +03:00
|
|
|
if task_id is None:
|
2016-11-01 06:49:24 +03:00
|
|
|
task_id = util.get_input('Enter task id: ')
|
2016-10-10 01:22:15 +03:00
|
|
|
if file is None:
|
2016-11-01 06:49:24 +03:00
|
|
|
file = util.get_input(
|
2016-10-10 01:22:15 +03:00
|
|
|
'Enter task-relative file path to retrieve [stdout.txt]: ')
|
|
|
|
if file == '' or file is None:
|
|
|
|
file = 'stdout.txt'
|
|
|
|
# get first running task if specified
|
|
|
|
if task_id == '@FIRSTRUNNING':
|
|
|
|
logger.debug('attempting to get first running task in job {}'.format(
|
|
|
|
job_id))
|
|
|
|
while True:
|
|
|
|
tasks = batch_client.task.list(
|
|
|
|
job_id,
|
|
|
|
task_list_options=batchmodels.TaskListOptions(
|
|
|
|
filter='state eq \'running\'',
|
2017-05-02 08:16:48 +03:00
|
|
|
select='id,state',
|
2016-10-10 01:22:15 +03:00
|
|
|
),
|
|
|
|
)
|
|
|
|
for task in tasks:
|
|
|
|
task_id = task.id
|
|
|
|
break
|
|
|
|
if task_id == '@FIRSTRUNNING':
|
|
|
|
time.sleep(1)
|
|
|
|
else:
|
|
|
|
break
|
|
|
|
# check if file exists on disk; a possible race condition here is
|
|
|
|
# understood
|
|
|
|
fp = pathlib.Path(pathlib.Path(file).name)
|
|
|
|
if (fp.exists() and
|
2016-11-01 06:49:24 +03:00
|
|
|
not util.confirm_action(
|
2016-10-10 01:22:15 +03:00
|
|
|
config, 'file overwrite of {}'.format(file))):
|
|
|
|
raise RuntimeError('file already exists: {}'.format(file))
|
|
|
|
logger.debug('attempting to retrieve file {} from job={} task={}'.format(
|
|
|
|
file, job_id, task_id))
|
|
|
|
stream = batch_client.file.get_from_task(job_id, task_id, file)
|
|
|
|
with fp.open('wb') as f:
|
2016-11-01 06:49:24 +03:00
|
|
|
for fdata in stream:
|
|
|
|
f.write(fdata)
|
2016-10-10 01:22:15 +03:00
|
|
|
logger.debug('file {} retrieved from job={} task={} bytes={}'.format(
|
|
|
|
file, job_id, task_id, fp.stat().st_size))
|
|
|
|
|
|
|
|
|
2016-10-14 00:11:52 +03:00
|
|
|
def get_all_files_via_task(batch_client, config, filespec=None):
|
|
|
|
# type: (batch.BatchServiceClient, dict, str) -> None
|
|
|
|
"""Get all files from a task
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
:param dict config: configuration dict
|
2016-10-28 06:51:36 +03:00
|
|
|
:param str filespec: filespec (jobid,taskid,include_pattern)
|
2016-10-14 00:11:52 +03:00
|
|
|
"""
|
|
|
|
if filespec is None:
|
|
|
|
job_id = None
|
|
|
|
task_id = None
|
2016-10-16 00:30:44 +03:00
|
|
|
incl = None
|
2016-10-14 00:11:52 +03:00
|
|
|
else:
|
2016-10-28 06:51:36 +03:00
|
|
|
job_id, task_id, incl = filespec.split(',')
|
2016-10-14 00:11:52 +03:00
|
|
|
if job_id is None:
|
2016-11-01 06:49:24 +03:00
|
|
|
job_id = util.get_input('Enter job id: ')
|
2016-10-14 00:11:52 +03:00
|
|
|
if task_id is None:
|
2016-11-01 06:49:24 +03:00
|
|
|
task_id = util.get_input('Enter task id: ')
|
2016-10-16 00:30:44 +03:00
|
|
|
if incl is None:
|
2016-11-01 06:49:24 +03:00
|
|
|
incl = util.get_input('Enter filter: ')
|
2016-10-29 21:00:31 +03:00
|
|
|
if incl is not None and len(incl) == 0:
|
|
|
|
incl = None
|
2016-10-14 00:11:52 +03:00
|
|
|
# get first running task if specified
|
|
|
|
if task_id == '@FIRSTRUNNING':
|
|
|
|
logger.debug('attempting to get first running task in job {}'.format(
|
|
|
|
job_id))
|
|
|
|
while True:
|
|
|
|
tasks = batch_client.task.list(
|
|
|
|
job_id,
|
|
|
|
task_list_options=batchmodels.TaskListOptions(
|
|
|
|
filter='state eq \'running\'',
|
2017-05-02 08:16:48 +03:00
|
|
|
select='id,state',
|
2016-10-14 00:11:52 +03:00
|
|
|
),
|
|
|
|
)
|
|
|
|
for task in tasks:
|
|
|
|
task_id = task.id
|
|
|
|
break
|
|
|
|
if task_id == '@FIRSTRUNNING':
|
|
|
|
time.sleep(1)
|
|
|
|
else:
|
|
|
|
break
|
|
|
|
# iterate through all files in task and download them
|
|
|
|
logger.debug('downloading files to {}/{}'.format(job_id, task_id))
|
|
|
|
files = batch_client.file.list_from_task(job_id, task_id, recursive=True)
|
|
|
|
i = 0
|
2016-10-21 07:18:31 +03:00
|
|
|
dirs_created = set('.')
|
2016-10-14 00:11:52 +03:00
|
|
|
for file in files:
|
|
|
|
if file.is_directory:
|
|
|
|
continue
|
2016-10-16 00:30:44 +03:00
|
|
|
if incl is not None and not fnmatch.fnmatch(file.name, incl):
|
|
|
|
continue
|
2016-10-14 00:11:52 +03:00
|
|
|
fp = pathlib.Path(job_id, task_id, file.name)
|
|
|
|
if str(fp.parent) not in dirs_created:
|
|
|
|
fp.parent.mkdir(mode=0o750, parents=True, exist_ok=True)
|
|
|
|
dirs_created.add(str(fp.parent))
|
2016-10-16 00:30:44 +03:00
|
|
|
stream = batch_client.file.get_from_task(job_id, task_id, file.name)
|
2016-10-14 00:11:52 +03:00
|
|
|
with fp.open('wb') as f:
|
2016-11-01 06:49:24 +03:00
|
|
|
for fdata in stream:
|
|
|
|
f.write(fdata)
|
2016-10-14 00:11:52 +03:00
|
|
|
i += 1
|
|
|
|
if i == 0:
|
2016-10-16 00:30:44 +03:00
|
|
|
logger.error('no files found for task {} job {} include={}'.format(
|
|
|
|
task_id, job_id, incl if incl is not None else ''))
|
2016-10-14 00:11:52 +03:00
|
|
|
else:
|
2016-10-16 00:30:44 +03:00
|
|
|
logger.info(
|
|
|
|
'all task files retrieved from job={} task={} include={}'.format(
|
|
|
|
job_id, task_id, incl if incl is not None else ''))
|
2016-10-27 07:39:00 +03:00
|
|
|
|
|
|
|
|
2016-10-28 06:51:36 +03:00
|
|
|
def get_all_files_via_node(batch_client, config, filespec=None):
|
2016-10-27 07:39:00 +03:00
|
|
|
# type: (batch.BatchServiceClient, dict, str) -> None
|
|
|
|
"""Get a file node style
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
:param dict config: configuration dict
|
2016-10-28 06:51:36 +03:00
|
|
|
:param str filespec: filespec (nodeid,include_pattern)
|
2016-10-27 07:39:00 +03:00
|
|
|
"""
|
2016-10-28 06:51:36 +03:00
|
|
|
if filespec is None:
|
|
|
|
node_id = None
|
|
|
|
incl = None
|
|
|
|
else:
|
|
|
|
node_id, incl = filespec.split(',')
|
|
|
|
if node_id is None:
|
2016-11-01 06:49:24 +03:00
|
|
|
node_id = util.get_input('Enter node id: ')
|
2016-10-28 06:51:36 +03:00
|
|
|
if incl is None:
|
2016-11-01 06:49:24 +03:00
|
|
|
incl = util.get_input('Enter filter: ')
|
2016-10-27 07:39:00 +03:00
|
|
|
if node_id is None or len(node_id) == 0:
|
|
|
|
raise ValueError('node id is invalid')
|
2016-10-29 21:00:31 +03:00
|
|
|
if incl is not None and len(incl) == 0:
|
|
|
|
incl = None
|
2016-11-13 09:13:55 +03:00
|
|
|
pool_id = settings.pool_id(config)
|
2016-10-27 07:39:00 +03:00
|
|
|
logger.debug('downloading files to {}/{}'.format(pool_id, node_id))
|
|
|
|
files = batch_client.file.list_from_compute_node(
|
|
|
|
pool_id, node_id, recursive=True)
|
|
|
|
i = 0
|
|
|
|
dirs_created = set('.')
|
|
|
|
for file in files:
|
|
|
|
if file.is_directory:
|
|
|
|
continue
|
|
|
|
if incl is not None and not fnmatch.fnmatch(file.name, incl):
|
|
|
|
continue
|
|
|
|
fp = pathlib.Path(pool_id, node_id, file.name)
|
|
|
|
if str(fp.parent) not in dirs_created:
|
|
|
|
fp.parent.mkdir(mode=0o750, parents=True, exist_ok=True)
|
|
|
|
dirs_created.add(str(fp.parent))
|
|
|
|
stream = batch_client.file.get_from_compute_node(
|
|
|
|
pool_id, node_id, file.name)
|
|
|
|
with fp.open('wb') as f:
|
2016-11-01 06:49:24 +03:00
|
|
|
for fdata in stream:
|
|
|
|
f.write(fdata)
|
2016-10-27 07:39:00 +03:00
|
|
|
i += 1
|
|
|
|
if i == 0:
|
|
|
|
logger.error('no files found for pool {} node {} include={}'.format(
|
|
|
|
pool_id, node_id, incl if incl is not None else ''))
|
|
|
|
else:
|
|
|
|
logger.info(
|
|
|
|
'all files retrieved from pool={} node={} include={}'.format(
|
|
|
|
pool_id, node_id, incl if incl is not None else ''))
|
2016-10-14 00:11:52 +03:00
|
|
|
|
|
|
|
|
2016-10-28 06:51:36 +03:00
|
|
|
def get_file_via_node(batch_client, config, filespec=None):
|
2016-10-10 01:22:15 +03:00
|
|
|
# type: (batch.BatchServiceClient, dict, str) -> None
|
|
|
|
"""Get a file node style
|
|
|
|
:param batch_client: The batch client to use.
|
2016-10-14 00:11:52 +03:00
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
2016-10-10 01:22:15 +03:00
|
|
|
:param dict config: configuration dict
|
2016-10-28 06:51:36 +03:00
|
|
|
:param str filespec: filespec (nodeid,filename)
|
2016-10-10 01:22:15 +03:00
|
|
|
"""
|
2016-10-28 06:51:36 +03:00
|
|
|
if filespec is None:
|
|
|
|
node_id = None
|
|
|
|
file = None
|
|
|
|
else:
|
|
|
|
node_id, file = filespec.split(',')
|
|
|
|
if node_id is None:
|
2016-11-01 06:49:24 +03:00
|
|
|
node_id = util.get_input('Enter node id: ')
|
2016-10-28 06:51:36 +03:00
|
|
|
if file is None:
|
2016-11-01 06:49:24 +03:00
|
|
|
file = util.get_input(
|
2016-10-28 06:51:36 +03:00
|
|
|
'Enter node-relative file path to retrieve: ')
|
2016-10-10 01:22:15 +03:00
|
|
|
if node_id is None or len(node_id) == 0:
|
|
|
|
raise ValueError('node id is invalid')
|
|
|
|
if file == '' or file is None:
|
|
|
|
raise RuntimeError('specified invalid file to retrieve')
|
2016-11-13 09:13:55 +03:00
|
|
|
pool_id = settings.pool_id(config)
|
2016-10-10 01:22:15 +03:00
|
|
|
# check if file exists on disk; a possible race condition here is
|
|
|
|
# understood
|
|
|
|
fp = pathlib.Path(pathlib.Path(file).name)
|
|
|
|
if (fp.exists() and
|
2016-11-01 06:49:24 +03:00
|
|
|
not util.confirm_action(
|
2016-10-10 01:22:15 +03:00
|
|
|
config, 'file overwrite of {}'.format(file))):
|
|
|
|
raise RuntimeError('file already exists: {}'.format(file))
|
|
|
|
logger.debug('attempting to retrieve file {} from pool={} node={}'.format(
|
|
|
|
file, pool_id, node_id))
|
|
|
|
stream = batch_client.file.get_from_compute_node(pool_id, node_id, file)
|
|
|
|
with fp.open('wb') as f:
|
2016-11-01 06:49:24 +03:00
|
|
|
for fdata in stream:
|
|
|
|
f.write(fdata)
|
2016-10-10 01:22:15 +03:00
|
|
|
logger.debug('file {} retrieved from pool={} node={} bytes={}'.format(
|
|
|
|
file, pool_id, node_id, fp.stat().st_size))
|
2016-10-10 07:01:11 +03:00
|
|
|
|
|
|
|
|
2016-10-14 00:11:52 +03:00
|
|
|
def list_jobs(batch_client, config):
|
|
|
|
# type: (azure.batch.batch_service_client.BatchServiceClient, dict) -> None
|
|
|
|
"""List all jobs
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
:param dict config: configuration dict
|
|
|
|
"""
|
|
|
|
jobs = batch_client.job.list()
|
|
|
|
i = 0
|
|
|
|
for job in jobs:
|
|
|
|
logger.info('job_id={} [state={} pool_id={}]'.format(
|
|
|
|
job.id, job.state, job.pool_info.pool_id))
|
|
|
|
i += 1
|
|
|
|
if i == 0:
|
|
|
|
logger.error('no jobs found')
|
|
|
|
|
|
|
|
|
2016-11-09 22:59:01 +03:00
|
|
|
def list_tasks(batch_client, config, jobid=None):
|
|
|
|
# type: (azure.batch.batch_service_client.BatchServiceClient, dict,
|
2017-05-23 05:45:25 +03:00
|
|
|
# str, bool) -> bool
|
2016-10-14 00:11:52 +03:00
|
|
|
"""List tasks for specified jobs
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
:param dict config: configuration dict
|
2016-11-09 22:59:01 +03:00
|
|
|
:param str jobid: job id to list tasks from
|
2017-05-23 05:45:25 +03:00
|
|
|
:rtype: bool
|
|
|
|
:return: if all tasks have completed under job(s)
|
2016-10-14 00:11:52 +03:00
|
|
|
"""
|
2017-05-23 05:45:25 +03:00
|
|
|
all_complete = True
|
2017-04-30 08:16:35 +03:00
|
|
|
if util.is_none_or_empty(jobid):
|
|
|
|
jobs = settings.job_specifications(config)
|
|
|
|
else:
|
|
|
|
jobs = [{'id': jobid}]
|
|
|
|
for job in jobs:
|
|
|
|
jobid = settings.job_id(job)
|
2016-10-14 00:11:52 +03:00
|
|
|
i = 0
|
|
|
|
try:
|
2017-04-30 08:16:35 +03:00
|
|
|
tasks = batch_client.task.list(jobid)
|
2016-10-14 00:11:52 +03:00
|
|
|
for task in tasks:
|
2016-10-28 06:51:36 +03:00
|
|
|
if task.execution_info is not None:
|
2017-05-13 00:40:06 +03:00
|
|
|
if task.execution_info.failure_info is not None:
|
|
|
|
ei = (' failure_info=(category={} code={} '
|
2016-10-28 06:51:36 +03:00
|
|
|
'message={})').format(
|
2017-05-13 00:40:06 +03:00
|
|
|
task.execution_info.failure_info.category,
|
|
|
|
task.execution_info.failure_info.code,
|
|
|
|
task.execution_info.failure_info.message)
|
2016-10-28 06:51:36 +03:00
|
|
|
else:
|
2016-11-23 20:06:37 +03:00
|
|
|
if (task.execution_info.end_time is not None and
|
|
|
|
task.execution_info.start_time is not None):
|
|
|
|
duration = (task.execution_info.end_time -
|
|
|
|
task.execution_info.start_time)
|
|
|
|
else:
|
|
|
|
duration = 'n/a'
|
|
|
|
ei = (' start_time={} end_time={} duration={} '
|
2016-10-28 06:51:36 +03:00
|
|
|
'exit_code={}').format(
|
|
|
|
task.execution_info.start_time,
|
|
|
|
task.execution_info.end_time,
|
2016-11-23 20:06:37 +03:00
|
|
|
duration,
|
2016-10-28 06:51:36 +03:00
|
|
|
task.execution_info.exit_code)
|
|
|
|
else:
|
|
|
|
ei = ''
|
2017-01-26 19:22:02 +03:00
|
|
|
some_extra_info = ('none', 'none', ei)
|
|
|
|
if task.node_info is not None:
|
|
|
|
some_extra_info = (
|
|
|
|
task.node_info.pool_id,
|
|
|
|
task.node_info.node_id, ei)
|
2016-10-14 00:11:52 +03:00
|
|
|
logger.info(
|
2017-01-31 20:31:21 +03:00
|
|
|
'job_id={} task_id={} [state={} max_retries={} '
|
|
|
|
'retention_time={} pool_id={} node_id={}{}]'.format(
|
2017-04-30 08:16:35 +03:00
|
|
|
jobid, task.id, task.state,
|
2017-01-31 20:31:21 +03:00
|
|
|
task.constraints.max_task_retry_count,
|
|
|
|
task.constraints.retention_time, *some_extra_info))
|
2017-05-23 05:45:25 +03:00
|
|
|
if task.state != batchmodels.TaskState.completed:
|
|
|
|
all_complete = False
|
2016-10-14 00:11:52 +03:00
|
|
|
i += 1
|
|
|
|
except batchmodels.batch_error.BatchErrorException as ex:
|
|
|
|
if 'The specified job does not exist' in ex.message.value:
|
2017-04-30 08:16:35 +03:00
|
|
|
logger.error('{} job does not exist'.format(jobid))
|
2017-05-01 00:46:38 +03:00
|
|
|
continue
|
|
|
|
else:
|
|
|
|
raise
|
2016-10-14 00:11:52 +03:00
|
|
|
if i == 0:
|
2017-04-30 08:16:35 +03:00
|
|
|
logger.error('no tasks found for job {}'.format(jobid))
|
2017-05-23 05:45:25 +03:00
|
|
|
return all_complete
|
2016-10-14 00:11:52 +03:00
|
|
|
|
|
|
|
|
2016-11-09 02:06:40 +03:00
|
|
|
def list_task_files(batch_client, config, jobid=None, taskid=None):
|
|
|
|
# type: (azure.batch.batch_service_client.BatchServiceClient, dict,
|
|
|
|
# str, str) -> None
|
2016-10-14 00:11:52 +03:00
|
|
|
"""List task files for specified jobs
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
:param dict config: configuration dict
|
2016-11-09 02:06:40 +03:00
|
|
|
:param str jobid: job id to list
|
|
|
|
:param str taskid: task id to list
|
2016-10-14 00:11:52 +03:00
|
|
|
"""
|
2017-04-30 08:16:35 +03:00
|
|
|
if util.is_none_or_empty(jobid):
|
|
|
|
jobs = settings.job_specifications(config)
|
|
|
|
else:
|
|
|
|
jobs = [{'id': jobid}]
|
|
|
|
for job in jobs:
|
|
|
|
jobid = settings.job_id(job)
|
2016-10-14 00:11:52 +03:00
|
|
|
i = 0
|
|
|
|
try:
|
2017-04-30 08:16:35 +03:00
|
|
|
tasks = batch_client.task.list(
|
|
|
|
jobid,
|
|
|
|
task_list_options=batchmodels.TaskListOptions(select='id'))
|
2016-10-14 00:11:52 +03:00
|
|
|
for task in tasks:
|
2016-11-09 02:06:40 +03:00
|
|
|
if taskid is not None and taskid != task.id:
|
|
|
|
continue
|
2016-10-14 00:11:52 +03:00
|
|
|
j = 0
|
|
|
|
files = batch_client.file.list_from_task(
|
2017-04-30 08:16:35 +03:00
|
|
|
jobid, taskid, recursive=True)
|
2016-10-14 00:11:52 +03:00
|
|
|
for file in files:
|
|
|
|
if file.is_directory:
|
|
|
|
continue
|
|
|
|
logger.info(
|
|
|
|
'task_id={} file={} [job_id={} lmt={} '
|
|
|
|
'bytes={}]'.format(
|
2017-04-30 08:16:35 +03:00
|
|
|
taskid, file.name, jobid,
|
2016-10-14 00:11:52 +03:00
|
|
|
file.properties.last_modified,
|
|
|
|
file.properties.content_length))
|
|
|
|
j += 1
|
|
|
|
if j == 0:
|
|
|
|
logger.error('no files found for task {} job {}'.format(
|
2017-04-30 08:16:35 +03:00
|
|
|
taskid, jobid))
|
2016-10-14 00:11:52 +03:00
|
|
|
i += 1
|
|
|
|
except batchmodels.batch_error.BatchErrorException as ex:
|
|
|
|
if 'The specified job does not exist' in ex.message.value:
|
2017-04-30 08:16:35 +03:00
|
|
|
logger.error('{} job does not exist'.format(jobid))
|
2017-05-01 00:46:38 +03:00
|
|
|
continue
|
|
|
|
else:
|
|
|
|
raise
|
2016-10-14 00:11:52 +03:00
|
|
|
if i == 0:
|
2017-04-30 08:16:35 +03:00
|
|
|
logger.error('no tasks found for job {}'.format(jobid))
|
2016-10-14 00:11:52 +03:00
|
|
|
|
|
|
|
|
2017-05-13 00:40:06 +03:00
|
|
|
def generate_docker_login_settings(config, for_ssh=False):
|
|
|
|
# type: (dict, bool) -> tuple
|
2017-05-03 06:11:24 +03:00
|
|
|
"""Generate docker login environment variables and command line
|
|
|
|
for login/re-login
|
|
|
|
:param dict config: configuration object
|
2017-05-13 00:40:06 +03:00
|
|
|
:param bool for_ssh: for direct SSH use
|
2017-05-03 06:11:24 +03:00
|
|
|
:rtype: tuple
|
|
|
|
:return: (env vars, login cmds)
|
|
|
|
"""
|
|
|
|
# get private registry settings
|
|
|
|
preg = settings.docker_registry_private_settings(config)
|
|
|
|
# get encryption settings
|
|
|
|
encrypt = settings.batch_shipyard_encryption_enabled(config)
|
|
|
|
# populate command and env vars
|
|
|
|
cmd = []
|
|
|
|
env = []
|
|
|
|
if preg.server:
|
|
|
|
env.append(
|
|
|
|
batchmodels.EnvironmentSetting(
|
|
|
|
'DOCKER_LOGIN_SERVER', preg.server)
|
|
|
|
)
|
|
|
|
env.append(
|
|
|
|
batchmodels.EnvironmentSetting(
|
|
|
|
'DOCKER_LOGIN_USERNAME', preg.user)
|
|
|
|
)
|
|
|
|
env.append(
|
|
|
|
batchmodels.EnvironmentSetting(
|
|
|
|
'DOCKER_LOGIN_PASSWORD',
|
|
|
|
crypto.encrypt_string(encrypt, preg.password, config))
|
|
|
|
)
|
|
|
|
if encrypt:
|
|
|
|
cmd.append(
|
|
|
|
'DOCKER_LOGIN_PASSWORD='
|
|
|
|
'`echo $DOCKER_LOGIN_PASSWORD | base64 -d | '
|
|
|
|
'openssl rsautl -decrypt -inkey '
|
|
|
|
'$AZ_BATCH_NODE_STARTUP_DIR/certs/key.pem`')
|
|
|
|
cmd.append(
|
|
|
|
'docker login -u $DOCKER_LOGIN_USERNAME '
|
|
|
|
'-p $DOCKER_LOGIN_PASSWORD $DOCKER_LOGIN_SERVER')
|
|
|
|
else:
|
|
|
|
hubuser, hubpw = settings.docker_registry_login(config, 'hub')
|
|
|
|
if hubuser:
|
|
|
|
env.append(
|
|
|
|
batchmodels.EnvironmentSetting(
|
|
|
|
'DOCKER_LOGIN_USERNAME', hubuser)
|
|
|
|
)
|
|
|
|
env.append(
|
|
|
|
batchmodels.EnvironmentSetting(
|
|
|
|
'DOCKER_LOGIN_PASSWORD',
|
|
|
|
crypto.encrypt_string(encrypt, hubpw, config))
|
|
|
|
)
|
|
|
|
if encrypt:
|
|
|
|
cmd.append(
|
|
|
|
'DOCKER_LOGIN_PASSWORD='
|
|
|
|
'`echo $DOCKER_LOGIN_PASSWORD | base64 -d | '
|
|
|
|
'openssl rsautl -decrypt -inkey '
|
|
|
|
'$AZ_BATCH_NODE_STARTUP_DIR/certs/key.pem`')
|
|
|
|
cmd.append(
|
|
|
|
'docker login -u $DOCKER_LOGIN_USERNAME '
|
|
|
|
'-p $DOCKER_LOGIN_PASSWORD')
|
2017-05-13 00:40:06 +03:00
|
|
|
# transform env and cmd into single command for ssh
|
2017-05-29 00:57:36 +03:00
|
|
|
if for_ssh and len(cmd) > 0:
|
2017-06-30 20:20:19 +03:00
|
|
|
srv = None
|
|
|
|
for ev in env:
|
|
|
|
if ev.name == 'DOCKER_LOGIN_PASSWORD':
|
|
|
|
pw = ev.value
|
|
|
|
elif ev.name == 'DOCKER_LOGIN_USERNAME':
|
|
|
|
user = ev.value
|
|
|
|
elif ev.name == 'DOCKER_LOGIN_SERVER':
|
|
|
|
srv = ev.value
|
2017-05-13 00:40:06 +03:00
|
|
|
key = '${}'.format('DOCKER_LOGIN_PASSWORD')
|
2017-05-29 00:57:36 +03:00
|
|
|
if encrypt:
|
2017-06-30 20:20:19 +03:00
|
|
|
pw = cmd[0][22:].replace(key, pw)
|
2017-05-29 00:57:36 +03:00
|
|
|
cmd = cmd[1].replace(key, pw)
|
|
|
|
else:
|
|
|
|
cmd = cmd[0].replace(key, pw)
|
2017-05-13 00:40:06 +03:00
|
|
|
key = '${}'.format('DOCKER_LOGIN_USERNAME')
|
2017-06-30 20:20:19 +03:00
|
|
|
cmd = cmd.replace(key, user)
|
|
|
|
if util.is_not_empty(srv):
|
|
|
|
key = '${}'.format('DOCKER_LOGIN_SERVER')
|
|
|
|
cmd = cmd.replace(key, srv)
|
2017-05-29 00:57:36 +03:00
|
|
|
if encrypt:
|
|
|
|
key = 'openssl'
|
|
|
|
if key in cmd:
|
|
|
|
cmd = cmd.replace(key, 'sudo {}'.format(key))
|
|
|
|
key = '$AZ_BATCH_NODE_STARTUP_DIR'
|
|
|
|
if key in cmd:
|
|
|
|
start_mnt = '/'.join((
|
|
|
|
settings.temp_disk_mountpoint(config), 'batch', 'tasks',
|
|
|
|
'startup',
|
|
|
|
))
|
|
|
|
cmd = cmd.replace(key, start_mnt)
|
2017-05-13 00:40:06 +03:00
|
|
|
return None, [cmd]
|
2017-05-03 06:11:24 +03:00
|
|
|
return env, cmd
|
|
|
|
|
|
|
|
|
2017-05-18 04:13:24 +03:00
|
|
|
def _format_generic_task_id(tasknum):
|
|
|
|
# type: (int) -> str
|
|
|
|
"""Format a generic task id from a task number
|
|
|
|
:param int tasknum: task number
|
|
|
|
:rtype: str
|
|
|
|
:return: generic task id
|
|
|
|
"""
|
|
|
|
if tasknum > 99999:
|
|
|
|
return '{}{}'.format(_GENERIC_DOCKER_TASK_PREFIX, tasknum)
|
|
|
|
else:
|
|
|
|
return '{0}{1:05d}'.format(_GENERIC_DOCKER_TASK_PREFIX, tasknum)
|
|
|
|
|
|
|
|
|
|
|
|
def _generate_next_generic_task_id(
|
2017-05-23 05:45:25 +03:00
|
|
|
batch_client, job_id, tasklist=None, reserved=None, task_map=None,
|
|
|
|
last_task_id=None):
|
2016-10-28 09:41:47 +03:00
|
|
|
# type: (azure.batch.batch_service_client.BatchServiceClient, str,
|
2017-05-23 05:45:25 +03:00
|
|
|
# list, str, dict, str) -> Tuple[list, str]
|
2016-10-28 09:41:47 +03:00
|
|
|
"""Generate the next generic task id
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
:param str job_id: job id
|
2017-05-18 04:13:24 +03:00
|
|
|
:param list tasklist: list of current (committed) tasks in job
|
2016-10-28 09:41:47 +03:00
|
|
|
:param str reserved: reserved task id
|
2017-05-18 04:13:24 +03:00
|
|
|
:param dict task_map: map of pending tasks to add to the job
|
2017-05-23 05:45:25 +03:00
|
|
|
:param str last_task_id: last task id
|
2017-05-18 04:13:24 +03:00
|
|
|
:rtype: tuple
|
|
|
|
:return: (list of committed task ids for job, next generic docker task id)
|
2016-10-28 09:41:47 +03:00
|
|
|
"""
|
|
|
|
# get filtered, sorted list of generic docker task ids
|
|
|
|
try:
|
2017-05-18 04:13:24 +03:00
|
|
|
if util.is_none_or_empty(tasklist):
|
|
|
|
tasklist = batch_client.task.list(
|
|
|
|
job_id,
|
|
|
|
task_list_options=batchmodels.TaskListOptions(
|
|
|
|
filter='startswith(id, \'{}\')'.format(
|
|
|
|
_GENERIC_DOCKER_TASK_PREFIX),
|
|
|
|
select='id'))
|
|
|
|
tasklist = list(tasklist)
|
2017-01-26 01:49:23 +03:00
|
|
|
tasknum = sorted([int(x.id.split('-')[-1]) for x in tasklist])[-1] + 1
|
2016-10-28 09:41:47 +03:00
|
|
|
except (batchmodels.batch_error.BatchErrorException, IndexError):
|
|
|
|
tasknum = 0
|
|
|
|
if reserved is not None:
|
|
|
|
tasknum_reserved = int(reserved.split('-')[-1])
|
|
|
|
while tasknum == tasknum_reserved:
|
|
|
|
tasknum += 1
|
2017-05-18 04:13:24 +03:00
|
|
|
id = _format_generic_task_id(tasknum)
|
|
|
|
if task_map is not None:
|
|
|
|
while id in task_map:
|
2017-05-23 05:45:25 +03:00
|
|
|
try:
|
|
|
|
if (last_task_id is not None and
|
|
|
|
last_task_id.startswith(_GENERIC_DOCKER_TASK_PREFIX)):
|
|
|
|
tasknum = int(last_task_id.split('-')[-1])
|
|
|
|
last_task_id = None
|
|
|
|
except Exception:
|
|
|
|
last_task_id = None
|
2017-05-18 04:13:24 +03:00
|
|
|
tasknum += 1
|
|
|
|
id = _format_generic_task_id(tasknum)
|
|
|
|
return tasklist, id
|
|
|
|
|
|
|
|
|
|
|
|
def _add_task_collection(batch_client, job_id, task_map):
|
|
|
|
# type: (batch.BatchServiceClient, str, dict) -> None
|
|
|
|
"""Add a collection of tasks to a job
|
|
|
|
:param batch_client: The batch client to use.
|
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
|
|
|
:param str job_id: job to add to
|
|
|
|
:param dict task_map: task collection map to add
|
|
|
|
"""
|
|
|
|
all_tasks = list(task_map.values())
|
|
|
|
start = 0
|
|
|
|
slice = 100 # can only submit up to 100 tasks at a time
|
|
|
|
while True:
|
|
|
|
end = start + slice
|
|
|
|
if end > len(all_tasks):
|
|
|
|
end = len(all_tasks)
|
|
|
|
chunk = all_tasks[start:end]
|
|
|
|
logger.debug('submitting {} tasks ({} -> {}) to job {}'.format(
|
|
|
|
len(chunk), start, end - 1, job_id))
|
|
|
|
try:
|
|
|
|
results = batch_client.task.add_collection(job_id, chunk)
|
|
|
|
except batchmodels.BatchErrorException as e:
|
|
|
|
if e.error.code == 'RequestBodyTooLarge':
|
|
|
|
# collection contents are too large, reduce and retry
|
|
|
|
if slice == 1:
|
|
|
|
raise
|
|
|
|
slice = slice >> 1
|
|
|
|
if slice < 1:
|
|
|
|
slice = 1
|
|
|
|
logger.error(
|
|
|
|
('task collection slice was too big, retrying with '
|
|
|
|
'slice={}').format(slice))
|
|
|
|
continue
|
|
|
|
else:
|
|
|
|
# go through result and retry just failed tasks
|
|
|
|
while True:
|
|
|
|
retry = []
|
|
|
|
for result in results.value:
|
|
|
|
if result.status == batchmodels.TaskAddStatus.client_error:
|
|
|
|
logger.error(
|
|
|
|
('skipping retry of adding task {} as it '
|
|
|
|
'returned a client error (code={} message={}) '
|
|
|
|
'for job {}').format(
|
|
|
|
result.task_id, result.error.code,
|
|
|
|
result.error.message, job_id))
|
|
|
|
elif (result.status ==
|
|
|
|
batchmodels.TaskAddStatus.server_error):
|
|
|
|
retry.append(task_map[result.task_id])
|
|
|
|
if len(retry) > 0:
|
|
|
|
logger.debug('retrying adding {} tasks to job {}'.format(
|
|
|
|
len(retry), job_id))
|
|
|
|
results = batch_client.task.add_collection(job_id, retry)
|
|
|
|
else:
|
|
|
|
break
|
|
|
|
if end == len(all_tasks):
|
|
|
|
break
|
|
|
|
start += slice
|
|
|
|
slice = 100
|
|
|
|
logger.info('submitted all {} tasks to job {}'.format(
|
|
|
|
len(task_map), job_id))
|
2016-10-28 09:41:47 +03:00
|
|
|
|
|
|
|
|
2016-11-01 09:12:14 +03:00
|
|
|
def add_jobs(
|
2017-01-19 21:15:32 +03:00
|
|
|
batch_client, blob_client, keyvault_client, config, jpfile, bxfile,
|
|
|
|
recreate=False, tail=None):
|
2016-10-10 07:01:11 +03:00
|
|
|
# type: (batch.BatchServiceClient, azureblob.BlockBlobService,
|
2017-01-19 21:15:32 +03:00
|
|
|
# azure.keyvault.KeyVaultClient, dict, tuple, tuple, bool,
|
|
|
|
# str) -> None
|
2016-10-10 07:01:11 +03:00
|
|
|
"""Add jobs
|
|
|
|
:param batch_client: The batch client to use.
|
2016-10-14 00:11:52 +03:00
|
|
|
:type batch_client: `azure.batch.batch_service_client.BatchServiceClient`
|
2016-10-10 07:01:11 +03:00
|
|
|
:param azure.storage.blob.BlockBlobService blob_client: blob client
|
2017-01-19 21:15:32 +03:00
|
|
|
:param azure.keyvault.KeyVaultClient keyvault_client: keyvault client
|
2016-10-10 07:01:11 +03:00
|
|
|
:param dict config: configuration dict
|
|
|
|
:param tuple jpfile: jobprep file
|
2016-10-17 00:42:25 +03:00
|
|
|
:param tuple bxfile: blobxfer file
|
2016-11-01 09:12:14 +03:00
|
|
|
:param bool recreate: recreate job if completed
|
2016-11-13 09:31:15 +03:00
|
|
|
:param str tail: tail specified file of last job/task added
|
2016-10-10 07:01:11 +03:00
|
|
|
"""
|
|
|
|
# get the pool inter-node comm setting
|
2016-11-13 09:13:55 +03:00
|
|
|
bs = settings.batch_shipyard_settings(config)
|
|
|
|
pool = settings.pool_settings(config)
|
2017-03-10 01:38:16 +03:00
|
|
|
try:
|
|
|
|
cloud_pool = batch_client.pool.get(pool.id)
|
|
|
|
except batchmodels.batch_error.BatchErrorException as ex:
|
|
|
|
if 'The specified pool does not exist.' in ex.message.value:
|
|
|
|
logger.error('{} pool does not exist'.format(pool.id))
|
|
|
|
if util.confirm_action(
|
|
|
|
config, 'add jobs to nonexistant pool {}'.format(pool.id)):
|
|
|
|
cloud_pool = None
|
|
|
|
else:
|
|
|
|
logger.error(
|
|
|
|
'not submitting jobs to nonexistant pool {}'.format(
|
|
|
|
pool.id))
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
raise
|
2017-05-03 06:11:24 +03:00
|
|
|
preg = settings.docker_registry_private_settings(config)
|
2017-03-10 01:38:16 +03:00
|
|
|
global_resources = settings.global_resources_docker_images(config)
|
2016-11-13 09:31:15 +03:00
|
|
|
lastjob = None
|
2017-05-23 05:45:25 +03:00
|
|
|
lasttaskid = None
|
2016-11-13 09:13:55 +03:00
|
|
|
for jobspec in settings.job_specifications(config):
|
2017-03-10 01:38:16 +03:00
|
|
|
job_id = settings.job_id(jobspec)
|
|
|
|
# perform checks:
|
|
|
|
# 1. check docker images in task against pre-loaded on pool
|
|
|
|
# 2. if tasks have dependencies, set it if so
|
|
|
|
# 3. if there are multi-instance tasks
|
2017-04-03 20:47:52 +03:00
|
|
|
auto_complete = settings.job_auto_complete(jobspec)
|
2017-03-10 01:38:16 +03:00
|
|
|
multi_instance = False
|
|
|
|
mi_docker_container_name = None
|
|
|
|
reserved_task_id = None
|
|
|
|
uses_task_dependencies = False
|
|
|
|
missing_images = []
|
|
|
|
allow_run_on_missing = settings.job_allow_run_on_missing(jobspec)
|
2017-05-18 04:13:24 +03:00
|
|
|
existing_tasklist = None
|
2017-05-03 06:11:24 +03:00
|
|
|
# check for public pull on missing setting
|
|
|
|
if (allow_run_on_missing and
|
|
|
|
preg.allow_public_docker_hub_pull_on_missing):
|
|
|
|
logger.warning(
|
|
|
|
'allow run on missing image and allow public docker hub '
|
|
|
|
'pull on missing are both enabled. Note that allow public '
|
|
|
|
'pull on missing will not work in this situation.')
|
2017-03-10 01:38:16 +03:00
|
|
|
for task in settings.job_tasks(jobspec):
|
|
|
|
# check if task docker image is set in config.json
|
|
|
|
di = settings.task_docker_image(task)
|
|
|
|
if di not in global_resources:
|
|
|
|
if allow_run_on_missing:
|
|
|
|
logger.warning(
|
|
|
|
('docker image {} not pre-loaded on pool for a '
|
|
|
|
'task specified in job {}').format(di, job_id))
|
|
|
|
missing_images.append(di)
|
|
|
|
else:
|
|
|
|
raise RuntimeError(
|
|
|
|
('not submitting job {} with missing docker image {} '
|
|
|
|
'pre-load on pool {}').format(job_id, di, pool.id))
|
|
|
|
# do not break, check to ensure ids are set on each task if
|
|
|
|
# task dependencies are set
|
|
|
|
if settings.has_depends_on_task(task):
|
|
|
|
uses_task_dependencies = True
|
|
|
|
if settings.is_multi_instance_task(task):
|
2017-04-03 20:47:52 +03:00
|
|
|
if multi_instance and auto_complete:
|
2017-03-10 01:38:16 +03:00
|
|
|
raise ValueError(
|
|
|
|
'cannot specify more than one multi-instance task '
|
|
|
|
'per job with auto completion enabled')
|
|
|
|
multi_instance = True
|
|
|
|
mi_docker_container_name = settings.task_name(task)
|
|
|
|
if util.is_none_or_empty(mi_docker_container_name):
|
|
|
|
_id = settings.task_id(task)
|
|
|
|
if util.is_none_or_empty(_id):
|
2017-05-18 04:13:24 +03:00
|
|
|
existing_tasklist, reserved_task_id = \
|
|
|
|
_generate_next_generic_task_id(
|
|
|
|
batch_client, job_id,
|
|
|
|
tasklist=existing_tasklist)
|
2017-03-10 01:38:16 +03:00
|
|
|
settings.set_task_id(task, reserved_task_id)
|
|
|
|
_id = '{}-{}'.format(job_id, reserved_task_id)
|
|
|
|
settings.set_task_name(task, _id)
|
|
|
|
mi_docker_container_name = settings.task_name(task)
|
|
|
|
del _id
|
2017-04-04 00:16:21 +03:00
|
|
|
# define max task retry count constraint for this task if set
|
|
|
|
job_constraints = None
|
|
|
|
max_task_retries = settings.job_max_task_retries(jobspec)
|
2017-05-23 19:29:00 +03:00
|
|
|
max_wall_time = settings.job_max_wall_time(jobspec)
|
|
|
|
if max_task_retries is not None or max_wall_time is not None:
|
2017-04-04 00:16:21 +03:00
|
|
|
job_constraints = batchmodels.JobConstraints(
|
2017-05-23 19:29:00 +03:00
|
|
|
max_task_retry_count=max_task_retries,
|
|
|
|
max_wall_clock_time=max_wall_time,
|
2017-04-04 00:16:21 +03:00
|
|
|
)
|
2017-03-10 01:38:16 +03:00
|
|
|
# construct job prep
|
2017-04-04 00:16:21 +03:00
|
|
|
jpcmd = []
|
2017-05-03 06:11:24 +03:00
|
|
|
if len(missing_images) > 0 and allow_run_on_missing:
|
|
|
|
# we don't want symmetric difference as we just want to
|
|
|
|
# block on pre-loaded images only
|
|
|
|
gr = list(set(global_resources) - set(missing_images))
|
|
|
|
else:
|
|
|
|
gr = global_resources
|
|
|
|
if len(gr) > 0:
|
|
|
|
jpcmd.append('$AZ_BATCH_NODE_STARTUP_DIR/wd/{} {}'.format(
|
|
|
|
jpfile[0], ' '.join(gr)))
|
|
|
|
del gr
|
2017-04-04 00:16:21 +03:00
|
|
|
# job prep: digest any input_data
|
2016-11-01 06:49:24 +03:00
|
|
|
addlcmds = data.process_input_data(config, bxfile, jobspec)
|
2016-10-14 23:59:19 +03:00
|
|
|
if addlcmds is not None:
|
|
|
|
jpcmd.append(addlcmds)
|
|
|
|
del addlcmds
|
2017-04-04 00:16:21 +03:00
|
|
|
jptask = None
|
|
|
|
if len(jpcmd) > 0:
|
|
|
|
jptask = batchmodels.JobPreparationTask(
|
|
|
|
command_line=util.wrap_commands_in_shell(jpcmd),
|
2016-10-10 07:01:11 +03:00
|
|
|
wait_for_success=True,
|
2017-03-01 23:45:32 +03:00
|
|
|
user_identity=_RUN_ELEVATED,
|
2016-10-10 07:01:11 +03:00
|
|
|
rerun_on_node_reboot_after_success=False,
|
2017-04-04 00:16:21 +03:00
|
|
|
)
|
|
|
|
del jpcmd
|
|
|
|
# construct job release for multi-instance auto-complete
|
|
|
|
jrtask = None
|
2017-04-03 20:47:52 +03:00
|
|
|
if multi_instance and auto_complete:
|
2017-04-04 00:16:21 +03:00
|
|
|
jrtask = batchmodels.JobReleaseTask(
|
2016-11-01 06:49:24 +03:00
|
|
|
command_line=util.wrap_commands_in_shell(
|
2016-10-29 03:44:39 +03:00
|
|
|
['docker kill {}'.format(mi_docker_container_name),
|
2016-10-28 09:41:47 +03:00
|
|
|
'docker rm -v {}'.format(mi_docker_container_name)]),
|
2017-03-01 23:45:32 +03:00
|
|
|
user_identity=_RUN_ELEVATED,
|
2016-10-10 07:01:11 +03:00
|
|
|
)
|
2017-04-04 00:16:21 +03:00
|
|
|
# job prep task must exist
|
|
|
|
if jptask is None:
|
|
|
|
jptask = batchmodels.JobPreparationTask(
|
|
|
|
command_line='echo',
|
|
|
|
wait_for_success=False,
|
|
|
|
user_identity=_RUN_ELEVATED,
|
|
|
|
rerun_on_node_reboot_after_success=False,
|
|
|
|
)
|
|
|
|
# create job
|
|
|
|
job = batchmodels.JobAddParameter(
|
|
|
|
id=settings.job_id(jobspec),
|
|
|
|
pool_info=batchmodels.PoolInformation(pool_id=pool.id),
|
|
|
|
constraints=job_constraints,
|
|
|
|
uses_task_dependencies=uses_task_dependencies,
|
|
|
|
job_preparation_task=jptask,
|
|
|
|
job_release_task=jrtask,
|
2017-06-26 23:20:49 +03:00
|
|
|
metadata=[
|
|
|
|
batchmodels.MetadataItem(
|
|
|
|
name=settings.get_metadata_version_name(),
|
|
|
|
value=__version__,
|
|
|
|
),
|
|
|
|
],
|
2017-07-20 19:34:44 +03:00
|
|
|
priority=settings.job_priority(jobspec),
|
2017-04-04 00:16:21 +03:00
|
|
|
)
|
|
|
|
lastjob = job.id
|
2017-03-10 01:38:16 +03:00
|
|
|
logger.info('Adding job {} to pool {}'.format(job.id, pool.id))
|
2016-10-10 07:01:11 +03:00
|
|
|
try:
|
|
|
|
batch_client.job.add(job)
|
2017-05-03 06:11:24 +03:00
|
|
|
if settings.verbose(config) and jptask is not None:
|
|
|
|
logger.debug('Job prep command: {}'.format(
|
|
|
|
jptask.command_line))
|
2016-10-10 07:01:11 +03:00
|
|
|
except batchmodels.batch_error.BatchErrorException as ex:
|
2016-11-01 09:12:14 +03:00
|
|
|
if ('The specified job is already in a completed state.' in
|
|
|
|
ex.message.value):
|
|
|
|
if recreate:
|
|
|
|
# get job state
|
|
|
|
_job = batch_client.job.get(job.id)
|
|
|
|
if _job.state == batchmodels.JobState.completed:
|
|
|
|
del_jobs(
|
|
|
|
batch_client, config, jobid=job.id, wait=True)
|
|
|
|
time.sleep(1)
|
|
|
|
batch_client.job.add(job)
|
|
|
|
else:
|
|
|
|
raise
|
|
|
|
elif 'The specified job already exists' in ex.message.value:
|
2016-10-10 07:01:11 +03:00
|
|
|
# cannot re-use an existing job if multi-instance due to
|
|
|
|
# job release requirement
|
2017-04-03 20:47:52 +03:00
|
|
|
if multi_instance and auto_complete:
|
2016-10-10 07:01:11 +03:00
|
|
|
raise
|
2017-06-26 23:20:49 +03:00
|
|
|
else:
|
|
|
|
# retrieve job and check for version consistency
|
|
|
|
_job = batch_client.job.get(job.id)
|
2017-07-20 01:34:46 +03:00
|
|
|
_check_metadata_mismatch('job', _job.metadata)
|
2016-10-10 07:01:11 +03:00
|
|
|
else:
|
|
|
|
raise
|
|
|
|
del multi_instance
|
2016-10-28 09:41:47 +03:00
|
|
|
del mi_docker_container_name
|
2017-03-10 01:38:16 +03:00
|
|
|
del uses_task_dependencies
|
2016-11-13 09:13:55 +03:00
|
|
|
# get base env vars from job
|
|
|
|
job_env_vars = settings.job_environment_variables(jobspec)
|
2017-01-25 00:54:26 +03:00
|
|
|
_job_env_vars_secid = \
|
|
|
|
settings.job_environment_variables_keyvault_secret_id(jobspec)
|
2017-01-19 21:15:32 +03:00
|
|
|
if util.is_not_empty(_job_env_vars_secid):
|
|
|
|
jevs = keyvault.get_secret(
|
|
|
|
keyvault_client, _job_env_vars_secid, value_is_json=True)
|
|
|
|
job_env_vars = util.merge_dict(job_env_vars, jevs or {})
|
|
|
|
del jevs
|
|
|
|
del _job_env_vars_secid
|
2016-10-10 07:01:11 +03:00
|
|
|
# add all tasks under job
|
2017-05-18 04:13:24 +03:00
|
|
|
task_map = {}
|
2016-11-13 09:13:55 +03:00
|
|
|
for _task in settings.job_tasks(jobspec):
|
|
|
|
_task_id = settings.task_id(_task)
|
|
|
|
if util.is_none_or_empty(_task_id):
|
2017-05-18 04:13:24 +03:00
|
|
|
existing_tasklist, _task_id = _generate_next_generic_task_id(
|
|
|
|
batch_client, job.id, tasklist=existing_tasklist,
|
2017-05-23 05:45:25 +03:00
|
|
|
reserved=reserved_task_id, task_map=task_map,
|
|
|
|
last_task_id=lasttaskid)
|
2016-11-13 09:13:55 +03:00
|
|
|
settings.set_task_id(_task, _task_id)
|
2016-11-21 01:55:36 +03:00
|
|
|
if util.is_none_or_empty(settings.task_name(_task)):
|
|
|
|
settings.set_task_name(_task, '{}-{}'.format(job.id, _task_id))
|
2016-11-13 09:13:55 +03:00
|
|
|
del _task_id
|
2017-03-10 01:38:16 +03:00
|
|
|
task = settings.task_settings(
|
2017-03-14 18:52:09 +03:00
|
|
|
cloud_pool, config, pool, jobspec, _task, missing_images)
|
2017-01-19 21:15:32 +03:00
|
|
|
# retrieve keyvault task env vars
|
2017-01-25 00:54:26 +03:00
|
|
|
if util.is_not_empty(
|
|
|
|
task.environment_variables_keyvault_secret_id):
|
2017-01-19 21:15:32 +03:00
|
|
|
task_env_vars = keyvault.get_secret(
|
2017-01-25 00:54:26 +03:00
|
|
|
keyvault_client,
|
|
|
|
task.environment_variables_keyvault_secret_id,
|
2017-01-19 21:15:32 +03:00
|
|
|
value_is_json=True)
|
|
|
|
task_env_vars = util.merge_dict(
|
|
|
|
task.environment_variables, task_env_vars or {})
|
|
|
|
else:
|
|
|
|
task_env_vars = task.environment_variables
|
2017-01-19 09:03:26 +03:00
|
|
|
# merge job and task env vars
|
2017-01-19 21:15:32 +03:00
|
|
|
env_vars = util.merge_dict(job_env_vars, task_env_vars)
|
|
|
|
del task_env_vars
|
2016-10-10 07:01:11 +03:00
|
|
|
# get and create env var file
|
|
|
|
sas_urls = None
|
2016-11-13 22:35:09 +03:00
|
|
|
if util.is_not_empty(env_vars) or task.infiniband or task.gpu:
|
2016-10-10 07:01:11 +03:00
|
|
|
envfileloc = '{}taskrf-{}/{}{}'.format(
|
2016-11-13 09:13:55 +03:00
|
|
|
bs.storage_entity_prefix, job.id, task.id, task.envfile)
|
2016-10-17 00:42:25 +03:00
|
|
|
f = tempfile.NamedTemporaryFile(mode='wb', delete=False)
|
2016-10-10 07:01:11 +03:00
|
|
|
fname = f.name
|
|
|
|
try:
|
2016-11-13 22:35:09 +03:00
|
|
|
if util.is_not_empty(env_vars):
|
|
|
|
for key in env_vars:
|
|
|
|
f.write('{}={}\n'.format(
|
|
|
|
key, env_vars[key]).encode('utf8'))
|
2016-11-13 09:13:55 +03:00
|
|
|
if task.infiniband:
|
2016-10-17 00:42:25 +03:00
|
|
|
f.write(b'I_MPI_FABRICS=shm:dapl\n')
|
|
|
|
f.write(b'I_MPI_DAPL_PROVIDER=ofa-v2-ib0\n')
|
|
|
|
f.write(b'I_MPI_DYNAMIC_CONNECTION=0\n')
|
2016-10-10 07:01:11 +03:00
|
|
|
# create a manpath entry for potentially buggy
|
|
|
|
# intel mpivars.sh
|
2016-10-17 00:42:25 +03:00
|
|
|
f.write(b'MANPATH=/usr/share/man:/usr/local/man\n')
|
2016-11-13 22:35:09 +03:00
|
|
|
if task.gpu:
|
|
|
|
f.write(b'CUDA_CACHE_DISABLE=0\n')
|
|
|
|
f.write(b'CUDA_CACHE_MAXSIZE=1073741824\n')
|
|
|
|
# use absolute path due to non-expansion
|
|
|
|
f.write(
|
|
|
|
('CUDA_CACHE_PATH={}/batch/tasks/'
|
|
|
|
'.nv/ComputeCache\n').format(
|
2016-11-14 10:26:48 +03:00
|
|
|
settings.temp_disk_mountpoint(
|
|
|
|
config)).encode('utf8'))
|
2016-10-10 07:01:11 +03:00
|
|
|
# close and upload env var file
|
|
|
|
f.close()
|
2016-11-01 06:49:24 +03:00
|
|
|
sas_urls = storage.upload_resource_files(
|
2016-10-10 07:01:11 +03:00
|
|
|
blob_client, config, [(envfileloc, fname)])
|
|
|
|
finally:
|
|
|
|
os.unlink(fname)
|
|
|
|
del f
|
|
|
|
del fname
|
|
|
|
if len(sas_urls) != 1:
|
|
|
|
raise RuntimeError('unexpected number of sas urls')
|
2016-11-13 09:13:55 +03:00
|
|
|
# check if this is a multi-instance task
|
2016-10-10 07:01:11 +03:00
|
|
|
mis = None
|
2016-11-13 09:13:55 +03:00
|
|
|
if settings.is_multi_instance_task(_task):
|
2016-10-10 07:01:11 +03:00
|
|
|
mis = batchmodels.MultiInstanceSettings(
|
2016-11-13 09:13:55 +03:00
|
|
|
number_of_instances=task.multi_instance.num_instances,
|
|
|
|
coordination_command_line=util.wrap_commands_in_shell(
|
|
|
|
task.multi_instance.coordination_command, wait=False),
|
2016-10-10 07:01:11 +03:00
|
|
|
common_resource_files=[],
|
|
|
|
)
|
|
|
|
# add common resource files for multi-instance
|
2016-11-13 09:13:55 +03:00
|
|
|
if util.is_not_empty(task.multi_instance.resource_files):
|
|
|
|
for rf in task.multi_instance.resource_files:
|
2016-10-10 07:01:11 +03:00
|
|
|
mis.common_resource_files.append(
|
|
|
|
batchmodels.ResourceFile(
|
2016-11-13 09:13:55 +03:00
|
|
|
file_path=rf.file_path,
|
|
|
|
blob_source=rf.blob_source,
|
|
|
|
file_mode=rf.file_mode,
|
2016-10-10 07:01:11 +03:00
|
|
|
)
|
|
|
|
)
|
|
|
|
# set application command
|
|
|
|
task_commands = [
|
2016-11-13 09:13:55 +03:00
|
|
|
'{} {} {}'.format(
|
|
|
|
task.docker_exec_cmd, task.name, task.command)
|
2016-10-10 07:01:11 +03:00
|
|
|
]
|
|
|
|
else:
|
|
|
|
task_commands = [
|
2016-11-13 09:13:55 +03:00
|
|
|
'env | grep AZ_BATCH_ >> {}'.format(task.envfile),
|
2016-10-10 07:01:11 +03:00
|
|
|
'{} {} {}{}'.format(
|
2016-11-13 09:13:55 +03:00
|
|
|
task.docker_run_cmd,
|
|
|
|
' '.join(task.docker_run_options),
|
|
|
|
task.image,
|
|
|
|
'{}'.format(
|
|
|
|
' ' + task.command) if task.command else '')
|
2016-10-10 07:01:11 +03:00
|
|
|
]
|
2017-05-03 06:11:24 +03:00
|
|
|
# get docker login if missing images
|
|
|
|
if len(missing_images) > 0 and allow_run_on_missing:
|
|
|
|
taskenv, logincmd = generate_docker_login_settings(config)
|
|
|
|
logincmd.extend(task_commands)
|
|
|
|
task_commands = logincmd
|
|
|
|
else:
|
|
|
|
taskenv = None
|
2016-10-14 23:59:19 +03:00
|
|
|
# digest any input_data
|
2016-11-01 06:49:24 +03:00
|
|
|
addlcmds = data.process_input_data(
|
2016-11-13 09:13:55 +03:00
|
|
|
config, bxfile, _task, on_task=True)
|
2016-10-14 23:59:19 +03:00
|
|
|
if addlcmds is not None:
|
|
|
|
task_commands.insert(0, addlcmds)
|
2016-10-17 00:42:25 +03:00
|
|
|
# digest any output data
|
2016-11-01 06:49:24 +03:00
|
|
|
addlcmds = data.process_output_data(
|
2016-11-13 09:13:55 +03:00
|
|
|
config, bxfile, _task)
|
2016-10-17 00:42:25 +03:00
|
|
|
if addlcmds is not None:
|
|
|
|
task_commands.append(addlcmds)
|
2016-10-14 23:59:19 +03:00
|
|
|
del addlcmds
|
2017-01-31 20:31:21 +03:00
|
|
|
# set task constraints
|
|
|
|
task_constraints = batchmodels.TaskConstraints(
|
|
|
|
retention_time=task.retention_time,
|
|
|
|
max_task_retry_count=task.max_task_retries,
|
2017-05-23 19:29:00 +03:00
|
|
|
max_wall_clock_time=task.max_wall_time,
|
2017-01-31 20:31:21 +03:00
|
|
|
)
|
2016-10-10 07:01:11 +03:00
|
|
|
# create task
|
|
|
|
batchtask = batchmodels.TaskAddParameter(
|
2016-11-13 09:13:55 +03:00
|
|
|
id=task.id,
|
2016-11-01 06:49:24 +03:00
|
|
|
command_line=util.wrap_commands_in_shell(task_commands),
|
2017-03-01 23:45:32 +03:00
|
|
|
user_identity=_RUN_ELEVATED,
|
2016-10-10 07:01:11 +03:00
|
|
|
resource_files=[],
|
2016-11-13 22:35:09 +03:00
|
|
|
multi_instance_settings=mis,
|
2017-01-24 18:46:52 +03:00
|
|
|
constraints=task_constraints,
|
2017-05-03 06:11:24 +03:00
|
|
|
environment_settings=taskenv,
|
2016-10-10 07:01:11 +03:00
|
|
|
)
|
2016-10-17 00:42:25 +03:00
|
|
|
# add envfile
|
2016-10-10 07:01:11 +03:00
|
|
|
if sas_urls is not None:
|
|
|
|
batchtask.resource_files.append(
|
|
|
|
batchmodels.ResourceFile(
|
2016-11-13 09:13:55 +03:00
|
|
|
file_path=str(task.envfile),
|
2016-10-10 07:01:11 +03:00
|
|
|
blob_source=next(iter(sas_urls.values())),
|
|
|
|
file_mode='0640',
|
|
|
|
)
|
|
|
|
)
|
2016-10-17 00:42:25 +03:00
|
|
|
sas_urls = None
|
2016-10-10 07:01:11 +03:00
|
|
|
# add additional resource files
|
2016-11-13 09:13:55 +03:00
|
|
|
if util.is_not_empty(task.resource_files):
|
|
|
|
for rf in task.resource_files:
|
2016-10-10 07:01:11 +03:00
|
|
|
batchtask.resource_files.append(
|
|
|
|
batchmodels.ResourceFile(
|
2016-11-13 09:13:55 +03:00
|
|
|
file_path=rf.file_path,
|
|
|
|
blob_source=rf.blob_source,
|
|
|
|
file_mode=rf.file_mode,
|
2016-10-10 07:01:11 +03:00
|
|
|
)
|
|
|
|
)
|
|
|
|
# add task dependencies
|
2017-01-12 20:23:25 +03:00
|
|
|
if (util.is_not_empty(task.depends_on) or
|
|
|
|
util.is_not_empty(task.depends_on_range)):
|
|
|
|
if util.is_not_empty(task.depends_on_range):
|
|
|
|
task_id_ranges = [batchmodels.TaskIdRange(
|
|
|
|
task.depends_on_range[0], task.depends_on_range[1])]
|
|
|
|
else:
|
|
|
|
task_id_ranges = None
|
2016-10-10 07:01:11 +03:00
|
|
|
batchtask.depends_on = batchmodels.TaskDependencies(
|
2017-01-12 20:23:25 +03:00
|
|
|
task_ids=task.depends_on,
|
|
|
|
task_id_ranges=task_id_ranges,
|
2016-10-10 07:01:11 +03:00
|
|
|
)
|
|
|
|
# create task
|
2016-11-13 09:13:55 +03:00
|
|
|
if settings.verbose(config):
|
2016-10-14 23:59:19 +03:00
|
|
|
if mis is not None:
|
2017-05-18 04:13:24 +03:00
|
|
|
logger.debug(
|
|
|
|
'multi-instance task coordination command: {}'.format(
|
2016-10-14 23:59:19 +03:00
|
|
|
mis.coordination_command_line))
|
2017-05-18 04:13:24 +03:00
|
|
|
logger.debug('task: {} command: {}'.format(
|
2016-11-13 09:13:55 +03:00
|
|
|
task.id, batchtask.command_line))
|
2017-05-18 04:13:24 +03:00
|
|
|
if task.id in task_map:
|
|
|
|
raise RuntimeError(
|
|
|
|
'duplicate task id detected: {} for job {}'.format(
|
|
|
|
task.id, job.id))
|
|
|
|
task_map[task.id] = batchtask
|
2017-05-23 05:45:25 +03:00
|
|
|
lasttaskid = task.id
|
2017-05-18 04:13:24 +03:00
|
|
|
# add task collection to job
|
|
|
|
_add_task_collection(batch_client, job.id, task_map)
|
2017-06-09 21:34:26 +03:00
|
|
|
# patch job if job autocompletion is needed
|
2017-05-18 04:13:24 +03:00
|
|
|
if auto_complete:
|
2017-06-09 21:34:26 +03:00
|
|
|
batch_client.job.patch(
|
2017-05-18 04:13:24 +03:00
|
|
|
job_id=job.id,
|
2017-06-09 21:34:26 +03:00
|
|
|
job_patch_parameter=batchmodels.JobPatchParameter(
|
2017-05-18 04:13:24 +03:00
|
|
|
pool_info=batchmodels.PoolInformation(pool_id=pool.id),
|
|
|
|
on_all_tasks_complete=batchmodels.
|
|
|
|
OnAllTasksComplete.terminate_job))
|
2016-11-13 09:31:15 +03:00
|
|
|
# tail file if specified
|
|
|
|
if tail:
|
|
|
|
stream_file_and_wait_for_task(
|
|
|
|
batch_client, config, filespec='{},{},{}'.format(
|
2017-05-23 05:45:25 +03:00
|
|
|
lastjob, lasttaskid, tail), disk=False)
|