Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / baserlib.py @ 303bc802

History | View | Annotate | Download (14.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
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

    
39

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

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

    
51

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

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

    
60

    
61
_OPCODE_ATTRS = _BuildOpcodeAttributes()
62

    
63

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

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

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

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

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

    
84
  return map(_MapId, ids)
85

    
86

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

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

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

    
96

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

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

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

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

    
112

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

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

119
  @return: a list of mapped dictionaries
120

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

    
128

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

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

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

143
  """
144
  result = {}
145

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

    
153
  return result
154

    
155

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

159
  Parameter types are checked.
160

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

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

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

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

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

    
196
    params.update(static)
197

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

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

    
207
  return op
208

    
209

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

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

    
220
    raise
221

    
222

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

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

229
  @param msg: the message
230

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

    
235

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

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

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

    
251
  return value
252

    
253

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

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

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

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

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

    
277
  if exptype is _DEFAULT:
278
    return value
279

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

    
282

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

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

    
293
  def __init__(self, items, queryargs, req, _client_cls=None):
294
    """Generic resource constructor.
295

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

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

    
306
    if _client_cls is None:
307
      _client_cls = luxi.Client
308

    
309
    self._client_cls = _client_cls
310

    
311
  def _GetRequestBody(self):
312
    """Returns the body data.
313

314
    """
315
    return self._req.private.body_data
316

    
317
  request_body = property(fget=_GetRequestBody)
318

    
319
  def _checkIntVariable(self, name, default=0):
320
    """Return the parsed value of an int argument.
321

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

    
336
  def _checkStringVariable(self, name, default=None):
337
    """Return the parsed value of an int argument.
338

339
    """
340
    val = self.queryargs.get(name, default)
341
    if isinstance(val, list):
342
      if val:
343
        val = val[0]
344
      else:
345
        val = default
346
    return val
347

    
348
  def getBodyParameter(self, name, *args):
349
    """Check and return the value for a given parameter.
350

351
    If a second parameter is not given, an error will be returned,
352
    otherwise this parameter specifies the default value.
353

354
    @param name: the required parameter
355

356
    """
357
    if args:
358
      return CheckParameter(self.request_body, name, default=args[0])
359

    
360
    return CheckParameter(self.request_body, name)
361

    
362
  def useLocking(self):
363
    """Check if the request specifies locking.
364

365
    """
366
    return bool(self._checkIntVariable("lock"))
367

    
368
  def useBulk(self):
369
    """Check if the request specifies bulk querying.
370

371
    """
372
    return bool(self._checkIntVariable("bulk"))
373

    
374
  def useForce(self):
375
    """Check if the request specifies a forced operation.
376

377
    """
378
    return bool(self._checkIntVariable("force"))
379

    
380
  def dryRun(self):
381
    """Check if the request specifies dry-run mode.
382

383
    """
384
    return bool(self._checkIntVariable("dry-run"))
385

    
386
  def GetClient(self, query=False):
387
    """Wrapper for L{luxi.Client} with HTTP-specific error handling.
388

389
    @param query: this signifies that the client will only be used for
390
        queries; if the build-time parameter enable-split-queries is
391
        enabled, then the client will be connected to the query socket
392
        instead of the masterd socket
393

394
    """
395
    if query and constants.ENABLE_SPLIT_QUERY:
396
      address = constants.QUERY_SOCKET
397
    else:
398
      address = None
399
    # Could be a function, pylint: disable=R0201
400
    try:
401
      return self._client_cls(address=address)
402
    except luxi.NoMasterError, err:
403
      raise http.HttpBadGateway("Can't connect to master daemon: %s" % err)
404
    except luxi.PermissionError:
405
      raise http.HttpInternalServerError("Internal error: no permission to"
406
                                         " connect to the master daemon")
407

    
408
  def SubmitJob(self, op, cl=None):
409
    """Generic wrapper for submit job, for better http compatibility.
410

411
    @type op: list
412
    @param op: the list of opcodes for the job
413
    @type cl: None or luxi.Client
414
    @param cl: optional luxi client to use
415
    @rtype: string
416
    @return: the job ID
417

418
    """
419
    if cl is None:
420
      cl = self.GetClient()
421
    try:
422
      return cl.SubmitJob(op)
423
    except errors.JobQueueFull:
424
      raise http.HttpServiceUnavailable("Job queue is full, needs archiving")
425
    except errors.JobQueueDrainError:
426
      raise http.HttpServiceUnavailable("Job queue is drained, cannot submit")
427
    except luxi.NoMasterError, err:
428
      raise http.HttpBadGateway("Master seems to be unreachable: %s" % err)
429
    except luxi.PermissionError:
430
      raise http.HttpInternalServerError("Internal error: no permission to"
431
                                         " connect to the master daemon")
432
    except luxi.TimeoutError, err:
433
      raise http.HttpGatewayTimeout("Timeout while talking to the master"
434
                                    " daemon: %s" % err)
435

    
436

    
437
def GetResourceOpcodes(cls):
438
  """Returns all opcodes used by a resource.
439

440
  """
441
  return frozenset(filter(None, (getattr(cls, op_attr, None)
442
                                 for (_, op_attr, _, _) in _OPCODE_ATTRS)))
443

    
444

    
445
class _MetaOpcodeResource(type):
446
  """Meta class for RAPI resources.
447

448
  """
449
  def __call__(mcs, *args, **kwargs):
450
    """Instantiates class and patches it for use by the RAPI daemon.
451

452
    """
453
    # Access to private attributes of a client class, pylint: disable=W0212
454
    obj = type.__call__(mcs, *args, **kwargs)
455

    
456
    for (method, op_attr, rename_attr, fn_attr) in _OPCODE_ATTRS:
457
      if hasattr(obj, method):
458
        # If the method handler is already defined, "*_RENAME" or "Get*OpInput"
459
        # shouldn't be (they're only used by the automatically generated
460
        # handler)
461
        assert not hasattr(obj, rename_attr)
462
        assert not hasattr(obj, fn_attr)
463
      else:
464
        # Try to generate handler method on handler instance
465
        try:
466
          opcode = getattr(obj, op_attr)
467
        except AttributeError:
468
          pass
469
        else:
470
          setattr(obj, method,
471
                  compat.partial(obj._GenericHandler, opcode,
472
                                 getattr(obj, rename_attr, None),
473
                                 getattr(obj, fn_attr, obj._GetDefaultData)))
474

    
475
    return obj
476

    
477

    
478
class OpcodeResource(ResourceBase):
479
  """Base class for opcode-based RAPI resources.
480

481
  Instances of this class automatically gain handler functions through
482
  L{_MetaOpcodeResource} for any method for which a C{$METHOD$_OPCODE} variable
483
  is defined at class level. Subclasses can define a C{Get$Method$OpInput}
484
  method to do their own opcode input processing (e.g. for static values). The
485
  C{$METHOD$_RENAME} variable defines which values are renamed (see
486
  L{baserlib.FillOpcode}).
487

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

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

502
  @cvar POST_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
503
    automatically generate a POST handler submitting the opcode
504
  @cvar POST_RENAME: Set this to rename parameters in the DELETE handler (see
505
    L{baserlib.FillOpcode})
506
  @ivar GetPostOpInput: Define this to override the default method for
507
    getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
508

509
  @cvar DELETE_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
510
    automatically generate a GET handler submitting the opcode
511
  @cvar DELETE_RENAME: Set this to rename parameters in the DELETE handler (see
512
    L{baserlib.FillOpcode})
513
  @ivar GetDeleteOpInput: Define this to override the default method for
514
    getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
515

516
  """
517
  __metaclass__ = _MetaOpcodeResource
518

    
519
  def _GetDefaultData(self):
520
    return (self.request_body, None)
521

    
522
  def _GenericHandler(self, opcode, rename, fn):
523
    (body, static) = fn()
524
    op = FillOpcode(opcode, body, static, rename=rename)
525
    return self.SubmitJob([op])