Statistics
| Branch: | Tag: | Revision:

root / lib / hypervisor / hv_lxc.py @ 0bbec3af

History | View | Annotate | Download (15.7 kB)

1
#
2
#
3

    
4
# Copyright (C) 2010, 2013 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, hvparams=None):
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, hvparams=None):
167
    """Get instance properties.
168

169
    @type instance_name: string
170
    @param instance_name: the instance name
171
    @type hvparams: dict of strings
172
    @param hvparams: hvparams to be used with this instance
173
    @rtype: tuple of strings
174
    @return: (name, id, memory, vcpus, stat, times)
175

176
    """
177
    # TODO: read container info from the cgroup mountpoint
178

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

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

    
194
  def GetAllInstancesInfo(self):
195
    """Get properties of all instances.
196

197
    @return: [(name, id, memory, vcpus, stat, times),...]
198

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

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

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

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

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

    
235
    # TODO: additional mounts, if we disable CAP_SYS_ADMIN
236

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

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

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

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

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

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

    
286
    return "\n".join(out) + "\n"
287

    
288
  def StartInstance(self, instance, block_devices, startup_paused):
289
    """Start an instance.
290

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

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

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

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

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

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

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

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

338
    """
339
    if name is None:
340
      name = instance.name
341

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

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

    
359
    if not os.path.ismount(root_dir):
360
      return
361

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

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

    
376
  def RebootInstance(self, instance):
377
    """Reboot an instance.
378

379
    This is not (yet) implemented (in Ganeti) for the LXC hypervisor.
380

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

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

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

394
    """
395
    # Currently lxc instances don't have memory limits
396
    pass
397

    
398
  def GetNodeInfo(self, hvparams=None):
399
    """Return information about the node.
400

401
    This is just a wrapper over the base GetLinuxNodeInfo method.
402

403
    @type hvparams: dict of strings
404
    @param hvparams: hypervisor parameters, not used in this class
405

406
    @return: a dict with the following keys (values in MiB):
407
          - memory_total: the total memory size on the node
408
          - memory_free: the available memory on the node for instances
409
          - memory_dom0: the memory used by the node itself, if available
410

411
    """
412
    return self.GetLinuxNodeInfo()
413

    
414
  @classmethod
415
  def GetInstanceConsole(cls, instance, hvparams, beparams):
416
    """Return a command for connecting to the console of an instance.
417

418
    """
419
    return objects.InstanceConsole(instance=instance.name,
420
                                   kind=constants.CONS_SSH,
421
                                   host=instance.primary_node,
422
                                   user=constants.SSH_CONSOLE_USER,
423
                                   command=["lxc-console", "-n", instance.name])
424

    
425
  def Verify(self, hvparams=None):
426
    """Verify the hypervisor.
427

428
    For the LXC manager, it just checks the existence of the base dir.
429

430
    @type hvparams: dict of strings
431
    @param hvparams: hypervisor parameters to be verified against; not used here
432

433
    @return: Problem description if something is wrong, C{None} otherwise
434

435
    """
436
    msgs = []
437

    
438
    if not os.path.exists(self._ROOT_DIR):
439
      msgs.append("The required directory '%s' does not exist" %
440
                  self._ROOT_DIR)
441

    
442
    try:
443
      self._GetCgroupMountPoint()
444
    except errors.HypervisorError, err:
445
      msgs.append(str(err))
446

    
447
    return self._FormatVerifyResults(msgs)
448

    
449
  @classmethod
450
  def PowercycleNode(cls):
451
    """LXC powercycle, just a wrapper over Linux powercycle.
452

453
    """
454
    cls.LinuxPowercycle()
455

    
456
  def MigrateInstance(self, instance, target, live):
457
    """Migrate an instance.
458

459
    @type instance: L{objects.Instance}
460
    @param instance: the instance to be migrated
461
    @type target: string
462
    @param target: hostname (usually ip) of the target node
463
    @type live: boolean
464
    @param live: whether to do a live or non-live migration
465

466
    """
467
    raise HypervisorError("Migration is not supported by the LXC hypervisor")
468

    
469
  def GetMigrationStatus(self, instance):
470
    """Get the migration status
471

472
    @type instance: L{objects.Instance}
473
    @param instance: the instance that is being migrated
474
    @rtype: L{objects.MigrationStatus}
475
    @return: the status of the current migration (one of
476
             L{constants.HV_MIGRATION_VALID_STATUSES}), plus any additional
477
             progress info that can be retrieved from the hypervisor
478

479
    """
480
    raise HypervisorError("Migration is not supported by the LXC hypervisor")