diff --git a/src/salix-live-installer/__init__.py b/src/salix-live-installer/__init__.py index a1c757c..c8daf5d 100644 --- a/src/salix-live-installer/__init__.py +++ b/src/salix-live-installer/__init__.py @@ -15,7 +15,6 @@ from kernel import * from keyboard import * from language import * -from module import * from mounting import * from package import * from salt import * diff --git a/src/salix-live-installer/execute.py b/src/salix-live-installer/execute.py index d04f27d..477f622 100644 --- a/src/salix-live-installer/execute.py +++ b/src/salix-live-installer/execute.py @@ -16,6 +16,8 @@ def execCall(cmd, shell = True, env = {'LANG' : 'en_US'}): """ Execute a command and return the exit code. The command is executed by default in a /bin/sh shell and using english locale. + The output of the command is not read. With some commands, it hangs if the output is not read when run in a shell. + For this type of command, prefer using execGetOutput even if you don't read the return value or using shell = False. """ return subprocess.call(cmd, shell = shell, env = env) diff --git a/src/salix-live-installer/freesize.py b/src/salix-live-installer/freesize.py index 0110a6e..4d20a5d 100644 --- a/src/salix-live-installer/freesize.py +++ b/src/salix-live-installer/freesize.py @@ -12,7 +12,18 @@ import os from stat import * from execute import * -import mounting + +def _getMountPoint(device): + "Copied from 'mounting' module to break circular dependancies" + mountpoint = None + for line in execGetOutput('mount', shell = False): + p, _, mp, _ = line.split(' ', 3) # 3 splits max, _ is discarded + if os.path.islink(p): + p = os.path.realpath(p) + if p == device: + mountpoint = mp + break + return mountpoint def getHumanSize(size): "Returns the human readable format of the size in bytes" @@ -26,6 +37,7 @@ def getHumanSize(size): def getSizes(path): """ + Compute the different sizes of the fileystem denoted by path (either a device or a file in filesystem). Returns the following sizes (in a dictionary): - size (total size) - free (total free size) @@ -35,7 +47,7 @@ def getSizes(path): + all of them with the corresponding 'Human' suffix. """ if S_ISBLK(os.stat(path).st_mode): - mountpoint = getMountPoint(path) + mountpoint = _getMountPoint(path) if mountpoint: # mounted, so will use mountpoint to get information about different sizes path = mountpoint @@ -118,10 +130,11 @@ def getUsedSize(path, blocksize = None): assertTrue(stats['used'] == None) assertTrue(stats['uuUsed'] == None) print 'getUsedSize(.)' - stats = getUsedSize('.') - print stats - assertTrue(stats['size'] > 0) - print 'getUsedSize(., 16384)' - stats = getUsedSize('.', 16384) - print stats - assertTrue(stats['size'] > 0) + stats1 = getUsedSize('.') + print stats1 + assertTrue(stats1['size'] > 0) + print 'getUsedSize(., 524288)' + stats2 = getUsedSize('.', 524288) + print stats2 + assertTrue(stats2['size'] > 0) + assertTrue(stats2['size'] > stats1['size']) diff --git a/src/salix-live-installer/fs.py b/src/salix-live-installer/fs.py index 6fba7d4..125a679 100644 --- a/src/salix-live-installer/fs.py +++ b/src/salix-live-installer/fs.py @@ -11,6 +11,7 @@ - makeFs """ from execute import * +from freesize import getSizes import os from stat import * import re @@ -18,46 +19,58 @@ def getFsType(partitionDevice): """ Returns the file system type for that partition. - 'partitionDevice' should no be prefilled with '/dev/'. + 'partitionDevice' should no be prefilled with '/dev/' if it's a block device. + It can be a full path if the partition is contains in a file. Returns 'Extended' if the partition is an extended partition and has no filesystem. """ - checkRoot() - if S_ISBLK(os.stat('/dev/{0}'.format(partitionDevice)).st_mode): + if os.path.exists('/dev/{0}'.format(partitionDevice)) and S_ISBLK(os.stat('/dev/{0}'.format(partitionDevice)).st_mode): + path = '/dev/{0}'.format(partitionDevice) + elif os.path.isfile(partitionDevice): + path = partitionDevice + else: + fstype = False + if path: try: - fstype = execGetOutput(['/sbin/blkid', '-c', '/dev/null', '-s', 'TYPE', '-o', 'value', '/dev/{0}'.format(partitionDevice)])[0] + fstype = execGetOutput(['/sbin/blkid', '-s', 'TYPE', '-o', 'value', path], shell = False)[0] except subprocess.CalledProcessError as e: + fstype = False + if fstype == '': fstype = 'Extended' - else: - fstype = False return fstype def getFsLabel(partitionDevice): """ Returns the label for that partition (if any). - 'partitionDevice' should no be prefilled with '/dev/'. + 'partitionDevice' should no be prefilled with '/dev/' if it's a block device. + It can be a full path if the partition is contains in a file. """ - checkRoot() - if S_ISBLK(os.stat('/dev/{0}'.format(partitionDevice)).st_mode): - try: - label = execGetOutput(['/sbin/blkid', '-c', '/dev/null', '-s', 'LABEL', '-o', 'value', '/dev/{0}'.format(partitionDevice)])[0] - except subprocess.CalledProcessError as e: - label = '' + if os.path.exists('/dev/{0}'.format(partitionDevice)) and S_ISBLK(os.stat('/dev/{0}'.format(partitionDevice)).st_mode): + path = '/dev/{0}'.format(partitionDevice) + elif os.path.isfile(partitionDevice): + path = partitionDevice else: label = False + if path: + try: + label = execGetOutput(['/sbin/blkid', '-s', 'LABEL', '-o', 'value', path], shell = False)[0] + except subprocess.CalledProcessError as e: + label = False return label -def makeFs(partitionDevice, fsType, label=None, options=None, force=False): +def makeFs(partitionDevice, fsType, label=None, force=False, options=None): """ Creates a filesystem on the device. - 'partitionDevice' should no be prefilled with '/dev/'. + 'partitionDevice' should no be prefilled with '/dev/' if it's a block device. 'fsType' could be ext2, ext3, ext4, xfs, reiserfs, jfs, btrfs, ntfs, fat16, fat32, swap - Use 'options' to force passing these options to the creation process (use a list) Use 'force=True' if you want to force creating the filesystem and if 'partitionDevice' is a full path to a file (not a block device). + Use 'options' to force passing these options to the creation process (use a list) """ if force and os.path.exists(partitionDevice): path = partitionDevice else: path = '/dev/{0}'.format(partitionDevice) + if not os.path.exists(path): + raise IOError('{0} does not exist'.format(path)) if not S_ISBLK(os.stat(path).st_mode): raise IOError('{0} is not a block device'.format(path)) if fsType not in ('ext2', 'ext3', 'ext4', 'xfs', 'reiserfs', 'jfs', 'btrfs', 'ntfs', 'fat16', 'fat32', 'swap'): @@ -82,7 +95,7 @@ def makeFs(partitionDevice, fsType, label=None, options=None, force=False): def _makeExtFs(path, version, label, options, force): "ExtX block size: 4k per default in /etc/mke2fs.conf" - cmd = '/sbin/mkfs.ext{0:d}'.format(version) + cmd = ['/sbin/mkfs.ext{0:d}'.format(version)] if not options: options = [] if label: @@ -92,23 +105,33 @@ def _makeExtFs(path, version, label, options, force): options.append(label) if force: options.append('-F') - return execCall([cmd].extend(options)) + cmd.extend(options) + cmd.append(path) + return execCall(cmd, shell = False) def _makeXfs(path, label, options, force): "http://blog.peacon.co.uk/wiki/Creating_and_Tuning_XFS_Partitions" - cmd = '/sbin/mkfs.xfs' + cmd = ['/sbin/mkfs.xfs'] if not options: - options = ['-l', 'size=64m,lazy-count=1', '-f'] - # -f is neccessary to have this or you cannot create XFS on a non-empty partition or disk + options = ['-f'] # -f is neccessary to have this or you cannot create XFS on a non-empty partition or disk + if os.path.isfile(path): + size = os.stat(path).st_size + else: + size = getSizes(path)['size'] + if size > 104857600: # > 100M + options = ['-l', 'size=64m,lazy-count=1'] # optimizations if label: if len(label) > 12: # max 12 chars label = label[0:11] options.append('-L') options.append(label) - return execCall([cmd].extend(options)) + cmd.extend(options) + cmd.append(path) + print 'cmd={0}'.format(cmd) + return execCall(cmd, shell = False) def _makeReiserfs(path, label, options, force): - cmd = '/sbin/mkfs.reiserfs' + cmd = ['/sbin/mkfs.reiserfs'] if not options: options = [] if label: @@ -119,10 +142,12 @@ def _makeReiserfs(path, label, options, force): if force: options.append('-f') options.append('-f') # twice for no confirmation - return execCall([cmd].extend(options)) + cmd.extend(options) + cmd.append(path) + return execCall(cmd, shell = False) def _makeJfs(path, label, options, force): - cmd = '/sbin/mkfs.jfs' + cmd = ['/sbin/mkfs.jfs'] if not options: options = ['-f'] # if not specified, will ask to continue if label: @@ -132,10 +157,12 @@ def _makeJfs(path, label, options, force): options.append(label) if force: pass # no need to do anything - return execCall([cmd].extend(options)) + cmd.extend(options) + cmd.append(path) + return execCall(cmd, shell = False) def _makeBtrfs(path, label, options, force): - cmd = '/sbin/mkfs.btrfs' + cmd = ['/sbin/mkfs.btrfs'] if not options: options = [] if label: @@ -143,10 +170,12 @@ def _makeBtrfs(path, label, options, force): options.append(label) # no restriction on size if force: pass # no need to do anything - return execCall([cmd].extend(options)) + cmd.extend(options) + cmd.append(path) + return execCall(cmd, shell = False) def _makeNtfs(path, label, options, force): - cmd = '/sbin/mkfs.ntfs' + cmd = ['/sbin/mkfs.ntfs'] if not options: options = ['-Q'] if label: @@ -156,10 +185,12 @@ def _makeNtfs(path, label, options, force): options.append(label) if force: options.append('-F') - return execCall([cmd].extend(options)) + cmd.extend(options) + cmd.append(path) + return execCall(cmd, shell = False) def _makeFat(path, is32, label, options, force): - cmd = '/sbin/mkfs.vfat' + cmd = ['/sbin/mkfs.vfat'] if is32: size = ['-F', '32'] else: @@ -175,10 +206,12 @@ def _makeFat(path, is32, label, options, force): options.append(label) if force: options.append('-I') # permit to use whole disk - return execCall([cmd].extend(options)) + cmd.extend(options) + cmd.append(path) + return execCall(cmd, shell = False) def _makeSwap(path, label, options, force): - cmd = '/sbin/mkswap' + cmd = ['/sbin/mkswap'] if not options: options = ['-f'] # it is neccessary to have this or you cannot create a swap on a non-empty partition or disk if label: @@ -186,7 +219,9 @@ def _makeSwap(path, label, options, force): options.append(label) if force: pass # nothing to do, writing to a file is always ok - return execCall([cmd].extend(options)) + cmd.extend(options) + cmd.append(path) + return execCall(cmd, shell = False) # Unit test if __name__ == '__main__': @@ -199,4 +234,18 @@ def _makeSwap(path, label, options, force): assertTrue(len(fstype) > 0) assertTrue(label) assertTrue(len(label) > 0) - # TODO : add Unit Test for making filesystems + for ft in ('ext2', 'ext4', 'xfs', 'reiserfs', 'jfs', 'btrfs', 'ntfs', 'fat16', 'fat32', 'swap'): + f = '{0}.fs'.format(ft) + if ft == 'btrfs': + size = 300 # btrfs minimum size is 256M + else: + size = 50 + execCall(['dd', 'if=/dev/zero', 'of={0}'.format(f), 'bs=1M', 'count={0:d}'.format(size)], shell = False) + assertEquals(0, makeFs(f, ft, 'test_{0}'.format(ft), True)) + if ft in ('fat16', 'fat32'): + expectedFt = 'vfat' + else: + expectedFt = ft + assertEquals(expectedFt, getFsType(f)) + assertEquals('test_{0}'.format(ft), getFsLabel(f)) + os.unlink(f) diff --git a/src/salix-live-installer/module.py b/src/salix-live-installer/module.py deleted file mode 100644 index e105d81..0000000 --- a/src/salix-live-installer/module.py +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# vim: set et ai sta sw=2 ts=2 tw=0: -""" -Handling squashfs/SaLT modules, like creating, installing, … -""" diff --git a/src/salix-live-installer/mounting.py b/src/salix-live-installer/mounting.py index d32b7a4..9de4da0 100644 --- a/src/salix-live-installer/mounting.py +++ b/src/salix-live-installer/mounting.py @@ -10,22 +10,25 @@ - umountDevice """ from execute import * +from fs import getFsType import os from stat import * +_tempMountDir = '/mnt/.tempSalt' + def getMountPoint(device): """ Will find the mount point to this 'device' or None if not mounted. """ mountpoint = None - if S_ISBLK(os.stat(device).st_mode): - for line in open('/proc/mounts').read().splitlines(): - p, mp, _ = line.split(' ', 2) # 2 splits max, _ is discarded - if os.path.islink(p): - p = os.path.realpath(p) - if p == device: - mountpoint = mp - break + path = os.path.abspath(device) + for line in execGetOutput('mount', shell = False): + p, _, mp, _ = line.split(' ', 3) # 3 splits max, _ is discarded + if os.path.islink(p): + p = os.path.realpath(p) + if p == path: + mountpoint = mp + break return mountpoint def isMounted(device): @@ -37,19 +40,39 @@ def isMounted(device): else: return False -def mountDevice(device, fsType, mountPoint = None): +def _deleteMountPoint(mountPoint): + # delete the empty directory + try: + os.rmdir(mountPoint) + except: + pass + # delete the temporary directory if not empty + if os.path.isdir(_tempMountDir): + try: + os.rmdir(_tempMountDir) + except: + pass + +def mountDevice(device, fsType = None, mountPoint = None): """ Mount the 'device' of 'fsType' filesystem under 'mountPoint'. - If 'mountPoint' is not specified, '/mnt/.temp/device' will be used. + If 'mountPoint' is not specified, '{0}/device' will be used. Returns False if it fails. - """ + """.format(_tempMountDir) + if not fsType: + fsType = getFsType(device) + manualMP = False if not mountPoint: - mountPoint = '/mnt/.temp/{0}'.format(device) + mountPoint = '{0}/{1}'.format(_tempMountDir, os.path.basename(device)) if os.path.exists(mountPoint): return False + manualMP = True if not os.path.exists(mountPoint): - os.path.makedirs(mountPoint) - return execCall(['mount', '-t', fsType, device, path]) + os.makedirs(mountPoint) + ret = execCall(['mount', '-t', fsType, device, mountPoint], shell = False) + if ret != 0 and manualMP: + _deleteMountPoint(mountPoint) + return ret def umountDevice(deviceOrPath, tryLazyUmount = True, deleteMountPoint = True): """ @@ -58,26 +81,17 @@ def umountDevice(deviceOrPath, tryLazyUmount = True, deleteMountPoint = True): Will delete the mount point if 'deleteMountPoint' is True. Returns False if it fails. """ - if S_ISBLK(os.stat(device).st_mode): - path = getMountPoint(device) + if S_ISBLK(os.stat(deviceOrPath).st_mode): + path = getMountPoint(deviceOrPath) else: path = deviceOrPath - if os.path.ismounted(path): - ret = execCall(['umount', path]) - if not ret: - ret = execCall(['umount', '-l', path]) - if ret and deleteMountPoint: - # delete the empty directory - try: - os.rmdir(path) - except: - pass - # delete the temporary directory if not empty - if os.path.isdir('/mnt/.temp'): - try: - os.rmdir('/mnt/.temp') - except: - pass + mountPoint = getMountPoint(path) + if mountPoint: + ret = execCall(['umount', mountPoint], shell = False) + if ret != 0: + ret = execCall(['umount', '-l', mountPoint], shell = False) + if ret == 0 and deleteMountPoint: + _deleteMountPoint(mountPoint) return ret else: return False @@ -85,3 +99,14 @@ def umountDevice(deviceOrPath, tryLazyUmount = True, deleteMountPoint = True): # Unit test if __name__ == '__main__': from assertPlus import * + from fs import * + checkRoot() # need to be root to mount/umount + execCall(['dd', 'if=/dev/zero', 'of=ext4.fs', 'bs=1M', 'count=50'], shell = False) + makeFs('ext4.fs', 'ext4', 'test ext4', True) + assertFalse(isMounted('ext4.fs')) + assertEquals(0, mountDevice('ext4.fs')) + assertTrue(isMounted('ext4.fs')) + assertEquals('{0}/ext4.fs'.format(_tempMountDir), getMountPoint('ext4.fs')) + assertEquals(0, umountDevice('ext4.fs')) + assertFalse(isMounted('ext4.fs')) + os.unlink('ext4.fs') diff --git a/src/salix-live-installer/salt.py b/src/salix-live-installer/salt.py index fa3e290..c5aeddc 100644 --- a/src/salix-live-installer/salt.py +++ b/src/salix-live-installer/salt.py @@ -1,3 +1,123 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # vim: set et ai sta sw=2 ts=2 tw=0: +""" +SaLT functions: + - getSaLTVersion + - isSaLTLiveEnv + - isSaLTLiveCloneEnv + - getSaLTLiveMountPoint + - getSaLTRootDir + - getSaLTIdentFile + - getSaLTBaseDir + - listSaLTModules + - installSaLTModule +""" +import os +import glob +import re +from execute import execCall + +def getSaLTVersion(): + """ + Returns the SaLT version if run in a SaLT Live environement + """ + _checkLive() + return open('/mnt/salt/salt-version', 'r').read().strip() + +def isSaLTLiveEnv(): + """ + Returns True if it is executed in a SaLT Live environment, False otherwise + """ + return os.path.isfile('/mnt/salt/salt-version') and os.path.isfile('/mnt/salt/tmp/distro_infos') + +def _checkLive(): + if not isSaLTLiveEnv(): + raise Exception('Not in SaLT Live environment.') + +def isSaLTLiveCloneEnv(): + """ + Returns Trus if it is exectued in a SaLT LiveClone environment, False otherwise + """ + if not isSaLTLiveEnv(): + return False + else: + moduledir = '{0}/{1}/{2}/modules'.format(getSaLTLiveMountPoint(), getSaLTBaseDir(), getSaLTRootDir()) + return os.path.isfile(moduledir + '/01-clone.salt') + +def getSaLTLiveMountPoint(): + """ + Returns the SaLT source mount point path. It could be the mountpoint of the optical drive, or the USB stick for example. + """ + _checkLive() + try: + # format: + # mountpoint:device + ret = open('/mnt/salt/tmp/distro_infos', 'r').read().splitlines()[0].split(':', 1)[0] + except: + ret = None + return ret + +def getSaLTRootDir(): + """ + Returns the SaLT ROOT_DIR, so the name of the directory containing the modules. + This is not a full path but relative to the BASEDIR. + """ + _checkLive() + ret = None + for line in open('/mnt/salt/etc/salt.cfg', 'r').read().splitlines(): + if line.startswith('ROOT_DIR='): + ret = line.split('=', 1)[1] + break + return ret + +def getSaLTIdentFile(): + """ + Returns the SaLT IDENT_FILE, so the name of the file located at the root of a filesystem containing some SaLT information for this Live session. + This is not a full path but relative to the mount point. + """ + _checkLive() + ret = None + for line in open('/mnt/salt/etc/salt.cfg', 'r').read().splitlines(): + if line.startswith('IDENT_FILE='): + ret = line.split('=', 1)[1] + break + return ret + +def getSaLTBaseDir(): + """ + Returns the SaLT BASEDIR, so the name of the directory containing all files for this Live session. + This is not a full path but relative to the mount point. + """ + _checkLive() + mountpoint = getSaLTLiveMountPoint() + identfile = getSaLTIdentFile() + ret = None + if mountpoint and identfile: + for line in open('{0}/{1}'.format(mountpoint, identfile), 'r').read().splitlines(): + if line.startswith('BASEDIR='): + ret = line.split('=', 1)[1] + break + if ret != None and len(ret) == 0: + ret = '.' # for not having empty path. GNU is ok having a path like a/b//c/d but it's preferable to have a/b/./c/d if possible + return ret + +def listSaLTModules(): + """ + Returns a list of modules names for this Live session. + """ + _checkLive() + moduledir = '{0}/{1}/{2}/modules'.format(getSaLTLiveMountPoint(), getSaLTBaseDir(), getSaLTRootDir()) + return sorted(map(lambda(x): re.sub(r'.*/([^/]+).salt$', r'\1', x), glob.glob('{0}/*.salt'.format(moduledir)))) + +def installSaLTModule(moduleName, targetMountPoint): + """ + Install the module 'moduleName' of this Live session into the targetMountPoint. + """ + _checkLive() + if not os.path.isdir('/mnt/salt/mnt/{1}'.format(moduleName)): + raise IOError("The module '{0}' does not exists".format(moduleName)) + if not os.path.isdir(targetMountPoint): + raise IOError("The target mount point '{0}' does not exists".format(targetMountPoint)) + # TODO Pythonic way ?? + execCall(['cp', '--preserve', '-r', '-f', '/mnt/salt/mnt/{1}/*'.format(moduleName), targetMountPoint], shell = False)