services.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  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 BaseRegionService
  7. from cloudbridge.cloud.base.services import BaseSecurityGroupService
  8. from cloudbridge.cloud.base.services import BaseSecurityService
  9. from cloudbridge.cloud.providers.gce import helpers
  10. from collections import namedtuple
  11. import hashlib
  12. import googleapiclient
  13. from retrying import retry
  14. from .resources import GCEMachineImage
  15. from .resources import GCEInstanceType
  16. from .resources import GCEKeyPair
  17. from .resources import GCERegion
  18. class GCESecurityService(BaseSecurityService):
  19. def __init__(self, provider):
  20. super(GCESecurityService, self).__init__(provider)
  21. # Initialize provider services
  22. self._key_pairs = GCEKeyPairService(provider)
  23. @property
  24. def key_pairs(self):
  25. return self._key_pairs
  26. @property
  27. def security_groups(self):
  28. raise NotImplementedError(
  29. "GCECloudProvider does not implement this service")
  30. class GCEKeyPairService(BaseKeyPairService):
  31. GCEKeyInfo = namedtuple('GCEKeyInfo', 'format public_key email')
  32. def __init__(self, provider):
  33. super(GCEKeyPairService, self).__init__(provider)
  34. self._gce_projects = None
  35. @property
  36. def gce_projects(self):
  37. if not self._gce_projects:
  38. self._gce_projects = self.provider.gce_compute.projects()
  39. return self._gce_projects
  40. def get(self, key_pair_id):
  41. """
  42. Returns a KeyPair given its ID.
  43. """
  44. for kp in self.list():
  45. if kp.id == key_pair_id:
  46. return kp
  47. else:
  48. return None
  49. def _iter_gce_key_pairs(self):
  50. """
  51. Iterates through the project's metadata, yielding a GCEKeyInfo object
  52. for each entry in commonInstanceMetaData/items
  53. """
  54. metadata = self._get_common_metadata()
  55. for kpinfo in self._iter_gce_ssh_keys(metadata):
  56. yield kpinfo
  57. def _get_common_metadata(self):
  58. """
  59. Get a project's commonInstanceMetadata entry
  60. """
  61. metadata = self.gce_projects.get(
  62. project=self.provider.project_name).execute()
  63. return metadata["commonInstanceMetadata"]
  64. def _get_or_add_sshkey_entry(self, metadata):
  65. """
  66. Get the sshKeys entry from commonInstanceMetadata/items.
  67. If an entry does not exist, adds a new empty entry
  68. """
  69. sshkey_entry = None
  70. entries = [item for item in metadata["items"]
  71. if item["key"] == "sshKeys"]
  72. if entries:
  73. sshkey_entry = entries[0]
  74. else: # add a new entry
  75. sshkey_entry = {"key": "sshKeys", "value": ""}
  76. metadata["items"].append(sshkey_entry)
  77. return sshkey_entry
  78. def _iter_gce_ssh_keys(self, metadata):
  79. """
  80. Iterates through the ssh keys given a commonInstanceMetadata dict,
  81. yielding a GCEKeyInfo object for each entry in
  82. commonInstanceMetaData/items
  83. """
  84. sshkeys = self._get_or_add_sshkey_entry(metadata)["value"]
  85. for key in sshkeys.split("\n"):
  86. # elems should be "ssh-rsa <public_key> <email>"
  87. elems = key.split(" ")
  88. if elems and elems[0]: # ignore blank lines
  89. yield GCEKeyPairService.GCEKeyInfo(elems[0], elems[1],
  90. elems[2])
  91. def gce_metadata_save_op(self, callback):
  92. """
  93. Carries out a metadata save operation. In GCE, a fingerprint based
  94. locking mechanism is used to prevent lost updates. A new fingerprint
  95. is returned each time metadata is retrieved. Therefore, this method
  96. retrieves the metadata, invokes the provided callback with that
  97. metadata, and saves the metadata using the original fingerprint
  98. immediately afterwards, ensuring that update conflicts can be detected.
  99. """
  100. def _save_common_metadata():
  101. metadata = self._get_common_metadata()
  102. # add a new entry if one doesn'te xist
  103. sshkey_entry = self._get_or_add_sshkey_entry(metadata)
  104. gce_kp_list = callback(self._iter_gce_ssh_keys(metadata))
  105. entry = ""
  106. for gce_kp in gce_kp_list:
  107. entry = entry + u"{0} {1} {2}\n".format(gce_kp.format,
  108. gce_kp.public_key,
  109. gce_kp.email)
  110. sshkey_entry["value"] = entry.rstrip()
  111. # common_metadata will have the current fingerprint at this point
  112. operation = self.gce_projects.setCommonInstanceMetadata(
  113. project=self.provider.project_name, body=metadata).execute()
  114. self.provider.wait_for_global_operation(operation)
  115. # Retry a few times if the fingerprints conflict
  116. retry_decorator = retry(stop_max_attempt_number=5)
  117. retry_decorator(_save_common_metadata)()
  118. def gce_kp_to_id(self, gce_kp):
  119. """
  120. Accept a GCEKeyInfo object and return a unique
  121. ID for it
  122. """
  123. md5 = hashlib.md5()
  124. md5.update(gce_kp.public_key)
  125. return md5.hexdigest()
  126. def list(self, limit=None, marker=None):
  127. key_pairs = []
  128. for gce_kp in self._iter_gce_key_pairs():
  129. kp_id = self.gce_kp_to_id(gce_kp)
  130. kp_name = gce_kp.email
  131. key_pairs.append(GCEKeyPair(self.provider, kp_id, kp_name))
  132. return ClientPagedResultList(self.provider, key_pairs,
  133. limit=limit, marker=marker)
  134. def find(self, name, limit=None, marker=None):
  135. """
  136. Searches for a key pair by a given list of attributes.
  137. """
  138. found_kps = []
  139. for kp in self.list():
  140. if kp.name == name:
  141. found_kps.append(kp)
  142. return ClientPagedResultList(self.provider, found_kps,
  143. limit=limit, marker=marker)
  144. def create(self, name):
  145. kp = self.find(name=name)
  146. if kp:
  147. return kp
  148. private_key, public_key = helpers.generate_key_pair()
  149. kp_info = GCEKeyPairService.GCEKeyInfo(name + u":ssh-rsa",
  150. public_key, name)
  151. def _add_kp(gce_kp_generator):
  152. kp_list = []
  153. # Add the new key pair
  154. kp_list.append(kp_info)
  155. for gce_kp in gce_kp_generator:
  156. kp_list.append(gce_kp)
  157. return kp_list
  158. self.gce_metadata_save_op(_add_kp)
  159. return GCEKeyPair(self.provider, self.gce_kp_to_id(kp_info), name,
  160. kp_material=private_key)
  161. class GCESecurityGroupService(BaseSecurityGroupService):
  162. def __init__(self, provider):
  163. super(GCESecurityGroupService, self).__init__(provider)
  164. class GCEInstanceTypesService(BaseInstanceTypesService):
  165. def __init__(self, provider):
  166. super(GCEInstanceTypesService, self).__init__(provider)
  167. @property
  168. def instance_data(self):
  169. response = self.provider.gce_compute \
  170. .machineTypes() \
  171. .list(project=self.provider.project_name,
  172. zone=self.provider.default_zone) \
  173. .execute()
  174. return response['items']
  175. def get(self, instance_type_id):
  176. for inst_type in self.instance_data:
  177. if inst_type.get('id') == instance_type_id:
  178. return GCEInstanceType(self.provider, inst_type)
  179. return None
  180. def find(self, **kwargs):
  181. matched_inst_types = []
  182. for inst_type in self.instance_data:
  183. is_match = True
  184. for key, value in kwargs.iteritems():
  185. if key not in inst_type:
  186. raise TypeError("The attribute key is not valid.")
  187. if inst_type.get(key) != value:
  188. is_match = False
  189. break
  190. if is_match:
  191. matched_inst_types.append(
  192. GCEInstanceType(self.provider, inst_type))
  193. return matched_inst_types
  194. def list(self, limit=None, marker=None):
  195. inst_types = [GCEInstanceType(self.provider, inst_type)
  196. for inst_type in self.instance_data]
  197. return ClientPagedResultList(self.provider, inst_types,
  198. limit=limit, marker=marker)
  199. class GCERegionService(BaseRegionService):
  200. def __init__(self, provider):
  201. super(GCERegionService, self).__init__(provider)
  202. def get(self, region_id):
  203. try:
  204. region = self.provider.gce_compute \
  205. .regions() \
  206. .get(project=self.provider.project_name,
  207. region=region_id) \
  208. .execute()
  209. # Handle the case when region_id is not valid
  210. except googleapiclient.errors.HttpError:
  211. return None
  212. if region:
  213. return GCERegion(self.provider, region)
  214. else:
  215. return None
  216. def list(self, limit=None, marker=None):
  217. regions_response = self.provider.gce_compute.regions().list(
  218. project=self.provider.project_name).execute()
  219. regions = [GCERegion(self.provider, region)
  220. for region in regions_response['items']]
  221. return ClientPagedResultList(self.provider, regions,
  222. limit=limit, marker=marker)
  223. @property
  224. def current(self):
  225. return self.get(self.provider.region_name)
  226. class GCEImageService(BaseImageService):
  227. def __init__(self, provider):
  228. super(GCEImageService, self).__init__(provider)
  229. def get(self, image_name):
  230. """
  231. Returns an Image given its name
  232. """
  233. try:
  234. image = self.provider.gce_compute \
  235. .images() \
  236. .get(project=self.provider.project_name,
  237. image=image_name) \
  238. .execute()
  239. if image:
  240. return GCEMachineImage(self.provider, image)
  241. except TypeError as type_error:
  242. # The API will throw an TypeError, if parameter `image` does not
  243. # match the pattern "[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?".
  244. print("TypeError: {0}".format(type_error))
  245. return None
  246. except googleapiclient.errors.HttpError as http_error:
  247. print("googleapiclient.errors.HttpError: {0}".format(http_error))
  248. return None
  249. def find(self, name, limit=None, marker=None):
  250. """
  251. Searches for an image by a given list of attributes
  252. """
  253. filters = {'name': name}
  254. try:
  255. response = self.provider.gce_compute \
  256. .images() \
  257. .list(project=self.provider.project_name) \
  258. .execute()
  259. except googleapiclient.errors.HttpError as http_error:
  260. print("googleapiclient.errors.HttpError: {0}".format(http_error))
  261. return []
  262. if 'items' not in response:
  263. return []
  264. images = [GCEMachineImage(self.provider, image) for image
  265. in response['items']
  266. if 'name' in image and image['name'] == filters['name']]
  267. return ClientPagedResultList(self.provider, images,
  268. limit=limit, marker=marker)
  269. def list(self, limit=None, marker=None):
  270. """
  271. List all images.
  272. """
  273. try:
  274. response = self.provider.gce_compute \
  275. .images() \
  276. .list(project=self.provider.project_name) \
  277. .execute()
  278. except googleapiclient.errors.HttpError as http_error:
  279. print("googleapiclient.errors.HttpError: {0}".format(http_error))
  280. return []
  281. if 'items' not in response:
  282. return []
  283. images = [GCEMachineImage(self.provider, image) for image
  284. in response['items']]
  285. return ClientPagedResultList(self.provider, images,
  286. limit=limit, marker=marker)
  287. class GCEComputeService(BaseComputeService):
  288. # TODO: implement GCEComputeService
  289. def __init__(self, provider):
  290. super(GCEComputeService, self).__init__(provider)
  291. self._instance_type_svc = GCEInstanceTypesService(self.provider)
  292. self._region_svc = GCERegionService(self.provider)
  293. self._images_svc = GCEImageService(self.provider)
  294. @property
  295. def images(self):
  296. return self._images_svc
  297. @property
  298. def instance_types(self):
  299. return self._instance_type_svc
  300. @property
  301. def instances(self):
  302. raise NotImplementedError("To be implemented")
  303. @property
  304. def regions(self):
  305. return self._region_svc