Statistics
| Branch: | Tag: | Revision:

root / tools / cluster-merge @ 9b945588

History | View | Annotate | Download (20.2 kB)

1 1f7d3f7d René Nussbaumer
#!/usr/bin/python
2 1f7d3f7d René Nussbaumer
#
3 1f7d3f7d René Nussbaumer
4 3d8f154f Stephen Shirley
# Copyright (C) 2010 Google Inc.
5 1f7d3f7d René Nussbaumer
#
6 1f7d3f7d René Nussbaumer
# This program is free software; you can redistribute it and/or modify
7 1f7d3f7d René Nussbaumer
# it under the terms of the GNU General Public License as published by
8 1f7d3f7d René Nussbaumer
# the Free Software Foundation; either version 2 of the License, or
9 1f7d3f7d René Nussbaumer
# (at your option) any later version.
10 1f7d3f7d René Nussbaumer
#
11 1f7d3f7d René Nussbaumer
# This program is distributed in the hope that it will be useful, but
12 1f7d3f7d René Nussbaumer
# WITHOUT ANY WARRANTY; without even the implied warranty of
13 1f7d3f7d René Nussbaumer
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 1f7d3f7d René Nussbaumer
# General Public License for more details.
15 1f7d3f7d René Nussbaumer
#
16 1f7d3f7d René Nussbaumer
# You should have received a copy of the GNU General Public License
17 1f7d3f7d René Nussbaumer
# along with this program; if not, write to the Free Software
18 1f7d3f7d René Nussbaumer
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 1f7d3f7d René Nussbaumer
# 02110-1301, USA.
20 1f7d3f7d René Nussbaumer
21 1f7d3f7d René Nussbaumer
"""Tool to merge two or more clusters together.
22 1f7d3f7d René Nussbaumer
23 1f7d3f7d René Nussbaumer
The clusters have to run the same version of Ganeti!
24 1f7d3f7d René Nussbaumer
25 1f7d3f7d René Nussbaumer
"""
26 1f7d3f7d René Nussbaumer
27 1f7d3f7d René Nussbaumer
# pylint: disable-msg=C0103
28 1f7d3f7d René Nussbaumer
# C0103: Invalid name cluster-merge
29 1f7d3f7d René Nussbaumer
30 1f7d3f7d René Nussbaumer
import logging
31 1f7d3f7d René Nussbaumer
import os
32 1f7d3f7d René Nussbaumer
import optparse
33 1f7d3f7d René Nussbaumer
import shutil
34 1f7d3f7d René Nussbaumer
import sys
35 1f7d3f7d René Nussbaumer
import tempfile
36 1f7d3f7d René Nussbaumer
37 1f7d3f7d René Nussbaumer
from ganeti import cli
38 1f7d3f7d René Nussbaumer
from ganeti import config
39 1f7d3f7d René Nussbaumer
from ganeti import constants
40 1f7d3f7d René Nussbaumer
from ganeti import errors
41 1f7d3f7d René Nussbaumer
from ganeti import ssh
42 1f7d3f7d René Nussbaumer
from ganeti import utils
43 1f7d3f7d René Nussbaumer
44 1f7d3f7d René Nussbaumer
45 1a615be0 Stephen Shirley
_GROUPS_MERGE = "merge"
46 1a615be0 Stephen Shirley
_GROUPS_RENAME = "rename"
47 1a615be0 Stephen Shirley
_CLUSTERMERGE_ECID = "clustermerge-ecid"
48 1a615be0 Stephen Shirley
49 1f7d3f7d René Nussbaumer
PAUSE_PERIOD_OPT = cli.cli_option("-p", "--watcher-pause-period", default=1800,
50 1f7d3f7d René Nussbaumer
                                  action="store", type="int",
51 1f7d3f7d René Nussbaumer
                                  dest="pause_period",
52 1f7d3f7d René Nussbaumer
                                  help=("Amount of time in seconds watcher"
53 1f7d3f7d René Nussbaumer
                                        " should be suspended from running"))
54 1a615be0 Stephen Shirley
GROUPS_OPT = cli.cli_option("--groups", default=None, metavar="STRATEGY",
55 9b945588 Stephen Shirley
                            choices=(_GROUPS_MERGE, _GROUPS_RENAME),
56 9b945588 Stephen Shirley
                            dest="groups",
57 1a615be0 Stephen Shirley
                            help=("How to handle groups that have the"
58 1a615be0 Stephen Shirley
                                  " same name (One of: %s/%s)" %
59 1a615be0 Stephen Shirley
                                  (_GROUPS_MERGE, _GROUPS_RENAME)))
60 1f7d3f7d René Nussbaumer
61 1f7d3f7d René Nussbaumer
62 71bbe910 Stephen Shirley
def Flatten(unflattened_list):
63 1f7d3f7d René Nussbaumer
  """Flattens a list.
64 1f7d3f7d René Nussbaumer
65 71bbe910 Stephen Shirley
  @param unflattened_list: A list of unflattened list objects.
66 71bbe910 Stephen Shirley
  @return: A flattened list
67 1f7d3f7d René Nussbaumer
68 1f7d3f7d René Nussbaumer
  """
69 71bbe910 Stephen Shirley
  flattened_list = []
70 1f7d3f7d René Nussbaumer
71 71bbe910 Stephen Shirley
  for item in unflattened_list:
72 1f7d3f7d René Nussbaumer
    if isinstance(item, list):
73 71bbe910 Stephen Shirley
      flattened_list.extend(Flatten(item))
74 1f7d3f7d René Nussbaumer
    else:
75 71bbe910 Stephen Shirley
      flattened_list.append(item)
76 71bbe910 Stephen Shirley
  return flattened_list
