yum研究笔记

yum(http://linux.duke.edu/projects/yum/)是Fedora平台上默认安装的、最好用的系统升级工具。但是自从公司安装了防火墙之后,yum总是出现以下错误:
"[Errno -1] Header is not complete."

首先说明一下我的平台信息:Fedora Core 5, Python和yum都是安装系统时自带的, 版本分别是2.4和2.6.1。

yum的FAQ以及邮件列表都说了:这不是yum的错,而是某个透明代理的错。我抓包也证实了这一点:yum发出的HTTP GET请求使用的是HTTP 1.1,并且有HTTP 1.1特有的一个Range头部域,如"Range: bytes=440-6633";而我的透明代理(即公司的防火墙)返回的响应是HTTP1.0, 没有"Range"头部域--自然yum罢工了。用国内的镜像库行不通,因为还是得被公司的防火墙干掉。(后来发现公司的防火墙不支持HTTP Report方法,导致svn下载"http://"这类URL的库不能成功,但"svn://"和"https://"这类URL没有问题。例如svn checkout http://scm.sipfoundry.org/rep/sftf。)


yum的FAQ对此问题给了如下几招:
The solutions to this problem are:
   1.      Get your proxy software/firmware updated so that it properly implements HTTP 1.1
   2.      Use an FTP repository, where byte ranges are more commonly supported by the proxy
   3.      Create a local mirror with rsync and then point your yum.conf to that local mirror
   4.      Don't use yum

办法1行不通,我没权利升级公司防火墙。就因为linux系统要升级而升级公司防火墙--绝大多数系统维护人员肯定不干,包括我这公司的。
办法3行不通,公司的防火墙并没有允许rsync这个端口的对外连接。

按照办法2的指示,我决定采用FTP repository。/etc/yum.repos.d/目录下有好几个.repo文件,但从各个库描述的enabled设置来看,起作用的只有fedora-core.repo,fedora-updates.repo,fedora-extras.repo中的[core], [updates], [extras]三个库。先把 /etc/yum.repos.d/下所有的.repo文件转移到其他目录备份好,再自己写了一个.repo文件(名字任意),它在原来的[core], [updates], [extras]三个库配置基础上做了修改:

[core]
name=Fedora Core $releasever - $basearch
#baseurl=http://download.fedora.redhat.com/pub/fedora/linux/core/$releasever/$basearch/os/
#mirrorlist=http://fedora.redhat.com/download/mirrors/fedora-core-$releasever
baseurl=ftp://ftp.wsisiz.edu.pl/mirror/download.fedora.redhat.com/linux/core/$releasever/$basearch/os
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora file:///etc/pki/rpm-gpg/RPM-GPG-KEY

[updates]
name=Fedora Core $releasever - $basearch - Updates
#baseurl=http://download.fedora.redhat.com/pub/fedora/linux/core/updates/$releasever/$basearch/
#mirrorlist=http://fedora.redhat.com/download/mirrors/updates-released-fc$releasever
baseurl=ftp://ftp.wsisiz.edu.pl/mirror/download.fedora.redhat.com/linux/core/updates/$releasever/$basearch/
enabled=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora

[extras]
name=Fedora Extras $releasever - $basearch
#baseurl=http://download.fedora.redhat.com/pub/fedora/linux/extras/$releasever/$basearch/
#mirrorlist=http://fedora.redhat.com/download/mirrors/fedora-extras-$releasever
baseurl=ftp://ftp.wsisiz.edu.pl/mirror/download.fedora.redhat.com/linux/extras/$releasever/$basearch/
enabled=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-extras
gpgcheck=1

在上述配置中,被注释掉的baseurl和mirrorlist是原来的配置。为了调试方便,我注释掉了mirrorlist。(其实,baseurl可以指定多个URL:每个URL位于以TAB键开始的一行。)3个库都位于ftp.wsisiz.edu.pl这个镜像站点。要注意的是不同的mirror有不同的布局,甚至缺少上述3个库中的某几个库。所以改用其他mirror并不只是改baseurl中的服务器域名那么简单,要自己登录这个FTP服务器验证URL的有效性。一个有效的baseurl的特征是:baseurl所指位置存在名repodata目录,此目录下又存在名为repomd.xml文件。baseurl中的$releasever和$basearch两个变量分别表示本机的Fedora发布版本号和基本硬件平台,对于X86平台的FC5而言分别是5和i386。配置方面的疑问,查man帮助即可。

上述配置后,yum还是出错,并且总是在下载第一个库的repodata/repomd.xml文件时出现"111 conection refused"。但我用NcFTP这个交互式FTP客户端是可以下载成功的。通过比较了wget/kget/cURL/NcFTP/yum的FTP下载行为,我有如下总结:
(1)PORT/PASV模式问题(下载同一文件ftp://ftp.freshrpms.net/pub/freshrpms/ayo/fedora/linux/5/i386/freshrpms/repodata/repomd.xml)
以上FTP客户端模式采用PASV模式。wget,cUrl,yum在PASV模式遭遇“拒绝连接”错误后终止了。而kget,NcFTP能切换模式(KGet:PASV->EPSV->EPRT, NcFTP: PASV->PORT),所以下载能成功。
(2)目录层次问题(下载同一文件ftp://ftp.wsisiz.edu.pl/mirror/download.fedora.redhat.com/linux/core/5/i386/os/repodata/repomd.xml)
wget,KGet登录FTP服务器后,一个CWD命令直接进入文件所在目录;而yum则是逐层进入文件所在目录。
ftp.wsisiz.edu.pl上的mirror是个软链接,所以CWD命令直接进入文件所在目录失败了。
urllib.py中的 class ftpwrapper,函数 init()最后执行了逐层进入目录。

查man帮助得知yum是Python写的。于是我决定研究一下它的代码,一是为了搞定问题,二是为了提高Python水平。又因为是Python写的,所以瞎折腾不会有啥严重后果,即使有也好恢复。我从网上download了yum-3.1.3的包,多次修改、安装,发现其安装后的大致结构如下:

1)/usr/bin/yum是yum的入口。它实际上是个Python脚本,首行表明以/usr/bin/python解释它。如果系统上安装了多个版本的Python解释器,要注意一下它对应的解释器是哪一个。它只是import yummain并且执行其main()函数。
2)/usr/share/yum-cli/下对yum的所有命令提供了支持。其中最重要的文件是yummain.py,它定义了main()函数。
3)剩下的就是安装到Python解释器site-packages中的rpmUtils和yum两个目录。
3.1)rpmUtils用于从rpm中提取版本、依赖关系等特征。rpmUtils依赖于site-packages中的rpm目录,这是rpm的Python绑定。它属于Redhat的一个名为rpm-python的项目,在Fedora上一般安装了。
3.2)下载文件由yum/yumRepo.py中的YumRepository::__get()方法实现。而此方法由依赖于一个名为urlgrabber的模块(位于site-packages)提供的URLGrabber::urlgrab()来做实际下载:

