resources.py 71 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077
  1. """
  2. DataTypes used by this provider
  3. """
  4. import hashlib
  5. import inspect
  6. import io
  7. import json
  8. import math
  9. import re
  10. import uuid
  11. import cloudbridge as cb
  12. from cloudbridge.cloud.base.resources import BaseAttachmentInfo
  13. from cloudbridge.cloud.base.resources import BaseBucket
  14. from cloudbridge.cloud.base.resources import BaseBucketObject
  15. from cloudbridge.cloud.base.resources import BaseFloatingIP
  16. from cloudbridge.cloud.base.resources import BaseFloatingIPContainer
  17. from cloudbridge.cloud.base.resources import BaseGatewayContainer
  18. from cloudbridge.cloud.base.resources import BaseInstance
  19. from cloudbridge.cloud.base.resources import BaseInternetGateway
  20. from cloudbridge.cloud.base.resources import BaseKeyPair
  21. from cloudbridge.cloud.base.resources import BaseLaunchConfig
  22. from cloudbridge.cloud.base.resources import BaseMachineImage
  23. from cloudbridge.cloud.base.resources import BaseNetwork
  24. from cloudbridge.cloud.base.resources import BasePlacementZone
  25. from cloudbridge.cloud.base.resources import BaseRegion
  26. from cloudbridge.cloud.base.resources import BaseRouter
  27. from cloudbridge.cloud.base.resources import BaseSnapshot
  28. from cloudbridge.cloud.base.resources import BaseSubnet
  29. from cloudbridge.cloud.base.resources import BaseVMFirewall
  30. from cloudbridge.cloud.base.resources import BaseVMFirewallRule
  31. from cloudbridge.cloud.base.resources import BaseVMType
  32. from cloudbridge.cloud.base.resources import BaseVolume
  33. from cloudbridge.cloud.base.resources import ClientPagedResultList
  34. from cloudbridge.cloud.base.resources import ServerPagedResultList
  35. from cloudbridge.cloud.interfaces.resources import GatewayState
  36. from cloudbridge.cloud.interfaces.resources import InstanceState
  37. from cloudbridge.cloud.interfaces.resources import MachineImageState
  38. from cloudbridge.cloud.interfaces.resources import NetworkState
  39. from cloudbridge.cloud.interfaces.resources import RouterState
  40. from cloudbridge.cloud.interfaces.resources import SnapshotState
  41. from cloudbridge.cloud.interfaces.resources import SubnetState
  42. from cloudbridge.cloud.interfaces.resources import VolumeState
  43. from cloudbridge.cloud.providers.gce import helpers
  44. import googleapiclient
  45. # Older versions of Python do not have a built-in set data-structure.
  46. try:
  47. set
  48. except NameError:
  49. from sets import Set as set
  50. class GCEKeyPair(BaseKeyPair):
  51. def __init__(self, provider, kp_id, kp_name, kp_material=None):
  52. super(GCEKeyPair, self).__init__(provider, None)
  53. self._kp_id = kp_id
  54. self._kp_name = kp_name
  55. self._kp_material = kp_material
  56. @property
  57. def id(self):
  58. return self._kp_id
  59. @property
  60. def name(self):
  61. # use e-mail as keyname if possible, or ID if not
  62. return self._kp_name or self.id
  63. def delete(self):
  64. svc = self._provider.security.key_pairs
  65. def _delete_key(gce_kp_generator):
  66. kp_list = []
  67. for gce_kp in gce_kp_generator:
  68. if svc.gce_kp_to_id(gce_kp) == self.id:
  69. continue
  70. else:
  71. kp_list.append(gce_kp)
  72. return kp_list
  73. svc.gce_metadata_save_op(_delete_key)
  74. @property
  75. def material(self):
  76. return self._kp_material
  77. @material.setter
  78. def material(self, value):
  79. self._kp_material = value
  80. class GCEVMType(BaseVMType):
  81. def __init__(self, provider, instance_dict):
  82. super(GCEVMType, self).__init__(provider)
  83. self._inst_dict = instance_dict
  84. @property
  85. def resource_url(self):
  86. return self._inst_dict.get('selfLink')
  87. @property
  88. def id(self):
  89. return self._inst_dict.get('selfLink')
  90. @property
  91. def name(self):
  92. return self._inst_dict.get('name')
  93. @property
  94. def family(self):
  95. return self._inst_dict.get('kind')
  96. @property
  97. def vcpus(self):
  98. return self._inst_dict.get('guestCpus')
  99. @property
  100. def ram(self):
  101. return self._inst_dict.get('memoryMb')
  102. @property
  103. def size_root_disk(self):
  104. return 0
  105. @property
  106. def size_ephemeral_disks(self):
  107. return int(self._inst_dict.get('maximumPersistentDisksSizeGb'))
  108. @property
  109. def num_ephemeral_disks(self):
  110. return self._inst_dict.get('maximumPersistentDisks')
  111. @property
  112. def extra_data(self):
  113. return {key: val for key, val in self._inst_dict.items()
  114. if key not in ['id', 'name', 'kind', 'guestCpus', 'memoryMb',
  115. 'maximumPersistentDisksSizeGb',
  116. 'maximumPersistentDisks']}
  117. class GCEPlacementZone(BasePlacementZone):
  118. def __init__(self, provider, zone, region):
  119. super(GCEPlacementZone, self).__init__(provider)
  120. if isinstance(zone, GCEPlacementZone):
  121. # pylint:disable=protected-access
  122. self._gce_zone = zone._gce_zone
  123. self._gce_region = zone._gce_region
  124. else:
  125. self._gce_zone = zone
  126. self._gce_region = region
  127. @property
  128. def id(self):
  129. """
  130. Get the zone id
  131. :rtype: ``str``
  132. :return: ID for this zone as returned by the cloud middleware.
  133. """
  134. return self._gce_zone.get('selfLink')
  135. @property
  136. def name(self):
  137. """
  138. Get the zone name.
  139. :rtype: ``str``
  140. :return: Name for this zone as returned by the cloud middleware.
  141. """
  142. return self._gce_zone.get('name')
  143. @property
  144. def region_name(self):
  145. """
  146. Get the region that this zone belongs to.
  147. :rtype: ``str``
  148. :return: Name of this zone's region as returned by the cloud middleware
  149. """
  150. return self._gce_region
  151. class GCERegion(BaseRegion):
  152. def __init__(self, provider, gce_region):
  153. super(GCERegion, self).__init__(provider)
  154. self._gce_region = gce_region
  155. @property
  156. def id(self):
  157. return self._gce_region.get('selfLink')
  158. @property
  159. def name(self):
  160. return self._gce_region.get('name')
  161. @property
  162. def zones(self):
  163. """
  164. Accesss information about placement zones within this region.
  165. """
  166. zones_response = (self._provider
  167. .gce_compute
  168. .zones()
  169. .list(project=self._provider.project_name)
  170. .execute())
  171. zones = [zone for zone in zones_response['items']
  172. if zone['region'] == self._gce_region['selfLink']]
  173. return [GCEPlacementZone(self._provider, zone, self.name)
  174. for zone in zones]
  175. class GCEFirewallsDelegate(object):
  176. DEFAULT_NETWORK = 'default'
  177. _NETWORK_URL_PREFIX = 'global/networks/'
  178. def __init__(self, provider):
  179. self._provider = provider
  180. self._list_response = None
  181. @staticmethod
  182. def tag_network_id(tag, network_name):
  183. """
  184. Generate an ID for a (tag, network name) pair.
  185. """
  186. md5 = hashlib.md5()
  187. md5.update("{0}-{1}".format(tag, network_name).encode('ascii'))
  188. return md5.hexdigest()
  189. @property
  190. def provider(self):
  191. return self._provider
  192. @property
  193. def tag_networks(self):
  194. """
  195. List all (tag, network name) pairs that are in at least one firewall.
  196. """
  197. out = set()
  198. for firewall in self.iter_firewalls():
  199. network_name = self.network_name(firewall)
  200. if network_name is not None:
  201. out.add((firewall['targetTags'][0], network_name))
  202. return out
  203. def network_name(self, firewall):
  204. """
  205. Extract the network name of a firewall.
  206. """
  207. if 'network' not in firewall:
  208. return GCEFirewallsDelegate.DEFAULT_NETWORK
  209. url = self._provider.parse_url(firewall['network'])
  210. return url.parameters['network']
  211. def get_tag_network_from_id(self, tag_network_id):
  212. """
  213. Map an ID back to the (tag, network name) pair.
  214. """
  215. for tag, network_name in self.tag_networks:
  216. current_id = GCEFirewallsDelegate.tag_network_id(tag, network_name)
  217. if current_id == tag_network_id:
  218. return (tag, network_name)
  219. return (None, None)
  220. def delete_tag_network_with_id(self, tag_network_id):
  221. """
  222. Delete all firewalls in a given network with a specific target tag.
  223. """
  224. tag, network_name = self.get_tag_network_from_id(tag_network_id)
  225. if tag is None:
  226. return
  227. for firewall in self.iter_firewalls(tag, network_name):
  228. self._delete_firewall(firewall)
  229. self._update_list_response()
  230. def add_firewall(self, tag, ip_protocol, port, source_range, source_tag,
  231. description, network_name):
  232. """
  233. Create a new firewall.
  234. """
  235. if self.find_firewall(tag, ip_protocol, port, source_range,
  236. source_tag, network_name) is not None:
  237. return True
  238. # Do not let the user accidentally open traffic from the world by not
  239. # explicitly specifying the source.
  240. if source_tag is None and source_range is None:
  241. return False
  242. firewall = {
  243. 'name': 'firewall-{0}'.format(uuid.uuid4()),
  244. 'network': GCEFirewallsDelegate._NETWORK_URL_PREFIX + network_name,
  245. 'allowed': [{'IPProtocol': str(ip_protocol)}],
  246. 'targetTags': [tag]}
  247. if description is not None:
  248. firewall['description'] = description
  249. if port is not None:
  250. firewall['allowed'][0]['ports'] = [port]
  251. if source_range is not None:
  252. firewall['sourceRanges'] = [source_range]
  253. if source_tag is not None:
  254. firewall['sourceTags'] = [source_tag]
  255. project_name = self._provider.project_name
  256. try:
  257. response = (self._provider
  258. .gce_compute
  259. .firewalls()
  260. .insert(project=project_name,
  261. body=firewall)
  262. .execute())
  263. self._provider.wait_for_operation(response)
  264. # TODO: process the response and handle errors.
  265. except googleapiclient.errors.HttpError as http_error:
  266. cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
  267. return False
  268. finally:
  269. self._update_list_response()
  270. return True
  271. def find_firewall(self, tag, ip_protocol, port, source_range, source_tag,
  272. network_name):
  273. """
  274. Find a firewall with give parameters.
  275. """
  276. if source_range is None and source_tag is None:
  277. source_range = '0.0.0.0/0'
  278. for firewall in self.iter_firewalls(tag, network_name):
  279. if firewall['allowed'][0]['IPProtocol'] != ip_protocol:
  280. continue
  281. if not self._check_list_in_dict(firewall['allowed'][0], 'ports',
  282. port):
  283. continue
  284. if not self._check_list_in_dict(firewall, 'sourceRanges',
  285. source_range):
  286. continue
  287. if not self._check_list_in_dict(firewall, 'sourceTags',
  288. source_tag):
  289. continue
  290. return firewall['id']
  291. return None
  292. def get_firewall_info(self, firewall_id):
  293. """
  294. Extract firewall properties to into a dictionary for easy of use.
  295. """
  296. info = {}
  297. for firewall in self.iter_firewalls():
  298. if firewall['id'] != firewall_id:
  299. continue
  300. if ('sourceRanges' in firewall and
  301. len(firewall['sourceRanges']) == 1):
  302. info['source_range'] = firewall['sourceRanges'][0]
  303. if 'sourceTags' in firewall and len(firewall['sourceTags']) == 1:
  304. info['source_tag'] = firewall['sourceTags'][0]
  305. if 'targetTags' in firewall and len(firewall['targetTags']) == 1:
  306. info['target_tag'] = firewall['targetTags'][0]
  307. if 'IPProtocol' in firewall['allowed'][0]:
  308. info['ip_protocol'] = firewall['allowed'][0]['IPProtocol']
  309. if ('ports' in firewall['allowed'][0] and
  310. len(firewall['allowed'][0]['ports']) == 1):
  311. info['port'] = firewall['allowed'][0]['ports'][0]
  312. info['network_name'] = self.network_name(firewall)
  313. return info
  314. return info
  315. def delete_firewall_id(self, firewall_id):
  316. """
  317. Delete a firewall with a given ID.
  318. """
  319. for firewall in self.iter_firewalls():
  320. if firewall['id'] == firewall_id:
  321. self._delete_firewall(firewall)
  322. self._update_list_response()
  323. def iter_firewalls(self, tag=None, network_name=None):
  324. """
  325. Iterate through all firewalls. Can optionally iterate through firewalls
  326. with a given tag and/or in a network.
  327. """
  328. if self._list_response is None:
  329. self._update_list_response()
  330. for firewall in self._list_response:
  331. if ('targetTags' not in firewall or
  332. len(firewall['targetTags']) != 1):
  333. continue
  334. if 'allowed' not in firewall or len(firewall['allowed']) != 1:
  335. continue
  336. if tag is not None and firewall['targetTags'][0] != tag:
  337. continue
  338. if network_name is None:
  339. yield firewall
  340. continue
  341. firewall_network_name = self.network_name(firewall)
  342. if firewall_network_name == network_name:
  343. yield firewall
  344. def _delete_firewall(self, firewall):
  345. """
  346. Delete a given firewall.
  347. """
  348. project_name = self._provider.project_name
  349. try:
  350. response = (self._provider
  351. .gce_compute
  352. .firewalls()
  353. .delete(project=project_name,
  354. firewall=firewall['name'])
  355. .execute())
  356. self._provider.wait_for_operation(response)
  357. except googleapiclient.errors.HttpError as http_error:
  358. cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
  359. return False
  360. # TODO: process the response and handle errors.
  361. return True
  362. def _update_list_response(self):
  363. """
  364. Sync the local cache of all firewalls with the server.
  365. """
  366. self._list_response = list(
  367. helpers.iter_all(self._provider.gce_compute.firewalls(),
  368. project=self._provider.project_name))
  369. def _check_list_in_dict(self, dictionary, field_name, value):
  370. """
  371. Verify that a given field in a dictionary is a singlton list [value].
  372. """
  373. if field_name not in dictionary:
  374. return value is None
  375. if (value is None or len(dictionary[field_name]) != 1 or
  376. dictionary[field_name][0] != value):
  377. return False
  378. return True
  379. class GCEVMFirewall(BaseVMFirewall):
  380. def __init__(self, delegate, tag, network=None, description=None):
  381. super(GCEVMFirewall, self).__init__(delegate.provider, tag)
  382. self._description = description
  383. self._delegate = delegate
  384. if network is None:
  385. self._network = delegate.provider.networking.networks.get_by_name(
  386. GCEFirewallsDelegate.DEFAULT_NETWORK)
  387. else:
  388. self._network = network
  389. @property
  390. def id(self):
  391. """
  392. Return the ID of this VM firewall which is determined based on the
  393. network and the target tag corresponding to this VM firewall.
  394. """
  395. return GCEFirewallsDelegate.tag_network_id(self._vm_firewall,
  396. self._network.name)
  397. @property
  398. def name(self):
  399. """
  400. Return the name of the VM firewall which is the same as the
  401. corresponding tag name.
  402. """
  403. return self._vm_firewall
  404. @property
  405. def description(self):
  406. """
  407. The description of the VM firewall is even explicitly given when the
  408. VM firewall is created or is determined from a VM firewall rule, i.e. a
  409. GCE firewall, in the VM firewall.
  410. If the GCE firewalls are created using this API, they all have the same
  411. description.
  412. """
  413. if self._description is not None:
  414. return self._description
  415. for firewall in self._delegate.iter_firewalls(self._vm_firewall,
  416. self._network.name):
  417. if 'description' in firewall:
  418. return firewall['description']
  419. return None
  420. @property
  421. def network_id(self):
  422. return self._network.id
  423. @property
  424. def rules(self):
  425. out = []
  426. for firewall in self._delegate.iter_firewalls(self._vm_firewall,
  427. self._network.name):
  428. out.append(GCEVMFirewallRule(self._delegate, firewall['id']))
  429. return out
  430. @staticmethod
  431. def to_port_range(from_port, to_port):
  432. if from_port is not None and to_port is not None:
  433. return '%d-%d' % (from_port, to_port)
  434. elif from_port is not None:
  435. return from_port
  436. else:
  437. return to_port
  438. def add_rule(self, ip_protocol, from_port=None, to_port=None,
  439. cidr_ip=None, src_group=None):
  440. port = GCEVMFirewall.to_port_range(from_port, to_port)
  441. src_tag = src_group.name if src_group is not None else None
  442. self._delegate.add_firewall(self._vm_firewall, ip_protocol, port,
  443. cidr_ip, src_tag, self.description,
  444. self._network.name)
  445. return self.get_rule(ip_protocol, from_port, to_port, cidr_ip,
  446. src_group)
  447. def get_rule(self, ip_protocol=None, from_port=None, to_port=None,
  448. cidr_ip=None, src_group=None):
  449. port = GCEVMFirewall.to_port_range(from_port, to_port)
  450. src_tag = src_group.name if src_group is not None else None
  451. firewall_id = self._delegate.find_firewall(
  452. self._vm_firewall, ip_protocol, port, cidr_ip, src_tag,
  453. self._network.name)
  454. if firewall_id is None:
  455. return None
  456. return GCEVMFirewallRule(self._delegate, firewall_id)
  457. def to_json(self):
  458. attr = inspect.getmembers(self, lambda a: not(inspect.isroutine(a)))
  459. js = {k: v for(k, v) in attr if not k.startswith('_')}
  460. json_rules = [r.to_json() for r in self.rules]
  461. js['rules'] = [json.loads(r) for r in json_rules]
  462. return json.dumps(js, sort_keys=True)
  463. def delete(self):
  464. for rule in self.rules:
  465. rule.delete()
  466. class GCEVMFirewallRule(BaseVMFirewallRule):
  467. def __init__(self, delegate, firewall_id):
  468. super(GCEVMFirewallRule, self).__init__(
  469. delegate.provider, firewall_id, None)
  470. self._delegate = delegate
  471. @property
  472. def parent(self):
  473. """
  474. Return the VM firewall to which this rule belongs.
  475. """
  476. info = self._delegate.get_firewall_info(self._rule)
  477. if info is None:
  478. return None
  479. if 'target_tag' not in info or info['network_name'] is None:
  480. return None
  481. network = self._delegate.network.get_by_name(info['network_name'])
  482. if network is None:
  483. return None
  484. return GCEVMFirewall(self._delegate, info['target_tag'], network)
  485. @property
  486. def id(self):
  487. return self._rule
  488. @property
  489. def ip_protocol(self):
  490. info = self._delegate.get_firewall_info(self._rule)
  491. if info is None or 'ip_protocol' not in info:
  492. return None
  493. return info['ip_protocol']
  494. @property
  495. def from_port(self):
  496. info = self._delegate.get_firewall_info(self._rule)
  497. if info is None or 'port' not in info:
  498. return 0
  499. port = info['port']
  500. if port.isdigit():
  501. return int(port)
  502. parts = port.split('-')
  503. if len(parts) > 2 or len(parts) < 1:
  504. return 0
  505. if parts[0].isdigit():
  506. return int(parts[0])
  507. return 0
  508. @property
  509. def to_port(self):
  510. info = self._delegate.get_firewall_info(self._rule)
  511. if info is None or 'port' not in info:
  512. return 0
  513. port = info['port']
  514. if port.isdigit():
  515. return int(port)
  516. parts = port.split('-')
  517. if len(parts) > 2 or len(parts) < 1:
  518. return 0
  519. if parts[-1].isdigit():
  520. return int(parts[-1])
  521. return 0
  522. @property
  523. def cidr_ip(self):
  524. """
  525. Return the IP of machines from which this rule allows traffic.
  526. """
  527. info = self._delegate.get_firewall_info(self._rule)
  528. if info is None or 'source_range' not in info:
  529. return None
  530. return info['source_range']
  531. @property
  532. def group(self):
  533. """
  534. Return the VM firewall from which this rule allows traffic.
  535. """
  536. info = self._delegate.get_firewall_info(self._rule)
  537. if info is None:
  538. return None
  539. if 'source_tag' not in info or info['network_name'] is None:
  540. return None
  541. network = self._delegate.provider.networking.networks.get_by_name(
  542. info['network_name'])
  543. if network is None:
  544. return None
  545. return GCEVMFirewall(self._delegate, info['source_tag'], network)
  546. def to_json(self):
  547. attr = inspect.getmembers(self, lambda a: not(inspect.isroutine(a)))
  548. js = {k: v for(k, v) in attr if not k.startswith('_')}
  549. js['group'] = self.group.id if self.group else ''
  550. js['parent'] = self.parent.id if self.parent else ''
  551. return json.dumps(js, sort_keys=True)
  552. def delete(self):
  553. self._delegate.delete_firewall_id(self._rule)
  554. class GCEMachineImage(BaseMachineImage):
  555. IMAGE_STATE_MAP = {
  556. 'PENDING': MachineImageState.PENDING,
  557. 'READY': MachineImageState.AVAILABLE,
  558. 'FAILED': MachineImageState.ERROR
  559. }
  560. def __init__(self, provider, image):
  561. super(GCEMachineImage, self).__init__(provider)
  562. if isinstance(image, GCEMachineImage):
  563. # pylint:disable=protected-access
  564. self._gce_image = image._gce_image
  565. else:
  566. self._gce_image = image
  567. @property
  568. def resource_url(self):
  569. return self._gce_image.get('selfLink')
  570. @property
  571. def id(self):
  572. """
  573. Get the image identifier.
  574. :rtype: ``str``
  575. :return: ID for this instance as returned by the cloud middleware.
  576. """
  577. return self._gce_image.get('selfLink')
  578. @property
  579. def name(self):
  580. """
  581. Get the image name.
  582. :rtype: ``str``
  583. :return: Name for this image as returned by the cloud middleware.
  584. """
  585. return self._gce_image['name']
  586. @property
  587. def description(self):
  588. """
  589. Get the image description.
  590. :rtype: ``str``
  591. :return: Description for this image as returned by the cloud middleware
  592. """
  593. return self._gce_image.get('description', '')
  594. @property
  595. def min_disk(self):
  596. """
  597. Returns the minimum size of the disk that's required to
  598. boot this image (in GB)
  599. :rtype: ``int``
  600. :return: The minimum disk size needed by this image
  601. """
  602. return int(math.ceil(float(self._gce_image.get('diskSizeGb'))))
  603. def delete(self):
  604. """
  605. Delete this image
  606. """
  607. (self._provider
  608. .gce_compute
  609. .images()
  610. .delete(project=self._provider.project_name,
  611. image=self.name)
  612. .execute())
  613. @property
  614. def state(self):
  615. return GCEMachineImage.IMAGE_STATE_MAP.get(
  616. self._gce_image['status'], MachineImageState.UNKNOWN)
  617. def refresh(self):
  618. """
  619. Refreshes the state of this instance by re-querying the cloud provider
  620. for its latest state.
  621. """
  622. resource_link = self._gce_image['selfLink']
  623. project_pattern = 'projects/(.*?)/'
  624. match = re.search(project_pattern, resource_link)
  625. if match:
  626. project = match.group(1)
  627. else:
  628. cb.log.warning("Project name is not found.")
  629. return
  630. try:
  631. response = (self._provider
  632. .gce_compute
  633. .images()
  634. .get(project=project, image=self.name)
  635. .execute())
  636. self._gce_image = response
  637. except googleapiclient.errors.HttpError:
  638. # If the resource does not exist, its status is UNKNOWN.
  639. self._gce_image['status'] = 'UNKNOWN'
  640. class GCEInstance(BaseInstance):
  641. # https://cloud.google.com/compute/docs/reference/latest/instances
  642. # The status of the instance. One of the following values:
  643. # PROVISIONING, STAGING, RUNNING, STOPPING, SUSPENDING, SUSPENDED,
  644. # and TERMINATED.
  645. INSTANCE_STATE_MAP = {
  646. 'PROVISIONING': InstanceState.PENDING,
  647. 'STAGING': InstanceState.PENDING,
  648. 'RUNNING': InstanceState.RUNNING,
  649. 'STOPPING': InstanceState.CONFIGURING,
  650. 'TERMINATED': InstanceState.STOPPED,
  651. 'SUSPENDING': InstanceState.CONFIGURING,
  652. 'SUSPENDED': InstanceState.STOPPED
  653. }
  654. def __init__(self, provider, gce_instance):
  655. super(GCEInstance, self).__init__(provider)
  656. self._gce_instance = gce_instance
  657. @property
  658. def resource_url(self):
  659. return self._gce_instance.get('selfLink')
  660. @property
  661. def id(self):
  662. """
  663. Get the instance identifier.
  664. A GCE instance is uniquely identified by its selfLink, which is used
  665. as its id.
  666. """
  667. return self._gce_instance.get('selfLink')
  668. @property
  669. def name(self):
  670. """
  671. Get the instance name.
  672. """
  673. return self._gce_instance['name']
  674. @name.setter
  675. # pylint:disable=arguments-differ
  676. def name(self, value):
  677. """
  678. Set the instance name.
  679. """
  680. # In GCE, the name of the instance is provided by the client when
  681. # initially creating the resource. The name cannot be changed after
  682. # the instance is created.
  683. cb.log.warning("Setting instance name after it is created is not "
  684. "supported by this provider.")
  685. @property
  686. def public_ips(self):
  687. """
  688. Get all the public IP addresses for this instance.
  689. """
  690. ips = []
  691. network_interfaces = self._gce_instance.get('networkInterfaces')
  692. if network_interfaces is not None and len(network_interfaces) > 0:
  693. access_configs = network_interfaces[0].get('accessConfigs')
  694. if access_configs is not None and len(access_configs) > 0:
  695. # https://cloud.google.com/compute/docs/reference/beta/instances
  696. # An array of configurations for this interface. Currently,
  697. # only one access config, ONE_TO_ONE_NAT, is supported. If
  698. # there are no accessConfigs specified, then this instance will
  699. # have no external internet access.
  700. access_config = access_configs[0]
  701. if 'natIP' in access_config:
  702. ips.append(access_config['natIP'])
  703. for ip in self._provider.networking.floating_ips:
  704. if ip.in_use():
  705. if ip.private_ip in self.private_ips:
  706. ips.append(ip.public_ip)
  707. return ips
  708. @property
  709. def private_ips(self):
  710. """
  711. Get all the private IP addresses for this instance.
  712. """
  713. network_interfaces = self._gce_instance.get('networkInterfaces')
  714. if network_interfaces is None or len(network_interfaces) == 0:
  715. return []
  716. if 'networkIP' in network_interfaces[0]:
  717. return [network_interfaces[0]['networkIP']]
  718. else:
  719. return []
  720. @property
  721. def vm_type_id(self):
  722. """
  723. Get the instance type name.
  724. """
  725. return self._gce_instance.get('machineType')
  726. @property
  727. def vm_type(self):
  728. """
  729. Get the instance type.
  730. """
  731. machine_type_uri = self._gce_instance.get('machineType')
  732. if machine_type_uri is None:
  733. return None
  734. parsed_uri = self._provider.parse_url(machine_type_uri)
  735. return GCEVMType(self._provider, parsed_uri.get_resource())
  736. def reboot(self):
  737. """
  738. Reboot this instance.
  739. """
  740. if self.state == InstanceState.STOPPED:
  741. (self._provider
  742. .gce_compute
  743. .instances()
  744. .start(project=self._provider.project_name,
  745. zone=self._provider.default_zone,
  746. instance=self.name)
  747. .execute())
  748. else:
  749. (self._provider
  750. .gce_compute
  751. .instances()
  752. .reset(project=self._provider.project_name,
  753. zone=self._provider.default_zone,
  754. instance=self.name)
  755. .execute())
  756. def delete(self):
  757. """
  758. Permanently terminate this instance.
  759. """
  760. (self._provider
  761. .gce_compute
  762. .instances()
  763. .delete(project=self._provider.project_name,
  764. zone=self._provider.default_zone,
  765. instance=self.name)
  766. .execute())
  767. def stop(self):
  768. """
  769. Stop this instance.
  770. """
  771. (self._provider
  772. .gce_compute
  773. .instances()
  774. .stop(project=self._provider.project_name,
  775. zone=self._provider.default_zone,
  776. instance=self.name)
  777. .execute())
  778. @property
  779. def image_id(self):
  780. """
  781. Get the image ID for this insance.
  782. """
  783. if 'disks' not in self._gce_instance:
  784. return None
  785. for disk in self._gce_instance['disks']:
  786. if 'boot' in disk and disk['boot']:
  787. disk_url = self._provider.parse_url(disk['source'])
  788. return disk_url.get_resource().get('sourceImage')
  789. return None
  790. @property
  791. def zone_id(self):
  792. """
  793. Get the placement zone id where this instance is running.
  794. """
  795. return self._gce_instance.get('zone')
  796. @property
  797. def vm_firewalls(self):
  798. """
  799. Get the VM firewalls associated with this instance.
  800. """
  801. network_url = self._gce_instance.get('networkInterfaces')[0].get(
  802. 'network')
  803. url = self._provider.parse_url(network_url)
  804. network_name = url.parameters['network']
  805. if 'items' not in self._gce_instance['tags']:
  806. return []
  807. tags = self._gce_instance['tags']['items']
  808. # Tags are mapped to non-empty VM firewalls under the instance network.
  809. # Unmatched tags are ignored.
  810. sgs = (self._provider.security
  811. .vm_firewalls.find_by_network_and_tags(
  812. network_name, tags))
  813. return sgs
  814. @property
  815. def vm_firewall_ids(self):
  816. """
  817. Get the VM firewall IDs associated with this instance.
  818. """
  819. sg_ids = []
  820. for sg in self.vm_firewalls:
  821. sg_ids.append(sg.id)
  822. return sg_ids
  823. @property
  824. def key_pair_name(self):
  825. """
  826. Get the name of the key pair associated with this instance.
  827. """
  828. return self._provider.security.key_pairs.name
  829. def create_image(self, name):
  830. """
  831. Create a new image based on this instance.
  832. """
  833. self.assert_valid_resource_name(name)
  834. if 'disks' not in self._gce_instance:
  835. cb.log.error('Failed to create image: no disks found.')
  836. return
  837. for disk in self._gce_instance['disks']:
  838. if 'boot' in disk and disk['boot']:
  839. image_body = {
  840. 'name': name,
  841. 'sourceDisk': disk['source']
  842. }
  843. operation = (self._provider
  844. .gce_compute
  845. .images()
  846. .insert(project=self._provider.project_name,
  847. body=image_body)
  848. .execute())
  849. self._provider.wait_for_operation(operation)
  850. return
  851. cb.log.error('Failed to create image: no boot disk found.')
  852. def _get_existing_target_instance(self):
  853. """
  854. Return the target instance corrsponding to this instance.
  855. If there is no target instance for this instance, return None.
  856. """
  857. self_url = self._provider.parse_url(self._gce_instance['selfLink'])
  858. try:
  859. for target_instance in helpers.iter_all(
  860. self._provider.gce_compute.targetInstances(),
  861. project=self_url.parameters['project'],
  862. zone=self_url.parameters['zone']):
  863. url = self._provider.parse_url(target_instance['instance'])
  864. if url.parameters['instance'] == self.name:
  865. return target_instance
  866. except Exception as e:
  867. cb.log.warning('Exception while listing target instances: %s', e)
  868. return None
  869. def _get_target_instance(self):
  870. """
  871. Return the target instance corresponding to this instance.
  872. If there is no target instance for this instance, create one.
  873. """
  874. existing_target_instance = self._get_existing_target_instance()
  875. if existing_target_instance:
  876. return existing_target_instance
  877. # No targetInstance exists for this instance. Create one.
  878. self_url = self._provider.parse_url(self._gce_instance['selfLink'])
  879. body = {'name': 'target-instance-{0}'.format(uuid.uuid4()),
  880. 'instance': self._gce_instance['selfLink']}
  881. try:
  882. response = (self._provider
  883. .gce_compute
  884. .targetInstances()
  885. .insert(project=self_url.parameters['project'],
  886. zone=self_url.parameters['zone'],
  887. body=body)
  888. .execute())
  889. self._provider.wait_for_operation(
  890. response, zone=self_url.parameters['zone'])
  891. except Exception as e:
  892. cb.log.warning('Exception while inserting a target instance: %s',
  893. e)
  894. return None
  895. # The following method should find the target instance that we
  896. # successfully created above.
  897. return self._get_existing_target_instance()
  898. def _redirect_existing_rule(self, ip, target_instance):
  899. """
  900. Redirect the forwarding rule of the given IP to the given Instance.
  901. """
  902. new_zone = (self._provider.parse_url(target_instance['zone'])
  903. .parameters['zone'])
  904. new_name = target_instance['name']
  905. new_url = target_instance['selfLink']
  906. try:
  907. for rule in helpers.iter_all(
  908. self._provider.gce_compute.forwardingRules(),
  909. project=self._provider.project_name,
  910. region=ip.region):
  911. if rule['IPAddress'] != ip.public_ip:
  912. continue
  913. parsed_target_url = self._provider.parse_url(rule['target'])
  914. old_zone = parsed_target_url.parameters['zone']
  915. old_name = parsed_target_url.parameters['targetInstance']
  916. if old_zone == new_zone and old_name == new_name:
  917. return True
  918. response = (self._provider
  919. .gce_compute
  920. .forwardingRules()
  921. .setTarget(
  922. project=self._provider.project_name,
  923. region=ip.region,
  924. forwardingRule=rule['name'],
  925. body={'target': new_url})
  926. .execute())
  927. self._provider.wait_for_operation(response, region=ip.region)
  928. return True
  929. except Exception as e:
  930. cb.log.warning(
  931. 'Exception while listing/changing forwarding rules: %s', e)
  932. return False
  933. def _forward(self, ip, target_instance):
  934. """
  935. Forward the traffic to a given IP to a given instance.
  936. If there is already a forwarding rule for the IP, it is redirected;
  937. otherwise, a new forwarding rule is created.
  938. """
  939. if self._redirect_existing_rule(ip, target_instance):
  940. return True
  941. body = {'name': 'forwarding-rule-{0}'.format(uuid.uuid4()),
  942. 'IPAddress': ip.public_ip,
  943. 'target': target_instance['selfLink']}
  944. try:
  945. response = (self._provider
  946. .gce_compute
  947. .forwardingRules()
  948. .insert(project=self._provider.project_name,
  949. region=ip.region,
  950. body=body)
  951. .execute())
  952. self._provider.wait_for_operation(response, region=ip.region)
  953. except Exception as e:
  954. cb.log.warning('Exception while inserting a forwarding rule: %s',
  955. e)
  956. return False
  957. return True
  958. def _delete_existing_rule(self, ip, target_instance):
  959. """
  960. Stop forwarding traffic to an instance by deleting the forwarding rule.
  961. """
  962. zone = (self._provider.parse_url(target_instance['zone'])
  963. .parameters['zone'])
  964. name = target_instance['name']
  965. try:
  966. for rule in helpers.iter_all(
  967. self._provider.gce_compute.forwardingRules(),
  968. project=self._provider.project_name,
  969. region=ip.region):
  970. if rule['IPAddress'] != ip.public_ip:
  971. continue
  972. parsed_target_url = self._provider.parse_url(rule['target'])
  973. temp_zone = parsed_target_url.parameters['zone']
  974. temp_name = parsed_target_url.parameters['targetInstance']
  975. if temp_zone != zone or temp_name != name:
  976. cb.log.warning(
  977. '"%s" is forwarded to "%s" in zone "%s"',
  978. ip.public_ip, temp_name, temp_zone)
  979. return False
  980. response = (self._provider
  981. .gce_compute
  982. .forwardingRules()
  983. .delete(
  984. project=self._provider.project_name,
  985. region=ip.region,
  986. forwardingRule=rule['name'])
  987. .execute())
  988. self._provider.wait_for_operation(response, region=ip.region)
  989. return True
  990. except Exception as e:
  991. cb.log.warning(
  992. 'Exception while listing/deleting forwarding rules: %s', e)
  993. return False
  994. return True
  995. def add_floating_ip(self, ip_address):
  996. """
  997. Add an elastic IP address to this instance.
  998. """
  999. for ip in self._provider.networking.floating_ips:
  1000. if ip.public_ip == ip_address:
  1001. if ip.in_use():
  1002. if ip.private_ip not in self.private_ips:
  1003. cb.log.warning(
  1004. 'Floating IP "%s" is already associated to "%s".',
  1005. ip_address, self.name)
  1006. return
  1007. target_instance = self._get_target_instance()
  1008. if not target_instance:
  1009. cb.log.warning(
  1010. 'Could not create a targetInstance for "%s"',
  1011. self.name)
  1012. return
  1013. if not self._forward(ip, target_instance):
  1014. cb.log.warning('Could not forward "%s" to "%s"',
  1015. ip.public_ip, target_instance['selfLink'])
  1016. return
  1017. cb.log.warning('Floating IP "%s" does not exist.', ip_address)
  1018. def remove_floating_ip(self, ip_address):
  1019. """
  1020. Remove a elastic IP address from this instance.
  1021. """
  1022. for ip in self._provider.networking.floating_ips:
  1023. if ip.public_ip == ip_address:
  1024. if not ip.in_use() or ip.private_ip not in self.private_ips:
  1025. cb.log.warning(
  1026. 'Floating IP "%s" is not associated to "%s".',
  1027. ip_address, self.name)
  1028. return
  1029. target_instance = self._get_target_instance()
  1030. if not target_instance:
  1031. # We should not be here.
  1032. cb.log.warning('Something went wrong! "%s" is associated '
  1033. 'to "%s" with no target instance',
  1034. ip_address, self.name)
  1035. return
  1036. if not self._delete_existing_rule(ip, target_instance):
  1037. cb.log.warning(
  1038. 'Could not remove floating IP "%s" from instance "%s"',
  1039. ip.public_ip, self.name)
  1040. return
  1041. cb.log.warning('Floating IP "%s" does not exist.', ip_address)
  1042. @property
  1043. def state(self):
  1044. return GCEInstance.INSTANCE_STATE_MAP.get(
  1045. self._gce_instance['status'], InstanceState.UNKNOWN)
  1046. def refresh(self):
  1047. """
  1048. Refreshes the state of this instance by re-querying the cloud provider
  1049. for its latest state.
  1050. """
  1051. self_link = self._gce_instance.get('selfLink')
  1052. try:
  1053. new_info = self._provider.parse_url(self_link).get_resource()
  1054. self._get_instance = new_info
  1055. except googleapiclient.errors.HttpError:
  1056. # If the resource does not exist, its status is UNKNOWN.
  1057. self._gce_instance['status'] = 'UNKNOWN'
  1058. def add_vm_firewall(self, sg):
  1059. raise NotImplementedError('To be implemented.')
  1060. def remove_vm_firewall(self, sg):
  1061. raise NotImplementedError('To be implemented.')
  1062. class GCENetwork(BaseNetwork):
  1063. DEFAULT_IPV4RANGE = '10.128.0.0/9'
  1064. def __init__(self, provider, network):
  1065. super(GCENetwork, self).__init__(provider)
  1066. self._network = network
  1067. self._gateway_container = GCEGatewayContainer(provider, self)
  1068. @property
  1069. def resource_url(self):
  1070. return self._network['selfLink']
  1071. @property
  1072. def id(self):
  1073. return self._network['selfLink']
  1074. @property
  1075. def name(self):
  1076. return self._network['name']
  1077. @property
  1078. def external(self):
  1079. """
  1080. All GCP networks can be connected to the Internet.
  1081. """
  1082. return True
  1083. @property
  1084. def state(self):
  1085. """
  1086. When a GCP network created by the CloudBridge API, we wait until the
  1087. network is ready.
  1088. """
  1089. if 'status' in self._network and self._network['status'] == 'UNKNOWN':
  1090. return NetworkState.UNKNOWN
  1091. return NetworkState.AVAILABLE
  1092. @property
  1093. def cidr_block(self):
  1094. if 'IPv4Range' in self._network:
  1095. # This is a legacy network.
  1096. return self._network['IPv4Range']
  1097. return GCENetwork.DEFAULT_IPV4RANGE
  1098. @property
  1099. def subnets(self):
  1100. return self._provider.networking.subnets.list(network=self)
  1101. def delete(self):
  1102. try:
  1103. response = (self._provider
  1104. .gce_compute
  1105. .networks()
  1106. .delete(project=self._provider.project_name,
  1107. network=self.name)
  1108. .execute())
  1109. if 'error' in response:
  1110. return False
  1111. self._provider.wait_for_operation(response)
  1112. except googleapiclient.errors.HttpError as http_error:
  1113. cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
  1114. return False
  1115. return True
  1116. def create_subnet(self, cidr_block, name=None, zone=None):
  1117. return self._provider.networking.subnets.create(
  1118. self, cidr_block, name, zone)
  1119. def refresh(self):
  1120. self_link = self._network.get('selfLink')
  1121. try:
  1122. new_info = self._provider.parse_url(self_link).get_resource()
  1123. self._network = new_info
  1124. except googleapiclient.errors.HttpError:
  1125. # If the resource does not exist, its status is UNKNOWN.
  1126. self._network['status'] = 'UNKNOWN'
  1127. @property
  1128. def gateways(self):
  1129. return self._gateway_container
  1130. class GCEFloatingIPContainer(BaseFloatingIPContainer):
  1131. def __init__(self, provider, gateway):
  1132. super(GCEFloatingIPContainer, self).__init__(provider, gateway)
  1133. def get(self, floating_ip_id):
  1134. try:
  1135. response = (self.provider
  1136. .gce_compute
  1137. .addresses()
  1138. .get(project=self.provider.project_name,
  1139. region=self.provider.region_name)
  1140. .execute())
  1141. return GCEFloatingIP(self.provider, response)
  1142. except googleapiclient.errors.HttpError as http_error:
  1143. cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
  1144. return None
  1145. def list(self, limit=None, marker=None):
  1146. max_result = limit if limit is not None and limit < 500 else 500
  1147. try:
  1148. response = (self.provider
  1149. .gce_compute
  1150. .addresses()
  1151. .list(project=self.provider.project_name,
  1152. region=self.provider.region_name,
  1153. maxResults=max_result,
  1154. pageToken=marker)
  1155. .execute())
  1156. ips = [GCEFloatingIP(self.provider, ip)
  1157. for ip in response.get('items', [])]
  1158. if len(ips) > max_result:
  1159. cb.log.warning('Expected at most %d results; got %d',
  1160. max_result, len(ips))
  1161. return ServerPagedResultList('nextPageToken' in response,
  1162. response.get('nextPageToken'),
  1163. False, data=ips)
  1164. except googleapiclient.errors.HttpError as http_error:
  1165. cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
  1166. return None
  1167. def create(self):
  1168. region = self.provider.region_name
  1169. ip_name = 'ip-{0}'.format(uuid.uuid4())
  1170. try:
  1171. response = (self.provider
  1172. .gce_compute
  1173. .addresses()
  1174. .insert(project=self.provider.project_name,
  1175. region=region,
  1176. body={'name': ip_name})
  1177. .execute())
  1178. if 'error' in response:
  1179. return None
  1180. self.provider.wait_for_operation(response, region=region)
  1181. return self.get(ip_name)
  1182. except googleapiclient.errors.HttpError as http_error:
  1183. cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
  1184. return None
  1185. class GCEFloatingIP(BaseFloatingIP):
  1186. _DEAD_INSTANCE = 'dead instance'
  1187. def __init__(self, provider, floating_ip):
  1188. super(GCEFloatingIP, self).__init__(provider)
  1189. self._ip = floating_ip
  1190. # We use regional IPs to simulate floating IPs not global IPs because
  1191. # global IPs can be forwarded only to load balancing resources, not to
  1192. # a specific instance. Find out the region to which the IP belongs.
  1193. url = provider.parse_url(self._ip['region'])
  1194. self._region = url.parameters['region']
  1195. # Check if the address is used by a resource.
  1196. self._rule = None
  1197. self._target_instance = None
  1198. if 'users' in floating_ip and len(floating_ip['users']) > 0:
  1199. if len(floating_ip['users']) > 1:
  1200. cb.log.warning('Address "%s" in use by more than one resource',
  1201. floating_ip['address'])
  1202. resource_parsed_url = provider.parse_url(floating_ip['users'][0])
  1203. resource = resource_parsed_url.get_resource()
  1204. if resource['kind'] == 'compute#forwardingRule':
  1205. self._rule = resource
  1206. target = provider.parse_url(resource['target']).get_resource()
  1207. if target['kind'] == 'compute#targetInstance':
  1208. url = provider.parse_url(target['instance'])
  1209. try:
  1210. self._target_instance = url.get_resource()
  1211. except googleapiclient.errors.HttpError:
  1212. self._target_instance = GCEFloatingIP._DEAD_INSTANCE
  1213. else:
  1214. cb.log.warning('Address "%s" is forwarded to a %s',
  1215. floating_ip['address'], target['kind'])
  1216. else:
  1217. cb.log.warning('Address "%s" in use by a %s',
  1218. floating_ip['address'], resource['kind'])
  1219. @property
  1220. def id(self):
  1221. return self._ip['selfLink']
  1222. @property
  1223. def region(self):
  1224. return self._region
  1225. @property
  1226. def public_ip(self):
  1227. return self._ip['address']
  1228. @property
  1229. def private_ip(self):
  1230. if (not self._target_instance or
  1231. self._target_instance == GCEFloatingIP._DEAD_INSTANCE):
  1232. return None
  1233. return self._target_instance['networkInterfaces'][0]['networkIP']
  1234. def in_use(self):
  1235. return True if self._target_instance else False
  1236. def delete(self):
  1237. project_name = self._provider.project_name
  1238. # First, delete the forwarding rule, if there is any.
  1239. if self._rule:
  1240. response = (self._provider
  1241. .gce_compute
  1242. .forwardingRules()
  1243. .delete(project=project_name,
  1244. region=self._region,
  1245. forwardingRule=self._rule['name'])
  1246. .execute())
  1247. self._provider.wait_for_operation(response, region=self._region)
  1248. # Release the address.
  1249. response = (self._provider
  1250. .gce_compute
  1251. .addresses()
  1252. .delete(project=project_name,
  1253. region=self._region,
  1254. address=self._ip['name'])
  1255. .execute())
  1256. self._provider.wait_for_operation(response, region=self._region)
  1257. def refresh(self):
  1258. self_link = self._ip.get('selfLink')
  1259. try:
  1260. new_info = self._provider.parse_url(self_link).get_resource()
  1261. self._ip = new_info
  1262. except googleapiclient.errors.HttpError:
  1263. # If the resource does not exist, its status is UNKNOWN.
  1264. self._network['status'] = 'UNKNOWN'
  1265. class GCERouter(BaseRouter):
  1266. def __init__(self, provider, router):
  1267. super(GCERouter, self).__init__(provider)
  1268. self._router = router
  1269. @property
  1270. def id(self):
  1271. return self._router['selfLink']
  1272. @property
  1273. def name(self):
  1274. return self._router['name']
  1275. def refresh(self):
  1276. self_link = self._router.get('selfLink')
  1277. try:
  1278. new_info = self._provider.parse_url(self_link).get_resource()
  1279. self._router = new_info
  1280. except googleapiclient.errors.HttpError:
  1281. # If the resource does not exist, its status is UNKNOWN.
  1282. self._router['status'] = 'UNKNOWN'
  1283. @property
  1284. def state(self):
  1285. # If the router info is refreshed after it is deleted, its status will
  1286. # be UNKNOWN.
  1287. if 'status' in self._router and self._router['status'] == 'UNKNOWN':
  1288. return RouterState.UNKNOWN
  1289. # GCE routers are always attached to a network.
  1290. return RouterState.ATTACHED
  1291. @property
  1292. def network_id(self):
  1293. parsed_url = self._provider.parse_url(self._router['network'])
  1294. network = parsed_url.get_resource()
  1295. return network['selfLink']
  1296. def delete(self):
  1297. response = (self._provider
  1298. .gce_compute
  1299. .routers()
  1300. .delete(project=self._provider.project_name,
  1301. region=self._router['region'],
  1302. router=self._router['name'])
  1303. .execute())
  1304. self._provider.wait_for_operation(response,
  1305. region=self._router['region'])
  1306. def attach_network(self, network_id):
  1307. if network_id == self.network_id:
  1308. return
  1309. cb.log.warning('GCE routers should be attached at creation time')
  1310. def detach_network(self, network_id):
  1311. cb.log.warning('GCE routers are always attached')
  1312. def add_route(self, subnet_id):
  1313. cb.log.warning('Not implemented')
  1314. def remove_route(self, subnet_id):
  1315. cb.log.warning('Not implemented')
  1316. class GCEGatewayContainer(BaseGatewayContainer):
  1317. _DEFAULT_GATEWAY_NAME = 'default-internet-gateway'
  1318. _GATEWAY_URL_PREFIX = 'global/gateways/'
  1319. def __init__(self, provider, network):
  1320. super(GCEGatewayContainer, self).__init__(provider, network)
  1321. self._default_internet_gateway = GCEInternetGateway(
  1322. provider,
  1323. {'id': (GCEGatewayContainer._GATEWAY_URL_PREFIX +
  1324. GCEGatewayContainer._DEFAULT_GATEWAY_NAME),
  1325. 'name': GCEGatewayContainer._DEFAULT_GATEWAY_NAME})
  1326. def get_or_create_inet_gateway(self, name):
  1327. GCEInternetGateway.assert_valid_resource_name(name)
  1328. return self._default_internet_gateway
  1329. def delete(self, gateway):
  1330. pass
  1331. def list(self, limit=None, marker=None):
  1332. return ClientPagedResultList(self._provider,
  1333. [self._default_internet_gateway],
  1334. limit=limit, marker=marker)
  1335. class GCEInternetGateway(BaseInternetGateway):
  1336. def __init__(self, provider, gateway):
  1337. super(GCEInternetGateway, self).__init__(provider)
  1338. self._gateway = gateway
  1339. self._fip_container = GCEFloatingIPContainer(provider, self)
  1340. @property
  1341. def id(self):
  1342. return self._gateway['id']
  1343. @property
  1344. def name(self):
  1345. return self._gateway['name']
  1346. def refresh(self):
  1347. pass
  1348. @property
  1349. def state(self):
  1350. return GatewayState.AVAILABLE
  1351. @property
  1352. def network_id(self):
  1353. """
  1354. GCE internet gateways are not attached to a network.
  1355. """
  1356. return None
  1357. def delete(self):
  1358. pass
  1359. @property
  1360. def floating_ips(self):
  1361. return self._fips_container
  1362. class GCESubnet(BaseSubnet):
  1363. def __init__(self, provider, subnet):
  1364. super(GCESubnet, self).__init__(provider)
  1365. self._subnet = subnet
  1366. @property
  1367. def id(self):
  1368. return self._subnet['selfLink']
  1369. @property
  1370. def name(self):
  1371. return self._subnet['name']
  1372. @name.setter
  1373. def name(self, value):
  1374. if value == self.name:
  1375. return
  1376. cb.log.warning('Cannot change the name of a GCE subnetwork')
  1377. @property
  1378. def cidr_block(self):
  1379. return self._subnet['ipCidrRange']
  1380. @property
  1381. def network_url(self):
  1382. return self._subnet['network']
  1383. @property
  1384. def network_id(self):
  1385. return self._provider.parse_url(
  1386. self.network_url).get_resource()['selfLink']
  1387. @property
  1388. def region(self):
  1389. return self._subnet['region']
  1390. @property
  1391. def zone(self):
  1392. return None
  1393. def delete(self):
  1394. return self._provider.networking.subnets.delete(self)
  1395. @property
  1396. def state(self):
  1397. if 'status' in self._subnet and self._subnet['status'] == 'UNKNOWN':
  1398. return SubnetState.UNKNOWN
  1399. return SubnetState.AVAILABEL
  1400. def refresh(self):
  1401. self_link = self._subnet.get('selfLink')
  1402. try:
  1403. new_info = self._provider.parse_url(self_link).get_resource()
  1404. self._subnet = new_info
  1405. except googleapiclient.errors.HttpError:
  1406. # If the resource does not exist, its status is UNKNOWN.
  1407. self._subnet['status'] = 'UNKNOWN'
  1408. class GCEVolume(BaseVolume):
  1409. VOLUME_STATE_MAP = {
  1410. 'RESTORING': VolumeState.CONFIGURING,
  1411. 'PENDING': VolumeState.CONFIGURING,
  1412. 'READY': VolumeState.AVAILABLE,
  1413. 'DONE': VolumeState.AVAILABLE,
  1414. 'RUNNING': VolumeState.IN_USE,
  1415. }
  1416. def __init__(self, provider, volume):
  1417. super(GCEVolume, self).__init__(provider)
  1418. self._volume = volume
  1419. @property
  1420. def id(self):
  1421. return self._volume.get('selfLink')
  1422. @property
  1423. def name(self):
  1424. """
  1425. Get the volume name.
  1426. """
  1427. return self._volume.get('name')
  1428. @property
  1429. def description(self):
  1430. labels = self._volume.get('labels')
  1431. if not labels or 'description' not in labels:
  1432. return ''
  1433. return labels.get('description', '')
  1434. @description.setter
  1435. def description(self, value):
  1436. request_body = {
  1437. 'labels': {'description': value.replace(' ', '_').lower()},
  1438. 'labelFingerprint': self._volume.get('labelFingerprint'),
  1439. }
  1440. try:
  1441. (self._provider
  1442. .gce_compute
  1443. .disks()
  1444. .setLabels(project=self._provider.project_name,
  1445. zone=self._provider.default_zone,
  1446. resource=self.name,
  1447. body=request_body)
  1448. .execute())
  1449. except Exception as e:
  1450. cb.log.warning('Exception while setting volume description: %s. '
  1451. 'Check for invalid characters in description. '
  1452. 'Should confirm to RFC1035.', e)
  1453. raise e
  1454. self.refresh()
  1455. @property
  1456. def size(self):
  1457. return self._volume.get('sizeGb')
  1458. @property
  1459. def create_time(self):
  1460. return self._volume.get('creationTimestamp')
  1461. @property
  1462. def zone_id(self):
  1463. return self._volume.get('zone')
  1464. @property
  1465. def source(self):
  1466. if 'sourceSnapshot' in self._volume:
  1467. snapshot_uri = self._volume.get('sourceSnapshot')
  1468. return GCESnapshot(
  1469. self._provider,
  1470. self._provider.parse_url(snapshot_uri).get_resource())
  1471. if 'sourceImage' in self._volume:
  1472. image_uri = self._volume.get('sourceImage')
  1473. return GCEMachineImage(
  1474. self._provider,
  1475. self._provider.parse_url(image_uri).get_resource())
  1476. return None
  1477. @property
  1478. def attachments(self):
  1479. # GCE Persistent Disk supports multiple instances attaching a READ-ONLY
  1480. # disk. In cloudbridge, volume usage pattern is that a disk is attached
  1481. # to a single instance in a read-write mode. Therefore, we only check
  1482. # the first user of a disk.
  1483. if 'users' in self._volume and len(self._volume) > 0:
  1484. if len(self._volume) > 1:
  1485. cb.log.warning("This volume is attached to multiple instances")
  1486. return BaseAttachmentInfo(self,
  1487. self._volume.get('users')[0],
  1488. None)
  1489. else:
  1490. return None
  1491. def attach(self, instance, device):
  1492. """
  1493. Attach this volume to an instance.
  1494. instance: The ID of an instance or an ``Instance`` object to
  1495. which this volume will be attached.
  1496. To use the disk, the user needs to mount the disk so that the operating
  1497. system can use the available storage space.
  1498. https://cloud.google.com/compute/docs/disks/add-persistent-disk
  1499. """
  1500. attach_disk_body = {
  1501. "source": self.id,
  1502. "deviceName": device,
  1503. }
  1504. instance_name = instance.name if isinstance(
  1505. instance,
  1506. GCEInstance) else instance
  1507. (self._provider
  1508. .gce_compute
  1509. .instances()
  1510. .attachDisk(project=self._provider.project_name,
  1511. zone=self._provider.default_zone,
  1512. instance=instance_name,
  1513. body=attach_disk_body)
  1514. .execute())
  1515. def detach(self, force=False):
  1516. """
  1517. Detach this volume from an instance.
  1518. """
  1519. # Check whether this volume is attached to an instance.
  1520. if not self.attachments:
  1521. return
  1522. parsed_uri = self._provider.parse_url(self.attachments.instance_id)
  1523. instance_data = parsed_uri.get_resource()
  1524. # Check whether the instance has this volume attached.
  1525. if 'disks' not in instance_data:
  1526. return
  1527. device_name = None
  1528. for disk in instance_data['disks']:
  1529. if ('source' in disk and 'deviceName' in disk and
  1530. disk['source'] == self.id):
  1531. device_name = disk['deviceName']
  1532. if not device_name:
  1533. return
  1534. (self._provider
  1535. .gce_compute
  1536. .instances()
  1537. .detachDisk(project=self._provider.project_name,
  1538. zone=self._provider.default_zone,
  1539. instance=instance_data.get('name'),
  1540. deviceName=device_name)
  1541. .execute())
  1542. def create_snapshot(self, name, description=None):
  1543. """
  1544. Create a snapshot of this Volume.
  1545. """
  1546. return self._provider.storage.snapshots.create(
  1547. name, self, description)
  1548. def delete(self):
  1549. """
  1550. Delete this volume.
  1551. """
  1552. (self._provider
  1553. .gce_compute
  1554. .disks()
  1555. .delete(project=self._provider.project_name,
  1556. zone=self._provider.default_zone,
  1557. disk=self.name)
  1558. .execute())
  1559. @property
  1560. def state(self):
  1561. return GCEVolume.VOLUME_STATE_MAP.get(
  1562. self._volume.get('status'), VolumeState.UNKNOWN)
  1563. def refresh(self):
  1564. """
  1565. Refreshes the state of this volume by re-querying the cloud provider
  1566. for its latest state.
  1567. """
  1568. self_link = self._volume.get('selfLink')
  1569. try:
  1570. new_info = self._provider.parse_url(self_link).get_resource()
  1571. self._volume = new_info
  1572. except googleapiclient.errors.HttpError:
  1573. # If the resource does not exist, its status is UNKNOWN.
  1574. self._volume['status'] = 'UNKNOWN'
  1575. class GCESnapshot(BaseSnapshot):
  1576. SNAPSHOT_STATE_MAP = {
  1577. 'PENDING': SnapshotState.PENDING,
  1578. 'READY': SnapshotState.AVAILABLE,
  1579. }
  1580. def __init__(self, provider, snapshot):
  1581. super(GCESnapshot, self).__init__(provider)
  1582. self._snapshot = snapshot
  1583. @property
  1584. def id(self):
  1585. return self._snapshot.get('selfLink')
  1586. @property
  1587. def name(self):
  1588. """
  1589. Get the snapshot name.
  1590. """
  1591. return self._snapshot.get('name')
  1592. @property
  1593. def description(self):
  1594. return self._snapshot.get('description')
  1595. @description.setter
  1596. def description(self, value):
  1597. raise NotImplementedError('Not supported by this provider.')
  1598. @property
  1599. def size(self):
  1600. return self._snapshot.get('diskSizeGb')
  1601. @property
  1602. def volume_id(self):
  1603. return self._snapshot.get('sourceDisk')
  1604. @property
  1605. def create_time(self):
  1606. return self._snapshot.get('creationTimestamp')
  1607. @property
  1608. def state(self):
  1609. return GCESnapshot.SNAPSHOT_STATE_MAP.get(
  1610. self._snapshot.get('status'), SnapshotState.UNKNOWN)
  1611. def refresh(self):
  1612. """
  1613. Refreshes the state of this snapshot by re-querying the cloud provider
  1614. for its latest state.
  1615. """
  1616. self_link = self._snapshot.get('selfLink')
  1617. try:
  1618. new_info = self._provider.parse_url(self_link).get_resource()
  1619. self._snapshot = new_info
  1620. except googleapiclient.errors.HttpError:
  1621. # If the resource does not exist, its status is UNKNOWN.
  1622. self._snapshot['status'] = 'UNKNOWN'
  1623. def delete(self):
  1624. """
  1625. Delete this snapshot.
  1626. """
  1627. (self._provider
  1628. .gce_compute
  1629. .snapshots()
  1630. .delete(project=self._provider.project_name,
  1631. snapshot=self.name)
  1632. .execute())
  1633. def create_volume(self, placement, size=None, volume_type=None, iops=None):
  1634. """
  1635. Create a new Volume from this Snapshot.
  1636. Args:
  1637. placement: GCE zone name, e.g. 'us-central1-f'.
  1638. size: The size of the new volume, in GiB (optional). Defaults to
  1639. the size of the snapshot.
  1640. volume_type: Type of persistent disk. Either 'pd-standard' or
  1641. 'pd-ssd'.
  1642. iops: Not supported by GCE.
  1643. """
  1644. vol_type = 'zones/{0}/diskTypes/{1}'.format(
  1645. placement,
  1646. 'pd-standard' if (volume_type != 'pd-standard' or
  1647. volume_type != 'pd-ssd') else volume_type)
  1648. disk_body = {
  1649. 'name': 'created-from-{0}'.format(self.name),
  1650. 'sizeGb': size if size is not None else self.size,
  1651. 'type': vol_type,
  1652. 'sourceSnapshot': self.id
  1653. }
  1654. operation = (self._provider
  1655. .gce_compute
  1656. .disks()
  1657. .insert(project=self._provider.project_name,
  1658. zone=placement,
  1659. body=disk_body)
  1660. .execute())
  1661. return self._provider.storage.volumes.get(
  1662. operation.get('targetLink'))
  1663. class GCSObject(BaseBucketObject):
  1664. def __init__(self, provider, bucket, obj):
  1665. super(GCSObject, self).__init__(provider)
  1666. self._bucket = bucket
  1667. self._obj = obj
  1668. @property
  1669. def id(self):
  1670. return self._obj['selfLink']
  1671. @property
  1672. def name(self):
  1673. return self._obj['name']
  1674. @property
  1675. def size(self):
  1676. return self._obj['size']
  1677. @property
  1678. def last_modified(self):
  1679. return self._obj['updated']
  1680. def iter_content(self):
  1681. return io.BytesIO(self._provider
  1682. .gcp_storage
  1683. .objects()
  1684. .get_media(bucket=self._obj['bucket'],
  1685. object=self.name)
  1686. .execute())
  1687. def upload(self, data):
  1688. """
  1689. Set the contents of this object to the given text.
  1690. """
  1691. media_body = googleapiclient.http.MediaIoBaseUpload(
  1692. io.BytesIO(data), mimetype='plain/text')
  1693. response = self._bucket.create_object_with_media_body(self.name,
  1694. media_body)
  1695. if response:
  1696. self._obj = response
  1697. def upload_from_file(self, path):
  1698. """
  1699. Upload a binary file.
  1700. """
  1701. with open(path, 'rb') as f:
  1702. media_body = googleapiclient.http.MediaIoBaseUpload(
  1703. f, 'application/octet-stream')
  1704. response = self._bucket.create_object_with_media_body(self.name,
  1705. media_body)
  1706. if response:
  1707. self._obj = response
  1708. def delete(self):
  1709. (self._provider
  1710. .gcp_storage
  1711. .objects()
  1712. .delete(bucket=self._obj['bucket'], object=self.name)
  1713. .execute())
  1714. def generate_url(self, expires_in=0):
  1715. return self._obj['mediaLink']
  1716. class GCSBucket(BaseBucket):
  1717. def __init__(self, provider, bucket):
  1718. super(GCSBucket, self).__init__(provider)
  1719. self._bucket = bucket
  1720. @property
  1721. def id(self):
  1722. return self._bucket['selfLink']
  1723. @property
  1724. def name(self):
  1725. """
  1726. Get this bucket's name.
  1727. """
  1728. return self._bucket['name']
  1729. def get(self, name):
  1730. """
  1731. Retrieve a given object from this bucket.
  1732. """
  1733. try:
  1734. response = (self._provider
  1735. .gcp_storage
  1736. .objects()
  1737. .get(bucket=self.name, object=name)
  1738. .execute())
  1739. if 'error' in response:
  1740. return None
  1741. return GCSObject(self._provider, self, response)
  1742. except googleapiclient.errors.HttpError as http_error:
  1743. cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
  1744. return None
  1745. def list(self, limit=None, marker=None, prefix=None):
  1746. """
  1747. List all objects within this bucket.
  1748. """
  1749. max_result = limit if limit is not None and limit < 500 else 500
  1750. try:
  1751. response = (self._provider
  1752. .gcp_storage
  1753. .objects()
  1754. .list(bucket=self.name,
  1755. prefix=prefix if prefix else '',
  1756. maxResults=max_result,
  1757. pageToken=marker)
  1758. .execute())
  1759. if 'error' in response:
  1760. return ServerPagedResultList(False, None, False, data=[])
  1761. objects = []
  1762. for obj in response.get('items', []):
  1763. objects.append(GCSObject(self._provider, self, obj))
  1764. if len(objects) > max_result:
  1765. cb.log.warning('Expected at most %d results; got %d',
  1766. max_result, len(objects))
  1767. return ServerPagedResultList('nextPageToken' in response,
  1768. response.get('nextPageToken'),
  1769. False, data=objects)
  1770. except googleapiclient.errors.HttpError as http_error:
  1771. cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
  1772. return ServerPagedResultList(False, None, False, data=[])
  1773. def delete(self, delete_contents=False):
  1774. """
  1775. Delete this bucket.
  1776. """
  1777. (self._provider
  1778. .gcp_storage
  1779. .buckets()
  1780. .delete(bucket=self.name)
  1781. .execute())
  1782. def create_object(self, name):
  1783. """
  1784. Create an empty plain text object.
  1785. """
  1786. response = self.create_object_with_media_body(
  1787. name,
  1788. googleapiclient.http.MediaIoBaseUpload(
  1789. io.BytesIO(''), mimetype='plain/text'))
  1790. return GCSObject(self._provider, self, response) if response else None
  1791. def create_object_with_media_body(self, name, media_body):
  1792. self.assert_valid_resource_name(name)
  1793. try:
  1794. response = (self._provider
  1795. .gcp_storage
  1796. .objects()
  1797. .insert(bucket=self.name,
  1798. body={'name': name},
  1799. media_body=media_body)
  1800. .execute())
  1801. if 'error' in response:
  1802. return None
  1803. return response
  1804. except googleapiclient.errors.HttpError as http_error:
  1805. cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
  1806. return None
  1807. class GCELaunchConfig(BaseLaunchConfig):
  1808. def __init__(self, provider):
  1809. super(GCELaunchConfig, self).__init__(provider)