Statistics
| Branch: | Tag: | Revision:

root / lib / storage / extstorage.py @ ab0c6a39

History | View | Annotate | Download (15.3 kB)

1
#
2
#
3

    
4
# Copyright (C) 2014 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
"""ExtStorage Interface related functionality
23

24
"""
25

    
26
import re
27
import stat
28
import os
29
import logging
30

    
31
from ganeti import utils
32
from ganeti import errors
33
from ganeti import constants
34
from ganeti import objects
35
from ganeti import pathutils
36
from ganeti.storage import base
37

    
38

    
39
class ExtStorageDevice(base.BlockDev):
40
  """A block device provided by an ExtStorage Provider.
41

42
  This class implements the External Storage Interface, which means
43
  handling of the externally provided block devices.
44

45
  """
46
  def __init__(self, unique_id, children, size, params, dyn_params, *args):
47
    """Attaches to an extstorage block device.
48

49
    """
50
    super(ExtStorageDevice, self).__init__(unique_id, children, size, params,
51
                                           dyn_params, *args)
52
    (self.name, self.uuid) = args
53

    
54
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
55
      raise ValueError("Invalid configuration data %s" % str(unique_id))
56

    
57
    self.driver, self.vol_name = unique_id
58
    self.ext_params = params
59

    
60
    self.major = self.minor = None
61
    self.uris = []
62
    self.Attach()
63

    
64
  @classmethod
65
  def Create(cls, unique_id, children, size, spindles, params, excl_stor,
66
             dyn_params, *args):
67
    """Create a new extstorage device.
68

69
    Provision a new volume using an extstorage provider, which will
70
    then be mapped to a block device.
71

72
    """
73
    (name, uuid) = args
74

    
75
    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
76
      raise errors.ProgrammerError("Invalid configuration data %s" %
77
                                   str(unique_id))
78
    if excl_stor:
79
      raise errors.ProgrammerError("extstorage device requested with"
80
                                   " exclusive_storage")
81

    
82
    # Call the External Storage's create script,
83
    # to provision a new Volume inside the External Storage
84
    _ExtStorageAction(constants.ES_ACTION_CREATE, unique_id,
85
                      params, size=str(size), name=name, uuid=uuid)
86

    
87
    return ExtStorageDevice(unique_id, children, size, params, dyn_params,
88
                            *args)
89

    
90
  def Remove(self):
91
    """Remove the extstorage device.
92

93
    """
94
    if not self.minor and not self.Attach():
95
      # The extstorage device doesn't exist.
96
      return
97

    
98
    # First shutdown the device (remove mappings).
99
    self.Shutdown()
100

    
101
    # Call the External Storage's remove script,
102
    # to remove the Volume from the External Storage
103
    _ExtStorageAction(constants.ES_ACTION_REMOVE, self.unique_id,
104
                      self.ext_params, name=self.name, uuid=self.uuid)
105

    
106
  def Rename(self, new_id):
107
    """Rename this device.
108

109
    """
110
    pass
111

    
112
  def Attach(self):
113
    """Attach to an existing extstorage device.
114

115
    This method maps the extstorage volume that matches our name with
116
    a corresponding block device and then attaches to this device.
117

118
    """
119
    self.attached = False
120

    
121
    # Call the External Storage's attach script,
122
    # to attach an existing Volume to a block device under /dev
123
    result = _ExtStorageAction(constants.ES_ACTION_ATTACH,
124
                               self.unique_id, self.ext_params,
125
                               name=self.name, uuid=self.uuid)
126

    
127
    # Attach script returns the block device path and optionally
128
    # the URIs to be used for userspace access (one URI for
129
    # each hypervisor supported).
130
    # If the provider doesn't support userspace access, then
131
    # the 'uris' variable will be an empty list.
132
    result = result.split("\n")
133
    self.dev_path = result[0]
134
    self.uris = result[1:]
135

    
136
    # Verify that dev_path exists and is a block device
137
    try:
138
      st = os.stat(self.dev_path)
139
    except OSError, err:
140
      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
141
      return False
142

    
143
    if not stat.S_ISBLK(st.st_mode):
144
      logging.error("%s is not a block device", self.dev_path)
145
      return False
