1
#-------------------------------------------------------------------------
2
# Copyright (c) Microsoft. All rights reserved.
4
# Licensed under the Apache License, Version 2.0 (the "License");
5
# you may not use this file except in compliance with the License.
6
# You may obtain a copy of the License at
7
# http://www.apache.org/licenses/LICENSE-2.0
9
# Unless required by applicable law or agreed to in writing, software
10
# distributed under the License is distributed on an "AS IS" BASIS,
11
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
# See the License for the specific language governing permissions and
13
# limitations under the License.
14
#--------------------------------------------------------------------------
21
from datetime import datetime
22
from xml.dom import minidom
23
from xml.sax.saxutils import escape as xml_escape
25
#--------------------------------------------------------------------------
28
__author__ = 'Microsoft Corp. <ptvshelp@microsoft.com>'
31
#Live ServiceClient URLs
32
BLOB_SERVICE_HOST_BASE = '.blob.core.windows.net'
33
QUEUE_SERVICE_HOST_BASE = '.queue.core.windows.net'
34
TABLE_SERVICE_HOST_BASE = '.table.core.windows.net'
35
SERVICE_BUS_HOST_BASE = '.servicebus.windows.net'
36
MANAGEMENT_HOST = 'management.core.windows.net'
38
#Development ServiceClient URLs
39
DEV_BLOB_HOST = '127.0.0.1:10000'
40
DEV_QUEUE_HOST = '127.0.0.1:10001'
41
DEV_TABLE_HOST = '127.0.0.1:10002'
43
#Default credentials for Development Storage Service
44
DEV_ACCOUNT_NAME = 'devstoreaccount1'
45
DEV_ACCOUNT_KEY = 'Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=='
47
# All of our error messages
48
_ERROR_CANNOT_FIND_PARTITION_KEY = 'Cannot find partition key in request.'
49
_ERROR_CANNOT_FIND_ROW_KEY = 'Cannot find row key in request.'
50
_ERROR_INCORRECT_TABLE_IN_BATCH = 'Table should be the same in a batch operations'
51
_ERROR_INCORRECT_PARTITION_KEY_IN_BATCH = 'Partition Key should be the same in a batch operations'
52
_ERROR_DUPLICATE_ROW_KEY_IN_BATCH = 'Row Keys should not be the same in a batch operations'
53
_ERROR_BATCH_COMMIT_FAIL = 'Batch Commit Fail'
54
_ERROR_MESSAGE_NOT_PEEK_LOCKED_ON_DELETE = 'Message is not peek locked and cannot be deleted.'
55
_ERROR_MESSAGE_NOT_PEEK_LOCKED_ON_UNLOCK = 'Message is not peek locked and cannot be unlocked.'
56
_ERROR_QUEUE_NOT_FOUND = 'Queue was not found'
57
_ERROR_TOPIC_NOT_FOUND = 'Topic was not found'
58
_ERROR_CONFLICT = 'Conflict'
59
_ERROR_NOT_FOUND = 'Not found'
60
_ERROR_UNKNOWN = 'Unknown error (%s)'
61
_ERROR_SERVICEBUS_MISSING_INFO = 'You need to provide servicebus namespace, access key and Issuer'
62
_ERROR_STORAGE_MISSING_INFO = 'You need to provide both account name and access key'
63
_ERROR_ACCESS_POLICY = 'share_access_policy must be either SignedIdentifier or AccessPolicy instance'
64
_ERROR_VALUE_SHOULD_NOT_BE_NULL = '%s should not be None.'
65
_ERROR_CANNOT_SERIALIZE_VALUE_TO_ENTITY = 'Cannot serialize the specified value (%s) to an entity. Please use an EntityProperty (which can specify custom types), int, str, bool, or datetime'
67
_USER_AGENT_STRING = 'pyazure/' + __version__
69
METADATA_NS = 'http://schemas.microsoft.com/ado/2007/08/dataservices/metadata'
71
class WindowsAzureData(object):
72
''' This is the base of data class. It is only used to check whether it is instance or not. '''
75
class WindowsAzureError(Exception):
76
''' WindowsAzure Excpetion base class. '''
77
def __init__(self, message):
78
Exception.__init__(self, message)
80
class WindowsAzureConflictError(WindowsAzureError):
81
'''Indicates that the resource could not be created because it already
83
def __init__(self, message):
84
self.message = message
86
class WindowsAzureMissingResourceError(WindowsAzureError):
87
'''Indicates that a request for a request for a resource (queue, table,
88
container, etc...) failed because the specified resource does not exist'''
89
def __init__(self, message):
90
self.message = message
95
class _Base64String(str):
98
class HeaderDict(dict):
99
def __getitem__(self, index):
100
return super(HeaderDict, self).__getitem__(index.lower())
102
def _get_readable_id(id_name, id_prefix_to_skip):
103
"""simplified an id to be more friendly for us people"""
104
# id_name is in the form 'https://namespace.host.suffix/name'
105
# where name may contain a forward slash!
106
pos = id_name.find('//')
109
if id_prefix_to_skip:
110
pos = id_name.find(id_prefix_to_skip, pos)
112
pos += len(id_prefix_to_skip)
113
pos = id_name.find('/', pos)
115
return id_name[pos+1:]
118
def _get_entry_properties(xmlstr, include_id, id_prefix_to_skip=None):
119
''' get properties from entry xml '''
120
xmldoc = minidom.parseString(xmlstr)
123
for entry in _get_child_nodes(xmldoc, 'entry'):
124
etag = entry.getAttributeNS(METADATA_NS, 'etag')
126
properties['etag'] = etag
127
for updated in _get_child_nodes(entry, 'updated'):
128
properties['updated'] = updated.firstChild.nodeValue
129
for name in _get_children_from_path(entry, 'author', 'name'):
130
if name.firstChild is not None:
131
properties['author'] = name.firstChild.nodeValue
134
for id in _get_child_nodes(entry, 'id'):
135
properties['name'] = _get_readable_id(id.firstChild.nodeValue, id_prefix_to_skip)
139
def _get_first_child_node_value(parent_node, node_name):
140
xml_attrs = _get_child_nodes(parent_node, node_name)
142
xml_attr = xml_attrs[0]
143
if xml_attr.firstChild:
144
value = xml_attr.firstChild.nodeValue
147
def _get_child_nodes(node, tagName):
148
return [childNode for childNode in node.getElementsByTagName(tagName)
149
if childNode.parentNode == node]
151
def _get_children_from_path(node, *path):
152
'''descends through a hierarchy of nodes returning the list of children
153
at the inner most level. Only returns children who share a common parent,
156
for index, child in enumerate(path):
157
if isinstance(child, basestring):
158
next = _get_child_nodes(cur, child)
160
next = _get_child_nodesNS(cur, *child)
161
if index == len(path) - 1:
169
def _get_child_nodesNS(node, ns, tagName):
170
return [childNode for childNode in node.getElementsByTagNameNS(ns, tagName)
171
if childNode.parentNode == node]
173
def _create_entry(entry_body):
174
''' Adds common part of entry to a given entry body and return the whole xml. '''
175
updated_str = datetime.utcnow().isoformat()
176
if datetime.utcnow().utcoffset() is None:
177
updated_str += '+00:00'
179
entry_start = '''<?xml version="1.0" encoding="utf-8" standalone="yes"?>
180
<entry xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata" xmlns="http://www.w3.org/2005/Atom" >
181
<title /><updated>{updated}</updated><author><name /></author><id />
182
<content type="application/xml">
183
{body}</content></entry>'''
184
return entry_start.format(updated=updated_str, body=entry_body)
186
def _to_datetime(strtime):
187
return datetime.strptime(strtime, "%Y-%m-%dT%H:%M:%S.%f")
189
_KNOWN_SERIALIZATION_XFORMS = {'include_apis':'IncludeAPIs',
190
'message_id': 'MessageId',
191
'content_md5':'Content-MD5',
192
'last_modified': 'Last-Modified',
193
'cache_control': 'Cache-Control',
194
'account_admin_live_email_id': 'AccountAdminLiveEmailId',
195
'service_admin_live_email_id': 'ServiceAdminLiveEmailId',
196
'subscription_id': 'SubscriptionID',
198
'private_id': 'PrivateID',
199
'os_virtual_hard_disk': 'OSVirtualHardDisk',
200
'logical_disk_size_in_gb':'LogicalDiskSizeInGB',
201
'logical_size_in_gb':'LogicalSizeInGB',
203
'persistent_vm_downtime_info':'PersistentVMDowntimeInfo',
206
def _get_serialization_name(element_name):
207
"""converts a Python name into a serializable name"""
208
known = _KNOWN_SERIALIZATION_XFORMS.get(element_name)
209
if known is not None:
212
if element_name.startswith('x_ms_'):
213
return element_name.replace('_', '-')
214
if element_name.endswith('_id'):
215
element_name = element_name.replace('_id', 'ID')
216
for name in ['content_', 'last_modified', 'if_', 'cache_control']:
217
if element_name.startswith(name):
218
element_name = element_name.replace('_', '-_')
220
return ''.join(name.capitalize() for name in element_name.split('_'))
223
if isinstance(value, unicode):
224
return value.encode('utf-8')
228
def _str_or_none(value):
229
if isinstance(value, unicode):
230
return value.encode('utf-8')
237
def _int_or_none(value):
241
return str(int(value))
243
def _bool_or_none(value):
247
if isinstance(value, bool):
255
def _convert_class_to_xml(source, xml_prefix = True):
261
xmlstr = '<?xml version="1.0" encoding="utf-8"?>'
263
if isinstance(source, list):
265
xmlstr += _convert_class_to_xml(value, False)
266
elif isinstance(source, WindowsAzureData):
267
class_name = source.__class__.__name__
268
xmlstr += '<' + class_name + '>'
269
for name, value in vars(source).iteritems():
270
if value is not None:
271
if isinstance(value, list) or isinstance(value, WindowsAzureData):
272
xmlstr += _convert_class_to_xml(value, False)
274
xmlstr += ('<' + _get_serialization_name(name) + '>' +
275
xml_escape(str(value)) + '</' +
276
_get_serialization_name(name) + '>')
277
xmlstr += '</' + class_name + '>'
280
def _find_namespaces_from_child(parent, child, namespaces):
281
"""Recursively searches from the parent to the child,
282
gathering all the applicable namespaces along the way"""
283
for cur_child in parent.childNodes:
284
if cur_child is child:
286
if _find_namespaces_from_child(cur_child, child, namespaces):
287
# we are the parent node
288
for key in cur_child.attributes.keys():
289
if key.startswith('xmlns:') or key == 'xmlns':
290
namespaces[key] = cur_child.attributes[key]
294
def _find_namespaces(parent, child):
296
for key in parent.documentElement.attributes.keys():
297
if key.startswith('xmlns:') or key == 'xmlns':
298
res[key] = parent.documentElement.attributes[key]
299
_find_namespaces_from_child(parent, child, res)
302
def _clone_node_with_namespaces(node_to_clone, original_doc):
303
clone = node_to_clone.cloneNode(True)
305
for key, value in _find_namespaces(original_doc, node_to_clone).iteritems():
306
clone.attributes[key] = value
310
def _convert_response_to_feeds(response, convert_func):
314
feeds = _list_of(Feed)
316
x_ms_continuation = HeaderDict()
317
for name, value in response.headers:
318
if 'x-ms-continuation' in name:
319
x_ms_continuation[name[len('x-ms-continuation')+1:]] = value
320
if x_ms_continuation:
321
setattr(feeds, 'x_ms_continuation', x_ms_continuation)
323
xmldoc = minidom.parseString(response.body)
324
xml_entries = _get_children_from_path(xmldoc, 'feed', 'entry')
326
xml_entries = _get_children_from_path(xmldoc, 'entry') #in some cases, response contains only entry but no feed
327
for xml_entry in xml_entries:
328
new_node = _clone_node_with_namespaces(xml_entry, xmldoc)
329
feeds.append(convert_func(new_node.toxml('utf-8')))
333
def _validate_not_none(param_name, param):
335
raise TypeError(_ERROR_VALUE_SHOULD_NOT_BE_NULL % (param_name))
337
def _fill_list_of(xmldoc, element_type, xml_element_name):
338
xmlelements = _get_child_nodes(xmldoc, xml_element_name)
339
return [_parse_response_body_from_xml_node(xmlelement, element_type) for xmlelement in xmlelements]
341
def _fill_scalar_list_of(xmldoc, element_type, parent_xml_element_name, xml_element_name):
342
'''Converts an xml fragment into a list of scalar types. The parent xml element contains a
343
flat list of xml elements which are converted into the specified scalar type and added to the list.
347
<Endpoint>http://{storage-service-name}.blob.core.windows.net/</Endpoint>
348
<Endpoint>http://{storage-service-name}.queue.core.windows.net/</Endpoint>
349
<Endpoint>http://{storage-service-name}.table.core.windows.net/</Endpoint>
352
parent_xml_element_name='Endpoints'
353
xml_element_name='Endpoint'
355
xmlelements = _get_child_nodes(xmldoc, parent_xml_element_name)
357
xmlelements = _get_child_nodes(xmlelements[0], xml_element_name)
358
return [_get_node_value(xmlelement, element_type) for xmlelement in xmlelements]
360
def _fill_dict(xmldoc, element_name):
361
xmlelements = _get_child_nodes(xmldoc, element_name)
364
for child in xmlelements[0].childNodes:
366
return_obj[child.nodeName] = child.firstChild.nodeValue
369
def _fill_dict_of(xmldoc, parent_xml_element_name, pair_xml_element_name, key_xml_element_name, value_xml_element_name):
370
'''Converts an xml fragment into a dictionary. The parent xml element contains a
371
list of xml elements where each element has a child element for the key, and another for the value.
383
</ExtendedProperties>
385
parent_xml_element_name='ExtendedProperties'
386
pair_xml_element_name='ExtendedProperty'
387
key_xml_element_name='Name'
388
value_xml_element_name='Value'
392
xmlelements = _get_child_nodes(xmldoc, parent_xml_element_name)
394
xmlelements = _get_child_nodes(xmlelements[0], pair_xml_element_name)
395
for pair in xmlelements:
396
keys = _get_child_nodes(pair, key_xml_element_name)
397
values = _get_child_nodes(pair, value_xml_element_name)
399
key = keys[0].firstChild.nodeValue
400
value = values[0].firstChild.nodeValue
401
return_obj[key] = value
405
def _fill_instance_child(xmldoc, element_name, return_type):
406
'''Converts a child of the current dom element to the specified type. The child name
408
xmlelements = _get_child_nodes(xmldoc, _get_serialization_name(element_name))
413
return_obj = return_type()
414
_fill_data_to_return_object(xmlelements[0], return_obj)
418
def _fill_instance_element(element, return_type):
419
"""Converts a DOM element into the specified object"""
420
return _parse_response_body_from_xml_node(element, return_type)
423
def _fill_data_minidom(xmldoc, element_name, data_member):
424
xmlelements = _get_child_nodes(xmldoc, _get_serialization_name(element_name))
426
if not xmlelements or not xmlelements[0].childNodes:
429
value = xmlelements[0].firstChild.nodeValue
431
if data_member is None:
433
elif isinstance(data_member, datetime):
434
return _to_datetime(value)
435
elif type(data_member) is types.BooleanType:
436
return value.lower() != 'false'
438
return type(data_member)(value)
440
def _get_node_value(xmlelement, data_type):
441
value = xmlelement.firstChild.nodeValue
442
if data_type is datetime:
443
return _to_datetime(value)
444
elif data_type is types.BooleanType:
445
return value.lower() != 'false'
447
return data_type(value)
449
def _get_request_body(request_body):
450
'''Converts an object into a request body. If it's None
451
we'll return an empty string, if it's one of our objects it'll
452
convert it to XML and return it. Otherwise we just use the object
454
if request_body is None:
456
elif isinstance(request_body, WindowsAzureData):
457
return _convert_class_to_xml(request_body)
459
return _str(request_body)
461
def _parse_enum_results_list(response, return_type, resp_type, item_type):
462
"""resp_body is the XML we received
463
resp_type is a string, such as Containers,
464
return_type is the type we're constructing, such as ContainerEnumResults
465
item_type is the type object of the item to be created, such as Container
467
This function then returns a ContainerEnumResults object with the
468
containers member populated with the results.
471
# parsing something like:
472
# <EnumerationResults ... >
479
# </EnumerationResults>
480
respbody = response.body
481
return_obj = return_type()
482
doc = minidom.parseString(respbody)
485
for enum_results in _get_child_nodes(doc, 'EnumerationResults'):
486
# path is something like Queues, Queue
487
for child in _get_children_from_path(enum_results, resp_type, resp_type[:-1]):
488
items.append(_fill_instance_element(child, item_type))
490
for name, value in vars(return_obj).iteritems():
491
if name == resp_type.lower(): # queues, Queues, this is the list its self which we populated above
494
value = _fill_data_minidom(enum_results, name, value)
495
if value is not None:
496
setattr(return_obj, name, value)
498
setattr(return_obj, resp_type.lower(), items)
501
def _parse_simple_list(response, type, item_type, list_name):
502
respbody = response.body
505
doc = minidom.parseString(respbody)
506
type_name = type.__name__
507
item_name = item_type.__name__
508
for item in _get_children_from_path(doc, type_name, item_name):
509
res_items.append(_fill_instance_element(item, item_type))
511
setattr(res, list_name, res_items)
514
def _parse_response(response, return_type):
516
parse the HTTPResponse's body and fill all the data into a class of return_type
518
return _parse_response_body_from_xml_text(response.body, return_type)
520
def _fill_data_to_return_object(node, return_obj):
521
members = dict(vars(return_obj))
522
for name, value in members.iteritems():
523
if isinstance(value, _list_of):
524
setattr(return_obj, name, _fill_list_of(node, value.list_type, value.xml_element_name))
525
elif isinstance(value, _scalar_list_of):
526
setattr(return_obj, name, _fill_scalar_list_of(node, value.list_type, _get_serialization_name(name), value.xml_element_name))
527
elif isinstance(value, _dict_of):
528
setattr(return_obj, name, _fill_dict_of(node, _get_serialization_name(name), value.pair_xml_element_name, value.key_xml_element_name, value.value_xml_element_name))
529
elif isinstance(value, WindowsAzureData):
530
setattr(return_obj, name, _fill_instance_child(node, name, value.__class__))
531
elif isinstance(value, dict):
532
setattr(return_obj, name, _fill_dict(node, _get_serialization_name(name)))
533
elif isinstance(value, _Base64String):
534
value = _fill_data_minidom(node, name, '')
535
if value is not None:
536
value = base64.b64decode(value)
538
value = value.decode('utf-8')
540
#always set the attribute, so we don't end up returning an object with type _Base64String
541
setattr(return_obj, name, value)
543
value = _fill_data_minidom(node, name, value)
544
if value is not None:
545
setattr(return_obj, name, value)
547
def _parse_response_body_from_xml_node(node, return_type):
549
parse the xml and fill all the data into a class of return_type
551
return_obj = return_type()
552
_fill_data_to_return_object(node, return_obj)
556
def _parse_response_body_from_xml_text(respbody, return_type):
558
parse the xml and fill all the data into a class of return_type
560
doc = minidom.parseString(respbody)
561
return_obj = return_type()
562
for node in _get_child_nodes(doc, return_type.__name__):
563
_fill_data_to_return_object(node, return_obj)
567
class _dict_of(dict):
568
"""a dict which carries with it the xml element names for key,val.
569
Used for deserializaion and construction of the lists"""
570
def __init__(self, pair_xml_element_name, key_xml_element_name, value_xml_element_name):
571
self.pair_xml_element_name = pair_xml_element_name
572
self.key_xml_element_name = key_xml_element_name
573
self.value_xml_element_name = value_xml_element_name
575
class _list_of(list):
576
"""a list which carries with it the type that's expected to go in it.
577
Used for deserializaion and construction of the lists"""
578
def __init__(self, list_type, xml_element_name=None):
579
self.list_type = list_type
580
if xml_element_name is None:
581
self.xml_element_name = list_type.__name__
583
self.xml_element_name = xml_element_name
585
class _scalar_list_of(list):
586
"""a list of scalar types which carries with it the type that's
587
expected to go in it along with its xml element name.
588
Used for deserializaion and construction of the lists"""
589
def __init__(self, list_type, xml_element_name):
590
self.list_type = list_type
591
self.xml_element_name = xml_element_name
593
def _update_request_uri_query_local_storage(request, use_local_storage):
594
''' create correct uri and query for the request '''
595
uri, query = _update_request_uri_query(request)
596
if use_local_storage:
597
return '/' + DEV_ACCOUNT_NAME + uri, query
600
def _update_request_uri_query(request):
601
'''pulls the query string out of the URI and moves it into
602
the query portion of the request object. If there are already
603
query parameters on the request the parameters in the URI will
604
appear after the existing parameters'''
606
if '?' in request.path:
607
request.path, _, query_string = request.path.partition('?')
609
query_params = query_string.split('&')
610
for query in query_params:
612
name, _, value = query.partition('=')
613
request.query.append((name, value))
615
request.path = urllib2.quote(request.path, '/()$=\',')
617
#add encoded queries to request.path.
620
for name, value in request.query:
621
if value is not None:
622
request.path += name + '=' + urllib2.quote(value, '/()$=\',') + '&'
623
request.path = request.path[:-1]
625
return request.path, request.query
627
def _dont_fail_on_exist(error):
628
''' don't throw exception if the resource exists. This is called by create_* APIs with fail_on_exist=False'''
629
if isinstance(error, WindowsAzureConflictError):
634
def _dont_fail_not_exist(error):
635
''' don't throw exception if the resource doesn't exist. This is called by create_* APIs with fail_on_exist=False'''
636
if isinstance(error, WindowsAzureMissingResourceError):
641
def _general_error_handler(http_error):
642
''' Simple error handler for azure.'''
643
if http_error.status == 409:
644
raise WindowsAzureConflictError(_ERROR_CONFLICT)
645
elif http_error.status == 404:
646
raise WindowsAzureMissingResourceError(_ERROR_NOT_FOUND)
648
if http_error.respbody is not None:
649
raise WindowsAzureError(_ERROR_UNKNOWN % http_error.message + '\n' + http_error.respbody)
651
raise WindowsAzureError(_ERROR_UNKNOWN % http_error.message)
653
def _parse_response_for_dict(response):
654
''' Extracts name-values from response header. Filter out the standard http headers.'''
658
http_headers = ['server', 'date', 'location', 'host',
659
'via', 'proxy-connection', 'connection']
660
return_dict = HeaderDict()
662
for name, value in response.headers:
663
if not name.lower() in http_headers:
664
return_dict[name] = value
668
def _parse_response_for_dict_prefix(response, prefixes):
669
''' Extracts name-values for names starting with prefix from response header. Filter out the standard http headers.'''
674
orig_dict = _parse_response_for_dict(response)
676
for name, value in orig_dict.iteritems():
677
for prefix_value in prefixes:
678
if name.lower().startswith(prefix_value.lower()):
679
return_dict[name] = value
685
def _parse_response_for_dict_filter(response, filter):
686
''' Extracts name-values for names in filter from response header. Filter out the standard http headers.'''
690
orig_dict = _parse_response_for_dict(response)
692
for name, value in orig_dict.iteritems():
693
if name.lower() in filter:
694
return_dict[name] = value