resources.py 70 KB

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