Statistics
| Branch: | Tag: | Revision:

root / lib / hypervisor / hv_lxc.py @ 4b5e40a5

History | View | Annotate | Download (10.8 kB)

1
#
2
#
3

    
4
# Copyright (C) 2010 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
"""LXC 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 LXCHypervisor(hv_base.BaseHypervisor):
39
  """LXC-based virtualization.
40

41
  Since current (Spring 2010) distributions are not yet ready for
42
  running under a container, the following changes must be done
43
  manually:
44
    - remove udev
45
    - disable the kernel log component of sysklogd/rsyslog/etc.,
46
      otherwise they will fail to read the log, and at least rsyslog
47
      will fill the filesystem with error messages
48

49
  TODO:
50
    - move hardcoded parameters into hypervisor parameters, once we
51
      have the container-parameter support
52
    - implement memory limits, but only optionally, depending on host
53
      kernel support
54

55
  Problems/issues:
56
    - LXC is very temperamental; in daemon mode, it succeeds or fails
57
      in launching the instance silently, without any error
58
      indication, and when failing it can leave network interfaces
59
      around, and future successful startups will list the instance
60
      twice
61
    - shutdown sequence of containers leaves the init 'dead', and the
62
      container effectively stopped, but LXC still believes the
63
      container to be running; need to investigate using the
64
      notify_on_release and release_agent feature of cgroups
65

66
  """
67
  _ROOT_DIR = constants.RUN_GANETI_DIR + "/lxc"
68
  _LOG_FILE = constants.LOG_DIR + "hv_lxc.log"
69
  _DEVS = [
70
    "c 1:3",   # /dev/null
71
    "c 1:5",   # /dev/zero
72
    "c 1:7",   # /dev/full
73
    "c 1:8",   # /dev/random
74
    "c 1:9",   # /dev/urandom
75
    "c 1:10",  # /dev/aio
76
    "c 5:0",   # /dev/tty
77
    "c 5:1",   # /dev/console
78
    "c 5:2",   # /dev/ptmx
79
    "c 136:*", # first block of Unix98 PTY slaves
80
    ]
81
  _DENIED_CAPABILITIES = [
82
    "mac_override",    # Allow MAC configuration or state changes
83
    # TODO: remove sys_admin too, for safety
84
    #"sys_admin",       # Perform  a range of system administration operations
85
    "sys_boot",        # Use reboot(2) and kexec_load(2)
86
    "sys_module",      # Load  and  unload kernel modules
87
    "sys_time",        # Set  system  clock, set real-time (hardware) clock
88
    ]
89
  _DIR_MODE = 0755
90

    
91
  PARAMETERS = {
92
    }
93

    
94
  def __init__(self):
95
    hv_base.BaseHypervisor.__init__(self)
96
    utils.EnsureDirs([(self._ROOT_DIR, self._DIR_MODE)])
97

    
98
  @staticmethod
99
  def _GetMountSubdirs(path):
100
    """Return the list of mountpoints under a given path.
101

102
    This function is Linux-specific.
103

104
    """
105
    #TODO(iustin): investigate and document non-linux options
106
    #(e.g. via mount output)
107
    data = []
108
    fh = open("/proc/mounts", "r")
109
    try:
110
      for line in fh:
111
        _, mountpoint, _ = line.split(" ", 2)
112
        if (mountpoint.startswith(path) and
113
            mountpoint != path):
114
          data.append(mountpoint)
115
    finally:
116
      fh.close()
117
    data.sort(key=lambda x: x.count("/"), reverse=True)
118
    return data
119

    
120
  @classmethod
121
  def _InstanceDir(cls, instance_name):
122
    """Return the root directory for an instance.
123

124
    """
125
    return utils.PathJoin(cls._ROOT_DIR, instance_name)
126

    
127
  @classmethod
128
  def _InstanceConfFile(cls, instance_name):
129
    """Return the configuration file for an instance.
130

