resources.py 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347
  1. """
  2. DataTypes used by this provider
  3. """
  4. import hashlib
  5. import inspect
  6. import logging
  7. from botocore.exceptions import ClientError
  8. import tenacity
  9. from cloudbridge.base.resources import BaseAttachmentInfo
  10. from cloudbridge.base.resources import BaseBucket
  11. from cloudbridge.base.resources import BaseBucketObject
  12. from cloudbridge.base.resources import BaseDnsRecord
  13. from cloudbridge.base.resources import BaseDnsZone
  14. from cloudbridge.base.resources import BaseFloatingIP
  15. from cloudbridge.base.resources import BaseInstance
  16. from cloudbridge.base.resources import BaseInternetGateway
  17. from cloudbridge.base.resources import BaseKeyPair
  18. from cloudbridge.base.resources import BaseLaunchConfig
  19. from cloudbridge.base.resources import BaseMachineImage
  20. from cloudbridge.base.resources import BaseNetwork
  21. from cloudbridge.base.resources import BasePlacementZone
  22. from cloudbridge.base.resources import BaseRegion
  23. from cloudbridge.base.resources import BaseRouter
  24. from cloudbridge.base.resources import BaseSnapshot
  25. from cloudbridge.base.resources import BaseSubnet
  26. from cloudbridge.base.resources import BaseVMFirewall
  27. from cloudbridge.base.resources import BaseVMFirewallRule
  28. from cloudbridge.base.resources import BaseVMType
  29. from cloudbridge.base.resources import BaseVolume
  30. from cloudbridge.interfaces.resources import GatewayState
  31. from cloudbridge.interfaces.resources import InstanceState
  32. from cloudbridge.interfaces.resources import MachineImageState
  33. from cloudbridge.interfaces.resources import NetworkState
  34. from cloudbridge.interfaces.resources import RouterState
  35. from cloudbridge.interfaces.resources import SnapshotState
  36. from cloudbridge.interfaces.resources import SubnetState
  37. from cloudbridge.interfaces.resources import VolumeState
  38. from .helpers import find_tag_value
  39. from .helpers import trim_empty_params
  40. from .subservices import AWSBucketObjectSubService
  41. from .subservices import AWSDnsRecordSubService
  42. from .subservices import AWSFloatingIPSubService
  43. from .subservices import AWSGatewaySubService
  44. from .subservices import AWSSubnetSubService
  45. from .subservices import AWSVMFirewallRuleSubService
  46. log = logging.getLogger(__name__)
  47. class AWSMachineImage(BaseMachineImage):
  48. IMAGE_STATE_MAP = {
  49. 'pending': MachineImageState.PENDING,
  50. 'transient': MachineImageState.PENDING,
  51. 'available': MachineImageState.AVAILABLE,
  52. 'deregistered': MachineImageState.PENDING,
  53. 'failed': MachineImageState.ERROR,
  54. 'error': MachineImageState.ERROR,
  55. 'invalid': MachineImageState.ERROR
  56. }
  57. def __init__(self, provider, image):
  58. super(AWSMachineImage, self).__init__(provider)
  59. if isinstance(image, AWSMachineImage):
  60. # pylint:disable=protected-access
  61. self._ec2_image = image._ec2_image
  62. else:
  63. self._ec2_image = image
  64. @property
  65. def id(self):
  66. return self._ec2_image.id
  67. @property
  68. def name(self):
  69. try:
  70. return self._ec2_image.name
  71. except (AttributeError, ClientError) as e:
  72. log.warn("Cannot get name for image {0}: {1}".format(self.id, e))
  73. @property
  74. # pylint:disable=arguments-differ
  75. def label(self):
  76. """
  77. .. note:: an instance must have a (case sensitive) tag ``Name``
  78. """
  79. return find_tag_value(self._ec2_image.tags, 'Name')
  80. @tenacity.retry(stop=tenacity.stop_after_attempt(5),
  81. retry=tenacity.retry_if_exception_type(ClientError),
  82. wait=tenacity.wait_fixed(5),
  83. reraise=True)
  84. def _set_label(self, value):
  85. self._ec2_image.create_tags(Tags=[{'Key': 'Name',
  86. 'Value': value or ""}])
  87. @label.setter
  88. # pylint:disable=arguments-differ
  89. def label(self, value):
  90. self.assert_valid_resource_label(value)
  91. self._set_label(value)
  92. @property
  93. def description(self):
  94. try:
  95. return self._ec2_image.description
  96. except AttributeError:
  97. return None
  98. @property
  99. def min_disk(self):
  100. vols = [bdm.get('Ebs', {}) for bdm in
  101. self._ec2_image.block_device_mappings if
  102. bdm.get('DeviceName') == self._ec2_image.root_device_name]
  103. if vols:
  104. return vols[0].get('VolumeSize')
  105. else:
  106. return None
  107. def delete(self):
  108. snapshot_id = [
  109. bdm.get('Ebs', {}).get('SnapshotId') for bdm in
  110. self._ec2_image.block_device_mappings if
  111. bdm.get('DeviceName') == self._ec2_image.root_device_name]
  112. self._ec2_image.deregister()
  113. self.wait_for([MachineImageState.UNKNOWN, MachineImageState.ERROR])
  114. snapshot = self._provider.storage.snapshots.get(snapshot_id[0])
  115. if snapshot:
  116. snapshot.delete()
  117. @property
  118. def state(self):
  119. try:
  120. return AWSMachineImage.IMAGE_STATE_MAP.get(
  121. self._ec2_image.state, MachineImageState.UNKNOWN)
  122. except Exception:
  123. # Ignore all exceptions when querying state
  124. return MachineImageState.UNKNOWN
  125. def refresh(self):
  126. self._ec2_image.reload()
  127. class AWSPlacementZone(BasePlacementZone):
  128. def __init__(self, provider, zone, region):
  129. super(AWSPlacementZone, self).__init__(provider)
  130. if isinstance(zone, AWSPlacementZone):
  131. # pylint:disable=protected-access
  132. self._aws_zone = zone._aws_zone
  133. # pylint:disable=protected-access
  134. self._aws_region = zone._aws_region
  135. else:
  136. self._aws_zone = zone
  137. self._aws_region = region
  138. @property
  139. def id(self):
  140. return self._aws_zone
  141. @property
  142. def name(self):
  143. return self.id
  144. @property
  145. def region_name(self):
  146. return self._aws_region
  147. class AWSVMType(BaseVMType):
  148. def __init__(self, provider, instance_dict):
  149. super(AWSVMType, self).__init__(provider)
  150. self._inst_dict = instance_dict
  151. @property
  152. def id(self):
  153. return str(self._inst_dict.get('InstanceType'))
  154. @property
  155. def name(self):
  156. return self.id
  157. @property
  158. def family(self):
  159. # Limited to whether CurrentGeneration or not
  160. curr = self._inst_dict.get('CurrentGeneration')
  161. if curr:
  162. return 'CurrentGeneration'
  163. return None
  164. @property
  165. def vcpus(self):
  166. vcpus = self._inst_dict.get('VCpuInfo')
  167. if vcpus:
  168. return vcpus.get('DefaultVCpus', 0)
  169. return 0
  170. @property
  171. def ram(self):
  172. ram = self._inst_dict.get('MemoryInfo')
  173. if ram:
  174. mib = ram.get('SizeInMiB', 0)
  175. return mib / 1024
  176. return 0
  177. @property
  178. def size_root_disk(self):
  179. return 0
  180. @property
  181. def size_ephemeral_disks(self):
  182. storage = self._inst_dict.get('InstanceStorageInfo')
  183. if storage:
  184. return storage.get('TotalSizeInGB', 0)
  185. return 0
  186. @property
  187. def num_ephemeral_disks(self):
  188. storage = self._inst_dict.get('InstanceStorageInfo')
  189. if storage:
  190. disks = storage.get("Disks", [])
  191. count = 0
  192. for disk in disks:
  193. count += disk.get("Count", 0)
  194. return count
  195. return 0
  196. @property
  197. def extra_data(self):
  198. return {key: val for key, val in self._inst_dict.items()
  199. if key not in ["InstanceType", "VCpuInfo", "MemoryInfo"]}
  200. class AWSInstance(BaseInstance):
  201. # ref:
  202. # http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-lifecycle.html
  203. INSTANCE_STATE_MAP = {
  204. 'pending': InstanceState.PENDING,
  205. 'running': InstanceState.RUNNING,
  206. 'shutting-down': InstanceState.CONFIGURING,
  207. 'terminated': InstanceState.DELETED,
  208. 'stopping': InstanceState.CONFIGURING,
  209. 'stopped': InstanceState.STOPPED
  210. }
  211. def __init__(self, provider, ec2_instance):
  212. super(AWSInstance, self).__init__(provider)
  213. self._ec2_instance = ec2_instance
  214. self._unknown_state = False
  215. @property
  216. def id(self):
  217. return self._ec2_instance.id
  218. @property
  219. def name(self):
  220. return self.id
  221. @property
  222. # pylint:disable=arguments-differ
  223. def label(self):
  224. """
  225. .. note:: an instance must have a (case sensitive) tag ``Name``
  226. """
  227. return find_tag_value(self._ec2_instance.tags, 'Name')
  228. @tenacity.retry(stop=tenacity.stop_after_attempt(5),
  229. retry=tenacity.retry_if_exception_type(ClientError),
  230. wait=tenacity.wait_fixed(5),
  231. reraise=True)
  232. def _set_label(self, value):
  233. self._ec2_instance.create_tags(Tags=[{'Key': 'Name',
  234. 'Value': value or ""}])
  235. @label.setter
  236. # pylint:disable=arguments-differ
  237. def label(self, value):
  238. self.assert_valid_resource_label(value)
  239. self._set_label(value)
  240. @property
  241. def public_ips(self):
  242. return ([self._ec2_instance.public_ip_address]
  243. if self._ec2_instance.public_ip_address else [])
  244. @property
  245. def private_ips(self):
  246. return ([self._ec2_instance.private_ip_address]
  247. if self._ec2_instance.private_ip_address else [])
  248. @property
  249. def vm_type_id(self):
  250. return self._ec2_instance.instance_type
  251. @property
  252. def vm_type(self):
  253. return self._provider.compute.vm_types.find(
  254. name=self._ec2_instance.instance_type)[0]
  255. @property
  256. def create_time(self):
  257. """
  258. Get the instance creation time
  259. """
  260. return self._ec2_instance.launch_time
  261. def reboot(self):
  262. self._ec2_instance.reboot()
  263. def start(self):
  264. response = self._ec2_instance.start()
  265. states = ['pending', 'running']
  266. if response['StartingInstances'][0]['CurrentState']['Name'] in states:
  267. return True
  268. else:
  269. return False
  270. def stop(self):
  271. response = self._ec2_instance.stop()
  272. states = ['stopping', 'stopped']
  273. if response['StoppingInstances'][0]['CurrentState']['Name'] in states:
  274. return True
  275. else:
  276. return False
  277. @property
  278. def image_id(self):
  279. return self._ec2_instance.image_id
  280. @property
  281. def zone_id(self):
  282. return self._ec2_instance.placement.get('AvailabilityZone')
  283. @property
  284. def subnet_id(self):
  285. return self._ec2_instance.subnet_id
  286. @property
  287. def vm_firewalls(self):
  288. return [
  289. self._provider.security.vm_firewalls.get(fw_id)
  290. for fw_id in self.vm_firewall_ids
  291. ]
  292. @property
  293. def vm_firewall_ids(self):
  294. return list(set([
  295. group.get('GroupId') for group in
  296. self._ec2_instance.security_groups
  297. ]))
  298. @property
  299. def key_pair_id(self):
  300. return self._ec2_instance.key_name
  301. @tenacity.retry(stop=tenacity.stop_after_attempt(5),
  302. retry=tenacity.retry_if_exception_type(ClientError),
  303. wait=tenacity.wait_fixed(5),
  304. reraise=True)
  305. def _wait_for_image(self, image):
  306. self._provider.ec2_conn.meta.client.get_waiter('image_exists').wait(
  307. ImageIds=[image.id])
  308. def create_image(self, label):
  309. self.assert_valid_resource_label(label)
  310. name = self._generate_name_from_label(label, 'cb-img')
  311. image = AWSMachineImage(self._provider,
  312. self._ec2_instance.create_image(Name=name))
  313. # Wait for the image to exist
  314. self._wait_for_image(image)
  315. # Add image label
  316. image.label = label
  317. # Return the image
  318. image.refresh()
  319. return image
  320. def _get_fip(self, floating_ip):
  321. """Get a floating IP object based on the supplied allocation ID."""
  322. return self._provider.networking._floating_ips.get(None, floating_ip)
  323. def add_floating_ip(self, floating_ip):
  324. fip = (floating_ip if isinstance(floating_ip, AWSFloatingIP)
  325. else self._get_fip(floating_ip))
  326. # pylint:disable=protected-access
  327. params = trim_empty_params({
  328. 'InstanceId': self.id,
  329. 'PublicIp': None if self._ec2_instance.vpc_id else
  330. fip.public_ip,
  331. 'AllocationId': fip._ip.allocation_id})
  332. self._provider.ec2_conn.meta.client.associate_address(**params)
  333. self.refresh()
  334. def remove_floating_ip(self, floating_ip):
  335. fip = (floating_ip if isinstance(floating_ip, AWSFloatingIP)
  336. else self._get_fip(floating_ip))
  337. # pylint:disable=protected-access
  338. params = trim_empty_params({
  339. 'PublicIp': None if self._ec2_instance.vpc_id else
  340. fip.public_ip,
  341. 'AssociationId': fip._ip.association_id})
  342. self._provider.ec2_conn.meta.client.disassociate_address(**params)
  343. self.refresh()
  344. def add_vm_firewall(self, firewall):
  345. self._ec2_instance.modify_attribute(
  346. Groups=self.vm_firewall_ids + [firewall.id])
  347. def remove_vm_firewall(self, firewall):
  348. self._ec2_instance.modify_attribute(
  349. Groups=([fw_id for fw_id in self.vm_firewall_ids
  350. if fw_id != firewall.id]))
  351. @property
  352. def state(self):
  353. if self._unknown_state:
  354. return InstanceState.UNKNOWN
  355. try:
  356. return AWSInstance.INSTANCE_STATE_MAP.get(
  357. self._ec2_instance.state['Name'], InstanceState.UNKNOWN)
  358. except Exception:
  359. # Ignore all exceptions when querying state
  360. return InstanceState.UNKNOWN
  361. def refresh(self):
  362. try:
  363. self._ec2_instance.reload()
  364. self._unknown_state = False
  365. except ClientError:
  366. # The instance no longer exists and cannot be refreshed.
  367. # set the state to unknown
  368. self._unknown_state = True
  369. # pylint:disable=unused-argument
  370. def _wait_till_exists(self, timeout=None, interval=None):
  371. self._ec2_instance.wait_until_exists()
  372. # refresh again to make sure instance status is in sync
  373. self._ec2_instance.reload()
  374. class AWSVolume(BaseVolume):
  375. # Ref:
  376. # http://docs.aws.amazon.com/AWSEC2/latest/CommandLineReference/
  377. # ApiReference-cmd-DescribeVolumes.html
  378. VOLUME_STATE_MAP = {
  379. 'creating': VolumeState.CREATING,
  380. 'available': VolumeState.AVAILABLE,
  381. 'in-use': VolumeState.IN_USE,
  382. 'deleting': VolumeState.CONFIGURING,
  383. 'deleted': VolumeState.DELETED,
  384. 'error': VolumeState.ERROR
  385. }
  386. def __init__(self, provider, volume):
  387. super(AWSVolume, self).__init__(provider)
  388. self._volume = volume
  389. self._unknown_state = False
  390. @property
  391. def id(self):
  392. return self._volume.id
  393. @property
  394. def name(self):
  395. return self.id
  396. @property
  397. # pylint:disable=arguments-differ
  398. def label(self):
  399. try:
  400. return find_tag_value(self._volume.tags, 'Name')
  401. except ClientError as e:
  402. log.warn("Cannot get label for volume {0}: {1}".format(self.id, e))
  403. @tenacity.retry(stop=tenacity.stop_after_attempt(5),
  404. retry=tenacity.retry_if_exception_type(ClientError),
  405. wait=tenacity.wait_fixed(5),
  406. reraise=True)
  407. def _set_label(self, value):
  408. self._volume.create_tags(Tags=[{'Key': 'Name', 'Value': value or ""}])
  409. @label.setter
  410. # pylint:disable=arguments-differ
  411. def label(self, value):
  412. self.assert_valid_resource_label(value)
  413. self._set_label(value)
  414. @property
  415. def description(self):
  416. return find_tag_value(self._volume.tags, 'Description')
  417. @description.setter
  418. def description(self, value):
  419. self._volume.create_tags(Tags=[{'Key': 'Description',
  420. 'Value': value or ""}])
  421. @property
  422. def size(self):
  423. return self._volume.size
  424. @property
  425. def create_time(self):
  426. return self._volume.create_time
  427. @property
  428. def zone_id(self):
  429. return self._volume.availability_zone
  430. @property
  431. def source(self):
  432. if self._volume.snapshot_id:
  433. return self._provider.storage.snapshots.get(
  434. self._volume.snapshot_id)
  435. return None
  436. @property
  437. def attachments(self):
  438. return [
  439. BaseAttachmentInfo(self,
  440. a.get('InstanceId'),
  441. a.get('Device'))
  442. for a in self._volume.attachments
  443. ][0] if self._volume.attachments else None
  444. @tenacity.retry(stop=tenacity.stop_after_attempt(5),
  445. retry=tenacity.retry_if_exception_type(Exception),
  446. wait=tenacity.wait_fixed(5),
  447. reraise=True)
  448. def _wait_till_volume_attached(self, instance_id):
  449. self.refresh()
  450. if not self.attachments.instance_id == instance_id:
  451. raise Exception(f"Volume {self.id} is not yet attached to"
  452. f"instance {instance_id}")
  453. def attach(self, instance, device):
  454. instance_id = instance.id if isinstance(
  455. instance,
  456. AWSInstance) else instance
  457. self._volume.attach_to_instance(InstanceId=instance_id,
  458. Device=device)
  459. self._wait_till_volume_attached(instance_id)
  460. def detach(self, force=False):
  461. a = self.attachments
  462. if a:
  463. self._volume.detach_from_instance(
  464. InstanceId=a.instance_id,
  465. Device=a.device,
  466. Force=force)
  467. def create_snapshot(self, label, description=None):
  468. self.assert_valid_resource_label(label)
  469. snap = AWSSnapshot(
  470. self._provider,
  471. self._volume.create_snapshot(
  472. TagSpecifications=[{'ResourceType': 'snapshot',
  473. 'Tags': [{'Key': 'Name',
  474. 'Value': label}]}],
  475. Description=description or ""))
  476. snap.wait_till_ready()
  477. return snap
  478. @property
  479. def state(self):
  480. if self._unknown_state:
  481. return VolumeState.UNKNOWN
  482. try:
  483. return AWSVolume.VOLUME_STATE_MAP.get(
  484. self._volume.state, VolumeState.UNKNOWN)
  485. except Exception:
  486. # Ignore all exceptions when querying state
  487. return VolumeState.UNKNOWN
  488. def refresh(self):
  489. try:
  490. self._volume.reload()
  491. self._unknown_state = False
  492. except ClientError:
  493. # The volume no longer exists and cannot be refreshed.
  494. # set the status to unknown
  495. self._unknown_state = True
  496. class AWSSnapshot(BaseSnapshot):
  497. # Ref: http://docs.aws.amazon.com/AWSEC2/latest/CommandLineReference/
  498. # ApiReference-cmd-DescribeSnapshots.html
  499. SNAPSHOT_STATE_MAP = {
  500. 'pending': SnapshotState.PENDING,
  501. 'deleting': SnapshotState.PENDING,
  502. 'completed': SnapshotState.AVAILABLE,
  503. 'error': SnapshotState.ERROR
  504. }
  505. def __init__(self, provider, snapshot):
  506. super(AWSSnapshot, self).__init__(provider)
  507. self._snapshot = snapshot
  508. self._unknown_state = False
  509. @property
  510. def id(self):
  511. return self._snapshot.id
  512. @property
  513. def name(self):
  514. return self.id
  515. @property
  516. # pylint:disable=arguments-differ
  517. def label(self):
  518. try:
  519. return find_tag_value(self._snapshot.tags, 'Name')
  520. except ClientError as e:
  521. log.warn("Cannot get label for snap {0}: {1}".format(self.id, e))
  522. @tenacity.retry(stop=tenacity.stop_after_attempt(5),
  523. retry=tenacity.retry_if_exception_type(ClientError),
  524. wait=tenacity.wait_fixed(5),
  525. reraise=True)
  526. def _set_label(self, value):
  527. self._snapshot.create_tags(Tags=[{'Key': 'Name',
  528. 'Value': value or ""}])
  529. @label.setter
  530. # pylint:disable=arguments-differ
  531. def label(self, value):
  532. self.assert_valid_resource_label(value)
  533. self._set_label(value)
  534. @property
  535. def description(self):
  536. return find_tag_value(self._snapshot.tags, 'Description')
  537. @description.setter
  538. def description(self, value):
  539. self._snapshot.create_tags(Tags=[{
  540. 'Key': 'Description', 'Value': value or ""}])
  541. @property
  542. def size(self):
  543. return self._snapshot.volume_size
  544. @property
  545. def volume_id(self):
  546. return self._snapshot.volume_id
  547. @property
  548. def create_time(self):
  549. return self._snapshot.start_time
  550. @property
  551. def state(self):
  552. if self._unknown_state:
  553. return SnapshotState.UNKNOWN
  554. try:
  555. return AWSSnapshot.SNAPSHOT_STATE_MAP.get(
  556. self._snapshot.state, SnapshotState.UNKNOWN)
  557. except Exception:
  558. # Ignore all exceptions when querying state
  559. return SnapshotState.UNKNOWN
  560. def refresh(self):
  561. try:
  562. self._snapshot.reload()
  563. self._unknown_state = False
  564. except ClientError:
  565. # The snapshot no longer exists and cannot be refreshed.
  566. # set the status to unknown
  567. self._unknown_state = True
  568. def create_volume(self, size=None, volume_type=None, iops=None):
  569. label = "from-snap-{0}".format(self.label or self.id)
  570. cb_vol = self._provider.storage.volumes.create(
  571. label=label,
  572. size=size,
  573. snapshot=self.id)
  574. cb_vol.wait_till_ready()
  575. return cb_vol
  576. class AWSKeyPair(BaseKeyPair):
  577. def __init__(self, provider, key_pair):
  578. super(AWSKeyPair, self).__init__(provider, key_pair)
  579. class AWSVMFirewall(BaseVMFirewall):
  580. def __init__(self, provider, _vm_firewall):
  581. super(AWSVMFirewall, self).__init__(provider, _vm_firewall)
  582. self._rule_container = AWSVMFirewallRuleSubService(provider, self)
  583. @property
  584. def name(self):
  585. """
  586. Return the name of this VM firewall.
  587. """
  588. return self._vm_firewall.group_name
  589. @property
  590. def label(self):
  591. try:
  592. return find_tag_value(self._vm_firewall.tags, 'Name')
  593. except ClientError:
  594. return None
  595. @tenacity.retry(stop=tenacity.stop_after_attempt(5),
  596. retry=tenacity.retry_if_exception_type(ClientError),
  597. wait=tenacity.wait_fixed(5),
  598. reraise=True)
  599. def _set_label(self, value):
  600. self._vm_firewall.create_tags(Tags=[{'Key': 'Name',
  601. 'Value': value or ""}])
  602. @label.setter
  603. # pylint:disable=arguments-differ
  604. def label(self, value):
  605. self.assert_valid_resource_label(value)
  606. self._set_label(value)
  607. @property
  608. def description(self):
  609. try:
  610. return find_tag_value(self._vm_firewall.tags, 'Description')
  611. except ClientError:
  612. return None
  613. @description.setter
  614. # pylint:disable=arguments-differ
  615. def description(self, value):
  616. self._vm_firewall.create_tags(Tags=[{'Key': 'Description',
  617. 'Value': value or ""}])
  618. @property
  619. def network_id(self):
  620. return self._vm_firewall.vpc_id
  621. @property
  622. def rules(self):
  623. return self._rule_container
  624. def refresh(self):
  625. self._vm_firewall.reload()
  626. def to_json(self):
  627. attr = inspect.getmembers(self, lambda a: not inspect.isroutine(a))
  628. js = {k: v for (k, v) in attr if not k.startswith('_')}
  629. json_rules = [r.to_json() for r in self.rules]
  630. js['rules'] = json_rules
  631. if js.get('network_id'):
  632. js.pop('network_id') # Omit for consistency across cloud providers
  633. return js
  634. class AWSVMFirewallRule(BaseVMFirewallRule):
  635. def __init__(self, parent_fw, direction, rule):
  636. self._direction = direction
  637. super(AWSVMFirewallRule, self).__init__(parent_fw, rule)
  638. # cache id
  639. md5 = hashlib.md5()
  640. md5.update(self._name.encode('ascii'))
  641. self._id = md5.hexdigest()
  642. @property
  643. def id(self):
  644. return self._id
  645. @property
  646. def direction(self):
  647. return self._direction
  648. @property
  649. def protocol(self):
  650. return self._rule.get('IpProtocol')
  651. @property
  652. def from_port(self):
  653. return self._rule.get('FromPort')
  654. @property
  655. def to_port(self):
  656. return self._rule.get('ToPort')
  657. @property
  658. def cidr(self):
  659. if len(self._rule.get('IpRanges') or []) > 0:
  660. return self._rule['IpRanges'][0].get('CidrIp')
  661. return None
  662. @property
  663. def src_dest_fw_id(self):
  664. if len(self._rule.get('UserIdGroupPairs') or []) > 0:
  665. return self._rule['UserIdGroupPairs'][0]['GroupId']
  666. else:
  667. return None
  668. @property
  669. def src_dest_fw(self):
  670. if self.src_dest_fw_id:
  671. return AWSVMFirewall(
  672. self._provider,
  673. self._provider.ec2_conn.SecurityGroup(self.src_dest_fw_id))
  674. else:
  675. return None
  676. @staticmethod
  677. def _construct_ip_perms(protocol, from_port, to_port, cidr,
  678. src_dest_fw_id):
  679. return {
  680. 'IpProtocol': protocol,
  681. 'FromPort': from_port,
  682. 'ToPort': to_port,
  683. 'IpRanges': [{'CidrIp': cidr}] if cidr else None,
  684. 'UserIdGroupPairs': [{
  685. 'GroupId': src_dest_fw_id}
  686. ] if src_dest_fw_id else None
  687. }
  688. class AWSBucketObject(BaseBucketObject):
  689. class BucketObjIterator():
  690. CHUNK_SIZE = 4096
  691. def __init__(self, body):
  692. self.body = body
  693. def __iter__(self):
  694. while True:
  695. data = self.read(self.CHUNK_SIZE)
  696. if data:
  697. yield data
  698. else:
  699. break
  700. def read(self, length):
  701. return self.body.read(amt=length)
  702. def close(self):
  703. return self.body.close()
  704. def __init__(self, provider, obj):
  705. super(AWSBucketObject, self).__init__(provider)
  706. self._obj = obj
  707. @property
  708. def id(self):
  709. return self._obj.key
  710. @property
  711. def name(self):
  712. return self.id
  713. @property
  714. def size(self):
  715. try:
  716. return self._obj.content_length
  717. except AttributeError: # we're dealing with s3.ObjectSummary
  718. return self._obj.size
  719. @property
  720. def last_modified(self):
  721. return self._obj.last_modified.strftime("%Y-%m-%dT%H:%M:%S.%f")
  722. def iter_content(self):
  723. return self.BucketObjIterator(self._obj.get().get('Body'))
  724. def upload(self, data):
  725. self._obj.put(Body=data)
  726. def upload_from_file(self, path):
  727. self._obj.upload_file(path)
  728. def delete(self):
  729. self._obj.delete()
  730. def generate_url(self, expires_in, writable=False):
  731. return self._provider.s3_conn.meta.client.generate_presigned_url(
  732. 'put_object' if writable else 'get_object',
  733. Params={'Bucket': self._obj.bucket_name, 'Key': self.id},
  734. ExpiresIn=expires_in)
  735. def refresh(self):
  736. self._obj.load()
  737. class AWSBucket(BaseBucket):
  738. def __init__(self, provider, bucket):
  739. super(AWSBucket, self).__init__(provider)
  740. self._bucket = bucket
  741. self._object_container = AWSBucketObjectSubService(provider, self)
  742. @property
  743. def id(self):
  744. return self._bucket.name
  745. @property
  746. def name(self):
  747. return self.id
  748. @property
  749. def objects(self):
  750. return self._object_container
  751. class AWSRegion(BaseRegion):
  752. def __init__(self, provider, aws_region):
  753. super(AWSRegion, self).__init__(provider)
  754. self._aws_region = aws_region
  755. @property
  756. def id(self):
  757. return self._aws_region.get('RegionName')
  758. @property
  759. def name(self):
  760. return self.id
  761. @property
  762. def zones(self):
  763. if self.id == self._provider.region_name: # optimisation
  764. conn = self._provider.ec2_conn
  765. else:
  766. # pylint:disable=protected-access
  767. conn = self._provider._connect_ec2_region(region_name=self.id)
  768. zones = (conn.meta.client.describe_availability_zones()
  769. .get('AvailabilityZones', []))
  770. return [AWSPlacementZone(self._provider, zone.get('ZoneName'),
  771. self.id)
  772. for zone in zones]
  773. class AWSNetwork(BaseNetwork):
  774. # Ref:
  775. # docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeVpcs.html
  776. _NETWORK_STATE_MAP = {
  777. 'pending': NetworkState.PENDING,
  778. 'available': NetworkState.AVAILABLE,
  779. }
  780. def __init__(self, provider, network):
  781. super(AWSNetwork, self).__init__(provider)
  782. self._vpc = network
  783. self._unknown_state = False
  784. self._gtw_container = AWSGatewaySubService(provider, self)
  785. self._subnet_svc = AWSSubnetSubService(provider, self)
  786. @property
  787. def id(self):
  788. return self._vpc.id
  789. @property
  790. def name(self):
  791. return self.id
  792. @property
  793. def label(self):
  794. return find_tag_value(self._vpc.tags, 'Name')
  795. @tenacity.retry(stop=tenacity.stop_after_attempt(5),
  796. retry=tenacity.retry_if_exception_type(ClientError),
  797. wait=tenacity.wait_fixed(5),
  798. reraise=True)
  799. def _set_label(self, value):
  800. self._vpc.create_tags(Tags=[{'Key': 'Name', 'Value': value or ""}])
  801. @label.setter
  802. # pylint:disable=arguments-differ
  803. def label(self, value):
  804. self.assert_valid_resource_label(value)
  805. self._set_label(value)
  806. @property
  807. def external(self):
  808. """
  809. For AWS, all VPC networks can be connected to the Internet so always
  810. return ``True``.
  811. """
  812. return True
  813. @property
  814. def state(self):
  815. if self._unknown_state:
  816. return NetworkState.UNKNOWN
  817. try:
  818. return AWSNetwork._NETWORK_STATE_MAP.get(
  819. self._vpc.state, NetworkState.UNKNOWN)
  820. except Exception:
  821. # Ignore all exceptions when querying state
  822. return NetworkState.UNKNOWN
  823. @property
  824. def cidr_block(self):
  825. return self._vpc.cidr_block
  826. @property
  827. def subnets(self):
  828. return self._subnet_svc
  829. def refresh(self):
  830. try:
  831. self._vpc.reload()
  832. self._unknown_state = False
  833. except ClientError:
  834. # The network no longer exists and cannot be refreshed.
  835. # set the status to unknown
  836. self._unknown_state = True
  837. @tenacity.retry(stop=tenacity.stop_after_attempt(5),
  838. retry=tenacity.retry_if_exception_type(ClientError),
  839. wait=tenacity.wait_fixed(5),
  840. reraise=True)
  841. def _wait_for_vpc(self):
  842. self._vpc.wait_until_exists()
  843. self._vpc.wait_until_available()
  844. def wait_till_ready(self, timeout=None, interval=None):
  845. self._wait_for_vpc()
  846. self.refresh()
  847. @property
  848. def gateways(self):
  849. return self._gtw_container
  850. class AWSSubnet(BaseSubnet):
  851. # http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeSubnets.html
  852. _SUBNET_STATE_MAP = {
  853. 'pending': SubnetState.PENDING,
  854. 'available': SubnetState.AVAILABLE,
  855. }
  856. def __init__(self, provider, subnet):
  857. super(AWSSubnet, self).__init__(provider)
  858. self._subnet = subnet
  859. self._unknown_state = False
  860. @property
  861. def id(self):
  862. return self._subnet.id
  863. @property
  864. def name(self):
  865. return self.id
  866. @property
  867. def label(self):
  868. return find_tag_value(self._subnet.tags, 'Name')
  869. @tenacity.retry(stop=tenacity.stop_after_attempt(5),
  870. retry=tenacity.retry_if_exception_type(ClientError),
  871. wait=tenacity.wait_fixed(5),
  872. reraise=True)
  873. def _set_label(self, value):
  874. self._subnet.create_tags(Tags=[{'Key': 'Name', 'Value': value or ""}])
  875. @label.setter
  876. # pylint:disable=arguments-differ
  877. def label(self, value):
  878. self.assert_valid_resource_label(value)
  879. self._set_label(value)
  880. @property
  881. def cidr_block(self):
  882. return self._subnet.cidr_block
  883. @property
  884. def network_id(self):
  885. return self._subnet.vpc_id
  886. @property
  887. def zone(self):
  888. return AWSPlacementZone(self._provider, self._subnet.availability_zone,
  889. self._provider.region_name)
  890. @property
  891. def state(self):
  892. if self._unknown_state:
  893. return SubnetState.UNKNOWN
  894. try:
  895. return self._SUBNET_STATE_MAP.get(
  896. self._subnet.state, SubnetState.UNKNOWN)
  897. except Exception:
  898. # Ignore all exceptions when querying state
  899. return SubnetState.UNKNOWN
  900. def refresh(self):
  901. try:
  902. self._subnet.reload()
  903. self._unknown_state = False
  904. except ClientError:
  905. # subnet no longer exists
  906. self._unknown_state = True
  907. class AWSFloatingIP(BaseFloatingIP):
  908. def __init__(self, provider, floating_ip):
  909. super(AWSFloatingIP, self).__init__(provider)
  910. self._ip = floating_ip
  911. @property
  912. def id(self):
  913. return self._ip.allocation_id
  914. @property
  915. def public_ip(self):
  916. return self._ip.public_ip
  917. @property
  918. def private_ip(self):
  919. return self._ip.private_ip_address
  920. @property
  921. def in_use(self):
  922. return True if self._ip.association_id else False
  923. def refresh(self):
  924. self._ip.reload()
  925. class AWSRouter(BaseRouter):
  926. def __init__(self, provider, route_table):
  927. super(AWSRouter, self).__init__(provider)
  928. self._route_table = route_table
  929. @property
  930. def id(self):
  931. return self._route_table.id
  932. @property
  933. def name(self):
  934. return self.id
  935. @property
  936. def label(self):
  937. return find_tag_value(self._route_table.tags, 'Name')
  938. @tenacity.retry(stop=tenacity.stop_after_attempt(5),
  939. retry=tenacity.retry_if_exception_type(ClientError),
  940. wait=tenacity.wait_fixed(5),
  941. reraise=True)
  942. def _set_label(self, value):
  943. self._route_table.create_tags(Tags=[{'Key': 'Name',
  944. 'Value': value or ""}])
  945. @label.setter
  946. # pylint:disable=arguments-differ
  947. def label(self, value):
  948. self.assert_valid_resource_label(value)
  949. self._set_label(value)
  950. def refresh(self):
  951. try:
  952. self._route_table.reload()
  953. except ClientError:
  954. self._route_table.associations = None
  955. @property
  956. def state(self):
  957. if self._route_table.associations:
  958. return RouterState.ATTACHED
  959. return RouterState.DETACHED
  960. @property
  961. def network_id(self):
  962. return self._route_table.vpc_id
  963. @tenacity.retry(stop=tenacity.stop_after_attempt(5),
  964. retry=tenacity.retry_if_exception_type(Exception),
  965. wait=tenacity.wait_fixed(5),
  966. reraise=True)
  967. def _wait_till_subnet_attached(self, subnet_id):
  968. self.refresh()
  969. association = [a for a in self._route_table.associations
  970. if a.subnet_id == subnet_id]
  971. if not association:
  972. raise Exception(
  973. f"Subnet {subnet_id} not attached to route table {self.id}")
  974. def attach_subnet(self, subnet):
  975. subnet_id = subnet.id if isinstance(subnet, AWSSubnet) else subnet
  976. self._route_table.associate_with_subnet(SubnetId=subnet_id)
  977. self._wait_till_subnet_attached(subnet_id)
  978. def detach_subnet(self, subnet):
  979. subnet_id = subnet.id if isinstance(subnet, AWSSubnet) else subnet
  980. associations = [a for a in self._route_table.associations
  981. if a.subnet_id == subnet_id]
  982. for a in associations:
  983. a.delete()
  984. self.refresh()
  985. @property
  986. def subnets(self):
  987. return [AWSSubnet(self._provider, rta.subnet)
  988. for rta in self._route_table.associations if rta.subnet]
  989. def attach_gateway(self, gateway):
  990. gw_id = (gateway.id if isinstance(gateway, AWSInternetGateway)
  991. else gateway)
  992. if self._route_table.create_route(
  993. DestinationCidrBlock='0.0.0.0/0', GatewayId=gw_id):
  994. return True
  995. return False
  996. def detach_gateway(self, gateway):
  997. gw_id = (gateway.id if isinstance(gateway, AWSInternetGateway)
  998. else gateway)
  999. return self._provider.ec2_conn.meta.client.detach_internet_gateway(
  1000. InternetGatewayId=gw_id, VpcId=self._route_table.vpc_id)
  1001. class AWSInternetGateway(BaseInternetGateway):
  1002. def __init__(self, provider, gateway):
  1003. super(AWSInternetGateway, self).__init__(provider)
  1004. self._gateway = gateway
  1005. self._gateway.state = ''
  1006. self._fips_container = AWSFloatingIPSubService(provider, self)
  1007. @property
  1008. def id(self):
  1009. return self._gateway.id
  1010. @property
  1011. def name(self):
  1012. return find_tag_value(self._gateway.tags, 'Name')
  1013. def refresh(self):
  1014. try:
  1015. self._gateway.reload()
  1016. except ClientError:
  1017. self._gateway.state = GatewayState.UNKNOWN
  1018. @property
  1019. def state(self):
  1020. if self._gateway.state == GatewayState.UNKNOWN:
  1021. return GatewayState.UNKNOWN
  1022. else:
  1023. return GatewayState.AVAILABLE
  1024. @property
  1025. def network_id(self):
  1026. if self._gateway.attachments:
  1027. return self._gateway.attachments[0].get('VpcId')
  1028. return None
  1029. @property
  1030. def floating_ips(self):
  1031. return self._fips_container
  1032. class AWSLaunchConfig(BaseLaunchConfig):
  1033. def __init__(self, provider):
  1034. super(AWSLaunchConfig, self).__init__(provider)
  1035. class AWSDnsZone(BaseDnsZone):
  1036. def __init__(self, provider, dns_zone):
  1037. super(AWSDnsZone, self).__init__(provider)
  1038. self._dns_zone = dns_zone
  1039. self._dns_record_container = AWSDnsRecordSubService(provider, self)
  1040. @property
  1041. def id(self):
  1042. # The ID contains a slash, do not allow this
  1043. return self.escape_zone_id(self.aws_id)
  1044. @property
  1045. def aws_id(self):
  1046. return self._dns_zone.get('Id')
  1047. @staticmethod
  1048. def escape_zone_id(value):
  1049. return value.replace("/", "-") if value else None
  1050. @staticmethod
  1051. def unescape_zone_id(value):
  1052. return value.replace("-", "/") if value else None
  1053. @property
  1054. def name(self):
  1055. return self._dns_zone.get('Name')
  1056. @property
  1057. def admin_email(self):
  1058. comment = self._dns_zone.get('Config', {}).get('Comment')
  1059. if comment:
  1060. email_field = comment.split(",")[0].split("=")
  1061. if email_field[0] == "admin_email":
  1062. return email_field[1]
  1063. else:
  1064. return None
  1065. else:
  1066. return None
  1067. @property
  1068. def records(self):
  1069. return self._dns_record_container
  1070. class AWSDnsRecord(BaseDnsRecord):
  1071. def __init__(self, provider, dns_zone, dns_record):
  1072. super(AWSDnsRecord, self).__init__(provider)
  1073. self._dns_zone = dns_zone
  1074. self._dns_rec = dns_record
  1075. @property
  1076. def id(self):
  1077. return self._dns_rec.get('Name') + ":" + self._dns_rec.get('Type')
  1078. @property
  1079. def name(self):
  1080. return self._dns_rec.get('Name')
  1081. @property
  1082. def zone_id(self):
  1083. return self._dns_zone.id
  1084. @property
  1085. def type(self):
  1086. return self._dns_rec.get('Type')
  1087. @property
  1088. def data(self):
  1089. return [rec.get('Value') for rec in
  1090. self._dns_rec.get('ResourceRecords')]
  1091. @property
  1092. def ttl(self):
  1093. return self._dns_rec.get('TTL')
  1094. def delete(self):
  1095. # pylint:disable=protected-access
  1096. return self._provider.dns._records.delete(self._dns_zone, self)