Examples
mixed-office
A hybrid environment: a Windows file server (a VM, it has no container form) next to an intranet web app (a container), on one network. The hybrid target runs each the light way; vagrant and the other VM targets run both as VMs. The checks reach both from the inside.
Tested: Shows the hybrid target: a Windows file-server VM next to a container intranet on one network.
version: 1
name: mixed-office
networks:
office: { cidr: 192.168.58.0/24 }
machines:
files01:
networks: { office: 10 }
services: [{ port: 445 }]
resources: { cpus: 2, memory_mb: 3072 }
vm:
os: windows-server-2019
provision: [provision/share.ps1]
intranet:
networks: { office: 20 }
services: [{ port: 80, http: true, publish: 8083 }]
docker: { image: nginx:1.27 }
vm:
os: debian-12
provision: [provision/intranet.sh]
checks:
- checks/both-answer.sh
Targets
hybrid, vagrant, proxmox, cloud-vm. See Targets for what each means.
Checks
checks/both-answer.sh
See Checks.
Generated files
What isoloom generate writes for this spec, as committed next to it. Don't edit them: change
the spec and generate again (isoloom check fails in CI when they're out of date).
.isoloom/vagrant/Vagrantfile
# Generated by isoloom from isoloom.yml. Don't edit: change isoloom.yml and run
# `isoloom generate`. `isoloom check` fails when this file is out of date.
# Start: cd .isoloom/vagrant && vagrant up
# Stop: cd .isoloom/vagrant && vagrant destroy -f
ROOT = File.expand_path("../..", __dir__)
# Copied into each VM: the project, without version control or generated files.
PROJECT = Dir.children(ROOT).reject { |e| [".git", ".isoloom", ".vagrant"].include?(e) }.sort
# ESXi (vagrant-vmware-esxi): ESXI_VIRTUAL_NETWORK lists port groups, comma-separated: the
# management network first, then one per network, in order (office); a missing one reuses the last.
ESXI_NETWORKS = ENV.fetch("ESXI_VIRTUAL_NETWORK", "VM Network").split(",").map(&:strip)
LAB_NETWORKS = ["office"]
def esxi_networks(nets)
[ESXI_NETWORKS[0]] + nets.map { |n| ESXI_NETWORKS[1 + LAB_NETWORKS.index(n)] || ESXI_NETWORKS[-1] }
end
Vagrant.configure("2") do |config|
config.vm.synced_folder ".", "/vagrant", disabled: true
config.vm.boot_timeout = 900
config.vm.define "files01" do |m|
m.vm.box = "StefanScherer/windows_2019"
m.vm.box_version = "2021.05.15"
m.vm.box_architecture = "amd64"
m.vm.hostname = "files01"
m.vm.network "forwarded_port", guest: 3389, host: 3389, id: "rdp", disabled: true
m.vm.guest = :windows
m.vm.communicator = "winrm"
m.winrm.username = "vagrant"
m.winrm.password = "vagrant"
m.winrm.transport = :plaintext
m.winrm.basic_auth_only = true
m.winrm.retry_limit = 30
m.winrm.retry_delay = 10
m.vm.network "private_network", ip: "192.168.58.10", netmask: "255.255.255.0", virtualbox__intnet: "isoloom-mixed-office-office", libvirt__network_name: "isoloom-mixed-office-office", libvirt__dhcp_enabled: false
m.vm.provider "virtualbox" do |v|
v.name = "mixed-office · files01"
v.cpus = 2
v.memory = 3072
end
m.vm.provider "vmware_desktop" do |v|
v.vmx["displayName"] = "mixed-office · files01"
v.vmx["numvcpus"] = "2"
v.vmx["memsize"] = "3072"
end
m.vm.provider "parallels" do |v|
v.name = "mixed-office · files01"
v.cpus = 2
v.memory = 3072
end
m.vm.provider "utm" do |v|
v.name = "mixed-office · files01"
v.cpus = 2
v.memory = 3072
end
m.vm.provider "qemu" do |v|
v.smp = "cpus=2"
v.memory = "3072M"
end
m.vm.provider "vmware_esxi" do |v|
v.esxi_hostname = ENV.fetch("ESXI_HOSTNAME", "")
v.esxi_hostport = ENV.fetch("ESXI_HOSTPORT", "22").to_i
v.esxi_username = ENV.fetch("ESXI_USERNAME", "root")
v.esxi_password = "env:ESXI_PASSWORD"
v.esxi_disk_store = ENV["ESXI_DATASTORE"] if ENV["ESXI_DATASTORE"]
v.esxi_virtual_network = esxi_networks(["office"])
v.guest_name = "mixed-office-files01"
v.guest_numvcpus = 2
v.guest_memsize = 3072
end
m.vm.provider "libvirt" do |v|
v.cpus = 2
v.memory = 3072
end
m.vm.provision "shell", name: "hosts", inline: "$h = \"$env:SystemRoot\\System32\\drivers\\etc\\hosts\"; foreach ($l in @('192.168.58.20 intranet')) { if (-not (Select-String -Path $h -SimpleMatch $l -Quiet)) { Add-Content -Path $h -Value $l } }"
m.vm.provision "shell", name: "provision/share.ps1", path: File.join(ROOT, "provision/share.ps1")
end
config.vm.define "intranet" do |m|
m.vm.box = "bento/debian-12"
m.vm.box_architecture = "amd64"
m.vm.hostname = "intranet"
m.vm.network "private_network", ip: "192.168.58.20", netmask: "255.255.255.0", virtualbox__intnet: "isoloom-mixed-office-office", libvirt__network_name: "isoloom-mixed-office-office", libvirt__dhcp_enabled: false
m.vm.network "forwarded_port", guest: 80, host: 8083, host_ip: "127.0.0.1"
m.vm.provider "virtualbox" do |v|
v.name = "mixed-office · intranet"
v.cpus = 1
v.memory = 1024
end
m.vm.provider "vmware_desktop" do |v|
v.vmx["displayName"] = "mixed-office · intranet"
v.vmx["numvcpus"] = "1"
v.vmx["memsize"] = "1024"
end
m.vm.provider "parallels" do |v|
v.name = "mixed-office · intranet"
v.cpus = 1
v.memory = 1024
end
m.vm.provider "utm" do |v|
v.name = "mixed-office · intranet"
v.cpus = 1
v.memory = 1024
end
m.vm.provider "qemu" do |v|
v.smp = "cpus=1"
v.memory = "1024M"
end
m.vm.provider "vmware_esxi" do |v|
v.esxi_hostname = ENV.fetch("ESXI_HOSTNAME", "")
v.esxi_hostport = ENV.fetch("ESXI_HOSTPORT", "22").to_i
v.esxi_username = ENV.fetch("ESXI_USERNAME", "root")
v.esxi_password = "env:ESXI_PASSWORD"
v.esxi_disk_store = ENV["ESXI_DATASTORE"] if ENV["ESXI_DATASTORE"]
v.esxi_virtual_network = esxi_networks(["office"])
v.guest_name = "mixed-office-intranet"
v.guest_numvcpus = 1
v.guest_memsize = 1024
end
m.vm.provider "libvirt" do |v, o|
o.vm.box = "generic/debian12"
v.cpus = 1
v.memory = 1024
end
PROJECT.each do |entry|
m.vm.provision "file", source: File.join(ROOT, entry), destination: "/tmp/isoloom-project/#{entry}"
end
m.vm.provision "shell", name: "project", inline: "rm -rf /opt/isoloom && mv /tmp/isoloom-project /opt/isoloom"
m.vm.provision "shell", name: "provision/intranet.sh", inline: "cd /opt/isoloom && sh provision/intranet.sh"
end
config.vm.define "isoloom-controller" do |m|
m.vm.box = "bento/debian-12"
m.vm.hostname = "isoloom-controller"
m.vm.network "private_network", ip: "192.168.58.253", netmask: "255.255.255.0", virtualbox__intnet: "isoloom-mixed-office-office", libvirt__network_name: "isoloom-mixed-office-office", libvirt__dhcp_enabled: false
m.vm.provider "virtualbox" do |v|
v.name = "mixed-office · controller"
v.cpus = 1
v.memory = 1024
end
m.vm.provider "vmware_desktop" do |v|
v.vmx["displayName"] = "mixed-office · controller"
v.vmx["numvcpus"] = "1"
v.vmx["memsize"] = "1024"
end
m.vm.provider "parallels" do |v|
v.name = "mixed-office · controller"
v.cpus = 1
v.memory = 1024
end
m.vm.provider "libvirt" do |v, o|
o.vm.box = "generic/debian12"
v.cpus = 1
v.memory = 1024
end
m.vm.provider "vmware_esxi" do |v|
v.esxi_hostname = ENV.fetch("ESXI_HOSTNAME", "")
v.esxi_hostport = ENV.fetch("ESXI_HOSTPORT", "22").to_i
v.esxi_username = ENV.fetch("ESXI_USERNAME", "root")
v.esxi_password = "env:ESXI_PASSWORD"
v.esxi_disk_store = ENV["ESXI_DATASTORE"] if ENV["ESXI_DATASTORE"]
v.esxi_virtual_network = esxi_networks(["office"])
v.guest_name = "mixed-office-controller"
v.guest_numvcpus = 1
v.guest_memsize = 1024
end
m.vm.provision "shell", name: "hosts", inline: "for l in '192.168.58.10 files01' '192.168.58.20 intranet'; do grep -qxF \"$l\" /etc/hosts || echo \"$l\" >> /etc/hosts; done"
PROJECT.each do |entry|
m.vm.provision "file", source: File.join(ROOT, entry), destination: "/tmp/isoloom-project/#{entry}"
end
m.vm.provision "shell", name: "project", inline: "rm -rf /opt/isoloom && mv /tmp/isoloom-project /opt/isoloom"
m.vm.provision "shell", name: "controller", inline: <<~'SH'
set -e
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq python3-venv sshpass curl netcat-openbsd >/dev/null
[ -x /opt/ansible/bin/ansible-playbook ] || { python3 -m venv /opt/ansible && /opt/ansible/bin/pip install -q 'ansible-core>=2.15,<2.17' pywinrm; }
mkdir -p /etc/isoloom
cat > /etc/isoloom/inventory.ini <<'INV'
[linux]
intranet ansible_host=192.168.58.20
[windows]
files01 ansible_host=192.168.58.10 ansible_port=5985 ansible_winrm_scheme=http ansible_winrm_transport=basic
[linux:vars]
ansible_user=vagrant
ansible_password=vagrant
ansible_become=true
[windows:vars]
ansible_user=vagrant
ansible_password=vagrant
ansible_connection=winrm
ansible_winrm_server_cert_validation=ignore
ansible_winrm_operation_timeout_sec=400
ansible_winrm_read_timeout_sec=500
INV
SH
m.vm.provision "shell", name: "checks", run: "never", inline: <<~'SH'
export PATH=/opt/ansible/bin:$PATH ANSIBLE_HOST_KEY_CHECKING=False
failed=0
echo '== checks/both-answer.sh'
(cd /opt/isoloom && sh checks/both-answer.sh) || failed=1
exit $failed
SH
end
end
.isoloom/hybrid/Vagrantfile
# Generated by isoloom from isoloom.yml. Don't edit: change isoloom.yml and run
# `isoloom generate`. `isoloom check` fails when this file is out of date.
# Start: cd .isoloom/hybrid && vagrant up
# Stop: cd .isoloom/hybrid && vagrant destroy -f
ROOT = File.expand_path("../..", __dir__)
# Copied into each VM: the project, without version control or generated files.
PROJECT = Dir.children(ROOT).reject { |e| [".git", ".isoloom", ".vagrant"].include?(e) }.sort
# ESXi (vagrant-vmware-esxi): ESXI_VIRTUAL_NETWORK lists port groups, comma-separated: the
# management network first, then one per network, in order (office); a missing one reuses the last.
ESXI_NETWORKS = ENV.fetch("ESXI_VIRTUAL_NETWORK", "VM Network").split(",").map(&:strip)
LAB_NETWORKS = ["office"]
def esxi_networks(nets)
[ESXI_NETWORKS[0]] + nets.map { |n| ESXI_NETWORKS[1 + LAB_NETWORKS.index(n)] || ESXI_NETWORKS[-1] }
end
Vagrant.configure("2") do |config|
config.vm.synced_folder ".", "/vagrant", disabled: true
config.vm.boot_timeout = 900
config.vm.define "files01" do |m|
m.vm.box = "StefanScherer/windows_2019"
m.vm.box_version = "2021.05.15"
m.vm.box_architecture = "amd64"
m.vm.hostname = "files01"
m.vm.network "forwarded_port", guest: 3389, host: 3389, id: "rdp", disabled: true
m.vm.guest = :windows
m.vm.communicator = "winrm"
m.winrm.username = "vagrant"
m.winrm.password = "vagrant"
m.winrm.transport = :plaintext
m.winrm.basic_auth_only = true
m.winrm.retry_limit = 30
m.winrm.retry_delay = 10
m.vm.network "private_network", ip: "192.168.58.10", netmask: "255.255.255.0", virtualbox__intnet: "isoloom-mixed-office-office", libvirt__network_name: "isoloom-mixed-office-office", libvirt__dhcp_enabled: false
m.vm.provider "virtualbox" do |v|
v.name = "mixed-office · files01"
v.cpus = 2
v.memory = 3072
end
m.vm.provider "vmware_desktop" do |v|
v.vmx["displayName"] = "mixed-office · files01"
v.vmx["numvcpus"] = "2"
v.vmx["memsize"] = "3072"
end
m.vm.provider "parallels" do |v|
v.name = "mixed-office · files01"
v.cpus = 2
v.memory = 3072
end
m.vm.provider "utm" do |v|
v.name = "mixed-office · files01"
v.cpus = 2
v.memory = 3072
end
m.vm.provider "qemu" do |v|
v.smp = "cpus=2"
v.memory = "3072M"
end
m.vm.provider "vmware_esxi" do |v|
v.esxi_hostname = ENV.fetch("ESXI_HOSTNAME", "")
v.esxi_hostport = ENV.fetch("ESXI_HOSTPORT", "22").to_i
v.esxi_username = ENV.fetch("ESXI_USERNAME", "root")
v.esxi_password = "env:ESXI_PASSWORD"
v.esxi_disk_store = ENV["ESXI_DATASTORE"] if ENV["ESXI_DATASTORE"]
v.esxi_virtual_network = esxi_networks(["office"])
v.guest_name = "mixed-office-files01"
v.guest_numvcpus = 2
v.guest_memsize = 3072
end
m.vm.provider "libvirt" do |v|
v.cpus = 2
v.memory = 3072
end
m.vm.provision "shell", name: "hosts", inline: "$h = \"$env:SystemRoot\\System32\\drivers\\etc\\hosts\"; foreach ($l in @('192.168.58.20 intranet')) { if (-not (Select-String -Path $h -SimpleMatch $l -Quiet)) { Add-Content -Path $h -Value $l } }"
m.vm.provision "shell", name: "provision/share.ps1", path: File.join(ROOT, "provision/share.ps1")
end
config.vm.define "isoloom-controller" do |m|
m.vm.box = "bento/debian-12"
m.vm.hostname = "isoloom-controller"
m.vm.network "private_network", ip: "192.168.58.253", netmask: "255.255.255.0", virtualbox__intnet: "isoloom-mixed-office-office", libvirt__network_name: "isoloom-mixed-office-office", libvirt__dhcp_enabled: false
m.vm.provider "virtualbox" do |v|
v.name = "mixed-office · controller"
v.cpus = 1
v.memory = 1024
end
m.vm.provider "vmware_desktop" do |v|
v.vmx["displayName"] = "mixed-office · controller"
v.vmx["numvcpus"] = "1"
v.vmx["memsize"] = "1024"
end
m.vm.provider "parallels" do |v|
v.name = "mixed-office · controller"
v.cpus = 1
v.memory = 1024
end
m.vm.provider "libvirt" do |v, o|
o.vm.box = "generic/debian12"
v.cpus = 1
v.memory = 1024
end
m.vm.provider "vmware_esxi" do |v|
v.esxi_hostname = ENV.fetch("ESXI_HOSTNAME", "")
v.esxi_hostport = ENV.fetch("ESXI_HOSTPORT", "22").to_i
v.esxi_username = ENV.fetch("ESXI_USERNAME", "root")
v.esxi_password = "env:ESXI_PASSWORD"
v.esxi_disk_store = ENV["ESXI_DATASTORE"] if ENV["ESXI_DATASTORE"]
v.esxi_virtual_network = esxi_networks(["office"])
v.guest_name = "mixed-office-controller"
v.guest_numvcpus = 1
v.guest_memsize = 1024
end
m.vm.provision "shell", name: "hosts", inline: "for l in '192.168.58.10 files01' '192.168.58.20 intranet'; do grep -qxF \"$l\" /etc/hosts || echo \"$l\" >> /etc/hosts; done"
PROJECT.each do |entry|
m.vm.provision "file", source: File.join(ROOT, entry), destination: "/tmp/isoloom-project/#{entry}"
end
m.vm.provision "shell", name: "project", inline: "rm -rf /opt/isoloom && mv /tmp/isoloom-project /opt/isoloom"
m.vm.provision "shell", name: "controller", inline: <<~'SH'
set -e
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq python3-venv sshpass curl netcat-openbsd >/dev/null
[ -x /opt/ansible/bin/ansible-playbook ] || { python3 -m venv /opt/ansible && /opt/ansible/bin/pip install -q 'ansible-core>=2.15,<2.17' pywinrm; }
mkdir -p /etc/isoloom
cat > /etc/isoloom/inventory.ini <<'INV'
[linux]
[windows]
files01 ansible_host=192.168.58.10 ansible_port=5985 ansible_winrm_scheme=http ansible_winrm_transport=basic
[linux:vars]
ansible_user=vagrant
ansible_password=vagrant
ansible_become=true
[windows:vars]
ansible_user=vagrant
ansible_password=vagrant
ansible_connection=winrm
ansible_winrm_server_cert_validation=ignore
ansible_winrm_operation_timeout_sec=400
ansible_winrm_read_timeout_sec=500
INV
SH
m.vm.provision "shell", name: "checks", run: "never", inline: <<~'SH'
export PATH=/opt/ansible/bin:$PATH ANSIBLE_HOST_KEY_CHECKING=False
failed=0
echo '== checks/both-answer.sh'
(cd /opt/isoloom && sh checks/both-answer.sh) || failed=1
exit $failed
SH
end
# The containers' host: on their networks, letting Docker give each its own address there.
config.vm.define "isoloom-docker" do |m|
m.vm.box = "bento/debian-12"
m.vm.hostname = "isoloom-docker"
m.vm.network "forwarded_port", guest: 8083, host: 8083, host_ip: "127.0.0.1"
m.vm.network "private_network", ip: "192.168.58.253", auto_config: false, mac: "0A15574BB300", virtualbox__intnet: "isoloom-mixed-office-office"
m.vm.provider "virtualbox" do |v|
v.name = "mixed-office · containers"
v.cpus = 2
v.memory = 1536
v.customize ["modifyvm", :id, "--nicpromisc2", "allow-all"]
end
m.vm.provision "shell", name: "docker", inline: "command -v docker >/dev/null || curl -fsSL https://get.docker.com | sh"
PROJECT.each do |entry|
m.vm.provision "file", source: File.join(ROOT, entry), destination: "/tmp/isoloom-project/#{entry}"
end
m.vm.provision "file", source: File.join(__dir__, "compose.yml"), destination: "/tmp/isoloom-project/.isoloom/hybrid/compose.yml"
m.vm.provision "shell", name: "project", inline: "rm -rf /opt/isoloom && mv /tmp/isoloom-project /opt/isoloom"
m.vm.provision "shell", name: "networks", inline: <<~'SH'
set -e
IF=$(ip -o link | grep -i '0a:15:57:4b:b3:00' | awk -F': ' '{print $2}')
ip link set "$IF" up promisc on
docker network inspect isoloom-mixed-office-office >/dev/null 2>&1 || docker network create -d macvlan --subnet 192.168.58.0/24 --gateway 192.168.58.1 -o parent="$IF" isoloom-mixed-office-office
SH
m.vm.provision "shell", name: "containers", inline: "cd /opt/isoloom && docker compose -f .isoloom/hybrid/compose.yml up -d --build --wait --wait-timeout 900"
end
end
.isoloom/hybrid/compose.yml
# Generated by isoloom from isoloom.yml. Don't edit: change isoloom.yml and run
# `isoloom generate`. `isoloom check` fails when this file is out of date.
# The containers of a hybrid environment: run on the isoloom-docker VM, on the networks it
# creates there (macvlan, shared with the VMs). Started by the Vagrantfile next to it.
name: mixed-office
services:
intranet:
image: nginx:1.27
platform: linux/amd64
hostname: intranet
networks:
office:
ipv4_address: 192.168.58.20
gw_priority: 1
isoloom-publish: null
ports:
- 8083:80
healthcheck:
test:
- CMD-SHELL
- (nc -z 127.0.0.1 80 2>/dev/null || bash -c '</dev/tcp/127.0.0.1/80' 2>/dev/null)
interval: 5s
timeout: 3s
retries: 60
start_period: 10s
labels:
isoloom.service.80: http:80
restart: unless-stopped
extra_hosts:
- files01:192.168.58.10
networks:
isoloom-publish: null
office:
external: true
name: isoloom-mixed-office-office