Pass debug mode to noded for OS-related calls
[ganeti-local] / tools / cfgupgrade
index 9825cf6..e7be591 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 #
 
-# Copyright (C) 2007, 2008 Google Inc.
+# Copyright (C) 2007, 2008, 2009 Google Inc.
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -21,8 +21,8 @@
 
 """Tool to upgrade the configuration file.
 
-This code handles only the types supported by simplejson. As an example, "set"
-is a "list".
+This code handles only the types supported by simplejson. As an
+example, 'set' is a 'list'.
 
 """
 
@@ -31,29 +31,41 @@ import os
 import os.path
 import sys
 import optparse
-import tempfile
 import logging
-import errno
 
 from ganeti import constants
 from ganeti import serializer
 from ganeti import utils
 from ganeti import cli
+from ganeti import bootstrap
+from ganeti import config
 
 
-# We need to keep filenames locally because they might be renamed between
-# versions.
-CONFIG_DATA_PATH = constants.DATA_DIR + "/config.data"
-SERVER_PEM_PATH = constants.DATA_DIR + "/server.pem"
-KNOWN_HOSTS_PATH = constants.DATA_DIR + "/known_hosts"
-SSCONF_CLUSTER_NAME_PATH = constants.DATA_DIR + "/ssconf_cluster_name"
-SSCONF_CONFIG_VERSION_PATH = constants.DATA_DIR + "/ssconf_config_version"
-
 options = None
 args = None
 
-# Unique object to identify calls without default value
-NoDefault = object()
+# Dictionary with instance old keys, and new hypervisor keys
+INST_HV_CHG = {
+  'hvm_pae': constants.HV_PAE,
+  'vnc_bind_address': constants.HV_VNC_BIND_ADDRESS,
+  'initrd_path': constants.HV_INITRD_PATH,
+  'hvm_nic_type': constants.HV_NIC_TYPE,
+  'kernel_path': constants.HV_KERNEL_PATH,
+  'hvm_acpi': constants.HV_ACPI,
+  'hvm_cdrom_image_path': constants.HV_CDROM_IMAGE_PATH,
+  'hvm_boot_order': constants.HV_BOOT_ORDER,
+  'hvm_disk_type': constants.HV_DISK_TYPE,
+  }
+
+# Instance beparams changes
+INST_BE_CHG = {
+  'vcpus': constants.BE_VCPUS,
+  'memory': constants.BE_MEMORY,
+  'auto_balance': constants.BE_AUTO_BALANCE,
+  }
+
+# Field names
+F_SERIAL = 'serial_no'
 
 
 class Error(Exception):
@@ -61,33 +73,6 @@ class Error(Exception):
   pass
 
 
