runsisi's

technical notes

使用 Vagrant 搭建虚机环境

2019-01-12 runsisi#devops

Vagrant 是一个单机版的虚机管理系统,和 OpenStack 类似,但远远比 OpenStack 轻量级,当然提供的功能也要少很多,因为它的定位非常简单:Vagrant enables users to create and configure lightweight, reproducible, and portable development environments。说简单点就是可以通过命令行基于基础镜像非常容易的定义、构建以及销毁虚机。

Vagrant 是一个跨平台软件,支持 Linux/Windows/Mac 三大平台,在 Docker 火起来之前,github 上的开源项目多会提供 Vagrantfile 供用户搭建测试环境,在该文件里可以通过 ruby 脚本定义虚机参数,通过 ansible/salt 或者简单的 shell 脚本初始化虚机,当然随着 Docker 的普及,很多项目逐渐有用 Dockerfile 替代 Vagrantfile 的趋势。

虽然 Docker 很火,但仍然有它不适用的地方,OpenStack 虽然很强,但太复杂,很多时候我们的需求非常简单,几台可以快速创建、销毁的虚机,仅此而已,此时 Vagrant 完全可以满足需求。

制作镜像

与 docker pull 类似,vagrant box add 可以从网上拉取镜像(镜像在这里只是一个通用的名词而已,类似于基础系统模板等,并不真是 OpenStack Glance 这种镜像),当然由于虚机的镜像通常很大,因此我们一般都是在本地通过 VirtualBox 手工创建虚机(有 veewee[1] 这样的工具实现虚机创建的自动化),然后基于这个虚机创建一个通用的镜像,也有标准的 Packer[2] 工具制作镜像(不止是支持 Vagrant 也支持 OpenStack 等各种常见的镜像格式)。这里我们以在 Ubuntu 16.04 机器上手工创建一个 CentOS 7.3 系统的基础镜像为例,介绍基础镜像的制作过程。

  1. 当然第一步是安装 VirtualBox 和 vagrant(注意不要使用系统自带的 apt 源中的 virtualbox,会导致主机挂死),可以直接去 VirtualBox 官网下载 deb 安装包,也可以使用官方的 apt 源:
~$ wget -q https://www.virtualbox.org/download/oracle_vbox_2016.asc -O- | sudo apt-key add -
~$ sudo vi /etc/apt/source.list
deb https://download.virtualbox.org/virtualbox/debian xenial contrib
~$ sudo apt update && sudo apt install virtualbox-5.2 dkms

去 Vagrant 官网下载 deb 安装包并安装:

~$ sudo gdebi vagrant_2.0.2_x86_64.deb
Reading package lists... Done
Building dependency tree
Reading state information... Done
Reading state information... Done

no description given
Do you want to install the software package? [y/N]:

本文以 VirtualBox 5.2 和 Vagrant 2.0 版本为例。

如果安装 VirtualBox 时报内核模块的错(此时虽然可以打开 VirtualBox 界面创建虚机,但运行虚机同样会报错),建议更新系统并重启,然后执行如下命令重新编译 VirtualBox 内核模块:

~$ sudo /sbin/rcvboxdrv setup

也可以使用下面的命令实现:

~$ sudo /sbin/vboxconfig
  1. 打开 VirtualBox 图形界面,用以往所熟悉的方式安装 CentOS 7.3 系统,在虚机硬件配置时可以去掉 Audio, USB 的支持(而且如果不去掉的话,可能会报如下错误,虽然可以给 vagrant 所在机器的 VirtualBox 安装扩展模块解决,但毕竟比较麻烦),通常来说我们都用不到这些功能;
Stderr: VBoxManage: error: Implementation of the USB 2.0 controller not found!
VBoxManage: error: Because the USB 2.0 controller state is part of the saved VM state, the VM cannot be started. To fix this problem, either install the 'Oracle VM VirtualBox Extension Pack' or disable USB 2.0 support in the VM settings.
VBoxManage: error: Note! This error could also mean that an incompatible version of the 'Oracle VM VirtualBox Extension Pack' is installed (VERR_NOT_FOUND)
VBoxManage: error: Details: code NS_ERROR_FAILURE (0x80004005), component ConsoleWrap, interface IConsole

