Skip to content

Commit b749f00

Browse files
authored
Merge pull request #44 from schubergphilis/add-custom-patch-script
Handle XenServer patching automatically
2 parents e2a5df4 + 2332eb8 commit b749f00

File tree

7 files changed

+266
-13
lines changed

7 files changed

+266
-13
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
*.swp
33
config
44
*.iml
5-
.idea/
5+
.idea/
6+
xenserver_patches/

cloudstackops/xenserver.py

Lines changed: 98 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import sys
2525
import time
2626
import os
27+
import requests
2728

2829
# Fabric
2930
from fabric.api import *
@@ -49,9 +50,12 @@
4950
# Class to handle XenServer patching
5051
class xenserver():
5152

52-
def __init__(self, ssh_user='root', threads=5):
53+
def __init__(self, ssh_user='root', threads=5, pre_empty_script='xenserver_pre_empty_script.sh',
54+
post_empty_script='xenserver_post_empty_script.sh'):
5355
self.ssh_user = ssh_user
5456
self.threads = threads
57+
self.pre_empty_script = pre_empty_script
58+
self.post_empty_script = post_empty_script
5559

5660
# Wait for hypervisor to become alive again
5761
def check_connect(self, host):
@@ -74,16 +78,15 @@ def check_connect(self, host):
7478
# Remove progress indication
7579
sys.stdout.write("\033[F")
7680
print "Note: Host " + host.name + " is able to do XE stuff again! "
77-
print "Note: Waiting 30s to allow the hypervisor to connect.."
78-
time.sleep(30)
81+
print "Note: Waiting 60s to allow the hypervisor to connect.."
82+
time.sleep(60)
7983
return True
8084

8185
# Check if we can use xapi
8286
def check_xapi(self, host):
8387
try:
84-
with settings(host_string=self.ssh_user + "@" + host.ipaddress):
85-
with warn_only():
86-
result = fab.run("xe host-enable host=" + host.name)
88+
with settings(warn_only=True, host_string=self.ssh_user + "@" + host.ipaddress):
89+
result = fab.run("xe host-enable host=" + host.name)
8790
if result.return_code == 0:
8891
return True
8992
else:
@@ -169,11 +172,16 @@ def host_evacuate(self, host):
169172

170173
# Reboot a host when all conditions are met
171174
def host_reboot(self, host, halt_hypervisor=False):
172-
# Disbale host
175+
# Disable host
173176
if self.host_disable(host) is False:
174177
print "Error: Disabling host " + host.name + " failed."
175178
return False
176179

180+
# Execute pre-empty-script
181+
if self.exec_script_on_hypervisor(host, self.pre_empty_script) is False:
182+
print "Error: Executing script '" + self.pre_empty_script + "' on host " + host.name + " failed."
183+
return False
184+
177185
# Then evacuate it
178186
if self.host_evacuate(host) is False:
179187
print "Error: Evacuating host " + host.name + " failed."
@@ -185,6 +193,11 @@ def host_reboot(self, host, halt_hypervisor=False):
185193
return False
186194
print "Note: Host " + host.name + " has no VMs running, continuing"
187195

196+
# Execute post-empty-script
197+
if self.exec_script_on_hypervisor(host, self.post_empty_script) is False:
198+
print "Error: Executing script '" + self.post_empty_script + "' on host " + host.name + " failed."
199+
return False
200+
188201
# Finally reboot it
189202
try:
190203
with settings(host_string=self.ssh_user + "@" + host.ipaddress):
@@ -209,6 +222,16 @@ def host_reboot(self, host, halt_hypervisor=False):
209222
print "Error: Enabling host " + host.name + " failed."
210223
return False
211224

