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