from urlgrabber.grabber import URLGrabber

    def __get(self, url=None, relative=None, local=None, start=None, end=None,
            copy_local=0, checkfunc=None, text=None, reget='simple', cache=True):
        if url:
            (scheme, netloc, path, query, fragid) = urlparse.urlsplit(url)
        if url is not None and scheme != "media":
            ug = URLGrabber(keepalive = self.keepalive,
                            bandwidth = self.bandwidth,
                            retry = self.retries,
                            throttle = self.throttle,
                            progress_obj = self.callback,
                            copy_local = copy_local,
                            reget = reget,
                            proxies = self.proxy_dict,
                            failure_callback = self.failure_obj,
                            interrupt_callback=self.interrupt_callback,
                            timeout=self.timeout,
                            checkfunc=checkfunc,
                            http_headers=headers,
                            )

            remote = url + '/' + relative

            try:
                result = ug.urlgrab(remote, local,
                                    text=text,
                                    range=(start, end),
                                    )
            except URLGrabError, e:
                raise Errors.RepoError, /
                    "failed to retrieve %s from %s/nerror was %s" % (relative, self.id, e)

urlgrabber主要是针对yum的网络下载需要而开发的,但是具有通用性。它是yum的一个附属项目,主页http://linux.duke.edu/projects/urlgrabber/。

显然,FTP下载未能切换PORT/PASV模式,这是urlgrabber的责任。于是我下载了urlgrabber-3.1.0。通过一番研究,发现FTP下载都是通过urlgrabber/byterange.py文件中的ftpwrapper.retrfile()函数来做的。这儿的ftpwrapper实际上是包装了标准库urllib中的ftpwrapper以提供range支持。而标准库urllib和urllib2中的FTP操作都是基于标准库ftplib。