77 1f7d3f7d René Nussbaumer
78 1f7d3f7d René Nussbaumer
79 1f7d3f7d René Nussbaumer
class MergerData(object):
80 1f7d3f7d René Nussbaumer
  """Container class to hold data used for merger.
81 1f7d3f7d René Nussbaumer
82 1f7d3f7d René Nussbaumer
  """
83 1f7d3f7d René Nussbaumer
  def __init__(self, cluster, key_path, nodes, instances, config_path=None):
84 1f7d3f7d René Nussbaumer
    """Initialize the container.
85 1f7d3f7d René Nussbaumer
86 1f7d3f7d René Nussbaumer
    @param cluster: The name of the cluster
87 1f7d3f7d René Nussbaumer
    @param key_path: Path to the ssh private key used for authentication
88 1f7d3f7d René Nussbaumer
    @param nodes: List of nodes in the merging cluster
89 1f7d3f7d René Nussbaumer
    @param instances: List of instances running on merging cluster
90 3d2e7a27 Stephen Shirley
    @param config_path: Path to the merging cluster config
91 1f7d3f7d René Nussbaumer
92 1f7d3f7d René Nussbaumer
    """
93 1f7d3f7d René Nussbaumer
    self.cluster = cluster
94 1f7d3f7d René Nussbaumer
    self.key_path = key_path
95 1f7d3f7d René Nussbaumer
    self.nodes = nodes
96 3d2e7a27 Stephen Shirley
    self.instances = instances
97 3d2e7a27 Stephen Shirley
    self.config_path = config_path
98 1f7d3f7d René Nussbaumer
99 1f7d3f7d René Nussbaumer
100 1f7d3f7d René Nussbaumer
class Merger(object):
101 1f7d3f7d René Nussbaumer
  """Handling the merge.
102 1f7d3f7d René Nussbaumer
103 1f7d3f7d René Nussbaumer
  """
104 1a615be0 Stephen Shirley
  def __init__(self, clusters, pause_period, groups):
105 1f7d3f7d René Nussbaumer
    """Initialize object with sane defaults and infos required.
106 1f7d3f7d René Nussbaumer
107 1f7d3f7d René Nussbaumer
    @param clusters: The list of clusters to merge in
108 1f7d3f7d René Nussbaumer
    @param pause_period: The time watcher shall be disabled for
109 1a615be0 Stephen Shirley
    @param groups: How to handle group conflicts
110 1f7d3f7d René Nussbaumer
111 1f7d3f7d René Nussbaumer
    """
112 1f7d3f7d René Nussbaumer
    self.merger_data = []
113 1f7d3f7d René Nussbaumer
    self.clusters = clusters
114 1f7d3f7d René Nussbaumer
    self.pause_period = pause_period
115 1f7d3f7d René Nussbaumer
    self.work_dir = tempfile.mkdtemp(suffix="cluster-merger")
116 be8aecab Stephen Shirley
    (self.cluster_name, ) = cli.GetClient().QueryConfigValues(["cluster_name"])
117 1f7d3f7d René Nussbaumer
    self.ssh_runner = ssh.SshRunner(self.cluster_name)
118 1a615be0 Stephen Shirley
    self.groups = groups
119 1f7d3f7d René Nussbaumer
120 1f7d3f7d René Nussbaumer
  def Setup(self):
121 1f7d3f7d René Nussbaumer
    """Sets up our end so we can do the merger.
122 1f7d3f7d René Nussbaumer
123 1f7d3f7d René Nussbaumer
    This method is setting us up as a preparation for the merger.
124 1f7d3f7d René Nussbaumer
    It makes the initial contact and gathers information needed.
125 1f7d3f7d René Nussbaumer
126 1f7d3f7d René Nussbaumer
    @raise errors.RemoteError: for errors in communication/grabbing
127 1f7d3f7d René Nussbaumer
128 1f7d3f7d René Nussbaumer
    """
129 1f7d3f7d René Nussbaumer
    (remote_path, _, _) = ssh.GetUserFiles("root")
130 1f7d3f7d René Nussbaumer
131 be8aecab Stephen Shirley
    if self.cluster_name in self.clusters:
132 be8aecab Stephen Shirley
      raise errors.CommandError("Cannot merge cluster %s with itself" %
133 be8aecab Stephen Shirley
                                self.cluster_name)
134 be8aecab Stephen Shirley
135 1f7d3f7d René Nussbaumer
    # Fetch remotes private key
136 1f7d3f7d René Nussbaumer
    for cluster in self.clusters:
137 1f7d3f7d René Nussbaumer
      result = self._RunCmd(cluster, "cat %s" % remote_path, batch=False,
138 1f7d3f7d René Nussbaumer
                            ask_key=False)
139 1f7d3f7d René Nussbaumer
      if result.failed:
140 1f7d3f7d René Nussbaumer
        raise errors.RemoteError("There was an error while grabbing ssh private"
141 1f7d3f7d René Nussbaumer
                                 " key from %s. Fail reason: %s; output: %s" %
142 1f7d3f7d René Nussbaumer
                                 (cluster, result.fail_reason, result.output))
143 1f7d3f7d René Nussbaumer
144 c4feafe8 Iustin Pop
      key_path = utils.PathJoin(self.work_dir, cluster)
145 1f7d3f7d René Nussbaumer
      utils.WriteFile(key_path, mode=0600, data=result.stdout)
146 1f7d3f7d René Nussbaumer
147 1f7d3f7d René Nussbaumer
      result = self._RunCmd(cluster, "gnt-node list -o name --no-header",
148 1f7d3f7d René Nussbaumer
                            private_key=key_path)
149 1f7d3f7d René Nussbaumer
      if result.failed:
150 1f7d3f7d René Nussbaumer
        raise errors.RemoteError("Unable to retrieve list of nodes from %s."
151 1f7d3f7d René Nussbaumer
                                 " Fail reason: %s; output: %s" %
152 1f7d3f7d René Nussbaumer
                                 (cluster, result.fail_reason, result.output))
153 1f7d3f7d René Nussbaumer
      nodes = result.stdout.splitlines()
154 1f7d3f7d René Nussbaumer
155 1f7d3f7d René Nussbaumer
      result = self._RunCmd(cluster, "gnt-instance list -o name --no-header",
156 1f7d3f7d René Nussbaumer
                            private_key=key_path)
157 1f7d3f7d René Nussbaumer
      if result.failed:
158 1f7d3f7d René Nussbaumer
        raise errors.RemoteError("Unable to retrieve list of instances from"
159 1f7d3f7d René Nussbaumer
                                 " %s. Fail reason: %s; output: %s" %
160 1f7d3f7d René Nussbaumer
                                 (cluster, result.fail_reason, result.output))
161 1f7d3f7d René Nussbaumer
      instances = result.stdout.splitlines()
162 1f7d3f7d René Nussbaumer
163 1f7d3f7d René Nussbaumer
      self.merger_data.append(MergerData(cluster, key_path, nodes, instances))
164 1f7d3f7d René Nussbaumer
165 1f7d3f7d René Nussbaumer
  def _PrepareAuthorizedKeys(self):
166 1f7d3f7d René Nussbaumer
    """Prepare the authorized_keys on every merging node.
167 1f7d3f7d René Nussbaumer
168 1f7d3f7d René Nussbaumer
    This method add our public key to remotes authorized_key for further
169 1f7d3f7d René Nussbaumer
    communication.
170 1f7d3f7d René Nussbaumer
171 1f7d3f7d René Nussbaumer
    """
172 1f7d3f7d René Nussbaumer
    (_, pub_key_file, auth_keys) = ssh.GetUserFiles("root")
173 1f7d3f7d René Nussbaumer
    pub_key = utils.ReadFile(pub_key_file)
174 1f7d3f7d René Nussbaumer
175 1f7d3f7d René Nussbaumer
    for data in self.merger_data:
176 1f7d3f7d René Nussbaumer
      for node in data.nodes:
177 1f7d3f7d René Nussbaumer
        result = self._RunCmd(node, ("cat >> %s << '!EOF.'\n%s!EOF.\n" %
178 1f7d3f7d René Nussbaumer
                                     (auth_keys, pub_key)),
179 1f7d3f7d René Nussbaumer
                              private_key=data.key_path)
180 1f7d3f7d René Nussbaumer
181 1f7d3f7d René Nussbaumer
        if result.failed:
182 1f7d3f7d René Nussbaumer
          raise errors.RemoteError("Unable to add our public key to %s in %s."
183 1f7d3f7d René Nussbaumer
                                   " Fail reason: %s; output: %s" %
184 1f7d3f7d René Nussbaumer
                                   (node, data.cluster, result.fail_reason,
185 1f7d3f7d René Nussbaumer
                                    result.output))
186 1f7d3f7d René Nussbaumer
187 1f7d3f7d René Nussbaumer
  def _RunCmd(self, hostname, command, user="root", use_cluster_key=False,
188 1f7d3f7d René Nussbaumer
              strict_host_check=False, private_key=None, batch=True,
189 1f7d3f7d René Nussbaumer
              ask_key=False):
190 1f7d3f7d René Nussbaumer
    """Wrapping SshRunner.Run with default parameters.
191 1f7d3f7d René Nussbaumer
192 454723b5 Iustin Pop
    For explanation of parameters see L{ganeti.ssh.SshRunner.Run}.
193 1f7d3f7d René Nussbaumer
194 1f7d3f7d René Nussbaumer
    """
195 1f7d3f7d René Nussbaumer
    return self.ssh_runner.Run(hostname=hostname, command=command, user=user,
196 1f7d3f7d René Nussbaumer
                               use_cluster_key=use_cluster_key,
197 1f7d3f7d René Nussbaumer
                               strict_host_check=strict_host_check,
198 1f7d3f7d René Nussbaumer
                               private_key=private_key, batch=batch,
199 1f7d3f7d René Nussbaumer
                               ask_key=ask_key)
200 1f7d3f7d René Nussbaumer
201 1f7d3f7d René Nussbaumer
  def _StopMergingInstances(self):
202 1f7d3f7d René Nussbaumer
    """Stop instances on merging clusters.
203 1f7d3f7d René Nussbaumer
204 1f7d3f7d René Nussbaumer
    """
205 1f7d3f7d René Nussbaumer
    for cluster in self.clusters:
206 1f7d3f7d René Nussbaumer
      result = self._RunCmd(cluster, "gnt-instance shutdown --all"
207 1f7d3f7d René Nussbaumer
                                     " --force-multiple")
208 1f7d3f7d René Nussbaumer
209 1f7d3f7d René Nussbaumer
      if result.failed:
210 1f7d3f7d René Nussbaumer
        raise errors.RemoteError("Unable to stop instances on %s."
211 1f7d3f7d René Nussbaumer
                                 " Fail reason: %s; output: %s" %
212 1f7d3f7d René Nussbaumer
                                 (cluster, result.fail_reason, result.output))
213 1f7d3f7d René Nussbaumer
214 1f7d3f7d René Nussbaumer
  def _DisableWatcher(self):
215 1f7d3f7d René Nussbaumer
    """Disable watch on all merging clusters, including ourself.
216 1f7d3f7d René Nussbaumer
217 1f7d3f7d René Nussbaumer
    """
218 1f7d3f7d René Nussbaumer
    for cluster in ["localhost"] + self.clusters:
219 1f7d3f7d René Nussbaumer
      result = self._RunCmd(cluster, "gnt-cluster watcher pause %d" %
220 1f7d3f7d René Nussbaumer
                                     self.pause_period)
221 1f7d3f7d René Nussbaumer
222 1f7d3f7d René Nussbaumer
      if result.failed:
223 1f7d3f7d René Nussbaumer
        raise errors.RemoteError("Unable to pause watcher on %s."
224 1f7d3f7d René Nussbaumer
                                 " Fail reason: %s; output: %s" %
225 1f7d3f7d René Nussbaumer
                                 (cluster, result.fail_reason, result.output))
226 1f7d3f7d René Nussbaumer
227 1f7d3f7d René Nussbaumer
  def _StopDaemons(self):
228 1f7d3f7d René Nussbaumer
    """Stop all daemons on merging nodes.
229 1f7d3f7d René Nussbaumer
230 1f7d3f7d René Nussbaumer
    """
231 d8aab233 René Nussbaumer
    cmd = "%s stop-all" % constants.DAEMON_UTIL
232 1f7d3f7d René Nussbaumer
    for data in self.merger_data:
233 1f7d3f7d René Nussbaumer
      for node in data.nodes:
234 d8aab233 René Nussbaumer
        result = self._RunCmd(node, cmd)
235 1f7d3f7d René Nussbaumer
236 1f7d3f7d René Nussbaumer
        if result.failed:
237 1f7d3f7d René Nussbaumer
          raise errors.RemoteError("Unable to stop daemons on %s."
238 1f7d3f7d René Nussbaumer
                                   " Fail reason: %s; output: %s." %
239 1f7d3f7d René Nussbaumer
                                   (node, result.fail_reason, result.output))
240 1f7d3f7d René Nussbaumer
241 1f7d3f7d René Nussbaumer
  def _FetchRemoteConfig(self):
242 1f7d3f7d René Nussbaumer
    """Fetches and stores remote cluster config from the master.
243 1f7d3f7d René Nussbaumer
244 1f7d3f7d René Nussbaumer
    This step is needed before we can merge the config.
245 1f7d3f7d René Nussbaumer
246 1f7d3f7d René Nussbaumer
    """
247 1f7d3f7d René Nussbaumer
    for data in self.merger_data:
248 1f7d3f7d René Nussbaumer
      result = self._RunCmd(data.cluster, "cat %s" %
249 1f7d3f7d René Nussbaumer
                                          constants.CLUSTER_CONF_FILE)
250 1f7d3f7d René Nussbaumer
251 1f7d3f7d René Nussbaumer
      if result.failed:
252 1f7d3f7d René Nussbaumer
        raise errors.RemoteError("Unable to retrieve remote config on %s."
253 1f7d3f7d René Nussbaumer
                                 " Fail reason: %s; output %s" %
254 1f7d3f7d René Nussbaumer
                                 (data.cluster, result.fail_reason,
255 1f7d3f7d René Nussbaumer
                                  result.output))
256 1f7d3f7d René Nussbaumer
257 c4feafe8 Iustin Pop
      data.config_path = utils.PathJoin(self.work_dir, "%s_config.data" %
258 c4feafe8 Iustin Pop
                                        data.cluster)
259 1f7d3f7d René Nussbaumer
      utils.WriteFile(data.config_path, data=result.stdout)
260 1f7d3f7d René Nussbaumer
261 1f7d3f7d René Nussbaumer
  # R0201: Method could be a function
262 1f7d3f7d René Nussbaumer
  def _KillMasterDaemon(self): # pylint: disable-msg=R0201
263 1f7d3f7d René Nussbaumer
    """Kills the local master daemon.
264 1f7d3f7d René Nussbaumer
265 1f7d3f7d René Nussbaumer
    @raise errors.CommandError: If unable to kill
266 1f7d3f7d René Nussbaumer
267 1f7d3f7d René Nussbaumer
    """
268 1f7d3f7d René Nussbaumer
    result = utils.RunCmd([constants.DAEMON_UTIL, "stop-master"])
269 1f7d3f7d René Nussbaumer
    if result.failed:
270 1f7d3f7d René Nussbaumer
      raise errors.CommandError("Unable to stop master daemons."
271 1f7d3f7d René Nussbaumer
                                " Fail reason: %s; output: %s" %
272 1f7d3f7d René Nussbaumer
                                (result.fail_reason, result.output))
273 1f7d3f7d René Nussbaumer
274 1f7d3f7d René Nussbaumer
  def _MergeConfig(self):
275 1f7d3f7d René Nussbaumer
    """Merges all foreign config into our own config.
276 1f7d3f7d René Nussbaumer
277 1f7d3f7d René Nussbaumer
    """
278 1f7d3f7d René Nussbaumer
    my_config = config.ConfigWriter(offline=True)
279 1f7d3f7d René Nussbaumer
    fake_ec_id = 0 # Needs to be uniq over the whole config merge
280 1f7d3f7d René Nussbaumer
281 1f7d3f7d René Nussbaumer
    for data in self.merger_data:
282 e1ab08db Stephen Shirley
      other_config = config.ConfigWriter(data.config_path, accept_foreign=True)
283 8f44674f Stephen Shirley
      self._MergeNodeGroups(my_config, other_config)
284 1f7d3f7d René Nussbaumer
285 1f7d3f7d René Nussbaumer
      for node in other_config.GetNodeList():
286 1f7d3f7d René Nussbaumer
        node_info = other_config.GetNodeInfo(node)
287 a536aaac Stephen Shirley
        my_config.AddNode(node_info, _CLUSTERMERGE_ECID + str(fake_ec_id))
288 1f7d3f7d René Nussbaumer
        fake_ec_id += 1
289 1f7d3f7d René Nussbaumer
290 1f7d3f7d René Nussbaumer
      for instance in other_config.GetInstanceList():
