Skip to content

Commit fa05f2e

Browse files
committed
Merge pull request #29 from schubergphilis/os_preference
Import of rebalance OS script
2 parents a517c50 + 13a844c commit fa05f2e

2 files changed

Lines changed: 336 additions & 0 deletions

File tree

cloudstackops/cloudstackops.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1627,3 +1627,28 @@ def emptyHypervisor(self, hostID):
16271627
sys.stdout.flush()
16281628
return False
16291629
return True
1630+
1631+
# list oscategories
1632+
def listOsCategories(self,args):
1633+
args = self.remove_empty_values(args)
1634+
1635+
apicall = listOsCategories.listOsCategoriesCmd()
1636+
apicall.id = (str(args['id'])) if 'id' in args and len(args['id']) >0 else None
1637+
apicall.name = (str(args['name'])) if 'name' in args and len(args['name']) >0 else None
1638+
apicall.keyword = (str(args['keyword'])) if 'keyword' in args and len(args['keyword']) >0 else None
1639+
1640+
# Call CloudStack API
1641+
return self._callAPI(apicall)
1642+
1643+
# list ostypes
1644+
def listOsTypes(self,args):
1645+
args = self.remove_empty_values(args)
1646+
1647+
apicall = listOsTypes.listOsTypesCmd()
1648+
apicall.id = (str(args['id'])) if 'id' in args and len(args['id']) >0 else None
1649+
apicall.oscategoryid = (str(args['oscategoryid'])) if 'oscategoryid' in args and len(args['oscategoryid']) >0 else None
1650+
apicall.keyword = (str(args['keyword'])) if 'keyword' in args and len(args['keyword']) >0 else None
1651+
1652+
# Call CloudStack API
1653+
return self._callAPI(apicall)
1654+

