File: /volume1/@appstore/HyperBackup/addon/azure_blob/python/azure_agent.py
#!/usr/bin/env python3
#-*-coding: utf-8 -*-
import os, sys, json, ssl
import urllib.parse
from struct import pack, unpack
from time import strptime, mktime
from syslog import syslog, LOG_ERR
from ctypes import CDLL, create_string_buffer, c_char_p, c_uint
# add include path
from pathlib import Path
script_path = Path(os.path.realpath(sys.argv[0]))
sys.path.insert(1, str(script_path.parent.joinpath('azure-sdk-for-python')))
sys.path.insert(2, str(script_path.parent.joinpath('module')))
sys.path.insert(3, str(script_path.parents[2].joinpath('common').joinpath('python_modules')))
debug = False
def log_debug(*args):
if debug:
print >> sys.stderr, ' '.join(args)
def _is_scoket_exception(e):
import socket
# NOTE: socket.error
# In python 2, a child class of IOError, ref: https://docs.python.org/2/library/socket.html?#socket.error
# In python 3, alias of OSError, ref: https://docs.python.org/3/library/socket.html#socket.error
if type(e) == socket.gaierror or issubclass(type(e), socket.error) or type(e) == socket.herror or type(e) == socket.timeout:
return True
return False
def _convert_socket_error_code(message, auth_done):
error_code = -1
if -1 != message.find("[Errno -2] Name or service not known"):
if auth_done:
error_code = -4
else:
error_code = -2
elif -1 != message.find("[Errno -3] Temporary failure in name resolution"):
error_code = -4
elif -1 != message.find("[Errno -5] No address associated with hostname"):
error_code = -4
elif -1 != message.find("[Errno 110] Connection timed out"):
error_code = 408
elif -1 != message.find("[Errno 101] Network is unreachable"):
error_code = -4
elif -1 != message.find("[Errno 111] Connection refused"):
error_code = -4
elif -1 != message.find("[Errno 113] No route to host"):
error_code = -4
elif -1 != message.find("[Errno 104] Connection reset by peer"):
error_code = -4
elif -1 != message.find("[Errno 32] Broken pipe"):
error_code = 408
return error_code
def _is_ssl_timeout(message):
#ssl.SSLErr_timeouteption log
if -1 != message.find("timed out"):
# The read operation timed out
# The write operation timed out
# The handshake operation timed out
return True
return False
def _convert_socket_exception(e, auth_done):
import socket
error_code = -1
error_msg = 'Unknown'
error_cls = 'Unknown'
try:
error_cls = type(e).__name__
if hasattr(e, '__module__') and e.__module__:
error_cls = e.__module__ + '.' + error_cls
if type(e) == socket.gaierror:
if hasattr(e, 'strerror') and e.strerror:
error_msg = e.strerror
if e.errno == -2:
# socket.gaierror: [Errno -2] Name or service not known
# 1. dns not set
# 2. set to a wrong dns
if auth_done:
error_code = -4
else:
error_code = -2
elif e.errno == -3:
# socket.gaierror: [Errno -3] Temporary failure in name resolution
# man gai_strerror, and find EAI_AGAIN:
# The name server returned a temporary failure indication. Try again later.
error_code = -4
elif e.errno == -5:
# socket.gaierror: [Errno -5] No address associated with hostname
# man gai_strerror, and find EAI_NODATA:
# The specified network host exists, but does not have any network addresses defined.
error_code = -4
elif e.errno < 100 and e.errno > 0:
error_code = e.errno
elif type(e) == socket.error or type(e) == socket.herror:
if hasattr(e, 'strerror') and e.strerror:
error_msg = e.strerror
elif hasattr(e, 'args'):
error_msg = str(e.args)
if -1 != error_msg.find('Tunnel connection failed'):
error_code = -4
if e.errno == socket.errno.ETIMEDOUT:
# socket.error: [Errno 110] Connection timed out
error_code = 408
elif e.errno == socket.errno.ENETUNREACH:
# socket.error: [Errno 101] Network is unreachable
error_code = -4
elif e.errno == socket.errno.ECONNREFUSED:
# socket.error: [Errno 111] Connection refused
error_code = -4
elif e.errno == socket.errno.EHOSTUNREACH:
# socket.error: [Errno 113] No route to host
error_code = -4
elif e.errno == socket.errno.ECONNRESET:
# socket.error: [Errno 104] Connection reset by peer
error_code = -4
elif e.errno == socket.errno.EPIPE:
# socket.error: [Errno 32] Broken pipe
error_code = 408
elif e.errno < 100 and e.errno > 0:
error_code = e.errno
#syslog(LOG_ERR, "get network error %d:%s" % (e.errno, error_msg))
elif type(e) == socket.timeout:
error_msg = 'timed out'
error_code = 408
#ref: https://www.python.org/dev/peps/pep-3151/
elif type(e) == BrokenPipeError:
#EPIPE, ESHUTDOWN
error_msg = 'Broken pipe'
error_code = 408
elif type(e) == ConnectionRefusedError:
#ECONNREFUSED
if hasattr(e, 'strerror') and e.strerror:
error_msg = e.strerror
error_code = -4
elif type(e) == ConnectionResetError:
#ECONNRESET
if hasattr(e, 'strerror') and e.strerror:
error_msg = e.strerror
error_code = -4
elif type(e) == ConnectionError:
if hasattr(e, 'strerror') and e.strerror:
error_msg = e.strerror
error_code = _convert_socket_error_code(str(e), auth_done)
else:
syslog(LOG_ERR, "BUG: exception [%s], type [%s]" % (str(e), type(e)))
#syslog(LOG_ERR, "error %s:%d:%s" % (error_cls, error_code, str(error_msg)))
if -1 == error_code:
if hasattr(e, "args"):
syslog(LOG_ERR, "error args: [%s]" % (str(e.args)))
if hasattr(e, "errno"):
syslog(LOG_ERR, "error errno: [%s]" % (str(e.errno)))
if hasattr(e, "strerror"):
syslog(LOG_ERR, "error strerror: [%s]" % (str(e.strerror)))
except Exception as ee:
syslog(LOG_ERR, "parse socket exception failed: [%s]" % (str(ee)))
pass
return {
'success': False,
'error_class': error_cls,
'error_message': error_msg,
'error_code': error_code
}
class AzureErrorBody():
'''
Windows Azure Error Body class from xml.
https://msdn.microsoft.com/en-us/library/azure/dd179382.aspx
'''
# parsing something like:
#<?xml version="1.0" encoding="utf-8"?>
#<Error>
# <Code>...</Code>
# <Message>...</Message>
# <AuthenticationErrorDetail>...</AuthenticationErrorDetail>
# <QueryParameterName>...</QueryParameterName>
# <QueryParameterValue>...</QueryParameterValue>
# <Reason>...</Reason>
#</Error>
def __init__(self):
self.code = u''
self.message = u''
self.authenticationerrordetail = u''
def _convert_exception(e, auth_done):
import azure
import requests
import http.client
import azure.storage.common._error
#import pdb; pdb.set_trace()
error_code = -1
error_msg = 'Unknown'
error_cls = 'Unknown'
try:
error_cls = type(e).__name__
if hasattr(e, '__module__') and e.__module__:
error_cls = e.__module__ + '.' + error_cls
try:
error_msg = str(e).split('\n')[0]
log_debug("\033[31mexception: \033[0m", error_msg)
except:
log_debug("\033[31mexception: \033[0m", "could not parse error msg")
if type(e) == azure.common.AzureConflictHttpError:
error_msg = str(e.args)
error_code = 409
elif type(e) == azure.common.AzureMissingResourceHttpError:
error_msg = str(e.args)
error_code = 404
elif type(e) == azure.common.AzureHttpError:
error_msg = str(e.args)
error_code = e.status_code;
if "AuthenticationFailed" == e.error_code:
error_code = -2
elif "InvalidResourceName" == e.error_code:
error_code = -5
elif error_msg == azure.storage.common._error._ERROR_STORAGE_MISSING_INFO:
error_code = -2
elif type(e) == azure.storage.common._error.AzureSigningError:
error_msg = str(e.args)
error_code = -2
elif type(e) == TypeError:
# this exception will raise when base64 decode for auth failed
if error_msg == "Incorrect padding":
error_code = -2
elif type(e) == UnicodeDecodeError:
error_code = -5
elif type(e) == azure.common.AzureException:
error_msg = str(e.args)
error_code = -4
if type(e.args[0]) == requests.packages.urllib3.exceptions.ProtocolError:
if "BadStatusLine" in str(e.args[0]):
error_code = -4
elif _is_scoket_exception(e.args[0].args[1]):
return _convert_socket_exception(e.args[0].args[1], auth_done)
else:
syslog(LOG_ERR, "ProtocolError: exception [%s], type [%s]" % (str(e.args[0].args[1]), type(e.args[0].args[1])))
elif type(e.args[0]) == requests.packages.urllib3.exceptions.MaxRetryError:
if hasattr(e.args[0], 'reason') and e.args[0].reason:
if type(e.args[0].reason) == requests.packages.urllib3.exceptions.NewConnectionError:
error_code = _convert_socket_error_code(str(e.args[0].reason), auth_done)
else:
syslog(LOG_ERR, "MaxRetryError: exception [%s], type [%s]" % (str(e.args[0].reason), type(e.args[0].reason)))
elif type(e.args[0]) == ssl.SSLError or type(e.args[0]) == requests.packages.urllib3.exceptions.SSLError:
error_msg = str(e.args[0])
if _is_ssl_timeout(str(error_msg)):
error_code = 408
else:
syslog(LOG_ERR, "SSLError: exception [%s], type [%s]" % (str(error_msg), type(error_msg)))
elif type(e.args[0]) == requests.exceptions.SSLError:
error_code = -4
# EOF occurred in violation of protocol
# SSL: DECRYPTION_FAILED_OR_BAD_RECORD_MAC
# SSL: SSLV3_ALERT_BAD_RECORD_MAC
# SSL: SSLV3_ALERT_ILLEGAL_PARAMETER
# SSL: TLSV1_ALERT_DECRYPT_ERROR
# SSL: TLSV1_ALERT_DECODE_ERROR
# SSL: CERTIFICATE_VERIFY_FAILED
if _is_ssl_timeout(str(e.args[0])):
error_code = 408
elif type(e.args[0]) == requests.packages.urllib3.exceptions.NewConnectionError:
error_code = _convert_socket_error_code(str(e.args[0]), auth_done)
elif type(e.args[0]) == requests.packages.urllib3.exceptions.ConnectTimeoutError:
error_code = 408
elif type(e.args[0]) == requests.packages.urllib3.exceptions.ReadTimeoutError:
error_code = 408
elif type(e.args[0]) == requests.exceptions.ConnectTimeout:
error_code = 408
elif type(e.args[0]) == requests.exceptions.ReadTimeout:
error_code = 408
elif _is_scoket_exception(e.args[0]):
return _convert_socket_exception(e.args[0], auth_done)
elif type(e) == http.client.ResponseNotReady:
error_code = -4
elif type(e) == http.client.BadStatusLine:
error_code = -4
elif type(e) == http.client.IncompleteRead:
error_code = -4
elif type(e) == ssl.SSLError:
error_code = -4
if hasattr(e, "strerror") and e.strerror:
# SSL: DECRYPTION_FAILED_OR_BAD_RECORD_MAC
# SSL: SSLV3_ALERT_BAD_RECORD_MAC
# SSL: CERTIFICATE_VERIFY_FAILED
error_msg = e.strerror
if _is_ssl_timeout(str(error_msg)):
error_code = 408
elif type(e) == ssl.SSLEOFError:
# ssl.SSLEOFError: [Errno 8] EOF occurred in violation of protocol
if hasattr(e, 'strerror') and e.strerror:
error_msg = e.strerror
error_code = -4
elif type(e) == TimeoutError:
#ETIMEDOUT
error_msg = 'timed out'
error_code = 408
elif type(e) == IOError:
if hasattr(e, 'strerror') and e.strerror:
error_msg = e.strerror
error_code = _convert_socket_error_code(str(e), auth_done)
elif _is_scoket_exception(e):
return _convert_socket_exception(e, auth_done)
else:
syslog(LOG_ERR, "exception [%s]" % str(e))
syslog(LOG_ERR, "type [%s]" % type(e))
#syslog(LOG_ERR, "error %s:%d:%s" % (error_cls, error_code, str(error_msg)))
if -1 == error_code:
if hasattr(e, "args"):
syslog(LOG_ERR, "error args: [%s]" % (str(e.args)))
if hasattr(e, "errno"):
syslog(LOG_ERR, "error errno: [%s]" % (str(e.errno)))
if hasattr(e, "strerror"):
syslog(LOG_ERR, "error strerror: [%s]" % (str(e.strerror)))
except Exception as ee:
syslog(LOG_ERR, "parse exception failed. %s" % str(ee))
pass
return {
'success': False,
'error_class': error_cls,
'error_message': error_msg,
'error_code': error_code
}
def _convert_time_str(val):
origin_tz = None
if os.environ.get('TZ'):
origin_tz = os.environ.get('TZ')
os.environ['TZ'] = 'GMT'
timestamp = int(mktime(val.timetuple()))
if origin_tz:
os.environ['TZ'] = origin_tz
return timestamp
def _convert_properties(properties):
out_json = {
# Fri, 25 Jul 2014 09:36:56 GMT
'LastModified': _convert_time_str(properties.last_modified),
'MD5': properties.content_settings.content_md5,
'ETag': properties.etag,
'ContentType': properties.content_settings.content_type,
'ContentLength': properties.content_length,
'BlobType': properties.blob_type
}
return out_json
def _convert_properties_by_dict(properties, metadata):
out_json = {
'LastModified': _convert_time_str(properties.last_modified),
'MD5': properties.content_settings.content_md5,
# remove etag dobule quote
'ETag': properties.etag[1:-1],
'ContentType': properties.content_settings.content_type,
'ContentLength': properties.content_length,
'BlobType': properties.blob_type
}
if 'hdi_isfolder' in metadata:
out_json['hdi_isfolder'] = 'true' == metadata['hdi_isfolder']
return out_json
def _convert_properties_by_put(properties):
out_json = {
'LastModified': _convert_time_str(properties.last_modified),
'MD5': '',
#'MD5': properties['content-md5'],
# remove etag dobule quote
'ETag': properties.etag[1:-1],
}
return out_json
def _convert_properties_by_commit(properties):
out_json = {
'LastModified': _convert_time_str(properties['last-modified']),
'MD5': '',
# remove etag dobule quote
'ETag': properties['etag'][1:-1],
}
return out_json
def _convert_block_id(id_prefix, counter):
return id_prefix + '_{0:08d}'.format(counter)
class SimpleIO(object):
def read_int(self):
data = bytes()
n = 4
while n > 0:
read_data = sys.stdin.buffer.read(n)
if 0 == len(read_data):
raise StopIteration
data += read_data
n -= len(read_data)
if data:
return unpack('i', data)[0]
else:
# check eof?
raise StopIteration
def read_string(self):
n = self.read_int()
if 0 == n:
return ''
data = bytes()
while n > 0:
read_data = sys.stdin.buffer.read(n)
if 0 == len(read_data):
raise StopIteration
data += read_data
n -= len(read_data)
if data:
return data.decode('utf-8')
else:
raise SystemError
def read_json(self):
json_str = self.read_string()
if json_str:
return json.loads(json_str)
else:
return None
def write_int(self, val):
data = pack('i', val)
sys.stdout.buffer.write(data)
sys.stdout.flush()
def write_string(self, val):
self.write_int(len(val))
sys.stdout.write(val)
sys.stdout.flush()
# log
def write_json(self, val):
s = json.dumps(val)
self.write_string(s)
def write_exception(self, e, auth_done = False):
self.write_json(_convert_exception(e, auth_done))
class AzureBlobStoreageApi(object):
def __init__(self):
from azure.storage.blob.baseblobservice import BaseBlobService
from azure.storage.blob.blockblobservice import BlockBlobService
blob_service_opt = {
"account_name": os.environ.get('AZURE_ACCESS_KEY'),
"account_key": os.environ.get('AZURE_SECRET_KEY'),
"protocol": os.environ.get('AZURE_SCHEME', 'https')
}
if os.environ.get('AZURE_HOST_BASE') == 'china':
blob_service_opt["endpoint_suffix"] = 'core.chinacloudapi.cn'
elif os.environ.get('AZURE_HOST_BASE'):
blob_service_opt["endpoint_suffix"] = os.environ.get('AZURE_HOST_BASE')
# Export synoproxy settings to environment if needed
if "endpoint_suffix" in blob_service_opt:
azure_blob_url = '%s.blob.%s' % (blob_service_opt["account_name"], blob_service_opt["endpoint_suffix"])
else:
azure_blob_url = '%s.blob.core.windows.net' % (blob_service_opt['account_name'])
synoproxy_exporter = SynoProxyExporter(azure_blob_url)
synoproxy_exporter.export_proxy_settings()
# os.environ['AZURE_DEBUG'] = 'yes'
self._azure = BaseBlobService(**blob_service_opt)
self._azure_blob = BlockBlobService(**blob_service_opt)
del os.environ['AZURE_ACCESS_KEY']
del os.environ['AZURE_SECRET_KEY']
log_debug('agent created')
def createContainer(self, in_json):
kwargs = {
'fail_on_exist': True
}
if in_json.get('PublicAccess'):
kwargs['x_ms_blob_public_access'] = in_json['PublicAccess']
res = self._azure.create_container(in_json['container'], **kwargs)
return {'success': res}
def getContainerProperties(self, in_json):
res = self._azure.get_container_properties(in_json['container'])
return {
'success': True,
'LastModified': _convert_time_str(res.properties.last_modified),
'ETag': res.properties.etag
}
def listContainers(self, in_json):
kwargs = {}
# FIXME add prefix and marker to ta
for key in ('prefix', 'marker'):
if in_json.get(key):
kwargs[key.lower()] = in_json[key]
res = self._azure.list_containers(**kwargs)
out_json = {
'success': True,
'container': []
}
for rec in res:
out_json['container'].append({
'Name': rec.name
})
return out_json
def deleteContainer(self, in_json):
kwargs = {
'fail_not_exist': True
}
res = self._azure.delete_container(in_json['container'], **kwargs)
return {'success': res}
def createBlockBlob(self, in_json):
res = self._azure_blob.create_blob_from_path(
in_json['container'],
in_json['blob'],
in_json['fileInput'])
return {'success': True,
'Properties': _convert_properties_by_put(res)
}
def listBlobs(self, in_json):
from azure.storage.blob.models import Blob, BlobPrefix
kwargs = {}
for key in ('delimiter', 'prefix', 'marker'):
if in_json.get(key):
kwargs[key.lower()] = in_json[key]
res = self._azure.list_blobs(in_json['container'], **kwargs)
targetType = in_json.get('TargetType', 'all')
count = 0
out_json = {
'success': True,
'folder': [],
'file': []
}
for rec in res:
if targetType == 'folder' or targetType == 'all':
if type(rec) == BlobPrefix:
out_json['folder'].append({'Name': rec.name})
count += 1
if targetType == 'file' or targetType == 'all':
if type(rec) == Blob:
out_json['file'].append({
'Name': rec.name,
'Properties': _convert_properties(rec.properties)
})
count += 1
if res.next_marker:
out_json['NextMarker'] = res.next_marker
out_json['count'] = count
return out_json
def getBlobProperties(self, in_json):
res = self._azure.get_blob_properties(
in_json['container'],
in_json['blob'])
return {
'success': True,
'Properties': _convert_properties_by_dict(res.properties, res.metadata),
}
def deleteBlob(self, in_json):
from azure.storage.blob.models import DeleteSnapshot
res = self._azure.delete_blob(
in_json['container'],
in_json['blob'],
delete_snapshots=DeleteSnapshot.Include)
return {'success': True}
def getBlob(self, in_json):
res = self._azure.get_blob_to_path(
in_json['container'],
in_json['blob'],
in_json['fileOutput'])
return {'success': True}
def start_server():
io = SimpleIO()
try:
api = AzureBlobStoreageApi()
except Exception as e:
io.write_exception(e)
return False
io.write_string('start')
auth_done = False
while True:
try:
in_json = io.read_json()
fn_name = in_json['fn']
fn = getattr(api, fn_name)
del in_json['fn']
if fn is None:
raise SystemError('no such fn: ' + fn_name)
#log_debug('\033[34mexecute: ' + fn_name + ' ' + json.dumps(in_json) + '\033[0m');
res = fn(in_json)
if not auth_done and res['success']:
auth_done = True
io.write_json(res)
except StopIteration:
break
except Exception as e:
io.write_exception(e, auth_done)
continue
class SynoProxyExporter:
def __init__(self, host, port=None):
self.host = host
self.port = port
self.synoproxy_host = ''
self.synoproxy_port = ''
self.synoproxy_auth = ''
self.default_port = 443 # https port
def _get_hostport(self, host, port):
if port is None:
i = host.rfind(':')
j = host.rfind(']') # ipv6 addresses have [...]
if i > j:
try:
port = int(host[i + 1:])
except ValueError:
if host[i + 1:] == "": # http://foo.com:/ == http://foo.com/
port = self.default_port
else:
raise ValueError("nonnumeric port: '%s'" % host[i + 1:])
host = host[:i]
else:
port = self.default_port
if host and host[0] == '[' and host[-1] == ']':
host = host[1:-1]
return (host, port)
def _get_syno_proxy_info(self):
(synoproxy_host, synoproxy_port, synoproxy_auth) = (None, ) * 3
BUFSIZE = 4096
synoproxy = CDLL('libsynoproxy.so')
proxy_addr_buf = create_string_buffer(BUFSIZE)
proxy_auth_buf = create_string_buffer(BUFSIZE)
if 1 == synoproxy.SYNOProxyGetAddrByUrl(
c_char_p((self.host + ':' + str(self.port)).encode('utf-8')), proxy_addr_buf, c_uint(BUFSIZE)):
(synoproxy_host, synoproxy_port) = self._get_hostport(proxy_addr_buf.value.decode('utf-8'), None)
if 1 == synoproxy.SYNOProxyGetAuth(proxy_auth_buf, c_uint(BUFSIZE)):
# in format <user>:<password>
synoproxy_auth = proxy_auth_buf.value.decode('utf-8')
return (synoproxy_host, synoproxy_port, synoproxy_auth)
def export_proxy_settings(self):
(self.synoproxy_host, self.synoproxy_port, self.synoproxy_auth) = self._get_syno_proxy_info()
if self.synoproxy_host is None or self.synoproxy_port is None:
# No synoproxy setting found, do nothing
return
synoproxy_destination = "%s:%s" % (self.synoproxy_host, self.synoproxy_port)
if None is self.synoproxy_auth:
os.environ["HTTP_PROXY"] = "http://%s" % (synoproxy_destination)
os.environ["HTTPS_PROXY"] = "http://%s" % (synoproxy_destination)
return
# User and password is present in setting, but we need to handle special character
# self.synoproxy_auth is in format <user>:<password>, so we do url encoding without changing ':'
synoproxy_auth_encoded = urllib.parse.quote(self.synoproxy_auth, safe=':')
os.environ["HTTP_PROXY"] = "http://%s@%s" % (synoproxy_auth_encoded, synoproxy_destination)
os.environ["HTTPS_PROXY"] = "http://%s@%s" % (synoproxy_auth_encoded, synoproxy_destination)
if __name__ == '__main__':
try:
res = start_server()
sys.exit(0 if res else 1)
except Exception as e:
syslog(LOG_ERR, "Error: exception [%s], type [%s]" % (str(e), type(e)));