Statistics
| Branch: | Tag: | Revision:

root / lib / backend.py @ e7323b5e

History | View | Annotate | Download (99.4 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2010 Google Inc.
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14
# General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19
# 02110-1301, USA.
20

    
21

    
22
"""Functions used by the node daemon
23

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

29
"""
30

    
31
# pylint: disable-msg=E1103
32

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

    
37

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

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

    
63

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

    
78

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

82
  Its argument is the error message.
83

84
  """
85

    
86

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

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

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

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

    
109

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

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

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

    
119

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

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

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

    
132

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

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

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

    
152

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

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

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

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

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

    
182

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

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

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

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

    
204
  return frozenset(allowed_files)
205

    
206

    
207
_ALLOWED_UPLOAD_FILES = _BuildUploadFileList()
208

    
209

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

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

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

    
220

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

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

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

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

    
241

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

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

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

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

    
262
  err_msgs = []
263
  # either start the master and rapi daemons
264
  if start_daemons:
265
    if no_voting:
266
      masterd_args = "--no-voting --yes-do-it"
267
    else:
268
      masterd_args = ""
269

    
270
    env = {
271
      "EXTRA_MASTERD_ARGS": masterd_args,
272
      }
273

    
274
    result = utils.RunCmd([constants.DAEMON_UTIL, "start-master"], env=env)
275
    if result.failed:
276
      msg = "Can't start Ganeti master: %s" % result.output
277
      logging.error(msg)
278
      err_msgs.append(msg)
279
  # or activate the IP
280
  else:
281
    if netutils.TcpPing(master_ip, constants.DEFAULT_NODED_PORT):
282
      if netutils.IPAddress.Own(master_ip):
283
        # we already have the ip:
284
        logging.debug("Master IP already configured, doing nothing")
285
      else:
286
        msg = "Someone else has the master ip, not activating"
287
        logging.error(msg)
288
        err_msgs.append(msg)
289
    else:
290
      netmask = 32
291
      if netutils.IP6Address.IsValid(master_ip):
292
        netmask = 128
293

    
294
      result = utils.RunCmd(["ip", "address", "add",
295
                             "%s/%d" % (master_ip, netmask),
296
                             "dev", master_netdev, "label",
297
                             "%s:0" % master_netdev])
298
      if result.failed:
299
        msg = "Can't activate master IP: %s" % result.output
300
        logging.error(msg)
301
        err_msgs.append(msg)
302

    
303
      # we ignore the exit code of the following cmds
304
      if netutils.IP4Address.IsValid(master_ip):
305
        utils.RunCmd(["arping", "-q", "-U", "-c 3", "-I", master_netdev, "-s",
306
                      master_ip, master_ip])
307
      elif netutils.IP6Address.IsValid(master_ip):
308
        utils.RunCmd(["ndisc6", "-q", "-r 3", master_ip, master_netdev])
309

    
310
  if err_msgs:
311
    _Fail("; ".join(err_msgs))
312

    
313

    
314
def StopMaster(stop_daemons):
315
  """Deactivate this node as master.
316

317
  The function will always try to deactivate the IP address of the
318
  master. It will also stop the master daemons depending on the
319
  stop_daemons parameter.
320

321
  @type stop_daemons: boolean
322
  @param stop_daemons: whether to also stop the master daemons
323
      (ganeti-masterd and ganeti-rapi)
324
  @rtype: None
325

326
  """
327
  # TODO: log and report back to the caller the error failures; we
328
  # need to decide in which case we fail the RPC for this
329

    
330
  # GetMasterInfo will raise an exception if not able to return data
331
  master_netdev, master_ip, _ = GetMasterInfo()
332

    
333
  netmask = 32
334
  if netutils.IP6Address.IsValid(master_ip):
335
    netmask = 128
336

    
337
  result = utils.RunCmd(["ip", "address", "del",
338
                         "%s/%d" % (master_ip, netmask),
339
                         "dev", master_netdev])
340
  if result.failed:
341
    logging.error("Can't remove the master IP, error: %s", result.output)
342
    # but otherwise ignore the failure
343

    
344
  if stop_daemons:
345
    result = utils.RunCmd([constants.DAEMON_UTIL, "stop-master"])
346
    if result.failed:
347
      logging.error("Could not stop Ganeti master, command %s had exitcode %s"
348
                    " and error %s",
349
                    result.cmd, result.exit_code, result.output)
350

    
351

    
352
def AddNode(dsa, dsapub, rsa, rsapub, sshkey, sshpub):
353
  """Joins this node to the cluster.
354

355
  This does the following:
356
      - updates the hostkeys of the machine (rsa and dsa)
357
      - adds the ssh private key to the user
358
      - adds the ssh public key to the users' authorized_keys file
359

360
  @type dsa: str
361
  @param dsa: the DSA private key to write
362
  @type dsapub: str
363
  @param dsapub: the DSA public key to write
364
  @type rsa: str
365
  @param rsa: the RSA private key to write
366
  @type rsapub: str
367
  @param rsapub: the RSA public key to write
368
  @type sshkey: str
369
  @param sshkey: the SSH private key to write
370
  @type sshpub: str
371
  @param sshpub: the SSH public key to write
372
  @rtype: boolean
373
  @return: the success of the operation
374

375
  """
376
  sshd_keys =  [(constants.SSH_HOST_RSA_PRIV, rsa, 0600),
377
                (constants.SSH_HOST_RSA_PUB, rsapub, 0644),
378
                (constants.SSH_HOST_DSA_PRIV, dsa, 0600),
379
                (constants.SSH_HOST_DSA_PUB, dsapub, 0644)]
380
  for name, content, mode in sshd_keys:
381
    utils.WriteFile(name, data=content, mode=mode)
382

    
383
  try:
384
    priv_key, pub_key, auth_keys = ssh.GetUserFiles(constants.GANETI_RUNAS,
385
                                                    mkdir=True)
386
  except errors.OpExecError, err:
387
    _Fail("Error while processing user ssh files: %s", err, exc=True)
388

    
389
  for name, content in [(priv_key, sshkey), (pub_key, sshpub)]:
390
    utils.WriteFile(name, data=content, mode=0600)
391

    
392
  utils.AddAuthorizedKey(auth_keys, sshpub)
393

    
394
  result = utils.RunCmd([constants.DAEMON_UTIL, "reload-ssh-keys"])
395
  if result.failed:
396
    _Fail("Unable to reload SSH keys (command %r, exit code %s, output %r)",
397
          result.cmd, result.exit_code, result.output)
398

    
399

    
400
def LeaveCluster(modify_ssh_setup):
401
  """Cleans up and remove the current node.
402

403
  This function cleans up and prepares the current node to be removed
404
  from the cluster.
405

406
  If processing is successful, then it raises an
407
  L{errors.QuitGanetiException} which is used as a special case to
408
  shutdown the node daemon.
409

410
  @param modify_ssh_setup: boolean
411

412
  """
413
  _CleanDirectory(constants.DATA_DIR)
414
  _CleanDirectory(constants.CRYPTO_KEYS_DIR)
415
  JobQueuePurge()
416

    
417
  if modify_ssh_setup:
418
    try:
419
      priv_key, pub_key, auth_keys = ssh.GetUserFiles(constants.GANETI_RUNAS)
420

    
421
      utils.RemoveAuthorizedKey(auth_keys, utils.ReadFile(pub_key))
422

    
423
      utils.RemoveFile(priv_key)
424
      utils.RemoveFile(pub_key)
425
    except errors.OpExecError:
426
      logging.exception("Error while processing ssh files")
427

    
428
  try:
429
    utils.RemoveFile(constants.CONFD_HMAC_KEY)
430
    utils.RemoveFile(constants.RAPI_CERT_FILE)
431
    utils.RemoveFile(constants.NODED_CERT_FILE)
432
  except: # pylint: disable-msg=W0702
433
    logging.exception("Error while removing cluster secrets")
434

    
435
  result = utils.RunCmd([constants.DAEMON_UTIL, "stop", constants.CONFD])
436
  if result.failed:
437
    logging.error("Command %s failed with exitcode %s and error %s",
438
                  result.cmd, result.exit_code, result.output)
439

    
440
  # Raise a custom exception (handled in ganeti-noded)
441
  raise errors.QuitGanetiException(True, 'Shutdown scheduled')
442

    
443

    
444
def GetNodeInfo(vgname, hypervisor_type):
445
  """Gives back a hash with different information about the node.
446

447
  @type vgname: C{string}
448
  @param vgname: the name of the volume group to ask for disk space information
449
  @type hypervisor_type: C{str}
450
  @param hypervisor_type: the name of the hypervisor to ask for
451
      memory information
452
  @rtype: C{dict}
453
  @return: dictionary with the following keys:
454
      - vg_size is the size of the configured volume group in MiB
455
      - vg_free is the free size of the volume group in MiB
456
      - memory_dom0 is the memory allocated for domain0 in MiB
457
      - memory_free is the currently available (free) ram in MiB
458
      - memory_total is the total number of ram in MiB
459

460
  """
461
  outputarray = {}
462
  vginfo = _GetVGInfo(vgname)
463
  outputarray['vg_size'] = vginfo['vg_size']
464
  outputarray['vg_free'] = vginfo['vg_free']
465

    
466
  hyper = hypervisor.GetHypervisor(hypervisor_type)
467
  hyp_info = hyper.GetNodeInfo()
468
  if hyp_info is not None:
469
    outputarray.update(hyp_info)
470

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

    
473
  return outputarray
474

    
475

    
476
def VerifyNode(what, cluster_name):
477
  """Verify the status of the local node.
478

479
  Based on the input L{what} parameter, various checks are done on the
480
  local node.
481

482
  If the I{filelist} key is present, this list of
483
  files is checksummed and the file/checksum pairs are returned.
484

485
  If the I{nodelist} key is present, we check that we have
486
  connectivity via ssh with the target nodes (and check the hostname
487
  report).
488

489
  If the I{node-net-test} key is present, we check that we have
490
  connectivity to the given nodes via both primary IP and, if
491
  applicable, secondary IPs.
492

493
  @type what: C{dict}
494
  @param what: a dictionary of things to check:
495
      - filelist: list of files for which to compute checksums
496
      - nodelist: list of nodes we should check ssh communication with
497
      - node-net-test: list of nodes we should check node daemon port
498
        connectivity with
499
      - hypervisor: list with hypervisors to run the verify for
500
  @rtype: dict
501
  @return: a dictionary with the same keys as the input dict, and
502
      values representing the result of the checks
503

504
  """
505
  result = {}
506
  my_name = netutils.Hostname.GetSysName()
507
  port = netutils.GetDaemonPort(constants.NODED)
508

    
509
  if constants.NV_HYPERVISOR in what:
510
    result[constants.NV_HYPERVISOR] = tmp = {}
511
    for hv_name in what[constants.NV_HYPERVISOR]:
512
      try:
513
        val = hypervisor.GetHypervisor(hv_name).Verify()
514
      except errors.HypervisorError, err:
515
        val = "Error while checking hypervisor: %s" % str(err)
516
      tmp[hv_name] = val
517

    
518
  if constants.NV_FILELIST in what:
519
    result[constants.NV_FILELIST] = utils.FingerprintFiles(
520
      what[constants.NV_FILELIST])
521

    
522
  if constants.NV_NODELIST in what:
523
    result[constants.NV_NODELIST] = tmp = {}
524
    random.shuffle(what[constants.NV_NODELIST])
525
    for node in what[constants.NV_NODELIST]:
526
      success, message = _GetSshRunner(cluster_name).VerifyNodeHostname(node)
527
      if not success:
528
        tmp[node] = message
529

    
530
  if constants.NV_NODENETTEST in what:
531
    result[constants.NV_NODENETTEST] = tmp = {}
532
    my_pip = my_sip = None
533
    for name, pip, sip in what[constants.NV_NODENETTEST]:
534
      if name == my_name:
535
        my_pip = pip
536
        my_sip = sip
537
        break
538
    if not my_pip:
539
      tmp[my_name] = ("Can't find my own primary/secondary IP"
540
                      " in the node list")
541
    else:
542
      for name, pip, sip in what[constants.NV_NODENETTEST]:
543
        fail = []
544
        if not netutils.TcpPing(pip, port, source=my_pip):
545
          fail.append("primary")
546
        if sip != pip:
547
          if not netutils.TcpPing(sip, port, source=my_sip):
548
            fail.append("secondary")
549
        if fail:
550
          tmp[name] = ("failure using the %s interface(s)" %
551
                       " and ".join(fail))
552

    
553
  if constants.NV_MASTERIP in what:
554
    # FIXME: add checks on incoming data structures (here and in the
555
    # rest of the function)
556
    master_name, master_ip = what[constants.NV_MASTERIP]
557
    if master_name == my_name:
558
      source = constants.IP4_ADDRESS_LOCALHOST
559
    else:
560
      source = None
561
    result[constants.NV_MASTERIP] = netutils.TcpPing(master_ip, port,
562
                                                  source=source)
563

    
564
  if constants.NV_LVLIST in what:
565
    try:
566
      val = GetVolumeList(what[constants.NV_LVLIST])
567
    except RPCFail, err:
568
      val = str(err)
569
    result[constants.NV_LVLIST] = val
570

    
571
  if constants.NV_INSTANCELIST in what:
572
    # GetInstanceList can fail
573
    try:
574
      val = GetInstanceList(what[constants.NV_INSTANCELIST])
575
    except RPCFail, err:
576
      val = str(err)
577
    result[constants.NV_INSTANCELIST] = val
578

    
579
  if constants.NV_VGLIST in what:
580
    result[constants.NV_VGLIST] = utils.ListVolumeGroups()
581

    
582
  if constants.NV_PVLIST in what:
583
    result[constants.NV_PVLIST] = \
584
      bdev.LogicalVolume.GetPVInfo(what[constants.NV_PVLIST],
585
                                   filter_allocatable=False)
586

    
587
  if constants.NV_VERSION in what:
588
    result[constants.NV_VERSION] = (constants.PROTOCOL_VERSION,
589
                                    constants.RELEASE_VERSION)
590

    
591
  if constants.NV_HVINFO in what:
592
    hyper = hypervisor.GetHypervisor(what[constants.NV_HVINFO])
593
    result[constants.NV_HVINFO] = hyper.GetNodeInfo()
594

    
595
  if constants.NV_DRBDLIST in what:
596
    try:
597
      used_minors = bdev.DRBD8.GetUsedDevs().keys()
598
    except errors.BlockDeviceError, err:
599
      logging.warning("Can't get used minors list", exc_info=True)
600
      used_minors = str(err)
601
    result[constants.NV_DRBDLIST] = used_minors
602

    
603
  if constants.NV_DRBDHELPER in what:
604
    status = True
605
    try:
606
      payload = bdev.BaseDRBD.GetUsermodeHelper()
607
    except errors.BlockDeviceError, err:
608
      logging.error("Can't get DRBD usermode helper: %s", str(err))
609
      status = False
610
      payload = str(err)
611
    result[constants.NV_DRBDHELPER] = (status, payload)
612

    
613
  if constants.NV_NODESETUP in what:
614
    result[constants.NV_NODESETUP] = tmpr = []
615
    if not os.path.isdir("/sys/block") or not os.path.isdir("/sys/class/net"):
616
      tmpr.append("The sysfs filesytem doesn't seem to be mounted"
617
                  " under /sys, missing required directories /sys/block"
618
                  " and /sys/class/net")
619
    if (not os.path.isdir("/proc/sys") or
620
        not os.path.isfile("/proc/sysrq-trigger")):
621
      tmpr.append("The procfs filesystem doesn't seem to be mounted"
622
                  " under /proc, missing required directory /proc/sys and"
623
                  " the file /proc/sysrq-trigger")
624

    
625
  if constants.NV_TIME in what:
626
    result[constants.NV_TIME] = utils.SplitTime(time.time())
627

    
628
  if constants.NV_OSLIST in what:
629
    result[constants.NV_OSLIST] = DiagnoseOS()
630

    
631
  return result
632

    
633

    
634
def GetVolumeList(vg_name):
635
  """Compute list of logical volumes and their size.
636

637
  @type vg_name: str
638
  @param vg_name: the volume group whose LVs we should list
639
  @rtype: dict
640
  @return:
641
      dictionary of all partions (key) with value being a tuple of
642
      their size (in MiB), inactive and online status::
643

644
        {'test1': ('20.06', True, True)}
645

646
      in case of errors, a string is returned with the error
647
      details.
648

649
  """
650
  lvs = {}
651
  sep = '|'
652
  result = utils.RunCmd(["lvs", "--noheadings", "--units=m", "--nosuffix",
653
                         "--separator=%s" % sep,
654
                         "-olv_name,lv_size,lv_attr", vg_name])
655
  if result.failed:
656
    _Fail("Failed to list logical volumes, lvs output: %s", result.output)
657

    
658
  valid_line_re = re.compile("^ *([^|]+)\|([0-9.]+)\|([^|]{6})\|?$")
659
  for line in result.stdout.splitlines():
660
    line = line.strip()
661
    match = valid_line_re.match(line)
662
    if not match:
663
      logging.error("Invalid line returned from lvs output: '%s'", line)
664
      continue
665
    name, size, attr = match.groups()
666
    inactive = attr[4] == '-'
667
    online = attr[5] == 'o'
668
    virtual = attr[0] == 'v'
669
    if virtual:
670
      # we don't want to report such volumes as existing, since they
671
      # don't really hold data
672
      continue
673
    lvs[name] = (size, inactive, online)
674

    
675
  return lvs
676

    
677

    
678
def ListVolumeGroups():
679
  """List the volume groups and their size.
680

681
  @rtype: dict
682
  @return: dictionary with keys volume name and values the
683
      size of the volume
684

685
  """
686
  return utils.ListVolumeGroups()
687

    
688

    
689
def NodeVolumes():
690
  """List all volumes on this node.
691

692
  @rtype: list
693
  @return:
694
    A list of dictionaries, each having four keys:
695
      - name: the logical volume name,
696
      - size: the size of the logical volume
697
      - dev: the physical device on which the LV lives
698
      - vg: the volume group to which it belongs
699

700
    In case of errors, we return an empty list and log the
701
    error.
702

703
    Note that since a logical volume can live on multiple physical
704
    volumes, the resulting list might include a logical volume
705
    multiple times.
706

707
  """
708
  result = utils.RunCmd(["lvs", "--noheadings", "--units=m", "--nosuffix",
709
                         "--separator=|",
710
                         "--options=lv_name,lv_size,devices,vg_name"])
711
  if result.failed:
712
    _Fail("Failed to list logical volumes, lvs output: %s",
713
          result.output)
714

    
715
  def parse_dev(dev):
716
    return dev.split('(')[0]
717

    
718
  def handle_dev(dev):
719
    return [parse_dev(x) for x in dev.split(",")]
720

    
721
  def map_line(line):
722
    line = [v.strip() for v in line]
723
    return [{'name': line[0], 'size': line[1],
724
             'dev': dev, 'vg': line[3]} for dev in handle_dev(line[2])]
725

    
726
  all_devs = []
727
  for line in result.stdout.splitlines():
728
    if line.count('|') >= 3:
729
      all_devs.extend(map_line(line.split('|')))
730
    else:
731
      logging.warning("Strange line in the output from lvs: '%s'", line)
732
  return all_devs
733

    
734

    
735
def BridgesExist(bridges_list):
736
  """Check if a list of bridges exist on the current node.
737

738
  @rtype: boolean
739
  @return: C{True} if all of them exist, C{False} otherwise
740

741
  """
742
  missing = []
743
  for bridge in bridges_list:
744
    if not utils.BridgeExists(bridge):
745
      missing.append(bridge)
746

    
747
  if missing:
748
    _Fail("Missing bridges %s", utils.CommaJoin(missing))
749

    
750

    
751
def GetInstanceList(hypervisor_list):
752
  """Provides a list of instances.
753

754
  @type hypervisor_list: list
755
  @param hypervisor_list: the list of hypervisors to query information
756

757
  @rtype: list
758
  @return: a list of all running instances on the current node
759
    - instance1.example.com
760
    - instance2.example.com
761

762
  """
763
  results = []
764
  for hname in hypervisor_list:
765
    try:
766
      names = hypervisor.GetHypervisor(hname).ListInstances()
767
      results.extend(names)
768
    except errors.HypervisorError, err:
769
      _Fail("Error enumerating instances (hypervisor %s): %s",
770
            hname, err, exc=True)
771

    
772
  return results
773

    
774

    
775
def GetInstanceInfo(instance, hname):
776
  """Gives back the information about an instance as a dictionary.
777

778
  @type instance: string
779
  @param instance: the instance name
780
  @type hname: string
781
  @param hname: the hypervisor type of the instance
782

783
  @rtype: dict
784
  @return: dictionary with the following keys:
785
      - memory: memory size of instance (int)
786
      - state: xen state of instance (string)
787
      - time: cpu time of instance (float)
788

789
  """
790
  output = {}
791

    
792
  iinfo = hypervisor.GetHypervisor(hname).GetInstanceInfo(instance)
793
  if iinfo is not None:
794
    output['memory'] = iinfo[2]
795
    output['state'] = iinfo[4]
796
    output['time'] = iinfo[5]
797

    
798
  return output
799

    
800

    
801
def GetInstanceMigratable(instance):
802
  """Gives whether an instance can be migrated.
803

804
  @type instance: L{objects.Instance}
805
  @param instance: object representing the instance to be checked.
806

807
  @rtype: tuple
808
  @return: tuple of (result, description) where:
809
      - result: whether the instance can be migrated or not
810
      - description: a description of the issue, if relevant
811

812
  """
813
  hyper = hypervisor.GetHypervisor(instance.hypervisor)
814
  iname = instance.name
815
  if iname not in hyper.ListInstances():
816
    _Fail("Instance %s is not running", iname)
817

    
818
  for idx in range(len(instance.disks)):
819
    link_name = _GetBlockDevSymlinkPath(iname, idx)
820
    if not os.path.islink(link_name):
821
      logging.warning("Instance %s is missing symlink %s for disk %d",
822
                      iname, link_name, idx)
823

    
824

    
825
def GetAllInstancesInfo(hypervisor_list):
826
  """Gather data about all instances.
827

828
  This is the equivalent of L{GetInstanceInfo}, except that it
829
  computes data for all instances at once, thus being faster if one
830
  needs data about more than one instance.
831

832
  @type hypervisor_list: list
833
  @param hypervisor_list: list of hypervisors to query for instance data
834

835
  @rtype: dict
836
  @return: dictionary of instance: data, with data having the following keys:
837
      - memory: memory size of instance (int)
838
      - state: xen state of instance (string)
839
      - time: cpu time of instance (float)
840
      - vcpus: the number of vcpus
841

842
  """
843
  output = {}
844

    
845
  for hname in hypervisor_list:
846
    iinfo = hypervisor.GetHypervisor(hname).GetAllInstancesInfo()
847
    if iinfo:
848
      for name, _, memory, vcpus, state, times in iinfo:
849
        value = {
850
          'memory': memory,
851
          'vcpus': vcpus,
852
          'state': state,
853
          'time': times,
854
          }
855
        if name in output:
856
          # we only check static parameters, like memory and vcpus,
857
          # and not state and time which can change between the
858
          # invocations of the different hypervisors
859
          for key in 'memory', 'vcpus':
860
            if value[key] != output[name][key]:
861
              _Fail("Instance %s is running twice"
862
                    " with different parameters", name)
863
        output[name] = value
864

    
865
  return output
866

    
867

    
868
def _InstanceLogName(kind, os_name, instance):
869
  """Compute the OS log filename for a given instance and operation.
870

871
  The instance name and os name are passed in as strings since not all
872
  operations have these as part of an instance object.
873

874
  @type kind: string
875
  @param kind: the operation type (e.g. add, import, etc.)
876
  @type os_name: string
877
  @param os_name: the os name
878
  @type instance: string
879
  @param instance: the name of the instance being imported/added/etc.
880

881
  """
882
  # TODO: Use tempfile.mkstemp to create unique filename
883
  base = ("%s-%s-%s-%s.log" %
884
          (kind, os_name, instance, utils.TimestampForFilename()))
885
  return utils.PathJoin(constants.LOG_OS_DIR, base)
886

    
887

    
888
def InstanceOsAdd(instance, reinstall, debug):
889
  """Add an OS to an instance.
890

891
  @type instance: L{objects.Instance}
892
  @param instance: Instance whose OS is to be installed
893
  @type reinstall: boolean
894
  @param reinstall: whether this is an instance reinstall
895
  @type debug: integer
896
  @param debug: debug level, passed to the OS scripts
897
  @rtype: None
898

899
  """
900
  inst_os = OSFromDisk(instance.os)
901

    
902
  create_env = OSEnvironment(instance, inst_os, debug)
903
  if reinstall:
904
    create_env['INSTANCE_REINSTALL'] = "1"
905

    
906
  logfile = _InstanceLogName("add", instance.os, instance.name)
907

    
908
  result = utils.RunCmd([inst_os.create_script], env=create_env,
909
                        cwd=inst_os.path, output=logfile,)
910
  if result.failed:
911
    logging.error("os create command '%s' returned error: %s, logfile: %s,"
912
                  " output: %s", result.cmd, result.fail_reason, logfile,
913
                  result.output)
914
    lines = [utils.SafeEncode(val)
915
             for val in utils.TailFile(logfile, lines=20)]
916
    _Fail("OS create script failed (%s), last lines in the"
917
          " log file:\n%s", result.fail_reason, "\n".join(lines), log=False)
918

    
919

    
920
def RunRenameInstance(instance, old_name, debug):
921
  """Run the OS rename script for an instance.
922

923
  @type instance: L{objects.Instance}
924
  @param instance: Instance whose OS is to be installed
925
  @type old_name: string
926
  @param old_name: previous instance name
927
  @type debug: integer
928
  @param debug: debug level, passed to the OS scripts
929
  @rtype: boolean
930
  @return: the success of the operation
931

932
  """
933
  inst_os = OSFromDisk(instance.os)
934

    
935
  rename_env = OSEnvironment(instance, inst_os, debug)
936
  rename_env['OLD_INSTANCE_NAME'] = old_name
937

    
938
  logfile = _InstanceLogName("rename", instance.os,
939
                             "%s-%s" % (old_name, instance.name))
940

    
941
  result = utils.RunCmd([inst_os.rename_script], env=rename_env,
942
                        cwd=inst_os.path, output=logfile)
943

    
944
  if result.failed:
945
    logging.error("os create command '%s' returned error: %s output: %s",
946
                  result.cmd, result.fail_reason, result.output)
947
    lines = [utils.SafeEncode(val)
948
             for val in utils.TailFile(logfile, lines=20)]
949
    _Fail("OS rename script failed (%s), last lines in the"
950
          " log file:\n%s", result.fail_reason, "\n".join(lines), log=False)
951

    
952

    
953
def _GetVGInfo(vg_name):
954
  """Get information about the volume group.
955

956
  @type vg_name: str
957
  @param vg_name: the volume group which we query
958
  @rtype: dict
959
  @return:
960
    A dictionary with the following keys:
961
      - C{vg_size} is the total size of the volume group in MiB
962
      - C{vg_free} is the free size of the volume group in MiB
963
      - C{pv_count} are the number of physical disks in that VG
964

965
    If an error occurs during gathering of data, we return the same dict
966
    with keys all set to None.
967

968
  """
969
  retdic = dict.fromkeys(["vg_size", "vg_free", "pv_count"])
970

    
971
  retval = utils.RunCmd(["vgs", "-ovg_size,vg_free,pv_count", "--noheadings",
972
                         "--nosuffix", "--units=m", "--separator=:", vg_name])
973

    
974
  if retval.failed:
975
    logging.error("volume group %s not present", vg_name)
976
    return retdic
977
  valarr = retval.stdout.strip().rstrip(':').split(':')
978
  if len(valarr) == 3:
979
    try:
980
      retdic = {
981
        "vg_size": int(round(float(valarr[0]), 0)),
982
        "vg_free": int(round(float(valarr[1]), 0)),
983
        "pv_count": int(valarr[2]),
984
        }
985
    except (TypeError, ValueError), err:
986
      logging.exception("Fail to parse vgs output: %s", err)
987
  else:
988
    logging.error("vgs output has the wrong number of fields (expected"
989
                  " three): %s", str(valarr))
990
  return retdic
991

    
992

    
993
def _GetBlockDevSymlinkPath(instance_name, idx):
994
  return utils.PathJoin(constants.DISK_LINKS_DIR,
995
                        "%s:%d" % (instance_name, idx))
996

    
997

    
998
def _SymlinkBlockDev(instance_name, device_path, idx):
999
  """Set up symlinks to a instance's block device.
1000

1001
  This is an auxiliary function run when an instance is start (on the primary
1002
  node) or when an instance is migrated (on the target node).
1003

1004

1005
  @param instance_name: the name of the target instance
1006
  @param device_path: path of the physical block device, on the node
1007
  @param idx: the disk index
1008
  @return: absolute path to the disk's symlink
1009

1010
  """
1011
  link_name = _GetBlockDevSymlinkPath(instance_name, idx)
1012
  try:
1013
    os.symlink(device_path, link_name)
1014
  except OSError, err:
1015
    if err.errno == errno.EEXIST:
1016
      if (not os.path.islink(link_name) or
1017
          os.readlink(link_name) != device_path):
1018
        os.remove(link_name)
1019
        os.symlink(device_path, link_name)
1020
    else:
1021
      raise
1022

    
1023
  return link_name
1024

    
1025

    
1026
def _RemoveBlockDevLinks(instance_name, disks):
1027
  """Remove the block device symlinks belonging to the given instance.
1028

1029
  """
1030
  for idx, _ in enumerate(disks):
1031
    link_name = _GetBlockDevSymlinkPath(instance_name, idx)
1032
    if os.path.islink(link_name):
1033
      try:
1034
        os.remove(link_name)
1035
      except OSError:
1036
        logging.exception("Can't remove symlink '%s'", link_name)
1037

    
1038

    
1039
def _GatherAndLinkBlockDevs(instance):
1040
  """Set up an instance's block device(s).
1041

1042
  This is run on the primary node at instance startup. The block
1043
  devices must be already assembled.
1044

1045
  @type instance: L{objects.Instance}
1046
  @param instance: the instance whose disks we shoul assemble
1047
  @rtype: list
1048
  @return: list of (disk_object, device_path)
1049

1050
  """
1051
  block_devices = []
1052
  for idx, disk in enumerate(instance.disks):
1053
    device = _RecursiveFindBD(disk)
1054
    if device is None:
1055
      raise errors.BlockDeviceError("Block device '%s' is not set up." %
1056
                                    str(disk))
1057
    device.Open()
1058
    try:
1059
      link_name = _SymlinkBlockDev(instance.name, device.dev_path, idx)
1060
    except OSError, e:
1061
      raise errors.BlockDeviceError("Cannot create block device symlink: %s" %
1062
                                    e.strerror)
1063

    
1064
    block_devices.append((disk, link_name))
1065

    
1066
  return block_devices
1067

    
1068

    
1069
def StartInstance(instance):
1070
  """Start an instance.
1071

1072
  @type instance: L{objects.Instance}
1073
  @param instance: the instance object
1074
  @rtype: None
1075

1076
  """
1077
  running_instances = GetInstanceList([instance.hypervisor])
1078

    
1079
  if instance.name in running_instances:
1080
    logging.info("Instance %s already running, not starting", instance.name)
1081
    return
1082

    
1083
  try:
1084
    block_devices = _GatherAndLinkBlockDevs(instance)
1085
    hyper = hypervisor.GetHypervisor(instance.hypervisor)
1086
    hyper.StartInstance(instance, block_devices)
1087
  except errors.BlockDeviceError, err:
1088
    _Fail("Block device error: %s", err, exc=True)
1089
  except errors.HypervisorError, err:
1090
    _RemoveBlockDevLinks(instance.name, instance.disks)
1091
    _Fail("Hypervisor error: %s", err, exc=True)
1092

    
1093

    
1094
def InstanceShutdown(instance, timeout):
1095
  """Shut an instance down.
1096

1097
  @note: this functions uses polling with a hardcoded timeout.
1098

1099
  @type instance: L{objects.Instance}
1100
  @param instance: the instance object
1101
  @type timeout: integer
1102
  @param timeout: maximum timeout for soft shutdown
1103
  @rtype: None
1104

1105
  """
1106
  hv_name = instance.hypervisor
1107
  hyper = hypervisor.GetHypervisor(hv_name)
1108
  iname = instance.name
1109

    
1110
  if instance.name not in hyper.ListInstances():
1111
    logging.info("Instance %s not running, doing nothing", iname)
1112
    return
1113

    
1114
  class _TryShutdown:
1115
    def __init__(self):
1116
      self.tried_once = False
1117

    
1118
    def __call__(self):
1119
      if iname not in hyper.ListInstances():
1120
        return
1121

    
1122
      try:
1123
        hyper.StopInstance(instance, retry=self.tried_once)
1124
      except errors.HypervisorError, err:
1125
        if iname not in hyper.ListInstances():
1126
          # if the instance is no longer existing, consider this a
1127
          # success and go to cleanup
1128
          return
1129

    
1130
        _Fail("Failed to stop instance %s: %s", iname, err)
1131

    
1132
      self.tried_once = True
1133

    
1134
      raise utils.RetryAgain()
1135

    
1136
  try:
1137
    utils.Retry(_TryShutdown(), 5, timeout)
1138
  except utils.RetryTimeout:
1139
    # the shutdown did not succeed
1140
    logging.error("Shutdown of '%s' unsuccessful, forcing", iname)
1141

    
1142
    try:
1143
      hyper.StopInstance(instance, force=True)
1144
    except errors.HypervisorError, err:
1145
      if iname in hyper.ListInstances():
1146
        # only raise an error if the instance still exists, otherwise
1147
        # the error could simply be "instance ... unknown"!
1148
        _Fail("Failed to force stop instance %s: %s", iname, err)
1149

    
1150
    time.sleep(1)
1151

    
1152
    if iname in hyper.ListInstances():
1153
      _Fail("Could not shutdown instance %s even by destroy", iname)
1154

    
1155
  try:
1156
    hyper.CleanupInstance(instance.name)
1157
  except errors.HypervisorError, err:
1158
    logging.warning("Failed to execute post-shutdown cleanup step: %s", err)
1159

    
1160
  _RemoveBlockDevLinks(iname, instance.disks)
1161

    
1162

    
1163
def InstanceReboot(instance, reboot_type, shutdown_timeout):
1164
  """Reboot an instance.
1165

1166
  @type instance: L{objects.Instance}
1167
  @param instance: the instance object to reboot
1168
  @type reboot_type: str
1169
  @param reboot_type: the type of reboot, one the following
1170
    constants:
1171
      - L{constants.INSTANCE_REBOOT_SOFT}: only reboot the
1172
        instance OS, do not recreate the VM
1173
      - L{constants.INSTANCE_REBOOT_HARD}: tear down and
1174
        restart the VM (at the hypervisor level)
1175
      - the other reboot type (L{constants.INSTANCE_REBOOT_FULL}) is
1176
        not accepted here, since that mode is handled differently, in
1177
        cmdlib, and translates into full stop and start of the
1178
        instance (instead of a call_instance_reboot RPC)
1179
  @type shutdown_timeout: integer
1180
  @param shutdown_timeout: maximum timeout for soft shutdown
1181
  @rtype: None
1182

1183
  """
1184
  running_instances = GetInstanceList([instance.hypervisor])
1185

    
1186
  if instance.name not in running_instances:
1187
    _Fail("Cannot reboot instance %s that is not running", instance.name)
1188

    
1189
  hyper = hypervisor.GetHypervisor(instance.hypervisor)
1190
  if reboot_type == constants.INSTANCE_REBOOT_SOFT:
1191
    try:
1192
      hyper.RebootInstance(instance)
1193
    except errors.HypervisorError, err:
1194
      _Fail("Failed to soft reboot instance %s: %s", instance.name, err)
1195
  elif reboot_type == constants.INSTANCE_REBOOT_HARD:
1196
    try:
1197
      InstanceShutdown(instance, shutdown_timeout)
1198
      return StartInstance(instance)
1199
    except errors.HypervisorError, err:
1200
      _Fail("Failed to hard reboot instance %s: %s", instance.name, err)
1201
  else:
1202
    _Fail("Invalid reboot_type received: %s", reboot_type)
1203

    
1204

    
1205
def MigrationInfo(instance):
1206
  """Gather information about an instance to be migrated.
1207

1208
  @type instance: L{objects.Instance}
1209
  @param instance: the instance definition
1210

1211
  """
1212
  hyper = hypervisor.GetHypervisor(instance.hypervisor)
1213
  try:
1214
    info = hyper.MigrationInfo(instance)
1215
  except errors.HypervisorError, err:
1216
    _Fail("Failed to fetch migration information: %s", err, exc=True)
1217
  return info
1218

    
1219

    
1220
def AcceptInstance(instance, info, target):
1221
  """Prepare the node to accept an instance.
1222

1223
  @type instance: L{objects.Instance}
1224
  @param instance: the instance definition
1225
  @type info: string/data (opaque)
1226
  @param info: migration information, from the source node
1227
  @type target: string
1228
  @param target: target host (usually ip), on this node
1229

1230
  """
1231
  hyper = hypervisor.GetHypervisor(instance.hypervisor)
1232
  try:
1233
    hyper.AcceptInstance(instance, info, target)
1234
  except errors.HypervisorError, err:
1235
    _Fail("Failed to accept instance: %s", err, exc=True)
1236

    
1237

    
1238
def FinalizeMigration(instance, info, success):
1239
  """Finalize any preparation to accept an instance.
1240

1241
  @type instance: L{objects.Instance}
1242
  @param instance: the instance definition
1243
  @type info: string/data (opaque)
1244
  @param info: migration information, from the source node
1245
  @type success: boolean
1246
  @param success: whether the migration was a success or a failure
1247

1248
  """
1249
  hyper = hypervisor.GetHypervisor(instance.hypervisor)
1250
  try:
1251
    hyper.FinalizeMigration(instance, info, success)
1252
  except errors.HypervisorError, err:
1253
    _Fail("Failed to finalize migration: %s", err, exc=True)
1254

    
1255

    
1256
def MigrateInstance(instance, target, live):
1257
  """Migrates an instance to another node.
1258

1259
  @type instance: L{objects.Instance}
1260
  @param instance: the instance definition
1261
  @type target: string
1262
  @param target: the target node name
1263
  @type live: boolean
1264
  @param live: whether the migration should be done live or not (the
1265
      interpretation of this parameter is left to the hypervisor)
1266
  @rtype: tuple
1267
  @return: a tuple of (success, msg) where:
1268
      - succes is a boolean denoting the success/failure of the operation
1269
      - msg is a string with details in case of failure
1270

1271
  """
1272
  hyper = hypervisor.GetHypervisor(instance.hypervisor)
1273

    
1274
  try:
1275
    hyper.MigrateInstance(instance, target, live)
1276
  except errors.HypervisorError, err:
1277
    _Fail("Failed to migrate instance: %s", err, exc=True)
1278

    
1279

    
1280
def BlockdevCreate(disk, size, owner, on_primary, info):
1281
  """Creates a block device for an instance.
1282

1283
  @type disk: L{objects.Disk}
1284
  @param disk: the object describing the disk we should create
1285
  @type size: int
1286
  @param size: the size of the physical underlying device, in MiB
1287
  @type owner: str
1288
  @param owner: the name of the instance for which disk is created,
1289
      used for device cache data
1290
  @type on_primary: boolean
1291
  @param on_primary:  indicates if it is the primary node or not
1292
  @type info: string
1293
  @param info: string that will be sent to the physical device
1294
      creation, used for example to set (LVM) tags on LVs
1295

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

1300
  """
1301
  # TODO: remove the obsolete 'size' argument
1302
  # pylint: disable-msg=W0613
1303
  clist = []
1304
  if disk.children:
1305
    for child in disk.children:
1306
      try:
1307
        crdev = _RecursiveAssembleBD(child, owner, on_primary)
1308
      except errors.BlockDeviceError, err:
1309
        _Fail("Can't assemble device %s: %s", child, err)
1310
      if on_primary or disk.AssembleOnSecondary():
1311
        # we need the children open in case the device itself has to
1312
        # be assembled
1313
        try:
1314
          # pylint: disable-msg=E1103
1315
          crdev.Open()
1316
        except errors.BlockDeviceError, err:
1317
          _Fail("Can't make child '%s' read-write: %s", child, err)
1318
      clist.append(crdev)
1319

    
1320
  try:
1321
    device = bdev.Create(disk.dev_type, disk.physical_id, clist, disk.size)
1322
  except errors.BlockDeviceError, err:
1323
    _Fail("Can't create block device: %s", err)
1324

    
1325
  if on_primary or disk.AssembleOnSecondary():
1326
    try:
1327
      device.Assemble()
1328
    except errors.BlockDeviceError, err:
1329
      _Fail("Can't assemble device after creation, unusual event: %s", err)
1330
    device.SetSyncSpeed(constants.SYNC_SPEED)
1331
    if on_primary or disk.OpenOnSecondary():
1332
      try:
1333
        device.Open(force=True)
1334
      except errors.BlockDeviceError, err:
1335
        _Fail("Can't make device r/w after creation, unusual event: %s", err)
1336
    DevCacheManager.UpdateCache(device.dev_path, owner,
1337
                                on_primary, disk.iv_name)
1338

    
1339
  device.SetInfo(info)
1340

    
1341
  return device.unique_id
1342

    
1343

    
1344
def BlockdevRemove(disk):
1345
  """Remove a block device.
1346

1347
  @note: This is intended to be called recursively.
1348

1349
  @type disk: L{objects.Disk}
1350
  @param disk: the disk object we should remove
1351
  @rtype: boolean
1352
  @return: the success of the operation
1353

1354
  """
1355
  msgs = []
1356
  try:
1357
    rdev = _RecursiveFindBD(disk)
1358
  except errors.BlockDeviceError, err:
1359
    # probably can't attach
1360
    logging.info("Can't attach to device %s in remove", disk)
1361
    rdev = None
1362
  if rdev is not None:
1363
    r_path = rdev.dev_path
1364
    try:
1365
      rdev.Remove()
1366
    except errors.BlockDeviceError, err:
1367
      msgs.append(str(err))
1368
    if not msgs:
1369
      DevCacheManager.RemoveCache(r_path)
1370

    
1371
  if disk.children:
1372
    for child in disk.children:
1373
      try:
1374
        BlockdevRemove(child)
1375
      except RPCFail, err:
1376
        msgs.append(str(err))
1377

    
1378
  if msgs:
1379
    _Fail("; ".join(msgs))
1380

    
1381

    
1382
def _RecursiveAssembleBD(disk, owner, as_primary):
1383
  """Activate a block device for an instance.
1384

1385
  This is run on the primary and secondary nodes for an instance.
1386

1387
  @note: this function is called recursively.
1388

1389
  @type disk: L{objects.Disk}
1390
  @param disk: the disk we try to assemble
1391
  @type owner: str
1392
  @param owner: the name of the instance which owns the disk
1393
  @type as_primary: boolean
1394
  @param as_primary: if we should make the block device
1395
      read/write
1396

1397
  @return: the assembled device or None (in case no device
1398
      was assembled)
1399
  @raise errors.BlockDeviceError: in case there is an error
1400
      during the activation of the children or the device
1401
      itself
1402

1403
  """
1404
  children = []
1405
  if disk.children:
1406
    mcn = disk.ChildrenNeeded()
1407
    if mcn == -1:
1408
      mcn = 0 # max number of Nones allowed
1409
    else:
1410
      mcn = len(disk.children) - mcn # max number of Nones
1411
    for chld_disk in disk.children:
1412
      try:
1413
        cdev = _RecursiveAssembleBD(chld_disk, owner, as_primary)
1414
      except errors.BlockDeviceError, err:
1415
        if children.count(None) >= mcn:
1416
          raise
1417
        cdev = None
1418
        logging.error("Error in child activation (but continuing): %s",
1419
                      str(err))
1420
      children.append(cdev)
1421

    
1422
  if as_primary or disk.AssembleOnSecondary():
1423
    r_dev = bdev.Assemble(disk.dev_type, disk.physical_id, children, disk.size)
1424
    r_dev.SetSyncSpeed(constants.SYNC_SPEED)
1425
    result = r_dev
1426
    if as_primary or disk.OpenOnSecondary():
1427
      r_dev.Open()
1428
    DevCacheManager.UpdateCache(r_dev.dev_path, owner,
1429
                                as_primary, disk.iv_name)
1430

    
1431
  else:
1432
    result = True
1433
  return result
1434

    
1435

    
1436
def BlockdevAssemble(disk, owner, as_primary):
1437
  """Activate a block device for an instance.
1438

1439
  This is a wrapper over _RecursiveAssembleBD.
1440

1441
  @rtype: str or boolean
1442
  @return: a C{/dev/...} path for primary nodes, and
1443
      C{True} for secondary nodes
1444

1445
  """
1446
  try:
1447
    result = _RecursiveAssembleBD(disk, owner, as_primary)
1448
    if isinstance(result, bdev.BlockDev):
1449
      # pylint: disable-msg=E1103
1450
      result = result.dev_path
1451
  except errors.BlockDeviceError, err:
1452
    _Fail("Error while assembling disk: %s", err, exc=True)
1453

    
1454
  return result
1455

    
1456

    
1457
def BlockdevShutdown(disk):
1458
  """Shut down a block device.
1459

1460
  First, if the device is assembled (Attach() is successful), then
1461
  the device is shutdown. Then the children of the device are
1462
  shutdown.
1463

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

1468
  @type disk: L{objects.Disk}
1469
  @param disk: the description of the disk we should
1470
      shutdown
1471
  @rtype: None
1472

1473
  """
1474
  msgs = []
1475
  r_dev = _RecursiveFindBD(disk)
1476
  if r_dev is not None:
1477
    r_path = r_dev.dev_path
1478
    try:
1479
      r_dev.Shutdown()
1480
      DevCacheManager.RemoveCache(r_path)
1481
    except errors.BlockDeviceError, err:
1482
      msgs.append(str(err))
1483

    
1484
  if disk.children:
1485
    for child in disk.children:
1486
      try:
1487
        BlockdevShutdown(child)
1488
      except RPCFail, err:
1489
        msgs.append(str(err))
1490

    
1491
  if msgs:
1492
    _Fail("; ".join(msgs))
1493

    
1494

    
1495
def BlockdevAddchildren(parent_cdev, new_cdevs):
1496
  """Extend a mirrored block device.
1497

1498
  @type parent_cdev: L{objects.Disk}
1499
  @param parent_cdev: the disk to which we should add children
1500
  @type new_cdevs: list of L{objects.Disk}
1501
  @param new_cdevs: the list of children which we should add
1502
  @rtype: None
1503

1504
  """
1505
  parent_bdev = _RecursiveFindBD(parent_cdev)
1506
  if parent_bdev is None:
1507
    _Fail("Can't find parent device '%s' in add children", parent_cdev)
1508
  new_bdevs = [_RecursiveFindBD(disk) for disk in new_cdevs]
1509
  if new_bdevs.count(None) > 0:
1510
    _Fail("Can't find new device(s) to add: %s:%s", new_bdevs, new_cdevs)
1511
  parent_bdev.AddChildren(new_bdevs)
1512

    
1513

    
1514
def BlockdevRemovechildren(parent_cdev, new_cdevs):
1515
  """Shrink a mirrored block device.
1516

1517
  @type parent_cdev: L{objects.Disk}
1518
  @param parent_cdev: the disk from which we should remove children
1519
  @type new_cdevs: list of L{objects.Disk}
1520
  @param new_cdevs: the list of children which we should remove
1521
  @rtype: None
1522

1523
  """
1524
  parent_bdev = _RecursiveFindBD(parent_cdev)
1525
  if parent_bdev is None:
1526
    _Fail("Can't find parent device '%s' in remove children", parent_cdev)
1527
  devs = []
1528
  for disk in new_cdevs:
1529
    rpath = disk.StaticDevPath()
1530
    if rpath is None:
1531
      bd = _RecursiveFindBD(disk)
1532
      if bd is None:
1533
        _Fail("Can't find device %s while removing children", disk)
1534
      else:
1535
        devs.append(bd.dev_path)
1536
    else:
1537
      if not utils.IsNormAbsPath(rpath):
1538
        _Fail("Strange path returned from StaticDevPath: '%s'", rpath)
1539
      devs.append(rpath)
1540
  parent_bdev.RemoveChildren(devs)
1541

    
1542

    
1543
def BlockdevGetmirrorstatus(disks):
1544
  """Get the mirroring status of a list of devices.
1545

1546
  @type disks: list of L{objects.Disk}
1547
  @param disks: the list of disks which we should query
1548
  @rtype: disk
1549
  @return:
1550
      a list of (mirror_done, estimated_time) tuples, which
1551
      are the result of L{bdev.BlockDev.CombinedSyncStatus}
1552
  @raise errors.BlockDeviceError: if any of the disks cannot be
1553
      found
1554

1555
  """
1556
  stats = []
1557
  for dsk in disks:
1558
    rbd = _RecursiveFindBD(dsk)
1559
    if rbd is None:
1560
      _Fail("Can't find device %s", dsk)
1561

    
1562
    stats.append(rbd.CombinedSyncStatus())
1563

    
1564
  return stats
1565

    
1566

    
1567
def _RecursiveFindBD(disk):
1568
  """Check if a device is activated.
1569

1570
  If so, return information about the real device.
1571

1572
  @type disk: L{objects.Disk}
1573
  @param disk: the disk object we need to find
1574

1575
  @return: None if the device can't be found,
1576
      otherwise the device instance
1577

1578
  """
1579
  children = []
1580
  if disk.children:
1581
    for chdisk in disk.children:
1582
      children.append(_RecursiveFindBD(chdisk))
1583

    
1584
  return bdev.FindDevice(disk.dev_type, disk.physical_id, children, disk.size)
1585

    
1586

    
1587
def _OpenRealBD(disk):
1588
  """Opens the underlying block device of a disk.
1589

1590
  @type disk: L{objects.Disk}
1591
  @param disk: the disk object we want to open
1592

1593
  """
1594
  real_disk = _RecursiveFindBD(disk)
1595
  if real_disk is None:
1596
    _Fail("Block device '%s' is not set up", disk)
1597

    
1598
  real_disk.Open()
1599

    
1600
  return real_disk
1601

    
1602

    
1603
def BlockdevFind(disk):
1604
  """Check if a device is activated.
1605

1606
  If it is, return information about the real device.
1607

1608
  @type disk: L{objects.Disk}
1609
  @param disk: the disk to find
1610
  @rtype: None or objects.BlockDevStatus
1611
  @return: None if the disk cannot be found, otherwise a the current
1612
           information
1613

1614
  """
1615
  try:
1616
    rbd = _RecursiveFindBD(disk)
1617
  except errors.BlockDeviceError, err:
1618
    _Fail("Failed to find device: %s", err, exc=True)
1619

    
1620
  if rbd is None:
1621
    return None
1622

    
1623
  return rbd.GetSyncStatus()
1624

    
1625

    
1626
def BlockdevGetsize(disks):
1627
  """Computes the size of the given disks.
1628

1629
  If a disk is not found, returns None instead.
1630

1631
  @type disks: list of L{objects.Disk}
1632
  @param disks: the list of disk to compute the size for
1633
  @rtype: list
1634
  @return: list with elements None if the disk cannot be found,
1635
      otherwise the size
1636

1637
  """
1638
  result = []
1639
  for cf in disks:
1640
    try:
1641
      rbd = _RecursiveFindBD(cf)
1642
    except errors.BlockDeviceError:
1643
      result.append(None)
1644
      continue
1645
    if rbd is None:
1646
      result.append(None)
1647
    else:
1648
      result.append(rbd.GetActualSize())
1649
  return result
1650

    
1651

    
1652
def BlockdevExport(disk, dest_node, dest_path, cluster_name):
1653
  """Export a block device to a remote node.
1654

1655
  @type disk: L{objects.Disk}
1656
  @param disk: the description of the disk to export
1657
  @type dest_node: str
1658
  @param dest_node: the destination node to export to
1659
  @type dest_path: str
1660
  @param dest_path: the destination path on the target node
1661
  @type cluster_name: str
1662
  @param cluster_name: the cluster name, needed for SSH hostalias
1663
  @rtype: None
1664

1665
  """
1666
  real_disk = _OpenRealBD(disk)
1667

    
1668
  # the block size on the read dd is 1MiB to match our units
1669
  expcmd = utils.BuildShellCmd("set -e; set -o pipefail; "
1670
                               "dd if=%s bs=1048576 count=%s",
1671
                               real_disk.dev_path, str(disk.size))
1672

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

    
1682
  remotecmd = _GetSshRunner(cluster_name).BuildCmd(dest_node,
1683
                                                   constants.GANETI_RUNAS,
1684
                                                   destcmd)
1685

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

    
1689
  result = utils.RunCmd(["bash", "-c", command])
1690

    
1691
  if result.failed:
1692
    _Fail("Disk copy command '%s' returned error: %s"
1693
          " output: %s", command, result.fail_reason, result.output)
1694

    
1695

    
1696
def UploadFile(file_name, data, mode, uid, gid, atime, mtime):
1697
  """Write a file to the filesystem.
1698

1699
  This allows the master to overwrite(!) a file. It will only perform
1700
  the operation if the file belongs to a list of configuration files.
1701

1702
  @type file_name: str
1703
  @param file_name: the target file name
1704
  @type data: str
1705
  @param data: the new contents of the file
1706
  @type mode: int
1707
  @param mode: the mode to give the file (can be None)
1708
  @type uid: int
1709
  @param uid: the owner of the file (can be -1 for default)
1710
  @type gid: int
1711
  @param gid: the group of the file (can be -1 for default)
1712
  @type atime: float
1713
  @param atime: the atime to set on the file (can be None)
1714
  @type mtime: float
1715
  @param mtime: the mtime to set on the file (can be None)
1716
  @rtype: None
1717

1718
  """
1719
  if not os.path.isabs(file_name):
1720
    _Fail("Filename passed to UploadFile is not absolute: '%s'", file_name)
1721

    
1722
  if file_name not in _ALLOWED_UPLOAD_FILES:
1723
    _Fail("Filename passed to UploadFile not in allowed upload targets: '%s'",
1724
          file_name)
1725

    
1726
  raw_data = _Decompress(data)
1727

    
1728
  utils.WriteFile(file_name, data=raw_data, mode=mode, uid=uid, gid=gid,
1729
                  atime=atime, mtime=mtime)
1730

    
1731

    
1732
def WriteSsconfFiles(values):
1733
  """Update all ssconf files.
1734

1735
  Wrapper around the SimpleStore.WriteFiles.
1736

1737
  """
1738
  ssconf.SimpleStore().WriteFiles(values)
1739

    
1740

    
1741
def _ErrnoOrStr(err):
1742
  """Format an EnvironmentError exception.
1743

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

1748
  @type err: L{EnvironmentError}
1749
  @param err: the exception to format
1750

1751
  """
1752
  if hasattr(err, 'errno'):
1753
    detail = errno.errorcode[err.errno]
1754
  else:
1755
    detail = str(err)
1756
  return detail
1757

    
1758

    
1759
def _OSOndiskAPIVersion(os_dir):
1760
  """Compute and return the API version of a given OS.
1761

1762
  This function will try to read the API version of the OS residing in
1763
  the 'os_dir' directory.
1764

1765
  @type os_dir: str
1766
  @param os_dir: the directory in which we should look for the OS
1767
  @rtype: tuple
1768
  @return: tuple (status, data) with status denoting the validity and
1769
      data holding either the vaid versions or an error message
1770

1771
  """
1772
  api_file = utils.PathJoin(os_dir, constants.OS_API_FILE)
1773

    
1774
  try:
1775
    st = os.stat(api_file)
1776
  except EnvironmentError, err:
1777
    return False, ("Required file '%s' not found under path %s: %s" %
1778
                   (constants.OS_API_FILE, os_dir, _ErrnoOrStr(err)))
1779

    
1780
  if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1781
    return False, ("File '%s' in %s is not a regular file" %
1782
                   (constants.OS_API_FILE, os_dir))
1783

    
1784
  try:
1785
    api_versions = utils.ReadFile(api_file).splitlines()
1786
  except EnvironmentError, err:
1787
    return False, ("Error while reading the API version file at %s: %s" %
1788
                   (api_file, _ErrnoOrStr(err)))
1789

    
1790
  try:
1791
    api_versions = [int(version.strip()) for version in api_versions]
1792
  except (TypeError, ValueError), err:
1793
    return False, ("API version(s) can't be converted to integer: %s" %
1794
                   str(err))
1795

    
1796
  return True, api_versions
1797

    
1798

    
1799
def DiagnoseOS(top_dirs=None):
1800
  """Compute the validity for all OSes.
1801

1802
  @type top_dirs: list
1803
  @param top_dirs: the list of directories in which to
1804
      search (if not given defaults to
1805
      L{constants.OS_SEARCH_PATH})
1806
  @rtype: list of L{objects.OS}
1807
  @return: a list of tuples (name, path, status, diagnose, variants,
1808
      parameters, api_version) for all (potential) OSes under all
1809
      search paths, where:
1810
          - name is the (potential) OS name
1811
          - path is the full path to the OS
1812
          - status True/False is the validity of the OS
1813
          - diagnose is the error message for an invalid OS, otherwise empty
1814
          - variants is a list of supported OS variants, if any
1815
          - parameters is a list of (name, help) parameters, if any
1816
          - api_version is a list of support OS API versions
1817

1818
  """
1819
  if top_dirs is None:
1820
    top_dirs = constants.OS_SEARCH_PATH
1821

    
1822
  result = []
1823
  for dir_name in top_dirs:
1824
    if os.path.isdir(dir_name):
1825
      try:
1826
        f_names = utils.ListVisibleFiles(dir_name)
1827
      except EnvironmentError, err:
1828
        logging.exception("Can't list the OS directory %s: %s", dir_name, err)
1829
        break
1830
      for name in f_names:
1831
        os_path = utils.PathJoin(dir_name, name)
1832
        status, os_inst = _TryOSFromDisk(name, base_dir=dir_name)
1833
        if status:
1834
          diagnose = ""
1835
          variants = os_inst.supported_variants
1836
          parameters = os_inst.supported_parameters
1837
          api_versions = os_inst.api_versions
1838
        else:
1839
          diagnose = os_inst
1840
          variants = parameters = api_versions = []
1841
        result.append((name, os_path, status, diagnose, variants,
1842
                       parameters, api_versions))
1843

    
1844
  return result
1845

    
1846

    
1847
def _TryOSFromDisk(name, base_dir=None):
1848
  """Create an OS instance from disk.
1849

1850
  This function will return an OS instance if the given name is a
1851
  valid OS name.
1852

1853
  @type base_dir: string
1854
  @keyword base_dir: Base directory containing OS installations.
1855
                     Defaults to a search in all the OS_SEARCH_PATH dirs.
1856
  @rtype: tuple
1857
  @return: success and either the OS instance if we find a valid one,
1858
      or error message
1859

1860
  """
1861
  if base_dir is None:
1862
    os_dir = utils.FindFile(name, constants.OS_SEARCH_PATH, os.path.isdir)
1863
  else:
1864
    os_dir = utils.FindFile(name, [base_dir], os.path.isdir)
1865

    
1866
  if os_dir is None:
1867
    return False, "Directory for OS %s not found in search path" % name
1868

    
1869
  status, api_versions = _OSOndiskAPIVersion(os_dir)
1870
  if not status:
1871
    # push the error up
1872
    return status, api_versions
1873

    
1874
  if not constants.OS_API_VERSIONS.intersection(api_versions):
1875
    return False, ("API version mismatch for path '%s': found %s, want %s." %
1876
                   (os_dir, api_versions, constants.OS_API_VERSIONS))
1877

    
1878
  # OS Files dictionary, we will populate it with the absolute path names
1879
  os_files = dict.fromkeys(constants.OS_SCRIPTS)
1880

    
1881
  if max(api_versions) >= constants.OS_API_V15:
1882
    os_files[constants.OS_VARIANTS_FILE] = ''
1883

    
1884
  if max(api_versions) >= constants.OS_API_V20:
1885
    os_files[constants.OS_PARAMETERS_FILE] = ''
1886
  else:
1887
    del os_files[constants.OS_SCRIPT_VERIFY]
1888

    
1889
  for filename in os_files:
1890
    os_files[filename] = utils.PathJoin(os_dir, filename)
1891

    
1892
    try:
1893
      st = os.stat(os_files[filename])
1894
    except EnvironmentError, err:
1895
      return False, ("File '%s' under path '%s' is missing (%s)" %
1896
                     (filename, os_dir, _ErrnoOrStr(err)))
1897

    
1898
    if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
1899
      return False, ("File '%s' under path '%s' is not a regular file" %
1900
                     (filename, os_dir))
1901

    
1902
    if filename in constants.OS_SCRIPTS:
1903
      if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
1904
        return False, ("File '%s' under path '%s' is not executable" %
1905
                       (filename, os_dir))
1906

    
1907
  variants = []
1908
  if constants.OS_VARIANTS_FILE in os_files:
1909
    variants_file = os_files[constants.OS_VARIANTS_FILE]
1910
    try:
1911
      variants = utils.ReadFile(variants_file).splitlines()
1912
    except EnvironmentError, err:
1913
      return False, ("Error while reading the OS variants file at %s: %s" %
1914
                     (variants_file, _ErrnoOrStr(err)))
1915
    if not variants:
1916
      return False, ("No supported os variant found")
1917

    
1918
  parameters = []
1919
  if constants.OS_PARAMETERS_FILE in os_files:
1920
    parameters_file = os_files[constants.OS_PARAMETERS_FILE]
1921
    try:
1922
      parameters = utils.ReadFile(parameters_file).splitlines()
1923
    except EnvironmentError, err:
1924
      return False, ("Error while reading the OS parameters file at %s: %s" %
1925
                     (parameters_file, _ErrnoOrStr(err)))
1926
    parameters = [v.split(None, 1) for v in parameters]
1927

    
1928
  os_obj = objects.OS(name=name, path=os_dir,
1929
                      create_script=os_files[constants.OS_SCRIPT_CREATE],
1930
                      export_script=os_files[constants.OS_SCRIPT_EXPORT],
1931
                      import_script=os_files[constants.OS_SCRIPT_IMPORT],
1932
                      rename_script=os_files[constants.OS_SCRIPT_RENAME],
1933
                      verify_script=os_files.get(constants.OS_SCRIPT_VERIFY,
1934
                                                 None),
1935
                      supported_variants=variants,
1936
                      supported_parameters=parameters,
1937
                      api_versions=api_versions)
1938
  return True, os_obj
1939

    
1940

    
1941
def OSFromDisk(name, base_dir=None):
1942
  """Create an OS instance from disk.
1943

1944
  This function will return an OS instance if the given name is a
1945
  valid OS name. Otherwise, it will raise an appropriate
1946
  L{RPCFail} exception, detailing why this is not a valid OS.
1947

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

1951
  @type base_dir: string
1952
  @keyword base_dir: Base directory containing OS installations.
1953
                     Defaults to a search in all the OS_SEARCH_PATH dirs.
1954
  @rtype: L{objects.OS}
1955
  @return: the OS instance if we find a valid one
1956
  @raise RPCFail: if we don't find a valid OS
1957

1958
  """
1959
  name_only = name.split("+", 1)[0]
1960
  status, payload = _TryOSFromDisk(name_only, base_dir)
1961

    
1962
  if not status:
1963
    _Fail(payload)
1964

    
1965
  return payload
1966

    
1967

    
1968
def OSCoreEnv(inst_os, os_params, debug=0):
1969
  """Calculate the basic environment for an os script.
1970

1971
  @type inst_os: L{objects.OS}
1972
  @param inst_os: operating system for which the environment is being built
1973
  @type os_params: dict
1974
  @param os_params: the OS parameters
1975
  @type debug: integer
1976
  @param debug: debug level (0 or 1, for OS Api 10)
1977
  @rtype: dict
1978
  @return: dict of environment variables
1979
  @raise errors.BlockDeviceError: if the block device
1980
      cannot be found
1981

1982
  """
1983
  result = {}
1984
  api_version = \
1985
    max(constants.OS_API_VERSIONS.intersection(inst_os.api_versions))
1986
  result['OS_API_VERSION'] = '%d' % api_version
1987
  result['OS_NAME'] = inst_os.name
1988
  result['DEBUG_LEVEL'] = '%d' % debug
1989

    
1990
  # OS variants
1991
  if api_version >= constants.OS_API_V15:
1992
    try:
1993
      variant = inst_os.name.split('+', 1)[1]
1994
    except IndexError:
1995
      variant = inst_os.supported_variants[0]
1996
    result['OS_VARIANT'] = variant
1997

    
1998
  # OS params
1999
  for pname, pvalue in os_params.items():
2000
    result['OSP_%s' % pname.upper()] = pvalue
2001

    
2002
  return result
2003

    
2004

    
2005
def OSEnvironment(instance, inst_os, debug=0):
2006
  """Calculate the environment for an os script.
2007

2008
  @type instance: L{objects.Instance}
2009
  @param instance: target instance for the os script run
2010
  @type inst_os: L{objects.OS}
2011
  @param inst_os: operating system for which the environment is being built
2012
  @type debug: integer
2013
  @param debug: debug level (0 or 1, for OS Api 10)
2014
  @rtype: dict
2015
  @return: dict of environment variables
2016
  @raise errors.BlockDeviceError: if the block device
2017
      cannot be found
2018

2019
  """
2020
  result = OSCoreEnv(inst_os, instance.osparams, debug=debug)
2021

    
2022
  result['INSTANCE_NAME'] = instance.name
2023
  result['INSTANCE_OS'] = instance.os
2024
  result['HYPERVISOR'] = instance.hypervisor
2025
  result['DISK_COUNT'] = '%d' % len(instance.disks)
2026
  result['NIC_COUNT'] = '%d' % len(instance.nics)
2027

    
2028
  # Disks
2029
  for idx, disk in enumerate(instance.disks):
2030
    real_disk = _OpenRealBD(disk)
2031
    result['DISK_%d_PATH' % idx] = real_disk.dev_path
2032
    result['DISK_%d_ACCESS' % idx] = disk.mode
2033
    if constants.HV_DISK_TYPE in instance.hvparams:
2034
      result['DISK_%d_FRONTEND_TYPE' % idx] = \
2035
        instance.hvparams[constants.HV_DISK_TYPE]
2036
    if disk.dev_type in constants.LDS_BLOCK:
2037
      result['DISK_%d_BACKEND_TYPE' % idx] = 'block'
2038
    elif disk.dev_type == constants.LD_FILE:
2039
      result['DISK_%d_BACKEND_TYPE' % idx] = \
2040
        'file:%s' % disk.physical_id[0]
2041

    
2042
  # NICs
2043
  for idx, nic in enumerate(instance.nics):
2044
    result['NIC_%d_MAC' % idx] = nic.mac
2045
    if nic.ip:
2046
      result['NIC_%d_IP' % idx] = nic.ip
2047
    result['NIC_%d_MODE' % idx] = nic.nicparams[constants.NIC_MODE]
2048
    if nic.nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED:
2049
      result['NIC_%d_BRIDGE' % idx] = nic.nicparams[constants.NIC_LINK]
2050
    if nic.nicparams[constants.NIC_LINK]:
2051
      result['NIC_%d_LINK' % idx] = nic.nicparams[constants.NIC_LINK]
2052
    if constants.HV_NIC_TYPE in instance.hvparams:
2053
      result['NIC_%d_FRONTEND_TYPE' % idx] = \
2054
        instance.hvparams[constants.HV_NIC_TYPE]
2055

    
2056
  # HV/BE params
2057
  for source, kind in [(instance.beparams, "BE"), (instance.hvparams, "HV")]:
2058
    for key, value in source.items():
2059
      result["INSTANCE_%s_%s" % (kind, key)] = str(value)
2060

    
2061
  return result
2062

    
2063

    
2064
def BlockdevGrow(disk, amount):
2065
  """Grow a stack of block devices.
2066

2067
  This function is called recursively, with the childrens being the
2068
  first ones to resize.
2069

2070
  @type disk: L{objects.Disk}
2071
  @param disk: the disk to be grown
2072
  @rtype: (status, result)
2073
  @return: a tuple with the status of the operation
2074
      (True/False), and the errors message if status
2075
      is False
2076

2077
  """
2078
  r_dev = _RecursiveFindBD(disk)
2079
  if r_dev is None:
2080
    _Fail("Cannot find block device %s", disk)
2081

    
2082
  try:
2083
    r_dev.Grow(amount)
2084
  except errors.BlockDeviceError, err:
2085
    _Fail("Failed to grow block device: %s", err, exc=True)
2086

    
2087

    
2088
def BlockdevSnapshot(disk):
2089
  """Create a snapshot copy of a block device.
2090

2091
  This function is called recursively, and the snapshot is actually created
2092
  just for the leaf lvm backend device.
2093

2094
  @type disk: L{objects.Disk}
2095
  @param disk: the disk to be snapshotted
2096
  @rtype: string
2097
  @return: snapshot disk path
2098

2099
  """
2100
  if disk.dev_type == constants.LD_DRBD8:
2101
    if not disk.children:
2102
      _Fail("DRBD device '%s' without backing storage cannot be snapshotted",
2103
            disk.unique_id)
2104
    return BlockdevSnapshot(disk.children[0])
2105
  elif disk.dev_type == constants.LD_LV:
2106
    r_dev = _RecursiveFindBD(disk)
2107
    if r_dev is not None:
2108
      # FIXME: choose a saner value for the snapshot size
2109
      # let's stay on the safe side and ask for the full size, for now
2110
      return r_dev.Snapshot(disk.size)
2111
    else:
2112
      _Fail("Cannot find block device %s", disk)
2113
  else:
2114
    _Fail("Cannot snapshot non-lvm block device '%s' of type '%s'",
2115
          disk.unique_id, disk.dev_type)
2116

    
2117

    
2118
def FinalizeExport(instance, snap_disks):
2119
  """Write out the export configuration information.
2120

2121
  @type instance: L{objects.Instance}
2122
  @param instance: the instance which we export, used for
2123
      saving configuration
2124
  @type snap_disks: list of L{objects.Disk}
2125
  @param snap_disks: list of snapshot block devices, which
2126
      will be used to get the actual name of the dump file
2127

2128
  @rtype: None
2129

2130
  """
2131
  destdir = utils.PathJoin(constants.EXPORT_DIR, instance.name + ".new")
2132
  finaldestdir = utils.PathJoin(constants.EXPORT_DIR, instance.name)
2133

    
2134
  config = objects.SerializableConfigParser()
2135

    
2136
  config.add_section(constants.INISECT_EXP)
2137
  config.set(constants.INISECT_EXP, 'version', '0')
2138
  config.set(constants.INISECT_EXP, 'timestamp', '%d' % int(time.time()))
2139
  config.set(constants.INISECT_EXP, 'source', instance.primary_node)
2140
  config.set(constants.INISECT_EXP, 'os', instance.os)
2141
  config.set(constants.INISECT_EXP, 'compression', 'gzip')
2142

    
2143
  config.add_section(constants.INISECT_INS)
2144
  config.set(constants.INISECT_INS, 'name', instance.name)
2145
  config.set(constants.INISECT_INS, 'memory', '%d' %
2146
             instance.beparams[constants.BE_MEMORY])
2147
  config.set(constants.INISECT_INS, 'vcpus', '%d' %
2148
             instance.beparams[constants.BE_VCPUS])
2149
  config.set(constants.INISECT_INS, 'disk_template', instance.disk_template)
2150
  config.set(constants.INISECT_INS, 'hypervisor', instance.hypervisor)
2151

    
2152
  nic_total = 0
2153
  for nic_count, nic in enumerate(instance.nics):
2154
    nic_total += 1
2155
    config.set(constants.INISECT_INS, 'nic%d_mac' %
2156
               nic_count, '%s' % nic.mac)
2157
    config.set(constants.INISECT_INS, 'nic%d_ip' % nic_count, '%s' % nic.ip)
2158
    for param in constants.NICS_PARAMETER_TYPES:
2159
      config.set(constants.INISECT_INS, 'nic%d_%s' % (nic_count, param),
2160
                 '%s' % nic.nicparams.get(param, None))
2161
  # TODO: redundant: on load can read nics until it doesn't exist
2162
  config.set(constants.INISECT_INS, 'nic_count' , '%d' % nic_total)
2163

    
2164
  disk_total = 0
2165
  for disk_count, disk in enumerate(snap_disks):
2166
    if disk:
2167
      disk_total += 1
2168
      config.set(constants.INISECT_INS, 'disk%d_ivname' % disk_count,
2169
                 ('%s' % disk.iv_name))
2170
      config.set(constants.INISECT_INS, 'disk%d_dump' % disk_count,
2171
                 ('%s' % disk.physical_id[1]))
2172
      config.set(constants.INISECT_INS, 'disk%d_size' % disk_count,
2173
                 ('%d' % disk.size))
2174

    
2175
  config.set(constants.INISECT_INS, 'disk_count' , '%d' % disk_total)
2176

    
2177
  # New-style hypervisor/backend parameters
2178

    
2179
  config.add_section(constants.INISECT_HYP)
2180
  for name, value in instance.hvparams.items():
2181
    if name not in constants.HVC_GLOBALS:
2182
      config.set(constants.INISECT_HYP, name, str(value))
2183

    
2184
  config.add_section(constants.INISECT_BEP)
2185
  for name, value in instance.beparams.items():
2186
    config.set(constants.INISECT_BEP, name, str(value))
2187

    
2188
  config.add_section(constants.INISECT_OSP)
2189
  for name, value in instance.osparams.items():
2190
    config.set(constants.INISECT_OSP, name, str(value))
2191

    
2192
  utils.WriteFile(utils.PathJoin(destdir, constants.EXPORT_CONF_FILE),
2193
                  data=config.Dumps())
2194
  shutil.rmtree(finaldestdir, ignore_errors=True)
2195
  shutil.move(destdir, finaldestdir)
2196

    
2197

    
2198
def ExportInfo(dest):
2199
  """Get export configuration information.
2200

2201
  @type dest: str
2202
  @param dest: directory containing the export
2203

2204
  @rtype: L{objects.SerializableConfigParser}
2205
  @return: a serializable config file containing the
2206
      export info
2207

2208
  """
2209
  cff = utils.PathJoin(dest, constants.EXPORT_CONF_FILE)
2210

    
2211
  config = objects.SerializableConfigParser()
2212
  config.read(cff)
2213

    
2214
  if (not config.has_section(constants.INISECT_EXP) or
2215
      not config.has_section(constants.INISECT_INS)):
2216
    _Fail("Export info file doesn't have the required fields")
2217

    
2218
  return config.Dumps()
2219

    
2220

    
2221
def ListExports():
2222
  """Return a list of exports currently available on this machine.
2223

2224
  @rtype: list
2225
  @return: list of the exports
2226

2227
  """
2228
  if os.path.isdir(constants.EXPORT_DIR):
2229
    return sorted(utils.ListVisibleFiles(constants.EXPORT_DIR))
2230
  else:
2231
    _Fail("No exports directory")
2232

    
2233

    
2234
def RemoveExport(export):
2235
  """Remove an existing export from the node.
2236

2237
  @type export: str
2238
  @param export: the name of the export to remove
2239
  @rtype: None
2240

2241
  """
2242
  target = utils.PathJoin(constants.EXPORT_DIR, export)
2243

    
2244
  try:
2245
    shutil.rmtree(target)
2246
  except EnvironmentError, err:
2247
    _Fail("Error while removing the export: %s", err, exc=True)
2248

    
2249

    
2250
def BlockdevRename(devlist):
2251
  """Rename a list of block devices.
2252

2253
  @type devlist: list of tuples
2254
  @param devlist: list of tuples of the form  (disk,
2255
      new_logical_id, new_physical_id); disk is an
2256
      L{objects.Disk} object describing the current disk,
2257
      and new logical_id/physical_id is the name we
2258
      rename it to
2259
  @rtype: boolean
2260
  @return: True if all renames succeeded, False otherwise
2261

2262
  """
2263
  msgs = []
2264
  result = True
2265
  for disk, unique_id in devlist:
2266
    dev = _RecursiveFindBD(disk)
2267
    if dev is None:
2268
      msgs.append("Can't find device %s in rename" % str(disk))
2269
      result = False
2270
      continue
2271
    try:
2272
      old_rpath = dev.dev_path
2273
      dev.Rename(unique_id)
2274
      new_rpath = dev.dev_path
2275
      if old_rpath != new_rpath:
2276
        DevCacheManager.RemoveCache(old_rpath)
2277
        # FIXME: we should add the new cache information here, like:
2278
        # DevCacheManager.UpdateCache(new_rpath, owner, ...)
2279
        # but we don't have the owner here - maybe parse from existing
2280
        # cache? for now, we only lose lvm data when we rename, which
2281
        # is less critical than DRBD or MD
2282
    except errors.BlockDeviceError, err:
2283
      msgs.append("Can't rename device '%s' to '%s': %s" %
2284
                  (dev, unique_id, err))
2285
      logging.exception("Can't rename device '%s' to '%s'", dev, unique_id)
2286
      result = False
2287
  if not result:
2288
    _Fail("; ".join(msgs))
2289

    
2290

    
2291
def _TransformFileStorageDir(file_storage_dir):
2292
  """Checks whether given file_storage_dir is valid.
2293

2294
  Checks wheter the given file_storage_dir is within the cluster-wide
2295
  default file_storage_dir stored in SimpleStore. Only paths under that
2296
  directory are allowed.
2297

2298
  @type file_storage_dir: str
2299
  @param file_storage_dir: the path to check
2300

2301
  @return: the normalized path if valid, None otherwise
2302

2303
  """
2304
  if not constants.ENABLE_FILE_STORAGE:
2305
    _Fail("File storage disabled at configure time")
2306
  cfg = _GetConfig()
2307
  file_storage_dir = os.path.normpath(file_storage_dir)
2308
  base_file_storage_dir = cfg.GetFileStorageDir()
2309
  if (os.path.commonprefix([file_storage_dir, base_file_storage_dir]) !=
2310
      base_file_storage_dir):
2311
    _Fail("File storage directory '%s' is not under base file"
2312
          " storage directory '%s'", file_storage_dir, base_file_storage_dir)
2313
  return file_storage_dir
2314

    
2315

    
2316
def CreateFileStorageDir(file_storage_dir):
2317
  """Create file storage directory.
2318

2319
  @type file_storage_dir: str
2320
  @param file_storage_dir: directory to create
2321

2322
  @rtype: tuple
2323
  @return: tuple with first element a boolean indicating wheter dir
2324
      creation was successful or not
2325

2326
  """
2327
  file_storage_dir = _TransformFileStorageDir(file_storage_dir)
2328
  if os.path.exists(file_storage_dir):
2329
    if not os.path.isdir(file_storage_dir):
2330
      _Fail("Specified storage dir '%s' is not a directory",
2331
            file_storage_dir)
2332
  else:
2333
    try:
2334
      os.makedirs(file_storage_dir, 0750)
2335
    except OSError, err:
2336
      _Fail("Cannot create file storage directory '%s': %s",
2337
            file_storage_dir, err, exc=True)
2338

    
2339

    
2340
def RemoveFileStorageDir(file_storage_dir):
2341
  """Remove file storage directory.
2342

2343
  Remove it only if it's empty. If not log an error and return.
2344

2345
  @type file_storage_dir: str
2346
  @param file_storage_dir: the directory we should cleanup
2347
  @rtype: tuple (success,)
2348
  @return: tuple of one element, C{success}, denoting
2349
      whether the operation was successful
2350

2351
  """
2352
  file_storage_dir = _TransformFileStorageDir(file_storage_dir)
2353
  if os.path.exists(file_storage_dir):
2354
    if not os.path.isdir(file_storage_dir):
2355
      _Fail("Specified Storage directory '%s' is not a directory",
2356
            file_storage_dir)
2357
    # deletes dir only if empty, otherwise we want to fail the rpc call
2358
    try:
2359
      os.rmdir(file_storage_dir)
2360
    except OSError, err:
2361
      _Fail("Cannot remove file storage directory '%s': %s",
2362
            file_storage_dir, err)
2363

    
2364

    
2365
def RenameFileStorageDir(old_file_storage_dir, new_file_storage_dir):
2366
  """Rename the file storage directory.
2367

2368
  @type old_file_storage_dir: str
2369
  @param old_file_storage_dir: the current path
2370
  @type new_file_storage_dir: str
2371
  @param new_file_storage_dir: the name we should rename to
2372
  @rtype: tuple (success,)
2373
  @return: tuple of one element, C{success}, denoting
2374
      whether the operation was successful
2375

2376
  """
2377
  old_file_storage_dir = _TransformFileStorageDir(old_file_storage_dir)
2378
  new_file_storage_dir = _TransformFileStorageDir(new_file_storage_dir)
2379
  if not os.path.exists(new_file_storage_dir):
2380
    if os.path.isdir(old_file_storage_dir):
2381
      try:
2382
        os.rename(old_file_storage_dir, new_file_storage_dir)
2383
      except OSError, err:
2384
        _Fail("Cannot rename '%s' to '%s': %s",
2385
              old_file_storage_dir, new_file_storage_dir, err)
2386
    else:
2387
      _Fail("Specified storage dir '%s' is not a directory",
2388
            old_file_storage_dir)
2389
  else:
2390
    if os.path.exists(old_file_storage_dir):
2391
      _Fail("Cannot rename '%s' to '%s': both locations exist",
2392
            old_file_storage_dir, new_file_storage_dir)
2393

    
2394

    
2395
def _EnsureJobQueueFile(file_name):
2396
  """Checks whether the given filename is in the queue directory.
2397

2398
  @type file_name: str
2399
  @param file_name: the file name we should check
2400
  @rtype: None
2401
  @raises RPCFail: if the file is not valid
2402

2403
  """
2404
  queue_dir = os.path.normpath(constants.QUEUE_DIR)
2405
  result = (os.path.commonprefix([queue_dir, file_name]) == queue_dir)
2406

    
2407
  if not result:
2408
    _Fail("Passed job queue file '%s' does not belong to"
2409
          " the queue directory '%s'", file_name, queue_dir)
2410

    
2411

    
2412
def JobQueueUpdate(file_name, content):
2413
  """Updates a file in the queue directory.
2414

2415
  This is just a wrapper over L{utils.WriteFile}, with proper
2416
  checking.
2417

2418
  @type file_name: str
2419
  @param file_name: the job file name
2420
  @type content: str
2421
  @param content: the new job contents
2422
  @rtype: boolean
2423
  @return: the success of the operation
2424

2425
  """
2426
  _EnsureJobQueueFile(file_name)
2427

    
2428
  # Write and replace the file atomically
2429
  utils.WriteFile(file_name, data=_Decompress(content))
2430

    
2431

    
2432
def JobQueueRename(old, new):
2433
  """Renames a job queue file.
2434

2435
  This is just a wrapper over os.rename with proper checking.
2436

2437
  @type old: str
2438
  @param old: the old (actual) file name
2439
  @type new: str
2440
  @param new: the desired file name
2441
  @rtype: tuple
2442
  @return: the success of the operation and payload
2443

2444
  """
2445
  _EnsureJobQueueFile(old)
2446
  _EnsureJobQueueFile(new)
2447

    
2448
  utils.RenameFile(old, new, mkdir=True)
2449

    
2450

    
2451
def BlockdevClose(instance_name, disks):
2452
  """Closes the given block devices.
2453

2454
  This means they will be switched to secondary mode (in case of
2455
  DRBD).
2456

2457
  @param instance_name: if the argument is not empty, the symlinks
2458
      of this instance will be removed
2459
  @type disks: list of L{objects.Disk}
2460
  @param disks: the list of disks to be closed
2461
  @rtype: tuple (success, message)
2462
  @return: a tuple of success and message, where success
2463
      indicates the succes of the operation, and message
2464
      which will contain the error details in case we
2465
      failed
2466

2467
  """
2468
  bdevs = []
2469
  for cf in disks:
2470
    rd = _RecursiveFindBD(cf)
2471
    if rd is None:
2472
      _Fail("Can't find device %s", cf)
2473
    bdevs.append(rd)
2474

    
2475
  msg = []
2476
  for rd in bdevs:
2477
    try:
2478
      rd.Close()
2479
    except errors.BlockDeviceError, err:
2480
      msg.append(str(err))
2481
  if msg:
2482
    _Fail("Can't make devices secondary: %s", ",".join(msg))
2483
  else:
2484
    if instance_name:
2485
      _RemoveBlockDevLinks(instance_name, disks)
2486

    
2487

    
2488
def ValidateHVParams(hvname, hvparams):
2489
  """Validates the given hypervisor parameters.
2490

2491
  @type hvname: string
2492
  @param hvname: the hypervisor name
2493
  @type hvparams: dict
2494
  @param hvparams: the hypervisor parameters to be validated
2495
  @rtype: None
2496

2497
  """
2498
  try:
2499
    hv_type = hypervisor.GetHypervisor(hvname)
2500
    hv_type.ValidateParameters(hvparams)
2501
  except errors.HypervisorError, err:
2502
    _Fail(str(err), log=False)
2503

    
2504

    
2505
def _CheckOSPList(os_obj, parameters):
2506
  """Check whether a list of parameters is supported by the OS.
2507

2508
  @type os_obj: L{objects.OS}
2509
  @param os_obj: OS object to check
2510
  @type parameters: list
2511
  @param parameters: the list of parameters to check
2512

2513
  """
2514
  supported = [v[0] for v in os_obj.supported_parameters]
2515
  delta = frozenset(parameters).difference(supported)
2516
  if delta:
2517
    _Fail("The following parameters are not supported"
2518
          " by the OS %s: %s" % (os_obj.name, utils.CommaJoin(delta)))
2519

    
2520

    
2521
def ValidateOS(required, osname, checks, osparams):
2522
  """Validate the given OS' parameters.
2523

2524
  @type required: boolean
2525
  @param required: whether absence of the OS should translate into
2526
      failure or not
2527
  @type osname: string
2528
  @param osname: the OS to be validated
2529
  @type checks: list
2530
  @param checks: list of the checks to run (currently only 'parameters')
2531
  @type osparams: dict
2532
  @param osparams: dictionary with OS parameters
2533
  @rtype: boolean
2534
  @return: True if the validation passed, or False if the OS was not
2535
      found and L{required} was false
2536

2537
  """
2538
  if not constants.OS_VALIDATE_CALLS.issuperset(checks):
2539
    _Fail("Unknown checks required for OS %s: %s", osname,
2540
          set(checks).difference(constants.OS_VALIDATE_CALLS))
2541

    
2542
  name_only = osname.split("+", 1)[0]
2543
  status, tbv = _TryOSFromDisk(name_only, None)
2544

    
2545
  if not status:
2546
    if required:
2547
      _Fail(tbv)
2548
    else:
2549
      return False
2550

    
2551
  if max(tbv.api_versions) < constants.OS_API_V20:
2552
    return True
2553

    
2554
  if constants.OS_VALIDATE_PARAMETERS in checks:
2555
    _CheckOSPList(tbv, osparams.keys())
2556

    
2557
  validate_env = OSCoreEnv(tbv, osparams)
2558
  result = utils.RunCmd([tbv.verify_script] + checks, env=validate_env,
2559
                        cwd=tbv.path)
2560
  if result.failed:
2561
    logging.error("os validate command '%s' returned error: %s output: %s",
2562
                  result.cmd, result.fail_reason, result.output)
2563
    _Fail("OS validation script failed (%s), output: %s",
2564
          result.fail_reason, result.output, log=False)
2565

    
2566
  return True
2567

    
2568

    
2569
def DemoteFromMC():
2570
  """Demotes the current node from master candidate role.
2571

2572
  """
2573
  # try to ensure we're not the master by mistake
2574
  master, myself = ssconf.GetMasterAndMyself()
2575
  if master == myself:
2576
    _Fail("ssconf status shows I'm the master node, will not demote")
2577

    
2578
  result = utils.RunCmd([constants.DAEMON_UTIL, "check", constants.MASTERD])
2579
  if not result.failed:
2580
    _Fail("The master daemon is running, will not demote")
2581

    
2582
  try:
2583
    if os.path.isfile(constants.CLUSTER_CONF_FILE):
2584
      utils.CreateBackup(constants.CLUSTER_CONF_FILE)
2585
  except EnvironmentError, err:
2586
    if err.errno != errno.ENOENT:
2587
      _Fail("Error while backing up cluster file: %s", err, exc=True)
2588

    
2589
  utils.RemoveFile(constants.CLUSTER_CONF_FILE)
2590

    
2591

    
2592
def _GetX509Filenames(cryptodir, name):
2593
  """Returns the full paths for the private key and certificate.
2594

2595
  """
2596
  return (utils.PathJoin(cryptodir, name),
2597
          utils.PathJoin(cryptodir, name, _X509_KEY_FILE),
2598
          utils.PathJoin(cryptodir, name, _X509_CERT_FILE))
2599

    
2600

    
2601
def CreateX509Certificate(validity, cryptodir=constants.CRYPTO_KEYS_DIR):
2602
  """Creates a new X509 certificate for SSL/TLS.
2603

2604
  @type validity: int
2605
  @param validity: Validity in seconds
2606
  @rtype: tuple; (string, string)
2607
  @return: Certificate name and public part
2608

2609
  """
2610
  (key_pem, cert_pem) = \
2611
    utils.GenerateSelfSignedX509Cert(netutils.Hostname.GetSysName(),
2612
                                     min(validity, _MAX_SSL_CERT_VALIDITY))
2613

    
2614
  cert_dir = tempfile.mkdtemp(dir=cryptodir,
2615
                              prefix="x509-%s-" % utils.TimestampForFilename())
2616
  try:
2617
    name = os.path.basename(cert_dir)
2618
    assert len(name) > 5
2619

    
2620
    (_, key_file, cert_file) = _GetX509Filenames(cryptodir, name)
2621

    
2622
    utils.WriteFile(key_file, mode=0400, data=key_pem)
2623
    utils.WriteFile(cert_file, mode=0400, data=cert_pem)
2624

    
2625
    # Never return private key as it shouldn't leave the node
2626
    return (name, cert_pem)
2627
  except Exception:
2628
    shutil.rmtree(cert_dir, ignore_errors=True)
2629
    raise
2630

    
2631

    
2632
def RemoveX509Certificate(name, cryptodir=constants.CRYPTO_KEYS_DIR):
2633
  """Removes a X509 certificate.
2634

2635
  @type name: string
2636
  @param name: Certificate name
2637

2638
  """
2639
  (cert_dir, key_file, cert_file) = _GetX509Filenames(cryptodir, name)
2640

    
2641
  utils.RemoveFile(key_file)
2642
  utils.RemoveFile(cert_file)
2643

    
2644
  try:
2645
    os.rmdir(cert_dir)
2646
  except EnvironmentError, err:
2647
    _Fail("Cannot remove certificate directory '%s': %s",
2648
          cert_dir, err)
2649

    
2650

    
2651
def _GetImportExportIoCommand(instance, mode, ieio, ieargs):
2652
  """Returns the command for the requested input/output.
2653

2654
  @type instance: L{objects.Instance}
2655
  @param instance: The instance object
2656
  @param mode: Import/export mode
2657
  @param ieio: Input/output type
2658
  @param ieargs: Input/output arguments
2659

2660
  """
2661
  assert mode in (constants.IEM_IMPORT, constants.IEM_EXPORT)
2662

    
2663
  env = None
2664
  prefix = None
2665
  suffix = None
2666
  exp_size = None
2667

    
2668
  if ieio == constants.IEIO_FILE:
2669
    (filename, ) = ieargs
2670

    
2671
    if not utils.IsNormAbsPath(filename):
2672
      _Fail("Path '%s' is not normalized or absolute", filename)
2673

    
2674
    directory = os.path.normpath(os.path.dirname(filename))
2675

    
2676
    if (os.path.commonprefix([constants.EXPORT_DIR, directory]) !=
2677
        constants.EXPORT_DIR):
2678
      _Fail("File '%s' is not under exports directory '%s'",
2679
            filename, constants.EXPORT_DIR)
2680

    
2681
    # Create directory
2682
    utils.Makedirs(directory, mode=0750)
2683

    
2684
    quoted_filename = utils.ShellQuote(filename)
2685

    
2686
    if mode == constants.IEM_IMPORT:
2687
      suffix = "> %s" % quoted_filename
2688
    elif mode == constants.IEM_EXPORT:
2689
      suffix = "< %s" % quoted_filename
2690

    
2691
      # Retrieve file size
2692
      try:
2693
        st = os.stat(filename)
2694
      except EnvironmentError, err:
2695
        logging.error("Can't stat(2) %s: %s", filename, err)
2696
      else:
2697
        exp_size = utils.BytesToMebibyte(st.st_size)
2698

    
2699
  elif ieio == constants.IEIO_RAW_DISK:
2700
    (disk, ) = ieargs
2701

    
2702
    real_disk = _OpenRealBD(disk)
2703

    
2704
    if mode == constants.IEM_IMPORT:
2705
      # we set here a smaller block size as, due to transport buffering, more
2706
      # than 64-128k will mostly ignored; we use nocreat to fail if the device
2707
      # is not already there or we pass a wrong path; we use notrunc to no
2708
      # attempt truncate on an LV device; we use oflag=dsync to not buffer too
2709
      # much memory; this means that at best, we flush every 64k, which will
2710
      # not be very fast
2711
      suffix = utils.BuildShellCmd(("| dd of=%s conv=nocreat,notrunc"
2712
                                    " bs=%s oflag=dsync"),
2713
                                    real_disk.dev_path,
2714
                                    str(64 * 1024))
2715

    
2716
    elif mode == constants.IEM_EXPORT:
2717
      # the block size on the read dd is 1MiB to match our units
2718
      prefix = utils.BuildShellCmd("dd if=%s bs=%s count=%s |",
2719
                                   real_disk.dev_path,
2720
                                   str(1024 * 1024), # 1 MB
2721
                                   str(disk.size))
2722
      exp_size = disk.size
2723

    
2724
  elif ieio == constants.IEIO_SCRIPT:
2725
    (disk, disk_index, ) = ieargs
2726

    
2727
    assert isinstance(disk_index, (int, long))
2728

    
2729
    real_disk = _OpenRealBD(disk)
2730

    
2731
    inst_os = OSFromDisk(instance.os)
2732
    env = OSEnvironment(instance, inst_os)
2733

    
2734
    if mode == constants.IEM_IMPORT:
2735
      env["IMPORT_DEVICE"] = env["DISK_%d_PATH" % disk_index]
2736
      env["IMPORT_INDEX"] = str(disk_index)
2737
      script = inst_os.import_script
2738

    
2739
    elif mode == constants.IEM_EXPORT:
2740
      env["EXPORT_DEVICE"] = real_disk.dev_path
2741
      env["EXPORT_INDEX"] = str(disk_index)
2742
      script = inst_os.export_script
2743

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

    
2747
    if mode == constants.IEM_IMPORT:
2748
      suffix = "| %s" % script_cmd
2749

    
2750
    elif mode == constants.IEM_EXPORT:
2751
      prefix = "%s |" % script_cmd
2752

    
2753
    # Let script predict size
2754
    exp_size = constants.IE_CUSTOM_SIZE
2755

    
2756
  else:
2757
    _Fail("Invalid %s I/O mode %r", mode, ieio)
2758

    
2759
  return (env, prefix, suffix, exp_size)
2760

    
2761

    
2762
def _CreateImportExportStatusDir(prefix):
2763
  """Creates status directory for import/export.
2764

2765
  """
2766
  return tempfile.mkdtemp(dir=constants.IMPORT_EXPORT_DIR,
2767
                          prefix=("%s-%s-" %
2768
                                  (prefix, utils.TimestampForFilename())))
2769

    
2770

    
2771
def StartImportExportDaemon(mode, opts, host, port, instance, ieio, ieioargs):
2772
  """Starts an import or export daemon.
2773

2774
  @param mode: Import/output mode
2775
  @type opts: L{objects.ImportExportOptions}
2776
  @param opts: Daemon options
2777
  @type host: string
2778
  @param host: Remote host for export (None for import)
2779
  @type port: int
2780
  @param port: Remote port for export (None for import)
2781
  @type instance: L{objects.Instance}
2782
  @param instance: Instance object
2783
  @param ieio: Input/output type
2784
  @param ieioargs: Input/output arguments
2785

2786
  """
2787
  if mode == constants.IEM_IMPORT:
2788
    prefix = "import"
2789

    
2790
    if not (host is None and port is None):
2791
      _Fail("Can not specify host or port on import")
2792

    
2793
  elif mode == constants.IEM_EXPORT:
2794
    prefix = "export"
2795

    
2796
    if host is None or port is None:
2797
      _Fail("Host and port must be specified for an export")
2798

    
2799
  else:
2800
    _Fail("Invalid mode %r", mode)
2801

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

    
2805
  (cmd_env, cmd_prefix, cmd_suffix, exp_size) = \
2806
    _GetImportExportIoCommand(instance, mode, ieio, ieioargs)
2807

    
2808
  if opts.key_name is None:
2809
    # Use server.pem
2810
    key_path = constants.NODED_CERT_FILE
2811
    cert_path = constants.NODED_CERT_FILE
2812
    assert opts.ca_pem is None
2813
  else:
2814
    (_, key_path, cert_path) = _GetX509Filenames(constants.CRYPTO_KEYS_DIR,
2815
                                                 opts.key_name)
2816
    assert opts.ca_pem is not None
2817

    
2818
  for i in [key_path, cert_path]:
2819
    if not os.path.exists(i):
2820
      _Fail("File '%s' does not exist" % i)
2821

    
2822
  status_dir = _CreateImportExportStatusDir(prefix)
2823
  try:
2824
    status_file = utils.PathJoin(status_dir, _IES_STATUS_FILE)
2825
    pid_file = utils.PathJoin(status_dir, _IES_PID_FILE)
2826
    ca_file = utils.PathJoin(status_dir, _IES_CA_FILE)
2827

    
2828
    if opts.ca_pem is None:
2829
      # Use server.pem
2830
      ca = utils.ReadFile(constants.NODED_CERT_FILE)
2831
    else:
2832
      ca = opts.ca_pem
2833

    
2834
    # Write CA file
2835
    utils.WriteFile(ca_file, data=ca, mode=0400)
2836

    
2837
    cmd = [
2838
      constants.IMPORT_EXPORT_DAEMON,
2839
      status_file, mode,
2840
      "--key=%s" % key_path,
2841
      "--cert=%s" % cert_path,
2842
      "--ca=%s" % ca_file,
2843
      ]
2844

    
2845
    if host:
2846
      cmd.append("--host=%s" % host)
2847

    
2848
    if port:
2849
      cmd.append("--port=%s" % port)
2850

    
2851
    if opts.compress:
2852
      cmd.append("--compress=%s" % opts.compress)
2853

    
2854
    if opts.magic:
2855
      cmd.append("--magic=%s" % opts.magic)
2856

    
2857
    if exp_size is not None:
2858
      cmd.append("--expected-size=%s" % exp_size)
2859

    
2860
    if cmd_prefix:
2861
      cmd.append("--cmd-prefix=%s" % cmd_prefix)
2862

    
2863
    if cmd_suffix:
2864
      cmd.append("--cmd-suffix=%s" % cmd_suffix)
2865

    
2866
    logfile = _InstanceLogName(prefix, instance.os, instance.name)
2867

    
2868
    # TODO: Once _InstanceLogName uses tempfile.mkstemp, StartDaemon has
2869
    # support for receiving a file descriptor for output
2870
    utils.StartDaemon(cmd, env=cmd_env, pidfile=pid_file,
2871
                      output=logfile)
2872

    
2873
    # The import/export name is simply the status directory name
2874
    return os.path.basename(status_dir)
2875

    
2876
  except Exception:
2877
    shutil.rmtree(status_dir, ignore_errors=True)
2878
    raise
2879

    
2880

    
2881
def GetImportExportStatus(names):
2882
  """Returns import/export daemon status.
2883

2884
  @type names: sequence
2885
  @param names: List of names
2886
  @rtype: List of dicts
2887
  @return: Returns a list of the state of each named import/export or None if a
2888
           status couldn't be read
2889

2890
  """
2891
  result = []
2892

    
2893
  for name in names:
2894
    status_file = utils.PathJoin(constants.IMPORT_EXPORT_DIR, name,
2895
                                 _IES_STATUS_FILE)
2896

    
2897
    try:
2898
      data = utils.ReadFile(status_file)
2899
    except EnvironmentError, err:
2900
      if err.errno != errno.ENOENT:
2901
        raise
2902
      data = None
2903

    
2904
    if not data:
2905
      result.append(None)
2906
      continue
2907

    
2908
    result.append(serializer.LoadJson(data))
2909

    
2910
  return result
2911

    
2912

    
2913
def AbortImportExport(name):
2914
  """Sends SIGTERM to a running import/export daemon.
2915

2916
  """
2917
  logging.info("Abort import/export %s", name)
2918

    
2919
  status_dir = utils.PathJoin(constants.IMPORT_EXPORT_DIR, name)
2920
  pid = utils.ReadLockedPidFile(utils.PathJoin(status_dir, _IES_PID_FILE))
2921

    
2922
  if pid:
2923
    logging.info("Import/export %s is running with PID %s, sending SIGTERM",
2924
                 name, pid)
2925
    utils.IgnoreProcessNotFound(os.kill, pid, signal.SIGTERM)
2926

    
2927

    
2928
def CleanupImportExport(name):
2929
  """Cleanup after an import or export.
2930

2931
  If the import/export daemon is still running it's killed. Afterwards the
2932
  whole status directory is removed.
2933

2934
  """
2935
  logging.info("Finalizing import/export %s", name)
2936

    
2937
  status_dir = utils.PathJoin(constants.IMPORT_EXPORT_DIR, name)
2938

    
2939
  pid = utils.ReadLockedPidFile(utils.PathJoin(status_dir, _IES_PID_FILE))
2940

    
2941
  if pid:
2942
    logging.info("Import/export %s is still running with PID %s",
2943
                 name, pid)
2944
    utils.KillProcess(pid, waitpid=False)
2945

    
2946
  shutil.rmtree(status_dir, ignore_errors=True)
2947

    
2948

    
2949
def _FindDisks(nodes_ip, disks):
2950
  """Sets the physical ID on disks and returns the block devices.
2951

2952
  """
2953
  # set the correct physical ID
2954
  my_name = netutils.Hostname.GetSysName()
2955
  for cf in disks:
2956
    cf.SetPhysicalID(my_name, nodes_ip)
2957

    
2958
  bdevs = []
2959

    
2960
  for cf in disks:
2961
    rd = _RecursiveFindBD(cf)
2962
    if rd is None:
2963
      _Fail("Can't find device %s", cf)
2964
    bdevs.append(rd)
2965
  return bdevs
2966

    
2967

    
2968
def DrbdDisconnectNet(nodes_ip, disks):
2969
  """Disconnects the network on a list of drbd devices.
2970

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

    
2974
  # disconnect disks
2975
  for rd in bdevs:
2976
    try:
2977
      rd.DisconnectNet()
2978
    except errors.BlockDeviceError, err:
2979
      _Fail("Can't change network configuration to standalone mode: %s",
2980
            err, exc=True)
2981

    
2982

    
2983
def DrbdAttachNet(nodes_ip, disks, instance_name, multimaster):
2984
  """Attaches the network on a list of drbd devices.
2985

2986
  """
2987
  bdevs = _FindDisks(nodes_ip, disks)
2988

    
2989
  if multimaster:
2990
    for idx, rd in enumerate(bdevs):
2991
      try:
2992
        _SymlinkBlockDev(instance_name, rd.dev_path, idx)
2993
      except EnvironmentError, err:
2994
        _Fail("Can't create symlink: %s", err)
2995
  # reconnect disks, switch to new master configuration and if
2996
  # needed primary mode
2997
  for rd in bdevs:
2998
    try:
2999
      rd.AttachNet(multimaster)
3000
    except errors.BlockDeviceError, err:
3001
      _Fail("Can't change network configuration: %s", err)
3002

    
3003
  # wait until the disks are connected; we need to retry the re-attach
3004
  # if the device becomes standalone, as this might happen if the one
3005
  # node disconnects and reconnects in a different mode before the
3006
  # other node reconnects; in this case, one or both of the nodes will
3007
  # decide it has wrong configuration and switch to standalone
3008

    
3009
  def _Attach():
3010
    all_connected = True
3011

    
3012
    for rd in bdevs:
3013
      stats = rd.GetProcStatus()
3014

    
3015
      all_connected = (all_connected and
3016
                       (stats.is_connected or stats.is_in_resync))
3017

    
3018
      if stats.is_standalone:
3019
        # peer had different config info and this node became
3020
        # standalone, even though this should not happen with the
3021
        # new staged way of changing disk configs
3022
        try:
3023
          rd.AttachNet(multimaster)
3024
        except errors.BlockDeviceError, err:
3025
          _Fail("Can't change network configuration: %s", err)
3026

    
3027
    if not all_connected:
3028
      raise utils.RetryAgain()
3029

    
3030
  try:
3031
    # Start with a delay of 100 miliseconds and go up to 5 seconds
3032
    utils.Retry(_Attach, (0.1, 1.5, 5.0), 2 * 60)
3033
  except utils.RetryTimeout:
3034
    _Fail("Timeout in disk reconnecting")
3035

    
3036
  if multimaster:
3037
    # change to primary mode
3038
    for rd in bdevs:
3039
      try:
3040
        rd.Open()
3041
      except errors.BlockDeviceError, err:
3042
        _Fail("Can't change to primary mode: %s", err)
3043

    
3044

    
3045
def DrbdWaitSync(nodes_ip, disks):
3046
  """Wait until DRBDs have synchronized.
3047

3048
  """
3049
  def _helper(rd):
3050
    stats = rd.GetProcStatus()
3051
    if not (stats.is_connected or stats.is_in_resync):
3052
      raise utils.RetryAgain()
3053
    return stats
3054

    
3055
  bdevs = _FindDisks(nodes_ip, disks)
3056

    
3057
  min_resync = 100
3058
  alldone = True
3059
  for rd in bdevs:
3060
    try:
3061
      # poll each second for 15 seconds
3062
      stats = utils.Retry(_helper, 1, 15, args=[rd])
3063
    except utils.RetryTimeout:
3064
      stats = rd.GetProcStatus()
3065
      # last check
3066
      if not (stats.is_connected or stats.is_in_resync):
3067
        _Fail("DRBD device %s is not in sync: stats=%s", rd, stats)
3068
    alldone = alldone and (not stats.is_in_resync)
3069
    if stats.sync_percent is not None:
3070
      min_resync = min(min_resync, stats.sync_percent)
3071

    
3072
  return (alldone, min_resync)
3073

    
3074

    
3075
def GetDrbdUsermodeHelper():
3076
  """Returns DRBD usermode helper currently configured.
3077

3078
  """
3079
  try:
3080
    return bdev.BaseDRBD.GetUsermodeHelper()
3081
  except errors.BlockDeviceError, err:
3082
    _Fail(str(err))
3083

    
3084

    
3085
def PowercycleNode(hypervisor_type):
3086
  """Hard-powercycle the node.
3087

3088
  Because we need to return first, and schedule the powercycle in the
3089
  background, we won't be able to report failures nicely.
3090

3091
  """
3092
  hyper = hypervisor.GetHypervisor(hypervisor_type)
3093
  try:
3094
    pid = os.fork()
3095
  except OSError:
3096
    # if we can't fork, we'll pretend that we're in the child process
3097
    pid = 0
3098
  if pid > 0:
3099
    return "Reboot scheduled in 5 seconds"
3100
  # ensure the child is running on ram
3101
  try:
3102
    utils.Mlockall()
3103
  except Exception: # pylint: disable-msg=W0703
3104
    pass
3105
  time.sleep(5)
3106
  hyper.PowercycleNode()
3107

    
3108

    
3109
class HooksRunner(object):
3110
  """Hook runner.
3111

3112
  This class is instantiated on the node side (ganeti-noded) and not
3113
  on the master side.
3114

3115
  """
3116
  def __init__(self, hooks_base_dir=None):
3117
    """Constructor for hooks runner.
3118

3119
    @type hooks_base_dir: str or None
3120
    @param hooks_base_dir: if not None, this overrides the
3121
        L{constants.HOOKS_BASE_DIR} (useful for unittests)
3122

3123
    """
3124
    if hooks_base_dir is None:
3125
      hooks_base_dir = constants.HOOKS_BASE_DIR
3126
    # yeah, _BASE_DIR is not valid for attributes, we use it like a
3127
    # constant
3128
    self._BASE_DIR = hooks_base_dir # pylint: disable-msg=C0103
3129

    
3130
  def RunHooks(self, hpath, phase, env):
3131
    """Run the scripts in the hooks directory.
3132

3133
    @type hpath: str
3134
    @param hpath: the path to the hooks directory which
3135
        holds the scripts
3136
    @type phase: str
3137
    @param phase: either L{constants.HOOKS_PHASE_PRE} or
3138
        L{constants.HOOKS_PHASE_POST}
3139
    @type env: dict
3140
    @param env: dictionary with the environment for the hook
3141
    @rtype: list
3142
    @return: list of 3-element tuples:
3143
      - script path
3144
      - script result, either L{constants.HKR_SUCCESS} or
3145
        L{constants.HKR_FAIL}
3146
      - output of the script
3147

3148
    @raise errors.ProgrammerError: for invalid input
3149
        parameters
3150

3151
    """
3152
    if phase == constants.HOOKS_PHASE_PRE:
3153
      suffix = "pre"
3154
    elif phase == constants.HOOKS_PHASE_POST:
3155
      suffix = "post"
3156
    else:
3157
      _Fail("Unknown hooks phase '%s'", phase)
3158

    
3159

    
3160
    subdir = "%s-%s.d" % (hpath, suffix)
3161
    dir_name = utils.PathJoin(self._BASE_DIR, subdir)
3162

    
3163
    results = []
3164

    
3165
    if not os.path.isdir(dir_name):
3166
      # for non-existing/non-dirs, we simply exit instead of logging a
3167
      # warning at every operation
3168
      return results
3169

    
3170
    runparts_results = utils.RunParts(dir_name, env=env, reset_env=True)
3171

    
3172
    for (relname, relstatus, runresult)  in runparts_results:
3173
      if relstatus == constants.RUNPARTS_SKIP:
3174
        rrval = constants.HKR_SKIP
3175
        output = ""
3176
      elif relstatus == constants.RUNPARTS_ERR:
3177
        rrval = constants.HKR_FAIL
3178
        output = "Hook script execution error: %s" % runresult
3179
      elif relstatus == constants.RUNPARTS_RUN:
3180
        if runresult.failed:
3181
          rrval = constants.HKR_FAIL
3182
        else:
3183
          rrval = constants.HKR_SUCCESS
3184
        output = utils.SafeEncode(runresult.output.strip())
3185
      results.append(("%s/%s" % (subdir, relname), rrval, output))
3186

    
3187
    return results
3188

    
3189

    
3190
class IAllocatorRunner(object):
3191
  """IAllocator runner.
3192

3193
  This class is instantiated on the node side (ganeti-noded) and not on
3194
  the master side.
3195

3196
  """
3197
  @staticmethod
3198
  def Run(name, idata):
3199
    """Run an iallocator script.
3200

3201
    @type name: str
3202
    @param name: the iallocator script name
3203
    @type idata: str
3204
    @param idata: the allocator input data
3205

3206
    @rtype: tuple
3207
    @return: two element tuple of:
3208
       - status
3209
       - either error message or stdout of allocator (for success)
3210

3211
    """
3212
    alloc_script = utils.FindFile(name, constants.IALLOCATOR_SEARCH_PATH,
3213
                                  os.path.isfile)
3214
    if alloc_script is None:
3215
      _Fail("iallocator module '%s' not found in the search path", name)
3216

    
3217
    fd, fin_name = tempfile.mkstemp(prefix="ganeti-iallocator.")
3218
    try:
3219
      os.write(fd, idata)
3220
      os.close(fd)
3221
      result = utils.RunCmd([alloc_script, fin_name])
3222
      if result.failed:
3223
        _Fail("iallocator module '%s' failed: %s, output '%s'",
3224
              name, result.fail_reason, result.output)
3225
    finally:
3226
      os.unlink(fin_name)
3227

    
3228
    return result.stdout
3229

    
3230

    
3231
class DevCacheManager(object):
3232
  """Simple class for managing a cache of block device information.
3233

3234
  """
3235
  _DEV_PREFIX = "/dev/"
3236
  _ROOT_DIR = constants.BDEV_CACHE_DIR
3237

    
3238
  @classmethod
3239
  def _ConvertPath(cls, dev_path):
3240
    """Converts a /dev/name path to the cache file name.
3241

3242
    This replaces slashes with underscores and strips the /dev
3243
    prefix. It then returns the full path to the cache file.
3244

3245
    @type dev_path: str
3246
    @param dev_path: the C{/dev/} path name
3247
    @rtype: str
3248
    @return: the converted path name
3249

3250
    """
3251
    if dev_path.startswith(cls._DEV_PREFIX):
3252
      dev_path = dev_path[len(cls._DEV_PREFIX):]
3253
    dev_path = dev_path.replace("/", "_")
3254
    fpath = utils.PathJoin(cls._ROOT_DIR, "bdev_%s" % dev_path)
3255
    return fpath
3256

    
3257
  @classmethod
3258
  def UpdateCache(cls, dev_path, owner, on_primary, iv_name):
3259
    """Updates the cache information for a given device.
3260

3261
    @type dev_path: str
3262
    @param dev_path: the pathname of the device
3263
    @type owner: str
3264
    @param owner: the owner (instance name) of the device
3265
    @type on_primary: bool
3266
    @param on_primary: whether this is the primary
3267
        node nor not
3268
    @type iv_name: str
3269
    @param iv_name: the instance-visible name of the
3270
        device, as in objects.Disk.iv_name
3271

3272
    @rtype: None
3273

3274
    """
3275
    if dev_path is None:
3276
      logging.error("DevCacheManager.UpdateCache got a None dev_path")
3277
      return
3278
    fpath = cls._ConvertPath(dev_path)
3279
    if on_primary:
3280
      state = "primary"
3281
    else:
3282
      state = "secondary"
3283
    if iv_name is None:
3284
      iv_name = "not_visible"
3285
    fdata = "%s %s %s\n" % (str(owner), state, iv_name)
3286
    try:
3287
      utils.WriteFile(fpath, data=fdata)
3288
    except EnvironmentError, err:
3289
      logging.exception("Can't update bdev cache for %s: %s", dev_path, err)
3290

    
3291
  @classmethod
3292
  def RemoveCache(cls, dev_path):
3293
    """Remove data for a dev_path.
3294

3295
    This is just a wrapper over L{utils.RemoveFile} with a converted
3296
    path name and logging.
3297

3298
    @type dev_path: str
3299
    @param dev_path: the pathname of the device
3300

3301
    @rtype: None
3302

3303
    """
3304
    if dev_path is None:
3305
      logging.error("DevCacheManager.RemoveCache got a None dev_path")
3306
      return
3307
    fpath = cls._ConvertPath(dev_path)
3308
    try:
3309
      utils.RemoveFile(fpath)
3310
    except EnvironmentError, err:
3311
      logging.exception("Can't update bdev cache for %s: %s", dev_path, err)