Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / baserlib.py @ 0ee0bc74

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
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 pathutils
40
from ganeti import utils
41

    
42

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

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

    
54

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

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

    
63

    
64
OPCODE_ATTRS = _BuildOpcodeAttributes()
65

    
66

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

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

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

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

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

    
87
  return map(_MapId, ids)
88

    
89

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

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

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

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

    
105

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

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

112
  @return: a list of mapped dictionaries
113

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

    
121

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

125
  Parameter types are checked.
126

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

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

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

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

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

    
162
    params.update(static)
163

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

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

    
173
  return op
174

    
175

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

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

    
186
    raise
187

    
188

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

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

195
  @param msg: the message
196

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

    
201

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

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

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

    
217
  return value
218

    
219

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

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

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

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

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

    
243
  if exptype is _DEFAULT:
244
    return value
245

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

    
248

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

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

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

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

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

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

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

    
277
    self._client_cls = _client_cls
278

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

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

    
285
  request_body = property(fget=_GetRequestBody)
286

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

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

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

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

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

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

322
    @param name: the required parameter
323

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

    
328
    return CheckParameter(self.request_body, name)
329

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

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

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

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

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

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

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

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

    
354
  def GetClient(self, query=True):
355
    """Wrapper for L{luxi.Client} with HTTP-specific error handling.
356

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

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

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

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

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

    
404

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

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

    
412

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

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

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

    
423

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

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

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

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

    
454
    return obj
455

    
456

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

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

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

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

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

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

501
  """
502
  __metaclass__ = _MetaOpcodeResource
503

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

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

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

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

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

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

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

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