Statistics
| Branch: | Tag: | Revision:

root / tools / cfgupgrade @ 5349519d

History | View | Annotate | Download (21.6 kB)

1 0006af7d Michael Hanselmann
#!/usr/bin/python
2 0006af7d Michael Hanselmann
#
3 0006af7d Michael Hanselmann
4 fdb85e3d Bernardo Dal Seno
# Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012, 2013 Google Inc.
5 0006af7d Michael Hanselmann
#
6 0006af7d Michael Hanselmann
# This program is free software; you can redistribute it and/or modify
7 0006af7d Michael Hanselmann
# it under the terms of the GNU General Public License as published by
8 0006af7d Michael Hanselmann
# the Free Software Foundation; either version 2 of the License, or
9 0006af7d Michael Hanselmann
# (at your option) any later version.
10 0006af7d Michael Hanselmann
#
11 0006af7d Michael Hanselmann
# This program is distributed in the hope that it will be useful, but
12 0006af7d Michael Hanselmann
# WITHOUT ANY WARRANTY; without even the implied warranty of
13 0006af7d Michael Hanselmann
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 0006af7d Michael Hanselmann
# General Public License for more details.
15 0006af7d Michael Hanselmann
#
16 0006af7d Michael Hanselmann
# You should have received a copy of the GNU General Public License
17 0006af7d Michael Hanselmann
# along with this program; if not, write to the Free Software
18 0006af7d Michael Hanselmann
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 0006af7d Michael Hanselmann
# 02110-1301, USA.
20 0006af7d Michael Hanselmann
21 0006af7d Michael Hanselmann
22 0006af7d Michael Hanselmann
"""Tool to upgrade the configuration file.
23 0006af7d Michael Hanselmann
24 a421fdeb Iustin Pop
This code handles only the types supported by simplejson. As an
25 a421fdeb Iustin Pop
example, 'set' is a 'list'.
26 0006af7d Michael Hanselmann
27 0006af7d Michael Hanselmann
"""
28 0006af7d Michael Hanselmann
29 0006af7d Michael Hanselmann
30 0006af7d Michael Hanselmann
import os
31 0006af7d Michael Hanselmann
import os.path
32 0006af7d Michael Hanselmann
import sys
33 0006af7d Michael Hanselmann
import optparse
34 eda37a5a Michael Hanselmann
import logging
35 7939f60c Michael Hanselmann
import time
36 7939f60c Michael Hanselmann
from cStringIO import StringIO
37 0006af7d Michael Hanselmann
38 95e4a814 Michael Hanselmann
from ganeti import constants
39 95e4a814 Michael Hanselmann
from ganeti import serializer
40 319856a9 Michael Hanselmann
from ganeti import utils
41 f97c7901 Michael Hanselmann
from ganeti import cli
42 a421fdeb Iustin Pop
from ganeti import bootstrap
43 ac4d25b6 Iustin Pop
from ganeti import config
44 011974df Michael Hanselmann
from ganeti import netutils
45 09bf5d24 Michael Hanselmann
from ganeti import pathutils
46 0006af7d Michael Hanselmann
47 effc1b86 Jose A. Lopes
from ganeti.utils import version
48 effc1b86 Jose A. Lopes
49 0006af7d Michael Hanselmann
50 319856a9 Michael Hanselmann
options = None
51 319856a9 Michael Hanselmann
args = None
52 0006af7d Michael Hanselmann
53 6f285030 Iustin Pop
54 93fd9bb1 Iustin Pop
#: Target major version we will upgrade to
55 93fd9bb1 Iustin Pop
TARGET_MAJOR = 2
56 93fd9bb1 Iustin Pop
#: Target minor version we will upgrade to
57 c777c5fc Helga Velroyen
TARGET_MINOR = 12
58 1709435e Bernardo Dal Seno
#: Target major version for downgrade
59 1709435e Bernardo Dal Seno
DOWNGRADE_MAJOR = 2
60 1709435e Bernardo Dal Seno
#: Target minor version for downgrade
61 c777c5fc Helga Velroyen
DOWNGRADE_MINOR = 11
62 93fd9bb1 Iustin Pop
63 7187a877 Helga Velroyen
# map of legacy device types
64 7187a877 Helga Velroyen
# (mapping differing old LD_* constants to new DT_* constants)
65 7187a877 Helga Velroyen
DEV_TYPE_OLD_NEW = {"lvm": constants.DT_PLAIN, "drbd8": constants.DT_DRBD8}
66 7187a877 Helga Velroyen
# (mapping differing new DT_* constants to old LD_* constants)
67 8cb2b4f4 Helga Velroyen
DEV_TYPE_NEW_OLD = dict((v, k) for k, v in DEV_TYPE_OLD_NEW.items())
68 7187a877 Helga Velroyen
69 93fd9bb1 Iustin Pop
70 319856a9 Michael Hanselmann
class Error(Exception):
71 319856a9 Michael Hanselmann
  """Generic exception"""
72 319856a9 Michael Hanselmann
  pass
73 0006af7d Michael Hanselmann
74 0006af7d Michael Hanselmann
75 eda37a5a Michael Hanselmann
def SetupLogging():
76 eda37a5a Michael Hanselmann
  """Configures the logging module.
77 eda37a5a Michael Hanselmann
78 eda37a5a Michael Hanselmann
  """
79 eda37a5a Michael Hanselmann
  formatter = logging.Formatter("%(asctime)s: %(message)s")
80 eda37a5a Michael Hanselmann
81 eda37a5a Michael Hanselmann
  stderr_handler = logging.StreamHandler()
82 eda37a5a Michael Hanselmann
  stderr_handler.setFormatter(formatter)
83 eda37a5a Michael Hanselmann
  if options.debug:
84 eda37a5a Michael Hanselmann
    stderr_handler.setLevel(logging.NOTSET)
85 eda37a5a Michael Hanselmann
  elif options.verbose:
86 eda37a5a Michael Hanselmann
    stderr_handler.setLevel(logging.INFO)
87 eda37a5a Michael Hanselmann
  else:
88 011974df Michael Hanselmann
    stderr_handler.setLevel(logging.WARNING)
89 eda37a5a Michael Hanselmann
90 eda37a5a Michael Hanselmann
  root_logger = logging.getLogger("")
