Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / baserlib.py @ b166ef84

History | View | Annotate | Download (11.2 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 == constants.TAG_INSTANCE or kind == constants.TAG_NODE:
96
    if not name:
97
      raise http.HttpBadRequest("Missing name on tag request")
98
    cl = GetClient()
99
    if kind == constants.TAG_INSTANCE:
100
      fn = cl.QueryInstances
101
    else:
102
      fn = cl.QueryNodes
103
    result = fn(names=[name], fields=["tags"], use_locking=False)
104
    if not result or not result[0]:
105
      raise http.HttpBadGateway("Invalid response from tag query")
106
    tags = result[0][0]
107
  elif kind == constants.TAG_CLUSTER:
108
    ssc = ssconf.SimpleStore()
109
    tags = ssc.GetClusterTags()
110

    
111
  return list(tags)
112

    
113

    
114
def _Tags_PUT(kind, tags, name, dry_run):
115
  """Helper function to set tags.
116

117
  """
118
  return SubmitJob([opcodes.OpTagsSet(kind=kind, name=name,
119
                                      tags=tags, dry_run=dry_run)])
120

    
121

    
122
def _Tags_DELETE(kind, tags, name, dry_run):
123
  """Helper function to delete tags.
124

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

    
129

    
130
def MapBulkFields(itemslist, fields):
131
  """Map value to field name in to one dictionary.
132

133
  @param itemslist: a list of items values
134
  @param fields: a list of items names
135

136
  @return: a list of mapped dictionaries
137

138
  """
139
  items_details = []
140
  for item in itemslist:
141
    mapped = MapFields(fields, item)
142
    items_details.append(mapped)
143
  return items_details
144

    
145

    
146
def MakeParamsDict(opts, params):
147
  """Makes params dictionary out of a option set.
148

149
  This function returns a dictionary needed for hv or be parameters. But only
150
  those fields which provided in the option set. Takes parameters frozensets
151
  from constants.
152

153
  @type opts: dict
154
  @param opts: selected options
155
  @type params: frozenset
156
  @param params: subset of options
157
  @rtype: dict
158
  @return: dictionary of options, filtered by given subset.
159

160
  """
161
  result = {}
162

    
163
  for p in params:
164
    try:
165
      value = opts[p]
166
    except KeyError:
167
      continue
168
    result[p] = value
169

    
170
  return result
171

    
172

    
173
def FillOpcode(opcls, body, static, rename=None):
174
  """Fills an opcode with body parameters.
175

176
  Parameter types are checked.
177

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

188
  """
189
  CheckType(body, dict, "Body contents")
190

    
191
  # Make copy to be modified
192
  params = body.copy()
193

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

    
204
  if static:
205
    overwritten = set(params.keys()) & set(static.keys())
206
    if overwritten:
207
      raise http.HttpBadRequest("Can't overwrite static parameters %r" %
208
                                overwritten)
209

    
210
    params.update(static)
211

    
212
  # Convert keys to strings (simplejson decodes them as unicode)
213
  params = dict((str(key), value) for (key, value) in params.items())
214

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

    
221
  return op
222

    
223

    
224
def SubmitJob(op, cl=None):
225
  """Generic wrapper for submit job, for better http compatibility.
226

227
  @type op: list
228
  @param op: the list of opcodes for the job
229
  @type cl: None or luxi.Client
230
  @param cl: optional luxi client to use
231
  @rtype: string
232
  @return: the job ID
233

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

    
252

    
253
def HandleItemQueryErrors(fn, *args, **kwargs):
254
  """Converts errors when querying a single item.
255

256
  """
257
  try:
258
    return fn(*args, **kwargs)
259
  except errors.OpPrereqError, err:
260
    if len(err.args) == 2 and err.args[1] == errors.ECODE_NOENT:
261
      raise http.HttpNotFound()
262

    
263
    raise
264

    
265

    
266
def GetClient():
267
  """Geric wrapper for luxi.Client(), for better http compatiblity.
268

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

    
278

    
279
def FeedbackFn(msg):
280
  """Feedback logging function for jobs.
281

282
  We don't have a stdout for printing log messages, so log them to the
283
  http log at least.
284

285
  @param msg: the message
286

287
  """
288
  (_, log_type, log_msg) = msg
289
  logging.info("%s: %s", log_type, log_msg)
290

    
291

    
292
def CheckType(value, exptype, descr):
293
  """Abort request if value type doesn't match expected type.
294

295
  @param value: Value
296
  @type exptype: type
297
  @param exptype: Expected type
298
  @type descr: string
299
  @param descr: Description of value
300
  @return: Value (allows inline usage)
301

302
  """
303
  if not isinstance(value, exptype):
304
    raise http.HttpBadRequest("%s: Type is '%s', but '%s' is expected" %
305
                              (descr, type(value).__name__, exptype.__name__))
306

    
307
  return value
308

    
309

    
310
def CheckParameter(data, name, default=_DEFAULT, exptype=_DEFAULT):
311
  """Check and return the value for a given parameter.
312

313
  If no default value was given and the parameter doesn't exist in the input
314
  data, an error is raise.
315

316
  @type data: dict
317
  @param data: Dictionary containing input data
318
  @type name: string
319
  @param name: Parameter name
320
  @param default: Default value (can be None)
321
  @param exptype: Expected type (can be None)
322

323
  """
324
  try:
325
    value = data[name]
326
  except KeyError:
327
    if default is not _DEFAULT:
328
      return default
329

    
330
    raise http.HttpBadRequest("Required parameter '%s' is missing" %
331
                              name)
332

    
333
  if exptype is _DEFAULT:
334
    return value
335

    
336
  return CheckType(value, exptype, "'%s' parameter" % name)
337

    
338

    
339
class R_Generic(object):
340
  """Generic class for resources.
341

342
  """
343
  # Default permission requirements
344
  GET_ACCESS = []
345
  PUT_ACCESS = [rapi.RAPI_ACCESS_WRITE]
346
  POST_ACCESS = [rapi.RAPI_ACCESS_WRITE]
347
  DELETE_ACCESS = [rapi.RAPI_ACCESS_WRITE]
348

    
349
  def __init__(self, items, queryargs, req):
350
    """Generic resource constructor.
351

352
    @param items: a list with variables encoded in the URL
353
    @param queryargs: a dictionary with additional options from URL
354

355
    """
356
    self.items = items
357
    self.queryargs = queryargs
358
    self._req = req
359

    
360
  def _GetRequestBody(self):
361
    """Returns the body data.
362

363
    """
364
    return self._req.private.body_data
365

    
366
  request_body = property(fget=_GetRequestBody)
367

    
368
  def _checkIntVariable(self, name, default=0):
369
    """Return the parsed value of an int argument.
370

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

    
385
  def _checkStringVariable(self, name, default=None):
386
    """Return the parsed value of an int argument.
387

388
    """
389
    val = self.queryargs.get(name, default)
390
    if isinstance(val, list):
391
      if val:
392
        val = val[0]
393
      else:
394
        val = default
395
    return val
396

    
397
  def getBodyParameter(self, name, *args):
398
    """Check and return the value for a given parameter.
399

400
    If a second parameter is not given, an error will be returned,
401
    otherwise this parameter specifies the default value.
402

403
    @param name: the required parameter
404

405
    """
406
    if args:
407
      return CheckParameter(self.request_body, name, default=args[0])
408

    
409
    return CheckParameter(self.request_body, name)
410

    
411
  def useLocking(self):
412
    """Check if the request specifies locking.
413

414
    """
415
    return bool(self._checkIntVariable("lock"))
416

    
417
  def useBulk(self):
418
    """Check if the request specifies bulk querying.
419

420
    """
421
    return bool(self._checkIntVariable("bulk"))
422

    
423
  def useForce(self):
424
    """Check if the request specifies a forced operation.
425

426
    """
427
    return bool(self._checkIntVariable("force"))
428

    
429
  def dryRun(self):
430
    """Check if the request specifies dry-run mode.
431

432
    """
433
    return bool(self._checkIntVariable("dry-run"))