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