diff --git a/pyazure/hostedservices.py b/pyazure/hostedservices.py index ac0c270..b657e91 100644 --- a/pyazure/hostedservices.py +++ b/pyazure/hostedservices.py @@ -46,13 +46,12 @@ class HostedServices(ServiceManagementEndpoint): - def __init__(self, management_cert_path, subscription_id): + def __init__(self, *args, **kwargs): log.debug("init hosted service") self.wasm_ops = self.get_wasm_ops() self._locations = None self.last_response_data = None - super(HostedServices, self).__init__(management_cert_path, - subscription_id) + super(HostedServices, self).__init__(*args, **kwargs) @property def base_url(self): @@ -64,7 +63,7 @@ def locations(self): # cached list of data center locations for deployments if not self._locations: self._locations = list(Locations(self.cert, - self.sub_id).list_locations()) + self.sub_id, self.key).list_locations()) return self._locations def get_wasm_ops(self): diff --git a/pyazure/locations.py b/pyazure/locations.py index bd0d464..a0135de 100644 --- a/pyazure/locations.py +++ b/pyazure/locations.py @@ -37,11 +37,10 @@ class Locations(ServiceManagementEndpoint): - def __init__(self, management_cert_path, subscription_id): + def __init__(self, *args, **kwargs): log.debug('init locations') self.wasm_ops = [] - super(Locations, self).__init__(management_cert_path, - subscription_id) + super(Locations, self).__init__(*args, **kwargs) @property def base_url(self): diff --git a/pyazure/pyazure.py b/pyazure/pyazure.py index 5d331cf..955c3e9 100644 --- a/pyazure/pyazure.py +++ b/pyazure/pyazure.py @@ -42,7 +42,8 @@ class PyAzure(object): def __init__(self, storage_account_name=DEVSTORE_ACCOUNT, storage_account_key=None, use_path_style_uris=False, - management_cert_path=None, subscription_id=None): + management_cert_path=None, management_key_path=None, + subscription_id=None): self.storage_account = storage_account_name if storage_account_name == DEVSTORE_ACCOUNT: blob_host = DEVSTORE_BLOB_HOST @@ -56,7 +57,8 @@ def __init__(self, storage_account_name=DEVSTORE_ACCOUNT, table_host = CLOUD_TABLE_HOST if management_cert_path and subscription_id: - self.wasm = WASM(management_cert_path, subscription_id) + self.wasm = WASM(management_cert_path, subscription_id, + management_key_path) else: self.wasm = None @@ -132,13 +134,15 @@ def set_storage_account(self, storage_account_name, create=False, class WASM(object): - """Class exposing Windows Azure Service Management operations. + """Single class that conveniently exposes Windows Azure Service Management + operations from those implemented and explicitly exposed by the individual + service wrappers. Using WASM ---------- >>> import pyazure >>> pa = pyazure.PyAzure(management_cert_path=MANAGEMENT_CERT, - ... subscription_id=SUBSCRIPTION_ID) + ... subscription_id=SUBSCRIPTION_ID, management_key_path=MANAGEMENT_KEY) >>> 'Anywhere Asia' in pa.wasm.list_locations() True >>> request_id = pa.wasm.create_storage_account('pyazuretest','doctest', @@ -146,7 +150,7 @@ class WASM(object): >>> pa.wasm.wait_for_request(request_id) True >>> (pa.wasm.get_operation_status(request_id) == - ... {'HttpStatusCode': '200', 'Status': 'Succeeded'}) + ... {'HttpStatusCode': 200, 'Status': 'Succeeded'}) True >>> request_id = pa.wasm.create_storage_account( ... 'pyazuretestwithaverylongname','doctest','anywhere us') @@ -188,19 +192,27 @@ class WASM(object): WASMError: (404, 'ResourceNotFound', 'The requested storage account was not found.') """ - def __init__(self, management_cert_path, subscription_id): + def __init__(self, management_cert_path, subscription_id, + management_key_path=None): + """Initialise the various API interfaces. + + Note that management_key_path is not required (except for Python 2.5), + as the key can be included in the certificate chain file. The OpenSSL + command line can create an appropriate PEM file like so: + openssl pkcs12 -in azure.pfx -out azure.pem -nodes + """ from hostedservices import HostedServices, ServiceConfiguration from storageaccounts import StorageAccounts from locations import Locations self.service_api = HostedServices(management_cert_path, - subscription_id) + subscription_id, management_key_path) self.ServiceConfiguration = ServiceConfiguration self.storage_api = StorageAccounts(management_cert_path, - subscription_id) + subscription_id, management_key_path) self.location_api = Locations(management_cert_path, - subscription_id) + subscription_id, management_key_path) self._sme = ServiceManagementEndpoint(management_cert_path, - subscription_id) + subscription_id, management_key_path) self.WASMError = WASMError self.get_operation_status = self._sme.get_operation_status self.request_done = self._sme.request_done @@ -234,13 +246,13 @@ def usage(): import getopt try: opts, _ = getopt.getopt(sys.argv[1:], - 'hs:c:v', - ['help','subscription_id','management_cert','verbose']) + 'hvs:c:k:', + ['help','verbose','subscription_id','management_cert', + 'management_key']) except getopt.GetoptError, e: print str(e) sys.exit(2) - sub_id = None - cert = None + sub_id = cert = key = None loud = False for opt, arg in opts: if opt in ('-h','--help'): @@ -250,12 +262,15 @@ def usage(): sub_id = arg elif opt in ('-c','--management_cert'): cert = arg + elif opt in ('-k','--management_key'): + key = arg elif opt in ('-v','--verbose'): loud = True if sub_id is None or cert is None: usage() sys.exit(2) doctest.testmod( - extraglobs={'SUBSCRIPTION_ID':sub_id, 'MANAGEMENT_CERT':cert}, + extraglobs={'SUBSCRIPTION_ID':sub_id, 'MANAGEMENT_CERT':cert, + 'MANAGEMENT_KEY':key}, verbose = loud) sys.exit() diff --git a/pyazure/storageaccounts.py b/pyazure/storageaccounts.py index 1aba0c0..8db2fa0 100644 --- a/pyazure/storageaccounts.py +++ b/pyazure/storageaccounts.py @@ -40,12 +40,11 @@ class StorageAccounts(ServiceManagementEndpoint): wasm_ops = [] - def __init__(self, management_cert_path, subscription_id): + def __init__(self, *args, **kwargs): log.debug('init storage accounts') self.wasm_ops = self.get_wasm_ops() self._locations = None - super(StorageAccounts, self).__init__(management_cert_path, - subscription_id) + super(StorageAccounts, self).__init__(*args, **kwargs) @property def base_url(self): @@ -57,7 +56,7 @@ def locations(self): # cached list of data center locations for deployments if not self._locations: self._locations = list(Locations(self.cert, - self.sub_id).list_locations()) + self.sub_id, self.key).list_locations()) return self._locations def get_wasm_ops(self): diff --git a/pyazure/util.py b/pyazure/util.py index cc6b5cf..803e5b8 100644 --- a/pyazure/util.py +++ b/pyazure/util.py @@ -510,14 +510,22 @@ def create_data_connection_string(storage_account_name, storage_account_key): class ServiceManagementEndpoint(object): """Base class for the various service management API operation groups.""" - def __init__(self, management_cert_path, subscription_id): + def __init__(self, management_cert_path, subscription_id, + management_key_path=None): if not os.path.isfile(management_cert_path): raise ValueError('Management certificate not readable or not ' - + 'a real file') + + 'a real file', management_cert_path) + if (management_key_path is not None) and \ + not os.path.isfile(management_key_path): + raise ValueError('Management key not readable or not ' + + 'a real file', management_key_path) self.cert = management_cert_path + self.key = management_key_path self.sub_id = subscription_id + log.debug('init ServiceManagementEndpoint; cert:%s, key:%s, sub_id:%s', + self.cert, self.key, self.sub_id) self._cert_handler = \ - HTTPSClientAuthHandler(self.cert) + HTTPSClientAuthHandler(self.cert, self.key) self._opener = \ urllib2.build_opener(self._cert_handler) @@ -542,12 +550,12 @@ def urlopen(self, request): except WASMError: # OK, WASMError exception successfully crafted raise - except Exception, e2: + except Exception: log.error("Could't create Windows Azure error exception " + "following HTTPError:%s%s%s" + "there was probably a problem querying the service.", - os.sep, str(e), os.sep) - raise (e2, e) + os.linesep, str(e), os.linesep) + raise def get_operation_status(self, request_id): """The Get Operation Status operation returns the status of the @@ -569,6 +577,11 @@ def get_operation_status(self, request_id): # Succeeded or Failed... result['HttpStatusCode'] = ET.findtext( './/{%s}HttpStatusCode' % NAMESPACE_MANAGEMENT) + try: + result['HttpStatusCode'] = int(result['HttpStatusCode']) + except ValueError: + log.error("Couldn't convert HttpStatusCode: %s, from operation " + "response", result['HttpStatusCode']) if result['Status'] == 'Succeeded': return result # Status must be 'Failed', get additional error info @@ -589,21 +602,19 @@ def request_done(self, request_id): else: raise op_status['Error'] - @retry(float('infinity'),delay_ceiling=20, percolate_excs=(WASMError)) + @retry(float('infinity'), delay_ceiling=20, percolate_excs=(WASMError)) def wait_for_request(self, request_id): """Example showing how to repeatedly poll asynchronous operation status with retry and backoff provided by retry decorator. Tries forever.""" return self.request_done(request_id) - def _get_wa_error(self, response): + def _get_wa_error(self, response, response_ET=None): """Extracts error details from a urlopen response, including extended - WA error details that might be included in the response body.""" - if not isinstance(response, etree.ElementTree): - response_data = response.read() - ET = etree.parse(StringIO(response_data)) - else: - # assume response is a preparsed response body - ET = response + WA error details that might be included in the response body. + + response_ET can be passed in if it has already been read from the + network, to save re-parsing.""" + ET = etree.parse(response) if response_ET is None else response_ET if 'Error' in ET.getroot().tag: error = ET.getroot() else: @@ -617,7 +628,7 @@ def _get_wa_error(self, response): http_status_code = response.code else: http_status_code = int(http_status_code) - # NB: careful: bool(Element instance) returns False! + # NB: careful: bool(Element_instance) returns False! if error is not None: # the service returned extended WA error info wa_code = error.findtext('{%s}Code' % NAMESPACE_MANAGEMENT) @@ -634,9 +645,12 @@ def _raise_wa_error(self, response): class HTTPSClientAuthHandler(urllib2.HTTPSHandler): # thanks to: http://stackoverflow.com/questions/5896380/https-connection- # using-pem-certificate/5899320#5899320 - def __init__(self, cert): + def __init__(self, cert, key=None): urllib2.HTTPSHandler.__init__(self, debuglevel=0) - self.key = '/pandora/messagelab/blair/azure/BlairBethwaiteAzure1.pfx.pem.key' # assume key & cert together in PEM encoded cert_file + # key & cert can be: + # 1) together in PEM encoded cert chain file (Python >= 2.6) + # 2) seperated into cert and key PEM files, required under Python 2.5 + self.key = key self.cert = cert def https_open(self, req):