Statistics
| Branch: | Tag: | Revision:

root / lib / storage / extstorage.py @ 7d81bb8b

History | View | Annotate | Download (15.9 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
  def Snapshot(self, snapshot_name):
252
    """Take a snapshot of the block device.
253

254
    """
255
    # Call the External Storage's setinfo script,
256
    # to set metadata for an existing Volume inside the External Storage
257
    _ExtStorageAction(constants.ES_ACTION_SNAPSHOT, self.unique_id,
258
                      self.ext_params, snapshot_name=snapshot_name)
259

    
260

    
261
def _ExtStorageAction(action, unique_id, ext_params,
262
                      size=None, grow=None, metadata=None,
263
                      name=None, uuid=None, snapshot_name=None):
264
  """Take an External Storage action.
265

266
  Take an External Storage action concerning or affecting
267
  a specific Volume inside the External Storage.
268

269
  @type action: string
270
  @param action: which action to perform. One of:
271
                 create / remove / grow / attach / detach
272
  @type unique_id: tuple (driver, vol_name)
273
  @param unique_id: a tuple containing the type of ExtStorage (driver)
274
                    and the Volume name
275
  @type ext_params: dict
276
  @param ext_params: ExtStorage parameters
277
  @type size: integer
278
  @param size: the size of the Volume in mebibytes
279
  @type grow: integer
280
  @param grow: the new size in mebibytes (after grow)
281
  @type metadata: string
282
  @param metadata: metadata info of the Volume, for use by the provider
283
  @type name: string
284
  @param name: name of the Volume (objects.Disk.name)
285
  @type uuid: string
286
  @param uuid: uuid of the Volume (objects.Disk.uuid)
287
  @rtype: None or a block device path (during attach)
288

289
  """
290
  driver, vol_name = unique_id
291

    
292
  # Create an External Storage instance of type `driver'
293
  status, inst_es = ExtStorageFromDisk(driver)
294
  if not status:
295
    base.ThrowError("%s" % inst_es)
296

    
297
  # Create the basic environment for the driver's scripts
298
  create_env = _ExtStorageEnvironment(unique_id, ext_params, size,
299
                                      grow, metadata, name, uuid,
300
                                      snapshot_name)
301

    
302
  # Do not use log file for action `attach' as we need
303
  # to get the output from RunResult
304
  # TODO: find a way to have a log file for attach too
305
  logfile = None
306
  if action is not constants.ES_ACTION_ATTACH:
307
    logfile = _VolumeLogName(action, driver, vol_name)
308

    
309
  # Make sure the given action results in a valid script
310
  if action not in constants.ES_SCRIPTS:
311
    base.ThrowError("Action '%s' doesn't result in a valid ExtStorage script" %
312
                    action)
313

    
314
  # Find out which external script to run according the given action
315
  script_name = action + "_script"
316
  script = getattr(inst_es, script_name)
317

    
318
  # Run the external script
319
  result = utils.RunCmd([script], env=create_env,
320
                        cwd=inst_es.path, output=logfile,)
321
  if result.failed:
322
    logging.error("External storage's %s command '%s' returned"
323
                  " error: %s, logfile: %s, output: %s",
324
                  action, result.cmd, result.fail_reason,
325
                  logfile, result.output)
326

    
327
    # If logfile is 'None' (during attach), it breaks TailFile
328
    # TODO: have a log file for attach too
329
    if action is not constants.ES_ACTION_ATTACH:
330
      lines = [utils.SafeEncode(val)
331
               for val in utils.TailFile(logfile, lines=20)]
332
    else:
333
      lines = result.output[-20:]
334

    
335
    base.ThrowError("External storage's %s script failed (%s), last"
336
                    " lines of output:\n%s",
337
                    action, result.fail_reason, "\n".join(lines))
338

    
339
  if action == constants.ES_ACTION_ATTACH:
340
    return result.stdout
341

    
342

    
343
def ExtStorageFromDisk(name, base_dir=None):
344
  """Create an ExtStorage instance from disk.
345

346
  This function will return an ExtStorage instance
347
  if the given name is a valid ExtStorage name.
348

349
  @type base_dir: string
350
  @keyword base_dir: Base directory containing ExtStorage installations.
351
                     Defaults to a search in all the ES_SEARCH_PATH dirs.
352
  @rtype: tuple
353
  @return: True and the ExtStorage instance if we find a valid one, or
354
      False and the diagnose message on error
355

356
  """
357
  if base_dir is None:
358
    es_base_dir = pathutils.ES_SEARCH_PATH
359
  else:
360
    es_base_dir = [base_dir]
361

    
362
  es_dir = utils.FindFile(name, es_base_dir, os.path.isdir)
363

    
364
  if es_dir is None:
365
    return False, ("Directory for External Storage Provider %s not"
366
                   " found in search path" % name)
367

    
368
  # ES Files dictionary, we will populate it with the absolute path
369
  # names; if the value is True, then it is a required file, otherwise
370
  # an optional one
371
  es_files = dict.fromkeys(constants.ES_SCRIPTS, True)
372

    
373
  es_files[constants.ES_PARAMETERS_FILE] = True
374

    
375
  for (filename, _) in es_files.items():
376
    es_files[filename] = utils.PathJoin(es_dir, filename)
377

    
378
    try:
379
      st = os.stat(es_files[filename])
380
    except EnvironmentError, err:
381
      return False, ("File '%s' under path '%s' is missing (%s)" %
382
                     (filename, es_dir, utils.ErrnoOrStr(err)))
383

    
384
    if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
385
      return False, ("File '%s' under path '%s' is not a regular file" %
386
                     (filename, es_dir))
387

    
388
    if filename in constants.ES_SCRIPTS:
389
      if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
390
        return False, ("File '%s' under path '%s' is not executable" %
391
                       (filename, es_dir))
392

    
393
  parameters = []
394
  if constants.ES_PARAMETERS_FILE in es_files:
395
    parameters_file = es_files[constants.ES_PARAMETERS_FILE]
396
    try:
397
      parameters = utils.ReadFile(parameters_file).splitlines()
398
    except EnvironmentError, err:
399
      return False, ("Error while reading the EXT parameters file at %s: %s" %
400
                     (parameters_file, utils.ErrnoOrStr(err)))
401
    parameters = [v.split(None, 1) for v in parameters]
402

    
403
  es_obj = \
404
    objects.ExtStorage(name=name, path=es_dir,
405
                       create_script=es_files[constants.ES_SCRIPT_CREATE],
406
                       remove_script=es_files[constants.ES_SCRIPT_REMOVE],
407
                       grow_script=es_files[constants.ES_SCRIPT_GROW],
408
                       attach_script=es_files[constants.ES_SCRIPT_ATTACH],
409
                       detach_script=es_files[constants.ES_SCRIPT_DETACH],
410
                       setinfo_script=es_files[constants.ES_SCRIPT_SETINFO],
411
                       verify_script=es_files[constants.ES_SCRIPT_VERIFY],
412
                       snapshot_script=es_files[constants.ES_SCRIPT_SNAPSHOT],
413
                       supported_parameters=parameters)
414
  return True, es_obj
415

    
416

    
417
def _ExtStorageEnvironment(unique_id, ext_params,
418
                           size=None, grow=None, metadata=None,
419
                           name=None, uuid=None, snapshot_name=None):
420
  """Calculate the environment for an External Storage script.
421

422
  @type unique_id: tuple (driver, vol_name)
423
  @param unique_id: ExtStorage pool and name of the Volume
424
  @type ext_params: dict
425
  @param ext_params: the EXT parameters
426
  @type size: string
427
  @param size: size of the Volume (in mebibytes)
428
  @type grow: string
429
  @param grow: new size of Volume after grow (in mebibytes)
430
  @type metadata: string
431
  @param metadata: metadata info of the Volume
432
  @type name: string
433
  @param name: name of the Volume (objects.Disk.name)
434
  @type uuid: string
435
  @param uuid: uuid of the Volume (objects.Disk.uuid)
436
  @rtype: dict
437
  @return: dict of environment variables
438

439
  """
440
  vol_name = unique_id[1]
441

    
442
  result = {}
443
  result["VOL_NAME"] = vol_name
444

    
445
  # EXT params
446
  for pname, pvalue in ext_params.items():
447
    result["EXTP_%s" % pname.upper()] = str(pvalue)
448

    
449
  if size is not None:
450
    result["VOL_SIZE"] = size
451

    
452
  if grow is not None:
453
    result["VOL_NEW_SIZE"] = grow
454

    
455
  if metadata is not None:
456
    result["VOL_METADATA"] = metadata
457

    
458
  if name is not None:
459
    result["VOL_CNAME"] = name
460

    
461
  if uuid is not None:
462
    result["VOL_UUID"] = uuid
463

    
464
  if snapshot_name is not None:
465
    result["VOL_SNAPSHOT_NAME"] = snapshot_name
466

    
467
  return result
468

    
469

    
470
def _VolumeLogName(kind, es_name, volume):
471
  """Compute the ExtStorage log filename for a given Volume and operation.
472

473
  @type kind: string
474
  @param kind: the operation type (e.g. create, remove etc.)
475
  @type es_name: string
476
  @param es_name: the ExtStorage name
477
  @type volume: string
478
  @param volume: the name of the Volume inside the External Storage
479

480
  """
481
  # Check if the extstorage log dir is a valid dir
482
  if not os.path.isdir(pathutils.LOG_ES_DIR):
483
    base.ThrowError("Cannot find log directory: %s", pathutils.LOG_ES_DIR)
484

    
485
  # TODO: Use tempfile.mkstemp to create unique filename
486
  basename = ("%s-%s-%s-%s.log" %
487
              (kind, es_name, volume, utils.TimestampForFilename()))
488
  return utils.PathJoin(pathutils.LOG_ES_DIR, basename)