Skip to content
Browse files

Initial commit

  • Loading branch information...
1 parent 22edbcb commit ff9c06b4c77b06067b40c5a0f342e2b15dc99601 Patrick Devine committed
Sorry, we could not display the entire diff because too many files (1,004) changed.
View
71 README.md
@@ -1,4 +1,69 @@
-weasel
-======
+Weasel for ESX
+==============
+
+Copyright (c) 2008-2010 VMware, Inc.
+
+Weasel 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 version 2 and no later version.
+
+Weasel 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
+version 2 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.
+
+
+ABOUT WEASEL
+
+Weasel is a replacement for Red Hat Linux's Anaconda installer and is used
+for installing ESX 4. As such, it normally runs from a Red Hat Enterprise 3
+"Console OS" running underneath VMware ESX Server.
+
+Weasel is written almost entirely in Python, and supports a fairly robust
+set of features including:
+
+ - graphical installation
+ - text based installation
+ - scripted (kickstart) installation
+ - multiple types of boot media including CD-ROM, DVD, USB, and PXE
+ - network based installation through http, ftp and nfs
+ - asynchronous storage and network driver disks
+ - support for creating multiple filesystem types including ext3 and vmfs
+ - installation of Red Hat Enterprise Linux into a special container called
+ the "Console VMDK"
+ - extensive unit tests which can be run to validate the installer code
+ - dynamic partition checking to ensure that any set of partitions will
+ be able to house the operating system at installation time.
+
+The graphical installation front end uses the PyGTK library and each screen
+was created with the glade tool.
+
+
+SUPPORT
+
+For support for Weasel, please check with VMware's Community Forums at:
+
+http://communities.vmware.com/community/vmtn/vsphere/upgradecenter
+
+
+
+RUNNING WEASEL
+
+Running Weasel is different than running other Linux-based operating system
+installers. Even though it was developed to run on top of the Linux based
+Console OS and shares many concepts with installers such as Anaconda like RPM
+based packaging, Weasel will not run directly on Linux. There is no direct
+module support for loading modules; in the ESX installer this is taken care
+of by initscripts which are executed before the installer is run and at
+the module load stage.
+
+It is however possible to test Weasel in Linux. To start the graphical
+installer from the weasel directory, invoke the command:
+
+ $ python test/caged_weasel.py weasel.py --nox
+
-Weasel is an Operating System installer similar to Redhat's Anaconda. When you insert the ESX Installation DVD, this program guides you through the steps of network configuration, disk selection, etc. Or it can perform an automated install based on a script similar to Redhat kickstart scripts.
View
587 applychoices.py
@@ -0,0 +1,587 @@
+###############################################################################
+# Copyright (c) 2008-2009 VMware, Inc.
+#
+# This file is part of Weasel.
+#
+# Weasel 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 version 2 and no later version.
+#
+# Weasel 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
+# version 2 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.
+#
+
+'''Performs the actual installation based on the data in userchoices.
+
+See userchoices.py
+'''
+
+import sys
+import exception
+from log import log
+
+import userchoices
+
+class StopProgress(Exception):
+ '''Exception thrown by ProgressCallback.popStatus() when progress has been
+ stopped. XXX Unused'''
+ pass
+
+class StdoutProgressDelegate:
+ '''Basic output delegate for the ProgressCallback that writes to stdout.'''
+
+ def __init__(self):
+ self.lastmsg = None
+
+ def reportit(self, code, callback):
+ """Prepare status message for text installer delivery.
+ Keep 'code' as a back door selector... just in case we get
+ into trouble.
+ """
+ pct = callback.getProgress()
+ msg = callback.getLastMessage()
+ if msg: # The message will be empty when everything completes.
+ msg = "- %s" % msg
+ text = "%3d%% Complete %s" % (pct * 100.0, msg)
+ if msg == self.lastmsg:
+ return # Don't repeat identical message.
+ sys.stdout.write("%s\n" % text) # TODO: may need finer control later
+ sys.stdout.flush()
+
+ self.lastmsg = msg
+
+ def progressStatusStarted(self, callback):
+ self.reportit("SS", callback)
+
+ def progressStatusFinished(self, callback):
+ self.reportit("SF", callback)
+
+ def progressStatusGroupStarted(self, callback):
+ self.reportit("GS", callback)
+
+ def progressStatusGroupFinished(self, callback):
+ self.reportit("GF", callback)
+
+
+class ProgressCallback(object):
+
+ '''Callback class used by worker code to signal progress to the
+ UI code. Before starting their processing, the worker code adds
+ their status information to the callback. The status consists of
+ a descriptive string and the number of units-of-work they will
+ accomplish when finished. The total amount of work to be
+ performed across different workers is recorded by a "status
+ group". So, progress is computed as the number of units-of-work
+ completed so far by the workers versus the total recorded by the
+ group. For example, the following code performs three
+ units-of-work broken down into two tasks:
+
+ >>> pc = ProgressCallback()
+ >>> pc.pushStatusGroup(3) # three units-of-work will be done overall
+ >>> pc.pushStatus("First Thing", 2) # This task will do two units
+ >>> # ... do two units-of-work here ...
+ ... pc.popStatus() # Signal completion of work.
+ >>> "%0.2f" % pc.getProgress() # Check how much progress has been made.
+ '0.67'
+ >>> pc.pushStatus("Second Thing")
+ >>> # ... do one unit-of-work here ...
+ ... pc.popStatus()
+ >>> "%0.2f" % pc.getProgress()
+ '1.00'
+
+ As you might have guessed, the class presents a stack interface, so you
+ can further subdivide a task by pushing another group and its sub-tasks.
+ Overall progress is still updated when tasks are nested like this, just
+ in fractional amounts relative to units-of-work higher up in the stack.
+ For example, the following code breaks down installation into
+ partitioning all of the hard drives and then further subdivides it for
+ the individual drives:
+
+ >>> pc = ProgressCallback()
+ >>> pc.pushStatusGroup(20) # 20 units-of-work will be done for the install
+ >>> pc.pushStatus("Partitioning Hard Drives", 5) # 5 units for partitioning
+ >>> pc.pushStatusGroup(2)
+ >>> pc.pushStatus("Partitioning /dev/sda")
+ >>> pc.popStatus()
+ >>> # Check overall progress. We have done half of the partitioning work
+ ... # and partitioning is 1/4th of the overall work, so we have done
+ ... # 1/8th of the overall work.
+ ... "%0.2f" % pc.getProgress()
+ '0.12'
+ >>> # Pop the partitioning task. Even though we did not do the other
+ ... # partitioning task, the overall progress should be correct anyways.
+ ... pc.popStatusGroup()
+ >>> pc.popStatus()
+ >>> "%0.2f" % pc.getProgress()
+ '0.25'
+
+
+ XXX In the future, pushStatus should return a context manager.
+ '''
+
+ def __init__(self, delegate=None):
+ '''Construct a ProgressCallback with an optional delegate object. The
+ delegate can implement any or all of the following methods:
+
+ progressStatusStarted(progressCallback)
+ Called when a new task has been pushed onto the stack.
+ progressStatusGroupStarted(progressCallback)
+ Called when a new task group has been pushed onto the stack.
+ progressStatusFinished(progressCallback)
+ Called when a task has been popped off of the stack.
+ progressStatusGroupFinished(progressCallback)
+ Called when a task group has been popped off of the stack.
+
+ For example:
+
+ >>> class MyDelegate:
+ ... def progressStatusFinished(self, callback):
+ ... print "%3d%% Completed" % (callback.getProgress() * 100)
+ >>> pc = ProgressCallback(MyDelegate())
+ >>> pc.pushStatusGroup(20)
+ >>> pc.pushStatus("Hello, World!")
+ >>> pc.popStatus()
+ 5% Completed
+ '''
+
+ self.statusStack = []
+ self.delegate = delegate
+ self.stopped = False
+ return
+
+ def _isStatusGroup(self, index):
+ '''Return True if the given index is for a task group.'''
+
+ return index % 2 == 0
+
+ def _isStatus(self, index):
+ '''Return False if the given index is for a task.'''
+
+ return index % 2 == 1
+
+ def _tellDelegate(self, method):
+ '''Call the given delegate method, if there is a delegate and it
+ implements the method.'''
+
+ if method == 'progressStatusStarted':
+ log.info("status: progress=%0.2f; messageID=%s; %s" % (
+ self.getProgress(),
+ self.getLastMessageID(),
+ self.getLastMessage()))
+ if hasattr(self.delegate, method):
+ getattr(self.delegate, method)(self)
+
+ def stop(self):
+ '''Stop progress. XXX Unused'''
+ self.stopped = True
+
+ def pushStatusGroup(self, totalPortions):
+ '''Push a task group onto the stack. The totalPortions value
+ should be the sum of all the sub-task portions.
+
+ Note: only a single group should be pushed for the higher-level task.
+ So, do not do this:
+
+ >>> pc = ProgressCallback()
+ >>> pc.pushStatusGroup(1)
+ >>> pc.pushStatus("Nested")
+ >>> pc.pushStatusGroup(1)
+ >>> pc.popStatusGroup()
+ >>> pc.pushStatusGroup(1) # NO!
+ Traceback (most recent call last):
+ . . .
+ AssertionError: only one sub-group allowed per-task
+ '''
+
+ assert self._isStatusGroup(len(self.statusStack)), "top is not a task"
+ assert (not self.statusStack or
+ self.statusStack[-1]['remaining'] == 1.0), \
+ "only one sub-group allowed per-task"
+
+ self.statusStack.append({
+ 'portion' : float(totalPortions),
+ 'remaining' : 1.0,
+ })
+
+ self._tellDelegate('progressStatusGroupStarted')
+
+ def pushStatus(self, msg, portion=1, msgID='NONE'):
+ '''Push a new task onto the stack with the given values:
+
+ msg The english message to show to the user.
+ portion The number of units-of-work that this task accomplishes
+ compared to its peer tasks.
+ msgID "Enumerated value" for the message, intended for use by
+ clients that need to do localization.
+
+ Note: You must push a group before adding your actual tasks. The
+ root group is used to track the overall progress.
+ '''
+
+ assert self._isStatus(len(self.statusStack)), "top is not a group"
+ assert portion >= 0
+
+ self.statusStack.append({
+ 'msg' : msg,
+ 'msgID' : msgID,
+ 'portion' : float(portion),
+ 'remaining' : 1.0,
+ })
+
+ self._tellDelegate('progressStatusStarted')
+
+ def _pop(self, delegateMethod):
+ '''Pop the top task off of the stack and update the amount of work
+ remaining in the tasks higher up in the stack. The delegateMethod
+ is the name of the method in the delegate to execute after updating
+ the progress values, but before popping the task off the stack.
+ '''
+
+ assert self.statusStack, "no tasks/groups on the stack"
+
+ # Subtract the units-of-work remaining in this task from the upper
+ # levels, scaling the value along the way.
+ ratio = self.statusStack[-1]['remaining']
+
+ for taskIndex in reversed(range(1, len(self.statusStack))):
+ child = self.statusStack[taskIndex]
+ parent = self.statusStack[taskIndex - 1]
+ if not self._isStatusGroup(taskIndex):
+ # Scale the remaining value based on the task's portion within
+ # the group. The scale between a group and its higher-level
+ # task is the same we there's no scaling.
+ ratio *= child['portion'] / parent['portion']
+ parent['remaining'] -= ratio
+
+ if len(self.statusStack) == 1:
+ # XXX sillyness to get around rounding errors and show 100%...
+ self.statusStack[0]['remaining'] = 0.0
+ log.info("status: progress=1.0; messageID=None;")
+
+ self._tellDelegate(delegateMethod)
+ self.statusStack.pop()
+
+ if self.stopped and len(self.statusStack) >= 2:
+ raise StopProgress, "Stopped while %s" % self.getLastMessage()
+
+ def popStatus(self):
+ assert self._isStatus(len(self.statusStack) - 1), "top is not a task"
+
+ self._pop('progressStatusFinished')
+
+ def popStatusGroup(self):
+ assert self._isStatusGroup(len(self.statusStack) - 1), \
+ "top is not a group"
+
+ self._pop('progressStatusGroupFinished')
+
+ def getProgress(self):
+ '''Return the current progress through the tasks as a floating point
+ value between zero and one.
+ '''
+
+ assert self.statusStack, "no active tasks"
+
+ return 1.0 - self.statusStack[0]['remaining']
+
+ def getLastMessageID(self):
+ # XXX Merge this with getLastMessage()?
+ if len(self.statusStack) >= 2:
+ for status in reversed(self.statusStack):
+ if 'msgID' in status and status['msgID'] != 'NONE':
+ return status['msgID']
+ return "NONE"
+
+ def getLastMessage(self):
+ '''Return the message from the last task pushed onto the stack. If a
+ group was the last thing pushed onto the stack, the message will be
+ from the task above it.
+
+ >>> pc = ProgressCallback()
+ >>> pc.pushStatusGroup(10)
+ >>> pc.pushStatus("Hello, World!")
+ >>> pc.getLastMessage()
+ 'Hello, World!'
+ >>> pc.pushStatusGroup(20)
+ >>> pc.getLastMessage()
+ 'Hello, World!'
+ '''
+
+ if len(self.statusStack) < 2:
+ return ""
+ else:
+ return self.statusStack[-1 - len(self.statusStack) % 2]['msg']
+
+
+class Context:
+ def __init__(self, cb=None):
+ if not cb:
+ cb = ProgressCallback()
+ self.cb = cb
+
+
+def _loadDriverSteps():
+ import customdrivers
+
+ # XXX - don't add any steps after LOADDRIVERS since a non-critical
+ # exception can handle there which is passed up through to the
+ # gui.
+
+ retval = [
+ (5, 'Unpack Drivers', 'UNPACK',
+ customdrivers.hostActionUnpackDrivers),
+ (1, 'Rebuilding Map File', 'MAPFILE',
+ customdrivers.hostActionRebuildSimpleMap),
+ (80, 'Loading Drivers', 'LOADDRIVERS',
+ customdrivers.hostActionLoadDrivers),
+ ]
+
+ return retval
+
+def _installSteps():
+ '''Returns a list of steps needed to perform a complete installation.'''
+
+ import partition
+ import fsset
+ import packages
+ import services
+ import workarounds
+ import users
+ import bootloader
+ import script
+ import networking
+ import firewall
+ import esxlicense
+ import devices
+ import timezone
+ import timedate
+ import systemsettings
+ import fstab
+ import esxconf
+ import scriptwriter
+ import log as logmod
+
+ # Map of installation steps. Each step has a name and a tuple conntaining
+ # the following values:
+ #
+ # portion The units-of-work done by this step, relative to the others
+ # desc A human-readable description of what the step is doing.
+ # msgID A machine-readable description of the step.
+ # func The function that implements the step.
+ retval = [
+ (10, 'Clearing Partitions', 'CLEARPART',
+ partition.hostActionClearPartitions),
+
+ (1, 'Removing Unwanted VMDK Files', 'CLEARVMDK',
+ esxconf.hostActionRemoveVmdk),
+
+ (10, 'Partitioning Physical Hard Drives', 'PARTPHYS',
+ partition.hostActionPartitionPhysicalDevices),
+ (10, 'Partitioning Virtual Hard Drives', 'PARTVIRT',
+ partition.hostActionPartitionVirtualDevices),
+
+ (5, 'Mounting File Systems', 'MOUNT',
+ partition.hostActionMountFileSystems),
+ (1, 'Mounting File Systems', 'MOUNT',
+ fsset.hostActionMountPseudoFS),
+
+ (1, 'Copy Installer Log', 'LOG', logmod.hostActionCopyLogs),
+
+ (80, 'Installing Packages', 'PACKAGES',
+ packages.hostActionInstallPackages),
+
+ (1, 'Service Settings', 'SERVICES', services.hostAction),
+
+ (1, 'Setup Module Loading', 'MODPROBE',
+ workarounds.hostActionRedirectLKMLoading),
+
+ (1, 'Create fstab', 'FSTAB', fstab.hostActionWriteFstab),
+ (1, 'Boot Location', 'BOOTLOADER', devices.hostActionSetupVmdk),
+
+ # networking.hostAction destroys the network setup for our installation
+ # environment, so we have to do this here. :-/
+ (1, 'Updating esxupdate Database', 'ESXUPDATEDB',
+ workarounds.hostActionUpdateEsxupdateDatabase),
+
+ (1, 'Installing Network Configuration', 'NETWORKING',
+ networking.hostAction), #used to be workarounds.copynetconfig
+
+ (1, 'Setting Timezone', 'TIMEZONE', timezone.hostActionTimezone),
+ (1, 'Setting Time and Date', 'TIMEDATE', timedate.hostActionTimedate),
+ (1, 'Setting Keyboard', 'KEYBOARD', systemsettings.hostActionKeyboard),
+ (1, 'Setting Language', 'LANGUAGE', systemsettings.hostActionLang),
+
+ (1, 'Set Console Memory', 'CONSOLEMEM',
+ workarounds.hostActionSetCosMemory),
+
+ (1, 'Copying ESX Configuration', 'ESXCONF',
+ esxconf.hostActionCopyConfig),
+
+ # Firewall depends on esx.conf being installed.
+ (1, 'Firewall Settings', 'FIREWALL', firewall.hostAction),
+
+ (1, 'Configuring Authentication', 'AUTH',
+ users.hostActionAuthentication),
+
+ (1, 'Setting License', 'LICENSE', esxlicense.hostAction),
+
+ (1, 'Configuring User Accounts', 'ROOTPASS',
+ users.hostActionSetupAccounts),
+
+ (5, 'Boot Setup', 'BOOTLOADER', bootloader.hostAction),
+
+ (1, 'Running "%post" Script', 'POSTSCRIPT',
+ script.hostActionPostScript),
+
+ (1, 'Writing ks.cfg', 'KS_SCRIPT',
+ scriptwriter.hostAction),
+
+ (1, 'Boot Setup', 'BOOTLOADER', bootloader.hostActionRebuildInitrd),
+
+ (1, 'Checking esx.conf consistency', 'VALIDATE',
+ esxconf.validateAction),
+ ]
+
+ return retval
+
+def _upgradeSteps():
+ '''Returns a list of steps needed to perform an upgrade.'''
+
+ import partition
+ import fsset
+ import fstab
+ import devices
+ import packages
+ import workarounds
+ import migrate
+ import users
+ import networking
+ import esxlicense
+ import bootloader
+ import script
+ import esxconf
+ import log as logmod
+
+ retval = [
+ (1, 'Add Upgrade Log', 'LOG', logmod.hostActionAddUpgradeLogs),
+
+ (10, 'Partitioning Virtual Hard Drives', 'PARTVIRT',
+ partition.hostActionPartitionVirtualDevices),
+
+ (5, 'Mounting File Systems', 'MOUNT',
+ partition.hostActionMountFileSystems),
+ (1, 'Mounting File Systems', 'MOUNT',
+ fsset.hostActionMountPseudoFS),
+
+ (1, 'Copy Installer Log', 'LOG', logmod.hostActionCopyLogs),
+
+ (1, 'Create fstab', 'FSTAB', fstab.hostActionWriteFstab),
+ (1, 'Boot Location', 'BOOTLOADER', devices.hostActionSetupVmdk),
+
+ (1, 'Migrating Configuration', 'MIGRATING',
+ migrate.hostActionPrePackages),
+
+ (80, 'Installing Packages', 'PACKAGES',
+ packages.hostActionInstallPackages),
+
+ (1, 'Setup Module Loading', 'MODPROBE',
+ workarounds.hostActionRedirectLKMLoading),
+
+ (1, 'Migrating fstab', 'MIGRATE_FSTAB', fstab.hostActionMigrateFstab),
+ (1, 'Migrating Configuration', 'MIGRATING', migrate.hostAction),
+ (1, 'Migrating Groups', 'MIGRATING',
+ users.hostActionMigrateGroupFile),
+ (1, 'Migrating Users', 'MIGRATING',
+ users.hostActionMigratePasswdFile),
+
+ # networking.hostAction destroys the network setup for our installation
+ # environment, so we have to do this here. :-/
+ (1, 'Updating esxupdate Database', 'ESXUPDATEDB',
+ workarounds.hostActionUpdateEsxupdateDatabase),
+
+ (1, 'Updating Network Configuration', 'NETWORKING',
+ networking.hostActionUpdate),
+
+ (1, 'Setting License', 'LICENSE', esxlicense.hostAction),
+
+ (1, 'Set Console Memory', 'CONSOLEMEM',
+ workarounds.hostActionSetCosMemory),
+
+ (1, 'Copying ESX Configuration', 'ESXCONF',
+ esxconf.hostActionCopyConfig),
+
+ (1, 'Writing Cleanup Script', 'MIGRATING',
+ migrate.hostActionCleanupScripts),
+
+ (1, 'Running "%post" Script', 'POSTSCRIPT',
+ script.hostActionPostScript),
+
+ # Do this after the %post script in case it fails, we want to boot back
+ # into the old cos.
+ (5, 'Boot Setup', 'BOOTLOADER', bootloader.hostAction),
+
+ (1, 'Checking esx.conf consistency', 'VALIDATE',
+ esxconf.validateAction),
+ ]
+
+ return retval
+
+def doit(context, stepListType='install'):
+ '''Executes the steps needed to do the actual install or upgrade.'''
+
+ if stepListType == 'install':
+ if userchoices.getUpgrade():
+ steps = _upgradeSteps()
+ else:
+ steps = _installSteps()
+ elif stepListType == 'loadDrivers':
+ steps = _loadDriverSteps()
+
+ assert steps
+
+ try:
+ context.cb.pushStatusGroup(sum([step[0] for step in steps]))
+ for portion, desc, msgID, func in steps:
+ context.cb.pushStatus(desc, portion, msgID=msgID)
+ func(context)
+ context.cb.popStatus()
+ context.cb.popStatusGroup()
+
+ if stepListType == 'install':
+ log.info("installation complete")
+ elif stepListType == 'loadDrivers':
+ log.info("driver loading complete")
+ except exception.InstallCancelled:
+ pass
+
+ return
+
+def ensureDriversAreLoaded(func):
+ '''Load all of the drivers'''
+ import customdrivers
+
+ def _wrapper(*args, **kwargs):
+ if not customdrivers.DRIVERS_LOADED:
+ context = Context(ProgressCallback(StdoutProgressDelegate()))
+ sys.stdout.write("Loading system drivers...\n")
+ try:
+ doit(context, stepListType='loadDrivers')
+ except customdrivers.ScriptLoadError, msg:
+ # one or more of the init scripts failed to load however
+ # not in a critical manner. we've already logged it, so
+ # just keep on truckin'
+ pass
+
+ return func(*args, **kwargs)
+
+ return _wrapper
+
+if __name__ == "__main__":
+ import doctest
+ doctest.testmod()
View
537 boot_cmdline.py
@@ -0,0 +1,537 @@
+#! /usr/bin/env python
+
+###############################################################################
+# Copyright (c) 2008-2009 VMware, Inc.
+#
+# This file is part of Weasel.
+#
+# Weasel 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 version 2 and no later version.
+#
+# Weasel 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
+# version 2 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.
+#
+
+'''
+boot_cmdline.py
+
+This module is responsible for parsing arguments that were set on the
+"boot:" command line
+'''
+import os
+import re
+import sys
+import util
+import media
+import shlex
+import cdutil
+import devices
+import networking
+import userchoices
+import applychoices
+import remote_files
+from log import log
+from consts import ExitCodes, CDROM_DEVICE_PATH
+from grubupdate import GRUB_CONF_PREV
+
+USB_MOUNT_PATH = "/mnt/usbdisk"
+UUID_MOUNT_PATH = "/mnt/by-uuid"
+CDROM_MOUNT_PATH = "/mnt/cdrom"
+
+genericErr = ('There was a problem with the %s specified on the command'
+ ' line. Error: %s.')
+
+KERNEL_BUFFER_LEN = 256
+
+def failWithLog(msg):
+ log.error("installation aborted")
+ log.error(msg)
+ sys.exit(ExitCodes.WAIT_THEN_REBOOT)
+
+class NetworkChoiceMaker(object):
+ '''A NetworkChoiceMaker object will add choices to userchoices that
+ are necessary consequences of the user adding some earlier choice.
+ For example, if the user chooses an IP address, but doesn't make a
+ choice for the netmask, we need to make a guess for the netmask.
+ If the user doesn't choose ANY networking options, then the
+ assumption is that we're not doing a network install and the
+ NetworkChoiceMaker object's "needed" attribute will be False.
+ '''
+ def __init__(self):
+ self.needed = False
+
+ @applychoices.ensureDriversAreLoaded
+ def setup(self):
+ log.debug('Setting network options for media downloads')
+ nicChoices = userchoices.getDownloadNic()
+ nic = nicChoices.get('device', None)
+
+ if nic and not nic.isLinkUp:
+ failWithLog(('The specified network interface card (Name: %s'
+ ' MAC Address: %s) is not plugged in.'
+ ' Installation cannot continue as requested') %\
+ (nic.name, nic.macAddress))
+ elif not nic and not networking.getPluggedInAvailableNIC(None):
+ # Check for an available NIC before we go further.
+ # It's best to fail early and provide a descriptive error
+ failWithLog('This system does not have a network interface'
+ ' card that is plugged in, or all network'
+ ' interface cards are already claimed. '
+ ' Installation cannot continue as requested')
+
+ # Create a netmask if it was left out
+ ip = nicChoices.get('ip', None)
+ netmask = nicChoices.get('netmask', None)
+
+ if netmask and not ip:
+ failWithLog('Netmask specified, but no IP given.')
+ if ip and not netmask:
+ log.warn('IP specified, but no netmask given. Guessing netmask.')
+ try:
+ netmask = networking.utils.calculateNetmask(ip)
+ except ValueError, ex:
+ msg = ((genericErr + ' A netmask could not be created.')
+ % ('IP Address', str(ex)))
+ failWithLog(msg)
+ nicChoices.update(netmask=netmask)
+ userchoices.setDownloadNic(**nicChoices)
+
+ log.debug(" nic options from boot command line -- %s" % nicChoices)
+ log.debug(" net options from boot command line -- %s" %
+ userchoices.getDownloadNetwork())
+
+ def updateNetworkChoices(self, **kwargs):
+ self.needed = True
+ netChoices = userchoices.getDownloadNetwork()
+ if not netChoices:
+ newArgs = dict(gateway='', nameserver1='', nameserver2='',
+ hostname='localhost')
+ else:
+ newArgs = netChoices
+ newArgs.update(kwargs)
+ userchoices.setDownloadNetwork(**newArgs)
+
+ def updateNicChoices(self, **kwargs):
+ self.needed = True
+ nicChoices = userchoices.getDownloadNic()
+ if not nicChoices:
+ # was empty - this is the first time populating it.
+ newArgs = dict(device='', vlanID='') #set defaults
+ else:
+ newArgs = nicChoices
+ newArgs.update(kwargs)
+ userchoices.setDownloadNic(**newArgs)
+
+# module-level NetworkChoiceMaker object
+__networkChoiceMaker = None
+def getNetworkChoiceMaker():
+ global __networkChoiceMaker
+ if not __networkChoiceMaker:
+ __networkChoiceMaker = NetworkChoiceMaker()
+ return __networkChoiceMaker
+
+
+def _setDownloadIP(match):
+ '''Handle the "ip=..." option.'''
+ ip = match.group(1)
+ try:
+ networking.utils.sanityCheckIPString(ip)
+ except ValueError, ex:
+ failWithLog(genericErr % ('IP Address', str(ex)))
+
+ networkChoiceMaker = getNetworkChoiceMaker()
+ networkChoiceMaker.updateNicChoices(ip=ip,
+ bootProto=userchoices.NIC_BOOT_STATIC)
+
+def _setDownloadNetmask(match):
+ '''Handle the "netmask=..." option.'''
+ netmask = match.group(1)
+ try:
+ networking.utils.sanityCheckNetmaskString(netmask)
+ except ValueError, ex:
+ failWithLog(genericErr % ('Netmask', str(ex)))
+
+ networkChoiceMaker = getNetworkChoiceMaker()
+ networkChoiceMaker.updateNicChoices(netmask=netmask)
+
+def _setDownloadGateway(match):
+ '''Handle the "gateway=..." option.'''
+ gateway = match.group(1)
+ try:
+ networking.utils.sanityCheckGatewayString(gateway)
+ except ValueError, ex:
+ failWithLog(genericErr % ('Gateway Address', str(ex)))
+
+ networkChoiceMaker = getNetworkChoiceMaker()
+ networkChoiceMaker.updateNetworkChoices(gateway=gateway)
+
+
+@applychoices.ensureDriversAreLoaded
+def _setNetDevice(match):
+ '''Handle the "netdevice=..." option.'''
+ # The pxelinux BOOTIF option uses dashes instead of colons.
+ nicName = match.group(1).replace('-', ':')
+ try:
+ if ':' in nicName:
+ # assume it is a MAC address
+ nic = networking.findPhysicalNicByMacAddress(nicName)
+ if not nic:
+ raise ValueError('No NIC found with MAC address %s' % nicName)
+ else:
+ # assume it is a vmnicXX style name
+ nic = networking.findPhysicalNicByName(nicName)
+ if not nic:
+ raise ValueError('No NIC found with name %s' % nicName)
+ except ValueError, ex:
+ failWithLog(genericErr % ('Network Device', str(ex)))
+
+ networkChoiceMaker = getNetworkChoiceMaker()
+ networkChoiceMaker.updateNicChoices(device=nic)
+
+@applychoices.ensureDriversAreLoaded
+def _setVlanID(match):
+ '''Handle the "vlanID=..." option.'''
+ vlanID = match.group(1)
+ try:
+ networking.utils.sanityCheckVlanID(vlanID)
+ except ValueError, ex:
+ failWithLog(genericErr % ('VLAN ID', str(ex)))
+
+ networkChoiceMaker = getNetworkChoiceMaker()
+ networkChoiceMaker.updateNicChoices(vlanID=vlanID)
+
+def _setDownloadNameserver(match):
+ '''Handle the "nameserver=..." option.'''
+ nameserver = match.group(1)
+ try:
+ networking.utils.sanityCheckIPString(nameserver)
+ except ValueError, ex:
+ failWithLog(genericErr % ('Nameserver Address', str(ex)))
+
+ networkChoiceMaker = getNetworkChoiceMaker()
+ networkChoiceMaker.updateNetworkChoices(nameserver1=nameserver)
+
+def _upgradeOption(_match):
+ # TODO: We're in an upgrade, probably have to run through the init scripts
+ # to get storage up and running.
+ return []
+
+def _urlOption(match):
+ '''Handle the "url=..." option.'''
+ return [('--url', match.group(1))]
+
+def _ksFileOption(match):
+ '''Handle the "ks=http://<urn>", "ks=file://<path>", etc option.'''
+ filePath = match.group(1)
+ if remote_files.isURL(filePath) and not filePath.startswith('file'):
+ networkChoiceMaker = getNetworkChoiceMaker()
+ networkChoiceMaker.needed = True
+ return [('-s', filePath)]
+
+def _ksNFSOption(match):
+ '''Handle the "ks=nfs:<host>:/path" option.'''
+ log.warn("The 'ks=nfs:<host>:/path/to/file' format is deprecated.")
+ log.warn("Please use the 'nfs://<host>/path/to/file' format instead.")
+ networkChoiceMaker = getNetworkChoiceMaker()
+ networkChoiceMaker.needed = True
+ return [('-s', "nfs://%s%s" % match.groups())]
+
+@applychoices.ensureDriversAreLoaded
+def _ksFileUUIDOption(match):
+ uuid = match.group(1)
+ path = match.group(2)
+
+ diskSet = devices.DiskSet(forceReprobe=True)
+ diskPartTuple = diskSet.findFirstPartitionMatching(uuid=uuid)
+ if diskPartTuple:
+ disk, _part = diskPartTuple
+ userchoices.addDriveUse(disk.name, 'kickstart')
+
+ mountPath = os.path.join(UUID_MOUNT_PATH, uuid)
+ if not os.path.exists(mountPath):
+ os.makedirs(mountPath)
+ if util.mount(uuid, mountPath, isUUID=True):
+ os.rmdir(mountPath)
+ failWithLog("error: cannot mount partition with UUID: %s\n" % uuid)
+
+ ksPath = os.path.join(mountPath, path[1:])
+ return [('-s', ksPath)]
+
+def _ksFileCdromOption(match):
+ path = match.group(1)
+
+ if not os.path.exists(CDROM_MOUNT_PATH):
+ os.makedirs(CDROM_MOUNT_PATH)
+
+ for cdPath in cdutil.cdromDevicePaths():
+ if util.mount(cdPath, CDROM_MOUNT_PATH):
+ log.warn("cannot mount cd-rom in %s" % cdPath)
+ continue
+
+ ksPath = os.path.join(CDROM_MOUNT_PATH, path.lstrip('/'))
+ if os.path.exists(ksPath):
+ return [('-s', ksPath)]
+
+ util.umount(CDROM_MOUNT_PATH)
+
+ failWithLog("cannot find kickstart file on cd-rom with path -- %s" % path)
+
+def _usbOption(match):
+ '''Handle the "ks=usb" and "ks=usb:<path>" option.'''
+
+ try:
+ ksFile = match.group(1)
+ except IndexError:
+ ksFile = "ks.cfg"
+
+ firstTime = True
+ while True:
+ if not firstTime:
+ # XXX Maybe we should just stop retrying after awhile?
+ log.info("Insert a USB storage device that contains '%s' "
+ "file to perform a scripted install..." % ksFile)
+ util.rawInputCountdown("\rrescanning in %2d second(s), "
+ "press <enter> to rescan immediately", 10)
+ firstTime = False
+
+ diskSet = devices.DiskSet(forceReprobe=True)
+
+ usbDisks = [disk for disk in diskSet.values()
+ if disk.driverName == devices.DiskDev.DRIVER_USB_STORAGE]
+
+ if not usbDisks:
+ log.info("") # XXX just for spacing
+ log.warn("No USB storage found.")
+ continue
+
+ kickstartPath = os.path.join(USB_MOUNT_PATH, ksFile.lstrip('/'))
+
+ if not os.path.exists(USB_MOUNT_PATH):
+ os.makedirs(USB_MOUNT_PATH)
+
+ for disk in usbDisks:
+ for part in disk.partitions:
+ if part.partitionId == -1:
+ continue
+
+ if (part.getFsTypeName() not in ("ext2", "ext3", "vfat")):
+ # Don't try mounting partitions with filesystems that aren't
+ # likely to be on a usb key.
+ continue
+
+ if util.mount(part.consoleDevicePath,
+ USB_MOUNT_PATH,
+ fsTypeName=part.getFsTypeName()):
+ log.warn("Unable to mount '%s'" % part.consoleDevicePath)
+ continue
+
+ if os.path.exists(kickstartPath):
+ userchoices.addDriveUse(disk.name, 'kickstart')
+ return [('-s', kickstartPath)]
+
+ if util.umount(USB_MOUNT_PATH):
+ failWithLog("Unable to umount '%s'" % USB_MOUNT_PATH)
+
+ log.info("")
+ log.warn("%s was not found on any attached USB storage." % ksFile)
+
+def _debugOption(_match):
+ return [('-d', None)]
+
+def _debugPatchOption(match):
+ return [('--debugpatch', match.group(1))]
+
+def _textOption(_match):
+ return [('-t', None)]
+
+def _mediaCheckOption(_match):
+ return [('--mediacheck', None)]
+
+def _askMediaOption(_match):
+ return [('--askmedia', None)]
+
+def _noEjectOption(_match):
+ return [('--noeject', None)]
+
+def _setVideoDriver(match):
+ '''Handle the video driver selection'''
+ return [('--videodriver', match.group(1))]
+
+def _setSerialTty(_match):
+ return [('--serial', None)]
+
+@applychoices.ensureDriversAreLoaded
+def _bootpartOption(match):
+ uuid = match.group(1)
+ if not util.uuidToDevicePath(uuid):
+ failWithLog("error: cannot find device for UUID: %s\n" % uuid)
+
+ userchoices.setBootUUID(uuid)
+
+ mountPath = util.mountByUuid(uuid)
+ if not mountPath:
+ failWithLog("error: cannot mount boot partition with UUID -- %s" % uuid)
+
+ restoredGrubConf = False
+ for prefix in ("boot/grub", "grub"):
+ path = os.path.join(mountPath, prefix, "grub.conf")
+ if os.path.exists(path):
+ tmpPath = os.tempnam(os.path.dirname(path), "grub.conf")
+ os.symlink(os.path.basename(GRUB_CONF_PREV), tmpPath)
+ # Use rename so the replacement is atomic.
+ os.rename(tmpPath, path)
+ restoredGrubConf = True
+ break
+ if not restoredGrubConf:
+ log.warn("could not restore %s, upgrade failure will not "
+ "reboot into ESX v3" % GRUB_CONF_PREV)
+
+ util.umount(mountPath)
+
+ return []
+
+@applychoices.ensureDriversAreLoaded
+def _rootpartOption(match):
+ uuid = match.group(1)
+ if not util.uuidToDevicePath(uuid):
+ failWithLog("error: cannot find device for UUID: %s\n" % uuid)
+
+ userchoices.setRootUUID(uuid)
+
+ return []
+
+def _ignoreOption(_match):
+ return
+
+@applychoices.ensureDriversAreLoaded
+def _sourceOption(match):
+ '''Handle the "source=<path>" option.'''
+ path = match.group(1)
+
+ # If the CD is in a drive that is only detected after all the drivers are
+ # loaded then we need to rerun the script that finds the install CD.
+ if not os.path.exists(path):
+ media.runtimeActionMountMedia()
+ if not os.path.exists(path):
+ failWithLog("error: cannot find source -- %s\n" % path)
+
+ if path == CDROM_DEVICE_PATH:
+ pass
+ else:
+ userchoices.setMediaDescriptor(media.MediaDescriptor(
+ partPath=path, partFsName="iso9660"))
+
+ return []
+
+
+def translateBootCmdLine(cmdline):
+ '''Translate any commands from the given command-line, which is presumably
+ from '/proc/cmdline'.
+
+ The 'ks=' option, for example, takes one of the following arguments:
+
+ file:///<path> The path to the kickstart file, no mounts are done.
+
+ The return value is a list of (option, value) pairs that match what the
+ getopt function would return.
+
+ >>> translateBootCmdLine("foo")
+ []
+
+ >>> translateBootCmdLine("linux ks=file:///ks.cfg")
+ [('-s', '/ks.cfg')]
+ >>> translateBootCmdLine("licks=file:///ks.cfg")
+ []
+ '''
+
+ if len(cmdline) == KERNEL_BUFFER_LEN:
+ log.warn("boot command line might have been truncated to %d bytes" %
+ KERNEL_BUFFER_LEN)
+
+ retval = []
+
+ # The set of options that are currently handled. Organized as a list of
+ # pairs where the first element is the regex to match and the second is
+ # the function that takes the regex and returns a list of (option, value)
+ # pairs that match what getopt would return. The function is also free
+ # to perform any necessary setup, like mounting devices.
+ # NOTE: order is important
+ options = [
+ (r'upgrade', _upgradeOption),
+ (r'ip=([^:]+).*', _setDownloadIP),
+ (r'netmask=(.+)', _setDownloadNetmask),
+ (r'gateway=(.+)', _setDownloadGateway),
+ (r'nameserver=(.+)', _setDownloadNameserver),
+ (r'ks=(/.+)', _ksFileOption),
+ (r'ks=UUID:([^:]+):(/.+)', _ksFileUUIDOption),
+ (r'ks=cdrom:(/.+)', _ksFileCdromOption),
+ (r'ks=((?:file|http|https|ftp|nfs)://.+)', _ksFileOption),
+ (r'ks=usb:(/.+)', _usbOption),
+ (r'ks=usb', _usbOption),
+ (r'ks=nfs:([^:/]+):(/.+)', _ksNFSOption),
+ (r'debug', _debugOption),
+ (r'debugpatch=(.+)', _debugPatchOption),
+ (r'text', _textOption),
+ (r'url=(.+)', _urlOption),
+ (r'ksdevice=(.+)', _setNetDevice), #for compatibility with anaconda
+ (r'netdevice=(.+)', _setNetDevice),
+ (r'vlanid=(.+)', _setVlanID),
+ (r'bootpart=(.+)', _bootpartOption),
+ (r'rootpart=(.+)', _rootpartOption),
+ (r'source=(/.+)', _sourceOption),
+ (r'askmedia', _askMediaOption),
+ (r'askmethod', _askMediaOption),
+ (r'noeject', _noEjectOption),
+ (r'mediacheck', _mediaCheckOption),
+ (r'videodriver=(.+)', _setVideoDriver),
+ (r'console=ttyS.*', _setSerialTty),
+
+ # For compatibility with pxelinux
+ (r'ip=[^:]+:[^:]+:([^:]+):[^:]+', _setDownloadGateway),
+ (r'ip=[^:]+:[^:]+:[^:]+:(.+)', _setDownloadNetmask),
+ # The first two hex digits are the hardware interface type. Usually
+ # 01 for ethernet.
+ (r'BOOTIF=\w\w-(.+)', _setNetDevice),
+
+ # Suppress complaints about the standard boot options.
+ # TODO: Decide whether to add all the possible options or drop the
+ # log level of the "if not foundMatch" case below.
+ (r'initrd=.+', _ignoreOption),
+ (r'mem=.+', _ignoreOption),
+ (r'BOOT_IMAGE=.+', _ignoreOption),
+ (r'vmkopts=.+', _ignoreOption),
+ (r'quiet', _ignoreOption),
+ ]
+
+ try:
+ for token in shlex.split(cmdline):
+ foundMatch = False
+ for regex, func in options:
+ match = re.match('^%s$' % regex, token)
+ if match:
+ foundMatch = True
+ result = func(match)
+ if result:
+ retval.extend(result)
+ if not foundMatch:
+ log.info('Weasel skipped boot command line token (%s)' % token)
+ networkChoiceMaker = getNetworkChoiceMaker()
+ if networkChoiceMaker.needed:
+ networkChoiceMaker.setup()
+ except ValueError, e:
+ # shlex.split will throw an error if quotation is bad.
+ failWithLog("error: invalid boot command line -- %s" % str(e))
+
+ return retval
+
+def tidyAction():
+ # Tidy up if 'ks=usb' was used.
+ if os.path.exists(USB_MOUNT_PATH):
+ util.umount(USB_MOUNT_PATH)
View
751 bootloader.py
@@ -0,0 +1,751 @@
+#! /usr/bin/python
+
+###############################################################################
+# Copyright (c) 2008-2009 VMware, Inc.
+#
+# This file is part of Weasel.
+#
+# Weasel 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 version 2 and no later version.
+#
+# Weasel 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
+# version 2 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.
+#
+
+'''
+Functions to configure and install the bootloader (GRUB)
+
+When working with this module, you should keep the following in mind, there
+are 4 contexts you have to keep track of:
+ 1. installer environment (for regular commands)
+ 2. chroot environment (during install, for commands chrooted to /mnt/sysimage)
+ 3. GRUB boot-up environment (post-install, when the BIOS hands off to GRUB)
+ 4. GRUB shell environment (during install, disk mapping comes from device.map)
+'''
+
+import os
+import re
+import shutil
+import glob
+import errno
+import xml.dom.minidom
+
+import consts
+import devices
+import packages
+import workarounds
+import userchoices
+import partition
+import vmkctl
+import migrate
+
+from grubupdate import insertTitle, removeTitle, changeSetting, GRUB_CONF_PREV
+from log import log
+from util import execCommand, \
+ XMLGetTextInUniqueElement, \
+ getUuid, syncKernelBufferToDisk
+
+
+# -------- CONTENT OF THE CONF FILES --------
+
+grub_conf_header = '''\
+###################### grub.conf #####################
+# this file was generated by bootloader.py
+#
+# Any entries in this file marked with the comment
+# #vmware:autogenerated esx
+# Should not be edited by hand. They are likely to
+# be clobbered on or before the next reboot.
+#
+timeout=5
+%(passwordLine)s
+'''
+
+# NOTE: the "root" command will always point to disk hd0 because we only
+# support putting the kernel on the first (according to BIOS order) disk
+grub_conf_entry = '''\
+title %(label)s
+ #vmware:autogenerated esx
+ root (hd0,%(slashBootPartNum)d)
+ uppermem %(upperMemInKB)s
+ kernel %(bootDirRelativeToGrub)s%(kernel_file)s %(kernelOptions)s quiet
+ initrd %(bootDirRelativeToGrub)s%(initrd_file)s
+'''
+
+device_map = '''\
+###################### device.map #####################
+# this file was generated by bootloader.py
+# it is not needed after /usr/sbin/grub has been run
+#
+# (hd0) in this context doesn't mean "first disk" it's
+# just a pointer to the /dev/whatever disk, so the
+# "0" component of (hd0) is functionally arbitrary
+(hd0) %(installerDevDiskName)s
+'''
+
+sysconfig_grub = '''\
+###################### sysconfig/grub #####################
+# this file was generated by bootloader.py
+# it is read by mkinitrd
+#
+
+boot=%(devPartName)s
+
+#
+# forcelba: some BIOSes mis-inform grub by not supplying a
+# correct LBA support bitmap, even when they do
+# have the support for LBA. Using forcelba tells
+# grub to ignore the misinformation.
+forcelba=0
+'''
+
+
+# -------- FILE NAMES -------
+
+instRoot = consts.HOST_ROOT
+grub_dir = instRoot + 'boot/grub/'
+grub_conf_file = grub_dir + 'grub.conf'
+sysconfig_grub_file = instRoot + 'etc/sysconfig/grub'
+device_map_file = '/tmp/device.map' #temp file for /usr/sbin/grub
+kernel_file = 'vmlinuz'
+initrd_file = 'initrd.img'
+grub_stage_img_dir = instRoot + 'usr/share/grub/x86_64-redhat/'
+
+# -------- COMMANDS --------
+
+cmd_esxcfgboot = '/usr/sbin/esxcfg-boot'
+
+# grub_exec_cmd
+# "root BLAH": specifies what partition to find the stage1 file
+# using (hd0,0) just means "use the first partition of whichever
+# device was called (hd0) in device_map_file
+# "setup BLAH" actually installs grub, using images found on the specified disk
+cmd_grub = '/usr/sbin/grub --batch --device-map='+ device_map_file +''' <<EOF
+root (hd0,%(slashBootPartNum)s)
+setup %(grubInstallLocation)s
+quit
+EOF'''
+
+
+
+class PartitionNotFound(Exception):
+ def __init__(self, partname):
+ Exception.__init__(self, 'No %s partition has been defined' % partname)
+
+class MissingConsoleDevicePath(Exception):
+ def __init__(self, partition):
+ Exception.__init__(self, '''\
+ Partition (%s) is missing a console device path. This may indicate
+ that the partition requests have not yet been fitted to the disk.
+ ''' % partition.name)
+
+class GrubCheckException(Exception):
+ def __init__(self, msg):
+ Exception.__init__(self, msg)
+ self.msg = msg
+
+
+#------------------------------------------------------------------------------
+def getKernelOptions(kernelMemInMB):
+ '''Create the list of options that get appended onto the "kernel"
+ line of the grub.conf file.
+ '''
+ disks = devices.DiskSet()
+ rootPartition = disks.findPartitionContaining('/', searchVirtual=True)
+ if not rootPartition:
+ raise PartitionNotFound('/')
+
+ devPartName = rootPartition.consoleDevicePath
+ if not devPartName:
+ # expected /dev/sda1 for the 1-disk case
+ raise MissingConsoleDevicePath(rootPartition)
+ rootPartUuid = getUuid(devPartName)
+
+ # ro: mount "/" read-only in the initrd environment but when the init
+ # scripts start up it magically gets remounted rw.
+ options = 'ro'
+
+ # root: tell the kernel where to find "/"
+ options += ' root=UUID='+ rootPartUuid
+
+ # mem: tell the kernel how much memory it can use
+ options += ' mem='+ kernelMemInMB +'M'
+
+ choices = userchoices.getBoot()
+ if choices and choices['kernelParams']:
+ options += ' '+ choices['kernelParams']
+ return options
+
+#------------------------------------------------------------------------------
+def getPasswordLine():
+ '''Create the (optional) "password" line of the grub.conf file.
+ [password [--md5] PASSWD]
+ '''
+ passwordLine = ''
+ password = ''
+
+ choices = userchoices.getBoot()
+ if choices:
+ password = choices['password']
+
+ if (password
+ and choices['passwordType'] == userchoices.BOOT_PASSWORD_TYPE_MD5):
+ passwordLine = 'password --md5 '+ password
+ elif password:
+ passwordLine = 'password '+ password
+ return passwordLine
+
+#------------------------------------------------------------------------------
+def getKernelLabelAndVersion():
+ ''' Grab the kernel_name and kernel_version from the packages.xml file '''
+ packagesXML = packages.getPackagesXML(None)
+
+ kernLabel = packagesXML.kernLabel.encode('ascii')
+ kernVersion = packagesXML.kernVersion.encode('ascii')
+
+ return kernLabel, kernVersion
+
+#------------------------------------------------------------------------------
+def findGrubInstallDevice(slashBootPartition):
+ '''Find the device (the disk) to install grub onto (write the MBR).
+ In the future, this might return something other than the disk
+ containing the /boot partition. For example, a userchoices attribute
+ may override the selection, or the policy might change to
+ "the first device in GetOrderedDrives()".
+ '''
+ disks = devices.DiskSet()
+ disk = disks.findDiskContainingPartition(slashBootPartition)
+ assert disk, "Just found /boot partition, but then it's disk vanished!"
+ return disk
+
+
+#------------------------------------------------------------------------------
+def grubDiskAndPartitionIndicies(part):
+ '''Grub defines disks and partitions by their indicies as they are found
+ in the boot order. It has no knowledge of sdX versus hdX versus CCISS
+ versus DAC960. It just knows the first, second, third, etc. and expects
+ them to be indicated in its conf files in a format like (hd0,0)
+ So this function, for a given partition, gets the index of the disk and
+ the index of the partition on said disk.
+
+ Raise a KeyError if it can not be found in the DiskSet.
+ '''
+ disks = devices.DiskSet()
+ orderedDrives = disks.getOrderedDrives()
+
+ for diskIndex, drive in enumerate(orderedDrives):
+ for candidatePart in drive.partitions:
+ if candidatePart == part:
+ # logical partitions ALWAYS start at 4, so use partitionId
+ grubPartitionNum = candidatePart.partitionId-1
+ log.debug('Disk/partition %s/%s enumerated as %d,%d '
+ 'for grub' % (drive.name, candidatePart.name,
+ diskIndex, grubPartitionNum))
+ return (diskIndex, grubPartitionNum)
+
+ nonStandardDisk = disks.findDiskContainingPartition(part)
+ if nonStandardDisk:
+ # XXX this should disappear...
+ # logical partitions ALWAYS start at 4, so use partitionId
+ grubPartitionNum = part.partitionId-1
+ log.debug('Non-standard disk/partition 0/%d' % (grubPartitionNum))
+ return (0, grubPartitionNum)
+
+ partName = part.getName()
+ raise KeyError('Partition not found in disk set: %s' % partName)
+
+#------------------------------------------------------------------------------
+def grubDiskIndex(disk):
+ '''Operates like grubDiskAndPartitionIndicies but it only cares about
+ the disk index.
+ '''
+ disks = devices.DiskSet()
+ orderedDrives = disks.getOrderedDrives()
+
+ for diskIndex, candidateDisk in enumerate(orderedDrives):
+ if candidateDisk == disk:
+ return diskIndex
+
+ raise KeyError('Disk not found in disk set: %s' % disk.name)
+
+#------------------------------------------------------------------------------
+
+def getStringSubstitutionDict():
+ label, version = getKernelLabelAndVersion()
+
+ # Get the kernel mem to put in the boot command line.
+ kernelMemInMB = vmkctl.MemoryInfoImpl().GetServiceConsoleReservedMem()
+ upperMemInKB = str(kernelMemInMB*1024)
+
+ disks = devices.DiskSet()
+
+ if userchoices.getUpgrade():
+ # We reuse the boot partition from the old install.
+ bootPath = os.path.join(consts.ESX3_INSTALLATION, "boot")
+ else:
+ bootPath = "/boot"
+ slashBootPartition = disks.findPartitionContaining(bootPath)
+ if not slashBootPartition:
+ raise PartitionNotFound(bootPath)
+
+ devPartName = slashBootPartition.consoleDevicePath
+ if not devPartName:
+ raise MissingConsoleDevicePath(slashBootPartition)
+ slashBootPartUuid = getUuid(devPartName)
+
+ diskIndex, partIndex = grubDiskAndPartitionIndicies(slashBootPartition)
+ if diskIndex != 0:
+ log.warn('Installing GRUB to the MBR of a disk that was not the first'
+ ' disk reported by the BIOS. User must change their BIOS'
+ ' settings if they want to boot from this disk')
+ slashBootPartNum = partIndex
+
+ grubInstallDevice = findGrubInstallDevice(slashBootPartition)
+
+ # This code protects against instability with the /vmfs partition
+ # going missing. We try to use the canonical path first, however if it
+ # isn't there, use the /dev/sdX path and warn. The variable is used
+ # in device.map.
+
+ if os.path.exists(grubInstallDevice.path):
+ installerDevDiskName = grubInstallDevice.path
+ elif os.path.exists(grubInstallDevice.consoleDevicePath):
+ log.warn('The normal path to the boot disk did not exist. '
+ 'Using console device path instead.')
+ installerDevDiskName = grubInstallDevice.consoleDevicePath
+ else:
+ raise RuntimeError, "Couldn't find a place to write GRUB."
+
+ # decide between installing to the MBR or the first partition
+ bootChoices = userchoices.getBoot()
+ if bootChoices and \
+ bootChoices['location'] == userchoices.BOOT_LOC_PARTITION:
+ grubInstallLocation = '(hd0,%s)' % slashBootPartNum
+ log.info('Installing GRUB to the first partition of %s ' %
+ installerDevDiskName)
+ else:
+ grubInstallLocation = '(hd0)'
+ log.info('Installing GRUB to the MBR of (%s)' % installerDevDiskName)
+
+ # need to tell grub.conf where it will find the kernel and initrd
+ # relative to the root of the partition it searches.
+ if slashBootPartition.mountPoint in [
+ '/boot', os.path.join(consts.ESX3_INSTALLATION, "boot")]:
+ bootDirRelativeToGrub = '/'
+ elif slashBootPartition.mountPoint in ['/', consts.ESX3_INSTALLATION]:
+ bootDirRelativeToGrub = '/boot/'
+
+ kernelOptions = getKernelOptions(str(kernelMemInMB))
+
+ passwordLine = getPasswordLine()
+
+
+ substitutes = dict(
+ label = label,
+ version = version,
+ devPartName = devPartName,
+ kernel_file = kernel_file,
+ initrd_file = initrd_file,
+ passwordLine = passwordLine,
+ upperMemInKB = upperMemInKB,
+ kernelMemInMB = kernelMemInMB,
+ kernelOptions = kernelOptions,
+ slashBootPartNum = slashBootPartNum,
+ slashBootPartUuid = slashBootPartUuid,
+ grubInstallLocation = grubInstallLocation,
+ installerDevDiskName = installerDevDiskName,
+ bootDirRelativeToGrub = bootDirRelativeToGrub,
+ )
+ return substitutes
+
+
+#------------------------------------------------------------------------------
+def copyGrubStageImages():
+ '''make sure all the "stage" images are stored on the boot
+ partition.
+ This replaces `/sbin/grub-install --just-copy` that was in anaconda
+ '''
+
+ choices = userchoices.getBoot()
+ if choices and choices['doNotInstall']:
+ log.info('Skipping the writing of the bootloader to disk')
+ return
+
+ if not os.path.exists(grub_dir):
+ os.makedirs(grub_dir) #makes directory for the stage images
+
+ stageImgFilenames = glob.glob(grub_stage_img_dir + '*')
+ if not stageImgFilenames:
+ raise Exception('grub package directory (%s) was empty!'
+ % grub_stage_img_dir)
+ for fname in stageImgFilenames:
+ basename = os.path.basename(fname)
+ destination = grub_dir + basename
+ shutil.copy(fname, destination)
+
+
+#------------------------------------------------------------------------------
+def makeBackups():
+ '''Backup old grub configuration files files'''
+ for fname in [ grub_conf_file,
+ sysconfig_grub_file,
+ ]:
+ try:
+ shutil.copy(fname, fname + '.old')
+ except IOError, ex:
+ if ex.errno == errno.ENOENT: #file not found error
+ log.info('File %s was not present. No backup made' % fname)
+ pass
+ else:
+ raise
+
+
+#------------------------------------------------------------------------------
+def parseGrubConf(grubConf):
+ '''Parse a grub.conf file into a list of dictionaries for each title.'''
+ retval = []
+
+ currentTitle = None
+ for line in grubConf.split('\n'):
+ if line.lstrip().startswith('title '):
+ currentTitle = { 'body' : '' }
+ retval.append(currentTitle)
+ if currentTitle is not None:
+ currentTitle['body'] += "%s\n" % line
+ key = line.strip().split(' ')[0]
+ currentTitle[key] = line
+
+ return retval
+
+#------------------------------------------------------------------------------
+def writeUpgradeFiles(grubContents):
+ def ensureBootPrefix(path):
+ if not path.startswith("/boot"):
+ path = os.path.join("/boot", path.lstrip('/'))
+ return path
+
+ titlesToRemove = []
+ pathsToRemove = set()
+
+ # Loop through the grub.conf looking for old ESX v3 titles.
+ for title in parseGrubConf(grubContents):
+ if 'kernel' not in title:
+ # Title has no kernel to boot?
+ continue
+
+ m = re.search(r'root=UUID=(\S+)', title['kernel'])
+ if not m:
+ # Cannot find root partition UUID
+ continue
+
+ if m.group(1) != userchoices.getRootUUID()['uuid']:
+ # Does not match the 3.x root partition UUID.
+ continue
+
+ titlesToRemove.append(title)
+
+ m = re.search(r'/vmlinuz-([^\s]+)', title['kernel'])
+ if m:
+ path = m.group(1)
+ pathsToRemove.add(ensureBootPrefix("vmlinuz-%s" % path))
+ pathsToRemove.add(ensureBootPrefix("vmlinux-%s" % path))
+ pathsToRemove.add(ensureBootPrefix("config-%s" % path))
+ pathsToRemove.add(ensureBootPrefix("System.map-%s" % path))
+ pathsToRemove.add(ensureBootPrefix("System.map"))
+ pathsToRemove.add(ensureBootPrefix("kernel.h"))
+
+ m = re.search(r'(/[^\s]+)', title['initrd'])
+ if m:
+ path = m.group(1)
+ pathsToRemove.add(ensureBootPrefix(path))
+
+ header = "# BEGIN ESX v3 title"
+ footer = "# END ESX v3 title"
+ for title in titlesToRemove:
+ body = title['body'].strip()
+ grubContents = grubContents.replace(
+ body, "%s\n%s\n%s\n" % (header, body, footer), 1)
+
+ fp = open(migrate.CLEANUP_PATH, 'a')
+ fp.write("\n# Remove old ESX v3 titles in grub.conf\n"
+ "sed -i -e '/^%s/,/^%s/d' /boot/grub/grub.conf\n" % (
+ header, footer))
+ if pathsToRemove:
+ fp.write("\n# Remove old ESX v3 boot files:\n")
+ for path in pathsToRemove:
+ fp.write("rm -f %s\n" % path)
+ fp.write("rm -f /usr/sbin/cleanup-esx3")
+ fp.close()
+
+ return grubContents
+
+#------------------------------------------------------------------------------
+def writeGrubConfFiles(stringSubstitutionDict):
+ '''make grub config files'''
+ # make sure the expected directories exist
+ if not os.path.exists(grub_dir):
+ os.makedirs(grub_dir)
+ if not os.path.exists(os.path.dirname(sysconfig_grub_file)):
+ os.makedirs(os.path.dirname(sysconfig_grub_file))
+
+ newEntry = grub_conf_entry % stringSubstitutionDict
+
+ debugEntrySubstDict = stringSubstitutionDict.copy()
+
+ debugEntrySubstDict['label'] = "Troubleshooting mode"
+ debugEntrySubstDict['bootDirRelativeToGrub'] += "trouble/"
+ debugEntrySubstDict['kernelOptions'] += " trouble"
+ debugEntry = grub_conf_entry % debugEntrySubstDict
+
+ choices = userchoices.getBoot()
+ if (os.path.exists(grub_conf_file) and
+ (userchoices.getUpgrade() or choices.get('upgrade', False))):
+ # For an upgrade, we need to preserve all the settings in the file,
+ # not just the titles. Otherwise, we lose things like password.
+ grubFile = open(grub_conf_file)
+ grubContents = grubFile.read()
+ grubFile.close()
+
+ grubContents = removeTitle(grubContents, newEntry.split('\n')[0])
+ grubContents = removeTitle(grubContents, debugEntry.split('\n')[0])
+
+ grubContents = grubContents.replace('VMware ESX Server',
+ 'VMware ESX Server 3')
+ grubContents = grubContents.replace(
+ 'Service Console only',
+ 'ESX Server 3 Service Console only')
+ else:
+ grubContents = grub_conf_header % stringSubstitutionDict
+
+ grubContents = insertTitle(grubContents, debugEntry)
+ grubContents = insertTitle(grubContents, newEntry)
+ grubContents = changeSetting(grubContents, "default", "0")
+
+ if userchoices.getUpgrade():
+ grubContents = writeUpgradeFiles(grubContents)
+
+ # Write the whole file out first and then use rename to atomically install
+ # it in the directory, we don't want to put a broken file in there during
+ # an upgrade.
+ tmpPath = os.tempnam(os.path.dirname(grub_conf_file), "grub.conf")
+ fp = open(tmpPath, 'w')
+ fp.write(grubContents)
+ fp.close()
+
+ os.chmod(tmpPath, 0600)
+
+ os.rename(tmpPath, grub_conf_file)
+
+ fp = open(device_map_file, 'w')
+ fp.write(device_map % stringSubstitutionDict)
+ fp.close()
+
+ fp = open(sysconfig_grub_file, 'w')
+ fp.write(sysconfig_grub % stringSubstitutionDict)
+ fp.close()
+
+#------------------------------------------------------------------------------
+def makeInitialRamdisk(stringSubstitutionDict):
+ '''Makes the initial ramdisk.'''
+ #these 2 should go away soon...
+ os.system('touch %s/etc/vmware/sysboot.conf' % instRoot);
+
+ # actually do the mkinitrd
+ log.info('Making initrd: ' + cmd_esxcfgboot)
+ execCommand("%s -b --update-trouble" % cmd_esxcfgboot, root=instRoot)
+
+
+#------------------------------------------------------------------------------
+def validateGrubPassword(password):
+ '''Validates the GRUB Password.
+ The GRUB Manual didn't seem to have any specification (section 13.2.10).
+ Experimentation shows that the limit is 30 chars
+ Raises ValueError if invalid.
+ '''
+ if not 1 <= len(password) <= 30:
+ raise ValueError('GRUB Password length must be between 1 and 30')
+ return True
+
+#------------------------------------------------------------------------------
+class MBRWriter:
+ '''Class in charge of writing to the Master Boot Record.'''
+ def __enter(self):
+ '''This function will ATTEMPT to sync the filesystem
+ See the grub manual, section 15.2 for information
+ about why this is necessary
+
+ NOTE: Ideally we'd like to do this by unmounting/mounting
+ the /boot directory, but if something goes wrong between the
+ unmount and the mount, it creates a nightmarish situation
+ for diagnosing the bug. QA would not be happy.
+ '''
+ # Yes, three times.
+ # TODO: talk to some storage guy to see if this is REALLY necessary
+ syncKernelBufferToDisk()
+ syncKernelBufferToDisk()
+ syncKernelBufferToDisk()
+
+ def __exit(self):
+ # Yes, three times.
+ syncKernelBufferToDisk()
+ syncKernelBufferToDisk()
+ syncKernelBufferToDisk()
+
+ def write(self, stringSubstitutionDict):
+ choices = userchoices.getBoot()
+ if choices and choices['doNotInstall']:
+ log.info('Skipping the writing of the bootloader to disk')
+ return
+
+ self.__enter()
+
+ cmd = cmd_grub % stringSubstitutionDict
+
+ rc, stdout, stderr = execCommand(cmd)
+
+ self.checkGrubOutput(stdout)
+
+ self.__exit()
+
+ def checkGrubOutput(self, grubOutString):
+ '''Right now this will throw an exception on any GRUB error.
+ If we want to get fancy in the future, we can actually parse/lex
+ the output from GRUB'''
+ for line in grubOutString.splitlines():
+ if line.startswith('Error'):
+ raise GrubCheckException(grubOutString)
+
+
+#------------------------------------------------------------------------------
+def sanityCheck(stringSubstitutionDict, throwOnInsane=False):
+ def fail(extraMsg=''):
+ log.error('No. ' + extraMsg)
+ if throwOnInsane:
+ raise Exception('Sanity check failed. '+ extraMsg)
+
+ log.info('Bootloader tasks complete. Sanity checking the system...')
+ log.info('Are the kernel and initrd present in /mnt/sysimage/boot/?')
+ bootDir = instRoot +'boot/'
+ if os.path.isfile(bootDir+ 'vmlinuz'):
+ log.info('Yes.')
+ else:
+ fail()
+ if os.path.isfile(bootDir + 'initrd.img'):
+ log.info('Yes.')
+ else:
+ fail()
+ log.info('Is the initrd > 10M?')
+ if os.path.getsize(bootDir + 'initrd.img') > 10*(1024*1024):
+ log.info('Yes.')
+ else:
+ fail()
+
+ commented_out_for_now_because_it_causes_PSOD = '''
+ log.info('Does the target disk have the GRUB signature?')
+ # get the last two bytes of the first 512 bytes on the disk
+ cmd = 'dd bs=2 count=1 skip=255 if=%(installerDevDiskName)s |od -x' %\
+ stringSubstitutionDict
+ rc, stdout, stderr = execCommand(cmd)
+ if 'aa55' in stdout:
+ log.info('Yes.')
+ else:
+ fail('Grub signature not found at the tail of the first 512 bytes')
+ '''
+
+
+#------------------------------------------------------------------------------
+def _sectorHasGrub(contents):
+ # Gleaned from checkbootloader.py
+ # http://aurora.fhive.net/distributions/aurora/build-2.0/sparc/os/
+ # RHupdates/checkbootloader.py
+
+ # XXX I don't like this, but it's what the maintainer suggested :(
+ return "GRUB" in contents and contents[-2:] == "\x55\xaa"
+
+#------------------------------------------------------------------------------
+def discoverLocation(subDict):
+ '''Discover where grub is installed, either on the MBR or the first sector
+ of the /boot partition.
+ '''
+ wholeDiskDev, _partNum = partition.splitPath(subDict['devPartName'])
+
+ diskFile = open(wholeDiskDev)
+ mbrContents = diskFile.read(512)
+ diskFile.close()
+
+ partFile = open(subDict['devPartName'])
+ partBootContents = partFile.read(512)
+ partFile.close()
+
+ loc = None
+ doNotInstall = False
+ if _sectorHasGrub(partBootContents):
+ log.debug("found grub installed on the /boot partition")
+ loc = userchoices.BOOT_LOC_PARTITION
+ elif _sectorHasGrub(mbrContents):
+ log.debug("found grub installed on the MBR")
+ loc = userchoices.BOOT_LOC_MBR
+ else:
+ log.warn("grub was not found, not upgrading boot loader")
+ doNotInstall = True
+
+ userchoices.setBoot(True, doNotInstall, location=loc)
+
+#------------------------------------------------------------------------------
+def runtimeAction():
+ pass
+
+#------------------------------------------------------------------------------
+def hostAction(context):
+ subDict = getStringSubstitutionDict()
+ log.debug('Dumping bootloader variables...')
+ safeDict = subDict.copy()
+ del safeDict['passwordLine']
+ log.debug(str(safeDict))
+
+ makeBackups()
+
+ if userchoices.getUpgrade():
+ discoverLocation(subDict)
+ subDict = getStringSubstitutionDict()
+
+ context.cb.pushStatusGroup(5)
+ context.cb.pushStatus('Copying the GRUB images')
+ copyGrubStageImages()
+ context.cb.popStatus()
+
+ context.cb.pushStatus('Writing the GRUB config files')
+ writeGrubConfFiles(subDict)
+ context.cb.popStatus()
+
+ context.cb.pushStatus('Making the initial ramdisk')
+ makeInitialRamdisk(subDict)
+ context.cb.popStatus()
+
+ context.cb.pushStatus('Writing GRUB to the Master Boot Record')
+ w = MBRWriter()
+ w.write(subDict)
+ context.cb.popStatus()
+ context.cb.popStatusGroup()
+
+ sanityCheck(subDict)
+
+#------------------------------------------------------------------------------
+def hostActionRebuildInitrd(context):
+ context.cb.pushStatusGroup(1)
+ context.cb.pushStatus('Checking if the initial ramdisk has to be rebuilt')
+ # If nothing has changed then this is a noop.
+ execCommand("%s --rebuild -b --update-trouble" % cmd_esxcfgboot,
+ root=instRoot)
+ context.cb.popStatus()
+ context.cb.popStatusGroup()
+
+if __name__ == '__main__':
+ print 'This should probably do some simple test'
View
362 brandiso.py
@@ -0,0 +1,362 @@
+#! /usr/bin/env python
+
+###############################################################################
+# Copyright (c) 2008-2009 VMware, Inc.
+#
+# This file is part of Weasel.
+#
+# Weasel 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
+# version 2 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.
+#
+###############################################################################
+#
+# brandiso.py [options]
+#
+# Implant an ISO image with a checksum and the given volume identifier
+# string. See usage for more details.
+
+import os
+import sys
+import getopt
+import struct
+try:
+ import task_progress
+except ImportError:
+ # fake the module if we can't find it.
+ class TaskProgress(object):
+ def taskStarted(*args): pass
+ def taskProgress(*args): pass
+ def taskFinish(*args): pass
+ task_progress = TaskProgress()
+
+class BrandISOException(Exception):
+ '''Exception raise by the brandiso module'''
+
+VERBOSE = 0
+
+SECTOR_SIZE = 2048
+
+ID_FIELD = 'vol id'
+CHECKSUM_FIELD = 'app use'
+# offset of md5 checksum in the appdata field, in case appdata is used
+# for some other purposes as well
+CHECKSUM_OFFSET = 0
+
+CHECKSUM_SIZE = 16
+READ_BLOCK_SIZE = 4096
+
+# Map of byte lengths to struct.pack format letters.
+PACK_FORMAT = {
+ 1 : "B",
+ 2 : "H",
+ 4 : "I",
+ }
+
+SEEK_SET = 0
+SEEK_CUR = 1
+SEEK_END = 2
+
+# Conversion functions for each field type:
+# sec - The contents of the sector.
+# o - Offset of the field in bytes.
+# s - Size of the field in bytes.
+def TYPE_RAW(sec, o, s):
+ return ' '.join(["%02X" % ord(ch) for ch in sec[o:o + s]])
+def TYPE_STR(sec, o, s):
+ return sec[o:o + s].strip()
+def TYPE_LSB_INT(sec, o, s):
+ return str(struct.unpack("<%s" % PACK_FORMAT[s], sec[o:o + s])[0])
+def TYPE_MSB_INT(sec, o, s):
+ return str(struct.unpack(">%s" % PACK_FORMAT[s], sec[o:o + s])[0])
+def TYPE_DATETIME(sec, o, s):
+ return ("%s-%s-%s %s:%s:%s.%s %d" %
+ struct.unpack("4s2s2s2s2s2s2s1b", sec[o:o + s]))
+
+# primary volume descriptor fields
+# reference: http://www.ccs.neu.edu/home/bchafy/cdb/info/iso9660.txt,
+# http://www.mactech.com/articles/develop/issue_03/high_sierra.html
+VOL_DESC_STRUCT = (
+ (1, TYPE_LSB_INT, 'type'), # must be 1 for primary volume descriptor
+ (5, TYPE_STR, 'std id'), # must be "CD001"
+ (1, TYPE_LSB_INT, 'std ver'), # must be 1
+ (1, TYPE_LSB_INT, 'flags'), # 0 in primary volume descriptor
+ (32, TYPE_STR, 'sys id'),
+ (32, TYPE_STR, 'vol id'),
+ (8, TYPE_RAW, 'reserved 0'), # zeros
+ (4, TYPE_LSB_INT, 'lsb vol size'), # volume size in LSB byte order, in sectors
+ (4, TYPE_MSB_INT, 'msb vol size'), # volume size in MSB byte order, in sectors
+ (32, TYPE_RAW, 'escape seq'), # zeros in primary volume descriptor
+ (2, TYPE_LSB_INT, 'lsb vol count'), # number of volumes
+ (2, TYPE_MSB_INT, 'msb vol count'),
+ (2, TYPE_LSB_INT, 'lsb vol set seq num'), # which volume in volume set (not used)
+ (2, TYPE_MSB_INT, 'msb vol set seq num'),
+ (2, TYPE_LSB_INT, 'lsb sec size'), # sector size, 2048
+ (2, TYPE_MSB_INT, 'msb sec size'),
+ (4, TYPE_LSB_INT, 'lsb path table size'), # number of bytes in path table
+ (4, TYPE_MSB_INT, 'msb path table size'),
+ (4, TYPE_LSB_INT, 'lsb path table 1'), # mandatory
+ (4, TYPE_LSB_INT, 'lsb path table 2'), # optional
+ (4, TYPE_MSB_INT, 'msb path table 1'), # mandatory
+ (4, TYPE_MSB_INT, 'msb path table 2'), # optional
+ (34, TYPE_RAW, 'root dir'), # duplicate root directory entry
+ (128, TYPE_STR, 'vol set id'),
+ (128, TYPE_STR, 'publisher id'),
+ (128, TYPE_STR, 'data preparer id'),
+ (128, TYPE_STR, 'app id'),
+ (37, TYPE_RAW, 'copyright file id'),
+ (37, TYPE_RAW, 'abstract file id'),
+ (37, TYPE_RAW, 'bibliographical file id'),
+ (17, TYPE_DATETIME, 'vol created'), # creation time
+ (17, TYPE_DATETIME, 'vol modified'), # last modification time
+ (17, TYPE_DATETIME, 'vol expires'), # expiration
+ (17, TYPE_DATETIME, 'vol effective'), # ?
+ (1, TYPE_LSB_INT, 'file struct std ver'), # 1
+ (1, TYPE_RAW, 'reserved 1'), # must be 0
+ (512, TYPE_RAW, 'app use'), # reserved for application use (usually zeros)
+ (653, TYPE_RAW, 'future'), # zeros
+)
+
+fields = {}
+
+# fills in the offset field
+def fill_fields():
+ off = 0
+ for (sz, tpfn, nm) in VOL_DESC_STRUCT:
+ fields[nm] = (off, sz, tpfn)
+ off += sz
+
+# returns md5 hash of a file with a hole,
+# the hole contents is treated as zeros
+def calc_md5(f, zpos, zlen, isosize):
+ from md5 import md5
+
+ f.seek(0, SEEK_END)
+ fileSize = f.tell()
+ task_progress.taskStarted('brandiso.calc_md5', fileSize)
+ m = md5()
+ f.seek(0, SEEK_SET)
+ pos = 0
+ while pos < isosize:
+ # Read up to the start of the zero-d out spot, skip that and then read
+ # through the rest of the file.
+ if pos < zpos:
+ blockSize = min(zpos - pos, READ_BLOCK_SIZE)
+ elif pos == zpos:
+ task_progress.taskProgress('brandiso.calc_md5', zlen)
+ pos = zpos + zlen
+ f.seek(pos, SEEK_SET)
+ m.update('\0' * zlen) # Still need to update the digest with zeroes.
+ continue
+ else:
+ blockSize = min(isosize - pos, READ_BLOCK_SIZE)
+
+ block = f.read(blockSize)
+ if not block:
+ break
+
+ task_progress.taskProgress('brandiso.calc_md5', len(block))
+ m.update(block)
+ pos += len(block)
+
+ digest = m.digest()
+ assert len(digest) == CHECKSUM_SIZE
+ task_progress.taskFinish('brandiso.calc_md5')
+
+ return digest
+
+
+# prints fields of the primary volume descriptor
+def print_primary(sec):
+ for _sz, _fn, fld in VOL_DESC_STRUCT: # ('sys id', 'vol id', 'app use')
+ (off, sz, fn) = fields[fld]
+ print "%s: %s" % (fld, fn(sec, off, sz))
+
+# seeks to the primary volume descriptor and returns that sector
+def seek_to_primary(img):
+ # contents of the first 16 sectors is not specified
+ img.seek(SECTOR_SIZE * 16)
+
+ # processing volume descriptors
+ while True:
+ sec = img.read(SECTOR_SIZE)
+ if len(sec) != SECTOR_SIZE:
+ raise BrandISOException('Unexpected EOF (wrong ISO image?)')
+
+ # terminating descriptor
+ if ord(sec[0]) == 255:
+ raise BrandISOException('No primary descriptor found!')
+
+ #primary descriptor
+ if ord(sec[0]) == 1:
+ img.seek(-SECTOR_SIZE, SEEK_CUR)
+ return sec
+
+# returns printable representation of binary data
+def pretty(bin):
+ return ''.join(["%02x" % ord(c) for c in bin])
+
+def iso_size(sec):
+ size_off, size_len, size_fn = fields['lsb vol size']
+ isosecsize = int(size_fn(sec, size_off, size_len))
+ secsize_off, secsize_len, secsize_fn = fields['lsb sec size']
+ secsize = int(secsize_fn(sec, secsize_off, secsize_len))
+
+ return isosecsize * secsize
+
+# brands an image with specified appid and writes md5 hash into appdata field
+def brand_iso(filename, idstr):
+ img = open(filename, 'r+b')
+ try:
+ sec = seek_to_primary(img)
+
+ sec_off = img.tell()
+ chsum_off, _, _ = fields[CHECKSUM_FIELD]
+ id_off, id_len, _ = fields[ID_FIELD]
+
+ # writing id
+ img.seek(sec_off + id_off, os.SEEK_SET)
+ img.write(idstr[:id_len])
+ print "ID written:", idstr[:id_len]
+
+ digest = calc_md5(img,
+ sec_off + chsum_off + CHECKSUM_OFFSET,
+ CHECKSUM_SIZE,
+ iso_size(sec))
+
+ # writing checksum
+ img.seek(sec_off + chsum_off + CHECKSUM_OFFSET, os.SEEK_SET)
+ img.write(digest)
+ print "Checksum written:", pretty(digest)
+ finally:
+ img.close()
+
+# verifies an md5 hash of a branded image and prints its sys id
+def extract_iso_checksums(filename):
+ img = open(filename, 'rb')
+ retval = None
+ try:
+ sec = seek_to_primary(img)
+
+ sec_off = img.tell()
+ chsum_off, _, _ = fields[CHECKSUM_FIELD]
+ id_off, id_len, _ = fields[ID_FIELD]
+ written_digest = sec[chsum_off + CHECKSUM_OFFSET:
+ chsum_off + CHECKSUM_OFFSET + CHECKSUM_SIZE]
+ id_str = sec[id_off: id_off + id_len]
+
+ # calculating the actual checksum
+ digest = calc_md5(img, sec_off + chsum_off + CHECKSUM_OFFSET,
+ CHECKSUM_SIZE,
+ iso_size(sec))
+
+ retval = (written_digest, digest, id_str)
+ finally:
+ img.close()
+
+ return retval
+
+def verify_iso(filename):
+ written_digest, digest, id_str = extract_iso_checksums(filename)
+ if digest == written_digest:
+ print "%s: OK" % id_str.strip()
+ else:
+ print "%s: FAILED recorded %s != actual %s" % (
+ id_str.strip(), pretty(written_digest), pretty(digest))
+ sys.exit(1)
+
+# prints fields of the primary volume descriptor
+def list_iso(filename):
+ img = open(filename, 'rb')
+ try:
+ print_primary(seek_to_primary(img))
+ finally:
+ img.close()
+
+# zero-fill the checksum placeholder
+def zfill_iso(filename):
+ img = open(filename, 'r+b')
+ try:
+ seek_to_primary(img)
+ sec_off = img.tell()
+ chsum_off, _, _ = fields[CHECKSUM_FIELD]
+
+ img.seek(sec_off + chsum_off + CHECKSUM_OFFSET, os.SEEK_SET)
+ img.write('\0' * CHECKSUM_SIZE)
+ finally:
+ img.close()
+
+def usage(argv):
+ print "Usage: %s [-hv] <mode> <ISO image>" % argv[0]
+ print
+ print "Mode options:"
+ print " -b <vol id> Brand an ISO image with a checksum and the given"
+ print " volume identifier string."
+ print " -c Check the checksum in a branded ISO image."
+ print " -l List the fields of the primary volume descriptor."
+ print " -z Zero out the checksum (for debugging)."
+ print
+ print "Options:"
+ print " -h Print this help message."
+ print " -v Increase verbosity."
+ print
+ print "Example:"
+ print " $ %s -c newiso.iso" % argv[0]
+ print (" : FAILED recorded 20202020202020202020202020202020 != "
+ "actual 6eb7d639e6605dc187179d3bcabd9de2")
+ print " $ %s -b 'My ISO v1.0' newiso.iso" % argv[0]
+ print " $ %s -c newiso.iso" % argv[0]
+ print " My ISO v1.0: OK"
+
+#
+# init code
+#
+
+fill_fields()
+
+if __name__ == '__main__':
+
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], "hvb:clz")
+
+ if len(args) != 1:
+ raise getopt.error("expecting ISO path")
+
+ isoPath = args[0]
+
+ modeFunction = None
+ for opt, arg in opts:
+ if opt == "-h":
+ usage()
+ sys.exit()
+ elif opt == "-v":
+ VERBOSE += 1
+ elif opt == "-b":
+ _id_off, id_len, _ = fields[ID_FIELD]
+ if len(arg) > id_len:
+ raise getopt.error(
+ "volume identifier must be <= %d characters" % id_len)
+ modeFunction = lambda: brand_iso(isoPath, arg)
+ elif opt == "-c":
+ modeFunction = lambda: verify_iso(isoPath)
+ elif opt == "-l":
+ modeFunction = lambda: list_iso(isoPath)
+ elif opt == "-z":
+ modeFunction = lambda: zfill_iso(isoPath)
+
+ if not modeFunction:
+ raise getopt.error("expecting mode argument (i.e. -b, -l, -c, -z)")
+
+ modeFunction()
+ except IOError, ioe:
+ sys.stderr.write("error: %s\n" % str(ioe))
+ sys.exit(1)
+ except getopt.error, e:
+ sys.stderr.write("error: %s\n" % str(e))
+ usage(sys.argv)
+ sys.exit(1)
View
63 cdutil.py
@@ -0,0 +1,63 @@
+'''Utility functions for CD install.'''
+
+###############################################################################
+# Copyright (c) 2008-2009 VMware, Inc.
+#
+# This file is part of Weasel.
+#
+# Weasel 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 version 2 and no later version.
+#
+# Weasel is distributed in the hope that it will be useful, but WITHOUT