Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / baserlib.py @ 0232b768

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
from ganeti import pathutils
39

    
40

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

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

    
52

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

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

    
61

    
62
_OPCODE_ATTRS = _BuildOpcodeAttributes()
63

    
64

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

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

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

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

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

    
85
  return map(_MapId, ids)
86

    
87

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

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

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

    
97

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

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

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

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

    
113

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

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

120
  @return: a list of mapped dictionaries
121

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

    
129

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

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

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

144
  """
145
  result = {}
146

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

    
154
  return result
155

    
156

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

160
  Parameter types are checked.
161

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

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

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

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

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

    
197
    params.update(static)
198

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

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

    
208
  return op
209

    
210

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

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

    
221
    raise
222

    
223

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

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

230
  @param msg: the message
231

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

    
236

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

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

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

    
252
  return value
253

    
254

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

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

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

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

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

    
278
  if exptype is _DEFAULT:
279
    return value
280

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

    
283

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

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

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

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

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

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

    
310
    self._client_cls = _client_cls
311

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

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

    
318
  request_body = property(fget=_GetRequestBody)
319

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

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

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

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

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

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

355
    @param name: the required parameter
356

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

    
361
    return CheckParameter(self.request_body, name)
362

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
437

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

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

    
445

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

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

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

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

    
476
    return obj
477

    
478

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

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

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

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

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

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

517
  """
518
  __metaclass__ = _MetaOpcodeResource
519

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

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