resources.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683
  1. """
  2. DataTypes used by this provider
  3. """
  4. from cloudbridge.cloud.base.resources import BaseInstanceType
  5. from cloudbridge.cloud.base.resources import BaseKeyPair
  6. from cloudbridge.cloud.base.resources import BaseMachineImage
  7. from cloudbridge.cloud.base.resources import BasePlacementZone
  8. from cloudbridge.cloud.base.resources import BaseRegion
  9. from cloudbridge.cloud.base.resources import BaseSecurityGroup
  10. from cloudbridge.cloud.base.resources import BaseSecurityGroupRule
  11. # Older versions of Python do not have a built-in set data-structure.
  12. try:
  13. set
  14. except NameError:
  15. from sets import Set as set
  16. import hashlib
  17. import inspect
  18. import json
  19. import re
  20. class GCEKeyPair(BaseKeyPair):
  21. def __init__(self, provider, kp_id, kp_name, kp_material=None):
  22. super(GCEKeyPair, self).__init__(provider, None)
  23. self._kp_id = kp_id
  24. self._kp_name = kp_name
  25. self._kp_material = kp_material
  26. @property
  27. def id(self):
  28. return self._kp_id
  29. @property
  30. def name(self):
  31. # use e-mail as keyname if possible, or ID if not
  32. return self._kp_name or self.id
  33. def delete(self):
  34. svc = self._provider.security.key_pairs
  35. def _delete_key(gce_kp_generator):
  36. kp_list = []
  37. for gce_kp in gce_kp_generator:
  38. if svc.gce_kp_to_id(gce_kp) == self.id:
  39. continue
  40. else:
  41. kp_list.append(gce_kp)
  42. return kp_list
  43. svc.gce_metadata_save_op(_delete_key)
  44. @property
  45. def material(self):
  46. return self._kp_material
  47. @material.setter
  48. def material(self, value):
  49. self._kp_material = value
  50. class GCEInstanceType(BaseInstanceType):
  51. def __init__(self, provider, instance_dict):
  52. super(GCEInstanceType, self).__init__(provider)
  53. self._inst_dict = instance_dict
  54. @property
  55. def id(self):
  56. return str(self._inst_dict.get('id'))
  57. @property
  58. def name(self):
  59. return self._inst_dict.get('name')
  60. @property
  61. def family(self):
  62. return self._inst_dict.get('kind')
  63. @property
  64. def vcpus(self):
  65. return self._inst_dict.get('guestCpus')
  66. @property
  67. def ram(self):
  68. return self._inst_dict.get('memoryMb')
  69. @property
  70. def size_root_disk(self):
  71. return 0
  72. @property
  73. def size_ephemeral_disks(self):
  74. return int(self._inst_dict.get('maximumPersistentDisksSizeGb'))
  75. @property
  76. def num_ephemeral_disks(self):
  77. return self._inst_dict.get('maximumPersistentDisks')
  78. @property
  79. def extra_data(self):
  80. return {key: val for key, val in self._inst_dict.items()
  81. if key not in ['id', 'name', 'kind', 'guestCpus', 'memoryMb',
  82. 'maximumPersistentDisksSizeGb',
  83. 'maximumPersistentDisks']}
  84. class GCEPlacementZone(BasePlacementZone):
  85. def __init__(self, provider, zone, region):
  86. super(GCEPlacementZone, self).__init__(provider)
  87. if isinstance(zone, GCEPlacementZone):
  88. # pylint:disable=protected-access
  89. self._gce_zone = zone._gce_zone
  90. self._gce_region = zone._gce_region
  91. else:
  92. self._gce_zone = zone
  93. self._gce_region = region
  94. @property
  95. def id(self):
  96. """
  97. Get the zone id
  98. :rtype: ``str``
  99. :return: ID for this zone as returned by the cloud middleware.
  100. """
  101. return self._gce_zone
  102. @property
  103. def name(self):
  104. """
  105. Get the zone name.
  106. :rtype: ``str``
  107. :return: Name for this zone as returned by the cloud middleware.
  108. """
  109. return self._gce_zone
  110. @property
  111. def region_name(self):
  112. """
  113. Get the region that this zone belongs to.
  114. :rtype: ``str``
  115. :return: Name of this zone's region as returned by the cloud middleware
  116. """
  117. return self._gce_region
  118. class GCERegion(BaseRegion):
  119. def __init__(self, provider, gce_region):
  120. super(GCERegion, self).__init__(provider)
  121. self._gce_region = gce_region
  122. @property
  123. def id(self):
  124. # In GCE API, region has an 'id' property, whose values are '1220',
  125. # '1100', '1000', '1230', etc. Here we use 'name' property (such
  126. # as 'asia-east1', 'europe-west1', 'us-central1', 'us-east1') as
  127. # 'id' to represent the region for the consistency with AWS
  128. # implementation and ease of use.
  129. return self._gce_region['name']
  130. @property
  131. def name(self):
  132. return self._gce_region['name']
  133. @property
  134. def zones(self):
  135. """
  136. Accesss information about placement zones within this region.
  137. """
  138. zones_response = self._provider.gce_compute.zones().list(
  139. project=self._provider.project_name).execute()
  140. zones = [zone for zone in zones_response['items']
  141. if zone['region'] == self._gce_region['selfLink']]
  142. return [GCEPlacementZone(self._provider, zone['name'], self.name)
  143. for zone in zones]
  144. class GCEFirewallsDelegate(object):
  145. DEFAULT_NETWORK = 'default'
  146. _NETWORK_URL_PREFIX = 'global/networks/'
  147. def __init__(self, provider):
  148. self._provider = provider
  149. self._list_response = None
  150. @staticmethod
  151. def tag_network_id(tag, network):
  152. """
  153. Generate an ID for a (tag, network) pair.
  154. """
  155. md5 = hashlib.md5()
  156. md5.update("{0}-{1}".format(tag, network).encode('ascii'))
  157. return md5.hexdigest()
  158. @staticmethod
  159. def network(firewall):
  160. """
  161. Extract the network name of a firewall.
  162. """
  163. if 'network' not in firewall:
  164. return GCEFirewallsDelegate.DEFAULT_NETWORK
  165. match = re.search(
  166. GCEFirewallsDelegate._NETWORK_URL_PREFIX + '([^/]*)$',
  167. firewall['network'])
  168. if match and len(match.groups()) == 1:
  169. return match.group(1)
  170. return None
  171. @property
  172. def provider(self):
  173. return self._provider
  174. @property
  175. def tag_networks(self):
  176. """
  177. List all (tag, network) pairs that are used in at least one firewall.
  178. """
  179. out = set()
  180. for firewall in self.iter_firewalls():
  181. network = GCEFirewallsDelegate.network(firewall)
  182. if network is not None:
  183. out.add((firewall['targetTags'][0], network))
  184. return out
  185. def get_tag_network_from_id(self, tag_network_id):
  186. """
  187. Map an ID back to the (tag, network) pair.
  188. """
  189. for tag, network in self.tag_networks:
  190. current_id = GCEFirewallsDelegate.tag_network_id(tag, network)
  191. if current_id == tag_network_id:
  192. return (tag, network)
  193. return (None, None)
  194. def delete_tag_network_with_id(self, tag_network_id):
  195. """
  196. Delete all firewalls in a given network with a specific target tag.
  197. """
  198. tag, network = self.get_tag_network_from_id(tag_network_id)
  199. if tag is None:
  200. return
  201. for firewall in self.iter_firewalls(tag, network):
  202. self._delete_firewall(firewall)
  203. self._update_list_response()
  204. def add_firewall(self, tag, ip_protocol, port, source_range, source_tag,
  205. description, network):
  206. """
  207. Create a new firewall.
  208. """
  209. if self.find_firewall(tag, ip_protocol, port, source_range,
  210. source_tag, network) is not None:
  211. return True
  212. # Do not let the user accidentally open traffic from the world by not
  213. # explicitly specifying the source.
  214. if source_tag is None and source_range is None:
  215. return False
  216. firewall_number = 1
  217. suffixes = []
  218. for firewall in self.iter_firewalls(tag, network):
  219. suffix = firewall['name'].split('-')[-1]
  220. if suffix.isdigit():
  221. suffixes.append(int(suffix))
  222. for suffix in sorted(suffixes):
  223. if firewall_number == suffix:
  224. firewall_number += 1
  225. firewall = {
  226. 'name': '%s-%s-rule-%d' % (network, tag, firewall_number),
  227. 'network': GCEFirewallsDelegate._NETWORK_URL_PREFIX + network,
  228. 'allowed': [{'IPProtocol': str(ip_protocol)}],
  229. 'targetTags': [tag]}
  230. if description is not None:
  231. firewall['description'] = description
  232. if port is not None:
  233. firewall['allowed'][0]['ports'] = [port]
  234. if source_range is not None:
  235. firewall['sourceRanges'] = [source_range]
  236. if source_tag is not None:
  237. firewall['sourceTags'] = [source_tag]
  238. project_name = self._provider.project_name
  239. try:
  240. response = (self._provider.gce_compute
  241. .firewalls()
  242. .insert(project=project_name,
  243. body=firewall)
  244. .execute())
  245. self._provider.wait_for_global_operation(response)
  246. # TODO: process the response and handle errors.
  247. return True
  248. except:
  249. return False
  250. finally:
  251. self._update_list_response()
  252. def find_firewall(self, tag, ip_protocol, port, source_range, source_tag,
  253. network):
  254. """
  255. Find a firewall with give parameters.
  256. """
  257. if source_range is None and source_tag is None:
  258. source_range = '0.0.0.0/0'
  259. for firewall in self.iter_firewalls(tag, network):
  260. if firewall['allowed'][0]['IPProtocol'] != ip_protocol:
  261. continue
  262. if not self._check_list_in_dict(firewall['allowed'][0], 'ports',
  263. port):
  264. continue
  265. if not self._check_list_in_dict(firewall, 'sourceRanges',
  266. source_range):
  267. continue
  268. if not self._check_list_in_dict(firewall, 'sourceTags', source_tag):
  269. continue
  270. return firewall['id']
  271. return None
  272. def get_firewall_info(self, firewall_id):
  273. """
  274. Extract firewall properties to into a dictionary for easy of use.
  275. """
  276. info = {}
  277. for firewall in self.iter_firewalls():
  278. if firewall['id'] != firewall_id:
  279. continue
  280. if ('sourceRanges' in firewall and
  281. len(firewall['sourceRanges']) == 1):
  282. info['source_range'] = firewall['sourceRanges'][0]
  283. if 'sourceTags' in firewall and len(firewall['sourceTags']) == 1:
  284. info['source_tag'] = firewall['sourceTags'][0]
  285. if 'targetTags' in firewall and len(firewall['targetTags']) == 1:
  286. info['target_tag'] = firewall['targetTags'][0]
  287. if 'IPProtocol' in firewall['allowed'][0]:
  288. info['ip_protocol'] = firewall['allowed'][0]['IPProtocol']
  289. if ('ports' in firewall['allowed'][0] and
  290. len(firewall['allowed'][0]['ports']) == 1):
  291. info['port'] = firewall['allowed'][0]['ports'][0]
  292. info['network'] = GCEFirewallsDelegate.network(firewall)
  293. return info
  294. return info
  295. def delete_firewall_id(self, firewall_id):
  296. """
  297. Delete a firewall with a given ID.
  298. """
  299. for firewall in self.iter_firewalls():
  300. if firewall['id'] == firewall_id:
  301. self._delete_firewall(firewall)
  302. self._update_list_response()
  303. def iter_firewalls(self, tag=None, network=None):
  304. """
  305. Iterate through all firewalls. Can optionally iterate through firewalls
  306. with a given tag and/or in a network.
  307. """
  308. if self._list_response is None:
  309. self._update_list_response()
  310. if 'items' not in self._list_response:
  311. return
  312. for firewall in self._list_response['items']:
  313. if 'targetTags' not in firewall or len(firewall['targetTags']) != 1:
  314. continue
  315. if 'allowed' not in firewall or len(firewall['allowed']) != 1:
  316. continue
  317. if tag is not None and firewall['targetTags'][0] != tag:
  318. continue
  319. if network is None:
  320. yield firewall
  321. continue
  322. firewall_network = GCEFirewallsDelegate.network(firewall)
  323. if firewall_network == network:
  324. yield firewall
  325. def _delete_firewall(self, firewall):
  326. """
  327. Delete a given firewall.
  328. """
  329. project_name = self._provider.project_name
  330. try:
  331. response = (self._provider.gce_compute
  332. .firewalls()
  333. .delete(project=project_name,
  334. firewall=firewall['name'])
  335. .execute())
  336. self._provider.wait_for_global_operation(response)
  337. # TODO: process the response and handle errors.
  338. return True
  339. except:
  340. return False
  341. def _update_list_response(self):
  342. """
  343. Sync the local cache of all firewalls with the server.
  344. """
  345. self._list_response = (
  346. self._provider.gce_compute
  347. .firewalls()
  348. .list(project=self._provider.project_name)
  349. .execute())
  350. def _check_list_in_dict(self, dictionary, field_name, value):
  351. """
  352. Verify that a given field in a dictionary is a singlton list [value].
  353. """
  354. if field_name not in dictionary:
  355. return value is None
  356. if (value is None or
  357. len(dictionary[field_name]) != 1 or
  358. dictionary[field_name][0] != value):
  359. return False
  360. return True
  361. class GCESecurityGroup(BaseSecurityGroup):
  362. def __init__(self, delegate, tag,
  363. network=GCEFirewallsDelegate.DEFAULT_NETWORK,
  364. description=None):
  365. super(GCESecurityGroup, self).__init__(delegate.provider, tag)
  366. self._description = description
  367. self._delegate = delegate
  368. self._network = network
  369. if self._network is None:
  370. self._network = GCEFirewallsDelegate.DEFAULT_NETWORK
  371. @property
  372. def id(self):
  373. """
  374. Return the ID of this security group which is determined based on the
  375. network and the target tag corresponding to this security group.
  376. """
  377. return GCEFirewallsDelegate.tag_network_id(self._security_group,
  378. self._network)
  379. @property
  380. def name(self):
  381. """
  382. Return the name of the security group which is the same as the
  383. corresponding tag name.
  384. """
  385. return self._security_group
  386. @property
  387. def description(self):
  388. """
  389. The description of the security group is even explicitly given when the
  390. group is created or is determined from a firewall in the group.
  391. If the firewalls are created using this API, they all have the same
  392. description.
  393. """
  394. if self._description is not None:
  395. return self._description
  396. for firewall in self._delegate.iter_firewalls(self._security_group,
  397. self._network):
  398. if 'description' in firewall:
  399. return firewall['description']
  400. return None
  401. @property
  402. def rules(self):
  403. out = []
  404. for firewall in self._delegate.iter_firewalls(self._security_group,
  405. self._network):
  406. out.append(GCESecurityGroupRule(self._delegate, firewall['id']))
  407. return out
  408. @staticmethod
  409. def to_port_range(from_port, to_port):
  410. if from_port is not None and to_port is not None:
  411. return '%d-%d' % (from_port, to_port)
  412. elif from_port is not None:
  413. return from_port
  414. else:
  415. return to_port
  416. def add_rule(self, ip_protocol, from_port=None, to_port=None,
  417. cidr_ip=None, src_group=None):
  418. port = GCESecurityGroup.to_port_range(from_port, to_port)
  419. src_tag = src_group.name if src_group is not None else None
  420. self._delegate.add_firewall(self._security_group, ip_protocol, port,
  421. cidr_ip, src_tag, self.description,
  422. self._network)
  423. return self.get_rule(ip_protocol, from_port, to_port, cidr_ip,
  424. src_group)
  425. def get_rule(self, ip_protocol=None, from_port=None, to_port=None,
  426. cidr_ip=None, src_group=None):
  427. port = GCESecurityGroup.to_port_range(from_port, to_port)
  428. src_tag = src_group.name if src_group is not None else None
  429. firewall_id = self._delegate.find_firewall(
  430. self._security_group, ip_protocol, port, cidr_ip, src_tag,
  431. self._network)
  432. if firewall_id is None:
  433. return None
  434. return GCESecurityGroupRule(self._delegate, firewall_id)
  435. def to_json(self):
  436. attr = inspect.getmembers(self, lambda a: not(inspect.isroutine(a)))
  437. js = {k: v for(k, v) in attr if not k.startswith('_')}
  438. json_rules = [r.to_json() for r in self.rules]
  439. js['rules'] = [json.loads(r) for r in json_rules]
  440. return json.dumps(js, sort_keys=True)
  441. def delete(self):
  442. for rule in self.rules:
  443. rule.delete()
  444. class GCESecurityGroupRule(BaseSecurityGroupRule):
  445. def __init__(self, delegate, firewall_id):
  446. super(GCESecurityGroupRule, self).__init__(
  447. delegate.provider, firewall_id, None)
  448. self._delegate = delegate
  449. @property
  450. def parent(self):
  451. """
  452. Return the security group to which this rule belongs.
  453. """
  454. info = self._delegate.get_firewall_info(self._rule)
  455. if info is None or 'target_tag' not in info or info['network'] is None:
  456. return None
  457. return GCESecurityGroup(self._delegate, info['target_tag'],
  458. info['network'])
  459. @property
  460. def id(self):
  461. return self._rule
  462. @property
  463. def ip_protocol(self):
  464. info = self._delegate.get_firewall_info(self._rule)
  465. if info is None or 'ip_protocol' not in info:
  466. return None
  467. return info['ip_protocol']
  468. @property
  469. def from_port(self):
  470. info = self._delegate.get_firewall_info(self._rule)
  471. if info is None or 'port' not in info:
  472. return 0
  473. port = info['port']
  474. if port.isdigit():
  475. return int(port)
  476. parts = port.split('-')
  477. if len(parts) > 2 or len(parts) < 1:
  478. return 0
  479. if parts[0].isdigit():
  480. return int(parts[0])
  481. return 0
  482. @property
  483. def to_port(self):
  484. info = self._delegate.get_firewall_info(self._rule)
  485. if info is None or 'port' not in info:
  486. return 0
  487. port = info['port']
  488. if port.isdigit():
  489. return int(port)
  490. parts = port.split('-')
  491. if len(parts) > 2 or len(parts) < 1:
  492. return 0
  493. if parts[-1].isdigit():
  494. return int(parts[-1])
  495. return 0
  496. @property
  497. def cidr_ip(self):
  498. """
  499. Return the IP of machines from which this rule allows traffic.
  500. """
  501. info = self._delegate.get_firewall_info(self._rule)
  502. if info is None or 'source_range' not in info:
  503. return None
  504. return info['source_range']
  505. @property
  506. def group(self):
  507. """
  508. Return the security group from which this rule allows traffic.
  509. """
  510. info = self._delegate.get_firewall_info(self._rule)
  511. if info is None or 'source_tag' not in info or info['network'] is None:
  512. return None
  513. return GCESecurityGroup(self._delegate, info['source_tag'],
  514. info['network'])
  515. def to_json(self):
  516. attr = inspect.getmembers(self, lambda a: not(inspect.isroutine(a)))
  517. js = {k: v for(k, v) in attr if not k.startswith('_')}
  518. js['group'] = self.group.id if self.group else ''
  519. js['parent'] = self.parent.id if self.parent else ''
  520. return json.dumps(js, sort_keys=True)
  521. def delete(self):
  522. self._delegate.delete_firewall_id(self._rule)
  523. class GCEMachineImage(BaseMachineImage):
  524. IMAGE_STATE_MAP = {
  525. 'PENDING': MachineImageState.PENDING,
  526. 'READY': MachineImageState.AVAILABLE,
  527. 'FAILED': MachineImageState.ERROR
  528. }
  529. def __init__(self, provider, image):
  530. super(GCEMachineImage, self).__init__(provider)
  531. if isinstance(image, GCEMachineImage):
  532. # pylint:disable=protected-access
  533. self._gce_image = image._gce_image
  534. else:
  535. self._gce_image = image
  536. @property
  537. def id(self):
  538. """
  539. Get the image identifier.
  540. :rtype: ``str``
  541. :return: ID for this instance as returned by the cloud middleware.
  542. """
  543. return self._gce_image['name']
  544. @property
  545. def name(self):
  546. """
  547. Get the image name.
  548. :rtype: ``str``
  549. :return: Name for this image as returned by the cloud middleware.
  550. """
  551. return self._gce_image['name']
  552. @property
  553. def description(self):
  554. """
  555. Get the image description.
  556. :rtype: ``str``
  557. :return: Description for this image as returned by the cloud middleware
  558. """
  559. return self._gce_image.get('description', '')
  560. def delete(self):
  561. """
  562. Delete this image
  563. """
  564. request = self._provider.gce_compute.images().delete(
  565. project=self._provider.project_name, image=self.name)
  566. request.execute()
  567. @property
  568. def state(self):
  569. return GCEMachineImage.IMAGE_STATE_MAP.get(
  570. self._gce_image['status'], MachineImageState.UNKNOWN)
  571. def refresh(self):
  572. """
  573. Refreshes the state of this instance by re-querying the cloud provider
  574. for its latest state.
  575. """
  576. resource_link = self._gce_image['selfLink']
  577. project_pattern = 'projects/(.*?)/'
  578. match = re.search(project_pattern, resource_link)
  579. if match:
  580. project = match.group(1)
  581. else:
  582. cb.log.warning("Project name is not found.")
  583. return
  584. try:
  585. response = self._provider.gce_compute \
  586. .images() \
  587. .get(project=project,
  588. image=self.name) \
  589. .execute()
  590. if response:
  591. # pylint:disable=protected-access
  592. self._gce_image = response
  593. except googleapiclient.errors.HttpError as http_error:
  594. # image no longer exists
  595. cb.log.warning(
  596. "googleapiclient.errors.HttpError: {0}".format(http_error))
  597. self._gce_image['status'] = "unknown"