services.py 44 KB

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