291 1f7d3f7d René Nussbaumer
        instance_info = other_config.GetInstanceInfo(instance)
292 1f7d3f7d René Nussbaumer
293 1f7d3f7d René Nussbaumer
        # Update the DRBD port assignments
294 1f7d3f7d René Nussbaumer
        # This is a little bit hackish
295 1f7d3f7d René Nussbaumer
        for dsk in instance_info.disks:
296 1f7d3f7d René Nussbaumer
          if dsk.dev_type in constants.LDS_DRBD:
297 1f7d3f7d René Nussbaumer
            port = my_config.AllocatePort()
298 1f7d3f7d René Nussbaumer
299 1f7d3f7d René Nussbaumer
            logical_id = list(dsk.logical_id)
300 1f7d3f7d René Nussbaumer
            logical_id[2] = port
301 1f7d3f7d René Nussbaumer
            dsk.logical_id = tuple(logical_id)
302 1f7d3f7d René Nussbaumer
303 1f7d3f7d René Nussbaumer
            physical_id = list(dsk.physical_id)
304 1f7d3f7d René Nussbaumer
            physical_id[1] = physical_id[3] = port
305 1f7d3f7d René Nussbaumer
            dsk.physical_id = tuple(physical_id)
306 1f7d3f7d René Nussbaumer
307 a536aaac Stephen Shirley
        my_config.AddInstance(instance_info,
308 a536aaac Stephen Shirley
                              _CLUSTERMERGE_ECID + str(fake_ec_id))
309 1f7d3f7d René Nussbaumer
        fake_ec_id += 1
310 1f7d3f7d René Nussbaumer
311 1f7d3f7d René Nussbaumer
  # R0201: Method could be a function
312 8f44674f Stephen Shirley
  def _MergeNodeGroups(self, my_config, other_config):
313 8f44674f Stephen Shirley
    """Adds foreign node groups
314 8f44674f Stephen Shirley
315 8f44674f Stephen Shirley
    ConfigWriter.AddNodeGroup takes care of making sure there are no conflicts.
316 8f44674f Stephen Shirley
    """
317 8f44674f Stephen Shirley
    # pylint: disable-msg=R0201
318 9b945588 Stephen Shirley
    logging.info("Node group conflict strategy: %s", self.groups)
319 1a615be0 Stephen Shirley
320 1a615be0 Stephen Shirley
    my_grps = my_config.GetAllNodeGroupsInfo().values()
321 1a615be0 Stephen Shirley
    other_grps = other_config.GetAllNodeGroupsInfo().values()
322 1a615be0 Stephen Shirley
323 1a615be0 Stephen Shirley
    # Check for node group naming conflicts:
324 1a615be0 Stephen Shirley
    conflicts = []
325 1a615be0 Stephen Shirley
    for other_grp in other_grps:
326 1a615be0 Stephen Shirley
      for my_grp in my_grps:
327 1a615be0 Stephen Shirley
        if other_grp.name == my_grp.name:
328 1a615be0 Stephen Shirley
          conflicts.append(other_grp)
329 1a615be0 Stephen Shirley
330 1a615be0 Stephen Shirley
    if conflicts:
331 1a615be0 Stephen Shirley
      conflict_names = utils.CommaJoin([g.name for g in conflicts])
332 9b945588 Stephen Shirley
      logging.info("Node groups in both local and remote cluster: %s",
333 1a615be0 Stephen Shirley
                   conflict_names)
334 1a615be0 Stephen Shirley
335 1a615be0 Stephen Shirley
      # User hasn't specified how to handle conflicts
336 1a615be0 Stephen Shirley
      if not self.groups:
337 1a615be0 Stephen Shirley
        raise errors.CommandError("The following node group(s) are in both"
338 1a615be0 Stephen Shirley
                                  " clusters, and no merge strategy has been"
339 1a615be0 Stephen Shirley
                                  " supplied (see the --groups option): %s" %
340 1a615be0 Stephen Shirley
                                  conflict_names)
341 1a615be0 Stephen Shirley
342 1a615be0 Stephen Shirley
      # User wants to rename conflicts
343 3a969900 Stephen Shirley
      elif self.groups == _GROUPS_RENAME:
344 1a615be0 Stephen Shirley
        for grp in conflicts:
345 1a615be0 Stephen Shirley
          new_name = "%s-%s" % (grp.name, other_config.GetClusterName())
346 1a615be0 Stephen Shirley
          logging.info("Renaming remote node group from %s to %s"
347 9b945588 Stephen Shirley
                       " to resolve conflict", grp.name, new_name)
348 1a615be0 Stephen Shirley
          grp.name = new_name
349 1a615be0 Stephen Shirley
350 3a969900 Stephen Shirley
      # User wants to merge conflicting groups
351 3a969900 Stephen Shirley
      elif self.groups == 'merge':
352 3a969900 Stephen Shirley
        for other_grp in conflicts:
353 9b945588 Stephen Shirley
          logging.info("Merging local and remote '%s' groups", other_grp.name)
354 3a969900 Stephen Shirley
          for node_name in other_grp.members[:]:
355 3a969900 Stephen Shirley
            node = other_config.GetNodeInfo(node_name)
356 9b945588 Stephen Shirley
            # Access to a protected member of a client class
357 9b945588 Stephen Shirley
            # pylint: disable-msg=W0212
358 3a969900 Stephen Shirley
            other_config._UnlockedRemoveNodeFromGroup(node)
359 3a969900 Stephen Shirley
360 9b945588 Stephen Shirley
            # Access to a protected member of a client class
361 9b945588 Stephen Shirley
            # pylint: disable-msg=W0212
362 3a969900 Stephen Shirley
            my_grp_uuid = my_config._UnlockedLookupNodeGroup(other_grp.name)
