Statistics
| Branch: | Tag: | Revision:

root / lib / hypervisor / hv_lxc.py @ e4157912

History | View | Annotate | Download (15.2 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=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 LXCHypervisor(hv_base.BaseHypervisor):
41
  """LXC-based virtualization.
42

43
  TODO:
44
    - move hardcoded parameters into hypervisor parameters, once we
45
      have the container-parameter support
46

47
  Problems/issues:
48
    - LXC is very temperamental; in daemon mode, it succeeds or fails
49
      in launching the instance silently, without any error
50
      indication, and when failing it can leave network interfaces
51
      around, and future successful startups will list the instance
52
      twice
53

54
  """
55
  _ROOT_DIR = pathutils.RUN_DIR + "/lxc"
56
  _DEVS = [
57
    "c 1:3",   # /dev/null
58
    "c 1:5",   # /dev/zero
59
    "c 1:7",   # /dev/full
60
    "c 1:8",   # /dev/random
61
    "c 1:9",   # /dev/urandom
62
    "c 1:10",  # /dev/aio
63
    "c 5:0",   # /dev/tty
64
    "c 5:1",   # /dev/console
65
    "c 5:2",   # /dev/ptmx
66
    "c 136:*", # first block of Unix98 PTY slaves
67
    ]
68
  _DENIED_CAPABILITIES = [
69
    "mac_override",    # Allow MAC configuration or state changes
70
    # TODO: remove sys_admin too, for safety
71
    #"sys_admin",       # Perform  a range of system administration operations
72
    "sys_boot",        # Use reboot(2) and kexec_load(2)
73
    "sys_module",      # Load  and  unload kernel modules
74
    "sys_time",        # Set  system  clock, set real-time (hardware) clock
75
    ]
76
  _DIR_MODE = 0755
77

    
78
  PARAMETERS = {
79
    constants.HV_CPU_MASK: hv_base.OPT_CPU_MASK_CHECK,
80
    }
81

    
82
  def __init__(self):
83
    hv_base.BaseHypervisor.__init__(self)
84
    utils.EnsureDirs([(self._ROOT_DIR, self._DIR_MODE)])
85

    
86
  @staticmethod
87
  def _GetMountSubdirs(path):
88
    """Return the list of mountpoints under a given path.
89

90
    """
91
    result = []
92
    for _, mountpoint, _, _ in utils.GetMounts():
93
      if (mountpoint.startswith(path) and
94
          mountpoint != path):
95
        result.append(mountpoint)
96

    
97
    result.sort(key=lambda x: x.count("/"), reverse=True)
98
    return result
99

    
100
  @classmethod
101
  def _InstanceDir(cls, instance_name):
102
    """Return the root directory for an instance.
103

104
    """
105
    return utils.PathJoin(cls._ROOT_DIR, instance_name)
106

    
107
  @classmethod
108
  def _InstanceConfFile(cls, instance_name):
109
    """Return the configuration file for an instance.
110

111
    """
112
    return utils.PathJoin(cls._ROOT_DIR, instance_name + ".conf")
113

    
114
  @classmethod
115
  def _InstanceLogFile(cls, instance_name):
116
    """Return the log file for an instance.
117

118
    """
119
    return utils.PathJoin(cls._ROOT_DIR, instance_name + ".log")
120

    
121
  @classmethod
122
  def _GetCgroupMountPoint(cls):
123
    for _, mountpoint, fstype, _ in utils.GetMounts():
124
      if fstype == "cgroup":
125
        return mountpoint
126
    raise errors.HypervisorError("The cgroup filesystem is not mounted")
127

    
128
  @classmethod
129
  def _GetCgroupCpuList(cls, instance_name):
130
    """Return the list of CPU ids for an instance.
131

132
    """
133
    cgroup = cls._GetCgroupMountPoint()
134
    try:
135
      cpus = utils.ReadFile(utils.PathJoin(cgroup, 'lxc',
136
                                           instance_name,
137
                                           "cpuset.cpus"))
138
    except EnvironmentError, err:
139
      raise errors.HypervisorError("Getting CPU list for instance"
140
                                   " %s failed: %s" % (instance_name, err))
141

    
142
    return utils.ParseCpuMask(cpus)
143

    
144
  @classmethod
145
  def _GetCgroupMemoryLimit(cls, instance_name):
146
    """Return the memory limit for an instance
147

148
    """
149
    cgroup = cls._GetCgroupMountPoint()
150
    try:
151
      memory = int(utils.ReadFile(utils.PathJoin(cgroup, 'lxc',
152
                                                 instance_name,
153
                                                 "memory.limit_in_bytes")))
154
    except EnvironmentError:
155
      # memory resource controller may be disabled, ignore
156
      memory = 0
157

    
158
    return memory
159

    
160
  def ListInstances(self):
161
    """Get the list of running instances.
162

163
    """
164
    return [iinfo[0] for iinfo in self.GetAllInstancesInfo()]
165

    
166
  def GetInstanceInfo(self, instance_name):
167
    """Get instance properties.
168

169
    @type instance_name: string
170
    @param instance_name: the instance name
171
    @rtype: tuple of strings
172
    @return: (name, id, memory, vcpus, stat, times)
173

174
    """
175
    # TODO: read container info from the cgroup mountpoint
176

    
177
    result = utils.RunCmd(["lxc-info", "-s", "-n", instance_name])
178
    if result.failed:
179
      raise errors.HypervisorError("Running lxc-info failed: %s" %
180
                                   result.output)
181
    # lxc-info output examples:
182
    # 'state: STOPPED
183
    # 'state: RUNNING
184
    _, state = result.stdout.rsplit(None, 1)
185
    if state != "RUNNING":
186
      return None
187

    
188
    cpu_list = self._GetCgroupCpuList(instance_name)
189
    memory = self._GetCgroupMemoryLimit(instance_name) / (1024 ** 2)
190
    return (instance_name, 0, memory, len(cpu_list), 0, 0)
191

    
192
  def GetAllInstancesInfo(self):
193
    """Get properties of all instances.
194

195
    @return: [(name, id, memory, vcpus, stat, times),...]
196

197
    """
198
    data = []
199
    for name in os.listdir(self._ROOT_DIR):
200
      try:
201
        info = self.GetInstanceInfo(name)
202
      except errors.HypervisorError:
203
        continue
204
      if info:
205
        data.append(info)
206
    return data
207

    
208
  def _CreateConfigFile(self, instance, root_dir):
209
    """Create an lxc.conf file for an instance.
210

211
    """
212
    out = []
213
    # hostname
214
    out.append("lxc.utsname = %s" % instance.name)
215

    
216
    # separate pseudo-TTY instances
217
    out.append("lxc.pts = 255")
218
    # standard TTYs
219
    out.append("lxc.tty = 6")
220
    # console log file
221
    console_log = utils.PathJoin(self._ROOT_DIR, instance.name + ".console")
222
    try:
223
      utils.WriteFile(console_log, data="", mode=constants.SECURE_FILE_MODE)
224
    except EnvironmentError, err:
225
      raise errors.HypervisorError("Creating console log file %s for"
226
                                   " instance %s failed: %s" %
227
                                   (console_log, instance.name, err))
228
    out.append("lxc.console = %s" % console_log)
229

    
230
    # root FS
231
    out.append("lxc.rootfs = %s" % root_dir)
232

    
233
    # TODO: additional mounts, if we disable CAP_SYS_ADMIN
234

    
235
    # CPUs
236
    if instance.hvparams[constants.HV_CPU_MASK]:
237
      cpu_list = utils.ParseCpuMask(instance.hvparams[constants.HV_CPU_MASK])
238
      cpus_in_mask = len(cpu_list)
239
      if cpus_in_mask != instance.beparams["vcpus"]:
240
        raise errors.HypervisorError("Number of VCPUs (%d) doesn't match"
241
                                     " the number of CPUs in the"
242
                                     " cpu_mask (%d)" %
243
                                     (instance.beparams["vcpus"],
244
                                      cpus_in_mask))
245
      out.append("lxc.cgroup.cpuset.cpus = %s" %
246
                 instance.hvparams[constants.HV_CPU_MASK])
247

    
248
    # Memory
249
    # Conditionally enable, memory resource controller might be disabled
250
    cgroup = self._GetCgroupMountPoint()
251
    if os.path.exists(utils.PathJoin(cgroup, 'memory.limit_in_bytes')):
252
      out.append("lxc.cgroup.memory.limit_in_bytes = %dM" %
253
                 instance.beparams[constants.BE_MAXMEM])
254

    
255
    if os.path.exists(utils.PathJoin(cgroup, 'memory.memsw.limit_in_bytes')):
256
      out.append("lxc.cgroup.memory.memsw.limit_in_bytes = %dM" %
257
                 instance.beparams[constants.BE_MAXMEM])
258

    
259
    # Device control
260
    # deny direct device access
261
    out.append("lxc.cgroup.devices.deny = a")
262
    for devinfo in self._DEVS:
263
      out.append("lxc.cgroup.devices.allow = %s rw" % devinfo)
264

    
265
    # Networking
266
    for idx, nic in enumerate(instance.nics):
267
      out.append("# NIC %d" % idx)
268
      mode = nic.nicparams[constants.NIC_MODE]
269
      link = nic.nicparams[constants.NIC_LINK]
270
      if mode == constants.NIC_MODE_BRIDGED:
271
        out.append("lxc.network.type = veth")
272
        out.append("lxc.network.link = %s" % link)
273
      else:
274
        raise errors.HypervisorError("LXC hypervisor only supports"
275
                                     " bridged mode (NIC %d has mode %s)" %
276
                                     (idx, mode))
277
      out.append("lxc.network.hwaddr = %s" % nic.mac)
278
      out.append("lxc.network.flags = up")
279

    
280
    # Capabilities
281
    for cap in self._DENIED_CAPABILITIES:
282
      out.append("lxc.cap.drop = %s" % cap)
283

    
284
    return "\n".join(out) + "\n"
285

    
286
  def StartInstance(self, instance, block_devices, startup_paused):
287
    """Start an instance.
288

289
    For LXC, we try to mount the block device and execute 'lxc-start'.
290
    We use volatile containers.
291

292
    """
293
    root_dir = self._InstanceDir(instance.name)
294
    try:
295
      utils.EnsureDirs([(root_dir, self._DIR_MODE)])
296
    except errors.GenericError, err:
297
      raise HypervisorError("Creating instance directory failed: %s", str(err))
298

    
299
    conf_file = self._InstanceConfFile(instance.name)
300
    utils.WriteFile(conf_file, data=self._CreateConfigFile(instance, root_dir))
301

    
302
    log_file = self._InstanceLogFile(instance.name)
303
    if not os.path.exists(log_file):
304
      try:
305
        utils.WriteFile(log_file, data="", mode=constants.SECURE_FILE_MODE)
306
      except EnvironmentError, err:
307
        raise errors.HypervisorError("Creating hypervisor log file %s for"
308
                                     " instance %s failed: %s" %
309
                                     (log_file, instance.name, err))
310

    
311
    if not os.path.ismount(root_dir):
312
      if not block_devices:
313
        raise HypervisorError("LXC needs at least one disk")
314

    
315
      sda_dev_path = block_devices[0][1]
316
      result = utils.RunCmd(["mount", sda_dev_path, root_dir])
317
      if result.failed:
318
        raise HypervisorError("Mounting the root dir of LXC instance %s"
319
                              " failed: %s" % (instance.name, result.output))
320
    result = utils.RunCmd(["lxc-start", "-n", instance.name,
321
                           "-o", log_file,
322
                           "-l", "DEBUG",
323
                           "-f", conf_file, "-d"])
324
    if result.failed:
325
      raise HypervisorError("Running the lxc-start script failed: %s" %
326
                            result.output)
327

    
328
  def StopInstance(self, instance, force=False, retry=False, name=None):
329
    """Stop an instance.
330

331
    This method has complicated cleanup tests, as we must:
332
      - try to kill all leftover processes
333
      - try to unmount any additional sub-mountpoints
334
      - finally unmount the instance dir
335

336
    """
337
    if name is None:
338
      name = instance.name
339

    
340
    root_dir = self._InstanceDir(name)
341
    if not os.path.exists(root_dir):
342
      return
343

    
344
    if name in self.ListInstances():
345
      # Signal init to shutdown; this is a hack
346
      if not retry and not force:
347
        result = utils.RunCmd(["chroot", root_dir, "poweroff"])
348
        if result.failed:
349
          raise HypervisorError("Running 'poweroff' on the instance"
350
                                " failed: %s" % result.output)
351
      time.sleep(2)
352
      result = utils.RunCmd(["lxc-stop", "-n", name])
353
      if result.failed:
354
        logging.warning("Error while doing lxc-stop for %s: %s", name,
355
                        result.output)
356

    
357
    if not os.path.ismount(root_dir):
358
      return
359

    
360
    for mpath in self._GetMountSubdirs(root_dir):
361
      result = utils.RunCmd(["umount", mpath])
362
      if result.failed:
363
        logging.warning("Error while umounting subpath %s for instance %s: %s",
364
                        mpath, name, result.output)
365

    
366
    result = utils.RunCmd(["umount", root_dir])
367
    if result.failed and force:
368
      msg = ("Processes still alive in the chroot: %s" %
369
             utils.RunCmd("fuser -vm %s" % root_dir).output)
370
      logging.error(msg)
371
      raise HypervisorError("Unmounting the chroot dir failed: %s (%s)" %
372
                            (result.output, msg))
373

    
374
  def RebootInstance(self, instance):
375
    """Reboot an instance.
376

377
    This is not (yet) implemented (in Ganeti) for the LXC hypervisor.
378

379
    """
380
    # TODO: implement reboot
381
    raise HypervisorError("The LXC hypervisor doesn't implement the"
382
                          " reboot functionality")
383

    
384
  def BalloonInstanceMemory(self, instance, mem):
385
    """Balloon an instance memory to a certain value.
386

387
    @type instance: L{objects.Instance}
388
    @param instance: instance to be accepted
389
    @type mem: int
390
    @param mem: actual memory size to use for instance runtime
391

392
    """
393
    # Currently lxc instances don't have memory limits
394
    pass
395

    
396
  def GetNodeInfo(self):
397
    """Return information about the node.
398

399
    This is just a wrapper over the base GetLinuxNodeInfo method.
400

401
    @return: a dict with the following keys (values in MiB):
402
          - memory_total: the total memory size on the node
403
          - memory_free: the available memory on the node for instances
404
          - memory_dom0: the memory used by the node itself, if available
405

406
    """
407
    return self.GetLinuxNodeInfo()
408

    
409
  @classmethod
410
  def GetInstanceConsole(cls, instance, hvparams, beparams):
411
    """Return a command for connecting to the console of an instance.
412

413
    """
414
    return objects.InstanceConsole(instance=instance.name,
415
                                   kind=constants.CONS_SSH,
416
                                   host=instance.primary_node,
417
                                   user=constants.SSH_CONSOLE_USER,
418
                                   command=["lxc-console", "-n", instance.name])
419

    
420
  def Verify(self):
421
    """Verify the hypervisor.
422

423
    For the LXC manager, it just checks the existence of the base dir.
424

425
    @return: Problem description if something is wrong, C{None} otherwise
426

427
    """
428
    if os.path.exists(self._ROOT_DIR):
429
      return None
430
    else:
431
      return "The required directory '%s' does not exist" % self._ROOT_DIR
432

    
433
  @classmethod
434
  def PowercycleNode(cls):
435
    """LXC powercycle, just a wrapper over Linux powercycle.
436

437
    """
438
    cls.LinuxPowercycle()
439

    
440
  def MigrateInstance(self, instance, target, live):
441
    """Migrate an instance.
442

443
    @type instance: L{objects.Instance}
444
    @param instance: the instance to be migrated
445
    @type target: string
446
    @param target: hostname (usually ip) of the target node
447
    @type live: boolean
448
    @param live: whether to do a live or non-live migration
449

450
    """
451
    raise HypervisorError("Migration is not supported by the LXC hypervisor")
452

    
453
  def GetMigrationStatus(self, instance):
454
    """Get the migration status
455

456
    @type instance: L{objects.Instance}
457
    @param instance: the instance that is being migrated
458
    @rtype: L{objects.MigrationStatus}
459
    @return: the status of the current migration (one of
460
             L{constants.HV_MIGRATION_VALID_STATUSES}), plus any additional
461
             progress info that can be retrieved from the hypervisor
462

463
    """
464
    raise HypervisorError("Migration is not supported by the LXC hypervisor")