services.py 52 KB

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