Statistics
| Branch: | Tag: | Revision:

root / lib / hypervisor / hv_lxc.py @ fb62843c

History | View | Annotate | Download (15.6 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, hvparams=None):
195
    """Get properties of all instances.
196

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

403
    See L{BaseHypervisor.GetLinuxNodeInfo}.
404

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

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

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

    
419
  def Verify(self, hvparams=None):
420
    """Verify the hypervisor.
421

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

424
    @type hvparams: dict of strings
425
    @param hvparams: hypervisor parameters to be verified against; not used here
426

427
    @return: Problem description if something is wrong, C{None} otherwise
428

429
    """
430
    msgs = []
431

    
432
    if not os.path.exists(self._ROOT_DIR):
433
      msgs.append("The required directory '%s' does not exist" %
434
                  self._ROOT_DIR)
435

    
436
    try:
437
      self._GetCgroupMountPoint()
438
    except errors.HypervisorError, err:
439
      msgs.append(str(err))
440

    
441
    return self._FormatVerifyResults(msgs)
442

    
443
  @classmethod
444
  def PowercycleNode(cls, hvparams=None):
445
    """LXC powercycle, just a wrapper over Linux powercycle.
446

447
    @type hvparams: dict of strings
448
    @param hvparams: hypervisor params to be used on this node
449

450
    """
451
    cls.LinuxPowercycle()
452

    
453
  def MigrateInstance(self, cluster_name, instance, target, live):
454
    """Migrate an instance.
455

456
    @type cluster_name: string
457
    @param cluster_name: name of the cluster
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")