131
    """
132
    return utils.PathJoin(cls._ROOT_DIR, instance_name + ".conf")
133

    
134
  def ListInstances(self):
135
    """Get the list of running instances.
136

137
    """
138
    result = utils.RunCmd(["lxc-ls"])
139
    if result.failed:
140
      raise errors.HypervisorError("Can't run lxc-ls: %s" % result.output)
141
    return result.stdout.splitlines()
142

    
143
  def GetInstanceInfo(self, instance_name):
144
    """Get instance properties.
145

146
    @type instance_name: string
147
    @param instance_name: the instance name
148

149
    @return: (name, id, memory, vcpus, stat, times)
150

151
    """
152
    # TODO: read container info from the cgroup mountpoint
153
    return (instance_name, 0, 0, 0, 0, 0)
154

    
155
  def GetAllInstancesInfo(self):
156
    """Get properties of all instances.
157

158
    @return: [(name, id, memory, vcpus, stat, times),...]
159

160
    """
161
    # TODO: read container info from the cgroup mountpoint
162
    data = []
163
    for name in self.ListInstances():
164
      data.append((name, 0, 0, 0, 0, 0))
165
    return data
166

    
167
  def _CreateConfigFile(self, instance, root_dir):
168
    """Create an lxc.conf file for an instance"""
169
    out = []
170
    # hostname
171
    out.append("lxc.utsname = %s" % instance.name)
172

    
173
    # separate pseudo-TTY instances
174
    out.append("lxc.pts = 255")
175
    # standard TTYs/console
176
    out.append("lxc.tty = 6")
177

    
178
    # root FS
179
    out.append("lxc.rootfs = %s" % root_dir)
180

    
181
    # TODO: additional mounts, if we disable CAP_SYS_ADMIN
182

    
183
    # Device control
184
    # deny direct device access
185
    out.append("lxc.cgroup.devices.deny = a")
186
    for devinfo in self._DEVS:
187
      out.append("lxc.cgroup.devices.allow = %s rw" % devinfo)
188

    
189
    # Networking
190
    for idx, nic in enumerate(instance.nics):
191
      out.append("# NIC %d" % idx)
192
      mode = nic.nicparams[constants.NIC_MODE]
193
      link = nic.nicparams[constants.NIC_LINK]
194
      if mode == constants.NIC_MODE_BRIDGED:
195
        out.append("lxc.network.type = veth")
196
        out.append("lxc.network.link = %s" % link)
197
      else:
198
        raise errors.HypervisorError("LXC hypervisor only supports"
199
                                     " bridged mode (NIC %d has mode %s)" %
200
                                     (idx, mode))
201
      out.append("lxc.network.hwaddr = %s" % nic.mac)
202
      out.append("lxc.network.flags = up")
203

    
204
    # Capabilities
205
    for cap in self._DENIED_CAPABILITIES:
206
      out.append("lxc.cap.drop = %s" % cap)
207

    
208
    return "\n".join(out) + "\n"
209

    
210
  def StartInstance(self, instance, block_devices):
211
    """Start an instance.
212

213
    For LCX, we try to mount the block device and execute 'lxc-start
214
    start' (we use volatile containers).
215

216
    """
217
    root_dir = self._InstanceDir(instance.name)
218
    try:
219
      utils.EnsureDirs([(root_dir, self._DIR_MODE)])
220
    except errors.GenericError, err:
221
      raise HypervisorError("Cannot create instance directory: %s", str(err))
222

    
223
    conf_file = self._InstanceConfFile(instance.name)
224
    utils.WriteFile(conf_file, data=self._CreateConfigFile(instance, root_dir))
225

    
226
    if not os.path.ismount(root_dir):
227
      if not block_devices:
228
        raise HypervisorError("LXC needs at least one disk")
229

    
230
      sda_dev_path = block_devices[0][1]
231
      result = utils.RunCmd(["mount", sda_dev_path, root_dir])
232
      if result.failed:
233
        raise HypervisorError("Can't mount the chroot dir: %s" % result.output)
234
    # TODO: replace the global log file with a per-instance log file
235
    result = utils.RunCmd(["lxc-start", "-n", instance.name,
236
                           "-o", self._LOG_FILE, "-l", "DEBUG",
237
                           "-f", conf_file, "-d"])
238
    if result.failed:
239
      raise HypervisorError("Running the lxc-start script failed: %s" %
240
                            result.output)
241

    
242
  def StopInstance(self, instance, force=False, retry=False, name=None):
243
    """Stop an instance.
