Statistics
| Branch: | Tag: | Revision:

root / lib / hypervisor / hv_lxc.py @ c42be2c0

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, 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),
193
            hv_base.HvInstanceState.RUNNING, 0)
194

    
195
  def GetAllInstancesInfo(self, hvparams=None):
196
    """Get properties of all instances.
197

198
    @type hvparams: dict of strings
199
    @param hvparams: hypervisor parameter
200
    @return: [(name, id, memory, vcpus, stat, times),...]
201

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

    
213
  def _CreateConfigFile(self, instance, root_dir):
214
    """Create an lxc.conf file for an instance.
215

216
    """
217
    out = []
218
    # hostname
219
    out.append("lxc.utsname = %s" % instance.name)
220

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

    
235
    # root FS
236
    out.append("lxc.rootfs = %s" % root_dir)
237

    
238
    # TODO: additional mounts, if we disable CAP_SYS_ADMIN
239

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

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

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

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

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

    
285
    # Capabilities
286
    for cap in self._DENIED_CAPABILITIES:
287
      out.append("lxc.cap.drop = %s" % cap)
288

    
289
    return "\n".join(out) + "\n"
290

    
291
  def StartInstance(self, instance, block_devices, startup_paused):
292
    """Start an instance.
293

294
    For LXC, we try to mount the block device and execute 'lxc-start'.
295
    We use volatile containers.
296

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

    
304
    conf_file = self._InstanceConfFile(instance.name)
305
    utils.WriteFile(conf_file, data=self._CreateConfigFile(instance, root_dir))
306

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

    
316
    if not os.path.ismount(root_dir):
317
      if not block_devices:
318
        raise HypervisorError("LXC needs at least one disk")
319

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

    
333
  def StopInstance(self, instance, force=False, retry=False, name=None):
334
    """Stop an instance.
335

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

341
    """
342
    if name is None:
343
      name = instance.name
344

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

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

    
362
    if not os.path.ismount(root_dir):
363
      return
364

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

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

    
379
  def RebootInstance(self, instance):
380
    """Reboot an instance.
381

382
    This is not (yet) implemented (in Ganeti) for the LXC hypervisor.
383

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

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

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

397
    """
398
    # Currently lxc instances don't have memory limits
399
    pass
400

    
401
  def GetNodeInfo(self, hvparams=None):
402
    """Return information about the node.
403

404
    See L{BaseHypervisor.GetLinuxNodeInfo}.
405

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

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

414
    """
415
    ndparams = node_group.FillND(primary_node)
416
    return objects.InstanceConsole(instance=instance.name,
417
                                   kind=constants.CONS_SSH,
418
                                   host=primary_node.name,
419
                                   port=ndparams.get(constants.ND_SSH_PORT),
420
                                   user=constants.SSH_CONSOLE_USER,
421
                                   command=["lxc-console", "-n", instance.name])
422

    
423
  def Verify(self, hvparams=None):
424
    """Verify the hypervisor.
425

426
    For the LXC manager, it just checks the existence of the base dir.
427

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

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

433
    """
434
    msgs = []
435

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

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

    
445
    return self._FormatVerifyResults(msgs)
446

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

451
    @type hvparams: dict of strings
452
    @param hvparams: hypervisor params to be used on this node
453

454
    """
455
    cls.LinuxPowercycle()
456

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

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

469
    """
470
    raise HypervisorError("Migration is not supported by the LXC hypervisor")
471

    
472
  def GetMigrationStatus(self, instance):
473
    """Get the migration status
474

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

482
    """
483
    raise HypervisorError("Migration is not supported by the LXC hypervisor")