Statistics
| Branch: | Tag: | Revision:

root / lib / backend.py @ e5a45a16

History | View | Annotate | Download (80.6 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

27
"""
28

    
29

    
30
import os
31
import os.path
32
import shutil
33
import time
34
import stat
35
import errno
36
import re
37
import subprocess
38
import random
39
import logging
40
import tempfile
41
import zlib
42
import base64
43

    
44
from ganeti import errors
45
from ganeti import utils
46
from ganeti import ssh
47
from ganeti import hypervisor
48
from ganeti import constants
49
from ganeti import bdev
50
from ganeti import objects
51
from ganeti import ssconf
52

    
53

    
54
class RPCFail(Exception):
55
  """Class denoting RPC failure.
56

57
  Its argument is the error message.
58

59
  """
60

    
61
def _Fail(msg, *args, **kwargs):
62
  """Log an error and the raise an RPCFail exception.
63

64
  This exception is then handled specially in the ganeti daemon and
65
  turned into a 'failed' return type. As such, this function is a
66
  useful shortcut for logging the error and returning it to the master
67
  daemon.
68

69
  @type msg: string
70
  @param msg: the text of the exception
71
  @raise RPCFail
72

73
  """
74
  if args:
75
    msg = msg % args
76
  if "log" not in kwargs or kwargs["log"]: # if we should log this error
77
    if "exc" in kwargs and kwargs["exc"]:
78
      logging.exception(msg)
79
    else:
80
      logging.error(msg)
81
  raise RPCFail(msg)
82

    
83

    
84
def _GetConfig():
85
  """Simple wrapper to return a SimpleStore.
86

87
  @rtype: L{ssconf.SimpleStore}
88
  @return: a SimpleStore instance
89

90
  """
91
  return ssconf.SimpleStore()
92

    
93

    
94
def _GetSshRunner(cluster_name):
95
  """Simple wrapper to return an SshRunner.
96

97
  @type cluster_name: str
98
  @param cluster_name: the cluster name, which is needed
99
      by the SshRunner constructor
100
  @rtype: L{ssh.SshRunner}
101
  @return: an SshRunner instance
102

103
  """
104
  return ssh.SshRunner(cluster_name)
105

    
106

    
107
def _Decompress(data):
108
  """Unpacks data compressed by the RPC client.
109

110
  @type data: list or tuple
111
  @param data: Data sent by RPC client
112
  @rtype: str
113
  @return: Decompressed data
114

115
  """
116
  assert isinstance(data, (list, tuple))
117
  assert len(data) == 2
118
  (encoding, content) = data
119
  if encoding == constants.RPC_ENCODING_NONE:
120
    return content
121
  elif encoding == constants.RPC_ENCODING_ZLIB_BASE64:
122
    return zlib.decompress(base64.b64decode(content))
123
  else:
124
    raise AssertionError("Unknown data encoding")
125

    
126

    
127
def _CleanDirectory(path, exclude=None):
128
  """Removes all regular files in a directory.
129

130
  @type path: str
131
  @param path: the directory to clean
132
  @type exclude: list
133
  @param exclude: list of files to be excluded, defaults
134
      to the empty list
135

136
  """
137
  if not os.path.isdir(path):
138
    return
139
  if exclude is None:
140
    exclude = []
141
  else:
142
    # Normalize excluded paths
143
    exclude = [os.path.normpath(i) for i in exclude]
144

    
145
  for rel_name in utils.ListVisibleFiles(path):
146
    full_name = os.path.normpath(os.path.join(path, rel_name))
147
    if full_name in exclude:
148
      continue
149
    if os.path.isfile(full_name) and not os.path.islink(full_name):
150
      utils.RemoveFile(full_name)
151

    
152

    
153
def _BuildUploadFileList():
154
  """Build the list of allowed upload files.
155

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

158
  """
159
  allowed_files = set([
160
    constants.CLUSTER_CONF_FILE,
161
    constants.ETC_HOSTS,
162
    constants.SSH_KNOWN_HOSTS_FILE,
163
    constants.VNC_PASSWORD_FILE,
164
    constants.RAPI_CERT_FILE,
165
    constants.RAPI_USERS_FILE,
166
    ])
167

    
168
  for hv_name in constants.HYPER_TYPES:
169
    hv_class = hypervisor.GetHypervisorClass(hv_name)
170
    allowed_files.update(hv_class.GetAncillaryFiles())
171

    
172
  return frozenset(allowed_files)
173

    
174

    
175
_ALLOWED_UPLOAD_FILES = _BuildUploadFileList()
176

    
177

    
178
def JobQueuePurge():
179
  """Removes job queue files and archived jobs.
180

181
  @rtype: tuple
182
  @return: True, None
183

184
  """
185
  _CleanDirectory(constants.QUEUE_DIR, exclude=[constants.JOB_QUEUE_LOCK_FILE])
186
  _CleanDirectory(constants.JOB_QUEUE_ARCHIVE_DIR)
187

    
188

    
189
def GetMasterInfo():
190
  """Returns master information.
191

192
  This is an utility function to compute master information, either
193
  for consumption here or from the node daemon.
194

195
  @rtype: tuple
196
  @return: master_netdev, master_ip, master_name
197
  @raise RPCFail: in case of errors
198

199
  """
200
  try:
201
    cfg = _GetConfig()
202
    master_netdev = cfg.GetMasterNetdev()
203
    master_ip = cfg.GetMasterIP()
204
    master_node = cfg.GetMasterNode()
205
  except errors.ConfigurationError, err:
206
    _Fail("Cluster configuration incomplete: %s", err, exc=True)
207
  return (master_netdev, master_ip, master_node)
208

    
209

    
210
def StartMaster(start_daemons, no_voting):
211
  """Activate local node as master node.
212

213
  The function will always try activate the IP address of the master
214
  (unless someone else has it). It will also start the master daemons,
215
  based on the start_daemons parameter.
216

217
  @type start_daemons: boolean
218
  @param start_daemons: whether to also start the master
219
      daemons (ganeti-masterd and ganeti-rapi)
220
  @type no_voting: boolean
221
  @param no_voting: whether to start ganeti-masterd without a node vote
222
      (if start_daemons is True), but still non-interactively
223
  @rtype: None
224

225
  """
226
  # GetMasterInfo will raise an exception if not able to return data
227
  master_netdev, master_ip, _ = GetMasterInfo()
228

    
229
  err_msgs = []
230
  if utils.TcpPing(master_ip, constants.DEFAULT_NODED_PORT):
231
    if utils.OwnIpAddress(master_ip):
232
      # we already have the ip:
233
      logging.debug("Master IP already configured, doing nothing")
234
    else:
235
      msg = "Someone else has the master ip, not activating"
236
      logging.error(msg)
237
      err_msgs.append(msg)
238
  else:
239
    result = utils.RunCmd(["ip", "address", "add", "%s/32" % master_ip,
240
                           "dev", master_netdev, "label",
241
                           "%s:0" % master_netdev])
242
    if result.failed:
243
      msg = "Can't activate master IP: %s" % result.output
244
      logging.error(msg)
245
      err_msgs.append(msg)
246

    
247
    result = utils.RunCmd(["arping", "-q", "-U", "-c 3", "-I", master_netdev,
248
                           "-s", master_ip, master_ip])
249
    # we'll ignore the exit code of arping
250

    
251
  # and now start the master and rapi daemons
252
  if start_daemons:
253
    daemons_params = {
254
        'ganeti-masterd': [],
255
        'ganeti-rapi': [],
256
        }
257
    if no_voting:
258
      daemons_params['ganeti-masterd'].append('--no-voting')
259
      daemons_params['ganeti-masterd'].append('--yes-do-it')
260
    for daemon in daemons_params:
261
      cmd = [daemon]
262
      cmd.extend(daemons_params[daemon])
263
      result = utils.RunCmd(cmd)
264
      if result.failed:
265
        msg = "Can't start daemon %s: %s" % (daemon, result.output)
266
        logging.error(msg)
267
        err_msgs.append(msg)
268

    
269
  if err_msgs:
270
    _Fail("; ".join(err_msgs))
271

    
272

    
273
def StopMaster(stop_daemons):
274
  """Deactivate this node as master.
275

276
  The function will always try to deactivate the IP address of the
277
  master. It will also stop the master daemons depending on the
278
  stop_daemons parameter.
279

280
  @type stop_daemons: boolean
281
  @param stop_daemons: whether to also stop the master daemons
282
      (ganeti-masterd and ganeti-rapi)
283
  @rtype: None
284

285
  """
286
  # TODO: log and report back to the caller the error failures; we
287
  # need to decide in which case we fail the RPC for this
288

    
289
  # GetMasterInfo will raise an exception if not able to return data
290
  master_netdev, master_ip, _ = GetMasterInfo()
291

    
292
  result = utils.RunCmd(["ip", "address", "del", "%s/32" % master_ip,
293
                         "dev", master_netdev])
294
  if result.failed:
295
    logging.error("Can't remove the master IP, error: %s", result.output)
296
    # but otherwise ignore the failure
297

    
298
  if stop_daemons:
299
    # stop/kill the rapi and the master daemon
300
    for daemon in constants.RAPI_PID, constants.MASTERD_PID:
301
      utils.KillProcess(utils.ReadPidFile(utils.DaemonPidFileName(daemon)))
302

    
303

    
304
def AddNode(dsa, dsapub, rsa, rsapub, sshkey, sshpub):
305
  """Joins this node to the cluster.
306

307
  This does the following:
308
      - updates the hostkeys of the machine (rsa and dsa)
309
      - adds the ssh private key to the user
310
      - adds the ssh public key to the users' authorized_keys file
311

312
  @type dsa: str
313
  @param dsa: the DSA private key to write
314
  @type dsapub: str
315
  @param dsapub: the DSA public key to write
316
  @type rsa: str
317
  @param rsa: the RSA private key to write
318
  @type rsapub: str
319
  @param rsapub: the RSA public key to write
320
  @type sshkey: str
321
  @param sshkey: the SSH private key to write
322
  @type sshpub: str
323
  @param sshpub: the SSH public key to write
324
  @rtype: boolean
325
  @return: the success of the operation
326

327
  """
328
  sshd_keys =  [(constants.SSH_HOST_RSA_PRIV, rsa, 0600),
329
                (constants.SSH_HOST_RSA_PUB, rsapub, 0644),
330
                (constants.SSH_HOST_DSA_PRIV, dsa, 0600),
331
                (constants.SSH_HOST_DSA_PUB, dsapub, 0644)]
332
  for name, content, mode in sshd_keys:
333
    utils.WriteFile(name, data=content, mode=mode)
334

    
335
  try:
336
    priv_key, pub_key, auth_keys = ssh.GetUserFiles(constants.GANETI_RUNAS,
337
                                                    mkdir=True)
338
  except errors.OpExecError, err:
339
    _Fail("Error while processing user ssh files: %s", err, exc=True)
340

    
341
  for name, content in [(priv_key, sshkey), (pub_key, sshpub)]:
342
    utils.WriteFile(name, data=content, mode=0600)
343

    
344
  utils.AddAuthorizedKey(auth_keys, sshpub)
345

    
346
  utils.RunCmd([constants.SSH_INITD_SCRIPT, "restart"])
347

    
348

    
349
def LeaveCluster():
350
  """Cleans up and remove the current node.
351

352
  This function cleans up and prepares the current node to be removed
353
  from the cluster.
354

355
  If processing is successful, then it raises an
356
  L{errors.QuitGanetiException} which is used as a special case to
357
  shutdown the node daemon.
358

359
  """
360
  _CleanDirectory(constants.DATA_DIR)
361
  JobQueuePurge()
362

    
363
  try:
364
    priv_key, pub_key, auth_keys = ssh.GetUserFiles(constants.GANETI_RUNAS)
365

    
366
    f = open(pub_key, 'r')
367
    try:
368
      utils.RemoveAuthorizedKey(auth_keys, f.read(8192))
369
    finally:
370
      f.close()
371

    
372
    utils.RemoveFile(priv_key)
373
    utils.RemoveFile(pub_key)
374
  except errors.OpExecError:
375
    logging.exception("Error while processing ssh files")
376

    
377
  # Raise a custom exception (handled in ganeti-noded)
378
  raise errors.QuitGanetiException(True, 'Shutdown scheduled')
379

    
380

    
381
def GetNodeInfo(vgname, hypervisor_type):
382
  """Gives back a hash with different information about the node.
383

384
  @type vgname: C{string}
385
  @param vgname: the name of the volume group to ask for disk space information
386
  @type hypervisor_type: C{str}
387
  @param hypervisor_type: the name of the hypervisor to ask for
388
      memory information
389
  @rtype: C{dict}
390
  @return: dictionary with the following keys:
391
      - vg_size is the size of the configured volume group in MiB
392
      - vg_free is the free size of the volume group in MiB
393
      - memory_dom0 is the memory allocated for domain0 in MiB
394
      - memory_free is the currently available (free) ram in MiB
395
      - memory_total is the total number of ram in MiB
396

397
  """
398
  outputarray = {}
399
  vginfo = _GetVGInfo(vgname)
400
  outputarray['vg_size'] = vginfo['vg_size']
401
  outputarray['vg_free'] = vginfo['vg_free']
402

    
403
  hyper = hypervisor.GetHypervisor(hypervisor_type)
404
  hyp_info = hyper.GetNodeInfo()
405
  if hyp_info is not None:
406
    outputarray.update(hyp_info)
407

    
408
  f = open("/proc/sys/kernel/random/boot_id", 'r')
409
  try:
410
    outputarray["bootid"] = f.read(128).rstrip("\n")
411
  finally:
412
    f.close()
413

    
414
  return outputarray
415

    
416

    
417
def VerifyNode(what, cluster_name):
418
  """Verify the status of the local node.
419

420
  Based on the input L{what} parameter, various checks are done on the
421
  local node.
422

423
  If the I{filelist} key is present, this list of
424
  files is checksummed and the file/checksum pairs are returned.
425

426
  If the I{nodelist} key is present, we check that we have
427
  connectivity via ssh with the target nodes (and check the hostname
428
  report).
429

430
  If the I{node-net-test} key is present, we check that we have
431
  connectivity to the given nodes via both primary IP and, if
432
  applicable, secondary IPs.
433

434
  @type what: C{dict}
435
  @param what: a dictionary of things to check:
436
      - filelist: list of files for which to compute checksums
437
      - nodelist: list of nodes we should check ssh communication with
438
      - node-net-test: list of nodes we should check node daemon port
439
        connectivity with
440
      - hypervisor: list with hypervisors to run the verify for
441
  @rtype: dict
442
  @return: a dictionary with the same keys as the input dict, and
443
      values representing the result of the checks
444

445
  """
446
  result = {}
447

    
448
  if constants.NV_HYPERVISOR in what:
449
    result[constants.NV_HYPERVISOR] = tmp = {}
450
    for hv_name in what[constants.NV_HYPERVISOR]:
451
      tmp[hv_name] = hypervisor.GetHypervisor(hv_name).Verify()
452

    
453
  if constants.NV_FILELIST in what:
454
    result[constants.NV_FILELIST] = utils.FingerprintFiles(
455
      what[constants.NV_FILELIST])
456

    
457
  if constants.NV_NODELIST in what:
458
    result[constants.NV_NODELIST] = tmp = {}
459
    random.shuffle(what[constants.NV_NODELIST])
460
    for node in what[constants.NV_NODELIST]:
461
      success, message = _GetSshRunner(cluster_name).VerifyNodeHostname(node)
462
      if not success:
463
        tmp[node] = message
464

    
465
  if constants.NV_NODENETTEST in what:
466
    result[constants.NV_NODENETTEST] = tmp = {}
467
    my_name = utils.HostInfo().name
468
    my_pip = my_sip = None
469
    for name, pip, sip in what[constants.NV_NODENETTEST]:
470
      if name == my_name:
471
        my_pip = pip
472
        my_sip = sip
473
        break
474
    if not my_pip:
475
      tmp[my_name] = ("Can't find my own primary/secondary IP"
476
                      " in the node list")
477
    else:
478
      port = utils.GetNodeDaemonPort()
479
      for name, pip, sip in what[constants.NV_NODENETTEST]:
480
        fail = []
481
        if not utils.TcpPing(pip, port, source=my_pip):
482
          fail.append("primary")
483
        if sip != pip:
484
          if not utils.TcpPing(sip, port, source=my_sip):
485
            fail.append("secondary")
486
        if fail:
487
          tmp[name] = ("failure using the %s interface(s)" %
488
                       " and ".join(fail))
489

    
490
  if constants.NV_LVLIST in what:
491
    result[constants.NV_LVLIST] = GetVolumeList(what[constants.NV_LVLIST])
492

    
493
  if constants.NV_INSTANCELIST in what:
494
    result[constants.NV_INSTANCELIST] = GetInstanceList(
495
      what[constants.NV_INSTANCELIST])
496

    
497
  if constants.NV_VGLIST in what:
498
    result[constants.NV_VGLIST] = utils.ListVolumeGroups()
499

    
500
  if constants.NV_VERSION in what:
501
    result[constants.NV_VERSION] = (constants.PROTOCOL_VERSION,
502
                                    constants.RELEASE_VERSION)
503

    
504
  if constants.NV_HVINFO in what:
505
    hyper = hypervisor.GetHypervisor(what[constants.NV_HVINFO])
506
    result[constants.NV_HVINFO] = hyper.GetNodeInfo()
507

    
508
  if constants.NV_DRBDLIST in what:
509
    try:
510
      used_minors = bdev.DRBD8.GetUsedDevs().keys()
511
    except errors.BlockDeviceError, err:
512
      logging.warning("Can't get used minors list", exc_info=True)
513
      used_minors = str(err)
514
    result[constants.NV_DRBDLIST] = used_minors
515

    
516
  return result
517

    
518

    
519
def GetVolumeList(vg_name):
520
  """Compute list of logical volumes and their size.
521

522
  @type vg_name: str
523
  @param vg_name: the volume group whose LVs we should list
524
  @rtype: dict
525
  @return:
526
      dictionary of all partions (key) with value being a tuple of
527
      their size (in MiB), inactive and online status::
528

529
        {'test1': ('20.06', True, True)}
530

531
      in case of errors, a string is returned with the error
532
      details.
533

534
  """
535
  lvs = {}
536
  sep = '|'
537
  result = utils.RunCmd(["lvs", "--noheadings", "--units=m", "--nosuffix",
538
                         "--separator=%s" % sep,
539
                         "-olv_name,lv_size,lv_attr", vg_name])
540
  if result.failed:
541
    _Fail("Failed to list logical volumes, lvs output: %s", result.output)
542

    
543
  valid_line_re = re.compile("^ *([^|]+)\|([0-9.]+)\|([^|]{6})\|?$")
544
  for line in result.stdout.splitlines():
545
    line = line.strip()
546
    match = valid_line_re.match(line)
547
    if not match:
548
      logging.error("Invalid line returned from lvs output: '%s'", line)
549
      continue
550
    name, size, attr = match.groups()
551
    inactive = attr[4] == '-'
552
    online = attr[5] == 'o'
553
    lvs[name] = (size, inactive, online)
554

    
555
  return lvs
556

    
557

    
558
def ListVolumeGroups():
559
  """List the volume groups and their size.
560

561
  @rtype: dict
562
  @return: dictionary with keys volume name and values the
563
      size of the volume
564

565
  """
566
  return utils.ListVolumeGroups()
567

    
568

    
569
def NodeVolumes():
570
  """List all volumes on this node.
571

572
  @rtype: list
573
  @return:
574
    A list of dictionaries, each having four keys:
575
      - name: the logical volume name,
576
      - size: the size of the logical volume
577
      - dev: the physical device on which the LV lives
578
      - vg: the volume group to which it belongs
579

580
    In case of errors, we return an empty list and log the
581
    error.
582

583
    Note that since a logical volume can live on multiple physical
584
    volumes, the resulting list might include a logical volume
585
    multiple times.
586

587
  """
588
  result = utils.RunCmd(["lvs", "--noheadings", "--units=m", "--nosuffix",
589
                         "--separator=|",
590
                         "--options=lv_name,lv_size,devices,vg_name"])
591
  if result.failed:
592
    _Fail("Failed to list logical volumes, lvs output: %s",
593
          result.output)
594

    
595
  def parse_dev(dev):
596
    if '(' in dev:
597
      return dev.split('(')[0]
598
    else:
599
      return dev
600

    
601
  def map_line(line):
602
    return {
603
      'name': line[0].strip(),
604
      'size': line[1].strip(),
605
      'dev': parse_dev(line[2].strip()),
606
      'vg': line[3].strip(),
607
    }
608

    
609
  return [map_line(line.split('|')) for line in result.stdout.splitlines()
610
          if line.count('|') >= 3]
611

    
612

    
613
def BridgesExist(bridges_list):
614
  """Check if a list of bridges exist on the current node.
615

616
  @rtype: boolean
617
  @return: C{True} if all of them exist, C{False} otherwise
618

619
  """
620
  missing = []
621
  for bridge in bridges_list:
622
    if not utils.BridgeExists(bridge):
623
      missing.append(bridge)
624

    
625
  if missing:
626
    _Fail("Missing bridges %s", ", ".join(missing))
627

    
628

    
629
def GetInstanceList(hypervisor_list):
630
  """Provides a list of instances.
631

632
  @type hypervisor_list: list
633
  @param hypervisor_list: the list of hypervisors to query information
634

635
  @rtype: list
636
  @return: a list of all running instances on the current node
637
    - instance1.example.com
638
    - instance2.example.com
639

640
  """
641
  results = []
642
  for hname in hypervisor_list:
643
    try:
644
      names = hypervisor.GetHypervisor(hname).ListInstances()
645
      results.extend(names)
646
    except errors.HypervisorError, err:
647
      _Fail("Error enumerating instances (hypervisor %s): %s",
648
            hname, err, exc=True)
649

    
650
  return results
651

    
652

    
653
def GetInstanceInfo(instance, hname):
654
  """Gives back the information about an instance as a dictionary.
655

656
  @type instance: string
657
  @param instance: the instance name
658
  @type hname: string
659
  @param hname: the hypervisor type of the instance
660

661
  @rtype: dict
662
  @return: dictionary with the following keys:
663
      - memory: memory size of instance (int)
664
      - state: xen state of instance (string)
665
      - time: cpu time of instance (float)
666

667
  """
668
  output = {}
669

    
670
  iinfo = hypervisor.GetHypervisor(hname).GetInstanceInfo(instance)
671
  if iinfo is not None:
672
    output['memory'] = iinfo[2]
673
    output['state'] = iinfo[4]
674
    output['time'] = iinfo[5]
675

    
676
  return output
677

    
678

    
679
def GetInstanceMigratable(instance):
680
  """Gives whether an instance can be migrated.
681

682
  @type instance: L{objects.Instance}
683
  @param instance: object representing the instance to be checked.
684

685
  @rtype: tuple
686
  @return: tuple of (result, description) where:
687
      - result: whether the instance can be migrated or not
688
      - description: a description of the issue, if relevant
689

690
  """
691
  hyper = hypervisor.GetHypervisor(instance.hypervisor)
692
  iname = instance.name
693
  if iname not in hyper.ListInstances():
694
    _Fail("Instance %s is not running", iname)
695

    
696
  for idx in range(len(instance.disks)):
697
    link_name = _GetBlockDevSymlinkPath(iname, idx)
698
    if not os.path.islink(link_name):
699
      _Fail("Instance %s was not restarted since ganeti 1.2.5", iname)
700

    
701

    
702
def GetAllInstancesInfo(hypervisor_list):
703
  """Gather data about all instances.
704

705
  This is the equivalent of L{GetInstanceInfo}, except that it
706
  computes data for all instances at once, thus being faster if one
707
  needs data about more than one instance.
708

709
  @type hypervisor_list: list
710
  @param hypervisor_list: list of hypervisors to query for instance data
711

712
  @rtype: dict
713
  @return: dictionary of instance: data, with data having the following keys:
714
      - memory: memory size of instance (int)
715
      - state: xen state of instance (string)
716
      - time: cpu time of instance (float)
717
      - vcpus: the number of vcpus
718

719
  """
720
  output = {}
721

    
722
  for hname in hypervisor_list:
723
    iinfo = hypervisor.GetHypervisor(hname).GetAllInstancesInfo()
724
    if iinfo:
725
      for name, _, memory, vcpus, state, times in iinfo:
726
        value = {
727
          'memory': memory,
728
          'vcpus': vcpus,
729
          'state': state,
730
          'time': times,
731
          }
732
        if name in output:
733
          # we only check static parameters, like memory and vcpus,
734
          # and not state and time which can change between the
735
          # invocations of the different hypervisors
736
          for key in 'memory', 'vcpus':
737
            if value[key] != output[name][key]:
738
              _Fail("Instance %s is running twice"
739
                    " with different parameters", name)
740
        output[name] = value
741

    
742
  return output
743

    
744

    
745
def InstanceOsAdd(instance, reinstall):
746
  """Add an OS to an instance.
747

748
  @type instance: L{objects.Instance}
749
  @param instance: Instance whose OS is to be installed
750
  @type reinstall: boolean
751
  @param reinstall: whether this is an instance reinstall
752
  @rtype: None
753

754
  """
755
  inst_os = OSFromDisk(instance.os)
756

    
757
  create_env = OSEnvironment(instance, inst_os)
758
  if reinstall:
759
    create_env['INSTANCE_REINSTALL'] = "1"
760

    
761
  logfile = "%s/add-%s-%s-%d.log" % (constants.LOG_OS_DIR, instance.os,
762
                                     instance.name, int(time.time()))
763

    
764
  result = utils.RunCmd([inst_os.create_script], env=create_env,
765
                        cwd=inst_os.path, output=logfile,)
766
  if result.failed:
767
    logging.error("os create command '%s' returned error: %s, logfile: %s,"
768
                  " output: %s", result.cmd, result.fail_reason, logfile,
769
                  result.output)
770
    lines = [utils.SafeEncode(val)
771
             for val in utils.TailFile(logfile, lines=20)]
772
    _Fail("OS create script failed (%s), last lines in the"
773
          " log file:\n%s", result.fail_reason, "\n".join(lines), log=False)
774

    
775

    
776
def RunRenameInstance(instance, old_name):
777
  """Run the OS rename script for an instance.
778

779
  @type instance: L{objects.Instance}
780
  @param instance: Instance whose OS is to be installed
781
  @type old_name: string
782
  @param old_name: previous instance name
783
  @rtype: boolean
784
  @return: the success of the operation
785

786
  """
787
  inst_os = OSFromDisk(instance.os)
788

    
789
  rename_env = OSEnvironment(instance, inst_os)
790
  rename_env['OLD_INSTANCE_NAME'] = old_name
791

    
792
  logfile = "%s/rename-%s-%s-%s-%d.log" % (constants.LOG_OS_DIR, instance.os,
793
                                           old_name,
794
                                           instance.name, int(time.time()))
795

    
796
  result = utils.RunCmd([inst_os.rename_script], env=rename_env,
797
                        cwd=inst_os.path, output=logfile)
798

    
799
  if result.failed:
800
    logging.error("os create command '%s' returned error: %s output: %s",
801
                  result.cmd, result.fail_reason, result.output)
802
    lines = [utils.SafeEncode(val)
803
             for val in utils.TailFile(logfile, lines=20)]
804
    _Fail("OS rename script failed (%s), last lines in the"
805
          " log file:\n%s", result.fail_reason, "\n".join(lines), log=False)
806

    
807

    
808
def _GetVGInfo(vg_name):
809
  """Get information about the volume group.
810

811
  @type vg_name: str
812
  @param vg_name: the volume group which we query
813
  @rtype: dict
814
  @return:
815
    A dictionary with the following keys:
816
      - C{vg_size} is the total size of the volume group in MiB
817
      - C{vg_free} is the free size of the volume group in MiB
818
      - C{pv_count} are the number of physical disks in that VG
819

820
    If an error occurs during gathering of data, we return the same dict
821
    with keys all set to None.
822

823
  """
824
  retdic = dict.fromkeys(["vg_size", "vg_free", "pv_count"])
825

    
826
  retval = utils.RunCmd(["vgs", "-ovg_size,vg_free,pv_count", "--noheadings",
827
                         "--nosuffix", "--units=m", "--separator=:", vg_name])
828

    
829
  if retval.failed:
830
    logging.error("volume group %s not present", vg_name)
831
    return retdic
832
  valarr = retval.stdout.strip().rstrip(':').split(':')
833
  if len(valarr) == 3:
834
    try:
835
      retdic = {
836
        "vg_size": int(round(float(valarr[0]), 0)),
837
        "vg_free": int(round(float(valarr[1]), 0)),
838
        "pv_count": int(valarr[2]),
839
        }
840
    except ValueError, err:
841
      logging.exception("Fail to parse vgs output: %s", err)
842
  else:
843
    logging.error("vgs output has the wrong number of fields (expected"
844
                  " three): %s", str(valarr))
845
  return retdic
846

    
847

    
848
def _GetBlockDevSymlinkPath(instance_name, idx):
849
  return os.path.join(constants.DISK_LINKS_DIR,
850
                      "%s:%d" % (instance_name, idx))
851

    
852

    
853
def _SymlinkBlockDev(instance_name, device_path, idx):
854
  """Set up symlinks to a instance's block device.
855

856
  This is an auxiliary function run when an instance is start (on the primary
857
  node) or when an instance is migrated (on the target node).
858

859

860
  @param instance_name: the name of the target instance
861
  @param device_path: path of the physical block device, on the node
862
  @param idx: the disk index
863
  @return: absolute path to the disk's symlink
864

865
  """
866
  link_name = _GetBlockDevSymlinkPath(instance_name, idx)
867
  try:
868
    os.symlink(device_path, link_name)
869
  except OSError, err:
870
    if err.errno == errno.EEXIST:
871
      if (not os.path.islink(link_name) or
872
          os.readlink(link_name) != device_path):
873
        os.remove(link_name)
874
        os.symlink(device_path, link_name)
875
    else:
876
      raise
877

    
878
  return link_name
879

    
880

    
881
def _RemoveBlockDevLinks(instance_name, disks):
882
  """Remove the block device symlinks belonging to the given instance.
883

884
  """
885
  for idx, _ in enumerate(disks):
886
    link_name = _GetBlockDevSymlinkPath(instance_name, idx)
887
    if os.path.islink(link_name):
888
      try:
889
        os.remove(link_name)
890
      except OSError:
891
        logging.exception("Can't remove symlink '%s'", link_name)
892

    
893

    
894
def _GatherAndLinkBlockDevs(instance):
895
  """Set up an instance's block device(s).
896

897
  This is run on the primary node at instance startup. The block
898
  devices must be already assembled.
899

900
  @type instance: L{objects.Instance}
901
  @param instance: the instance whose disks we shoul assemble
902
  @rtype: list
903
  @return: list of (disk_object, device_path)
904

905
  """
906
  block_devices = []
907
  for idx, disk in enumerate(instance.disks):
908
    device = _RecursiveFindBD(disk)
909
    if device is None:
910
      raise errors.BlockDeviceError("Block device '%s' is not set up." %
911
                                    str(disk))
912
    device.Open()
913
    try:
914
      link_name = _SymlinkBlockDev(instance.name, device.dev_path, idx)
915
    except OSError, e:
916
      raise errors.BlockDeviceError("Cannot create block device symlink: %s" %
917
                                    e.strerror)
918

    
919
    block_devices.append((disk, link_name))
920

    
921
  return block_devices
922

    
923

    
924
def StartInstance(instance):
925
  """Start an instance.
926

927
  @type instance: L{objects.Instance}
928
  @param instance: the instance object
929
  @rtype: None
930

931
  """
932
  running_instances = GetInstanceList([instance.hypervisor])
933

    
934
  if instance.name in running_instances:
935
    logging.info("Instance %s already running, not starting", instance.name)
936
    return
937

    
938
  try:
939
    block_devices = _GatherAndLinkBlockDevs(instance)
940
    hyper = hypervisor.GetHypervisor(instance.hypervisor)
941
    hyper.StartInstance(instance, block_devices)
942
  except errors.BlockDeviceError, err:
943
    _Fail("Block device error: %s", err, exc=True)
944
  except errors.HypervisorError, err:
945
    _RemoveBlockDevLinks(instance.name, instance.disks)
946
    _Fail("Hypervisor error: %s", err, exc=True)
947

    
948

    
949
def InstanceShutdown(instance):
950
  """Shut an instance down.
951

952
  @note: this functions uses polling with a hardcoded timeout.
953

954
  @type instance: L{objects.Instance}
955
  @param instance: the instance object
956
  @rtype: None
957

958
  """
959
  hv_name = instance.hypervisor
960
  running_instances = GetInstanceList([hv_name])
961
  iname = instance.name
962

    
963
  if iname not in running_instances:
964
    logging.info("Instance %s not running, doing nothing", iname)
965
    return
966

    
967
  hyper = hypervisor.GetHypervisor(hv_name)
968
  try:
969
    hyper.StopInstance(instance)
970
  except errors.HypervisorError, err:
971
    _Fail("Failed to stop instance %s: %s", iname, err)
972

    
973
  # test every 10secs for 2min
974

    
975
  time.sleep(1)
976
  for _ in range(11):
977
    if instance.name not in GetInstanceList([hv_name]):
978
      break
979
    time.sleep(10)
980
  else:
981
    # the shutdown did not succeed
982
    logging.error("Shutdown of '%s' unsuccessful, using destroy", iname)
983

    
984
    try:
985
      hyper.StopInstance(instance, force=True)
986
    except errors.HypervisorError, err:
987
      _Fail("Failed to force stop instance %s: %s", iname, err)
988

    
989
    time.sleep(1)
990
    if instance.name in GetInstanceList([hv_name]):
991
      _Fail("Could not shutdown instance %s even by destroy", iname)
992

    
993
  _RemoveBlockDevLinks(iname, instance.disks)
994

    
995

    
996
def InstanceReboot(instance, reboot_type):
997
  """Reboot an instance.
998

999
  @type instance: L{objects.Instance}
1000
  @param instance: the instance object to reboot
1001
  @type reboot_type: str
1002
  @param reboot_type: the type of reboot, one the following
1003
    constants:
1004
      - L{constants.INSTANCE_REBOOT_SOFT}: only reboot the
1005
        instance OS, do not recreate the VM
1006
      - L{constants.INSTANCE_REBOOT_HARD}: tear down and
1007
        restart the VM (at the hypervisor level)
1008
      - the other reboot type (L{constants.INSTANCE_REBOOT_FULL}) is
1009
        not accepted here, since that mode is handled differently, in
1010
        cmdlib, and translates into full stop and start of the
1011
        instance (instead of a call_instance_reboot RPC)
1012
  @rtype: None
1013

1014
  """
1015
  running_instances = GetInstanceList([instance.hypervisor])
1016

    
1017
  if instance.name not in running_instances:
1018
    _Fail("Cannot reboot instance %s that is not running", instance.name)
1019

    
1020
  hyper = hypervisor.GetHypervisor(instance.hypervisor)
1021
  if reboot_type == constants.INSTANCE_REBOOT_SOFT:
1022
    try:
1023
      hyper.RebootInstance(instance)
1024
    except errors.HypervisorError, err:
1025
      _Fail("Failed to soft reboot instance %s: %s", instance.name, err)
1026
  elif reboot_type == constants.INSTANCE_REBOOT_HARD:
1027
    try:
1028
      InstanceShutdown(instance)
1029
      return StartInstance(instance)
1030
    except errors.HypervisorError, err:
1031
      _Fail("Failed to hard reboot instance %s: %s", instance.name, err)
1032
  else:
1033
    _Fail("Invalid reboot_type received: %s", reboot_type)
1034

    
1035

    
1036
def MigrationInfo(instance):
1037
  """Gather information about an instance to be migrated.
1038

1039
  @type instance: L{objects.Instance}
1040
  @param instance: the instance definition
1041

1042
  """
1043
  hyper = hypervisor.GetHypervisor(instance.hypervisor)
1044
  try:
1045
    info = hyper.MigrationInfo(instance)
1046
  except errors.HypervisorError, err:
1047
    _Fail("Failed to fetch migration information: %s", err, exc=True)
1048
  return info
1049

    
1050

    
1051
def AcceptInstance(instance, info, target):
1052
  """Prepare the node to accept an instance.
1053

1054
  @type instance: L{objects.Instance}
1055
  @param instance: the instance definition
1056
  @type info: string/data (opaque)
1057
  @param info: migration information, from the source node
1058
  @type target: string
1059
  @param target: target host (usually ip), on this node
1060

1061
  """
1062
  hyper = hypervisor.GetHypervisor(instance.hypervisor)
1063
  try:
1064
    hyper.AcceptInstance(instance, info, target)
1065
  except errors.HypervisorError, err:
1066
    _Fail("Failed to accept instance: %s", err, exc=True)
1067

    
1068

    
1069
def FinalizeMigration(instance, info, success):
1070
  """Finalize any preparation to accept an instance.
1071

1072
  @type instance: L{objects.Instance}
1073
  @param instance: the instance definition
1074
  @type info: string/data (opaque)
1075
  @param info: migration information, from the source node
1076
  @type success: boolean
1077
  @param success: whether the migration was a success or a failure
1078

1079
  """
1080
  hyper = hypervisor.GetHypervisor(instance.hypervisor)
1081
  try:
1082
    hyper.FinalizeMigration(instance, info, success)
1083
  except errors.HypervisorError, err:
1084
    _Fail("Failed to finalize migration: %s", err, exc=True)
1085

    
1086

    
1087
def MigrateInstance(instance, target, live):
1088
  """Migrates an instance to another node.
1089

1090
  @type instance: L{objects.Instance}
1091
  @param instance: the instance definition
1092
  @type target: string
1093
  @param target: the target node name
1094
  @type live: boolean
1095
  @param live: whether the migration should be done live or not (the
1096
      interpretation of this parameter is left to the hypervisor)
1097
  @rtype: tuple
1098
  @return: a tuple of (success, msg) where:
1099
      - succes is a boolean denoting the success/failure of the operation
1100
      - msg is a string with details in case of failure
1101

1102
  """
1103
  hyper = hypervisor.GetHypervisor(instance.hypervisor)
1104

    
1105
  try:
1106
    hyper.MigrateInstance(instance.name, target, live)
1107
  except errors.HypervisorError, err:
1108
    _Fail("Failed to migrate instance: %s", err, exc=True)
1109

    
1110

    
1111
def BlockdevCreate(disk, size, owner, on_primary, info):
1112
  """Creates a block device for an instance.
1113

1114
  @type disk: L{objects.Disk}
1115
  @param disk: the object describing the disk we should create
1116
  @type size: int
1117
  @param size: the size of the physical underlying device, in MiB
1118
  @type owner: str
1119
  @param owner: the name of the instance for which disk is created,
1120
      used for device cache data
1121
  @type on_primary: boolean
1122
  @param on_primary:  indicates if it is the primary node or not
1123
  @type info: string
1124
  @param info: string that will be sent to the physical device
1125
      creation, used for example to set (LVM) tags on LVs
1126

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

1131
  """
1132
  clist = []
1133
  if disk.children:
1134
    for child in disk.children:
1135
      try:
1136
        crdev = _RecursiveAssembleBD(child, owner, on_primary)
1137
      except errors.BlockDeviceError, err:
1138
        _Fail("Can't assemble device %s: %s", child, err)
1139
      if on_primary or disk.AssembleOnSecondary():
1140
        # we need the children open in case the device itself has to
1141
        # be assembled
1142
        try:
1143
          crdev.Open()
1144
        except errors.BlockDeviceError, err:
1145
          _Fail("Can't make child '%s' read-write: %s", child, err)
1146
      clist.append(crdev)
1147

    
1148
  try:
1149
    device = bdev.Create(disk.dev_type, disk.physical_id, clist, disk.size)
1150
  except errors.BlockDeviceError, err:
1151
    _Fail("Can't create block device: %s", err)
1152

    
1153
  if on_primary or disk.AssembleOnSecondary():
1154
    try:
1155
      device.Assemble()
1156
    except errors.BlockDeviceError, err:
1157
      _Fail("Can't assemble device after creation, unusual event: %s", err)
1158
    device.SetSyncSpeed(constants.SYNC_SPEED)
1159
    if on_primary or disk.OpenOnSecondary():
1160
      try:
1161
        device.Open(force=True)
1162
      except errors.BlockDeviceError, err:
1163
        _Fail("Can't make device r/w after creation, unusual event: %s", err)
1164
    DevCacheManager.UpdateCache(device.dev_path, owner,
1165
                                on_primary, disk.iv_name)
1166

    
1167
  device.SetInfo(info)
1168

    
1169
  return device.unique_id
1170

    
1171

    
1172
def BlockdevRemove(disk):
1173
  """Remove a block device.
1174

1175
  @note: This is intended to be called recursively.
1176

1177
  @type disk: L{objects.Disk}
1178
  @param disk: the disk object we should remove
1179
  @rtype: boolean
1180
  @return: the success of the operation
1181

1182
  """
1183
  msgs = []
1184
  try:
1185
    rdev = _RecursiveFindBD(disk)
1186
  except errors.BlockDeviceError, err:
1187
    # probably can't attach
1188
    logging.info("Can't attach to device %s in remove", disk)
1189
    rdev = None
1190
  if rdev is not None:
1191
    r_path = rdev.dev_path
1192
    try:
1193
      rdev.Remove()
1194
    except errors.BlockDeviceError, err:
1195
      msgs.append(str(err))
1196
    if not msgs:
1197
      DevCacheManager.RemoveCache(r_path)
1198

    
1199
  if disk.children:
1200
    for child in disk.children:
1201
      try:
1202
        BlockdevRemove(child)
1203
      except RPCFail, err:
1204
        msgs.append(str(err))
1205

    
1206
  if msgs:
1207
    _Fail("; ".join(msgs))
1208

    
1209

    
1210
def _RecursiveAssembleBD(disk, owner, as_primary):
1211
  """Activate a block device for an instance.
1212

1213
  This is run on the primary and secondary nodes for an instance.
1214

1215
  @note: this function is called recursively.
1216

1217
  @type disk: L{objects.Disk}
1218
  @param disk: the disk we try to assemble
1219
  @type owner: str
1220
  @param owner: the name of the instance which owns the disk
1221
  @type as_primary: boolean
1222
  @param as_primary: if we should make the block device
1223
      read/write
1224

1225
  @return: the assembled device or None (in case no device
1226
      was assembled)
1227
  @raise errors.BlockDeviceError: in case there is an error
1228
      during the activation of the children or the device
1229
      itself
1230

1231
  """
1232
  children = []
1233
  if disk.children:
1234
    mcn = disk.ChildrenNeeded()
1235
    if mcn == -1:
1236
      mcn = 0 # max number of Nones allowed
1237
    else:
1238
      mcn = len(disk.children) - mcn # max number of Nones
1239
    for chld_disk in disk.children:
1240
      try:
1241
        cdev = _RecursiveAssembleBD(chld_disk, owner, as_primary)
1242
      except errors.BlockDeviceError, err:
1243
        if children.count(None) >= mcn:
1244
          raise
1245
        cdev = None
1246
        logging.error("Error in child activation (but continuing): %s",
1247
                      str(err))
1248
      children.append(cdev)
1249

    
1250
  if as_primary or disk.AssembleOnSecondary():
1251
    r_dev = bdev.Assemble(disk.dev_type, disk.physical_id, children, disk.size)
1252
    r_dev.SetSyncSpeed(constants.SYNC_SPEED)
1253
    result = r_dev
1254
    if as_primary or disk.OpenOnSecondary():
1255
      r_dev.Open()
1256
    DevCacheManager.UpdateCache(r_dev.dev_path, owner,
1257
                                as_primary, disk.iv_name)
1258

    
1259
  else:
1260
    result = True
1261
  return result
1262

    
1263

    
1264
def BlockdevAssemble(disk, owner, as_primary):
1265
  """Activate a block device for an instance.
1266

1267
  This is a wrapper over _RecursiveAssembleBD.
1268

1269
  @rtype: str or boolean
1270
  @return: a C{/dev/...} path for primary nodes, and
1271
      C{True} for secondary nodes
1272

1273
  """
1274
  try:
1275
    result = _RecursiveAssembleBD(disk, owner, as_primary)
1276
    if isinstance(result, bdev.BlockDev):
1277
      result = result.dev_path
1278
  except errors.BlockDeviceError, err:
1279
    _Fail("Error while assembling disk: %s", err, exc=True)
1280

    
1281
  return result
1282

    
1283

    
1284
def BlockdevShutdown(disk):
1285
  """Shut down a block device.
1286

1287
  First, if the device is assembled (Attach() is successful), then
1288
  the device is shutdown. Then the children of the device are
1289
  shutdown.
1290

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

1295
  @type disk: L{objects.Disk}
1296
  @param disk: the description of the disk we should
1297
      shutdown
1298
  @rtype: None
1299

1300
  """
1301
  msgs = []
1302
  r_dev = _RecursiveFindBD(disk)
1303
  if r_dev is not None:
1304
    r_path = r_dev.dev_path
1305
    try:
1306
      r_dev.Shutdown()
1307
      DevCacheManager.RemoveCache(r_path)
1308
    except errors.BlockDeviceError, err:
1309
      msgs.append(str(err))
1310

    
1311
  if disk.children:
1312
    for child in disk.children:
1313
      try:
1314
        BlockdevShutdown(child)
1315
      except RPCFail, err:
1316
        msgs.append(str(err))
1317

    
1318
  if msgs:
1319
    _Fail("; ".join(msgs))
1320

    
1321

    
1322
def BlockdevAddchildren(parent_cdev, new_cdevs):
1323
  """Extend a mirrored block device.
1324

1325
  @type parent_cdev: L{objects.Disk}
1326
  @param parent_cdev: the disk to which we should add children
1327
  @type new_cdevs: list of L{objects.Disk}
1328
  @param new_cdevs: the list of children which we should add
1329
  @rtype: None
1330

1331
  """
1332
  parent_bdev = _RecursiveFindBD(parent_cdev)
1333
  if parent_bdev is None:
1334
    _Fail("Can't find parent device '%s' in add children", parent_cdev)
1335
  new_bdevs = [_RecursiveFindBD(disk) for disk in new_cdevs]
1336
  if new_bdevs.count(None) > 0:
1337
    _Fail("Can't find new device(s) to add: %s:%s", new_bdevs, new_cdevs)
1338
  parent_bdev.AddChildren(new_bdevs)
1339

    
1340

    
1341
def BlockdevRemovechildren(parent_cdev, new_cdevs):
1342
  """Shrink a mirrored block device.
1343

1344
  @type parent_cdev: L{objects.Disk}
1345
  @param parent_cdev: the disk from which we should remove children
1346
  @type new_cdevs: list of L{objects.Disk}
1347
  @param new_cdevs: the list of children which we should remove
1348
  @rtype: None
1349

1350
  """
1351
  parent_bdev = _RecursiveFindBD(parent_cdev)
1352
  if parent_bdev is None:
1353
    _Fail("Can't find parent device '%s' in remove children", parent_cdev)
1354
  devs = []
1355
  for disk in new_cdevs:
1356
    rpath = disk.StaticDevPath()
1357
    if rpath is None:
1358
      bd = _RecursiveFindBD(disk)
1359
      if bd is None:
1360
        _Fail("Can't find device %s while removing children", disk)
1361
      else:
1362
        devs.append(bd.dev_path)
1363
    else:
1364
      devs.append(rpath)
1365
  parent_bdev.RemoveChildren(devs)
1366

    
1367

    
1368
def BlockdevGetmirrorstatus(disks):
1369
  """Get the mirroring status of a list of devices.
1370

1371
  @type disks: list of L{objects.Disk}
1372
  @param disks: the list of disks which we should query
1373
  @rtype: disk
1374
  @return:
1375
      a list of (mirror_done, estimated_time) tuples, which
1376
      are the result of L{bdev.BlockDev.CombinedSyncStatus}
1377
  @raise errors.BlockDeviceError: if any of the disks cannot be
1378
      found
1379

1380
  """
1381
  stats = []
1382
  for dsk in disks:
1383
    rbd = _RecursiveFindBD(dsk)
1384
    if rbd is None:
1385
      _Fail("Can't find device %s", dsk)
1386
    stats.append(rbd.CombinedSyncStatus())
1387
  return stats
1388

    
1389

    
1390
def _RecursiveFindBD(disk):
1391
  """Check if a device is activated.
1392

1393
  If so, return information about the real device.
1394

1395
  @type disk: L{objects.Disk}
1396
  @param disk: the disk object we need to find
1397

1398
  @return: None if the device can't be found,
1399
      otherwise the device instance
1400

1401
  """
1402
  children = []
1403
  if disk.children:
1404
    for chdisk in disk.children:
1405
      children.append(_RecursiveFindBD(chdisk))
1406

    
1407
  return bdev.FindDevice(disk.dev_type, disk.physical_id, children, disk.size)
1408

    
1409

    
1410
def BlockdevFind(disk):
1411
  """Check if a device is activated.
1412

1413
  If it is, return information about the real device.
1414

1415
  @type disk: L{objects.Disk}
1416
  @param disk: the disk to find
1417
  @rtype: None or tuple
1418
  @return: None if the disk cannot be found, otherwise a
1419
      tuple (device_path, major, minor, sync_percent,
1420
      estimated_time, is_degraded)
1421

1422
  """
1423
  try:
1424
    rbd = _RecursiveFindBD(disk)
1425
  except errors.BlockDeviceError, err:
1426
    _Fail("Failed to find device: %s", err, exc=True)
1427
  if rbd is None:
1428
    return None
1429
  return (rbd.dev_path, rbd.major, rbd.minor) + rbd.GetSyncStatus()
1430

    
1431

    
1432
def UploadFile(file_name, data, mode, uid, gid, atime, mtime):
1433
  """Write a file to the filesystem.
1434

1435
  This allows the master to overwrite(!) a file. It will only perform
1436
  the operation if the file belongs to a list of configuration files.
1437

1438
  @type file_name: str
1439
  @param file_name: the target file name
1440
  @type data: str
1441
  @param data: the new contents of the file
1442
  @type mode: int
1443
  @param mode: the mode to give the file (can be None)
1444
  @type uid: int
1445
  @param uid: the owner of the file (can be -1 for default)
1446
  @type gid: int
1447
  @param gid: the group of the file (can be -1 for default)
1448
  @type atime: float
1449
  @param atime: the atime to set on the file (can be None)
1450
  @type mtime: float
1451
  @param mtime: the mtime to set on the file (can be None)
1452
  @rtype: None
1453

1454
  """
1455
  if not os.path.isabs(file_name):
1456
    _Fail("Filename passed to UploadFile is not absolute: '%s'", file_name)
1457

    
1458
  if file_name not in _ALLOWED_UPLOAD_FILES:
1459
    _Fail("Filename passed to UploadFile not in allowed upload targets: '%s'",
1460
          file_name)
1461

    
1462
  raw_data = _Decompress(data)
1463

    
1464
  utils.WriteFile(file_name, data=raw_data, mode=mode, uid=uid, gid=gid,
1465
                  atime=atime, mtime=mtime)
1466

    
1467

    
1468
def WriteSsconfFiles(values):
1469
  """Update all ssconf files.
1470

1471
  Wrapper around the SimpleStore.WriteFiles.
1472

1473
  """
1474
  ssconf.SimpleStore().WriteFiles(values)
1475

    
1476

    
1477
def _ErrnoOrStr(err):
1478
  """Format an EnvironmentError exception.
1479

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

1484
  @type err: L{EnvironmentError}
1485
  @param err: the exception to format
1486

1487
  """
1488
  if hasattr(err, 'errno'):
1489
    detail = errno.errorcode[err.errno]
1490
  else:
1491
    detail = str(err)
1492
  return detail
1493

    
1494

    
1495
def _OSOndiskAPIVersion(name, os_dir):
1496
  """Compute and return the API version of a given OS.
1497

1498
  This function will try to read the API version of the OS given by
1499
  the 'name' parameter and residing in the 'os_dir' directory.
1500

1501
  @type name: str
1502
  @param name: the OS name we should look for
1503
  @type os_dir: str
1504
  @param os_dir: the directory inwhich we should look for the OS
1505
  @rtype: tuple
1506
  @return: tuple (status, data) with status denoting the validity and
1507
      data holding either the vaid versions or an error message
1508

1509
  """
1510
  api_file = os.path.sep.join([os_dir, "ganeti_api_version"])
1511

    
1512
  try:
1513
    st = os.stat(api_file)
1514
  except EnvironmentError, err:
1515
    return False, ("Required file 'ganeti_api_version' file not"
1516
                   " found under path %s: %s" % (os_dir, _ErrnoOrStr(err)))
1517

    
1518
  if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1519
    return False, ("File 'ganeti_api_version' file at %s is not"
1520
                   " a regular file" % os_dir)
1521

    
1522
  try:
1523
    api_versions = utils.ReadFile(api_file).splitlines()
1524
  except EnvironmentError, err:
1525
    return False, ("Error while reading the API version file at %s: %s" %
1526
                   (api_file, _ErrnoOrStr(err)))
1527

    
1528
  try:
1529
    api_versions = [int(version.strip()) for version in api_versions]
1530
  except (TypeError, ValueError), err:
1531
    return False, ("API version(s) can't be converted to integer: %s" %
1532
                   str(err))
1533

    
1534
  return True, api_versions
1535

    
1536

    
1537
def DiagnoseOS(top_dirs=None):
1538
  """Compute the validity for all OSes.
1539

1540
  @type top_dirs: list
1541
  @param top_dirs: the list of directories in which to
1542
      search (if not given defaults to
1543
      L{constants.OS_SEARCH_PATH})
1544
  @rtype: list of L{objects.OS}
1545
  @return: a list of tuples (name, path, status, diagnose)
1546
      for all (potential) OSes under all search paths, where:
1547
          - name is the (potential) OS name
1548
          - path is the full path to the OS
1549
          - status True/False is the validity of the OS
1550
          - diagnose is the error message for an invalid OS, otherwise empty
1551

1552
  """
1553
  if top_dirs is None:
1554
    top_dirs = constants.OS_SEARCH_PATH
1555

    
1556
  result = []
1557
  for dir_name in top_dirs:
1558
    if os.path.isdir(dir_name):
1559
      try:
1560
        f_names = utils.ListVisibleFiles(dir_name)
1561
      except EnvironmentError, err:
1562
        logging.exception("Can't list the OS directory %s: %s", dir_name, err)
1563
        break
1564
      for name in f_names:
1565
        os_path = os.path.sep.join([dir_name, name])
1566
        status, os_inst = _TryOSFromDisk(name, base_dir=dir_name)
1567
        if status:
1568
          diagnose = ""
1569
        else:
1570
          diagnose = os_inst
1571
        result.append((name, os_path, status, diagnose))
1572

    
1573
  return result
1574

    
1575

    
1576
def _TryOSFromDisk(name, base_dir=None):
1577
  """Create an OS instance from disk.
1578

1579
  This function will return an OS instance if the given name is a
1580
  valid OS name.
1581

1582
  @type base_dir: string
1583
  @keyword base_dir: Base directory containing OS installations.
1584
                     Defaults to a search in all the OS_SEARCH_PATH dirs.
1585
  @rtype: tuple
1586
  @return: success and either the OS instance if we find a valid one,
1587
      or error message
1588

1589
  """
1590
  if base_dir is None:
1591
    os_dir = utils.FindFile(name, constants.OS_SEARCH_PATH, os.path.isdir)
1592
    if os_dir is None:
1593
      return False, "Directory for OS %s not found in search path" % name
1594
  else:
1595
    os_dir = os.path.sep.join([base_dir, name])
1596

    
1597
  status, api_versions = _OSOndiskAPIVersion(name, os_dir)
1598
  if not status:
1599
    # push the error up
1600
    return status, api_versions
1601

    
1602
  if not constants.OS_API_VERSIONS.intersection(api_versions):
1603
    return False, ("API version mismatch for path '%s': found %s, want %s." %
1604
                   (os_dir, api_versions, constants.OS_API_VERSIONS))
1605

    
1606
  # OS Scripts dictionary, we will populate it with the actual script names
1607
  os_scripts = dict.fromkeys(constants.OS_SCRIPTS)
1608

    
1609
  for script in os_scripts:
1610
    os_scripts[script] = os.path.sep.join([os_dir, script])
1611

    
1612
    try:
1613
      st = os.stat(os_scripts[script])
1614
    except EnvironmentError, err:
1615
      return False, ("Script '%s' under path '%s' is missing (%s)" %
1616
                     (script, os_dir, _ErrnoOrStr(err)))
1617

    
1618
    if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
1619
      return False, ("Script '%s' under path '%s' is not executable" %
1620
                     (script, os_dir))
1621

    
1622
    if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1623
      return False, ("Script '%s' under path '%s' is not a regular file" %
1624
                     (script, os_dir))
1625

    
1626
  os_obj = objects.OS(name=name, path=os_dir,
1627
                      create_script=os_scripts[constants.OS_SCRIPT_CREATE],
1628
                      export_script=os_scripts[constants.OS_SCRIPT_EXPORT],
1629
                      import_script=os_scripts[constants.OS_SCRIPT_IMPORT],
1630
                      rename_script=os_scripts[constants.OS_SCRIPT_RENAME],
1631
                      api_versions=api_versions)
1632
  return True, os_obj
1633

    
1634

    
1635
def OSFromDisk(name, base_dir=None):
1636
  """Create an OS instance from disk.
1637

1638
  This function will return an OS instance if the given name is a
1639
  valid OS name. Otherwise, it will raise an appropriate
1640
  L{RPCFail} exception, detailing why this is not a valid OS.
1641

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

1645
  @type base_dir: string
1646
  @keyword base_dir: Base directory containing OS installations.
1647
                     Defaults to a search in all the OS_SEARCH_PATH dirs.
1648
  @rtype: L{objects.OS}
1649
  @return: the OS instance if we find a valid one
1650
  @raise RPCFail: if we don't find a valid OS
1651

1652
  """
1653
  status, payload = _TryOSFromDisk(name, base_dir)
1654

    
1655
  if not status:
1656
    _Fail(payload)
1657

    
1658
  return payload
1659

    
1660

    
1661
def OSEnvironment(instance, os, debug=0):
1662
  """Calculate the environment for an os script.
1663

1664
  @type instance: L{objects.Instance}
1665
  @param instance: target instance for the os script run
1666
  @type os: L{objects.OS}
1667
  @param os: operating system for which the environment is being built
1668
  @type debug: integer
1669
  @param debug: debug level (0 or 1, for OS Api 10)
1670
  @rtype: dict
1671
  @return: dict of environment variables
1672
  @raise errors.BlockDeviceError: if the block device
1673
      cannot be found
1674

1675
  """
1676
  result = {}
1677
  api_version = max(constants.OS_API_VERSIONS.intersection(os.api_versions))
1678
  result['OS_API_VERSION'] = '%d' % api_version
1679
  result['INSTANCE_NAME'] = instance.name
1680
  result['INSTANCE_OS'] = instance.os
1681
  result['HYPERVISOR'] = instance.hypervisor
1682
  result['DISK_COUNT'] = '%d' % len(instance.disks)
1683
  result['NIC_COUNT'] = '%d' % len(instance.nics)
1684
  result['DEBUG_LEVEL'] = '%d' % debug
1685
  for idx, disk in enumerate(instance.disks):
1686
    real_disk = _RecursiveFindBD(disk)
1687
    if real_disk is None:
1688
      raise errors.BlockDeviceError("Block device '%s' is not set up" %
1689
                                    str(disk))
1690
    real_disk.Open()
1691
    result['DISK_%d_PATH' % idx] = real_disk.dev_path
1692
    result['DISK_%d_ACCESS' % idx] = disk.mode
1693
    if constants.HV_DISK_TYPE in instance.hvparams:
1694
      result['DISK_%d_FRONTEND_TYPE' % idx] = \
1695
        instance.hvparams[constants.HV_DISK_TYPE]
1696
    if disk.dev_type in constants.LDS_BLOCK:
1697
      result['DISK_%d_BACKEND_TYPE' % idx] = 'block'
1698
    elif disk.dev_type == constants.LD_FILE:
1699
      result['DISK_%d_BACKEND_TYPE' % idx] = \
1700
        'file:%s' % disk.physical_id[0]
1701
  for idx, nic in enumerate(instance.nics):
1702
    result['NIC_%d_MAC' % idx] = nic.mac
1703
    if nic.ip:
1704
      result['NIC_%d_IP' % idx] = nic.ip
1705
    result['NIC_%d_MODE' % idx] = nic.nicparams[constants.NIC_MODE]
1706
    if nic.nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED:
1707
      result['NIC_%d_BRIDGE' % idx] = nic.nicparams[constants.NIC_LINK]
1708
    if nic.nicparams[constants.NIC_LINK]:
1709
      result['NIC_%d_LINK' % idx] = nic.nicparams[constants.NIC_LINK]
1710
    if constants.HV_NIC_TYPE in instance.hvparams:
1711
      result['NIC_%d_FRONTEND_TYPE' % idx] = \
1712
        instance.hvparams[constants.HV_NIC_TYPE]
1713

    
1714
  for source, kind in [(instance.beparams, "BE"), (instance.hvparams, "HV")]:
1715
    for key, value in source.items():
1716
      result["INSTANCE_%s_%s" % (kind, key)] = str(value)
1717

    
1718
  return result
1719

    
1720
def BlockdevGrow(disk, amount):
1721
  """Grow a stack of block devices.
1722

1723
  This function is called recursively, with the childrens being the
1724
  first ones to resize.
1725

1726
  @type disk: L{objects.Disk}
1727
  @param disk: the disk to be grown
1728
  @rtype: (status, result)
1729
  @return: a tuple with the status of the operation
1730
      (True/False), and the errors message if status
1731
      is False
1732

1733
  """
1734
  r_dev = _RecursiveFindBD(disk)
1735
  if r_dev is None:
1736
    _Fail("Cannot find block device %s", disk)
1737

    
1738
  try:
1739
    r_dev.Grow(amount)
1740
  except errors.BlockDeviceError, err:
1741
    _Fail("Failed to grow block device: %s", err, exc=True)
1742

    
1743

    
1744
def BlockdevSnapshot(disk):
1745
  """Create a snapshot copy of a block device.
1746

1747
  This function is called recursively, and the snapshot is actually created
1748
  just for the leaf lvm backend device.
1749

1750
  @type disk: L{objects.Disk}
1751
  @param disk: the disk to be snapshotted
1752
  @rtype: string
1753
  @return: snapshot disk path
1754

1755
  """
1756
  if disk.children:
1757
    if len(disk.children) == 1:
1758
      # only one child, let's recurse on it
1759
      return BlockdevSnapshot(disk.children[0])
1760
    else:
1761
      # more than one child, choose one that matches
1762
      for child in disk.children:
1763
        if child.size == disk.size:
1764
          # return implies breaking the loop
1765
          return BlockdevSnapshot(child)
1766
  elif disk.dev_type == constants.LD_LV:
1767
    r_dev = _RecursiveFindBD(disk)
1768
    if r_dev is not None:
1769
      # let's stay on the safe side and ask for the full size, for now
1770
      return r_dev.Snapshot(disk.size)
1771
    else:
1772
      _Fail("Cannot find block device %s", disk)
1773
  else:
1774
    _Fail("Cannot snapshot non-lvm block device '%s' of type '%s'",
1775
          disk.unique_id, disk.dev_type)
1776

    
1777

    
1778
def ExportSnapshot(disk, dest_node, instance, cluster_name, idx):
1779
  """Export a block device snapshot to a remote node.
1780

1781
  @type disk: L{objects.Disk}
1782
  @param disk: the description of the disk to export
1783
  @type dest_node: str
1784
  @param dest_node: the destination node to export to
1785
  @type instance: L{objects.Instance}
1786
  @param instance: the instance object to whom the disk belongs
1787
  @type cluster_name: str
1788
  @param cluster_name: the cluster name, needed for SSH hostalias
1789
  @type idx: int
1790
  @param idx: the index of the disk in the instance's disk list,
1791
      used to export to the OS scripts environment
1792
  @rtype: None
1793

1794
  """
1795
  inst_os = OSFromDisk(instance.os)
1796
  export_env = OSEnvironment(instance, inst_os)
1797

    
1798
  export_script = inst_os.export_script
1799

    
1800
  logfile = "%s/exp-%s-%s-%s.log" % (constants.LOG_OS_DIR, inst_os.name,
1801
                                     instance.name, int(time.time()))
1802
  if not os.path.exists(constants.LOG_OS_DIR):
1803
    os.mkdir(constants.LOG_OS_DIR, 0750)
1804
  real_disk = _RecursiveFindBD(disk)
1805
  if real_disk is None:
1806
    _Fail("Block device '%s' is not set up", disk)
1807

    
1808
  real_disk.Open()
1809

    
1810
  export_env['EXPORT_DEVICE'] = real_disk.dev_path
1811
  export_env['EXPORT_INDEX'] = str(idx)
1812

    
1813
  destdir = os.path.join(constants.EXPORT_DIR, instance.name + ".new")
1814
  destfile = disk.physical_id[1]
1815

    
1816
  # the target command is built out of three individual commands,
1817
  # which are joined by pipes; we check each individual command for
1818
  # valid parameters
1819
  expcmd = utils.BuildShellCmd("cd %s; %s 2>%s", inst_os.path,
1820
                               export_script, logfile)
1821

    
1822
  comprcmd = "gzip"
1823

    
1824
  destcmd = utils.BuildShellCmd("mkdir -p %s && cat > %s/%s",
1825
                                destdir, destdir, destfile)
1826
  remotecmd = _GetSshRunner(cluster_name).BuildCmd(dest_node,
1827
                                                   constants.GANETI_RUNAS,
1828
                                                   destcmd)
1829

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

    
1833
  result = utils.RunCmd(command, env=export_env)
1834

    
1835
  if result.failed:
1836
    _Fail("OS snapshot export command '%s' returned error: %s"
1837
          " output: %s", command, result.fail_reason, result.output)
1838

    
1839

    
1840
def FinalizeExport(instance, snap_disks):
1841
  """Write out the export configuration information.
1842

1843
  @type instance: L{objects.Instance}
1844
  @param instance: the instance which we export, used for
1845
      saving configuration
1846
  @type snap_disks: list of L{objects.Disk}
1847
  @param snap_disks: list of snapshot block devices, which
1848
      will be used to get the actual name of the dump file
1849

1850
  @rtype: None
1851

1852
  """
1853
  destdir = os.path.join(constants.EXPORT_DIR, instance.name + ".new")
1854
  finaldestdir = os.path.join(constants.EXPORT_DIR, instance.name)
1855

    
1856
  config = objects.SerializableConfigParser()
1857

    
1858
  config.add_section(constants.INISECT_EXP)
1859
  config.set(constants.INISECT_EXP, 'version', '0')
1860
  config.set(constants.INISECT_EXP, 'timestamp', '%d' % int(time.time()))
1861
  config.set(constants.INISECT_EXP, 'source', instance.primary_node)
1862
  config.set(constants.INISECT_EXP, 'os', instance.os)
1863
  config.set(constants.INISECT_EXP, 'compression', 'gzip')
1864

    
1865
  config.add_section(constants.INISECT_INS)
1866
  config.set(constants.INISECT_INS, 'name', instance.name)
1867
  config.set(constants.INISECT_INS, 'memory', '%d' %
1868
             instance.beparams[constants.BE_MEMORY])
1869
  config.set(constants.INISECT_INS, 'vcpus', '%d' %
1870
             instance.beparams[constants.BE_VCPUS])
1871
  config.set(constants.INISECT_INS, 'disk_template', instance.disk_template)
1872

    
1873
  nic_total = 0
1874
  for nic_count, nic in enumerate(instance.nics):
1875
    nic_total += 1
1876
    config.set(constants.INISECT_INS, 'nic%d_mac' %
1877
               nic_count, '%s' % nic.mac)
1878
    config.set(constants.INISECT_INS, 'nic%d_ip' % nic_count, '%s' % nic.ip)
1879
    config.set(constants.INISECT_INS, 'nic%d_bridge' % nic_count,
1880
               '%s' % nic.bridge)
1881
  # TODO: redundant: on load can read nics until it doesn't exist
1882
  config.set(constants.INISECT_INS, 'nic_count' , '%d' % nic_total)
1883

    
1884
  disk_total = 0
1885
  for disk_count, disk in enumerate(snap_disks):
1886
    if disk:
1887
      disk_total += 1
1888
      config.set(constants.INISECT_INS, 'disk%d_ivname' % disk_count,
1889
                 ('%s' % disk.iv_name))
1890
      config.set(constants.INISECT_INS, 'disk%d_dump' % disk_count,
1891
                 ('%s' % disk.physical_id[1]))
1892
      config.set(constants.INISECT_INS, 'disk%d_size' % disk_count,
1893
                 ('%d' % disk.size))
1894

    
1895
  config.set(constants.INISECT_INS, 'disk_count' , '%d' % disk_total)
1896

    
1897
  utils.WriteFile(os.path.join(destdir, constants.EXPORT_CONF_FILE),
1898
                  data=config.Dumps())
1899
  shutil.rmtree(finaldestdir, True)
1900
  shutil.move(destdir, finaldestdir)
1901

    
1902

    
1903
def ExportInfo(dest):
1904
  """Get export configuration information.
1905

1906
  @type dest: str
1907
  @param dest: directory containing the export
1908

1909
  @rtype: L{objects.SerializableConfigParser}
1910
  @return: a serializable config file containing the
1911
      export info
1912

1913
  """
1914
  cff = os.path.join(dest, constants.EXPORT_CONF_FILE)
1915

    
1916
  config = objects.SerializableConfigParser()
1917
  config.read(cff)
1918

    
1919
  if (not config.has_section(constants.INISECT_EXP) or
1920
      not config.has_section(constants.INISECT_INS)):
1921
    _Fail("Export info file doesn't have the required fields")
1922

    
1923
  return config.Dumps()
1924

    
1925

    
1926
def ImportOSIntoInstance(instance, src_node, src_images, cluster_name):
1927
  """Import an os image into an instance.
1928

1929
  @type instance: L{objects.Instance}
1930
  @param instance: instance to import the disks into
1931
  @type src_node: string
1932
  @param src_node: source node for the disk images
1933
  @type src_images: list of string
1934
  @param src_images: absolute paths of the disk images
1935
  @rtype: list of boolean
1936
  @return: each boolean represent the success of importing the n-th disk
1937

1938
  """
1939
  inst_os = OSFromDisk(instance.os)
1940
  import_env = OSEnvironment(instance, inst_os)
1941
  import_script = inst_os.import_script
1942

    
1943
  logfile = "%s/import-%s-%s-%s.log" % (constants.LOG_OS_DIR, instance.os,
1944
                                        instance.name, int(time.time()))
1945
  if not os.path.exists(constants.LOG_OS_DIR):
1946
    os.mkdir(constants.LOG_OS_DIR, 0750)
1947

    
1948
  comprcmd = "gunzip"
1949
  impcmd = utils.BuildShellCmd("(cd %s; %s >%s 2>&1)", inst_os.path,
1950
                               import_script, logfile)
1951

    
1952
  final_result = []
1953
  for idx, image in enumerate(src_images):
1954
    if image:
1955
      destcmd = utils.BuildShellCmd('cat %s', image)
1956
      remotecmd = _GetSshRunner(cluster_name).BuildCmd(src_node,
1957
                                                       constants.GANETI_RUNAS,
1958
                                                       destcmd)
1959
      command = '|'.join([utils.ShellQuoteArgs(remotecmd), comprcmd, impcmd])
1960
      import_env['IMPORT_DEVICE'] = import_env['DISK_%d_PATH' % idx]
1961
      import_env['IMPORT_INDEX'] = str(idx)
1962
      result = utils.RunCmd(command, env=import_env)
1963
      if result.failed:
1964
        logging.error("Disk import command '%s' returned error: %s"
1965
                      " output: %s", command, result.fail_reason,
1966
                      result.output)
1967
        final_result.append("error importing disk %d: %s, %s" %
1968
                            (idx, result.fail_reason, result.output[-100]))
1969

    
1970
  if final_result:
1971
    _Fail("; ".join(final_result), log=False)
1972

    
1973

    
1974
def ListExports():
1975
  """Return a list of exports currently available on this machine.
1976

1977
  @rtype: list
1978
  @return: list of the exports
1979

1980
  """
1981
  if os.path.isdir(constants.EXPORT_DIR):
1982
    return utils.ListVisibleFiles(constants.EXPORT_DIR)
1983
  else:
1984
    _Fail("No exports directory")
1985

    
1986

    
1987
def RemoveExport(export):
1988
  """Remove an existing export from the node.
1989

1990
  @type export: str
1991
  @param export: the name of the export to remove
1992
  @rtype: None
1993

1994
  """
1995
  target = os.path.join(constants.EXPORT_DIR, export)
1996

    
1997
  try:
1998
    shutil.rmtree(target)
1999
  except EnvironmentError, err:
2000
    _Fail("Error while removing the export: %s", err, exc=True)
2001

    
2002

    
2003
def BlockdevRename(devlist):
2004
  """Rename a list of block devices.
2005

2006
  @type devlist: list of tuples
2007
  @param devlist: list of tuples of the form  (disk,
2008
      new_logical_id, new_physical_id); disk is an
2009
      L{objects.Disk} object describing the current disk,
2010
      and new logical_id/physical_id is the name we
2011
      rename it to
2012
  @rtype: boolean
2013
  @return: True if all renames succeeded, False otherwise
2014

2015
  """
2016
  msgs = []
2017
  result = True
2018
  for disk, unique_id in devlist:
2019
    dev = _RecursiveFindBD(disk)
2020
    if dev is None:
2021
      msgs.append("Can't find device %s in rename" % str(disk))
2022
      result = False
2023
      continue
2024
    try:
2025
      old_rpath = dev.dev_path
2026
      dev.Rename(unique_id)
2027
      new_rpath = dev.dev_path
2028
      if old_rpath != new_rpath:
2029
        DevCacheManager.RemoveCache(old_rpath)
2030
        # FIXME: we should add the new cache information here, like:
2031
        # DevCacheManager.UpdateCache(new_rpath, owner, ...)
2032
        # but we don't have the owner here - maybe parse from existing
2033
        # cache? for now, we only lose lvm data when we rename, which
2034
        # is less critical than DRBD or MD
2035
    except errors.BlockDeviceError, err:
2036
      msgs.append("Can't rename device '%s' to '%s': %s" %
2037
                  (dev, unique_id, err))
2038
      logging.exception("Can't rename device '%s' to '%s'", dev, unique_id)
2039
      result = False
2040
  if not result:
2041
    _Fail("; ".join(msgs))
2042

    
2043

    
2044
def _TransformFileStorageDir(file_storage_dir):
2045
  """Checks whether given file_storage_dir is valid.
2046

2047
  Checks wheter the given file_storage_dir is within the cluster-wide
2048
  default file_storage_dir stored in SimpleStore. Only paths under that
2049
  directory are allowed.
2050

2051
  @type file_storage_dir: str
2052
  @param file_storage_dir: the path to check
2053

2054
  @return: the normalized path if valid, None otherwise
2055

2056
  """
2057
  cfg = _GetConfig()
2058
  file_storage_dir = os.path.normpath(file_storage_dir)
2059
  base_file_storage_dir = cfg.GetFileStorageDir()
2060
  if (not os.path.commonprefix([file_storage_dir, base_file_storage_dir]) ==
2061
      base_file_storage_dir):
2062
    _Fail("File storage directory '%s' is not under base file"
2063
          " storage directory '%s'", file_storage_dir, base_file_storage_dir)
2064
  return file_storage_dir
2065

    
2066

    
2067
def CreateFileStorageDir(file_storage_dir):
2068
  """Create file storage directory.
2069

2070
  @type file_storage_dir: str
2071
  @param file_storage_dir: directory to create
2072

2073
  @rtype: tuple
2074
  @return: tuple with first element a boolean indicating wheter dir
2075
      creation was successful or not
2076

2077
  """
2078
  file_storage_dir = _TransformFileStorageDir(file_storage_dir)
2079
  if os.path.exists(file_storage_dir):
2080
    if not os.path.isdir(file_storage_dir):
2081
      _Fail("Specified storage dir '%s' is not a directory",
2082
            file_storage_dir)
2083
  else:
2084
    try:
2085
      os.makedirs(file_storage_dir, 0750)
2086
    except OSError, err:
2087
      _Fail("Cannot create file storage directory '%s': %s",
2088
            file_storage_dir, err, exc=True)
2089

    
2090

    
2091
def RemoveFileStorageDir(file_storage_dir):
2092
  """Remove file storage directory.
2093

2094
  Remove it only if it's empty. If not log an error and return.
2095

2096
  @type file_storage_dir: str
2097
  @param file_storage_dir: the directory we should cleanup
2098
  @rtype: tuple (success,)
2099
  @return: tuple of one element, C{success}, denoting
2100
      whether the operation was successful
2101

2102
  """
2103
  file_storage_dir = _TransformFileStorageDir(file_storage_dir)
2104
  if os.path.exists(file_storage_dir):
2105
    if not os.path.isdir(file_storage_dir):
2106
      _Fail("Specified Storage directory '%s' is not a directory",
2107
            file_storage_dir)
2108
    # deletes dir only if empty, otherwise we want to fail the rpc call
2109
    try:
2110
      os.rmdir(file_storage_dir)
2111
    except OSError, err:
2112
      _Fail("Cannot remove file storage directory '%s': %s",
2113
            file_storage_dir, err)
2114

    
2115

    
2116
def RenameFileStorageDir(old_file_storage_dir, new_file_storage_dir):
2117
  """Rename the file storage directory.
2118

2119
  @type old_file_storage_dir: str
2120
  @param old_file_storage_dir: the current path
2121
  @type new_file_storage_dir: str
2122
  @param new_file_storage_dir: the name we should rename to
2123
  @rtype: tuple (success,)
2124
  @return: tuple of one element, C{success}, denoting
2125
      whether the operation was successful
2126

2127
  """
2128
  old_file_storage_dir = _TransformFileStorageDir(old_file_storage_dir)
2129
  new_file_storage_dir = _TransformFileStorageDir(new_file_storage_dir)
2130
  if not os.path.exists(new_file_storage_dir):
2131
    if os.path.isdir(old_file_storage_dir):
2132
      try:
2133
        os.rename(old_file_storage_dir, new_file_storage_dir)
2134
      except OSError, err:
2135
        _Fail("Cannot rename '%s' to '%s': %s",
2136
              old_file_storage_dir, new_file_storage_dir, err)
2137
    else:
2138
      _Fail("Specified storage dir '%s' is not a directory",
2139
            old_file_storage_dir)
2140
  else:
2141
    if os.path.exists(old_file_storage_dir):
2142
      _Fail("Cannot rename '%s' to '%s': both locations exist",
2143
            old_file_storage_dir, new_file_storage_dir)
2144

    
2145

    
2146
def _EnsureJobQueueFile(file_name):
2147
  """Checks whether the given filename is in the queue directory.
2148

2149
  @type file_name: str
2150
  @param file_name: the file name we should check
2151
  @rtype: None
2152
  @raises RPCFail: if the file is not valid
2153

2154
  """
2155
  queue_dir = os.path.normpath(constants.QUEUE_DIR)
2156
  result = (os.path.commonprefix([queue_dir, file_name]) == queue_dir)
2157

    
2158
  if not result:
2159
    _Fail("Passed job queue file '%s' does not belong to"
2160
          " the queue directory '%s'", file_name, queue_dir)
2161

    
2162

    
2163
def JobQueueUpdate(file_name, content):
2164
  """Updates a file in the queue directory.
2165

2166
  This is just a wrapper over L{utils.WriteFile}, with proper
2167
  checking.
2168

2169
  @type file_name: str
2170
  @param file_name: the job file name
2171
  @type content: str
2172
  @param content: the new job contents
2173
  @rtype: boolean
2174
  @return: the success of the operation
2175

2176
  """
2177
  _EnsureJobQueueFile(file_name)
2178

    
2179
  # Write and replace the file atomically
2180
  utils.WriteFile(file_name, data=_Decompress(content))
2181

    
2182

    
2183
def JobQueueRename(old, new):
2184
  """Renames a job queue file.
2185

2186
  This is just a wrapper over os.rename with proper checking.
2187

2188
  @type old: str
2189
  @param old: the old (actual) file name
2190
  @type new: str
2191
  @param new: the desired file name
2192
  @rtype: tuple
2193
  @return: the success of the operation and payload
2194

2195
  """
2196
  _EnsureJobQueueFile(old)
2197
  _EnsureJobQueueFile(new)
2198

    
2199
  utils.RenameFile(old, new, mkdir=True)
2200

    
2201

    
2202
def JobQueueSetDrainFlag(drain_flag):
2203
  """Set the drain flag for the queue.
2204

2205
  This will set or unset the queue drain flag.
2206

2207
  @type drain_flag: boolean
2208
  @param drain_flag: if True, will set the drain flag, otherwise reset it.
2209
  @rtype: truple
2210
  @return: always True, None
2211
  @warning: the function always returns True
2212

2213
  """
2214
  if drain_flag:
2215
    utils.WriteFile(constants.JOB_QUEUE_DRAIN_FILE, data="", close=True)
2216
  else:
2217
    utils.RemoveFile(constants.JOB_QUEUE_DRAIN_FILE)
2218

    
2219

    
2220
def BlockdevClose(instance_name, disks):
2221
  """Closes the given block devices.
2222

2223
  This means they will be switched to secondary mode (in case of
2224
  DRBD).
2225

2226
  @param instance_name: if the argument is not empty, the symlinks
2227
      of this instance will be removed
2228
  @type disks: list of L{objects.Disk}
2229
  @param disks: the list of disks to be closed
2230
  @rtype: tuple (success, message)
2231
  @return: a tuple of success and message, where success
2232
      indicates the succes of the operation, and message
2233
      which will contain the error details in case we
2234
      failed
2235

2236
  """
2237
  bdevs = []
2238
  for cf in disks:
2239
    rd = _RecursiveFindBD(cf)
2240
    if rd is None:
2241
      _Fail("Can't find device %s", cf)
2242
    bdevs.append(rd)
2243

    
2244
  msg = []
2245
  for rd in bdevs:
2246
    try:
2247
      rd.Close()
2248
    except errors.BlockDeviceError, err:
2249
      msg.append(str(err))
2250
  if msg:
2251
    _Fail("Can't make devices secondary: %s", ",".join(msg))
2252
  else:
2253
    if instance_name:
2254
      _RemoveBlockDevLinks(instance_name, disks)
2255

    
2256

    
2257
def ValidateHVParams(hvname, hvparams):
2258
  """Validates the given hypervisor parameters.
2259

2260
  @type hvname: string
2261
  @param hvname: the hypervisor name
2262
  @type hvparams: dict
2263
  @param hvparams: the hypervisor parameters to be validated
2264
  @rtype: None
2265

2266
  """
2267
  try:
2268
    hv_type = hypervisor.GetHypervisor(hvname)
2269
    hv_type.ValidateParameters(hvparams)
2270
  except errors.HypervisorError, err:
2271
    _Fail(str(err), log=False)
2272

    
2273

    
2274
def DemoteFromMC():
2275
  """Demotes the current node from master candidate role.
2276

2277
  """
2278
  # try to ensure we're not the master by mistake
2279
  master, myself = ssconf.GetMasterAndMyself()
2280
  if master == myself:
2281
    _Fail("ssconf status shows I'm the master node, will not demote")
2282
  pid_file = utils.DaemonPidFileName(constants.MASTERD_PID)
2283
  if utils.IsProcessAlive(utils.ReadPidFile(pid_file)):
2284
    _Fail("The master daemon is running, will not demote")
2285
  try:
2286
    if os.path.isfile(constants.CLUSTER_CONF_FILE):
2287
      utils.CreateBackup(constants.CLUSTER_CONF_FILE)
2288
  except EnvironmentError, err:
2289
    if err.errno != errno.ENOENT:
2290
      _Fail("Error while backing up cluster file: %s", err, exc=True)
2291
  utils.RemoveFile(constants.CLUSTER_CONF_FILE)
2292

    
2293

    
2294
def _FindDisks(nodes_ip, disks):
2295
  """Sets the physical ID on disks and returns the block devices.
2296

2297
  """
2298
  # set the correct physical ID
2299
  my_name = utils.HostInfo().name
2300
  for cf in disks:
2301
    cf.SetPhysicalID(my_name, nodes_ip)
2302

    
2303
  bdevs = []
2304

    
2305
  for cf in disks:
2306
    rd = _RecursiveFindBD(cf)
2307
    if rd is None:
2308
      _Fail("Can't find device %s", cf)
2309
    bdevs.append(rd)
2310
  return bdevs
2311

    
2312

    
2313
def DrbdDisconnectNet(nodes_ip, disks):
2314
  """Disconnects the network on a list of drbd devices.
2315

2316
  """
2317
  bdevs = _FindDisks(nodes_ip, disks)
2318

    
2319
  # disconnect disks
2320
  for rd in bdevs:
2321
    try:
2322
      rd.DisconnectNet()
2323
    except errors.BlockDeviceError, err:
2324
      _Fail("Can't change network configuration to standalone mode: %s",
2325
            err, exc=True)
2326

    
2327

    
2328
def DrbdAttachNet(nodes_ip, disks, instance_name, multimaster):
2329
  """Attaches the network on a list of drbd devices.
2330

2331
  """
2332
  bdevs = _FindDisks(nodes_ip, disks)
2333

    
2334
  if multimaster:
2335
    for idx, rd in enumerate(bdevs):
2336
      try:
2337
        _SymlinkBlockDev(instance_name, rd.dev_path, idx)
2338
      except EnvironmentError, err:
2339
        _Fail("Can't create symlink: %s", err)
2340
  # reconnect disks, switch to new master configuration and if
2341
  # needed primary mode
2342
  for rd in bdevs:
2343
    try:
2344
      rd.AttachNet(multimaster)
2345
    except errors.BlockDeviceError, err:
2346
      _Fail("Can't change network configuration: %s", err)
2347
  # wait until the disks are connected; we need to retry the re-attach
2348
  # if the device becomes standalone, as this might happen if the one
2349
  # node disconnects and reconnects in a different mode before the
2350
  # other node reconnects; in this case, one or both of the nodes will
2351
  # decide it has wrong configuration and switch to standalone
2352
  RECONNECT_TIMEOUT = 2 * 60
2353
  sleep_time = 0.100 # start with 100 miliseconds
2354
  timeout_limit = time.time() + RECONNECT_TIMEOUT
2355
  while time.time() < timeout_limit:
2356
    all_connected = True
2357
    for rd in bdevs:
2358
      stats = rd.GetProcStatus()
2359
      if not (stats.is_connected or stats.is_in_resync):
2360
        all_connected = False
2361
      if stats.is_standalone:
2362
        # peer had different config info and this node became
2363
        # standalone, even though this should not happen with the
2364
        # new staged way of changing disk configs
2365
        try:
2366
          rd.AttachNet(multimaster)
2367
        except errors.BlockDeviceError, err:
2368
          _Fail("Can't change network configuration: %s", err)
2369
    if all_connected:
2370
      break
2371
    time.sleep(sleep_time)
2372
    sleep_time = min(5, sleep_time * 1.5)
2373
  if not all_connected:
2374
    _Fail("Timeout in disk reconnecting")
2375
  if multimaster:
2376
    # change to primary mode
2377
    for rd in bdevs:
2378
      try:
2379
        rd.Open()
2380
      except errors.BlockDeviceError, err:
2381
        _Fail("Can't change to primary mode: %s", err)
2382

    
2383

    
2384
def DrbdWaitSync(nodes_ip, disks):
2385
  """Wait until DRBDs have synchronized.
2386

2387
  """
2388
  bdevs = _FindDisks(nodes_ip, disks)
2389

    
2390
  min_resync = 100
2391
  alldone = True
2392
  for rd in bdevs:
2393
    stats = rd.GetProcStatus()
2394
    if not (stats.is_connected or stats.is_in_resync):
2395
      _Fail("DRBD device %s is not in sync: stats=%s", rd, stats)
2396
    alldone = alldone and (not stats.is_in_resync)
2397
    if stats.sync_percent is not None:
2398
      min_resync = min(min_resync, stats.sync_percent)
2399

    
2400
  return (alldone, min_resync)
2401

    
2402

    
2403
def PowercycleNode(hypervisor_type):
2404
  """Hard-powercycle the node.
2405

2406
  Because we need to return first, and schedule the powercycle in the
2407
  background, we won't be able to report failures nicely.
2408

2409
  """
2410
  hyper = hypervisor.GetHypervisor(hypervisor_type)
2411
  try:
2412
    pid = os.fork()
2413
  except OSError:
2414
    # if we can't fork, we'll pretend that we're in the child process
2415
    pid = 0
2416
  if pid > 0:
2417
    return "Reboot scheduled in 5 seconds"
2418
  time.sleep(5)
2419
  hyper.PowercycleNode()
2420

    
2421

    
2422
class HooksRunner(object):
2423
  """Hook runner.
2424

2425
  This class is instantiated on the node side (ganeti-noded) and not
2426
  on the master side.
2427

2428
  """
2429
  RE_MASK = re.compile("^[a-zA-Z0-9_-]+$")
2430

    
2431
  def __init__(self, hooks_base_dir=None):
2432
    """Constructor for hooks runner.
2433

2434
    @type hooks_base_dir: str or None
2435
    @param hooks_base_dir: if not None, this overrides the
2436
        L{constants.HOOKS_BASE_DIR} (useful for unittests)
2437

2438
    """
2439
    if hooks_base_dir is None:
2440
      hooks_base_dir = constants.HOOKS_BASE_DIR
2441
    self._BASE_DIR = hooks_base_dir
2442

    
2443
  @staticmethod
2444
  def ExecHook(script, env):
2445
    """Exec one hook script.
2446

2447
    @type script: str
2448
    @param script: the full path to the script
2449
    @type env: dict
2450
    @param env: the environment with which to exec the script
2451
    @rtype: tuple (success, message)
2452
    @return: a tuple of success and message, where success
2453
        indicates the succes of the operation, and message
2454
        which will contain the error details in case we
2455
        failed
2456

2457
    """
2458
    # exec the process using subprocess and log the output
2459
    fdstdin = None
2460
    try:
2461
      fdstdin = open("/dev/null", "r")
2462
      child = subprocess.Popen([script], stdin=fdstdin, stdout=subprocess.PIPE,
2463
                               stderr=subprocess.STDOUT, close_fds=True,
2464
                               shell=False, cwd="/", env=env)
2465
      output = ""
2466
      try:
2467
        output = child.stdout.read(4096)
2468
        child.stdout.close()
2469
      except EnvironmentError, err:
2470
        output += "Hook script error: %s" % str(err)
2471

    
2472
      while True:
2473
        try:
2474
          result = child.wait()
2475
          break
2476
        except EnvironmentError, err:
2477
          if err.errno == errno.EINTR:
2478
            continue
2479
          raise
2480
    finally:
2481
      # try not to leak fds
2482
      for fd in (fdstdin, ):
2483
        if fd is not None:
2484
          try:
2485
            fd.close()
2486
          except EnvironmentError, err:
2487
            # just log the error
2488
            #logging.exception("Error while closing fd %s", fd)
2489
            pass
2490

    
2491
    return result == 0, utils.SafeEncode(output.strip())
2492

    
2493
  def RunHooks(self, hpath, phase, env):
2494
    """Run the scripts in the hooks directory.
2495

2496
    @type hpath: str
2497
    @param hpath: the path to the hooks directory which
2498
        holds the scripts
2499
    @type phase: str
2500
    @param phase: either L{constants.HOOKS_PHASE_PRE} or
2501
        L{constants.HOOKS_PHASE_POST}
2502
    @type env: dict
2503
    @param env: dictionary with the environment for the hook
2504
    @rtype: list
2505
    @return: list of 3-element tuples:
2506
      - script path
2507
      - script result, either L{constants.HKR_SUCCESS} or
2508
        L{constants.HKR_FAIL}
2509
      - output of the script
2510

2511
    @raise errors.ProgrammerError: for invalid input
2512
        parameters
2513

2514
    """
2515
    if phase == constants.HOOKS_PHASE_PRE:
2516
      suffix = "pre"
2517
    elif phase == constants.HOOKS_PHASE_POST:
2518
      suffix = "post"
2519
    else:
2520
      _Fail("Unknown hooks phase '%s'", phase)
2521

    
2522
    rr = []
2523

    
2524
    subdir = "%s-%s.d" % (hpath, suffix)
2525
    dir_name = "%s/%s" % (self._BASE_DIR, subdir)
2526
    try:
2527
      dir_contents = utils.ListVisibleFiles(dir_name)
2528
    except OSError:
2529
      # FIXME: must log output in case of failures
2530
      return rr
2531

    
2532
    # we use the standard python sort order,
2533
    # so 00name is the recommended naming scheme
2534
    dir_contents.sort()
2535
    for relname in dir_contents:
2536
      fname = os.path.join(dir_name, relname)
2537
      if not (os.path.isfile(fname) and os.access(fname, os.X_OK) and
2538
          self.RE_MASK.match(relname) is not None):
2539
        rrval = constants.HKR_SKIP
2540
        output = ""
2541
      else:
2542
        result, output = self.ExecHook(fname, env)
2543
        if not result:
2544
          rrval = constants.HKR_FAIL
2545
        else:
2546
          rrval = constants.HKR_SUCCESS
2547
      rr.append(("%s/%s" % (subdir, relname), rrval, output))
2548

    
2549
    return rr
2550

    
2551

    
2552
class IAllocatorRunner(object):
2553
  """IAllocator runner.
2554

2555
  This class is instantiated on the node side (ganeti-noded) and not on
2556
  the master side.
2557

2558
  """
2559
  def Run(self, name, idata):
2560
    """Run an iallocator script.
2561

2562
    @type name: str
2563
    @param name: the iallocator script name
2564
    @type idata: str
2565
    @param idata: the allocator input data
2566

2567
    @rtype: tuple
2568
    @return: two element tuple of:
2569
       - status
2570
       - either error message or stdout of allocator (for success)
2571

2572
    """
2573
    alloc_script = utils.FindFile(name, constants.IALLOCATOR_SEARCH_PATH,
2574
                                  os.path.isfile)
2575
    if alloc_script is None:
2576
      _Fail("iallocator module '%s' not found in the search path", name)
2577

    
2578
    fd, fin_name = tempfile.mkstemp(prefix="ganeti-iallocator.")
2579
    try:
2580
      os.write(fd, idata)
2581
      os.close(fd)
2582
      result = utils.RunCmd([alloc_script, fin_name])
2583
      if result.failed:
2584
        _Fail("iallocator module '%s' failed: %s, output '%s'",
2585
              name, result.fail_reason, result.output)
2586
    finally:
2587
      os.unlink(fin_name)
2588

    
2589
    return result.stdout
2590

    
2591

    
2592
class DevCacheManager(object):
2593
  """Simple class for managing a cache of block device information.
2594

2595
  """
2596
  _DEV_PREFIX = "/dev/"
2597
  _ROOT_DIR = constants.BDEV_CACHE_DIR
2598

    
2599
  @classmethod
2600
  def _ConvertPath(cls, dev_path):
2601
    """Converts a /dev/name path to the cache file name.
2602

2603
    This replaces slashes with underscores and strips the /dev
2604
    prefix. It then returns the full path to the cache file.
2605

2606
    @type dev_path: str
2607
    @param dev_path: the C{/dev/} path name
2608
    @rtype: str
2609
    @return: the converted path name
2610

2611
    """
2612
    if dev_path.startswith(cls._DEV_PREFIX):
2613
      dev_path = dev_path[len(cls._DEV_PREFIX):]
2614
    dev_path = dev_path.replace("/", "_")
2615
    fpath = "%s/bdev_%s" % (cls._ROOT_DIR, dev_path)
2616
    return fpath
2617

    
2618
  @classmethod
2619
  def UpdateCache(cls, dev_path, owner, on_primary, iv_name):
2620
    """Updates the cache information for a given device.
2621

2622
    @type dev_path: str
2623
    @param dev_path: the pathname of the device
2624
    @type owner: str
2625
    @param owner: the owner (instance name) of the device
2626
    @type on_primary: bool
2627
    @param on_primary: whether this is the primary
2628
        node nor not
2629
    @type iv_name: str
2630
    @param iv_name: the instance-visible name of the
2631
        device, as in objects.Disk.iv_name
2632

2633
    @rtype: None
2634

2635
    """
2636
    if dev_path is None:
2637
      logging.error("DevCacheManager.UpdateCache got a None dev_path")
2638
      return
2639
    fpath = cls._ConvertPath(dev_path)
2640
    if on_primary:
2641
      state = "primary"
2642
    else:
2643
      state = "secondary"
2644
    if iv_name is None:
2645
      iv_name = "not_visible"
2646
    fdata = "%s %s %s\n" % (str(owner), state, iv_name)
2647
    try:
2648
      utils.WriteFile(fpath, data=fdata)
2649
    except EnvironmentError, err:
2650
      logging.exception("Can't update bdev cache for %s: %s", dev_path, err)
2651

    
2652
  @classmethod
2653
  def RemoveCache(cls, dev_path):
2654
    """Remove data for a dev_path.
2655

2656
    This is just a wrapper over L{utils.RemoveFile} with a converted
2657
    path name and logging.
2658

2659
    @type dev_path: str
2660
    @param dev_path: the pathname of the device
2661

2662
    @rtype: None
2663

2664
    """
2665
    if dev_path is None:
2666
      logging.error("DevCacheManager.RemoveCache got a None dev_path")
2667
      return
2668
    fpath = cls._ConvertPath(dev_path)
2669
    try:
2670
      utils.RemoveFile(fpath)
2671
    except EnvironmentError, err:
2672
      logging.exception("Can't update bdev cache for %s: %s", dev_path, err)