244

245
    This method has complicated cleanup tests, as we must:
246
      - try to kill all leftover processes
247
      - try to unmount any additional sub-mountpoints
248
      - finally unmount the instance dir
249

250
    """
251
    if name is None:
252
      name = instance.name
253

    
254
    root_dir = self._InstanceDir(name)
255
    if not os.path.exists(root_dir):
256
      return
257

    
258
    if name in self.ListInstances():
259
      # Signal init to shutdown; this is a hack
260
      if not retry and not force:
261
        result = utils.RunCmd(["chroot", root_dir, "poweroff"])
262
        if result.failed:
263
          raise HypervisorError("Can't run 'poweroff' for the instance: %s" %
264
                                result.output)
265
      time.sleep(2)
266
      result = utils.RunCmd(["lxc-stop", "-n", name])
267
      if result.failed:
268
        logging.warning("Error while doing lxc-stop for %s: %s", name,
269
                        result.output)
270

    
271
    for mpath in self._GetMountSubdirs(root_dir):
272
      result = utils.RunCmd(["umount", mpath])
273
      if result.failed:
274
        logging.warning("Error while umounting subpath %s for instance %s: %s",
275
                        mpath, name, result.output)
276

    
277
    result = utils.RunCmd(["umount", root_dir])
278
    if result.failed and force:
279
      msg = ("Processes still alive in the chroot: %s" %
280
             utils.RunCmd("fuser -vm %s" % root_dir).output)
281
      logging.error(msg)
282
      raise HypervisorError("Can't umount the chroot dir: %s (%s)" %
283
                            (result.output, msg))
284

    
285
  def RebootInstance(self, instance):
286
    """Reboot an instance.
287

288
    This is not (yet) implemented (in Ganeti) for the LXC hypervisor.
289

290
    """
291
    # TODO: implement reboot
292
    raise HypervisorError("The LXC hypervisor doesn't implement the"
293
                          " reboot functionality")
294

    
295
  def GetNodeInfo(self):
296
    """Return information about the node.
297

298
    This is just a wrapper over the base GetLinuxNodeInfo method.
299

300
    @return: a dict with the following keys (values in MiB):
301
          - memory_total: the total memory size on the node
302
          - memory_free: the available memory on the node for instances
303
          - memory_dom0: the memory used by the node itself, if available
304

305
    """
306
    return self.GetLinuxNodeInfo()
307

    
308
  @classmethod
309
  def GetShellCommandForConsole(cls, instance, hvparams, beparams):
310
    """Return a command for connecting to the console of an instance.
311

312
    """
313
    return "lxc-console -n %s" % instance.name
314

    
315
  def Verify(self):
316
    """Verify the hypervisor.
317

318
    For the chroot manager, it just checks the existence of the base dir.
319

320
    """
321
    if not os.path.exists(self._ROOT_DIR):
322
      return "The required directory '%s' does not exist." % self._ROOT_DIR
323

    
324
  @classmethod
325
  def PowercycleNode(cls):
326
    """LXC powercycle, just a wrapper over Linux powercycle.
327

328
    """
329
    cls.LinuxPowercycle()
330

    
331
  def MigrateInstance(self, instance, target, live):
332
    """Migrate an instance.
333

334
    @type instance: L{objects.Instance}
335
    @param instance: the instance to be migrated
336
    @type target: string
337
    @param target: hostname (usually ip) of the target node
338
    @type live: boolean
339
    @param live: whether to do a live or non-live migration
340

341
    """
342
    raise HypervisorError("Migration not supported by the LXC hypervisor")