当前位置: 首页 >> 开源操作系统 >> yum研究笔记
 

yum研究笔记

作者:      来源:http://blog.csdn.net/kenny_yu     发表时间:2007-03-12     浏览次数:      字号:    

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(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/
但公司的防火墙并没有允许这个端口的对外连接,所以这个办法也行不通。

按照办法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。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()最后执行了逐层进入目录。

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协议的历史发展来看,主动连接是较先采用的规范。

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

查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两个目录。
rpmUtils用于从rpm中提取版本、依赖关系等特征。rpmUtils依赖于site-packages中的rpm目录,这是rpm的Python绑定。它属于Redhat的一个名为rpm-python的项目,在Fedora上一般安装了。
下载文件最终是通过调用一个urlgrabber外部模块(位于site-packages)来做的。请看yum/yumRepo.py中的YumRepository.__get()方法:

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异常。从网络抓报来看,恰好是发出PASV命令之后。从FTP规范上讲:客户端为了获得文件列表(NLST命令。由于建立数据连接失败,所以没有看到这个命令),先发出PASV命令使服务器处于被动模式,然后尝试建立数据连接,但服务器没有响应这个TCP连接,所以Python的ftplib库nlst()抛出socket.error异常,原因是'connection refused'。

为了捕获这个异常并转到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.err_perm, reason:
                    raise IOError, ('ftp error', reason), sys.exc_info()[2]
            except ftplib.err_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,发现还是不行:yum老是有异常退出,从抓包来看仍然是在进入PASV模式时,从输出看不到异常的trackback信息。(这是我走的弯路。如果以上操作正确的话,不会出现这种情况。)

应该是有未处理的异常。查看/usr/share/yum-cli/yummain.py发现有3个try_except语句没有捕获全部异常。我给这几个语句最后都添加一个不带表达式的except子句,把捕获的异常信息存储到一个文本文件中。例如
    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)

要注意的是yum把sys.stdout和sys.stderr重定向了,所以print异常信息是显示不到屏幕上的。请参考Python标准库trackback的文档。

再次运行yum,查看记录异常信息的那个文件,得知仍然是byterange.py中的"self.ftp.nlst(file)"一句抛出的socket.error异常。我是改成了下面这个样子:(因为我以为ftplib.err_perm包括了socket.error。这是被winpdb单步调试误导的!)
       if file and not isdir:
            # Use nlst to see if the file exists at all
            try:
                self.ftp.nlst(file)
            except ftplib.err_perm, reason: #1
                try:
                    # Fall back to PORT instead of PASV mode
                    self.ftp.set_pasv(False)
                    self.ftp.nlst(file)
                except ftplib.err_perm, reason: #2
                    raise IOError, ('ftp error', reason), sys.exc_info()[2] #3

我发现修改byterange.py的怪现象:上面修改后yum正常运行了。我改回成不捕获socket.error,再复原,发现yum还是在进入PASV模式时发生异常,却没有被我的except捕获(没有记录到我的traceback日志文件)。我把urllib.py里的ftpwrapper.retrfile()修改为捕获socket.error,记录traceback到另一个日志文件后执行sys.exit(3)。从这个日志文件看出这时yum用的ftpwrapper不是urlgrabber中的那个,而是Python标准库urllib中的那个。--这大概是yum的BUG,可能是由于某种原因yum没有给urllib2安装FtpRangeHandler(没有仔细研究)。

urlgrabber/grabber.py中导入range支持的代码是:(我检查了一下,这儿的ImportError并没有发生)

try:
    # add in range support conditionally too
    import byterange
    from byterange import HTTPRangeHandler, FileRangeHandler, \
         FTPRangeHandler, range_tuple_normalize, range_tuple_to_header, \
         RangeError
except ImportError, msg:
    range_handlers = ()
    RangeError = None
    have_range = 0
else:
    range_handlers = (HTTPRangeHandler(), FileRangeHandler(), FTPRangeHandler())
    have_range = 1


这时的urllib.py中输出的trackback信息如下:

