PageRenderTime 34ms CodeModel.GetById 29ms RepoModel.GetById 1ms app.codeStats 0ms

/boto/utils.py

https://github.com/d1on/boto
Python | 691 lines | 517 code | 18 blank | 156 comment | 39 complexity | 55771ee7dd9cab06c764404e6ad02300 MD5 | raw file
  1. # Copyright (c) 2006-2010 Mitch Garnaat http://garnaat.org/
  2. # Copyright (c) 2010, Eucalyptus Systems, Inc.
  3. # All rights reserved.
  4. #
  5. # Permission is hereby granted, free of charge, to any person obtaining a
  6. # copy of this software and associated documentation files (the
  7. # "Software"), to deal in the Software without restriction, including
  8. # without limitation the rights to use, copy, modify, merge, publish, dis-
  9. # tribute, sublicense, and/or sell copies of the Software, and to permit
  10. # persons to whom the Software is furnished to do so, subject to the fol-
  11. # lowing conditions:
  12. #
  13. # The above copyright notice and this permission notice shall be included
  14. # in all copies or substantial portions of the Software.
  15. #
  16. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
  17. # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
  18. # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
  19. # SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  20. # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
  22. # IN THE SOFTWARE.
  23. #
  24. # Parts of this code were copied or derived from sample code supplied by AWS.
  25. # The following notice applies to that code.
  26. #
  27. # This software code is made available "AS IS" without warranties of any
  28. # kind. You may copy, display, modify and redistribute the software
  29. # code either by itself or as incorporated into your code; provided that
  30. # you do not remove any proprietary notices. Your use of this software
  31. # code is at your own risk and you waive any claim against Amazon
  32. # Digital Services, Inc. or its affiliates with respect to your use of
  33. # this software code. (c) 2006 Amazon Digital Services, Inc. or its
  34. # affiliates.
  35. """
  36. Some handy utility functions used by several classes.
  37. """
  38. import urllib
  39. import urllib2
  40. import imp
  41. import subprocess
  42. import StringIO
  43. import time
  44. import logging.handlers
  45. import boto
  46. import tempfile
  47. import smtplib
  48. import datetime
  49. from email.MIMEMultipart import MIMEMultipart
  50. from email.MIMEBase import MIMEBase
  51. from email.MIMEText import MIMEText
  52. from email.Utils import formatdate
  53. from email import Encoders
  54. import gzip
  55. try:
  56. import hashlib
  57. _hashfn = hashlib.sha512
  58. except ImportError:
  59. import md5
  60. _hashfn = md5.md5
  61. # List of Query String Arguments of Interest
  62. qsa_of_interest = ['acl', 'location', 'logging', 'partNumber', 'policy',
  63. 'requestPayment', 'torrent', 'versioning', 'versionId',
  64. 'versions', 'website', 'uploads', 'uploadId',
  65. 'response-content-type', 'response-content-language',
  66. 'response-expires', 'reponse-cache-control',
  67. 'response-content-disposition',
  68. 'response-content-encoding']
  69. # generates the aws canonical string for the given parameters
  70. def canonical_string(method, path, headers, expires=None,
  71. provider=None):
  72. if not provider:
  73. provider = boto.provider.get_default()
  74. interesting_headers = {}
  75. for key in headers:
  76. lk = key.lower()
  77. if headers[key] != None and (lk in ['content-md5', 'content-type', 'date'] or
  78. lk.startswith(provider.header_prefix)):
  79. interesting_headers[lk] = headers[key].strip()
  80. # these keys get empty strings if they don't exist
  81. if not interesting_headers.has_key('content-type'):
  82. interesting_headers['content-type'] = ''
  83. if not interesting_headers.has_key('content-md5'):
  84. interesting_headers['content-md5'] = ''
  85. # just in case someone used this. it's not necessary in this lib.
  86. if interesting_headers.has_key(provider.date_header):
  87. interesting_headers['date'] = ''
  88. # if you're using expires for query string auth, then it trumps date
  89. # (and provider.date_header)
  90. if expires:
  91. interesting_headers['date'] = str(expires)
  92. sorted_header_keys = interesting_headers.keys()
  93. sorted_header_keys.sort()
  94. buf = "%s\n" % method
  95. for key in sorted_header_keys:
  96. val = interesting_headers[key]
  97. if key.startswith(provider.header_prefix):
  98. buf += "%s:%s\n" % (key, val)
  99. else:
  100. buf += "%s\n" % val
  101. # don't include anything after the first ? in the resource...
  102. # unless it is one of the QSA of interest, defined above
  103. t = path.split('?')
  104. buf += t[0]
  105. if len(t) > 1:
  106. qsa = t[1].split('&')
  107. qsa = [ a.split('=') for a in qsa]
  108. qsa = [ a for a in qsa if a[0] in qsa_of_interest ]
  109. if len(qsa) > 0:
  110. qsa.sort(cmp=lambda x,y:cmp(x[0], y[0]))
  111. qsa = [ '='.join(a) for a in qsa ]
  112. buf += '?'
  113. buf += '&'.join(qsa)
  114. return buf
  115. def merge_meta(headers, metadata, provider=None):
  116. if not provider:
  117. provider = boto.provider.get_default()
  118. metadata_prefix = provider.metadata_prefix
  119. final_headers = headers.copy()
  120. for k in metadata.keys():
  121. if k.lower() in ['cache-control', 'content-md5', 'content-type',
  122. 'content-encoding', 'content-disposition',
  123. 'date', 'expires']:
  124. final_headers[k] = metadata[k]
  125. else:
  126. final_headers[metadata_prefix + k] = metadata[k]
  127. return final_headers
  128. def get_aws_metadata(headers, provider=None):
  129. if not provider:
  130. provider = boto.provider.get_default()
  131. metadata_prefix = provider.metadata_prefix
  132. metadata = {}
  133. for hkey in headers.keys():
  134. if hkey.lower().startswith(metadata_prefix):
  135. val = urllib.unquote_plus(headers[hkey])
  136. try:
  137. metadata[hkey[len(metadata_prefix):]] = unicode(val, 'utf-8')
  138. except UnicodeDecodeError:
  139. metadata[hkey[len(metadata_prefix):]] = val
  140. del headers[hkey]
  141. return metadata
  142. def retry_url(url, retry_on_404=True, num_retries=10):
  143. for i in range(0, num_retries):
  144. try:
  145. req = urllib2.Request(url)
  146. resp = urllib2.urlopen(req)
  147. return resp.read()
  148. except urllib2.HTTPError, e:
  149. # in 2.6 you use getcode(), in 2.5 and earlier you use code
  150. if hasattr(e, 'getcode'):
  151. code = e.getcode()
  152. else:
  153. code = e.code
  154. if code == 404 and not retry_on_404:
  155. return ''
  156. except:
  157. pass
  158. boto.log.exception('Caught exception reading instance data')
  159. time.sleep(2**i)
  160. boto.log.error('Unable to read instance data, giving up')
  161. return ''
  162. def _get_instance_metadata(url):
  163. d = {}
  164. data = retry_url(url)
  165. if data:
  166. fields = data.split('\n')
  167. for field in fields:
  168. if field.endswith('/'):
  169. d[field[0:-1]] = _get_instance_metadata(url + field)
  170. else:
  171. p = field.find('=')
  172. if p > 0:
  173. key = field[p+1:]
  174. resource = field[0:p] + '/openssh-key'
  175. else:
  176. key = resource = field
  177. val = retry_url(url + resource)
  178. p = val.find('\n')
  179. if p > 0:
  180. val = val.split('\n')
  181. d[key] = val
  182. return d
  183. def get_instance_metadata(version='latest', url='http://169.254.169.254'):
  184. """
  185. Returns the instance metadata as a nested Python dictionary.
  186. Simple values (e.g. local_hostname, hostname, etc.) will be
  187. stored as string values. Values such as ancestor-ami-ids will
  188. be stored in the dict as a list of string values. More complex
  189. fields such as public-keys and will be stored as nested dicts.
  190. """
  191. return _get_instance_metadata('%s/%s/meta-data/' % (url, version))
  192. def get_instance_userdata(version='latest', sep=None,
  193. url='http://169.254.169.254'):
  194. ud_url = '%s/%s/user-data' % (url,version)
  195. user_data = retry_url(ud_url, retry_on_404=False)
  196. if user_data:
  197. if sep:
  198. l = user_data.split(sep)
  199. user_data = {}
  200. for nvpair in l:
  201. t = nvpair.split('=')
  202. user_data[t[0].strip()] = t[1].strip()
  203. return user_data
  204. ISO8601 = '%Y-%m-%dT%H:%M:%SZ'
  205. ISO8601_MS = '%Y-%m-%dT%H:%M:%S.%fZ'
  206. def get_ts(ts=None):
  207. if not ts:
  208. ts = time.gmtime()
  209. return time.strftime(ISO8601, ts)
  210. def parse_ts(ts):
  211. try:
  212. dt = datetime.datetime.strptime(ts, ISO8601)
  213. return dt
  214. except ValueError:
  215. dt = datetime.datetime.strptime(ts, ISO8601_MS)
  216. return dt
  217. def find_class(module_name, class_name=None):
  218. if class_name:
  219. module_name = "%s.%s" % (module_name, class_name)
  220. modules = module_name.split('.')
  221. c = None
  222. try:
  223. for m in modules[1:]:
  224. if c:
  225. c = getattr(c, m)
  226. else:
  227. c = getattr(__import__(".".join(modules[0:-1])), m)
  228. return c
  229. except:
  230. return None
  231. def update_dme(username, password, dme_id, ip_address):
  232. """
  233. Update your Dynamic DNS record with DNSMadeEasy.com
  234. """
  235. dme_url = 'https://www.dnsmadeeasy.com/servlet/updateip'
  236. dme_url += '?username=%s&password=%s&id=%s&ip=%s'
  237. s = urllib2.urlopen(dme_url % (username, password, dme_id, ip_address))
  238. return s.read()
  239. def fetch_file(uri, file=None, username=None, password=None):
  240. """
  241. Fetch a file based on the URI provided. If you do not pass in a file pointer
  242. a tempfile.NamedTemporaryFile, or None if the file could not be
  243. retrieved is returned.
  244. The URI can be either an HTTP url, or "s3://bucket_name/key_name"
  245. """
  246. boto.log.info('Fetching %s' % uri)
  247. if file == None:
  248. file = tempfile.NamedTemporaryFile()
  249. try:
  250. if uri.startswith('s3://'):
  251. bucket_name, key_name = uri[len('s3://'):].split('/', 1)
  252. c = boto.connect_s3(aws_access_key_id=username, aws_secret_access_key=password)
  253. bucket = c.get_bucket(bucket_name)
  254. key = bucket.get_key(key_name)
  255. key.get_contents_to_file(file)
  256. else:
  257. if username and password:
  258. passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
  259. passman.add_password(None, uri, username, password)
  260. authhandler = urllib2.HTTPBasicAuthHandler(passman)
  261. opener = urllib2.build_opener(authhandler)
  262. urllib2.install_opener(opener)
  263. s = urllib2.urlopen(uri)
  264. file.write(s.read())
  265. file.seek(0)
  266. except:
  267. raise
  268. boto.log.exception('Problem Retrieving file: %s' % uri)
  269. file = None
  270. return file
  271. class ShellCommand(object):
  272. def __init__(self, command, wait=True, fail_fast=False, cwd = None):
  273. self.exit_code = 0
  274. self.command = command
  275. self.log_fp = StringIO.StringIO()
  276. self.wait = wait
  277. self.fail_fast = fail_fast
  278. self.run(cwd = cwd)
  279. def run(self, cwd=None):
  280. boto.log.info('running:%s' % self.command)
  281. self.process = subprocess.Popen(self.command, shell=True, stdin=subprocess.PIPE,
  282. stdout=subprocess.PIPE, stderr=subprocess.PIPE,
  283. cwd=cwd)
  284. if(self.wait):
  285. while self.process.poll() == None:
  286. time.sleep(1)
  287. t = self.process.communicate()
  288. self.log_fp.write(t[0])
  289. self.log_fp.write(t[1])
  290. boto.log.info(self.log_fp.getvalue())
  291. self.exit_code = self.process.returncode
  292. if self.fail_fast and self.exit_code != 0:
  293. raise Exception("Command " + self.command + " failed with status " + self.exit_code)
  294. return self.exit_code
  295. def setReadOnly(self, value):
  296. raise AttributeError
  297. def getStatus(self):
  298. return self.exit_code
  299. status = property(getStatus, setReadOnly, None, 'The exit code for the command')
  300. def getOutput(self):
  301. return self.log_fp.getvalue()
  302. output = property(getOutput, setReadOnly, None, 'The STDIN and STDERR output of the command')
  303. class AuthSMTPHandler(logging.handlers.SMTPHandler):
  304. """
  305. This class extends the SMTPHandler in the standard Python logging module
  306. to accept a username and password on the constructor and to then use those
  307. credentials to authenticate with the SMTP server. To use this, you could
  308. add something like this in your boto config file:
  309. [handler_hand07]
  310. class=boto.utils.AuthSMTPHandler
  311. level=WARN
  312. formatter=form07
  313. args=('localhost', 'username', 'password', 'from@abc', ['user1@abc', 'user2@xyz'], 'Logger Subject')
  314. """
  315. def __init__(self, mailhost, username, password, fromaddr, toaddrs, subject):
  316. """
  317. Initialize the handler.
  318. We have extended the constructor to accept a username/password
  319. for SMTP authentication.
  320. """
  321. logging.handlers.SMTPHandler.__init__(self, mailhost, fromaddr, toaddrs, subject)
  322. self.username = username
  323. self.password = password
  324. def emit(self, record):
  325. """
  326. Emit a record.
  327. Format the record and send it to the specified addressees.
  328. It would be really nice if I could add authorization to this class
  329. without having to resort to cut and paste inheritance but, no.
  330. """
  331. try:
  332. port = self.mailport
  333. if not port:
  334. port = smtplib.SMTP_PORT
  335. smtp = smtplib.SMTP(self.mailhost, port)
  336. smtp.login(self.username, self.password)
  337. msg = self.format(record)
  338. msg = "From: %s\r\nTo: %s\r\nSubject: %s\r\nDate: %s\r\n\r\n%s" % (
  339. self.fromaddr,
  340. ','.join(self.toaddrs),
  341. self.getSubject(record),
  342. formatdate(), msg)
  343. smtp.sendmail(self.fromaddr, self.toaddrs, msg)
  344. smtp.quit()
  345. except (KeyboardInterrupt, SystemExit):
  346. raise
  347. except:
  348. self.handleError(record)
  349. class LRUCache(dict):
  350. """A dictionary-like object that stores only a certain number of items, and
  351. discards its least recently used item when full.
  352. >>> cache = LRUCache(3)
  353. >>> cache['A'] = 0
  354. >>> cache['B'] = 1
  355. >>> cache['C'] = 2
  356. >>> len(cache)
  357. 3
  358. >>> cache['A']
  359. 0
  360. Adding new items to the cache does not increase its size. Instead, the least
  361. recently used item is dropped:
  362. >>> cache['D'] = 3
  363. >>> len(cache)
  364. 3
  365. >>> 'B' in cache
  366. False
  367. Iterating over the cache returns the keys, starting with the most recently
  368. used:
  369. >>> for key in cache:
  370. ... print key
  371. D
  372. A
  373. C
  374. This code is based on the LRUCache class from Genshi which is based on
  375. Mighty's LRUCache from ``myghtyutils.util``, written
  376. by Mike Bayer and released under the MIT license (Genshi uses the
  377. BSD License). See:
  378. http://svn.myghty.org/myghtyutils/trunk/lib/myghtyutils/util.py
  379. """
  380. class _Item(object):
  381. def __init__(self, key, value):
  382. self.previous = self.next = None
  383. self.key = key
  384. self.value = value
  385. def __repr__(self):
  386. return repr(self.value)
  387. def __init__(self, capacity):
  388. self._dict = dict()
  389. self.capacity = capacity
  390. self.head = None
  391. self.tail = None
  392. def __contains__(self, key):
  393. return key in self._dict
  394. def __iter__(self):
  395. cur = self.head
  396. while cur:
  397. yield cur.key
  398. cur = cur.next
  399. def __len__(self):
  400. return len(self._dict)
  401. def __getitem__(self, key):
  402. item = self._dict[key]
  403. self._update_item(item)
  404. return item.value
  405. def __setitem__(self, key, value):
  406. item = self._dict.get(key)
  407. if item is None:
  408. item = self._Item(key, value)
  409. self._dict[key] = item
  410. self._insert_item(item)
  411. else:
  412. item.value = value
  413. self._update_item(item)
  414. self._manage_size()
  415. def __repr__(self):
  416. return repr(self._dict)
  417. def _insert_item(self, item):
  418. item.previous = None
  419. item.next = self.head
  420. if self.head is not None:
  421. self.head.previous = item
  422. else:
  423. self.tail = item
  424. self.head = item
  425. self._manage_size()
  426. def _manage_size(self):
  427. while len(self._dict) > self.capacity:
  428. del self._dict[self.tail.key]
  429. if self.tail != self.head:
  430. self.tail = self.tail.previous
  431. self.tail.next = None
  432. else:
  433. self.head = self.tail = None
  434. def _update_item(self, item):
  435. if self.head == item:
  436. return
  437. previous = item.previous
  438. previous.next = item.next
  439. if item.next is not None:
  440. item.next.previous = previous
  441. else:
  442. self.tail = previous
  443. item.previous = None
  444. item.next = self.head
  445. self.head.previous = self.head = item
  446. class Password(object):
  447. """
  448. Password object that stores itself as hashed.
  449. Hash defaults to SHA512 if available, MD5 otherwise.
  450. """
  451. hashfunc=_hashfn
  452. def __init__(self, str=None, hashfunc=None):
  453. """
  454. Load the string from an initial value, this should be the raw hashed password.
  455. """
  456. self.str = str
  457. if hashfunc:
  458. self.hashfunc = hashfunc
  459. def set(self, value):
  460. self.str = self.hashfunc(value).hexdigest()
  461. def __str__(self):
  462. return str(self.str)
  463. def __eq__(self, other):
  464. if other == None:
  465. return False
  466. return str(self.hashfunc(other).hexdigest()) == str(self.str)
  467. def __len__(self):
  468. if self.str:
  469. return len(self.str)
  470. else:
  471. return 0
  472. def notify(subject, body=None, html_body=None, to_string=None, attachments=None, append_instance_id=True):
  473. attachments = attachments or []
  474. if append_instance_id:
  475. subject = "[%s] %s" % (boto.config.get_value("Instance", "instance-id"), subject)
  476. if not to_string:
  477. to_string = boto.config.get_value('Notification', 'smtp_to', None)
  478. if to_string:
  479. try:
  480. from_string = boto.config.get_value('Notification', 'smtp_from', 'boto')
  481. msg = MIMEMultipart()
  482. msg['From'] = from_string
  483. msg['Reply-To'] = from_string
  484. msg['To'] = to_string
  485. msg['Date'] = formatdate(localtime=True)
  486. msg['Subject'] = subject
  487. if body:
  488. msg.attach(MIMEText(body))
  489. if html_body:
  490. part = MIMEBase('text', 'html')
  491. part.set_payload(html_body)
  492. Encoders.encode_base64(part)
  493. msg.attach(part)
  494. for part in attachments:
  495. msg.attach(part)
  496. smtp_host = boto.config.get_value('Notification', 'smtp_host', 'localhost')
  497. # Alternate port support
  498. if boto.config.get_value("Notification", "smtp_port"):
  499. server = smtplib.SMTP(smtp_host, int(boto.config.get_value("Notification", "smtp_port")))
  500. else:
  501. server = smtplib.SMTP(smtp_host)
  502. # TLS support
  503. if boto.config.getbool("Notification", "smtp_tls"):
  504. server.ehlo()
  505. server.starttls()
  506. server.ehlo()
  507. smtp_user = boto.config.get_value('Notification', 'smtp_user', '')
  508. smtp_pass = boto.config.get_value('Notification', 'smtp_pass', '')
  509. if smtp_user:
  510. server.login(smtp_user, smtp_pass)
  511. server.sendmail(from_string, to_string, msg.as_string())
  512. server.quit()
  513. except:
  514. boto.log.exception('notify failed')
  515. def get_utf8_value(value):
  516. if not isinstance(value, str) and not isinstance(value, unicode):
  517. value = str(value)
  518. if isinstance(value, unicode):
  519. return value.encode('utf-8')
  520. else:
  521. return value
  522. def mklist(value):
  523. if not isinstance(value, list):
  524. if isinstance(value, tuple):
  525. value = list(value)
  526. else:
  527. value = [value]
  528. return value
  529. def pythonize_name(name, sep='_'):
  530. s = ''
  531. if name[0].isupper:
  532. s = name[0].lower()
  533. for c in name[1:]:
  534. if c.isupper():
  535. s += sep + c.lower()
  536. else:
  537. s += c
  538. return s
  539. def write_mime_multipart(content, compress=False, deftype='text/plain', delimiter=':'):
  540. """Description:
  541. :param content: A list of tuples of name-content pairs. This is used
  542. instead of a dict to ensure that scripts run in order
  543. :type list of tuples:
  544. :param compress: Use gzip to compress the scripts, defaults to no compression
  545. :type bool:
  546. :param deftype: The type that should be assumed if nothing else can be figured out
  547. :type str:
  548. :param delimiter: mime delimiter
  549. :type str:
  550. :return: Final mime multipart
  551. :rtype: str:
  552. """
  553. wrapper = MIMEMultipart()
  554. for name,con in content:
  555. definite_type = guess_mime_type(con, deftype)
  556. maintype, subtype = definite_type.split('/', 1)
  557. if maintype == 'text':
  558. mime_con = MIMEText(con, _subtype=subtype)
  559. else:
  560. mime_con = MIMEBase(maintype, subtype)
  561. mime_con.set_payload(con)
  562. # Encode the payload using Base64
  563. Encoders.encode_base64(mime_con)
  564. mime_con.add_header('Content-Disposition', 'attachment', filename=name)
  565. wrapper.attach(mime_con)
  566. rcontent = wrapper.as_string()
  567. if compress:
  568. buf = StringIO.StringIO()
  569. gz = gzip.GzipFile(mode='wb', fileobj=buf)
  570. try:
  571. gz.write(rcontent)
  572. finally:
  573. gz.close()
  574. rcontent = buf.getvalue()
  575. return rcontent
  576. def guess_mime_type(content, deftype):
  577. """Description: Guess the mime type of a block of text
  578. :param content: content we're finding the type of
  579. :type str:
  580. :param deftype: Default mime type
  581. :type str:
  582. :rtype: <type>:
  583. :return: <description>
  584. """
  585. #Mappings recognized by cloudinit
  586. starts_with_mappings={
  587. '#include' : 'text/x-include-url',
  588. '#!' : 'text/x-shellscript',
  589. '#cloud-config' : 'text/cloud-config',
  590. '#upstart-job' : 'text/upstart-job',
  591. '#part-handler' : 'text/part-handler',
  592. '#cloud-boothook' : 'text/cloud-boothook'
  593. }
  594. rtype = deftype
  595. for possible_type,mimetype in starts_with_mappings.items():
  596. if content.startswith(possible_type):
  597. rtype = mimetype
  598. break
  599. return(rtype)