363 9b945588 Stephen Shirley
364 9b945588 Stephen Shirley
            # Access to a protected member of a client class
365 9b945588 Stephen Shirley
            # pylint: disable-msg=W0212
366 3a969900 Stephen Shirley
            my_config._UnlockedAddNodeToGroup(node, my_grp_uuid)
367 3a969900 Stephen Shirley
            node.group = my_grp_uuid
368 3a969900 Stephen Shirley
          # Remove from list of groups to add
369 3a969900 Stephen Shirley
          other_grps.remove(other_grp)
370 3a969900 Stephen Shirley
371 1a615be0 Stephen Shirley
    for grp in other_grps:
372 8f44674f Stephen Shirley
      #TODO: handle node group conflicts
373 8f44674f Stephen Shirley
      my_config.AddNodeGroup(grp, _CLUSTERMERGE_ECID)
374 8f44674f Stephen Shirley
375 8f44674f Stephen Shirley
  # R0201: Method could be a function
376 1f7d3f7d René Nussbaumer
  def _StartMasterDaemon(self, no_vote=False): # pylint: disable-msg=R0201
377 1f7d3f7d René Nussbaumer
    """Starts the local master daemon.
378 1f7d3f7d René Nussbaumer
379 1f7d3f7d René Nussbaumer
    @param no_vote: Should the masterd started without voting? default: False
380 1f7d3f7d René Nussbaumer
    @raise errors.CommandError: If unable to start daemon.
381 1f7d3f7d René Nussbaumer
382 1f7d3f7d René Nussbaumer
    """
383 1f7d3f7d René Nussbaumer
    env = {}
384 1f7d3f7d René Nussbaumer
    if no_vote:
385 1f7d3f7d René Nussbaumer
      env["EXTRA_MASTERD_ARGS"] = "--no-voting --yes-do-it"
386 1f7d3f7d René Nussbaumer
387 1f7d3f7d René Nussbaumer
    result = utils.RunCmd([constants.DAEMON_UTIL, "start-master"], env=env)
388 1f7d3f7d René Nussbaumer
    if result.failed:
389 1f7d3f7d René Nussbaumer
      raise errors.CommandError("Couldn't start ganeti master."
390 1f7d3f7d René Nussbaumer
                                " Fail reason: %s; output: %s" %
391 1f7d3f7d René Nussbaumer
                                (result.fail_reason, result.output))
392 1f7d3f7d René Nussbaumer
393 1f7d3f7d René Nussbaumer
  def _ReaddMergedNodesAndRedist(self):
394 1f7d3f7d René Nussbaumer
    """Readds all merging nodes and make sure their config is up-to-date.
395 1f7d3f7d René Nussbaumer
396 1f7d3f7d René Nussbaumer
    @raise errors.CommandError: If anything fails.
397 1f7d3f7d René Nussbaumer
398 1f7d3f7d René Nussbaumer
    """
399 1f7d3f7d René Nussbaumer
    for data in self.merger_data:
400 1f7d3f7d René Nussbaumer
      for node in data.nodes:
401 1f7d3f7d René Nussbaumer
        result = utils.RunCmd(["gnt-node", "add", "--readd",
402 53991408 Stephen Shirley
                               "--no-ssh-key-check", "--force-join", node])
403 1f7d3f7d René Nussbaumer
        if result.failed:
404 1f7d3f7d René Nussbaumer
          raise errors.CommandError("Couldn't readd node %s. Fail reason: %s;"
405 1f7d3f7d René Nussbaumer
                                    " output: %s" % (node, result.fail_reason,
406 1f7d3f7d René Nussbaumer
                                                     result.output))
407 1f7d3f7d René Nussbaumer
408 1f7d3f7d René Nussbaumer
    result = utils.RunCmd(["gnt-cluster", "redist-conf"])
409 1f7d3f7d René Nussbaumer
    if result.failed:
410 1f7d3f7d René Nussbaumer
      raise errors.CommandError("Redistribution failed. Fail reason: %s;"
411 1f7d3f7d René Nussbaumer
                                " output: %s" % (result.fail_reason,
412 1f7d3f7d René Nussbaumer
                                                result.output))
413 1f7d3f7d René Nussbaumer
414 1f7d3f7d René Nussbaumer
  # R0201: Method could be a function
415 1f7d3f7d René Nussbaumer
  def _StartupAllInstances(self): # pylint: disable-msg=R0201
416 1f7d3f7d René Nussbaumer
    """Starts up all instances (locally).
417 1f7d3f7d René Nussbaumer
418 1f7d3f7d René Nussbaumer
    @raise errors.CommandError: If unable to start clusters
419 1f7d3f7d René Nussbaumer
420 1f7d3f7d René Nussbaumer
    """
421 1f7d3f7d René Nussbaumer
    result = utils.RunCmd(["gnt-instance", "startup", "--all",
422 1f7d3f7d René Nussbaumer
                           "--force-multiple"])
423 1f7d3f7d René Nussbaumer
    if result.failed:
424 1f7d3f7d René Nussbaumer
      raise errors.CommandError("Unable to start all instances."
425 1f7d3f7d René Nussbaumer
                                " Fail reason: %s; output: %s" %
426 1f7d3f7d René Nussbaumer
                                (result.fail_reason, result.output))
427 1f7d3f7d René Nussbaumer
428 1f7d3f7d René Nussbaumer
  # R0201: Method could be a function
429 1f7d3f7d René Nussbaumer
  def _VerifyCluster(self): # pylint: disable-msg=R0201
430 1f7d3f7d René Nussbaumer
    """Runs gnt-cluster verify to verify the health.
431 1f7d3f7d René Nussbaumer
432 1f7d3f7d René Nussbaumer
    @raise errors.ProgrammError: If cluster fails on verification
433 1f7d3f7d René Nussbaumer
434 1f7d3f7d René Nussbaumer
    """
