Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / baserlib.py @ 4c864b55

History | View | Annotate | Download (14.4 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=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
from ganeti import compat
37

    
38

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

    
42
#: Supported HTTP methods
43
_SUPPORTED_METHODS = frozenset([
44
  http.HTTP_DELETE,
45
  http.HTTP_GET,
46
  http.HTTP_POST,
47
  http.HTTP_PUT,
48
  ])
49

    
50

    
51
def _BuildOpcodeAttributes():
52
  """Builds list of attributes used for per-handler opcodes.
53

54
  """
55
  return [(method, "%s_OPCODE" % method, "%s_RENAME" % method,
56
           "Get%sOpInput" % method.capitalize())
57
          for method in _SUPPORTED_METHODS]
58

    
59

    
60
_OPCODE_ATTRS = _BuildOpcodeAttributes()
61

    
62

    
63
def BuildUriList(ids, uri_format, uri_fields=("name", "uri")):
64
  """Builds a URI list as used by index resources.
65

66
  @param ids: list of ids as strings
67
  @param uri_format: format to be applied for URI
68
  @param uri_fields: optional parameter for field IDs
69

70
  """
71
  (field_id, field_uri) = uri_fields
72

    
73
  def _MapId(m_id):
74
    return {
75
      field_id: m_id,
76
      field_uri: uri_format % m_id,
77
      }
78

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

    
83
  return map(_MapId, ids)
84

    
85

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

89
  @param sequence: sequence of lists
90
  @param index: index of field
91

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

    
95

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

99
  Example::
100
      >>> MapFields(["a", "b"], ["foo", 123])
101
      {'a': 'foo', 'b': 123}
102

103
  @param names: field names (list of strings)
104
  @param data: field data (list)
105

106
  """
107
  if len(names) != len(data):
108
    raise AttributeError("Names and data must have the same length")
109
  return dict(zip(names, data))
110

    
111

    
112
def MapBulkFields(itemslist, fields):
113
  """Map value to field name in to one dictionary.
114

115
  @param itemslist: a list of items values
116
  @param fields: a list of items names
117

118
  @return: a list of mapped dictionaries
119

120
  """
121
  items_details = []
122
  for item in itemslist:
123
    mapped = MapFields(fields, item)
124
    items_details.append(mapped)
125
  return items_details
126

    
127

    
128
def MakeParamsDict(opts, params):
129
  """Makes params dictionary out of a option set.
130

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

135
  @type opts: dict
136
  @param opts: selected options
137
  @type params: frozenset
138
  @param params: subset of options
139
  @rtype: dict
140
  @return: dictionary of options, filtered by given subset.
141

142
  """
143
  result = {}
144

    
145
  for p in params:
146
    try:
147
      value = opts[p]
148
    except KeyError:
149
      continue
150
    result[p] = value
151

    
152
  return result
153

    
154

    
155
def FillOpcode(opcls, body, static, rename=None):
156
  """Fills an opcode with body parameters.
157

158
  Parameter types are checked.
159

160
  @type opcls: L{opcodes.OpCode}
161
  @param opcls: Opcode class
162
  @type body: dict
163
  @param body: Body parameters as received from client
164
  @type static: dict
165
  @param static: Static parameters which can't be modified by client
166
  @type rename: dict
167
  @param rename: Renamed parameters, key as old name, value as new name
168
  @return: Opcode object
169

170
  """
171
  if body is None:
172
    params = {}
173
  else:
174
    CheckType(body, dict, "Body contents")
175

    
176
    # Make copy to be modified
177
    params = body.copy()
178

    
179
  if rename:
180
    for old, new in rename.items():
181
      if new in params and old in params:
182
        raise http.HttpBadRequest("Parameter '%s' was renamed to '%s', but"
183
                                  " both are specified" %
184
                                  (old, new))
185
      if old in params:
186
        assert new not in params
187
        params[new] = params.pop(old)
188

    
189
  if static:
190
    overwritten = set(params.keys()) & set(static.keys())
191
    if overwritten:
192
      raise http.HttpBadRequest("Can't overwrite static parameters %r" %
193
                                overwritten)
194

    
195
    params.update(static)
196

    
197
  # Convert keys to strings (simplejson decodes them as unicode)
198
  params = dict((str(key), value) for (key, value) in params.items())
199

    
200
  try:
201
    op = opcls(**params) # pylint: disable=W0142
202
    op.Validate(False)
203
  except (errors.OpPrereqError, TypeError), err:
204
    raise http.HttpBadRequest("Invalid body parameters: %s" % err)
205

    
206
  return op
207

    
208

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

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

    
219
    raise
220

    
221

    
222
def FeedbackFn(msg):
223
  """Feedback logging function for jobs.
224

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

228
  @param msg: the message
229

230
  """
231
  (_, log_type, log_msg) = msg
232
  logging.info("%s: %s", log_type, log_msg)
233

    
234

    
235
def CheckType(value, exptype, descr):
236
  """Abort request if value type doesn't match expected type.
237

238
  @param value: Value
239
  @type exptype: type
240
  @param exptype: Expected type
241
  @type descr: string
242
  @param descr: Description of value
243
  @return: Value (allows inline usage)
244

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

    
250
  return value
251

    
252

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

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

259
  @type data: dict
260
  @param data: Dictionary containing input data
261
  @type name: string
262
  @param name: Parameter name
263
  @param default: Default value (can be None)
264
  @param exptype: Expected type (can be None)
265

266
  """
267
  try:
268
    value = data[name]
269
  except KeyError:
270
    if default is not _DEFAULT:
271
      return default
272

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

    
276
  if exptype is _DEFAULT:
277
    return value
278

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

    
281

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

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

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

295
    @param items: a list with variables encoded in the URL
296
    @param queryargs: a dictionary with additional options from URL
297
    @param req: Request context
298
    @param _client_cls: L{luxi} client class (unittests only)
299

300
    """
301
    self.items = items
302
    self.queryargs = queryargs
303
    self._req = req
304
    self._client_cls = _client_cls
305

    
306
  def _GetRequestBody(self):
307
    """Returns the body data.
308

309
    """
310
    return self._req.private.body_data
311

    
312
  request_body = property(fget=_GetRequestBody)
313

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

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

    
331
  def _checkStringVariable(self, name, default=None):
332
    """Return the parsed value of an int argument.
333

334
    """
335
    val = self.queryargs.get(name, default)
336
    if isinstance(val, list):
337
      if val:
338
        val = val[0]
339
      else:
340
        val = default
341
    return val
342

    
343
  def getBodyParameter(self, name, *args):
344
    """Check and return the value for a given parameter.
345

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

349
    @param name: the required parameter
350

351
    """
352
    if args:
353
      return CheckParameter(self.request_body, name, default=args[0])
354

    
355
    return CheckParameter(self.request_body, name)
356

    
357
  def useLocking(self):
358
    """Check if the request specifies locking.
359

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

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

366
    """
367
    return bool(self._checkIntVariable("bulk"))
368

    
369
  def useForce(self):
370
    """Check if the request specifies a forced operation.
371

372
    """
373
    return bool(self._checkIntVariable("force"))
374

    
375
  def dryRun(self):
376
    """Check if the request specifies dry-run mode.
377

378
    """
379
    return bool(self._checkIntVariable("dry-run"))
380

    
381
  def GetClient(self):
382
    """Wrapper for L{luxi.Client} with HTTP-specific error handling.
383

384
    """
385
    # Could be a function, pylint: disable=R0201
386
    try:
387
      return self._client_cls()
388
    except luxi.NoMasterError, err:
389
      raise http.HttpBadGateway("Can't connect to master daemon: %s" % err)
390
    except luxi.PermissionError:
391
      raise http.HttpInternalServerError("Internal error: no permission to"
392
                                         " connect to the master daemon")
393

    
394
  def SubmitJob(self, op, cl=None):
395
    """Generic wrapper for submit job, for better http compatibility.
396

397
    @type op: list
398
    @param op: the list of opcodes for the job
399
    @type cl: None or luxi.Client
400
    @param cl: optional luxi client to use
401
    @rtype: string
402
    @return: the job ID
403

404
    """
405
    if cl is None:
406
      cl = self.GetClient()
407
    try:
408
      return cl.SubmitJob(op)
409
    except errors.JobQueueFull:
410
      raise http.HttpServiceUnavailable("Job queue is full, needs archiving")
411
    except errors.JobQueueDrainError:
412
      raise http.HttpServiceUnavailable("Job queue is drained, cannot submit")
413
    except luxi.NoMasterError, err:
414
      raise http.HttpBadGateway("Master seems to be unreachable: %s" % err)
415
    except luxi.PermissionError:
416
      raise http.HttpInternalServerError("Internal error: no permission to"
417
                                         " connect to the master daemon")
418
    except luxi.TimeoutError, err:
419
      raise http.HttpGatewayTimeout("Timeout while talking to the master"
420
                                    " daemon: %s" % err)
421

    
422

    
423
def GetResourceOpcodes(cls):
424
  """Returns all opcodes used by a resource.
425

426
  """
427
  return frozenset(filter(None, (getattr(cls, op_attr, None)
428
                                 for (_, op_attr, _, _) in _OPCODE_ATTRS)))
429

    
430

    
431
class _MetaOpcodeResource(type):
432
  """Meta class for RAPI resources.
433

434
  """
435
  def __call__(mcs, *args, **kwargs):
436
    """Instantiates class and patches it for use by the RAPI daemon.
437

438
    """
439
    # Access to private attributes of a client class, pylint: disable=W0212
440
    obj = type.__call__(mcs, *args, **kwargs)
441

    
442
    for (method, op_attr, rename_attr, fn_attr) in _OPCODE_ATTRS:
443
      if hasattr(obj, method):
444
        # If the method handler is already defined, "*_RENAME" or "Get*OpInput"
445
        # shouldn't be (they're only used by the automatically generated
446
        # handler)
447
        assert not hasattr(obj, rename_attr)
448
        assert not hasattr(obj, fn_attr)
449
      else:
450
        # Try to generate handler method on handler instance
451
        try:
452
          opcode = getattr(obj, op_attr)
453
        except AttributeError:
454
          pass
455
        else:
456
          setattr(obj, method,
457
                  compat.partial(obj._GenericHandler, opcode,
458
                                 getattr(obj, rename_attr, None),
459
                                 getattr(obj, fn_attr, obj._GetDefaultData)))
460

    
461
    return obj
462

    
463

    
464
class OpcodeResource(ResourceBase):
465
  """Base class for opcode-based RAPI resources.
466

467
  Instances of this class automatically gain handler functions through
468
  L{_MetaOpcodeResource} for any method for which a C{$METHOD$_OPCODE} variable
469
  is defined at class level. Subclasses can define a C{Get$Method$OpInput}
470
  method to do their own opcode input processing (e.g. for static values). The
471
  C{$METHOD$_RENAME} variable defines which values are renamed (see
472
  L{FillOpcode}).
473

474
  @cvar GET_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
475
    automatically generate a GET handler submitting the opcode
476
  @cvar GET_RENAME: Set this to rename parameters in the GET handler (see
477
    L{FillOpcode})
478
  @ivar GetGetOpInput: Define this to override the default method for
479
    getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
480

481
  @cvar PUT_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
482
    automatically generate a PUT handler submitting the opcode
483
  @cvar PUT_RENAME: Set this to rename parameters in the PUT handler (see
484
    L{FillOpcode})
485
  @ivar GetPutOpInput: Define this to override the default method for
486
    getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
487

488
  @cvar POST_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
489
    automatically generate a POST handler submitting the opcode
490
  @cvar POST_RENAME: Set this to rename parameters in the DELETE handler (see
491
    L{FillOpcode})
492
  @ivar GetPostOpInput: Define this to override the default method for
493
    getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
494

495
  @cvar DELETE_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
496
    automatically generate a GET handler submitting the opcode
497
  @cvar DELETE_RENAME: Set this to rename parameters in the DELETE handler (see
498
    L{FillOpcode})
499
  @ivar GetDeleteOpInput: Define this to override the default method for
500
    getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
501

502
  """
503
  __metaclass__ = _MetaOpcodeResource
504

    
505
  def _GetDefaultData(self):
506
    return (self.request_body, None)
507

    
508
  def _GenericHandler(self, opcode, rename, fn):
509
    (body, static) = fn()
510
    op = FillOpcode(opcode, body, static, rename=rename)
511
    return self.SubmitJob([op])