Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / baserlib.py @ da3faf9d

History | View | Annotate | Download (14.7 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
    assert isinstance(queryargs, dict)
267

    
268
    self.items = items
269
    self.queryargs = queryargs
270
    self._req = req
271

    
272
    if _client_cls is None:
273
      _client_cls = luxi.Client
274

    
275
    self._client_cls = _client_cls
276

    
277
  def _GetRequestBody(self):
278
    """Returns the body data.
279

280
    """
281
    return self._req.private.body_data
282

    
283
  request_body = property(fget=_GetRequestBody)
284

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

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

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

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

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

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

320
    @param name: the required parameter
321

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

    
326
    return CheckParameter(self.request_body, name)
327

    
328
  def useLocking(self):
329
    """Check if the request specifies locking.
330

331
    """
332
    return bool(self._checkIntVariable("lock"))
333

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

337
    """
338
    return bool(self._checkIntVariable("bulk"))
339

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

343
    """
344
    return bool(self._checkIntVariable("force"))
345

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

349
    """
350
    return bool(self._checkIntVariable("dry-run"))
351

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

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

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

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

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

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

    
402

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

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

    
410

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

414
  @type handler: L{ResourceBase}
415
  @type method: string
416
  @rtype: string or None
417

418
  """
419
  return getattr(handler, "%s_ACCESS" % method, None)
420

    
421

    
422
class _MetaOpcodeResource(type):
423
  """Meta class for RAPI resources.
424

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

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

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

    
452
    return obj
453

    
454

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

458
  Instances of this class automatically gain handler functions through
459
  L{_MetaOpcodeResource} for any method for which a C{$METHOD$_OPCODE} variable
460
  is defined at class level. Subclasses can define a C{Get$Method$OpInput}
461
  method to do their own opcode input processing (e.g. for static values). The
462
  C{$METHOD$_RENAME} variable defines which values are renamed (see
463
  L{baserlib.FillOpcode}).
464

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

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

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

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

493
  """
494
  __metaclass__ = _MetaOpcodeResource
495

    
496
  def _GetDefaultData(self):
497
    return (self.request_body, None)
498

    
499
  def _GetCommonStatic(self):
500
    """Return the static parameters common to all the RAPI calls
501

502
    """
503
    common_static = {}
504
    return common_static
505

    
506
  def _GenericHandler(self, opcode, rename, fn):
507
    (body, specific_static) = fn()
508
    static = self._GetCommonStatic()
509
    if specific_static:
510
      static.update(specific_static)
511
    op = FillOpcode(opcode, body, static, rename=rename)
512
    return self.SubmitJob([op])