Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / baserlib.py @ 20ba96f5

History | View | Annotate | Download (14.1 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 = compat.UniqueFrozenset([
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 MapFields(names, data):
89
  """Maps two lists into one dictionary.
90

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

95
  @param names: field names (list of strings)
96
  @param data: field data (list)
97

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

    
103

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

107
  @param itemslist: a list of items values
108
  @param fields: a list of items names
109

110
  @return: a list of mapped dictionaries
111

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

    
119

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

123
  Parameter types are checked.
124

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

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

    
141
    # Make copy to be modified
142
    params = body.copy()
143

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

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

    
160
    params.update(static)
161

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

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

    
171
  return op
172

    
173

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

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

    
184
    raise
185

    
186

    
187
def FeedbackFn(msg):
188
  """Feedback logging function for jobs.
189

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

193
  @param msg: the message
194

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

    
199

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

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

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

    
215
  return value
216

    
217

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

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

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

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

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

    
241
  if exptype is _DEFAULT:
242
    return value
243

    
244
  return CheckType(value, exptype, "'%s' parameter" % name)
245

    
246

    
247
class ResourceBase(object):
248
  """Generic class for resources.
249

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

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

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

265
    """
266
    self.items = items
267
    self.queryargs = queryargs
268
    self._req = req
269

    
270
    if _client_cls is None:
271
      _client_cls = luxi.Client
272

    
273
    self._client_cls = _client_cls
274

    
275
  def _GetRequestBody(self):
276
    """Returns the body data.
277

278
    """
279
    return self._req.private.body_data
280

    
281
  request_body = property(fget=_GetRequestBody)
282

    
283
  def _checkIntVariable(self, name, default=0):
284
    """Return the parsed value of an int argument.
285

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

    
300
  def _checkStringVariable(self, name, default=None):
301
    """Return the parsed value of a string argument.
302

303
    """
304
    val = self.queryargs.get(name, default)
305
    if isinstance(val, list):
306
      if val:
307
        val = val[0]
308
      else:
309
        val = default
310
    return val
311

    
312
  def getBodyParameter(self, name, *args):
313
    """Check and return the value for a given parameter.
314

315
    If a second parameter is not given, an error will be returned,
316
    otherwise this parameter specifies the default value.
317

318
    @param name: the required parameter
319

320
    """
321
    if args:
322
      return CheckParameter(self.request_body, name, default=args[0])
323

    
324
    return CheckParameter(self.request_body, name)
325

    
326
  def useLocking(self):
327
    """Check if the request specifies locking.
328

329
    """
330
    return bool(self._checkIntVariable("lock"))
331

    
332
  def useBulk(self):
333
    """Check if the request specifies bulk querying.
334

335
    """
336
    return bool(self._checkIntVariable("bulk"))
337

    
338
  def useForce(self):
339
    """Check if the request specifies a forced operation.
340

341
    """
342
    return bool(self._checkIntVariable("force"))
343

    
344
  def dryRun(self):
345
    """Check if the request specifies dry-run mode.
346

347
    """
348
    return bool(self._checkIntVariable("dry-run"))
349

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

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

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

    
372
  def SubmitJob(self, op, cl=None):
373
    """Generic wrapper for submit job, for better http compatibility.
374

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

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

    
400

    
401
def GetResourceOpcodes(cls):
402
  """Returns all opcodes used by a resource.
403

404
  """
405
  return frozenset(filter(None, (getattr(cls, op_attr, None)
406
                                 for (_, op_attr, _, _) in _OPCODE_ATTRS)))
407

    
408

    
409
class _MetaOpcodeResource(type):
410
  """Meta class for RAPI resources.
411

412
  """
413
  def __call__(mcs, *args, **kwargs):
414
    """Instantiates class and patches it for use by the RAPI daemon.
415

416
    """
417
    # Access to private attributes of a client class, pylint: disable=W0212
418
    obj = type.__call__(mcs, *args, **kwargs)
419

    
420
    for (method, op_attr, rename_attr, fn_attr) in _OPCODE_ATTRS:
421
      if hasattr(obj, method):
422
        # If the method handler is already defined, "*_RENAME" or "Get*OpInput"
423
        # shouldn't be (they're only used by the automatically generated
424
        # handler)
425
        assert not hasattr(obj, rename_attr)
426
        assert not hasattr(obj, fn_attr)
427
      else:
428
        # Try to generate handler method on handler instance
429
        try:
430
          opcode = getattr(obj, op_attr)
431
        except AttributeError:
432
          pass
433
        else:
434
          setattr(obj, method,
435
                  compat.partial(obj._GenericHandler, opcode,
436
                                 getattr(obj, rename_attr, None),
437
                                 getattr(obj, fn_attr, obj._GetDefaultData)))
438

    
439
    return obj
440

    
441

    
442
class OpcodeResource(ResourceBase):
443
  """Base class for opcode-based RAPI resources.
444

445
  Instances of this class automatically gain handler functions through
446
  L{_MetaOpcodeResource} for any method for which a C{$METHOD$_OPCODE} variable
447
  is defined at class level. Subclasses can define a C{Get$Method$OpInput}
448
  method to do their own opcode input processing (e.g. for static values). The
449
  C{$METHOD$_RENAME} variable defines which values are renamed (see
450
  L{baserlib.FillOpcode}).
451

452
  @cvar GET_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
453
    automatically generate a GET handler submitting the opcode
454
  @cvar GET_RENAME: Set this to rename parameters in the GET handler (see
455
    L{baserlib.FillOpcode})
456
  @ivar GetGetOpInput: Define this to override the default method for
457
    getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
458

459
  @cvar PUT_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
460
    automatically generate a PUT handler submitting the opcode
461
  @cvar PUT_RENAME: Set this to rename parameters in the PUT handler (see
462
    L{baserlib.FillOpcode})
463
  @ivar GetPutOpInput: Define this to override the default method for
464
    getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
465

466
  @cvar POST_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
467
    automatically generate a POST handler submitting the opcode
468
  @cvar POST_RENAME: Set this to rename parameters in the POST handler (see
469
    L{baserlib.FillOpcode})
470
  @ivar GetPostOpInput: Define this to override the default method for
471
    getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
472

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

480
  """
481
  __metaclass__ = _MetaOpcodeResource
482

    
483
  def _GetDefaultData(self):
484
    return (self.request_body, None)
485

    
486
  def _GenericHandler(self, opcode, rename, fn):
487
    (body, static) = fn()
488
    op = FillOpcode(opcode, body, static, rename=rename)
489
    return self.SubmitJob([op])