225+
# Execute script on hypervisor
226+
def exec_script_on_hypervisor(self, host, script):
227+
script = script.split('/')[-1]
228+
print "Note: Executing script '%s' on host %s.." % (script, host.name)
229+
try:
230+
with settings(show('output'), host_string=self.ssh_user + "@" + host.ipaddress):
231+
return fab.run("bash /tmp/" + script)
232+
except:
233+
return False
234+
212235
# Get VM count of a hypervisor
213236
def host_get_vms(self, host):
214237
try:
@@ -275,6 +298,12 @@ def put_scripts(self, host):
275298
'/tmp/xenserver_fake_pvtools.sh', mode=0755)
276299
put('xenserver_parallel_evacuate.py',
277300
'/tmp/xenserver_parallel_evacuate.py', mode=0755)
301+
if len(self.pre_empty_script) > 0:
302+
put(self.pre_empty_script,
303+
'/tmp/' + self.pre_empty_script.split('/')[-1], mode=0755)
304+
if len(self.post_empty_script) > 0:
305+
put(self.post_empty_script,
306+
'/tmp/' + self.post_empty_script.split('/')[-1], mode=0755)
278307
return True
279308
except:
280309
print "Warning: Could not upload check scripts to host " + host.name + ". Continuing anyway."
@@ -308,3 +337,65 @@ def get_bond_status(self, host):
308337
return fab.run("python /tmp/xenserver_check_bonds.py | awk {'print $1'} | tr -d \":\"")
309338
except:
310339
return False
340+
341+
# Download XenServer patch
342+
def download_patch(self, url):
343+
filename = url.split("/")[-1]
344+
345+
directory = "xenserver_patches"
346+
if not os.path.exists(directory):
347+
os.makedirs(directory)
348+
349+
destination_file = os.getcwd() + '/' + directory + '/' + filename
350+
try:
351+
local_length = int(os.path.getsize(destination_file))
352+
except:
353+
local_length = 0
354+
355+
print "Note: Executing request.."
356+
try:
357+
response = requests.get(url, stream=True)
358+
remote_length = int(response.headers.get('Content-Length', 0))
359+
if not response.ok:
360+
return False
361+
except:
362+
return False
363+
364+
# Do we need to download?
365+
print "Note: The remote length is %s, local length is %s" % (remote_length, local_length)
366+
367+
if remote_length == local_length:
368+
print "Note: Skipping download because file is already downloaded."
369+
return True
370+
371+
with open(destination_file, 'wb') as handle:
372+
# Download file
373+
print "Note: Downloading file.."
374+
375+
for block in response.iter_content(1024):
376+
handle.write(block)
377+
return True
378+
379+
# Upload patches to poolmaster
380+
def put_patches_to_poolmaster(self, host):
381+
print "Note: Uploading patches to poolmaster.."
382+
try:
383+
with settings(host_string=self.ssh_user + "@" + host.ipaddress):
384+
run('rm -rf /root/xenserver_patches/')
385+
run('mkdir -p /root/xenserver_patches')
386+
put('xenserver_patches/*', '/root/xenserver_patches')
387+
put('xenserver_upload_patches_to_poolmaster.sh',
388+
'/root/xenserver_patches/xenserver_upload_patches_to_poolmaster.sh', mode=0755)
389+
return True
390+
except:
391+
print "Warning: Could not upload patches to host " + host.name + "."
392+
return False
393+
394+
# Upload patches to XenServer
395+
def upload_patches_to_xenserver(self, host):
396+
print "Note: We're uploading the patches to XenServer"
397+
try:
398+
with settings(show('output'), host_string=self.ssh_user + "@" + host.ipaddress):
399+
return fab.run("bash /root/xenserver_patches/xenserver_upload_patches_to_poolmaster.sh")
400+
except:
401+
return False

xenserver_patches_to_install.txt

Whitespace-only changes.

