PageRenderTime 49ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/boto-2.5.2/boto/manage/volume.py

#
Python | 420 lines | 347 code | 29 blank | 44 comment | 53 complexity | 6e119b3c70bbc49ab7ebf8b31b31a605 MD5 | raw file
  1. # Copyright (c) 2006-2009 Mitch Garnaat http://garnaat.org/
  2. #
  3. # Permission is hereby granted, free of charge, to any person obtaining a
  4. # copy of this software and associated documentation files (the
  5. # "Software"), to deal in the Software without restriction, including
  6. # without limitation the rights to use, copy, modify, merge, publish, dis-
  7. # tribute, sublicense, and/or sell copies of the Software, and to permit
  8. # persons to whom the Software is furnished to do so, subject to the fol-
  9. # lowing conditions:
  10. #
  11. # The above copyright notice and this permission notice shall be included
  12. # in all copies or substantial portions of the Software.
  13. #
  14. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
  15. # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
  16. # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
  17. # SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  18. # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
  20. # IN THE SOFTWARE.
  21. from __future__ import with_statement
  22. from boto.sdb.db.model import Model
  23. from boto.sdb.db.property import StringProperty, IntegerProperty, ListProperty, ReferenceProperty, CalculatedProperty
  24. from boto.manage.server import Server
  25. from boto.manage import propget
  26. import boto.utils
  27. import boto.ec2
  28. import time
  29. import traceback
  30. from contextlib import closing
  31. import datetime
  32. class CommandLineGetter(object):
  33. def get_region(self, params):
  34. if not params.get('region', None):
  35. prop = self.cls.find_property('region_name')
  36. params['region'] = propget.get(prop, choices=boto.ec2.regions)
  37. def get_zone(self, params):
  38. if not params.get('zone', None):
  39. prop = StringProperty(name='zone', verbose_name='EC2 Availability Zone',
  40. choices=self.ec2.get_all_zones)
  41. params['zone'] = propget.get(prop)
  42. def get_name(self, params):
  43. if not params.get('name', None):
  44. prop = self.cls.find_property('name')
  45. params['name'] = propget.get(prop)
  46. def get_size(self, params):
  47. if not params.get('size', None):
  48. prop = IntegerProperty(name='size', verbose_name='Size (GB)')
  49. params['size'] = propget.get(prop)
  50. def get_mount_point(self, params):
  51. if not params.get('mount_point', None):
  52. prop = self.cls.find_property('mount_point')
  53. params['mount_point'] = propget.get(prop)
  54. def get_device(self, params):
  55. if not params.get('device', None):
  56. prop = self.cls.find_property('device')
  57. params['device'] = propget.get(prop)
  58. def get(self, cls, params):
  59. self.cls = cls
  60. self.get_region(params)
  61. self.ec2 = params['region'].connect()
  62. self.get_zone(params)
  63. self.get_name(params)
  64. self.get_size(params)
  65. self.get_mount_point(params)
  66. self.get_device(params)
  67. class Volume(Model):
  68. name = StringProperty(required=True, unique=True, verbose_name='Name')
  69. region_name = StringProperty(required=True, verbose_name='EC2 Region')
  70. zone_name = StringProperty(required=True, verbose_name='EC2 Zone')
  71. mount_point = StringProperty(verbose_name='Mount Point')
  72. device = StringProperty(verbose_name="Device Name", default='/dev/sdp')
  73. volume_id = StringProperty(required=True)
  74. past_volume_ids = ListProperty(item_type=str)
  75. server = ReferenceProperty(Server, collection_name='volumes',
  76. verbose_name='Server Attached To')
  77. volume_state = CalculatedProperty(verbose_name="Volume State",
  78. calculated_type=str, use_method=True)
  79. attachment_state = CalculatedProperty(verbose_name="Attachment State",
  80. calculated_type=str, use_method=True)
  81. size = CalculatedProperty(verbose_name="Size (GB)",
  82. calculated_type=int, use_method=True)
  83. @classmethod
  84. def create(cls, **params):
  85. getter = CommandLineGetter()
  86. getter.get(cls, params)
  87. region = params.get('region')
  88. ec2 = region.connect()
  89. zone = params.get('zone')
  90. size = params.get('size')
  91. ebs_volume = ec2.create_volume(size, zone.name)
  92. v = cls()
  93. v.ec2 = ec2
  94. v.volume_id = ebs_volume.id
  95. v.name = params.get('name')
  96. v.mount_point = params.get('mount_point')
  97. v.device = params.get('device')
  98. v.region_name = region.name
  99. v.zone_name = zone.name
  100. v.put()
  101. return v
  102. @classmethod
  103. def create_from_volume_id(cls, region_name, volume_id, name):
  104. vol = None
  105. ec2 = boto.ec2.connect_to_region(region_name)
  106. rs = ec2.get_all_volumes([volume_id])
  107. if len(rs) == 1:
  108. v = rs[0]
  109. vol = cls()
  110. vol.volume_id = v.id
  111. vol.name = name
  112. vol.region_name = v.region.name
  113. vol.zone_name = v.zone
  114. vol.put()
  115. return vol
  116. def create_from_latest_snapshot(self, name, size=None):
  117. snapshot = self.get_snapshots()[-1]
  118. return self.create_from_snapshot(name, snapshot, size)
  119. def create_from_snapshot(self, name, snapshot, size=None):
  120. if size < self.size:
  121. size = self.size
  122. ec2 = self.get_ec2_connection()
  123. if self.zone_name == None or self.zone_name == '':
  124. # deal with the migration case where the zone is not set in the logical volume:
  125. current_volume = ec2.get_all_volumes([self.volume_id])[0]
  126. self.zone_name = current_volume.zone
  127. ebs_volume = ec2.create_volume(size, self.zone_name, snapshot)
  128. v = Volume()
  129. v.ec2 = self.ec2
  130. v.volume_id = ebs_volume.id
  131. v.name = name
  132. v.mount_point = self.mount_point
  133. v.device = self.device
  134. v.region_name = self.region_name
  135. v.zone_name = self.zone_name
  136. v.put()
  137. return v
  138. def get_ec2_connection(self):
  139. if self.server:
  140. return self.server.ec2
  141. if not hasattr(self, 'ec2') or self.ec2 == None:
  142. self.ec2 = boto.ec2.connect_to_region(self.region_name)
  143. return self.ec2
  144. def _volume_state(self):
  145. ec2 = self.get_ec2_connection()
  146. rs = ec2.get_all_volumes([self.volume_id])
  147. return rs[0].volume_state()
  148. def _attachment_state(self):
  149. ec2 = self.get_ec2_connection()
  150. rs = ec2.get_all_volumes([self.volume_id])
  151. return rs[0].attachment_state()
  152. def _size(self):
  153. if not hasattr(self, '__size'):
  154. ec2 = self.get_ec2_connection()
  155. rs = ec2.get_all_volumes([self.volume_id])
  156. self.__size = rs[0].size
  157. return self.__size
  158. def install_xfs(self):
  159. if self.server:
  160. self.server.install('xfsprogs xfsdump')
  161. def get_snapshots(self):
  162. """
  163. Returns a list of all completed snapshots for this volume ID.
  164. """
  165. ec2 = self.get_ec2_connection()
  166. rs = ec2.get_all_snapshots()
  167. all_vols = [self.volume_id] + self.past_volume_ids
  168. snaps = []
  169. for snapshot in rs:
  170. if snapshot.volume_id in all_vols:
  171. if snapshot.progress == '100%':
  172. snapshot.date = boto.utils.parse_ts(snapshot.start_time)
  173. snapshot.keep = True
  174. snaps.append(snapshot)
  175. snaps.sort(cmp=lambda x, y: cmp(x.date, y.date))
  176. return snaps
  177. def attach(self, server=None):
  178. if self.attachment_state == 'attached':
  179. print 'already attached'
  180. return None
  181. if server:
  182. self.server = server
  183. self.put()
  184. ec2 = self.get_ec2_connection()
  185. ec2.attach_volume(self.volume_id, self.server.instance_id, self.device)
  186. def detach(self, force=False):
  187. state = self.attachment_state
  188. if state == 'available' or state == None or state == 'detaching':
  189. print 'already detached'
  190. return None
  191. ec2 = self.get_ec2_connection()
  192. ec2.detach_volume(self.volume_id, self.server.instance_id, self.device, force)
  193. self.server = None
  194. self.put()
  195. def checkfs(self, use_cmd=None):
  196. if self.server == None:
  197. raise ValueError('server attribute must be set to run this command')
  198. # detemine state of file system on volume, only works if attached
  199. if use_cmd:
  200. cmd = use_cmd
  201. else:
  202. cmd = self.server.get_cmdshell()
  203. status = cmd.run('xfs_check %s' % self.device)
  204. if not use_cmd:
  205. cmd.close()
  206. if status[1].startswith('bad superblock magic number 0'):
  207. return False
  208. return True
  209. def wait(self):
  210. if self.server == None:
  211. raise ValueError('server attribute must be set to run this command')
  212. with closing(self.server.get_cmdshell()) as cmd:
  213. # wait for the volume device to appear
  214. cmd = self.server.get_cmdshell()
  215. while not cmd.exists(self.device):
  216. boto.log.info('%s still does not exist, waiting 10 seconds' % self.device)
  217. time.sleep(10)
  218. def format(self):
  219. if self.server == None:
  220. raise ValueError('server attribute must be set to run this command')
  221. status = None
  222. with closing(self.server.get_cmdshell()) as cmd:
  223. if not self.checkfs(cmd):
  224. boto.log.info('make_fs...')
  225. status = cmd.run('mkfs -t xfs %s' % self.device)
  226. return status
  227. def mount(self):
  228. if self.server == None:
  229. raise ValueError('server attribute must be set to run this command')
  230. boto.log.info('handle_mount_point')
  231. with closing(self.server.get_cmdshell()) as cmd:
  232. cmd = self.server.get_cmdshell()
  233. if not cmd.isdir(self.mount_point):
  234. boto.log.info('making directory')
  235. # mount directory doesn't exist so create it
  236. cmd.run("mkdir %s" % self.mount_point)
  237. else:
  238. boto.log.info('directory exists already')
  239. status = cmd.run('mount -l')
  240. lines = status[1].split('\n')
  241. for line in lines:
  242. t = line.split()
  243. if t and t[2] == self.mount_point:
  244. # something is already mounted at the mount point
  245. # unmount that and mount it as /tmp
  246. if t[0] != self.device:
  247. cmd.run('umount %s' % self.mount_point)
  248. cmd.run('mount %s /tmp' % t[0])
  249. cmd.run('chmod 777 /tmp')
  250. break
  251. # Mount up our new EBS volume onto mount_point
  252. cmd.run("mount %s %s" % (self.device, self.mount_point))
  253. cmd.run('xfs_growfs %s' % self.mount_point)
  254. def make_ready(self, server):
  255. self.server = server
  256. self.put()
  257. self.install_xfs()
  258. self.attach()
  259. self.wait()
  260. self.format()
  261. self.mount()
  262. def freeze(self):
  263. if self.server:
  264. return self.server.run("/usr/sbin/xfs_freeze -f %s" % self.mount_point)
  265. def unfreeze(self):
  266. if self.server:
  267. return self.server.run("/usr/sbin/xfs_freeze -u %s" % self.mount_point)
  268. def snapshot(self):
  269. # if this volume is attached to a server
  270. # we need to freeze the XFS file system
  271. try:
  272. self.freeze()
  273. if self.server == None:
  274. snapshot = self.get_ec2_connection().create_snapshot(self.volume_id)
  275. else:
  276. snapshot = self.server.ec2.create_snapshot(self.volume_id)
  277. boto.log.info('Snapshot of Volume %s created: %s' % (self.name, snapshot))
  278. except Exception:
  279. boto.log.info('Snapshot error')
  280. boto.log.info(traceback.format_exc())
  281. finally:
  282. status = self.unfreeze()
  283. return status
  284. def get_snapshot_range(self, snaps, start_date=None, end_date=None):
  285. l = []
  286. for snap in snaps:
  287. if start_date and end_date:
  288. if snap.date >= start_date and snap.date <= end_date:
  289. l.append(snap)
  290. elif start_date:
  291. if snap.date >= start_date:
  292. l.append(snap)
  293. elif end_date:
  294. if snap.date <= end_date:
  295. l.append(snap)
  296. else:
  297. l.append(snap)
  298. return l
  299. def trim_snapshots(self, delete=False):
  300. """
  301. Trim the number of snapshots for this volume. This method always
  302. keeps the oldest snapshot. It then uses the parameters passed in
  303. to determine how many others should be kept.
  304. The algorithm is to keep all snapshots from the current day. Then
  305. it will keep the first snapshot of the day for the previous seven days.
  306. Then, it will keep the first snapshot of the week for the previous
  307. four weeks. After than, it will keep the first snapshot of the month
  308. for as many months as there are.
  309. """
  310. snaps = self.get_snapshots()
  311. # Always keep the oldest and the newest
  312. if len(snaps) <= 2:
  313. return snaps
  314. snaps = snaps[1:-1]
  315. now = datetime.datetime.now(snaps[0].date.tzinfo)
  316. midnight = datetime.datetime(year=now.year, month=now.month,
  317. day=now.day, tzinfo=now.tzinfo)
  318. # Keep the first snapshot from each day of the previous week
  319. one_week = datetime.timedelta(days=7, seconds=60*60)
  320. print midnight-one_week, midnight
  321. previous_week = self.get_snapshot_range(snaps, midnight-one_week, midnight)
  322. print previous_week
  323. if not previous_week:
  324. return snaps
  325. current_day = None
  326. for snap in previous_week:
  327. if current_day and current_day == snap.date.day:
  328. snap.keep = False
  329. else:
  330. current_day = snap.date.day
  331. # Get ourselves onto the next full week boundary
  332. if previous_week:
  333. week_boundary = previous_week[0].date
  334. if week_boundary.weekday() != 0:
  335. delta = datetime.timedelta(days=week_boundary.weekday())
  336. week_boundary = week_boundary - delta
  337. # Keep one within this partial week
  338. partial_week = self.get_snapshot_range(snaps, week_boundary, previous_week[0].date)
  339. if len(partial_week) > 1:
  340. for snap in partial_week[1:]:
  341. snap.keep = False
  342. # Keep the first snapshot of each week for the previous 4 weeks
  343. for i in range(0, 4):
  344. weeks_worth = self.get_snapshot_range(snaps, week_boundary-one_week, week_boundary)
  345. if len(weeks_worth) > 1:
  346. for snap in weeks_worth[1:]:
  347. snap.keep = False
  348. week_boundary = week_boundary - one_week
  349. # Now look through all remaining snaps and keep one per month
  350. remainder = self.get_snapshot_range(snaps, end_date=week_boundary)
  351. current_month = None
  352. for snap in remainder:
  353. if current_month and current_month == snap.date.month:
  354. snap.keep = False
  355. else:
  356. current_month = snap.date.month
  357. if delete:
  358. for snap in snaps:
  359. if not snap.keep:
  360. boto.log.info('Deleting %s(%s) for %s' % (snap, snap.date, self.name))
  361. snap.delete()
  362. return snaps
  363. def grow(self, size):
  364. pass
  365. def copy(self, snapshot):
  366. pass
  367. def get_snapshot_from_date(self, date):
  368. pass
  369. def delete(self, delete_ebs_volume=False):
  370. if delete_ebs_volume:
  371. self.detach()
  372. ec2 = self.get_ec2_connection()
  373. ec2.delete_volume(self.volume_id)
  374. Model.delete(self)
  375. def archive(self):
  376. # snapshot volume, trim snaps, delete volume-id
  377. pass