146

    
147
    self.major = os.major(st.st_rdev)
148
    self.minor = os.minor(st.st_rdev)
149
    self.attached = True
150

    
151
    return True
152

    
153
  def Assemble(self):
154
    """Assemble the device.
155

156
    """
157
    pass
158

    
159
  def Shutdown(self):
160
    """Shutdown the device.
161

162
    """
163
    if not self.minor and not self.Attach():
164
      # The extstorage device doesn't exist.
165
      return
166

    
167
    # Call the External Storage's detach script,
168
    # to detach an existing Volume from it's block device under /dev
169
    _ExtStorageAction(constants.ES_ACTION_DETACH, self.unique_id,
170
                      self.ext_params, name=self.name, uuid=self.uuid)
171

    
172
    self.minor = None
173
    self.dev_path = None
174

    
175
  def Open(self, force=False):
176
    """Make the device ready for I/O.
177

178
    """
179
    pass
180

    
181
  def Close(self):
182
    """Notifies that the device will no longer be used for I/O.
183

184
    """
185
    pass
186

    
187
  def Grow(self, amount, dryrun, backingstore, excl_stor):
188
    """Grow the Volume.
189

190
    @type amount: integer
191
    @param amount: the amount (in mebibytes) to grow with
192
    @type dryrun: boolean
193
    @param dryrun: whether to execute the operation in simulation mode
194
        only, without actually increasing the size
195

196
    """
197
    if not backingstore:
198
      return
199
    if not self.Attach():
200
      base.ThrowError("Can't attach to extstorage device during Grow()")
201

    
202
    if dryrun:
203
      # we do not support dry runs of resize operations for now.
204
      return
205

    
206
    new_size = self.size + amount
207

    
208
    # Call the External Storage's grow script,
209
    # to grow an existing Volume inside the External Storage
210
    _ExtStorageAction(constants.ES_ACTION_GROW, self.unique_id,
211
                      self.ext_params, size=str(self.size), grow=str(new_size),
212
                      name=self.name, uuid=self.uuid)
213

    
214
  def SetInfo(self, text):
215
    """Update metadata with info text.
216

217
    """
218
    # Replace invalid characters
219
    text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
220
    text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
221

    
222
    # Only up to 128 characters are allowed
223
    text = text[:128]
224

    
225
    # Call the External Storage's setinfo script,
226
    # to set metadata for an existing Volume inside the External Storage
227
    _ExtStorageAction(constants.ES_ACTION_SETINFO, self.unique_id,
228
                      self.ext_params, metadata=text,
229
                      name=self.name, uuid=self.uuid)
230

    
231
  def GetUserspaceAccessUri(self, hypervisor):
232
    """Generate KVM userspace URIs to be used as `-drive file` settings.
233

234
    @see: L{base.BlockDev.GetUserspaceAccessUri}
235

236
    """
237
    if not self.Attach():
238
      base.ThrowError("Can't attach to ExtStorage device")
239

    
240
    # If the provider supports userspace access, the attach script has
241
    # returned a list of URIs prefixed with the corresponding hypervisor.
242
    prefix = hypervisor.lower() + ":"
243
    for uri in self.uris:
244
      if uri[:len(prefix)].lower() == prefix:
245
        return uri[len(prefix):]
246

    
247
    base.ThrowError("Userspace access is not supported by the '%s'"
248
                    " ExtStorage provider for the '%s' hypervisor"
249
                    % (self.driver, hypervisor))
250

    
251

    
252
def _ExtStorageAction(action, unique_id, ext_params,
253
                      size=None, grow=None, metadata=None,
254
                      name=None, uuid=None):
255
  """Take an External Storage action.
256

257
  Take an External Storage action concerning or affecting
258
  a specific Volume inside the External Storage.
259

260
  @type action: string
261
  @param action: which action to perform. One of:
262
                 create / remove / grow / attach / detach
263
  @type unique_id: tuple (driver, vol_name)
264
  @param unique_id: a tuple containing the type of ExtStorage (driver)
265
                    and the Volume name
266
  @type ext_params: dict
267
  @param ext_params: ExtStorage parameters
268
  @type size: integer
269
  @param size: the size of the Volume in mebibytes
270
  @type grow: integer
271
  @param grow: the new size in mebibytes (after grow)
272
  @type metadata: string
273
  @param metadata: metadata info of the Volume, for use by the provider
274
  @type name: string
275
  @param name: name of the Volume (objects.Disk.name)
276
  @type uuid: string
277
  @param uuid: uuid of the Volume (objects.Disk.uuid)
278
  @rtype: None or a block device path (during attach)
279

280
  """
