OpenStack Compute (Nova) Client
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

base.py 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. # Copyright 2010 Jacob Kaplan-Moss
  2. # Copyright 2011 OpenStack Foundation
  3. # All Rights Reserved.
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  6. # not use this file except in compliance with the License. You may obtain
  7. # a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  13. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  14. # License for the specific language governing permissions and limitations
  15. # under the License.
  16. """
  17. Base utilities to build API operation managers and objects on top of.
  18. """
  19. import contextlib
  20. import hashlib
  21. import os
  22. from novaclient import exceptions
  23. from novaclient import utils
  24. # Python 2.4 compat
  25. try:
  26. all
  27. except NameError:
  28. def all(iterable):
  29. return True not in (not x for x in iterable)
  30. def getid(obj):
  31. """
  32. Abstracts the common pattern of allowing both an object or an object's ID
  33. as a parameter when dealing with relationships.
  34. """
  35. try:
  36. return obj.id
  37. except AttributeError:
  38. return obj
  39. class Manager(utils.HookableMixin):
  40. """
  41. Managers interact with a particular type of API (servers, flavors, images,
  42. etc.) and provide CRUD operations for them.
  43. """
  44. resource_class = None
  45. def __init__(self, api):
  46. self.api = api
  47. def _list(self, url, response_key, obj_class=None, body=None):
  48. if body:
  49. _resp, body = self.api.client.post(url, body=body)
  50. else:
  51. _resp, body = self.api.client.get(url)
  52. if obj_class is None:
  53. obj_class = self.resource_class
  54. data = body[response_key]
  55. # NOTE(ja): keystone returns values as list as {'values': [ ... ]}
  56. # unlike other services which just return the list...
  57. if isinstance(data, dict):
  58. try:
  59. data = data['values']
  60. except KeyError:
  61. pass
  62. with self.completion_cache('human_id', obj_class, mode="w"):
  63. with self.completion_cache('uuid', obj_class, mode="w"):
  64. return [obj_class(self, res, loaded=True)
  65. for res in data if res]
  66. @contextlib.contextmanager
  67. def completion_cache(self, cache_type, obj_class, mode):
  68. """
  69. The completion cache store items that can be used for bash
  70. autocompletion, like UUIDs or human-friendly IDs.
  71. A resource listing will clear and repopulate the cache.
  72. A resource create will append to the cache.
  73. Delete is not handled because listings are assumed to be performed
  74. often enough to keep the cache reasonably up-to-date.
  75. """
  76. base_dir = utils.env('NOVACLIENT_UUID_CACHE_DIR',
  77. default="~/.novaclient")
  78. # NOTE(sirp): Keep separate UUID caches for each username + endpoint
  79. # pair
  80. username = utils.env('OS_USERNAME', 'NOVA_USERNAME')
  81. url = utils.env('OS_URL', 'NOVA_URL')
  82. uniqifier = hashlib.md5(username + url).hexdigest()
  83. cache_dir = os.path.expanduser(os.path.join(base_dir, uniqifier))
  84. try:
  85. os.makedirs(cache_dir, 0o755)
  86. except OSError:
  87. # NOTE(kiall): This is typicaly either permission denied while
  88. # attempting to create the directory, or the directory
  89. # already exists. Either way, don't fail.
  90. pass
  91. resource = obj_class.__name__.lower()
  92. filename = "%s-%s-cache" % (resource, cache_type.replace('_', '-'))
  93. path = os.path.join(cache_dir, filename)
  94. cache_attr = "_%s_cache" % cache_type
  95. try:
  96. setattr(self, cache_attr, open(path, mode))
  97. except IOError:
  98. # NOTE(kiall): This is typicaly a permission denied while
  99. # attempting to write the cache file.
  100. pass
  101. try:
  102. yield
  103. finally:
  104. cache = getattr(self, cache_attr, None)
  105. if cache:
  106. cache.close()
  107. delattr(self, cache_attr)
  108. def write_to_completion_cache(self, cache_type, val):
  109. cache = getattr(self, "_%s_cache" % cache_type, None)
  110. if cache:
  111. cache.write("%s\n" % val)
  112. def _get(self, url, response_key):
  113. _resp, body = self.api.client.get(url)
  114. return self.resource_class(self, body[response_key], loaded=True)
  115. def _create(self, url, body, response_key, return_raw=False, **kwargs):
  116. self.run_hooks('modify_body_for_create', body, **kwargs)
  117. _resp, body = self.api.client.post(url, body=body)
  118. if return_raw:
  119. return body[response_key]
  120. with self.completion_cache('human_id', self.resource_class, mode="a"):
  121. with self.completion_cache('uuid', self.resource_class, mode="a"):
  122. return self.resource_class(self, body[response_key])
  123. def _delete(self, url):
  124. _resp, _body = self.api.client.delete(url)
  125. def _update(self, url, body, response_key=None, **kwargs):
  126. self.run_hooks('modify_body_for_update', body, **kwargs)
  127. _resp, body = self.api.client.put(url, body=body)
  128. if body:
  129. if response_key:
  130. return self.resource_class(self, body[response_key])
  131. else:
  132. return self.resource_class(self, body)
  133. class ManagerWithFind(Manager):
  134. """
  135. Like a `Manager`, but with additional `find()`/`findall()` methods.
  136. """
  137. def find(self, **kwargs):
  138. """
  139. Find a single item with attributes matching ``**kwargs``.
  140. This isn't very efficient: it loads the entire list then filters on
  141. the Python side.
  142. """
  143. matches = self.findall(**kwargs)
  144. num_matches = len(matches)
  145. if num_matches == 0:
  146. msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
  147. raise exceptions.NotFound(404, msg)
  148. elif num_matches > 1:
  149. raise exceptions.NoUniqueMatch
  150. else:
  151. return matches[0]
  152. def findall(self, **kwargs):
  153. """
  154. Find all items with attributes matching ``**kwargs``.
  155. This isn't very efficient: it loads the entire list then filters on
  156. the Python side.
  157. """
  158. found = []
  159. searches = kwargs.items()
  160. for obj in self.list():
  161. try:
  162. if all(getattr(obj, attr) == value
  163. for (attr, value) in searches):
  164. found.append(obj)
  165. except AttributeError:
  166. continue
  167. return found
  168. def list(self):
  169. raise NotImplementedError
  170. class BootingManagerWithFind(ManagerWithFind):
  171. """Like a `ManagerWithFind`, but has the ability to boot servers."""
  172. def _boot(self, resource_url, response_key, name, image, flavor,
  173. ipgroup=None, meta=None, files=None,
  174. reservation_id=None, return_raw=False, min_count=None,
  175. max_count=None, **kwargs):
  176. """
  177. Create (boot) a new server.
  178. :param name: Something to name the server.
  179. :param image: The :class:`Image` to boot with.
  180. :param flavor: The :class:`Flavor` to boot onto.
  181. :param ipgroup: An initial :class:`IPGroup` for this server.
  182. :param meta: A dict of arbitrary key/value metadata to store for this
  183. server. A maximum of five entries is allowed, and both
  184. keys and values must be 255 characters or less.
  185. :param files: A dict of files to overrwrite on the server upon boot.
  186. Keys are file names (i.e. ``/etc/passwd``) and values
  187. are the file contents (either as a string or as a
  188. file-like object). A maximum of five entries is allowed,
  189. and each file must be 10k or less.
  190. :param reservation_id: a UUID for the set of servers being requested.
  191. :param return_raw: If True, don't try to coearse the result into
  192. a Resource object.
  193. """
  194. body = {"server": {
  195. "name": name,
  196. "imageId": getid(image),
  197. "flavorId": getid(flavor),
  198. }}
  199. if ipgroup:
  200. body["server"]["sharedIpGroupId"] = getid(ipgroup)
  201. if meta:
  202. body["server"]["metadata"] = meta
  203. if reservation_id:
  204. body["server"]["reservation_id"] = reservation_id
  205. if not min_count:
  206. min_count = 1
  207. if not max_count:
  208. max_count = min_count
  209. body["server"]["min_count"] = min_count
  210. body["server"]["max_count"] = max_count
  211. # Files are a slight bit tricky. They're passed in a "personality"
  212. # list to the POST. Each item is a dict giving a file name and the
  213. # base64-encoded contents of the file. We want to allow passing
  214. # either an open file *or* some contents as files here.
  215. if files:
  216. personality = body['server']['personality'] = []
  217. for filepath, file_or_string in files.items():
  218. if hasattr(file_or_string, 'read'):
  219. data = file_or_string.read()
  220. else:
  221. data = file_or_string
  222. personality.append({
  223. 'path': filepath,
  224. 'contents': data.encode('base64'),
  225. })
  226. return self._create(resource_url, body, response_key,
  227. return_raw=return_raw, **kwargs)
  228. class Resource(object):
  229. """
  230. A resource represents a particular instance of an object (server, flavor,
  231. etc). This is pretty much just a bag for attributes.
  232. :param manager: Manager object
  233. :param info: dictionary representing resource attributes
  234. :param loaded: prevent lazy-loading if set to True
  235. """
  236. HUMAN_ID = False
  237. NAME_ATTR = 'name'
  238. def __init__(self, manager, info, loaded=False):
  239. self.manager = manager
  240. self._info = info
  241. self._add_details(info)
  242. self._loaded = loaded
  243. # NOTE(sirp): ensure `id` is already present because if it isn't we'll
  244. # enter an infinite loop of __getattr__ -> get -> __init__ ->
  245. # __getattr__ -> ...
  246. if 'id' in self.__dict__ and len(str(self.id)) == 36:
  247. self.manager.write_to_completion_cache('uuid', self.id)
  248. human_id = self.human_id
  249. if human_id:
  250. self.manager.write_to_completion_cache('human_id', human_id)
  251. @property
  252. def human_id(self):
  253. """Subclasses may override this provide a pretty ID which can be used
  254. for bash completion.
  255. """
  256. if self.NAME_ATTR in self.__dict__ and self.HUMAN_ID:
  257. return utils.slugify(getattr(self, self.NAME_ATTR))
  258. return None
  259. def _add_details(self, info):
  260. for (k, v) in info.iteritems():
  261. try:
  262. setattr(self, k, v)
  263. self._info[k] = v
  264. except AttributeError:
  265. # In this case we already defined the attribute on the class
  266. pass
  267. def __getattr__(self, k):
  268. if k not in self.__dict__:
  269. #NOTE(bcwaldon): disallow lazy-loading if already loaded once
  270. if not self.is_loaded():
  271. self.get()
  272. return self.__getattr__(k)
  273. raise AttributeError(k)
  274. else:
  275. return self.__dict__[k]
  276. def __repr__(self):
  277. reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and
  278. k != 'manager')
  279. info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
  280. return "<%s %s>" % (self.__class__.__name__, info)
  281. def get(self):
  282. # set_loaded() first ... so if we have to bail, we know we tried.
  283. self.set_loaded(True)
  284. if not hasattr(self.manager, 'get'):
  285. return
  286. new = self.manager.get(self.id)
  287. if new:
  288. self._add_details(new._info)
  289. def __eq__(self, other):
  290. if not isinstance(other, self.__class__):
  291. return False
  292. if hasattr(self, 'id') and hasattr(other, 'id'):
  293. return self.id == other.id
  294. return self._info == other._info
  295. def is_loaded(self):
  296. return self._loaded
  297. def set_loaded(self, val):
  298. self._loaded = val