services.py 50 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262
  1. import hashlib
  2. import time
  3. import uuid
  4. from collections import namedtuple
  5. import cloudbridge as cb
  6. from cloudbridge.cloud.base.resources import BaseNetwork
  7. from cloudbridge.cloud.base.resources import ClientPagedResultList
  8. from cloudbridge.cloud.base.resources import ServerPagedResultList
  9. from cloudbridge.cloud.base.services import BaseBucketService
  10. from cloudbridge.cloud.base.services import BaseComputeService
  11. from cloudbridge.cloud.base.services import BaseImageService
  12. from cloudbridge.cloud.base.services import BaseInstanceService
  13. from cloudbridge.cloud.base.services import BaseKeyPairService
  14. from cloudbridge.cloud.base.services import BaseNetworkService
  15. from cloudbridge.cloud.base.services import BaseNetworkingService
  16. from cloudbridge.cloud.base.services import BaseRegionService
  17. from cloudbridge.cloud.base.services import BaseRouterService
  18. from cloudbridge.cloud.base.services import BaseSecurityService
  19. from cloudbridge.cloud.base.services import BaseSnapshotService
  20. from cloudbridge.cloud.base.services import BaseStorageService
  21. from cloudbridge.cloud.base.services import BaseSubnetService
  22. from cloudbridge.cloud.base.services import BaseVMFirewallService
  23. from cloudbridge.cloud.base.services import BaseVMTypeService
  24. from cloudbridge.cloud.base.services import BaseVolumeService
  25. from cloudbridge.cloud.interfaces.exceptions import DuplicateResourceException
  26. from cloudbridge.cloud.interfaces.resources import TrafficDirection
  27. from cloudbridge.cloud.interfaces.resources import VMFirewall
  28. from cloudbridge.cloud.providers.gce import helpers
  29. import googleapiclient
  30. from retrying import retry
  31. from .resources import GCEFirewallsDelegate
  32. from .resources import GCEInstance
  33. from .resources import GCEKeyPair
  34. from .resources import GCELaunchConfig
  35. from .resources import GCEMachineImage
  36. from .resources import GCENetwork
  37. from .resources import GCEPlacementZone
  38. from .resources import GCERegion
  39. from .resources import GCERouter
  40. from .resources import GCESnapshot
  41. from .resources import GCESubnet
  42. from .resources import GCEVMFirewall
  43. from .resources import GCEVMType
  44. from .resources import GCEVolume
  45. from .resources import GCSBucket
  46. class GCESecurityService(BaseSecurityService):
  47. def __init__(self, provider):
  48. super(GCESecurityService, self).__init__(provider)
  49. # Initialize provider services
  50. self._key_pairs = GCEKeyPairService(provider)
  51. self._vm_firewalls = GCEVMFirewallService(provider)
  52. @property
  53. def key_pairs(self):
  54. return self._key_pairs
  55. @property
  56. def vm_firewalls(self):
  57. return self._vm_firewalls
  58. class GCEKeyPairService(BaseKeyPairService):
  59. GCEKeyInfo = namedtuple('GCEKeyInfo', 'format public_key email')
  60. def __init__(self, provider):
  61. super(GCEKeyPairService, self).__init__(provider)
  62. self._gce_projects = None
  63. @property
  64. def gce_projects(self):
  65. if not self._gce_projects:
  66. self._gce_projects = self.provider.gce_compute.projects()
  67. return self._gce_projects
  68. def get(self, key_pair_id):
  69. """
  70. Returns a KeyPair given its ID.
  71. """
  72. for kp in self:
  73. if kp.id == key_pair_id:
  74. return kp
  75. else:
  76. return None
  77. def _iter_gce_key_pairs(self):
  78. """
  79. Iterates through the project's metadata, yielding a GCEKeyInfo object
  80. for each entry in commonInstanceMetaData/items
  81. """
  82. metadata = self._get_common_metadata()
  83. for kpinfo in self._iter_gce_ssh_keys(metadata):
  84. yield kpinfo
  85. def _get_common_metadata(self):
  86. """
  87. Get a project's commonInstanceMetadata entry
  88. """
  89. metadata = self.gce_projects.get(
  90. project=self.provider.project_name).execute()
  91. return metadata["commonInstanceMetadata"]
  92. def _get_or_add_sshkey_entry(self, metadata):
  93. """
  94. Get the ssh-keys entry from commonInstanceMetadata/items.
  95. If an entry does not exist, adds a new empty entry
  96. """
  97. sshkey_entry = None
  98. entries = [item for item in metadata.get('items', [])
  99. if item['key'] == 'ssh-keys']
  100. if entries:
  101. sshkey_entry = entries[0]
  102. else: # add a new entry
  103. sshkey_entry = {'key': 'ssh-keys', 'value': ''}
  104. if 'items' not in metadata:
  105. metadata['items'] = [sshkey_entry]
  106. else:
  107. metadata['items'].append(sshkey_entry)
  108. return sshkey_entry
  109. def _iter_gce_ssh_keys(self, metadata):
  110. """
  111. Iterates through the ssh keys given a commonInstanceMetadata dict,
  112. yielding a GCEKeyInfo object for each entry in
  113. commonInstanceMetaData/items
  114. """
  115. sshkeys = self._get_or_add_sshkey_entry(metadata)["value"]
  116. for key in sshkeys.split("\n"):
  117. # elems should be "ssh-rsa <public_key> <email>"
  118. elems = key.split(" ")
  119. if elems and elems[0]: # ignore blank lines
  120. yield GCEKeyPairService.GCEKeyInfo(
  121. elems[0], elems[1].encode('ascii'), elems[2])
  122. def gce_metadata_save_op(self, callback):
  123. """
  124. Carries out a metadata save operation. In GCE, a fingerprint based
  125. locking mechanism is used to prevent lost updates. A new fingerprint
  126. is returned each time metadata is retrieved. Therefore, this method
  127. retrieves the metadata, invokes the provided callback with that
  128. metadata, and saves the metadata using the original fingerprint
  129. immediately afterwards, ensuring that update conflicts can be detected.
  130. """
  131. def _save_common_metadata():
  132. metadata = self._get_common_metadata()
  133. # add a new entry if one doesn'te xist
  134. sshkey_entry = self._get_or_add_sshkey_entry(metadata)
  135. gce_kp_list = callback(self._iter_gce_ssh_keys(metadata))
  136. entry = ""
  137. for gce_kp in gce_kp_list:
  138. entry = entry + u"{0} {1} {2}\n".format(gce_kp.format,
  139. gce_kp.public_key,
  140. gce_kp.email)
  141. sshkey_entry["value"] = entry.rstrip()
  142. # common_metadata will have the current fingerprint at this point
  143. operation = self.gce_projects.setCommonInstanceMetadata(
  144. project=self.provider.project_name, body=metadata).execute()
  145. self.provider.wait_for_operation(operation)
  146. # Retry a few times if the fingerprints conflict
  147. retry_decorator = retry(stop_max_attempt_number=5)
  148. retry_decorator(_save_common_metadata)()
  149. def gce_kp_to_id(self, gce_kp):
  150. """
  151. Accept a GCEKeyInfo object and return a unique
  152. ID for it
  153. """
  154. md5 = hashlib.md5()
  155. md5.update(gce_kp.public_key)
  156. return md5.hexdigest()
  157. def list(self, limit=None, marker=None):
  158. key_pairs = []
  159. for gce_kp in self._iter_gce_key_pairs():
  160. kp_id = self.gce_kp_to_id(gce_kp)
  161. key_pairs.append(GCEKeyPair(self.provider, kp_id, gce_kp.email))
  162. return ClientPagedResultList(self.provider, key_pairs,
  163. limit=limit, marker=marker)
  164. def find(self, **kwargs):
  165. """
  166. Searches for a key pair by a given list of attributes.
  167. """
  168. kp_name = kwargs.get('name', None)
  169. kp_id = kwargs.get('id', None)
  170. for parameter in kwargs:
  171. if parameter not in ('id', 'name'):
  172. cb.log.error('Unrecognised parameters for search: %s. '
  173. 'Supported attributes: id, name', parameter)
  174. out = []
  175. for kp in self:
  176. if kp_name is not None and kp.name != kp_name:
  177. continue
  178. if kp_id is not None and kp.id != kp_id:
  179. continue
  180. out.append(kp)
  181. return out
  182. def create(self, name, public_key_material=None):
  183. GCEKeyPair.assert_valid_resource_name(name)
  184. if self.find(name=name):
  185. raise DuplicateResourceException(
  186. 'A KeyPair with the same name %s exists', name)
  187. private_key = None
  188. if not public_key_material:
  189. private_key, public_key_material = helpers.generate_key_pair()
  190. parts = public_key_material.split(' ')
  191. if len(parts) == 2:
  192. public_key_material = parts[1]
  193. kp_info = GCEKeyPairService.GCEKeyInfo(
  194. '%s:ssh-rsa' % name, public_key_material, name)
  195. def _add_kp(gce_kp_generator):
  196. kp_list = []
  197. # Add the new key pair
  198. kp_list.append(kp_info)
  199. for gce_kp in gce_kp_generator:
  200. kp_list.append(gce_kp)
  201. return kp_list
  202. self.gce_metadata_save_op(_add_kp)
  203. return GCEKeyPair(self.provider, self.gce_kp_to_id(kp_info), name,
  204. private_key)
  205. class GCEVMFirewallService(BaseVMFirewallService):
  206. def __init__(self, provider):
  207. super(GCEVMFirewallService, self).__init__(provider)
  208. self._delegate = GCEFirewallsDelegate(provider)
  209. def get(self, group_id):
  210. tag, network_name = self._delegate.get_tag_network_from_id(group_id)
  211. if tag is None:
  212. return None
  213. network = self.provider.networking.networks.get_by_name(network_name)
  214. return GCEVMFirewall(self._delegate, tag, network)
  215. def list(self, limit=None, marker=None):
  216. vm_firewalls = []
  217. for tag, network_name in self._delegate.tag_networks:
  218. network = self.provider.networking.networks.get_by_name(
  219. network_name)
  220. vm_firewall = GCEVMFirewall(self._delegate, tag, network)
  221. vm_firewalls.append(vm_firewall)
  222. return ClientPagedResultList(self.provider, vm_firewalls,
  223. limit=limit, marker=marker)
  224. def create(self, name, description, network_id=None):
  225. GCEVMFirewall.assert_valid_resource_name(name)
  226. network = self.provider.networking.networks.get(network_id)
  227. fw = GCEVMFirewall(self._delegate, name, network, description)
  228. # This rule exists implicitly. Add it explicitly so that the firewall
  229. # is not empty and the rule is shown by list/get/find methods.
  230. fw.rules.create_with_priority(
  231. direction=TrafficDirection.OUTBOUND, protocol='tcp',
  232. priority=65534, cidr='0.0.0.0/0')
  233. return fw
  234. def find(self, name, limit=None, marker=None):
  235. """
  236. Finds a non-empty VM firewall. If a VM firewall with the given name
  237. does not exist, or if it does not contain any rules, an empty list is
  238. returned.
  239. """
  240. out = []
  241. for tag, network_name in self._delegate.tag_networks:
  242. if tag == name:
  243. network = self.provider.networking.networks.get_by_name(
  244. network_name)
  245. out.append(GCEVMFirewall(self._delegate, name, network))
  246. return out
  247. def delete(self, group_id):
  248. return self._delegate.delete_tag_network_with_id(group_id)
  249. def find_by_network_and_tags(self, network_name, tags):
  250. """
  251. Finds non-empty VM firewalls by network name and VM firewall names
  252. (tags). If no matching VM firewall is found, an empty list is returned.
  253. """
  254. vm_firewalls = []
  255. for tag, net_name in self._delegate.tag_networks:
  256. if network_name != net_name:
  257. continue
  258. if tag not in tags:
  259. continue
  260. network = self.provider.networking.networks.get_by_name(net_name)
  261. vm_firewalls.append(
  262. GCEVMFirewall(self._delegate, tag, network))
  263. return vm_firewalls
  264. class GCEVMTypeService(BaseVMTypeService):
  265. def __init__(self, provider):
  266. super(GCEVMTypeService, self).__init__(provider)
  267. @property
  268. def instance_data(self):
  269. response = (self.provider
  270. .gce_compute
  271. .machineTypes()
  272. .list(project=self.provider.project_name,
  273. zone=self.provider.default_zone)
  274. .execute())
  275. return response['items']
  276. def get(self, vm_type_id):
  277. vm_type = self.provider.get_resource('machineTypes', vm_type_id)
  278. return GCEVMType(self.provider, vm_type) if vm_type else None
  279. def find(self, **kwargs):
  280. matched_inst_types = []
  281. for inst_type in self.instance_data:
  282. is_match = True
  283. for key, value in kwargs.iteritems():
  284. if key not in inst_type:
  285. raise TypeError("The attribute key is not valid.")
  286. if inst_type.get(key) != value:
  287. is_match = False
  288. break
  289. if is_match:
  290. matched_inst_types.append(
  291. GCEVMType(self.provider, inst_type))
  292. return matched_inst_types
  293. def list(self, limit=None, marker=None):
  294. inst_types = [GCEVMType(self.provider, inst_type)
  295. for inst_type in self.instance_data]
  296. return ClientPagedResultList(self.provider, inst_types,
  297. limit=limit, marker=marker)
  298. class GCERegionService(BaseRegionService):
  299. def __init__(self, provider):
  300. super(GCERegionService, self).__init__(provider)
  301. def get(self, region_id):
  302. region = self.provider.get_resource('regions', region_id,
  303. region=region_id)
  304. return GCERegion(self.provider, region) if region else None
  305. def list(self, limit=None, marker=None):
  306. max_result = limit if limit is not None and limit < 500 else 500
  307. regions_response = (self.provider
  308. .gce_compute
  309. .regions()
  310. .list(project=self.provider.project_name,
  311. maxResults=max_result,
  312. pageToken=marker)
  313. .execute())
  314. regions = [GCERegion(self.provider, region)
  315. for region in regions_response['items']]
  316. if len(regions) > max_result:
  317. cb.log.warning('Expected at most %d results; got %d',
  318. max_result, len(regions))
  319. return ServerPagedResultList('nextPageToken' in regions_response,
  320. regions_response.get('nextPageToken'),
  321. False, data=regions)
  322. @property
  323. def current(self):
  324. return self.get(self.provider.region_name)
  325. class GCEImageService(BaseImageService):
  326. def __init__(self, provider):
  327. super(GCEImageService, self).__init__(provider)
  328. self._public_images = None
  329. _PUBLIC_IMAGE_PROJECTS = ['centos-cloud', 'coreos-cloud', 'debian-cloud',
  330. 'opensuse-cloud', 'ubuntu-os-cloud']
  331. def _retrieve_public_images(self):
  332. if self._public_images is not None:
  333. return
  334. self._public_images = []
  335. try:
  336. for project in GCEImageService._PUBLIC_IMAGE_PROJECTS:
  337. for image in helpers.iter_all(
  338. self.provider.gce_compute.images(), project=project):
  339. self._public_images.append(
  340. GCEMachineImage(self.provider, image))
  341. except googleapiclient.errors.HttpError as http_error:
  342. cb.log.warning("googleapiclient.errors.HttpError: {0}".format(
  343. http_error))
  344. def get(self, image_id):
  345. """
  346. Returns an Image given its id
  347. """
  348. image = self.provider.get_resource('images', image_id)
  349. if image:
  350. return GCEMachineImage(self.provider, image)
  351. self._retrieve_public_images()
  352. for public_image in self._public_images:
  353. if public_image.id == image_id or public_image.name == image_id:
  354. return public_image
  355. return None
  356. def find(self, name, limit=None, marker=None):
  357. """
  358. Searches for an image by a given list of attributes
  359. """
  360. filters = {'name': name}
  361. # Retrieve all available images by setting limit to sys.maxsize
  362. images = [image for image in self if image.name == filters['name']]
  363. return ClientPagedResultList(self.provider, images,
  364. limit=limit, marker=marker)
  365. def list(self, limit=None, marker=None):
  366. """
  367. List all images.
  368. """
  369. self._retrieve_public_images()
  370. images = []
  371. if (self.provider.project_name not in
  372. GCEImageService._PUBLIC_IMAGE_PROJECTS):
  373. try:
  374. for image in helpers.iter_all(
  375. self.provider.gce_compute.images(),
  376. project=self.provider.project_name):
  377. images.append(GCEMachineImage(self.provider, image))
  378. except googleapiclient.errors.HttpError as http_error:
  379. cb.log.warning(
  380. "googleapiclient.errors.HttpError: {0}".format(http_error))
  381. images.extend(self._public_images)
  382. return ClientPagedResultList(self.provider, images,
  383. limit=limit, marker=marker)
  384. class GCEInstanceService(BaseInstanceService):
  385. def __init__(self, provider):
  386. super(GCEInstanceService, self).__init__(provider)
  387. def create(self, name, image, vm_type, subnet, zone=None,
  388. key_pair=None, vm_firewalls=None, user_data=None,
  389. launch_config=None, **kwargs):
  390. """
  391. Creates a new virtual machine instance.
  392. """
  393. GCEInstance.assert_valid_resource_name(name)
  394. zone_name = self.provider.default_zone
  395. if zone:
  396. if not isinstance(zone, GCEPlacementZone):
  397. zone = GCEPlacementZone(
  398. self.provider,
  399. self.provider.get_resource('zones', zone, zone=zone))
  400. zone_name = zone.name
  401. if not isinstance(vm_type, GCEVMType):
  402. vm_type = self.provider.compute.vm_types.get(vm_type)
  403. network_interface = {'accessConfigs': [{'type': 'ONE_TO_ONE_NAT',
  404. 'name': 'External NAT'}]}
  405. if subnet:
  406. network_interface['subnetwork'] = subnet.id
  407. else:
  408. network_interface['network'] = 'global/networks/default'
  409. num_roots = 0
  410. disks = []
  411. boot_disk = None
  412. if isinstance(launch_config, GCELaunchConfig):
  413. for disk in launch_config.block_devices:
  414. if not disk.source:
  415. volume_name = 'disk-{0}'.format(uuid.uuid4())
  416. volume_size = disk.size if disk.size else 1
  417. volume = self.provider.storage.volumes.create(
  418. volume_name, volume_size, zone)
  419. volume.wait_till_ready()
  420. source_field = 'source'
  421. source_value = volume.id
  422. elif isinstance(disk.source, GCEMachineImage):
  423. source_field = 'initializeParams'
  424. # Explicitly set diskName; otherwise, instance name will be
  425. # used by default which may collide with existing disks.
  426. source_value = {
  427. 'sourceImage': disk.source.id,
  428. 'diskName': 'image-disk-{0}'.format(uuid.uuid4())}
  429. elif isinstance(disk.source, GCEVolume):
  430. source_field = 'source'
  431. source_value = disk.source.id
  432. elif isinstance(disk.source, GCESnapshot):
  433. volume = disk.source.create_volume(zone, size=disk.size)
  434. volume.wait_till_ready()
  435. source_field = 'source'
  436. source_value = volume.id
  437. else:
  438. cb.log.warning('Unknown disk source')
  439. continue
  440. autoDelete = True
  441. if disk.delete_on_terminate is not None:
  442. autoDelete = disk.delete_on_terminate
  443. num_roots += 1 if disk.is_root else 0
  444. if disk.is_root and not boot_disk:
  445. boot_disk = {'boot': True,
  446. 'autoDelete': autoDelete,
  447. source_field: source_value}
  448. else:
  449. disks.append({'boot': False,
  450. 'autoDelete': autoDelete,
  451. source_field: source_value})
  452. if num_roots > 1:
  453. cb.log.warning('The launch config contains %d boot disks. Will '
  454. 'use the first one', num_roots)
  455. if image:
  456. if boot_disk:
  457. cb.log.warning('A boot image is given while the launch config '
  458. 'contains a boot disk, too. The launch config '
  459. 'will be used')
  460. else:
  461. if not isinstance(image, GCEMachineImage):
  462. image = self.provider.compute.images.get(image)
  463. # Explicitly set diskName; otherwise, instance name will be
  464. # used by default which may conflict with existing disks.
  465. boot_disk = {
  466. 'boot': True,
  467. 'autoDelete': True,
  468. 'initializeParams': {
  469. 'sourceImage': image.id,
  470. 'diskName': 'image-disk-{0}'.format(uuid.uuid4())}}
  471. if not boot_disk:
  472. cb.log.warning('No boot disk is given')
  473. return None
  474. # The boot disk must be the first disk attached to the instance.
  475. disks.insert(0, boot_disk)
  476. config = {
  477. 'name': name,
  478. 'machineType': vm_type.resource_url,
  479. 'disks': disks,
  480. 'networkInterfaces': [network_interface]
  481. }
  482. if vm_firewalls and isinstance(vm_firewalls, list):
  483. vm_firewall_names = []
  484. if isinstance(vm_firewalls[0], VMFirewall):
  485. vm_firewall_names = [f.name for f in vm_firewalls]
  486. elif isinstance(vm_firewalls[0], str):
  487. vm_firewall_names = vm_firewalls
  488. if len(vm_firewall_names) > 0:
  489. config['tags'] = {}
  490. config['tags']['items'] = vm_firewall_names
  491. try:
  492. operation = (self.provider
  493. .gce_compute.instances()
  494. .insert(project=self.provider.project_name,
  495. zone=zone_name,
  496. body=config)
  497. .execute())
  498. except googleapiclient.errors.HttpError as http_error:
  499. # If the operation request fails, the API will raise
  500. # googleapiclient.errors.HttpError.
  501. cb.log.warning(
  502. "googleapiclient.errors.HttpError: {0}".format(http_error))
  503. return None
  504. instance_id = operation.get('targetLink')
  505. self.provider.wait_for_operation(operation, zone=zone_name)
  506. return self.get(instance_id)
  507. def get(self, instance_id):
  508. """
  509. Returns an instance given its name. Returns None
  510. if the object does not exist.
  511. A GCE instance is uniquely identified by its selfLink, which is used
  512. as its id.
  513. """
  514. instance = self.provider.get_resource('instances', instance_id)
  515. return GCEInstance(self.provider, instance) if instance else None
  516. def find(self, name, limit=None, marker=None):
  517. """
  518. Searches for instances by instance name.
  519. :return: a list of Instance objects
  520. """
  521. instances = [instance for instance in self.list()
  522. if instance.name == name]
  523. if limit and len(instances) > limit:
  524. instances = instances[:limit]
  525. return instances
  526. def list(self, limit=None, marker=None):
  527. """
  528. List all instances.
  529. """
  530. # For GCE API, Acceptable values are 0 to 500, inclusive.
  531. # (Default: 500).
  532. max_result = limit if limit is not None and limit < 500 else 500
  533. response = (self.provider
  534. .gce_compute
  535. .instances()
  536. .list(project=self.provider.project_name,
  537. zone=self.provider.default_zone,
  538. maxResults=max_result,
  539. pageToken=marker)
  540. .execute())
  541. instances = [GCEInstance(self.provider, inst)
  542. for inst in response.get('items', [])]
  543. if len(instances) > max_result:
  544. cb.log.warning('Expected at most %d results; got %d',
  545. max_result, len(instances))
  546. return ServerPagedResultList('nextPageToken' in response,
  547. response.get('nextPageToken'),
  548. False, data=instances)
  549. def create_launch_config(self):
  550. return GCELaunchConfig(self.provider)
  551. class GCEComputeService(BaseComputeService):
  552. # TODO: implement GCEComputeService
  553. def __init__(self, provider):
  554. super(GCEComputeService, self).__init__(provider)
  555. self._instance_svc = GCEInstanceService(self.provider)
  556. self._vm_type_svc = GCEVMTypeService(self.provider)
  557. self._region_svc = GCERegionService(self.provider)
  558. self._images_svc = GCEImageService(self.provider)
  559. @property
  560. def images(self):
  561. return self._images_svc
  562. @property
  563. def vm_types(self):
  564. return self._vm_type_svc
  565. @property
  566. def instances(self):
  567. return self._instance_svc
  568. @property
  569. def regions(self):
  570. return self._region_svc
  571. class GCENetworkingService(BaseNetworkingService):
  572. def __init__(self, provider):
  573. super(GCENetworkingService, self).__init__(provider)
  574. self._network_service = GCENetworkService(self.provider)
  575. self._subnet_service = GCESubnetService(self.provider)
  576. self._router_service = GCERouterService(self.provider)
  577. @property
  578. def networks(self):
  579. return self._network_service
  580. @property
  581. def subnets(self):
  582. return self._subnet_service
  583. @property
  584. def routers(self):
  585. return self._router_service
  586. class GCENetworkService(BaseNetworkService):
  587. def __init__(self, provider):
  588. super(GCENetworkService, self).__init__(provider)
  589. def get(self, network_id):
  590. network = self.provider.get_resource('networks', network_id)
  591. return GCENetwork(self.provider, network) if network else None
  592. def find(self, name, limit=None, marker=None):
  593. """
  594. GCE networks are global. There is at most one network with a given
  595. name.
  596. """
  597. network = self.get(name)
  598. return [network] if network else []
  599. def get_by_name(self, network_name):
  600. if network_name is None:
  601. return None
  602. networks = self.list(filter='name eq %s' % network_name)
  603. return None if len(networks) == 0 else networks[0]
  604. def list(self, limit=None, marker=None, filter=None):
  605. networks = []
  606. try:
  607. response = (self.provider
  608. .gce_compute
  609. .networks()
  610. .list(project=self.provider.project_name,
  611. filter=filter)
  612. .execute())
  613. for network in response.get('items', []):
  614. networks.append(GCENetwork(self.provider, network))
  615. except googleapiclient.errors.HttpError as http_error:
  616. cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
  617. return ClientPagedResultList(self.provider, networks,
  618. limit=limit, marker=marker)
  619. def _create(self, name, cidr_block, create_subnetworks):
  620. """
  621. Possible values for 'create_subnetworks' are:
  622. None: For creating a legacy (non-subnetted) network.
  623. True: For creating an auto mode VPC network. This also creates a
  624. subnetwork in every region.
  625. False: For creating a custom mode VPC network. Subnetworks should be
  626. created manually.
  627. """
  628. if create_subnetworks is not None and cidr_block is not None:
  629. cb.log.warning('cidr_block is ignored in non-legacy networks. '
  630. 'Auto mode networks use the default CIDR of '
  631. '%s. For custom networks, you should create subnets'
  632. 'in each region with explicit CIDR blocks',
  633. GCENetwork.DEFAULT_IPV4RANGE)
  634. cidr_block = None
  635. networks = self.list(filter='name eq %s' % name)
  636. if len(networks) > 0:
  637. return networks[0]
  638. body = {'name': name}
  639. if cidr_block:
  640. body['IPv4Range'] = cidr_block
  641. else:
  642. body['autoCreateSubnetworks'] = create_subnetworks
  643. try:
  644. response = (self.provider
  645. .gce_compute
  646. .networks()
  647. .insert(project=self.provider.project_name,
  648. body=body)
  649. .execute())
  650. if 'error' in response:
  651. return None
  652. self.provider.wait_for_operation(response)
  653. networks = self.list(filter='name eq %s' % name)
  654. return None if len(networks) == 0 else networks[0]
  655. except googleapiclient.errors.HttpError as http_error:
  656. cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
  657. return None
  658. def create(self, name, cidr_block):
  659. """
  660. Creates an auto mode VPC network with default subnets. It is possible
  661. to add additional subnets later.
  662. """
  663. GCENetwork.assert_valid_resource_name(name)
  664. return self._create(name, cidr_block, False)
  665. def get_or_create_default(self):
  666. return self._create(GCEFirewallsDelegate.DEFAULT_NETWORK, None, True)
  667. class GCERouterService(BaseRouterService):
  668. def __init__(self, provider):
  669. super(GCERouterService, self).__init__(provider)
  670. def get(self, router_id):
  671. return self._get_in_region(router_id)
  672. def find(self, name, limit=None, marker=None):
  673. routers = []
  674. for region in self.provider.compute.regions.list():
  675. router = self._get_in_region(name, region.name)
  676. if router:
  677. routers.append(router)
  678. return ClientPagedResultList(self.provider, routers, limit=limit,
  679. marker=marker)
  680. def list(self, limit=None, marker=None):
  681. region = self.provider.region_name
  682. max_result = limit if limit is not None and limit < 500 else 500
  683. response = (self.provider
  684. .gce_compute
  685. .routers()
  686. .list(project=self.provider.project_name,
  687. region=region,
  688. maxResults=max_result,
  689. pageToken=marker)
  690. .execute())
  691. routers = []
  692. for router in response.get('items', []):
  693. routers.append(GCERouter(self.provider, router))
  694. if len(routers) > max_result:
  695. cb.log.warning('Expected at most %d results; go %d',
  696. max_result, len(routers))
  697. return ServerPagedResultList('nextPageToken' in response,
  698. response.get('nextPageToken'),
  699. False, data=routers)
  700. def create(self, name, network):
  701. name = name if name else 'router-{0}'.format(uuid.uuid4())
  702. GCERouter.assert_valid_resource_name(name)
  703. if not isinstance(network, GCENetwork):
  704. network = self.provider.networking.networks.get(network)
  705. network_url = network.resource_url
  706. region_name = self.provider.region_name
  707. try:
  708. response = (self.provider
  709. .gce_compute
  710. .routers()
  711. .insert(project=self.provider.project_name,
  712. region=region_name,
  713. body={'name': name,
  714. 'network': network_url})
  715. .execute())
  716. if 'error' in response:
  717. return None
  718. self.provider.wait_for_operation(response, region=region_name)
  719. return self._get_in_region(name, region_name)
  720. except googleapiclient.errors.HttpError as http_error:
  721. cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
  722. return None
  723. def delete(self, router):
  724. region_name = self.provider.region_name
  725. name = router.name if isinstance(router, GCERouter) else router
  726. response = (self.provider
  727. .gce_compute
  728. .routers()
  729. .delete(project=self.provider.project_name,
  730. region=region_name,
  731. router=name)
  732. .execute())
  733. self._provider.wait_for_operation(response, region=region_name)
  734. def _get_in_region(self, router_id, region=None):
  735. region_name = self.provider.region_name
  736. if region:
  737. if not isinstance(region, GCERegion):
  738. region = self.provider.compute.regions.get(region)
  739. region_name = region.name
  740. router = self.provider.get_resource(
  741. 'routers', router_id, region=region_name)
  742. return GCERouter(self.provider, router) if router else None
  743. class GCESubnetService(BaseSubnetService):
  744. def __init__(self, provider):
  745. super(GCESubnetService, self).__init__(provider)
  746. def get(self, subnet_id):
  747. subnet = self.provider.get_resource('subnetworks', subnet_id)
  748. return GCESubnet(self.provider, subnet) if subnet else None
  749. def list(self, network=None, zone=None, limit=None, marker=None):
  750. """
  751. If the zone is not given, we list all subnetworks, in all regions.
  752. """
  753. filter = None
  754. if network is not None:
  755. filter = 'network eq %s' % network.resource_url
  756. region_names = []
  757. if zone:
  758. region_names.append(self._zone_to_region_name(zone))
  759. else:
  760. for r in self.provider.compute.regions.list():
  761. region_names.append(r.name)
  762. subnets = []
  763. for region_name in region_names:
  764. response = (self.provider
  765. .gce_compute
  766. .subnetworks()
  767. .list(project=self.provider.project_name,
  768. region=region_name,
  769. filter=filter)
  770. .execute())
  771. for subnet in response.get('items', []):
  772. subnets.append(GCESubnet(self.provider, subnet))
  773. return ClientPagedResultList(self.provider, subnets,
  774. limit=limit, marker=marker)
  775. def create(self, network, cidr_block, name=None, zone=None):
  776. """
  777. GCE subnets are regional. The region is inferred from the zone if a
  778. zone is provided; otherwise, the default region, as set in the
  779. provider, is used.
  780. If a subnet with overlapping IP range exists already, we return that
  781. instead of creating a new subnet. In this case, other parameters, i.e.
  782. the name and the zone, are ignored.
  783. """
  784. if not name:
  785. name = 'subnet-{0}'.format(uuid.uuid4())
  786. GCESubnet.assert_valid_resource_name(name)
  787. region_name = self._zone_to_region_name(zone)
  788. for subnet in self.iter(network=network):
  789. if BaseNetwork.cidr_blocks_overlap(subnet.cidr_block, cidr_block):
  790. if subnet.region_name != region_name:
  791. cb.log.error('Failed to create subnetwork in region %s: '
  792. 'the given IP range %s overlaps with a '
  793. 'subnetwork in a different region %s',
  794. region_name, cidr_block, subnet.region_name)
  795. return None
  796. return subnet
  797. if subnet.name == name and subnet.region_name == region_name:
  798. return subnet
  799. body = {'ipCidrRange': cidr_block,
  800. 'name': name,
  801. 'network': network.resource_url,
  802. 'region': region_name}
  803. try:
  804. response = (self.provider
  805. .gce_compute
  806. .subnetworks()
  807. .insert(project=self.provider.project_name,
  808. region=region_name,
  809. body=body)
  810. .execute())
  811. if 'error' in response:
  812. cb.log.warning('Error while creating a subnet: %s',
  813. response['error'])
  814. return None
  815. self.provider.wait_for_operation(response, region=region_name)
  816. return self.get(name)
  817. except googleapiclient.errors.HttpError as http_error:
  818. cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
  819. return None
  820. def get_or_create_default(self, zone=None):
  821. """
  822. Every GCP project comes with a default auto mode VPC network. An auto
  823. mode VPC network has exactly one subnetwork per region. This method
  824. returns the subnetwork of the default network that spans the given
  825. zone.
  826. """
  827. network = self.provider.networking.networks.get_or_create_default()
  828. subnets = list(self.iter(network=network, zone=zone))
  829. if len(subnets) > 1:
  830. cb.log.warning('The default network has more than one subnetwork '
  831. 'in a region')
  832. if len(subnets) > 0:
  833. return subnets[0]
  834. cb.log.warning('The default network has no subnetwork in a region')
  835. return None
  836. def delete(self, subnet):
  837. try:
  838. response = (self.provider
  839. .gce_compute
  840. .subnetworks()
  841. .delete(project=self.provider.project_name,
  842. region=subnet.region_name,
  843. subnetwork=subnet.name)
  844. .execute())
  845. self._provider.wait_for_operation(
  846. response, region=subnet.region_name)
  847. except googleapiclient.errors.HttpError as http_error:
  848. cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
  849. def _zone_to_region_name(self, zone):
  850. if zone:
  851. if not isinstance(zone, GCEPlacementZone):
  852. zone = GCEPlacementZone(
  853. self.provider,
  854. self.provider.get_resource('zones', zone, zone=zone))
  855. return zone.region_name
  856. return self.provider.region_name
  857. class GCPStorageService(BaseStorageService):
  858. def __init__(self, provider):
  859. super(GCPStorageService, self).__init__(provider)
  860. # Initialize provider services
  861. self._volume_svc = GCEVolumeService(self.provider)
  862. self._snapshot_svc = GCESnapshotService(self.provider)
  863. self._bucket_svc = GCSBucketService(self.provider)
  864. @property
  865. def volumes(self):
  866. return self._volume_svc
  867. @property
  868. def snapshots(self):
  869. return self._snapshot_svc
  870. @property
  871. def buckets(self):
  872. return self._bucket_svc
  873. class GCEVolumeService(BaseVolumeService):
  874. def __init__(self, provider):
  875. super(GCEVolumeService, self).__init__(provider)
  876. def get(self, volume_id):
  877. """
  878. Returns a volume given its id.
  879. """
  880. vol = self.provider.get_resource('disks', volume_id)
  881. return GCEVolume(self.provider, vol) if vol else None
  882. def find(self, name, limit=None, marker=None):
  883. """
  884. Searches for a volume by a given list of attributes.
  885. """
  886. filtr = 'name eq ' + name
  887. max_result = limit if limit is not None and limit < 500 else 500
  888. response = (self.provider
  889. .gce_compute
  890. .disks()
  891. .list(project=self.provider.project_name,
  892. zone=self.provider.default_zone,
  893. filter=filtr,
  894. maxResults=max_result,
  895. pageToken=marker)
  896. .execute())
  897. gce_vols = [GCEVolume(self.provider, vol)
  898. for vol in response.get('items', [])]
  899. if len(gce_vols) > max_result:
  900. cb.log.warning('Expected at most %d results; got %d',
  901. max_result, len(gce_vols))
  902. return ServerPagedResultList('nextPageToken' in response,
  903. response.get('nextPageToken'),
  904. False, data=gce_vols)
  905. def list(self, limit=None, marker=None):
  906. """
  907. List all volumes.
  908. limit: The maximum number of volumes to return. The returned
  909. ResultList's is_truncated property can be used to determine
  910. whether more records are available.
  911. """
  912. # For GCE API, Acceptable values are 0 to 500, inclusive.
  913. # (Default: 500).
  914. max_result = limit if limit is not None and limit < 500 else 500
  915. response = (self.provider
  916. .gce_compute
  917. .disks()
  918. .list(project=self.provider.project_name,
  919. zone=self.provider.default_zone,
  920. maxResults=max_result,
  921. pageToken=marker)
  922. .execute())
  923. gce_vols = [GCEVolume(self.provider, vol)
  924. for vol in response.get('items', [])]
  925. if len(gce_vols) > max_result:
  926. cb.log.warning('Expected at most %d results; got %d',
  927. max_result, len(gce_vols))
  928. return ServerPagedResultList('nextPageToken' in response,
  929. response.get('nextPageToken'),
  930. False, data=gce_vols)
  931. def create(self, name, size, zone, snapshot=None, description=None):
  932. """
  933. Creates a new volume.
  934. Argument `name` must be 1-63 characters long, and comply with RFC1035.
  935. Specifically, the name must be 1-63 characters long and match the
  936. regular expression [a-z]([-a-z0-9]*[a-z0-9])? which means the first
  937. character must be a lowercase letter, and all following characters must
  938. be a dash, lowercase letter, or digit, except the last character, which
  939. cannot be a dash.
  940. """
  941. GCEVolume.assert_valid_resource_name(name)
  942. if not isinstance(zone, GCEPlacementZone):
  943. zone = GCEPlacementZone(
  944. self.provider,
  945. self.provider.get_resource('zones', zone, zone=zone))
  946. zone_name = zone.name
  947. snapshot_id = snapshot.id if isinstance(
  948. snapshot, GCESnapshot) and snapshot else snapshot
  949. disk_body = {
  950. 'name': name,
  951. 'sizeGb': size,
  952. 'type': 'zones/{0}/diskTypes/{1}'.format(zone_name, 'pd-standard'),
  953. 'sourceSnapshot': snapshot_id,
  954. 'description': description,
  955. }
  956. operation = (self.provider
  957. .gce_compute
  958. .disks()
  959. .insert(
  960. project=self._provider.project_name,
  961. zone=zone_name,
  962. body=disk_body)
  963. .execute())
  964. return self.get(operation.get('targetLink'))
  965. class GCESnapshotService(BaseSnapshotService):
  966. def __init__(self, provider):
  967. super(GCESnapshotService, self).__init__(provider)
  968. def get(self, snapshot_id):
  969. """
  970. Returns a snapshot given its id.
  971. """
  972. snapshot = self.provider.get_resource('snapshots', snapshot_id)
  973. return GCESnapshot(self.provider, snapshot) if snapshot else None
  974. def find(self, name, limit=None, marker=None):
  975. """
  976. Searches for a snapshot by a given list of attributes.
  977. """
  978. filtr = 'name eq ' + name
  979. max_result = limit if limit is not None and limit < 500 else 500
  980. response = (self.provider
  981. .gce_compute
  982. .snapshots()
  983. .list(project=self.provider.project_name,
  984. filter=filtr,
  985. maxResults=max_result,
  986. pageToken=marker)
  987. .execute())
  988. snapshots = [GCESnapshot(self.provider, snapshot)
  989. for snapshot in response.get('items', [])]
  990. if len(snapshots) > max_result:
  991. cb.log.warning('Expected at most %d results; got %d',
  992. max_result, len(snapshots))
  993. return ServerPagedResultList('nextPageToken' in response,
  994. response.get('nextPageToken'),
  995. False, data=snapshots)
  996. def list(self, limit=None, marker=None):
  997. """
  998. List all snapshots.
  999. """
  1000. max_result = limit if limit is not None and limit < 500 else 500
  1001. response = (self.provider
  1002. .gce_compute
  1003. .snapshots()
  1004. .list(project=self.provider.project_name,
  1005. maxResults=max_result,
  1006. pageToken=marker)
  1007. .execute())
  1008. snapshots = [GCESnapshot(self.provider, snapshot)
  1009. for snapshot in response.get('items', [])]
  1010. if len(snapshots) > max_result:
  1011. cb.log.warning('Expected at most %d results; got %d',
  1012. max_result, len(snapshots))
  1013. return ServerPagedResultList('nextPageToken' in response,
  1014. response.get('nextPageToken'),
  1015. False, data=snapshots)
  1016. def create(self, name, volume, description=None):
  1017. """
  1018. Creates a new snapshot of a given volume.
  1019. """
  1020. GCESnapshot.assert_valid_resource_name(name)
  1021. volume_name = volume.name if isinstance(volume, GCEVolume) else volume
  1022. snapshot_body = {
  1023. "name": name,
  1024. "description": description
  1025. }
  1026. operation = (self.provider
  1027. .gce_compute
  1028. .disks()
  1029. .createSnapshot(
  1030. project=self.provider.project_name,
  1031. zone=self.provider.default_zone,
  1032. disk=volume_name, body=snapshot_body)
  1033. .execute())
  1034. if 'zone' not in operation:
  1035. return None
  1036. self.provider.wait_for_operation(operation,
  1037. zone=self.provider.default_zone)
  1038. snapshots = self.provider.storage.snapshots.find(name=name)
  1039. if snapshots:
  1040. return snapshots[0]
  1041. else:
  1042. return None
  1043. class GCSBucketService(BaseBucketService):
  1044. def __init__(self, provider):
  1045. super(GCSBucketService, self).__init__(provider)
  1046. def get(self, bucket_id):
  1047. """
  1048. Returns a bucket given its ID. Returns ``None`` if the bucket
  1049. does not exist or if the user does not have permission to access the
  1050. bucket.
  1051. """
  1052. bucket = self.provider.get_resource('buckets', bucket_id)
  1053. return GCSBucket(self.provider, bucket) if bucket else None
  1054. def find(self, name, limit=None, marker=None):
  1055. """
  1056. Searches in bucket names for a substring.
  1057. """
  1058. buckets = [bucket for bucket in self if name in bucket.name]
  1059. return ClientPagedResultList(self.provider, buckets, limit=limit,
  1060. marker=marker)
  1061. def list(self, limit=None, marker=None):
  1062. """
  1063. List all containers.
  1064. """
  1065. max_result = limit if limit is not None and limit < 500 else 500
  1066. try:
  1067. response = (self.provider
  1068. .gcs_storage
  1069. .buckets()
  1070. .list(project=self.provider.project_name,
  1071. maxResults=max_result,
  1072. pageToken=marker)
  1073. .execute())
  1074. if 'error' in response:
  1075. return ServerPagedResultList(False, None, False, data=[])
  1076. buckets = []
  1077. for bucket in response.get('items', []):
  1078. buckets.append(GCSBucket(self.provider, bucket))
  1079. if len(buckets) > max_result:
  1080. cb.log.warning('Expected at most %d results; got %d',
  1081. max_result, len(buckets))
  1082. return ServerPagedResultList('nextPageToken' in response,
  1083. response.get('nextPageToken'),
  1084. False, data=buckets)
  1085. except googleapiclient.errors.HttpError as http_error:
  1086. cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
  1087. return ServerPagedResultList(False, None, False, data=[])
  1088. def create(self, name, location=None):
  1089. """
  1090. Create a new bucket and returns it. Returns None if creation fails.
  1091. """
  1092. GCSBucket.assert_valid_resource_name(name)
  1093. body = {'name': name}
  1094. if location:
  1095. body['location'] = location
  1096. try:
  1097. response = (self.provider
  1098. .gcs_storage
  1099. .buckets()
  1100. .insert(project=self.provider.project_name,
  1101. body=body)
  1102. .execute())
  1103. if 'error' in response:
  1104. return None
  1105. # GCS has a rate limit of 1 operation per 2 seconds for bucket
  1106. # creation/deletion: https://cloud.google.com/storage/quotas.
  1107. # Throttle here to avoid future failures.
  1108. time.sleep(2)
  1109. return GCSBucket(self.provider, response)
  1110. except googleapiclient.errors.HttpError as http_error:
  1111. cb.log.warning('googleapiclient.errors.HttpError: %s', http_error)
  1112. return None