services.py 44 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076
  1. """Services implemented by the AWS provider."""
  2. import ipaddress
  3. import logging
  4. import string
  5. from botocore.exceptions import ClientError
  6. import cachetools
  7. import requests
  8. import cloudbridge.cloud.base.helpers as cb_helpers
  9. from cloudbridge.cloud.base.middleware import implement
  10. from cloudbridge.cloud.base.resources import ClientPagedResultList
  11. from cloudbridge.cloud.base.services import BaseBucketObjectService
  12. from cloudbridge.cloud.base.services import BaseBucketService
  13. from cloudbridge.cloud.base.services import BaseComputeService
  14. from cloudbridge.cloud.base.services import BaseImageService
  15. from cloudbridge.cloud.base.services import BaseInstanceService
  16. from cloudbridge.cloud.base.services import BaseKeyPairService
  17. from cloudbridge.cloud.base.services import BaseNetworkService
  18. from cloudbridge.cloud.base.services import BaseNetworkingService
  19. from cloudbridge.cloud.base.services import BaseRegionService
  20. from cloudbridge.cloud.base.services import BaseRouterService
  21. from cloudbridge.cloud.base.services import BaseSecurityService
  22. from cloudbridge.cloud.base.services import BaseSnapshotService
  23. from cloudbridge.cloud.base.services import BaseStorageService
  24. from cloudbridge.cloud.base.services import BaseSubnetService
  25. from cloudbridge.cloud.base.services import BaseVMFirewallService
  26. from cloudbridge.cloud.base.services import BaseVMTypeService
  27. from cloudbridge.cloud.base.services import BaseVolumeService
  28. from cloudbridge.cloud.interfaces.exceptions import DuplicateResourceException
  29. from cloudbridge.cloud.interfaces.exceptions import \
  30. InvalidConfigurationException
  31. from cloudbridge.cloud.interfaces.exceptions import InvalidParamException
  32. from cloudbridge.cloud.interfaces.resources import KeyPair
  33. from cloudbridge.cloud.interfaces.resources import MachineImage
  34. from cloudbridge.cloud.interfaces.resources import Network
  35. from cloudbridge.cloud.interfaces.resources import PlacementZone
  36. from cloudbridge.cloud.interfaces.resources import Snapshot
  37. from cloudbridge.cloud.interfaces.resources import VMFirewall
  38. from cloudbridge.cloud.interfaces.resources import VMType
  39. from cloudbridge.cloud.interfaces.resources import Volume
  40. from .helpers import BotoEC2Service
  41. from .helpers import BotoS3Service
  42. from .resources import AWSBucket
  43. from .resources import AWSBucketObject
  44. from .resources import AWSInstance
  45. from .resources import AWSKeyPair
  46. from .resources import AWSLaunchConfig
  47. from .resources import AWSMachineImage
  48. from .resources import AWSNetwork
  49. from .resources import AWSPlacementZone
  50. from .resources import AWSRegion
  51. from .resources import AWSRouter
  52. from .resources import AWSSnapshot
  53. from .resources import AWSSubnet
  54. from .resources import AWSVMFirewall
  55. from .resources import AWSVMType
  56. from .resources import AWSVolume
  57. log = logging.getLogger(__name__)
  58. class AWSSecurityService(BaseSecurityService):
  59. def __init__(self, provider):
  60. super(AWSSecurityService, self).__init__(provider)
  61. # Initialize provider services
  62. self._key_pairs = AWSKeyPairService(provider)
  63. self._vm_firewalls = AWSVMFirewallService(provider)
  64. @property
  65. def key_pairs(self):
  66. return self._key_pairs
  67. @property
  68. def vm_firewalls(self):
  69. return self._vm_firewalls
  70. class AWSKeyPairService(BaseKeyPairService):
  71. def __init__(self, provider):
  72. super(AWSKeyPairService, self).__init__(provider)
  73. self.svc = BotoEC2Service(provider=self.provider,
  74. cb_resource=AWSKeyPair,
  75. boto_collection_name='key_pairs')
  76. @implement(event_pattern="provider.security.key_pairs.get",
  77. priority=BaseKeyPairService.STANDARD_EVENT_PRIORITY)
  78. def _get(self, key_pair_id):
  79. log.debug("Getting Key Pair Service %s", key_pair_id)
  80. return self.svc.get(key_pair_id)
  81. @implement(event_pattern="provider.security.key_pairs.list",
  82. priority=BaseKeyPairService.STANDARD_EVENT_PRIORITY)
  83. def _list(self, limit=None, marker=None):
  84. return self.svc.list(limit=limit, marker=marker)
  85. @implement(event_pattern="provider.security.key_pairs.find",
  86. priority=BaseKeyPairService.STANDARD_EVENT_PRIORITY)
  87. def _find(self, **kwargs):
  88. name = kwargs.pop('name', None)
  89. # All kwargs should have been popped at this time.
  90. if len(kwargs) > 0:
  91. raise InvalidParamException(
  92. "Unrecognised parameters for search: %s. Supported "
  93. "attributes: %s" % (kwargs, 'name'))
  94. log.debug("Searching for Key Pair %s", name)
  95. return self.svc.find(filter_name='key-name', filter_value=name)
  96. @implement(event_pattern="provider.security.key_pairs.create",
  97. priority=BaseKeyPairService.STANDARD_EVENT_PRIORITY)
  98. def _create(self, name, public_key_material=None):
  99. private_key = None
  100. if not public_key_material:
  101. public_key_material, private_key = cb_helpers.generate_key_pair()
  102. try:
  103. kp = self.svc.create('import_key_pair', KeyName=name,
  104. PublicKeyMaterial=public_key_material)
  105. kp.material = private_key
  106. return kp
  107. except ClientError as e:
  108. if e.response['Error']['Code'] == 'InvalidKeyPair.Duplicate':
  109. raise DuplicateResourceException(
  110. 'Keypair already exists with name {0}'.format(name))
  111. else:
  112. raise e
  113. @implement(event_pattern="provider.security.key_pairs.delete",
  114. priority=BaseKeyPairService.STANDARD_EVENT_PRIORITY)
  115. def _delete(self, key_pair_id):
  116. aws_kp = self.svc.get_raw(key_pair_id)
  117. aws_kp.delete()
  118. class AWSVMFirewallService(BaseVMFirewallService):
  119. def __init__(self, provider):
  120. super(AWSVMFirewallService, self).__init__(provider)
  121. self.svc = BotoEC2Service(provider=self.provider,
  122. cb_resource=AWSVMFirewall,
  123. boto_collection_name='security_groups')
  124. @implement(event_pattern="provider.security.vm_firewalls.get",
  125. priority=BaseVMFirewallService.STANDARD_EVENT_PRIORITY)
  126. def _get(self, vm_firewall_id):
  127. log.debug("Getting Firewall Service with the id: %s", vm_firewall_id)
  128. return self.svc.get(vm_firewall_id)
  129. @implement(event_pattern="provider.security.vm_firewalls.list",
  130. priority=BaseVMFirewallService.STANDARD_EVENT_PRIORITY)
  131. def _list(self, limit=None, marker=None):
  132. return self.svc.list(limit=limit, marker=marker)
  133. @cb_helpers.deprecated_alias(network_id='network')
  134. @implement(event_pattern="provider.security.vm_firewalls.create",
  135. priority=BaseVMFirewallService.STANDARD_EVENT_PRIORITY)
  136. def _create(self, label, network, description=None):
  137. name = AWSVMFirewall._generate_name_from_label(label, 'cb-fw')
  138. network_id = network.id if isinstance(network, Network) else network
  139. obj = self.svc.create('create_security_group', GroupName=name,
  140. Description=description or name,
  141. VpcId=network_id)
  142. obj.label = label
  143. return obj
  144. @implement(event_pattern="provider.security.vm_firewalls.find",
  145. priority=BaseVMFirewallService.STANDARD_EVENT_PRIORITY)
  146. def _find(self, **kwargs):
  147. # Filter by name or label
  148. label = kwargs.pop('label', None)
  149. log.debug("Searching for Firewall Service %s", label)
  150. # All kwargs should have been popped at this time.
  151. if len(kwargs) > 0:
  152. raise InvalidParamException(
  153. "Unrecognised parameters for search: %s. Supported "
  154. "attributes: %s" % (kwargs, 'label'))
  155. return self.svc.find(filter_name='tag:Name',
  156. filter_value=label)
  157. @implement(event_pattern="provider.security.vm_firewalls.delete",
  158. priority=BaseVMFirewallService.STANDARD_EVENT_PRIORITY)
  159. def _delete(self, vm_firewall_id):
  160. aws_fw = self.svc.get_raw(vm_firewall_id)
  161. aws_fw.delete()
  162. class AWSStorageService(BaseStorageService):
  163. def __init__(self, provider):
  164. super(AWSStorageService, self).__init__(provider)
  165. # Initialize provider services
  166. self._volume_svc = AWSVolumeService(self.provider)
  167. self._snapshot_svc = AWSSnapshotService(self.provider)
  168. self._bucket_svc = AWSBucketService(self.provider)
  169. self._bucket_obj_svc = AWSBucketObjectService(self.provider)
  170. @property
  171. def volumes(self):
  172. return self._volume_svc
  173. @property
  174. def snapshots(self):
  175. return self._snapshot_svc
  176. @property
  177. def buckets(self):
  178. return self._bucket_svc
  179. @property
  180. def bucket_objects(self):
  181. return self._bucket_obj_svc
  182. class AWSVolumeService(BaseVolumeService):
  183. def __init__(self, provider):
  184. super(AWSVolumeService, self).__init__(provider)
  185. self.svc = BotoEC2Service(provider=self.provider,
  186. cb_resource=AWSVolume,
  187. boto_collection_name='volumes')
  188. @implement(event_pattern="provider.storage.volumes.get",
  189. priority=BaseVolumeService.STANDARD_EVENT_PRIORITY)
  190. def _get(self, volume_id):
  191. return self.svc.get(volume_id)
  192. @implement(event_pattern="provider.storage.volumes.find",
  193. priority=BaseVolumeService.STANDARD_EVENT_PRIORITY)
  194. def _find(self, **kwargs):
  195. label = kwargs.pop('label', None)
  196. # All kwargs should have been popped at this time.
  197. if len(kwargs) > 0:
  198. raise InvalidParamException(
  199. "Unrecognised parameters for search: %s. Supported "
  200. "attributes: %s" % (kwargs, 'label'))
  201. log.debug("Searching for AWS Volume Service %s", label)
  202. return self.svc.find(filter_name='tag:Name', filter_value=label)
  203. @implement(event_pattern="provider.storage.volumes.list",
  204. priority=BaseVolumeService.STANDARD_EVENT_PRIORITY)
  205. def _list(self, limit=None, marker=None):
  206. return self.svc.list(limit=limit, marker=marker)
  207. @implement(event_pattern="provider.storage.volumes.create",
  208. priority=BaseVolumeService.STANDARD_EVENT_PRIORITY)
  209. def _create(self, label, size, zone, snapshot=None, description=None):
  210. zone_id = zone.id if isinstance(zone, PlacementZone) else zone
  211. snapshot_id = snapshot.id if isinstance(
  212. snapshot, AWSSnapshot) and snapshot else snapshot
  213. cb_vol = self.svc.create('create_volume', Size=size,
  214. AvailabilityZone=zone_id,
  215. SnapshotId=snapshot_id)
  216. # Wait until ready to tag instance
  217. cb_vol.wait_till_ready()
  218. cb_vol.label = label
  219. if description:
  220. cb_vol.description = description
  221. return cb_vol
  222. @implement(event_pattern="provider.storage.volumes.delete",
  223. priority=BaseVolumeService.STANDARD_EVENT_PRIORITY)
  224. def _delete(self, volume_id):
  225. if isinstance(volume_id, AWSVolume):
  226. aws_vol = volume_id._volume
  227. else:
  228. aws_vol = self.svc.get_raw(volume_id)
  229. aws_vol.delete()
  230. class AWSSnapshotService(BaseSnapshotService):
  231. def __init__(self, provider):
  232. super(AWSSnapshotService, self).__init__(provider)
  233. self.svc = BotoEC2Service(provider=self.provider,
  234. cb_resource=AWSSnapshot,
  235. boto_collection_name='snapshots')
  236. @implement(event_pattern="provider.storage.snapshots.get",
  237. priority=BaseSnapshotService.STANDARD_EVENT_PRIORITY)
  238. def _get(self, snapshot_id):
  239. return self.svc.get(snapshot_id)
  240. @implement(event_pattern="provider.storage.snapshots.find",
  241. priority=BaseSnapshotService.STANDARD_EVENT_PRIORITY)
  242. def _find(self, **kwargs):
  243. # Filter by description or label
  244. label = kwargs.get('label', None)
  245. obj_list = []
  246. if label:
  247. log.debug("Searching for AWS Snapshot with label %s", label)
  248. obj_list.extend(self.svc.find(filter_name='tag:Name',
  249. filter_value=label,
  250. OwnerIds=['self']))
  251. else:
  252. obj_list = list(self)
  253. filters = ['label']
  254. return cb_helpers.generic_find(filters, kwargs, obj_list)
  255. @implement(event_pattern="provider.storage.snapshots.list",
  256. priority=BaseSnapshotService.STANDARD_EVENT_PRIORITY)
  257. def _list(self, limit=None, marker=None):
  258. return self.svc.list(limit=limit, marker=marker,
  259. OwnerIds=['self'])
  260. @implement(event_pattern="provider.storage.snapshots.create",
  261. priority=BaseSnapshotService.STANDARD_EVENT_PRIORITY)
  262. def _create(self, label, volume, description=None):
  263. volume_id = volume.id if isinstance(volume, AWSVolume) else volume
  264. cb_snap = self.svc.create('create_snapshot', VolumeId=volume_id)
  265. # Wait until ready to tag instance
  266. cb_snap.wait_till_ready()
  267. cb_snap.label = label
  268. if cb_snap.description:
  269. cb_snap.description = description
  270. return cb_snap
  271. @implement(event_pattern="provider.storage.snapshots.delete",
  272. priority=BaseSnapshotService.STANDARD_EVENT_PRIORITY)
  273. def _delete(self, snapshot_id):
  274. if isinstance(snapshot_id, AWSSnapshot):
  275. aws_snap = snapshot_id._snapshot
  276. aws_snap = self.svc.get_raw(snapshot_id)
  277. aws_snap.delete()
  278. class AWSBucketService(BaseBucketService):
  279. def __init__(self, provider):
  280. super(AWSBucketService, self).__init__(provider)
  281. self.svc = BotoS3Service(provider=self.provider,
  282. cb_resource=AWSBucket,
  283. boto_collection_name='buckets')
  284. @implement(event_pattern="provider.storage.buckets.get",
  285. priority=BaseBucketService.STANDARD_EVENT_PRIORITY)
  286. def _get(self, bucket_id):
  287. """
  288. Returns a bucket given its ID. Returns ``None`` if the bucket
  289. does not exist.
  290. """
  291. try:
  292. # Make a call to make sure the bucket exists. There's an edge case
  293. # where a 403 response can occur when the bucket exists but the
  294. # user simply does not have permissions to access it. See below.
  295. self.provider.s3_conn.meta.client.head_bucket(Bucket=bucket_id)
  296. return AWSBucket(self.provider,
  297. self.provider.s3_conn.Bucket(bucket_id))
  298. except ClientError as e:
  299. # If 403, it means the bucket exists, but the user does not have
  300. # permissions to access the bucket. However, limited operations
  301. # may be permitted (with a session token for example), so return a
  302. # Bucket instance to allow further operations.
  303. # http://stackoverflow.com/questions/32331456/using-boto-upload-file-to-s3-
  304. # sub-folder-when-i-have-no-permissions-on-listing-fo
  305. if e.response['Error']['Code'] == "403":
  306. log.warning("AWS Bucket %s already exists but user doesn't "
  307. "have enough permissions to access the bucket",
  308. bucket_id)
  309. return AWSBucket(self.provider,
  310. self.provider.s3_conn.Bucket(bucket_id))
  311. # For all other responses, it's assumed that the bucket does not exist.
  312. return None
  313. @implement(event_pattern="provider.storage.buckets.list",
  314. priority=BaseBucketService.STANDARD_EVENT_PRIORITY)
  315. def _list(self, limit, marker):
  316. return self.svc.list(limit=limit, marker=marker)
  317. @implement(event_pattern="provider.storage.buckets.create",
  318. priority=BaseBucketService.STANDARD_EVENT_PRIORITY)
  319. def _create(self, name, location):
  320. AWSBucket.assert_valid_resource_name(name)
  321. location = location or self.provider.region_name
  322. # Due to an API issue in S3, specifying us-east-1 as a
  323. # LocationConstraint results in an InvalidLocationConstraint.
  324. # Therefore, it must be special-cased and omitted altogether.
  325. # See: https://github.com/boto/boto3/issues/125
  326. # In addition, us-east-1 also behaves differently when it comes
  327. # to raising duplicate resource exceptions, so perform a manual
  328. # check
  329. if location == 'us-east-1':
  330. try:
  331. # check whether bucket already exists
  332. self.provider.s3_conn.meta.client.head_bucket(Bucket=name)
  333. except ClientError as e:
  334. if e.response['Error']['Code'] == "404":
  335. # bucket doesn't exist, go ahead and create it
  336. return self.svc.create('create_bucket', Bucket=name)
  337. raise DuplicateResourceException(
  338. 'Bucket already exists with name {0}'.format(name))
  339. else:
  340. try:
  341. return self.svc.create('create_bucket', Bucket=name,
  342. CreateBucketConfiguration={
  343. 'LocationConstraint': location
  344. })
  345. except ClientError as e:
  346. if e.response['Error']['Code'] == "BucketAlreadyOwnedByYou":
  347. raise DuplicateResourceException(
  348. 'Bucket already exists with name {0}'.format(name))
  349. else:
  350. raise
  351. @implement(event_pattern="provider.storage.buckets.delete",
  352. priority=BaseBucketService.STANDARD_EVENT_PRIORITY)
  353. def _delete(self, bucket_id):
  354. bucket = self._get(bucket_id)
  355. if bucket:
  356. bucket._bucket.delete()
  357. class AWSBucketObjectService(BaseBucketObjectService):
  358. def __init__(self, provider):
  359. super(AWSBucketObjectService, self).__init__(provider)
  360. def get(self, bucket, object_id):
  361. try:
  362. # pylint:disable=protected-access
  363. obj = bucket._bucket.Object(object_id)
  364. # load() throws an error if object does not exist
  365. obj.load()
  366. return AWSBucketObject(self.provider, obj)
  367. except ClientError:
  368. return None
  369. def list(self, bucket, limit=None, marker=None, prefix=None):
  370. if prefix:
  371. # pylint:disable=protected-access
  372. boto_objs = bucket._bucket.objects.filter(Prefix=prefix)
  373. else:
  374. # pylint:disable=protected-access
  375. boto_objs = bucket._bucket.objects.all()
  376. objects = [AWSBucketObject(self.provider, obj) for obj in boto_objs]
  377. return ClientPagedResultList(self.provider, objects,
  378. limit=limit, marker=marker)
  379. def find(self, bucket, **kwargs):
  380. obj_list = [AWSBucketObject(self.provider, o)
  381. for o in bucket._bucket.objects.all()]
  382. filters = ['name']
  383. matches = cb_helpers.generic_find(filters, kwargs, obj_list)
  384. return ClientPagedResultList(self.provider, list(matches),
  385. limit=None, marker=None)
  386. def create(self, bucket, name):
  387. # pylint:disable=protected-access
  388. obj = bucket._bucket.Object(name)
  389. return AWSBucketObject(self.provider, obj)
  390. class AWSComputeService(BaseComputeService):
  391. def __init__(self, provider):
  392. super(AWSComputeService, self).__init__(provider)
  393. self._vm_type_svc = AWSVMTypeService(self.provider)
  394. self._instance_svc = AWSInstanceService(self.provider)
  395. self._region_svc = AWSRegionService(self.provider)
  396. self._images_svc = AWSImageService(self.provider)
  397. @property
  398. def images(self):
  399. return self._images_svc
  400. @property
  401. def vm_types(self):
  402. return self._vm_type_svc
  403. @property
  404. def instances(self):
  405. return self._instance_svc
  406. @property
  407. def regions(self):
  408. return self._region_svc
  409. class AWSImageService(BaseImageService):
  410. def __init__(self, provider):
  411. super(AWSImageService, self).__init__(provider)
  412. self.svc = BotoEC2Service(provider=self.provider,
  413. cb_resource=AWSMachineImage,
  414. boto_collection_name='images')
  415. def get(self, image_id):
  416. log.debug("Getting AWS Image Service with the id: %s", image_id)
  417. return self.svc.get(image_id)
  418. def find(self, **kwargs):
  419. # Filter by name or label
  420. label = kwargs.pop('label', None)
  421. # Popped here, not used in the generic find
  422. owner = kwargs.pop('owners', None)
  423. # All kwargs should have been popped at this time.
  424. if len(kwargs) > 0:
  425. raise InvalidParamException(
  426. "Unrecognised parameters for search: %s. Supported "
  427. "attributes: %s" % (kwargs, 'label'))
  428. extra_args = {}
  429. if owner:
  430. extra_args.update(Owners=owner)
  431. # The original list is made by combining both searches by "tag:Name"
  432. # and "AMI name" to allow for searches of public images
  433. if label:
  434. log.debug("Searching for AWS Image Service %s", label)
  435. obj_list = []
  436. obj_list.extend(self.svc.find(filter_name='name',
  437. filter_value=label, **extra_args))
  438. obj_list.extend(self.svc.find(filter_name='tag:Name',
  439. filter_value=label, **extra_args))
  440. return obj_list
  441. else:
  442. return []
  443. def list(self, filter_by_owner=True, limit=None, marker=None):
  444. return self.svc.list(Owners=['self'] if filter_by_owner else
  445. ['amazon', 'self'],
  446. limit=limit, marker=marker)
  447. class AWSInstanceService(BaseInstanceService):
  448. def __init__(self, provider):
  449. super(AWSInstanceService, self).__init__(provider)
  450. self.svc = BotoEC2Service(provider=self.provider,
  451. cb_resource=AWSInstance,
  452. boto_collection_name='instances')
  453. def _resolve_launch_options(self, subnet=None, zone_id=None,
  454. vm_firewalls=None):
  455. """
  456. Work out interdependent launch options.
  457. Some launch options are required and interdependent so make sure
  458. they conform to the interface contract.
  459. :type subnet: ``Subnet``
  460. :param subnet: Subnet object within which to launch.
  461. :type zone_id: ``str``
  462. :param zone_id: ID of the zone where the launch should happen.
  463. :type vm_firewalls: ``list`` of ``id``
  464. :param vm_firewalls: List of firewall IDs.
  465. :rtype: triplet of ``str``
  466. :return: Subnet ID, zone ID and VM firewall IDs for launch.
  467. :raise ValueError: In case a conflicting combination is found.
  468. """
  469. if subnet:
  470. # subnet's zone takes precedence
  471. zone_id = subnet.zone.id
  472. if isinstance(vm_firewalls, list) and isinstance(
  473. vm_firewalls[0], VMFirewall):
  474. vm_firewall_ids = [fw.id for fw in vm_firewalls]
  475. else:
  476. vm_firewall_ids = vm_firewalls
  477. return subnet.id, zone_id, vm_firewall_ids
  478. def _process_block_device_mappings(self, launch_config):
  479. """
  480. Processes block device mapping information
  481. and returns a Boto BlockDeviceMapping object. If new volumes
  482. are requested (source is None and destination is VOLUME), they will be
  483. created and the relevant volume ids included in the mapping.
  484. """
  485. bdml = []
  486. # Assign letters from f onwards
  487. # http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/device_naming.html
  488. next_letter = iter(list(string.ascii_lowercase[6:]))
  489. # assign ephemeral devices from 0 onwards
  490. ephemeral_counter = 0
  491. for device in launch_config.block_devices:
  492. bdm = {}
  493. if device.is_volume:
  494. # Generate the device path
  495. bdm['DeviceName'] = \
  496. '/dev/sd' + ('a1' if device.is_root else next(next_letter))
  497. ebs_def = {}
  498. if isinstance(device.source, Snapshot):
  499. ebs_def['SnapshotId'] = device.source.id
  500. elif isinstance(device.source, Volume):
  501. # TODO: We could create a snapshot from the volume
  502. # and use that instead.
  503. # Not supported
  504. pass
  505. elif isinstance(device.source, MachineImage):
  506. # Not supported
  507. pass
  508. else:
  509. # source is None, but destination is volume, therefore
  510. # create a blank volume. This requires a size though.
  511. if not device.size:
  512. raise InvalidConfigurationException(
  513. "The source is none and the destination is a"
  514. " volume. Therefore, you must specify a size.")
  515. ebs_def['DeleteOnTermination'] = device.delete_on_terminate \
  516. or True
  517. if device.size:
  518. ebs_def['VolumeSize'] = device.size
  519. if ebs_def:
  520. bdm['Ebs'] = ebs_def
  521. else: # device is ephemeral
  522. bdm['VirtualName'] = 'ephemeral%s' % ephemeral_counter
  523. # Append the config
  524. bdml.append(bdm)
  525. return bdml
  526. def create_launch_config(self):
  527. return AWSLaunchConfig(self.provider)
  528. @implement(event_pattern="provider.compute.instances.create",
  529. priority=BaseInstanceService.STANDARD_EVENT_PRIORITY)
  530. def _create(self, label, image, vm_type, subnet, zone,
  531. key_pair=None, vm_firewalls=None, user_data=None,
  532. launch_config=None, **kwargs):
  533. image_id = image.id if isinstance(image, MachineImage) else image
  534. vm_size = vm_type.id if \
  535. isinstance(vm_type, VMType) else vm_type
  536. subnet = (self.provider.networking.subnets.get(subnet)
  537. if isinstance(subnet, str) else subnet)
  538. zone_id = zone.id if isinstance(zone, PlacementZone) else zone
  539. key_pair_name = key_pair.name if isinstance(
  540. key_pair,
  541. KeyPair) else key_pair
  542. if launch_config:
  543. bdm = self._process_block_device_mappings(launch_config)
  544. else:
  545. bdm = None
  546. subnet_id, zone_id, vm_firewall_ids = \
  547. self._resolve_launch_options(subnet, zone_id, vm_firewalls)
  548. placement = {'AvailabilityZone': zone_id} if zone_id else None
  549. inst = self.svc.create('create_instances',
  550. ImageId=image_id,
  551. MinCount=1,
  552. MaxCount=1,
  553. KeyName=key_pair_name,
  554. SecurityGroupIds=vm_firewall_ids or None,
  555. UserData=str(user_data) or None,
  556. InstanceType=vm_size,
  557. Placement=placement,
  558. BlockDeviceMappings=bdm,
  559. SubnetId=subnet_id
  560. )
  561. if inst and len(inst) == 1:
  562. # Wait until the resource exists
  563. # pylint:disable=protected-access
  564. inst[0]._wait_till_exists()
  565. # Tag the instance w/ the name
  566. inst[0].label = label
  567. return inst[0]
  568. raise ValueError(
  569. 'Expected a single object response, got a list: %s' % inst)
  570. @implement(event_pattern="provider.compute.instances.get",
  571. priority=BaseInstanceService.STANDARD_EVENT_PRIORITY)
  572. def _get(self, instance_id):
  573. return self.svc.get(instance_id)
  574. @implement(event_pattern="provider.compute.instances.find",
  575. priority=BaseInstanceService.STANDARD_EVENT_PRIORITY)
  576. def _find(self, **kwargs):
  577. label = kwargs.pop('label', None)
  578. # All kwargs should have been popped at this time.
  579. if len(kwargs) > 0:
  580. raise InvalidParamException(
  581. "Unrecognised parameters for search: %s. Supported "
  582. "attributes: %s" % (kwargs, 'label'))
  583. return self.svc.find(filter_name='tag:Name', filter_value=label)
  584. @implement(event_pattern="provider.compute.instances.list",
  585. priority=BaseInstanceService.STANDARD_EVENT_PRIORITY)
  586. def _list(self, limit=None, marker=None):
  587. return self.svc.list(limit=limit, marker=marker)
  588. @implement(event_pattern="provider.compute.instances.delete",
  589. priority=BaseInstanceService.STANDARD_EVENT_PRIORITY)
  590. def _delete(self, instance_id):
  591. aws_ins = self.svc.get_raw(instance_id)
  592. aws_ins.terminate()
  593. class AWSVMTypeService(BaseVMTypeService):
  594. def __init__(self, provider):
  595. super(AWSVMTypeService, self).__init__(provider)
  596. @property
  597. @cachetools.cached(cachetools.TTLCache(maxsize=1, ttl=24*3600))
  598. def instance_data(self):
  599. """
  600. Fetch info about the available instances.
  601. To update this information, update the file pointed to by the
  602. ``provider.AWS_INSTANCE_DATA_DEFAULT_URL`` above. The content for this
  603. file should be obtained from this repo:
  604. https://github.com/powdahound/ec2instances.info (in particular, this
  605. file: https://raw.githubusercontent.com/powdahound/ec2instances.info/
  606. master/www/instances.json).
  607. """
  608. r = requests.get(self.provider.config.get(
  609. "aws_instance_info_url",
  610. self.provider.AWS_INSTANCE_DATA_DEFAULT_URL))
  611. # Some instances are only available in certain regions. Use pricing
  612. # info to determine and filter out instance types that are not
  613. # available in the current region
  614. vm_types_list = r.json()
  615. return [vm_type for vm_type in vm_types_list
  616. if vm_type.get('pricing', {}).get(self.provider.region_name)]
  617. @implement(event_pattern="provider.compute.vm_types.list",
  618. priority=BaseVMTypeService.STANDARD_EVENT_PRIORITY)
  619. def _list(self, limit=None, marker=None):
  620. vm_types = [AWSVMType(self.provider, vm_type)
  621. for vm_type in self.instance_data]
  622. return ClientPagedResultList(self.provider, vm_types,
  623. limit=limit, marker=marker)
  624. class AWSRegionService(BaseRegionService):
  625. def __init__(self, provider):
  626. super(AWSRegionService, self).__init__(provider)
  627. @implement(event_pattern="provider.compute.regions.get",
  628. priority=BaseRegionService.STANDARD_EVENT_PRIORITY)
  629. def _get(self, region_id):
  630. log.debug("Getting AWS Region Service with the id: %s",
  631. region_id)
  632. region = [r for r in self if r.id == region_id]
  633. if region:
  634. return region[0]
  635. else:
  636. return None
  637. @implement(event_pattern="provider.compute.regions.list",
  638. priority=BaseRegionService.STANDARD_EVENT_PRIORITY)
  639. def _list(self, limit=None, marker=None):
  640. regions = [
  641. AWSRegion(self.provider, region) for region in
  642. self.provider.ec2_conn.meta.client.describe_regions()
  643. .get('Regions', [])]
  644. return ClientPagedResultList(self.provider, regions,
  645. limit=limit, marker=marker)
  646. @property
  647. def current(self):
  648. return self.get(self._provider.region_name)
  649. class AWSNetworkingService(BaseNetworkingService):
  650. def __init__(self, provider):
  651. super(AWSNetworkingService, self).__init__(provider)
  652. self._network_service = AWSNetworkService(self.provider)
  653. self._subnet_service = AWSSubnetService(self.provider)
  654. self._router_service = AWSRouterService(self.provider)
  655. @property
  656. def networks(self):
  657. return self._network_service
  658. @property
  659. def subnets(self):
  660. return self._subnet_service
  661. @property
  662. def routers(self):
  663. return self._router_service
  664. class AWSNetworkService(BaseNetworkService):
  665. def __init__(self, provider):
  666. super(AWSNetworkService, self).__init__(provider)
  667. self.svc = BotoEC2Service(provider=self.provider,
  668. cb_resource=AWSNetwork,
  669. boto_collection_name='vpcs')
  670. @implement(event_pattern="provider.networking.networks.get",
  671. priority=BaseNetworkService.STANDARD_EVENT_PRIORITY)
  672. def _get(self, network_id):
  673. return self.svc.get(network_id)
  674. @implement(event_pattern="provider.networking.networks.list",
  675. priority=BaseNetworkService.STANDARD_EVENT_PRIORITY)
  676. def _list(self, limit=None, marker=None):
  677. return self.svc.list(limit=limit, marker=marker)
  678. @implement(event_pattern="provider.networking.networks.find",
  679. priority=BaseNetworkService.STANDARD_EVENT_PRIORITY)
  680. def _find(self, **kwargs):
  681. label = kwargs.pop('label', None)
  682. # All kwargs should have been popped at this time.
  683. if len(kwargs) > 0:
  684. raise InvalidParamException(
  685. "Unrecognised parameters for search: %s. Supported "
  686. "attributes: %s" % (kwargs, 'label'))
  687. log.debug("Searching for AWS Network Service %s", label)
  688. return self.svc.find(filter_name='tag:Name', filter_value=label)
  689. @implement(event_pattern="provider.networking.networks.create",
  690. priority=BaseNetworkService.STANDARD_EVENT_PRIORITY)
  691. def _create(self, label, cidr_block):
  692. AWSNetwork.assert_valid_resource_label(label)
  693. cb_net = self.svc.create('create_vpc', CidrBlock=cidr_block)
  694. # Wait until ready to tag instance
  695. cb_net.wait_till_ready()
  696. if label:
  697. cb_net.label = label
  698. return cb_net
  699. @implement(event_pattern="provider.networking.networks.delete",
  700. priority=BaseNetworkService.STANDARD_EVENT_PRIORITY)
  701. def _delete(self, network_id):
  702. net = self.get(network_id)
  703. net._vpc.delete()
  704. def get_or_create_default(self):
  705. # # Look for provided default network
  706. # for net in self.provider.networking.networks:
  707. # if net._vpc.is_default:
  708. # return net
  709. # No provider-default, try CB-default instead
  710. default_nets = self.provider.networking.networks.find(
  711. label=AWSNetwork.CB_DEFAULT_NETWORK_LABEL)
  712. if default_nets:
  713. return default_nets[0]
  714. else:
  715. log.info("Creating a CloudBridge-default network labeled %s",
  716. AWSNetwork.CB_DEFAULT_NETWORK_LABEL)
  717. return self.provider.networking.networks.create(
  718. label=AWSNetwork.CB_DEFAULT_NETWORK_LABEL,
  719. cidr_block=AWSNetwork.CB_DEFAULT_IPV4RANGE)
  720. class AWSSubnetService(BaseSubnetService):
  721. def __init__(self, provider):
  722. super(AWSSubnetService, self).__init__(provider)
  723. self.svc = BotoEC2Service(provider=self.provider,
  724. cb_resource=AWSSubnet,
  725. boto_collection_name='subnets')
  726. @implement(event_pattern="provider.networking.subnets.get",
  727. priority=BaseSubnetService.STANDARD_EVENT_PRIORITY)
  728. def _get(self, subnet_id):
  729. return self.svc.get(subnet_id)
  730. @implement(event_pattern="provider.networking.subnets.list",
  731. priority=BaseSubnetService.STANDARD_EVENT_PRIORITY)
  732. def _list(self, network=None, limit=None, marker=None):
  733. network_id = network.id if isinstance(network, AWSNetwork) else network
  734. if network_id:
  735. return self.svc.find(
  736. filter_name='vpc-id', filter_value=network_id,
  737. limit=limit, marker=marker)
  738. else:
  739. return self.svc.list(limit=limit, marker=marker)
  740. @implement(event_pattern="provider.networking.subnets.find",
  741. priority=BaseSubnetService.STANDARD_EVENT_PRIORITY)
  742. def _find(self, network=None, **kwargs):
  743. label = kwargs.pop('label', None)
  744. # All kwargs should have been popped at this time.
  745. if len(kwargs) > 0:
  746. raise InvalidParamException(
  747. "Unrecognised parameters for search: %s. Supported "
  748. "attributes: %s" % (kwargs, 'label'))
  749. log.debug("Searching for AWS Subnet Service %s", label)
  750. return self.svc.find(filter_name='tag:Name', filter_value=label)
  751. @implement(event_pattern="provider.networking.subnets.create",
  752. priority=BaseSubnetService.STANDARD_EVENT_PRIORITY)
  753. def _create(self, label, network, cidr_block, zone):
  754. zone_name = zone.name if isinstance(
  755. zone, AWSPlacementZone) else zone
  756. network_id = network.id if isinstance(network, AWSNetwork) else network
  757. subnet = self.svc.create('create_subnet',
  758. VpcId=network_id,
  759. CidrBlock=cidr_block,
  760. AvailabilityZone=zone_name)
  761. if label:
  762. subnet.label = label
  763. return subnet
  764. @implement(event_pattern="provider.networking.subnets.delete",
  765. priority=BaseSubnetService.STANDARD_EVENT_PRIORITY)
  766. def _delete(self, subnet_id):
  767. aws_sn = self.svc.get_raw(subnet_id)
  768. aws_sn.delete()
  769. def get_or_create_default(self, zone):
  770. zone_name = zone.name if isinstance(zone, AWSPlacementZone) else zone
  771. # # Look for provider default subnet in current zone
  772. # if zone_name:
  773. # snl = self.svc.find('availabilityZone', zone_name)
  774. #
  775. # else:
  776. # snl = self.svc.list()
  777. # # Find first available default subnet by sorted order
  778. # # of availability zone. Prefer zone us-east-1a over 1e,
  779. # # because newer zones tend to have less compatibility
  780. # # with different instance types (e.g. c5.large not available
  781. # # on us-east-1e as of 14 Dec. 2017).
  782. # # pylint:disable=protected-access
  783. # snl.sort(key=lambda sn: sn._subnet.availability_zone)
  784. #
  785. # for sn in snl:
  786. # # pylint:disable=protected-access
  787. # if sn._subnet.default_for_az:
  788. # return sn
  789. # If no provider-default subnet has been found, look for
  790. # cloudbridge-default by label. We suffix labels by availability zone,
  791. # thus we add the wildcard for the regular expression to find the
  792. # subnet
  793. snl = self.find(label=AWSSubnet.CB_DEFAULT_SUBNET_LABEL + "*")
  794. if snl:
  795. snl.sort(key=lambda sn: sn._subnet.availability_zone)
  796. if not zone_name:
  797. return snl[0]
  798. for subnet in snl:
  799. if subnet.zone.name == zone_name:
  800. return subnet
  801. # No default Subnet exists, try to create a CloudBridge-specific
  802. # subnet. This involves creating the network, subnets, internet
  803. # gateway, and connecting it all together so that the network has
  804. # Internet connectivity.
  805. # Check if a default net already exists and get it or create on
  806. default_net = self.provider.networking.networks.get_or_create_default()
  807. # Get/create an internet gateway for the default network and a
  808. # corresponding router if it does not already exist.
  809. # NOTE: Comment this out because the docs instruct users to setup
  810. # network connectivity manually. There's a bit of discrepancy here
  811. # though because the provider-default network will have Internet
  812. # connectivity (unlike the CloudBridge-default network with this
  813. # being commented) and is hence left in the codebase.
  814. # default_gtw = default_net.gateways.get_or_create_inet_gateway()
  815. # router_label = "{0}-router".format(
  816. # AWSNetwork.CB_DEFAULT_NETWORK_LABEL)
  817. # default_routers = self.provider.networking.routers.find(
  818. # label=router_label)
  819. # if len(default_routers) == 0:
  820. # default_router = self.provider.networking.routers.create(
  821. # router_label, default_net)
  822. # default_router.attach_gateway(default_gtw)
  823. # else:
  824. # default_router = default_routers[0]
  825. # Create a subnet in each of the region's zones
  826. region = self.provider.compute.regions.get(self.provider.region_name)
  827. default_sn = None
  828. # Determine how many subnets we'll need for the default network and the
  829. # number of available zones. We need to derive a non-overlapping
  830. # network size for each subnet within the parent net so figure those
  831. # subnets here. `<net>.subnets` method will do this but we need to give
  832. # it a prefix. Determining that prefix depends on the size of the
  833. # network and should be incorporate the number of zones. So iterate
  834. # over potential number of subnets until enough can be created to
  835. # accommodate the number of available zones. That is where the fixed
  836. # number comes from in the for loop as that many iterations will yield
  837. # more potential subnets than any region has zones.
  838. ip_net = ipaddress.ip_network(AWSNetwork.CB_DEFAULT_IPV4RANGE)
  839. for x in range(5):
  840. if len(region.zones) <= len(list(ip_net.subnets(
  841. prefixlen_diff=x))):
  842. prefixlen_diff = x
  843. break
  844. subnets = list(ip_net.subnets(prefixlen_diff=prefixlen_diff))
  845. for i, z in reversed(list(enumerate(region.zones))):
  846. sn_label = "{0}-{1}".format(AWSSubnet.CB_DEFAULT_SUBNET_LABEL,
  847. z.id[-1])
  848. log.info("Creating a default CloudBridge subnet %s: %s" %
  849. (sn_label, str(subnets[i])))
  850. sn = self.create(sn_label, default_net, str(subnets[i]), z)
  851. # Create a route table entry between the SN and the inet gateway
  852. # See note above about why this is commented
  853. # default_router.attach_subnet(sn)
  854. if zone and zone_name == z.name:
  855. default_sn = sn
  856. # No specific zone was supplied; return the last created subnet
  857. # The list was originally reversed to have the last subnet be in zone a
  858. if not default_sn:
  859. default_sn = sn
  860. return default_sn
  861. class AWSRouterService(BaseRouterService):
  862. """For AWS, a CloudBridge router corresponds to an AWS Route Table."""
  863. def __init__(self, provider):
  864. super(AWSRouterService, self).__init__(provider)
  865. self.svc = BotoEC2Service(provider=self.provider,
  866. cb_resource=AWSRouter,
  867. boto_collection_name='route_tables')
  868. @implement(event_pattern="provider.networking.routers.get",
  869. priority=BaseRouterService.STANDARD_EVENT_PRIORITY)
  870. def _get(self, router_id):
  871. return self.svc.get(router_id)
  872. @implement(event_pattern="provider.networking.routers.find",
  873. priority=BaseRouterService.STANDARD_EVENT_PRIORITY)
  874. def _find(self, **kwargs):
  875. label = kwargs.pop('label', None)
  876. # All kwargs should have been popped at this time.
  877. if len(kwargs) > 0:
  878. raise InvalidParamException(
  879. "Unrecognised parameters for search: %s. Supported "
  880. "attributes: %s" % (kwargs, 'label'))
  881. log.debug("Searching for AWS Router Service %s", label)
  882. return self.svc.find(filter_name='tag:Name', filter_value=label)
  883. @implement(event_pattern="provider.networking.routers.list",
  884. priority=BaseRouterService.STANDARD_EVENT_PRIORITY)
  885. def _list(self, limit=None, marker=None):
  886. return self.svc.list(limit=limit, marker=marker)
  887. @implement(event_pattern="provider.networking.routers.create",
  888. priority=BaseRouterService.STANDARD_EVENT_PRIORITY)
  889. def _create(self, label, network):
  890. network_id = network.id if isinstance(network, AWSNetwork) else network
  891. cb_router = self.svc.create('create_route_table', VpcId=network_id)
  892. if label:
  893. cb_router.label = label
  894. return cb_router
  895. @implement(event_pattern="provider.networking.routers.delete",
  896. priority=BaseRouterService.STANDARD_EVENT_PRIORITY)
  897. def _delete(self, router_id):
  898. aws_router = self.svc.get_raw(router_id)
  899. aws_router.delete()