rebalanceOSTypesOnCluster.py

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
#!/usr/bin/python
2+
3+
# Copyright 2015, Schuberg Philis BV
4+
#
5+
# Licensed to the Apache Software Foundation (ASF) under one
6+
# or more contributor license agreements. See the NOTICE file
7+
# distributed with this work for additional information
8+
# regarding copyright ownership. The ASF licenses this file
9+
# to you under the Apache License, Version 2.0 (the
10+
# "License"); you may not use this file except in compliance
11+
# with the License. You may obtain a copy of the License at
12+
#
13+
# http://www.apache.org/licenses/LICENSE-2.0
14+
#
15+
# Unless required by applicable law or agreed to in writing,
16+
# software distributed under the License is distributed on an
17+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
18+
# KIND, either express or implied. See the License for the
19+
# specific language governing permissions and limitations
20+
# under the License.
21+
22+
# Script to rebalance OS types on a cluster
23+
# Remi Bergsma - rbergsma@schubergphilis.com
24+
25+
import time
26+
import sys, getopt
27+
from cloudstackops import cloudstackops
28+
import os.path
29+
from random import choice
30+
from prettytable import PrettyTable
31+
from datetime import date
32+
import re
33+
import operator
34+
35+
# Function to handle our arguments
36+
def handleArguments(argv):
37+
global DEBUG
38+
DEBUG = 0
39+
global DRYRUN
40+
DRYRUN = 1
41+
global configProfileName
42+
configProfileName = ''
43+
global isProjectVm
44+
isProjectVm = 0
45+
global clusterName
46+
clusterName = ''
47+
48+
# Usage message
49+
help = "Usage: " + os.path.basename(__file__) + ' --config-profile|-c -n <cluster name> [--debug --exec --is-projectvm]'
50+
51+
try:
52+
opts, args = getopt.getopt(argv,"hc:n:p:",["config-profile=","cluster","debug","exec","is-projectvm"])
53+
except getopt.GetoptError as e:
54+
print "Error: " + str(e)
55+
print help
56+
sys.exit(2)
57+
58+
if len(opts) == 0:
59+
print help
60+
sys.exit(2)
61+
62+
for opt, arg in opts:
63+
if opt == '-h':
64+
print help
65+
sys.exit()
66+
elif opt in ("-c", "--config-profile"):
67+
configProfileName = arg
68+
elif opt in ("-n", "--cluster"):
69+
clusterName = arg
70+
elif opt in ("--debug"):
71+
DEBUG = 1
72+
elif opt in ("--exec"):
73+
DRYRUN = 0
74+
elif opt in ("--is-projectvm"):
75+
isProjectVm = 1
76+
77+
# Default to cloudmonkey default config file
78+
if len(configProfileName) == 0:
79+
configProfileName = "config"
80+
81+
if len(clusterName) == 0:
82+
print "ERROR: Please provide cluster name"
83+
print help
84+
sys.exit(1)
85+
86+
# Check available memory
87+
def hostHasEnhoughMemory(h):
88+
# Available memory
89+
memoryavailable = h.memorytotal - h.memoryallocated
90+
print "Host " + h.name + " has available memory: " + str(memoryavailable)
91+
92+
# Don't try if host has less than 10GB memory left or if vm does not fit at all
93+
# vm.memory is in Mega Bytes
94+
if memoryavailable < (10 * 1024 * 1024 * 1024):
95+
print "Warning: Skipping " + h.name + " as it has not enough free memory (" + str(memoryavailable) + ")."
96+
return False
97+
return True
98+
99+
# Get host with min/max instances
100+
def sortHostByVmCounter(vmcounter,reverse=False):
101+
return sorted(vmcounter.items(), key=lambda x:x[1], reverse=reverse)
102+
103+
# Get host with min/max memory
104+
def sortHostByMemory(hosts,reverse=False):
105+
return dict(sorted(hosts.items(), key=lambda x:x[1].memoryallocated, reverse=reverse))
106+
107+
# Parse arguments
108+
if __name__ == "__main__":
109+
handleArguments(sys.argv[1:])
110+
111+
# Handle project parameter
112+
if isProjectVm == 1:
113+
projectParam = "true"
114+
else:
115+
projectParam = "false"
116+
117+
# Init our class
118+
c = cloudstackops.CloudStackOps(DEBUG,DRYRUN)
119+
120+
if DEBUG == 1:
121+
print "Warning: Debug mode is enabled!"
122+
123+
if DRYRUN == 1:
124+
print "Warning: dry-run mode is enabled, not running any commands!"
125+
126+
# make credentials file known to our class
127+
c.configProfileName = configProfileName
128+
129+
# Init the CloudStack API
130+
c.initCloudStackAPI()
131+
132+
if len(clusterName) > 1:
133+
clusterID = c.checkCloudStackName({'csname': clusterName, 'csApiCall': 'listClusters'})
134+
135+
if DEBUG == 1:
136+
print "API address: " + c.apiurl
137+
print "ApiKey: " + c.apikey
138+
print "SecretKey: " + c.secretkey
139+
140+
fromClusterHostsData = c.getHostsFromCluster(clusterID)
141+
if fromClusterHostsData == 1 or fromClusterHostsData == None:
142+
print
143+
sys.stdout.write("\033[F")
144+
print "No (enabled) hosts found on cluster " + clustername
145+
146+
# Settings
147+
minInstances = 10
148+
maxInstances = 25
149+
osFamilies = []
150+
osFamilies.append('Windows')
151+
osFamilies.append('RedHat')
152+
osData = {}
153+
154+
# Build the data for each OS Family
155+
for family in osFamilies:
156+
osData[family] = {}
157+
osData[family]['grandCounter'] = 0
158+
osData[family]['vms'] = {}
159+
osData[family]['vmcounter'] = {}
160+
161+
# Figure out OStype
162+
osCat = c.listOsCategories({'name': family})
163+
keyword = 'Red' if family == 'RedHat' else 'Server'
164+
osTypes = c.listOsTypes({'oscategoryid': osCat[0].id, 'keyword': keyword })
165+
osData[family]['types'] = []
166+
osData[family]['hosts'] = {}
167+
168+
if osTypes is None:
169+
print "Warning: No OS Types found for " + family + " skipping.."
170+
continue
171+
172+
for type in osTypes:
173+
osData[family]['types'].append(type.id)
174+
175+
# Look at all hosts in the cluster
176+
for fromHostData in fromClusterHostsData:
177+
osData[family]['vmcounter'][fromHostData.name] = 0
178+
osData[family]['vms'][fromHostData.name] = {}
179+
osData[family]['hosts'][fromHostData.name] = fromHostData
180+
181+
if DEBUG ==1:
182+
print "# Looking for VMS on node " + fromHostData.name
183+
print "# Memory of this host: " + str(fromHostData.memorytotal)
184+
185+
# Get all vm's: project and non project
186+
vmdata_non_project = c.listVirtualmachines({'hostid': fromHostData.id, 'isProjectVm': 'false' })
187+
vmdata_project = c.listVirtualmachines({'hostid': fromHostData.id, 'isProjectVm': 'true' })
188+
189+
if vmdata_project is None and vmdata_non_project is None:
190+
print "Note: No vm's of type " + family + " found on " + fromHostData.name
191+
continue
192+
if vmdata_project is None and vmdata_non_project is not None:
193+
vmdata = vmdata_non_project
194+
if vmdata_project is not None and vmdata_non_project is None:
195+
vmdata = vmdata_project
196+
if vmdata_project is not None and vmdata_non_project is not None:
197+
vmdata = vmdata_non_project + vmdata_project
198+
199+
oscounter = 0
200+
for vm in vmdata:
201+
if DEBUG == 1:
202+
print vm.name + " -> " + str(vm.guestosid)
203+
if vm.guestosid in osData[family]['types']:
204+
osData[family]['vms'][fromHostData.name][vm.id] = vm
205+
osData[family]['vmcounter'][fromHostData.name] += 1
206+
207+
# Cluster wide counters
208+
osData[family]['grandCounter'] += osData[family]['vmcounter'][fromHostData.name]
209+
210+
# Sort by most memory free
211+
osData[family]['hosts'] = sortHostByMemory(osData[family]['hosts'], False)
212+
213+
print "Note: Cluster " + clusterName + " has " + str(osData['RedHat']['grandCounter']) + " Red Hat vm's and " + str(osData['Windows']['grandCounter']) + " Windows Server vm's"
214+
215+
# Process the generated OS Family data
216+
for family, familyData in osData.iteritems():
217+
print
218+
print "Note: Processing " + family + " Family"
219+
print "Note: ======================================="
220+
migrateTo = []
221+
migrateFrom = []
222+
223+
if 'vmcounter' not in familyData.keys():
224+
print "Warning: key vmcounter not found"
225+
if DEBUG == 1:
226+
c.pp.pprint(familyData)
227+
continue
228+
229+
if DEBUG == 1:
230+
print "DEBUG: Overview: "
231+
c.pp.pprint(familyData['vmcounter'])
232+
print
233+
234+
for h in familyData['vmcounter']:
235+
if familyData['vmcounter'][h] == 0:
236+
if DEBUG == 1:
237+
print "DEBUG: No VMs on " + h + " running family " + family
238+
continue
239+
240+
if DEBUG == 1:
241+
print h + " " + str(familyData['vmcounter'][h])
242+
243+
if familyData['vmcounter'][h] >= minInstances and familyData['vmcounter'][h] < maxInstances:
244+
if DEBUG == 1:
245+
print h + " is migration-to candidate!"
246+
247+
# Available memory
248+
if not hostHasEnhoughMemory(familyData['hosts'][h]):
249+
continue
250+
251+
migrateTo.append(h)
252+
253+
elif familyData['vmcounter'][h] < minInstances:
254+
if DEBUG == 1:
255+
print h + " is migration-from candidate!"
256+
migrateFrom.append(h)
257+
258+
if DEBUG == 1:
259+
print "DEBUG: MigrateTo:"
260+
print migrateTo
261+
262+
# If no host with minCounter vm's, then select the one with the most
263+
if len(migrateTo) == 0:
264+
maxHosts = sortHostByVmCounter(familyData['vmcounter'], True)
265+
print "Note: Hosts in sorted order:"
266+
print maxHosts
267+
maxHost = ""
268+
269+
# Select the best host to migrate to
270+
for d in maxHosts:
271+
# Get hostname
272+
m = d[0]
273+
# Available memory
274+
if not hostHasEnhoughMemory(familyData['hosts'][m]):
275+
continue
276+
# Too many instances already
277+
if familyData['vmcounter'][m] >= maxInstances:
278+
print "Note: Skipping " + m + " because it has more than maxInstances vm's already " + str(maxInstances)
279+
continue
280+
# Take the next best one
281+
maxHost = m
282+
print "Note: Selecting " + m + " because it already has some instances running."
283+
break
284+
285+
# If it did not work, halt
286+
if len(maxHost) == 0:
287+
print "Error: Could not select a suitable host. Halting."
288+
sys.exit(1)
289+
290+
if DEBUG == 1:
291+
print "DEBUG: Selecting the host with max vm's already."
292+
migrateTo.append(maxHost)
293+
294+
osData[family]['vmcounterafter'] = osData[family]['vmcounter']
295+
296+
# Display what we'd do
297+
for h in migrateFrom:
298+
for key,vm in familyData['vms'][h].iteritems():
299+
to = choice(migrateTo)
300+
if to != h:
301+
print "Note: Would have migrated " + vm.name + " (from " + h + " to " + str(to) + ") " + str(vm.memory) + " mem"
302+
osData[family]['vmcounterafter'][to] +=1
303+
osData[family]['vmcounterafter'][h] -=1
304+
else:
305+
print "Note: Skipping " + vm.name + " (already on " + h + " / " + to + ")"
306+
307+
print "DEBUG Result after migration:"
308+
c.pp.pprint(osData[family]['vmcounterafter'])
309+
310+
if DEBUG == 1:
311+
print "Note: We're done!"

0 commit comments

Comments
 (0)