281
  driver, vol_name = unique_id
282

    
283
  # Create an External Storage instance of type `driver'
284
  status, inst_es = ExtStorageFromDisk(driver)
285
  if not status:
286
    base.ThrowError("%s" % inst_es)
287

    
288
  # Create the basic environment for the driver's scripts
289
  create_env = _ExtStorageEnvironment(unique_id, ext_params, size,
290
                                      grow, metadata, name, uuid)
291

    
292
  # Do not use log file for action `attach' as we need
293
  # to get the output from RunResult
294
  # TODO: find a way to have a log file for attach too
295
  logfile = None
296
  if action is not constants.ES_ACTION_ATTACH:
297
    logfile = _VolumeLogName(action, driver, vol_name)
298

    
299
  # Make sure the given action results in a valid script
300
  if action not in constants.ES_SCRIPTS:
301
    base.ThrowError("Action '%s' doesn't result in a valid ExtStorage script" %
302
                    action)
303

    
304
  # Find out which external script to run according the given action
305
  script_name = action + "_script"
306
  script = getattr(inst_es, script_name)
307

    
308
  # Run the external script
309
  result = utils.RunCmd([script], env=create_env,
310
                        cwd=inst_es.path, output=logfile,)
311
  if result.failed:
312
    logging.error("External storage's %s command '%s' returned"
313
                  " error: %s, logfile: %s, output: %s",
314
                  action, result.cmd, result.fail_reason,
315
                  logfile, result.output)
316

    
317
    # If logfile is 'None' (during attach), it breaks TailFile
318
    # TODO: have a log file for attach too
319
    if action is not constants.ES_ACTION_ATTACH:
320
      lines = [utils.SafeEncode(val)
321
               for val in utils.TailFile(logfile, lines=20)]
322
    else:
323
      lines = result.output[-20:]
324

    
325
    base.ThrowError("External storage's %s script failed (%s), last"
326
                    " lines of output:\n%s",
327
                    action, result.fail_reason, "\n".join(lines))
328

    
329
  if action == constants.ES_ACTION_ATTACH:
330
    return result.stdout
331

    
332

    
333
def ExtStorageFromDisk(name, base_dir=None):
334
  """Create an ExtStorage instance from disk.
335

336
  This function will return an ExtStorage instance
337
  if the given name is a valid ExtStorage name.
338

339
  @type base_dir: string
340
  @keyword base_dir: Base directory containing ExtStorage installations.
341
                     Defaults to a search in all the ES_SEARCH_PATH dirs.
342
  @rtype: tuple
343
  @return: True and the ExtStorage instance if we find a valid one, or
344
      False and the diagnose message on error
345

346
  """
347
  if base_dir is None:
348
    es_base_dir = pathutils.ES_SEARCH_PATH
349
  else:
350
    es_base_dir = [base_dir]
351

    
352
  es_dir = utils.FindFile(name, es_base_dir, os.path.isdir)
353

    
354
  if es_dir is None:
355
    return False, ("Directory for External Storage Provider %s not"
356
                   " found in search path" % name)
357

    
358
  # ES Files dictionary, we will populate it with the absolute path
359
  # names; if the value is True, then it is a required file, otherwise
360
  # an optional one
361
  es_files = dict.fromkeys(constants.ES_SCRIPTS, True)
362

    
363
  es_files[constants.ES_PARAMETERS_FILE] = True
364

    
365
  for (filename, _) in es_files.items():
366
    es_files[filename] = utils.PathJoin(es_dir, filename)
367

    
368
    try:
369
      st = os.stat(es_files[filename])
370
    except EnvironmentError, err:
371
      return False, ("File '%s' under path '%s' is missing (%s)" %
372
                     (filename, es_dir, utils.ErrnoOrStr(err)))
