lib/lxc/container.rb

Summary

Maintainability
A
3 hrs
Test Coverage
class LXC

  # Container Error Class
  class ContainerError < LXCError; end

  # Main Container Class
  #
  # @author Zachary Patten <zachary AT jovelabs DOT com>
  class Container
    require 'timeout'
    require 'tempfile'

    # An array containing the valid container states extracted from the LXC
    # c-source code.
    STATES = %w(stopped starting running stopping aborting freezing frozen thawed not_created).map(&:to_sym)

    # @!method stopped?
    #   Returns true if the container is stopped, false otherwise.
    #   @return [Boolean]
    #
    # @!method starting?
    #   Returns true if the container is starting, false otherwise.
    #   @return [Boolean]
    #
    # @!method running?
    #   Returns true if the container is running, false otherwise.
    #   @return [Boolean]
    #
    # @!method stopping?
    #   Returns true if the container is stopping, false otherwise.
    #   @return [Boolean]
    #
    # @!method aborting?
    #   Returns true if the container is aborting, false otherwise.
    #   @return [Boolean]
    #
    # @!method freezing?
    #   Returns true if the container is freezing, false otherwise.
    #   @return [Boolean]
    #
    # @!method frozen?
    #   Returns true if the container is frozen, false otherwise.
    #   @return [Boolean]
    #
    # @!method thawed?
    #   Returns true if the container is thawed, false otherwise.
    #   @return [Boolean]
    STATES.each do |state|
      define_method "#{state}?" do
        (self.state == state)
      end
    end

    # RegEx pattern for extracting the container state from the "lxc-info"
    # command output.
    REGEX_STATE = /^state:\s+([\w]+)$/

    # RegEx pattern for extracting the container PID from the "lxc-info"
    # command output.
    REGEX_PID = /^pid:\s+([-\d]+)$/

    # Returns the container name
    #
    # @return [String] Container name
    attr_reader :name

    # Returns the parent LXC class instance
    #
    # @return [LXC] Parent LXC class instance.
    attr_reader :lxc

    # @param [Hash] options Options hash.
    # @option options [LXC] :lxc Our parent LXC class instance.
    # @option options [String] :name The name of the container.
    def initialize(options={})
      @lxc  = options[:lxc]
      @name = options[:name]

      raise ContainerError, "You must supply a LXC object!" if @lxc.nil?
      raise ContainerError, "You must supply a container name!" if (@name.nil? || @name.empty?)
    end

    # LXC configuration class
    #
    # Gets the LXC configuration class object
    #
    # @return [LXC::Config] Returns the LXC configuration object.
    def config
      @config ||= LXC::Config.new(@lxc, "/etc/lxc/#{@name}")
    end

    # Create the container
    #
    # Runs the "lxc-create" command.
    #
    # @param [Array] args Additional command-line arguments.
    # @return [Array<String>] Lines of output from the executed command.
    def create(*args)
      self.exec("lxc-create", *args)
    end

    # Destroy the container
    #
    # Runs the "lxc-destroy" command.  If the container has not been stopped
    # first then this will fail unless '-f' is passed as an argument.  See
    # the 'lxc-destroy' man page for more details.
    #
    # @param [Array] args Additional command-line arguments.
    # @return [Array<String>] Lines of output from the executed command.
    def destroy(*args)
      self.exec("lxc-destroy", *args)
    end

    # Start the container
    #
    # Runs the "lxc-start" command.
    #
    # @param [Array] args Additional command-line arguments.
    # @return [Array<String>] Lines of output from the executed command.
    def start(*args)
      self.exec("lxc-start", *args)
    end

    # Start an ephemeral copy of the container
    #
    # Runs the "lxc-start-ephemeral" command.
    #
    # @return [Array<String>] Lines of output from the executed command.
    # @see lxc-start-ephemeral
    def start_ephemeral(*args)
      self.lxc.exec("lxc-start-ephemeral", *args)
    end

    # Clone the container
    #
    # Runs the "lxc-clone" command.
    #
    # @return [Array<String>] Lines of output from the executed command.
    # @see lxc-clone
    def clone(*args)
      self.lxc.exec("lxc-clone", *args)
    end

    # Stop the container
    #
    # Runs the "lxc-stop" command.
    #
    # @param [Array] args Additional command-line arguments.
    # @return [Array<String>] Lines of output from the executed command.
    def stop(*args)
      self.exec("lxc-stop", *args)
    end

    # Restart the container
    def restart(options={})
      self.stop
      self.start
    end
    alias :reload :restart

    # Freeze the container
    #
    # Runs the "lxc-freeze" command.
    #
    # @param [Array] args Additional command-line arguments.
    # @return [Array<String>] Lines of output from the executed command.
    def freeze(*args)
      self.exec("lxc-freeze", *args)
    end

    # Unfreeze (thaw) the container
    #
    # Runs the "lxc-unfreeze" command.
    #
    # @param [Array] args Additional command-line arguments.
    # @return [Array<String>] Lines of output from the executed command.
    def unfreeze(*args)
      self.exec("lxc-unfreeze", *args)
    end

    # Information on the container
    #
    # Runs the "lxc-info" command.
    #
    # @param [Array] args Additional command-line arguments.
    # @return [Array<String>] Lines of output from the executed command.
    def info(*args)
      self.exec("lxc-info", *args).split("\n").uniq.flatten
    end

    # State of the container
    #
    # Runs the "lxc-info" command with the "--state" flag.
    #
    # @param [Array] args Additional command-line arguments.
    # @return [Symbol] Current state of the container.
    def state(*args)
      if self.exists?
        result = self.info("--state", *args).collect{ |str| str.scan(REGEX_STATE) }
        result.flatten!.compact!

        (result.first.strip.downcase.to_sym rescue :unknown)
      else
        :not_created
      end
    end

    # PID of the container
    #
    # Runs the "lxc-info" command with the "--pid" flag.
    #
    # @param [Array] args Additional command-line arguments.
    # @return [Integer] Current PID of the container.
    def pid(*args)
      result = self.info("--pid", *args).collect{ |str| str.scan(REGEX_PID) }
      result.flatten!.compact!

      (result.first.strip.to_i rescue -1)
    end

    # Does the container exist?
    #
    # @return [Boolean] Returns true if the container exists, false otherwise.
    def exists?
      @lxc.exists?(self.name)
    end

    # Run an application inside a container
    #
    # Launches the container, executing the supplied application inside it.
    #
    # @see lxc-execute
    def execute(*args)
      self.exec("lxc-execute", "-f", self.config.filename, "--", *args)
    end

    # Run an application inside a container
    #
    # Executes the supplied application inside a container.  The container must
    # already be running.
    #
    # @see lxc-attach
    def attach(*args)
      self.exec("lxc-attach", *args)
    end

    # Bootstrap a container
    #
    # Renders the supplied text blob inside a container as a script and executes
    # it via lxc-attach.  The container must already be running.
    #
    # @see lxc-attach
    #
    # @param [String] content The content to render in the container and
    #   execute.  This is generally a bash script of some sort for example.
    # @return [String] The output of *lxc-attach*.
    def bootstrap(content)
      output = nil

      ZTK::RescueRetry.try(:tries => 5, :on => ContainerError) do
        tempfile = Tempfile.new("bootstrap")
        lxc_tempfile  = File.join("", "tmp", File.basename(tempfile.path))
        host_tempfile = File.join(self.fs_root, lxc_tempfile)

        self.lxc.runner.file(:target => host_tempfile, :chmod => '0755', :chown => 'root:root') do |file|
          file.puts(content)
        end

        output = self.attach(%(-- /bin/bash #{lxc_tempfile}))

        if !(output =~ /#{lxc_tempfile}: No such file or directory/).nil?
          raise ContainerError, "We could not find the bootstrap file!"
        end
      end

      output
    end

    # Launch a console for the container
    #
    # @see lxc-console
    def console
      self.exec("lxc-console")
    end

    # Wait for a specific container state
    #
    # Runs the "lxc-wait" command.
    #
    # The timeout only works when using remote control via SSH and will orphan
    # the process ('lxc-wait') on the remote host.
    #
    # @param [Symbol,Array] states A single symbol or an array of symbols
    #   representing container states for which we will wait for the container
    #   to change state to.
    # @param [Integer] timeout How long in seconds we will wait before the
    #   operation times out.
    # @return [Boolean] Returns true of the state change happened, false
    #   otherwise.
    def wait(states, timeout=60)
      state_arg = [states].flatten.map do |state|
        state.to_s.upcase.strip
      end.join('|')

      begin
        Timeout.timeout(timeout) do
          self.exec("lxc-wait", "-s", %('#{state_arg}'))
        end
      rescue Timeout::Error => e
        return false
      end

      true
    end

    # Current Memory Usage
    def memory_usage
      result = self.exec(%(lxc-cgroup), %(memory.usage_in_bytes))
      (result.empty? ? 0 : result.to_i)
    end

    # CPU Time in Seconds
    def cpu_usage
      result = self.exec(%(lxc-cgroup), %(cpuacct.usage))
      (result.empty? ? 0 : (result.to_i / 1E9).to_i)
    end

    # Current Disk Usage
    def disk_usage(ephemeral=false)
      result = (@lxc.exec(%(du -sb #{self.fs_root(ephemeral)})).split.first rescue nil)
      ((result.nil? || result.empty?) ? 0 : result.to_i)
    end

    # Linux container command execution wrapper
    #
    # Executes the supplied command by injecting the container name into the
    # argument list and then passes to the arguments to the top-level LXC class
    # exec method.
    #
    # @param [Array] args Additional command-line arguments.
    # @return [Array<String>] Stripped output text of the executed command.
    #
    # @see LXC#exec
    def exec(*args)
      arguments = Array.new
      arguments << args.shift
      arguments << %(-n #{self.name})
      arguments << args
      arguments.flatten!.compact!

      @lxc.exec(*arguments)
    end

    # Root directory for the containers file system
    #
    # @param [Boolean] ephemeral True if we should construct a path to the
    #   union filesystem used by ephemeral containers.  False if we should
    #   construct a traditional path.
    # @return [String] The root directory for the container.
    def fs_root(ephemeral=false)
      if (ephemeral == true)
        File.join(self.container_root, 'delta0')
      else
        File.join(self.container_root, 'rootfs')
      end
    end

    # Static information about the containers filesystems
    #
    # @return [String] The path to the containers fstab.
    def fs_tab
      File.join(self.container_root, 'fstab')
    end

    # Directory for the container
    #
    # @return [String] The directory for the container.
    def container_root
      File.join('/', 'var', 'lib', 'lxc', self.name)
    end

    # Provides a concise string representation of the class
    # @return [String]
    def inspect
      tags = Array.new
      tags << "name=#{self.name.inspect}"
      tags = tags.join(' ')

      "#<LXC::Container #{tags}>"
    end

  end
end