91 eda37a5a Michael Hanselmann
  root_logger.setLevel(logging.NOTSET)
92 eda37a5a Michael Hanselmann
  root_logger.addHandler(stderr_handler)
93 eda37a5a Michael Hanselmann
94 eda37a5a Michael Hanselmann
95 011974df Michael Hanselmann
def CheckHostname(path):
96 011974df Michael Hanselmann
  """Ensures hostname matches ssconf value.
97 011974df Michael Hanselmann
98 011974df Michael Hanselmann
  @param path: Path to ssconf file
99 011974df Michael Hanselmann
100 011974df Michael Hanselmann
  """
101 011974df Michael Hanselmann
  ssconf_master_node = utils.ReadOneLineFile(path)
102 011974df Michael Hanselmann
  hostname = netutils.GetHostname().name
103 011974df Michael Hanselmann
104 011974df Michael Hanselmann
  if ssconf_master_node == hostname:
105 011974df Michael Hanselmann
    return True
106 011974df Michael Hanselmann
107 011974df Michael Hanselmann
  logging.warning("Warning: ssconf says master node is '%s', but this"
108 011974df Michael Hanselmann
                  " machine's name is '%s'; this tool must be run on"
109 011974df Michael Hanselmann
                  " the master node", ssconf_master_node, hostname)
110 011974df Michael Hanselmann
  return False
111 011974df Michael Hanselmann
112 3c286190 Dimitris Aragiorgis
113 e94fc80c Bernardo Dal Seno
def _FillIPolicySpecs(default_ipolicy, ipolicy):
114 e94fc80c Bernardo Dal Seno
  if "minmax" in ipolicy:
115 41044e04 Bernardo Dal Seno
    for (key, spec) in ipolicy["minmax"][0].items():
116 41044e04 Bernardo Dal Seno
      for (par, val) in default_ipolicy["minmax"][0][key].items():
117 e94fc80c Bernardo Dal Seno
        if par not in spec:
118 e94fc80c Bernardo Dal Seno
          spec[par] = val
119 e94fc80c Bernardo Dal Seno
120 e94fc80c Bernardo Dal Seno
121 e94fc80c Bernardo Dal Seno
def UpgradeIPolicy(ipolicy, default_ipolicy, isgroup):
122 0b94cda8 Bernardo Dal Seno
  minmax_keys = ["min", "max"]
123 0b94cda8 Bernardo Dal Seno
  if any((k in ipolicy) for k in minmax_keys):
124 0b94cda8 Bernardo Dal Seno
    minmax = {}
125 0b94cda8 Bernardo Dal Seno
    for key in minmax_keys:
126 0b94cda8 Bernardo Dal Seno
      if key in ipolicy:
127 e94fc80c Bernardo Dal Seno
        if ipolicy[key]:
128 e94fc80c Bernardo Dal Seno
          minmax[key] = ipolicy[key]
129 0b94cda8 Bernardo Dal Seno
        del ipolicy[key]
130 e94fc80c Bernardo Dal Seno
    if minmax:
131 41044e04 Bernardo Dal Seno
      ipolicy["minmax"] = [minmax]
132 e94fc80c Bernardo Dal Seno
  if isgroup and "std" in ipolicy:
133 e94fc80c Bernardo Dal Seno
    del ipolicy["std"]
134 e94fc80c Bernardo Dal Seno
  _FillIPolicySpecs(default_ipolicy, ipolicy)
135 0b94cda8 Bernardo Dal Seno
136 0b94cda8 Bernardo Dal Seno
137 58bf877f Dimitris Aragiorgis
def UpgradeNetworks(config_data):
138 58bf877f Dimitris Aragiorgis
  networks = config_data.get("networks", None)
139 58bf877f Dimitris Aragiorgis
  if not networks:
140 58bf877f Dimitris Aragiorgis
    config_data["networks"] = {}
141 58bf877f Dimitris Aragiorgis
142 58bf877f Dimitris Aragiorgis
143 0b94cda8 Bernardo Dal Seno
def UpgradeCluster(config_data):
144 0b94cda8 Bernardo Dal Seno
  cluster = config_data.get("cluster", None)
145 0b94cda8 Bernardo Dal Seno
  if cluster is None:
146 0b94cda8 Bernardo Dal Seno
    raise Error("Cannot find cluster")
147 e94fc80c Bernardo Dal Seno
  ipolicy = cluster.setdefault("ipolicy", None)
148 0b94cda8 Bernardo Dal Seno
  if ipolicy:
149 e94fc80c Bernardo Dal Seno
    UpgradeIPolicy(ipolicy, constants.IPOLICY_DEFAULTS, False)
150 0359e5d0 Spyros Trigazis
  ial_params = cluster.get("default_iallocator_params", None)
151 0359e5d0 Spyros Trigazis
  if not ial_params:
152 0359e5d0 Spyros Trigazis
    cluster["default_iallocator_params"] = {}
153 b3cc1646 Helga Velroyen
  if not "candidate_certs" in cluster:
154 b3cc1646 Helga Velroyen
    cluster["candidate_certs"] = {}
155 8a5d326f Jose A. Lopes
  cluster["instance_communication_network"] = \
156 8a5d326f Jose A. Lopes
    cluster.get("instance_communication_network", "")
157 0b94cda8 Bernardo Dal Seno
158 0b94cda8 Bernardo Dal Seno
159 58bf877f Dimitris Aragiorgis
def UpgradeGroups(config_data):
160 e94fc80c Bernardo Dal Seno
  cl_ipolicy = config_data["cluster"].get("ipolicy")
161 58bf877f Dimitris Aragiorgis
  for group in config_data["nodegroups"].values():
162 58bf877f Dimitris Aragiorgis
    networks = group.get("networks", None)
163 58bf877f Dimitris Aragiorgis
    if not networks:
164 58bf877f Dimitris Aragiorgis
      group["networks"] = {}
165 0b94cda8 Bernardo Dal Seno
    ipolicy = group.get("ipolicy", None)
166 0b94cda8 Bernardo Dal Seno
    if ipolicy:
167 e94fc80c Bernardo Dal Seno
      if cl_ipolicy is None:
168 e94fc80c Bernardo Dal Seno
        raise Error("A group defines an instance policy but there is no"
169 e94fc80c Bernardo Dal Seno
                    " instance policy at cluster level")
