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