summaryrefslogtreecommitdiff
path: root/vmgr
blob: fb2f98c0da528898ae6a6253af9bfe27f3fe5f8e (plain)
    1 #!/usr/bin/env bash
    2 #
    3 # Vmgr makes deploying templatized virtual machines easy and fast
    4 # Copyright (C)  2016  Aaron Ball <nullspoon@oper.io>
    5 #
    6 # This program is free software: you can redistribute it and/or modify
    7 # it under the terms of the GNU General Public License as published by
    8 # the Free Software Foundation, either version 3 of the License, or
    9 # (at your option) any later version.
   10 #
   11 # This program is distributed in the hope that it will be useful,
   12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
   13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   14 # GNU General Public License for more details.
   15 #
   16 # You should have received a copy of the GNU General Public License
   17 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
   18 #
   19 
   20 #
   21 # Jei! Ég get að skrifa kóðann!!
   22 #
   23 
   24 network='default'
   25 prefixmac='52:54:00:7e:70:'
   26 prefixip='192.168.122.'
   27 
   28 
   29 binvirsh='/usr/bin/virsh'
   30 binvirt_install='/usr/bin/virt-install'
   31 binqemu_img='/usr/bin/qemu-img'
   32 bincp='/bin/cp'
   33 
   34 
   35 #
   36 # Standard logging function. Prints standardized output. Timestamps all errors
   37 # and infos.
   38 #
   39 # @param level Log level to print
   40 # @param msg   All remaining arguments to be printed as message
   41 #
   42 function log {
   43   level=${1}
   44   shift
   45   msg=${@}
   46 
   47   d=$(date '+%F %T')
   48 
   49   oldifs=${IFS}
   50   IFS="
"
   51 
   52   for i in ${msg}; do
   53     if [[ ${level} == 'error' ]]; then
   54       echo -n "${d}   ERROR   "
   55     elif [[ ${level} == 'info' ]]; then
   56       echo -n "${d}   INFO   "
   57     elif [[ ${level} == 'debug' ]]; then
   58       echo -n "${d}   DEBUG   "
   59     elif [[ ${level} == 'fatal' ]]; then
   60       echo -n "${d}   FATAL   "
   61     fi
   62 
   63     echo -e ${i}
   64   done
   65 
   66   IFS=${oldifs}
   67   # Exit 1 if message was fatal
   68   [[ ${1} == 'fatal' ]] && exit 1
   69 }
   70 
   71 
   72 #
   73 # Executes virt-install with the specified arguments.
   74 #
   75 # @param name Name of the virtual machine (what displays in libvirt utils)
   76 # @param disk Path to the disk file
   77 # @param mac  Mac address of the primary nic (eth0)
   78 #             Note that this is important as it will determine the ip address
   79 #             of the system.
   80 #
   81 function install {
   82   local name=${1}
   83   local disk=${2}
   84   local mac=${3}
   85   local mem=4096
   86   
   87   out=$(${binvirt_install} \
   88     --name ${name} \
   89     --ram=${mem} \
   90     --virt-type=kvm \
   91     --disk "${disk}" \
   92     -w network=default,mac=${mac} \
   93     --graphics vnc,password=${name} \
   94     --noautoconsole \
   95     --import 2>&1)
   96 
   97   # Log the output of virt install
   98   if [[ $? -gt 0 ]]; then
   99     log error "${out}"
  100   else
  101     log info "${out}"
  102   fi
  103 }
  104 
  105 
  106 #
  107 # Clones a disk. Supports two speed options, fast or small.
  108 # Fast just does a simple copy of the disk from source to destination.
  109 # Small uses qemu-img to recompress the disk from source to destination to save
  110 # space.
  111 #
  112 # @param src   Path to source disk to clone
  113 # @param dest  Path to location that the source disk will clone to
  114 # @param speed Speed of conversion (options: small, fast).
  115 #              "fast" uses cp, but is less storage-conscious.
  116 #              "small" uses qemu-img convert, which is slower, but smaller as
  117 #              it recompresses the disk.
  118 #
  119 function clone_disk {
  120   local src=${1}
  121   local dest=${2}
  122   local speed=${3}
  123 
  124   [[ -z ${1} ]] && echo "ERROR: Template path required (arg 1)." && exit 1
  125   [[ -z ${2} ]] && echo "ERROR: Destination disk path required (arg 2)." && exit 1 
  126   [[ -z ${3} ]] && echo "ERROR: Clone speed required (arg 3)." && exit 1 
  127 
  128   if [[ ${speed} == 'fast' ]]; then
  129     ${bincp} ${src} ${dest}
  130   elif [[ ${speed} == 'small' ]]; then
  131     ${binqemu_img} convert -O qcow2 -p -c ${src} ${dest}
  132   else
  133     log error "Unknown speed: ${speed}"
  134     exit 1
  135   fi
  136 }
  137 
  138 
  139 #
  140 # Reserves an ip on the specified network for mac address ${mac}. Also inserts
  141 # dns entry for the specified name.
  142 #
  143 # @param network Virsh network name of reservation (for virsh net-*)
  144 # @param name    Name of the host (this will resolve in dns)
  145 # @param mac     Mac address to reserve the ip with
  146 # @param ip      IP address to reserve
  147 #
  148 function net_reserve_ip {
  149   local network=${1}
  150   local name=${2}
  151   local mac=${3}
  152   local ip=${4}
  153 
  154   xml="<host mac='${mac}' name='${name}' ip='${ip}'/>"
  155   # Perform reservation command
  156   out=$(${binvirsh} net-update ${network} add ip-dhcp-host "${xml}" 2>&1)
  157 
  158   # Log the output of ip reservation operation
  159   if [[ $? -gt 0 ]]; then
  160     log error "A problem occured reserving ip address ${ip} for mac ${mac}."
  161     log error "${out}"
  162     exit 1
  163   else
  164     log info "${out}"
  165   fi
  166 }
  167 
  168 
  169 #
  170 # Deletes an ip address reservation for the specified network.
  171 #
  172 # @param network Virsh network name of reservation (for virsh net-*)
  173 # @param name    Name of the host (this is what resolves in dns)
  174 # @param mac     Mac address the IP is reserved to
  175 # @param ip      IP address that will be deleted
  176 #
  177 function net_rm_reservation {
  178   local network=${1}
  179   local name=${2}
  180   local mac=${3}
  181   local ip=${4}
  182 
  183   xml="<host mac='${mac}' name='${name}' ip='${ip}'/>"
  184   # Perform reservation command
  185   ${binvirsh} net-update ${network} delete ip-dhcp-host "${xml}"
  186 
  187   if [[ $? -gt 0 ]]; then
  188     log error "A problem occured deleting ip reservation (${ip}) for mac ${mac}."
  189     exit 1
  190   fi
  191 }
  192 
  193 
  194 #
  195 # Determines the next available vm index based on network mac address usage.
  196 #
  197 # @param network
  198 # @param prefixmac
  199 #
  200 function get_next_index {
  201   local network=${1}
  202   local prefixmac=${2}
  203 
  204   # Start at 10, to allow for 0-9 as network-resources that aren't transient.
  205   index=10
  206 
  207   # Get list of hosts
  208   hostxml="$(${binvirsh} net-dumpxml ${network} | grep '<host' )"
  209 
  210   # For each index, check if the mac address exists
  211   while [ $(echo ${hostxml} | grep "mac='${prefixmac}${index}'" -c ) -ne 0 ]; do
  212     # Use this fancy printf statement to ensure we're always zero padded.
  213     index=$(printf "%0*d" 2 $((${index} + 1)))
  214   done
  215 
  216   # Unused index found. Echo.
  217   echo "${index}"
  218 }
  219 
  220 
  221 #
  222 # Parses a hostname or fqdn to determine a host's 2-digit index (00-99).
  223 #
  224 # @param name Host name or fqdn to parse for host index
  225 #
  226 function get_index_from_name {
  227   local name=${1}
  228 
  229 	# Reset name so we grab whatever is at the front of an fqdn. If name isn't an
  230   # fqdn, this will return the same.
  231   name=$(echo ${name} | cut -d '.' -f 1)
  232 
  233   # Get length of the hostname
  234   local len=$((${#name} - 2))
  235 
  236   # Strip index off end of hostname
  237   # Note that we assume a 2 digit
  238   local index=${name:$len:2}
  239 
  240   # Return index
  241   echo ${index}
  242 }
  243 
  244 
  245 #
  246 # Creates a new vm from the specified template. Seamlessly handles host index
  247 # increment (appended to prefix). Also handles mac address generation, ip
  248 # reservation and assignment, and disk cloning from template.
  249 #
  250 # @param prefix   Host prefix (index will be appended to this to make hostname)
  251 # @param template Path to disk template the vm will be created from
  252 # @param domain   Domain to be appended to the hostname. Defaults to empty.
  253 #
  254 function vm_new {
  255   [[ -z ${1} ]] && log fatal "A prefix name for the new vm is required."
  256   [[ -z ${2} ]] && log fatal "A template disk path is required."
  257 
  258   local prefix=${1}
  259   local template=${2}
  260   local domain=${3}
  261 
  262   # Get next mac/index/ip
  263   local next=$(get_next_index ${network} ${prefixmac})
  264   local mac="${prefixmac}${next}"
  265   local ip="${prefixip}${next}"
  266   local name="${prefix}${next}${domain}"
  267   local disk="${name}.sda.qcow2"
  268 
  269   # Clone the disk
  270   log info "Cloning disk from ${template} to ${disk}"
  271   clone_disk ${template} ${disk} 'fast'
  272 
  273   log info "Reserving ip address ${ip} for mac ${mac}"
  274   net_reserve_ip ${network} ${name} ${mac} ${ip}
  275 
  276   log info "Installing VM into libvirtd."
  277   install ${name} ${disk} ${mac} 
  278 }
  279 
  280 #
  281 # Shuts down and deletes specified vm Also removes network ip address
  282 # reservation as well as all related storage.
  283 #
  284 # @param name Name of vm to be destroyed
  285 #
  286 function vm_rm {
  287   [[ -z ${1} ]] && echo "Please a vm name to be deleted." && exit 1
  288 
  289   local name=${1}
  290 
  291   # Get next mac/index/ip/disk
  292   local index=$(get_index_from_name ${name})
  293   local mac="${prefixmac}${index}"
  294   local ip="${prefixip}${index}"
  295   local disk="${name}.sda.qcow2"
  296 
  297   # Get storage pool name (for refresh later)
  298   local pool=$(basename $(pwd))
  299 
  300 	# Shut the VM down forcibly (it's about to be deleted, so we don't care about
  301   # a friendly shutdown here)
  302   ${binvirsh} destroy ${name}
  303 
  304 	# Refresh the pool after VM destruction. If we don't do this, virsh
  305 	# intermittently fails on storage removal, with a permission denied error.
  306 	# This is because it doesn't know that the disk is no longer in use, so a
  307   # pool-refresh is required.
  308   log info "Refreshing storage pool ${pool} to ensure disk removal success."
  309   poolout=$(${binvirsh} pool-refresh ${pool} 2>&1)
  310 
  311   if [[ $? -gt 0 ]]; then
  312     log error "${poolout}"
  313   else
  314     log info "${poolout}"
  315   fi
  316 
  317   # Delete the VM and remove all of its storage
  318   log info -n "Removing VM ${name} and all storage."
  319   undefout=$(${binvirsh} undefine --remove-all-storage ${name})
  320 
  321   if [[ $? -gt 0 ]]; then
  322     log error "${undefout}"
  323   else
  324     log info "${undefout}"
  325   fi
  326 
  327   # Remove its network entry
  328   log info "Deleting ip reservation for ${name}, mac ${mac}, ip ${ip}"
  329   net_rm_reservation ${network} ${name} ${mac} ${ip}
  330 }
  331 
  332 
  333 #
  334 # Lists all VMs currently running, their expected ips, and mac addresses.
  335 #
  336 function vm_ls {
  337   names=$(${binvirsh} list --name)
  338 
  339   output='Host  IP  MAC\n---- -- ---\n'
  340   for i in ${names}; do
  341     index=$(get_index_from_name ${i})
  342     output="${output}${i}   ${prefixip}${index}   ${prefixmac}${index}\n"
  343   done
  344   echo -e ${output} | column -c 80 -t
  345 }
  346 
  347 
  348 #
  349 # Everyone loves a main function
  350 #
  351 function main {
  352   action=${1}
  353   shift;
  354 
  355   args=(${@})
  356 
  357   if [[ ${action} == 'new' ]]; then
  358     vm_new ${args[@]}
  359   elif [[ ${action} == 'rm' ]]; then
  360     vm_rm ${args[@]}
  361   elif [[ ${action} == 'ls' ]]; then
  362     vm_ls
  363   elif [[ ${action} == '' ]]; then
  364     log error "Please specify an action (new, rm, ls)"
  365     exit 1
  366   else
  367     log error "Unknown action: ${action}"
  368     exit 1
  369   fi
  370 }
  371 
  372 main ${@}

Generated by cgit