373

    
374
    if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
375
      return False, ("File '%s' under path '%s' is not a regular file" %
376
                     (filename, es_dir))
377

    
378
    if filename in constants.ES_SCRIPTS:
379
      if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
380
        return False, ("File '%s' under path '%s' is not executable" %
381
                       (filename, es_dir))
382

    
383
  parameters = []
384
  if constants.ES_PARAMETERS_FILE in es_files:
385
    parameters_file = es_files[constants.ES_PARAMETERS_FILE]
386
    try:
387
      parameters = utils.ReadFile(parameters_file).splitlines()
388
    except EnvironmentError, err:
389
      return False, ("Error while reading the EXT parameters file at %s: %s" %
390
                     (parameters_file, utils.ErrnoOrStr(err)))
391
    parameters = [v.split(None, 1) for v in parameters]
392

    
393
  es_obj = \
394
    objects.ExtStorage(name=name, path=es_dir,
395
                       create_script=es_files[constants.ES_SCRIPT_CREATE],
396
                       remove_script=es_files[constants.ES_SCRIPT_REMOVE],
397
                       grow_script=es_files[constants.ES_SCRIPT_GROW],
398
                       attach_script=es_files[constants.ES_SCRIPT_ATTACH],
399
                       detach_script=es_files[constants.ES_SCRIPT_DETACH],
400
                       setinfo_script=es_files[constants.ES_SCRIPT_SETINFO],
401
                       verify_script=es_files[constants.ES_SCRIPT_VERIFY],
402
                       supported_parameters=parameters)
403
  return True, es_obj
404

    
405

    
406
def _ExtStorageEnvironment(unique_id, ext_params,
407
                           size=None, grow=None, metadata=None,
408
                           name=None, uuid=None):
409
  """Calculate the environment for an External Storage script.
410

411
  @type unique_id: tuple (driver, vol_name)
412
  @param unique_id: ExtStorage pool and name of the Volume
413
  @type ext_params: dict
414
  @param ext_params: the EXT parameters
415
  @type size: string
416
  @param size: size of the Volume (in mebibytes)
417
  @type grow: string
418
  @param grow: new size of Volume after grow (in mebibytes)
419
  @type metadata: string
420
  @param metadata: metadata info of the Volume
421
  @type name: string
422
  @param name: name of the Volume (objects.Disk.name)
423
  @type uuid: string
424
  @param uuid: uuid of the Volume (objects.Disk.uuid)
425
  @rtype: dict
426
  @return: dict of environment variables
427

428
  """
429
  vol_name = unique_id[1]
430

    
431
  result = {}
432
  result["VOL_NAME"] = vol_name
433

    
434
  # EXT params
435
  for pname, pvalue in ext_params.items():
436
    result["EXTP_%s" % pname.upper()] = str(pvalue)
437

    
438
  if size is not None:
439
    result["VOL_SIZE"] = size
440

    
441
  if grow is not None:
442
    result["VOL_NEW_SIZE"] = grow
443

    
444
  if metadata is not None:
445
    result["VOL_METADATA"] = metadata
446

    
447
  if name is not None:
448
    result["VOL_CNAME"] = name
449

    
450
  if uuid is not None:
451
    result["VOL_UUID"] = uuid
452

    
453
  return result
454

    
455

    
456
def _VolumeLogName(kind, es_name, volume):
457
  """Compute the ExtStorage log filename for a given Volume and operation.
458

459
  @type kind: string
460
  @param kind: the operation type (e.g. create, remove etc.)
461
  @type es_name: string
462
  @param es_name: the ExtStorage name
463
  @type volume: string
464
  @param volume: the name of the Volume inside the External Storage
465

466
  """
467
  # Check if the extstorage log dir is a valid dir
468
  if not os.path.isdir(pathutils.LOG_ES_DIR):
469
    base.ThrowError("Cannot find log directory: %s", pathutils.LOG_ES_DIR)
470

    
471
  # TODO: Use tempfile.mkstemp to create unique filename
472
  basename = ("%s-%s-%s-%s.log" %
473
              (kind, es_name, volume, utils.TimestampForFilename()))
474
  return utils.PathJoin(pathutils.LOG_ES_DIR, basename)