byterange.py文件中的ftpwrapper.retrfile()函数片段如下:

       if file and not isdir:
           # Use nlst to see if the file exists at all
           try:
               self.ftp.nlst(file)
           except ftplib.error_perm, reason:
               raise IOError, ('ftp error', reason), sys.exc_info()[2]

通过单步调试(我用winpdb)发现总是在self.ftp.nlst(file)处抛出socket.error异常。socket.error与ftplib.error_perm没有交集,都属于ftplib.all_errors。从网络抓报来看,恰好是发出PASV命令之后。从FTP规范上讲:客户端为了获得文件列表(NLST命令。由于建立数据连接失败,所以没有看到这个命令),先发出PASV命令使服务器处于被动模式,然后尝试建立数据连接,但服务器没有响应这个TCP连接,所以Python的ftplib库nlst()抛出socket.error异常,原因是'connection refused'。

Python2.5文档指出ftplib数据模式默认为PASV。为了捕获这个异常并转到PORT模式,我将上述代码改为(注意:要修改的文件是site-packages/urlgrabber/中的那个byterange.py):
       if file and not isdir:
            # Use nlst to see if the file exists at all
            try:
                self.ftp.nlst(file)
            except socket.error:
                try:
                    # Fall back to PORT instead of PASV mode
                    self.ftp.set_pasv(False)
                    self.ftp.nlst(file)
                except ftplib.error_perm, reason:
                    raise IOError, ('ftp error', reason), sys.exc_info()[2]
            except ftplib.error_perm, reason:
                raise IOError, ('ftp error', reason), sys.exc_info()[2]


我写了个小程序(暂时称为pftpget.py)验证效果,它基于上述ftpwrapper。

#!/usr/bin/env python
import urllib2
import urlgrabber
from urlgrabber.byterange import *
range_handler = FTPRangeHandler()
opener = urllib2.build_opener(range_handler)
# install it
urllib2.install_opener(opener)
# create Request and set Range header
req = urllib2.Request('ftp://ftp.freshrpms.net/pub/freshrpms/ayo/fedora/linux/5/i386/freshrpms/repodata/repomd.xml')
f = urllib2.urlopen(req)

下载成功!从抓包来看,pftpget.py在PASV模式失败后切换到了PORT模式。

在以上尝试过程中,发现yum把sys.stdout和sys.stderr重定向了,所以yum莫名奇妙退出时根本不知道异常何处抛出的,即使在抛出点print异常信息也是显示不到屏幕上的。我查看/usr/share/yum-cli/yummain.py发现有3个try_except语句没有捕获全部异常。我给这几个语句最后都添加一个不带表达式的except子句以捕获其他任何异常,然后利用标准库trackback把捕获的异常信息存储到一个文本文件中。例如:

    try:
        result, resultmsgs = do()
    except plugins.PluginYumExit, e:
        exPluginExit(e)
...
    # I add this except to catch all other exceptions and store into a text file.
    except:
        base.log(2, 'uncaught exception, see /home/kenny/traceback.txt/n')
        import traceback
        f = open('/home/kenny/traceback.txt','w')
        traceback.print_exc(file = f)
        f.flush()
        f.close()
        sys.exit(3)

我发现了修改byterange.py的怪现象:上面修改后yum正常运行了。我改回成不捕获socket.error,再复原,发现yum还是在进入PASV模式时发生异常,却没有被我的except捕获(没有记录到我的traceback日志文件)。后来发现这时yum用的ftpwrapper不是urlgrabber中的那个,而是Python标准库urllib中的那个。我对urllib.py里的ftpwrapper.retrfile()做与byterange.py里的ftpwrapper.retrfile()完全相同的修改之后,问题解决了。这时的yum没有给urllib2安装FtpRangeHandler,我认为这是yum的一BUG。我检查了urlgrabber/grabber.py中导入range支持的代码,HTTPRangeHandler和FTPRangeHandler的import都是成功的(ImportError并没有发生)。究竟为什么这时yum不用FTPRangeHandler?我没有深究下去。