435 1f7d3f7d René Nussbaumer
    result = utils.RunCmd(["gnt-cluster", "verify"])
436 1f7d3f7d René Nussbaumer
    if result.failed:
437 1f7d3f7d René Nussbaumer
      raise errors.CommandError("Verification of cluster failed."
438 1f7d3f7d René Nussbaumer
                                " Fail reason: %s; output: %s" %
439 1f7d3f7d René Nussbaumer
                                (result.fail_reason, result.output))
440 1f7d3f7d René Nussbaumer
441 1f7d3f7d René Nussbaumer
  def Merge(self):
442 1f7d3f7d René Nussbaumer
    """Does the actual merge.
443 1f7d3f7d René Nussbaumer
444 1f7d3f7d René Nussbaumer
    It runs all the steps in the right order and updates the user about steps
445 1f7d3f7d René Nussbaumer
    taken. Also it keeps track of rollback_steps to undo everything.
446 1f7d3f7d René Nussbaumer
447 1f7d3f7d René Nussbaumer
    """
448 1f7d3f7d René Nussbaumer
    rbsteps = []
449 1f7d3f7d René Nussbaumer
    try:
450 1f7d3f7d René Nussbaumer
      logging.info("Pre cluster verification")
451 1f7d3f7d René Nussbaumer
      self._VerifyCluster()
452 1f7d3f7d René Nussbaumer
453 1f7d3f7d René Nussbaumer
      logging.info("Prepare authorized_keys")
454 1f7d3f7d René Nussbaumer
      rbsteps.append("Remove our key from authorized_keys on nodes:"
455 1f7d3f7d René Nussbaumer
                     " %(nodes)s")
456 1f7d3f7d René Nussbaumer
      self._PrepareAuthorizedKeys()
457 1f7d3f7d René Nussbaumer
458 1f7d3f7d René Nussbaumer
      rbsteps.append("Start all instances again on the merging"
459 1f7d3f7d René Nussbaumer
                     " clusters: %(clusters)s")
460 1f7d3f7d René Nussbaumer
      logging.info("Stopping merging instances (takes a while)")
461 1f7d3f7d René Nussbaumer
      self._StopMergingInstances()
462 1f7d3f7d René Nussbaumer
463 1f7d3f7d René Nussbaumer
      logging.info("Disable watcher")
464 1f7d3f7d René Nussbaumer
      self._DisableWatcher()
465 1f7d3f7d René Nussbaumer
      logging.info("Stop daemons on merging nodes")
466 1f7d3f7d René Nussbaumer
      self._StopDaemons()
467 1f7d3f7d René Nussbaumer
      logging.info("Merging config")
468 1f7d3f7d René Nussbaumer
      self._FetchRemoteConfig()
469 d8aab233 René Nussbaumer
470 9c03a67a Stephen Shirley
      logging.info("Stopping master daemon")
471 9c03a67a Stephen Shirley
      self._KillMasterDaemon()
472 9c03a67a Stephen Shirley
473 9c03a67a Stephen Shirley
      rbsteps.append("Restore %s from another master candidate"
474 9c03a67a Stephen Shirley
                     " and restart master daemon" %
475 9c03a67a Stephen Shirley
                     constants.CLUSTER_CONF_FILE)
476 9c03a67a Stephen Shirley
      self._MergeConfig()
477 9c03a67a Stephen Shirley
      self._StartMasterDaemon(no_vote=True)
478 9c03a67a Stephen Shirley
479 9c03a67a Stephen Shirley
      # Point of no return, delete rbsteps
480 9c03a67a Stephen Shirley
      del rbsteps[:]
481 9c03a67a Stephen Shirley
482 9c03a67a Stephen Shirley
      logging.warning("We are at the point of no return. Merge can not easily"
483 9c03a67a Stephen Shirley
                      " be undone after this point.")
484 9c03a67a Stephen Shirley
      logging.info("Readd nodes")
485 9c03a67a Stephen Shirley
      self._ReaddMergedNodesAndRedist()
486 9c03a67a Stephen Shirley
487 9c03a67a Stephen Shirley
      logging.info("Merge done, restart master daemon normally")
488 9c03a67a Stephen Shirley
      self._KillMasterDaemon()
489 9c03a67a Stephen Shirley
      self._StartMasterDaemon()
490 d8aab233 René Nussbaumer
491 1f7d3f7d René Nussbaumer
      logging.info("Starting instances again")
492 1f7d3f7d René Nussbaumer
      self._StartupAllInstances()
493 1f7d3f7d René Nussbaumer
      logging.info("Post cluster verification")
494 1f7d3f7d René Nussbaumer
      self._VerifyCluster()
495 1f7d3f7d René Nussbaumer
    except errors.GenericError, e:
496 1f7d3f7d René Nussbaumer
      logging.exception(e)
497 1f7d3f7d René Nussbaumer
498 1f7d3f7d René Nussbaumer
      if rbsteps:
499 1f7d3f7d René Nussbaumer
        nodes = Flatten([data.nodes for data in self.merger_data])
500 1f7d3f7d René Nussbaumer
        info = {
501 1f7d3f7d René Nussbaumer
          "clusters": self.clusters,
502 1f7d3f7d René Nussbaumer
          "nodes": nodes,
503 1f7d3f7d René Nussbaumer
          }
504 1f7d3f7d René Nussbaumer
        logging.critical("In order to rollback do the following:")
505 1f7d3f7d René Nussbaumer
        for step in rbsteps:
506 3f1cf151 René Nussbaumer
          logging.critical("  * %s", step % info)
507 1f7d3f7d René Nussbaumer
      else:
508 1f7d3f7d René Nussbaumer
        logging.critical("Nothing to rollback.")
509 1f7d3f7d René Nussbaumer
510 1f7d3f7d René Nussbaumer
      # TODO: Keep track of steps done for a flawless resume?
511 1f7d3f7d René Nussbaumer
512 1f7d3f7d René Nussbaumer
  def Cleanup(self):
513 1f7d3f7d René Nussbaumer
    """Clean up our environment.
514 1f7d3f7d René Nussbaumer
515 1f7d3f7d René Nussbaumer
    This cleans up remote private keys and configs and after that
516 1f7d3f7d René Nussbaumer
    deletes the temporary directory.
517 1f7d3f7d René Nussbaumer
518 1f7d3f7d René Nussbaumer
    """
519 1f7d3f7d René Nussbaumer
    shutil.rmtree(self.work_dir)
520 1f7d3f7d René Nussbaumer
521 1f7d3f7d René Nussbaumer
522 1f7d3f7d René Nussbaumer
def SetupLogging(options):
523 1f7d3f7d René Nussbaumer
  """Setting up logging infrastructure.
524 1f7d3f7d René Nussbaumer
525 1f7d3f7d René Nussbaumer
  @param options: Parsed command line options
526 1f7d3f7d René Nussbaumer
527 1f7d3f7d René Nussbaumer
  """
528 1f7d3f7d René Nussbaumer
  formatter = logging.Formatter("%(asctime)s: %(levelname)s %(message)s")
529 1f7d3f7d René Nussbaumer
530 1f7d3f7d René Nussbaumer
  stderr_handler = logging.StreamHandler()
531 1f7d3f7d René Nussbaumer
  stderr_handler.setFormatter(formatter)
532 1f7d3f7d René Nussbaumer
  if options.debug:
533 1f7d3f7d René Nussbaumer
    stderr_handler.setLevel(logging.NOTSET)
534 1f7d3f7d René Nussbaumer
  elif options.verbose:
535 1f7d3f7d René Nussbaumer
    stderr_handler.setLevel(logging.INFO)
536 1f7d3f7d René Nussbaumer
  else:
537 1f7d3f7d René Nussbaumer
    stderr_handler.setLevel(logging.ERROR)
538 1f7d3f7d René Nussbaumer
539 1f7d3f7d René Nussbaumer
  root_logger = logging.getLogger("")
540 1f7d3f7d René Nussbaumer
  root_logger.setLevel(logging.NOTSET)
541 1f7d3f7d René Nussbaumer
  root_logger.addHandler(stderr_handler)
542 1f7d3f7d René Nussbaumer
543 1f7d3f7d René Nussbaumer
544 1f7d3f7d René Nussbaumer
def main():
545 1f7d3f7d René Nussbaumer
  """Main routine.
546 1f7d3f7d René Nussbaumer
547 1f7d3f7d René Nussbaumer
  """
548 1f7d3f7d René Nussbaumer
  program = os.path.basename(sys.argv[0])
549 1f7d3f7d René Nussbaumer
550 1a615be0 Stephen Shirley
  parser = optparse.OptionParser(usage=("%%prog [--debug|--verbose]"
551 1f7d3f7d René Nussbaumer
                                        " [--watcher-pause-period SECONDS]"
552 1a615be0 Stephen Shirley
                                        " [--groups [%s|%s]]"
553 1a615be0 Stephen Shirley
                                        " <cluster> [<cluster...>]" %
554 1a615be0 Stephen Shirley
                                        (_GROUPS_MERGE, _GROUPS_RENAME)),
555 1f7d3f7d René Nussbaumer
                                        prog=program)
556 1f7d3f7d René Nussbaumer
  parser.add_option(cli.DEBUG_OPT)
557 1f7d3f7d René Nussbaumer
  parser.add_option(cli.VERBOSE_OPT)
558 1f7d3f7d René Nussbaumer
  parser.add_option(PAUSE_PERIOD_OPT)
559 1a615be0 Stephen Shirley
  parser.add_option(GROUPS_OPT)
560 1f7d3f7d René Nussbaumer
561 1f7d3f7d René Nussbaumer
  (options, args) = parser.parse_args()
562 1f7d3f7d René Nussbaumer
563 1f7d3f7d René Nussbaumer
  SetupLogging(options)
564 1f7d3f7d René Nussbaumer
565 1f7d3f7d René Nussbaumer
  if not args:
566 1f7d3f7d René Nussbaumer
    parser.error("No clusters specified")
567 1f7d3f7d René Nussbaumer
568 1a615be0 Stephen Shirley
  cluster_merger = Merger(utils.UniqueSequence(args), options.pause_period,
569 1a615be0 Stephen Shirley
                          options.groups)
570 1f7d3f7d René Nussbaumer
  try:
571 1f7d3f7d René Nussbaumer
    try:
572 1f7d3f7d René Nussbaumer
      cluster_merger.Setup()
573 1f7d3f7d René Nussbaumer
      cluster_merger.Merge()
574 1f7d3f7d René Nussbaumer
    except errors.GenericError, e:
575 1f7d3f7d René Nussbaumer
      logging.exception(e)
576 1f7d3f7d René Nussbaumer
      return constants.EXIT_FAILURE
577 1f7d3f7d René Nussbaumer
  finally:
578 1f7d3f7d René Nussbaumer
    cluster_merger.Cleanup()
579 1f7d3f7d René Nussbaumer
580 1f7d3f7d René Nussbaumer
  return constants.EXIT_SUCCESS
581 1f7d3f7d René Nussbaumer
582 1f7d3f7d René Nussbaumer
583 1f7d3f7d René Nussbaumer
if __name__ == "__main__":
584 1f7d3f7d René Nussbaumer
  sys.exit(main())