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

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