#! /usr/bin/python """ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Note that you can find additional information about the GNU GPL here: http://www.gnu.org/licenses/licenses.html#GPL """ from constraint import * from subprocess import Popen, PIPE def onlyOneNotNull(*args): list = [arg != 0 for arg in args] return list.count(True) def sumOfArgs(total, *args): return total == sum(args) def moreThanArgs(total, *args): return total >= sum(args) RANGE_FACTOR = 4 REAL_REF_RANGE = [0, 0.25, 0.5, 1, 1.5, 2, 2.5, 3, 5, 4, 6, 8, 12, 16, 24, 32, 48, 64, 96, 128, 192, 256, 384, 512, 768, 1024, 2048] REF_RANGE = [int(n*RANGE_FACTOR) for n in REAL_REF_RANGE] def rg(a, b=None): if b==None: min = 0 max = a else: min = a max = b result = [] for n in REF_RANGE: if n >= min * RANGE_FACTOR and n < max * RANGE_FACTOR: result.append(n) return result class ConfigReader: ram = None disks = None def __init__(self, defaultRam=377, defaultDisks={"sda":9}): self.ram = defaultRam self.disks = defaultDisks def read(self): self.readRam(self.ram) self.readDisks(self.disks) def readRam(self, defaultRam): pipe = Popen("free", shell=True, stdout=PIPE).stdout output = pipe.read() returnCode = pipe.close() if returnCode != None or output == None or output == "": output = """ total used free shared buffers cached Mem: 262144 156088 3084 0 5520 85480 -/+ buffers/cache: 65088 94084 Swap: 294800 860 293940 """ print "Could not read amount of RAM from OS, making the following wild assumption:" print output try: memLine = output.splitlines()[1] assert(memLine.split()[0] == "Mem:") mem = int(memLine.split()[1]) except: # could not read amount of memory print "Could not read amount of RAM from OS, making the following wild assumption:" + str(defaultRam) + " MB" mem = defaultRam * 1024 self.ram = int(round(1.0*mem/1024)) # in MB def readDisks(self, defaultDisks): pipe = Popen("source /usr/lib/fai/subroutines-linux; set_disk_info; echo $device_size", shell=True, executable="/bin/bash", stdout=PIPE).stdout output = pipe.read() returnCode = pipe.close() print "FAI returned that information about disks: " + output if returnCode != None: print "and returned a returnCode of: " + str(returnCode) if returnCode != None or output == None or output == "": output = "\n".join([key + " " + str(defaultDisks[key]*1024*1024) for key in defaultDisks.keys()]) print "Could not read number and size of disks from FAI, making the following wild assumption:" print output try: self.disks = {} for disk in output.splitlines(): diskName, diskSize = disk.split() diskSize = float(diskSize) self.disks[diskName] = int(round(diskSize/1024/1024)) # in GB except: print "Could not read number and size of disks from FAI, making the following wild assumption:" + str(defaultDisks) self.disks = defaultDisks class Partitionner: disks = None partitioning = None ram = None minRoot = 0.5 maxRoot = 1.0 minUsr = 3 maxUsr = 10 minVar = 2 maxVar = 3 minHome = 0.5 def __init__(self): self.readConfig def readConfig(self): configReader = ConfigReader() configReader.read() self.ram = configReader.ram self.disks = configReader.disks print "RAM: " + str(self.ram) + " MB" print "Disks: " + ", ".join([key + " (" + str(self.disks[key]) + " GB)" for key in self.disks.keys()]) def solve(self): print "Initializing..." if self.ram == None or self.disks == None: self.readConfig() disks = self.disks.values() nbOfDisks = len(disks) total = reduce(lambda x, y: x + y, disks) ram = self.ram # in MB if ram <= 128: swap = 3.0 * ram / 1024 # in GB else: if ram <= 512: swap = 2.0 * ram / 1024 # in GB else: swap = 1.0 * ram / 1024 # in GB print "SWAP: " + str(swap) minRoot = self.minRoot maxRoot = self.maxRoot minUsr = self.minUsr maxUsr = self.maxUsr minVar = self.minVar maxVar = self.maxVar minHome = self.minHome print "Stating the partitioning problem..." problem = Problem() for i, disk in enumerate(disks): problem.addVariable("disk" + str(i), [disk * RANGE_FACTOR]) problem.addVariable("swap" + str(i), [0, int(round(swap * RANGE_FACTOR))]) print "SWAPi range: " + str([0, int(round(swap * RANGE_FACTOR))]) swaps = reduce(lambda x, y: x + (y,), ["swap" + str(i) for i in range(nbOfDisks)], ()) problem.addConstraint(onlyOneNotNull, swaps) problem.addVariable("swap", [int(round(swap * RANGE_FACTOR))]) print "SWAP range: " + str([int(round(swap * RANGE_FACTOR))]) problem.addConstraint(sumOfArgs, ("swap",) + swaps) for i, disk in enumerate(disks): problem.addVariable("usr" + str(i), [0] + rg(minUsr, min(maxUsr, disks[i]) + 1)) usrs = reduce(lambda x, y: x + (y,), ["usr" + str(i) for i in range(nbOfDisks)], ()) problem.addConstraint(onlyOneNotNull, usrs) problem.addVariable("usr", rg(minUsr, min(maxUsr, max(disks)) +1)) problem.addConstraint(sumOfArgs, ("usr",) + usrs) for i, disk in enumerate(disks): problem.addVariable("var" + str(i), [0] + rg(minVar, min(maxVar, disks[i]) + 1)) vars = reduce(lambda x, y: x + (y,), ["var" + str(i) for i in range(nbOfDisks)], ()) problem.addConstraint(onlyOneNotNull, vars) problem.addVariable("var", rg(minVar, min(maxVar, max(disks)) +1)) problem.addConstraint(sumOfArgs, ("var",) + vars) for i, disk in enumerate(disks): problem.addVariable("root" + str(i), [0] + rg(minRoot, min(maxRoot, disks[i]) + 1)) roots = reduce(lambda x, y: x + (y,), ["root" + str(i) for i in range(nbOfDisks)], ()) problem.addConstraint(onlyOneNotNull, roots) problem.addVariable("root", rg(minRoot, min(maxRoot, max(disks)) +1)) problem.addConstraint(sumOfArgs, ("root",) + roots) for i, disk in enumerate(disks): problem.addVariable("home" + str(i), [0] + rg(minHome, disks[i] + 1)) homes = reduce(lambda x, y: x + (y,), ["home" + str(i) for i in range(nbOfDisks)], ()) problem.addConstraint(onlyOneNotNull, homes) problem.addVariable("home", rg(minHome, max(disks) +1)) problem.addConstraint(sumOfArgs, ("home",) + homes) for i, disk in enumerate(disks): problem.addVariable("extra" + str(i), rg(disks[i] + 1)) print "Extra space: " + str(total - swap - minRoot - minUsr - minVar - minHome + 1) problem.addVariable("extra", rg(total - swap - minRoot - minUsr - minVar - minHome + 1)) extras = reduce(lambda x, y: x + (y,), ["extra" + str(i) for i in range(nbOfDisks)], ()) problem.addConstraint(sumOfArgs, ("extra",) + extras) problem.addConstraint(lambda home, extra: home > extra, ("home", "extra")) for i, disk in enumerate(disks): problem.addConstraint(moreThanArgs, ("disk" + str(i), "swap" + str(i), "var" + str(i), "usr" + str(i), "root" + str(i), "home" + str(i), "extra" + str(i))) problem.addConstraint(lambda x, y: x == 0 or y == 0, ("extra" + str(i), "home" + str(i))) problem.addConstraint(lambda x, y: x == 0 or y == 0, ("extra" + str(i), "usr" + str(i))) problem.addVariable("total", [total * RANGE_FACTOR]) problem.addConstraint(lambda t,r,u,h,e,v,s: t >= r+u+h+e+v+s, ("total", "root", "usr", "home", "extra", "var", "swap")) print "Solving the partitioning problem..." try: sols = problem.getSolutions() except KeyError: print "Error, could not find a solution!" sols = [] print "Found %s possible partitions" % str(len(sols)) # optimize those solutions for sol in sols: for i, disk in enumerate(disks): # assign unpartitioned space on disk to the extra partition total = sol["swap" + str(i)] + sol["var" + str(i)] + sol["usr" + str(i)] + sol["root" + str(i)] + sol["home" + str(i)] + sol["extra" + str(i)] unused = sol["disk" + str(i)] - total sol["extra" + str(i)] += unused sol["extra"] += unused # merge small extra partitions into bigger usr partitions if sol["extra"+str(i)] > 0 and sol["usr"+str(i)] >= sol["extra"+str(i)] * 2: sol["usr"+str(i)] += sol["extra"+str(i)] sol["usr"] += sol["extra"+str(i)] sol["extra"+str(i)] = 0 sol["extra"] -= sol["extra"+str(i)] # merge small extra partitions into bigger var partitions if sol["extra"+str(i)] > 0 and sol["var"+str(i)] >= sol["extra"+str(i)] * 2: sol["var"+str(i)] += sol["extra"+str(i)] sol["var"] += sol["extra"+str(i)] sol["extra"+str(i)] = 0 sol["extra"] -= sol["extra"+str(i)] # divide by RANGE_FACTOR for key in sol.keys(): sol[key] = 1.0 * sol[key] / RANGE_FACTOR def score(sol): rootRatio = 1.0 * (sol["root"] - minRoot) / (maxRoot - minRoot) usrRatio = 1.0 * (sol["usr"] - minUsr) / (maxUsr - minUsr) varRatio = 1.0 * (sol["var"] - minVar) / (maxVar - minVar) homeRatio = 1.0 * (min(sol["home"], maxUsr) - minHome) / (maxUsr - minHome) avgRatio = (rootRatio + usrRatio + varRatio + homeRatio) / 4 # root, usr, var and home should be well balanced balanceScore = (abs(rootRatio - avgRatio) + abs(usrRatio - avgRatio) + abs(varRatio - avgRatio) + abs(homeRatio - avgRatio)) / 4 # < 1, to be minimized # there should not be much space assigned to extra partitions extraScore = sol["extra"] / sol["home"] # < 1, to be minimized sc = balanceScore + extraScore # swap and usr should not be on same disk (performance optimization) # swap and root should not be on same disk (performance optimization) for i, disk in enumerate(disks): if sol["swap"+str(i)] > 0: if sol["usr"+str(i)] > 0: sc = sc * 1.2 if sol["root"+str(i)] > 0: sc = sc * 1.2 # swap and usr (or root) should not be on same IDE controller if int((1.0*i/2 - int(1.0*i/2))*2) == 0: # even if sol.has_key("usr" + str(i+1)): if sol["usr" + str(i+1)] > 0: sc = sc * 1.1 if sol.has_key("root" + str(i+1)): if sol["root" + str(i+1)] > 0: sc = sc * 1.1 else: # odd if sol.has_key("usr" + str(i-1)): if sol["usr" + str(i-1)] > 0: sc = sc * 1.1 if sol.has_key("root" + str(i-1)): if sol["root" + str(i-1)] > 0: sc = sc * 1.1 return sc print "Sorting out the optimal partitions..." sols.sort(key=score) partitions = [] for bestSol in sols[:6]: partition = [{"SCORE":score(bestSol)}] for i, disk in enumerate(disks): device = {} device["TOTAL"] = disk for type in ["root", "swap", "usr", "var", "home", "extra"]: if bestSol[type + str(i)] > 0: device[type] = bestSol[type + str(i)] partition.append(device) partitions.append(partition) print "Best partitions found:" for partition in partitions: print " Score = " + str(partition[0]["SCORE"]) for device in partition[1:]: print " " + str(device) print print "Taking the first one" self.partitioning = partitions[0] def partition(self): self.solve() def faiDiskConfigs(self): if self.partitioning == None: self.solve() result = """# disk configuration automatically optimized by www.akasig.org # # [mount options] [;extra options] """ partitionName = {"root":"/", \ "swap":"swap", \ "usr":"/usr", \ "var":"/var", \ "home":"/home", \ "extra":"/extra" } partitionSizeTolerance = {"root":0.1, \ "swap":0, \ "usr":0.1, \ "var":0.1, \ "home":-0.1, \ "extra":0.5} # how much variation is tolerated on the calculated size of that partition (-0.x means that a tolerance of -x% to no max) partitionMountOptions = {"root":"rw,errors=remount-ro", \ "swap":"rw", \ "usr":"rw", \ "var":"rw", \ "home":"rw,nosuid", \ "extra":"rw" } partitionExtraOptions = {"root":"-c -j ext3", \ "swap":"", \ "usr":"-j ext3", \ "var":"-m 5 -j ext3", \ "home":"-m 1 -j ext3", \ "extra":"-m 5 -j ext3"} for i, device in enumerate(self.partitioning[1:]): result += "disk_config disk" + str(i+1) + "\n" firstPartition = True # see FAI documentation for more information about how the partition index is defined primaryIndex = 0 logicalIndex = 4 keys = device.keys() keys.sort(key=lambda x: x != "root") # put the "root" key first in the list for key in keys: partStr = "" if firstPartition == True or key == "root": firstPartition = False partStr = "primary " primaryIndex += 1 index = primaryIndex else: partStr = "logical " logicalIndex += 1 index = logicalIndex if key in partitionName.keys(): calculatedSize = device[key]*1024 if partitionSizeTolerance[key] < 0: partitionSize = str(int(calculatedSize * (1+partitionSizeTolerance[key]) )) + "-" elif partitionSizeTolerance[key] == 0: partitionSize = str(int(calculatedSize)) else: partitionSize = str(int(calculatedSize * (1-partitionSizeTolerance[key]) )) + "-" + str(int(calculatedSize * (1+partitionSizeTolerance[key]) )) if key == "extra": result += partStr + partitionName[key] + str(i+1) + " " + partitionSize + " " + partitionMountOptions[key] + " ;" + partitionExtraOptions[key] + "\n" else: result += partStr + partitionName[key] + " " + partitionSize + " " + partitionMountOptions[key] + " ;" + partitionExtraOptions[key] + "\n" if key == "home": result += "#" + partStr + partitionName[key] + " preserve" + str(index) + " " + partitionMountOptions[key] + " ;" + partitionExtraOptions[key] + "\n" result += "\n" return result part = Partitionner() part.partition() disk_config = part.faiDiskConfigs() disk_configFile = open('/tmp/MYCLASS','w') disk_configFile.write(disk_config) disk_configFile.close()