扩展模块可以通过命令行安装:

~$ sudo VBoxManage extpack install Oracle_VM_VirtualBox_Extension_Pack-5.2.22.vbox-extpack
VirtualBox Extension Pack Personal Use and Evaluation License (PUEL)
...

0%...10%...20%...30%...40%...50%...60%...70%...80%...90%...100%
Successfully installed "Oracle VM VirtualBox Extension Pack".

也可以通过图形界面安装:

extension

  1. 修改存储控制器的 Port Count,以便在后面定义虚机时使用该控制器可以挂载多块硬盘(但最近发现好像又不需要了,比较奇怪):

storage

  1. CentOS 安装之后网口默认不会启用,需要修改网口配置文件,将其中的 ONBOOT 选项改成 yes 并重启系统生效,否则后面通过 vagrant 创建的虚机无法正常登录,当然也无法通过 yum 进行软件的安装:
~$ sudo vi /etc/sysconfig/network-scripts/ifcfg-enpxxx
ONBOOT=yes
  1. 然后进入虚机配置源(使用官方的 Everything DVD 作为源,这样才不会将现有的系统升级,特别是一旦使用网络上的源会要求内核升级才能编译安装下一步骤中的 Guest Additions),安装 bzip2、kernel-devel 以及 gcc:
~$ sudo yum install bzip2 kernel-devel gcc
  1. 在 VirtualBox 的 Devices 菜单中的选择 Insert Guest Additions CD Image(如果提示找不到该 CD 则会自动去网络上下载,如果因为网络原因,可以自己按照提示用下载工具等下载,然后放入 ~/.config/VirtualBox/ 目录中),然后去虚机中 mount 该 CD 并安装 Guest Additions:
~$ sudo mount /dev/sr0 /media
~$ cd /media
~$ sudo ./VBoxLinuxAdditions.run
  1. 系统安装完可以做一些自己想要的优化,最好可以为普通用户增加无需密码的 sudo 权限,这样后面以普通用户执行需要管理员权限的命令时会比较方便(当然后面也可以以 root 用户登录,但如果是 Ubuntu 虚机要考虑到 Ubuntu 是禁止 root 用户登录的);
  2. 关闭刚才创建的虚机;
  3. 查看刚才创建的虚机:
~$ VBoxManage list vms
"centos7.3" {23b8aa39-acb7-4f19-88a5-f7a2dd973a56}
  1. 根据查询出来的虚机名字制作 Vagrant 镜像:
~$ vagrant package --base centos7.3 --output centos7.3.box
==> centos7.3: Exporting VM...
==> centos7.3: Compressing package to: /home/runsisi/centos7.3.box

这里 --base 参数指定的是刚才查询出来的虚机名称,如果虚机名称中有空格则需要使用双引号括起来,--output 参数指定的是输出的镜像文件名字,可以加上路径,没路径的话就输出在当前路径下;

  1. 注册刚才生成的镜像,这样后续定义虚机时才能引用到上一步制作的镜像:
~$ vagrant box add centos7.3 ./centos7.3.box
==> box: Box file was not detected as metadata. Adding it directly...
==> box: Adding box 'centos7.3' (v0) for provider:
    box: Unpacking necessary files from: file:///home/runsisi/centos7.3.box
==> box: Successfully added box 'centos7.3' (v0) for 'virtualbox'!

其中 add 子命令第一个参数指定的是镜像名字(当同时还指定 —name 参数时,–name 被忽略),可以随意定义,后续在定义虚机时会需要用到,此时可以删掉刚才生成的 centos7.3.box 文件,因为 vagrant box add 会拷贝一份到 Vagrant 自己的私有目录下(镜像库)。

