mirror of
https://github.com/clearlinux/WALinuxAgent.git
synced 2026-09-05 21:31:41 +00:00
[#823] -- Show config options in use
[#784] -- Adjust http retry and logging Signed-off-by: Brendan Dixon <brendandixon@me.com>
This commit is contained in:
@@ -128,6 +128,11 @@ class Agent(object):
|
||||
update_handler = get_update_handler()
|
||||
update_handler.run()
|
||||
|
||||
def show_configuration(self):
|
||||
configuration = conf.get_configuration()
|
||||
for k in sorted(configuration.keys()):
|
||||
print("{0} = {1}".format(k, configuration[k]))
|
||||
|
||||
def main(args=[]):
|
||||
"""
|
||||
Parse command line arguments, exit with usage() on error.
|
||||
@@ -157,6 +162,8 @@ def main(args=[]):
|
||||
agent.daemon()
|
||||
elif command == "run-exthandlers":
|
||||
agent.run_exthandlers()
|
||||
elif command == "show-configuration":
|
||||
agent.show_configuration()
|
||||
except Exception:
|
||||
logger.error(u"Failed to run '{0}': {1}",
|
||||
command,
|
||||
@@ -198,6 +205,8 @@ def parse_args(sys_args):
|
||||
verbose = True
|
||||
elif re.match("^([-/]*)force", a):
|
||||
force = True
|
||||
elif re.match("^([-/]*show-configuration", a):
|
||||
cmd = "show-configuration"
|
||||
elif re.match("^([-/]*)(help|usage|\\?)", a):
|
||||
cmd = "help"
|
||||
else:
|
||||
|
||||
@@ -85,6 +85,69 @@ def load_conf_from_file(conf_file_path, conf=__conf__):
|
||||
raise AgentConfigError(("Failed to load conf file:{0}, {1}"
|
||||
"").format(conf_file_path, err))
|
||||
|
||||
__SWITCH_OPTIONS__ = {
|
||||
"OS.EnableRDMA" : False,
|
||||
"OS.UpdateRdmaDriver" : False,
|
||||
"OS.CheckRdmaDriver" : False,
|
||||
"Logs.Verbose" : False,
|
||||
"OS.EnableFIPS" : False,
|
||||
"Provisioning.Enabled" : True,
|
||||
"Provisioning.UseCloudInit" : False,
|
||||
"Provisioning.AllowResetSysUser" : False,
|
||||
"Provisioning.RegenerateSshHostKeyPair" : False,
|
||||
"Provisioning.DeleteRootPassword" : False,
|
||||
"Provisioning.DecodeCustomData" : False,
|
||||
"Provisioning.ExecuteCustomData" : False,
|
||||
"Provisioning.MonitorHostName" : False,
|
||||
"DetectScvmmEnv" : False,
|
||||
"ResourceDisk.Format" : False,
|
||||
"DetectScvmmEnv" : False,
|
||||
"ResourceDisk.Format" : False,
|
||||
"ResourceDisk.EnableSwap" : False,
|
||||
"AutoUpdate.Enabled" : True,
|
||||
"EnableOverProvisioning" : False,
|
||||
"OS.AllowHTTP" : False
|
||||
}
|
||||
|
||||
__STRING_OPTIONS__ = {
|
||||
"Lib.Dir" : "/var/lib/waagent",
|
||||
"DVD.MountPoint" : "/mnt/cdrom/secure",
|
||||
"Pid.File" : "/var/run/waagent.pid",
|
||||
"Extension.LogDir" : "/var/log/azure",
|
||||
"OS.OpensslPath" : "/usr/bin/openssl",
|
||||
"OS.SshDir" : "/etc/ssh",
|
||||
"OS.HomeDir" : "/home",
|
||||
"OS.PasswordPath" : "/etc/shadow",
|
||||
"OS.SudoersDir" : "/etc/sudoers.d",
|
||||
"OS.RootDeviceScsiTimeout" : None,
|
||||
"Provisioning.SshHostKeyPairType" : "rsa",
|
||||
"Provisioning.PasswordCryptId" : "6",
|
||||
"HttpProxy.Host" : None,
|
||||
"ResourceDisk.MountPoint" : "/mnt/resource",
|
||||
"ResourceDisk.MountOptions" : None,
|
||||
"ResourceDisk.Filesystem" : "ext3",
|
||||
"AutoUpdate.GAFamily" : "Prod"
|
||||
}
|
||||
|
||||
__INTEGER_OPTIONS__ = {
|
||||
"Provisioning.PasswordCryptSaltLength" : 10,
|
||||
"HttpProxy.Port" : None,
|
||||
"ResourceDisk.SwapSizeMB" : 0,
|
||||
"Autoupdate.Frequency" : 3600
|
||||
}
|
||||
|
||||
def get_configuration(conf=__conf__):
|
||||
options = {}
|
||||
for option in __SWITCH_OPTIONS__:
|
||||
options[option] = conf.get_switch(option, __SWITCH_OPTIONS__[option])
|
||||
|
||||
for option in __STRING_OPTIONS__:
|
||||
options[option] = conf.get(option, __STRING_OPTIONS__[option])
|
||||
|
||||
for option in __INTEGER_OPTIONS__:
|
||||
options[option] = conf.get_int(option, __INTEGER_OPTIONS__[option])
|
||||
|
||||
return options
|
||||
|
||||
def enable_rdma(conf=__conf__):
|
||||
return conf.get_switch("OS.EnableRDMA", False) or \
|
||||
@@ -251,4 +314,7 @@ def get_autoupdate_frequency(conf=__conf__):
|
||||
return conf.get_int("Autoupdate.Frequency", 3600)
|
||||
|
||||
def get_enable_overprovisioning(conf=__conf__):
|
||||
return conf.get_switch("EnableOverProvisioning", False)
|
||||
return conf.get_switch("EnableOverProvisioning", False)
|
||||
|
||||
def get_allow_http(conf=__conf__):
|
||||
return conf.get_switch("OS.AllowHTTP", False)
|
||||
|
||||
@@ -86,7 +86,6 @@ class DhcpError(AgentError):
|
||||
def __init__(self, msg=None, inner=None):
|
||||
super(DhcpError, self).__init__('000006', msg, inner)
|
||||
|
||||
|
||||
class OSUtilError(AgentError):
|
||||
"""
|
||||
Failed to perform operation to OS configuration
|
||||
@@ -148,3 +147,12 @@ class UpdateError(AgentError):
|
||||
|
||||
def __init__(self, msg=None, inner=None):
|
||||
super(UpdateError, self).__init__('000012', msg, inner)
|
||||
|
||||
|
||||
class BadRequestError(HttpError):
|
||||
"""
|
||||
The server rejected the request (i.e., status code 400)
|
||||
"""
|
||||
|
||||
def __init__(self, msg=None, inner=None):
|
||||
super(BadRequestError, self).__init__(msg, inner)
|
||||
|
||||
@@ -22,7 +22,8 @@ import json
|
||||
import traceback
|
||||
|
||||
from azurelinuxagent.common import logger
|
||||
from azurelinuxagent.common.exception import ProtocolError, HttpError
|
||||
from azurelinuxagent.common.exception import BadRequestError, \
|
||||
HttpError, ProtocolError
|
||||
from azurelinuxagent.common.future import ustr, httpclient
|
||||
from azurelinuxagent.common.utils import restutil
|
||||
from azurelinuxagent.common.utils import textutil
|
||||
@@ -85,10 +86,10 @@ class HostPluginProtocol(object):
|
||||
try:
|
||||
headers = {HEADER_CONTAINER_ID: self.container_id}
|
||||
response = restutil.http_get(url, headers)
|
||||
if response.status != httpclient.OK:
|
||||
if restutil.request_failed(response):
|
||||
logger.error(
|
||||
"HostGAPlugin: Failed Get API versions: {0}".format(
|
||||
self.read_response_error(response)))
|
||||
restutil.read_response_error(response)))
|
||||
else:
|
||||
return_val = ustr(remove_bom(response.read()), encoding='utf-8')
|
||||
|
||||
@@ -117,42 +118,7 @@ class HostPluginProtocol(object):
|
||||
return url, headers
|
||||
|
||||
def put_vm_log(self, content):
|
||||
"""
|
||||
Try to upload the given content to the host plugin
|
||||
:param deployment_id: the deployment id, which is obtained from the
|
||||
goal state (tenant name)
|
||||
:param container_id: the container id, which is obtained from the
|
||||
goal state
|
||||
:param content: the binary content of the zip file to upload
|
||||
:return:
|
||||
"""
|
||||
if not self.ensure_initialized():
|
||||
raise ProtocolError("HostGAPlugin: Host plugin channel is not available")
|
||||
|
||||
if content is None \
|
||||
or self.container_id is None \
|
||||
or self.deployment_id is None:
|
||||
logger.error(
|
||||
"HostGAPlugin: Invalid arguments passed: "
|
||||
"[{0}], [{1}], [{2}]".format(
|
||||
content,
|
||||
self.container_id,
|
||||
self.deployment_id))
|
||||
return
|
||||
url = URI_FORMAT_PUT_LOG.format(self.endpoint, HOST_PLUGIN_PORT)
|
||||
|
||||
headers = {"x-ms-vmagentlog-deploymentid": self.deployment_id,
|
||||
"x-ms-vmagentlog-containerid": self.container_id}
|
||||
logger.periodic(
|
||||
logger.EVERY_FIFTEEN_MINUTES,
|
||||
"HostGAPlugin: Put VM log to [{0}]".format(url))
|
||||
try:
|
||||
response = restutil.http_put(url, content, headers)
|
||||
if response.status != httpclient.OK:
|
||||
logger.error("HostGAPlugin: Put log failed: Code {0}".format(
|
||||
response.status))
|
||||
except HttpError as e:
|
||||
logger.error("HostGAPlugin: Put log exception: {0}".format(e))
|
||||
raise NotImplementedError("Unimplemented")
|
||||
|
||||
def put_vm_status(self, status_blob, sas_url, config_blob_type=None):
|
||||
"""
|
||||
@@ -169,6 +135,7 @@ class HostPluginProtocol(object):
|
||||
|
||||
logger.verbose("HostGAPlugin: Posting VM status")
|
||||
try:
|
||||
|
||||
blob_type = status_blob.type if status_blob.type else config_blob_type
|
||||
|
||||
if blob_type == "BlockBlob":
|
||||
@@ -176,17 +143,14 @@ class HostPluginProtocol(object):
|
||||
else:
|
||||
self._put_page_blob_status(sas_url, status_blob)
|
||||
|
||||
if not HostPluginProtocol.is_default_channel():
|
||||
except Exception as e:
|
||||
# If the HostPlugin rejects the request,
|
||||
# let the error continue, but set to use the HostPlugin
|
||||
if isinstance(e, BadRequestError):
|
||||
logger.verbose("HostGAPlugin: Setting host plugin as default channel")
|
||||
HostPluginProtocol.set_default_channel(True)
|
||||
except Exception as e:
|
||||
message = "HostGAPlugin: Exception Put VM status: {0}, {1}".format(e, traceback.format_exc())
|
||||
from azurelinuxagent.common.event import WALAEventOperation, report_event
|
||||
report_event(op=WALAEventOperation.ReportStatus,
|
||||
is_success=False,
|
||||
message=message)
|
||||
logger.warn("HostGAPlugin: resetting default channel")
|
||||
HostPluginProtocol.set_default_channel(False)
|
||||
|
||||
raise
|
||||
|
||||
def _put_block_blob_status(self, sas_url, status_blob):
|
||||
url = URI_FORMAT_PUT_VM_STATUS.format(self.endpoint, HOST_PLUGIN_PORT)
|
||||
@@ -198,9 +162,9 @@ class HostPluginProtocol(object):
|
||||
bytearray(status_blob.data, encoding='utf-8')),
|
||||
headers=self._build_status_headers())
|
||||
|
||||
if response.status != httpclient.OK:
|
||||
if restutil.request_failed(response):
|
||||
raise HttpError("HostGAPlugin: Put BlockBlob failed: {0}".format(
|
||||
self.read_response_error(response)))
|
||||
restutil.read_response_error(response)))
|
||||
else:
|
||||
logger.verbose("HostGAPlugin: Put BlockBlob status succeeded")
|
||||
|
||||
@@ -219,10 +183,10 @@ class HostPluginProtocol(object):
|
||||
status_blob.get_page_blob_create_headers(status_size)),
|
||||
headers=self._build_status_headers())
|
||||
|
||||
if response.status != httpclient.OK:
|
||||
if restutil.request_failed(response):
|
||||
raise HttpError(
|
||||
"HostGAPlugin: Failed PageBlob clean-up: {0}".format(
|
||||
self.read_response_error(response)))
|
||||
restutil.read_response_error(response)))
|
||||
else:
|
||||
logger.verbose("HostGAPlugin: PageBlob clean-up succeeded")
|
||||
|
||||
@@ -249,11 +213,11 @@ class HostPluginProtocol(object):
|
||||
buf),
|
||||
headers=self._build_status_headers())
|
||||
|
||||
if response.status != httpclient.OK:
|
||||
if restutil.request_failed(response):
|
||||
raise HttpError(
|
||||
"HostGAPlugin Error: Put PageBlob bytes [{0},{1}]: " \
|
||||
"{2}".format(
|
||||
start, end, self.read_response_error(response)))
|
||||
start, end, restutil.read_response_error(response)))
|
||||
|
||||
# Advance to the next page (if any)
|
||||
start = end
|
||||
@@ -287,26 +251,3 @@ class HostPluginProtocol(object):
|
||||
if PY_VERSION_MAJOR > 2:
|
||||
return s.decode('utf-8')
|
||||
return s
|
||||
|
||||
@staticmethod
|
||||
def read_response_error(response):
|
||||
result = ''
|
||||
if response is not None:
|
||||
try:
|
||||
body = remove_bom(response.read())
|
||||
result = "[{0}: {1}] {2}".format(response.status,
|
||||
response.reason,
|
||||
body)
|
||||
|
||||
# this result string is passed upstream to several methods
|
||||
# which do a raise HttpError() or a format() of some kind;
|
||||
# as a result it cannot have any unicode characters
|
||||
if PY_VERSION_MAJOR < 3:
|
||||
result = ustr(result, encoding='ascii', errors='ignore')
|
||||
else:
|
||||
result = result\
|
||||
.encode(encoding='ascii', errors='ignore')\
|
||||
.decode(encoding='ascii', errors='ignore')
|
||||
except Exception:
|
||||
logger.warn(traceback.format_exc())
|
||||
return result
|
||||
|
||||
@@ -88,7 +88,7 @@ class MetadataProtocol(Protocol):
|
||||
except HttpError as e:
|
||||
raise ProtocolError(ustr(e))
|
||||
|
||||
if resp.status != httpclient.OK:
|
||||
if restutil.request_failed(resp):
|
||||
raise ProtocolError("{0} - GET: {1}".format(resp.status, url))
|
||||
|
||||
data = resp.read()
|
||||
@@ -103,7 +103,7 @@ class MetadataProtocol(Protocol):
|
||||
resp = restutil.http_put(url, json.dumps(data), headers=headers)
|
||||
except HttpError as e:
|
||||
raise ProtocolError(ustr(e))
|
||||
if resp.status != httpclient.OK:
|
||||
if restutil.request_failed(resp):
|
||||
raise ProtocolError("{0} - PUT: {1}".format(resp.status, url))
|
||||
|
||||
def _post_data(self, url, data, headers=None):
|
||||
|
||||
@@ -318,7 +318,7 @@ class Protocol(DataContract):
|
||||
def download_ext_handler_pkg(self, uri, headers=None):
|
||||
try:
|
||||
resp = restutil.http_get(uri, chk_proxy=True, headers=headers)
|
||||
if resp.status == restutil.httpclient.OK:
|
||||
if restutil.request_succeeded(resp):
|
||||
return resp.read()
|
||||
except Exception as e:
|
||||
logger.warn("Failed to download from: {0}".format(uri), e)
|
||||
|
||||
@@ -26,7 +26,8 @@ import azurelinuxagent.common.conf as conf
|
||||
import azurelinuxagent.common.utils.fileutil as fileutil
|
||||
import azurelinuxagent.common.utils.textutil as textutil
|
||||
|
||||
from azurelinuxagent.common.exception import ProtocolNotFoundError
|
||||
from azurelinuxagent.common.exception import BadRequestError, \
|
||||
ProtocolNotFoundError
|
||||
from azurelinuxagent.common.future import httpclient, bytebuffer
|
||||
from azurelinuxagent.common.protocol.hostplugin import HostPluginProtocol
|
||||
from azurelinuxagent.common.protocol.restapi import *
|
||||
@@ -96,7 +97,10 @@ class WireProtocol(Protocol):
|
||||
cryptutil = CryptUtil(conf.get_openssl_cmd())
|
||||
cryptutil.gen_transport_cert(trans_prv_file, trans_cert_file)
|
||||
|
||||
self.client.update_goal_state(forced=True)
|
||||
self.update_goal_state(forced=True)
|
||||
|
||||
def update_goal_state(self, forced=False, max_retry=3):
|
||||
self.client.update_goal_state(forced=forced, max_retry=max_retry)
|
||||
|
||||
def get_vminfo(self):
|
||||
goal_state = self.client.get_goal_state()
|
||||
@@ -117,7 +121,7 @@ class WireProtocol(Protocol):
|
||||
|
||||
def get_vmagent_manifests(self):
|
||||
# Update goal state to get latest extensions config
|
||||
self.client.update_goal_state()
|
||||
self.update_goal_state()
|
||||
goal_state = self.client.get_goal_state()
|
||||
ext_conf = self.client.get_ext_conf()
|
||||
return ext_conf.vmagent_manifests, goal_state.incarnation
|
||||
@@ -130,7 +134,7 @@ class WireProtocol(Protocol):
|
||||
def get_ext_handlers(self):
|
||||
logger.verbose("Get extension handler config")
|
||||
# Update goal state to get latest extensions config
|
||||
self.client.update_goal_state()
|
||||
self.update_goal_state()
|
||||
goal_state = self.client.get_goal_state()
|
||||
ext_conf = self.client.get_ext_conf()
|
||||
# In wire protocol, incarnation is equivalent to ETag
|
||||
@@ -533,29 +537,27 @@ class WireClient(object):
|
||||
self.req_count = 0
|
||||
|
||||
def call_wireserver(self, http_req, *args, **kwargs):
|
||||
"""
|
||||
Call wire server; handle throttling (403), resource gone (410) and
|
||||
service unavailable (503).
|
||||
"""
|
||||
self.prevent_throttling()
|
||||
for retry in range(0, 3):
|
||||
|
||||
try:
|
||||
# Never use the HTTP proxy for wireserver
|
||||
kwargs['chk_proxy'] = False
|
||||
resp = http_req(*args, **kwargs)
|
||||
if resp.status == httpclient.FORBIDDEN:
|
||||
logger.warn("Sending too many requests to wire server. ")
|
||||
logger.info("Sleeping {0}s to avoid throttling.",
|
||||
LONG_WAITING_INTERVAL)
|
||||
time.sleep(LONG_WAITING_INTERVAL)
|
||||
elif resp.status == httpclient.SERVICE_UNAVAILABLE:
|
||||
logger.warn("Service temporarily unavailable, sleeping {0}s "
|
||||
"before retrying.", LONG_WAITING_INTERVAL)
|
||||
time.sleep(LONG_WAITING_INTERVAL)
|
||||
elif resp.status == httpclient.GONE:
|
||||
msg = args[0] if len(args) > 0 else ""
|
||||
raise WireProtocolResourceGone(msg)
|
||||
else:
|
||||
return resp
|
||||
raise ProtocolError(("Calling wire server failed: "
|
||||
"{0}").format(resp.status))
|
||||
except Exception as e:
|
||||
raise ProtocolError("[Wireserver Exception] {0}".format(
|
||||
ustr(e)))
|
||||
|
||||
if resp is not None and resp.status == httpclient.GONE:
|
||||
msg = args[0] if len(args) > 0 else ""
|
||||
raise WireProtocolResourceGone(msg)
|
||||
|
||||
elif restutil.request_failed(resp):
|
||||
msg = "[Wireserver Failed] URI {0} ".format(args[0])
|
||||
if resp is not None:
|
||||
msg += " [HTTP Failed] Status Code {0}".format(resp.status)
|
||||
raise ProtocolError(msg)
|
||||
|
||||
return resp
|
||||
|
||||
def decode_config(self, data):
|
||||
if data is None:
|
||||
@@ -565,16 +567,9 @@ class WireClient(object):
|
||||
return xml_text
|
||||
|
||||
def fetch_config(self, uri, headers):
|
||||
try:
|
||||
resp = self.call_wireserver(restutil.http_get,
|
||||
uri,
|
||||
headers=headers)
|
||||
except HttpError as e:
|
||||
raise ProtocolError(ustr(e))
|
||||
|
||||
if resp.status != httpclient.OK:
|
||||
raise ProtocolError("{0} - {1}".format(resp.status, uri))
|
||||
|
||||
resp = self.call_wireserver(restutil.http_get,
|
||||
uri,
|
||||
headers=headers)
|
||||
return self.decode_config(resp.read())
|
||||
|
||||
def fetch_cache(self, local_file):
|
||||
@@ -595,25 +590,11 @@ class WireClient(object):
|
||||
|
||||
@staticmethod
|
||||
def call_storage_service(http_req, *args, **kwargs):
|
||||
"""
|
||||
Call storage service, handle SERVICE_UNAVAILABLE(503)
|
||||
"""
|
||||
|
||||
# Default to use the configured HTTP proxy
|
||||
if not 'chk_proxy' in kwargs or kwargs['chk_proxy'] is None:
|
||||
kwargs['chk_proxy'] = True
|
||||
|
||||
for retry in range(0, 3):
|
||||
resp = http_req(*args, **kwargs)
|
||||
if resp.status == httpclient.SERVICE_UNAVAILABLE:
|
||||
logger.warn("Storage service is temporarily unavailable. ")
|
||||
logger.info("Will retry in {0} seconds. ",
|
||||
LONG_WAITING_INTERVAL)
|
||||
time.sleep(LONG_WAITING_INTERVAL)
|
||||
else:
|
||||
return resp
|
||||
raise ProtocolError(("Calling storage endpoint failed: "
|
||||
"{0}").format(resp.status))
|
||||
return http_req(*args, **kwargs)
|
||||
|
||||
def fetch_manifest(self, version_uris):
|
||||
logger.verbose("Fetch manifest")
|
||||
@@ -621,47 +602,61 @@ class WireClient(object):
|
||||
response = None
|
||||
if not HostPluginProtocol.is_default_channel():
|
||||
response = self.fetch(version.uri)
|
||||
|
||||
if not response:
|
||||
if HostPluginProtocol.is_default_channel():
|
||||
logger.verbose("Using host plugin as default channel")
|
||||
else:
|
||||
logger.verbose("Manifest could not be downloaded, falling back to host plugin")
|
||||
host = self.get_host_plugin()
|
||||
uri, headers = host.get_artifact_request(version.uri)
|
||||
response = self.fetch(uri, headers, chk_proxy=False)
|
||||
if not response:
|
||||
host = self.get_host_plugin(force_update=True)
|
||||
logger.info("Retry fetch in {0} seconds",
|
||||
SHORT_WAITING_INTERVAL)
|
||||
time.sleep(SHORT_WAITING_INTERVAL)
|
||||
else:
|
||||
host.manifest_uri = version.uri
|
||||
logger.verbose("Manifest downloaded successfully from host plugin")
|
||||
if not HostPluginProtocol.is_default_channel():
|
||||
logger.info("Setting host plugin as default channel")
|
||||
HostPluginProtocol.set_default_channel(True)
|
||||
logger.verbose("Failed to download manifest, "
|
||||
"switching to host plugin")
|
||||
|
||||
try:
|
||||
host = self.get_host_plugin()
|
||||
uri, headers = host.get_artifact_request(version.uri)
|
||||
response = self.fetch(uri, headers, chk_proxy=False)
|
||||
|
||||
# If the HostPlugin rejects the request,
|
||||
# let the error continue, but set to use the HostPlugin
|
||||
except BadRequestError:
|
||||
HostPluginProtocol.set_default_channel(True)
|
||||
raise
|
||||
|
||||
host.manifest_uri = version.uri
|
||||
logger.verbose("Manifest downloaded successfully from host plugin")
|
||||
if not HostPluginProtocol.is_default_channel():
|
||||
logger.info("Setting host plugin as default channel")
|
||||
HostPluginProtocol.set_default_channel(True)
|
||||
|
||||
if response:
|
||||
return response
|
||||
|
||||
raise ProtocolError("Failed to fetch manifest from all sources")
|
||||
|
||||
def fetch(self, uri, headers=None, chk_proxy=None):
|
||||
logger.verbose("Fetch [{0}] with headers [{1}]", uri, headers)
|
||||
return_value = None
|
||||
try:
|
||||
resp = self.call_storage_service(
|
||||
restutil.http_get,
|
||||
uri,
|
||||
headers,
|
||||
chk_proxy=chk_proxy)
|
||||
if resp.status == httpclient.OK:
|
||||
return_value = self.decode_config(resp.read())
|
||||
else:
|
||||
logger.warn("Could not fetch {0} [{1}]",
|
||||
uri,
|
||||
HostPluginProtocol.read_response_error(resp))
|
||||
restutil.http_get,
|
||||
uri,
|
||||
headers=headers,
|
||||
chk_proxy=chk_proxy)
|
||||
|
||||
if restutil.request_failed(resp):
|
||||
msg = "[Storage Failed] URI {0} ".format(uri)
|
||||
if resp is not None:
|
||||
msg += restutil.read_response_error(resp)
|
||||
logger.warn(msg)
|
||||
raise ProtocolError(msg)
|
||||
|
||||
return self.decode_config(resp.read())
|
||||
|
||||
except (HttpError, ProtocolError) as e:
|
||||
logger.verbose("Fetch failed from [{0}]: {1}", uri, e)
|
||||
return return_value
|
||||
|
||||
if isinstance(e, BadRequestError):
|
||||
raise
|
||||
|
||||
return None
|
||||
|
||||
def update_hosting_env(self, goal_state):
|
||||
if goal_state.hosting_env_uri is None:
|
||||
@@ -793,20 +788,45 @@ class WireClient(object):
|
||||
return self.ext_conf
|
||||
|
||||
def get_ext_manifest(self, ext_handler, goal_state):
|
||||
local_file = MANIFEST_FILE_NAME.format(ext_handler.name,
|
||||
goal_state.incarnation)
|
||||
local_file = os.path.join(conf.get_lib_dir(), local_file)
|
||||
xml_text = self.fetch_manifest(ext_handler.versionUris)
|
||||
self.save_cache(local_file, xml_text)
|
||||
return ExtensionManifest(xml_text)
|
||||
for update_goal_state in [False, True]:
|
||||
try:
|
||||
if update_goal_state:
|
||||
self.update_goal_state(forced=True)
|
||||
goal_state = self.get_goal_state()
|
||||
|
||||
local_file = MANIFEST_FILE_NAME.format(
|
||||
ext_handler.name,
|
||||
goal_state.incarnation)
|
||||
local_file = os.path.join(conf.get_lib_dir(), local_file)
|
||||
xml_text = self.fetch_manifest(ext_handler.versionUris)
|
||||
self.save_cache(local_file, xml_text)
|
||||
return ExtensionManifest(xml_text)
|
||||
|
||||
except BadRequestError:
|
||||
continue
|
||||
|
||||
raise ProtocolError("Failed to retrieve extension manifest")
|
||||
|
||||
def get_gafamily_manifest(self, vmagent_manifest, goal_state):
|
||||
local_file = MANIFEST_FILE_NAME.format(vmagent_manifest.family,
|
||||
goal_state.incarnation)
|
||||
local_file = os.path.join(conf.get_lib_dir(), local_file)
|
||||
xml_text = self.fetch_manifest(vmagent_manifest.versionsManifestUris)
|
||||
fileutil.write_file(local_file, xml_text)
|
||||
return ExtensionManifest(xml_text)
|
||||
for update_goal_state in [False, True]:
|
||||
try:
|
||||
if update_goal_state:
|
||||
self.update_goal_state(forced=True)
|
||||
goal_state = self.get_goal_state()
|
||||
|
||||
local_file = MANIFEST_FILE_NAME.format(
|
||||
vmagent_manifest.family,
|
||||
goal_state.incarnation)
|
||||
local_file = os.path.join(conf.get_lib_dir(), local_file)
|
||||
xml_text = self.fetch_manifest(
|
||||
vmagent_manifest.versionsManifestUris)
|
||||
fileutil.write_file(local_file, xml_text)
|
||||
return ExtensionManifest(xml_text)
|
||||
|
||||
except BadRequestError:
|
||||
continue
|
||||
|
||||
raise ProtocolError("Failed to retrieve GAFamily manifest")
|
||||
|
||||
def check_wire_protocol_version(self):
|
||||
uri = VERSION_INFO_URI.format(self.endpoint)
|
||||
@@ -825,39 +845,55 @@ class WireClient(object):
|
||||
raise ProtocolNotFoundError(error)
|
||||
|
||||
def upload_status_blob(self):
|
||||
ext_conf = self.get_ext_conf()
|
||||
|
||||
blob_uri = ext_conf.status_upload_blob
|
||||
blob_type = ext_conf.status_upload_blob_type
|
||||
|
||||
if blob_uri is not None:
|
||||
|
||||
if not blob_type in ["BlockBlob", "PageBlob"]:
|
||||
blob_type = "BlockBlob"
|
||||
logger.verbose("Status Blob type is unspecified "
|
||||
"-- assuming it is a BlockBlob")
|
||||
|
||||
for update_goal_state in [False, True]:
|
||||
try:
|
||||
self.status_blob.prepare(blob_type)
|
||||
if update_goal_state:
|
||||
self.update_goal_state(forced=True)
|
||||
|
||||
ext_conf = self.get_ext_conf()
|
||||
|
||||
blob_uri = ext_conf.status_upload_blob
|
||||
blob_type = ext_conf.status_upload_blob_type
|
||||
|
||||
if blob_uri is not None:
|
||||
|
||||
if not blob_type in ["BlockBlob", "PageBlob"]:
|
||||
blob_type = "BlockBlob"
|
||||
logger.verbose("Status Blob type is unspecified "
|
||||
"-- assuming it is a BlockBlob")
|
||||
|
||||
try:
|
||||
self.status_blob.prepare(blob_type)
|
||||
except Exception as e:
|
||||
self.report_status_event(
|
||||
"Exception creating status blob: {0}", ustr(e))
|
||||
return
|
||||
|
||||
if not HostPluginProtocol.is_default_channel():
|
||||
try:
|
||||
if self.status_blob.upload(blob_uri):
|
||||
return
|
||||
except HttpError as e:
|
||||
pass
|
||||
|
||||
host = self.get_host_plugin()
|
||||
host.put_vm_status(self.status_blob,
|
||||
ext_conf.status_upload_blob,
|
||||
ext_conf.status_upload_blob_type)
|
||||
HostPluginProtocol.set_default_channel(True)
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
# If the HostPlugin rejects the request,
|
||||
# let the error continue, but set to use the HostPlugin
|
||||
if isinstance(e, BadRequestError):
|
||||
HostPluginProtocol.set_default_channel(True)
|
||||
continue
|
||||
|
||||
self.report_status_event(
|
||||
"Exception creating status blob: {0}",
|
||||
e)
|
||||
"Exception uploading status blob: {0}", ustr(e))
|
||||
return
|
||||
|
||||
uploaded = False
|
||||
if not HostPluginProtocol.is_default_channel():
|
||||
try:
|
||||
uploaded = self.status_blob.upload(blob_uri)
|
||||
except HttpError as e:
|
||||
pass
|
||||
|
||||
if not uploaded:
|
||||
host = self.get_host_plugin()
|
||||
host.put_vm_status(self.status_blob,
|
||||
ext_conf.status_upload_blob,
|
||||
ext_conf.status_upload_blob_type)
|
||||
|
||||
def report_role_prop(self, thumbprint):
|
||||
goal_state = self.get_goal_state()
|
||||
role_prop = _build_role_properties(goal_state.container_id,
|
||||
@@ -898,11 +934,12 @@ class WireClient(object):
|
||||
health_report_uri,
|
||||
health_report,
|
||||
headers=headers,
|
||||
max_retry=30)
|
||||
max_retry=30,
|
||||
retry_delay=15)
|
||||
except HttpError as e:
|
||||
raise ProtocolError((u"Failed to send provision status: "
|
||||
u"{0}").format(e))
|
||||
if resp.status != httpclient.OK:
|
||||
if restutil.request_failed(resp):
|
||||
raise ProtocolError((u"Failed to send provision status: "
|
||||
u",{0}: {1}").format(resp.status,
|
||||
resp.read()))
|
||||
@@ -921,7 +958,7 @@ class WireClient(object):
|
||||
except HttpError as e:
|
||||
raise ProtocolError("Failed to send events:{0}".format(e))
|
||||
|
||||
if resp.status != httpclient.OK:
|
||||
if restutil.request_failed(resp):
|
||||
logger.verbose(resp.read())
|
||||
raise ProtocolError(
|
||||
"Failed to send events:{0}".format(resp.status))
|
||||
@@ -981,12 +1018,8 @@ class WireClient(object):
|
||||
"x-ms-guest-agent-public-x509-cert": cert
|
||||
}
|
||||
|
||||
def get_host_plugin(self, force_update=False):
|
||||
if self.host_plugin is None or force_update:
|
||||
if force_update:
|
||||
logger.warn("Forcing update of goal state")
|
||||
self.goal_state = None
|
||||
self.update_goal_state(forced=True)
|
||||
def get_host_plugin(self):
|
||||
if self.host_plugin is None:
|
||||
goal_state = self.get_goal_state()
|
||||
self.host_plugin = HostPluginProtocol(self.endpoint,
|
||||
goal_state.container_id,
|
||||
@@ -999,23 +1032,47 @@ class WireClient(object):
|
||||
|
||||
def get_artifacts_profile(self):
|
||||
artifacts_profile = None
|
||||
if self.has_artifacts_profile_blob():
|
||||
blob = self.ext_conf.artifacts_profile_blob
|
||||
logger.verbose("Getting the artifacts profile")
|
||||
profile = self.fetch(blob)
|
||||
for update_goal_state in [False, True]:
|
||||
try:
|
||||
if update_goal_state:
|
||||
self.update_goal_state(forced=True)
|
||||
|
||||
if profile is None:
|
||||
logger.warn("Download failed, falling back to host plugin")
|
||||
host = self.get_host_plugin()
|
||||
uri, headers = host.get_artifact_request(blob)
|
||||
profile = self.decode_config(self.fetch(uri, headers, chk_proxy=False))
|
||||
if self.has_artifacts_profile_blob():
|
||||
blob = self.ext_conf.artifacts_profile_blob
|
||||
|
||||
if not textutil.is_str_none_or_whitespace(profile):
|
||||
logger.verbose("Artifacts profile downloaded successfully")
|
||||
artifacts_profile = InVMArtifactsProfile(profile)
|
||||
profile = None
|
||||
if not HostPluginProtocol.is_default_channel():
|
||||
logger.verbose("Retrieving the artifacts profile")
|
||||
profile = self.fetch(blob)
|
||||
|
||||
return artifacts_profile
|
||||
if profile is None:
|
||||
if HostPluginProtocol.is_default_channel():
|
||||
logger.verbose("Using host plugin as default channel")
|
||||
else:
|
||||
logger.verbose("Failed to download artifacts profile, "
|
||||
"switching to host plugin")
|
||||
|
||||
host = self.get_host_plugin()
|
||||
uri, headers = host.get_artifact_request(blob)
|
||||
config = self.fetch(uri, headers, chk_proxy=False)
|
||||
profile = self.decode_config(config)
|
||||
|
||||
if not textutil.is_str_none_or_whitespace(profile):
|
||||
logger.verbose("Artifacts profile downloaded")
|
||||
artifacts_profile = InVMArtifactsProfile(profile)
|
||||
|
||||
return artifacts_profile
|
||||
|
||||
except BadRequestError:
|
||||
HostPluginProtocol.set_default_channel(True)
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
logger.warn(
|
||||
"Exception retrieving artifacts profile: {0}".format(
|
||||
ustr(e)))
|
||||
|
||||
return None
|
||||
|
||||
class VersionInfo(object):
|
||||
def __init__(self, xml_text):
|
||||
|
||||
@@ -18,19 +18,83 @@
|
||||
#
|
||||
|
||||
import time
|
||||
import traceback
|
||||
|
||||
import azurelinuxagent.common.conf as conf
|
||||
import azurelinuxagent.common.logger as logger
|
||||
from azurelinuxagent.common.exception import HttpError
|
||||
from azurelinuxagent.common.future import httpclient, urlparse
|
||||
import azurelinuxagent.common.utils.textutil as textutil
|
||||
|
||||
"""
|
||||
REST api util functions
|
||||
"""
|
||||
from azurelinuxagent.common.exception import BadRequestError, HttpError
|
||||
from azurelinuxagent.common.future import httpclient, urlparse, ustr
|
||||
from azurelinuxagent.common.version import PY_VERSION_MAJOR
|
||||
|
||||
RETRY_WAITING_INTERVAL = 3
|
||||
secure_warning = True
|
||||
|
||||
SECURE_WARNING_EMITTED = False
|
||||
|
||||
DEFAULT_RETRIES = 3
|
||||
|
||||
SHORT_DELAY_IN_SECONDS = 5
|
||||
LONG_DELAY_IN_SECONDS = 15
|
||||
|
||||
RETRY_CODES = [
|
||||
httpclient.RESET_CONTENT,
|
||||
httpclient.PARTIAL_CONTENT,
|
||||
httpclient.FORBIDDEN,
|
||||
httpclient.INTERNAL_SERVER_ERROR,
|
||||
httpclient.NOT_IMPLEMENTED,
|
||||
httpclient.SERVICE_UNAVAILABLE,
|
||||
httpclient.GATEWAY_TIMEOUT,
|
||||
httpclient.INSUFFICIENT_STORAGE
|
||||
]
|
||||
|
||||
OK_CODES = [
|
||||
httpclient.OK,
|
||||
httpclient.CREATED,
|
||||
httpclient.ACCEPTED
|
||||
]
|
||||
|
||||
THROTTLE_CODES = [
|
||||
httpclient.FORBIDDEN,
|
||||
httpclient.SERVICE_UNAVAILABLE
|
||||
]
|
||||
|
||||
RETRY_EXCEPTIONS = [
|
||||
httpclient.NotConnected,
|
||||
httpclient.IncompleteRead,
|
||||
httpclient.ImproperConnectionState,
|
||||
httpclient.BadStatusLine
|
||||
]
|
||||
|
||||
# Note:
|
||||
# - The Python library does not define constants for all possible
|
||||
# errno values; these come from the standard C/C++ header
|
||||
RETRY_IOERRORS = [
|
||||
64, # ENONET -- Machine is not on the network
|
||||
67, # ENOLINK -- Link has been severed
|
||||
70, # ECOMM -- Communication error on send
|
||||
78, # EREMCHG -- Remote address changed
|
||||
85, # ERESTART -- Interrupted system call should be restarted
|
||||
100, # ENETDOWN -- Network is down
|
||||
101, # ENETUNREACH -- Network is unreachable
|
||||
102, # ENETRESET -- Network dropped connection because of reset
|
||||
103, # ECONNABORTED -- Software caused connection abort
|
||||
104, # ECONNRESET -- Connection reset by peer
|
||||
111, # ECONNREFUSED -- Connection refused
|
||||
112 # EHOSTDOWN -- Host is down
|
||||
]
|
||||
|
||||
|
||||
def _is_retry_status(status, retry_codes=RETRY_CODES):
|
||||
return status in retry_codes
|
||||
|
||||
def _is_retry_errno(errno):
|
||||
return errno in RETRY_IOERRORS
|
||||
|
||||
def _is_retry_exception(e):
|
||||
return len([x for x in RETRY_EXCEPTIONS if isinstance(e, x)]) > 0
|
||||
|
||||
def _is_throttle_status(status):
|
||||
return status in THROTTLE_CODES
|
||||
|
||||
def _parse_url(url):
|
||||
o = urlparse(url)
|
||||
@@ -45,11 +109,7 @@ def _parse_url(url):
|
||||
return o.hostname, o.port, secure, rel_uri
|
||||
|
||||
|
||||
def get_http_proxy():
|
||||
"""
|
||||
Get http_proxy and https_proxy from environment variables.
|
||||
Username and password is not supported now.
|
||||
"""
|
||||
def _get_http_proxy():
|
||||
host = conf.get_httpproxy_host()
|
||||
port = conf.get_httpproxy_port()
|
||||
return host, port
|
||||
@@ -98,43 +158,63 @@ def _http_request(method, host, rel_uri, port=None, data=None, secure=False,
|
||||
return resp
|
||||
|
||||
|
||||
def http_request(method, url, data, headers=None, max_retry=3,
|
||||
chk_proxy=False):
|
||||
"""
|
||||
Sending http request to server
|
||||
On error, sleep 10 and retry max_retry times.
|
||||
"""
|
||||
def http_request(method,
|
||||
url, data, headers=None,
|
||||
chk_proxy=False,
|
||||
max_retry=DEFAULT_RETRIES,
|
||||
retry_codes=RETRY_CODES,
|
||||
retry_delay=SHORT_DELAY_IN_SECONDS):
|
||||
|
||||
global SECURE_WARNING_EMITTED
|
||||
|
||||
host, port, secure, rel_uri = _parse_url(url)
|
||||
global secure_warning
|
||||
|
||||
# Check proxy
|
||||
proxy_host, proxy_port = (None, None)
|
||||
if chk_proxy:
|
||||
proxy_host, proxy_port = get_http_proxy()
|
||||
proxy_host, proxy_port = _get_http_proxy()
|
||||
|
||||
# If httplib module is not built with ssl support. Fallback to http
|
||||
# If httplib module is not built with ssl support,
|
||||
# fallback to HTTP if allowed
|
||||
if secure and not hasattr(httpclient, "HTTPSConnection"):
|
||||
secure = False
|
||||
if secure_warning:
|
||||
logger.warn("httplib is not built with ssl support")
|
||||
secure_warning = False
|
||||
if not conf.get_allow_http():
|
||||
raise HttpError("HTTPS is unavailable and required")
|
||||
|
||||
# If httplib module doesn't support https tunnelling. Fallback to http
|
||||
secure = False
|
||||
if not SECURE_WARNING_EMITTED:
|
||||
logger.warn("Python does not include SSL support")
|
||||
SECURE_WARNING_EMITTED = True
|
||||
|
||||
# If httplib module doesn't support HTTPS tunnelling,
|
||||
# fallback to HTTP if allowed
|
||||
if secure and proxy_host is not None and proxy_port is not None \
|
||||
and not hasattr(httpclient.HTTPSConnection, "set_tunnel"):
|
||||
if not conf.get_allow_http():
|
||||
raise HttpError("HTTPS tunnelling is unavailable and required")
|
||||
|
||||
secure = False
|
||||
if secure_warning:
|
||||
logger.warn("httplib does not support https tunnelling "
|
||||
"(new in python 2.7)")
|
||||
secure_warning = False
|
||||
if not SECURE_WARNING_EMITTED:
|
||||
logger.warn("Python does not support HTTPS tunnelling")
|
||||
SECURE_WARNING_EMITTED = True
|
||||
|
||||
if proxy_host or proxy_port:
|
||||
logger.verbose("HTTP proxy: [{0}:{1}]", proxy_host, proxy_port)
|
||||
|
||||
retry_msg = ''
|
||||
log_msg = "HTTP {0}".format(method)
|
||||
for retry in range(0, max_retry):
|
||||
retry_interval = RETRY_WAITING_INTERVAL
|
||||
msg = ''
|
||||
attempt = 0
|
||||
delay = retry_delay
|
||||
|
||||
while attempt < max_retry:
|
||||
if attempt > 0:
|
||||
logger.info("[HTTP Retry] Attempt {0} of {1}: {2}",
|
||||
attempt+1,
|
||||
max_retry,
|
||||
msg)
|
||||
time.sleep(delay)
|
||||
|
||||
attempt += 1
|
||||
delay = retry_delay
|
||||
|
||||
try:
|
||||
resp = _http_request(method,
|
||||
host,
|
||||
@@ -145,55 +225,125 @@ def http_request(method, url, data, headers=None, max_retry=3,
|
||||
headers=headers,
|
||||
proxy_host=proxy_host,
|
||||
proxy_port=proxy_port)
|
||||
logger.verbose("HTTP response status: [{0}]", resp.status)
|
||||
logger.verbose("[HTTP Response] Status Code {0}", resp.status)
|
||||
|
||||
if request_failed(resp):
|
||||
if _is_retry_status(resp.status, retry_codes=retry_codes):
|
||||
msg = '[HTTP Retry] HTTP {0} Status Code {1}'.format(
|
||||
method, resp.status)
|
||||
if _is_throttle_status(resp.statue):
|
||||
delay = LONG_DELAY_IN_SECONDS
|
||||
logger.info("[HTTP Delay] Delay {0} seconds for " \
|
||||
"Status Code {1}".format(
|
||||
delay, resp.status))
|
||||
continue
|
||||
|
||||
if resp.status == httpclient.BAD_REQUEST:
|
||||
raise BadRequestError()
|
||||
|
||||
return resp
|
||||
|
||||
except httpclient.HTTPException as e:
|
||||
retry_msg = 'HTTP exception: {0} {1}'.format(log_msg, e)
|
||||
retry_interval = 5
|
||||
msg = '[HTTP Failed] HTTP {0} HttpException {1}'.format(method, e)
|
||||
if _is_retry_exception(e):
|
||||
continue
|
||||
break
|
||||
|
||||
except IOError as e:
|
||||
retry_msg = 'IO error: {0} {1}'.format(log_msg, e)
|
||||
# error 101: network unreachable; when the adapter resets we may
|
||||
# see this transient error for a short time, retry once.
|
||||
if e.errno == 101:
|
||||
retry_interval = RETRY_WAITING_INTERVAL
|
||||
max_retry = 1
|
||||
msg = '[HTTP Failed] HTTP {0} IOError {1}'.format(method, e)
|
||||
if _is_retry_errno(e.errno):
|
||||
continue
|
||||
break
|
||||
|
||||
raise HttpError(msg)
|
||||
|
||||
|
||||
def http_get(url, headers=None, chk_proxy=False,
|
||||
max_retry=DEFAULT_RETRIES,
|
||||
retry_codes=RETRY_CODES,
|
||||
retry_delay=SHORT_DELAY_IN_SECONDS):
|
||||
return http_request("GET",
|
||||
url, None, headers=headers,
|
||||
chk_proxy=chk_proxy,
|
||||
max_retry=max_retry,
|
||||
retry_codes=retry_codes,
|
||||
retry_delay=retry_delay)
|
||||
|
||||
|
||||
def http_head(url, headers=None, chk_proxy=False,
|
||||
max_retry=DEFAULT_RETRIES,
|
||||
retry_codes=RETRY_CODES,
|
||||
retry_delay=SHORT_DELAY_IN_SECONDS):
|
||||
return http_request("HEAD",
|
||||
url, None, headers=headers,
|
||||
chk_proxy=chk_proxy,
|
||||
max_retry=max_retry,
|
||||
retry_codes=retry_codes,
|
||||
retry_delay=retry_delay)
|
||||
|
||||
|
||||
def http_post(url, data, headers=None, chk_proxy=False,
|
||||
max_retry=DEFAULT_RETRIES,
|
||||
retry_codes=RETRY_CODES,
|
||||
retry_delay=SHORT_DELAY_IN_SECONDS):
|
||||
return http_request("POST",
|
||||
url, data, headers=headers,
|
||||
chk_proxy=chk_proxy,
|
||||
max_retry=max_retry,
|
||||
retry_codes=retry_codes,
|
||||
retry_delay=retry_delay)
|
||||
|
||||
|
||||
def http_put(url, data, headers=None, chk_proxy=False,
|
||||
max_retry=DEFAULT_RETRIES,
|
||||
retry_codes=RETRY_CODES,
|
||||
retry_delay=SHORT_DELAY_IN_SECONDS):
|
||||
return http_request("PUT",
|
||||
url, data, headers=headers,
|
||||
chk_proxy=chk_proxy,
|
||||
max_retry=max_retry,
|
||||
retry_codes=retry_codes,
|
||||
retry_delay=retry_delay)
|
||||
|
||||
|
||||
def http_delete(url, headers=None, chk_proxy=False,
|
||||
max_retry=DEFAULT_RETRIES,
|
||||
retry_codes=RETRY_CODES,
|
||||
retry_delay=SHORT_DELAY_IN_SECONDS):
|
||||
return http_request("DELETE",
|
||||
url, None, headers=headers,
|
||||
chk_proxy=chk_proxy,
|
||||
max_retry=max_retry,
|
||||
retry_codes=retry_codes,
|
||||
retry_delay=retry_delay)
|
||||
|
||||
def request_failed(resp, ok_codes=OK_CODES):
|
||||
return not request_succeeded(resp, ok_codes=ok_codes)
|
||||
|
||||
def request_succeeded(resp, ok_codes=OK_CODES):
|
||||
return resp is not None and resp.status in ok_codes
|
||||
|
||||
def read_response_error(resp):
|
||||
result = ''
|
||||
if resp is not None:
|
||||
try:
|
||||
result = "[HTTP Failed] [{0}: {1}] {2}".format(
|
||||
resp.status,
|
||||
resp.reason,
|
||||
resp.read())
|
||||
|
||||
# this result string is passed upstream to several methods
|
||||
# which do a raise HttpError() or a format() of some kind;
|
||||
# as a result it cannot have any unicode characters
|
||||
if PY_VERSION_MAJOR < 3:
|
||||
result = ustr(result, encoding='ascii', errors='ignore')
|
||||
else:
|
||||
retry_interval = 0
|
||||
max_retry = 0
|
||||
result = result\
|
||||
.encode(encoding='ascii', errors='ignore')\
|
||||
.decode(encoding='ascii', errors='ignore')
|
||||
|
||||
if retry < max_retry:
|
||||
logger.info("Retry [{0}/{1} - {3}]",
|
||||
retry+1,
|
||||
max_retry,
|
||||
retry_interval,
|
||||
retry_msg)
|
||||
time.sleep(retry_interval)
|
||||
result = textutil.replace_non_ascii(result)
|
||||
|
||||
raise HttpError("{0} failed".format(log_msg))
|
||||
|
||||
|
||||
def http_get(url, headers=None, max_retry=3, chk_proxy=False):
|
||||
return http_request("GET", url, data=None, headers=headers,
|
||||
max_retry=max_retry, chk_proxy=chk_proxy)
|
||||
|
||||
|
||||
def http_head(url, headers=None, max_retry=3, chk_proxy=False):
|
||||
return http_request("HEAD", url, None, headers=headers,
|
||||
max_retry=max_retry, chk_proxy=chk_proxy)
|
||||
|
||||
|
||||
def http_post(url, data, headers=None, max_retry=3, chk_proxy=False):
|
||||
return http_request("POST", url, data, headers=headers,
|
||||
max_retry=max_retry, chk_proxy=chk_proxy)
|
||||
|
||||
|
||||
def http_put(url, data, headers=None, max_retry=3, chk_proxy=False):
|
||||
return http_request("PUT", url, data, headers=headers,
|
||||
max_retry=max_retry, chk_proxy=chk_proxy)
|
||||
|
||||
|
||||
def http_delete(url, headers=None, max_retry=3, chk_proxy=False):
|
||||
return http_request("DELETE", url, None, headers=headers,
|
||||
max_retry=max_retry, chk_proxy=chk_proxy)
|
||||
|
||||
# End REST api util functions
|
||||
except Exception:
|
||||
logger.warn(traceback.format_exc())
|
||||
return result
|
||||
|
||||
@@ -259,6 +259,17 @@ def set_ini_config(config, name, val):
|
||||
config.insert(length - 1, text)
|
||||
|
||||
|
||||
def replace_non_ascii(incoming, replace_char=''):
|
||||
outgoing = ''
|
||||
if incoming is not None:
|
||||
for c in incoming:
|
||||
if str_to_ord(c) > 128:
|
||||
outgoing += replace_char
|
||||
else:
|
||||
outgoing += c
|
||||
return outgoing
|
||||
|
||||
|
||||
def remove_bom(c):
|
||||
'''
|
||||
bom is comprised of a sequence of three chars,0xef, 0xbb, 0xbf, in case of utf-8.
|
||||
|
||||
@@ -113,7 +113,7 @@ def get_distro():
|
||||
|
||||
AGENT_NAME = "WALinuxAgent"
|
||||
AGENT_LONG_NAME = "Azure Linux Agent"
|
||||
AGENT_VERSION = '2.2.14.1'
|
||||
AGENT_VERSION = '2.2.14.2'
|
||||
AGENT_LONG_VERSION = "{0}-{1}".format(AGENT_NAME, AGENT_VERSION)
|
||||
AGENT_DESCRIPTION = """
|
||||
The Azure Linux Agent supports the provisioning and running of Linux
|
||||
|
||||
+110
-90
@@ -41,7 +41,8 @@ import azurelinuxagent.common.utils.textutil as textutil
|
||||
from azurelinuxagent.common.event import add_event, add_periodic, \
|
||||
elapsed_milliseconds, \
|
||||
WALAEventOperation
|
||||
from azurelinuxagent.common.exception import UpdateError, ProtocolError
|
||||
from azurelinuxagent.common.exception import BadRequestError, \
|
||||
ProtocolError, UpdateError
|
||||
from azurelinuxagent.common.future import ustr
|
||||
from azurelinuxagent.common.osutil import get_osutil
|
||||
from azurelinuxagent.common.protocol import get_protocol_util
|
||||
@@ -349,84 +350,6 @@ class UpdateHandler(object):
|
||||
self._set_sentinal()
|
||||
return
|
||||
|
||||
def _upgrade_available(self, base_version=CURRENT_VERSION):
|
||||
# Emit an event expressing the state of AutoUpdate
|
||||
# Note:
|
||||
# - Duplicate events get suppressed; state transitions always emit
|
||||
add_event(
|
||||
AGENT_NAME,
|
||||
version=CURRENT_VERSION,
|
||||
op=WALAEventOperation.AutoUpdate,
|
||||
is_success=conf.get_autoupdate_enabled())
|
||||
|
||||
# Ignore new agents if updating is disabled
|
||||
if not conf.get_autoupdate_enabled():
|
||||
return False
|
||||
|
||||
now = time.time()
|
||||
if self.last_attempt_time is not None:
|
||||
next_attempt_time = self.last_attempt_time + conf.get_autoupdate_frequency()
|
||||
else:
|
||||
next_attempt_time = now
|
||||
if next_attempt_time > now:
|
||||
return False
|
||||
|
||||
family = conf.get_autoupdate_gafamily()
|
||||
logger.verbose("Checking for agent family {0} updates", family)
|
||||
|
||||
self.last_attempt_time = now
|
||||
try:
|
||||
protocol = self.protocol_util.get_protocol()
|
||||
manifest_list, etag = protocol.get_vmagent_manifests()
|
||||
except Exception as e:
|
||||
msg = u"Exception retrieving agent manifests: {0}".format(ustr(e))
|
||||
logger.warn(msg)
|
||||
add_event(
|
||||
AGENT_NAME,
|
||||
op=WALAEventOperation.Download,
|
||||
version=CURRENT_VERSION,
|
||||
is_success=False,
|
||||
message=msg)
|
||||
return False
|
||||
|
||||
manifests = [m for m in manifest_list.vmAgentManifests \
|
||||
if m.family == family and len(m.versionsManifestUris) > 0]
|
||||
if len(manifests) == 0:
|
||||
logger.verbose(u"Incarnation {0} has no agent family {1} updates", etag, family)
|
||||
return False
|
||||
|
||||
try:
|
||||
pkg_list = protocol.get_vmagent_pkgs(manifests[0])
|
||||
except ProtocolError as e:
|
||||
msg = u"Incarnation {0} failed to get {1} package list: " \
|
||||
u"{2}".format(
|
||||
etag,
|
||||
family,
|
||||
ustr(e))
|
||||
logger.warn(msg)
|
||||
add_event(
|
||||
AGENT_NAME,
|
||||
op=WALAEventOperation.Download,
|
||||
version=CURRENT_VERSION,
|
||||
is_success=False,
|
||||
message=msg)
|
||||
return False
|
||||
|
||||
# Set the agents to those available for download at least as current
|
||||
# as the existing agent and remove from disk any agent no longer
|
||||
# reported to the VM.
|
||||
# Note:
|
||||
# The code leaves on disk available, but blacklisted, agents so as to
|
||||
# preserve the state. Otherwise, those agents could be again
|
||||
# downloaded and inappropriately retried.
|
||||
host = self._get_host_plugin(protocol=protocol)
|
||||
self._set_agents([GuestAgent(pkg=pkg, host=host) for pkg in pkg_list.versions])
|
||||
self._purge_agents()
|
||||
self._filter_blacklisted_agents()
|
||||
|
||||
# Return True if agents more recent than the current are available
|
||||
return len(self.agents) > 0 and self.agents[0].version > base_version
|
||||
|
||||
def _ensure_no_orphans(self, orphan_wait_interval=ORPHAN_WAIT_INTERVAL):
|
||||
pid_files, ignored = self._write_pid_file()
|
||||
for pid_file in pid_files:
|
||||
@@ -615,6 +538,85 @@ class UpdateHandler(object):
|
||||
str(e))
|
||||
return
|
||||
|
||||
def _upgrade_available(self, base_version=CURRENT_VERSION):
|
||||
# Emit an event expressing the state of AutoUpdate
|
||||
# Note:
|
||||
# - Duplicate events get suppressed; state transitions always emit
|
||||
add_event(
|
||||
AGENT_NAME,
|
||||
version=CURRENT_VERSION,
|
||||
op=WALAEventOperation.AutoUpdate,
|
||||
is_success=conf.get_autoupdate_enabled())
|
||||
|
||||
# Ignore new agents if updating is disabled
|
||||
if not conf.get_autoupdate_enabled():
|
||||
return False
|
||||
|
||||
now = time.time()
|
||||
if self.last_attempt_time is not None:
|
||||
next_attempt_time = self.last_attempt_time + \
|
||||
conf.get_autoupdate_frequency()
|
||||
else:
|
||||
next_attempt_time = now
|
||||
if next_attempt_time > now:
|
||||
return False
|
||||
|
||||
family = conf.get_autoupdate_gafamily()
|
||||
logger.verbose("Checking for agent family {0} updates", family)
|
||||
|
||||
self.last_attempt_time = now
|
||||
protocol = self.protocol_util.get_protocol()
|
||||
|
||||
for update_goal_state in [False, True]:
|
||||
try:
|
||||
if update_goal_state:
|
||||
protocol.update_goal_state(forced=True)
|
||||
|
||||
manifest_list, etag = protocol.get_vmagent_manifests()
|
||||
|
||||
manifests = [m for m in manifest_list.vmAgentManifests \
|
||||
if m.family == family and \
|
||||
len(m.versionsManifestUris) > 0]
|
||||
if len(manifests) == 0:
|
||||
logger.verbose(u"Incarnation {0} has no {1} agent updates",
|
||||
etag, family)
|
||||
return False
|
||||
|
||||
pkg_list = protocol.get_vmagent_pkgs(manifests[0])
|
||||
|
||||
# Set the agents to those available for download at least as
|
||||
# current as the existing agent and remove from disk any agent
|
||||
# no longer reported to the VM.
|
||||
# Note:
|
||||
# The code leaves on disk available, but blacklisted, agents
|
||||
# so as to preserve the state. Otherwise, those agents could be
|
||||
# again downloaded and inappropriately retried.
|
||||
host = self._get_host_plugin(protocol=protocol)
|
||||
self._set_agents([GuestAgent(pkg=pkg, host=host) \
|
||||
for pkg in pkg_list.versions])
|
||||
|
||||
self._purge_agents()
|
||||
self._filter_blacklisted_agents()
|
||||
|
||||
# Return True if more recent agents are available
|
||||
return len(self.agents) > 0 and \
|
||||
self.agents[0].version > base_version
|
||||
|
||||
except Exception as e:
|
||||
if isinstance(e, BadRequestError):
|
||||
continue
|
||||
|
||||
msg = u"Exception retrieving agent manifests: {0}".format(
|
||||
ustr(e))
|
||||
logger.warn(msg)
|
||||
add_event(
|
||||
AGENT_NAME,
|
||||
op=WALAEventOperation.Download,
|
||||
version=CURRENT_VERSION,
|
||||
is_success=False,
|
||||
message=msg)
|
||||
return False
|
||||
|
||||
def _write_pid_file(self):
|
||||
pid_files = self._get_pid_files()
|
||||
|
||||
@@ -671,12 +673,16 @@ class GuestAgent(object):
|
||||
self._ensure_downloaded()
|
||||
self._ensure_loaded()
|
||||
except Exception as e:
|
||||
if isinstance(e, BadRequestError):
|
||||
raise
|
||||
|
||||
# Note the failure, blacklist the agent if the package downloaded
|
||||
# - An exception with a downloaded package indicates the package
|
||||
# is corrupt (e.g., missing the HandlerManifest.json file)
|
||||
self.mark_failure(is_fatal=os.path.isfile(self.get_agent_pkg_path()))
|
||||
|
||||
msg = u"Agent {0} download / load failed with exception: {1}".format(self.name, ustr(e))
|
||||
msg = u"Agent {0} install failed with exception: {1}".format(
|
||||
self.name, ustr(e))
|
||||
logger.warn(msg)
|
||||
add_event(
|
||||
AGENT_NAME,
|
||||
@@ -795,20 +801,29 @@ class GuestAgent(object):
|
||||
for uri in self.pkg.uris:
|
||||
if not HostPluginProtocol.is_default_channel() and self._fetch(uri.uri):
|
||||
break
|
||||
|
||||
elif self.host is not None and self.host.ensure_initialized():
|
||||
if not HostPluginProtocol.is_default_channel():
|
||||
logger.warn("Download unsuccessful, falling back to host plugin")
|
||||
logger.warn("Download failed, switching to host plugin")
|
||||
else:
|
||||
logger.verbose("Using host plugin as default channel")
|
||||
|
||||
uri, headers = self.host.get_artifact_request(uri.uri, self.host.manifest_uri)
|
||||
if self._fetch(uri, headers=headers, chk_proxy=False):
|
||||
if not HostPluginProtocol.is_default_channel():
|
||||
logger.verbose("Setting host plugin as default channel")
|
||||
HostPluginProtocol.set_default_channel(True)
|
||||
break
|
||||
else:
|
||||
logger.warn("Host plugin download unsuccessful")
|
||||
try:
|
||||
if self._fetch(uri, headers=headers, chk_proxy=False):
|
||||
if not HostPluginProtocol.is_default_channel():
|
||||
logger.verbose("Setting host plugin as default channel")
|
||||
HostPluginProtocol.set_default_channel(True)
|
||||
break
|
||||
else:
|
||||
logger.warn("Host plugin download failed")
|
||||
|
||||
# If the HostPlugin rejects the request,
|
||||
# let the error continue, but set to use the HostPlugin
|
||||
except BadRequestError:
|
||||
HostPluginProtocol.set_default_channel(True)
|
||||
raise
|
||||
|
||||
else:
|
||||
logger.error("No download channels available")
|
||||
|
||||
@@ -821,13 +836,14 @@ class GuestAgent(object):
|
||||
is_success=False,
|
||||
message=msg)
|
||||
raise UpdateError(msg)
|
||||
|
||||
return
|
||||
|
||||
def _fetch(self, uri, headers=None, chk_proxy=True):
|
||||
package = None
|
||||
try:
|
||||
resp = restutil.http_get(uri, chk_proxy=chk_proxy, headers=headers)
|
||||
if resp.status == restutil.httpclient.OK:
|
||||
if restutil.request_succeeded(resp):
|
||||
package = resp.read()
|
||||
fileutil.write_file(self.get_agent_pkg_path(),
|
||||
bytearray(package),
|
||||
@@ -835,12 +851,16 @@ class GuestAgent(object):
|
||||
logger.verbose(u"Agent {0} downloaded from {1}", self.name, uri)
|
||||
else:
|
||||
logger.verbose("Fetch was unsuccessful [{0}]",
|
||||
HostPluginProtocol.read_response_error(resp))
|
||||
restutil.read_response_error(resp))
|
||||
except restutil.HttpError as http_error:
|
||||
if isinstance(http_error, BadRequestError):
|
||||
raise
|
||||
|
||||
logger.verbose(u"Agent {0} download from {1} failed [{2}]",
|
||||
self.name,
|
||||
uri,
|
||||
http_error)
|
||||
|
||||
return package is not None
|
||||
|
||||
def _load_error(self):
|
||||
|
||||
@@ -107,3 +107,7 @@ OS.OpensslPath=None
|
||||
# handling until inVMArtifactsProfile.OnHold is false.
|
||||
# Default is disabled
|
||||
# EnableOverProvisioning=n
|
||||
|
||||
# Allow fallback to HTTP if HTTPS is unavailable
|
||||
# Note: Allowing HTTP (vs. HTTPS) may cause security risks
|
||||
OS.AllowHTTP=y
|
||||
|
||||
@@ -104,3 +104,7 @@ OS.SshDir=/etc/ssh
|
||||
# handling until inVMArtifactsProfile.OnHold is false.
|
||||
# Default is disabled
|
||||
# EnableOverProvisioning=n
|
||||
|
||||
# Allow fallback to HTTP if HTTPS is unavailable
|
||||
# Note: Allowing HTTP (vs. HTTPS) may cause security risks
|
||||
# OS.AllowHTTP=n
|
||||
|
||||
@@ -24,6 +24,48 @@ from tests.tools import *
|
||||
|
||||
|
||||
class TestConf(AgentTestCase):
|
||||
# Note:
|
||||
# -- These values *MUST* match those from data/test_waagent.conf
|
||||
EXPECTED_CONFIGURATION = {
|
||||
"Provisioning.Enabled" : True,
|
||||
"Provisioning.UseCloudInit" : True,
|
||||
"Provisioning.DeleteRootPassword" : True,
|
||||
"Provisioning.RegenerateSshHostKeyPair" : True,
|
||||
"Provisioning.SshHostKeyPairType" : "rsa",
|
||||
"Provisioning.MonitorHostName" : True,
|
||||
"Provisioning.DecodeCustomData" : False,
|
||||
"Provisioning.ExecuteCustomData" : False,
|
||||
"Provisioning.PasswordCryptId" : '6',
|
||||
"Provisioning.PasswordCryptSaltLength" : 10,
|
||||
"Provisioning.AllowResetSysUser" : False,
|
||||
"ResourceDisk.Format" : True,
|
||||
"ResourceDisk.Filesystem" : "ext4",
|
||||
"ResourceDisk.MountPoint" : "/mnt/resource",
|
||||
"ResourceDisk.EnableSwap" : False,
|
||||
"ResourceDisk.SwapSizeMB" : 0,
|
||||
"ResourceDisk.MountOptions" : None,
|
||||
"Logs.Verbose" : False,
|
||||
"OS.EnableFIPS" : True,
|
||||
"OS.RootDeviceScsiTimeout" : '300',
|
||||
"OS.OpensslPath" : '/usr/bin/openssl',
|
||||
"OS.SshDir" : "/notareal/path",
|
||||
"HttpProxy.Host" : None,
|
||||
"HttpProxy.Port" : None,
|
||||
"DetectScvmmEnv" : False,
|
||||
"Lib.Dir" : "/var/lib/waagent",
|
||||
"DVD.MountPoint" : "/mnt/cdrom/secure",
|
||||
"Pid.File" : "/var/run/waagent.pid",
|
||||
"Extension.LogDir" : "/var/log/azure",
|
||||
"OS.HomeDir" : "/home",
|
||||
"OS.EnableRDMA" : False,
|
||||
"OS.UpdateRdmaDriver" : False,
|
||||
"OS.CheckRdmaDriver" : False,
|
||||
"AutoUpdate.Enabled" : True,
|
||||
"AutoUpdate.GAFamily" : "Prod",
|
||||
"EnableOverProvisioning" : False,
|
||||
"OS.AllowHTTP" : False
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
AgentTestCase.setUp(self)
|
||||
self.conf = ConfigurationProvider()
|
||||
@@ -59,3 +101,11 @@ class TestConf(AgentTestCase):
|
||||
|
||||
def test_get_provision_cloudinit(self):
|
||||
self.assertTrue(get_provision_cloudinit(self.conf))
|
||||
|
||||
def test_get_configuration(self):
|
||||
configuration = conf.get_configuration(self.conf)
|
||||
self.assertTrue(len(configuration.keys()) > 0)
|
||||
for k in TestConf.EXPECTED_CONFIGURATION.keys():
|
||||
self.assertEqual(
|
||||
TestConf.EXPECTED_CONFIGURATION[k],
|
||||
configuration[k])
|
||||
|
||||
@@ -94,10 +94,13 @@ OS.SshDir=/notareal/path
|
||||
# Extension.LogDir=/var/log/azure
|
||||
|
||||
#
|
||||
# Home.Dir=/home
|
||||
# OS.HomeDir=/home
|
||||
|
||||
# Enable RDMA management and set up, should only be used in HPC images
|
||||
# OS.EnableRDMA=y
|
||||
# OS.EnableRDMA=n
|
||||
# OS.UpdateRdmaDriver=n
|
||||
# OS.CheckRdmaDriver=n
|
||||
|
||||
|
||||
# Enable or disable goal state processing auto-update, default is enabled
|
||||
# AutoUpdate.Enabled=y
|
||||
|
||||
+40
-5
@@ -828,6 +828,12 @@ class TestUpdate(UpdateTestCase):
|
||||
self.event_patch.stop()
|
||||
return
|
||||
|
||||
def _create_protocol(self, count=5, versions=None):
|
||||
latest_version = self.prepare_agents(count=count)
|
||||
if versions is None or len(versions) <= 0:
|
||||
versions = [latest_version]
|
||||
return ProtocolMock(versions=versions)
|
||||
|
||||
def _test_upgrade_available(
|
||||
self,
|
||||
base_version=FlexibleVersion(AGENT_VERSION),
|
||||
@@ -835,12 +841,9 @@ class TestUpdate(UpdateTestCase):
|
||||
versions=None,
|
||||
count=5):
|
||||
|
||||
latest_version = self.prepare_agents(count=count)
|
||||
if versions is None or len(versions) <= 0:
|
||||
versions = [latest_version]
|
||||
|
||||
if protocol is None:
|
||||
protocol = ProtocolMock(versions=versions)
|
||||
protocol = self._create_protocol(count=count, versions=versions)
|
||||
|
||||
self.update_handler.protocol_util = protocol
|
||||
conf.get_autoupdate_gafamily = Mock(return_value=protocol.family)
|
||||
|
||||
@@ -850,6 +853,16 @@ class TestUpdate(UpdateTestCase):
|
||||
self.assertTrue(self._test_upgrade_available())
|
||||
return
|
||||
|
||||
def test_upgrade_available_will_refresh_goal_state(self):
|
||||
protocol = self._create_protocol()
|
||||
protocol.emulate_stale_goal_state()
|
||||
self.assertTrue(self._test_upgrade_available(protocol=protocol))
|
||||
self.assertEqual(2, protocol.call_counts["get_vmagent_manifests"])
|
||||
self.assertEqual(1, protocol.call_counts["get_vmagent_pkgs"])
|
||||
self.assertEqual(1, protocol.call_counts["update_goal_state"])
|
||||
self.assertTrue(protocol.goal_state_forced)
|
||||
return
|
||||
|
||||
def test_get_latest_agent_excluded(self):
|
||||
self.prepare_agent(AGENT_VERSION)
|
||||
self.assertFalse(self._test_upgrade_available(
|
||||
@@ -1549,12 +1562,22 @@ class ProtocolMock(object):
|
||||
def __init__(self, family="TestAgent", etag=42, versions=None, client=None):
|
||||
self.family = family
|
||||
self.client = client
|
||||
self.call_counts = {
|
||||
"get_vmagent_manifests" : 0,
|
||||
"get_vmagent_pkgs" : 0,
|
||||
"update_goal_state" : 0
|
||||
}
|
||||
self.goal_state_is_stale = False
|
||||
self.goal_state_forced = False
|
||||
self.etag = etag
|
||||
self.versions = versions if versions is not None else []
|
||||
self.create_manifests()
|
||||
self.create_packages()
|
||||
return
|
||||
|
||||
def emulate_stale_goal_state(self):
|
||||
self.goal_state_is_stale = True
|
||||
|
||||
def create_manifests(self):
|
||||
self.agent_manifests = VMAgentManifestList()
|
||||
if len(self.versions) <= 0:
|
||||
@@ -1585,11 +1608,23 @@ class ProtocolMock(object):
|
||||
return self
|
||||
|
||||
def get_vmagent_manifests(self):
|
||||
self.call_counts["get_vmagent_manifests"] += 1
|
||||
if self.goal_state_is_stale:
|
||||
self.goal_state_is_stale = False
|
||||
raise BadRequestError()
|
||||
return self.agent_manifests, self.etag
|
||||
|
||||
def get_vmagent_pkgs(self, manifest):
|
||||
self.call_counts["get_vmagent_pkgs"] += 1
|
||||
if self.goal_state_is_stale:
|
||||
self.goal_state_is_stale = False
|
||||
raise BadRequestError()
|
||||
return self.agent_packages
|
||||
|
||||
def update_goal_state(self, forced=False, max_retry=3):
|
||||
self.call_counts["update_goal_state"] += 1
|
||||
self.goal_state_forced = self.goal_state_forced or forced
|
||||
return
|
||||
|
||||
class ResponseMock(Mock):
|
||||
def __init__(self, status=restutil.httpclient.OK, response=None, reason=None):
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#
|
||||
|
||||
from tests.tools import *
|
||||
from azurelinuxagent.common.exception import BadRequestError, HttpError
|
||||
from azurelinuxagent.common.future import httpclient
|
||||
from azurelinuxagent.common.utils.cryptutil import CryptUtil
|
||||
|
||||
@@ -53,6 +54,20 @@ DATA_FILE_EXT_AUTOUPGRADE_INTERNALVERSION["ext_conf"] = "wire/ext_conf_autoupgra
|
||||
|
||||
class WireProtocolData(object):
|
||||
def __init__(self, data_files=DATA_FILE):
|
||||
self.emulate_stale_goal_state = False
|
||||
self.call_counts = {
|
||||
"comp=versions" : 0,
|
||||
"/versions" : 0,
|
||||
"goalstate" : 0,
|
||||
"hostingenvuri" : 0,
|
||||
"sharedconfiguri" : 0,
|
||||
"certificatesuri" : 0,
|
||||
"extensionsconfiguri" : 0,
|
||||
"extensionArtifact" : 0,
|
||||
"manifest.xml" : 0,
|
||||
"manifest_of_ga.xml" : 0,
|
||||
"ExampleHandlerLinux" : 0
|
||||
}
|
||||
self.version_info = load_data(data_files.get("version_info"))
|
||||
self.goal_state = load_data(data_files.get("goal_state"))
|
||||
self.hosting_env = load_data(data_files.get("hosting_env"))
|
||||
@@ -67,32 +82,70 @@ class WireProtocolData(object):
|
||||
|
||||
def mock_http_get(self, url, *args, **kwargs):
|
||||
content = None
|
||||
if "versions" in url:
|
||||
content = self.version_info
|
||||
elif "goalstate" in url:
|
||||
content = self.goal_state
|
||||
elif "hostingenvuri" in url:
|
||||
content = self.hosting_env
|
||||
elif "sharedconfiguri" in url:
|
||||
content = self.shared_config
|
||||
elif "certificatesuri" in url:
|
||||
content = self.certs
|
||||
elif "extensionsconfiguri" in url:
|
||||
content = self.ext_conf
|
||||
elif "manifest.xml" in url:
|
||||
content = self.manifest
|
||||
elif "manifest_of_ga.xml" in url:
|
||||
content = self.ga_manifest
|
||||
elif "ExampleHandlerLinux" in url:
|
||||
content = self.ext
|
||||
resp = MagicMock()
|
||||
resp.status = httpclient.OK
|
||||
resp.read = Mock(return_value=content)
|
||||
return resp
|
||||
else:
|
||||
raise Exception("Bad url {0}".format(url))
|
||||
|
||||
resp = MagicMock()
|
||||
resp.status = httpclient.OK
|
||||
|
||||
# wire server versions
|
||||
if "comp=versions" in url:
|
||||
content = self.version_info
|
||||
self.call_counts["comp=versions"] += 1
|
||||
|
||||
# HostPlugin versions
|
||||
elif "/versions" in url:
|
||||
content = '["2015-09-01"]'
|
||||
self.call_counts["/versions"] += 1
|
||||
elif "goalstate" in url:
|
||||
content = self.goal_state
|
||||
self.call_counts["goalstate"] += 1
|
||||
elif "hostingenvuri" in url:
|
||||
content = self.hosting_env
|
||||
self.call_counts["hostingenvuri"] += 1
|
||||
elif "sharedconfiguri" in url:
|
||||
content = self.shared_config
|
||||
self.call_counts["sharedconfiguri"] += 1
|
||||
elif "certificatesuri" in url:
|
||||
content = self.certs
|
||||
self.call_counts["certificatesuri"] += 1
|
||||
elif "extensionsconfiguri" in url:
|
||||
content = self.ext_conf
|
||||
self.call_counts["extensionsconfiguri"] += 1
|
||||
|
||||
else:
|
||||
# A stale GoalState results in a 400 from the HostPlugin
|
||||
# for which the HTTP handler in restutil raises BadRequestError
|
||||
if self.emulate_stale_goal_state:
|
||||
if "extensionArtifact" in url:
|
||||
self.emulate_stale_goal_state = False
|
||||
self.call_counts["extensionArtifact"] += 1
|
||||
raise BadRequestError()
|
||||
else:
|
||||
raise HttpError()
|
||||
|
||||
# For HostPlugin requests, replace the URL with that passed
|
||||
# via the x-ms-artifact-location header
|
||||
if "extensionArtifact" in url:
|
||||
self.call_counts["extensionArtifact"] += 1
|
||||
if "headers" not in kwargs or \
|
||||
"x-ms-artifact-location" not in kwargs["headers"]:
|
||||
raise Exception("Bad HEADERS passed to HostPlugin: {0}",
|
||||
kwargs)
|
||||
url = kwargs["headers"]["x-ms-artifact-location"]
|
||||
|
||||
if "manifest.xml" in url:
|
||||
content = self.manifest
|
||||
self.call_counts["manifest.xml"] += 1
|
||||
elif "manifest_of_ga.xml" in url:
|
||||
content = self.ga_manifest
|
||||
self.call_counts["manifest_of_ga.xml"] += 1
|
||||
elif "ExampleHandlerLinux" in url:
|
||||
content = self.ext
|
||||
self.call_counts["ExampleHandlerLinux"] += 1
|
||||
resp.read = Mock(return_value=content)
|
||||
return resp
|
||||
else:
|
||||
raise Exception("Bad url {0}".format(url))
|
||||
|
||||
resp.read = Mock(return_value=content.encode("utf-8"))
|
||||
return resp
|
||||
|
||||
|
||||
@@ -146,6 +146,7 @@ class TestHostPlugin(AgentTestCase):
|
||||
test_goal_state = wire.GoalState(WireProtocolData(DATA_FILE).goal_state)
|
||||
status = restapi.VMStatus(status="Ready",
|
||||
message="Guest Agent is running")
|
||||
wire.HostPluginProtocol.set_default_channel(False)
|
||||
with patch.object(wire.HostPluginProtocol,
|
||||
"ensure_initialized",
|
||||
return_value=True):
|
||||
@@ -173,6 +174,7 @@ class TestHostPlugin(AgentTestCase):
|
||||
test_goal_state = wire.GoalState(WireProtocolData(DATA_FILE).goal_state)
|
||||
status = restapi.VMStatus(status="Ready",
|
||||
message="Guest Agent is running")
|
||||
wire.HostPluginProtocol.set_default_channel(False)
|
||||
with patch.object(wire.StatusBlob,
|
||||
"upload",
|
||||
return_value=False):
|
||||
@@ -211,6 +213,8 @@ class TestHostPlugin(AgentTestCase):
|
||||
bytearray(faux_status, encoding='utf-8'))
|
||||
|
||||
with patch.object(restutil, "http_request") as patch_http:
|
||||
patch_http.return_value = Mock(status=httpclient.OK)
|
||||
|
||||
wire_protocol_client.get_goal_state = Mock(return_value=test_goal_state)
|
||||
plugin = wire_protocol_client.get_host_plugin()
|
||||
|
||||
@@ -224,61 +228,6 @@ class TestHostPlugin(AgentTestCase):
|
||||
test_goal_state,
|
||||
exp_method, exp_url, exp_data)
|
||||
|
||||
def test_read_response_error(self):
|
||||
"""
|
||||
Validate the read_response_error method handles encoding correctly
|
||||
"""
|
||||
responses = ['message', b'message', '\x80message\x80']
|
||||
response = MagicMock()
|
||||
response.status = 'status'
|
||||
response.reason = 'reason'
|
||||
with patch.object(response, 'read') as patch_response:
|
||||
for s in responses:
|
||||
patch_response.return_value = s
|
||||
result = hostplugin.HostPluginProtocol.read_response_error(response)
|
||||
self.assertTrue('[status: reason]' in result)
|
||||
self.assertTrue('message' in result)
|
||||
|
||||
def test_read_response_bytes(self):
|
||||
response_bytes = '7b:0a:20:20:20:20:22:65:72:72:6f:72:43:6f:64:65:22:' \
|
||||
'3a:20:22:54:68:65:20:62:6c:6f:62:20:74:79:70:65:20:' \
|
||||
'69:73:20:69:6e:76:61:6c:69:64:20:66:6f:72:20:74:68:' \
|
||||
'69:73:20:6f:70:65:72:61:74:69:6f:6e:2e:22:2c:0a:20:' \
|
||||
'20:20:20:22:6d:65:73:73:61:67:65:22:3a:20:22:c3:af:' \
|
||||
'c2:bb:c2:bf:3c:3f:78:6d:6c:20:76:65:72:73:69:6f:6e:' \
|
||||
'3d:22:31:2e:30:22:20:65:6e:63:6f:64:69:6e:67:3d:22:' \
|
||||
'75:74:66:2d:38:22:3f:3e:3c:45:72:72:6f:72:3e:3c:43:' \
|
||||
'6f:64:65:3e:49:6e:76:61:6c:69:64:42:6c:6f:62:54:79:' \
|
||||
'70:65:3c:2f:43:6f:64:65:3e:3c:4d:65:73:73:61:67:65:' \
|
||||
'3e:54:68:65:20:62:6c:6f:62:20:74:79:70:65:20:69:73:' \
|
||||
'20:69:6e:76:61:6c:69:64:20:66:6f:72:20:74:68:69:73:' \
|
||||
'20:6f:70:65:72:61:74:69:6f:6e:2e:0a:52:65:71:75:65:' \
|
||||
'73:74:49:64:3a:63:37:34:32:39:30:63:62:2d:30:30:30:' \
|
||||
'31:2d:30:30:62:35:2d:30:36:64:61:2d:64:64:36:36:36:' \
|
||||
'61:30:30:30:22:2c:0a:20:20:20:20:22:64:65:74:61:69:' \
|
||||
'6c:73:22:3a:20:22:22:0a:7d'.split(':')
|
||||
expected_response = '[status: reason] {\n "errorCode": "The blob ' \
|
||||
'type is invalid for this operation.",\n ' \
|
||||
'"message": "<?xml version="1.0" ' \
|
||||
'encoding="utf-8"?>' \
|
||||
'<Error><Code>InvalidBlobType</Code><Message>The ' \
|
||||
'blob type is invalid for this operation.\n' \
|
||||
'RequestId:c74290cb-0001-00b5-06da-dd666a000",' \
|
||||
'\n "details": ""\n}'
|
||||
|
||||
response_string = ''.join(chr(int(b, 16)) for b in response_bytes)
|
||||
response = MagicMock()
|
||||
response.status = 'status'
|
||||
response.reason = 'reason'
|
||||
with patch.object(response, 'read') as patch_response:
|
||||
patch_response.return_value = response_string
|
||||
result = hostplugin.HostPluginProtocol.read_response_error(response)
|
||||
self.assertEqual(result, expected_response)
|
||||
try:
|
||||
raise HttpError("{0}".format(result))
|
||||
except HttpError as e:
|
||||
self.assertTrue(result in ustr(e))
|
||||
|
||||
def test_no_fallback(self):
|
||||
"""
|
||||
Validate fallback to upload status using HostGAPlugin is not happening
|
||||
@@ -318,6 +267,8 @@ class TestHostPlugin(AgentTestCase):
|
||||
bytearray(faux_status, encoding='utf-8'))
|
||||
|
||||
with patch.object(restutil, "http_request") as patch_http:
|
||||
patch_http.return_value = Mock(status=httpclient.OK)
|
||||
|
||||
with patch.object(wire.HostPluginProtocol,
|
||||
"get_api_versions") as patch_get:
|
||||
patch_get.return_value = api_versions
|
||||
|
||||
@@ -31,17 +31,15 @@ class TestMetadataProtocolGetters(AgentTestCase):
|
||||
return json.loads(ustr(load_data(path)), encoding="utf-8")
|
||||
|
||||
@patch("time.sleep")
|
||||
@patch("azurelinuxagent.common.protocol.metadata.restutil")
|
||||
def _test_getters(self, test_data, mock_restutil ,_):
|
||||
mock_restutil.http_get.side_effect = test_data.mock_http_get
|
||||
|
||||
protocol = MetadataProtocol()
|
||||
protocol.detect()
|
||||
protocol.get_vminfo()
|
||||
protocol.get_certs()
|
||||
ext_handlers, etag = protocol.get_ext_handlers()
|
||||
for ext_handler in ext_handlers.extHandlers:
|
||||
protocol.get_ext_handler_pkgs(ext_handler)
|
||||
def _test_getters(self, test_data ,_):
|
||||
with patch.object(restutil, 'http_get', test_data.mock_http_get):
|
||||
protocol = MetadataProtocol()
|
||||
protocol.detect()
|
||||
protocol.get_vminfo()
|
||||
protocol.get_certs()
|
||||
ext_handlers, etag = protocol.get_ext_handlers()
|
||||
for ext_handler in ext_handlers.extHandlers:
|
||||
protocol.get_ext_handler_pkgs(ext_handler)
|
||||
|
||||
def test_getters(self, *args):
|
||||
test_data = MetadataProtocolData(DATA_FILE)
|
||||
|
||||
+37
-20
@@ -25,30 +25,34 @@ wireserver_url = '168.63.129.16'
|
||||
|
||||
@patch("time.sleep")
|
||||
@patch("azurelinuxagent.common.protocol.wire.CryptUtil")
|
||||
@patch("azurelinuxagent.common.protocol.wire.restutil")
|
||||
class TestWireProtocolGetters(AgentTestCase):
|
||||
def _test_getters(self, test_data, mock_restutil, MockCryptUtil, _):
|
||||
mock_restutil.http_get.side_effect = test_data.mock_http_get
|
||||
|
||||
def setUp(self):
|
||||
super(TestWireProtocolGetters, self).setUp()
|
||||
HostPluginProtocol.set_default_channel(False)
|
||||
|
||||
def _test_getters(self, test_data, MockCryptUtil, _):
|
||||
MockCryptUtil.side_effect = test_data.mock_crypt_util
|
||||
|
||||
protocol = WireProtocol(wireserver_url)
|
||||
protocol.detect()
|
||||
protocol.get_vminfo()
|
||||
protocol.get_certs()
|
||||
ext_handlers, etag = protocol.get_ext_handlers()
|
||||
for ext_handler in ext_handlers.extHandlers:
|
||||
protocol.get_ext_handler_pkgs(ext_handler)
|
||||
with patch.object(restutil, 'http_get', test_data.mock_http_get):
|
||||
protocol = WireProtocol(wireserver_url)
|
||||
protocol.detect()
|
||||
protocol.get_vminfo()
|
||||
protocol.get_certs()
|
||||
ext_handlers, etag = protocol.get_ext_handlers()
|
||||
for ext_handler in ext_handlers.extHandlers:
|
||||
protocol.get_ext_handler_pkgs(ext_handler)
|
||||
|
||||
crt1 = os.path.join(self.tmp_dir,
|
||||
'33B0ABCE4673538650971C10F7D7397E71561F35.crt')
|
||||
crt2 = os.path.join(self.tmp_dir,
|
||||
'4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3.crt')
|
||||
prv2 = os.path.join(self.tmp_dir,
|
||||
'4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3.prv')
|
||||
crt1 = os.path.join(self.tmp_dir,
|
||||
'33B0ABCE4673538650971C10F7D7397E71561F35.crt')
|
||||
crt2 = os.path.join(self.tmp_dir,
|
||||
'4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3.crt')
|
||||
prv2 = os.path.join(self.tmp_dir,
|
||||
'4037FBF5F1F3014F99B5D6C7799E9B20E6871CB3.prv')
|
||||
|
||||
self.assertTrue(os.path.isfile(crt1))
|
||||
self.assertTrue(os.path.isfile(crt2))
|
||||
self.assertTrue(os.path.isfile(prv2))
|
||||
self.assertTrue(os.path.isfile(crt1))
|
||||
self.assertTrue(os.path.isfile(crt2))
|
||||
self.assertTrue(os.path.isfile(prv2))
|
||||
|
||||
def test_getters(self, *args):
|
||||
"""Normal case"""
|
||||
@@ -70,8 +74,21 @@ class TestWireProtocolGetters(AgentTestCase):
|
||||
test_data = WireProtocolData(DATA_FILE_EXT_NO_PUBLIC)
|
||||
self._test_getters(test_data, *args)
|
||||
|
||||
def test_getters_with_stale_goal_state(self, *args):
|
||||
test_data = WireProtocolData(DATA_FILE)
|
||||
test_data.emulate_stale_goal_state = True
|
||||
|
||||
self._test_getters(test_data, *args)
|
||||
# Ensure HostPlugin was invoked
|
||||
self.assertEqual(1, test_data.call_counts["/versions"])
|
||||
self.assertEqual(2, test_data.call_counts["extensionArtifact"])
|
||||
# Ensure the expected number of HTTP calls were made
|
||||
# -- Tracking calls to retrieve GoalState is problematic since it is
|
||||
# fetched often; however, the dependent documents, such as the
|
||||
# HostingEnvironmentConfig, will be retrieved the expected number
|
||||
self.assertEqual(2, test_data.call_counts["hostingenvuri"])
|
||||
|
||||
def test_call_storage_kwargs(self,
|
||||
mock_restutil,
|
||||
mock_cryptutil,
|
||||
mock_sleep):
|
||||
from azurelinuxagent.common.utils import restutil
|
||||
|
||||
@@ -17,12 +17,55 @@
|
||||
|
||||
import mock
|
||||
import os.path
|
||||
import sys
|
||||
|
||||
from azurelinuxagent.agent import *
|
||||
from azurelinuxagent.common.conf import *
|
||||
|
||||
from tests.tools import *
|
||||
|
||||
EXPECTED_CONFIGURATION = \
|
||||
"""AutoUpdate.Enabled = True
|
||||
AutoUpdate.GAFamily = Prod
|
||||
Autoupdate.Frequency = 3600
|
||||
DVD.MountPoint = /mnt/cdrom/secure
|
||||
DetectScvmmEnv = False
|
||||
EnableOverProvisioning = False
|
||||
Extension.LogDir = /var/log/azure
|
||||
HttpProxy.Host = None
|
||||
HttpProxy.Port = None
|
||||
Lib.Dir = /var/lib/waagent
|
||||
Logs.Verbose = False
|
||||
OS.AllowHTTP = False
|
||||
OS.CheckRdmaDriver = False
|
||||
OS.EnableFIPS = True
|
||||
OS.EnableRDMA = False
|
||||
OS.HomeDir = /home
|
||||
OS.OpensslPath = /usr/bin/openssl
|
||||
OS.PasswordPath = /etc/shadow
|
||||
OS.RootDeviceScsiTimeout = 300
|
||||
OS.SshDir = /notareal/path
|
||||
OS.SudoersDir = /etc/sudoers.d
|
||||
OS.UpdateRdmaDriver = False
|
||||
Pid.File = /var/run/waagent.pid
|
||||
Provisioning.AllowResetSysUser = False
|
||||
Provisioning.DecodeCustomData = False
|
||||
Provisioning.DeleteRootPassword = True
|
||||
Provisioning.Enabled = True
|
||||
Provisioning.ExecuteCustomData = False
|
||||
Provisioning.MonitorHostName = True
|
||||
Provisioning.PasswordCryptId = 6
|
||||
Provisioning.PasswordCryptSaltLength = 10
|
||||
Provisioning.RegenerateSshHostKeyPair = True
|
||||
Provisioning.SshHostKeyPairType = rsa
|
||||
Provisioning.UseCloudInit = True
|
||||
ResourceDisk.EnableSwap = False
|
||||
ResourceDisk.Filesystem = ext4
|
||||
ResourceDisk.Format = True
|
||||
ResourceDisk.MountOptions = None
|
||||
ResourceDisk.MountPoint = /mnt/resource
|
||||
ResourceDisk.SwapSizeMB = 0
|
||||
""".split('\n')
|
||||
|
||||
class TestAgent(AgentTestCase):
|
||||
|
||||
@@ -115,3 +158,11 @@ class TestAgent(AgentTestCase):
|
||||
self.assertTrue(os.path.isfile(ext_log_dir))
|
||||
self.assertFalse(os.path.isdir(ext_log_dir))
|
||||
mock_log.assert_called_once()
|
||||
|
||||
def test_agent_show_configuration(self):
|
||||
if not hasattr(sys.stdout, 'getvalue'):
|
||||
self.fail('Test requires at least Python 2.7 with buffered output')
|
||||
agent = Agent(False,
|
||||
conf_file_path=os.path.join(data_dir, "test_waagent.conf"))
|
||||
agent.show_configuration()
|
||||
self.assertEqual(EXPECTED_CONFIGURATION, sys.stdout.getvalue().split('\n'))
|
||||
|
||||
@@ -16,8 +16,12 @@
|
||||
#
|
||||
|
||||
import unittest
|
||||
|
||||
from azurelinuxagent.common.exception import BadRequestError, \
|
||||
HttpError, ProtocolError
|
||||
import azurelinuxagent.common.utils.restutil as restutil
|
||||
from azurelinuxagent.common.future import httpclient
|
||||
|
||||
from azurelinuxagent.common.future import httpclient, ustr
|
||||
from tests.tools import AgentTestCase, patch, Mock, MagicMock
|
||||
|
||||
|
||||
@@ -115,6 +119,175 @@ class TestHttpOperations(AgentTestCase):
|
||||
self.assertRaises(restutil.HttpError, restutil.http_get,
|
||||
"http://foo.bar")
|
||||
|
||||
@patch("time.sleep")
|
||||
@patch("azurelinuxagent.common.utils.restutil._http_request")
|
||||
def test_http_request_retries_status_codes(self, _http_request, _sleep):
|
||||
_http_request.side_effect = [
|
||||
Mock(status=httpclient.SERVICE_UNAVAILABLE),
|
||||
Mock(status=httpclient.OK)
|
||||
]
|
||||
|
||||
restutil.http_get("https://foo.bar")
|
||||
self.assertEqual(2, _http_request.call_count)
|
||||
self.assertEqual(1, _sleep.call_count)
|
||||
|
||||
@patch("time.sleep")
|
||||
@patch("azurelinuxagent.common.utils.restutil._http_request")
|
||||
def test_http_request_retries_passed_status_codes(self, _http_request, _sleep):
|
||||
# Ensure the code is not part of the standard set
|
||||
self.assertFalse(httpclient.UNAUTHORIZED in restutil.RETRY_CODES)
|
||||
|
||||
_http_request.side_effect = [
|
||||
Mock(status=httpclient.UNAUTHORIZED),
|
||||
Mock(status=httpclient.OK)
|
||||
]
|
||||
|
||||
restutil.http_get("https://foo.bar", retry_codes=[httpclient.UNAUTHORIZED])
|
||||
self.assertEqual(2, _http_request.call_count)
|
||||
self.assertEqual(1, _sleep.call_count)
|
||||
|
||||
@patch("time.sleep")
|
||||
@patch("azurelinuxagent.common.utils.restutil._http_request")
|
||||
def test_http_request_raises_for_bad_request(self, _http_request, _sleep):
|
||||
_http_request.side_effect = [
|
||||
Mock(status=httpclient.BAD_REQUEST)
|
||||
]
|
||||
|
||||
self.assertRaises(BadRequestError, restutil.http_get, "https://foo.bar")
|
||||
self.assertEqual(1, _http_request.call_count)
|
||||
|
||||
@patch("time.sleep")
|
||||
@patch("azurelinuxagent.common.utils.restutil._http_request")
|
||||
def test_http_request_retries_exceptions(self, _http_request, _sleep):
|
||||
# Testing each exception is difficult because they have varying
|
||||
# signatures; for now, test one and ensure the set is unchanged
|
||||
recognized_exceptions = [
|
||||
httpclient.NotConnected,
|
||||
httpclient.IncompleteRead,
|
||||
httpclient.ImproperConnectionState,
|
||||
httpclient.BadStatusLine
|
||||
]
|
||||
self.assertEqual(recognized_exceptions, restutil.RETRY_EXCEPTIONS)
|
||||
|
||||
_http_request.side_effect = [
|
||||
httpclient.IncompleteRead(''),
|
||||
Mock(status=httpclient.OK)
|
||||
]
|
||||
|
||||
restutil.http_get("https://foo.bar")
|
||||
self.assertEqual(2, _http_request.call_count)
|
||||
self.assertEqual(1, _sleep.call_count)
|
||||
|
||||
@patch("time.sleep")
|
||||
@patch("azurelinuxagent.common.utils.restutil._http_request")
|
||||
def test_http_request_retries_ioerrors(self, _http_request, _sleep):
|
||||
ioerror = IOError()
|
||||
|
||||
for errno in restutil.RETRY_IOERRORS:
|
||||
_http_request.reset_mock()
|
||||
_sleep.reset_mock()
|
||||
|
||||
ioerror.errno = errno
|
||||
|
||||
_http_request.side_effect = [
|
||||
ioerror,
|
||||
Mock(status=httpclient.OK)
|
||||
]
|
||||
|
||||
restutil.http_get("https://foo.bar")
|
||||
self.assertEqual(2, _http_request.call_count)
|
||||
self.assertEqual(1, _sleep.call_count)
|
||||
|
||||
def test_request_failed(self):
|
||||
self.assertTrue(restutil.request_failed(None))
|
||||
|
||||
resp = Mock()
|
||||
for status in restutil.OK_CODES:
|
||||
resp.status = status
|
||||
self.assertFalse(restutil.request_failed(resp))
|
||||
|
||||
self.assertFalse(httpclient.BAD_REQUEST in restutil.OK_CODES)
|
||||
resp.status = httpclient.BAD_REQUEST
|
||||
self.assertTrue(restutil.request_failed(resp))
|
||||
|
||||
self.assertFalse(
|
||||
restutil.request_failed(
|
||||
resp, ok_codes=[httpclient.BAD_REQUEST]))
|
||||
|
||||
def test_request_succeeded(self):
|
||||
self.assertFalse(restutil.request_succeeded(None))
|
||||
|
||||
resp = Mock()
|
||||
for status in restutil.OK_CODES:
|
||||
resp.status = status
|
||||
self.assertTrue(restutil.request_succeeded(resp))
|
||||
|
||||
self.assertFalse(httpclient.BAD_REQUEST in restutil.OK_CODES)
|
||||
resp.status = httpclient.BAD_REQUEST
|
||||
self.assertFalse(restutil.request_succeeded(resp))
|
||||
|
||||
self.assertTrue(
|
||||
restutil.request_succeeded(
|
||||
resp, ok_codes=[httpclient.BAD_REQUEST]))
|
||||
|
||||
def test_read_response_error(self):
|
||||
"""
|
||||
Validate the read_response_error method handles encoding correctly
|
||||
"""
|
||||
responses = ['message', b'message', '\x80message\x80']
|
||||
response = MagicMock()
|
||||
response.status = 'status'
|
||||
response.reason = 'reason'
|
||||
with patch.object(response, 'read') as patch_response:
|
||||
for s in responses:
|
||||
patch_response.return_value = s
|
||||
result = restutil.read_response_error(response)
|
||||
print("RESPONSE: {0}".format(s))
|
||||
print("RESULT: {0}".format(result))
|
||||
print("PRESENT: {0}".format('[status: reason]' in result))
|
||||
self.assertTrue('[status: reason]' in result)
|
||||
self.assertTrue('message' in result)
|
||||
|
||||
def test_read_response_bytes(self):
|
||||
response_bytes = '7b:0a:20:20:20:20:22:65:72:72:6f:72:43:6f:64:65:22:' \
|
||||
'3a:20:22:54:68:65:20:62:6c:6f:62:20:74:79:70:65:20:' \
|
||||
'69:73:20:69:6e:76:61:6c:69:64:20:66:6f:72:20:74:68:' \
|
||||
'69:73:20:6f:70:65:72:61:74:69:6f:6e:2e:22:2c:0a:20:' \
|
||||
'20:20:20:22:6d:65:73:73:61:67:65:22:3a:20:22:c3:af:' \
|
||||
'c2:bb:c2:bf:3c:3f:78:6d:6c:20:76:65:72:73:69:6f:6e:' \
|
||||
'3d:22:31:2e:30:22:20:65:6e:63:6f:64:69:6e:67:3d:22:' \
|
||||
'75:74:66:2d:38:22:3f:3e:3c:45:72:72:6f:72:3e:3c:43:' \
|
||||
'6f:64:65:3e:49:6e:76:61:6c:69:64:42:6c:6f:62:54:79:' \
|
||||
'70:65:3c:2f:43:6f:64:65:3e:3c:4d:65:73:73:61:67:65:' \
|
||||
'3e:54:68:65:20:62:6c:6f:62:20:74:79:70:65:20:69:73:' \
|
||||
'20:69:6e:76:61:6c:69:64:20:66:6f:72:20:74:68:69:73:' \
|
||||
'20:6f:70:65:72:61:74:69:6f:6e:2e:0a:52:65:71:75:65:' \
|
||||
'73:74:49:64:3a:63:37:34:32:39:30:63:62:2d:30:30:30:' \
|
||||
'31:2d:30:30:62:35:2d:30:36:64:61:2d:64:64:36:36:36:' \
|
||||
'61:30:30:30:22:2c:0a:20:20:20:20:22:64:65:74:61:69:' \
|
||||
'6c:73:22:3a:20:22:22:0a:7d'.split(':')
|
||||
expected_response = '[HTTP Failed] [status: reason] {\n "errorCode": "The blob ' \
|
||||
'type is invalid for this operation.",\n ' \
|
||||
'"message": "<?xml version="1.0" ' \
|
||||
'encoding="utf-8"?>' \
|
||||
'<Error><Code>InvalidBlobType</Code><Message>The ' \
|
||||
'blob type is invalid for this operation.\n' \
|
||||
'RequestId:c74290cb-0001-00b5-06da-dd666a000",' \
|
||||
'\n "details": ""\n}'
|
||||
|
||||
response_string = ''.join(chr(int(b, 16)) for b in response_bytes)
|
||||
response = MagicMock()
|
||||
response.status = 'status'
|
||||
response.reason = 'reason'
|
||||
with patch.object(response, 'read') as patch_response:
|
||||
patch_response.return_value = response_string
|
||||
result = restutil.read_response_error(response)
|
||||
self.assertEqual(result, expected_response)
|
||||
try:
|
||||
raise HttpError("{0}".format(result))
|
||||
except HttpError as e:
|
||||
self.assertTrue(result in ustr(e))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -34,6 +34,19 @@ class TestTextUtil(AgentTestCase):
|
||||
password_hash = textutil.gen_password_hash(data, 6, 10)
|
||||
self.assertNotEquals(None, password_hash)
|
||||
|
||||
def test_replace_non_ascii(self):
|
||||
data = ustr(b'\xef\xbb\xbfhehe', encoding='utf-8')
|
||||
self.assertEqual('hehe', textutil.replace_non_ascii(data))
|
||||
|
||||
data = "abcd\xa0e\xf0fghijk\xbblm"
|
||||
self.assertEqual("abcdefghijklm", textutil.replace_non_ascii(data))
|
||||
|
||||
data = "abcd\xa0e\xf0fghijk\xbblm"
|
||||
self.assertEqual("abcdXeXfghijkXlm",
|
||||
textutil.replace_non_ascii(data, replace_char='X'))
|
||||
|
||||
self.assertEqual('', textutil.replace_non_ascii(None))
|
||||
|
||||
def test_remove_bom(self):
|
||||
#Test bom could be removed
|
||||
data = ustr(b'\xef\xbb\xbfhehe', encoding='utf-8')
|
||||
|
||||
Reference in New Issue
Block a user