170 e94fc80c Bernardo Dal Seno
      UpgradeIPolicy(ipolicy, cl_ipolicy, True)
171 58bf877f Dimitris Aragiorgis
172 011974df Michael Hanselmann
173 c69b147d Bernardo Dal Seno
def GetExclusiveStorageValue(config_data):
174 c69b147d Bernardo Dal Seno
  """Return a conservative value of the exclusive_storage flag.
175 c69b147d Bernardo Dal Seno
176 c69b147d Bernardo Dal Seno
  Return C{True} if the cluster or at least a nodegroup have the flag set.
177 c69b147d Bernardo Dal Seno
178 c69b147d Bernardo Dal Seno
  """
179 c69b147d Bernardo Dal Seno
  ret = False
180 c69b147d Bernardo Dal Seno
  cluster = config_data["cluster"]
181 c69b147d Bernardo Dal Seno
  ndparams = cluster.get("ndparams")
182 c69b147d Bernardo Dal Seno
  if ndparams is not None and ndparams.get("exclusive_storage"):
183 c69b147d Bernardo Dal Seno
    ret = True
184 c69b147d Bernardo Dal Seno
  for group in config_data["nodegroups"].values():
185 c69b147d Bernardo Dal Seno
    ndparams = group.get("ndparams")
186 c69b147d Bernardo Dal Seno
    if ndparams is not None and ndparams.get("exclusive_storage"):
187 c69b147d Bernardo Dal Seno
      ret = True
188 c69b147d Bernardo Dal Seno
  return ret
189 c69b147d Bernardo Dal Seno
190 c69b147d Bernardo Dal Seno
191 5275a77f Thomas Thrainer
def RemovePhysicalId(disk):
192 5275a77f Thomas Thrainer
  if "children" in disk:
193 5275a77f Thomas Thrainer
    for d in disk["children"]:
194 5275a77f Thomas Thrainer
      RemovePhysicalId(d)
195 5275a77f Thomas Thrainer
  if "physical_id" in disk:
196 5275a77f Thomas Thrainer
    del disk["physical_id"]
197 5275a77f Thomas Thrainer
198 5275a77f Thomas Thrainer
199 7187a877 Helga Velroyen
def ChangeDiskDevType(disk, dev_type_map):
200 7187a877 Helga Velroyen
  """Replaces disk's dev_type attributes according to the given map.
201 7187a877 Helga Velroyen
202 7187a877 Helga Velroyen
  This can be used for both, up or downgrading the disks.
203 7187a877 Helga Velroyen
  """
204 7187a877 Helga Velroyen
  if disk["dev_type"] in dev_type_map:
205 7187a877 Helga Velroyen
    disk["dev_type"] = dev_type_map[disk["dev_type"]]
206 7187a877 Helga Velroyen
  if "children" in disk:
207 7187a877 Helga Velroyen
    for child in disk["children"]:
208 7187a877 Helga Velroyen
      ChangeDiskDevType(child, dev_type_map)
209 7187a877 Helga Velroyen
210 7187a877 Helga Velroyen
211 7187a877 Helga Velroyen
def UpgradeDiskDevType(disk):
212 7187a877 Helga Velroyen
  """Upgrades the disks' device type."""
213 7187a877 Helga Velroyen
  ChangeDiskDevType(disk, DEV_TYPE_OLD_NEW)
214 7187a877 Helga Velroyen
215 7187a877 Helga Velroyen
216 f032d55c Dimitris Aragiorgis
def UpgradeInstances(config_data):
217 7187a877 Helga Velroyen
  """Upgrades the instances' configuration."""
218 7187a877 Helga Velroyen
219 f032d55c Dimitris Aragiorgis
  network2uuid = dict((n["name"], n["uuid"])
220 f032d55c Dimitris Aragiorgis
                      for n in config_data["networks"].values())
221 bb553e5a Bernardo Dal Seno
  if "instances" not in config_data:
222 bb553e5a Bernardo Dal Seno
    raise Error("Can't find the 'instances' key in the configuration!")
223 bb553e5a Bernardo Dal Seno
224 c69b147d Bernardo Dal Seno
  missing_spindles = False
225 bb553e5a Bernardo Dal Seno
  for instance, iobj in config_data["instances"].items():
226 bb553e5a Bernardo Dal Seno
    for nic in iobj["nics"]:
227 f032d55c Dimitris Aragiorgis
      name = nic.get("network", None)
228 f032d55c Dimitris Aragiorgis
      if name:
229 f032d55c Dimitris Aragiorgis
        uuid = network2uuid.get(name, None)
230 f032d55c Dimitris Aragiorgis
        if uuid:
231 f032d55c Dimitris Aragiorgis
          print("NIC with network name %s found."
232 f032d55c Dimitris Aragiorgis
                " Substituting with uuid %s." % (name, uuid))
233 f032d55c Dimitris Aragiorgis
          nic["network"] = uuid
234 f032d55c Dimitris Aragiorgis
235 bb553e5a Bernardo Dal Seno
    if "disks" not in iobj:
236 bb553e5a Bernardo Dal Seno
      raise Error("Instance '%s' doesn't have a disks entry?!" % instance)
237 bb553e5a Bernardo Dal Seno
    disks = iobj["disks"]
238 bb553e5a Bernardo Dal Seno
    for idx, dobj in enumerate(disks):
239 5275a77f Thomas Thrainer
      RemovePhysicalId(dobj)
240 5275a77f Thomas Thrainer
241 bb553e5a Bernardo Dal Seno
      expected = "disk/%s" % idx
242 bb553e5a Bernardo Dal Seno
      current = dobj.get("iv_name", "")
243 bb553e5a Bernardo Dal Seno
      if current != expected:
244 bb553e5a Bernardo Dal Seno
        logging.warning("Updating iv_name for instance %s/disk %s"
245 bb553e5a Bernardo Dal Seno
                        " from '%s' to '%s'",
246 bb553e5a Bernardo Dal Seno
                        instance, idx, current, expected)
247 bb553e5a Bernardo Dal Seno
        dobj["iv_name"] = expected
248 7187a877 Helga Velroyen
249 7187a877 Helga Velroyen
      if "dev_type" in dobj:
250 7187a877 Helga Velroyen
        UpgradeDiskDevType(dobj)
251 7187a877 Helga Velroyen
252 c69b147d Bernardo Dal Seno
      if not "spindles" in dobj:
253 c69b147d Bernardo Dal Seno
        missing_spindles = True
254 c69b147d Bernardo Dal Seno
255 c69b147d Bernardo Dal Seno
  if GetExclusiveStorageValue(config_data) and missing_spindles:
256 c69b147d Bernardo Dal Seno
    # We cannot be sure that the instances that are missing spindles have
257 c69b147d Bernardo Dal Seno
    # exclusive storage enabled (the check would be more complicated), so we
258 c69b147d Bernardo Dal Seno
    # give a noncommittal message
259 c69b147d Bernardo Dal Seno
    logging.warning("Some instance disks could be needing to update the"
260 c69b147d Bernardo Dal Seno
                    " spindles parameter; you can check by running"
261 c69b147d Bernardo Dal Seno
                    " 'gnt-cluster verify', and fix any problem with"
262 c69b147d Bernardo Dal Seno
                    " 'gnt-cluster repair-disk-sizes'")
263 bb553e5a Bernardo Dal Seno
264 bb553e5a Bernardo Dal Seno
265 bb553e5a Bernardo Dal Seno
def UpgradeRapiUsers():
266 bb553e5a Bernardo Dal Seno
  if (os.path.isfile(options.RAPI_USERS_FILE_PRE24) and
267 bb553e5a Bernardo Dal Seno
      not os.path.islink(options.RAPI_USERS_FILE_PRE24)):
268 bb553e5a Bernardo Dal Seno
    if os.path.exists(options.RAPI_USERS_FILE):
269 bb553e5a Bernardo Dal Seno
      raise Error("Found pre-2.4 RAPI users file at %s, but another file"
270 bb553e5a Bernardo Dal Seno
                  " already exists at %s" %
271 bb553e5a Bernardo Dal Seno
                  (options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE))
