Skip to content
This repository has been archived by the owner on Jan 18, 2018. It is now read-only.

Commit

Permalink
Management API is now Python 2.5 compatible.
Browse files Browse the repository at this point in the history
Cert and key for management API can now be in separate files, default
behaviour assumes they are together but if management_key_path is passed
when instantiating ServiceManagementEndpoints, then that key will be
passed through to the underlying HTTPSConnection and hence SSL socket.
This is *required* under Python 2.5 as the httplib.HTTPSConnection
therein expects both to be set.

Fixed error handling code to work with lxml. Unfortunately there doesn't
seem to be a reliable way to use isinstance against an lxml ElementTree
instance due to the ElementTree class not being part of the public API.
The _get_wa_error method now accepts an extra argument for pre-parsed
response data in order to avoid type testing.
  • Loading branch information
bmb committed Dec 20, 2011
1 parent e56240e commit 4bec8ba
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 44 deletions.
7 changes: 3 additions & 4 deletions pyazure/hostedservices.py
Expand Up @@ -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):
Expand All @@ -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):
Expand Down
5 changes: 2 additions & 3 deletions pyazure/locations.py
Expand Up @@ -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):
Expand Down
45 changes: 30 additions & 15 deletions pyazure/pyazure.py
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -132,21 +134,23 @@ 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',
... 'anywhere us', 'Here is my description, not great is it?')
>>> 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')
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'):
Expand All @@ -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()
7 changes: 3 additions & 4 deletions pyazure/storageaccounts.py
Expand Up @@ -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):
Expand All @@ -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):
Expand Down
50 changes: 32 additions & 18 deletions pyazure/util.py
Expand Up @@ -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)

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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)
Expand All @@ -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):
Expand Down

0 comments on commit 4bec8ba

Please sign in to comment.