Skip to content

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