xenserver_post_empty_script.sh

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#!/usr/bin/env bash
2+
3+
echo "This is the post_empty script you could customise."
4+
5+
#echo "Downgrading openvswitch RPM to the XenServer default"
6+
rpm -Uvh http://10.200.10.10/software/xenserver/openvswitch-1.4.6-143.9926.i386.rpm --force --nodeps
7+
8+
echo "Applying patches"
9+
HOST_UUID=$(xe host-list name-label=${HOSTNAME} --minimal)
10+
11+
# Check for 6.5
12+
cat /etc/redhat-release | grep "6.5"
13+
if [ $? -eq 0 ]; then
14+
SERVICE_PACK_PATCH=XS65ESP1
15+
fi
16+
# Check for 6.2
17+
cat /etc/redhat-release | grep "6.2"
18+
if [ $? -eq 0 ]; then
19+
SERVICE_PACK_PATCH=XS62ESP1
20+
fi
21+
22+
# First apply SP1
23+
xe patch-list name-label=${SERVICE_PACK_PATCH} params=hosts --minimal | tr ',' '\n' | grep ${HOST_UUID}
24+
if [ $? -eq 0 ]; then
25+
echo Service Pack ${SERVICE_PACK_PATCH} is already installed, skipping.
26+
else
27+
echo Installing ${SERVICE_PACK_PATCH}...
28+
PATCH_UUID=$(xe patch-list name-label=${SERVICE_PACK_PATCH} | grep uuid | sed -e 's/^.*: //g')
29+
if [ ${PATCH_UUID} ]; then
30+
xe patch-apply uuid=${PATCH_UUID} host-uuid=${HOST_UUID}
31+
fi
32+
fi
33+
34+
# Apply any other available patch
35+
XEN_ALL_PATCHES=$(xe patch-list params=name-label --minimal | tr ',' '\n' )
36+
XEN_INSTALLED_PATCHES=$(xe patch-list hosts:contains=${HOST_UUID} params=name-label --minimal | tr ',' '\n' )
37+
38+
for patch in ${XEN_ALL_PATCHES}; do
39+
echo "Checking patch " ${patch}
40+
41+
# Check if already included
42+
echo ${XEN_INSTALLED_PATCHES} | grep ${patch} 2>&1 >/dev/null
43+
if [ $? -eq 0 ]; then
44+
echo Patch ${patch} is already installed, skipping.
45+
else
46+
echo Installing $patch...
47+
PATCH_UUID=$(xe patch-list name-label=${patch}| grep uuid | sed -e 's/^.*: //g')
48+
if [ ${PATCH_UUID} ]; then
49+
xe patch-apply uuid=${PATCH_UUID} host-uuid=${HOST_UUID}
50+
fi
51+
fi
52+
done
53+
54+
echo "Upgrading drivers"
55+
yum -y install bnx2x-* fnic* qla2* glnic* qlge* tg3* openvswitch-modules-xen*
56+
if [ $? -eq 0 ]; then
57+
echo "Yum commnand returned non-zero"
58+
exit 1
59+
fi
60+
61+
SYSTEM_HARDWARE=$(dmidecode -s system-product-name | grep -v "#")
62+
if [[ "${SYSTEM_HARDWARE}" == "ProLiant DL380 G7" ]]; then
63+
echo "Skip HPSA on HP ProLiant DL380 G7 or else the box won't boot"
64+
else
65+
yum -y install hpsa*
66+
if [ $? -eq 0 ]; then
67+
echo "Yum commnand returned non-zero"
68+
exit 1
69+
fi
70+
fi
71+
yum -y upgrade nicira-ovs-hypervisor-node
72+
if [ $? -eq 0 ]; then
73+
echo "Yum commnand returned non-zero"
74+
exit 1
75+
fi

xenserver_pre_empty_script.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/usr/bin/env bash
2+
3+
echo "This is the pre_empty script you could customise."

xenserver_rolling_reboot.py

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import time
2929
import os
3030
import getopt
31+
import glob
3132
from cloudstackops import cloudstackops
3233
from cloudstackops import xenserver
3334
# Fabric
@@ -57,6 +58,14 @@ def handleArguments(argv):
5758
threads = 5
5859
global halt_hypervisor
5960
halt_hypervisor = False
61+
global pre_empty_script
62+
pre_empty_script = 'xenserver_pre_empty_script.sh'
63+
global post_empty_script
64+
post_empty_script = 'xenserver_post_empty_script.sh'
65+
global patch_list_file
66+
patch_list_file = 'xenserver_patches_to_install.txt'
67+
global preserve_downloads
68+
preserve_downloads = False
6069

6170
# Usage message
6271
help = "Usage: ./" + os.path.basename(__file__) + ' [options]' + \
@@ -68,14 +77,21 @@ def handleArguments(argv):
6877
'\n --threads <nr>\t\t\t\tUse this number or concurrent migration threads ' + \
6978
'\n --halt\t\t\t\t\tInstead of the default reboot, halt the hypervisor (useful in case of hardware ' \
7079
'upgrades) ' + \
80+
'\n --pre-empty-script\t\t\t\tBash script to run on hypervisor before starting the live migrations to empty ' \
81+
'hypervisor (expected in same folder as this script)' + \
82+
'\n --post-empty-script\t\t\t\tBash script to run on hypervisor after a hypervisor has no more VMs running' \
83+
'\n --patch-list-file\t\t\t\tText file with URLs of patches to download and install. One per line. ' \
84+
'(expected in same folder as this script)' + \
85+
'\n --preserve-downloads\t\t\t\tPreserve downloads instead of wiping them and downloading again.' + \
7186
'\n --debug\t\t\t\t\tEnable debug mode' + \
7287
'\n --exec\t\t\t\t\tExecute for real' + \
7388
'\n --prepare\t\t\t\t\tExecute some prepare commands'
7489