-def ReadFile(file_name, default=NoDefault):
-  """Reads a file.
-
-  """
-  logging.debug("Reading %s", file_name)
-  try:
-    fh = open(file_name, 'r')
-  except IOError, err:
-    if default is not NoDefault and err.errno == errno.ENOENT:
-      return default
-    raise
-
-  try:
-    return fh.read()
-  finally:
-    fh.close()
-
-
-def WriteFile(file_name, data):
-  """Writes a configuration file.
-
-  """
-  logging.debug("Writing %s", file_name)
-  utils.WriteFile(file_name=file_name, data=data, mode=0600,
-                  dry_run=options.dry_run, backup=True)
-
-
 def SetupLogging():
   """Configures the logging module.
 
@@ -112,7 +97,7 @@ def main():
   """Main program.
 
   """
-  global options, args
+  global options, args # pylint: disable-msg=W0603
 
   program = os.path.basename(sys.argv[0])
 
@@ -124,11 +109,20 @@ def main():
                          " output file")
   parser.add_option(cli.FORCE_OPT)
   parser.add_option(cli.DEBUG_OPT)
-  parser.add_option('--verbose', dest='verbose',
-                    action="store_true",
-                    help="Verbose output")
+  parser.add_option(cli.VERBOSE_OPT)
+  parser.add_option('--path', help="Convert configuration in this"
+                    " directory instead of '%s'" % constants.DATA_DIR,
+                    default=constants.DATA_DIR, dest="data_dir")
   (options, args) = parser.parse_args()
 
+  # We need to keep filenames locally because they might be renamed between
+  # versions.
+  options.CONFIG_DATA_PATH = options.data_dir + "/config.data"
+  options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
+  options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
+  options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
+  options.HMAC_CLUSTER_KEY = options.data_dir + "/hmac.key"
+
   SetupLogging()
 
   # Option checking
@@ -136,79 +130,77 @@ def main():
     raise Error("No arguments expected")
 
   if not options.force:
-    usertext = ("%s MUST run on the master node. Is this the master"
-                " node?" % program)
+    usertext = ("%s MUST be run on the master node. Is this the master"
+                " node and are ALL instances down?" % program)
     if not cli.AskUser(usertext):
       sys.exit(1)
 
   # Check whether it's a Ganeti configuration directory
-  if not (os.path.isfile(CONFIG_DATA_PATH) and
-          os.path.isfile(SERVER_PEM_PATH) or
-          os.path.isfile(KNOWN_HOSTS_PATH)):
+  if not (os.path.isfile(options.CONFIG_DATA_PATH) and
+          os.path.isfile(options.SERVER_PEM_PATH) or
+          os.path.isfile(options.KNOWN_HOSTS_PATH)):
     raise Error(("%s does not seem to be a known Ganeti configuration"
-                 " directory") % constants.DATA_DIR)
-
-  config_version = ReadFile(SSCONF_CONFIG_VERSION_PATH, "1.2").strip()
-  logging.info("Found configuration version %s", config_version)
-
-  config_data = serializer.LoadJson(ReadFile(CONFIG_DATA_PATH))
-
-  # Ganeti 1.2?
-  if config_version == "1.2":
-    logging.info("Found a Ganeti 1.2 configuration")
-
-    old_config_version = config_data["cluster"].get("config_version", None)
-    logging.info("Found old configuration version %s", old_config_version)
-    if old_config_version not in (3, ):
-      raise Error("Unsupported configuration version: %s" %
-                  old_config_version)
-
-    # Make sure no instance uses remote_raid1 anymore
-    remote_raid1_instances = []
-    for instance in config_data["instances"]:
-      if instance["disk_template"] == "remote_raid1":
-        remote_raid1_instances.append(instance["name"])
-    if remote_raid1_instances:
-      for name in remote_raid1_instances:
-        logging.error("Instance %s still using remote_raid1 disk template")
-      raise Error("Unable to convert configuration as long as there are"
-                  " instances using remote_raid1 disk template")
-
-    # The configuration version will be stored in a ssconf file
-    if 'config_version' in config_data['cluster']:
-      del config_data['cluster']['config_version']
-
-    # Build content of new known_hosts file
-    cluster_name = ReadFile(SSCONF_CLUSTER_NAME_PATH).rstrip()
-    cluster_key = config_data['cluster']['rsahostkeypub']
-    known_hosts = "%s ssh-rsa %s\n" % (cluster_name, cluster_key)
+                 " directory") % options.data_dir)
 
-  else:
-    logging.info("Found a Ganeti 2.0 configuration")
+  config_data = serializer.LoadJson(utils.ReadFile(options.CONFIG_DATA_PATH))
 
-    if "config_version" in config_data["cluster"]:
-      raise Error("Inconsistent configuration: found config_data in"
-                  " configuration file")
+  try:
+    config_version = config_data["version"]
+  except KeyError:
+    raise Error("Unable to determine configuration version")
 
-    known_hosts = None
+  (config_major, config_minor, config_revision) = \
+    constants.SplitVersion(config_version)
 
-  config_version_str = "%s\n" % constants.BuildVersion(2, 0, 0)
-  try:
-    logging.info("Writing configuration file")
-    WriteFile(CONFIG_DATA_PATH, serializer.DumpJson(config_data))
+  logging.info("Found configuration version %s (%d.%d.%d)",
+               config_version, config_major, config_minor, config_revision)
 
-    logging.info("Writing configuration version %s",
-                 config_version_str.strip())
-    WriteFile(SSCONF_CONFIG_VERSION_PATH, config_version_str)
+  if "config_version" in config_data["cluster"]:
+    raise Error("Inconsistent configuration: found config_version in"
+                " configuration file")
+
+  if config_major == 2 and config_minor == 0:
+    if config_revision != 0:
+      logging.warning("Config revision is not 0")
+
+    config_data["version"] = constants.BuildVersion(2, 1, 0)
+
+  try:
+    logging.info("Writing configuration file to %s", options.CONFIG_DATA_PATH)
+    utils.WriteFile(file_name=options.CONFIG_DATA_PATH,
+                    data=serializer.DumpJson(config_data),
+                    mode=0600,
+                    dry_run=options.dry_run,
+                    backup=True)
+
+    if not options.dry_run:
+      if not os.path.exists(options.RAPI_CERT_FILE):
+        logging.debug("Writing RAPI certificate to %s", options.RAPI_CERT_FILE)
+        bootstrap.GenerateSelfSignedSslCert(options.RAPI_CERT_FILE)
+
+      if not os.path.exists(options.HMAC_CLUSTER_KEY):
+        logging.debug("Writing HMAC key to %s", options.HMAC_CLUSTER_KEY)
+        bootstrap.GenerateHmacKey(options.HMAC_CLUSTER_KEY)
 
-    if known_hosts is not None:
-      logging.info("Writing SSH known_hosts file (%s)", known_hosts.strip())
-      WriteFile(KNOWN_HOSTS_PATH, known_hosts)
   except:
-    logging.critical("Writing configuration failed. It is proably in an"
+    logging.critical("Writing configuration failed. It is probably in an"
                      " inconsistent state and needs manual intervention.")
     raise
 
+  # test loading the config file
+  if not options.dry_run:
+    logging.info("Testing the new config file...")
+    cfg = config.ConfigWriter(cfg_file=options.CONFIG_DATA_PATH,
+                              offline=True)
+    # if we reached this, it's all fine
+    vrfy = cfg.VerifyConfig()
+    if vrfy:
+      logging.error("Errors after conversion:")
+      for item in vrfy:
+        logging.error(" - %s", item)
+    del cfg
+    logging.info("File loaded successfully")
+
 
 if __name__ == "__main__":
   main()