Skip to content

Commit

Permalink
add hyperv support
Browse files Browse the repository at this point in the history
  • Loading branch information
rgl committed Jun 12, 2020
1 parent e57734f commit 3589373
Show file tree
Hide file tree
Showing 8 changed files with 258 additions and 30 deletions.
19 changes: 18 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
SHELL=bash
.SHELLFLAGS=-euo pipefail -c

VERSION=$(shell jq -r .variables.version ubuntu.json)

help:
@echo type make build-libvirt, make build-virtualbox or make build-vsphere
@echo type make build-libvirt, make build-virtualbox, make build-hyperv or make build-vsphere

build-libvirt: ubuntu-${VERSION}-amd64-libvirt.box
build-virtualbox: ubuntu-${VERSION}-amd64-virtualbox.box
build-hyperv: ubuntu-${VERSION}-amd64-hyperv.box
build-vsphere: ubuntu-${VERSION}-amd64-vsphere.box

ubuntu-${VERSION}-amd64-libvirt.box: preseed.txt provision.sh ubuntu.json Vagrantfile.template
Expand All @@ -23,6 +27,19 @@ ubuntu-${VERSION}-amd64-virtualbox.box: preseed.txt provision.sh ubuntu.json Vag
@echo to add to local vagrant install do:
@echo vagrant box add -f ubuntu-${VERSION}-amd64 ubuntu-${VERSION}-amd64-virtualbox.box

ubuntu-${VERSION}-amd64-hyperv.box: tmp/preseed-hyperv.txt provision.sh ubuntu.json Vagrantfile.template
rm -f $@
CHECKPOINT_DISABLE=1 PACKER_LOG=1 PACKER_LOG_PATH=$@.log \
packer build -only=ubuntu-${VERSION}-amd64-hyperv -on-error=abort -timestamp-ui ubuntu.json
@echo BOX successfully built!
@echo to add to local vagrant install do:
@echo vagrant box add -f ubuntu-${VERSION}-amd64 ubuntu-${VERSION}-amd64-hyperv.box

# see https://docs.microsoft.com/en-us/windows-server/virtualization/hyper-v/supported-ubuntu-virtual-machines-on-hyper-v
tmp/preseed-hyperv.txt: preseed.txt
mkdir -p tmp
sed -E 's,(d-i pkgsel/include string .+),\1 linux-image-virtual linux-tools-virtual linux-cloud-tools-virtual,g' preseed.txt >$@

