baserlib.py 9.37 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#
#

# Copyright (C) 2006, 2007, 2008 Google Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.


"""Remote API base resources library.

"""

Iustin Pop's avatar
Iustin Pop committed
26
27
28
29
# pylint: disable-msg=C0103

# C0103: Invalid name, since the R_* names are not conforming

Iustin Pop's avatar
Iustin Pop committed
30
31
import logging

32
from ganeti import luxi
33
from ganeti import rapi
34
from ganeti import http
35
36
from ganeti import ssconf
from ganeti import constants
Iustin Pop's avatar
Iustin Pop committed
37
38
from ganeti import opcodes
from ganeti import errors
39

40

41
42
43
44
# Dummy value to detect unchanged parameters
_DEFAULT = object()


45
46
47
def BuildUriList(ids, uri_format, uri_fields=("name", "uri")):
  """Builds a URI list as used by index resources.

Iustin Pop's avatar
Iustin Pop committed
48
49
50
  @param ids: list of ids as strings
  @param uri_format: format to be applied for URI
  @param uri_fields: optional parameter for field IDs
51
52
53

  """
  (field_id, field_uri) = uri_fields
54

55
56
57
58
59
60
61
62
63
64
65
66
67
  def _MapId(m_id):
    return { field_id: m_id, field_uri: uri_format % m_id, }

  # Make sure the result is sorted, makes it nicer to look at and simplifies
  # unittests.
  ids.sort()

  return map(_MapId, ids)


def ExtractField(sequence, index):
  """Creates a list containing one column out of a list of lists.

Iustin Pop's avatar
Iustin Pop committed
68
69
  @param sequence: sequence of lists
  @param index: index of field
70
71
72
73
74
75
76
77

  """
  return map(lambda item: item[index], sequence)


def MapFields(names, data):
  """Maps two lists into one dictionary.

Iustin Pop's avatar
Iustin Pop committed
78
79
80
  Example::
      >>> MapFields(["a", "b"], ["foo", 123])
      {'a': 'foo', 'b': 123}
81

Iustin Pop's avatar
Iustin Pop committed
82
83
  @param names: field names (list of strings)
  @param data: field data (list)
84
85
86
87

  """
  if len(names) != len(data):
    raise AttributeError("Names and data must have the same length")
88
  return dict(zip(names, data))
89
90


Iustin Pop's avatar
Iustin Pop committed
91
def _Tags_GET(kind, name):
92
93
94
  """Helper function to retrieve tags.

  """
95
96
  if kind == constants.TAG_INSTANCE or kind == constants.TAG_NODE:
    if not name:
Iustin Pop's avatar
Iustin Pop committed
97
98
      raise http.HttpBadRequest("Missing name on tag request")
    cl = GetClient()
99
100
101
102
103
104
105
106
107
108
109
110
    if kind == constants.TAG_INSTANCE:
      fn = cl.QueryInstances
    else:
      fn = cl.QueryNodes
    result = fn(names=[name], fields=["tags"], use_locking=False)
    if not result or not result[0]:
      raise http.HttpBadGateway("Invalid response from tag query")
    tags = result[0][0]
  elif kind == constants.TAG_CLUSTER:
    ssc = ssconf.SimpleStore()
    tags = ssc.GetClusterTags()

111
112
113
  return list(tags)


Iustin Pop's avatar
Iustin Pop committed
114
def _Tags_PUT(kind, tags, name, dry_run):
115
116
117
  """Helper function to set tags.

  """
Iustin Pop's avatar
Iustin Pop committed
118
119
  return SubmitJob([opcodes.OpAddTags(kind=kind, name=name,
                                      tags=tags, dry_run=dry_run)])
120
121


Iustin Pop's avatar
Iustin Pop committed
122
def _Tags_DELETE(kind, tags, name, dry_run):
123
124
125
  """Helper function to delete tags.

  """
