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 ${@}
|