注:这个镜像文件可以拷贝到其它机器上,并使用 vagrant box add 命令注册到该机器的 Vagrant 镜像库中。

至此虚机镜像制作完成,且已注册到 Vagrant 的镜像库中,可以通过如下命令查看当前机器所有已注册的镜像:

~$ vagrant box list
centos7.3 (virtualbox, 0)

也可以通过如下命令删除已注册的镜像:

~$ vagrant box remove centos7.3
Removing box 'centos7.3' (v0) with provider 'virtualbox'...

[1] https://github.com/jedi4ever/veewee

[2] https://github.com/hashicorp/packer

定义虚机

刚才我们注册了一个名字为 centos7.3 的镜像,我们可以以该镜像为基础定义虚机:

~$ vagrant init centos7.3
A `Vagrantfile` has been placed in this directory. You are now
ready to `vagrant up` your first virtual environment! Please read
the comments in the Vagrantfile as well as documentation on
`vagrantup.com` for more information on using Vagrant.

在当前目录下生成的 Vagrantfile 是一个 ruby 格式的虚机定义文件,可以修改该文件实现虚机(或多个虚机)的完全自定义。下面贴一个创建四个虚机的例子,更多的配置细节可以参考官方文档[1](不用太在意里面的细节,一般情况下照抄并根据自己的需求进行调整就行,比如磁盘的定义我也是以前自己参考 VBoxManage 的帮助文档摸索出来的,官方文档并没有):

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure(2) do |config|
  config.vm.box = "centos7.3"

  config.ssh.username = "runsisi"
  config.ssh.password = "123456"

  config.vm.provider "virtualbox" do |vb|
    vb.gui = false
    vb.cpus = 2
    vb.memory = "1024"
  end

  # define a single virtual machine
  config.vm.define "m" do |m|
    m.vm.hostname = "ceph"
    m.vm.network :private_network, ip: "192.168.32.2"
    m.vm.network :public_network, ip: "192.168.33.2", bridge: "enp2s0"
    # define one additional disk
    m.vm.provider "virtualbox" do |vb|
      dn = "disk0"
      df = "#{dn}.vdi"
      if ! File.file?(df)
        vb.customize [ "createmedium", "--filename", dn, "--size", "512000" ]
      end
      vb.customize [ "storageattach", :id, "--storagectl", "SATA", "--port", 1, "--device", 0, "--type", "hdd", "--medium", "#{df}" ]
    end
  end

  # define multiple virtual machines
  (0..2).each do |i|
    config.vm.define "m#{i}" do |m|
      m.vm.hostname = "ceph#{j}"
      m.vm.network :private_network, ip: "192.168.32.1#{i}"
      m.vm.network :public_network, ip: "192.168.33.1#{i}", bridge: "enp2s0"
      # define multiple additional disks
      (0..1).each do |d|
        m.vm.provider "virtualbox" do |vb|
          dn = "disk-#{i}-#{d}"
          df = "#{dn}.vdi"
          if ! File.file?(df)
            vb.customize [ "createmedium", "--filename", dn, "--size", "512000" ]
          end
          vb.customize [ "storageattach", :id, "--storagectl", "SATA", "--port", 1+d, "--device", 0, "--type", "hdd", "--medium", "#{df}" ]
        end
      end
    end
  end

  config.vm.provision "shell", inline: <<-SHELL
    echo "provision vm!!!"
  SHELL
end

其中的 public 网络为桥接网络,其中桥接口的名字 enp2s0 可以通过 VBoxManage 查到,private 网络为单台宿主机内部互通的网络(也就是虚机之间的通信不能跨宿主机,可以参考 Vagrant 的官方文档):