Iustin Pop's avatar
Iustin Pop committed
126
127
  return SubmitJob([opcodes.OpDelTags(kind=kind, name=name,
                                      tags=tags, dry_run=dry_run)])
128
129


130
131
132
def MapBulkFields(itemslist, fields):
  """Map value to field name in to one dictionary.

Iustin Pop's avatar
Iustin Pop committed
133
134
135
136
  @param itemslist: a list of items values
  @param fields: a list of items names

  @return: a list of mapped dictionaries
137
138
139
140
141
142
143
144
145

  """
  items_details = []
  for item in itemslist:
    mapped = MapFields(fields, item)
    items_details.append(mapped)
  return items_details


Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
146
def MakeParamsDict(opts, params):
Iustin Pop's avatar
Iustin Pop committed
147
  """Makes params dictionary out of a option set.
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172

  This function returns a dictionary needed for hv or be parameters. But only
  those fields which provided in the option set. Takes parameters frozensets
  from constants.

  @type opts: dict
  @param opts: selected options
  @type params: frozenset
  @param params: subset of options
  @rtype: dict
  @return: dictionary of options, filtered by given subset.

  """
  result = {}

  for p in params:
    try:
      value = opts[p]
    except KeyError:
      continue
    result[p] = value

  return result


Iustin Pop's avatar
Iustin Pop committed
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
def SubmitJob(op, cl=None):
  """Generic wrapper for submit job, for better http compatibility.

  @type op: list
  @param op: the list of opcodes for the job
  @type cl: None or luxi.Client
  @param cl: optional luxi client to use
  @rtype: string
  @return: the job ID

  """
  try:
    if cl is None:
      cl = GetClient()
    return cl.SubmitJob(op)
  except errors.JobQueueFull:
    raise http.HttpServiceUnavailable("Job queue is full, needs archiving")
  except errors.JobQueueDrainError:
    raise http.HttpServiceUnavailable("Job queue is drained, cannot submit")
  except luxi.NoMasterError, err:
    raise http.HttpBadGateway("Master seems to unreachable: %s" % str(err))
  except luxi.TimeoutError, err:
    raise http.HttpGatewayTimeout("Timeout while talking to the master"
                                  " daemon. Error: %s" % str(err))

198
199
200
201
202
203
204
205
206
207
208
209
210
211

def HandleItemQueryErrors(fn, *args, **kwargs):
  """Converts errors when querying a single item.

  """
  try:
    return fn(*args, **kwargs)
  except errors.OpPrereqError, err:
    if len(err.args) == 2 and err.args[1] == errors.ECODE_NOENT:
      raise http.HttpNotFound()

    raise


Iustin Pop's avatar
Iustin Pop committed
212
213
214
215
216
217
218
219
220
221
def GetClient():
  """Geric wrapper for luxi.Client(), for better http compatiblity.

  """
  try:
    return luxi.Client()
  except luxi.NoMasterError, err:
    raise http.HttpBadGateway("Master seems to unreachable: %s" % str(err))


222
223
def FeedbackFn(msg):
  """Feedback logging function for jobs.
Iustin Pop's avatar
Iustin Pop committed
224
225
226
227

  We don't have a stdout for printing log messages, so log them to the
  http log at least.

228
  @param msg: the message
229

Iustin Pop's avatar
Iustin Pop committed
230
  """
231
  (_, log_type, log_msg) = msg
Iustin Pop's avatar
Iustin Pop committed
232
233
234
  logging.info("%s: %s", log_type, log_msg)


235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
def CheckType(value, exptype, descr):
  """Abort request if value type doesn't match expected type.

  @param value: Value
  @type exptype: type
  @param exptype: Expected type
  @type descr: string
  @param descr: Description of value
  @return: Value (allows inline usage)

  """
  if not isinstance(value, exptype):
    raise http.HttpBadRequest("%s: Type is '%s', but '%s' is expected" %
                              (descr, type(value).__name__, exptype.__name__))

  return value