总结一下,要使yum转起来,要做如下修改:
(1)修改/etc/yum.repos.d/目录下的配置文件,使得yum采用FTP库。
(2)修改2处python脚本,使得FTP建立PASV模式数据连接失败时能切换到PORT模式(注:FC5/6系统的python版本都是2.4)。分别是:/usr/lib/python2.4/site-packages/urlgrabber/byterange.py文件中的ftpwrapper.retrfile()函数,/usr/lib/python2.4/urllib.py文件中的ftpwrapper.retrfile()函数。

哈哈!yum终于转起来了!

[附录1] 推荐几个较好的mirror
Fedora项目页面http://fedora.redhat.com/download/mirrors.html列出了许多mirror。有的URL连接不上,有的布局与http://download.fedora.redhat.com/pub/fedora/linux/不一致,有的缺少[extras]库。
以下是几个经我验证的,库完整且速度较快的FTP Server。它们的baseurl前半截:
ftp://rpmfind.net/linux/fedora/
ftp://fr.rpmfind.net/linux/fedora/
ftp://ftp.wsisiz.edu.pl/mirror/download.fedora.redhat.com/linux/
对[core]库,追加core/$releasever/$basearch/os/
对[updates],追加core/updates/$releasever/$basearch/
对[extras],追加extras/$releasever/$basearch/

freshrpms.net也是一个常用的RPM源,对上述Fedora源作了补充(如xmms的mp3和wma插件)。此yum源(安装方法见其主页)默认提供的全是HTTP库。我找到了此源的2个FTP库:
ftp://ftp.freshrpms.net/pub/freshrpms/ayo/fedora/linux/$releasever/$basearch/freshrpms/
ftp://ayo.pt.freshrpms.net/pub/freshrpms/ayo/fedora/linux/$releasever/$basearch/freshrpms/
可以注释掉freshrpms.repo中原来的baseurl和mirrorlist,将上述两FTP库之一作为baseurl。

rpm.livna.org收录了一些由于某些原因不能纳入Fedora源的rpm(如mplayer, vlc, nvidia驱动等)。此yum源的FTP库为:
ftp://rpm.livna.org/pub/rpm.livna.org/fedora/$releasever/$basearch/

到目前为止,我还没有找到位于国内公众网的yum源镜像,国内源镜像都位于教育网内的若干大学服务器。

[附录2] yum几个使用技巧
1. 安装指定的共享库
有时遇到缺少某个共享库(例如libstdc++.so.5)的情况,之前我总是在rpmfind.net上查找对应的rpm,然后用yum安装这个rpm。实际上有更简单直接的办法,就是直接用yum安装这个共享库:
[root@localhost kenny]# yum -y install libstdc++.so.5
yum会查找出这个so包含在compat-libstdc++-33这个rpm中,并下载和安装这个rpm。
2. 同时操作多个包
2.1 成组操作
yum的grouplist列出已安装和未安装的组; groupinfo/groupinstall/groupupdate/groupremove  group1 [group2] [...] 分别用于查询、安装、升级、删除指定的组。例如yum groupinstall "Web Server"安装了Fedora光盘上"Web Server"组的所有软件。并不是只有Fedora镜像yum库才有组的概念,在yum库的repodata/comps.xml可以创建自己的组。
2.2 使用通配符
包名字中可以包含通配符'*',达到同时操作所有匹配的rpm的目的。例如:
[root@localhost kenny]# yum -y --exclude=i686 update kernel*
升级了kernel,kernel-devel,kernel-headers这3个包。exclude选项用于排除针对i686的包,否则可能出现这样的情况:kernel是i586的,而kernel-devel却是i686的,导致编译、插入Linux设备驱动不能成功。
3. 自动执行系统升级
我编写了一个Python脚本,每天上午8点半升级,捕获yum的输出信息并Email给我。其实,有个称为yum-updatesd的服务(可用yum安装它)做得更好,可以配置检查间隔、通知方式、是否自动下载和安装等等。日志文件/var/log/yum.log记录了软件安装、升级信息。
4. yum的GUI前端
有yumex和kyum等,均要求事先配置好yum。不过我不喜欢用这些东西,大多数时候GUI不如命令行简洁、方便。

