Statistics
| Branch: | Tag: | Revision:

root / lib / hypervisor / hv_lxc.py @ 3ae003d8

History | View | Annotate | Download (15.8 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):
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
                   timeout=None):
330
    """Stop an instance.
331

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

337
    """
338
    assert(timeout is None or force is not None)
339

    
340
    if name is None:
341
      name = instance.name
342

    
343
    timeout_cmd = []
344
    if timeout is not None:
345
      timeout_cmd.extend(["timeout", str(timeout)])
346

    
347
    root_dir = self._InstanceDir(name)
348
    if not os.path.exists(root_dir):
349
      return
350

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

    
364
    if not os.path.ismount(root_dir):
365
      return
366

    
367
    for mpath in self._GetMountSubdirs(root_dir):
368
      result = utils.RunCmd(timeout_cmd.extend(["umount", mpath]))
369
      if result.failed:
370
        logging.warning("Error while umounting subpath %s for instance %s: %s",
371
                        mpath, name, result.output)
372

    
373
    result = utils.RunCmd(timeout_cmd.extend(["umount", root_dir]))
374
    if result.failed and force:
375
      msg = ("Processes still alive in the chroot: %s" %
376
             utils.RunCmd("fuser -vm %s" % root_dir).output)
377
      logging.error(msg)
378
      raise HypervisorError("Unmounting the chroot dir failed: %s (%s)" %
379
                            (result.output, msg))
380

    
381
  def RebootInstance(self, instance):
382
    """Reboot an instance.
383

384
    This is not (yet) implemented (in Ganeti) for the LXC hypervisor.
385

386
    """
387
    # TODO: implement reboot
388
    raise HypervisorError("The LXC hypervisor doesn't implement the"
389
                          " reboot functionality")
390

    
391
  def BalloonInstanceMemory(self, instance, mem):
392
    """Balloon an instance memory to a certain value.
393

394
    @type instance: L{objects.Instance}
395
    @param instance: instance to be accepted
396
    @type mem: int
397
    @param mem: actual memory size to use for instance runtime
398

399
    """
400
    # Currently lxc instances don't have memory limits
401
    pass
402

    
403
  def GetNodeInfo(self):
404
    """Return information about the node.
405

406
    This is just a wrapper over the base GetLinuxNodeInfo method.
407

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

413
    """
414
    return self.GetLinuxNodeInfo()
415

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

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

    
427
  def Verify(self):
428
    """Verify the hypervisor.
429

430
    For the LXC manager, it just checks the existence of the base dir.
431

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

434
    """
435
    msgs = []
436

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

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

    
446
    return self._FormatVerifyResults(msgs)
447

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

452
    """
453
    cls.LinuxPowercycle()
454

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

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

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

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

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

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

    
481
  def HotplugSupported(self, instance, action, dev_type):
482
    """Whether hotplug is supported.
483

484
    """
485
    raise HypervisorError("Hotplug not supported by the LXC hypervisor")