ubuntu-${VERSION}-amd64-vsphere.box: tmp/preseed-vsphere.txt provision.sh ubuntu-vsphere.json Vagrantfile.template
rm -f $@
PACKER_KEY_INTERVAL=10ms CHECKPOINT_DISABLE=1 PACKER_LOG=1 PACKER_LOG_PATH=$@.log \
Expand Down
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Currently this targets [Ubuntu 20.04 (Focal Fossa)](https://wiki.ubuntu.com/Foca

# Usage

Install Packer 1.6+ and Vagrant 2.2.9+.

## Ubuntu Host

On a Ubuntu host, install the dependencies by running the file at:
Expand Down Expand Up @@ -34,6 +36,8 @@ For more information see the [Vagrant NFS documentation](https://www.vagrantup.c
On a Windows host, install [Chocolatey](https://chocolatey.org/install), then execute the following PowerShell commands in a Administrator PowerShell window:

```powershell
# NB if you want to use Hyper-V see the Hyper-V section in this document
# and do not install virtualbox at all.
choco install -y virtualbox --params "/NoDesktopShortcut /ExtensionPack"
choco install -y packer vagrant jq msys2
```
Expand Down Expand Up @@ -90,6 +94,67 @@ exit
vagrant destroy -f
```

## Hyper-V usage

Install [Hyper-V](https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/quick-start/enable-hyper-v)
and also install the `Windows Sandbox` feature (for some reason,
installing this makes DHCP work properly in the vEthernet Default Switch).

Make sure your user is in the `Hyper-V Administrators` group
or you run with Administrative privileges.

Make sure your Virtual Switch (its vEthernet network adapter) is excluded
from the Windows Firewall protected network connections by executing the
following commands in a bash shell with Administrative privileges:

```bash
PowerShell -Command 'Get-NetFirewallProfile | Select-Object -Property Name,DisabledInterfaceAliases'
PowerShell -Command 'Set-NetFirewallProfile -DisabledInterfaceAliases (Get-NetAdapter -name "vEthernet*" | Where-Object {$_.ifIndex}).InterfaceAlias'
```

Create the base image in a bash shell with Administrative privileges:

```bash
cat >secrets.sh <<EOF
# set this value when you need to set the VM Switch Name.
export HYPERV_SWITCH_NAME='Default Switch'
# set this value when you need to set the VM VLAN ID.
export HYPERV_VLAN_ID=''
# set the credentials that the guest will use
# to connect to this host smb share.
# NB you should create a new local user named _vagrant_share
# and use that one here instead of your user credentials.
# NB it would be nice for this user to have its credentials
# automatically rotated, if you implement that feature,
# let me known!
export VAGRANT_SMB_USERNAME='_vagrant_share'
export VAGRANT_SMB_PASSWORD=''
# remove the virtual switch from the windows firewall.
# NB execute if the VM fails to obtain an IP address from DHCP.
PowerShell -Command 'Set-NetFirewallProfile -DisabledInterfaceAliases (Get-NetAdapter -name "vEthernet*" | Where-Object {$_.ifIndex}).InterfaceAlias'
EOF
source secrets.sh
make build-hyperv
```

Try the example guest:

**NB** You will need Administrative privileges to create the SMB share.

```bash
cd example
# grant $VAGRANT_SMB_USERNAME full permissions to the
# current directory.
# NB you must first install the Carbon PowerShell module
# with choco install -y carbon.
# TODO set VM screen resolution.
PowerShell -Command 'Import-Module Carbon; Grant-Permission . $env:VAGRANT_SMB_USERNAME FullControl'
vagrant up --provider=hyperv
vagrant ssh
exit
vagrant destroy -f
```

## VMware vSphere usage

Download [govc](https://github.com/vmware/govmomi/releases/latest) and place it inside your `/usr/local/bin` directory.
Expand Down
95 changes: 69 additions & 26 deletions example/Vagrantfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
ENV['VAGRANT_EXPERIMENTAL'] = 'typed_triggers'

require 'base64'
require 'digest/sha1'
require 'fileutils'
Expand All @@ -8,7 +10,7 @@ $provision_username = 'vagrant'
$provision_password = 'abracadabra'

def gzip_base64(data)
o = StringIO.new()
o = StringIO.new()
w = Zlib::GzipWriter.new(o)
w.write(data)
w.close()
Expand All @@ -18,31 +20,34 @@ end
# add the cloud-init data as a NoCloud cloud-init iso.
# NB libvirtd libvirt-qemu:kvm MUST have read permissions to the iso file path.
# see https://cloudinit.readthedocs.io/en/latest/topics/datasources/nocloud.html
def create_cloud_init_iso(config, group, cloud_init_user_data, cloud_init_network_config)
def create_cloud_init_iso(cloud_init_data_path, env, config, group, cloud_init_user_data, cloud_init_network_config)
if !File.exists?(cloud_init_data_path) || File.mtime(cloud_init_data_path) < File.mtime(__FILE__)
cloud_init_data_parent_path = File.dirname(cloud_init_data_path)
FileUtils.mkdir_p(cloud_init_data_parent_path, :mode => 0750)
FileUtils.chown(nil, group, cloud_init_data_parent_path) if group
FileUtils.rm_f(cloud_init_data_path)
FileUtils.mkdir_p('tmp/cidata')
File.write('tmp/cidata/meta-data', '{}')
File.write('tmp/cidata/user-data', "#cloud-config\n#{cloud_init_user_data.to_json}")
File.write('tmp/cidata/network-config', cloud_init_network_config.to_json)
env.ui.info "Creating the cloud-init cidata.iso file at #{cloud_init_data_path}..."
raise 'Failed to execute xorriso to create the cloud-init cidata.iso file' unless system(
'xorriso',
'-as', 'genisoimage',
'-output', cloud_init_data_path,
'-volid', 'cidata',
'-joliet',
'-rock',
'tmp/cidata')
env.ui.info 'The cloud-init cidata.iso file was created as:'
system('iso-info', '--no-header', '-i', cloud_init_data_path)
end
end
def create_cloud_init_iso_trigger(config, group, cloud_init_user_data, cloud_init_network_config)
cloud_init_data_path = "#{ENV['TMP'] || '/tmp'}/cidata/cidata-#{Digest::SHA1.hexdigest(__FILE__)}.iso"
config.trigger.before :up do |trigger|
trigger.ruby do |env, machine|
if !File.exists?(cloud_init_data_path) || File.mtime(cloud_init_data_path) < File.mtime(__FILE__)
cloud_init_data_parent_path = File.dirname(cloud_init_data_path)
FileUtils.mkdir_p(cloud_init_data_parent_path, :mode => 0750)
FileUtils.chown(nil, group, cloud_init_data_parent_path)
FileUtils.rm_f(cloud_init_data_path)
FileUtils.mkdir_p('tmp/cidata')
File.write('tmp/cidata/meta-data', '{}')
File.write('tmp/cidata/user-data', "#cloud-config\n#{cloud_init_user_data.to_json}")
File.write('tmp/cidata/network-config', cloud_init_network_config.to_json)
env.ui.info 'Creating the cloud-init cidata.iso file...'
raise 'Failed to execute xorriso to create the cloud-init cidata.iso file' unless system(
'xorriso',
'-as', 'genisoimage',
'-output', cloud_init_data_path,
'-volid', 'cidata',
'-joliet',
'-rock',
'tmp/cidata')
env.ui.info 'The cloud-init cidata.iso file was created as:'
system('iso-info', '--no-header', '-i', cloud_init_data_path)
end
create_cloud_init_iso(cloud_init_data_path, env, config, group, cloud_init_user_data, cloud_init_network_config)
end
end
cloud_init_data_path
Expand Down Expand Up @@ -96,9 +101,9 @@ Vagrant.configure(2) do |config|
lv.memory = 2048
lv.cpus = 2
lv.cpu_mode = 'host-passthrough'
lv.nested = true
lv.nested = true # nested virtualization.
lv.keymap = 'pt'
lv.storage :file, :device => :cdrom, :path => create_cloud_init_iso(config, 'kvm', cloud_init_user_data, cloud_init_network_config)
lv.storage :file, :device => :cdrom, :path => create_cloud_init_iso_trigger(config, 'kvm', cloud_init_user_data, cloud_init_network_config)
# add example firmware string.
# NB name has a maximum of 55 ascii characters.
# NB this will be available at /sys/firmware/qemu_fw_cfg/by_name/opt/com.example/message/raw
Expand All @@ -121,7 +126,45 @@ Vagrant.configure(2) do |config|
'--device', 0,
'--port', 1,
'--type', 'dvddrive',
'--medium', create_cloud_init_iso(config, nil, cloud_init_user_data, cloud_init_network_config)]
'--medium', create_cloud_init_iso_trigger(config, nil, cloud_init_user_data, cloud_init_network_config)]
end

config.vm.provider 'hyperv' do |hv, config|
hv.vmname = "#{File.basename(File.dirname(File.dirname(__FILE__)))}-example"
hv.linked_clone = true
hv.memory = 2048
hv.cpus = 2
hv.enable_virtualization_extensions = true # nested virtualization.
hv.vlan_id = ENV['HYPERV_VLAN_ID']
# see https://github.com/hashicorp/vagrant/issues/7915
# see https://github.com/hashicorp/vagrant/blob/10faa599e7c10541f8b7acf2f8a23727d4d44b6e/plugins/providers/hyperv/action/configure.rb#L21-L35
config.vm.network :private_network, bridge: ENV['HYPERV_SWITCH_NAME'] if ENV['HYPERV_SWITCH_NAME']
config.vm.synced_folder '.', '/vagrant',
type: 'smb',
smb_username: ENV['VAGRANT_SMB_USERNAME'] || ENV['USER'],
smb_password: ENV['VAGRANT_SMB_PASSWORD']
# add the cloud-init data iso to the hyperv vm.
config.trigger.before :'VagrantPlugins::HyperV::Action::StartInstance', type: :action do |trigger|
trigger.ruby do |env, machine|
create_cloud_init_iso('tmp/cidata.iso', env, config, nil, cloud_init_user_data, cloud_init_network_config)
system(
'PowerShell',
'-NoLogo',
'-NoProfile',
'-ExecutionPolicy',
'Bypass',
'-Command',
<<~COMMAND
$vmName = '#{machine.provider_config.vmname}'
$drive = @(Get-VMDvdDrive $vmName | Where-Object {$_.Path -like '*cidata.iso'})
if (!$drive) {
Write-Host 'Adding the cidata.iso DVD to the VM...'
Add-VMDvdDrive $vmName -Path $PWD/tmp/cidata.iso
}
COMMAND
)
end
end
end

config.vm.provider 'vsphere' do |vsphere, override|
Expand Down
4 changes: 4 additions & 0 deletions provision-guest-additions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ elif [ -n "$(lspci | grep VMware | head -1)" ]; then
# no need to install the VMware Guest Additions as they were
# already installed from tmp/preseed-vsphere.txt.
exit 0
elif [ "$(cat /sys/devices/virtual/dmi/id/sys_vendor)" == 'Microsoft Corporation' ]; then
# no need to install the Hyper-V Guest Additions (aka Linux Integration Services)
# as they were already installed from tmp/preseed-hyperv.txt.
exit 0
else
echo 'ERROR: Unknown VM host.'
exit 1
Expand Down
5 changes: 5 additions & 0 deletions provision-local-hyperv.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
PowerShell ^
-NoLogo ^
-NoProfile ^
-ExecutionPolicy Bypass ^
-File provision-local-hyperv.ps1
35 changes: 35 additions & 0 deletions provision-local-hyperv.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# this has access to the following environment variables (this
# also shows example values):
#
# PACKER_VERSION 1.6.0
# PACKER_VM_NAME packer-ubuntu-20.04-amd64-hyperv
# PACKER_BUILD_NAME ubuntu-20.04-amd64-hyperv
# PACKER_BUILDER_TYPE hyperv-iso
# PACKER_HTTP_ADDR 10.0.0.123:1
# PACKER_HTTP_IP 10.0.0.123
# PACKER_HTTP_PORT 1

Set-StrictMode -Version Latest
$ProgressPreference = 'SilentlyContinue'
$ErrorActionPreference = 'Stop'
trap {
Write-Output "ERROR: $_"
Write-Output (($_.ScriptStackTrace -split '\r?\n') -replace '^(.*)$','ERROR: $1')
Write-Output (($_.Exception.ToString() -split '\r?\n') -replace '^(.*)$','ERROR EXCEPTION: $1')
Exit 1
}

Import-Module Hyper-V

# NB Trim is needed because something is adding a
# trailing space to the value.
$vmName = $env:PACKER_VM_NAME.Trim()

# update the vm notes.
$notes = (Get-VM $vmName).Notes
$notes += "---`n"
$notes += "packer_version: $env:PACKER_VERSION`n"
$notes += "git_url: $(git config --get remote.origin.url)`n"
$notes += "git_branch: $(git rev-parse --abbrev-ref HEAD)`n"
$notes += "git_revision: $(git rev-parse HEAD)`n"
Set-VM $vmName -Notes $notes
14 changes: 12 additions & 2 deletions provision.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ fi
# install the nfs client to support nfs synced folders in vagrant.
apt-get install -y nfs-common

# install the smb client to support cifs/smb/samba synced folders in vagrant.
apt-get install -y --no-install-recommends cifs-utils

# install rsync to support rsync synced folders in vagrant.
apt-get install -y rsync

Expand Down Expand Up @@ -92,15 +95,22 @@ systemctl stop systemd-random-seed
rm -f /var/lib/systemd/random-seed

# clean packages.
apt-get -y autoremove
apt-get -y autoremove --purge
apt-get -y clean

# zero the free disk space -- for better compression of the box file.
# NB prefer discard/trim (safer; faster) over creating a big zero filled file
# (somewhat unsafe as it has to fill the entire disk, which might trigger
# a disk (near) full alarm; slower; slightly better compression).
if [ "$(lsblk -no DISC-GRAN $(findmnt -no SOURCE /) | awk '{print $1}')" != '0B' ]; then
fstrim -v /
while true; do
output="$(fstrim -v /)"
cat <<<"$output"
sync && sync && sleep 15
if [ "$output" == '/: 0 B (0 bytes) trimmed' ]; then
break
fi
done
else
dd if=/dev/zero of=/EMPTY bs=1M || true; rm -f /EMPTY
fi
Loading

0 comments on commit 3589373

Please sign in to comment.