Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / baserlib.py @ eb5ac108

History | View | Annotate | Download (11.4 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
  if body is None:
197
    params = {}
198
  else:
199
    CheckType(body, dict, "Body contents")
200

    
201
    # Make copy to be modified
202
    params = body.copy()
203

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

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

    
220
    params.update(static)
221

    
222
  # Convert keys to strings (simplejson decodes them as unicode)
223
  params = dict((str(key), value) for (key, value) in params.items())
224

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

    
231
  return op
232

    
233

    
234
def SubmitJob(op, cl=None):
235
  """Generic wrapper for submit job, for better http compatibility.
236

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

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

    
262

    
263
def HandleItemQueryErrors(fn, *args, **kwargs):
264
  """Converts errors when querying a single item.
265

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

    
273
    raise
274

    
275

    
276
def GetClient():
277
  """Geric wrapper for luxi.Client(), for better http compatiblity.
278

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

    
288

    
289
def FeedbackFn(msg):
290
  """Feedback logging function for jobs.
291

292
  We don't have a stdout for printing log messages, so log them to the
293
  http log at least.
294

295
  @param msg: the message
296

297
  """
298
  (_, log_type, log_msg) = msg
299
  logging.info("%s: %s", log_type, log_msg)
300

    
301

    
302
def CheckType(value, exptype, descr):
303
  """Abort request if value type doesn't match expected type.
304

305
  @param value: Value
306
  @type exptype: type
307
  @param exptype: Expected type
308
  @type descr: string
309
  @param descr: Description of value
310
  @return: Value (allows inline usage)
311

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

    
317
  return value
318

    
319

    
320
def CheckParameter(data, name, default=_DEFAULT, exptype=_DEFAULT):
321
  """Check and return the value for a given parameter.
322

323
  If no default value was given and the parameter doesn't exist in the input
324
  data, an error is raise.
325

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

333
  """
334
  try:
335
    value = data[name]
336
  except KeyError:
337
    if default is not _DEFAULT:
338
      return default
339

    
340
    raise http.HttpBadRequest("Required parameter '%s' is missing" %
341
                              name)
342

    
343
  if exptype is _DEFAULT:
344
    return value
345

    
346
  return CheckType(value, exptype, "'%s' parameter" % name)
347

    
348

    
349
class R_Generic(object):
350
  """Generic class for resources.
351

352
  """
353
  # Default permission requirements
354
  GET_ACCESS = []
355
  PUT_ACCESS = [rapi.RAPI_ACCESS_WRITE]
356
  POST_ACCESS = [rapi.RAPI_ACCESS_WRITE]
357
  DELETE_ACCESS = [rapi.RAPI_ACCESS_WRITE]
358

    
359
  def __init__(self, items, queryargs, req):
360
    """Generic resource constructor.
361

362
    @param items: a list with variables encoded in the URL
363
    @param queryargs: a dictionary with additional options from URL
364

365
    """
366
    self.items = items
367
    self.queryargs = queryargs
368
    self._req = req
369

    
370
  def _GetRequestBody(self):
371
    """Returns the body data.
372

373
    """
374
    return self._req.private.body_data
375

    
376
  request_body = property(fget=_GetRequestBody)
377

    
378
  def _checkIntVariable(self, name, default=0):
379
    """Return the parsed value of an int argument.
380

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

    
395
  def _checkStringVariable(self, name, default=None):
396
    """Return the parsed value of an int argument.
397

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

    
407
  def getBodyParameter(self, name, *args):
408
    """Check and return the value for a given parameter.
409

410
    If a second parameter is not given, an error will be returned,
411
    otherwise this parameter specifies the default value.
412

413
    @param name: the required parameter
414

415
    """
416
    if args:
417
      return CheckParameter(self.request_body, name, default=args[0])
418

    
419
    return CheckParameter(self.request_body, name)
420

    
421
  def useLocking(self):
422
    """Check if the request specifies locking.
423

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

    
427
  def useBulk(self):
428
    """Check if the request specifies bulk querying.
429

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

    
433
  def useForce(self):
434
    """Check if the request specifies a forced operation.
435

436
    """
437
    return bool(self._checkIntVariable("force"))
438

    
439
  def dryRun(self):
440
    """Check if the request specifies dry-run mode.
441

442
    """
443
    return bool(self._checkIntVariable("dry-run"))