Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / baserlib.py @ f87ec53f

History | View | Annotate | Download (10.1 kB)

1
#
2
#
3

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

    
21

    
22
"""Remote API base resources library.
23

24
"""
25

    
26
# pylint: disable-msg=C0103
27

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

    
30
import logging
31

    
32
from ganeti import luxi
33
from ganeti import rapi
34
from ganeti import http
35
from ganeti import errors
36

    
37

    
38
# Dummy value to detect unchanged parameters
39
_DEFAULT = object()
40

    
41

    
42
def BuildUriList(ids, uri_format, uri_fields=("name", "uri")):
43
  """Builds a URI list as used by index resources.
44

45
  @param ids: list of ids as strings
46
  @param uri_format: format to be applied for URI
47
  @param uri_fields: optional parameter for field IDs
48

49
  """
50
  (field_id, field_uri) = uri_fields
51

    
52
  def _MapId(m_id):
53
    return {
54
      field_id: m_id,
55
      field_uri: uri_format % m_id,
56
      }
57

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

    
62
  return map(_MapId, ids)
63

    
64

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

68
  @param sequence: sequence of lists
69
  @param index: index of field
70

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

    
74

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

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

82
  @param names: field names (list of strings)
83
  @param data: field data (list)
84

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

    
90

    
91
def MapBulkFields(itemslist, fields):
92
  """Map value to field name in to one dictionary.
93

94
  @param itemslist: a list of items values
95
  @param fields: a list of items names
96

97
  @return: a list of mapped dictionaries
98

99
  """
100
  items_details = []
101
  for item in itemslist:
102
    mapped = MapFields(fields, item)
103
    items_details.append(mapped)
104
  return items_details
105

    
106

    
107
def MakeParamsDict(opts, params):
108
  """Makes params dictionary out of a option set.
109

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

114
  @type opts: dict
115
  @param opts: selected options
116
  @type params: frozenset
117
  @param params: subset of options
118
  @rtype: dict
119
  @return: dictionary of options, filtered by given subset.
120

121
  """
122
  result = {}
123

    
124
  for p in params:
125
    try:
126
      value = opts[p]
127
    except KeyError:
128
      continue
129
    result[p] = value
130

    
131
  return result
132

    
133

    
134
def FillOpcode(opcls, body, static, rename=None):
135
  """Fills an opcode with body parameters.
136

137
  Parameter types are checked.
138

139
  @type opcls: L{opcodes.OpCode}
140
  @param opcls: Opcode class
141
  @type body: dict
142
  @param body: Body parameters as received from client
143
  @type static: dict
144
  @param static: Static parameters which can't be modified by client
145
  @type rename: dict
146
  @param rename: Renamed parameters, key as old name, value as new name
147
  @return: Opcode object
148

149
  """
150
  CheckType(body, dict, "Body contents")
151

    
152
  # Make copy to be modified
153
  params = body.copy()
154

    
155
  if rename:
156
    for old, new in rename.items():
157
      if new in params and old in params:
158
        raise http.HttpBadRequest("Parameter '%s' was renamed to '%s', but"
159
                                  " both are specified" %
160
                                  (old, new))
161
      if old in params:
162
        assert new not in params
163
        params[new] = params.pop(old)
164

    
165
  if static:
166
    overwritten = set(params.keys()) & set(static.keys())
167
    if overwritten:
168
      raise http.HttpBadRequest("Can't overwrite static parameters %r" %
169
                                overwritten)
170

    
171
    params.update(static)
172

    
173
  # Convert keys to strings (simplejson decodes them as unicode)
174
  params = dict((str(key), value) for (key, value) in params.items())
175

    
176
  try:
177
    op = opcls(**params) # pylint: disable-msg=W0142
178
    op.Validate(False)
179
  except (errors.OpPrereqError, TypeError), err:
180
    raise http.HttpBadRequest("Invalid body parameters: %s" % err)
181

    
182
  return op
183

    
184

    
185
def SubmitJob(op, cl=None):
186
  """Generic wrapper for submit job, for better http compatibility.
187

188
  @type op: list
189
  @param op: the list of opcodes for the job
190
  @type cl: None or luxi.Client
191
  @param cl: optional luxi client to use
192
  @rtype: string
193
  @return: the job ID
194

195
  """
196
  try:
197
    if cl is None:
198
      cl = GetClient()
199
    return cl.SubmitJob(op)
200
  except errors.JobQueueFull:
201
    raise http.HttpServiceUnavailable("Job queue is full, needs archiving")
202
  except errors.JobQueueDrainError:
203
    raise http.HttpServiceUnavailable("Job queue is drained, cannot submit")
