Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / baserlib.py @ a154caa4

History | View | Annotate | Download (15.9 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008, 2012 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
import ganeti.rpc.errors as rpcerr
34
from ganeti import rapi
35
from ganeti import http
36
from ganeti import errors
37
from ganeti import compat
38
from ganeti import constants
39
from ganeti import utils
40

    
41

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

    
45
#: Supported HTTP methods
46
_SUPPORTED_METHODS = compat.UniqueFrozenset([
47
  http.HTTP_DELETE,
48
  http.HTTP_GET,
49
  http.HTTP_POST,
50
  http.HTTP_PUT,
51
  ])
52

    
53

    
54
def _BuildOpcodeAttributes():
55
  """Builds list of attributes used for per-handler opcodes.
56

57
  """
58
  return [(method, "%s_OPCODE" % method, "%s_RENAME" % method,
59
           "Get%sOpInput" % method.capitalize())
60
          for method in _SUPPORTED_METHODS]
61

    
62

    
63
OPCODE_ATTRS = _BuildOpcodeAttributes()
64

    
65

    
66
def BuildUriList(ids, uri_format, uri_fields=("name", "uri")):
67
  """Builds a URI list as used by index resources.
68

69
  @param ids: list of ids as strings
70
  @param uri_format: format to be applied for URI
71
  @param uri_fields: optional parameter for field IDs
72

73
  """
74
  (field_id, field_uri) = uri_fields
75

    
76
  def _MapId(m_id):
77
    return {
78
      field_id: m_id,
79
      field_uri: uri_format % m_id,
80
      }
81

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

    
86
  return map(_MapId, ids)
87

    
88

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

92
  Example::
93
      >>> MapFields(["a", "b"], ["foo", 123])
94
      {'a': 'foo', 'b': 123}
95

96
  @param names: field names (list of strings)
97
  @param data: field data (list)
98

99
  """
100
  if len(names) != len(data):
101
    raise AttributeError("Names and data must have the same length")
102
  return dict(zip(names, data))
103

    
104

    
105
def MapBulkFields(itemslist, fields):
106
  """Map value to field name in to one dictionary.
107

108
  @param itemslist: a list of items values
109
  @param fields: a list of items names
110

111
  @return: a list of mapped dictionaries
112

113
  """
114
  items_details = []
115
  for item in itemslist:
116
    mapped = MapFields(fields, item)
117
    items_details.append(mapped)
118
  return items_details
119

    
120

    
121
def FillOpcode(opcls, body, static, rename=None):
122
  """Fills an opcode with body parameters.
123

124
  Parameter types are checked.
125

126
  @type opcls: L{opcodes.OpCode}
127
  @param opcls: Opcode class
128
  @type body: dict
129
  @param body: Body parameters as received from client
130
  @type static: dict
131
  @param static: Static parameters which can't be modified by client
132
  @type rename: dict
133
  @param rename: Renamed parameters, key as old name, value as new name
134
  @return: Opcode object
135

136
  """
137
  if body is None:
138
    params = {}
139
  else:
140
    CheckType(body, dict, "Body contents")
141

    
142
    # Make copy to be modified
143
    params = body.copy()
144

    
145
  if rename:
146
    for old, new in rename.items():
147
      if new in params and old in params:
148
        raise http.HttpBadRequest("Parameter '%s' was renamed to '%s', but"
149
                                  " both are specified" %
150
                                  (old, new))
151
      if old in params:
152
        assert new not in params
153
        params[new] = params.pop(old)
154

    
155
  if static:
156
    overwritten = set(params.keys()) & set(static.keys())
157
    if overwritten:
158
      raise http.HttpBadRequest("Can't overwrite static parameters %r" %
159
                                overwritten)
160

    
161
    params.update(static)
162

    
163
  # Convert keys to strings (simplejson decodes them as unicode)
164
  params = dict((str(key), value) for (key, value) in params.items())
165

    
166
  try:
167
    op = opcls(**params) # pylint: disable=W0142
168
    op.Validate(False)
169
  except (errors.OpPrereqError, TypeError), err:
170
    raise http.HttpBadRequest("Invalid body parameters: %s" % err)
171

    
172
  return op
173

    
174

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

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

    
185
    raise
186

    
187

    
188
def FeedbackFn(msg):
189
  """Feedback logging function for jobs.
190

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

194
  @param msg: the message
195

196
  """
197
  (_, log_type, log_msg) = msg
198
  logging.info("%s: %s", log_type, log_msg)
199

    
200

    
201
def CheckType(value, exptype, descr):
202
  """Abort request if value type doesn't match expected type.
203

204
  @param value: Value
205
  @type exptype: type
206
  @param exptype: Expected type
207
  @type descr: string
208
  @param descr: Description of value
209
  @return: Value (allows inline usage)
210

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

    
216
  return value
217

    
218

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

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

225
  @type data: dict
226
  @param data: Dictionary containing input data
227
  @type name: string
228
  @param name: Parameter name
229
  @param default: Default value (can be None)
230
  @param exptype: Expected type (can be None)
231

232
  """
233
  try:
234
    value = data[name]
235
  except KeyError:
236
    if default is not _DEFAULT:
237
      return default
238

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

    
242
  if exptype is _DEFAULT:
243
    return value
244

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

    
247

    
248
class ResourceBase(object):
249
  """Generic class for resources.
250

251
  """
252
  # Default permission requirements
253
  GET_ACCESS = []
254
  PUT_ACCESS = [rapi.RAPI_ACCESS_WRITE]
255
  POST_ACCESS = [rapi.RAPI_ACCESS_WRITE]
256
  DELETE_ACCESS = [rapi.RAPI_ACCESS_WRITE]
257

    
258
  def __init__(self, items, queryargs, req, _client_cls=None):
259
    """Generic resource constructor.
260

261
    @param items: a list with variables encoded in the URL
262
    @param queryargs: a dictionary with additional options from URL
263
    @param req: Request context
264
    @param _client_cls: L{luxi} client class (unittests only)
265

266
    """
267
    assert isinstance(queryargs, dict)
268

    
269
    self.items = items
270
    self.queryargs = queryargs
271
    self._req = req
272

    
273
    if _client_cls is None:
274
      _client_cls = luxi.Client
275

    
276
    self._client_cls = _client_cls
277

    
278
  def _GetRequestBody(self):
279
    """Returns the body data.
280

281
    """
282
    return self._req.private.body_data
283

    
284
  request_body = property(fget=_GetRequestBody)
285

    
286
  def _checkIntVariable(self, name, default=0):
287
    """Return the parsed value of an int argument.
288

289
    """
290
    val = self.queryargs.get(name, default)
291
    if isinstance(val, list):
292
      if val:
293
        val = val[0]
294
      else:
295
        val = default
296
    try:
297
      val = int(val)
298
    except (ValueError, TypeError):
299
      raise http.HttpBadRequest("Invalid value for the"
300
                                " '%s' parameter" % (name,))
301
    return val
302

    
303
  def _checkStringVariable(self, name, default=None):
304
    """Return the parsed value of a string argument.
305

306
    """
307
    val = self.queryargs.get(name, default)
308
    if isinstance(val, list):
309
      if val:
310
        val = val[0]
311
      else:
312
        val = default
313
    return val
314

    
315
  def getBodyParameter(self, name, *args):
316
    """Check and return the value for a given parameter.
317

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

321
    @param name: the required parameter
322

323
    """
324
    if args:
325
      return CheckParameter(self.request_body, name, default=args[0])
326

    
327
    return CheckParameter(self.request_body, name)
328

    
329
  def useLocking(self):
330
    """Check if the request specifies locking.
331

332
    """
333
    return bool(self._checkIntVariable("lock"))
334

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

338
    """
339
    return bool(self._checkIntVariable("bulk"))
340

    
341
  def useForce(self):
342
    """Check if the request specifies a forced operation.
343

344
    """
345
    return bool(self._checkIntVariable("force"))
346

    
347
  def dryRun(self):
348
    """Check if the request specifies dry-run mode.
349

350
    """
351
    return bool(self._checkIntVariable("dry-run"))
352

    
353
  def GetClient(self):
354
    """Wrapper for L{luxi.Client} with HTTP-specific error handling.
355

356
    """
357
    # Could be a function, pylint: disable=R0201
358
    try:
359
      return self._client_cls()
360
    except rpcerr.NoMasterError, err:
361
      raise http.HttpBadGateway("Can't connect to master daemon: %s" % err)
362
    except rpcerr.PermissionError:
363
      raise http.HttpInternalServerError("Internal error: no permission to"
364
                                         " connect to the master daemon")
365

    
366
  def SubmitJob(self, op, cl=None):
367
    """Generic wrapper for submit job, for better http compatibility.
368

369
    @type op: list
370
    @param op: the list of opcodes for the job
371
    @type cl: None or luxi.Client
372
    @param cl: optional luxi client to use
373
    @rtype: string
374
    @return: the job ID
375

376
    """
377
    if cl is None:
378
      cl = self.GetClient()
379
    try:
380
      return cl.SubmitJob(op)
381
    except errors.JobQueueFull:
382
      raise http.HttpServiceUnavailable("Job queue is full, needs archiving")
383
    except errors.JobQueueDrainError:
384
      raise http.HttpServiceUnavailable("Job queue is drained, cannot submit")
385
    except rpcerr.NoMasterError, err:
386
      raise http.HttpBadGateway("Master seems to be unreachable: %s" % err)
387
    except rpcerr.PermissionError:
388
      raise http.HttpInternalServerError("Internal error: no permission to"
389
                                         " connect to the master daemon")
390
    except rpcerr.TimeoutError, err:
391
      raise http.HttpGatewayTimeout("Timeout while talking to the master"
392
                                    " daemon: %s" % err)
393

    
394

    
395
def GetResourceOpcodes(cls):
396
  """Returns all opcodes used by a resource.
397

398
  """
399
  return frozenset(filter(None, (getattr(cls, op_attr, None)
400
                                 for (_, op_attr, _, _) in OPCODE_ATTRS)))
401

    
402

    
403
def GetHandlerAccess(handler, method):
404
  """Returns the access rights for a method on a handler.
405

406
  @type handler: L{ResourceBase}
407
  @type method: string
408
  @rtype: string or None
409

410
  """
411
  return getattr(handler, "%s_ACCESS" % method, None)
412

    
413

    
414
class _MetaOpcodeResource(type):
415
  """Meta class for RAPI resources.
416

417
  """
418
  def __call__(mcs, *args, **kwargs):
419
    """Instantiates class and patches it for use by the RAPI daemon.
420

421
    """
422
    # Access to private attributes of a client class, pylint: disable=W0212
423
    obj = type.__call__(mcs, *args, **kwargs)
424

    
425
    for (method, op_attr, rename_attr, fn_attr) in OPCODE_ATTRS:
426
      if hasattr(obj, method):
427
        # If the method handler is already defined, "*_RENAME" or "Get*OpInput"
428
        # shouldn't be (they're only used by the automatically generated
429
        # handler)
430
        assert not hasattr(obj, rename_attr)
431
        assert not hasattr(obj, fn_attr)
432
      else:
433
        # Try to generate handler method on handler instance
434
        try:
435
          opcode = getattr(obj, op_attr)
436
        except AttributeError:
437
          pass
438
        else:
439
          setattr(obj, method,
440
                  compat.partial(obj._GenericHandler, opcode,
441
                                 getattr(obj, rename_attr, None),
442
                                 getattr(obj, fn_attr, obj._GetDefaultData)))
443

    
444
    return obj
445

    
446

    
447
class OpcodeResource(ResourceBase):
448
  """Base class for opcode-based RAPI resources.
449

450
  Instances of this class automatically gain handler functions through
451
  L{_MetaOpcodeResource} for any method for which a C{$METHOD$_OPCODE} variable
452
  is defined at class level. Subclasses can define a C{Get$Method$OpInput}
453
  method to do their own opcode input processing (e.g. for static values). The
454
  C{$METHOD$_RENAME} variable defines which values are renamed (see
455
  L{baserlib.FillOpcode}).
456
  Still default behavior cannot be totally overriden. There are opcode params
457
  that are available to all opcodes, e.g. "depends". In case those params
458
  (currently only "depends") are found in the original request's body, they are
459
  added to the dictionary of parsed parameters and eventually passed to the
460
  opcode. If the parsed body is not represented as a dictionary object, the
461
  values are not added.
462

463
  @cvar GET_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
464
    automatically generate a GET handler submitting the opcode
465
  @cvar GET_RENAME: Set this to rename parameters in the GET handler (see
466
    L{baserlib.FillOpcode})
467
  @ivar GetGetOpInput: Define this to override the default method for
468
    getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
469

470
  @cvar PUT_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
471
    automatically generate a PUT handler submitting the opcode
472
  @cvar PUT_RENAME: Set this to rename parameters in the PUT handler (see
473
    L{baserlib.FillOpcode})
474
  @ivar GetPutOpInput: Define this to override the default method for
475
    getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
476

477
  @cvar POST_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
478
    automatically generate a POST handler submitting the opcode
479
  @cvar POST_RENAME: Set this to rename parameters in the POST handler (see
480
    L{baserlib.FillOpcode})
481
  @ivar GetPostOpInput: Define this to override the default method for
482
    getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
483

484
  @cvar DELETE_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
485
    automatically generate a DELETE handler submitting the opcode
486
  @cvar DELETE_RENAME: Set this to rename parameters in the DELETE handler (see
487
    L{baserlib.FillOpcode})
488
  @ivar GetDeleteOpInput: Define this to override the default method for
489
    getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
490

491
  """
492
  __metaclass__ = _MetaOpcodeResource
493

    
494
  def _GetDefaultData(self):
495
    return (self.request_body, None)
496

    
497
  def _GetRapiOpName(self):
498
    """Extracts the name of the RAPI operation from the class name
499

500
    """
501
    if self.__class__.__name__.startswith("R_2_"):
502
      return self.__class__.__name__[4:]
503
    return self.__class__.__name__
504

    
505
  def _GetCommonStatic(self):
506
    """Return the static parameters common to all the RAPI calls
507

508
    The reason is a parameter present in all the RAPI calls, and the reason
509
    trail has to be build for all of them, so the parameter is read here and
510
    used to build the reason trail, that is the actual parameter passed
511
    forward.
512

513
    """
514
    trail = []
515
    usr_reason = self._checkStringVariable("reason", default=None)
516
    if usr_reason:
517
      trail.append((constants.OPCODE_REASON_SRC_USER,
518
                    usr_reason,
519
                    utils.EpochNano()))
520
    reason_src = "%s:%s" % (constants.OPCODE_REASON_SRC_RLIB2,
521
                            self._GetRapiOpName())
522
    trail.append((reason_src, "", utils.EpochNano()))
523
    common_static = {
524
      "reason": trail,
525
      }
526
    return common_static
527

    
528
  def _GetDepends(self):
529
    ret = {}
530
    if isinstance(self.request_body, dict):
531
      depends = self.getBodyParameter("depends", None)
532
      if depends:
533
        ret.update({"depends": depends})
534
    return ret
535

    
536
  def _GenericHandler(self, opcode, rename, fn):
537
    (body, specific_static) = fn()
538
    if isinstance(body, dict):
539
      body.update(self._GetDepends())
540
    static = self._GetCommonStatic()
541
    if specific_static:
542
      static.update(specific_static)
543
    op = FillOpcode(opcode, body, static, rename=rename)
544
    return self.SubmitJob([op])