[附录3] FTP规范总结
FTP协议跟一般的TCP/IP协议有一点不同, 就是它要建立两个TCP连接(别的通常只要一个就行了):一个控制连接,一个数据连接。
其中数据连接又分两种,即由服务器请求连接,还是由客户端请求。前者称为主动模式(PORT模式),后者称为被动模式(PASV
模式)。
关于主动连接(PORT模式),参考TCP/IP红宝书:
对每一个文件传输(RETR命令)或目录列表(NLST命令)来说都要建立一个全新的数据连接。其一般过程如下:
1)正由于是客户发出命令要求建立数据连接,所以数据连接是在客户的控制下建立的。
2)客户通常在客户端主机上为所在数据连接端选择一个临时端口号。客户从该端口发布一个被动的打开。
3)客户使用PORT命令从控制连接上把端口号发向服务器。
"PORT 10,0,15,120,220,103",前4段是客户端IPv4地址,后两段是客户端的数据端口号的二进制表示,220*256+103=56423。
"EPRT |1|10.0.15.120|43358|",最后一段是客户端的数据端口号,即43358。
4)服务器在控制连接上接收端口号,并向客户端主机上的端口发布一个主动的打开。服务器的数据连接端一直使用端口20。
被动连接(PASV模式)较新,所以一些老的经典书籍上没有介绍。这方面的介绍散见于网上的文章中。总结一下:
建立PASV模式的数据连接的过程:
1) 客户发出PASV或EPSV命令。
2) 服务器为数据连接选择一个空闲端口,打开并监听。在控制连接上返回的响应包含了这个端口号。
3) 客户为数据连接选择一个临时端口号,主动连接到上述响应中包含的服务器端口号。
小结一下:
客户端有防火墙/代理/内网,采用port模式会有问题,一般要用pasv模式。这也是FlashFXP等客户程序的默认模式。
现在的多数FTP站点默认的是被动连接,反倒是主动连接比较少用了。
服务端有防火墙/代理/内网,采用pasv模式会有问题,一般要用port模式。客户程序要在设置上作对应修改,这成了特殊情况了,
当然从FTP协议的历史发展来看,主动连接是较先采用的规范。

[附录4] TCP连接建立与断开的步骤
为分析报文方便,把TCP连接建立与断开的步骤也总结如下:
1) 三阶段握手:客户端发[SYN] ,服务器收到后发[SYN ACK],客户端再发[ACK]。
2)客户端或服务器发[FIN],另一端收到后发[ACK]。

[附录5] 走过的弯路--winpdb的误导
我是在winpdb中做“修改-单步调试“的。winpdb曾经在我调试如下片段时误导了我。本以为这是winpdb的BUG,后来发现是我错了。

       if file and not isdir:
            # Use nlst to see if the file exists at all
            try:
                self.ftp.nlst(file)
            except ftplib.error_perm, reason: #1
                try:
                    # Fall back to PORT instead of PASV mode
                    self.ftp.set_pasv(False)
                    self.ftp.nlst(file)
                except ftplib.error_perm, reason: #2
                    raise IOError, ('ftp error', reason), sys.exc_info()[2] #3

winpdb单步执行代码时总是高亮度显示当前行。nlst()函数抛出socket.error异常后,先是行#1高亮,然后是行#3高亮,所以我以为这个行#3的raise语句被执行了(导致我认为以为ftplib.error_perm包括了socket.error)。实际上是:行#3高亮时,此行最左边有个字符'R'。字符 'R'是指执行点在当前行之右,当然就不会执行当前行了。winpdb在函数、复合语句结束时都会高亮显示此语句块最后一行并在此行最左边出现字符'R'。字符'L'表明执行点在当前行之左,也就是说接下来将执行当前行。字符'E'表明有未处理的异常。

[附录6] rsync基本用法
可以用rsync(http://rsync.samba.org/,最优秀的同步备份工具,For Windows的有免费的cwRsync)给Fedora的3个库建个本地镜像。http://fedora.redhat.com/download/mirrors.html列出的镜像中有几个rsync源,例如rsync://rpmfind.net/linux/fedora/core/。
rsync很好用,看看它的主页就差不多了。在远程启动rsync服务器(默认端口TCP 873),在本地执行一个命令就把服务器内容同步到本地的mirror目录(这些服务器都没有配置SSH验证):
[kenny@kenny ~]$ rsync -avzp rsync://rpmfind.net/linux/fedora/core/ mirror/

你可能感兴趣的:(python,FTP服务器,防火墙,File,服务器,import)