1、概述
Anaconda是RedHat、CentOS、Fedora等Linux的安装管理程序。它可以提供文本、图形等安装管理方式,并支持Kickstart等脚本提供自动安装的功能。此外,其还支持许多启动参数,熟悉这些参数可为安装带来很多方便。该程序的功能是把位于光盘或其他源上的数据包,根据设置安装到主机上。为实现该定制安装,它提供一个定制界面,可以实现交互式界面供用户选择配置(如选择语言,键盘,时区等信息)。Anaconda的大部分模块用Python编写,有少许的载入模块用C编写。
Anaconda支持的管理模式:
(1)Kickstart提供的自动化安装;
(2)对一个RedHat实施upgrade;
(3)Rescuse模式对不能启动的系统进行故障排除。
要进入安装步骤,需要先有一个引导程序引导启动一个特殊的Linux安装环境系统;引导有多种方式:
(1)基于网络方式的小型引导镜像,需要提供小型的引导镜像;
(2)U盘引导,通过可引导存储介质中的小型引导镜像启动安装过程;
(3)基于PXE的网络安装方式,要提供PXE的完整安装环境;
(4)其他bootloder引导(如GRUB)。
可用的安装方式:本地CDROM、硬盘驱动器、网络方式(NFS、FTP、HTTP)。
通过网络方式安装时,不论通过FTP、HTTP还是NFS方式共享安装,可以将安装光盘先拷贝到网络服务器上保存为iso镜像,然后loop挂载到共享目录或网页目录(当然,拷贝镜像中的所有文件到指定位置或直接挂载到共享目录也可),而通过NFS方式时,可以直接将光盘的iso文件放到共享目录即可,安装程序挂载共享目录后可以自动识别镜像。
注意思复制安装光盘,并保存为一个 iso 映像文件的方法(对于 DVD/CD):
# dd if=/dev/cdrom of=/location/of/disk/space/RHEL.iso bs=32k
注意拷贝时bs块大小设置为32k,我实验时设为1M,虽然减小了文件体积,但是安装读镜像时会报错。
对于Kickstart,它是一个利用Anconda工具实现服务器自动化安装的方法。通过生成的kickstart配置文件ks.cfg,服务器安装可以实现从裸机到全功能服务的的非交互式(无人值守式)安装配置;ks.cfg是一个简单的文本文件,文件包含Anconda在安装系统及安装后配置服务时所需要获取的一些必要配置信息(如键盘设置,语言设置,分区设置等)。Anconda直接从该文件中读取必要的配置,只要该文件信息配置正确无误且满足所有系统需求,就不再需要同用户进行交互获取信息,从而实现安装的自动化。但是配置中如果忽略任何必需的项目,安装程序会提示用户输入相关的项目的选择,就象用户在典型的安装过程中所遇到的一样。一旦用户进行了选择,安装会以非交互的方式(unattended)继续。使用kickstart可以实现流线化自动化的安装、快速大量的裸机部署、强制建立的一致性(软件包,分区,配置,监控,安全性)、以及减少人为的部署失误。
使用Kickstart方法安装的过程包括创建一个kickstart文件、创建有kickstart文件的引导介质或者使这个文件在网络上可用、筹备一个安装树、开始ks安装(anconda自身启动 -->选取ks安装模式--> 从ks文件读取配置 --> 最后安装)。创建kickstart配置文件可以使用任何文本编辑器,也可以使用图形化配置工具system-config-kickstat(需要安装system-config-kickstart.noarch包)。注意配置文件生成后,推荐使用ksvalidator命令检查配置文件语法及完整性错误,例如:
[root@bogon ~]# ksvalidator ks.cfg
not enough arguments for format string
Kickstart文件的语法及参数含义可参考 http://docs.redhat.com/docs/en-US/Red_Hat_Enterprise_Linux/6/html/Installation_Guide/s1-kickstart2-options.html。
我们以RHEL 6.0的安装为例来分析Anaconda。为紧跟新版本,anaconda源码则使用较新的在Fedora 15中使用的版本。先从Fedora的下载站点镜像列表http://mirrors.fedoraproject.org/publiclist/中选择一个站点,例如上海交大镜像站点的/fedora/linux/releases/15/Everything/source/SRPMS/目录中下载用于Fedora 15的最新版anaconda源码包anaconda-15.31-1.fc15.src.rpm,还要准备好RHEL 6.0的DVD安装光盘。
2、RedHat企业版6.0光盘的安装树介绍
(1)Packages目录:包含安装所需的所有二进制RPM包。
(2)HighAvailability、LoadBalancer、ResilientStorage、ScalableFileSystem、Server目录:五个文件夹包含了安装所需的所有RPM软件包信息。它们分别对应高可用性、负载均衡、弹性存储、可扩展文件系统、以及服务器基础的软件包信息。每个文件夹下都有一个Packages目录,它只是一个链接,指向顶级的../Packages目录。还有一个repodata目录,其中的类似于*-comps-rhel6-HighAvailability.xml的XML文件定义了对应特性的软件包信息,这些软件包被分成不同的组,每个组有一个ID。这样的repodata精确描述各个RPM包的详细信息,如依赖关系,包含文件,校验码信息等。可以通过在Kickstart文件的%packages段中指定组ID来安装相应组中的软件包。
(3)EFI目录:用于64位的基于EFI的系统引导。其中BOOT目录下的BOOTX64.conf为grub的配置文件,用于显示引导菜单。
(4)TRANS.TBL文件:记录当前目录的列表,用mkisofs的-T参数重新生成,主要是为了长文件名称。
(5).discinfo文件是安装介质的识别信息。.treeinfo文件记录不同安装方式安装程序所在的目录结构,如PXE方式时,内核kernel=images/pxeboot/vmlinuz,根文件系统initrd=images/pxeboot/initrd.img。
(6)isolinux目录:有开机引导系统安装的内核(vmlinuz)及RAM镜像(initrd.img),在引导系统时会载入内存,给系统的安装提供一个Linux安装引导平台,文件夹中还有在不同模式下显示信息的boot.msg文件,splash.jpg是特殊格式的引导过程背景图片(640*480)。安装时这个画面上的引导菜单内容在isolinux.cfg文件中指定。按Enter会自动进入图形界面安装模式,若按Esc,会显示"boot: "命令提示符,进入用户交互模式,界面上会有各种模式操作提示。键入"linux text",会进入文本安装模式。
(7)images目录:包含有各种引导镜像。最重要的是引导第二阶段安装需要用到的镜像文件install.img(rhel 5中是stage2.img),anaconda程序就在这个镜像文件中。另外还有用于制作微型启动光盘的boot.iso(为节省空间rhel 6.0中已经删除了,在rhel 5中是有的),有可放置于USB或其他大容量可引导介质的VFAT分区上,制作引导工具的镜像diskboot.img(rhel 5中有),也有用于制作PXE安装方式引导介质的pxeboot文件夹等。
3、Anaconda程序目录结构和源代码包概览
先用file命令查看install.img的文件系统类型,可知是suqashfs,用mount -o loop -t squashfs install.img ./img/的方式挂载出来。 除了主执行体/usr/bin/anaconda,其它安装脚本模块均在/usr/lib/anaconda主目录下。我们看一下整个anaconda主目录的结构:
/usr/bin/anaconda: 主程序,是python脚本。
/usr/lib/anaconda/installclasses: 定义了在安装过程中用户可选择的安装类型。每个安装类型描述文件根据相应安装类型的特点,分别对安装步骤、分区策略以及安装包的取舍给出了不同的方案。里面有两个文件fedora.py和rhel.py,分别针对fedora和rhel的安装类型。其他的Linux发行版可以定义它们自己的安装类型。
/usr/lib/anaconda/iw: 图形安装模式的模块。包含所有图形界面类所在的模块,每个图形界面对应一个类,负责相应安装步骤图形界面的具体外观显示及与用户的交互,(可能)调用anaconda主目录下的相关安装行为模块完成具体的安装操作。
/usr/lib/anaconda/storage: 存储配置的响应目录(如分区,FCOE, iSCSI, RAID, ZFCP的配置等)。
/usr/lib/anaconda/textw: 文本安装模式的模块。和iw子目录含义是一致的,只是包含的是文本安装模式的前端字符用户界面类所在的模块,每个字符用户界面对应一个类,负责与用户的交互,字符界面的采用了python的snack库。
/usr/lib/anaconda-runtime: 有init和loader程序。这是两个静态编译的程序,不依赖其他库,就是编译anaconda源代码目录下的loader目录下的C代码得到。这两个程序会放在最后用来启动安装过程的Linux initrd image里面。
/usr/anaconda主目录:如果说用户界面类是处理安装程序外观的话,则anaconda主目录下的各python模块执行每个安装界面背后具体的安装行为,包括那些无用户界面安装步骤的安装操作。
由此可见,主执行体/usr/bin/anaconda调用的大量例程分布在/usr/lib/anaconda目录下,安装过程要用到的资源文件(例如背景图片)则分布在/usr/share/anaconda目录下。Python的许多内置模块则在目录/usr/lib/pythonXX下,其中XX指版本号。
上面分析的是已经编译好的anaconda目录结构,现在概览一下anaconda源代码包的结构。Anaconda主要用Python编写,图形界面前端用pyGtk库(参考http://www.pygtk.org/)和Glade界面描述文件(参考http://glade.gnome.org/)编写。用来启动环境、加载模块、加载anaconda主体的loader程序用C编写,一些其他的硬件相关的部分也是用C编写。另外,bash和python脚本还用在一些管理性的任务中。
Aanaconda核心的源代码主要有:
(1)界面(Interface)
pyanaconda/cmdline.py
pyanaconda/gui.py
pyanaconda/installinterfacebase.py
pyanaconda/text.py
这些文件处理用户界面。anaconda支持3种用户界面,即图形、文本、命令行模式。每种模式的相应实现文件都包含用来画出各种窗体的类。
data/ui
pyanaconda/iw/
pyanaconda/textw/
iw目录包含图形界面屏幕的python文件,textw目录包含文件界面屏幕的python文件,ui目录包含图形模式所需的glade界面描述文件。通常开发者尽可能多地移除文本模式,把更多地东西移到图形界面,以便能使用glade。
pyanaconda/dispatch.py
调度器类Dispatcher是一个状态机,用来控制安装器中各步骤的移动。当一个Next或Back按钮被单击时,调度器知道要跳转到哪个屏幕,也知道根据相应的设置哪一步应该被忽略而直接跳过。每种安装模式都有它自己的要跳过或被添加返回的步骤集。Install类也可以指定需要跳过或需要添加的步骤。各种其他的与机器相关的细节,如anaconda检测和用户选择能够修改步骤集。
pyanaconda/vnc.py
用于控制对VNC进行设置(当在安装过程中请求了VNC时)。之后,安装进入图形模式。
(2)分区(Partitioning)
pyanaconda/storage/dasd.py
pyanaconda/storage/devicelibs/
pyanaconda/storage/fcoe.py
pyanaconda/storage/iscsi.py
pyanaconda/storage/zfcp.py
这些文件处理探测、配置、启动和停止anaconda支持的高级存储系统。这既包括硬件设备(FCOE, iSCSI, RAID, ZFCP等),也包含软件抽象(加密,逻辑卷管理lvm等)。其中LVM和RAID使用最普遍。
pyanaconda/storage/formats/
这个目录下的文件用来将一些文件系统或类似于文件系统的抽象写到存储设备。你可以把它看作是在pyanaconda/storage/devicelibs/之上的一层。类似于文件系统的抽象包括硬盘卷标、加密、与机器相关的引导分区、交换分区。
pyanaconda/partIntfHelpers.py
用来进行错误检查、输入验证、显示错误消息。图形和文本模式都要用到它。
pyanaconda/storage/__init__.py
pyanaconda/storage/errors.py
pyanaconda/storage/miscutils.py
pyanaconda/storage/size.py
pyanaconda/storage/storage_log.py
pyanaconda/storage/udev.py
这些文件形成存储模块的一个支持库,完成不适合分在其他组中的一些细小任务。从文件名即可看出其功能。__init__.py完成大部分的工作,包括读写存储相关的配置文件、检测现存的安装、各存储动作的协作、聚焦不同存储对象的数据、执行完整性检查。
pyanaconda/storage/deviceaction.py
pyanaconda/storage/devices.py
pyanaconda/storage/devicetree.py
pyanaconda/storage/partitioning.py
pyanaconda/storage/partspec.py
这组文件实现分区逻辑。它们使用DeviceTree抽象来存放现存的分区和请求、定义把存储请求写到硬盘的行为、处理自动分区(为默认方式)、并且知道怎么调整分区大小,以符合给定的分区容量。硬盘上分区的创建使用pyparted包来完成。
(3)引导器(Bootloader)
pyanaconda/bootloader.py
pyanaconda/booty/
这些文件控制把bootloader写到安装后的系统里。每种类型的机器有它自己的bootloader格式,因此在booty/目录下有相应的文件,bootloader.py则把它们粘合到一起。这对新的安装和更新非常有用。
(4)配置(Configuration)
pyanaconda/desktop.py
pyanaconda/firewall.py
pyanaconda/language.py
pyanaconda/network.py
pyanaconda/security.py
pyanaconda/timezone.py
pyanaconda/users.py
这些文件处理相关配置,这些配置步骤可以通过图形界面或kickstart进入。某种程度上它们影响安装过程(例如语言和键盘设置)。但是它们的主要目的是在安装过程的最后把这些配置写到安装后的系统里去。
(5)软件包安装(Package Installation)
pyanaconda/compssort.py
pyanaconda/backend.py
pyanaconda/image.py
pyanaconda/sortedtransaction.py
pyanaconda/yuminstall.py
这些文件控制软件包的安装。anaconda允许在后端安装多个软件包,虽然在安装树中某一时刻真正只有一个在使用yum、写入安装包的配置。
(6)安装类型(Installation Classes)
pyanaconda/installclass.py
pyanaconda/installclasses/
pyanaconda/product.py
安装类型定义形成一种安装轮廓的配置。这包括要显示或跳过的步骤、产品名称、安装方法、激活的库、配置设置等。这里主要用它来创建针对Fedora和RHEL的不同安装类型,其他的项目或ISV可以定义它们自己的安装类型。
(7)特殊模式(Special Modes)
pyanaconda/kickstart.py
Kickstart是一种通过给anaconda提供一个文件以实现自动化安装的方式。这个文件包含用户通过UI需要提供的所有数据。这个文件是解析器(在pykickstart包中)和anaconda内部构件之间的接口。它主要提供在anaconda期望的地方保存设置的方法。
data/icons
data/liveinst
liveinst/
pyanaconda/livecd.py
这些文件实现从Live CD安装。它们提供一种特殊的安装方法、一个特殊的软件包安装后端、以及一些用来从Live CD桌面上加载安装器的文件。
pyanaconda/rescue.py
pyanaconda/upgrade.py
这些文件提供与恢复模式和更新相关的方法。
(8)库(Library)
pyanaconda/__init__.py
pyanaconda/anaconda_log.py
pyanaconda/backend_log.py
pyanaconda/baseudev.py
pyanaconda/constants.py
pyanaconda/errors.py
pyanaconda/exception.py
pyanaconda/flags.py
pyanaconda/installmethod.py
pyanaconda/isys/
pyanaconda/iutil.py
pyanaconda/packages.py
pyanaconda/platform.py
pyanaconda/pyudev.py
pyanaconda/simpleconfig.py
pyanaconda/sitecustomize.py
pyanaconda/xutils.c
这组文件提供在安装器中使用的各种杂项功能,包括日志框架、硬件检测(通过udev接口)、进程控制、异常处理,以及其他任务。
(9)主程序(The Main Program)
anaconda
这是anaconda主程序,在源代码包的顶级目录中。它处理大量的环境设置、激活更新(如果存在)、读取kickstart文件、设置VNC,等等。当所有这些任务完成后,它把控制权交给dispatcher,以处理其余的安装过程。
(10)安装映像文件的构建(Image Building)
data/bootdisk/
data/command-stubs/
data/fonts/
scripts/
utils/
这些目录下的代码用来控制怎么建立安装环境,这包括创建初始ramdisk和stage2映像,添加某些必需命令的基本版,分割安装树为媒介大小的块,以及其他一些杂项任务。
(11)anaconda加载器(Loader)
loader/
该目录下的C代码实现initrd.img中的/init程序(实为指向/sbin/init程序)和/sbin/loader程序,loader程序用来加载主程序anaconda。
4、Anaconda启动分析
从“Linux内核启动过程分析”一节中我们知道,当开机从OS光盘启动,会先加载isolinux下可执行的内核映像vmlinuz,在内存中建立一个虚拟的根文件系统(rootfs),然后内核加载初始RAM磁盘initrd.img,建立一个安装Linux所需要的系统环境,这就是所谓的第一阶段。内核最后会运行initrd.img中的/init程序,由它来启动第二阶段的安装过程,即加载系统安装程序anaconda,执行具体的安装过程。注意如果通过网络方式安装(如NFS方式),则会根据安装树的NFS路径,通过mount把vmlinuz和initrd.img挂载到本地,像访问本地文件一样访问远程文件,以建立安装环境(在具体运行某个文件时会从网络下载到本地)。
initrd.img通常是一个用gzip压缩的cpio归档文件,需要加上.gz后缀并用gunzip解压成新的initrd.img,然后用cpio -i --make-directories < initrd.img释放其内容,生成一个小型的根文件系统。可以看到/init程序指向的是/sbin/init程序,里面还有loader程序,这就是编译anaconda源码时生成的两个程序。可见这个initrd.img中的/sbin/init程序是专门为anaconda定制的,常被称为installer类型的init。/sbin/loader程序可以看作是真正的anaconda自己的"init"程序,它由/init程序调用。
总结anaconda的两个阶段:
(1)第一阶段:加载安装树的isolinux目录下的内核映像vmlinuz和初始RAM磁盘initrd.img,建立安装环境。initrd.img中的/init程序调用/sbin/loader程序,loader加载kickstart文件,最后运行/usr/bin/anaconda主程序,进入第二阶段。
(2)第二阶段:anaconda程序加载各python和bash模块,执行各个安装步骤。
OK,分析的起点从loader/init.c的main函数开始。可以结合系统安装完后的anaconda log来分析,在/var/log下,主要有一般性的anaconda消息anaconda.log,由anaconda运行的所有外部程序信息anaconda.program.log,可扩展的存储模块信息anaconda.storage.log,网络接口配置相关信息anaconda.ifcfg.log,yum安装软件包的信息anaconda.yum.log,硬件相关的系统信息anaconda.syslog。注意如果系统安装失败,则这些文件的信息会一起放在一个anaconda-tb-identifier文件中,这里identifier是一个随机字符串。
文件的调用顺序为isolinux/vmlinuz--->isolinux/initrd.img--->/init--->/sbin/loader--->imagaes/install.img--->/usr/bin/anaconda。以最新的Fedora 15使用的Anaconda 15.31版本为例(注意RHEL 6.0使用的是比这老的版本,为了跟踪前沿,这里使用比较新的版本),Anaconda主程序的启动流程如下:
- /init (loader/init.c)
- --->setupEnv() 设置环境变量
- --->mount("/proc", "/proc", "proc",...) 挂载/proc文件系统
- --->mount("/dev", "/dev", "tmpfs",...) 创建/dev文件系统
- --->execl("/sbin/udevd", "/sbin/udevd",...) 启动udev
- --->mount("/sys", "/sys", "sysfs",...) 挂载/sys文件系统
- --->open("/dev/console", O_WRONLY) 打开控制台设备
- --->open("/dev/tty1", O_RDWR, 0) 打开tty1控制台,用于执行安装
- --->禁用Ctrl+Z、Ctrl+C等
- --->mount("/", "/", "ext2",...) 尝试重新挂载rootfs
- --->mount("none", "/tmp", "tmpfs",...) 挂载/tmp文件系统
- --->execve("/sbin/loader", argvc, env) 根据选项参数运行loader程序,替换init进程
-
- /sbin/loader (loader/loader.c:main())
- --->解析运行loader的选项参数
- --->pyanaconda/isys/log.c:openLog() 打开log
- --->fopen("/dev/tty3","a") 在tty3上显示硬件相关消息
- --->fopen("/tmp/anaconda.log", "a")
- --->fopen("/tmp/program.log", "a")
- --->loader.c:parseCmdLineFlags() 解析/proc/cmdline中的内核命令行参数,以获取ks文件的nfs路径
- --->readNetInfo() 读取/tmp/s390net文件中的网络配置信息(如果存在的话)
- --->driverdisk.c:readModuleInfo(arg, modInfo,...) 读取/lib/modules/module-info中的模块信息
- --->loader.c:checkForRam(-1) 检查内存容量是否足够
- --->pyanaconda/isys/mem.c:totalMemory()
- --->open("/proc/meminfo", O_RDONLY) 获取meminfo中"MemTotal"一行中的数据(kB为单位)
- --->loader.c:loadScsiDhModules() 加载SCSI模块
- (读取/lib/modules/<ver>/kernel/drivers/scsi/device_handler/下的模块)
- --->modules.c:mlLoadModuleSet(modNames) 加载用:分隔的模块列表
- --->modules.c:_doLoadModule()
- --->modules.c:mlSaveModuleState() 保存预加载的模块状态
- --->modules.c:processModuleLines() 一行一行地处理每个模块
- --->fopen("/proc/modules", "r") 读取/proc/modules中的所有模块信息
- --->modules.c:cb_savestate() 保存当前模块状态
- --->hardware.c:busProbe() 探测总线以加载所有知道的硬件设备
- --->hardware.c:detectHardware()
- -->execv("/sbin/udevadm", args) 运行udevadm来加载设备
- --->driverdisk.c:loadDriverDiskFromPartition() 加载自动检测到的第三方Driver Disk(如果有的话)
- --->loadDriverDisk(loaderData, "/tmp/drivers") 加载DD
- --->pyanaconda/isys/iface_start_NetworkManager() 启动NetworkManager
- --->execl(NETWORKMANAGER, NETWORKMANAGER,...)
- --->kickstart.c:getKickstartFile(&loaderData) 获取Kickstart文件并复制到/tmp/ks.cfg
- ################################### NFS 方式 #####################################
- --->nfsinstall.c:kickstartFromNfs(c+4, loaderData) 从NFS获取ks文件
- --->nfsinstall.c:getFileFromNfs()
- --->net.c:kickstartNetworkUp() 启动网卡
- --->pyanaconda/isys/iface.c:iface_ip2str() 获取系统IP
- --->nfsinstall.c:parseNfsHostPathOpts() 解析NFS的url路径
- --->拷贝kickstart文件到本地
- ################################## CD 方式 #######################################
- --->cdinstall.c:kickstartFromCD() 从光盘上获取ks文件ks.cfg
- --->kickstart.c:getKickstartFromBlockDevice()
- --->method.c:getFileFromBlockDevice()
- --->pyanaconda/isys/imount.c:doPwMount() 挂载光盘
- --->pyanaconda/isys/imount.c:mountCommandWrapper()
- --->execl("/bin/mount",...)
- ######################################################################################
- --->kickstart.c:runKickstart() 运行Kickstart
- --->导入一些python库,如pykickstart.parser
- --->getObject() 创建KickstartParser对象
- --->preprocessKickstart() 预处理,执行与pykickstart.parser.preprocessKickstart类似的任务
- --->readKickstart()
- -->PyObject_CallMethodObjArgs() 处理kickstart文件,解析各个属性并设置到parser对象上
- --->处理Kickstart数据
- --->loadKickstartModule() 载入kickstart模块
- --->setKickstartNfs() 解析NFS路径中的主机IP、路径名、及选项参数
- --->setDisplayMode() 解析安装模式(文本、命令行、或图形模式)
- --->处理其他各项数据
- --->net.c:kickstartNetworkUp() 再次启动网卡
- --->chooseNetworkInterface(loaderData) 选择并启动可使用的网卡(如果有多个网卡)
- --->writeEnabledNetInfo() 把网卡信息写入/etc/tsysconfig/network-scripts/ifcfg-DEVICE
- --->loader.c:doLoaderMain()
- --->cdinstall.c:findInstallCD() 挂载光盘,加载images/install.img等(光盘安装情况)
- --->pyanaconda/isys/imount.c:doPwMount("/mnt/source","iso9660",...)
- --->STEP_LANG和STEP_KBD 设置安装语言和键映射(若从CD/DVD安装则跳过这两步)
- --->lang.c:setLanguage()和pyanaconda/isys/lang.c:isysLoadKeymap()
- --->STEP_METHOD和STEP_DRIVER等 命令行模式下设置安装方法、驱动等(若为图形安装模式则跳过)
- --->selinux.c:loadpolicy() 加载SELinux策略
- --->execl("/sbin/load_policy",...)
- --->loader.c:spawnShell() 在tty2上打开Shell,这样在安装时可在tty2上登录
- --->open("/dev/tty2",...)
- --->execl("/bin/sh",...)
- --->execv(anacondaArgs[0], anacondaArgs) 开始运行/usr/bin/anaconda,替换当前进程
执行到/usr/bin/anaconda,就是安装程序的主体了,这是一个python脚本。可见/init程序会初始化console,/dev文件系统,mount相应的目录等等。然后调用loader程序,就开始了安装过程。loader程序中会打开log、进行network interface的配置等相关工作,如果指定了网络上的kickstart文件,他也会下载下来保存为/tmp/ks.cfg ,然后从kickstart配置文件中获取到install.img所在的位置(如光盘或者NFS上)。如果install.img是在光盘上的话,会被挂载到/mnt/source目录下。 install.img里面的anaconda程序运行所需要的很多python支持库、*.so文件等也都是在这时mount过来,一般是复制到/lib 目录下,然后配置好LD_LIBRARY_PATH环境变量,这样主安装程序运行时候就可以找到库了。当然硬盘也会被mount到一个目录下,这样安装程序才能把OS文件及各个软件包都写到这个目录去。
5、Anaconda各模块分析
从没计角度看,整个安装程序分为三层,从上层往下层依次是前端显示、调度中心、安装行为。如下图所示。
图1 Anaconda体系结构
Dispatcher类在主目录pyanaconda下的dispatch.py模块中,负责整个安装流程的控制,在安装过程中,某些安装步骤有的需要前置安装操作,有的又需要后置安装操作,而某些安装操作则是没有用户界面的,我们统称这些安装操作为无用户界面的安装操作,那么,这些没有用户界面的安装操作由谁来调度运行呢?答案就是Dispatcher。InstallControlWindow类控制安装过程中前端图形界面的显示,总体调度各个安装图形界面类,InstallControlWindow建立图形界面的主窗体,每个具体的图形安装界面可视为其子窗体。InstallControlWindow调用 Dispatcher控制整个安装流程。安装过程中,每个具体的图形安装界面均对应一个具体的类,由其对应的具体的类完成具体的安装任务,我们将这些图形界面类归为前端显示层,位于iw目录下的模块中。
anaconda将某些用户界面类中的具体安装操作分离出来,放到了另外一些模块中,这些模块放在了anaconda的主目录pyanaconda下。由Dispatcher直接调用的没有用户界面的安装步骤对应的函数也放在了anaconda的主目录下,我们将这些模块归为安装行为层。
dispatch.py模块中有一个序列(sequence)数据结构:installSteps,如下所示:
- installSteps = [
- ("language", ),
- ("keyboard", ),
- ("betanag", betaNagScreen, ),
- ("filtertype", ),
- ("filter", ),
- ("storageinit", storageInitialize, ),
- ("findrootparts", findRootParts, ),
- ("findinstall", ),
- ("network", ),
- ("timezone", ),
- ("accounts", ),
- ("setuptime", setupTimezone, ),
- ("parttype", ),
- ("cleardiskssel", ),
- ("autopartitionexecute", doAutoPartition, ),
- ("partition", ),
- ("upgrademount", upgradeMountFilesystems, ),
- ("restoretime", restoreTime, ),
- ("upgradecontinue", queryUpgradeContinue, ),
- ("upgradeswapsuggestion", upgradeSwapSuggestion, ),
-
-
-
- ]
installSteps中记录了有序排列的整个安装过程中所有可能的安装步骤,在生成具体的Dispatcher实例时,会根据安装类型制定对此进行相应裁减。installSteps中的条目(item)有两种格式,即( name )或( name, Function )。name代表安装步骤的名称,Function指安装操作的具体执行函数,这个函数会直接由Dispatcher调用。所有的安装步骤都把anaconda对象作为唯一的参数,当我们调用这个Function时,这个参数就会传进去。
我们假设当前安装步骤为autopartitionexecute,如果熟悉linux安装过程,你应该可以猜出这个安装步骤是分区方式选择,对应该安装步骤的函数为storage/partitioning.py中的doAutoPartition函数,代码片断如下:
- def doAutoPartition(anaconda):
-
-
-
- if anaconda.storage.doAutoPart:
- (disks, devs) = _createFreeSpacePartitions(anaconda)
- if disks == []:
- if anaconda.ksdata:
- msg = _("Could not find enough free space for automatic "
- "partitioning. Press 'OK' to exit the installer.")
- else:
- msg = _("Could not find enough free space for automatic "
- "partitioning, please use another partitioning method.")
-
- anaconda.intf.messageWindow(_("Error Partitioning"), msg,
- custom_icon='error')
-
- if anaconda.ksdata:
- sys.exit(0)
-
- anaconda.storage.reset()
- return DISPATCH_BACK
-
- _schedulePartitions(anaconda, disks)
-
-
- log.warning("not sanity checking devices because I don't know how yet")
-
-
-
- try:
- doPartitioning(anaconda.storage
- if anaconda.storage.doAutoPart:
- _scheduleLVs(anaconda, devs)
-
-
- growLVM(anaconda.storage)
- except PartitioningWarning as msg:
- if not anaconda.ksdata:
- anaconda.intf.messageWindow(_("Warnings During Automatic "
- "Partitioning"),
- _("Following warnings occurred during automatic "
- "partitioning:\n\n%s") % (msg,),
- custom_icon='warning')
- else:
- log.warning(msg)
- except PartitioningError as msg:
-
- anaconda.storage.reset()
- if not anaconda.ksdata:
- extra = ""
-
- if anaconda.displayMode != "t":
- anaconda.dispatch.skipStep("partition", skip = 0)
- else:
- extra = _("\n\nPress 'OK' to exit the installer.")
- anaconda.intf.messageWindow(_("Error Partitioning"),
- _("Could not allocate requested partitions: \n\n"
- "%(msg)s.%(extra)s") % {'msg': msg, 'extra': extra},
- custom_icon='error')
-
- if anaconda.ksdata:
- sys.exit(0)
- else:
- return DISPATCH_BACK
-
-
- (errors, warnings) = anaconda.storage.sanityCheck()
- if warnings:
- for warning in warnings:
- log.warning(warning)
- if errors:
- errortxt = "\n".join(errors)
- if anaconda.ksdata:
- extra = _("\n\nPress 'OK' to exit the installer.")
- else:
- extra = _("\n\nPress 'OK' to choose a different partitioning option.")
-
- anaconda.intf.messageWindow(_("Automatic Partitioning Errors"),
- _("The following errors occurred with your "
- "partitioning:\n\n%(errortxt)s\n\n"
- "This can happen if there is not enough "
- "space on your hard drive(s) for the "
- "installation. %(extra)s")
- % {'errortxt': errortxt, 'extra': extra},
- custom_icon='error')
-
-
-
- if anaconda.ksdata:
- anaconda.intf.messageWindow(_("Unrecoverable Error"),
- _("The system will now reboot."))
- sys.exit(0)
- anaconda.storage.reset()
- return DISPATCH_BACK
主要执行的步骤包括创建空闲分区,运行autopart分配或增长分区容量,分区完整性检查等。
(1) disptach.py: 下面我们看一下Dispatcher类的主要接口。
1)gotoNext & gotoPrev:这两个接口分别从当前安装步骤前进(后退)到下一个(上一个)具有用户界面的安装步骤,在图形界面安装模式下,由InstallControlWindow类调用,在字符模式下,由InstallInterface类(在text.py和cmdline.py中)调用。这两个函数只是简单的设置安装方向,然后调用moveStep函数,其核心操作是moveStep。
2)moveStep:我们来重点分析movestep函数,代码如下:
- def moveStep(self):
- if self.step == None:
- self.step = self.firstStep
- else:
- if self.step >= len(installSteps):
- return None
-
- log.info("leaving (%d) step %s" %(self._getDir(), installSteps[self.step][0]))
- self.step = self.step + self._getDir()
-
- if self.step >= len(installSteps):
- return None
-
- while self.step >= self.firstStep and self.step < len(installSteps) \
- and (self.stepInSkipList(self.step) or self.stepIsDirect(self.step)):
-
- if self.stepIsDirect(self.step) and not self.stepInSkipList(self.step):
- (stepName, stepFunc) = installSteps[self.step]
- log.info("moving (%d) to step %s" %(self._getDir(), stepName))
- log.debug("%s is a direct step" %(stepName,))
- rc = stepFunc(self.anaconda)
- if rc in [DISPATCH_BACK, DISPATCH_FORWARD]:
- self._setDir(rc)
- log.info("leaving (%d) step %s" %(self._getDir(), stepName))
-
-
- self.step = self.step + self._getDir()
- if self.step == len(installSteps):
- return None
-
- if (self.step < 0):
-
- self.step = 0
- while self.skipSteps.has_key(installSteps[self.step][0]):
- self.step = self.step + 1
- elif self.step >= len(installSteps):
- self.step = len(installSteps) - 1
- while self.skipSteps.has_key(installSteps[self.step][0]):
- self.step = self.step - 1
- log.info("moving (%d) to step %s" %(self._getDir(), installSteps[self.step][0]))
我们重点看一下程序while循环体,首先看一下循环条件:当下一个安装步骤是合法的,即在第一个安装步骤和最后一个安装步骤之间,并且(and)该步骤被跳过或者该步骤是一个无用户界面的安装步骤,即installSteps的条目的第二个元素是一个function,则进入循环体。进入循环后,Dispatcher直接调用该函数stepFunc执行安装操作。如果下一个安装步骤依然无用户界面,则步数加一向前,继续循环,直到下一个没有被跳过的具有用户界面的安装步骤,对于图形安装模式,Dispatcher将控制权交给guid.py中的InstallControlWindow,对于字符安装模式,Dispatcher将控制权交给InstallInterface。如果安装过程完成则退出循环。
3)currentStep:Dispatcher类的另一个主要接口,取得当前的安装步骤及其相关信息返回给调用者。在图形安装模式下,该函数主要在InstallControlWindow调度图形用户界面类时调用,在字符模式下,主要在InstallInterface调度字符用户界面时调用,这两个类通过该接口取得当前安装步骤的用户界面对应类及创建该用户界面类的实例所需的信息。
- def currentStep(self):
- if self.step == None:
- self.gotoNext()
- elif self.step >= len(installSteps):
- return (None, None)
-
- stepInfo = installSteps[self.step]
- step = stepInfo[0]
-
- return (step, self.anaconda)
另外,Dispatcher类的主要接口还有skipStep(self, stepToSkip, skip = 1, permanent = 0)是跳过安装步骤的函数。setStepList(self, *steps)是安装步骤设置函数,主要由安装类型实例调用,每个安装类型会根据自身的特点设置安装步骤。这些接口的实现逻辑都比较简单,这里不一一给出分析了。
(2)gui.py: 核心是字符安装模式的InstallInterface类和图形安装模式InstallControlWindow类的实现。看InstallControlWindow中的接口。
1)数据结构stepTopClass: 该字典中记录了安装过程中所有的具有图形用户界面的安装步骤。
- stepToClass = {
- "language" : ("language_gui", "LanguageWindow"),
- "keyboard" : ("kbd_gui", "KeyboardWindow"),
- "filtertype" : ("filter_type", "FilterTypeWindow"),
- "filter" : ("filter_gui", "FilterWindow"),
- "zfcpconfig" : ("zfcp_gui", "ZFCPWindow"),
- "partition" : ("partition_gui", "PartitionWindow"),
- "parttype" : ("autopart_type", "PartitionTypeWindow"),
- "cleardiskssel": ("cleardisks_gui", "ClearDisksWindow"),
- "findinstall" : ("examine_gui", "UpgradeExamineWindow"),
- "addswap" : ("upgrade_swap_gui", "UpgradeSwapWindow"),
- "upgrademigratefs" : ("upgrade_migratefs_gui", "UpgradeMigrateFSWindow"),
- "bootloader": ("bootloader_main_gui", "MainBootloaderWindow"),
- "upgbootloader": ("upgrade_bootloader_gui", "UpgradeBootloaderWindow"),
- "network" : ("network_gui", "NetworkWindow"),
- "timezone" : ("timezone_gui", "TimezoneWindow"),
- "accounts" : ("account_gui", "AccountWindow"),
- "tasksel": ("task_gui", "TaskWindow"),
- "group-selection": ("package_gui", "GroupSelectionWindow"),
- "install" : ("progress_gui", "InstallProgressWindow"),
- "complete" : ("congrats_gui", "CongratulationWindow"),
- }
每一个条目从左到右依次是安装步骤名称、图形界面类所在模块,图形界面类的名称。如language为安装步骤名称,language_gui为该步骤对应的图形界面类所在模块language_gui.py,LanguageWindow为图形界面对应的类名。
2)run: 启动图形安装界面的入口函数。该函数调用了setup_window接口,该接口调用gtk"绘制"图形安装界面的主窗体,然后控制权交给了gtk。
- def run (self):
- self.setup_theme()
- self.setup_window(False)
- gtk.main()
3)nextClicked & prevClicked:这两个接口分别执行从当前图形安装界面向前(向后)到下一个图形安装界面的操作,我们可以想象安装过程中当用户点击"下一步" 或"上一步"按钮时,这两个函数被调用。这两个函数首先调用主流程控制Dispatcher实例向前(向后)前进到下一个图形安装界面,然后调用setScreen函数设置图形界面。
- def prevClicked (self, *args):
- try:
- self.currentWindow.getPrev ()
- except StayOnScreen:
- return
-
- self.anaconda.dispatch.gotoPrev()
- self.setScreen ()
-
- def nextClicked (self, *args):
- try:
- rc = self.currentWindow.getNext ()
- except StayOnScreen:
- return
-
- self.anaconda.dispatch.gotoNext()
- self.setScreen ()
- 4)setScreen: 用于设置图形界面。代码如下:
- def setScreen (self):
-
- (step, anaconda) = self.anaconda.dispatch.currentStep()
- if step is None:
- gtk.main_quit()
- return
-
- if not stepToClass[step]:
- if self.anaconda.dispatch.dir == DISPATCH_FORWARD:
- return self.nextClicked()
- else:
- return self.prevClicked()
-
- (file, className) = stepToClass[step]
- newScreenClass = None
-
- while True:
- try:
- found = imp.find_module(file, iw.__path__)
- moduleName = 'pyanaconda.iw.%s' % file
- loaded = imp.load_module(moduleName, *found)
- newScreenClass = loaded.__dict__[className]
- break
- except ImportError, e:
- stdout_log.error("loading interface component %s" % className)
- stdout_log.error(traceback.format_exc())
- win = MessageWindow(_("Error!"),
- _("An error occurred when attempting "
- "to load an installer interface "
- "component.\n\nclassName = %s")
- % (className,),
- type="custom", custom_icon="warning",
- custom_buttons=[_("_Exit"),
- _("_Retry")])
- if not win.getrc():
- msg = _("The system will now reboot.")
- buttons = [_("_Reboot")]
-
- MessageWindow(_("Exiting"),
- msg,
- type="custom",
- custom_icon="warning",
- custom_buttons=buttons)
- sys.exit(0)
-
- ics = InstallControlState (self)
-
- ics.setPrevEnabled(self.anaconda.dispatch.canGoBack())
- self.destroyCurrentWindow()
- self.currentWindow = newScreenClass(ics)
-
- new_screen = self.currentWindow.getScreen(anaconda)
-
-
-
-
-
- if not new_screen:
- if self.anaconda.dispatch.dir == DISPATCH_FORWARD:
- self.anaconda.dispatch.gotoNext()
- else:
- self.anaconda.dispatch.gotoPrev()
-
- return self.setScreen()
-
- self.update (ics)
-
- self.installFrame.add(new_screen)
- self.installFrame.show_all()
-
- self.currentWindow.focus()
-
- self.handle = gobject.idle_add(self.handleRenderCallback)
-
- if self.reloadRcQueued:
- self.window.reset_rc_styles()
- self.reloadRcQueued = 0
前面的nextClicked和prevClicked函数已经通过Dispatcher将要进行的安装步骤标记为当前安装步骤,所以该函数首先通过Dispatcher的currentStep从Dispatcher的数据结构installSteps中取得当前安装步骤名称及相关信息,接下来,做了一下判断,如果Dispatcher的当前安装步骤不在字典stepToClass中,则忽略该步骤,调用nextClicked或prevClicked继续下一个图形界面安装步骤,直到下一个步骤在字典stepToClass中。验证通过后,从字典stepToClass中取得当前图形安装界面对应的类及该类所在模块,然后导入该模块并创建图形安装界面的实例,销毁前一个图形安装界面,并将新创建的图形界面实例置为当前安装界面,调用图形安装界面实例的getScreen函数生成该安装步骤的图形用户界面,然后显示。
至此,InstallControlWindow的主要逻辑已经分析完了,接下来涉及每个具体安装界面及其安装操作读者可以到iw目录下逐个深入分析。
(3)anaconda主程序: 图形环境运行是建立在X Server基础上的,对于图形模式,anaconda需要先运行X服务器,然后启动图形模式安装过程。而对于字符模式,anaconda的主执行体就作了一件事,启动字符模式安装过程。
- if __name__ == "__main__":
- setupPythonPath()
-
-
-
-
- (opts, args) = parseOptions()
- from pyanaconda.flags import flags
- if opts.images:
- flags.imageInstall = True
-
-
- import logging
- from pyanaconda import anaconda_log
- anaconda_log.init()
-
- log = logging.getLogger("anaconda")
- stdoutLog = logging.getLogger("anaconda.stdout")
-
-
-
- from pyanaconda import Anaconda
- anaconda = Anaconda()
- warnings.showwarning = AnacondaShowWarning
- iutil.setup_translations(gettext)
-
-
-
-
- check_memory(anaconda, opts, 't')
-
- if opts.unsupportedMode:
- stdoutLog.error("Running anaconda in %s mode is no longer supported." % opts.unsupportedMode)
- sys.exit(0)
-
-
-
-
- if opts.ksfile:
- kickstart.preScriptPass(anaconda, opts.ksfile)
- anaconda.ksdata = kickstart.parseKickstart(anaconda, opts.ksfile)
- opts.rescue = opts.rescue or anaconda.ksdata.rescue.rescue
-
-
-
- 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'
-
-
-
- if anaconda.displayMode == 'g' and not flags.preexisting_x11 and not flags.usevnc:
- try:
-
-
-
- def sigchld_handler(num, frame):
- raise OSError(0, "SIGCHLD caught when trying to start the X server.")
-
- def sigusr1_handler(num, frame):
- log.debug("X server has signalled a successful start.")
-
- 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",
- "-noreset"],
- close_fds=True, stdout=xout, stderr=xout,
- preexec_fn=preexec_fn)
-
- signal.pause()
-
- os.environ["DISPLAY"] = ":1"
- doStartupX11Actions()
- except (OSError, RuntimeError) as e:
- 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)
-
- set_x_resolution(opts.runres)
-
-
-
-
- anaconda.initInterface()
- anaconda.instClass.configure(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
主要工作包括引用模块路径设置、参数解析、设置log、内存检测、安装类型设置,然后调用pyanaconda/__init__.py::Anaconda类创建主执行体实例anaconda,接着解析kickstart文件,调用/usr/bin/Xorg(位于解开后的install.img中)程序启动X server,调用Anaconda类的initInterface()初始化界面,调用intf(是InstallInterface类的实例)的run()启动安装过程。对于字符安装模式,是直接调用InstallInterface实例的run接口。而对于图形安装模式,则是由InstallInterface实例的run接口间接的调用installcontrolwindow实例的run接口,从而启动图形界面。
(4)pyanaconda/__init__.py: 里面有Anaconda类,负责具体的启动安装过程。前面说过,安装的流程由Dispatcher控制,对于图形模式,图形模式的前端显示及与用户的交互由InstallControlWindow调度,而字符模式的前端显示层由InstallInterface调度。因此,启动安装过程,实际就是创建主要控制类的实例,调用实例的接口,启动安装过程,然后再由这几个主要的控制类的实例创建具体安装界面,创建安装行为类的实例,调用具体的函数完成具体的安装过程。
- class Anaconda(object):
- def __init__(self):
- import desktop, dispatch, firewall, security
- import system_config_keyboard.keyboard as keyboard
- from flags import flags
-
-
-
- self.dispatch = dispatch.Dispatcher(self)
-
-
-
-
- intf = property(_getInterface, _setInterface, _delInterface)
-
-
-
- def initInterface(self):
- if self._intf:
- raise RuntimeError, "Second attempt to initialize the InstallInterface"
-
-
- if self.displayMode == 'g':
- stdoutLog.info (_("Starting graphical installation."))
-
- try:
- from gui import InstallInterface
- except Exception, e:
- from flags import flags
- stdoutLog.error("Exception starting GUI installer: %s" %(e,))
-
-
- 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
主要的工作包括创建dispatch实例,初始化界面,创建InstallInterface实例,它最后会创建InstallControlWindow实例,生成图形界面。
整个Anaconda的运行流程如下图:
图2 Anaconda运行流程