Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / baserlib.py @ d1602edc

History | View | Annotate | Download (10.8 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):
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
  @return: Opcode object
185

186
  """
187
  CheckType(body, dict, "Body contents")
188

    
189
  if static:
190
    overwritten = set(body.keys()) & set(static.keys())
191
    if overwritten:
192
      raise http.HttpBadRequest("Can't overwrite static parameters %r" %
193
                                overwritten)
194

    
195
  # Combine parameters
196
  params = body.copy()
197

    
198
  if static:
199
    params.update(static)
200

    
201
  # Convert keys to strings (simplejson decodes them as unicode)
202
  params = dict((str(key), value) for (key, value) in params.items())
203

    
204
  try:
205
    op = opcls(**params) # pylint: disable-msg=W0142
206
    op.Validate(False)
207
  except (errors.OpPrereqError, TypeError), err:
208
    raise http.HttpBadRequest("Invalid body parameters: %s" % err)
209

    
210
  return op
211

    
212

    
213
def SubmitJob(op, cl=None):
214
  """Generic wrapper for submit job, for better http compatibility.
215

216
  @type op: list
217
  @param op: the list of opcodes for the job
218
  @type cl: None or luxi.Client
219
  @param cl: optional luxi client to use
220
  @rtype: string
221
  @return: the job ID
222

223
  """
224
  try:
225
    if cl is None:
226
      cl = GetClient()
227
    return cl.SubmitJob(op)
228
  except errors.JobQueueFull:
229
    raise http.HttpServiceUnavailable("Job queue is full, needs archiving")
230
  except errors.JobQueueDrainError:
231
    raise http.HttpServiceUnavailable("Job queue is drained, cannot submit")
232
  except luxi.NoMasterError, err:
233
    raise http.HttpBadGateway("Master seems to be unreachable: %s" % str(err))
234
  except luxi.PermissionError:
235
    raise http.HttpInternalServerError("Internal error: no permission to"
236
                                       " connect to the master daemon")
237
  except luxi.TimeoutError, err:
238
    raise http.HttpGatewayTimeout("Timeout while talking to the master"
239
                                  " daemon. Error: %s" % str(err))
240

    
241

    
242
def HandleItemQueryErrors(fn, *args, **kwargs):
243
  """Converts errors when querying a single item.
244

245
  """
246
  try:
247
    return fn(*args, **kwargs)
248
  except errors.OpPrereqError, err:
249
    if len(err.args) == 2 and err.args[1] == errors.ECODE_NOENT:
250
      raise http.HttpNotFound()
251

    
252
    raise
253

    
254

    
255
def GetClient():
256
  """Geric wrapper for luxi.Client(), for better http compatiblity.
257

258
  """
259
  try:
260
    return luxi.Client()
261
  except luxi.NoMasterError, err:
262
    raise http.HttpBadGateway("Master seems to unreachable: %s" % str(err))
263
  except luxi.PermissionError:
264
    raise http.HttpInternalServerError("Internal error: no permission to"
265
                                       " connect to the master daemon")
266

    
267

    
268
def FeedbackFn(msg):
269
  """Feedback logging function for jobs.
270

271
  We don't have a stdout for printing log messages, so log them to the
272
  http log at least.
273

274
  @param msg: the message
275

276
  """
277
  (_, log_type, log_msg) = msg
278
  logging.info("%s: %s", log_type, log_msg)
279

    
280

    
281
def CheckType(value, exptype, descr):
282
  """Abort request if value type doesn't match expected type.
283

284
  @param value: Value
285
  @type exptype: type
286
  @param exptype: Expected type
287
  @type descr: string
288
  @param descr: Description of value
289
  @return: Value (allows inline usage)
290

291
  """
292
  if not isinstance(value, exptype):
293
    raise http.HttpBadRequest("%s: Type is '%s', but '%s' is expected" %
294
                              (descr, type(value).__name__, exptype.__name__))
295

    
296
  return value
297

    
298

    
299
def CheckParameter(data, name, default=_DEFAULT, exptype=_DEFAULT):
300
  """Check and return the value for a given parameter.
301

302
  If no default value was given and the parameter doesn't exist in the input
303
  data, an error is raise.
304

305
  @type data: dict
306
  @param data: Dictionary containing input data
307
  @type name: string
308
  @param name: Parameter name
309
  @param default: Default value (can be None)
310
  @param exptype: Expected type (can be None)
311

312
  """
313
  try:
314
    value = data[name]
315
  except KeyError:
316
    if default is not _DEFAULT:
317
      return default
318

    
319
    raise http.HttpBadRequest("Required parameter '%s' is missing" %
320
                              name)
321

    
322
  if exptype is _DEFAULT:
323
    return value
324

    
325
  return CheckType(value, exptype, "'%s' parameter" % name)
326

    
327

    
328
class R_Generic(object):
329
  """Generic class for resources.
330

331
  """
332
  # Default permission requirements
333
  GET_ACCESS = []
334
  PUT_ACCESS = [rapi.RAPI_ACCESS_WRITE]
335
  POST_ACCESS = [rapi.RAPI_ACCESS_WRITE]
336
  DELETE_ACCESS = [rapi.RAPI_ACCESS_WRITE]
337

    
338
  def __init__(self, items, queryargs, req):
339
    """Generic resource constructor.
340

341
    @param items: a list with variables encoded in the URL
342
    @param queryargs: a dictionary with additional options from URL
343

344
    """
345
    self.items = items
346
    self.queryargs = queryargs
347
    self._req = req
348

    
349
  def _GetRequestBody(self):
350
    """Returns the body data.
351

352
    """
353
    return self._req.private.body_data
354

    
355
  request_body = property(fget=_GetRequestBody)
356

    
357
  def _checkIntVariable(self, name, default=0):
358
    """Return the parsed value of an int argument.
359

360
    """
361
    val = self.queryargs.get(name, default)
362
    if isinstance(val, list):
363
      if val:
364
        val = val[0]
365
      else:
366
        val = default
367
    try:
368
      val = int(val)
369
    except (ValueError, TypeError):
370
      raise http.HttpBadRequest("Invalid value for the"
371
                                " '%s' parameter" % (name,))
372
    return val
373

    
374
  def _checkStringVariable(self, name, default=None):
375
    """Return the parsed value of an int argument.
376

377
    """
378
    val = self.queryargs.get(name, default)
379
    if isinstance(val, list):
380
      if val:
381
        val = val[0]
382
      else:
383
        val = default
384
    return val
385

    
386
  def getBodyParameter(self, name, *args):
387
    """Check and return the value for a given parameter.
388

389
    If a second parameter is not given, an error will be returned,
390
    otherwise this parameter specifies the default value.
391

392
    @param name: the required parameter
393

394
    """
395
    if args:
396
      return CheckParameter(self.request_body, name, default=args[0])
397

    
398
    return CheckParameter(self.request_body, name)
399

    
400
  def useLocking(self):
401
    """Check if the request specifies locking.
402

403
    """
404
    return bool(self._checkIntVariable("lock"))
405

    
406
  def useBulk(self):
407
    """Check if the request specifies bulk querying.
408

409
    """
410
    return bool(self._checkIntVariable("bulk"))
411

    
412
  def useForce(self):
413
    """Check if the request specifies a forced operation.
414

415
    """
416
    return bool(self._checkIntVariable("force"))
417

    
418
  def dryRun(self):
419
    """Check if the request specifies dry-run mode.
420

421
    """
422
    return bool(self._checkIntVariable("dry-run"))