resources.py 20 KB

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