Traceback (most recent call last):
  File "/usr/lib/python2.4/urllib.py", line 774, in retrfile
    self.ftp.nlst(file)
  File "/usr/lib/python2.4/ftplib.py", line 448, in nlst
    self.retrlines(cmd, files.append)
  File "/usr/lib/python2.4/ftplib.py", line 396, in retrlines
    conn = self.transfercmd(cmd)
  File "/usr/lib/python2.4/ftplib.py", line 345, in transfercmd
    return self.ntransfercmd(cmd, rest)[0]
  File "/usr/lib/python2.4/ftplib.py", line 324, in ntransfercmd
    conn.connect(sa)
  File "<string>", line 1, in connect
error: (111, '\xe6\x8b\x92\xe7\xbb\x9d\xe8\xbf\x9e\xe6\x8e\xa5')

而yummain.py中输出的trackback信息如下:
Traceback (most recent call last):
  File "/usr/share/yum-cli/yummain.py", line 121, in main
    result, resultmsgs = do()
  File "/usr/share/yum-cli/cli.py", line 481, in doCommands
    return self.updatePkgs()
  File "/usr/share/yum-cli/cli.py", line 957, in updatePkgs
    self.doRepoSetup()
  File "/usr/share/yum-cli/cli.py", line 78, in doRepoSetup
    yum.YumBase.doRepoSetup(self, thisrepo=thisrepo)
  File "/usr/lib/python2.4/site-packages/yum/__init__.py", line 256, in doRepoSetup
    repo.getRepoXML(text=repo)
  File "/usr/lib/python2.4/site-packages/yum/repos.py", line 673, in getRepoXML
    cache=self.http_caching == 'all')
  File "/usr/lib/python2.4/site-packages/yum/repos.py", line 617, in get
    http_headers=headers,
  File "/usr/lib/python2.4/site-packages/urlgrabber/mirror.py", line 411, in urlgrab
    return self._mirror_try(func, url, kw)
  File "/usr/lib/python2.4/site-packages/urlgrabber/mirror.py", line 397, in _mirror_try
    return func_ref( *(fullurl,), **kwargs )
  File "/usr/lib/python2.4/site-packages/urlgrabber/grabber.py", line 784, in urlgrab
    return self._retry(opts, retryfunc, url, filename)
  File "/usr/lib/python2.4/site-packages/urlgrabber/grabber.py", line 702, in _retry
    r = apply(func, (opts,) + args, {})
  File "/usr/lib/python2.4/site-packages/urlgrabber/grabber.py", line 770, in retryfunc
    fo = URLGrabberFileObject(url, filename, opts)
  File "/usr/lib/python2.4/site-packages/urlgrabber/grabber.py", line 893, in __init__
    self._do_open()
  File "/usr/lib/python2.4/site-packages/urlgrabber/grabber.py", line 960, in _do_open
    fo, hdr = self._make_request(req, opener)
  File "/usr/lib/python2.4/site-packages/urlgrabber/grabber.py", line 1055, in _make_request
    fo = opener.open(req)
  File "/usr/lib/python2.4/urllib2.py", line 358, in open
    response = self._open(req, data)
  File "/usr/lib/python2.4/urllib2.py", line 376, in _open
    '_open', req)
  File "/usr/lib/python2.4/urllib2.py", line 337, in _call_chain
    result = func(*args)
  File "/usr/lib/python2.4/urllib2.py", line 1186, in ftp_open
    fp, retrlen = fw.retrfile(file, type)
  File "/usr/lib/python2.4/urllib.py", line 784, in retrfile
    sys.exit(3)
SystemExit: 3

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

哈哈!yum终于转起来了!

[附录: 推荐几个较好的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://fr2.rpmfind.net/linux/fedora/core/5/$ARCH/os/
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/

责任编辑 webmaster

 
 
 
 
 
评论更多>>
 
 
 
发表
 
姓名: QQ:
性别: MSN:
E-mail: 主页:
评分: 1 2 3 4 5
评论内容:
验证码:
  
  • 请遵守《互联网电子公告服务管理规定》及中华人民共和国其他各项有关法律法规。
  • 严禁发表危害国家安全、损害国家利益、破坏民族团结、破坏国家宗教政策、破坏社会稳定、侮辱、诽谤、教唆、淫秽等内容的评论 。
  • 用户需对自己在使用本站服务过程中的行为承担法律责任(直接或间接导致的)。
  • 本站管理员有权保留或删除评论内容。
  • 评论内容只代表网友个人观点,与本网站立场无关。
  •