resources.py 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268
  1. """
  2. DataTypes used by this provider
  3. """
  4. from cloudbridge.cloud.base.resources import BaseFloatingIP
  5. from cloudbridge.cloud.base.resources import BaseInstance
  6. from cloudbridge.cloud.base.resources import BaseInstanceType
  7. from cloudbridge.cloud.base.resources import BaseKeyPair
  8. from cloudbridge.cloud.base.resources import BaseMachineImage
  9. from cloudbridge.cloud.base.resources import BaseNetwork
  10. from cloudbridge.cloud.base.resources import BasePlacementZone
  11. from cloudbridge.cloud.base.resources import BaseRegion
  12. from cloudbridge.cloud.base.resources import BaseSecurityGroup
  13. from cloudbridge.cloud.base.resources import BaseSecurityGroupRule
  14. from cloudbridge.cloud.interfaces.resources import InstanceState
  15. from cloudbridge.cloud.interfaces.resources import MachineImageState
  16. import cloudbridge as cb
  17. # Older versions of Python do not have a built-in set data-structure.
  18. try:
  19. set
  20. except NameError:
  21. from sets import Set as set
  22. import hashlib
  23. import inspect
  24. import json
  25. import re
  26. import uuid
  27. class GCEKeyPair(BaseKeyPair):
  28. def __init__(self, provider, kp_id, kp_name, kp_material=None):
  29. super(GCEKeyPair, self).__init__(provider, None)
  30. self._kp_id = kp_id
  31. self._kp_name = kp_name
  32. self._kp_material = kp_material
  33. @property
  34. def id(self):
  35. return self._kp_id
  36. @property
  37. def name(self):
  38. # use e-mail as keyname if possible, or ID if not
  39. return self._kp_name or self.id
  40. def delete(self):
  41. svc = self._provider.security.key_pairs
  42. def _delete_key(gce_kp_generator):
  43. kp_list = []
  44. for gce_kp in gce_kp_generator:
  45. if svc.gce_kp_to_id(gce_kp) == self.id:
  46. continue
  47. else:
  48. kp_list.append(gce_kp)
  49. return kp_list
  50. svc.gce_metadata_save_op(_delete_key)
  51. @property
  52. def material(self):
  53. return self._kp_material
  54. @material.setter
  55. def material(self, value):
  56. self._kp_material = value
  57. class GCEInstanceType(BaseInstanceType):
  58. def __init__(self, provider, instance_dict):
  59. super(GCEInstanceType, self).__init__(provider)
  60. self._inst_dict = instance_dict
  61. @property
  62. def resource_url(self):
  63. return self._inst_dict.get('selfLink')
  64. @property
  65. def id(self):
  66. return str(self._inst_dict.get('id'))
  67. @property
  68. def name(self):
  69. return self._inst_dict.get('name')
  70. @property
  71. def family(self):
  72. return self._inst_dict.get('kind')
  73. @property
  74. def vcpus(self):
  75. return self._inst_dict.get('guestCpus')
  76. @property
  77. def ram(self):
  78. return self._inst_dict.get('memoryMb')
  79. @property
  80. def size_root_disk(self):
  81. return 0
  82. @property
  83. def size_ephemeral_disks(self):
  84. return int(self._inst_dict.get('maximumPersistentDisksSizeGb'))
  85. @property
  86. def num_ephemeral_disks(self):
  87. return self._inst_dict.get('maximumPersistentDisks')
  88. @property
  89. def extra_data(self):
  90. return {key: val for key, val in self._inst_dict.items()
  91. if key not in ['id', 'name', 'kind', 'guestCpus', 'memoryMb',
  92. 'maximumPersistentDisksSizeGb',
  93. 'maximumPersistentDisks']}
  94. class GCEPlacementZone(BasePlacementZone):
  95. def __init__(self, provider, zone, region):
  96. super(GCEPlacementZone, self).__init__(provider)
  97. if isinstance(zone, GCEPlacementZone):
  98. # pylint:disable=protected-access
  99. self._gce_zone = zone._gce_zone
  100. self._gce_region = zone._gce_region
  101. else:
  102. self._gce_zone = zone
  103. self._gce_region = region
  104. @property
  105. def id(self):
  106. """
  107. Get the zone id
  108. :rtype: ``str``
  109. :return: ID for this zone as returned by the cloud middleware.
  110. """
  111. return self._gce_zone
  112. @property
  113. def name(self):
  114. """
  115. Get the zone name.
  116. :rtype: ``str``
  117. :return: Name for this zone as returned by the cloud middleware.
  118. """
  119. return self._gce_zone
  120. @property
  121. def region_name(self):
  122. """
  123. Get the region that this zone belongs to.
  124. :rtype: ``str``
  125. :return: Name of this zone's region as returned by the cloud middleware
  126. """
  127. return self._gce_region
  128. class GCERegion(BaseRegion):
  129. def __init__(self, provider, gce_region):
  130. super(GCERegion, self).__init__(provider)
  131. self._gce_region = gce_region
  132. @property
  133. def id(self):
  134. # In GCE API, region has an 'id' property, whose values are '1220',
  135. # '1100', '1000', '1230', etc. Here we use 'name' property (such
  136. # as 'asia-east1', 'europe-west1', 'us-central1', 'us-east1') as
  137. # 'id' to represent the region for the consistency with AWS
  138. # implementation and ease of use.
  139. return self._gce_region['name']
  140. @property
  141. def name(self):
  142. return self._gce_region['name']
  143. @property
  144. def zones(self):
  145. """
  146. Accesss information about placement zones within this region.
  147. """
  148. zones_response = self._provider.gce_compute.zones().list(
  149. project=self._provider.project_name).execute()
  150. zones = [zone for zone in zones_response['items']
  151. if zone['region'] == self._gce_region['selfLink']]
  152. return [GCEPlacementZone(self._provider, zone['name'], self.name)
  153. for zone in zones]
  154. class GCEFirewallsDelegate(object):
  155. DEFAULT_NETWORK = 'default'
  156. _NETWORK_URL_PREFIX = 'global/networks/'
  157. def __init__(self, provider):
  158. self._provider = provider
  159. self._list_response = None
  160. @staticmethod
  161. def tag_network_id(tag, network_name):
  162. """
  163. Generate an ID for a (tag, network name) pair.
  164. """
  165. md5 = hashlib.md5()
  166. md5.update("{0}-{1}".format(tag, network_name).encode('ascii'))
  167. return md5.hexdigest()
  168. @property
  169. def provider(self):
  170. return self._provider
  171. @property
  172. def tag_networks(self):
  173. """
  174. List all (tag, network name) pairs that are in at least one firewall.
  175. """
  176. out = set()
  177. for firewall in self.iter_firewalls():
  178. network_name = self.network_name(firewall)
  179. if network_name is not None:
  180. out.add((firewall['targetTags'][0], network_name))
  181. return out
  182. def network_name(self, firewall):
  183. """
  184. Extract the network name of a firewall.
  185. """
  186. if 'network' not in firewall:
  187. return GCEFirewallsDelegate.DEFAULT_NETWORK
  188. url = self._provider.parse_url(firewall['network'])
  189. return url.parameters['network']
  190. def get_tag_network_from_id(self, tag_network_id):
  191. """
  192. Map an ID back to the (tag, network name) pair.
  193. """
  194. for tag, network_name in self.tag_networks:
  195. current_id = GCEFirewallsDelegate.tag_network_id(tag, network_name)
  196. if current_id == tag_network_id:
  197. return (tag, network_name)
  198. return (None, None)
  199. def delete_tag_network_with_id(self, tag_network_id):
  200. """
  201. Delete all firewalls in a given network with a specific target tag.
  202. """
  203. tag, network_name = self.get_tag_network_from_id(tag_network_id)
  204. if tag is None:
  205. return
  206. for firewall in self.iter_firewalls(tag, network_name):
  207. self._delete_firewall(firewall)
  208. self._update_list_response()
  209. def add_firewall(self, tag, ip_protocol, port, source_range, source_tag,
  210. description, network_name):
  211. """
  212. Create a new firewall.
  213. """
  214. if self.find_firewall(tag, ip_protocol, port, source_range,
  215. source_tag, network_name) is not None:
  216. return True
  217. # Do not let the user accidentally open traffic from the world by not
  218. # explicitly specifying the source.
  219. if source_tag is None and source_range is None:
  220. return False
  221. firewall = {
  222. 'name': 'firewall-{0}'.format(uuid.uuid4()),
  223. 'network': GCEFirewallsDelegate._NETWORK_URL_PREFIX + network_name,
  224. 'allowed': [{'IPProtocol': str(ip_protocol)}],
  225. 'targetTags': [tag]}
  226. if description is not None:
  227. firewall['description'] = description
  228. if port is not None:
  229. firewall['allowed'][0]['ports'] = [port]
  230. if source_range is not None:
  231. firewall['sourceRanges'] = [source_range]
  232. if source_tag is not None:
  233. firewall['sourceTags'] = [source_tag]
  234. project_name = self._provider.project_name
  235. try:
  236. response = (self._provider.gce_compute
  237. .firewalls()
  238. .insert(project=project_name,
  239. body=firewall)
  240. .execute())
  241. self._provider.wait_for_operation(response)
  242. # TODO: process the response and handle errors.
  243. except:
  244. return False
  245. finally:
  246. self._update_list_response()
  247. return True
  248. def find_firewall(self, tag, ip_protocol, port, source_range, source_tag,
  249. network_name):
  250. """
  251. Find a firewall with give parameters.
  252. """
  253. if source_range is None and source_tag is None:
  254. source_range = '0.0.0.0/0'
  255. for firewall in self.iter_firewalls(tag, network_name):
  256. if firewall['allowed'][0]['IPProtocol'] != ip_protocol:
  257. continue
  258. if not self._check_list_in_dict(firewall['allowed'][0], 'ports',
  259. port):
  260. continue
  261. if not self._check_list_in_dict(firewall, 'sourceRanges',
  262. source_range):
  263. continue
  264. if not self._check_list_in_dict(firewall, 'sourceTags', source_tag):
  265. continue
  266. return firewall['id']
  267. return None
  268. def get_firewall_info(self, firewall_id):
  269. """
  270. Extract firewall properties to into a dictionary for easy of use.
  271. """
  272. info = {}
  273. for firewall in self.iter_firewalls():
  274. if firewall['id'] != firewall_id:
  275. continue
  276. if ('sourceRanges' in firewall and
  277. len(firewall['sourceRanges']) == 1):
  278. info['source_range'] = firewall['sourceRanges'][0]
  279. if 'sourceTags' in firewall and len(firewall['sourceTags']) == 1:
  280. info['source_tag'] = firewall['sourceTags'][0]
  281. if 'targetTags' in firewall and len(firewall['targetTags']) == 1:
  282. info['target_tag'] = firewall['targetTags'][0]
  283. if 'IPProtocol' in firewall['allowed'][0]:
  284. info['ip_protocol'] = firewall['allowed'][0]['IPProtocol']
  285. if ('ports' in firewall['allowed'][0] and
  286. len(firewall['allowed'][0]['ports']) == 1):
  287. info['port'] = firewall['allowed'][0]['ports'][0]
  288. info['network_name'] = self.network_name(firewall)
  289. return info
  290. return info
  291. def delete_firewall_id(self, firewall_id):
  292. """
  293. Delete a firewall with a given ID.
  294. """
  295. for firewall in self.iter_firewalls():
  296. if firewall['id'] == firewall_id:
  297. self._delete_firewall(firewall)
  298. self._update_list_response()
  299. def iter_firewalls(self, tag=None, network_name=None):
  300. """
  301. Iterate through all firewalls. Can optionally iterate through firewalls
  302. with a given tag and/or in a network.
  303. """
  304. if self._list_response is None:
  305. self._update_list_response()
  306. if 'items' not in self._list_response:
  307. return
  308. for firewall in self._list_response['items']:
  309. if 'targetTags' not in firewall or len(firewall['targetTags']) != 1:
  310. continue
  311. if 'allowed' not in firewall or len(firewall['allowed']) != 1:
  312. continue
  313. if tag is not None and firewall['targetTags'][0] != tag:
  314. continue
  315. if network_name is None:
  316. yield firewall
  317. continue
  318. firewall_network_name = self.network_name(firewall)
  319. if firewall_network_name == network_name:
  320. yield firewall
  321. def _delete_firewall(self, firewall):
  322. """
  323. Delete a given firewall.
  324. """
  325. project_name = self._provider.project_name
  326. try:
  327. response = (self._provider.gce_compute
  328. .firewalls()
  329. .delete(project=project_name,
  330. firewall=firewall['name'])
  331. .execute())
  332. self._provider.wait_for_operation(response)
  333. except:
  334. return False
  335. # TODO: process the response and handle errors.
  336. return True
  337. def _update_list_response(self):
  338. """
  339. Sync the local cache of all firewalls with the server.
  340. """
  341. self._list_response = (
  342. self._provider.gce_compute
  343. .firewalls()
  344. .list(project=self._provider.project_name)
  345. .execute())
  346. def _check_list_in_dict(self, dictionary, field_name, value):
  347. """
  348. Verify that a given field in a dictionary is a singlton list [value].
  349. """
  350. if field_name not in dictionary:
  351. return value is None
  352. if (value is None or
  353. len(dictionary[field_name]) != 1 or
  354. dictionary[field_name][0] != value):
  355. return False
  356. return True
  357. class GCESecurityGroup(BaseSecurityGroup):
  358. def __init__(self, delegate, tag, network=None, description=None):
  359. super(GCESecurityGroup, self).__init__(delegate.provider, tag)
  360. self._description = description
  361. self._delegate = delegate
  362. if network is None:
  363. self._network = delegate.provider.network.get_by_name(
  364. GCEFirewallsDelegate.DEFAULT_NETWORK)
  365. else:
  366. self._network = network
  367. @property
  368. def id(self):
  369. """
  370. Return the ID of this security group which is determined based on the
  371. network and the target tag corresponding to this security group.
  372. """
  373. return GCEFirewallsDelegate.tag_network_id(self._security_group,
  374. self._network.name)
  375. @property
  376. def name(self):
  377. """
  378. Return the name of the security group which is the same as the
  379. corresponding tag name.
  380. """
  381. return self._security_group
  382. @property
  383. def description(self):
  384. """
  385. The description of the security group is even explicitly given when the
  386. group is created or is determined from a firewall in the group.
  387. If the firewalls are created using this API, they all have the same
  388. description.
  389. """
  390. if self._description is not None:
  391. return self._description
  392. for firewall in self._delegate.iter_firewalls(self._security_group,
  393. self._network.name):
  394. if 'description' in firewall:
  395. return firewall['description']
  396. return None
  397. @property
  398. def network_id(self):
  399. return self._network.id
  400. @property
  401. def rules(self):
  402. out = []
  403. for firewall in self._delegate.iter_firewalls(self._security_group,
  404. self._network.name):
  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.name)
  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.name)
  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:
  455. return None
  456. if 'target_tag' not in info or info['network_name'] is None:
  457. return None
  458. network = delegate.network.get_by_name(info['network_name'])
  459. if network is None:
  460. return None
  461. return GCESecurityGroup(self._delegate, info['target_tag'], network)
  462. @property
  463. def id(self):
  464. return self._rule
  465. @property
  466. def ip_protocol(self):
  467. info = self._delegate.get_firewall_info(self._rule)
  468. if info is None or 'ip_protocol' not in info:
  469. return None
  470. return info['ip_protocol']
  471. @property
  472. def from_port(self):
  473. info = self._delegate.get_firewall_info(self._rule)
  474. if info is None or 'port' not in info:
  475. return 0
  476. port = info['port']
  477. if port.isdigit():
  478. return int(port)
  479. parts = port.split('-')
  480. if len(parts) > 2 or len(parts) < 1:
  481. return 0
  482. if parts[0].isdigit():
  483. return int(parts[0])
  484. return 0
  485. @property
  486. def to_port(self):
  487. info = self._delegate.get_firewall_info(self._rule)
  488. if info is None or 'port' not in info:
  489. return 0
  490. port = info['port']
  491. if port.isdigit():
  492. return int(port)
  493. parts = port.split('-')
  494. if len(parts) > 2 or len(parts) < 1:
  495. return 0
  496. if parts[-1].isdigit():
  497. return int(parts[-1])
  498. return 0
  499. @property
  500. def cidr_ip(self):
  501. """
  502. Return the IP of machines from which this rule allows traffic.
  503. """
  504. info = self._delegate.get_firewall_info(self._rule)
  505. if info is None or 'source_range' not in info:
  506. return None
  507. return info['source_range']
  508. @property
  509. def group(self):
  510. """
  511. Return the security group from which this rule allows traffic.
  512. """
  513. info = self._delegate.get_firewall_info(self._rule)
  514. if info is None:
  515. return None
  516. if 'source_tag' not in info or info['network_name'] is None:
  517. return None
  518. network = self._delegate.provider.network.get_by_name(
  519. info['network_name'])
  520. if network is None:
  521. return None
  522. return GCESecurityGroup(self._delegate, info['source_tag'], network)
  523. def to_json(self):
  524. attr = inspect.getmembers(self, lambda a: not(inspect.isroutine(a)))
  525. js = {k: v for(k, v) in attr if not k.startswith('_')}
  526. js['group'] = self.group.id if self.group else ''
  527. js['parent'] = self.parent.id if self.parent else ''
  528. return json.dumps(js, sort_keys=True)
  529. def delete(self):
  530. self._delegate.delete_firewall_id(self._rule)
  531. class GCEMachineImage(BaseMachineImage):
  532. IMAGE_STATE_MAP = {
  533. 'PENDING': MachineImageState.PENDING,
  534. 'READY': MachineImageState.AVAILABLE,
  535. 'FAILED': MachineImageState.ERROR
  536. }
  537. def __init__(self, provider, image):
  538. super(GCEMachineImage, self).__init__(provider)
  539. if isinstance(image, GCEMachineImage):
  540. # pylint:disable=protected-access
  541. self._gce_image = image._gce_image
  542. else:
  543. self._gce_image = image
  544. @property
  545. def resource_url(self):
  546. return self._gce_image.get('selfLink')
  547. @property
  548. def id(self):
  549. """
  550. Get the image identifier.
  551. :rtype: ``str``
  552. :return: ID for this instance as returned by the cloud middleware.
  553. """
  554. return self._gce_image['name']
  555. @property
  556. def name(self):
  557. """
  558. Get the image name.
  559. :rtype: ``str``
  560. :return: Name for this image as returned by the cloud middleware.
  561. """
  562. return self._gce_image['name']
  563. @property
  564. def description(self):
  565. """
  566. Get the image description.
  567. :rtype: ``str``
  568. :return: Description for this image as returned by the cloud middleware
  569. """
  570. return self._gce_image.get('description', '')
  571. def delete(self):
  572. """
  573. Delete this image
  574. """
  575. request = self._provider.gce_compute.images().delete(
  576. project=self._provider.project_name, image=self.name)
  577. request.execute()
  578. @property
  579. def state(self):
  580. return GCEMachineImage.IMAGE_STATE_MAP.get(
  581. self._gce_image['status'], MachineImageState.UNKNOWN)
  582. def refresh(self):
  583. """
  584. Refreshes the state of this instance by re-querying the cloud provider
  585. for its latest state.
  586. """
  587. resource_link = self._gce_image['selfLink']
  588. project_pattern = 'projects/(.*?)/'
  589. match = re.search(project_pattern, resource_link)
  590. if match:
  591. project = match.group(1)
  592. else:
  593. cb.log.warning("Project name is not found.")
  594. return
  595. try:
  596. response = self._provider.gce_compute \
  597. .images() \
  598. .get(project=project,
  599. image=self.name) \
  600. .execute()
  601. if response:
  602. # pylint:disable=protected-access
  603. self._gce_image = response
  604. except googleapiclient.errors.HttpError as http_error:
  605. # image no longer exists
  606. cb.log.warning(
  607. "googleapiclient.errors.HttpError: {0}".format(http_error))
  608. self._gce_image['status'] = "unknown"
  609. class GCEInstance(BaseInstance):
  610. # https://cloud.google.com/compute/docs/reference/latest/instances
  611. # The status of the instance. One of the following values:
  612. # PROVISIONING, STAGING, RUNNING, STOPPING, SUSPENDING, SUSPENDED,
  613. # and TERMINATED.
  614. INSTANCE_STATE_MAP = {
  615. 'PROVISIONING': InstanceState.PENDING,
  616. 'STAGING': InstanceState.PENDING,
  617. 'RUNNING': InstanceState.RUNNING,
  618. 'STOPPING': InstanceState.CONFIGURING,
  619. 'TERMINATED': InstanceState.STOPPED,
  620. 'SUSPENDING': InstanceState.CONFIGURING,
  621. 'SUSPENDED': InstanceState.STOPPED
  622. }
  623. def __init__(self, provider, gce_instance):
  624. super(GCEInstance, self).__init__(provider)
  625. self._gce_instance = gce_instance
  626. @property
  627. def resource_url(self):
  628. return self._gce_instance.get('selfLink')
  629. @property
  630. def id(self):
  631. """
  632. Get the instance identifier.
  633. A GCE instance is uniquely identified by its selfLink, which is used
  634. as its id.
  635. """
  636. return self._gce_instance.get('selfLink')
  637. @property
  638. def name(self):
  639. """
  640. Get the instance name.
  641. """
  642. return self._gce_instance['name']
  643. @name.setter
  644. # pylint:disable=arguments-differ
  645. def name(self, value):
  646. """
  647. Set the instance name.
  648. """
  649. # In GCE, the name of the instance is provided by the client when
  650. # initially creating the resource. The name cannot be changed after
  651. # the instance is created.
  652. cb.log.warning("Setting instance name after it is created is not "
  653. "supported by this provider.")
  654. @property
  655. def public_ips(self):
  656. """
  657. Get all the public IP addresses for this instance.
  658. """
  659. ips = []
  660. network_interfaces = self._gce_instance.get('networkInterfaces')
  661. if network_interfaces is not None and len(network_interfaces) > 0:
  662. access_configs = network_interfaces[0].get('accessConfigs')
  663. if access_configs is not None and len(access_configs) > 0:
  664. # https://cloud.google.com/compute/docs/reference/beta/instances
  665. # An array of configurations for this interface. Currently, only
  666. # one access config, ONE_TO_ONE_NAT, is supported. If there are
  667. # no accessConfigs specified, then this instance will have no
  668. # external internet access.
  669. access_config = access_configs[0]
  670. if 'natIP' in access_config:
  671. ips.append(access_config['natIP'])
  672. for ip in self._provider.network.floating_ips():
  673. if ip.in_use():
  674. if ip.private_ip in self.private_ips:
  675. ips.append(ip.public_ip)
  676. return ips
  677. @property
  678. def private_ips(self):
  679. """
  680. Get all the private IP addresses for this instance.
  681. """
  682. network_interfaces = self._gce_instance.get('networkInterfaces')
  683. if network_interfaces is None or len(network_interfaces) == 0:
  684. return []
  685. if 'networkIP' in network_interfaces[0]:
  686. return [network_interfaces[0]['networkIP']]
  687. else:
  688. return []
  689. @property
  690. def instance_type_id(self):
  691. """
  692. Get the instance type name.
  693. """
  694. machine_type_uri = self._gce_instance.get('machineType')
  695. if machine_type_uri is None:
  696. return None
  697. instance_type = self._provider.get_gce_resource_data(machine_type_uri)
  698. return instance_type.get('name', None)
  699. @property
  700. def instance_type(self):
  701. """
  702. Get the instance type.
  703. """
  704. machine_type_uri = self._gce_instance.get('machineType')
  705. if machine_type_uri is None:
  706. return None
  707. instance_type = self._provider.get_gce_resource_data(machine_type_uri)
  708. return GCEInstanceType(self._provider, instance_type)
  709. def reboot(self):
  710. """
  711. Reboot this instance.
  712. """
  713. if self.state == InstanceState.STOPPED:
  714. self._provider.gce_compute \
  715. .instances() \
  716. .start(project=self._provider.project_name,
  717. zone=self._provider.default_zone,
  718. instance=self.name) \
  719. .execute()
  720. else:
  721. self._provider.gce_compute \
  722. .instances() \
  723. .reset(project=self._provider.project_name,
  724. zone=self._provider.default_zone,
  725. instance=self.name) \
  726. .execute()
  727. def terminate(self):
  728. """
  729. Permanently terminate this instance.
  730. """
  731. self._provider.gce_compute \
  732. .instances() \
  733. .delete(project=self._provider.project_name,
  734. zone=self._provider.default_zone,
  735. instance=self.name) \
  736. .execute()
  737. def stop(self):
  738. """
  739. Stop this instance.
  740. """
  741. self._provider.gce_compute \
  742. .instances() \
  743. .stop(project=self._provider.project_name,
  744. zone=self._provider.default_zone,
  745. instance=self.name) \
  746. .execute()
  747. @property
  748. def image_id(self):
  749. """
  750. Get the image ID for this insance.
  751. """
  752. raise NotImplementedError(
  753. 'To be implemented after GCEVolumeService.')
  754. return None
  755. @property
  756. def zone_id(self):
  757. """
  758. Get the placement zone id where this instance is running.
  759. """
  760. zone_uri = self._gce_instance.get('zone')
  761. if zone_uri is None:
  762. return None
  763. zone = self._provider.get_gce_resource_data(zone_uri)
  764. return zone.get('name', None)
  765. @property
  766. def security_groups(self):
  767. """
  768. Get the security groups associated with this instance.
  769. """
  770. network_url = self._gce_instance.get('networkInterfaces')[0].get(
  771. 'network')
  772. url = self._provider.parse_url(network_url)
  773. network_name = url.parameters['network']
  774. if 'items' not in self._gce_instance['tags']:
  775. return []
  776. tags = self._gce_instance['tags']['items']
  777. # Tags are mapped to non-empty security groups under the instance
  778. # network. Unmatched tags are ignored.
  779. sgs = (self._provider.security
  780. .security_groups.find_by_network_and_tags(
  781. network_name, tags))
  782. return sgs
  783. @property
  784. def security_group_ids(self):
  785. """
  786. Get the security groups IDs associated with this instance.
  787. """
  788. sg_ids = []
  789. for sg in self.security_groups:
  790. sg_ids.append(sg.id)
  791. return sg_ids
  792. @property
  793. def key_pair_name(self):
  794. """
  795. Get the name of the key pair associated with this instance.
  796. """
  797. return self._provider.security.key_pairs.name
  798. def create_image(self, name):
  799. """
  800. Create a new image based on this instance.
  801. """
  802. raise NotImplementedError(
  803. 'To be implemented after GCEVolumeService.')
  804. @property
  805. def _existing_target_instance(self):
  806. """
  807. Return the target instance corrsponding to this instance.
  808. If there is no target instance for this instance, return None.
  809. """
  810. self_url = self._provider.parse_url(self._gce_instance['selfLink'])
  811. try:
  812. response = (self._provider.gce_compute
  813. .targetInstances()
  814. .list(project=self._provider.project_name,
  815. zone=self_url.parameters['zone'])
  816. .execute())
  817. if 'items' not in response:
  818. return None
  819. for target_instance in response['items']:
  820. url = self._provider.parse_url(target_instance['instance'])
  821. if url.parameters['instance'] == self.name:
  822. return target_instance
  823. except Exception as e:
  824. cb.log.warning('Exception while listing target instances: %s', e)
  825. return None
  826. @property
  827. def _target_instance(self):
  828. """
  829. Return the target instance corresponding to this instance.
  830. If there is no target instance for this instance, create one.
  831. """
  832. existing_target_instance = self._existing_target_instance
  833. if existing_target_instance:
  834. return existing_target_instance
  835. # No targetInstance exists for this instance. Create one.
  836. self_url = self._provider.parse_url(self._gce_instance['selfLink'])
  837. body = {'name': 'target-instance-{0}'.format(uuid.uuid4()),
  838. 'instance': self._gce_instance['selfLink']}
  839. try:
  840. response = (self._provider.gce_compute
  841. .targetInstances()
  842. .insert(
  843. project=self._provider.project_name,
  844. zone=self_url.parameters['zone'],
  845. body=body)
  846. .execute())
  847. self._provider.wait_for_operation(
  848. response, zone=self_url.parameters['zone'])
  849. except Exception as e:
  850. cb.log.warning('Exception while inserting a target instance: %s', e)
  851. return None
  852. # The following method should find the target instance that we
  853. # successfully created above.
  854. return self._existing_target_instance
  855. def _redirect_existing_rule(self, ip, target_instance):
  856. """
  857. Redirect the forwarding rule of the given IP to the given Instance.
  858. """
  859. new_zone = (self._provider.parse_url(target_instance['zone'])
  860. .parameters['zone'])
  861. new_name = target_instance['name']
  862. new_url = target_instance['selfLink']
  863. try:
  864. response = (self._provider.gce_compute
  865. .forwardingRules()
  866. .list(project=self._provider.project_name,
  867. region=ip.region)
  868. .execute())
  869. if 'items' not in response:
  870. return False
  871. for rule in response['items']:
  872. if rule['IPAddress'] == ip.public_ip:
  873. parsed_target_url = self._provider.parse_url(rule['target'])
  874. old_zone = parsed_target_url.parameters['zone']
  875. old_name = parsed_target_url.parameters['targetInstance']
  876. if old_zone == new_zone and old_name == new_name:
  877. return True
  878. response = (self._provider
  879. .gce_compute
  880. .forwardingRules()
  881. .setTarget(
  882. project=self._provider.project_name,
  883. region=ip.region,
  884. forwardingRule=rule['name'],
  885. body={'target': new_url})
  886. .execute())
  887. self._provider.wait_for_operation(response,
  888. region=ip.region)
  889. return True
  890. except Exception as e:
  891. cb.log.warning(
  892. 'Exception while listing/changing forwarding rules: %s', e)
  893. return False
  894. def _forward(self, ip, target_instance):
  895. """
  896. Forward the traffic to a given IP to a given instance.
  897. If there is already a forwarding rule for the IP, it is redirected;
  898. otherwise, a new forwarding rule is created.
  899. """
  900. if self._redirect_existing_rule(ip, target_instance):
  901. return True
  902. body = {'name': 'forwarding-rule-{0}'.format(uuid.uuid4()),
  903. 'IPAddress': ip.public_ip,
  904. 'target': target_instance['selfLink']}
  905. try:
  906. response = (self._provider.gce_compute
  907. .forwardingRules()
  908. .insert(
  909. project=self._provider.project_name,
  910. region=ip.region,
  911. body=body)
  912. .execute())
  913. self._provider.wait_for_operation(response, region=ip.region)
  914. except Exception as e:
  915. cb.log.warning('Exception while inserting a forwarding rule: %s', e)
  916. return False
  917. return True
  918. def _delete_existing_rule(self, ip, target_instance):
  919. """
  920. Stop forwarding traffic to an instance by deleting the forwarding rule.
  921. """
  922. zone = (self._provider.parse_url(target_instance['zone'])
  923. .parameters['zone'])
  924. name = target_instance['name']
  925. try:
  926. response = (self._provider.gce_compute
  927. .forwardingRules()
  928. .list(project=self._provider.project_name,
  929. region=ip.region)
  930. .execute())
  931. if 'items' not in response:
  932. return False
  933. for rule in response['items']:
  934. if rule['IPAddress'] == ip.public_ip:
  935. parsed_target_url = self._provider.parse_url(rule['target'])
  936. temp_zone = parsed_target_url.parameters['zone']
  937. temp_name = parsed_target_url.parameters['targetInstance']
  938. if temp_zone != zone or temp_name != name:
  939. cb.log.warning('"%s" is forwarded to "%s" in zone "%s"',
  940. ip.public_ip, temp_name, temp_zone)
  941. return False
  942. response = (self._provider
  943. .gce_compute
  944. .forwardingRules()
  945. .delete(
  946. project=self._provider.project_name,
  947. region=ip.region,
  948. forwardingRule=rule['name'])
  949. .execute())
  950. self._provider.wait_for_operation(response,
  951. region=ip.region)
  952. except Exception as e:
  953. cb.log.warning(
  954. 'Exception while listing/deleting forwarding rules: %s', e)
  955. return False
  956. return True
  957. def add_floating_ip(self, ip_address):
  958. """
  959. Add an elastic IP address to this instance.
  960. """
  961. for ip in self._provider.network.floating_ips():
  962. if ip.public_ip == ip_address:
  963. if ip.in_use():
  964. if ip.private_ip not in self.private_ips:
  965. cb.log.warning(
  966. 'Floating IP "%s" is already associated to "%s".',
  967. ip_address, self.name)
  968. return
  969. target_instance = self._target_instance
  970. if not target_instance:
  971. cb.log.warning('Could not create a targetInstance for "%s"',
  972. self.name)
  973. return
  974. if not self._forward(ip, target_instance):
  975. cb.log.warning('Could not forward "%s" to "%s"',
  976. ip.public_ip, target_instance['selfLink'])
  977. return
  978. cb.log.warning('Floating IP "%s" does not exist.', ip_address)
  979. def remove_floating_ip(self, ip_address):
  980. """
  981. Remove a elastic IP address from this instance.
  982. """
  983. for ip in self._provider.network.floating_ips():
  984. if ip.public_ip == ip_address:
  985. if not ip.in_use() or ip.private_ip not in self.private_ips:
  986. cb.log.warning(
  987. 'Floating IP "%s" is not associated to "%s".',
  988. ip_address, self.name)
  989. return
  990. target_instance = self._target_instance
  991. if not target_instance:
  992. # We should not be here.
  993. cb.log.warning('Something went wrong! "%s" is associated '
  994. 'to "%s" with no target instance',
  995. ip_address, self.name)
  996. return
  997. if not self._delete_existing_rule(ip, target_instance):
  998. cb.log.warning(
  999. 'Could not remove floating IP "%s" from instance "%s"',
  1000. ip.public_ip, self.name)
  1001. return
  1002. cb.log.warning('Floating IP "%s" does not exist.', ip_address)
  1003. @property
  1004. def state(self):
  1005. return GCEInstance.INSTANCE_STATE_MAP.get(
  1006. self._gce_instance['status'], InstanceState.UNKNOWN)
  1007. def refresh(self):
  1008. """
  1009. Refreshes the state of this instance by re-querying the cloud provider
  1010. for its latest state.
  1011. """
  1012. self._gce_instance = self._provider.get_gce_resource_data(
  1013. self._gce_instance.get('selfLink'))
  1014. class GCENetwork(BaseNetwork):
  1015. def __init__(self, provider, network):
  1016. super(GCENetwork, self).__init__(provider)
  1017. self._network = network
  1018. @property
  1019. def resource_url(self):
  1020. return self._network['selfLink']
  1021. @property
  1022. def id(self):
  1023. return self._network['id']
  1024. @property
  1025. def name(self):
  1026. return self._network['name']
  1027. @property
  1028. def external(self):
  1029. raise NotImplementedError("To be implemented")
  1030. @property
  1031. def state(self):
  1032. raise NotImplementedError("To be implemented")
  1033. @property
  1034. def cidr_block(self):
  1035. return self._network['IPv4Range']
  1036. def delete(self):
  1037. try:
  1038. response = (self._provider
  1039. .gce_compute
  1040. .networks()
  1041. .delete(project=self._provider.project_name,
  1042. network=self.name)
  1043. .execute())
  1044. if 'error' in response:
  1045. return False
  1046. self._provider.wait_for_operation(response)
  1047. except:
  1048. return False
  1049. return True
  1050. def subnets(self):
  1051. raise NotImplementedError("To be implemented")
  1052. def create_subnet(self, cidr_block, name=None):
  1053. raise NotImplementedError("To be implemented")
  1054. def refresh(self):
  1055. return self.state
  1056. class GCEFloatingIP(BaseFloatingIP):
  1057. def __init__(self, provider, floating_ip):
  1058. super(GCEFloatingIP, self).__init__(provider)
  1059. self._ip = floating_ip
  1060. # We use regional IPs to simulate floating IPs not global IPs because
  1061. # global IPs can be forwarded only to load balancing resources, not to
  1062. # a specific instance. Find out the region to which the IP belongs.
  1063. url = provider.parse_url(self._ip['region'])
  1064. self._region = url.parameters['region']
  1065. # Check if the address is used by a resource.
  1066. self._rule = None
  1067. self._target_instance = None
  1068. if 'users' in floating_ip and len(floating_ip['users']) > 0:
  1069. if len(floating_ip['users']) > 1:
  1070. cb.log.warning('Address "%s" in use by more than one resource',
  1071. floating_ip['address'])
  1072. resource = provider.parse_url(floating_ip['users'][0]).get()
  1073. if resource['kind'] == 'compute#forwardingRule':
  1074. self._rule = resource
  1075. target = provider.parse_url(resource['target']).get()
  1076. if target['kind'] == 'compute#targetInstance':
  1077. url = provider.parse_url(target['instance'])
  1078. self._target_instance = url.get()
  1079. else:
  1080. cb.log.warning('Address "%s" is forwarded to a %s',
  1081. floating_ip['address'], target['kind'])
  1082. else:
  1083. cb.log.warning('Address "%s" in use by a %s',
  1084. floating_ip['address'], resource['kind'])
  1085. @property
  1086. def id(self):
  1087. return self._ip['id']
  1088. @property
  1089. def region(self):
  1090. return self._region
  1091. @property
  1092. def public_ip(self):
  1093. return self._ip['address']
  1094. @property
  1095. def private_ip(self):
  1096. if not self._target_instance:
  1097. return None
  1098. return self._target_instance['networkInterfaces'][0]['networkIP']
  1099. def in_use(self):
  1100. return True if self._target_instance else False
  1101. def delete(self):
  1102. project_name = self._provider.project_name
  1103. # First, delete the forwarding rule, if there is any.
  1104. if self._rule:
  1105. response = (self._provider.gce_compute
  1106. .forwardingRules()
  1107. .delete(project=project_name,
  1108. region=self._region,
  1109. forwardingRule=self._rule['name'])
  1110. .execute())
  1111. self._provider.wait_for_operation(response, region=self._region)
  1112. # Release the address.
  1113. response = (self._provider.gce_compute
  1114. .addresses()
  1115. .delete(project=project_name,
  1116. region=self._region,
  1117. address=self._ip['name'])
  1118. .execute())
  1119. self._provider.wait_for_operation(response, region=self._region)