services.py 35 KB

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