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

Generated by cgit