~$ VBoxManage list bridgedifs
Name:            enp2s0
GUID:            32706e65-3073-4000-8000-dc4a3e492ce7
DHCP:            Disabled
IPAddress:       10.120.130.140
NetworkMask:     255.255.255.0
IPV6Address:     fe80:0000:0000:0000:fc73:2001:0373:bd3a
IPV6NetworkMaskPrefixLength: 64
HardwareAddress: dc:4a:3e:49:2c:e7
MediumType:      Ethernet
Status:          Up
VBoxNetworkName: HostInterfaceNetworking-enp2s0

其中虚机已有的硬盘控制器信息可以通过如下命令查看(也可以直接在 VirtualBox 的虚机配置界面查看):

~$ VBoxManage showvminfo centos7.3
Storage Controller Name (0):            IDE
Storage Controller Name (1):            SATA

[1] https://www.vagrantup.com/docs/

设置虚机存储目录

默认情况,VirtualBox 把虚机存放在 ~/VirtualBox\ VMs/ 目录下,显然一旦虚机创建的比较多,用户目录的空间会不够,因此一般需要修改默认的虚机存放位置:

~$ VBoxManage setproperty machinefolder /data/vm

如果在后面启动虚机时出现如下的错误:

There was an error while executing `VBoxManage`, a CLI used by Vagrant
for controlling VirtualBox. The command and stderr is shown below.

Command: ["import", "/home/runsisi/.vagrant.d/boxes/centos7.5/0/virtualbox/box.ovf", "--vsys", "0", "--vmname", "centos7.5_1563605083543_41673", "--vsys", "0", "--unit", "14", "--disk", "/data/vm/centos7.5_1563605083543_41673/box-disk002.vmdk"]

Stderr: 0%...10%...20%...30%...40%...50%...60%...70%...80%...90%...100%
Interpreting /home/runsisi/.vagrant.d/boxes/centos7.5/0/virtualbox/box.ovf...
OK.
0%...
Progress state: NS_ERROR_INVALID_ARG
VBoxManage: error: Appliance import failed
VBoxManage: error: Code NS_ERROR_INVALID_ARG (0x80070057) - Invalid argument value (extended info not available)
VBoxManage: error: Context: "RTEXITCODE handleImportAppliance(HandlerArg*)" at line 957 of file VBoxManageAppliance.cpp

有两种可能,一是 /data/vm 目录没有空间,二是 VirtualBox 没有 /data/vm 目录的写权限。

启动、停止、销毁虚机

通过如下命令可以查看当前 Vagrantfile 定义的虚机的状态:

~$ vagrant status
Current machine states:

m                         not created (virtualbox)
m0                        not created (virtualbox)
m1                        not created (virtualbox)
m2                        not created (virtualbox)

不带参数的 vagrant up 可以启动所有虚机,而 vagrant up m 则可以只启动 m 这一台虚机,可以看到,这里的名字是 Vagrantfile 里 config.vm.define 语句定义的(后面的命令都以 m 这单个虚机为例)。

启动指定的虚机 m:

~$ vagrant up m
Bringing machine 'm' up with 'virtualbox' provider...
==> m: Importing base box 'centos7.3'...
==> m: Matching MAC address for NAT networking...
==> m: Setting the name of the VM: runsisi_m_1516862858996_17376
==> m: Fixed port collision for 22 => 2222. Now on port 2200.
==> m: Clearing any previously set network interfaces...

登录虚机 m(当然也可以直接在宿主机中使用 ssh 命令登录):

~$ vagrant ssh m
Last login: Thu Jan 25 14:55:22 2018 from 10.0.2.2

关闭虚机 m:

~$ vagrant ssh m
Last login: Thu Jan 25 14:55:22 2018 from 10.0.2.2

删除虚机 m:

~$ vagrant destroy m
    m: Are you sure you want to destroy the 'm' VM? [y/N] y
==> m: Forcing shutdown of VM...
==> m: Destroying VM and associated drives...

重启虚机 m:

~$ vagrant reload m

停止虚机 m(当然也可以在虚机内执行 shutdown 命令进行关机):

~$ vagrant halt m

Happy Vagrant!