Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / baserlib.py @ b459a848

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=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 {
57
      field_id: m_id,
58
      field_uri: uri_format % m_id,
59
      }
60

    
61
  # Make sure the result is sorted, makes it nicer to look at and simplifies
62
  # unittests.
63
  ids.sort()
64

    
65
  return map(_MapId, ids)
66

    
67

    
68
def ExtractField(sequence, index):
69
  """Creates a list containing one column out of a list of lists.
70

71
  @param sequence: sequence of lists
72
  @param index: index of field
73

74
  """
75
  return map(lambda item: item[index], sequence)
76

    
77

    
78
def MapFields(names, data):
79
  """Maps two lists into one dictionary.
80

81
  Example::
82
      >>> MapFields(["a", "b"], ["foo", 123])
83
      {'a': 'foo', 'b': 123}
84

85
  @param names: field names (list of strings)
86
  @param data: field data (list)
87

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

    
93

    
94
def _Tags_GET(kind, name):
95
  """Helper function to retrieve tags.
96

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

    
118
  return list(tags)
119

    
120

    
121
def _Tags_PUT(kind, tags, name, dry_run):
122
  """Helper function to set tags.
123

124
  """
125
  return SubmitJob([opcodes.OpTagsSet(kind=kind, name=name,
126
                                      tags=tags, dry_run=dry_run)])
127

    
128

    
129
def _Tags_DELETE(kind, tags, name, dry_run):
130
  """Helper function to delete tags.
131

132
  """
133
  return SubmitJob([opcodes.OpTagsDel(kind=kind, name=name,
134
                                      tags=tags, dry_run=dry_run)])
135

    
136

    
137
def MapBulkFields(itemslist, fields):
138
  """Map value to field name in to one dictionary.
139

140
  @param itemslist: a list of items values
141
  @param fields: a list of items names
142

143
  @return: a list of mapped dictionaries
144

145
  """
146
  items_details = []
147
  for item in itemslist:
148
    mapped = MapFields(fields, item)
149
    items_details.append(mapped)
150
  return items_details
151

    
152

    
153
def MakeParamsDict(opts, params):
154
  """Makes params dictionary out of a option set.
155

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

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

167
  """
168
  result = {}
169

    
170
  for p in params:
171
    try:
172
      value = opts[p]
173
    except KeyError:
174
      continue
175
    result[p] = value
176

    
177
  return result
178

    
179

    
180
def FillOpcode(opcls, body, static, rename=None):
181
  """Fills an opcode with body parameters.
182

183
  Parameter types are checked.
184

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

195
  """
196
  CheckType(body, dict, "Body contents")
197

    
198
  # Make copy to be modified
199
  params = body.copy()
200

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

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

    
217
    params.update(static)
218

    
219
  # Convert keys to strings (simplejson decodes them as unicode)
220
  params = dict((str(key), value) for (key, value) in params.items())
221

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

    
228
  return op
229

    
230

    
231
def SubmitJob(op, cl=None):
232
  """Generic wrapper for submit job, for better http compatibility.
233

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

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

    
259

    
260
def HandleItemQueryErrors(fn, *args, **kwargs):
261
  """Converts errors when querying a single item.
262

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

    
270
    raise
271

    
272

    
273
def GetClient():
274
  """Geric wrapper for luxi.Client(), for better http compatiblity.
275

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

    
285

    
286
def FeedbackFn(msg):
287
  """Feedback logging function for jobs.
288

289
  We don't have a stdout for printing log messages, so log them to the
290
  http log at least.
291

292
  @param msg: the message
293

294
  """
295
  (_, log_type, log_msg) = msg
296
  logging.info("%s: %s", log_type, log_msg)
297

    
298

    
299
def CheckType(value, exptype, descr):
300
  """Abort request if value type doesn't match expected type.
301

302
  @param value: Value
303
  @type exptype: type
304
  @param exptype: Expected type
305
  @type descr: string
306
  @param descr: Description of value
307
  @return: Value (allows inline usage)
308

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

    
314
  return value
315

    
316

    
317
def CheckParameter(data, name, default=_DEFAULT, exptype=_DEFAULT):
318
  """Check and return the value for a given parameter.
319

320
  If no default value was given and the parameter doesn't exist in the input
321
  data, an error is raise.
322

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

330
  """
331
  try:
332
    value = data[name]
333
  except KeyError:
334
    if default is not _DEFAULT:
335
      return default
336

    
337
    raise http.HttpBadRequest("Required parameter '%s' is missing" %
338
                              name)
339

    
340
  if exptype is _DEFAULT:
341
    return value
342

    
343
  return CheckType(value, exptype, "'%s' parameter" % name)
344

    
345

    
346
class R_Generic(object):
347
  """Generic class for resources.
348

349
  """
350
  # Default permission requirements
351
  GET_ACCESS = []
352
  PUT_ACCESS = [rapi.RAPI_ACCESS_WRITE]
353
  POST_ACCESS = [rapi.RAPI_ACCESS_WRITE]
354
  DELETE_ACCESS = [rapi.RAPI_ACCESS_WRITE]
355

    
356
  def __init__(self, items, queryargs, req):
357
    """Generic resource constructor.
358

359
    @param items: a list with variables encoded in the URL
360
    @param queryargs: a dictionary with additional options from URL
361

362
    """
363
    self.items = items
364
    self.queryargs = queryargs
365
    self._req = req
366

    
367
  def _GetRequestBody(self):
368
    """Returns the body data.
369

370
    """
371
    return self._req.private.body_data
372

    
373
  request_body = property(fget=_GetRequestBody)
374

    
375
  def _checkIntVariable(self, name, default=0):
376
    """Return the parsed value of an int argument.
377

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

    
392
  def _checkStringVariable(self, name, default=None):
393
    """Return the parsed value of an int argument.
394

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

    
404
  def getBodyParameter(self, name, *args):
405
    """Check and return the value for a given parameter.
406

407
    If a second parameter is not given, an error will be returned,
408
    otherwise this parameter specifies the default value.
409

410
    @param name: the required parameter
411

412
    """
413
    if args:
414
      return CheckParameter(self.request_body, name, default=args[0])
415

    
416
    return CheckParameter(self.request_body, name)
417

    
418
  def useLocking(self):
419
    """Check if the request specifies locking.
420

421
    """
422
    return bool(self._checkIntVariable("lock"))
423

    
424
  def useBulk(self):
425
    """Check if the request specifies bulk querying.
426

427
    """
428
    return bool(self._checkIntVariable("bulk"))
429

    
430
  def useForce(self):
431
    """Check if the request specifies a forced operation.
432

433
    """
434
    return bool(self._checkIntVariable("force"))
435

    
436
  def dryRun(self):
437
    """Check if the request specifies dry-run mode.
438

439
    """
440
    return bool(self._checkIntVariable("dry-run"))