7590
try:
7691
opts, args = getopt.getopt(
7792
argv, "hc:n:t:p", [
78-
"credentials-file=", "clustername=", "ignore-hosts=", "threads=", "halt", "debug", "exec", "prepare"])
93+
"credentials-file=", "clustername=", "ignore-hosts=", "threads=", "pre-empty-script=",
94+
"post-empty-script=", "patch-list-file=", "preserve-downloads", "halt", "debug", "exec", "prepare"])
7995
except getopt.GetoptError as e:
8096
print "Error: " + str(e)
8197
print help
@@ -95,6 +111,14 @@ def handleArguments(argv):
95111
ignoreHostList = arg
96112
elif opt in ("--halt"):
97113
halt_hypervisor = True
114+
elif opt in ("--pre-empty-script"):
115+
pre_empty_script = arg
116+
elif opt in ("--post-empty-script"):
117+
post_empty_script = arg
118+
elif opt in ("--patch-list-file"):
119+
patch_list_file = arg
120+
elif opt in ("--preserve-downloads"):
121+
preserve_downloads = True
98122
elif opt in ("--debug"):
99123
DEBUG = 1
100124
elif opt in ("--exec"):
@@ -108,7 +132,7 @@ def handleArguments(argv):
108132

109133
# Ignore host list
110134
if len(ignoreHostList) > 0:
111-
ignoreHosts = ignoreHostList.split(", ")
135+
ignoreHosts = ignoreHostList.replace(' ', '').split(",")
112136
else:
113137
ignoreHosts = []
114138

@@ -125,7 +149,7 @@ def handleArguments(argv):
125149
c = cloudstackops.CloudStackOps(DEBUG, DRYRUN)
126150

127151
# Init XenServer class
128-
x = xenserver.xenserver('root', threads)
152+
x = xenserver.xenserver('root', threads, pre_empty_script, post_empty_script)
129153
c.xenserver = x
130154

131155
# make credentials file known to our class
@@ -200,7 +224,11 @@ def handleArguments(argv):
200224
print " - Turn OFF XenServer poolHA for " + clustername
201225
print " - For any hypervisor it will do this (poolmaster " + poolmaster.name + " first):"
202226
print " - put it to Disabled aka Maintenance in XenServer"
227+
print " - download the patches in file --patch-list-file '" + patch_list_file + "'"
228+
print " (preserve downloads is set to " + str(preserve_downloads) + ")"
229+
print " - execute the --pre-empty-script script '" + pre_empty_script + "' on the hypervisor"
203230
print " - live migrate all VMs off of it using XenServer evacuate command"
231+
print " - execute the --post-empty-script script '" + post_empty_script + "' on the hypervisor"
204232
print " - when empty, it will reboot the hypervisor (halting is " + str(halt_hypervisor) + ")"
205233
print " - will wait for it to come back online (checks SSH connection)"
206234
print " - set the hypervisor to Enabled in XenServer"
@@ -249,6 +277,26 @@ def handleArguments(argv):
249277
disconnect_all()
250278
sys.exit(1)
251279

280+
# Download all XenServer patches
281+
if not preserve_downloads:
282+
print "Note: Deleting previously downloaded patches"
283+
files = glob.glob('xenserver_patches/*.zip')
284+
for f in files:
285+
print "Note: Removing previously downloaded patch " + f
286+
os.remove(f)
287+
288+
print "Note: Reading patches list '%s'" % patch_list_file
289+
with open(patch_list_file) as file_pointer:
290+
patches = file_pointer.read().splitlines()
291+
292+
for patch_url in patches:
293+
print "Note: Processing patch '%s'" % patch_url
294+
x.download_patch(patch_url)
295+
296+
# Upload the patches to poolmaster, then to XenServer
297+
x.put_patches_to_poolmaster(poolmaster)
298+
x.upload_patches_to_xenserver(poolmaster)
299+
252300
# Migrate all VMs off of pool master
253301
vm_count = x.host_get_vms(poolmaster)
254302
if vm_count:
@@ -274,8 +322,8 @@ def handleArguments(argv):
274322
disconnect_all()
275323
sys.exit(1)
276324

277-
print "Note: Waiting 30s to allow all hosts connect.."
278-
time.sleep(30)
325+
print "Note: Waiting 60s to allow all hosts connect.."
326+
time.sleep(60)
279327

280328
else:
281329
print "Warning: Skipping " + poolmaster.name + " due to --ignore-hosts setting"

0 commit comments

Comments
 (0)