diff --git a/plugins/provisioners/docker/cap/debian/docker_configure_auto_start.rb b/plugins/provisioners/docker/cap/debian/docker_configure_auto_start.rb new file mode 100644 index 000000000..52c438f20 --- /dev/null +++ b/plugins/provisioners/docker/cap/debian/docker_configure_auto_start.rb @@ -0,0 +1,15 @@ +module VagrantPlugins + module Docker + module Cap + module Debian + module DockerConfigureAutoStart + def self.docker_configure_auto_start(machine) + if ! machine.communicate.test('grep -q \'\-r=true\' /etc/init/docker.conf') + machine.communicate.sudo("sed -i.bak 's/docker -d/docker -d -r=true/' /etc/init/docker.conf ") + end + end + end + end + end + end +end diff --git a/plugins/provisioners/docker/cap/debian/docker_configure_vagrant_user.rb b/plugins/provisioners/docker/cap/debian/docker_configure_vagrant_user.rb new file mode 100644 index 000000000..1136ddd79 --- /dev/null +++ b/plugins/provisioners/docker/cap/debian/docker_configure_vagrant_user.rb @@ -0,0 +1,13 @@ +module VagrantPlugins + module Docker + module Cap + module Debian + module DockerConfigureVagrantUser + def self.docker_configure_vagrant_user(machine) + machine.communicate.sudo("usermod -a -G docker #{machine.config.ssh.username || "vagrant"}") + end + end + end + end + end +end diff --git a/plugins/provisioners/docker/cap/debian/docker_install.rb b/plugins/provisioners/docker/cap/debian/docker_install.rb new file mode 100644 index 000000000..8d5773465 --- /dev/null +++ b/plugins/provisioners/docker/cap/debian/docker_install.rb @@ -0,0 +1,26 @@ +module VagrantPlugins + module Docker + module Cap + module Debian + module DockerInstall + def self.docker_install(machine, version) + package = 'lxc-docker' + package << "-#{version}" if version != :latest + + machine.communicate.tap do |comm| + # TODO: Perform check on the host machine if aufs is installed and using LXC + if machine.provider_name != :lxc + comm.sudo("lsmod | grep aufs || modprobe aufs || apt-get install -y linux-image-extra-`uname -r`") + end + comm.sudo("apt-get install -y --force-yes -q curl") + comm.sudo("curl http://get.docker.io/gpg | apt-key add -") + comm.sudo("echo deb http://get.docker.io/ubuntu docker main > /etc/apt/sources.list.d/docker.list") + comm.sudo("apt-get update") + comm.sudo("apt-get install -y --force-yes -q xz-utils #{package} -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold'") + end + end + end + end + end + end +end diff --git a/plugins/provisioners/docker/cap/debian/docker_start_service.rb b/plugins/provisioners/docker/cap/debian/docker_start_service.rb new file mode 100644 index 000000000..a97bade8b --- /dev/null +++ b/plugins/provisioners/docker/cap/debian/docker_start_service.rb @@ -0,0 +1,13 @@ +module VagrantPlugins + module Docker + module Cap + module Debian + module DockerStartService + def self.docker_start_service(machine) + machine.communicate.sudo("service docker start") + end + end + end + end + end +end diff --git a/plugins/provisioners/docker/cap/linux/docker_installed.rb b/plugins/provisioners/docker/cap/linux/docker_installed.rb new file mode 100644 index 000000000..f5d693501 --- /dev/null +++ b/plugins/provisioners/docker/cap/linux/docker_installed.rb @@ -0,0 +1,13 @@ +module VagrantPlugins + module Docker + module Cap + module Linux + module DockerInstalled + def self.docker_installed(machine) + machine.communicate.test("test -f /usr/bin/docker", sudo: true) + end + end + end + end + end +end diff --git a/plugins/provisioners/docker/config.rb b/plugins/provisioners/docker/config.rb new file mode 100644 index 000000000..362e442db --- /dev/null +++ b/plugins/provisioners/docker/config.rb @@ -0,0 +1,43 @@ +require 'set' + +module VagrantPlugins + module Docker + class Config < Vagrant.plugin("2", :config) + attr_reader :images, :containers + attr_accessor :version + + def initialize + @images = Set.new + @containers = Hash.new + @version = UNSET_VALUE + end + + def images=(images) + @images = Set.new(images) + end + + def pull_images(*images) + @images += images.map(&:to_s) + end + + def run(name, **options) + params = options.dup + params[:image] = name + + # TODO: Validate provided parameters before assignment + @containers[name.to_s] = params + end + + def finalize! + @version = "latest" if @version == UNSET_VALUE + @version = @version.to_sym + end + + def merge(other) + super.tap do |result| + result.pull_images(*(other.images + self.images)) + end + end + end + end +end diff --git a/plugins/provisioners/docker/docker_client.rb b/plugins/provisioners/docker/docker_client.rb new file mode 100644 index 000000000..535c6e9fc --- /dev/null +++ b/plugins/provisioners/docker/docker_client.rb @@ -0,0 +1,78 @@ +require 'digest/sha1' + +module VagrantPlugins + module Docker + class DockerClient + def initialize(machine) + @machine = machine + end + + def pull_images(*images) + @machine.communicate.tap do |comm| + images.each do |image| + @machine.ui.info(I18n.t("vagrant.docker_pulling_single", name: image)) + comm.sudo("docker images | grep -q #{image} || docker pull #{image}") + end + end + end + + def start_service + if !daemon_running? && @machine.guest.capability?(:docker_start_service) + @machine.guest.capability(:docker_start_service) + end + end + + def daemon_running? + @machine.communicate.test('test -f /var/run/docker.pid') + end + + def run(containers) + containers.each do |name, config| + cids_dir = "/var/lib/vagrant/cids" + config[:cidfile] ||= "#{cids_dir}/#{Digest::SHA1.hexdigest name}" + + @machine.ui.info(I18n.t("vagrant.docker_running", name: name)) + @machine.communicate.sudo("mkdir -p #{cids_dir}") + run_container({ + name: name + }.merge(config)) + end + end + + def run_container(config) + raise "Container's cidfile was not provided!" if !config[:cidfile] + + id = "$(cat #{config[:cidfile]})" + + if container_exist?(id) + start_container(id) + else + create_container(config) + end + end + + def container_exist?(id) + @machine.communicate.test("sudo docker ps -a -q | grep -q #{id}") + end + + def start_container(id) + if !container_running?(id) + @machine.communicate.sudo("docker start #{id}") + end + end + + def container_running?(id) + @machine.communicate.test("sudo docker ps -q | grep #{id}") + end + + def create_container(config) + args = "-cidfile=#{config[:cidfile]} -d " + args << config[:args] if config[:args] + @machine.communicate.sudo %[ + rm -f #{config[:cidfile]} + docker run #{args} #{config[:image]} #{config[:cmd]} + ] + end + end + end +end diff --git a/plugins/provisioners/docker/docker_installer.rb b/plugins/provisioners/docker/docker_installer.rb new file mode 100644 index 000000000..7a1a0da14 --- /dev/null +++ b/plugins/provisioners/docker/docker_installer.rb @@ -0,0 +1,39 @@ +module VagrantPlugins + module Docker + class DockerInstaller + def initialize(machine, version) + @machine = machine + @version = version + end + + # This handles verifying the Docker installation, installing it if it was + # requested, and so on. This method will raise exceptions if things are + # wrong. + def ensure_installed + if !@machine.guest.capability?(:docker_installed) + @machine.ui.warn(I18n.t("vagrant.docker_cant_detect")) + return + end + + if !@machine.guest.capability(:docker_installed) + @machine.ui.info(I18n.t("vagrant.docker_installing", version: @version.to_s)) + @machine.guest.capability(:docker_install, @version) + + if !@machine.guest.capability(:docker_installed) + raise DockerError, :install_failed + end + end + + if @machine.guest.capability?(:docker_configure_auto_start) + @machine.guest.capability(:docker_configure_auto_start) + else + @machine.env.ui.warn I18n.t('vagrant.docker_auto_start_not_available') + end + + if @machine.guest.capability?(:docker_configure_vagrant_user) + @machine.guest.capability(:docker_configure_vagrant_user) + end + end + end + end +end diff --git a/plugins/provisioners/docker/plugin.rb b/plugins/provisioners/docker/plugin.rb new file mode 100644 index 000000000..72bc8463d --- /dev/null +++ b/plugins/provisioners/docker/plugin.rb @@ -0,0 +1,48 @@ +require "vagrant" + +module VagrantPlugins + module Docker + class Plugin < Vagrant.plugin("2") + name "docker" + description <<-DESC + Provides support for provisioning your virtual machines with + Docker images and containers. + DESC + + config(:docker, :provisioner) do + require_relative "config" + Config + end + + guest_capability("debian", "docker_install") do + require_relative "cap/debian/docker_install" + Cap::Debian::DockerInstall + end + + guest_capability("debian", "docker_configure_auto_start") do + require_relative "cap/debian/docker_configure_auto_start" + Cap::Debian::DockerConfigureAutoStart + end + + guest_capability("debian", "docker_configure_vagrant_user") do + require_relative "cap/debian/docker_configure_vagrant_user" + Cap::Debian::DockerConfigureVagrantUser + end + + guest_capability("debian", "docker_start_service") do + require_relative "cap/debian/docker_start_service" + Cap::Debian::DockerStartService + end + + guest_capability("linux", "docker_installed") do + require_relative "cap/linux/docker_installed" + Cap::Linux::DockerInstalled + end + + provisioner(:docker) do + require_relative "provisioner" + Provisioner + end + end + end +end diff --git a/plugins/provisioners/docker/provisioner.rb b/plugins/provisioners/docker/provisioner.rb new file mode 100644 index 000000000..eac195b22 --- /dev/null +++ b/plugins/provisioners/docker/provisioner.rb @@ -0,0 +1,43 @@ +require_relative "docker_client" +require_relative "docker_installer" + +module VagrantPlugins + module Docker + class DockerError < Vagrant::Errors::VagrantError + error_namespace("vagrant.provisioners.docker") + end + + # TODO: Improve handling of vagrant-lxc specifics (like checking for apparmor + # profile stuff + autocorrection) + class Provisioner < Vagrant.plugin("2", :provisioner) + def initialize(machine, config, installer = nil, client = nil) + super(machine, config) + + # TODO: Rename to installer / client (drop docker suffix) + @installer = installer || DockerInstaller.new(@machine, config.version) + @client = client || DockerClient.new(@machine) + end + + def provision + @logger = Log4r::Logger.new("vagrant::provisioners::docker") + + @logger.info("Checking for Docker installation...") + @installer.ensure_installed + + # Attempt to start service if not running + @client.start_service + raise DockerError, :not_running if !@client.daemon_running? + + if config.images.any? + @machine.ui.info(I18n.t("vagrant.docker_pulling_images")) + @client.pull_images(*config.images) + end + + if config.containers.any? + @machine.ui.info(I18n.t("vagrant.docker_starting_containers")) + @client.run(config.containers) + end + end + end + end +end diff --git a/templates/locales/en.yml b/templates/locales/en.yml index d4ad49eca..f89789c63 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -42,6 +42,21 @@ en: to automatically delete Chef nodes and clients. chef_run_list_empty: |- Warning: Chef run list is empty. This may not be what you want. + + docker_installing: |- + Installing Docker (%{version}) onto machine... + docker_pulling_images: + Pulling Docker images... + docker_pulling_single: |- + -- Image: %{name} + docker_running: |- + -- Container: %{name} + docker_starting_containers: + Starting Docker containers... + docker_auto_start_not_available: |- + Unable to configure automatic restart of Docker containers on + the guest machine + provisioner_cleanup: |- Running cleanup tasks for '%{name}' provisioner... @@ -1317,3 +1332,7 @@ en: playbook_path_invalid: "`playbook` for the Ansible provisioner does not exist on the host system: %{path}" inventory_path_invalid: "`inventory_path` for the Ansible provisioner does not exist on the host system: %{path}" extra_vars_invalid: "`extra_vars` for the Ansible provisioner must be a hash or a path to an existing file. Received: %{value} (as %{type})" + + docker: + not_running: "Docker is not running on the guest VM." + install_failed: "Docker installation failed." diff --git a/website/docs/source/layouts/layout.erb b/website/docs/source/layouts/layout.erb index 9f957dff3..33b51de4e 100644 --- a/website/docs/source/layouts/layout.erb +++ b/website/docs/source/layouts/layout.erb @@ -150,6 +150,7 @@