204
  except luxi.NoMasterError, err:
205
    raise http.HttpBadGateway("Master seems to be unreachable: %s" % str(err))
206
  except luxi.PermissionError:
207
    raise http.HttpInternalServerError("Internal error: no permission to"
208
                                       " connect to the master daemon")
209
  except luxi.TimeoutError, err:
210
    raise http.HttpGatewayTimeout("Timeout while talking to the master"
211
                                  " daemon. Error: %s" % str(err))
212

    
213

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

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

    
224
    raise
225

    
226

    
227
def GetClient():
228
  """Geric wrapper for luxi.Client(), for better http compatiblity.
229

230
  """
231
  try:
232
    return luxi.Client()
233
  except luxi.NoMasterError, err:
234
    raise http.HttpBadGateway("Master seems to unreachable: %s" % str(err))
235
  except luxi.PermissionError:
236
    raise http.HttpInternalServerError("Internal error: no permission to"
237
                                       " connect to the master daemon")
238

    
239

    
240
def FeedbackFn(msg):
241
  """Feedback logging function for jobs.
242

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

246
  @param msg: the message
247

248
  """
249
  (_, log_type, log_msg) = msg
250
  logging.info("%s: %s", log_type, log_msg)
251

    
252

    
253
def CheckType(value, exptype, descr):
254
  """Abort request if value type doesn't match expected type.
255

256
  @param value: Value
257
  @type exptype: type
258
  @param exptype: Expected type
259
  @type descr: string
260
  @param descr: Description of value
261
  @return: Value (allows inline usage)
262

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

    
268
  return value
269

    
270

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

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

277
  @type data: dict
278
  @param data: Dictionary containing input data
279
  @type name: string
280
  @param name: Parameter name
281
  @param default: Default value (can be None)
282
  @param exptype: Expected type (can be None)
283

284
  """
285
  try:
286
    value = data[name]
287
  except KeyError:
288
    if default is not _DEFAULT:
289
      return default
290

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

    
294
  if exptype is _DEFAULT:
295
    return value
296

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

    
299

    
300
class R_Generic(object):
301
  """Generic class for resources.
302

303
  """
304
  # Default permission requirements
305
  GET_ACCESS = []
306
  PUT_ACCESS = [rapi.RAPI_ACCESS_WRITE]
307
  POST_ACCESS = [rapi.RAPI_ACCESS_WRITE]
308
  DELETE_ACCESS = [rapi.RAPI_ACCESS_WRITE]
309

    
310
  def __init__(self, items, queryargs, req):
311
    """Generic resource constructor.
312

313
    @param items: a list with variables encoded in the URL
314
    @param queryargs: a dictionary with additional options from URL
315

316
    """
317
    self.items = items
318
    self.queryargs = queryargs
319
    self._req = req
320

    
321
  def _GetRequestBody(self):
322
    """Returns the body data.
323

324
    """
325
    return self._req.private.body_data
326

    
327
  request_body = property(fget=_GetRequestBody)
328

    
329
  def _checkIntVariable(self, name, default=0):
330
    """Return the parsed value of an int argument.
331

332
    """
333
    val = self.queryargs.get(name, default)
334
    if isinstance(val, list):
335
      if val:
336
        val = val[0]
337
      else:
338
        val = default
339
    try:
340
      val = int(val)
341
    except (ValueError, TypeError):
342
      raise http.HttpBadRequest("Invalid value for the"
343
                                " '%s' parameter" % (name,))
344
    return val
345

    
346
  def _checkStringVariable(self, name, default=None):
347
    """Return the parsed value of an int argument.
348

349
    """
350
    val = self.queryargs.get(name, default)
351
    if isinstance(val, list):
352
      if val:
353
        val = val[0]
354
      else:
355
        val = default
356
    return val
357

    
358
  def getBodyParameter(self, name, *args):
359
    """Check and return the value for a given parameter.
360

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

364
    @param name: the required parameter
365

366
    """
367
    if args:
368
      return CheckParameter(self.request_body, name, default=args[0])
369

    
370
    return CheckParameter(self.request_body, name)
371

    
372
  def useLocking(self):
373
    """Check if the request specifies locking.
374

375
    """
376
    return bool(self._checkIntVariable("lock"))
377

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

381
    """
382
    return bool(self._checkIntVariable("bulk"))
383

    
384
  def useForce(self):
385
    """Check if the request specifies a forced operation.
386

387
    """
388
    return bool(self._checkIntVariable("force"))
389

    
390
  def dryRun(self):
391
    """Check if the request specifies dry-run mode.
392

393
    """
394
    return bool(self._checkIntVariable("dry-run"))