1238 lines
41 KiB
Python
Executable File
1238 lines
41 KiB
Python
Executable File
#!/usr/bin/python
|
|
#
|
|
# anaconda: The Red Hat Linux Installation program
|
|
#
|
|
# Copyright (C) 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007
|
|
# Red Hat, Inc. All rights reserved.
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
# Author(s): Brent Fox <bfox@redhat.com>
|
|
# Mike Fulbright <msf@redhat.com>
|
|
# Jakub Jelinek <jakub@redhat.com>
|
|
# Jeremy Katz <katzj@redhat.com>
|
|
# Chris Lumens <clumens@redhat.com>
|
|
# Paul Nasrat <pnasrat@redhat.com>
|
|
# Erik Troan <ewt@rpath.com>
|
|
# Matt Wilson <msw@rpath.com>
|
|
#
|
|
|
|
# This toplevel file is a little messy at the moment...
|
|
|
|
import sys, os, re, time, subprocess
|
|
from optparse import OptionParser
|
|
from tempfile import mkstemp
|
|
|
|
# keep up with process ID of miniwm if we start it
|
|
|
|
miniwm_pid = None
|
|
|
|
# Make sure messages sent through python's warnings module get logged.
|
|
def AnacondaShowWarning(message, category, filename, lineno, file=sys.stderr, line=None):
|
|
log.warning("%s" % warnings.formatwarning(message, category, filename, lineno, line))
|
|
|
|
# start miniWM
|
|
def startMiniWM(root='/'):
|
|
(rd, wr) = os.pipe()
|
|
childpid = os.fork()
|
|
if not childpid:
|
|
if os.access("./mini-wm", os.X_OK):
|
|
cmd = "./mini-wm"
|
|
elif os.access(root + "/usr/bin/mini-wm", os.X_OK):
|
|
cmd = root + "/usr/bin/mini-wm"
|
|
else:
|
|
return None
|
|
|
|
os.dup2(wr, 1)
|
|
os.close(wr)
|
|
args = [cmd, '--display', ':1']
|
|
os.execv(args[0], args)
|
|
sys.exit (1)
|
|
else:
|
|
# We need to make sure that mini-wm is the first client to
|
|
# connect to the X server (see bug #108777). Wait for mini-wm
|
|
# to write back an acknowledge token.
|
|
os.read(rd, 1)
|
|
|
|
return childpid
|
|
|
|
# function to handle X startup special issues for anaconda
|
|
def doStartupX11Actions(runres="800x600"):
|
|
global miniwm_pid
|
|
|
|
setupGraphicalLinks()
|
|
|
|
# now start up mini-wm
|
|
try:
|
|
miniwm_pid = startMiniWM()
|
|
log.info("Started mini-wm")
|
|
|
|
except:
|
|
miniwm_pid = None
|
|
log.error("Unable to start mini-wm")
|
|
|
|
if miniwm_pid is not None:
|
|
import xutils
|
|
import gtk
|
|
|
|
try:
|
|
i = gtk.Invisible()
|
|
i.selection_owner_set("_ANACONDA_MINI_WM_RUNNING")
|
|
|
|
xutils.setRootResource('Xcursor.size', '24')
|
|
xutils.setRootResource('Xcursor.theme', 'Bluecurve')
|
|
xutils.setRootResource('Xcursor.theme_core', 'true')
|
|
|
|
xutils.setRootResource('Xft.antialias', '1')
|
|
xutils.setRootResource('Xft.hinting', '1')
|
|
xutils.setRootResource('Xft.hintstyle', 'hintslight')
|
|
xutils.setRootResource('Xft.rgba', 'none')
|
|
except:
|
|
sys.stderr.write("X SERVER STARTED, THEN FAILED");
|
|
raise RuntimeError, "X server failed to start"
|
|
|
|
def doShutdownX11Actions():
|
|
global miniwm_pid
|
|
|
|
if miniwm_pid is not None:
|
|
try:
|
|
os.kill(miniwm_pid, 15)
|
|
os.waitpid(miniwm_pid, 0)
|
|
except:
|
|
pass
|
|
|
|
def setupPythonUpdates():
|
|
from distutils.sysconfig import get_python_lib
|
|
|
|
if not os.path.exists("/tmp/updates"):
|
|
return
|
|
|
|
for pkg in os.listdir("/tmp/updates"):
|
|
d = "/tmp/updates/%s" % pkg
|
|
|
|
if not os.path.isdir(d):
|
|
continue
|
|
|
|
# See if the package exists in /usr/lib{64,}/python/?.?/site-packages.
|
|
# If it does, we can set it up as an update. If not, the pkg is
|
|
# likely a completely new directory and should not be looked at.
|
|
dest = "%s/%s" % (get_python_lib(), pkg)
|
|
if not os.access(dest, os.R_OK):
|
|
dest = "%s/%s" % (get_python_lib(1), pkg)
|
|
if not os.access(dest, os.R_OK):
|
|
continue
|
|
|
|
contents = os.listdir(d)
|
|
|
|
# Symlink over everything that's in the python libdir but not in
|
|
# the updates directory.
|
|
for f in filter(lambda fn: fn not in contents, os.listdir(dest)):
|
|
if f.endswith(".pyc") or f.endswith(".pyo"):
|
|
continue
|
|
|
|
os.symlink("%s/%s" % (dest, f), "/tmp/updates/%s/%s" % (pkg, f))
|
|
|
|
if os.access("/tmp/updates/70-anaconda.rules", os.R_OK):
|
|
import shutil
|
|
shutil.copyfile("/tmp/updates/70-anaconda.rules",
|
|
"/etc/udev/rules.d/70-anaconda.rules")
|
|
|
|
def parseOptions():
|
|
def resolution_cb (option, opt_str, value, parser):
|
|
parser.values.runres = value
|
|
|
|
op = OptionParser()
|
|
# Interface
|
|
op.add_option("-C", "--cmdline", dest="display_mode", action="store_const", const="c",
|
|
default="g")
|
|
op.add_option("-G", "--graphical", dest="display_mode", action="store_const", const="g")
|
|
op.add_option("-T", "--text", dest="display_mode", action="store_const", const="t")
|
|
|
|
# Network
|
|
op.add_option("--noipv4", action="store_true", default=False)
|
|
op.add_option("--noipv6", action="store_true", default=False)
|
|
op.add_option("--proxy")
|
|
op.add_option("--proxyAuth")
|
|
|
|
# Method of operation
|
|
op.add_option("--autostep", action="store_true", default=False)
|
|
op.add_option("-d", "--debug", dest="debug", action="store_true", default=False)
|
|
op.add_option("--kickstart", dest="ksfile")
|
|
op.add_option("--rescue", dest="rescue", action="store_true", default=False)
|
|
op.add_option("--targetarch", dest="targetArch", nargs=1, type="string")
|
|
|
|
op.add_option("-m", "--method", dest="method", default=None)
|
|
op.add_option("--repo", dest="method", default=None)
|
|
op.add_option("--stage2", dest="stage2", default=None)
|
|
|
|
op.add_option("--liveinst", action="store_true", default=False)
|
|
|
|
# Display
|
|
op.add_option("--headless", dest="isHeadless", action="store_true", default=False)
|
|
op.add_option("--nofb")
|
|
op.add_option("--resolution", action="callback", callback=resolution_cb, dest="runres",
|
|
default="800x600", nargs=1, type="string")
|
|
op.add_option("--serial", action="store_true", default=False)
|
|
op.add_option("--usefbx", dest="xdriver", action="store_const", const="fbdev")
|
|
op.add_option("--virtpconsole")
|
|
op.add_option("--vnc", action="store_true", default=False)
|
|
op.add_option("--vncconnect")
|
|
op.add_option("--xdriver", dest="xdriver", action="store", type="string", default=None)
|
|
|
|
# Language
|
|
op.add_option("--keymap")
|
|
op.add_option("--kbdtype")
|
|
op.add_option("--lang")
|
|
|
|
# Obvious
|
|
op.add_option("--loglevel")
|
|
op.add_option("--syslog")
|
|
|
|
op.add_option("--noselinux", dest="selinux", action="store_false", default=True)
|
|
op.add_option("--selinux", action="store_true")
|
|
|
|
op.add_option("--nompath", dest="mpath", action="store_false", default=True)
|
|
op.add_option("--mpath", action="store_true")
|
|
|
|
op.add_option("--nodmraid", dest="dmraid", action="store_false", default=True)
|
|
op.add_option("--dmraid", action="store_true")
|
|
|
|
op.add_option("--noibft", dest="ibft", action="store_false", default=True)
|
|
op.add_option("--ibft", action="store_true")
|
|
op.add_option("--noiscsi", dest="iscsi", action="store_false", default=False)
|
|
op.add_option("--iscsi", action="store_true")
|
|
|
|
# Miscellaneous
|
|
op.add_option("--module", action="append", default=[])
|
|
op.add_option("--nomount", dest="rescue_nomount", action="store_true", default=False)
|
|
op.add_option("--updates", dest="updateSrc", action="store", type="string")
|
|
op.add_option("--dogtail", dest="dogtail", action="store", type="string")
|
|
op.add_option("--dlabel", action="store_true", default=False)
|
|
|
|
# Deprecated, unloved, unused
|
|
op.add_option("-r", "--rootPath", dest="unsupportedMode",
|
|
action="store_const", const="root path")
|
|
op.add_option("-t", "--test", dest="unsupportedMode",
|
|
action="store_const", const="test")
|
|
|
|
return op.parse_args()
|
|
|
|
def setupPythonPath():
|
|
haveUpdates = False
|
|
for ndx in range(len(sys.path)-1, -1, -1):
|
|
if sys.path[ndx].endswith('updates'):
|
|
haveUpdates = True
|
|
break
|
|
|
|
if haveUpdates:
|
|
sys.path.insert(ndx+1, '/usr/lib/anaconda')
|
|
sys.path.insert(ndx+2, '/usr/lib/anaconda/textw')
|
|
sys.path.insert(ndx+3, '/usr/lib/anaconda/iw')
|
|
else:
|
|
sys.path.insert(0, '/usr/lib/anaconda')
|
|
sys.path.insert(1, '/usr/lib/anaconda/textw')
|
|
sys.path.insert(2, '/usr/lib/anaconda/iw')
|
|
|
|
sys.path.append('/usr/share/system-config-date')
|
|
|
|
def addPoPath(dir):
|
|
""" Looks to see what translations are under a given path and tells
|
|
the gettext module to use that path as the base dir """
|
|
for d in os.listdir(dir):
|
|
if not os.path.isdir("%s/%s" %(dir,d)):
|
|
continue
|
|
if not os.path.exists("%s/%s/LC_MESSAGES" %(dir,d)):
|
|
continue
|
|
for basename in os.listdir("%s/%s/LC_MESSAGES" %(dir,d)):
|
|
if not basename.endswith(".mo"):
|
|
continue
|
|
log.info("setting %s as translation source for %s" %(dir, basename[:-3]))
|
|
gettext.bindtextdomain(basename[:-3], dir)
|
|
|
|
def setupTranslations():
|
|
if os.path.isdir("/tmp/updates/po"):
|
|
addPoPath("/tmp/updates/po")
|
|
gettext.textdomain("anaconda")
|
|
|
|
def setupEnvironment():
|
|
# Silly GNOME stuff
|
|
if os.environ.has_key('HOME') and not os.environ.has_key("XAUTHORITY"):
|
|
os.environ['XAUTHORITY'] = os.environ['HOME'] + '/.Xauthority'
|
|
os.environ['HOME'] = '/tmp'
|
|
os.environ['LC_NUMERIC'] = 'C'
|
|
os.environ["GCONF_GLOBAL_LOCKS"] = "1"
|
|
|
|
# In theory, this gets rid of our LVM file descriptor warnings
|
|
os.environ["LVM_SUPPRESS_FD_WARNINGS"] = "1"
|
|
|
|
# make sure we have /sbin and /usr/sbin in our path
|
|
os.environ["PATH"] += ":/sbin:/usr/sbin"
|
|
|
|
# we can't let the LD_PRELOAD hang around because it will leak into
|
|
# rpm %post and the like. ick :/
|
|
if os.environ.has_key("LD_PRELOAD"):
|
|
del os.environ["LD_PRELOAD"]
|
|
|
|
os.environ["GLADEPATH"] = "/tmp/updates/:/tmp/updates/ui/:ui/:/usr/share/anaconda/ui/:/usr/share/python-meh/"
|
|
os.environ["PIXMAPPATH"] = "/tmp/updates/pixmaps/:/tmp/updates/:/tmp/product/pixmaps/:/tmp/product/:pixmaps/:/usr/share/anaconda/pixmaps/:/usr/share/pixmaps/:/usr/share/anaconda/:/usr/share/python-meh/"
|
|
|
|
def setupLoggingFromOpts(opts):
|
|
if opts.loglevel and anaconda_log.logLevelMap.has_key(opts.loglevel):
|
|
level = anaconda_log.logLevelMap[opts.loglevel]
|
|
anaconda_log.logger.tty_loglevel = level
|
|
anaconda_log.setHandlersLevel(log, level)
|
|
anaconda_log.setHandlersLevel(storage.storage_log.logger, level)
|
|
|
|
if opts.syslog:
|
|
anaconda_log.logger.remote_syslog = opts.syslog
|
|
if opts.syslog.find(":") != -1:
|
|
(host, port) = opts.syslog.split(":")
|
|
anaconda_log.logger.addSysLogHandler(log, host, port=int(port))
|
|
else:
|
|
anaconda_log.logger.addSysLogHandler(log, opts.syslog)
|
|
|
|
# ftp installs pass the password via a file in /tmp so
|
|
# ps doesn't show it
|
|
def expandFTPMethod(str):
|
|
ret = None
|
|
|
|
try:
|
|
filename = str[1:]
|
|
ret = open(filename, "r").readline()
|
|
ret = ret[:len(ret) - 1]
|
|
os.unlink(filename)
|
|
return ret
|
|
except:
|
|
return None
|
|
|
|
def runVNC():
|
|
global vncS
|
|
vncS.startServer()
|
|
|
|
child = os.fork()
|
|
if child == 0:
|
|
for p in ('/tmp/updates/pyrc.py', \
|
|
'/usr/lib/anaconda-runtime/pyrc.py'):
|
|
if os.access(p, os.R_OK|os.X_OK):
|
|
os.environ['PYTHONSTARTUP'] = p
|
|
break
|
|
|
|
while True:
|
|
# s390/s390x are the only places we /really/ need a shell on tty1,
|
|
# and everywhere else this just gets in the way of pdb. But we
|
|
# don't want to return, because that'll return try to start X
|
|
# a second time.
|
|
if iutil.isConsoleOnVirtualTerminal():
|
|
time.sleep(10000)
|
|
else:
|
|
print _("Press <enter> for a shell")
|
|
sys.stdin.readline()
|
|
iutil.execConsole()
|
|
|
|
def checkMemory(anaconda):
|
|
if iutil.memInstalled() < isys.MIN_RAM:
|
|
from snack import SnackScreen, ButtonChoiceWindow
|
|
|
|
screen = SnackScreen()
|
|
ButtonChoiceWindow(screen, _('Fatal Error'),
|
|
_('You do not have enough RAM to install %s '
|
|
'on this machine.\n'
|
|
'\n'
|
|
'Press <return> to reboot your system.\n')
|
|
%(product.productName,),
|
|
buttons = (_("OK"),))
|
|
screen.finish()
|
|
sys.exit(0)
|
|
|
|
# override display mode if machine cannot nicely run X
|
|
if not flags.usevnc:
|
|
if anaconda.displayMode not in ('t', 'c') and iutil.memInstalled() < isys.MIN_GUI_RAM:
|
|
stdoutLog.warning(_("You do not have enough RAM to use the graphical "
|
|
"installer. Starting text mode."))
|
|
anaconda.displayMode = 't'
|
|
time.sleep(2)
|
|
|
|
def setupGraphicalLinks():
|
|
for i in ( "imrc", "im_palette.pal", "gtk-2.0", "pango", "fonts",
|
|
"fb.modes"):
|
|
try:
|
|
if os.path.exists("/mnt/runtime/etc/%s" %(i,)):
|
|
os.symlink ("../mnt/runtime/etc/" + i, "/etc/" + i)
|
|
except:
|
|
pass
|
|
|
|
def handleSshPw(anaconda):
|
|
import users
|
|
u = users.Users(anaconda)
|
|
|
|
userdata = anaconda.ksdata.sshpw.dataList()
|
|
for ud in userdata:
|
|
if u.checkUserExists(ud.username, root="/"):
|
|
u.setUserPassword(username=ud.username, password=ud.password,
|
|
isCrypted=ud.isCrypted, lock=ud.lock)
|
|
else:
|
|
u.createUser(name=ud.username, password=ud.password,
|
|
isCrypted=ud.isCrypted, lock=ud.lock,
|
|
root="/")
|
|
|
|
del u
|
|
|
|
def createSshKey(algorithm, keyfile):
|
|
path = '/etc/ssh/%s' % (keyfile,)
|
|
argv = ['-q','-t',algorithm,'-f',path,'-C','','-N','']
|
|
log.info("running \"%s\"" % (" ".join(['ssh-keygen']+argv),))
|
|
|
|
so = "/tmp/ssh-keygen-%s-stdout.log" % (algorithm,)
|
|
se = "/tmp/ssh-keygen-%s-stderr.log" % (algorithm,)
|
|
iutil.execWithRedirect('ssh-keygen', argv, stdout=so, stderr=se)
|
|
|
|
def fork_orphan():
|
|
"""Forks an orphan.
|
|
|
|
Returns 1 in the parent and 0 in the orphaned child.
|
|
"""
|
|
intermediate = os.fork()
|
|
if not intermediate:
|
|
if os.fork():
|
|
# the intermediate child dies
|
|
os._exit(0)
|
|
return 0;
|
|
# the original process waits for the intermediate child
|
|
os.waitpid(intermediate, 0)
|
|
return 1
|
|
|
|
def startSsh():
|
|
if not flags.sshd:
|
|
return
|
|
if iutil.isS390():
|
|
return
|
|
|
|
if not fork_orphan():
|
|
os.mkdir("/var/log", 0755)
|
|
os.open("/var/log/lastlog", os.O_RDWR | os.O_CREAT, 0644)
|
|
ssh_keys = {
|
|
'rsa1':'ssh_host_key',
|
|
'rsa':'ssh_host_rsa_key',
|
|
'dsa':'ssh_host_dsa_key',
|
|
}
|
|
for (algorithm, keyfile) in ssh_keys.items():
|
|
createSshKey(algorithm, keyfile)
|
|
args = ["/sbin/sshd", "-f", "/etc/ssh/sshd_config.anaconda"]
|
|
os.execv("/sbin/sshd", args)
|
|
sys.exit(1)
|
|
|
|
def startDebugger(signum, frame):
|
|
import epdb
|
|
epdb.serve(skip=1)
|
|
|
|
class Anaconda(object):
|
|
def __init__(self):
|
|
import desktop, dispatch, firewall, security
|
|
import system_config_keyboard.keyboard as keyboard
|
|
from flags import flags
|
|
|
|
self._backend = None
|
|
self._bootloader = None
|
|
self.canReIPL = False
|
|
self.desktop = desktop.Desktop()
|
|
self.dir = None
|
|
self.dispatch = dispatch.Dispatcher(self)
|
|
self.displayMode = None
|
|
self.extraModules = []
|
|
self.firewall = firewall.Firewall()
|
|
self.id = None
|
|
self._instClass = None
|
|
self._instLanguage = None
|
|
self._intf = None
|
|
self.isHeadless = False
|
|
self.keyboard = keyboard.Keyboard()
|
|
self.ksdata = None
|
|
self.mediaDevice = None
|
|
self.methodstr = None
|
|
self._network = None
|
|
self._platform = None
|
|
self.proxy = None
|
|
self.proxyUsername = None
|
|
self.proxyPassword = None
|
|
self.reIPLMessage = None
|
|
self.rescue = False
|
|
self.rescue_mount = True
|
|
self.rootParts = None
|
|
self.rootPath = "/mnt/sysimage"
|
|
self.security = security.Security()
|
|
self.simpleFilter = True
|
|
self.stage2 = None
|
|
self._storage = None
|
|
self._timezone = None
|
|
self.updateSrc = None
|
|
self.upgrade = flags.cmdline.has_key("preupgrade")
|
|
self.upgradeRoot = None
|
|
self.upgradeSwapInfo = None
|
|
self._users = None
|
|
|
|
# *sigh* we still need to be able to write this out
|
|
self.xdriver = None
|
|
|
|
@property
|
|
def backend(self):
|
|
if not self._backend:
|
|
b = self.instClass.getBackend()
|
|
self._backend = apply(b, (self, ))
|
|
|
|
return self._backend
|
|
|
|
@property
|
|
def bootloader(self):
|
|
if not self._bootloader:
|
|
import booty
|
|
self._bootloader = booty.getBootloader(self)
|
|
|
|
return self._bootloader
|
|
|
|
@property
|
|
def firstboot(self):
|
|
from pykickstart.constants import FIRSTBOOT_SKIP, FIRSTBOOT_DEFAULT
|
|
|
|
if self.ksdata:
|
|
return self.ksdata.firstboot.firstboot
|
|
elif iutil.isS390():
|
|
return FIRSTBOOT_SKIP
|
|
else:
|
|
return FIRSTBOOT_DEFAULT
|
|
|
|
@property
|
|
def instClass(self):
|
|
if not self._instClass:
|
|
from installclass import DefaultInstall
|
|
self._instClass = DefaultInstall()
|
|
|
|
return self._instClass
|
|
|
|
@property
|
|
def instLanguage(self):
|
|
if not self._instLanguage:
|
|
import language
|
|
self._instLanguage = language.Language(self.displayMode)
|
|
|
|
return self._instLanguage
|
|
|
|
def _getInterface(self):
|
|
return self._intf
|
|
|
|
def _setInterface(self, v):
|
|
# "lambda cannot contain assignment"
|
|
self._intf = v
|
|
|
|
def _delInterface(self):
|
|
del self._intf
|
|
|
|
intf = property(_getInterface, _setInterface, _delInterface)
|
|
|
|
@property
|
|
def network(self):
|
|
if not self._network:
|
|
import network
|
|
self._network = network.Network()
|
|
|
|
return self._network
|
|
|
|
@property
|
|
def platform(self):
|
|
if not self._platform:
|
|
import platform
|
|
self._platform = platform.getPlatform(self)
|
|
|
|
return self._platform
|
|
|
|
@property
|
|
def protected(self):
|
|
import stat
|
|
|
|
if os.path.exists("/dev/live") and \
|
|
stat.S_ISBLK(os.stat("/dev/live")[stat.ST_MODE]):
|
|
return [os.readlink("/dev/live")]
|
|
elif self.methodstr and self.methodstr.startswith("hd:"):
|
|
method = self.methodstr[3:]
|
|
return [method.split(":", 3)[0]]
|
|
else:
|
|
return []
|
|
|
|
@property
|
|
def users(self):
|
|
if not self._users:
|
|
import users
|
|
self._users = users.Users(self)
|
|
|
|
return self._users
|
|
|
|
@property
|
|
def storage(self):
|
|
if not self._storage:
|
|
import storage
|
|
self._storage = storage.Storage(self)
|
|
|
|
return self._storage
|
|
|
|
@property
|
|
def timezone(self):
|
|
if not self._timezone:
|
|
import timezone
|
|
self._timezone = timezone.Timezone()
|
|
self._timezone.setTimezoneInfo(self.instLanguage.getDefaultTimeZone(self.rootPath))
|
|
|
|
return self._timezone
|
|
|
|
def dumpState(self):
|
|
from meh.dump import ReverseExceptionDump
|
|
from inspect import stack as _stack
|
|
|
|
# Skip the frames for dumpState and the signal handler.
|
|
stack = _stack()[2:]
|
|
stack.reverse()
|
|
exn = ReverseExceptionDump((None, None, stack), self.mehConfig)
|
|
|
|
(fd, filename) = mkstemp("", "anaconda-tb-", "/tmp")
|
|
fo = os.fdopen(fd, "w")
|
|
|
|
exn.write(self, fo)
|
|
|
|
def initInterface(self):
|
|
if self._intf:
|
|
raise RuntimeError, "Second attempt to initialize the InstallInterface"
|
|
|
|
# setup links required by graphical mode if installing and verify display mode
|
|
if self.displayMode == 'g':
|
|
stdoutLog.info (_("Starting graphical installation."))
|
|
|
|
try:
|
|
from gui import InstallInterface
|
|
except Exception, e:
|
|
stdoutLog.error("Exception starting GUI installer: %s" %(e,))
|
|
# if we're not going to really go into GUI mode, we need to get
|
|
# back to vc1 where the text install is going to pop up.
|
|
if not flags.livecdInstall:
|
|
isys.vtActivate (1)
|
|
stdoutLog.warning("GUI installer startup failed, falling back to text mode.")
|
|
self.displayMode = 't'
|
|
if 'DISPLAY' in os.environ.keys():
|
|
del os.environ['DISPLAY']
|
|
time.sleep(2)
|
|
|
|
if self.displayMode == 't':
|
|
from text import InstallInterface
|
|
if not os.environ.has_key("LANG"):
|
|
os.environ["LANG"] = "en_US.UTF-8"
|
|
|
|
if self.displayMode == 'c':
|
|
from cmdline import InstallInterface
|
|
|
|
self._intf = InstallInterface()
|
|
return self._intf
|
|
|
|
def writeXdriver(self, root = None):
|
|
# this should go away at some point, but until it does, we
|
|
# need to keep it around.
|
|
if self.xdriver is None:
|
|
return
|
|
if root is None:
|
|
root = self.rootPath
|
|
if not os.path.isdir("%s/etc/X11" %(root,)):
|
|
os.makedirs("%s/etc/X11" %(root,), mode=0755)
|
|
f = open("%s/etc/X11/xorg.conf" %(root,), 'w')
|
|
f.write('Section "Device"\n\tIdentifier "Videocard0"\n\tDriver "%s"\nEndSection\n' % self.xdriver)
|
|
f.close()
|
|
|
|
def setMethodstr(self, methodstr):
|
|
if methodstr.startswith("cdrom://"):
|
|
(device, tree) = string.split(methodstr[8:], ":", 1)
|
|
|
|
if not tree.startswith("/"):
|
|
tree = "/%s" %(tree,)
|
|
|
|
if device.startswith("/dev/"):
|
|
device = device[5:]
|
|
|
|
self.mediaDevice = device
|
|
self.methodstr = "cdrom://%s" % tree
|
|
else:
|
|
self.methodstr = methodstr
|
|
|
|
def requiresNetworkInstall(self):
|
|
fail = False
|
|
numNetDevs = isys.getNetworkDeviceCount()
|
|
|
|
if self.methodstr is not None:
|
|
if (self.methodstr.startswith("http") or \
|
|
self.methodstr.startswith("ftp://") or \
|
|
self.methodstr.startswith("nfs:")) and \
|
|
numNetDevs == 0:
|
|
fail = True
|
|
elif self.stage2 is not None:
|
|
if self.stage2.startswith("cdrom://") and \
|
|
not os.path.isdir("/mnt/stage2/Packages") and \
|
|
numNetDevs == 0:
|
|
fail = True
|
|
|
|
if fail:
|
|
log.error("network install required, but no network devices available")
|
|
|
|
return fail
|
|
|
|
def write(self):
|
|
self.writeXdriver()
|
|
self.instLanguage.write(self.rootPath)
|
|
|
|
self.timezone.write(self.rootPath)
|
|
self.network.write(instPath=self.rootPath, anaconda=self)
|
|
self.desktop.write(self.rootPath)
|
|
self.users.write(self.rootPath)
|
|
self.security.write(self.rootPath)
|
|
self.firewall.write(self.rootPath)
|
|
|
|
if self.ksdata:
|
|
for svc in self.ksdata.services.disabled:
|
|
iutil.execWithRedirect("/sbin/chkconfig",
|
|
[svc, "off"],
|
|
stdout="/dev/tty5", stderr="/dev/tty5",
|
|
root=self.rootPath)
|
|
|
|
for svc in self.ksdata.services.enabled:
|
|
iutil.execWithRedirect("/sbin/chkconfig",
|
|
[svc, "on"],
|
|
stdout="/dev/tty5", stderr="/dev/tty5",
|
|
root=self.rootPath)
|
|
|
|
def writeKS(self, filename):
|
|
import urllib
|
|
from pykickstart.version import versionToString, DEVEL
|
|
|
|
f = open(filename, "w")
|
|
|
|
f.write("# Kickstart file automatically generated by anaconda.\n\n")
|
|
f.write("#version=%s\n" % versionToString(DEVEL))
|
|
|
|
if self.upgrade:
|
|
f.write("upgrade\n");
|
|
else:
|
|
f.write("install\n");
|
|
|
|
m = None
|
|
|
|
if self.methodstr:
|
|
m = self.methodstr
|
|
elif self.stage2:
|
|
m = self.stage2
|
|
|
|
if m:
|
|
if m.startswith("cdrom:"):
|
|
f.write("cdrom\n")
|
|
elif m.startswith("hd:"):
|
|
if m.count(":") == 3:
|
|
(part, fs, dir) = string.split(m[3:], ":")
|
|
else:
|
|
(part, dir) = string.split(m[3:], ":")
|
|
|
|
f.write("harddrive --partition=%s --dir=%s\n" % (part, dir))
|
|
elif m.startswith("nfs:"):
|
|
if m.count(":") == 3:
|
|
(server, opts, dir) = string.split(m[4:], ":")
|
|
f.write("nfs --server=%s --opts=%s --dir=%s" % (server, opts, dir))
|
|
else:
|
|
(server, dir) = string.split(m[4:], ":")
|
|
f.write("nfs --server=%s --dir=%s\n" % (server, dir))
|
|
elif m.startswith("ftp://") or m.startswith("http"):
|
|
f.write("url --url=%s\n" % urllib.unquote(m))
|
|
|
|
# Some kickstart commands do not correspond to any anaconda UI
|
|
# component. If this is a kickstart install, we need to make sure
|
|
# the information from the input file ends up in the output file.
|
|
if self.ksdata:
|
|
f.write(self.ksdata.user.__str__())
|
|
f.write(self.ksdata.services.__str__())
|
|
f.write(self.ksdata.reboot.__str__())
|
|
|
|
self.instLanguage.writeKS(f)
|
|
|
|
if not self.isHeadless:
|
|
self.keyboard.writeKS(f)
|
|
self.network.writeKS(f)
|
|
|
|
self.timezone.writeKS(f)
|
|
self.users.writeKS(f)
|
|
self.security.writeKS(f)
|
|
self.firewall.writeKS(f)
|
|
|
|
self.storage.writeKS(f)
|
|
self.bootloader.writeKS(f)
|
|
|
|
if self.backend:
|
|
self.backend.writeKS(f)
|
|
self.backend.writePackagesKS(f, self)
|
|
|
|
# Also write out any scripts from the input ksfile.
|
|
if self.ksdata:
|
|
for s in self.ksdata.scripts:
|
|
f.write(s.__str__())
|
|
|
|
# make it so only root can read, could have password
|
|
os.chmod(filename, 0600)
|
|
|
|
if __name__ == "__main__":
|
|
setupPythonPath()
|
|
|
|
# Allow a file to be loaded as early as possible
|
|
try:
|
|
import updates_disk_hook
|
|
except ImportError:
|
|
pass
|
|
|
|
# Set up logging as early as possible.
|
|
import logging
|
|
import anaconda_log
|
|
|
|
log = logging.getLogger("anaconda")
|
|
stdoutLog = logging.getLogger("anaconda.stdout")
|
|
|
|
# pull this in to get product name and versioning
|
|
import product
|
|
|
|
# this handles setting up updates for pypackages to minimize the set needed
|
|
setupPythonUpdates()
|
|
|
|
import signal, string, isys, iutil, time
|
|
import warnings
|
|
import vnc
|
|
import users
|
|
import kickstart
|
|
import storage.storage_log
|
|
|
|
from flags import flags
|
|
|
|
# the following makes me very sad. -- katzj
|
|
# we have a slightly different set of udev rules in the second
|
|
# stage than the first stage. why this doesn't get picked up
|
|
# automatically, I don't know. but we need to trigger so that we
|
|
# have all the information about netdevs that we care about for
|
|
# NetworkManager in the udev database
|
|
from baseudev import udev_trigger, udev_settle
|
|
udev_trigger("net")
|
|
udev_settle()
|
|
# and for added fun, once doesn't seem to be enough? so we
|
|
# do it twice, it works and we scream at the world "OH WHY?"
|
|
udev_trigger("net")
|
|
udev_settle()
|
|
|
|
import gettext
|
|
_ = lambda x: gettext.ldgettext("anaconda", x)
|
|
|
|
anaconda = Anaconda()
|
|
warnings.showwarning = AnacondaShowWarning
|
|
setupTranslations()
|
|
|
|
# reset python's default SIGINT handler
|
|
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
|
signal.signal(signal.SIGSEGV, isys.handleSegv)
|
|
|
|
setupEnvironment()
|
|
|
|
pidfile = open("/var/run/anaconda.pid", "w")
|
|
pidfile.write("%s\n" % (os.getpid(),))
|
|
del pidfile
|
|
# add our own additional signal handlers
|
|
signal.signal(signal.SIGHUP, startDebugger)
|
|
|
|
# we need to do this really early so we make sure its done before rpm
|
|
# is imported
|
|
iutil.writeRpmPlatform()
|
|
|
|
graphical_failed = 0
|
|
vncS = vnc.VncServer() # The vnc Server object.
|
|
vncS.anaconda = anaconda
|
|
xserver_pid = None
|
|
|
|
(opts, args) = parseOptions()
|
|
|
|
if opts.unsupportedMode:
|
|
stdoutLog.error("Running anaconda in %s mode is no longer supported." % opts.unsupportedMode)
|
|
sys.exit(0)
|
|
|
|
# Now that we've got arguments, do some extra processing.
|
|
setupLoggingFromOpts(opts)
|
|
|
|
# Default is to prompt to mount the installed system.
|
|
anaconda.rescue_mount = not opts.rescue_nomount
|
|
|
|
if opts.dlabel: #autodetected driverdisc in use
|
|
flags.dlabel = True
|
|
|
|
anaconda.displayMode = opts.display_mode
|
|
anaconda.isHeadless = opts.isHeadless
|
|
|
|
if opts.noipv4:
|
|
flags.useIPv4 = False
|
|
|
|
if opts.noipv6:
|
|
flags.useIPv6 = False
|
|
|
|
if opts.proxy:
|
|
anaconda.proxy = opts.proxy
|
|
|
|
if opts.proxyAuth:
|
|
filename = opts.proxyAuth
|
|
ret = open(filename, "r").readlines()
|
|
os.unlink(filename)
|
|
|
|
anaconda.proxyUsername = ret[0].rstrip()
|
|
if len(ret) == 2:
|
|
anaconda.proxyPassword = ret[1].rstrip()
|
|
|
|
if opts.updateSrc:
|
|
anaconda.updateSrc = opts.updateSrc
|
|
|
|
if opts.method:
|
|
if opts.method[0] == '@':
|
|
opts.method = expandFTPMethod(opts.method)
|
|
|
|
anaconda.setMethodstr(opts.method)
|
|
else:
|
|
anaconda.methodstr = None
|
|
|
|
if opts.stage2:
|
|
if opts.stage2[0] == '@':
|
|
opts.stage2 = expandFTPMethod(opts.stage2)
|
|
|
|
anaconda.stage2 = opts.stage2
|
|
|
|
if opts.liveinst:
|
|
flags.livecdInstall = True
|
|
|
|
if opts.module:
|
|
for mod in opts.module:
|
|
(path, name) = string.split(mod, ":")
|
|
anaconda.extraModules.append((path, name))
|
|
|
|
if opts.vnc:
|
|
flags.usevnc = 1
|
|
anaconda.displayMode = 'g'
|
|
vncS.recoverVNCPassword()
|
|
|
|
# Only consider vncconnect when vnc is a param
|
|
if opts.vncconnect:
|
|
cargs = string.split(opts.vncconnect, ":")
|
|
vncS.vncconnecthost = cargs[0]
|
|
if len(cargs) > 1 and len(cargs[1]) > 0:
|
|
if len(cargs[1]) > 0:
|
|
vncS.vncconnectport = cargs[1]
|
|
|
|
if opts.ibft:
|
|
flags.ibft = 1
|
|
|
|
if opts.iscsi:
|
|
flags.iscsi = 1
|
|
|
|
if opts.targetArch:
|
|
flags.targetarch = opts.targetArch
|
|
|
|
# set flags
|
|
flags.dmraid = opts.dmraid
|
|
flags.mpath = opts.mpath
|
|
flags.selinux = opts.selinux
|
|
|
|
if opts.serial:
|
|
flags.serial = True
|
|
if opts.virtpconsole:
|
|
flags.virtpconsole = opts.virtpconsole
|
|
|
|
if opts.xdriver:
|
|
anaconda.xdriver = opts.xdriver
|
|
anaconda.writeXdriver(root="/")
|
|
|
|
if not flags.livecdInstall:
|
|
isys.auditDaemon()
|
|
|
|
# setup links required for all install types
|
|
for i in ( "services", "protocols", "nsswitch.conf", "joe", "selinux",
|
|
"mke2fs.conf" ):
|
|
try:
|
|
if os.path.exists("/mnt/runtime/etc/" + i):
|
|
os.symlink ("../mnt/runtime/etc/" + i, "/etc/" + i)
|
|
except:
|
|
pass
|
|
|
|
# This is the one place we do all kickstart file parsing.
|
|
if opts.ksfile:
|
|
kickstart.preScriptPass(anaconda, opts.ksfile)
|
|
anaconda.ksdata = kickstart.parseKickstart(anaconda, opts.ksfile)
|
|
opts.rescue = anaconda.ksdata.rescue.rescue
|
|
|
|
# we need to have a libuser.conf that points to the installer root for
|
|
# sshpw, but after that we start sshd, we need one that points to the
|
|
# install target.
|
|
luserConf = users.createLuserConf(instPath="")
|
|
handleSshPw(anaconda)
|
|
startSsh()
|
|
del(os.environ["LIBUSER_CONF"])
|
|
|
|
users.createLuserConf(anaconda.rootPath)
|
|
|
|
if opts.rescue:
|
|
anaconda.rescue = True
|
|
|
|
import rescue
|
|
|
|
if anaconda.ksdata:
|
|
anaconda.instClass.configure(anaconda)
|
|
|
|
# We need an interface before running kickstart execute methods for
|
|
# storage.
|
|
from snack import *
|
|
screen = SnackScreen()
|
|
anaconda.intf = rescue.RescueInterface(screen)
|
|
|
|
anaconda.ksdata.execute()
|
|
|
|
anaconda.intf = None
|
|
screen.finish()
|
|
|
|
# command line 'nomount' overrides kickstart /same for vnc/
|
|
anaconda.rescue_mount = not (opts.rescue_nomount or anaconda.ksdata.rescue.nomount)
|
|
|
|
rescue.runRescue(anaconda)
|
|
|
|
# shouldn't get back here
|
|
sys.exit(1)
|
|
|
|
if anaconda.ksdata:
|
|
if anaconda.ksdata.vnc.enabled:
|
|
flags.usevnc = 1
|
|
anaconda.displayMode = 'g'
|
|
|
|
if vncS.password == "":
|
|
vncS.password = anaconda.ksdata.vnc.password
|
|
|
|
if vncS.vncconnecthost == "":
|
|
vncS.vncconnecthost = anaconda.ksdata.vnc.host
|
|
|
|
if vncS.vncconnectport == "":
|
|
vncS.vncconnectport = anaconda.ksdata.vnc.port
|
|
|
|
flags.vncquestion = False
|
|
|
|
# disable VNC over text question when not enough memory is available
|
|
if iutil.memInstalled() < isys.MIN_GUI_RAM:
|
|
flags.vncquestion = False
|
|
|
|
|
|
if anaconda.displayMode == 't' and flags.vncquestion: #we prefer vnc over text mode, so ask about that
|
|
title = _("Would you like to use VNC?")
|
|
message = _("Text mode provides a limited set of installation options. "
|
|
"It does not allow you to specify your own partitioning "
|
|
"layout or package selections. Would you like to use VNC "
|
|
"mode instead?")
|
|
|
|
ret = vnc.askVncWindow(title, message)
|
|
if ret != -1:
|
|
anaconda.displayMode = 'g'
|
|
flags.usevnc = 1
|
|
if ret is not None:
|
|
vncS.password = ret
|
|
|
|
if opts.debug:
|
|
flags.debug = True
|
|
|
|
log.info("anaconda called with cmdline = %s" %(sys.argv,))
|
|
log.info("Display mode = %s" % anaconda.displayMode)
|
|
log.info("Default encoding = %s " % sys.getdefaultencoding())
|
|
|
|
checkMemory(anaconda)
|
|
|
|
#
|
|
# now determine if we're going to run in GUI or TUI mode
|
|
#
|
|
# if no X server, we have to use text mode
|
|
if not flags.livecdInstall and not iutil.isS390() and not os.access("/usr/bin/Xorg", os.X_OK):
|
|
stdoutLog.warning(_("Graphical installation is not available. "
|
|
"Starting text mode."))
|
|
time.sleep(2)
|
|
anaconda.displayMode = 't'
|
|
|
|
# s390/iSeries checks
|
|
if anaconda.isHeadless and anaconda.displayMode == "g" and not \
|
|
(os.environ.has_key("DISPLAY") or flags.usevnc):
|
|
stdoutLog.warning(_("DISPLAY variable not set. Starting text mode."))
|
|
anaconda.displayMode = 't'
|
|
graphical_failed = 1
|
|
time.sleep(2)
|
|
|
|
# if DISPLAY not set either vnc server failed to start or we're not
|
|
# running on a redirected X display, so start local X server
|
|
if anaconda.displayMode == 'g' and not os.environ.has_key('DISPLAY') and not flags.usevnc:
|
|
try:
|
|
# The following code depends on no SIGCHLD being delivered, possibly
|
|
# only except the one from a failing X.org. Thus make sure before
|
|
# entering this section that all the other children of anaconda have
|
|
# terminated or were forked into an orphan (which won't deliver a
|
|
# SIGCHLD to mess up the fragile signaling below).
|
|
|
|
# start X with its USR1 handler set to ignore. this will make it send
|
|
# us SIGUSR1 if it succeeds. if it fails, catch SIGCHLD and bomb out.
|
|
|
|
def sigchld_handler(num, frame):
|
|
raise OSError
|
|
|
|
def sigusr1_handler(num, frame):
|
|
pass
|
|
|
|
def preexec_fn():
|
|
signal.signal(signal.SIGUSR1, signal.SIG_IGN)
|
|
|
|
old_sigusr1 = signal.signal(signal.SIGUSR1, sigusr1_handler)
|
|
old_sigchld = signal.signal(signal.SIGCHLD, sigchld_handler)
|
|
xout = open("/dev/tty5", "w")
|
|
|
|
proc = subprocess.Popen(["Xorg", "-br", "-logfile", "/tmp/X.log",
|
|
":1", "vt6", "-s", "1440", "-ac",
|
|
"-nolisten", "tcp", "-dpi", "96"],
|
|
close_fds=True, stdout=xout, stderr=xout,
|
|
preexec_fn=preexec_fn)
|
|
|
|
signal.pause()
|
|
|
|
os.environ["DISPLAY"] = ":1"
|
|
doStartupX11Actions(opts.runres)
|
|
xserver_pid = proc.pid
|
|
except (OSError, RuntimeError):
|
|
stdoutLog.warning(" X startup failed, falling back to text mode")
|
|
anaconda.displayMode = 't'
|
|
graphical_failed = 1
|
|
time.sleep(2)
|
|
finally:
|
|
signal.signal(signal.SIGUSR1, old_sigusr1)
|
|
signal.signal(signal.SIGCHLD, old_sigchld)
|
|
|
|
if anaconda.displayMode == 't' and graphical_failed and not anaconda.ksdata:
|
|
ret = vnc.askVncWindow()
|
|
if ret != -1:
|
|
anaconda.displayMode = 'g'
|
|
flags.usevnc = 1
|
|
if ret is not None:
|
|
vncS.password = ret
|
|
|
|
# if they want us to use VNC do that now
|
|
if anaconda.displayMode == 'g' and flags.usevnc:
|
|
runVNC()
|
|
doStartupX11Actions(opts.runres)
|
|
|
|
# with X running we can initialize the UI interface
|
|
anaconda.initInterface()
|
|
anaconda.instClass.configure(anaconda)
|
|
|
|
# comment out the next line to make exceptions non-fatal
|
|
from exception import initExceptionHandling
|
|
anaconda.mehConfig = initExceptionHandling(anaconda)
|
|
|
|
# add our own additional signal handlers
|
|
signal.signal(signal.SIGUSR2, lambda signum, frame: anaconda.dumpState())
|
|
|
|
# download and run Dogtail script
|
|
if opts.dogtail:
|
|
try:
|
|
import urlgrabber
|
|
|
|
try:
|
|
fr = urlgrabber.urlopen(opts.dogtail)
|
|
except urlgrabber.grabber.URLGrabError, e:
|
|
log.error("Could not retrieve Dogtail script from %s.\nError was\n%s" % (opts.dogtail, e))
|
|
fr = None
|
|
|
|
if fr:
|
|
(fw, testcase) = mkstemp(prefix='testcase.py.', dir='/tmp')
|
|
os.write(fw, fr.read())
|
|
fr.close()
|
|
os.close(fw)
|
|
|
|
# download completed, run the test
|
|
if not os.fork():
|
|
# we are in the child
|
|
os.chmod(testcase, 0755)
|
|
os.execv(testcase, [testcase])
|
|
sys.exit(0)
|
|
else:
|
|
# we are in the parent, sleep to give time for the testcase to initialize
|
|
# todo: is this needed, how to avoid possible race conditions
|
|
time.sleep(1)
|
|
except Exception, e:
|
|
log.error("Exception %s while running Dogtail testcase" % e)
|
|
|
|
if opts.lang:
|
|
# this is lame, but make things match what we expect (#443408)
|
|
opts.lang = opts.lang.replace(".utf8", ".UTF-8")
|
|
anaconda.dispatch.skipStep("language", permanent = 1)
|
|
anaconda.instLanguage.instLang = opts.lang
|
|
anaconda.instLanguage.systemLang = opts.lang
|
|
anaconda.timezone.setTimezoneInfo(anaconda.instLanguage.getDefaultTimeZone(anaconda.rootPath))
|
|
|
|
if opts.keymap:
|
|
anaconda.dispatch.skipStep("keyboard", permanent = 1)
|
|
anaconda.keyboard.set(opts.keymap)
|
|
anaconda.keyboard.activate()
|
|
|
|
if anaconda.ksdata:
|
|
import storage
|
|
|
|
# Before we set up the storage system, we need to know which disks to
|
|
# ignore, etc. Luckily that's all in the kickstart data.
|
|
anaconda.storage.zeroMbr = anaconda.ksdata.zerombr.zerombr
|
|
anaconda.storage.ignoredDisks = anaconda.ksdata.ignoredisk.ignoredisk
|
|
anaconda.storage.exclusiveDisks = anaconda.ksdata.ignoredisk.onlyuse
|
|
|
|
if anaconda.ksdata.clearpart.type is not None:
|
|
anaconda.storage.clearPartType = anaconda.ksdata.clearpart.type
|
|
anaconda.storage.clearPartDisks = anaconda.ksdata.clearpart.drives
|
|
if anaconda.ksdata.clearpart.initAll:
|
|
anaconda.storage.reinitializeDisks = anaconda.ksdata.clearpart.initAll
|
|
|
|
storage.storageInitialize(anaconda)
|
|
|
|
# Now having initialized storage, we can apply all the other kickstart
|
|
# commands. This gives us the ability to check that storage commands
|
|
# are correctly formed and refer to actual devices.
|
|
anaconda.ksdata.execute()
|
|
|
|
# set up the headless case
|
|
if anaconda.isHeadless:
|
|
anaconda.dispatch.skipStep("keyboard", permanent = 1)
|
|
|
|
if not anaconda.ksdata:
|
|
anaconda.instClass.setSteps(anaconda)
|
|
else:
|
|
kickstart.setSteps(anaconda)
|
|
|
|
try:
|
|
anaconda.intf.run(anaconda)
|
|
except SystemExit, code:
|
|
anaconda.intf.shutdown()
|
|
|
|
if anaconda.ksdata and anaconda.ksdata.reboot.eject:
|
|
for drive in anaconda.storage.devicetree.devices:
|
|
if drive.type != "cdrom":
|
|
continue
|
|
|
|
log.info("attempting to eject %s" % drive.path)
|
|
drive.eject()
|
|
|
|
del anaconda.intf
|
|
|
|
# vim:tw=78:ts=4:et:sw=4
|