Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / baserlib.py @ da04c447

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

    
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=None):
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

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

    
308
    self._client_cls = _client_cls
309

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

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

    
316
  request_body = property(fget=_GetRequestBody)
317

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

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

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

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

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

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

353
    @param name: the required parameter
354

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

    
359
    return CheckParameter(self.request_body, name)
360

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

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

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

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

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

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

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

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

    
385
  def GetClient(self):
386
    """Wrapper for L{luxi.Client} with HTTP-specific error handling.
387

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

    
398
  def SubmitJob(self, op, cl=None):
399
    """Generic wrapper for submit job, for better http compatibility.
400

401
    @type op: list
402
    @param op: the list of opcodes for the job
403
    @type cl: None or luxi.Client
404
    @param cl: optional luxi client to use
405
    @rtype: string
406
    @return: the job ID
407

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

    
426

    
427
def GetResourceOpcodes(cls):
428
  """Returns all opcodes used by a resource.
429

430
  """
431
  return frozenset(filter(None, (getattr(cls, op_attr, None)
432
                                 for (_, op_attr, _, _) in _OPCODE_ATTRS)))
433

    
434

    
435
class _MetaOpcodeResource(type):
436
  """Meta class for RAPI resources.
437

438
  """
439
  def __call__(mcs, *args, **kwargs):
440
    """Instantiates class and patches it for use by the RAPI daemon.
441

442
    """
443
    # Access to private attributes of a client class, pylint: disable=W0212
444
    obj = type.__call__(mcs, *args, **kwargs)
445

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

    
465
    return obj
466

    
467

    
468
class OpcodeResource(ResourceBase):
469
  """Base class for opcode-based RAPI resources.
470

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

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

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

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

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

506
  """
507
  __metaclass__ = _MetaOpcodeResource
508

    
509
  def _GetDefaultData(self):
510
    return (self.request_body, None)
511

    
512
  def _GenericHandler(self, opcode, rename, fn):
513
    (body, static) = fn()
514
    op = FillOpcode(opcode, body, static, rename=rename)
515
    return self.SubmitJob([op])