services.py 45 KB

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