aboutsummaryrefslogtreecommitdiff
path: root/contrib/dev/vagrant
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/dev/vagrant')
-rw-r--r--contrib/dev/vagrant/Vagrantfile252
-rw-r--r--contrib/dev/vagrant/Vagrantfile.custom.example92
-rw-r--r--contrib/dev/vagrant/bootstrap.sh226
-rw-r--r--contrib/dev/vagrant/newest_dhcp_lease.py134
4 files changed, 704 insertions, 0 deletions
diff --git a/contrib/dev/vagrant/Vagrantfile b/contrib/dev/vagrant/Vagrantfile
new file mode 100644
index 000000000..f59fafaec
--- /dev/null
+++ b/contrib/dev/vagrant/Vagrantfile
@@ -0,0 +1,252 @@
+# -*- mode: ruby -*-
+# vi: set ft=ruby :
+
+require "ipaddr"
+
+# Check for required plugin(s)
+['vagrant-hostmanager'].each do |plugin|
+ unless Vagrant.has_plugin?(plugin)
+ raise "#{plugin} plugin not found. Please install it via 'vagrant plugin install #{plugin}'"
+ end
+end
+
+class VagrantConfigMissing < StandardError
+end
+
+vagrant_dir = File.expand_path(File.dirname(__FILE__))
+
+# Vagrantfile.custom contains user customization for the Vagrantfile
+# You shouldn't have to edit the Vagrantfile, ever.
+if File.exists?(File.join(vagrant_dir, 'Vagrantfile.custom'))
+ eval(IO.read(File.join(vagrant_dir, 'Vagrantfile.custom')), binding)
+end
+
+# Either libvirt or virtualbox
+PROVIDER ||= "libvirt"
+# Either centos or ubuntu
+DISTRO ||= "centos"
+
+# The libvirt graphics_ip used for each guest. Only applies if PROVIDER
+# is libvirt.
+GRAPHICSIP ||= "127.0.0.1"
+
+# The bootstrap.sh provision_script requires CentOS 7 or Ubuntu 15.10.
+# Provisioning other boxes than the default ones may therefore
+# require changes to bootstrap.sh.
+PROVISION_SCRIPT ||= "bootstrap.sh"
+
+PROVIDER_DEFAULTS ||= {
+ libvirt: {
+ centos: {
+ base_image: "centos/7",
+ bridge_interface: "virbr0",
+ vagrant_shared_folder: "/home/vagrant/sync",
+ sync_method: "nfs",
+ kolla_path: "/home/vagrant/kolla"
+ }
+ },
+ virtualbox: {
+ centos: {
+ base_image: "puppetlabs/centos-7.0-64-puppet",
+ bridge_interface: "wlp3s0b1",
+ vagrant_shared_folder: "/home/vagrant/sync",
+ sync_method: "virtualbox",
+ kolla_path: "/home/vagrant/kolla"
+ },
+ ubuntu: {
+ base_image: "ubuntu/wily64",
+ bridge_interface: "wlp3s0b1",
+ vagrant_shared_folder: "/home/vagrant/sync",
+ sync_method: "virtualbox",
+ kolla_path: "/home/vagrant/kolla"
+ }
+ }
+}
+
+# Whether the host network adapter is Wi-Fi.
+# On VirtualBox, the user must first manually create a NAT-Network
+# named "OSNetwork". The default network CIDR must be changed.
+# The Neutron external interface will be connected to this Network.
+WIFI = false unless self.class.const_defined?(:WIFI)
+
+# Whether to do Multi-node or All-in-One deployment
+MULTINODE = false unless self.class.const_defined?(:MULTINODE)
+
+# The following is only used when deploying in Multi-nodes
+NUMBER_OF_CONTROL_NODES ||= 3
+NUMBER_OF_COMPUTE_NODES ||= 1
+NUMBER_OF_STORAGE_NODES ||= 1
+NUMBER_OF_NETWORK_NODES ||= 1
+
+NODE_SETTINGS ||= {
+ aio: {
+ cpus: 4,
+ memory: 4096
+ },
+ operator: {
+ cpus: 1,
+ memory: 1024
+ },
+ control: {
+ cpus: 1,
+ memory: 2048
+ },
+ compute: {
+ cpus: 1,
+ memory: 1024
+ },
+ storage: {
+ cpus: 1,
+ memory: 1024
+ },
+ network: {
+ cpus: 1,
+ memory: 1024
+ }
+}
+
+# Configure a new SSH key and config so the operator is able to connect with
+# the other cluster nodes.
+unless File.file?(File.join(vagrant_dir, 'vagrantkey'))
+ system("ssh-keygen -f #{File.join(vagrant_dir, 'vagrantkey')} -N '' -C this-is-vagrant")
+end
+
+def get_default(setting)
+ PROVIDER_DEFAULTS[PROVIDER.to_sym][DISTRO.to_sym][setting]
+rescue
+ raise VagrantConfigMissing,
+ "Missing configuration for PROVIDER_DEFAULTS[#{PROVIDER}][#{DISTRO}][#{setting}]"
+end
+
+def get_setting(node, setting)
+ NODE_SETTINGS[node][setting]
+rescue
+ raise VagrantConfigMissing,
+ "Missing configuration for NODE_SETTINGS[#{node}][#{setting}]"
+end
+
+def configure_wifi_vbox_networking(vm)
+ # Even if adapters 1 & 2 don't need to be modified, if the order is to be
+ # maintained, some modification has to be done to them. This will maintain
+ # the association inside the guest OS: NIC1 -> eth0, NIC2 -> eth1, NIC3 ->
+ # eht2. The modifications for adapters 1 & 2 only change optional properties.
+ # Adapter 3 is enabled and connected to the NAT-Network named "OSNetwork",
+ # while also changing its optional properties. Since adapter 3 is used by
+ # Neutron for the external network, promiscuous mode is set to "allow-all".
+ # Also, use virtio as the adapter type, for better performance.
+ vm.customize ["modifyvm", :id, "--nictype1", "virtio"]
+ vm.customize ["modifyvm", :id, "--cableconnected1", "on"]
+ vm.customize ["modifyvm", :id, "--nicpromisc2", "deny"]
+ vm.customize ["modifyvm", :id, "--nictype2", "virtio"]
+ vm.customize ["modifyvm", :id, "--cableconnected2", "on"]
+ vm.customize ["modifyvm", :id, "--nic3", "natnetwork"]
+ vm.customize ["modifyvm", :id, "--nat-network3", "OSNetwork"]
+ vm.customize ["modifyvm", :id, "--nicpromisc3", "allow-all"]
+ vm.customize ["modifyvm", :id, "--nictype3", "virtio"]
+ vm.customize ["modifyvm", :id, "--cableconnected3", "on"]
+end
+
+def configure_wifi_if_enabled(vm)
+ if WIFI
+ case PROVIDER
+ when "virtualbox"
+ configure_wifi_vbox_networking(vm)
+# TODO(lucian-serb): Configure networking on Wi-Fi for other hypervisors.
+# when "libvirt"
+# configure_wifi_libvirt_networking(vm)
+ end
+ end
+end
+
+Vagrant.configure(2) do |config|
+ config.vm.box = get_default(:base_image)
+
+ # Next to the hostonly NAT-network there is a host-only network with all
+ # nodes attached. Plus, each node receives a 3rd adapter connected to the
+ # outside public network.
+ config.vm.network "private_network", type: "dhcp"
+ # On VirtualBox hosts with Wi-Fi, do not create a public bridged interface.
+ # A NAT-Network will be used instead.
+ # TODO(lucian-serb): Do the same for other hypervisors as well?
+ unless PROVIDER == "virtualbox" && WIFI
+ config.vm.network "public_network", dev: get_default(:bridge_interface), mode: 'bridge', type: 'bridge'
+ end
+
+ my_privatekey = File.read(File.join(vagrant_dir, "vagrantkey"))
+ my_publickey = File.read(File.join(vagrant_dir, "vagrantkey.pub"))
+
+ config.vm.provision :shell, inline: <<-EOS
+ mkdir -p /root/.ssh
+ echo '#{my_privatekey}' > /root/.ssh/id_rsa
+ chmod 600 /root/.ssh/id_rsa
+ echo '#{my_publickey}' > /root/.ssh/authorized_keys
+ chmod 600 /root/.ssh/authorized_keys
+ echo '#{my_publickey}' > /root/.ssh/id_rsa.pub
+ chmod 644 /root/.ssh/id_rsa.pub
+ mkdir -p /home/vagrant/.ssh
+ echo '#{my_privatekey}' >> /home/vagrant/.ssh/id_rsa
+ chmod 600 /home/vagrant/.ssh/*
+ echo 'Host *' > ~vagrant/.ssh/config
+ echo StrictHostKeyChecking no >> ~vagrant/.ssh/config
+ chown -R vagrant: /home/vagrant/.ssh
+ EOS
+
+ config.hostmanager.enabled = true
+ # Make sure hostmanager picks IP address of eth1
+ config.hostmanager.ip_resolver = proc do |vm, resolving_vm|
+ case PROVIDER
+ when "libvirt"
+ if vm.name
+ `python newest_dhcp_lease.py #{vm.name}`.chop
+ end
+ when "virtualbox"
+ if vm.id
+ `VBoxManage guestproperty get #{vm.id} "/VirtualBox/GuestInfo/Net/1/V4/IP"`.split()[1]
+ end
+ end
+ end
+
+ # The operator controls the deployment
+ config.vm.define "operator", primary: true do |admin|
+ admin.vm.hostname = "operator.local"
+ admin.vm.provision :shell, path: PROVISION_SCRIPT, args: "operator #{MULTINODE ? 'multinode' : 'aio'} #{get_default(:kolla_path)}"
+ admin.vm.synced_folder File.join(vagrant_dir, '..', '..'), get_default(:kolla_path), create:"True", type: get_default(:sync_method)
+ admin.vm.synced_folder File.join(vagrant_dir, 'storage', 'operator'), "/data/host", create:"True", type: get_default(:sync_method)
+ admin.vm.synced_folder File.join(vagrant_dir, 'storage', 'shared'), "/data/shared", create:"True", type: get_default(:sync_method)
+ admin.vm.synced_folder ".", get_default(:vagrant_shared_folder), disabled: true
+ admin.vm.provider PROVIDER do |vm|
+ vm.memory = MULTINODE ? get_setting(:operator, :memory) : get_setting(:aio, :memory)
+ vm.cpus = MULTINODE ? get_setting(:operator, :cpus) : get_setting(:aio, :cpus)
+ if PROVIDER == "libvirt"
+ vm.graphics_ip = GRAPHICSIP
+ end
+ configure_wifi_if_enabled(vm)
+ end
+ admin.hostmanager.aliases = "operator"
+ end
+
+ if MULTINODE
+ ['compute', 'storage', 'network', 'control'].each do |node_type|
+ (1..self.class.const_get("NUMBER_OF_#{node_type.upcase}_NODES")).each do |i|
+ hostname = "#{node_type}0#{i}"
+ config.vm.define hostname do |node|
+ node.vm.hostname = "#{hostname}.local"
+ node.vm.provision :shell, path: PROVISION_SCRIPT, args: "#{hostname} multinode #{get_default(:kolla_path)}"
+ node.vm.synced_folder File.join(vagrant_dir, 'storage', node_type), "/data/host", create:"True", type: get_default(:sync_method)
+ node.vm.synced_folder File.join(vagrant_dir, 'storage', 'shared'), "/data/shared", create:"True", type: get_default(:sync_method)
+ node.vm.synced_folder ".", get_default(:vagrant_shared_folder), disabled: true
+ node.vm.provider PROVIDER do |vm|
+ vm.memory = get_setting(node_type.to_sym, :memory)
+ vm.cpus = get_setting(node_type.to_sym, :cpus)
+ if PROVIDER == "libvirt"
+ vm.graphics_ip = GRAPHICSIP
+ end
+ configure_wifi_if_enabled(vm)
+ end
+ node.hostmanager.aliases = hostname
+ end
+ end
+ end
+ end
+
+end
diff --git a/contrib/dev/vagrant/Vagrantfile.custom.example b/contrib/dev/vagrant/Vagrantfile.custom.example
new file mode 100644
index 000000000..09af72f5b
--- /dev/null
+++ b/contrib/dev/vagrant/Vagrantfile.custom.example
@@ -0,0 +1,92 @@
+# -*- mode: ruby -*-
+# vi: set ft=ruby :
+
+# This file is an example of Vagrant configuration.
+# Copy it to Vagrantfile.custom and configure it to your liking to customize
+# the Vagrant deployment. The Vagrantfile.custom file is sourced by the
+# Vagrantfile, it has to be valid ruby code.
+
+# Either libvirt or virtualbox
+# PROVIDER = "libvirt"
+
+# Either centos or ubuntu
+# DISTRO = "centos"
+
+# The libvirt graphics_ip used for each guest. Only applies if PROVIDER
+# is libvirt.
+# GRAPHICSIP = "127.0.0.1"
+
+# The bootstrap.sh provision_script requires CentOS 7 or Ubuntu 15.10.
+# Provisioning other boxes than the default ones may therefore
+# require changes to bootstrap.sh.
+# PROVISION_SCRIPT = "bootstrap.sh"
+
+# PROVIDER_DEFAULTS = {
+# libvirt: {
+# centos: {
+# base_image: "centos/7",
+# bridge_interface: "virbr0",
+# vagrant_shared_folder: "/home/vagrant/sync",
+# sync_method: "nfs",
+# kolla_path: "/home/vagrant/kolla"
+# }
+# },
+# virtualbox: {
+# centos: {
+# base_image: "puppetlabs/centos-7.0-64-puppet",
+# bridge_interface: "wlp3s0b1",
+# vagrant_shared_folder: "/home/vagrant/sync",
+# sync_method: "virtualbox",
+# kolla_path: "/home/vagrant/kolla"
+# },
+# ubuntu: {
+# base_image: "ubuntu/wily64",
+# bridge_interface: "wlp3s0b1",
+# vagrant_shared_folder: "/home/vagrant/sync",
+# sync_method: "virtualbox",
+# kolla_path: "/home/vagrant/kolla"
+# }
+# }
+# }
+
+# Whether the host network adapter is Wi-Fi.
+# On VirtualBox, the user must first manually create a NAT-Network
+# named OSNetwork. The default network CIDR must be changed.
+# The Neutron external interface will be connected to this Network.
+# WIFI = false
+
+# Whether to do Multi-node or All-in-One deployment
+# MULTINODE = false
+
+# The following is only used when deploying in Multi-nodes
+# NUMBER_OF_CONTROL_NODES = 3
+# NUMBER_OF_COMPUTE_NODES = 1
+# NUMBER_OF_STORAGE_NODES = 1
+# NUMBER_OF_NETWORK_NODES = 1
+
+# NODE_SETTINGS = {
+# aio: {
+# cpus: 4,
+# memory: 4096
+# },
+# operator: {
+# cpus: 1,
+# memory: 1024
+# },
+# control: {
+# cpus: 1,
+# memory: 2048
+# },
+# compute: {
+# cpus: 1,
+# memory: 1024
+# },
+# storage: {
+# cpus: 1,
+# memory: 1024
+# },
+# network: {
+# cpus: 1,
+# memory: 1024
+# }
+# }
diff --git a/contrib/dev/vagrant/bootstrap.sh b/contrib/dev/vagrant/bootstrap.sh
new file mode 100644
index 000000000..ec89440b8
--- /dev/null
+++ b/contrib/dev/vagrant/bootstrap.sh
@@ -0,0 +1,226 @@
+#!/usr/bin/env bash
+#
+# Bootstrap script to configure all nodes.
+#
+# This script is intended to be used by vagrant to provision nodes.
+# To use it, set it as 'PROVISION_SCRIPT' inside your Vagrantfile.custom.
+# You can use Vagrantfile.custom.example as a template for this.
+
+VM=$1
+MODE=$2
+KOLLA_PATH=$3
+
+export http_proxy=
+export https_proxy=
+
+if [ "$MODE" = 'aio' ]; then
+ # Run registry on port 4000 since it may collide with keystone when doing AIO
+ REGISTRY_PORT=4000
+else
+ REGISTRY_PORT=5000
+fi
+REGISTRY_URL="operator.local"
+REGISTRY=${REGISTRY_URL}:${REGISTRY_PORT}
+ADMIN_PROTOCOL="http"
+
+function _ensure_lsb_release {
+ if [[ -x $(type lsb_release 2>/dev/null) ]]; then
+ return
+ fi
+
+ if [[ -x $(type apt-get 2>/dev/null) ]]; then
+ apt-get -y install lsb-release
+ elif [[ -x $(type yum 2>/dev/null) ]]; then
+ yum -y install redhat-lsb-core
+ fi
+}
+
+function _is_distro {
+ if [[ -z "$DISTRO" ]]; then
+ _ensure_lsb_release
+ DISTRO=$(lsb_release -si)
+ fi
+
+ [[ "$DISTRO" == "$1" ]]
+}
+
+function is_ubuntu {
+ _is_distro "Ubuntu"
+}
+
+function is_centos {
+ _is_distro "CentOS"
+}
+
+# Install common packages and do some prepwork.
+function prep_work {
+ if [[ "$(systemctl is-enabled firewalld)" = "enabled" ]]; then
+ systemctl stop firewalld
+ systemctl disable firewalld
+ fi
+
+ # This removes the fqdn from /etc/hosts's 127.0.0.1. This name.local will
+ # resolve to the public IP instead of localhost.
+ sed -i -r "s,^127\.0\.0\.1\s+.*,127\.0\.0\.1 localhost localhost.localdomain localhost4 localhost4.localdomain4," /etc/hosts
+
+ if is_centos; then
+ yum -y install epel-release
+ rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-7
+ yum -y install MySQL-python vim-enhanced python-pip python-devel gcc openssl-devel libffi-devel libxml2-devel libxslt-devel
+ elif is_ubuntu; then
+ apt-get update
+ apt-get -y install python-mysqldb python-pip python-dev build-essential libssl-dev libffi-dev libxml2-dev libxslt-dev
+ else
+ echo "Unsupported Distro: $DISTRO" 1>&2
+ exit 1
+ fi
+
+ pip install --upgrade docker-py
+}
+
+# Do some cleanup after the installation of kolla
+function cleanup {
+ if is_centos; then
+ yum clean all
+ elif is_ubuntu; then
+ apt-get clean
+ else
+ echo "Unsupported Distro: $DISTRO" 1>&2
+ exit 1
+ fi
+}
+
+# Install and configure a quick&dirty docker daemon.
+function install_docker {
+ if is_centos; then
+ cat >/etc/yum.repos.d/docker.repo <<-EOF
+[dockerrepo]
+name=Docker Repository
+baseurl=https://yum.dockerproject.org/repo/main/centos/7
+enabled=1
+gpgcheck=1
+gpgkey=https://yum.dockerproject.org/gpg
+EOF
+ # Also upgrade device-mapper here because of:
+ # https://github.com/docker/docker/issues/12108
+ # Upgrade lvm2 to get device-mapper installed
+ yum -y install docker-engine lvm2 device-mapper
+
+ # Despite it shipping with /etc/sysconfig/docker, Docker is not configured to
+ # load it from it's service file.
+ sed -i -r "s|(ExecStart)=(.+)|\1=/usr/bin/docker daemon --insecure-registry ${REGISTRY} --registry-mirror=http://${REGISTRY}|" /usr/lib/systemd/system/docker.service
+ sed -i 's|^MountFlags=.*|MountFlags=shared|' /usr/lib/systemd/system/docker.service
+
+ usermod -aG docker vagrant
+ elif is_ubuntu; then
+ apt-key adv --keyserver hkp://pgp.mit.edu:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D
+ echo "deb https://apt.dockerproject.org/repo ubuntu-wily main" > /etc/apt/sources.list.d/docker.list
+ apt-get update
+ apt-get -y install docker-engine
+ sed -i -r "s,(ExecStart)=(.+),\1=/usr/bin/docker daemon --insecure-registry ${REGISTRY} --registry-mirror=http://${REGISTRY}|" /lib/systemd/system/docker.service
+ else
+ echo "Unsupported Distro: $DISTRO" 1>&2
+ exit 1
+ fi
+
+ if [[ "${http_proxy}" != "" ]]; then
+ mkdir -p /etc/systemd/system/docker.service.d
+ cat >/etc/systemd/system/docker.service.d/http-proxy.conf <<-EOF
+[Service]
+Environment="HTTP_PROXY=${http_proxy}" "HTTPS_PROXY=${https_proxy}" "NO_PROXY=localhost,127.0.0.1,${REGISTRY_URL}"
+EOF
+
+ if [[ "$(grep http_ /etc/bashrc)" == "" ]]; then
+ echo "export http_proxy=${http_proxy}" >> /etc/bashrc
+ echo "export https_proxy=${https_proxy}" >> /etc/bashrc
+ fi
+ fi
+
+ systemctl daemon-reload
+ systemctl enable docker
+ systemctl start docker
+}
+
+function configure_kolla {
+ # Use local docker registry
+ sed -i -r "s,^[# ]*namespace *=.+$,namespace = ${REGISTRY}/lokolla," /etc/kolla/kolla-build.conf
+ sed -i -r "s,^[# ]*push *=.+$,push = True," /etc/kolla/kolla-build.conf
+ sed -i -r "s,^[# ]*docker_registry:.+$,docker_registry: \"${REGISTRY}\"," /etc/kolla/globals.yml
+ sed -i -r "s,^[# ]*docker_namespace:.+$,docker_namespace: \"lokolla\"," /etc/kolla/globals.yml
+ sed -i -r "s,^[# ]*docker_insecure_registry:.+$,docker_insecure_registry: \"True\"," /etc/kolla/globals.yml
+ # Set network interfaces
+ sed -i -r "s,^[# ]*network_interface:.+$,network_interface: \"eth1\"," /etc/kolla/globals.yml
+ sed -i -r "s,^[# ]*neutron_external_interface:.+$,neutron_external_interface: \"eth2\"," /etc/kolla/globals.yml
+ # Set VIP address to be on the vagrant private network
+ sed -i -r "s,^[# ]*kolla_internal_vip_address:.+$,kolla_internal_vip_address: \"172.28.128.254\"," /etc/kolla/globals.yml
+}
+
+# Configure the operator node and install some additional packages.
+function configure_operator {
+ if is_centos; then
+ yum -y install git mariadb
+ elif is_ubuntu; then
+ apt-get -y install git mariadb-client selinux-utils
+ else
+ echo "Unsupported Distro: $DISTRO" 1>&2
+ exit 1
+ fi
+
+ pip install --upgrade "ansible>=2" python-openstackclient python-neutronclient tox
+
+ pip install ${KOLLA_PATH}
+
+ # Set selinux to permissive
+ if [[ "$(getenforce)" == "Enforcing" ]]; then
+ sed -i -r "s,^SELINUX=.+$,SELINUX=permissive," /etc/selinux/config
+ setenforce permissive
+ fi
+
+ tox -c ${KOLLA_PATH}/tox.ini -e genconfig
+ cp -r ${KOLLA_PATH}/etc/kolla/ /etc/kolla
+ ${KOLLA_PATH}/tools/generate_passwords.py
+ mkdir -p /usr/share/kolla
+ chown -R vagrant: /etc/kolla /usr/share/kolla
+
+ configure_kolla
+
+ # Make sure Ansible uses scp.
+ cat > ~vagrant/.ansible.cfg <<EOF
+[defaults]
+forks=100
+remote_user = root
+
+[ssh_connection]
+scp_if_ssh=True
+EOF
+ chown vagrant: ~vagrant/.ansible.cfg
+
+ mkdir -p /etc/kolla/config/nova/
+ cat > /etc/kolla/config/nova/nova-compute.conf <<EOF
+[libvirt]
+virt_type=qemu
+EOF
+
+ # Launch a local registry (and mirror) to speed up pulling images.
+ if [[ ! $(docker ps -a -q -f name=registry) ]]; then
+ docker run -d \
+ --name registry \
+ --restart=always \
+ -p ${REGISTRY_PORT}:5000 \
+ -e STANDALONE=True \
+ -e MIRROR_SOURCE=https://registry-1.docker.io \
+ -e MIRROR_SOURCE_INDEX=https://index.docker.io \
+ -e STORAGE_PATH=/var/lib/registry \
+ -v /data/host/registry-storage:/var/lib/registry \
+ registry:2
+ fi
+}
+
+prep_work
+install_docker
+
+if [[ "$VM" = "operator" ]]; then
+ configure_operator
+fi
+
+cleanup
diff --git a/contrib/dev/vagrant/newest_dhcp_lease.py b/contrib/dev/vagrant/newest_dhcp_lease.py
new file mode 100644
index 000000000..bb229af1d
--- /dev/null
+++ b/contrib/dev/vagrant/newest_dhcp_lease.py
@@ -0,0 +1,134 @@
+#!/usr/bin/env python
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Command-line utility to get the IP address from the newest DHCP lease.
+
+It's written for using with vagrant-hostmanager and vagrant-libvirt plugins.
+Vagrant-hostmanager by default fetches only IP addresses from eth0 interfaces
+on VM-s. Therefore, the first purpose of this utility is to be able to fetch
+the address also from the other interfaces.
+
+Libvirt/virsh only lists all DHCP leases for the given network with timestamps.
+DHCP leases have their expiration time, but are not cleaned up after destroying
+VM. If someone destroys and sets up the VM with the same hostname, we have
+many DHCP leases for the same hostname and we have to look up for timestamp.
+That's the second purpose of this script.
+"""
+
+import argparse
+import csv
+import functools
+import operator
+import xml.etree.ElementTree as etree
+
+import libvirt
+
+
+class NoPrivateDHCPInterfaceException(Exception):
+ pass
+
+
+class NoDHCPLeaseException(Exception):
+ pass
+
+
+def libvirt_conn(f):
+ @functools.wraps(f)
+ def wrapper(*args, **kwargs):
+ conn = libvirt.openReadOnly('qemu:///system')
+ return f(conn, *args, **kwargs)
+ return wrapper
+
+
+@libvirt_conn
+def get_vir_network_dhcp_lease(conn, vm_name):
+ """Libvirt since 1.2.6 version provides DHCPLeases method in virNetwork.
+
+ That's the current official way for getting DHCP leases and this
+ information isn't stored anywhere else anymore.
+ """
+ domain_name = 'vagrant_' + vm_name
+ mac_address = get_mac_address(conn, domain_name)
+
+ network = conn.networkLookupByName('vagrant-private-dhcp')
+ dhcp_leases = libvirt.virNetwork.DHCPLeases(network)
+
+ vm_dhcp_leases = [lease for lease in dhcp_leases
+ if lease['mac'] == mac_address]
+
+ newest_vm_dhcp_lease = sorted(vm_dhcp_leases,
+ key=operator.itemgetter('expirytime'),
+ reverse=True)[0]['ipaddr']
+ return newest_vm_dhcp_lease
+
+
+def get_mac_address(conn, domain_name):
+ """Get MAC address from domain XML."""
+ domain = conn.lookupByName(domain_name)
+ domain_xml = domain.XMLDesc()
+ domain_tree = etree.fromstring(domain_xml)
+ devices = domain_tree.find('devices')
+ interfaces = devices.iterfind('interface')
+
+ for interface in interfaces:
+ source = interface.find('source')
+ if source is None or source.get('network') != 'vagrant-private-dhcp':
+ continue
+ mac_element = interface.find('mac')
+ mac_address = mac_element.get('address')
+ return mac_address
+
+ raise NoPrivateDHCPInterfaceException()
+
+
+@libvirt_conn
+def get_dnsmasq_dhcp_lease(conn, vm_name):
+ """In libvirt under 1.2.6 DHCP leases are stored in file.
+
+ There is no API for DHCP leases yet.
+ """
+ domain_name = 'vagrant_' + vm_name
+ mac_address = get_mac_address(conn, domain_name)
+
+ with open(
+ '/var/lib/libvirt/dnsmasq/vagrant-private-dhcp.leases'
+ ) as leases_file:
+ reader = csv.reader(leases_file, delimiter=' ')
+ for row in reader:
+ lease_mac, lease_ip, lease_vm_name = row[1:4]
+ if not (lease_mac == mac_address and lease_vm_name == vm_name):
+ continue
+ return lease_ip
+
+ raise NoDHCPLeaseException()
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('vm_name', help='Name of the virtual machine')
+
+ args = parser.parse_args()
+ vm_name = args.vm_name
+
+ if libvirt.getVersion() >= 1002006:
+ newest_dhcp_lease = get_vir_network_dhcp_lease(vm_name)
+ else:
+ newest_dhcp_lease = get_dnsmasq_dhcp_lease(vm_name)
+
+ print(newest_dhcp_lease)
+
+
+if __name__ == '__main__':
+ main()