services.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. from cloudbridge.cloud.base.resources import ClientPagedResultList
  2. from cloudbridge.cloud.base.services import BaseComputeService
  3. from cloudbridge.cloud.base.services import BaseImageService
  4. from cloudbridge.cloud.base.services import BaseInstanceTypesService
  5. from cloudbridge.cloud.base.services import BaseKeyPairService
  6. from cloudbridge.cloud.base.services import BaseNetworkService
  7. from cloudbridge.cloud.base.services import BaseRegionService
  8. from cloudbridge.cloud.base.services import BaseSecurityGroupService
  9. from cloudbridge.cloud.base.services import BaseSecurityService
  10. from cloudbridge.cloud.providers.gce import helpers
  11. import cloudbridge as cb
  12. from collections import namedtuple
  13. import hashlib
  14. import googleapiclient
  15. from retrying import retry
  16. import sys
  17. from .resources import GCEFirewallsDelegate
  18. from .resources import GCEMachineImage
  19. from .resources import GCENetwork
  20. from .resources import GCEInstanceType
  21. from .resources import GCEKeyPair
  22. from .resources import GCERegion
  23. from .resources import GCESecurityGroup
  24. from .resources import GCESecurityGroupRule
  25. class GCESecurityService(BaseSecurityService):
  26. def __init__(self, provider):
  27. super(GCESecurityService, self).__init__(provider)
  28. # Initialize provider services
  29. self._key_pairs = GCEKeyPairService(provider)
  30. self._security_groups = GCESecurityGroupService(provider)
  31. @property
  32. def key_pairs(self):
  33. return self._key_pairs
  34. @property
  35. def security_groups(self):
  36. return self._security_groups
  37. class GCEKeyPairService(BaseKeyPairService):
  38. GCEKeyInfo = namedtuple('GCEKeyInfo', 'format public_key email')
  39. def __init__(self, provider):
  40. super(GCEKeyPairService, self).__init__(provider)
  41. self._gce_projects = None
  42. @property
  43. def gce_projects(self):
  44. if not self._gce_projects:
  45. self._gce_projects = self.provider.gce_compute.projects()
  46. return self._gce_projects
  47. def get(self, key_pair_id):
  48. """
  49. Returns a KeyPair given its ID.
  50. """
  51. for kp in self.list():
  52. if kp.id == key_pair_id:
  53. return kp
  54. else:
  55. return None
  56. def _iter_gce_key_pairs(self):
  57. """
  58. Iterates through the project's metadata, yielding a GCEKeyInfo object
  59. for each entry in commonInstanceMetaData/items
  60. """
  61. metadata = self._get_common_metadata()
  62. for kpinfo in self._iter_gce_ssh_keys(metadata):
  63. yield kpinfo
  64. def _get_common_metadata(self):
  65. """
  66. Get a project's commonInstanceMetadata entry
  67. """
  68. metadata = self.gce_projects.get(
  69. project=self.provider.project_name).execute()
  70. return metadata["commonInstanceMetadata"]
  71. def _get_or_add_sshkey_entry(self, metadata):
  72. """
  73. Get the sshKeys 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["items"]
  78. if item["key"] == "sshKeys"]
  79. if entries:
  80. sshkey_entry = entries[0]
  81. else: # add a new entry
  82. sshkey_entry = {"key": "sshKeys", "value": ""}
  83. metadata["items"].append(sshkey_entry)
  84. return sshkey_entry
  85. def _iter_gce_ssh_keys(self, metadata):
  86. """
  87. Iterates through the ssh keys given a commonInstanceMetadata dict,
  88. yielding a GCEKeyInfo object for each entry in
  89. commonInstanceMetaData/items
  90. """
  91. sshkeys = self._get_or_add_sshkey_entry(metadata)["value"]
  92. for key in sshkeys.split("\n"):
  93. # elems should be "ssh-rsa <public_key> <email>"
  94. elems = key.split(" ")
  95. if elems and elems[0]: # ignore blank lines
  96. yield GCEKeyPairService.GCEKeyInfo(elems[0], elems[1],
  97. elems[2])
  98. def gce_metadata_save_op(self, callback):
  99. """
  100. Carries out a metadata save operation. In GCE, a fingerprint based
  101. locking mechanism is used to prevent lost updates. A new fingerprint
  102. is returned each time metadata is retrieved. Therefore, this method
  103. retrieves the metadata, invokes the provided callback with that
  104. metadata, and saves the metadata using the original fingerprint
  105. immediately afterwards, ensuring that update conflicts can be detected.
  106. """
  107. def _save_common_metadata():
  108. metadata = self._get_common_metadata()
  109. # add a new entry if one doesn'te xist
  110. sshkey_entry = self._get_or_add_sshkey_entry(metadata)
  111. gce_kp_list = callback(self._iter_gce_ssh_keys(metadata))
  112. entry = ""
  113. for gce_kp in gce_kp_list:
  114. entry = entry + u"{0} {1} {2}\n".format(gce_kp.format,
  115. gce_kp.public_key,
  116. gce_kp.email)
  117. sshkey_entry["value"] = entry.rstrip()
  118. # common_metadata will have the current fingerprint at this point
  119. operation = self.gce_projects.setCommonInstanceMetadata(
  120. project=self.provider.project_name, body=metadata).execute()
  121. self.provider.wait_for_global_operation(operation)
  122. # Retry a few times if the fingerprints conflict
  123. retry_decorator = retry(stop_max_attempt_number=5)
  124. retry_decorator(_save_common_metadata)()
  125. def gce_kp_to_id(self, gce_kp):
  126. """
  127. Accept a GCEKeyInfo object and return a unique
  128. ID for it
  129. """
  130. md5 = hashlib.md5()
  131. md5.update(gce_kp.public_key)
  132. return md5.hexdigest()
  133. def list(self, limit=None, marker=None):
  134. key_pairs = []
  135. for gce_kp in self._iter_gce_key_pairs():
  136. kp_id = self.gce_kp_to_id(gce_kp)
  137. kp_name = gce_kp.email
  138. key_pairs.append(GCEKeyPair(self.provider, kp_id, kp_name))
  139. return ClientPagedResultList(self.provider, key_pairs,
  140. limit=limit, marker=marker)
  141. def find(self, name, limit=None, marker=None):
  142. """
  143. Searches for a key pair by a given list of attributes.
  144. """
  145. found_kps = []
  146. for kp in self.list():
  147. if kp.name == name:
  148. found_kps.append(kp)
  149. return ClientPagedResultList(self.provider, found_kps,
  150. limit=limit, marker=marker)
  151. def create(self, name):
  152. kp = self.find(name=name)
  153. if kp:
  154. return kp
  155. private_key, public_key = helpers.generate_key_pair()
  156. kp_info = GCEKeyPairService.GCEKeyInfo(name + u":ssh-rsa",
  157. public_key, name)
  158. def _add_kp(gce_kp_generator):
  159. kp_list = []
  160. # Add the new key pair
  161. kp_list.append(kp_info)
  162. for gce_kp in gce_kp_generator:
  163. kp_list.append(gce_kp)
  164. return kp_list
  165. self.gce_metadata_save_op(_add_kp)
  166. return GCEKeyPair(self.provider, self.gce_kp_to_id(kp_info), name,
  167. kp_material=private_key)
  168. class GCESecurityGroupService(BaseSecurityGroupService):
  169. def __init__(self, provider):
  170. super(GCESecurityGroupService, self).__init__(provider)
  171. self._delegate = GCEFirewallsDelegate(provider)
  172. def get(self, group_id):
  173. tag, network_name = self._delegate.get_tag_network_from_id(group_id)
  174. if tag is None:
  175. return None
  176. network = self.provider.network.get_by_name(network_name)
  177. return GCESecurityGroup(self._delegate, tag, network)
  178. def list(self, limit=None, marker=None):
  179. security_groups = []
  180. for tag, network_name in self._delegate.tag_networks:
  181. network = self.provider.network.get_by_name(network_name)
  182. security_group = GCESecurityGroup(self._delegate, tag, network)
  183. security_groups.append(security_group)
  184. return ClientPagedResultList(self.provider, security_groups,
  185. limit=limit, marker=marker)
  186. def create(self, name, description, network_id=None):
  187. network = self.provider.network.get(network_id)
  188. return GCESecurityGroup(self._delegate, name, network, description)
  189. def find(self, name, limit=None, marker=None):
  190. """
  191. Finds a non-empty security group. If a security group with the given
  192. name does not exist, or if it does not contain any rules, an empty list
  193. is returned.
  194. """
  195. out = []
  196. for tag, network_name in self._delegate.tag_networks:
  197. if tag == name:
  198. network = self.provider.network.get_by_name(network_name)
  199. out.append(GCESecurityGroup(self._delegate, name, network))
  200. return out
  201. def delete(self, group_id):
  202. return self._delegate.delete_tag_network_with_id(group_id)
  203. class GCEInstanceTypesService(BaseInstanceTypesService):
  204. def __init__(self, provider):
  205. super(GCEInstanceTypesService, self).__init__(provider)
  206. @property
  207. def instance_data(self):
  208. response = self.provider.gce_compute \
  209. .machineTypes() \
  210. .list(project=self.provider.project_name,
  211. zone=self.provider.default_zone) \
  212. .execute()
  213. return response['items']
  214. def get(self, instance_type_id):
  215. for inst_type in self.instance_data:
  216. if inst_type.get('id') == instance_type_id:
  217. return GCEInstanceType(self.provider, inst_type)
  218. return None
  219. def find(self, **kwargs):
  220. matched_inst_types = []
  221. for inst_type in self.instance_data:
  222. is_match = True
  223. for key, value in kwargs.iteritems():
  224. if key not in inst_type:
  225. raise TypeError("The attribute key is not valid.")
  226. if inst_type.get(key) != value:
  227. is_match = False
  228. break
  229. if is_match:
  230. matched_inst_types.append(
  231. GCEInstanceType(self.provider, inst_type))
  232. return matched_inst_types
  233. def list(self, limit=None, marker=None):
  234. inst_types = [GCEInstanceType(self.provider, inst_type)
  235. for inst_type in self.instance_data]
  236. return ClientPagedResultList(self.provider, inst_types,
  237. limit=limit, marker=marker)
  238. class GCERegionService(BaseRegionService):
  239. def __init__(self, provider):
  240. super(GCERegionService, self).__init__(provider)
  241. def get(self, region_id):
  242. try:
  243. region = self.provider.gce_compute \
  244. .regions() \
  245. .get(project=self.provider.project_name,
  246. region=region_id) \
  247. .execute()
  248. # Handle the case when region_id is not valid
  249. except googleapiclient.errors.HttpError:
  250. return None
  251. if region:
  252. return GCERegion(self.provider, region)
  253. else:
  254. return None
  255. def list(self, limit=None, marker=None):
  256. regions_response = self.provider.gce_compute.regions().list(
  257. project=self.provider.project_name).execute()
  258. regions = [GCERegion(self.provider, region)
  259. for region in regions_response['items']]
  260. return ClientPagedResultList(self.provider, regions,
  261. limit=limit, marker=marker)
  262. @property
  263. def current(self):
  264. return self.get(self.provider.region_name)
  265. class GCEImageService(BaseImageService):
  266. def __init__(self, provider):
  267. super(GCEImageService, self).__init__(provider)
  268. self._public_images = None
  269. _PUBLIC_IMAGE_PROJECTS = ['centos-cloud', 'coreos-cloud', 'debian-cloud',
  270. 'opensuse-cloud', 'ubuntu-os-cloud']
  271. def _retrieve_public_images(self):
  272. if self._public_images is not None:
  273. return
  274. self._public_images = []
  275. for project in GCEImageService._PUBLIC_IMAGE_PROJECTS:
  276. try:
  277. response = self.provider.gce_compute \
  278. .images() \
  279. .list(project=project) \
  280. .execute()
  281. except googleapiclient.errors.HttpError as http_error:
  282. cb.log.warning("googleapiclient.errors.HttpError: {0}".format(
  283. http_error))
  284. if 'items' in response:
  285. self._public_images.extend(
  286. [GCEMachineImage(self.provider, image) for image
  287. in response['items']])
  288. def get(self, image_id):
  289. """
  290. Returns an Image given its id
  291. """
  292. try:
  293. image = self.provider.gce_compute \
  294. .images() \
  295. .get(project=self.provider.project_name,
  296. image=image_id) \
  297. .execute()
  298. if image:
  299. return GCEMachineImage(self.provider, image)
  300. except TypeError as type_error:
  301. # The API will throw an TypeError, if parameter `image` does not
  302. # match the pattern "[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?".
  303. cb.log.warning("TypeError: {0}".format(type_error))
  304. except googleapiclient.errors.HttpError as http_error:
  305. # If the image is not found in project-specific private images,
  306. # look for this image in public images.
  307. self._retrieve_public_images()
  308. for public_image in self._public_images:
  309. if public_image.id == image_id:
  310. return public_image
  311. cb.log.warning(
  312. "googleapiclient.errors.HttpError: {0}".format(http_error))
  313. return None
  314. def find(self, name, limit=None, marker=None):
  315. """
  316. Searches for an image by a given list of attributes
  317. """
  318. filters = {'name': name}
  319. # Retrieve all available images by setting limit to sys.maxsize
  320. images = [image for image in self if image.name == filters['name']]
  321. return ClientPagedResultList(self.provider, images,
  322. limit=limit, marker=marker)
  323. def list(self, limit=None, marker=None):
  324. """
  325. List all images.
  326. """
  327. self._retrieve_public_images()
  328. images = []
  329. if (self.provider.project_name not in
  330. GCEImageService._PUBLIC_IMAGE_PROJECTS):
  331. try:
  332. response = self.provider \
  333. .gce_compute \
  334. .images() \
  335. .list(project=self.provider.project_name) \
  336. .execute()
  337. if 'items' in response:
  338. images = [GCEMachineImage(self.provider, image) for image
  339. in response['items']]
  340. except googleapiclient.errors.HttpError as http_error:
  341. cb.log.warning(
  342. "googleapiclient.errors.HttpError: {0}".format(http_error))
  343. images.extend(self._public_images)
  344. return ClientPagedResultList(self.provider, images,
  345. limit=limit, marker=marker)
  346. class GCEComputeService(BaseComputeService):
  347. # TODO: implement GCEComputeService
  348. def __init__(self, provider):
  349. super(GCEComputeService, self).__init__(provider)
  350. self._instance_type_svc = GCEInstanceTypesService(self.provider)
  351. self._region_svc = GCERegionService(self.provider)
  352. self._images_svc = GCEImageService(self.provider)
  353. @property
  354. def images(self):
  355. return self._images_svc
  356. @property
  357. def instance_types(self):
  358. return self._instance_type_svc
  359. @property
  360. def instances(self):
  361. raise NotImplementedError("To be implemented")
  362. @property
  363. def regions(self):
  364. return self._region_svc
  365. class GCENetworkService(BaseNetworkService):
  366. def __init__(self, provider):
  367. super(GCENetworkService, self).__init__(provider)
  368. def get(self, network_id):
  369. if network_id is None:
  370. return None
  371. # networks = self.list(filter='id eq %s' % network_id) would be better.
  372. # But, there is a GCE API bug that causes an error if the network_id
  373. # has more than 19 digits. So, we list all networks and filter
  374. # ourselves.
  375. networks = self.list()
  376. for network in networks:
  377. if network.id == network_id:
  378. return network
  379. return None
  380. def get_by_name(self, network_name):
  381. if network_name is None:
  382. return None
  383. networks = self.list(filter='name eq %s' % network_name)
  384. return None if len(networks) == 0 else networks[0]
  385. def list(self, limit=None, marker=None, filter=None):
  386. try:
  387. response = (self.provider.gce_compute
  388. .networks()
  389. .list(project=self.provider.project_name,
  390. filter=filter)
  391. .execute())
  392. networks = []
  393. if 'items' in response:
  394. for network in response['items']:
  395. networks.append(GCENetwork(self.provider, network))
  396. return networks
  397. except:
  398. return []
  399. def create(self, name):
  400. try:
  401. networks = self.list(filter='name eq %s' % name)
  402. if len(networks) > 0:
  403. return networks[0]
  404. response = (self.provider.gce_compute
  405. .networks()
  406. .insert(project=self.provider.project_name,
  407. body={'name': name})
  408. .execute())
  409. if 'error' in response:
  410. return None
  411. self.provider.wait_for_global_operation(response)
  412. networks = self.list(filter='name eq %s' % name)
  413. return None if len(networks) == 0 else networks[0]
  414. except:
  415. return None
  416. @property
  417. def subnets(self):
  418. raise NotImplementedError('To be implemented')
  419. def floating_ips(self, network_id=None):
  420. raise NotImplementedError('To be implemented')
  421. def create_floating_ip(self):
  422. raise NotImplementedError('To be implemented')
  423. def routers(self):
  424. raise NotImplementedError('To be implemented')
  425. def create_router(self, name=None):
  426. raise NotImplementedError('To be implemented')