Statistics
| Branch: | Tag: | Revision:

root / lib / backend.py @ 84a12e40

History | View | Annotate | Download (98.8 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007 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
"""Functions used by the node daemon
23

24
@var _ALLOWED_UPLOAD_FILES: denotes which files are accepted in
25
     the L{UploadFile} function
26
@var _ALLOWED_CLEAN_DIRS: denotes which directories are accepted
27
     in the L{_CleanDirectory} function
28

29
"""
30

    
31
# pylint: disable-msg=E1103
32

    
33
# E1103: %s %r has no %r member (but some types could not be
34
# inferred), because the _TryOSFromDisk returns either (True, os_obj)
35
# or (False, "string") which confuses pylint
36

    
37

    
38
import os
39
import os.path
40
import shutil
41
import time
42
import stat
43
import errno
44
import re
45
import random
46
import logging
47
import tempfile
48
import zlib
49
import base64
50
import signal
51

    
52
from ganeti import errors
53
from ganeti import utils
54
from ganeti import ssh
55
from ganeti import hypervisor
56
from ganeti import constants
57
from ganeti import bdev
58
from ganeti import objects
59
from ganeti import ssconf
60
from ganeti import serializer
61
from ganeti import netutils
62

    
63

    
64
_BOOT_ID_PATH = "/proc/sys/kernel/random/boot_id"
65
_ALLOWED_CLEAN_DIRS = frozenset([
66
  constants.DATA_DIR,
67
  constants.JOB_QUEUE_ARCHIVE_DIR,
68
  constants.QUEUE_DIR,
69
  constants.CRYPTO_KEYS_DIR,
70
  ])
71
_MAX_SSL_CERT_VALIDITY = 7 * 24 * 60 * 60
72
_X509_KEY_FILE = "key"
73
_X509_CERT_FILE = "cert"
74
_IES_STATUS_FILE = "status"
75
_IES_PID_FILE = "pid"
76
_IES_CA_FILE = "ca"
77

    
78

    
79
class RPCFail(Exception):
80
  """Class denoting RPC failure.
81

82
  Its argument is the error message.
83

84
  """
85

    
86

    
87
def _Fail(msg, *args, **kwargs):
88
  """Log an error and the raise an RPCFail exception.
89

90
  This exception is then handled specially in the ganeti daemon and
91
  turned into a 'failed' return type. As such, this function is a
92
  useful shortcut for logging the error and returning it to the master
93
  daemon.
94

95
  @type msg: string
96
  @param msg: the text of the exception
97
  @raise RPCFail
98

99
  """
100
  if args:
101
    msg = msg % args
102
  if "log" not in kwargs or kwargs["log"]: # if we should log this error
103
    if "exc" in kwargs and kwargs["exc"]:
104
      logging.exception(msg)
105
    else:
106
      logging.error(msg)
107
  raise RPCFail(msg)
108

    
109

    
110
def _GetConfig():
111
  """Simple wrapper to return a SimpleStore.
112

113
  @rtype: L{ssconf.SimpleStore}
114
  @return: a SimpleStore instance
115

116
  """
117
  return ssconf.SimpleStore()
118

    
119

    
120
def _GetSshRunner(cluster_name):
121
  """Simple wrapper to return an SshRunner.
122

123
  @type cluster_name: str
124
  @param cluster_name: the cluster name, which is needed
125
      by the SshRunner constructor
126
  @rtype: L{ssh.SshRunner}
127
  @return: an SshRunner instance
128

129
  """
130
  return ssh.SshRunner(cluster_name)
131

    
132

    
133
def _Decompress(data):
134
  """Unpacks data compressed by the RPC client.
135

136
  @type data: list or tuple
137
  @param data: Data sent by RPC client
138
  @rtype: str
139
  @return: Decompressed data
140

141
  """
142
  assert isinstance(data, (list, tuple))
143
  assert len(data) == 2
144
  (encoding, content) = data
145
  if encoding == constants.RPC_ENCODING_NONE:
146
    return content
147
  elif encoding == constants.RPC_ENCODING_ZLIB_BASE64:
148
    return zlib.decompress(base64.b64decode(content))
149
  else:
150
    raise AssertionError("Unknown data encoding")
151

    
152

    
153
def _CleanDirectory(path, exclude=None):
154
  """Removes all regular files in a directory.
155

156
  @type path: str
157
  @param path: the directory to clean
158
  @type exclude: list
159
  @param exclude: list of files to be excluded, defaults
160
      to the empty list
161

162
  """
163
  if path not in _ALLOWED_CLEAN_DIRS:
164
    _Fail("Path passed to _CleanDirectory not in allowed clean targets: '%s'",
165
          path)
166

    
167
  if not os.path.isdir(path):
168
    return
169
  if exclude is None:
170
    exclude = []
171
  else:
172
    # Normalize excluded paths
173
    exclude = [os.path.normpath(i) for i in exclude]
174

    
175
  for rel_name in utils.ListVisibleFiles(path):
176
    full_name = utils.PathJoin(path, rel_name)
177
    if full_name in exclude:
178
      continue
179
    if os.path.isfile(full_name) and not os.path.islink(full_name):
180
      utils.RemoveFile(full_name)
181

    
182

    
183
def _BuildUploadFileList():
184
  """Build the list of allowed upload files.
185

186
  This is abstracted so that it's built only once at module import time.
187

188
  """
189
  allowed_files = set([
190
    constants.CLUSTER_CONF_FILE,
191
    constants.ETC_HOSTS,
192
    constants.SSH_KNOWN_HOSTS_FILE,
193
    constants.VNC_PASSWORD_FILE,
194
    constants.RAPI_CERT_FILE,
195
    constants.RAPI_USERS_FILE,
196
    constants.CONFD_HMAC_KEY,
197
    constants.CLUSTER_DOMAIN_SECRET_FILE,
198
    ])
199

    
200
  for hv_name in constants.HYPER_TYPES:
201
    hv_class = hypervisor.GetHypervisorClass(hv_name)
202
    allowed_files.update(hv_class.GetAncillaryFiles())
203

    
204
  return frozenset(allowed_files)
205

    
206

    
207
_ALLOWED_UPLOAD_FILES = _BuildUploadFileList()
208

    
209

    
210
def JobQueuePurge():
211
  """Removes job queue files and archived jobs.
212

213
  @rtype: tuple
214
  @return: True, None
215

216
  """
217
  _CleanDirectory(constants.QUEUE_DIR, exclude=[constants.JOB_QUEUE_LOCK_FILE])
218
  _CleanDirectory(constants.JOB_QUEUE_ARCHIVE_DIR)
219

    
220

    
221
def GetMasterInfo():
222
  """Returns master information.
223

224
  This is an utility function to compute master information, either
225
  for consumption here or from the node daemon.
226

227
  @rtype: tuple
228
  @return: master_netdev, master_ip, master_name
229
  @raise RPCFail: in case of errors
230

231
  """
232
  try:
233
    cfg = _GetConfig()
234
    master_netdev = cfg.GetMasterNetdev()
235
    master_ip = cfg.GetMasterIP()
236
    master_node = cfg.GetMasterNode()
237
  except errors.ConfigurationError, err:
238
    _Fail("Cluster configuration incomplete: %s", err, exc=True)
239
  return (master_netdev, master_ip, master_node)
240

    
241

    
242
def StartMaster(start_daemons, no_voting):
243
  """Activate local node as master node.
244

245
  The function will always try activate the IP address of the master
246
  (unless someone else has it). It will also start the master daemons,
247
  based on the start_daemons parameter.
248

249
  @type start_daemons: boolean
250
  @param start_daemons: whether to also start the master
251
      daemons (ganeti-masterd and ganeti-rapi)
252
  @type no_voting: boolean
253
  @param no_voting: whether to start ganeti-masterd without a node vote
254
      (if start_daemons is True), but still non-interactively
255
  @rtype: None
256

257
  """
258
  # GetMasterInfo will raise an exception if not able to return data
259
  master_netdev, master_ip, _ = GetMasterInfo()
260

    
261
  err_msgs = []
262
  if netutils.TcpPing(master_ip, constants.DEFAULT_NODED_PORT):
263
    if netutils.OwnIpAddress(master_ip):
264
      # we already have the ip:
265
      logging.debug("Master IP already configured, doing nothing")
266
    else:
267
      msg = "Someone else has the master ip, not activating"
268
      logging.error(msg)
269
      err_msgs.append(msg)
270
  else:
271
    result = utils.RunCmd(["ip", "address", "add", "%s/32" % master_ip,
272
                           "dev", master_netdev, "label",
273
                           "%s:0" % master_netdev])
274
    if result.failed:
275
      msg = "Can't activate master IP: %s" % result.output
276
      logging.error(msg)
277
      err_msgs.append(msg)
278

    
279
    result = utils.RunCmd(["arping", "-q", "-U", "-c 3", "-I", master_netdev,
280
                           "-s", master_ip, master_ip])
281
    # we'll ignore the exit code of arping
282

    
283
  # and now start the master and rapi daemons
284
  if start_daemons:
285
    if no_voting:
286
      masterd_args = "--no-voting --yes-do-it"
287
    else:
288
      masterd_args = ""
289

    
290
    env = {
291
      "EXTRA_MASTERD_ARGS": masterd_args,
292
      }
293

    
294
    result = utils.RunCmd([constants.DAEMON_UTIL, "start-master"], env=env)
295
    if result.failed:
296
      msg = "Can't start Ganeti master: %s" % result.output
297
      logging.error(msg)
298
      err_msgs.append(msg)
299

    
300
  if err_msgs:
301
    _Fail("; ".join(err_msgs))
302

    
303

    
304
def StopMaster(stop_daemons):
305
  """Deactivate this node as master.
306

307
  The function will always try to deactivate the IP address of the
308
  master. It will also stop the master daemons depending on the
309
  stop_daemons parameter.
310

311
  @type stop_daemons: boolean
312
  @param stop_daemons: whether to also stop the master daemons
313
      (ganeti-masterd and ganeti-rapi)
314
  @rtype: None
315

316
  """
317
  # TODO: log and report back to the caller the error failures; we
318
  # need to decide in which case we fail the RPC for this
319

    
320
  # GetMasterInfo will raise an exception if not able to return data
321
  master_netdev, master_ip, _ = GetMasterInfo()
322

    
323
  result = utils.RunCmd(["ip", "address", "del", "%s/32" % master_ip,
324
                         "dev", master_netdev])
325
  if result.failed:
326
    logging.error("Can't remove the master IP, error: %s", result.output)
327
    # but otherwise ignore the failure
328

    
329
  if stop_daemons:
330
    result = utils.RunCmd([constants.DAEMON_UTIL, "stop-master"])
331
    if result.failed:
332
      logging.error("Could not stop Ganeti master, command %s had exitcode %s"
333
                    " and error %s",
334
                    result.cmd, result.exit_code, result.output)
335

    
336

    
337
def AddNode(dsa, dsapub, rsa, rsapub, sshkey, sshpub):
338
  """Joins this node to the cluster.
339

340
  This does the following:
341
      - updates the hostkeys of the machine (rsa and dsa)
342
      - adds the ssh private key to the user
343
      - adds the ssh public key to the users' authorized_keys file
344

345
  @type dsa: str
346
  @param dsa: the DSA private key to write
347
  @type dsapub: str
348
  @param dsapub: the DSA public key to write
349
  @type rsa: str
350
  @param rsa: the RSA private key to write
351
  @type rsapub: str
352
  @param rsapub: the RSA public key to write
353
  @type sshkey: str
354
  @param sshkey: the SSH private key to write
355
  @type sshpub: str
356
  @param sshpub: the SSH public key to write
357
  @rtype: boolean
358
  @return: the success of the operation
359

360
  """
361
  sshd_keys =  [(constants.SSH_HOST_RSA_PRIV, rsa, 0600),
362
                (constants.SSH_HOST_RSA_PUB, rsapub, 0644),
363
                (constants.SSH_HOST_DSA_PRIV, dsa, 0600),
364
                (constants.SSH_HOST_DSA_PUB, dsapub, 0644)]
365
  for name, content, mode in sshd_keys:
366
    utils.WriteFile(name, data=content, mode=mode)
367

    
368
  try:
369
    priv_key, pub_key, auth_keys = ssh.GetUserFiles(constants.GANETI_RUNAS,
370
                                                    mkdir=True)
371
  except errors.OpExecError, err:
372
    _Fail("Error while processing user ssh files: %s", err, exc=True)
373

    
374
  for name, content in [(priv_key, sshkey), (pub_key, sshpub)]:
375
    utils.WriteFile(name, data=content, mode=0600)
376

    
377
  utils.AddAuthorizedKey(auth_keys, sshpub)
378

    
379
  result = utils.RunCmd([constants.DAEMON_UTIL, "reload-ssh-keys"])
380
  if result.failed:
381
    _Fail("Unable to reload SSH keys (command %r, exit code %s, output %r)",
382
          result.cmd, result.exit_code, result.output)
383

    
384

    
385
def LeaveCluster(modify_ssh_setup):
386
  """Cleans up and remove the current node.
387

388
  This function cleans up and prepares the current node to be removed
389
  from the cluster.
390

391
  If processing is successful, then it raises an
392
  L{errors.QuitGanetiException} which is used as a special case to
393
  shutdown the node daemon.
394

395
  @param modify_ssh_setup: boolean
396

397
  """
398
  _CleanDirectory(constants.DATA_DIR)
399
  _CleanDirectory(constants.CRYPTO_KEYS_DIR)
400
  JobQueuePurge()
401

    
402
  if modify_ssh_setup:
403
    try:
404
      priv_key, pub_key, auth_keys = ssh.GetUserFiles(constants.GANETI_RUNAS)
405

    
406
      utils.RemoveAuthorizedKey(auth_keys, utils.ReadFile(pub_key))
407

    
408
      utils.RemoveFile(priv_key)
409
      utils.RemoveFile(pub_key)
410
    except errors.OpExecError:
411
      logging.exception("Error while processing ssh files")
412

    
413
  try:
414
    utils.RemoveFile(constants.CONFD_HMAC_KEY)
415
    utils.RemoveFile(constants.RAPI_CERT_FILE)
416
    utils.RemoveFile(constants.NODED_CERT_FILE)
417
  except: # pylint: disable-msg=W0702
418
    logging.exception("Error while removing cluster secrets")
419

    
420
  result = utils.RunCmd([constants.DAEMON_UTIL, "stop", constants.CONFD])
421
  if result.failed:
422
    logging.error("Command %s failed with exitcode %s and error %s",
423
                  result.cmd, result.exit_code, result.output)
424

    
425
  # Raise a custom exception (handled in ganeti-noded)
426
  raise errors.QuitGanetiException(True, 'Shutdown scheduled')
427

    
428

    
429
def GetNodeInfo(vgname, hypervisor_type):
430
  """Gives back a hash with different information about the node.
431

432
  @type vgname: C{string}
433
  @param vgname: the name of the volume group to ask for disk space information
434
  @type hypervisor_type: C{str}
435
  @param hypervisor_type: the name of the hypervisor to ask for
436
      memory information
437
  @rtype: C{dict}
438
  @return: dictionary with the following keys:
439
      - vg_size is the size of the configured volume group in MiB
440
      - vg_free is the free size of the volume group in MiB
441
      - memory_dom0 is the memory allocated for domain0 in MiB
442
      - memory_free is the currently available (free) ram in MiB
443
      - memory_total is the total number of ram in MiB
444

445
  """
446
  outputarray = {}
447
  vginfo = _GetVGInfo(vgname)
448
  outputarray['vg_size'] = vginfo['vg_size']
449
  outputarray['vg_free'] = vginfo['vg_free']
450

    
451
  hyper = hypervisor.GetHypervisor(hypervisor_type)
452
  hyp_info = hyper.GetNodeInfo()
453
  if hyp_info is not None:
454
    outputarray.update(hyp_info)
455

    
456
  outputarray["bootid"] = utils.ReadFile(_BOOT_ID_PATH, size=128).rstrip("\n")
457

    
458
  return outputarray
459

    
460

    
461
def VerifyNode(what, cluster_name):
462
  """Verify the status of the local node.
463

464
  Based on the input L{what} parameter, various checks are done on the
465
  local node.
466

467
  If the I{filelist} key is present, this list of
468
  files is checksummed and the file/checksum pairs are returned.
469

470
  If the I{nodelist} key is present, we check that we have
471
  connectivity via ssh with the target nodes (and check the hostname
472
  report).
473

474
  If the I{node-net-test} key is present, we check that we have
475
  connectivity to the given nodes via both primary IP and, if
476
  applicable, secondary IPs.
477

478
  @type what: C{dict}
479
  @param what: a dictionary of things to check:
480
      - filelist: list of files for which to compute checksums
481
      - nodelist: list of nodes we should check ssh communication with
482
      - node-net-test: list of nodes we should check node daemon port
483
        connectivity with
484
      - hypervisor: list with hypervisors to run the verify for
485
  @rtype: dict
486
  @return: a dictionary with the same keys as the input dict, and
487
      values representing the result of the checks
488

489
  """
490
  result = {}
491
  my_name = netutils.HostInfo().name
492
  port = netutils.GetDaemonPort(constants.NODED)
493

    
494
  if constants.NV_HYPERVISOR in what:
495
    result[constants.NV_HYPERVISOR] = tmp = {}
496
    for hv_name in what[constants.NV_HYPERVISOR]:
497
      try:
498
        val = hypervisor.GetHypervisor(hv_name).Verify()
499
      except errors.HypervisorError, err:
500
        val = "Error while checking hypervisor: %s" % str(err)
501
      tmp[hv_name] = val
502

    
503
  if constants.NV_FILELIST in what:
504
    result[constants.NV_FILELIST] = utils.FingerprintFiles(
505
      what[constants.NV_FILELIST])
506

    
507
  if constants.NV_NODELIST in what:
508
    result[constants.NV_NODELIST] = tmp = {}
509
    random.shuffle(what[constants.NV_NODELIST])
510
    for node in what[constants.NV_NODELIST]:
511
      success, message = _GetSshRunner(cluster_name).VerifyNodeHostname(node)
512
      if not success:
513
        tmp[node] = message
514

    
515
  if constants.NV_NODENETTEST in what:
516
    result[constants.NV_NODENETTEST] = tmp = {}
517
    my_pip = my_sip = None
518
    for name, pip, sip in what[constants.NV_NODENETTEST]:
519
      if name == my_name:
520
        my_pip = pip
521
        my_sip = sip
522
        break
523
    if not my_pip:
524
      tmp[my_name] = ("Can't find my own primary/secondary IP"
525
                      " in the node list")
526
    else:
527
      for name, pip, sip in what[constants.NV_NODENETTEST]:
528
        fail = []
529
        if not netutils.TcpPing(pip, port, source=my_pip):
530
          fail.append("primary")
531
        if sip != pip:
532
          if not netutils.TcpPing(sip, port, source=my_sip):
533
            fail.append("secondary")
534
        if fail:
535
          tmp[name] = ("failure using the %s interface(s)" %
536
                       " and ".join(fail))
537

    
538
  if constants.NV_MASTERIP in what:
539
    # FIXME: add checks on incoming data structures (here and in the
540
    # rest of the function)
541
    master_name, master_ip = what[constants.NV_MASTERIP]
542
    if master_name == my_name:
543
      source = constants.IP4_ADDRESS_LOCALHOST
544
    else:
545
      source = None
546
    result[constants.NV_MASTERIP] = netutils.TcpPing(master_ip, port,
547
                                                  source=source)
548

    
549
  if constants.NV_LVLIST in what:
550
    try:
551
      val = GetVolumeList(what[constants.NV_LVLIST])
552
    except RPCFail, err:
553
      val = str(err)
554
    result[constants.NV_LVLIST] = val
555

    
556
  if constants.NV_INSTANCELIST in what:
557
    # GetInstanceList can fail
558
    try:
559
      val = GetInstanceList(what[constants.NV_INSTANCELIST])
560
    except RPCFail, err:
561
      val = str(err)
562
    result[constants.NV_INSTANCELIST] = val
563

    
564
  if constants.NV_VGLIST in what:
565
    result[constants.NV_VGLIST] = utils.ListVolumeGroups()
566

    
567
  if constants.NV_PVLIST in what:
568
    result[constants.NV_PVLIST] = \
569
      bdev.LogicalVolume.GetPVInfo(what[constants.NV_PVLIST],
570
                                   filter_allocatable=False)
571

    
572
  if constants.NV_VERSION in what:
573
    result[constants.NV_VERSION] = (constants.PROTOCOL_VERSION,
574
                                    constants.RELEASE_VERSION)
575

    
576
  if constants.NV_HVINFO in what:
577
    hyper = hypervisor.GetHypervisor(what[constants.NV_HVINFO])
578
    result[constants.NV_HVINFO] = hyper.GetNodeInfo()
579

    
580
  if constants.NV_DRBDLIST in what:
581
    try:
582
      used_minors = bdev.DRBD8.GetUsedDevs().keys()
583
    except errors.BlockDeviceError, err:
584
      logging.warning("Can't get used minors list", exc_info=True)
585
      used_minors = str(err)
586
    result[constants.NV_DRBDLIST] = used_minors
587

    
588
  if constants.NV_DRBDHELPER in what:
589
    status = True
590
    try:
591
      payload = bdev.BaseDRBD.GetUsermodeHelper()
592
    except errors.BlockDeviceError, err:
593
      logging.error("Can't get DRBD usermode helper: %s", str(err))
594
      status = False
595
      payload = str(err)
596
    result[constants.NV_DRBDHELPER] = (status, payload)
597

    
598
  if constants.NV_NODESETUP in what:
599
    result[constants.NV_NODESETUP] = tmpr = []
600
    if not os.path.isdir("/sys/block") or not os.path.isdir("/sys/class/net"):
601
      tmpr.append("The sysfs filesytem doesn't seem to be mounted"
602
                  " under /sys, missing required directories /sys/block"
603
                  " and /sys/class/net")
604
    if (not os.path.isdir("/proc/sys") or
605
        not os.path.isfile("/proc/sysrq-trigger")):
606
      tmpr.append("The procfs filesystem doesn't seem to be mounted"
607
                  " under /proc, missing required directory /proc/sys and"
608
                  " the file /proc/sysrq-trigger")
609

    
610
  if constants.NV_TIME in what:
611
    result[constants.NV_TIME] = utils.SplitTime(time.time())
612

    
613
  if constants.NV_OSLIST in what:
614
    result[constants.NV_OSLIST] = DiagnoseOS()
615

    
616
  return result
617

    
618

    
619
def GetVolumeList(vg_name):
620
  """Compute list of logical volumes and their size.
621

622
  @type vg_name: str
623
  @param vg_name: the volume group whose LVs we should list
624
  @rtype: dict
625
  @return:
626
      dictionary of all partions (key) with value being a tuple of
627
      their size (in MiB), inactive and online status::
628

629
        {'test1': ('20.06', True, True)}
630

631
      in case of errors, a string is returned with the error
632
      details.
633

634
  """
635
  lvs = {}
636
  sep = '|'
637
  result = utils.RunCmd(["lvs", "--noheadings", "--units=m", "--nosuffix",
638
                         "--separator=%s" % sep,
639
                         "-olv_name,lv_size,lv_attr", vg_name])
640
  if result.failed:
641
    _Fail("Failed to list logical volumes, lvs output: %s", result.output)
642

    
643
  valid_line_re = re.compile("^ *([^|]+)\|([0-9.]+)\|([^|]{6})\|?$")
644
  for line in result.stdout.splitlines():
645
    line = line.strip()
646
    match = valid_line_re.match(line)
647
    if not match:
648
      logging.error("Invalid line returned from lvs output: '%s'", line)
649
      continue
650
    name, size, attr = match.groups()
651
    inactive = attr[4] == '-'
652
    online = attr[5] == 'o'
653
    virtual = attr[0] == 'v'
654
    if virtual:
655
      # we don't want to report such volumes as existing, since they
656
      # don't really hold data
657
      continue
658
    lvs[name] = (size, inactive, online)
659

    
660
  return lvs
661

    
662

    
663
def ListVolumeGroups():
664
  """List the volume groups and their size.
665

666
  @rtype: dict
667
  @return: dictionary with keys volume name and values the
668
      size of the volume
669

670
  """
671
  return utils.ListVolumeGroups()
672

    
673

    
674
def NodeVolumes():
675
  """List all volumes on this node.
676

677
  @rtype: list
678
  @return:
679
    A list of dictionaries, each having four keys:
680
      - name: the logical volume name,
681
      - size: the size of the logical volume
682
      - dev: the physical device on which the LV lives
683
      - vg: the volume group to which it belongs
684

685
    In case of errors, we return an empty list and log the
686
    error.
687

688
    Note that since a logical volume can live on multiple physical
689
    volumes, the resulting list might include a logical volume
690
    multiple times.
691

692
  """
693
  result = utils.RunCmd(["lvs", "--noheadings", "--units=m", "--nosuffix",
694
                         "--separator=|",
695
                         "--options=lv_name,lv_size,devices,vg_name"])
696
  if result.failed:
697
    _Fail("Failed to list logical volumes, lvs output: %s",
698
          result.output)
699

    
700
  def parse_dev(dev):
701
    return dev.split('(')[0]
702

    
703
  def handle_dev(dev):
704
    return [parse_dev(x) for x in dev.split(",")]
705

    
706
  def map_line(line):
707
    line = [v.strip() for v in line]
708
    return [{'name': line[0], 'size': line[1],
709
             'dev': dev, 'vg': line[3]} for dev in handle_dev(line[2])]
710

    
711
  all_devs = []
712
  for line in result.stdout.splitlines():
713
    if line.count('|') >= 3:
714
      all_devs.extend(map_line(line.split('|')))
715
    else:
716
      logging.warning("Strange line in the output from lvs: '%s'", line)
717
  return all_devs
718

    
719

    
720
def BridgesExist(bridges_list):
721
  """Check if a list of bridges exist on the current node.
722

723
  @rtype: boolean
724
  @return: C{True} if all of them exist, C{False} otherwise
725

726
  """
727
  missing = []
728
  for bridge in bridges_list:
729
    if not utils.BridgeExists(bridge):
730
      missing.append(bridge)
731

    
732
  if missing:
733
    _Fail("Missing bridges %s", utils.CommaJoin(missing))
734

    
735

    
736
def GetInstanceList(hypervisor_list):
737
  """Provides a list of instances.
738

739
  @type hypervisor_list: list
740
  @param hypervisor_list: the list of hypervisors to query information
741

742
  @rtype: list
743
  @return: a list of all running instances on the current node
744
    - instance1.example.com
745
    - instance2.example.com
746

747
  """
748
  results = []
749
  for hname in hypervisor_list:
750
    try:
751
      names = hypervisor.GetHypervisor(hname).ListInstances()
752
      results.extend(names)
753
    except errors.HypervisorError, err:
754
      _Fail("Error enumerating instances (hypervisor %s): %s",
755
            hname, err, exc=True)
756

    
757
  return results
758

    
759

    
760
def GetInstanceInfo(instance, hname):
761
  """Gives back the information about an instance as a dictionary.
762

763
  @type instance: string
764
  @param instance: the instance name
765
  @type hname: string
766
  @param hname: the hypervisor type of the instance
767

768
  @rtype: dict
769
  @return: dictionary with the following keys:
770
      - memory: memory size of instance (int)
771
      - state: xen state of instance (string)
772
      - time: cpu time of instance (float)
773

774
  """
775
  output = {}
776

    
777
  iinfo = hypervisor.GetHypervisor(hname).GetInstanceInfo(instance)
778
  if iinfo is not None:
779
    output['memory'] = iinfo[2]
780
    output['state'] = iinfo[4]
781
    output['time'] = iinfo[5]
782

    
783
  return output
784

    
785

    
786
def GetInstanceMigratable(instance):
787
  """Gives whether an instance can be migrated.
788

789
  @type instance: L{objects.Instance}
790
  @param instance: object representing the instance to be checked.
791

792
  @rtype: tuple
793
  @return: tuple of (result, description) where:
794
      - result: whether the instance can be migrated or not
795
      - description: a description of the issue, if relevant
796

797
  """
798
  hyper = hypervisor.GetHypervisor(instance.hypervisor)
799
  iname = instance.name
800
  if iname not in hyper.ListInstances():
801
    _Fail("Instance %s is not running", iname)
802

    
803
  for idx in range(len(instance.disks)):
804
    link_name = _GetBlockDevSymlinkPath(iname, idx)
805
    if not os.path.islink(link_name):
806
      _Fail("Instance %s was not restarted since ganeti 1.2.5", iname)
807

    
808

    
809
def GetAllInstancesInfo(hypervisor_list):
810
  """Gather data about all instances.
811

812
  This is the equivalent of L{GetInstanceInfo}, except that it
813
  computes data for all instances at once, thus being faster if one
814
  needs data about more than one instance.
815

816
  @type hypervisor_list: list
817
  @param hypervisor_list: list of hypervisors to query for instance data
818

819
  @rtype: dict
820
  @return: dictionary of instance: data, with data having the following keys:
821
      - memory: memory size of instance (int)
822
      - state: xen state of instance (string)
823
      - time: cpu time of instance (float)
824
      - vcpus: the number of vcpus
825

826
  """
827
  output = {}
828

    
829
  for hname in hypervisor_list:
830
    iinfo = hypervisor.GetHypervisor(hname).GetAllInstancesInfo()
831
    if iinfo:
832
      for name, _, memory, vcpus, state, times in iinfo:
833
        value = {
834
          'memory': memory,
835
          'vcpus': vcpus,
836
          'state': state,
837
          'time': times,
838
          }
839
        if name in output:
840
          # we only check static parameters, like memory and vcpus,
841
          # and not state and time which can change between the
842
          # invocations of the different hypervisors
843
          for key in 'memory', 'vcpus':
844
            if value[key] != output[name][key]:
845
              _Fail("Instance %s is running twice"
846
                    " with different parameters", name)
847
        output[name] = value
848

    
849
  return output
850

    
851

    
852
def _InstanceLogName(kind, os_name, instance):
853
  """Compute the OS log filename for a given instance and operation.
854

855
  The instance name and os name are passed in as strings since not all
856
  operations have these as part of an instance object.
857

858
  @type kind: string
859
  @param kind: the operation type (e.g. add, import, etc.)
860
  @type os_name: string
861
  @param os_name: the os name
862
  @type instance: string
863
  @param instance: the name of the instance being imported/added/etc.
864

865
  """
866
  # TODO: Use tempfile.mkstemp to create unique filename
867
  base = ("%s-%s-%s-%s.log" %
868
          (kind, os_name, instance, utils.TimestampForFilename()))
869
  return utils.PathJoin(constants.LOG_OS_DIR, base)
870

    
871

    
872
def InstanceOsAdd(instance, reinstall, debug):
873
  """Add an OS to an instance.
874

875
  @type instance: L{objects.Instance}
876
  @param instance: Instance whose OS is to be installed
877
  @type reinstall: boolean
878
  @param reinstall: whether this is an instance reinstall
879
  @type debug: integer
880
  @param debug: debug level, passed to the OS scripts
881
  @rtype: None
882

883
  """
884
  inst_os = OSFromDisk(instance.os)
885

    
886
  create_env = OSEnvironment(instance, inst_os, debug)
887
  if reinstall:
888
    create_env['INSTANCE_REINSTALL'] = "1"
889

    
890
  logfile = _InstanceLogName("add", instance.os, instance.name)
891

    
892
  result = utils.RunCmd([inst_os.create_script], env=create_env,
893
                        cwd=inst_os.path, output=logfile,)
894
  if result.failed:
895
    logging.error("os create command '%s' returned error: %s, logfile: %s,"
896
                  " output: %s", result.cmd, result.fail_reason, logfile,
897
                  result.output)
898
    lines = [utils.SafeEncode(val)
899
             for val in utils.TailFile(logfile, lines=20)]
900
    _Fail("OS create script failed (%s), last lines in the"
901
          " log file:\n%s", result.fail_reason, "\n".join(lines), log=False)
902

    
903

    
904
def RunRenameInstance(instance, old_name, debug):
905
  """Run the OS rename script for an instance.
906

907
  @type instance: L{objects.Instance}
908
  @param instance: Instance whose OS is to be installed
909
  @type old_name: string
910
  @param old_name: previous instance name
911
  @type debug: integer
912
  @param debug: debug level, passed to the OS scripts
913
  @rtype: boolean
914
  @return: the success of the operation
915

916
  """
917
  inst_os = OSFromDisk(instance.os)
918

    
919
  rename_env = OSEnvironment(instance, inst_os, debug)
920
  rename_env['OLD_INSTANCE_NAME'] = old_name
921

    
922
  logfile = _InstanceLogName("rename", instance.os,
923
                             "%s-%s" % (old_name, instance.name))
924

    
925
  result = utils.RunCmd([inst_os.rename_script], env=rename_env,
926
                        cwd=inst_os.path, output=logfile)
927

    
928
  if result.failed:
929
    logging.error("os create command '%s' returned error: %s output: %s",
930
                  result.cmd, result.fail_reason, result.output)
931
    lines = [utils.SafeEncode(val)
932
             for val in utils.TailFile(logfile, lines=20)]
933
    _Fail("OS rename script failed (%s), last lines in the"
934
          " log file:\n%s", result.fail_reason, "\n".join(lines), log=False)
935

    
936

    
937
def _GetVGInfo(vg_name):
938
  """Get information about the volume group.
939

940
  @type vg_name: str
941
  @param vg_name: the volume group which we query
942
  @rtype: dict
943
  @return:
944
    A dictionary with the following keys:
945
      - C{vg_size} is the total size of the volume group in MiB
946
      - C{vg_free} is the free size of the volume group in MiB
947
      - C{pv_count} are the number of physical disks in that VG
948

949
    If an error occurs during gathering of data, we return the same dict
950
    with keys all set to None.
951

952
  """
953
  retdic = dict.fromkeys(["vg_size", "vg_free", "pv_count"])
954

    
955
  retval = utils.RunCmd(["vgs", "-ovg_size,vg_free,pv_count", "--noheadings",
956
                         "--nosuffix", "--units=m", "--separator=:", vg_name])
957

    
958
  if retval.failed:
959
    logging.error("volume group %s not present", vg_name)
960
    return retdic
961
  valarr = retval.stdout.strip().rstrip(':').split(':')
962
  if len(valarr) == 3:
963
    try:
964
      retdic = {
965
        "vg_size": int(round(float(valarr[0]), 0)),
966
        "vg_free": int(round(float(valarr[1]), 0)),
967
        "pv_count": int(valarr[2]),
968
        }
969
    except (TypeError, ValueError), err:
970
      logging.exception("Fail to parse vgs output: %s", err)
971
  else:
972
    logging.error("vgs output has the wrong number of fields (expected"
973
                  " three): %s", str(valarr))
974
  return retdic
975

    
976

    
977
def _GetBlockDevSymlinkPath(instance_name, idx):
978
  return utils.PathJoin(constants.DISK_LINKS_DIR,
979
                        "%s:%d" % (instance_name, idx))
980

    
981

    
982
def _SymlinkBlockDev(instance_name, device_path, idx):
983
  """Set up symlinks to a instance's block device.
984

985
  This is an auxiliary function run when an instance is start (on the primary
986
  node) or when an instance is migrated (on the target node).
987

988

989
  @param instance_name: the name of the target instance
990
  @param device_path: path of the physical block device, on the node
991
  @param idx: the disk index
992
  @return: absolute path to the disk's symlink
993

994
  """
995
  link_name = _GetBlockDevSymlinkPath(instance_name, idx)
996
  try:
997
    os.symlink(device_path, link_name)
998
  except OSError, err:
999
    if err.errno == errno.EEXIST:
1000
      if (not os.path.islink(link_name) or
1001
          os.readlink(link_name) != device_path):
1002
        os.remove(link_name)
1003
        os.symlink(device_path, link_name)
1004
    else:
1005
      raise
1006

    
1007
  return link_name
1008

    
1009

    
1010
def _RemoveBlockDevLinks(instance_name, disks):
1011
  """Remove the block device symlinks belonging to the given instance.
1012

1013
  """
1014
  for idx, _ in enumerate(disks):
1015
    link_name = _GetBlockDevSymlinkPath(instance_name, idx)
1016
    if os.path.islink(link_name):
1017
      try:
1018
        os.remove(link_name)
1019
      except OSError:
1020
        logging.exception("Can't remove symlink '%s'", link_name)
1021

    
1022

    
1023
def _GatherAndLinkBlockDevs(instance):
1024
  """Set up an instance's block device(s).
1025

1026
  This is run on the primary node at instance startup. The block
1027
  devices must be already assembled.
1028

1029
  @type instance: L{objects.Instance}
1030
  @param instance: the instance whose disks we shoul assemble
1031
  @rtype: list
1032
  @return: list of (disk_object, device_path)
1033

1034
  """
1035
  block_devices = []
1036
  for idx, disk in enumerate(instance.disks):
1037
    device = _RecursiveFindBD(disk)
1038
    if device is None:
1039
      raise errors.BlockDeviceError("Block device '%s' is not set up." %
1040
                                    str(disk))
1041
    device.Open()
1042
    try:
1043
      link_name = _SymlinkBlockDev(instance.name, device.dev_path, idx)
1044
    except OSError, e:
1045
      raise errors.BlockDeviceError("Cannot create block device symlink: %s" %
1046
                                    e.strerror)
1047

    
1048
    block_devices.append((disk, link_name))
1049

    
1050
  return block_devices
1051

    
1052

    
1053
def StartInstance(instance):
1054
  """Start an instance.
1055

1056
  @type instance: L{objects.Instance}
1057
  @param instance: the instance object
1058
  @rtype: None
1059

1060
  """
1061
  running_instances = GetInstanceList([instance.hypervisor])
1062

    
1063
  if instance.name in running_instances:
1064
    logging.info("Instance %s already running, not starting", instance.name)
1065
    return
1066

    
1067
  try:
1068
    block_devices = _GatherAndLinkBlockDevs(instance)
1069
    hyper = hypervisor.GetHypervisor(instance.hypervisor)
1070
    hyper.StartInstance(instance, block_devices)
1071
  except errors.BlockDeviceError, err:
1072
    _Fail("Block device error: %s", err, exc=True)
1073
  except errors.HypervisorError, err:
1074
    _RemoveBlockDevLinks(instance.name, instance.disks)
1075
    _Fail("Hypervisor error: %s", err, exc=True)
1076

    
1077

    
1078
def InstanceShutdown(instance, timeout):
1079
  """Shut an instance down.
1080

1081
  @note: this functions uses polling with a hardcoded timeout.
1082

1083
  @type instance: L{objects.Instance}
1084
  @param instance: the instance object
1085
  @type timeout: integer
1086
  @param timeout: maximum timeout for soft shutdown
1087
  @rtype: None
1088

1089
  """
1090
  hv_name = instance.hypervisor
1091
  hyper = hypervisor.GetHypervisor(hv_name)
1092
  iname = instance.name
1093

    
1094
  if instance.name not in hyper.ListInstances():
1095
    logging.info("Instance %s not running, doing nothing", iname)
1096
    return
1097

    
1098
  class _TryShutdown:
1099
    def __init__(self):
1100
      self.tried_once = False
1101

    
1102
    def __call__(self):
1103
      if iname not in hyper.ListInstances():
1104
        return
1105

    
1106
      try:
1107
        hyper.StopInstance(instance, retry=self.tried_once)
1108
      except errors.HypervisorError, err:
1109
        if iname not in hyper.ListInstances():
1110
          # if the instance is no longer existing, consider this a
1111
          # success and go to cleanup
1112
          return
1113

    
1114
        _Fail("Failed to stop instance %s: %s", iname, err)
1115

    
1116
      self.tried_once = True
1117

    
1118
      raise utils.RetryAgain()
1119

    
1120
  try:
1121
    utils.Retry(_TryShutdown(), 5, timeout)
1122
  except utils.RetryTimeout:
1123
    # the shutdown did not succeed
1124
    logging.error("Shutdown of '%s' unsuccessful, forcing", iname)
1125

    
1126
    try:
1127
      hyper.StopInstance(instance, force=True)
1128
    except errors.HypervisorError, err:
1129
      if iname in hyper.ListInstances():
1130
        # only raise an error if the instance still exists, otherwise
1131
        # the error could simply be "instance ... unknown"!
1132
        _Fail("Failed to force stop instance %s: %s", iname, err)
1133

    
1134
    time.sleep(1)
1135

    
1136
    if iname in hyper.ListInstances():
1137
      _Fail("Could not shutdown instance %s even by destroy", iname)
1138

    
1139
  try:
1140
    hyper.CleanupInstance(instance.name)
1141
  except errors.HypervisorError, err:
1142
    logging.warning("Failed to execute post-shutdown cleanup step: %s", err)
1143

    
1144
  _RemoveBlockDevLinks(iname, instance.disks)
1145

    
1146

    
1147
def InstanceReboot(instance, reboot_type, shutdown_timeout):
1148
  """Reboot an instance.
1149

1150
  @type instance: L{objects.Instance}
1151
  @param instance: the instance object to reboot
1152
  @type reboot_type: str
1153
  @param reboot_type: the type of reboot, one the following
1154
    constants:
1155
      - L{constants.INSTANCE_REBOOT_SOFT}: only reboot the
1156
        instance OS, do not recreate the VM
1157
      - L{constants.INSTANCE_REBOOT_HARD}: tear down and
1158
        restart the VM (at the hypervisor level)
1159
      - the other reboot type (L{constants.INSTANCE_REBOOT_FULL}) is
1160
        not accepted here, since that mode is handled differently, in
1161
        cmdlib, and translates into full stop and start of the
1162
        instance (instead of a call_instance_reboot RPC)
1163
  @type shutdown_timeout: integer
1164
  @param shutdown_timeout: maximum timeout for soft shutdown
1165
  @rtype: None
1166

1167
  """
1168
  running_instances = GetInstanceList([instance.hypervisor])
1169

    
1170
  if instance.name not in running_instances:
1171
    _Fail("Cannot reboot instance %s that is not running", instance.name)
1172

    
1173
  hyper = hypervisor.GetHypervisor(instance.hypervisor)
1174
  if reboot_type == constants.INSTANCE_REBOOT_SOFT:
1175
    try:
1176
      hyper.RebootInstance(instance)
1177
    except errors.HypervisorError, err:
1178
      _Fail("Failed to soft reboot instance %s: %s", instance.name, err)
1179
  elif reboot_type == constants.INSTANCE_REBOOT_HARD:
1180
    try:
1181
      InstanceShutdown(instance, shutdown_timeout)
1182
      return StartInstance(instance)
1183
    except errors.HypervisorError, err:
1184
      _Fail("Failed to hard reboot instance %s: %s", instance.name, err)
1185
  else:
1186
    _Fail("Invalid reboot_type received: %s", reboot_type)
1187

    
1188

    
1189
def MigrationInfo(instance):
1190
  """Gather information about an instance to be migrated.
1191

1192
  @type instance: L{objects.Instance}
1193
  @param instance: the instance definition
1194

1195
  """
1196
  hyper = hypervisor.GetHypervisor(instance.hypervisor)
1197
  try:
1198
    info = hyper.MigrationInfo(instance)
1199
  except errors.HypervisorError, err:
1200
    _Fail("Failed to fetch migration information: %s", err, exc=True)
1201
  return info
1202

    
1203

    
1204
def AcceptInstance(instance, info, target):
1205
  """Prepare the node to accept an instance.
1206

1207
  @type instance: L{objects.Instance}
1208
  @param instance: the instance definition
1209
  @type info: string/data (opaque)
1210
  @param info: migration information, from the source node
1211
  @type target: string
1212
  @param target: target host (usually ip), on this node
1213

1214
  """
1215
  hyper = hypervisor.GetHypervisor(instance.hypervisor)
1216
  try:
1217
    hyper.AcceptInstance(instance, info, target)
1218
  except errors.HypervisorError, err:
1219
    _Fail("Failed to accept instance: %s", err, exc=True)
1220

    
1221

    
1222
def FinalizeMigration(instance, info, success):
1223
  """Finalize any preparation to accept an instance.
1224

1225
  @type instance: L{objects.Instance}
1226
  @param instance: the instance definition
1227
  @type info: string/data (opaque)
1228
  @param info: migration information, from the source node
1229
  @type success: boolean
1230
  @param success: whether the migration was a success or a failure
1231

1232
  """
1233
  hyper = hypervisor.GetHypervisor(instance.hypervisor)
1234
  try:
1235
    hyper.FinalizeMigration(instance, info, success)
1236
  except errors.HypervisorError, err:
1237
    _Fail("Failed to finalize migration: %s", err, exc=True)
1238

    
1239

    
1240
def MigrateInstance(instance, target, live):
1241
  """Migrates an instance to another node.
1242

1243
  @type instance: L{objects.Instance}
1244
  @param instance: the instance definition
1245
  @type target: string
1246
  @param target: the target node name
1247
  @type live: boolean
1248
  @param live: whether the migration should be done live or not (the
1249
      interpretation of this parameter is left to the hypervisor)
1250
  @rtype: tuple
1251
  @return: a tuple of (success, msg) where:
1252
      - succes is a boolean denoting the success/failure of the operation
1253
      - msg is a string with details in case of failure
1254

1255
  """
1256
  hyper = hypervisor.GetHypervisor(instance.hypervisor)
1257

    
1258
  try:
1259
    hyper.MigrateInstance(instance, target, live)
1260
  except errors.HypervisorError, err:
1261
    _Fail("Failed to migrate instance: %s", err, exc=True)
1262

    
1263

    
1264
def BlockdevCreate(disk, size, owner, on_primary, info):
1265
  """Creates a block device for an instance.
1266

1267
  @type disk: L{objects.Disk}
1268
  @param disk: the object describing the disk we should create
1269
  @type size: int
1270
  @param size: the size of the physical underlying device, in MiB
1271
  @type owner: str
1272
  @param owner: the name of the instance for which disk is created,
1273
      used for device cache data
1274
  @type on_primary: boolean
1275
  @param on_primary:  indicates if it is the primary node or not
1276
  @type info: string
1277
  @param info: string that will be sent to the physical device
1278
      creation, used for example to set (LVM) tags on LVs
1279

1280
  @return: the new unique_id of the device (this can sometime be
1281
      computed only after creation), or None. On secondary nodes,
1282
      it's not required to return anything.
1283

1284
  """
1285
  # TODO: remove the obsolete 'size' argument
1286
  # pylint: disable-msg=W0613
1287
  clist = []
1288
  if disk.children:
1289
    for child in disk.children:
1290
      try:
1291
        crdev = _RecursiveAssembleBD(child, owner, on_primary)
1292
      except errors.BlockDeviceError, err:
1293
        _Fail("Can't assemble device %s: %s", child, err)
1294
      if on_primary or disk.AssembleOnSecondary():
1295
        # we need the children open in case the device itself has to
1296
        # be assembled
1297
        try:
1298
          # pylint: disable-msg=E1103
1299
          crdev.Open()
1300
        except errors.BlockDeviceError, err:
1301
          _Fail("Can't make child '%s' read-write: %s", child, err)
1302
      clist.append(crdev)
1303

    
1304
  try:
1305
    device = bdev.Create(disk.dev_type, disk.physical_id, clist, disk.size)
1306
  except errors.BlockDeviceError, err:
1307
    _Fail("Can't create block device: %s", err)
1308

    
1309
  if on_primary or disk.AssembleOnSecondary():
1310
    try:
1311
      device.Assemble()
1312
    except errors.BlockDeviceError, err:
1313
      _Fail("Can't assemble device after creation, unusual event: %s", err)
1314
    device.SetSyncSpeed(constants.SYNC_SPEED)
1315
    if on_primary or disk.OpenOnSecondary():
1316
      try:
1317
        device.Open(force=True)
1318
      except errors.BlockDeviceError, err:
1319
        _Fail("Can't make device r/w after creation, unusual event: %s", err)
1320
    DevCacheManager.UpdateCache(device.dev_path, owner,
1321
                                on_primary, disk.iv_name)
1322

    
1323
  device.SetInfo(info)
1324

    
1325
  return device.unique_id
1326

    
1327

    
1328
def BlockdevRemove(disk):
1329
  """Remove a block device.
1330

1331
  @note: This is intended to be called recursively.
1332

1333
  @type disk: L{objects.Disk}
1334
  @param disk: the disk object we should remove
1335
  @rtype: boolean
1336
  @return: the success of the operation
1337

1338
  """
1339
  msgs = []
1340
  try:
1341
    rdev = _RecursiveFindBD(disk)
1342
  except errors.BlockDeviceError, err:
1343
    # probably can't attach
1344
    logging.info("Can't attach to device %s in remove", disk)
1345
    rdev = None
1346
  if rdev is not None:
1347
    r_path = rdev.dev_path
1348
    try:
1349
      rdev.Remove()
1350
    except errors.BlockDeviceError, err:
1351
      msgs.append(str(err))
1352
    if not msgs:
1353
      DevCacheManager.RemoveCache(r_path)
1354

    
1355
  if disk.children:
1356
    for child in disk.children:
1357
      try:
1358
        BlockdevRemove(child)
1359
      except RPCFail, err:
1360
        msgs.append(str(err))
1361

    
1362
  if msgs:
1363
    _Fail("; ".join(msgs))
1364

    
1365

    
1366
def _RecursiveAssembleBD(disk, owner, as_primary):
1367
  """Activate a block device for an instance.
1368

1369
  This is run on the primary and secondary nodes for an instance.
1370

1371
  @note: this function is called recursively.
1372

1373
  @type disk: L{objects.Disk}
1374
  @param disk: the disk we try to assemble
1375
  @type owner: str
1376
  @param owner: the name of the instance which owns the disk
1377
  @type as_primary: boolean
1378
  @param as_primary: if we should make the block device
1379
      read/write
1380

1381
  @return: the assembled device or None (in case no device
1382
      was assembled)
1383
  @raise errors.BlockDeviceError: in case there is an error
1384
      during the activation of the children or the device
1385
      itself
1386

1387
  """
1388
  children = []
1389
  if disk.children:
1390
    mcn = disk.ChildrenNeeded()
1391
    if mcn == -1:
1392
      mcn = 0 # max number of Nones allowed
1393
    else:
1394
      mcn = len(disk.children) - mcn # max number of Nones
1395
    for chld_disk in disk.children:
1396
      try:
1397
        cdev = _RecursiveAssembleBD(chld_disk, owner, as_primary)
1398
      except errors.BlockDeviceError, err:
1399
        if children.count(None) >= mcn:
1400
          raise
1401
        cdev = None
1402
        logging.error("Error in child activation (but continuing): %s",
1403
                      str(err))
1404
      children.append(cdev)
1405

    
1406
  if as_primary or disk.AssembleOnSecondary():
1407
    r_dev = bdev.Assemble(disk.dev_type, disk.physical_id, children, disk.size)
1408
    r_dev.SetSyncSpeed(constants.SYNC_SPEED)
1409
    result = r_dev
1410
    if as_primary or disk.OpenOnSecondary():
1411
      r_dev.Open()
1412
    DevCacheManager.UpdateCache(r_dev.dev_path, owner,
1413
                                as_primary, disk.iv_name)
1414

    
1415
  else:
1416
    result = True
1417
  return result
1418

    
1419

    
1420
def BlockdevAssemble(disk, owner, as_primary):
1421
  """Activate a block device for an instance.
1422

1423
  This is a wrapper over _RecursiveAssembleBD.
1424

1425
  @rtype: str or boolean
1426
  @return: a C{/dev/...} path for primary nodes, and
1427
      C{True} for secondary nodes
1428

1429
  """
1430
  try:
1431
    result = _RecursiveAssembleBD(disk, owner, as_primary)
1432
    if isinstance(result, bdev.BlockDev):
1433
      # pylint: disable-msg=E1103
1434
      result = result.dev_path
1435
  except errors.BlockDeviceError, err:
1436
    _Fail("Error while assembling disk: %s", err, exc=True)
1437

    
1438
  return result
1439

    
1440

    
1441
def BlockdevShutdown(disk):
1442
  """Shut down a block device.
1443

1444
  First, if the device is assembled (Attach() is successful), then
1445
  the device is shutdown. Then the children of the device are
1446
  shutdown.
1447

1448
  This function is called recursively. Note that we don't cache the
1449
  children or such, as oppossed to assemble, shutdown of different
1450
  devices doesn't require that the upper device was active.
1451

1452
  @type disk: L{objects.Disk}
1453
  @param disk: the description of the disk we should
1454
      shutdown
1455
  @rtype: None
1456

1457
  """
1458
  msgs = []
1459
  r_dev = _RecursiveFindBD(disk)
1460
  if r_dev is not None:
1461
    r_path = r_dev.dev_path
1462
    try:
1463
      r_dev.Shutdown()
1464
      DevCacheManager.RemoveCache(r_path)
1465
    except errors.BlockDeviceError, err:
1466
      msgs.append(str(err))
1467

    
1468
  if disk.children:
1469
    for child in disk.children:
1470
      try:
1471
        BlockdevShutdown(child)
1472
      except RPCFail, err:
1473
        msgs.append(str(err))
1474

    
1475
  if msgs:
1476
    _Fail("; ".join(msgs))
1477

    
1478

    
1479
def BlockdevAddchildren(parent_cdev, new_cdevs):
1480
  """Extend a mirrored block device.
1481

1482
  @type parent_cdev: L{objects.Disk}
1483
  @param parent_cdev: the disk to which we should add children
1484
  @type new_cdevs: list of L{objects.Disk}
1485
  @param new_cdevs: the list of children which we should add
1486
  @rtype: None
1487

1488
  """
1489
  parent_bdev = _RecursiveFindBD(parent_cdev)
1490
  if parent_bdev is None:
1491
    _Fail("Can't find parent device '%s' in add children", parent_cdev)
1492
  new_bdevs = [_RecursiveFindBD(disk) for disk in new_cdevs]
1493
  if new_bdevs.count(None) > 0:
1494
    _Fail("Can't find new device(s) to add: %s:%s", new_bdevs, new_cdevs)
1495
  parent_bdev.AddChildren(new_bdevs)
1496

    
1497

    
1498
def BlockdevRemovechildren(parent_cdev, new_cdevs):
1499
  """Shrink a mirrored block device.
1500

1501
  @type parent_cdev: L{objects.Disk}
1502
  @param parent_cdev: the disk from which we should remove children
1503
  @type new_cdevs: list of L{objects.Disk}
1504
  @param new_cdevs: the list of children which we should remove
1505
  @rtype: None
1506

1507
  """
1508
  parent_bdev = _RecursiveFindBD(parent_cdev)
1509
  if parent_bdev is None:
1510
    _Fail("Can't find parent device '%s' in remove children", parent_cdev)
1511
  devs = []
1512
  for disk in new_cdevs:
1513
    rpath = disk.StaticDevPath()
1514
    if rpath is None:
1515
      bd = _RecursiveFindBD(disk)
1516
      if bd is None:
1517
        _Fail("Can't find device %s while removing children", disk)
1518
      else:
1519
        devs.append(bd.dev_path)
1520
    else:
1521
      if not utils.IsNormAbsPath(rpath):
1522
        _Fail("Strange path returned from StaticDevPath: '%s'", rpath)
1523
      devs.append(rpath)
1524
  parent_bdev.RemoveChildren(devs)
1525

    
1526

    
1527
def BlockdevGetmirrorstatus(disks):
1528
  """Get the mirroring status of a list of devices.
1529

1530
  @type disks: list of L{objects.Disk}
1531
  @param disks: the list of disks which we should query
1532
  @rtype: disk
1533
  @return:
1534
      a list of (mirror_done, estimated_time) tuples, which
1535
      are the result of L{bdev.BlockDev.CombinedSyncStatus}
1536
  @raise errors.BlockDeviceError: if any of the disks cannot be
1537
      found
1538

1539
  """
1540
  stats = []
1541
  for dsk in disks:
1542
    rbd = _RecursiveFindBD(dsk)
1543
    if rbd is None:
1544
      _Fail("Can't find device %s", dsk)
1545

    
1546
    stats.append(rbd.CombinedSyncStatus())
1547

    
1548
  return stats
1549

    
1550

    
1551
def _RecursiveFindBD(disk):
1552
  """Check if a device is activated.
1553

1554
  If so, return information about the real device.
1555

1556
  @type disk: L{objects.Disk}
1557
  @param disk: the disk object we need to find
1558

1559
  @return: None if the device can't be found,
1560
      otherwise the device instance
1561

1562
  """
1563
  children = []
1564
  if disk.children:
1565
    for chdisk in disk.children:
1566
      children.append(_RecursiveFindBD(chdisk))
1567

    
1568
  return bdev.FindDevice(disk.dev_type, disk.physical_id, children, disk.size)
1569

    
1570

    
1571
def _OpenRealBD(disk):
1572
  """Opens the underlying block device of a disk.
1573

1574
  @type disk: L{objects.Disk}
1575
  @param disk: the disk object we want to open
1576

1577
  """
1578
  real_disk = _RecursiveFindBD(disk)
1579
  if real_disk is None:
1580
    _Fail("Block device '%s' is not set up", disk)
1581

    
1582
  real_disk.Open()
1583

    
1584
  return real_disk
1585

    
1586

    
1587
def BlockdevFind(disk):
1588
  """Check if a device is activated.
1589

1590
  If it is, return information about the real device.
1591

1592
  @type disk: L{objects.Disk}
1593
  @param disk: the disk to find
1594
  @rtype: None or objects.BlockDevStatus
1595
  @return: None if the disk cannot be found, otherwise a the current
1596
           information
1597

1598
  """
1599
  try:
1600
    rbd = _RecursiveFindBD(disk)
1601
  except errors.BlockDeviceError, err:
1602
    _Fail("Failed to find device: %s", err, exc=True)
1603

    
1604
  if rbd is None:
1605
    return None
1606

    
1607
  return rbd.GetSyncStatus()
1608

    
1609

    
1610
def BlockdevGetsize(disks):
1611
  """Computes the size of the given disks.
1612

1613
  If a disk is not found, returns None instead.
1614

1615
  @type disks: list of L{objects.Disk}
1616
  @param disks: the list of disk to compute the size for
1617
  @rtype: list
1618
  @return: list with elements None if the disk cannot be found,
1619
      otherwise the size
1620

1621
  """
1622
  result = []
1623
  for cf in disks:
1624
    try:
1625
      rbd = _RecursiveFindBD(cf)
1626
    except errors.BlockDeviceError:
1627
      result.append(None)
1628
      continue
1629
    if rbd is None:
1630
      result.append(None)
1631
    else:
1632
      result.append(rbd.GetActualSize())
1633
  return result
1634

    
1635

    
1636
def BlockdevExport(disk, dest_node, dest_path, cluster_name):
1637
  """Export a block device to a remote node.
1638

1639
  @type disk: L{objects.Disk}
1640
  @param disk: the description of the disk to export
1641
  @type dest_node: str
1642
  @param dest_node: the destination node to export to
1643
  @type dest_path: str
1644
  @param dest_path: the destination path on the target node
1645
  @type cluster_name: str
1646
  @param cluster_name: the cluster name, needed for SSH hostalias
1647
  @rtype: None
1648

1649
  """
1650
  real_disk = _OpenRealBD(disk)
1651

    
1652
  # the block size on the read dd is 1MiB to match our units
1653
  expcmd = utils.BuildShellCmd("set -e; set -o pipefail; "
1654
                               "dd if=%s bs=1048576 count=%s",
1655
                               real_disk.dev_path, str(disk.size))
1656

    
1657
  # we set here a smaller block size as, due to ssh buffering, more
1658
  # than 64-128k will mostly ignored; we use nocreat to fail if the
1659
  # device is not already there or we pass a wrong path; we use
1660
  # notrunc to no attempt truncate on an LV device; we use oflag=dsync
1661
  # to not buffer too much memory; this means that at best, we flush
1662
  # every 64k, which will not be very fast
1663
  destcmd = utils.BuildShellCmd("dd of=%s conv=nocreat,notrunc bs=65536"
1664
                                " oflag=dsync", dest_path)
1665

    
1666
  remotecmd = _GetSshRunner(cluster_name).BuildCmd(dest_node,
1667
                                                   constants.GANETI_RUNAS,
1668
                                                   destcmd)
1669

    
1670
  # all commands have been checked, so we're safe to combine them
1671
  command = '|'.join([expcmd, utils.ShellQuoteArgs(remotecmd)])
1672

    
1673
  result = utils.RunCmd(["bash", "-c", command])
1674

    
1675
  if result.failed:
1676
    _Fail("Disk copy command '%s' returned error: %s"
1677
          " output: %s", command, result.fail_reason, result.output)
1678

    
1679

    
1680
def UploadFile(file_name, data, mode, uid, gid, atime, mtime):
1681
  """Write a file to the filesystem.
1682

1683
  This allows the master to overwrite(!) a file. It will only perform
1684
  the operation if the file belongs to a list of configuration files.
1685

1686
  @type file_name: str
1687
  @param file_name: the target file name
1688
  @type data: str
1689
  @param data: the new contents of the file
1690
  @type mode: int
1691
  @param mode: the mode to give the file (can be None)
1692
  @type uid: int
1693
  @param uid: the owner of the file (can be -1 for default)
1694
  @type gid: int
1695
  @param gid: the group of the file (can be -1 for default)
1696
  @type atime: float
1697
  @param atime: the atime to set on the file (can be None)
1698
  @type mtime: float
1699
  @param mtime: the mtime to set on the file (can be None)
1700
  @rtype: None
1701

1702
  """
1703
  if not os.path.isabs(file_name):
1704
    _Fail("Filename passed to UploadFile is not absolute: '%s'", file_name)
1705

    
1706
  if file_name not in _ALLOWED_UPLOAD_FILES:
1707
    _Fail("Filename passed to UploadFile not in allowed upload targets: '%s'",
1708
          file_name)
1709

    
1710
  raw_data = _Decompress(data)
1711

    
1712
  utils.WriteFile(file_name, data=raw_data, mode=mode, uid=uid, gid=gid,
1713
                  atime=atime, mtime=mtime)
1714

    
1715

    
1716
def WriteSsconfFiles(values):
1717
  """Update all ssconf files.
1718

1719
  Wrapper around the SimpleStore.WriteFiles.
1720

1721
  """
1722
  ssconf.SimpleStore().WriteFiles(values)
1723

    
1724

    
1725
def _ErrnoOrStr(err):
1726
  """Format an EnvironmentError exception.
1727

1728
  If the L{err} argument has an errno attribute, it will be looked up
1729
  and converted into a textual C{E...} description. Otherwise the
1730
  string representation of the error will be returned.
1731

1732
  @type err: L{EnvironmentError}
1733
  @param err: the exception to format
1734

1735
  """
1736
  if hasattr(err, 'errno'):
1737
    detail = errno.errorcode[err.errno]
1738
  else:
1739
    detail = str(err)
1740
  return detail
1741

    
1742

    
1743
def _OSOndiskAPIVersion(os_dir):
1744
  """Compute and return the API version of a given OS.
1745

1746
  This function will try to read the API version of the OS residing in
1747
  the 'os_dir' directory.
1748

1749
  @type os_dir: str
1750
  @param os_dir: the directory in which we should look for the OS
1751
  @rtype: tuple
1752
  @return: tuple (status, data) with status denoting the validity and
1753
      data holding either the vaid versions or an error message
1754

1755
  """
1756
  api_file = utils.PathJoin(os_dir, constants.OS_API_FILE)
1757

    
1758
  try:
1759
    st = os.stat(api_file)
1760
  except EnvironmentError, err:
1761
    return False, ("Required file '%s' not found under path %s: %s" %
1762
                   (constants.OS_API_FILE, os_dir, _ErrnoOrStr(err)))
1763

    
1764
  if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1765
    return False, ("File '%s' in %s is not a regular file" %
1766
                   (constants.OS_API_FILE, os_dir))
1767

    
1768
  try:
1769
    api_versions = utils.ReadFile(api_file).splitlines()
1770
  except EnvironmentError, err:
1771
    return False, ("Error while reading the API version file at %s: %s" %
1772
                   (api_file, _ErrnoOrStr(err)))
1773

    
1774
  try:
1775
    api_versions = [int(version.strip()) for version in api_versions]
1776
  except (TypeError, ValueError), err:
1777
    return False, ("API version(s) can't be converted to integer: %s" %
1778
                   str(err))
1779

    
1780
  return True, api_versions
1781

    
1782

    
1783
def DiagnoseOS(top_dirs=None):
1784
  """Compute the validity for all OSes.
1785

1786
  @type top_dirs: list
1787
  @param top_dirs: the list of directories in which to
1788
      search (if not given defaults to
1789
      L{constants.OS_SEARCH_PATH})
1790
  @rtype: list of L{objects.OS}
1791
  @return: a list of tuples (name, path, status, diagnose, variants,
1792
      parameters, api_version) for all (potential) OSes under all
1793
      search paths, where:
1794
          - name is the (potential) OS name
1795
          - path is the full path to the OS
1796
          - status True/False is the validity of the OS
1797
          - diagnose is the error message for an invalid OS, otherwise empty
1798
          - variants is a list of supported OS variants, if any
1799
          - parameters is a list of (name, help) parameters, if any
1800
          - api_version is a list of support OS API versions
1801

1802
  """
1803
  if top_dirs is None:
1804
    top_dirs = constants.OS_SEARCH_PATH
1805

    
1806
  result = []
1807
  for dir_name in top_dirs:
1808
    if os.path.isdir(dir_name):
1809
      try:
1810
        f_names = utils.ListVisibleFiles(dir_name)
1811
      except EnvironmentError, err:
1812
        logging.exception("Can't list the OS directory %s: %s", dir_name, err)
1813
        break
1814
      for name in f_names:
1815
        os_path = utils.PathJoin(dir_name, name)
1816
        status, os_inst = _TryOSFromDisk(name, base_dir=dir_name)
1817
        if status:
1818
          diagnose = ""
1819
          variants = os_inst.supported_variants
1820
          parameters = os_inst.supported_parameters
1821
          api_versions = os_inst.api_versions
1822
        else:
1823
          diagnose = os_inst
1824
          variants = parameters = api_versions = []
1825
        result.append((name, os_path, status, diagnose, variants,
1826
                       parameters, api_versions))
1827

    
1828
  return result
1829

    
1830

    
1831
def _TryOSFromDisk(name, base_dir=None):
1832
  """Create an OS instance from disk.
1833

1834
  This function will return an OS instance if the given name is a
1835
  valid OS name.
1836

1837
  @type base_dir: string
1838
  @keyword base_dir: Base directory containing OS installations.
1839
                     Defaults to a search in all the OS_SEARCH_PATH dirs.
1840
  @rtype: tuple
1841
  @return: success and either the OS instance if we find a valid one,
1842
      or error message
1843

1844
  """
1845
  if base_dir is None:
1846
    os_dir = utils.FindFile(name, constants.OS_SEARCH_PATH, os.path.isdir)
1847
  else:
1848
    os_dir = utils.FindFile(name, [base_dir], os.path.isdir)
1849

    
1850
  if os_dir is None:
1851
    return False, "Directory for OS %s not found in search path" % name
1852

    
1853
  status, api_versions = _OSOndiskAPIVersion(os_dir)
1854
  if not status:
1855
    # push the error up
1856
    return status, api_versions
1857

    
1858
  if not constants.OS_API_VERSIONS.intersection(api_versions):
1859
    return False, ("API version mismatch for path '%s': found %s, want %s." %
1860
                   (os_dir, api_versions, constants.OS_API_VERSIONS))
1861

    
1862
  # OS Files dictionary, we will populate it with the absolute path names
1863
  os_files = dict.fromkeys(constants.OS_SCRIPTS)
1864

    
1865
  if max(api_versions) >= constants.OS_API_V15:
1866
    os_files[constants.OS_VARIANTS_FILE] = ''
1867

    
1868
  if max(api_versions) >= constants.OS_API_V20:
1869
    os_files[constants.OS_PARAMETERS_FILE] = ''
1870
  else:
1871
    del os_files[constants.OS_SCRIPT_VERIFY]
1872

    
1873
  for filename in os_files:
1874
    os_files[filename] = utils.PathJoin(os_dir, filename)
1875

    
1876
    try:
1877
      st = os.stat(os_files[filename])
1878
    except EnvironmentError, err:
1879
      return False, ("File '%s' under path '%s' is missing (%s)" %
1880
                     (filename, os_dir, _ErrnoOrStr(err)))
1881

    
1882
    if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1883
      return False, ("File '%s' under path '%s' is not a regular file" %
1884
                     (filename, os_dir))
1885

    
1886
    if filename in constants.OS_SCRIPTS:
1887
      if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
1888
        return False, ("File '%s' under path '%s' is not executable" %
1889
                       (filename, os_dir))
1890

    
1891
  variants = []
1892
  if constants.OS_VARIANTS_FILE in os_files:
1893
    variants_file = os_files[constants.OS_VARIANTS_FILE]
1894
    try:
1895
      variants = utils.ReadFile(variants_file).splitlines()
1896
    except EnvironmentError, err:
1897
      return False, ("Error while reading the OS variants file at %s: %s" %
1898
                     (variants_file, _ErrnoOrStr(err)))
1899
    if not variants:
1900
      return False, ("No supported os variant found")
1901

    
1902
  parameters = []
1903
  if constants.OS_PARAMETERS_FILE in os_files:
1904
    parameters_file = os_files[constants.OS_PARAMETERS_FILE]
1905
    try:
1906
      parameters = utils.ReadFile(parameters_file).splitlines()
1907
    except EnvironmentError, err:
1908
      return False, ("Error while reading the OS parameters file at %s: %s" %
1909
                     (parameters_file, _ErrnoOrStr(err)))
1910
    parameters = [v.split(None, 1) for v in parameters]
1911

    
1912
  os_obj = objects.OS(name=name, path=os_dir,
1913
                      create_script=os_files[constants.OS_SCRIPT_CREATE],
1914
                      export_script=os_files[constants.OS_SCRIPT_EXPORT],
1915
                      import_script=os_files[constants.OS_SCRIPT_IMPORT],
1916
                      rename_script=os_files[constants.OS_SCRIPT_RENAME],
1917
                      verify_script=os_files.get(constants.OS_SCRIPT_VERIFY,
1918
                                                 None),
1919
                      supported_variants=variants,
1920
                      supported_parameters=parameters,
1921
                      api_versions=api_versions)
1922
  return True, os_obj
1923

    
1924

    
1925
def OSFromDisk(name, base_dir=None):
1926
  """Create an OS instance from disk.
1927

1928
  This function will return an OS instance if the given name is a
1929
  valid OS name. Otherwise, it will raise an appropriate
1930
  L{RPCFail} exception, detailing why this is not a valid OS.
1931

1932
  This is just a wrapper over L{_TryOSFromDisk}, which doesn't raise
1933
  an exception but returns true/false status data.
1934

1935
  @type base_dir: string
1936
  @keyword base_dir: Base directory containing OS installations.
1937
                     Defaults to a search in all the OS_SEARCH_PATH dirs.
1938
  @rtype: L{objects.OS}
1939
  @return: the OS instance if we find a valid one
1940
  @raise RPCFail: if we don't find a valid OS
1941

1942
  """
1943
  name_only = name.split("+", 1)[0]
1944
  status, payload = _TryOSFromDisk(name_only, base_dir)
1945

    
1946
  if not status:
1947
    _Fail(payload)
1948

    
1949
  return payload
1950

    
1951

    
1952
def OSCoreEnv(inst_os, os_params, debug=0):
1953
  """Calculate the basic environment for an os script.
1954

1955
  @type inst_os: L{objects.OS}
1956
  @param inst_os: operating system for which the environment is being built
1957
  @type os_params: dict
1958
  @param os_params: the OS parameters
1959
  @type debug: integer
1960
  @param debug: debug level (0 or 1, for OS Api 10)
1961
  @rtype: dict
1962
  @return: dict of environment variables
1963
  @raise errors.BlockDeviceError: if the block device
1964
      cannot be found
1965

1966
  """
1967
  result = {}
1968
  api_version = \
1969
    max(constants.OS_API_VERSIONS.intersection(inst_os.api_versions))
1970
  result['OS_API_VERSION'] = '%d' % api_version
1971
  result['OS_NAME'] = inst_os.name
1972
  result['DEBUG_LEVEL'] = '%d' % debug
1973

    
1974
  # OS variants
1975
  if api_version >= constants.OS_API_V15:
1976
    try:
1977
      variant = inst_os.name.split('+', 1)[1]
1978
    except IndexError:
1979
      variant = inst_os.supported_variants[0]
1980
    result['OS_VARIANT'] = variant
1981

    
1982
  # OS params
1983
  for pname, pvalue in os_params.items():
1984
    result['OSP_%s' % pname.upper()] = pvalue
1985

    
1986
  return result
1987

    
1988

    
1989
def OSEnvironment(instance, inst_os, debug=0):
1990
  """Calculate the environment for an os script.
1991

1992
  @type instance: L{objects.Instance}
1993
  @param instance: target instance for the os script run
1994
  @type inst_os: L{objects.OS}
1995
  @param inst_os: operating system for which the environment is being built
1996
  @type debug: integer
1997
  @param debug: debug level (0 or 1, for OS Api 10)
1998
  @rtype: dict
1999
  @return: dict of environment variables
2000
  @raise errors.BlockDeviceError: if the block device
2001
      cannot be found
2002

2003
  """
2004
  result = OSCoreEnv(inst_os, instance.osparams, debug=debug)
2005

    
2006
  result['INSTANCE_NAME'] = instance.name
2007
  result['INSTANCE_OS'] = instance.os
2008
  result['HYPERVISOR'] = instance.hypervisor
2009
  result['DISK_COUNT'] = '%d' % len(instance.disks)
2010
  result['NIC_COUNT'] = '%d' % len(instance.nics)
2011

    
2012
  # Disks
2013
  for idx, disk in enumerate(instance.disks):
2014
    real_disk = _OpenRealBD(disk)
2015
    result['DISK_%d_PATH' % idx] = real_disk.dev_path
2016
    result['DISK_%d_ACCESS' % idx] = disk.mode
2017
    if constants.HV_DISK_TYPE in instance.hvparams:
2018
      result['DISK_%d_FRONTEND_TYPE' % idx] = \
2019
        instance.hvparams[constants.HV_DISK_TYPE]
2020
    if disk.dev_type in constants.LDS_BLOCK:
2021
      result['DISK_%d_BACKEND_TYPE' % idx] = 'block'
2022
    elif disk.dev_type == constants.LD_FILE:
2023
      result['DISK_%d_BACKEND_TYPE' % idx] = \
2024
        'file:%s' % disk.physical_id[0]
2025

    
2026
  # NICs
2027
  for idx, nic in enumerate(instance.nics):
2028
    result['NIC_%d_MAC' % idx] = nic.mac
2029
    if nic.ip:
2030
      result['NIC_%d_IP' % idx] = nic.ip
2031
    result['NIC_%d_MODE' % idx] = nic.nicparams[constants.NIC_MODE]
2032
    if nic.nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED:
2033
      result['NIC_%d_BRIDGE' % idx] = nic.nicparams[constants.NIC_LINK]
2034
    if nic.nicparams[constants.NIC_LINK]:
2035
      result['NIC_%d_LINK' % idx] = nic.nicparams[constants.NIC_LINK]
2036
    if constants.HV_NIC_TYPE in instance.hvparams:
2037
      result['NIC_%d_FRONTEND_TYPE' % idx] = \
2038
        instance.hvparams[constants.HV_NIC_TYPE]
2039

    
2040
  # HV/BE params
2041
  for source, kind in [(instance.beparams, "BE"), (instance.hvparams, "HV")]:
2042
    for key, value in source.items():
2043
      result["INSTANCE_%s_%s" % (kind, key)] = str(value)
2044

    
2045
  return result
2046

    
2047

    
2048
def BlockdevGrow(disk, amount):
2049
  """Grow a stack of block devices.
2050

2051
  This function is called recursively, with the childrens being the
2052
  first ones to resize.
2053

2054
  @type disk: L{objects.Disk}
2055
  @param disk: the disk to be grown
2056
  @rtype: (status, result)
2057
  @return: a tuple with the status of the operation
2058
      (True/False), and the errors message if status
2059
      is False
2060

2061
  """
2062
  r_dev = _RecursiveFindBD(disk)
2063
  if r_dev is None:
2064
    _Fail("Cannot find block device %s", disk)
2065

    
2066
  try:
2067
    r_dev.Grow(amount)
2068
  except errors.BlockDeviceError, err:
2069
    _Fail("Failed to grow block device: %s", err, exc=True)
2070

    
2071

    
2072
def BlockdevSnapshot(disk):
2073
  """Create a snapshot copy of a block device.
2074

2075
  This function is called recursively, and the snapshot is actually created
2076
  just for the leaf lvm backend device.
2077

2078
  @type disk: L{objects.Disk}
2079
  @param disk: the disk to be snapshotted
2080
  @rtype: string
2081
  @return: snapshot disk path
2082

2083
  """
2084
  if disk.dev_type == constants.LD_DRBD8:
2085
    if not disk.children:
2086
      _Fail("DRBD device '%s' without backing storage cannot be snapshotted",
2087
            disk.unique_id)
2088
    return BlockdevSnapshot(disk.children[0])
2089
  elif disk.dev_type == constants.LD_LV:
2090
    r_dev = _RecursiveFindBD(disk)
2091
    if r_dev is not None:
2092
      # FIXME: choose a saner value for the snapshot size
2093
      # let's stay on the safe side and ask for the full size, for now
2094
      return r_dev.Snapshot(disk.size)
2095
    else:
2096
      _Fail("Cannot find block device %s", disk)
2097
  else:
2098
    _Fail("Cannot snapshot non-lvm block device '%s' of type '%s'",
2099
          disk.unique_id, disk.dev_type)
2100

    
2101

    
2102
def FinalizeExport(instance, snap_disks):
2103
  """Write out the export configuration information.
2104

2105
  @type instance: L{objects.Instance}
2106
  @param instance: the instance which we export, used for
2107
      saving configuration
2108
  @type snap_disks: list of L{objects.Disk}
2109
  @param snap_disks: list of snapshot block devices, which
2110
      will be used to get the actual name of the dump file
2111

2112
  @rtype: None
2113

2114
  """
2115
  destdir = utils.PathJoin(constants.EXPORT_DIR, instance.name + ".new")
2116
  finaldestdir = utils.PathJoin(constants.EXPORT_DIR, instance.name)
2117

    
2118
  config = objects.SerializableConfigParser()
2119

    
2120
  config.add_section(constants.INISECT_EXP)
2121
  config.set(constants.INISECT_EXP, 'version', '0')
2122
  config.set(constants.INISECT_EXP, 'timestamp', '%d' % int(time.time()))
2123
  config.set(constants.INISECT_EXP, 'source', instance.primary_node)
2124
  config.set(constants.INISECT_EXP, 'os', instance.os)
2125
  config.set(constants.INISECT_EXP, 'compression', 'gzip')
2126

    
2127
  config.add_section(constants.INISECT_INS)
2128
  config.set(constants.INISECT_INS, 'name', instance.name)
2129
  config.set(constants.INISECT_INS, 'memory', '%d' %
2130
             instance.beparams[constants.BE_MEMORY])
2131
  config.set(constants.INISECT_INS, 'vcpus', '%d' %
2132
             instance.beparams[constants.BE_VCPUS])
2133
  config.set(constants.INISECT_INS, 'disk_template', instance.disk_template)
2134
  config.set(constants.INISECT_INS, 'hypervisor', instance.hypervisor)
2135

    
2136
  nic_total = 0
2137
  for nic_count, nic in enumerate(instance.nics):
2138
    nic_total += 1
2139
    config.set(constants.INISECT_INS, 'nic%d_mac' %
2140
               nic_count, '%s' % nic.mac)
2141
    config.set(constants.INISECT_INS, 'nic%d_ip' % nic_count, '%s' % nic.ip)
2142
    for param in constants.NICS_PARAMETER_TYPES:
2143
      config.set(constants.INISECT_INS, 'nic%d_%s' % (nic_count, param),
2144
                 '%s' % nic.nicparams.get(param, None))
2145
  # TODO: redundant: on load can read nics until it doesn't exist
2146
  config.set(constants.INISECT_INS, 'nic_count' , '%d' % nic_total)
2147

    
2148
  disk_total = 0
2149
  for disk_count, disk in enumerate(snap_disks):
2150
    if disk:
2151
      disk_total += 1
2152
      config.set(constants.INISECT_INS, 'disk%d_ivname' % disk_count,
2153
                 ('%s' % disk.iv_name))
2154
      config.set(constants.INISECT_INS, 'disk%d_dump' % disk_count,
2155
                 ('%s' % disk.physical_id[1]))
2156
      config.set(constants.INISECT_INS, 'disk%d_size' % disk_count,
2157
                 ('%d' % disk.size))
2158

    
2159
  config.set(constants.INISECT_INS, 'disk_count' , '%d' % disk_total)
2160

    
2161
  # New-style hypervisor/backend parameters
2162

    
2163
  config.add_section(constants.INISECT_HYP)
2164
  for name, value in instance.hvparams.items():
2165
    if name not in constants.HVC_GLOBALS:
2166
      config.set(constants.INISECT_HYP, name, str(value))
2167

    
2168
  config.add_section(constants.INISECT_BEP)
2169
  for name, value in instance.beparams.items():
2170
    config.set(constants.INISECT_BEP, name, str(value))
2171

    
2172
  config.add_section(constants.INISECT_OSP)
2173
  for name, value in instance.osparams.items():
2174
    config.set(constants.INISECT_OSP, name, str(value))
2175

    
2176
  utils.WriteFile(utils.PathJoin(destdir, constants.EXPORT_CONF_FILE),
2177
                  data=config.Dumps())
2178
  shutil.rmtree(finaldestdir, ignore_errors=True)
2179
  shutil.move(destdir, finaldestdir)
2180

    
2181

    
2182
def ExportInfo(dest):
2183
  """Get export configuration information.
2184

2185
  @type dest: str
2186
  @param dest: directory containing the export
2187

2188
  @rtype: L{objects.SerializableConfigParser}
2189
  @return: a serializable config file containing the
2190
      export info
2191

2192
  """
2193
  cff = utils.PathJoin(dest, constants.EXPORT_CONF_FILE)
2194

    
2195
  config = objects.SerializableConfigParser()
2196
  config.read(cff)
2197

    
2198
  if (not config.has_section(constants.INISECT_EXP) or
2199
      not config.has_section(constants.INISECT_INS)):
2200
    _Fail("Export info file doesn't have the required fields")
2201

    
2202
  return config.Dumps()
2203

    
2204

    
2205
def ListExports():
2206
  """Return a list of exports currently available on this machine.
2207

2208
  @rtype: list
2209
  @return: list of the exports
2210

2211
  """
2212
  if os.path.isdir(constants.EXPORT_DIR):
2213
    return sorted(utils.ListVisibleFiles(constants.EXPORT_DIR))
2214
  else:
2215
    _Fail("No exports directory")
2216

    
2217

    
2218
def RemoveExport(export):
2219
  """Remove an existing export from the node.
2220

2221
  @type export: str
2222
  @param export: the name of the export to remove
2223
  @rtype: None
2224

2225
  """
2226
  target = utils.PathJoin(constants.EXPORT_DIR, export)
2227

    
2228
  try:
2229
    shutil.rmtree(target)
2230
  except EnvironmentError, err:
2231
    _Fail("Error while removing the export: %s", err, exc=True)
2232

    
2233

    
2234
def BlockdevRename(devlist):
2235
  """Rename a list of block devices.
2236

2237
  @type devlist: list of tuples
2238
  @param devlist: list of tuples of the form  (disk,
2239
      new_logical_id, new_physical_id); disk is an
2240
      L{objects.Disk} object describing the current disk,
2241
      and new logical_id/physical_id is the name we
2242
      rename it to
2243
  @rtype: boolean
2244
  @return: True if all renames succeeded, False otherwise
2245

2246
  """
2247
  msgs = []
2248
  result = True
2249
  for disk, unique_id in devlist:
2250
    dev = _RecursiveFindBD(disk)
2251
    if dev is None:
2252
      msgs.append("Can't find device %s in rename" % str(disk))
2253
      result = False
2254
      continue
2255
    try:
2256
      old_rpath = dev.dev_path
2257
      dev.Rename(unique_id)
2258
      new_rpath = dev.dev_path
2259
      if old_rpath != new_rpath:
2260
        DevCacheManager.RemoveCache(old_rpath)
2261
        # FIXME: we should add the new cache information here, like:
2262
        # DevCacheManager.UpdateCache(new_rpath, owner, ...)
2263
        # but we don't have the owner here - maybe parse from existing
2264
        # cache? for now, we only lose lvm data when we rename, which
2265
        # is less critical than DRBD or MD
2266
    except errors.BlockDeviceError, err:
2267
      msgs.append("Can't rename device '%s' to '%s': %s" %
2268
                  (dev, unique_id, err))
2269
      logging.exception("Can't rename device '%s' to '%s'", dev, unique_id)
2270
      result = False
2271
  if not result:
2272
    _Fail("; ".join(msgs))
2273

    
2274

    
2275
def _TransformFileStorageDir(file_storage_dir):
2276
  """Checks whether given file_storage_dir is valid.
2277

2278
  Checks wheter the given file_storage_dir is within the cluster-wide
2279
  default file_storage_dir stored in SimpleStore. Only paths under that
2280
  directory are allowed.
2281

2282
  @type file_storage_dir: str
2283
  @param file_storage_dir: the path to check
2284

2285
  @return: the normalized path if valid, None otherwise
2286

2287
  """
2288
  if not constants.ENABLE_FILE_STORAGE:
2289
    _Fail("File storage disabled at configure time")
2290
  cfg = _GetConfig()
2291
  file_storage_dir = os.path.normpath(file_storage_dir)
2292
  base_file_storage_dir = cfg.GetFileStorageDir()
2293
  if (os.path.commonprefix([file_storage_dir, base_file_storage_dir]) !=
2294
      base_file_storage_dir):
2295
    _Fail("File storage directory '%s' is not under base file"
2296
          " storage directory '%s'", file_storage_dir, base_file_storage_dir)
2297
  return file_storage_dir
2298

    
2299

    
2300
def CreateFileStorageDir(file_storage_dir):
2301
  """Create file storage directory.
2302

2303
  @type file_storage_dir: str
2304
  @param file_storage_dir: directory to create
2305

2306
  @rtype: tuple
2307
  @return: tuple with first element a boolean indicating wheter dir
2308
      creation was successful or not
2309

2310
  """
2311
  file_storage_dir = _TransformFileStorageDir(file_storage_dir)
2312
  if os.path.exists(file_storage_dir):
2313
    if not os.path.isdir(file_storage_dir):
2314
      _Fail("Specified storage dir '%s' is not a directory",
2315
            file_storage_dir)
2316
  else:
2317
    try:
2318
      os.makedirs(file_storage_dir, 0750)
2319
    except OSError, err:
2320
      _Fail("Cannot create file storage directory '%s': %s",
2321
            file_storage_dir, err, exc=True)
2322

    
2323

    
2324
def RemoveFileStorageDir(file_storage_dir):
2325
  """Remove file storage directory.
2326

2327
  Remove it only if it's empty. If not log an error and return.
2328

2329
  @type file_storage_dir: str
2330
  @param file_storage_dir: the directory we should cleanup
2331
  @rtype: tuple (success,)
2332
  @return: tuple of one element, C{success}, denoting
2333
      whether the operation was successful
2334

2335
  """
2336
  file_storage_dir = _TransformFileStorageDir(file_storage_dir)
2337
  if os.path.exists(file_storage_dir):
2338
    if not os.path.isdir(file_storage_dir):
2339
      _Fail("Specified Storage directory '%s' is not a directory",
2340
            file_storage_dir)
2341
    # deletes dir only if empty, otherwise we want to fail the rpc call
2342
    try:
2343
      os.rmdir(file_storage_dir)
2344
    except OSError, err:
2345
      _Fail("Cannot remove file storage directory '%s': %s",
2346
            file_storage_dir, err)
2347

    
2348

    
2349
def RenameFileStorageDir(old_file_storage_dir, new_file_storage_dir):
2350
  """Rename the file storage directory.
2351

2352
  @type old_file_storage_dir: str
2353
  @param old_file_storage_dir: the current path
2354
  @type new_file_storage_dir: str
2355
  @param new_file_storage_dir: the name we should rename to
2356
  @rtype: tuple (success,)
2357
  @return: tuple of one element, C{success}, denoting
2358
      whether the operation was successful
2359

2360
  """
2361
  old_file_storage_dir = _TransformFileStorageDir(old_file_storage_dir)
2362
  new_file_storage_dir = _TransformFileStorageDir(new_file_storage_dir)
2363
  if not os.path.exists(new_file_storage_dir):
2364
    if os.path.isdir(old_file_storage_dir):
2365
      try:
2366
        os.rename(old_file_storage_dir, new_file_storage_dir)
2367
      except OSError, err:
2368
        _Fail("Cannot rename '%s' to '%s': %s",
2369
              old_file_storage_dir, new_file_storage_dir, err)
2370
    else:
2371
      _Fail("Specified storage dir '%s' is not a directory",
2372
            old_file_storage_dir)
2373
  else:
2374
    if os.path.exists(old_file_storage_dir):
2375
      _Fail("Cannot rename '%s' to '%s': both locations exist",
2376
            old_file_storage_dir, new_file_storage_dir)
2377

    
2378

    
2379
def _EnsureJobQueueFile(file_name):
2380
  """Checks whether the given filename is in the queue directory.
2381

2382
  @type file_name: str
2383
  @param file_name: the file name we should check
2384
  @rtype: None
2385
  @raises RPCFail: if the file is not valid
2386

2387
  """
2388
  queue_dir = os.path.normpath(constants.QUEUE_DIR)
2389
  result = (os.path.commonprefix([queue_dir, file_name]) == queue_dir)
2390

    
2391
  if not result:
2392
    _Fail("Passed job queue file '%s' does not belong to"
2393
          " the queue directory '%s'", file_name, queue_dir)
2394

    
2395

    
2396
def JobQueueUpdate(file_name, content):
2397
  """Updates a file in the queue directory.
2398

2399
  This is just a wrapper over L{utils.WriteFile}, with proper
2400
  checking.
2401

2402
  @type file_name: str
2403
  @param file_name: the job file name
2404
  @type content: str
2405
  @param content: the new job contents
2406
  @rtype: boolean
2407
  @return: the success of the operation
2408

2409
  """
2410
  _EnsureJobQueueFile(file_name)
2411

    
2412
  # Write and replace the file atomically
2413
  utils.WriteFile(file_name, data=_Decompress(content))
2414

    
2415

    
2416
def JobQueueRename(old, new):
2417
  """Renames a job queue file.
2418

2419
  This is just a wrapper over os.rename with proper checking.
2420

2421
  @type old: str
2422
  @param old: the old (actual) file name
2423
  @type new: str
2424
  @param new: the desired file name
2425
  @rtype: tuple
2426
  @return: the success of the operation and payload
2427

2428
  """
2429
  _EnsureJobQueueFile(old)
2430
  _EnsureJobQueueFile(new)
2431

    
2432
  utils.RenameFile(old, new, mkdir=True)
2433

    
2434

    
2435
def BlockdevClose(instance_name, disks):
2436
  """Closes the given block devices.
2437

2438
  This means they will be switched to secondary mode (in case of
2439
  DRBD).
2440

2441
  @param instance_name: if the argument is not empty, the symlinks
2442
      of this instance will be removed
2443
  @type disks: list of L{objects.Disk}
2444
  @param disks: the list of disks to be closed
2445
  @rtype: tuple (success, message)
2446
  @return: a tuple of success and message, where success
2447
      indicates the succes of the operation, and message
2448
      which will contain the error details in case we
2449
      failed
2450

2451
  """
2452
  bdevs = []
2453
  for cf in disks:
2454
    rd = _RecursiveFindBD(cf)
2455
    if rd is None:
2456
      _Fail("Can't find device %s", cf)
2457
    bdevs.append(rd)
2458

    
2459
  msg = []
2460
  for rd in bdevs:
2461
    try:
2462
      rd.Close()
2463
    except errors.BlockDeviceError, err:
2464
      msg.append(str(err))
2465
  if msg:
2466
    _Fail("Can't make devices secondary: %s", ",".join(msg))
2467
  else:
2468
    if instance_name:
2469
      _RemoveBlockDevLinks(instance_name, disks)
2470

    
2471

    
2472
def ValidateHVParams(hvname, hvparams):
2473
  """Validates the given hypervisor parameters.
2474

2475
  @type hvname: string
2476
  @param hvname: the hypervisor name
2477
  @type hvparams: dict
2478
  @param hvparams: the hypervisor parameters to be validated
2479
  @rtype: None
2480

2481
  """
2482
  try:
2483
    hv_type = hypervisor.GetHypervisor(hvname)
2484
    hv_type.ValidateParameters(hvparams)
2485
  except errors.HypervisorError, err:
2486
    _Fail(str(err), log=False)
2487

    
2488

    
2489
def _CheckOSPList(os_obj, parameters):
2490
  """Check whether a list of parameters is supported by the OS.
2491

2492
  @type os_obj: L{objects.OS}
2493
  @param os_obj: OS object to check
2494
  @type parameters: list
2495
  @param parameters: the list of parameters to check
2496

2497
  """
2498
  supported = [v[0] for v in os_obj.supported_parameters]
2499
  delta = frozenset(parameters).difference(supported)
2500
  if delta:
2501
    _Fail("The following parameters are not supported"
2502
          " by the OS %s: %s" % (os_obj.name, utils.CommaJoin(delta)))
2503

    
2504

    
2505
def ValidateOS(required, osname, checks, osparams):
2506
  """Validate the given OS' parameters.
2507

2508
  @type required: boolean
2509
  @param required: whether absence of the OS should translate into
2510
      failure or not
2511
  @type osname: string
2512
  @param osname: the OS to be validated
2513
  @type checks: list
2514
  @param checks: list of the checks to run (currently only 'parameters')
2515
  @type osparams: dict
2516
  @param osparams: dictionary with OS parameters
2517
  @rtype: boolean
2518
  @return: True if the validation passed, or False if the OS was not
2519
      found and L{required} was false
2520

2521
  """
2522
  if not constants.OS_VALIDATE_CALLS.issuperset(checks):
2523
    _Fail("Unknown checks required for OS %s: %s", osname,
2524
          set(checks).difference(constants.OS_VALIDATE_CALLS))
2525

    
2526
  name_only = osname.split("+", 1)[0]
2527
  status, tbv = _TryOSFromDisk(name_only, None)
2528

    
2529
  if not status:
2530
    if required:
2531
      _Fail(tbv)
2532
    else:
2533
      return False
2534

    
2535
  if max(tbv.api_versions) < constants.OS_API_V20:
2536
    return True
2537

    
2538
  if constants.OS_VALIDATE_PARAMETERS in checks:
2539
    _CheckOSPList(tbv, osparams.keys())
2540

    
2541
  validate_env = OSCoreEnv(tbv, osparams)
2542
  result = utils.RunCmd([tbv.verify_script] + checks, env=validate_env,
2543
                        cwd=tbv.path)
2544
  if result.failed:
2545
    logging.error("os validate command '%s' returned error: %s output: %s",
2546
                  result.cmd, result.fail_reason, result.output)
2547
    _Fail("OS validation script failed (%s), output: %s",
2548
          result.fail_reason, result.output, log=False)
2549

    
2550
  return True
2551

    
2552

    
2553
def DemoteFromMC():
2554
  """Demotes the current node from master candidate role.
2555

2556
  """
2557
  # try to ensure we're not the master by mistake
2558
  master, myself = ssconf.GetMasterAndMyself()
2559
  if master == myself:
2560
    _Fail("ssconf status shows I'm the master node, will not demote")
2561

    
2562
  result = utils.RunCmd([constants.DAEMON_UTIL, "check", constants.MASTERD])
2563
  if not result.failed:
2564
    _Fail("The master daemon is running, will not demote")
2565

    
2566
  try:
2567
    if os.path.isfile(constants.CLUSTER_CONF_FILE):
2568
      utils.CreateBackup(constants.CLUSTER_CONF_FILE)
2569
  except EnvironmentError, err:
2570
    if err.errno != errno.ENOENT:
2571
      _Fail("Error while backing up cluster file: %s", err, exc=True)
2572

    
2573
  utils.RemoveFile(constants.CLUSTER_CONF_FILE)
2574

    
2575

    
2576
def _GetX509Filenames(cryptodir, name):
2577
  """Returns the full paths for the private key and certificate.
2578

2579
  """
2580
  return (utils.PathJoin(cryptodir, name),
2581
          utils.PathJoin(cryptodir, name, _X509_KEY_FILE),
2582
          utils.PathJoin(cryptodir, name, _X509_CERT_FILE))
2583

    
2584

    
2585
def CreateX509Certificate(validity, cryptodir=constants.CRYPTO_KEYS_DIR):
2586
  """Creates a new X509 certificate for SSL/TLS.
2587

2588
  @type validity: int
2589
  @param validity: Validity in seconds
2590
  @rtype: tuple; (string, string)
2591
  @return: Certificate name and public part
2592

2593
  """
2594
  (key_pem, cert_pem) = \
2595
    utils.GenerateSelfSignedX509Cert(netutils.HostInfo.SysName(),
2596
                                     min(validity, _MAX_SSL_CERT_VALIDITY))
2597

    
2598
  cert_dir = tempfile.mkdtemp(dir=cryptodir,
2599
                              prefix="x509-%s-" % utils.TimestampForFilename())
2600
  try:
2601
    name = os.path.basename(cert_dir)
2602
    assert len(name) > 5
2603

    
2604
    (_, key_file, cert_file) = _GetX509Filenames(cryptodir, name)
2605

    
2606
    utils.WriteFile(key_file, mode=0400, data=key_pem)
2607
    utils.WriteFile(cert_file, mode=0400, data=cert_pem)
2608

    
2609
    # Never return private key as it shouldn't leave the node
2610
    return (name, cert_pem)
2611
  except Exception:
2612
    shutil.rmtree(cert_dir, ignore_errors=True)
2613
    raise
2614

    
2615

    
2616
def RemoveX509Certificate(name, cryptodir=constants.CRYPTO_KEYS_DIR):
2617
  """Removes a X509 certificate.
2618

2619
  @type name: string
2620
  @param name: Certificate name
2621

2622
  """
2623
  (cert_dir, key_file, cert_file) = _GetX509Filenames(cryptodir, name)
2624

    
2625
  utils.RemoveFile(key_file)
2626
  utils.RemoveFile(cert_file)
2627

    
2628
  try:
2629
    os.rmdir(cert_dir)
2630
  except EnvironmentError, err:
2631
    _Fail("Cannot remove certificate directory '%s': %s",
2632
          cert_dir, err)
2633

    
2634

    
2635
def _GetImportExportIoCommand(instance, mode, ieio, ieargs):
2636
  """Returns the command for the requested input/output.
2637

2638
  @type instance: L{objects.Instance}
2639
  @param instance: The instance object
2640
  @param mode: Import/export mode
2641
  @param ieio: Input/output type
2642
  @param ieargs: Input/output arguments
2643

2644
  """
2645
  assert mode in (constants.IEM_IMPORT, constants.IEM_EXPORT)
2646

    
2647
  env = None
2648
  prefix = None
2649
  suffix = None
2650
  exp_size = None
2651

    
2652
  if ieio == constants.IEIO_FILE:
2653
    (filename, ) = ieargs
2654

    
2655
    if not utils.IsNormAbsPath(filename):
2656
      _Fail("Path '%s' is not normalized or absolute", filename)
2657

    
2658
    directory = os.path.normpath(os.path.dirname(filename))
2659

    
2660
    if (os.path.commonprefix([constants.EXPORT_DIR, directory]) !=
2661
        constants.EXPORT_DIR):
2662
      _Fail("File '%s' is not under exports directory '%s'",
2663
            filename, constants.EXPORT_DIR)
2664

    
2665
    # Create directory
2666
    utils.Makedirs(directory, mode=0750)
2667

    
2668
    quoted_filename = utils.ShellQuote(filename)
2669

    
2670
    if mode == constants.IEM_IMPORT:
2671
      suffix = "> %s" % quoted_filename
2672
    elif mode == constants.IEM_EXPORT:
2673
      suffix = "< %s" % quoted_filename
2674

    
2675
      # Retrieve file size
2676
      try:
2677
        st = os.stat(filename)
2678
      except EnvironmentError, err:
2679
        logging.error("Can't stat(2) %s: %s", filename, err)
2680
      else:
2681
        exp_size = utils.BytesToMebibyte(st.st_size)
2682

    
2683
  elif ieio == constants.IEIO_RAW_DISK:
2684
    (disk, ) = ieargs
2685

    
2686
    real_disk = _OpenRealBD(disk)
2687

    
2688
    if mode == constants.IEM_IMPORT:
2689
      # we set here a smaller block size as, due to transport buffering, more
2690
      # than 64-128k will mostly ignored; we use nocreat to fail if the device
2691
      # is not already there or we pass a wrong path; we use notrunc to no
2692
      # attempt truncate on an LV device; we use oflag=dsync to not buffer too
2693
      # much memory; this means that at best, we flush every 64k, which will
2694
      # not be very fast
2695
      suffix = utils.BuildShellCmd(("| dd of=%s conv=nocreat,notrunc"
2696
                                    " bs=%s oflag=dsync"),
2697
                                    real_disk.dev_path,
2698
                                    str(64 * 1024))
2699

    
2700
    elif mode == constants.IEM_EXPORT:
2701
      # the block size on the read dd is 1MiB to match our units
2702
      prefix = utils.BuildShellCmd("dd if=%s bs=%s count=%s |",
2703
                                   real_disk.dev_path,
2704
                                   str(1024 * 1024), # 1 MB
2705
                                   str(disk.size))
2706
      exp_size = disk.size
2707

    
2708
  elif ieio == constants.IEIO_SCRIPT:
2709
    (disk, disk_index, ) = ieargs
2710

    
2711
    assert isinstance(disk_index, (int, long))
2712

    
2713
    real_disk = _OpenRealBD(disk)
2714

    
2715
    inst_os = OSFromDisk(instance.os)
2716
    env = OSEnvironment(instance, inst_os)
2717

    
2718
    if mode == constants.IEM_IMPORT:
2719
      env["IMPORT_DEVICE"] = env["DISK_%d_PATH" % disk_index]
2720
      env["IMPORT_INDEX"] = str(disk_index)
2721
      script = inst_os.import_script
2722

    
2723
    elif mode == constants.IEM_EXPORT:
2724
      env["EXPORT_DEVICE"] = real_disk.dev_path
2725
      env["EXPORT_INDEX"] = str(disk_index)
2726
      script = inst_os.export_script
2727

    
2728
    # TODO: Pass special environment only to script
2729
    script_cmd = utils.BuildShellCmd("( cd %s && %s; )", inst_os.path, script)
2730

    
2731
    if mode == constants.IEM_IMPORT:
2732
      suffix = "| %s" % script_cmd
2733

    
2734
    elif mode == constants.IEM_EXPORT:
2735
      prefix = "%s |" % script_cmd
2736

    
2737
    # Let script predict size
2738
    exp_size = constants.IE_CUSTOM_SIZE
2739

    
2740
  else:
2741
    _Fail("Invalid %s I/O mode %r", mode, ieio)
2742

    
2743
  return (env, prefix, suffix, exp_size)
2744

    
2745

    
2746
def _CreateImportExportStatusDir(prefix):
2747
  """Creates status directory for import/export.
2748

2749
  """
2750
  return tempfile.mkdtemp(dir=constants.IMPORT_EXPORT_DIR,
2751
                          prefix=("%s-%s-" %
2752
                                  (prefix, utils.TimestampForFilename())))
2753

    
2754

    
2755
def StartImportExportDaemon(mode, opts, host, port, instance, ieio, ieioargs):
2756
  """Starts an import or export daemon.
2757

2758
  @param mode: Import/output mode
2759
  @type opts: L{objects.ImportExportOptions}
2760
  @param opts: Daemon options
2761
  @type host: string
2762
  @param host: Remote host for export (None for import)
2763
  @type port: int
2764
  @param port: Remote port for export (None for import)
2765
  @type instance: L{objects.Instance}
2766
  @param instance: Instance object
2767
  @param ieio: Input/output type
2768
  @param ieioargs: Input/output arguments
2769

2770
  """
2771
  if mode == constants.IEM_IMPORT:
2772
    prefix = "import"
2773

    
2774
    if not (host is None and port is None):
2775
      _Fail("Can not specify host or port on import")
2776

    
2777
  elif mode == constants.IEM_EXPORT:
2778
    prefix = "export"
2779

    
2780
    if host is None or port is None:
2781
      _Fail("Host and port must be specified for an export")
2782

    
2783
  else:
2784
    _Fail("Invalid mode %r", mode)
2785

    
2786
  if (opts.key_name is None) ^ (opts.ca_pem is None):
2787
    _Fail("Cluster certificate can only be used for both key and CA")
2788

    
2789
  (cmd_env, cmd_prefix, cmd_suffix, exp_size) = \
2790
    _GetImportExportIoCommand(instance, mode, ieio, ieioargs)
2791

    
2792
  if opts.key_name is None:
2793
    # Use server.pem
2794
    key_path = constants.NODED_CERT_FILE
2795
    cert_path = constants.NODED_CERT_FILE
2796
    assert opts.ca_pem is None
2797
  else:
2798
    (_, key_path, cert_path) = _GetX509Filenames(constants.CRYPTO_KEYS_DIR,
2799
                                                 opts.key_name)
2800
    assert opts.ca_pem is not None
2801

    
2802
  for i in [key_path, cert_path]:
2803
    if not os.path.exists(i):
2804
      _Fail("File '%s' does not exist" % i)
2805

    
2806
  status_dir = _CreateImportExportStatusDir(prefix)
2807
  try:
2808
    status_file = utils.PathJoin(status_dir, _IES_STATUS_FILE)
2809
    pid_file = utils.PathJoin(status_dir, _IES_PID_FILE)
2810
    ca_file = utils.PathJoin(status_dir, _IES_CA_FILE)
2811

    
2812
    if opts.ca_pem is None:
2813
      # Use server.pem
2814
      ca = utils.ReadFile(constants.NODED_CERT_FILE)
2815
    else:
2816
      ca = opts.ca_pem
2817

    
2818
    # Write CA file
2819
    utils.WriteFile(ca_file, data=ca, mode=0400)
2820

    
2821
    cmd = [
2822
      constants.IMPORT_EXPORT_DAEMON,
2823
      status_file, mode,
2824
      "--key=%s" % key_path,
2825
      "--cert=%s" % cert_path,
2826
      "--ca=%s" % ca_file,
2827
      ]
2828

    
2829
    if host:
2830
      cmd.append("--host=%s" % host)
2831

    
2832
    if port:
2833
      cmd.append("--port=%s" % port)
2834

    
2835
    if opts.compress:
2836
      cmd.append("--compress=%s" % opts.compress)
2837

    
2838
    if opts.magic:
2839
      cmd.append("--magic=%s" % opts.magic)
2840

    
2841
    if exp_size is not None:
2842
      cmd.append("--expected-size=%s" % exp_size)
2843

    
2844
    if cmd_prefix:
2845
      cmd.append("--cmd-prefix=%s" % cmd_prefix)
2846

    
2847
    if cmd_suffix:
2848
      cmd.append("--cmd-suffix=%s" % cmd_suffix)
2849

    
2850
    logfile = _InstanceLogName(prefix, instance.os, instance.name)
2851

    
2852
    # TODO: Once _InstanceLogName uses tempfile.mkstemp, StartDaemon has
2853
    # support for receiving a file descriptor for output
2854
    utils.StartDaemon(cmd, env=cmd_env, pidfile=pid_file,
2855
                      output=logfile)
2856

    
2857
    # The import/export name is simply the status directory name
2858
    return os.path.basename(status_dir)
2859

    
2860
  except Exception:
2861
    shutil.rmtree(status_dir, ignore_errors=True)
2862
    raise
2863

    
2864

    
2865
def GetImportExportStatus(names):
2866
  """Returns import/export daemon status.
2867

2868
  @type names: sequence
2869
  @param names: List of names
2870
  @rtype: List of dicts
2871
  @return: Returns a list of the state of each named import/export or None if a
2872
           status couldn't be read
2873

2874
  """
2875
  result = []
2876

    
2877
  for name in names:
2878
    status_file = utils.PathJoin(constants.IMPORT_EXPORT_DIR, name,
2879
                                 _IES_STATUS_FILE)
2880

    
2881
    try:
2882
      data = utils.ReadFile(status_file)
2883
    except EnvironmentError, err:
2884
      if err.errno != errno.ENOENT:
2885
        raise
2886
      data = None
2887

    
2888
    if not data:
2889
      result.append(None)
2890
      continue
2891

    
2892
    result.append(serializer.LoadJson(data))
2893

    
2894
  return result
2895

    
2896

    
2897
def AbortImportExport(name):
2898
  """Sends SIGTERM to a running import/export daemon.
2899

2900
  """
2901
  logging.info("Abort import/export %s", name)
2902

    
2903
  status_dir = utils.PathJoin(constants.IMPORT_EXPORT_DIR, name)
2904
  pid = utils.ReadLockedPidFile(utils.PathJoin(status_dir, _IES_PID_FILE))
2905

    
2906
  if pid:
2907
    logging.info("Import/export %s is running with PID %s, sending SIGTERM",
2908
                 name, pid)
2909
    utils.IgnoreProcessNotFound(os.kill, pid, signal.SIGTERM)
2910

    
2911

    
2912
def CleanupImportExport(name):
2913
  """Cleanup after an import or export.
2914

2915
  If the import/export daemon is still running it's killed. Afterwards the
2916
  whole status directory is removed.
2917

2918
  """
2919
  logging.info("Finalizing import/export %s", name)
2920

    
2921
  status_dir = utils.PathJoin(constants.IMPORT_EXPORT_DIR, name)
2922

    
2923
  pid = utils.ReadLockedPidFile(utils.PathJoin(status_dir, _IES_PID_FILE))
2924

    
2925
  if pid:
2926
    logging.info("Import/export %s is still running with PID %s",
2927
                 name, pid)
2928
    utils.KillProcess(pid, waitpid=False)
2929

    
2930
  shutil.rmtree(status_dir, ignore_errors=True)
2931

    
2932

    
2933
def _FindDisks(nodes_ip, disks):
2934
  """Sets the physical ID on disks and returns the block devices.
2935

2936
  """
2937
  # set the correct physical ID
2938
  my_name = netutils.HostInfo().name
2939
  for cf in disks:
2940
    cf.SetPhysicalID(my_name, nodes_ip)
2941

    
2942
  bdevs = []
2943

    
2944
  for cf in disks:
2945
    rd = _RecursiveFindBD(cf)
2946
    if rd is None:
2947
      _Fail("Can't find device %s", cf)
2948
    bdevs.append(rd)
2949
  return bdevs
2950

    
2951

    
2952
def DrbdDisconnectNet(nodes_ip, disks):
2953
  """Disconnects the network on a list of drbd devices.
2954

2955
  """
2956
  bdevs = _FindDisks(nodes_ip, disks)
2957

    
2958
  # disconnect disks
2959
  for rd in bdevs:
2960
    try:
2961
      rd.DisconnectNet()
2962
    except errors.BlockDeviceError, err:
2963
      _Fail("Can't change network configuration to standalone mode: %s",
2964
            err, exc=True)
2965

    
2966

    
2967
def DrbdAttachNet(nodes_ip, disks, instance_name, multimaster):
2968
  """Attaches the network on a list of drbd devices.
2969

2970
  """
2971
  bdevs = _FindDisks(nodes_ip, disks)
2972

    
2973
  if multimaster:
2974
    for idx, rd in enumerate(bdevs):
2975
      try:
2976
        _SymlinkBlockDev(instance_name, rd.dev_path, idx)
2977
      except EnvironmentError, err:
2978
        _Fail("Can't create symlink: %s", err)
2979
  # reconnect disks, switch to new master configuration and if
2980
  # needed primary mode
2981
  for rd in bdevs:
2982
    try:
2983
      rd.AttachNet(multimaster)
2984
    except errors.BlockDeviceError, err:
2985
      _Fail("Can't change network configuration: %s", err)
2986

    
2987
  # wait until the disks are connected; we need to retry the re-attach
2988
  # if the device becomes standalone, as this might happen if the one
2989
  # node disconnects and reconnects in a different mode before the
2990
  # other node reconnects; in this case, one or both of the nodes will
2991
  # decide it has wrong configuration and switch to standalone
2992

    
2993
  def _Attach():
2994
    all_connected = True
2995

    
2996
    for rd in bdevs:
2997
      stats = rd.GetProcStatus()
2998

    
2999
      all_connected = (all_connected and
3000
                       (stats.is_connected or stats.is_in_resync))
3001

    
3002
      if stats.is_standalone:
3003
        # peer had different config info and this node became
3004
        # standalone, even though this should not happen with the
3005
        # new staged way of changing disk configs
3006
        try:
3007
          rd.AttachNet(multimaster)
3008
        except errors.BlockDeviceError, err:
3009
          _Fail("Can't change network configuration: %s", err)
3010

    
3011
    if not all_connected:
3012
      raise utils.RetryAgain()
3013

    
3014
  try:
3015
    # Start with a delay of 100 miliseconds and go up to 5 seconds
3016
    utils.Retry(_Attach, (0.1, 1.5, 5.0), 2 * 60)
3017
  except utils.RetryTimeout:
3018
    _Fail("Timeout in disk reconnecting")
3019

    
3020
  if multimaster:
3021
    # change to primary mode
3022
    for rd in bdevs:
3023
      try:
3024
        rd.Open()
3025
      except errors.BlockDeviceError, err:
3026
        _Fail("Can't change to primary mode: %s", err)
3027

    
3028

    
3029
def DrbdWaitSync(nodes_ip, disks):
3030
  """Wait until DRBDs have synchronized.
3031

3032
  """
3033
  def _helper(rd):
3034
    stats = rd.GetProcStatus()
3035
    if not (stats.is_connected or stats.is_in_resync):
3036
      raise utils.RetryAgain()
3037
    return stats
3038

    
3039
  bdevs = _FindDisks(nodes_ip, disks)
3040

    
3041
  min_resync = 100
3042
  alldone = True
3043
  for rd in bdevs:
3044
    try:
3045
      # poll each second for 15 seconds
3046
      stats = utils.Retry(_helper, 1, 15, args=[rd])
3047
    except utils.RetryTimeout:
3048
      stats = rd.GetProcStatus()
3049
      # last check
3050
      if not (stats.is_connected or stats.is_in_resync):
3051
        _Fail("DRBD device %s is not in sync: stats=%s", rd, stats)
3052
    alldone = alldone and (not stats.is_in_resync)
3053
    if stats.sync_percent is not None:
3054
      min_resync = min(min_resync, stats.sync_percent)
3055

    
3056
  return (alldone, min_resync)
3057

    
3058

    
3059
def GetDrbdUsermodeHelper():
3060
  """Returns DRBD usermode helper currently configured.
3061

3062
  """
3063
  try:
3064
    return bdev.BaseDRBD.GetUsermodeHelper()
3065
  except errors.BlockDeviceError, err:
3066
    _Fail(str(err))
3067

    
3068

    
3069
def PowercycleNode(hypervisor_type):
3070
  """Hard-powercycle the node.
3071

3072
  Because we need to return first, and schedule the powercycle in the
3073
  background, we won't be able to report failures nicely.
3074

3075
  """
3076
  hyper = hypervisor.GetHypervisor(hypervisor_type)
3077
  try:
3078
    pid = os.fork()
3079
  except OSError:
3080
    # if we can't fork, we'll pretend that we're in the child process
3081
    pid = 0
3082
  if pid > 0:
3083
    return "Reboot scheduled in 5 seconds"
3084
  # ensure the child is running on ram
3085
  try:
3086
    utils.Mlockall()
3087
  except Exception: # pylint: disable-msg=W0703
3088
    pass
3089
  time.sleep(5)
3090
  hyper.PowercycleNode()
3091

    
3092

    
3093
class HooksRunner(object):
3094
  """Hook runner.
3095

3096
  This class is instantiated on the node side (ganeti-noded) and not
3097
  on the master side.
3098

3099
  """
3100
  def __init__(self, hooks_base_dir=None):
3101
    """Constructor for hooks runner.
3102

3103
    @type hooks_base_dir: str or None
3104
    @param hooks_base_dir: if not None, this overrides the
3105
        L{constants.HOOKS_BASE_DIR} (useful for unittests)
3106

3107
    """
3108
    if hooks_base_dir is None:
3109
      hooks_base_dir = constants.HOOKS_BASE_DIR
3110
    # yeah, _BASE_DIR is not valid for attributes, we use it like a
3111
    # constant
3112
    self._BASE_DIR = hooks_base_dir # pylint: disable-msg=C0103
3113

    
3114
  def RunHooks(self, hpath, phase, env):
3115
    """Run the scripts in the hooks directory.
3116

3117
    @type hpath: str
3118
    @param hpath: the path to the hooks directory which
3119
        holds the scripts
3120
    @type phase: str
3121
    @param phase: either L{constants.HOOKS_PHASE_PRE} or
3122
        L{constants.HOOKS_PHASE_POST}
3123
    @type env: dict
3124
    @param env: dictionary with the environment for the hook
3125
    @rtype: list
3126
    @return: list of 3-element tuples:
3127
      - script path
3128
      - script result, either L{constants.HKR_SUCCESS} or
3129
        L{constants.HKR_FAIL}
3130
      - output of the script
3131

3132
    @raise errors.ProgrammerError: for invalid input
3133
        parameters
3134

3135
    """
3136
    if phase == constants.HOOKS_PHASE_PRE:
3137
      suffix = "pre"
3138
    elif phase == constants.HOOKS_PHASE_POST:
3139
      suffix = "post"
3140
    else:
3141
      _Fail("Unknown hooks phase '%s'", phase)
3142

    
3143

    
3144
    subdir = "%s-%s.d" % (hpath, suffix)
3145
    dir_name = utils.PathJoin(self._BASE_DIR, subdir)
3146

    
3147
    results = []
3148

    
3149
    if not os.path.isdir(dir_name):
3150
      # for non-existing/non-dirs, we simply exit instead of logging a
3151
      # warning at every operation
3152
      return results
3153

    
3154
    runparts_results = utils.RunParts(dir_name, env=env, reset_env=True)
3155

    
3156
    for (relname, relstatus, runresult)  in runparts_results:
3157
      if relstatus == constants.RUNPARTS_SKIP:
3158
        rrval = constants.HKR_SKIP
3159
        output = ""
3160
      elif relstatus == constants.RUNPARTS_ERR:
3161
        rrval = constants.HKR_FAIL
3162
        output = "Hook script execution error: %s" % runresult
3163
      elif relstatus == constants.RUNPARTS_RUN:
3164
        if runresult.failed:
3165
          rrval = constants.HKR_FAIL
3166
        else:
3167
          rrval = constants.HKR_SUCCESS
3168
        output = utils.SafeEncode(runresult.output.strip())
3169
      results.append(("%s/%s" % (subdir, relname), rrval, output))
3170

    
3171
    return results
3172

    
3173

    
3174
class IAllocatorRunner(object):
3175
  """IAllocator runner.
3176

3177
  This class is instantiated on the node side (ganeti-noded) and not on
3178
  the master side.
3179

3180
  """
3181
  @staticmethod
3182
  def Run(name, idata):
3183
    """Run an iallocator script.
3184

3185
    @type name: str
3186
    @param name: the iallocator script name
3187
    @type idata: str
3188
    @param idata: the allocator input data
3189

3190
    @rtype: tuple
3191
    @return: two element tuple of:
3192
       - status
3193
       - either error message or stdout of allocator (for success)
3194

3195
    """
3196
    alloc_script = utils.FindFile(name, constants.IALLOCATOR_SEARCH_PATH,
3197
                                  os.path.isfile)
3198
    if alloc_script is None:
3199
      _Fail("iallocator module '%s' not found in the search path", name)
3200

    
3201
    fd, fin_name = tempfile.mkstemp(prefix="ganeti-iallocator.")
3202
    try:
3203
      os.write(fd, idata)
3204
      os.close(fd)
3205
      result = utils.RunCmd([alloc_script, fin_name])
3206
      if result.failed:
3207
        _Fail("iallocator module '%s' failed: %s, output '%s'",
3208
              name, result.fail_reason, result.output)
3209
    finally:
3210
      os.unlink(fin_name)
3211

    
3212
    return result.stdout
3213

    
3214

    
3215
class DevCacheManager(object):
3216
  """Simple class for managing a cache of block device information.
3217

3218
  """
3219
  _DEV_PREFIX = "/dev/"
3220
  _ROOT_DIR = constants.BDEV_CACHE_DIR
3221

    
3222
  @classmethod
3223
  def _ConvertPath(cls, dev_path):
3224
    """Converts a /dev/name path to the cache file name.
3225

3226
    This replaces slashes with underscores and strips the /dev
3227
    prefix. It then returns the full path to the cache file.
3228

3229
    @type dev_path: str
3230
    @param dev_path: the C{/dev/} path name
3231
    @rtype: str
3232
    @return: the converted path name
3233

3234
    """
3235
    if dev_path.startswith(cls._DEV_PREFIX):
3236
      dev_path = dev_path[len(cls._DEV_PREFIX):]
3237
    dev_path = dev_path.replace("/", "_")
3238
    fpath = utils.PathJoin(cls._ROOT_DIR, "bdev_%s" % dev_path)
3239
    return fpath
3240

    
3241
  @classmethod
3242
  def UpdateCache(cls, dev_path, owner, on_primary, iv_name):
3243
    """Updates the cache information for a given device.
3244

3245
    @type dev_path: str
3246
    @param dev_path: the pathname of the device
3247
    @type owner: str
3248
    @param owner: the owner (instance name) of the device
3249
    @type on_primary: bool
3250
    @param on_primary: whether this is the primary
3251
        node nor not
3252
    @type iv_name: str
3253
    @param iv_name: the instance-visible name of the
3254
        device, as in objects.Disk.iv_name
3255

3256
    @rtype: None
3257

3258
    """
3259
    if dev_path is None:
3260
      logging.error("DevCacheManager.UpdateCache got a None dev_path")
3261
      return
3262
    fpath = cls._ConvertPath(dev_path)
3263
    if on_primary:
3264
      state = "primary"
3265
    else:
3266
      state = "secondary"
3267
    if iv_name is None:
3268
      iv_name = "not_visible"
3269
    fdata = "%s %s %s\n" % (str(owner), state, iv_name)
3270
    try:
3271
      utils.WriteFile(fpath, data=fdata)
3272
    except EnvironmentError, err:
3273
      logging.exception("Can't update bdev cache for %s: %s", dev_path, err)
3274

    
3275
  @classmethod
3276
  def RemoveCache(cls, dev_path):
3277
    """Remove data for a dev_path.
3278

3279
    This is just a wrapper over L{utils.RemoveFile} with a converted
3280
    path name and logging.
3281

3282
    @type dev_path: str
3283
    @param dev_path: the pathname of the device
3284

3285
    @rtype: None
3286

3287
    """
3288
    if dev_path is None:
3289
      logging.error("DevCacheManager.RemoveCache got a None dev_path")
3290
      return
3291
    fpath = cls._ConvertPath(dev_path)
3292
    try:
3293
      utils.RemoveFile(fpath)
3294
    except EnvironmentError, err:
3295
      logging.exception("Can't update bdev cache for %s: %s", dev_path, err)