def CheckParameter(data, name, default=_DEFAULT, exptype=_DEFAULT):
  """Check and return the value for a given parameter.

  If no default value was given and the parameter doesn't exist in the input
  data, an error is raise.

  @type data: dict
  @param data: Dictionary containing input data
  @type name: string
  @param name: Parameter name
  @param default: Default value (can be None)
  @param exptype: Expected type (can be None)

  """
  try:
    value = data[name]
  except KeyError:
    if default is not _DEFAULT:
      return default

    raise http.HttpBadRequest("Required parameter '%s' is missing" %
                              name)

  if exptype is _DEFAULT:
    return value

  return CheckType(value, exptype, "'%s' parameter" % name)


282
283
284
285
class R_Generic(object):
  """Generic class for resources.

  """
286
287
288
289
290
291
  # Default permission requirements
  GET_ACCESS = []
  PUT_ACCESS = [rapi.RAPI_ACCESS_WRITE]
  POST_ACCESS = [rapi.RAPI_ACCESS_WRITE]
  DELETE_ACCESS = [rapi.RAPI_ACCESS_WRITE]

292
  def __init__(self, items, queryargs, req):
293
294
    """Generic resource constructor.

Iustin Pop's avatar
Iustin Pop committed
295
296
    @param items: a list with variables encoded in the URL
    @param queryargs: a dictionary with additional options from URL
297
298
299
300

    """
    self.items = items
    self.queryargs = queryargs
301
302
    self._req = req

303
304
305
306
307
308
309
  def _GetRequestBody(self):
    """Returns the body data.

    """
    return self._req.private.body_data

  request_body = property(fget=_GetRequestBody)
310

311
  def _checkIntVariable(self, name, default=0):
312
313
314
    """Return the parsed value of an int argument.

    """
315
    val = self.queryargs.get(name, default)
316
317
318
319
    if isinstance(val, list):
      if val:
        val = val[0]
      else:
320
        val = default
321
322
    try:
      val = int(val)
Michael Hanselmann's avatar
Michael Hanselmann committed
323
    except (ValueError, TypeError):
Iustin Pop's avatar
Iustin Pop committed
324
      raise http.HttpBadRequest("Invalid value for the"
325
326
327
                                " '%s' parameter" % (name,))
    return val

328
329
330
331
332
333
334
335
336
337
338
  def _checkStringVariable(self, name, default=None):
    """Return the parsed value of an int argument.

    """
    val = self.queryargs.get(name, default)
    if isinstance(val, list):
      if val:
        val = val[0]
      else:
        val = default
    return val
339

Iustin Pop's avatar
Iustin Pop committed
340
341
342
343
344
345
346
347
348
  def getBodyParameter(self, name, *args):
    """Check and return the value for a given parameter.

    If a second parameter is not given, an error will be returned,
    otherwise this parameter specifies the default value.

    @param name: the required parameter

    """
349
    if args:
Luca Bigliardi's avatar
Luca Bigliardi committed
350
      return CheckParameter(self.request_body, name, default=args[0])
351

Luca Bigliardi's avatar
Luca Bigliardi committed
352
    return CheckParameter(self.request_body, name)
Iustin Pop's avatar
Iustin Pop committed
353

354
355
356
357
  def useLocking(self):
    """Check if the request specifies locking.

    """
358
    return bool(self._checkIntVariable("lock"))
359
360
361
362
363

  def useBulk(self):
    """Check if the request specifies bulk querying.

    """
364
    return bool(self._checkIntVariable("bulk"))
Iustin Pop's avatar
Iustin Pop committed
365

366
367
368
369
  def useForce(self):
    """Check if the request specifies a forced operation.

    """
370
    return bool(self._checkIntVariable("force"))
371

Iustin Pop's avatar
Iustin Pop committed
372
373
374
375
  def dryRun(self):
    """Check if the request specifies dry-run mode.

    """
376
    return bool(self._checkIntVariable("dry-run"))