Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / baserlib.py @ 52fc37bd

History | View | Annotate | Download (16.2 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
from ganeti import rapi
34
from ganeti import http
35
from ganeti import errors
36
from ganeti import compat
37
from ganeti import constants
38
from ganeti import pathutils
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, query=False):
354
    """Wrapper for L{luxi.Client} with HTTP-specific error handling.
355

356
    @param query: this signifies that the client will only be used for
357
        queries; if the build-time parameter enable-split-queries is
358
        enabled, then the client will be connected to the query socket
359
        instead of the masterd socket
360

361
    """
362
    if query and constants.ENABLE_SPLIT_QUERY:
363
      address = pathutils.QUERY_SOCKET
364
    else:
365
      address = None
366
    # Could be a function, pylint: disable=R0201
367
    try:
368
      return self._client_cls(address=address)
369
    except luxi.NoMasterError, err:
370
      raise http.HttpBadGateway("Can't connect to master daemon: %s" % err)
371
    except luxi.PermissionError:
372
      raise http.HttpInternalServerError("Internal error: no permission to"
373
                                         " connect to the master daemon")
374

    
375
  def SubmitJob(self, op, cl=None):
376
    """Generic wrapper for submit job, for better http compatibility.
377

378
    @type op: list
379
    @param op: the list of opcodes for the job
380
    @type cl: None or luxi.Client
381
    @param cl: optional luxi client to use
382
    @rtype: string
383
    @return: the job ID
384

385
    """
386
    if cl is None:
387
      cl = self.GetClient()
388
    try:
389
      return cl.SubmitJob(op)
390
    except errors.JobQueueFull:
391
      raise http.HttpServiceUnavailable("Job queue is full, needs archiving")
392
    except errors.JobQueueDrainError:
393
      raise http.HttpServiceUnavailable("Job queue is drained, cannot submit")
394
    except luxi.NoMasterError, err:
395
      raise http.HttpBadGateway("Master seems to be unreachable: %s" % err)
396
    except luxi.PermissionError:
397
      raise http.HttpInternalServerError("Internal error: no permission to"
398
                                         " connect to the master daemon")
399
    except luxi.TimeoutError, err:
400
      raise http.HttpGatewayTimeout("Timeout while talking to the master"
401
                                    " daemon: %s" % err)
402

    
403

    
404
def GetResourceOpcodes(cls):
405
  """Returns all opcodes used by a resource.
406

407
  """
408
  return frozenset(filter(None, (getattr(cls, op_attr, None)
409
                                 for (_, op_attr, _, _) in OPCODE_ATTRS)))
410

    
411

    
412
def GetHandlerAccess(handler, method):
413
  """Returns the access rights for a method on a handler.
414

415
  @type handler: L{ResourceBase}
416
  @type method: string
417
  @rtype: string or None
418

419
  """
420
  return getattr(handler, "%s_ACCESS" % method, None)
421

    
422

    
423
class _MetaOpcodeResource(type):
424
  """Meta class for RAPI resources.
425

426
  """
427
  def __call__(mcs, *args, **kwargs):
428
    """Instantiates class and patches it for use by the RAPI daemon.
429

430
    """
431
    # Access to private attributes of a client class, pylint: disable=W0212
432
    obj = type.__call__(mcs, *args, **kwargs)
433

    
434
    for (method, op_attr, rename_attr, fn_attr) in OPCODE_ATTRS:
435
      if hasattr(obj, method):
436
        # If the method handler is already defined, "*_RENAME" or "Get*OpInput"
437
        # shouldn't be (they're only used by the automatically generated
438
        # handler)
439
        assert not hasattr(obj, rename_attr)
440
        assert not hasattr(obj, fn_attr)
441
      else:
442
        # Try to generate handler method on handler instance
443
        try:
444
          opcode = getattr(obj, op_attr)
445
        except AttributeError:
446
          pass
447
        else:
448
          setattr(obj, method,
449
                  compat.partial(obj._GenericHandler, opcode,
450
                                 getattr(obj, rename_attr, None),
451
                                 getattr(obj, fn_attr, obj._GetDefaultData)))
452

    
453
    return obj
454

    
455

    
456
class OpcodeResource(ResourceBase):
457
  """Base class for opcode-based RAPI resources.
458

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

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

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

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

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

500
  """
501
  __metaclass__ = _MetaOpcodeResource
502

    
503
  def _GetDefaultData(self):
504
    return (self.request_body, None)
505

    
506
  def _GetRapiOpName(self):
507
    """Extracts the name of the RAPI operation from the class name
508

509
    """
510
    if self.__class__.__name__.startswith("R_2_"):
511
      return self.__class__.__name__[4:]
512
    return self.__class__.__name__
513

    
514
  def _GetCommonStatic(self):
515
    """Return the static parameters common to all the RAPI calls
516

517
    The reason is a parameter present in all the RAPI calls, and the reason
518
    trail has to be build for all of them, so the parameter is read here and
519
    used to build the reason trail, that is the actual parameter passed
520
    forward.
521

522
    """
523
    trail = []
524
    usr_reason = self._checkStringVariable("reason", default=None)
525
    if usr_reason:
526
      trail.append((constants.OPCODE_REASON_SRC_USER,
527
                    usr_reason,
528
                    utils.EpochNano()))
529
    reason_src = "%s:%s" % (constants.OPCODE_REASON_SRC_RLIB2,
530
                            self._GetRapiOpName())
531
    trail.append((reason_src, "", utils.EpochNano()))
532
    common_static = {
533
      "reason": trail,
534
      }
535
    return common_static
536

    
537
  def _GetDepends(self):
538
    ret = {}
539
    if isinstance(self.request_body, dict):
540
      depends = self.getBodyParameter("depends", None)
541
      if depends:
542
        ret.update({"depends": depends})
543
    return ret
544

    
545
  def _GenericHandler(self, opcode, rename, fn):
546
    (body, specific_static) = fn()
547
    if isinstance(body, dict):
548
      body.update(self._GetDepends())
549
    static = self._GetCommonStatic()
550
    if specific_static:
551
      static.update(specific_static)
552
    op = FillOpcode(opcode, body, static, rename=rename)
553
    return self.SubmitJob([op])