Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / baserlib.py @ 414ebaf1

History | View | Annotate | Download (11.3 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008 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-msg=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 ssconf
36
from ganeti import constants
37
from ganeti import opcodes
38
from ganeti import errors
39

    
40

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

    
44

    
45
def BuildUriList(ids, uri_format, uri_fields=("name", "uri")):
46
  """Builds a URI list as used by index resources.
47

48
  @param ids: list of ids as strings
49
  @param uri_format: format to be applied for URI
50
  @param uri_fields: optional parameter for field IDs
51

52
  """
53
  (field_id, field_uri) = uri_fields
54

    
55
  def _MapId(m_id):
56
    return { field_id: m_id, field_uri: uri_format % m_id, }
57

    
58
  # Make sure the result is sorted, makes it nicer to look at and simplifies
59
  # unittests.
60
  ids.sort()
61

    
62
  return map(_MapId, ids)
63

    
64

    
65
def ExtractField(sequence, index):
66
  """Creates a list containing one column out of a list of lists.
67

68
  @param sequence: sequence of lists
69
  @param index: index of field
70

71
  """
72
  return map(lambda item: item[index], sequence)
73

    
74

    
75
def MapFields(names, data):
76
  """Maps two lists into one dictionary.
77

78
  Example::
79
      >>> MapFields(["a", "b"], ["foo", 123])
80
      {'a': 'foo', 'b': 123}
81

82
  @param names: field names (list of strings)
83
  @param data: field data (list)
84

85
  """
86
  if len(names) != len(data):
87
    raise AttributeError("Names and data must have the same length")
88
  return dict(zip(names, data))
89

    
90

    
91
def _Tags_GET(kind, name):
92
  """Helper function to retrieve tags.
93

94
  """
95
  if kind in (constants.TAG_INSTANCE,
96
              constants.TAG_NODEGROUP,
97
              constants.TAG_NODE):
98
    if not name:
99
      raise http.HttpBadRequest("Missing name on tag request")
100
    cl = GetClient()
101
    if kind == constants.TAG_INSTANCE:
102
      fn = cl.QueryInstances
103
    elif kind == constants.TAG_NODEGROUP:
104
      fn = cl.QueryGroups
105
    else:
106
      fn = cl.QueryNodes
107
    result = fn(names=[name], fields=["tags"], use_locking=False)
108
    if not result or not result[0]:
109
      raise http.HttpBadGateway("Invalid response from tag query")
110
    tags = result[0][0]
111
  elif kind == constants.TAG_CLUSTER:
112
    ssc = ssconf.SimpleStore()
113
    tags = ssc.GetClusterTags()
114

    
115
  return list(tags)
116

    
117

    
118
def _Tags_PUT(kind, tags, name, dry_run):
119
  """Helper function to set tags.
120

121
  """
122
  return SubmitJob([opcodes.OpTagsSet(kind=kind, name=name,
123
                                      tags=tags, dry_run=dry_run)])
124

    
125

    
126
def _Tags_DELETE(kind, tags, name, dry_run):
127
  """Helper function to delete tags.
128

129
  """
130
  return SubmitJob([opcodes.OpTagsDel(kind=kind, name=name,
131
                                      tags=tags, dry_run=dry_run)])
132

    
133

    
134
def MapBulkFields(itemslist, fields):
135
  """Map value to field name in to one dictionary.
136

137
  @param itemslist: a list of items values
138
  @param fields: a list of items names
139

140
  @return: a list of mapped dictionaries
141

142
  """
143
  items_details = []
144
  for item in itemslist:
145
    mapped = MapFields(fields, item)
146
    items_details.append(mapped)
147
  return items_details
148

    
149

    
150
def MakeParamsDict(opts, params):
151
  """Makes params dictionary out of a option set.
152

153
  This function returns a dictionary needed for hv or be parameters. But only
154
  those fields which provided in the option set. Takes parameters frozensets
155
  from constants.
156

157
  @type opts: dict
158
  @param opts: selected options
159
  @type params: frozenset
160
  @param params: subset of options
161
  @rtype: dict
162
  @return: dictionary of options, filtered by given subset.
163

164
  """
165
  result = {}
166

    
167
  for p in params:
168
    try:
169
      value = opts[p]
170
    except KeyError:
171
      continue
172
    result[p] = value
173

    
174
  return result
175

    
176

    
177
def FillOpcode(opcls, body, static, rename=None):
178
  """Fills an opcode with body parameters.
179

180
  Parameter types are checked.
181

182
  @type opcls: L{opcodes.OpCode}
183
  @param opcls: Opcode class
184
  @type body: dict
185
  @param body: Body parameters as received from client
186
  @type static: dict
187
  @param static: Static parameters which can't be modified by client
188
  @type rename: dict
189
  @param rename: Renamed parameters, key as old name, value as new name
190
  @return: Opcode object
191

192
  """
193
  CheckType(body, dict, "Body contents")
194

    
195
  # Make copy to be modified
196
  params = body.copy()
197

    
198
  if rename:
199
    for old, new in rename.items():
200
      if new in params and old in params:
201
        raise http.HttpBadRequest("Parameter '%s' was renamed to '%s', but"
202
                                  " both are specified" %
203
                                  (old, new))
204
      if old in params:
205
        assert new not in params
206
        params[new] = params.pop(old)
207

    
208
  if static:
209
    overwritten = set(params.keys()) & set(static.keys())
210
    if overwritten:
211
      raise http.HttpBadRequest("Can't overwrite static parameters %r" %
212
                                overwritten)
213

    
214
    params.update(static)
215

    
216
  # Convert keys to strings (simplejson decodes them as unicode)
217
  params = dict((str(key), value) for (key, value) in params.items())
218

    
219
  try:
220
    op = opcls(**params) # pylint: disable-msg=W0142
221
    op.Validate(False)
222
  except (errors.OpPrereqError, TypeError), err:
223
    raise http.HttpBadRequest("Invalid body parameters: %s" % err)
224

    
225
  return op
226

    
227

    
228
def SubmitJob(op, cl=None):
229
  """Generic wrapper for submit job, for better http compatibility.
230

231
  @type op: list
232
  @param op: the list of opcodes for the job
233
  @type cl: None or luxi.Client
234
  @param cl: optional luxi client to use
235
  @rtype: string
236
  @return: the job ID
237

238
  """
239
  try:
240
    if cl is None:
241
      cl = GetClient()
242
    return cl.SubmitJob(op)
243
  except errors.JobQueueFull:
244
    raise http.HttpServiceUnavailable("Job queue is full, needs archiving")
245
  except errors.JobQueueDrainError:
246
    raise http.HttpServiceUnavailable("Job queue is drained, cannot submit")
247
  except luxi.NoMasterError, err:
248
    raise http.HttpBadGateway("Master seems to be unreachable: %s" % str(err))
249
  except luxi.PermissionError:
250
    raise http.HttpInternalServerError("Internal error: no permission to"
251
                                       " connect to the master daemon")
252
  except luxi.TimeoutError, err:
253
    raise http.HttpGatewayTimeout("Timeout while talking to the master"
254
                                  " daemon. Error: %s" % str(err))
255

    
256

    
257
def HandleItemQueryErrors(fn, *args, **kwargs):
258
  """Converts errors when querying a single item.
259

260
  """
261
  try:
262
    return fn(*args, **kwargs)
263
  except errors.OpPrereqError, err:
264
    if len(err.args) == 2 and err.args[1] == errors.ECODE_NOENT:
265
      raise http.HttpNotFound()
266

    
267
    raise
268

    
269

    
270
def GetClient():
271
  """Geric wrapper for luxi.Client(), for better http compatiblity.
272

273
  """
274
  try:
275
    return luxi.Client()
276
  except luxi.NoMasterError, err:
277
    raise http.HttpBadGateway("Master seems to unreachable: %s" % str(err))
278
  except luxi.PermissionError:
279
    raise http.HttpInternalServerError("Internal error: no permission to"
280
                                       " connect to the master daemon")
281

    
282

    
283
def FeedbackFn(msg):
284
  """Feedback logging function for jobs.
285

286
  We don't have a stdout for printing log messages, so log them to the
287
  http log at least.
288

289
  @param msg: the message
290

291
  """
292
  (_, log_type, log_msg) = msg
293
  logging.info("%s: %s", log_type, log_msg)
294

    
295

    
296
def CheckType(value, exptype, descr):
297
  """Abort request if value type doesn't match expected type.
298

299
  @param value: Value
300
  @type exptype: type
301
  @param exptype: Expected type
302
  @type descr: string
303
  @param descr: Description of value
304
  @return: Value (allows inline usage)
305

306
  """
307
  if not isinstance(value, exptype):
308
    raise http.HttpBadRequest("%s: Type is '%s', but '%s' is expected" %
309
                              (descr, type(value).__name__, exptype.__name__))
310

    
311
  return value
312

    
313

    
314
def CheckParameter(data, name, default=_DEFAULT, exptype=_DEFAULT):
315
  """Check and return the value for a given parameter.
316

317
  If no default value was given and the parameter doesn't exist in the input
318
  data, an error is raise.
319

320
  @type data: dict
321
  @param data: Dictionary containing input data
322
  @type name: string
323
  @param name: Parameter name
324
  @param default: Default value (can be None)
325
  @param exptype: Expected type (can be None)
326

327
  """
328
  try:
329
    value = data[name]
330
  except KeyError:
331
    if default is not _DEFAULT:
332
      return default
333

    
334
    raise http.HttpBadRequest("Required parameter '%s' is missing" %
335
                              name)
336

    
337
  if exptype is _DEFAULT:
338
    return value
339

    
340
  return CheckType(value, exptype, "'%s' parameter" % name)
341

    
342

    
343
class R_Generic(object):
344
  """Generic class for resources.
345

346
  """
347
  # Default permission requirements
348
  GET_ACCESS = []
349
  PUT_ACCESS = [rapi.RAPI_ACCESS_WRITE]
350
  POST_ACCESS = [rapi.RAPI_ACCESS_WRITE]
351
  DELETE_ACCESS = [rapi.RAPI_ACCESS_WRITE]
352

    
353
  def __init__(self, items, queryargs, req):
354
    """Generic resource constructor.
355

356
    @param items: a list with variables encoded in the URL
357
    @param queryargs: a dictionary with additional options from URL
358

359
    """
360
    self.items = items
361
    self.queryargs = queryargs
362
    self._req = req
363

    
364
  def _GetRequestBody(self):
365
    """Returns the body data.
366

367
    """
368
    return self._req.private.body_data
369

    
370
  request_body = property(fget=_GetRequestBody)
371

    
372
  def _checkIntVariable(self, name, default=0):
373
    """Return the parsed value of an int argument.
374

375
    """
376
    val = self.queryargs.get(name, default)
377
    if isinstance(val, list):
378
      if val:
379
        val = val[0]
380
      else:
381
        val = default
382
    try:
383
      val = int(val)
384
    except (ValueError, TypeError):
385
      raise http.HttpBadRequest("Invalid value for the"
386
                                " '%s' parameter" % (name,))
387
    return val
388

    
389
  def _checkStringVariable(self, name, default=None):
390
    """Return the parsed value of an int argument.
391

392
    """
393
    val = self.queryargs.get(name, default)
394
    if isinstance(val, list):
395
      if val:
396
        val = val[0]
397
      else:
398
        val = default
399
    return val
400

    
401
  def getBodyParameter(self, name, *args):
402
    """Check and return the value for a given parameter.
403

404
    If a second parameter is not given, an error will be returned,
405
    otherwise this parameter specifies the default value.
406

407
    @param name: the required parameter
408

409
    """
410
    if args:
411
      return CheckParameter(self.request_body, name, default=args[0])
412

    
413
    return CheckParameter(self.request_body, name)
414

    
415
  def useLocking(self):
416
    """Check if the request specifies locking.
417

418
    """
419
    return bool(self._checkIntVariable("lock"))
420

    
421
  def useBulk(self):
422
    """Check if the request specifies bulk querying.
423

424
    """
425
    return bool(self._checkIntVariable("bulk"))
426

    
427
  def useForce(self):
428
    """Check if the request specifies a forced operation.
429

430
    """
431
    return bool(self._checkIntVariable("force"))
432

    
433
  def dryRun(self):
434
    """Check if the request specifies dry-run mode.
435

436
    """
437
    return bool(self._checkIntVariable("dry-run"))