services.py 38 KB

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