3d3caeba7fa8578ff446d8e1c5ec0fa37fc05d44
[ganeti-local] / lib / hypervisor / hv_chroot.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008, 2009 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 """Chroot manager hypervisor
23
24 """
25
26 import os
27 import os.path
28 import time
29 import logging
30
31 from ganeti import constants
32 from ganeti import errors # pylint: disable=W0611
33 from ganeti import utils
34 from ganeti import objects
35 from ganeti.hypervisor import hv_base
36 from ganeti.errors import HypervisorError
37
38
39 class ChrootManager(hv_base.BaseHypervisor):
40   """Chroot manager.
41
42   This not-really hypervisor allows ganeti to manage chroots. It has
43   special behaviour and requirements on the OS definition and the node
44   environemnt:
45     - the start and stop of the chroot environment are done via a
46       script called ganeti-chroot located in the root directory of the
47       first drive, which should be created by the OS definition
48     - this script must accept the start and stop argument and, on
49       shutdown, it should cleanly shutdown the daemons/processes
50       using the chroot
51     - the daemons run in chroot should only bind to the instance IP
52       (to which the OS create script has access via the instance name)
53     - since some daemons in the node could be listening on the wildcard
54       address, some ports might be unavailable
55     - the instance listing will show no memory usage
56     - on shutdown, the chroot manager will try to find all mountpoints
57       under the root dir of the instance and unmount them
58     - instance alive check is based on whether any process is using the chroot
59
60   """
61   _ROOT_DIR = constants.RUN_GANETI_DIR + "/chroot-hypervisor"
62
63   PARAMETERS = {
64     constants.HV_INIT_SCRIPT: (True, utils.IsNormAbsPath,
65                                "must be an absolute normalized path",
66                                None, None),
67     }
68
69   def __init__(self):
70     hv_base.BaseHypervisor.__init__(self)
71     utils.EnsureDirs([(self._ROOT_DIR, constants.RUN_DIRS_MODE)])
72
73   @staticmethod
74   def _IsDirLive(path):
75     """Check if a directory looks like a live chroot.
76
77     """
78     if not os.path.ismount(path):
79       return False
80     result = utils.RunCmd(["fuser", "-m", path])
81     return not result.failed
82
83   @staticmethod
84   def _GetMountSubdirs(path):
85     """Return the list of mountpoints under a given path.
86
87     """
88     result = []
89     for _, mountpoint, _, _ in utils.GetMounts():
90       if (mountpoint.startswith(path) and
91           mountpoint != path):
92         result.append(mountpoint)
93
94     result.sort(key=lambda x: x.count("/"), reverse=True)
95     return result
96
97   @classmethod
98   def _InstanceDir(cls, instance_name):
99     """Return the root directory for an instance.
100
101     """
102     return utils.PathJoin(cls._ROOT_DIR, instance_name)
103
104   def ListInstances(self):
105     """Get the list of running instances.
106
107     """
108     return [name for name in os.listdir(self._ROOT_DIR)
109             if self._IsDirLive(utils.PathJoin(self._ROOT_DIR, name))]
110
111   def GetInstanceInfo(self, instance_name):
112     """Get instance properties.
113
114     @type instance_name: string
115     @param instance_name: the instance name
116
117     @return: (name, id, memory, vcpus, stat, times)
118
119     """
120     dir_name = self._InstanceDir(instance_name)
121     if not self._IsDirLive(dir_name):
122       raise HypervisorError("Instance %s is not running" % instance_name)
123     return (instance_name, 0, 0, 0, 0, 0)
124
125   def GetAllInstancesInfo(self):
126     """Get properties of all instances.
127
128     @return: [(name, id, memory, vcpus, stat, times),...]
129
130     """
131     data = []
132     for file_name in os.listdir(self._ROOT_DIR):
133       path = utils.PathJoin(self._ROOT_DIR, file_name)
134       if self._IsDirLive(path):
135         data.append((file_name, 0, 0, 0, 0, 0))
136     return data
137
138   def StartInstance(self, instance, block_devices, startup_paused):
139     """Start an instance.
140
141     For the chroot manager, we try to mount the block device and
142     execute '/ganeti-chroot start'.
143
144     """
145     root_dir = self._InstanceDir(instance.name)
146     if not os.path.exists(root_dir):
147       try:
148         os.mkdir(root_dir)
149       except IOError, err:
150         raise HypervisorError("Failed to start instance %s: %s" %
151                               (instance.name, err))
152       if not os.path.isdir(root_dir):
153         raise HypervisorError("Needed path %s is not a directory" % root_dir)
154
155     if not os.path.ismount(root_dir):
156       if not block_devices:
157         raise HypervisorError("The chroot manager needs at least one disk")
158
159       sda_dev_path = block_devices[0][1]
160       result = utils.RunCmd(["mount", sda_dev_path, root_dir])
161       if result.failed:
162         raise HypervisorError("Can't mount the chroot dir: %s" % result.output)
163     init_script = instance.hvparams[constants.HV_INIT_SCRIPT]
164     result = utils.RunCmd(["chroot", root_dir, init_script, "start"])
165     if result.failed:
166       raise HypervisorError("Can't run the chroot start script: %s" %
167                             result.output)
168
169   def StopInstance(self, instance, force=False, retry=False, name=None):
170     """Stop an instance.
171
172     This method has complicated cleanup tests, as we must:
173       - try to kill all leftover processes
174       - try to unmount any additional sub-mountpoints
175       - finally unmount the instance dir
176
177     """
178     if name is None:
179       name = instance.name
180
181     root_dir = self._InstanceDir(name)
182     if not os.path.exists(root_dir) or not self._IsDirLive(root_dir):
183       return
184
185     # Run the chroot stop script only once
186     if not retry and not force:
187       result = utils.RunCmd(["chroot", root_dir, "/ganeti-chroot", "stop"])
188       if result.failed:
189         raise HypervisorError("Can't run the chroot stop script: %s" %
190                               result.output)
191
192     if not force:
193       utils.RunCmd(["fuser", "-k", "-TERM", "-m", root_dir])
194     else:
195       utils.RunCmd(["fuser", "-k", "-KILL", "-m", root_dir])
196       # 2 seconds at most should be enough for KILL to take action
197       time.sleep(2)
198
199     if self._IsDirLive(root_dir):
200       if force:
201         raise HypervisorError("Can't stop the processes using the chroot")
202       return
203
204   def CleanupInstance(self, instance_name):
205     """Cleanup after a stopped instance
206
207     """
208     root_dir = self._InstanceDir(instance_name)
209
210     if not os.path.exists(root_dir):
211       return
212
213     if self._IsDirLive(root_dir):
214       raise HypervisorError("Processes are still using the chroot")
215
216     for mpath in self._GetMountSubdirs(root_dir):
217       utils.RunCmd(["umount", mpath])
218
219     result = utils.RunCmd(["umount", root_dir])
220     if result.failed:
221       msg = ("Processes still alive in the chroot: %s" %
222              utils.RunCmd("fuser -vm %s" % root_dir).output)
223       logging.error(msg)
224       raise HypervisorError("Can't umount the chroot dir: %s (%s)" %
225                             (result.output, msg))
226
227   def RebootInstance(self, instance):
228     """Reboot an instance.
229
230     This is not (yet) implemented for the chroot manager.
231
232     """
233     raise HypervisorError("The chroot manager doesn't implement the"
234                           " reboot functionality")
235
236   def GetNodeInfo(self):
237     """Return information about the node.
238
239     This is just a wrapper over the base GetLinuxNodeInfo method.
240
241     @return: a dict with the following keys (values in MiB):
242           - memory_total: the total memory size on the node
243           - memory_free: the available memory on the node for instances
244           - memory_dom0: the memory used by the node itself, if available
245
246     """
247     return self.GetLinuxNodeInfo()
248
249   @classmethod
250   def GetInstanceConsole(cls, instance, # pylint: disable=W0221
251                          hvparams, beparams, root_dir=None):
252     """Return information for connecting to the console of an instance.
253
254     """
255     if root_dir is None:
256       root_dir = cls._InstanceDir(instance.name)
257       if not os.path.ismount(root_dir):
258         raise HypervisorError("Instance %s is not running" % instance.name)
259
260     return objects.InstanceConsole(instance=instance.name,
261                                    kind=constants.CONS_SSH,
262                                    host=instance.primary_node,
263                                    user=constants.GANETI_RUNAS,
264                                    command=["chroot", root_dir])
265
266   def Verify(self):
267     """Verify the hypervisor.
268
269     For the chroot manager, it just checks the existence of the base dir.
270
271     """
272     if not os.path.exists(self._ROOT_DIR):
273       return "The required directory '%s' does not exist." % self._ROOT_DIR
274
275   @classmethod
276   def PowercycleNode(cls):
277     """Chroot powercycle, just a wrapper over Linux powercycle.
278
279     """
280     cls.LinuxPowercycle()
281
282   def MigrateInstance(self, instance, target, live):
283     """Migrate an instance.
284
285     @type instance: L{objects.Instance}
286     @param instance: the instance to be migrated
287     @type target: string
288     @param target: hostname (usually ip) of the target node
289     @type live: boolean
290     @param live: whether to do a live or non-live migration
291
292     """
293     raise HypervisorError("Migration not supported by the chroot hypervisor")