272 bb553e5a Bernardo Dal Seno
    logging.info("Found pre-2.4 RAPI users file at %s, renaming to %s",
273 bb553e5a Bernardo Dal Seno
                 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
274 bb553e5a Bernardo Dal Seno
    if not options.dry_run:
275 bb553e5a Bernardo Dal Seno
      utils.RenameFile(options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE,
276 bb553e5a Bernardo Dal Seno
                       mkdir=True, mkdir_mode=0750)
277 bb553e5a Bernardo Dal Seno
278 bb553e5a Bernardo Dal Seno
  # Create a symlink for RAPI users file
279 bb553e5a Bernardo Dal Seno
  if (not (os.path.islink(options.RAPI_USERS_FILE_PRE24) or
280 bb553e5a Bernardo Dal Seno
           os.path.isfile(options.RAPI_USERS_FILE_PRE24)) and
281 bb553e5a Bernardo Dal Seno
      os.path.isfile(options.RAPI_USERS_FILE)):
282 bb553e5a Bernardo Dal Seno
    logging.info("Creating symlink from %s to %s",
283 bb553e5a Bernardo Dal Seno
                 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
284 bb553e5a Bernardo Dal Seno
    if not options.dry_run:
285 bb553e5a Bernardo Dal Seno
      os.symlink(options.RAPI_USERS_FILE, options.RAPI_USERS_FILE_PRE24)
286 bb553e5a Bernardo Dal Seno
287 bb553e5a Bernardo Dal Seno
288 bb553e5a Bernardo Dal Seno
def UpgradeWatcher():
289 bb553e5a Bernardo Dal Seno
  # Remove old watcher state file if it exists
290 bb553e5a Bernardo Dal Seno
  if os.path.exists(options.WATCHER_STATEFILE):
291 bb553e5a Bernardo Dal Seno
    logging.info("Removing watcher state file %s", options.WATCHER_STATEFILE)
292 bb553e5a Bernardo Dal Seno
    if not options.dry_run:
293 bb553e5a Bernardo Dal Seno
      utils.RemoveFile(options.WATCHER_STATEFILE)
294 bb553e5a Bernardo Dal Seno
295 bb553e5a Bernardo Dal Seno
296 bb553e5a Bernardo Dal Seno
def UpgradeFileStoragePaths(config_data):
297 bb553e5a Bernardo Dal Seno
  # Write file storage paths
298 bb553e5a Bernardo Dal Seno
  if not os.path.exists(options.FILE_STORAGE_PATHS_FILE):
299 bb553e5a Bernardo Dal Seno
    cluster = config_data["cluster"]
300 bb553e5a Bernardo Dal Seno
    file_storage_dir = cluster.get("file_storage_dir")
301 bb553e5a Bernardo Dal Seno
    shared_file_storage_dir = cluster.get("shared_file_storage_dir")
302 bb553e5a Bernardo Dal Seno
    del cluster
303 bb553e5a Bernardo Dal Seno
304 bb553e5a Bernardo Dal Seno
    logging.info("Ganeti 2.7 and later only allow whitelisted directories"
305 bb553e5a Bernardo Dal Seno
                 " for file storage; writing existing configuration values"
306 bb553e5a Bernardo Dal Seno
                 " into '%s'",
307 bb553e5a Bernardo Dal Seno
                 options.FILE_STORAGE_PATHS_FILE)
308 bb553e5a Bernardo Dal Seno
309 bb553e5a Bernardo Dal Seno
    if file_storage_dir:
310 bb553e5a Bernardo Dal Seno
      logging.info("File storage directory: %s", file_storage_dir)
311 bb553e5a Bernardo Dal Seno
    if shared_file_storage_dir:
312 bb553e5a Bernardo Dal Seno
      logging.info("Shared file storage directory: %s",
313 bb553e5a Bernardo Dal Seno
                   shared_file_storage_dir)
314 bb553e5a Bernardo Dal Seno
315 bb553e5a Bernardo Dal Seno
    buf = StringIO()
316 bb553e5a Bernardo Dal Seno
    buf.write("# List automatically generated from configuration by\n")
317 bb553e5a Bernardo Dal Seno
    buf.write("# cfgupgrade at %s\n" % time.asctime())
318 bb553e5a Bernardo Dal Seno
    if file_storage_dir:
319 bb553e5a Bernardo Dal Seno
      buf.write("%s\n" % file_storage_dir)
320 bb553e5a Bernardo Dal Seno
    if shared_file_storage_dir:
321 bb553e5a Bernardo Dal Seno
      buf.write("%s\n" % shared_file_storage_dir)
322 bb553e5a Bernardo Dal Seno
    utils.WriteFile(file_name=options.FILE_STORAGE_PATHS_FILE,
323 bb553e5a Bernardo Dal Seno
                    data=buf.getvalue(),
324 bb553e5a Bernardo Dal Seno
                    mode=0600,
325 bb553e5a Bernardo Dal Seno
                    dry_run=options.dry_run,
326 bb553e5a Bernardo Dal Seno
                    backup=True)
327 bb553e5a Bernardo Dal Seno
328 bb553e5a Bernardo Dal Seno
329 b555101c Thomas Thrainer
def GetNewNodeIndex(nodes_by_old_key, old_key, new_key_field):
330 b555101c Thomas Thrainer
  if old_key not in nodes_by_old_key:
331 b555101c Thomas Thrainer
    logging.warning("Can't find node '%s' in configuration, assuming that it's"
332 b555101c Thomas Thrainer
                    " already up-to-date", old_key)
333 b555101c Thomas Thrainer
    return old_key
334 b555101c Thomas Thrainer
  return nodes_by_old_key[old_key][new_key_field]
335 b555101c Thomas Thrainer
336 b555101c Thomas Thrainer
337 b555101c Thomas Thrainer
def ChangeNodeIndices(config_data, old_key_field, new_key_field):
338 b555101c Thomas Thrainer
  def ChangeDiskNodeIndices(disk):
339 7187a877 Helga Velroyen
    # Note: 'drbd8' is a legacy device type from pre 2.9 and needs to be
340 7187a877 Helga Velroyen
    # considered when up/downgrading from/to any versions touching 2.9 on the
341 7187a877 Helga Velroyen
    # way.
342 7187a877 Helga Velroyen
    drbd_disk_types = set(["drbd8"]) | constants.DTS_DRBD
343 7187a877 Helga Velroyen
    if disk["dev_type"] in drbd_disk_types:
344 b555101c Thomas Thrainer
      for i in range(0, 2):
345 b555101c Thomas Thrainer
        disk["logical_id"][i] = GetNewNodeIndex(nodes_by_old_key,
346 b555101c Thomas Thrainer
                                                disk["logical_id"][i],
347 b555101c Thomas Thrainer
                                                new_key_field)
348 b555101c Thomas Thrainer
    if "children" in disk:
349 b555101c Thomas Thrainer
      for child in disk["children"]:
350 b555101c Thomas Thrainer
        ChangeDiskNodeIndices(child)
351 b555101c Thomas Thrainer
352 b555101c Thomas Thrainer
  nodes_by_old_key = {}
353 b555101c Thomas Thrainer
  nodes_by_new_key = {}
354 b555101c Thomas Thrainer
  for (_, node) in config_data["nodes"].items():
355 b555101c Thomas Thrainer
    nodes_by_old_key[node[old_key_field]] = node
356 b555101c Thomas Thrainer
    nodes_by_new_key[node[new_key_field]] = node
357 b555101c Thomas Thrainer
358 b555101c Thomas Thrainer
  config_data["nodes"] = nodes_by_new_key
359 b555101c Thomas Thrainer
360 b555101c Thomas Thrainer
  cluster = config_data["cluster"]
361 b555101c Thomas Thrainer
  cluster["master_node"] = GetNewNodeIndex(nodes_by_old_key,
362 b555101c Thomas Thrainer
                                           cluster["master_node"],
363 b555101c Thomas Thrainer
                                           new_key_field)
364 b555101c Thomas Thrainer
365 b555101c Thomas Thrainer
  for inst in config_data["instances"].values():
366 b555101c Thomas Thrainer
    inst["primary_node"] = GetNewNodeIndex(nodes_by_old_key,
367 b555101c Thomas Thrainer
                                           inst["primary_node"],
368 b555101c Thomas Thrainer
                                           new_key_field)
369 b555101c Thomas Thrainer
    for disk in inst["disks"]:
370 b555101c Thomas Thrainer
      ChangeDiskNodeIndices(disk)
371 b555101c Thomas Thrainer
372 b555101c Thomas Thrainer
373 4d33e134 Thomas Thrainer
def ChangeInstanceIndices(config_data, old_key_field, new_key_field):
374 4d33e134 Thomas Thrainer
  insts_by_old_key = {}
375 4d33e134 Thomas Thrainer
  insts_by_new_key = {}
376 4d33e134 Thomas Thrainer
  for (_, inst) in config_data["instances"].items():
377 4d33e134 Thomas Thrainer
    insts_by_old_key[inst[old_key_field]] = inst
378 4d33e134 Thomas Thrainer
    insts_by_new_key[inst[new_key_field]] = inst
379 4d33e134 Thomas Thrainer
380 4d33e134 Thomas Thrainer
  config_data["instances"] = insts_by_new_key
381 4d33e134 Thomas Thrainer
382 4d33e134 Thomas Thrainer
383 b555101c Thomas Thrainer
def UpgradeNodeIndices(config_data):
384 b555101c Thomas Thrainer
  ChangeNodeIndices(config_data, "name", "uuid")
385 b555101c Thomas Thrainer
386 b555101c Thomas Thrainer
387 4d33e134 Thomas Thrainer
def UpgradeInstanceIndices(config_data):
388 4d33e134 Thomas Thrainer
  ChangeInstanceIndices(config_data, "name", "uuid")
389 4d33e134 Thomas Thrainer
390 4d33e134 Thomas Thrainer
391 bb553e5a Bernardo Dal Seno
def UpgradeAll(config_data):
392 effc1b86 Jose A. Lopes
  config_data["version"] = version.BuildVersion(TARGET_MAJOR, TARGET_MINOR, 0)
393 bb553e5a Bernardo Dal Seno
  UpgradeRapiUsers()
394 bb553e5a Bernardo Dal Seno
  UpgradeWatcher()
395 bb553e5a Bernardo Dal Seno
  UpgradeFileStoragePaths(config_data)
396 bb553e5a Bernardo Dal Seno
  UpgradeNetworks(config_data)
397 0b94cda8 Bernardo Dal Seno
  UpgradeCluster(config_data)
398 bb553e5a Bernardo Dal Seno
  UpgradeGroups(config_data)
399 bb553e5a Bernardo Dal Seno
  UpgradeInstances(config_data)
400 b555101c Thomas Thrainer
  UpgradeNodeIndices(config_data)
401 4d33e134 Thomas Thrainer
  UpgradeInstanceIndices(config_data)
402 bb553e5a Bernardo Dal Seno
403 f032d55c Dimitris Aragiorgis
404 8cd19bec Petr Pudlak
# DOWNGRADE ------------------------------------------------------------
405 8cd19bec Petr Pudlak
406 8cd19bec Petr Pudlak
407 4f7cc3c2 Klaus Aehlig
def DowngradeCluster(config_data):
408 4f7cc3c2 Klaus Aehlig
  cluster = config_data.get("cluster", None)
409 4f7cc3c2 Klaus Aehlig
  if not cluster:
410 4f7cc3c2 Klaus Aehlig
    raise Error("Cannot find the 'cluster' key in the configuration")
411 4f7cc3c2 Klaus Aehlig
412 4f7cc3c2 Klaus Aehlig
  if "osparams_private_cluster" in cluster:
413 4f7cc3c2 Klaus Aehlig
    del cluster["osparams_private_cluster"]
414 4f7cc3c2 Klaus Aehlig
415 8a5d326f Jose A. Lopes
  if "instance_communication_network" in cluster:
416 8a5d326f Jose A. Lopes
    del cluster["instance_communication_network"]
417 8a5d326f Jose A. Lopes
418 514dcbda Klaus Aehlig
419 65b526e7 Klaus Aehlig
def DowngradeInstances(config_data):
420 65b526e7 Klaus Aehlig
  instances = config_data.get("instances", None)
421 a21440d8 Helga Velroyen
  if instances is None:
422 65b526e7 Klaus Aehlig
    raise Error("Cannot find the 'instances' key in the configuration")
423 8a5d326f Jose A. Lopes
424 65b526e7 Klaus Aehlig
  for (_, iobj) in instances.items():
425 65b526e7 Klaus Aehlig
    if "osparams_private" in iobj:
426 65b526e7 Klaus Aehlig
      del iobj["osparams_private"]
427 65b526e7 Klaus Aehlig
428 514dcbda Klaus Aehlig
429 1709435e Bernardo Dal Seno
def DowngradeAll(config_data):
430 1709435e Bernardo Dal Seno
  # Any code specific to a particular version should be labeled that way, so
431 1709435e Bernardo Dal Seno
  # it can be removed when updating to the next version.
432 effc1b86 Jose A. Lopes
  config_data["version"] = version.BuildVersion(DOWNGRADE_MAJOR,
433 effc1b86 Jose A. Lopes
                                                DOWNGRADE_MINOR, 0)
434 4f7cc3c2 Klaus Aehlig
  DowngradeCluster(config_data)
435 65b526e7 Klaus Aehlig
  DowngradeInstances(config_data)
436 1709435e Bernardo Dal Seno
437 1709435e Bernardo Dal Seno
438 6d691282 Michael Hanselmann
def main():
439 6d691282 Michael Hanselmann
  """Main program.
440 6d691282 Michael Hanselmann
441 6d691282 Michael Hanselmann
  """
442 b459a848 Andrea Spadaccini
  global options, args # pylint: disable=W0603
443 6d691282 Michael Hanselmann
444 0006af7d Michael Hanselmann
  # Option parsing
445 95e4a814 Michael Hanselmann
  parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
446 3ccb3a64 Michael Hanselmann
  parser.add_option("--dry-run", dest="dry_run",
447 60edf71e Michael Hanselmann
                    action="store_true",
448 f4bc1f2c Michael Hanselmann
                    help="Try to do the conversion, but don't write"
449 f4bc1f2c Michael Hanselmann
                         " output file")
450 f97c7901 Michael Hanselmann
  parser.add_option(cli.FORCE_OPT)
451 eda37a5a Michael Hanselmann
  parser.add_option(cli.DEBUG_OPT)
452 9cdb9578 Iustin Pop
  parser.add_option(cli.VERBOSE_OPT)
453 011974df Michael Hanselmann
  parser.add_option("--ignore-hostname", dest="ignore_hostname",
454 011974df Michael Hanselmann
                    action="store_true", default=False,
455 011974df Michael Hanselmann
                    help="Don't abort if hostname doesn't match")
456 3ccb3a64 Michael Hanselmann
  parser.add_option("--path", help="Convert configuration in this"
457 09bf5d24 Michael Hanselmann
                    " directory instead of '%s'" % pathutils.DATA_DIR,
458 09bf5d24 Michael Hanselmann
                    default=pathutils.DATA_DIR, dest="data_dir")
459 7939f60c Michael Hanselmann
  parser.add_option("--confdir",
460 7939f60c Michael Hanselmann
                    help=("Use this directory instead of '%s'" %
461 7939f60c Michael Hanselmann
                          pathutils.CONF_DIR),
462 7939f60c Michael Hanselmann
                    default=pathutils.CONF_DIR, dest="conf_dir")
463 02e1292d Michael Hanselmann
  parser.add_option("--no-verify",
464 02e1292d Michael Hanselmann
                    help="Do not verify configuration after upgrade",
465 02e1292d Michael Hanselmann
                    action="store_true", dest="no_verify", default=False)
466 1709435e Bernardo Dal Seno
  parser.add_option("--downgrade",
467 1709435e Bernardo Dal Seno
                    help="Downgrade to the previous stable version",
468 1709435e Bernardo Dal Seno
                    action="store_true", dest="downgrade", default=False)
469 0006af7d Michael Hanselmann
  (options, args) = parser.parse_args()
470 0006af7d Michael Hanselmann
471 ac4d25b6 Iustin Pop
  # We need to keep filenames locally because they might be renamed between
472 ac4d25b6 Iustin Pop
  # versions.
473 0cddd44d Iustin Pop
  options.data_dir = os.path.abspath(options.data_dir)
474 ac4d25b6 Iustin Pop
  options.CONFIG_DATA_PATH = options.data_dir + "/config.data"
475 ac4d25b6 Iustin Pop
  options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
476 b3cc1646 Helga Velroyen
  options.CLIENT_PEM_PATH = options.data_dir + "/client.pem"
477 ac4d25b6 Iustin Pop
  options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
478 ac4d25b6 Iustin Pop
  options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
479 b6267745 Andrea Spadaccini
  options.SPICE_CERT_FILE = options.data_dir + "/spice.pem"
480 b6267745 Andrea Spadaccini
  options.SPICE_CACERT_FILE = options.data_dir + "/spice-ca.pem"
481 fdd9ac5b Michael Hanselmann
  options.RAPI_USERS_FILE = options.data_dir + "/rapi/users"
482 fdd9ac5b Michael Hanselmann
  options.RAPI_USERS_FILE_PRE24 = options.data_dir + "/rapi_users"
483 6b7d5878 Michael Hanselmann
  options.CONFD_HMAC_KEY = options.data_dir + "/hmac.key"
484 fc0726b9 Michael Hanselmann
  options.CDS_FILE = options.data_dir + "/cluster-domain-secret"
485 011974df Michael Hanselmann
  options.SSCONF_MASTER_NODE = options.data_dir + "/ssconf_master_node"
486 a292020f Michael Hanselmann
  options.WATCHER_STATEFILE = options.data_dir + "/watcher.data"
487 7939f60c Michael Hanselmann
  options.FILE_STORAGE_PATHS_FILE = options.conf_dir + "/file-storage-paths"
488 ac4d25b6 Iustin Pop
489 eda37a5a Michael Hanselmann
  SetupLogging()
490 eda37a5a Michael Hanselmann
491 0006af7d Michael Hanselmann
  # Option checking
492 0006af7d Michael Hanselmann
  if args:
493 95e4a814 Michael Hanselmann
    raise Error("No arguments expected")
494 1709435e Bernardo Dal Seno
  if options.downgrade and not options.no_verify:
495 1709435e Bernardo Dal Seno
    options.no_verify = True
496 0006af7d Michael Hanselmann
497 011974df Michael Hanselmann
  # Check master name
498 011974df Michael Hanselmann
  if not (CheckHostname(options.SSCONF_MASTER_NODE) or options.ignore_hostname):
499 011974df Michael Hanselmann
    logging.error("Aborting due to hostname mismatch")
500 011974df Michael Hanselmann
    sys.exit(constants.EXIT_FAILURE)
501 011974df Michael Hanselmann
502 319856a9 Michael Hanselmann
  if not options.force:
503 1709435e Bernardo Dal Seno
    if options.downgrade:
504 1709435e Bernardo Dal Seno
      usertext = ("The configuration is going to be DOWNGRADED to version %s.%s"
505 1709435e Bernardo Dal Seno
                  " Some configuration data might be removed if they don't fit"
506 1709435e Bernardo Dal Seno
                  " in the old format. Please make sure you have read the"
507 1709435e Bernardo Dal Seno
                  " upgrade notes (available in the UPGRADE file and included"
508 1709435e Bernardo Dal Seno
                  " in other documentation formats) to understand what they"
509 1709435e Bernardo Dal Seno
                  " are. Continue with *DOWNGRADING* the configuration?" %
510 1709435e Bernardo Dal Seno
                  (DOWNGRADE_MAJOR, DOWNGRADE_MINOR))
511 1709435e Bernardo Dal Seno
    else:
512 1709435e Bernardo Dal Seno
      usertext = ("Please make sure you have read the upgrade notes for"
513 1709435e Bernardo Dal Seno
                  " Ganeti %s (available in the UPGRADE file and included"
514 1709435e Bernardo Dal Seno
                  " in other documentation formats). Continue with upgrading"
515 1709435e Bernardo Dal Seno
                  " configuration?" % constants.RELEASE_VERSION)
516 f97c7901 Michael Hanselmann
    if not cli.AskUser(usertext):
517 a9221f09 Michael Hanselmann
      sys.exit(constants.EXIT_FAILURE)
518 319856a9 Michael Hanselmann
519 95e4a814 Michael Hanselmann
  # Check whether it's a Ganeti configuration directory
520 ac4d25b6 Iustin Pop
  if not (os.path.isfile(options.CONFIG_DATA_PATH) and
521 30acff6c Michael Hanselmann
          os.path.isfile(options.SERVER_PEM_PATH) and
522 ac4d25b6 Iustin Pop
          os.path.isfile(options.KNOWN_HOSTS_PATH)):
523 a9221f09 Michael Hanselmann
    raise Error(("%s does not seem to be a Ganeti configuration"
524 ac4d25b6 Iustin Pop
                 " directory") % options.data_dir)
525 95e4a814 Michael Hanselmann
526 7939f60c Michael Hanselmann
  if not os.path.isdir(options.conf_dir):
527 7939f60c Michael Hanselmann
    raise Error("Not a directory: %s" % options.conf_dir)
528 7939f60c Michael Hanselmann
529 11c31f5c Michael Hanselmann
  config_data = serializer.LoadJson(utils.ReadFile(options.CONFIG_DATA_PATH))
530 a421fdeb Iustin Pop
531 11c31f5c Michael Hanselmann
  try:
532 11c31f5c Michael Hanselmann
    config_version = config_data["version"]
533 11c31f5c Michael Hanselmann
  except KeyError:
534 11c31f5c Michael Hanselmann
    raise Error("Unable to determine configuration version")
535 0006af7d Michael Hanselmann
536 11c31f5c Michael Hanselmann
  (config_major, config_minor, config_revision) = \
537 effc1b86 Jose A. Lopes
    version.SplitVersion(config_version)
538 319856a9 Michael Hanselmann
539 11c31f5c Michael Hanselmann
  logging.info("Found configuration version %s (%d.%d.%d)",
540 11c31f5c Michael Hanselmann
               config_version, config_major, config_minor, config_revision)
541 319856a9 Michael Hanselmann
542 11c31f5c Michael Hanselmann
  if "config_version" in config_data["cluster"]:
543 11c31f5c Michael Hanselmann
    raise Error("Inconsistent configuration: found config_version in"
544 11c31f5c Michael Hanselmann
                " configuration file")
545 95e4a814 Michael Hanselmann
546 1709435e Bernardo Dal Seno
  # Downgrade to the previous stable version
547 1709435e Bernardo Dal Seno
  if options.downgrade:
548 f2e4363c Michele Tartara
    if not ((config_major == TARGET_MAJOR and config_minor == TARGET_MINOR) or
549 f2e4363c Michele Tartara
            (config_major == DOWNGRADE_MAJOR and
550 f2e4363c Michele Tartara
             config_minor == DOWNGRADE_MINOR)):
551 1709435e Bernardo Dal Seno
      raise Error("Downgrade supported only from the latest version (%s.%s),"
552 1709435e Bernardo Dal Seno
                  " found %s (%s.%s.%s) instead" %
553 1709435e Bernardo Dal Seno
                  (TARGET_MAJOR, TARGET_MINOR, config_version, config_major,
554 1709435e Bernardo Dal Seno
                   config_minor, config_revision))
555 1709435e Bernardo Dal Seno
    DowngradeAll(config_data)
556 1709435e Bernardo Dal Seno
557 c777c5fc Helga Velroyen
  # Upgrade from 2.{0..10} to 2.12
558 c777c5fc Helga Velroyen
  elif config_major == 2 and config_minor in range(0, 12):
559 aeb0c953 Michael Hanselmann
    if config_revision != 0:
560 a9221f09 Michael Hanselmann
      logging.warning("Config revision is %s, not 0", config_revision)
561 bb553e5a Bernardo Dal Seno
    UpgradeAll(config_data)
562 904910c4 Iustin Pop
563 93fd9bb1 Iustin Pop
  elif config_major == TARGET_MAJOR and config_minor == TARGET_MINOR:
564 a9221f09 Michael Hanselmann
    logging.info("No changes necessary")
565 a9221f09 Michael Hanselmann
566 a9221f09 Michael Hanselmann
  else:
567 a9221f09 Michael Hanselmann
    raise Error("Configuration version %d.%d.%d not supported by this tool" %
568 a9221f09 Michael Hanselmann
                (config_major, config_minor, config_revision))
569 aeb0c953 Michael Hanselmann
570 11c31f5c Michael Hanselmann
  try:
571 11c31f5c Michael Hanselmann
    logging.info("Writing configuration file to %s", options.CONFIG_DATA_PATH)
572 11c31f5c Michael Hanselmann
    utils.WriteFile(file_name=options.CONFIG_DATA_PATH,
573 11c31f5c Michael Hanselmann
                    data=serializer.DumpJson(config_data),
574 11c31f5c Michael Hanselmann
                    mode=0600,
575 11c31f5c Michael Hanselmann
                    dry_run=options.dry_run,
576 11c31f5c Michael Hanselmann
                    backup=True)
577 a421fdeb Iustin Pop
578 a421fdeb Iustin Pop
    if not options.dry_run:
579 5ae4945a Iustin Pop
      bootstrap.GenerateClusterCrypto(
580 b3cc1646 Helga Velroyen
        False, False, False, False, False,
581 5ae4945a Iustin Pop
        nodecert_file=options.SERVER_PEM_PATH,
582 5ae4945a Iustin Pop
        rapicert_file=options.RAPI_CERT_FILE,
583 5ae4945a Iustin Pop
        spicecert_file=options.SPICE_CERT_FILE,
584 5ae4945a Iustin Pop
        spicecacert_file=options.SPICE_CACERT_FILE,
585 5ae4945a Iustin Pop
        hmackey_file=options.CONFD_HMAC_KEY,
586 5ae4945a Iustin Pop
        cds_file=options.CDS_FILE)
587 aeb0c953 Michael Hanselmann
588 a9221f09 Michael Hanselmann
  except Exception:
589 11c31f5c Michael Hanselmann
    logging.critical("Writing configuration failed. It is probably in an"
590 95e4a814 Michael Hanselmann
                     " inconsistent state and needs manual intervention.")
591 95e4a814 Michael Hanselmann
    raise
592 0006af7d Michael Hanselmann
593 ac4d25b6 Iustin Pop
  # test loading the config file
594 fdb85e3d Bernardo Dal Seno
  all_ok = True
595 02e1292d Michael Hanselmann
  if not (options.dry_run or options.no_verify):
596 ac4d25b6 Iustin Pop
    logging.info("Testing the new config file...")
597 ac4d25b6 Iustin Pop
    cfg = config.ConfigWriter(cfg_file=options.CONFIG_DATA_PATH,
598 959b6fe5 Apollon Oikonomopoulos
                              accept_foreign=options.ignore_hostname,
599 ac4d25b6 Iustin Pop
                              offline=True)
600 ac4d25b6 Iustin Pop
    # if we reached this, it's all fine
601 ac4d25b6 Iustin Pop
    vrfy = cfg.VerifyConfig()
602 ac4d25b6 Iustin Pop
    if vrfy:
603 ac4d25b6 Iustin Pop
      logging.error("Errors after conversion:")
604 ac4d25b6 Iustin Pop
      for item in vrfy:
605 07b8a2b5 Iustin Pop
        logging.error(" - %s", item)
606 fdb85e3d Bernardo Dal Seno
      all_ok = False
607 fdb85e3d Bernardo Dal Seno
    else:
608 fdb85e3d Bernardo Dal Seno
      logging.info("File loaded successfully after upgrading")
609 ac4d25b6 Iustin Pop
    del cfg
610 ac4d25b6 Iustin Pop
611 1709435e Bernardo Dal Seno
  if options.downgrade:
612 1709435e Bernardo Dal Seno
    action = "downgraded"
613 1709435e Bernardo Dal Seno
    out_ver = "%s.%s" % (DOWNGRADE_MAJOR, DOWNGRADE_MINOR)
614 1709435e Bernardo Dal Seno
  else:
615 1709435e Bernardo Dal Seno
    action = "upgraded"
616 1709435e Bernardo Dal Seno
    out_ver = constants.RELEASE_VERSION
617 fdb85e3d Bernardo Dal Seno
  if all_ok:
618 1709435e Bernardo Dal Seno
    cli.ToStderr("Configuration successfully %s to version %s.",
619 1709435e Bernardo Dal Seno
                 action, out_ver)
620 fdb85e3d Bernardo Dal Seno
  else:
621 1709435e Bernardo Dal Seno
    cli.ToStderr("Configuration %s to version %s, but there are errors."
622 1709435e Bernardo Dal Seno
                 "\nPlease review the file.", action, out_ver)
623 66a66fa7 Michael Hanselmann
624 6d691282 Michael Hanselmann
625 6d691282 Michael Hanselmann
if __name__ == "__main__":
626 6d691282 Michael Hanselmann
  main()