星期二, 十二月 18, 2007

postfix inet_interfaces 问题

一个报警脚本,发邮件出现问题,重定向输出中包含如下报错:
send-mail: fatal: parameter inet_interfaces: no local interface found for 124.74.193.221
,/var/log/maillog 中的记录为:
Dec 16 12:09:24 www2 postfix/pickup[30784]: fatal: parameter inet_interfaces: no local interface found for 124.74.193.221
Dec 16 12:09:25 www2 postfix/master[23234]: warning: process /usr/libexec/postfix/pickup pid 30784 exit status 1
Dec 16 12:09:25 www2 postfix/master[23234]: warning: /usr/libexec/postfix/pickup: bad command startup -- throttling
重启 postfix,即 /etc/init.d/postfix restart 失败,/var/log/maillog 中的输出为:
Dec 18 10:28:16 www2 postfix/master[23234]: terminating on signal 15
Dec 18 10:28:17 www2 postfix/sendmail[11621]: fatal: parameter inet_interfaces: no local interface found for 124.74.193.221
Dec 18 10:28:25 www2 postfix[11627]: fatal: parameter inet_interfaces: no local interface found for 124.74.193.221
Dec 18 10:28:26 www2 postfix/sendmail[11629]: fatal: parameter inet_interfaces: no local interface found for 124.74.193.221
Dec 18 10:28:27 www2 postfix[11630]: fatal: parameter inet_interfaces: no local interface found for 124.74.193.221
Dec 18 10:28:29 www2 postfix/sendmail[11632]: fatal: parameter inet_interfaces: no local interface found for 124.74.193.221
但是这台主机的 IP 地址是 124.74.193.211,为什么会出现 221 呢?

因为前两天因为发邮件,将原来指向该主机的一个域名重新定向到了 124.74.193.221,在 /etc/postfix/main.cf 中的配置为:
myhostname = mail.groups.shopex.cn
inet_interfaces = $myhostname, localhost, 124.74.193.211, 192.168.0.211
而此时 mail.groups.shopex.cn 已经解析到 124.74.193.221,这样 postfix 会先做解析,结果就不对了。

postfix inet_interfaces 有参数可以指定不使用 DNS,应该是加 [] 的方式。

星期五, 十一月 30, 2007

python 一个 unit test 代码复用实例及 **kwargs 传递

>>> def func(a, **kwargs):
... print a
... print kwargs
...
>>> def f(a, **kwargs):
... func(a, **kwargs)
...
>>> f(1, x=1)
1
{'x': 1}
注意调用的时候的方式。

在做 Python Tree 的 unit testing 的时候用到了这种方式。因为 node('update', other, ...) 即 Tree.__update__() 方法有一个 ignore_none 参数,所以在测试它的时候必须考虑到。但是否使用 ignore_none=False 的两种情况下,环境的设定是一致的,所以最好能将 ignore_onne=False 作为 **kwargs 传递给一个统一的接口:
class TestUpdate(TestTreeBase):
def doUpdate(self, other, **kwargs):
self.root('update', other, **kwargs)
这时候 doUpdate() 再调用 update 操作的时候就可以使用这种形式了。

之所以要定义一个统一的 doUpdate() 接口是因为 node += other 的形式实际上是调用了 node.__update__(),所以为了增加代码的可重用行,我当然希望 TestUpdate 和 TestIadd 能够尽可能公用代码。因为基本环节的设置都是一样的,唯一不同的是,一个是调用 self.root('update', other),而另一个是调用 self.root += other。

不过 self.root('update', other) 还可以带两个参数 self.root('update', other, ignore_none=False),但 self.root += other 只能带一个参数即 other。

为了使复用成为可能,需要在两个类中分别定义这个统一接口:
class TestUpdate(TestTreeBase):
def doUpdate(self, other, **kwargs):
self.root('update', other, **kwargs)


def _setExistedNodes(self):
self.root.new = "/new"
self.root.new.trunk = "/new/trunk"
self.root.new.trunk.branch = "/new/trunk/branch"
self.root.new.trunk['x'] = "/new/trunk(x)"
self.root.new.branch = "/new/branch"
other = Tree(None)
other.new = None
other.new.trunk = "trunk_updated"
return other

def testUpdateExistedNode(self):
# Update an existed node, both its parent and childs
# should not be affected, nor to other nodes,
# no mather its parent is root or not because the previous tests
# have prove that the root node is same as normal nodes,
temp = self.root
other = self._setExistedNodes()
self.doUpdate(other)
self.assertNodeValue(self.root, "root")
self.assertNodeValue(self.root.new, "/new")
self.assertNodeValue(self.root.new.trunk, "trunk_updated")
self.assertNodeValue(self.root.new.trunk.branch, "/new/trunk/branch")
self.assertNodeIndexValue(self.root.new.trunk, 'x', "/new/trunk(x)")
self.assertNodeValue(self.root.new.branch, "/new/branch")
self.assertPrevious("testBaseCase")
self.failUnless(self.root is temp)

def testUpdateNodeNotIgnoreNone(self):
# Don't ignore "None" setting:
temp = self.root
other = self._setExistedNodes()
self.doUpdate(other, ignore_none=False)
self.assertNodeValue(self.root, None)
self.assertNodeValue(self.root.new, None)
self.assertNodeValue(self.root.new.trunk, "trunk_updated")
self.assertNodeValue(self.root.new.trunk.branch, "/new/trunk/branch")
self.assertNodeIndexValue(self.root.new.trunk, 'x', "/new/trunk(x)")
self.assertNodeValue(self.root.new.branch, "/new/branch")
self.assertPrevious("testBaseCase")
self.failUnless(self.root is temp)
......

class TestIadd(TestUpdate):
def doUpdate(self, other, **kwargs):
self.root += other


def testUpdateNodeNotIgnoreNone(self):
pass

def testUpdateIndexNotIgnoreNone(self):
pass

def testIaddTreeDictForNew(self):
temp = self.root
self.root += {
('new',) : Tree("/new"),
('new', ('x',)) : Tree("/new(x)"),
('new1', ('x', 'y')) : Tree("/new1(x)(y)"),
('new', 'branch', 'data') : Tree(['new', 'branch', 'data'])
}
self.assertNodeValue(self.root.new, "/new")
self.assertNodeIndexValue(self.root.new, 'x', "/new(x)")
self.assertNodeValue(self.root.new1, None)
self.assertNodeIndexValue(self.root.new1, 'x', None)
self.assertNodeIndexValue(self.root.new1['x'], 'y', "/new1(x)(y)")
self.assertNodeValue(self.root.new.branch, None)
self.assertNodeValue(self.root.new.branch.data, ['new', 'branch', 'data'])
self.assertPrevious("testBaseCase")
self.failUnless(self.root is temp)
......
注意两个类中 doUpdate() 接口的定义。对 TestIadd 来说,因为不存在 ignore_none 参数的问题,所以就让 testUpdateNodeNotIgnoreNone 和 testUpdateIndexNotIgnoreNone 直接通过好了。

__package__ = "caxes"
__revision__ = 263

星期五, 十一月 23, 2007

python class methods identity

对于 Python Tree,有个问题我一直很担心,就是每个 Tree instance 会占用多少内存?Python 下面似乎是没有 sizeof() 这样的东西。

最主要的一个问题是,每个 Tree instance 都包含了在 class 中定义的那些方法以及一些全局变量,每个 instance 自身的变量其实只有 _Tree__node_value 和 _Tree__node_items。

这样,我就需要查看一下所有的 instance 的类全局变量和方法是否都使用共享的内存:
>>> import tree
>>> a = tree.Tree(1)
>>> b = tree.Tree(2)
>>> a.__call__ is b.__call__
False
>>> dir(a)
['_Tree__id_visited', '_Tree__node_items', '_Tree__node_value', '_Tree__path_stack', '_Tree__used_names', '__add__', '__call__', '__cmp__', '__contains__', '__doc__', '__getitem__', '__has__', '__iadd__', '__init__', '__islike__', '__issame__', '__module__', '__search__', '__setattr__', '__setitem__', '__str__', '__traverse__', '__update__', '_one_node_get', '_one_node_set']
>>> a._Tree__node_value is b._Tree__node_value
False
>>> a.__dict__
{'_Tree__node_items': {}, '_Tree__node_value': 1}
>>> a._Tree__id_visited is b._Tree__id_visited
True
>>> a.__module__ is b.__module__
True
>>> a.__add__ is b.__add__
False
方法 is identity 检查结果是 false。那么是不是每个 instance 都是以不同的函数栈而并不共享呢?

看一个简单的例子:
>>> class Test:
... var = 1
... def func(self): pass
...
>>> x = Test()
>>> y = Test()
>>> x.var is y.var
True
>>> x.func is y.func
False
>>> id(x.var); id(y.var)
146132400
146132400
>>> id(x.func); id(y.func)
-1208243388
-1208243388
这里 id(x.func) 和 id(y.func) 的返回值一样。根据 help(id):
Help on built-in function id in module __builtin__:

id(...)
id(object) -> integer

Return the identity of an object. This is guaranteed to be unique among
simultaneously existing objects. (Hint: it's the object's memory address.)
它们应该是同一个对象,那为什么 is 检测返回 False 呢?

从 python-chinese@lists.python.cn 上得到了答案:

>>> dir(x.func)
['__call__', '__class__', '__cmp__', '__delattr__', '__doc__', '__get__', '__getattribute__', '__hash__', '__init__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__str__', 'im_class', 'im_func', 'im_self']
>>> id(x.func.im_func)
-1210950828
>>> id(y.func.im_func)
-1210950828
>>> id(y.func.im_self)
-1208531092
>>> id(x.func.im_self)
-1208581588
这也就是说,is 检测到了 im_self 的差异,所以返回 False。而 im_self 其实就是拥有这个 func 的 instance object,而 im 应该就是指 instance method。

不过在 Windows 下运行的结果却是不同的(IDLE):
>>> class Test:
var = 1
def func(self): pass


>>> x = Test()
>>> y = Test()
>>> id(x.var)
11228488
>>> id(y.var)
11228488
>>> id(x.func)
14432856
>>> id(y.func)
14368816

tcpdump -X

前两天公司浏览网页的时候发现总是被插入了东西,IE 的弹出框提示下载什么 ActiveX 控件。从浏览器的"产看源代码"可以看到插入了如下框体
"<"iframe src='http://5.xqhgm.com/2.htm' width=20 height=1">""<"/iframe">"
因为这种问题以前也出现过两次,是由于园区的路由器被攻击,从而相应的 TCP/IP 报文被修改所导致的。这可能导致浏览器去下载病毒,从而危害 Windows 系统;而且有些页面也无法正常访问了,比如 Windows update 页面。

打电话到园区机房去问。推三阻四的,说什么“其他 Windows 路由器的用户都没有报这个问题”云云,实在没有精力去和他们周旋了,而且对于 IDS 或 Linux 网关防毒软件也没什么深入研究,毕竟我也不是安全专家。

我想还是首先证明不是我们自己路由器的问题。当然最彻底的办法是换一块系统硬盘试试看,不过那要中断网络。所以应该用抓包来看看。抓包我用过 snort, ethereal 和 tcpdump,都不是特别熟悉。如果用 tcpdump 简单命令,只能抓取到 TCP/IP 包,对于应用层数据就没有办法了。但是我记得应该是可以用 tcpdump 抓取应用层的数据的,所以 google 了一下,发现可疑用 -x 参数,但得到的都是 hex 十六进制的数据,再 man 一下,查 -x 参数,发现可以用 -X 参数来同时获得 ASCII 数据,于是:
sh# tcpdump -i eth1 -Xls 0 "port 80"
sh# tcpdump -i eth1 -Xls 0 "port 80" | grep xqhgm
然后查一下内网,因为这台机器上自己也有 web apache,所以如果是这个路由器本身有问题,那么我访问它上面的站点的时候,应该也会有问题,并且抓包应该会有结果
sh# tcpdump -i eth0 -Xls 0 "src port 80 and src host 192.168.0.1" | grep xqhgm
但没有抓到东西,而且从其他检查如 ps aux/last 也没有看出异常。

今天再浏览网页,已经没有病毒提示了。

这样看来,tcpdump 是不是也应该可以完成我以前认为只有 snort 才能完成的事情?

使用 mind map 做时间管理

思维导图以前也尝试用过,但似乎不是特别有效果。倒是 XP(eXtreme Programming) 中使用的贴纸法似乎更合我的胃口。不过最近突然发现用思维导图来做计划似乎会是一个比较好的办法。

用 FreeMind,这样在 Linux 下应该也可以用。不过要花点时间看看,很长时间没有在自己的 Linux 桌面环境下用 Java 了。

detect ARP virus by tcpdump

http://www.yourlfs.org/sysadm_zh_CN.html#toc52 中,讨论了两种 ARP 病毒。但是,这次遇到了不同的 ARP 病毒。

伴随症状:作为路由器使用的 Linux 系统变慢,主要是在上面执行命令(远程 ssh)有延迟,但是通过 console tty 直接登入系统却没有发现变慢了,也没有异常的系统负载。而且用 arp 和 arping 却看不出来,必须使用嗅探器或 tcpdump:
[root@localhost ~]# tcpdump -i eth0 "arp"
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 96 bytes
22:13:04.891192 arp reply 192.168.0.154 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:04.951540 arp reply 192.168.0.155 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:04.990617 arp reply 192.168.0.158 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.036379 arp reply 192.168.0.160 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.081340 arp reply 192.168.0.167 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.112326 arp reply 192.168.0.168 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.141507 arp reply 192.168.0.169 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.191408 arp reply 192.168.0.171 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.203497 arp reply 192.168.0.189 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.236458 arp reply 192.168.0.190 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.266490 arp reply 192.168.0.196 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.336378 arp reply 192.168.0.197 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.353386 arp who-has 192.168.0.199 tell 192.168.0.1
22:13:05.373692 arp reply 192.168.0.198 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.406402 arp reply 192.168.0.199 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.567576 arp reply 192.168.0.200 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.606394 arp reply 192.168.0.211 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.691306 arp reply 192.168.0.212 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.788985 arp reply 192.168.0.222 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:13:05.801462 arp reply 192.168.0.230 is-at 00:e0:4d:07:3b:ff (oui Unknown)
......
22:10:42.613320 arp reply 192.168.0.130 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613348 arp reply 192.168.0.132 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613376 arp reply 192.168.0.133 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613407 arp reply 192.168.0.134 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613437 arp reply 192.168.0.137 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613467 arp reply 192.168.0.138 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613497 arp reply 192.168.0.141 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613526 arp reply 192.168.0.142 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613556 arp reply 192.168.0.143 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613586 arp reply 192.168.0.144 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613615 arp reply 192.168.0.145 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613645 arp reply 192.168.0.149 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613673 arp reply 192.168.0.151 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613704 arp reply 192.168.0.152 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613733 arp reply 192.168.0.153 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613764 arp reply 192.168.0.154 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613792 arp reply 192.168.0.155 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613823 arp reply 192.168.0.158 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613853 arp reply 192.168.0.160 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613884 arp reply 192.168.0.167 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613912 arp reply 192.168.0.168 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613943 arp reply 192.168.0.169 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.613973 arp reply 192.168.0.171 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614011 arp reply 192.168.0.189 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614362 arp reply 192.168.0.190 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614394 arp reply 192.168.0.196 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614424 arp reply 192.168.0.197 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614455 arp reply 192.168.0.198 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614484 arp reply 192.168.0.199 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614515 arp reply 192.168.0.200 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614545 arp reply 192.168.0.211 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614576 arp reply 192.168.0.212 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614606 arp reply 192.168.0.222 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614637 arp reply 192.168.0.230 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614667 arp reply 192.168.0.250 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614697 arp reply 192.168.0.253 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614727 arp reply 192.168.0.254 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614758 arp reply 192.168.0.1 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.614877 arp reply 192.168.0.1 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.615186 arp reply 192.168.0.1 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.615788 arp reply 192.168.0.1 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.616564 arp reply 192.168.0.1 is-at 00:e0:4d:07:3b:ff (oui Unknown)
22:10:42.682754 arp who-has 192.168.0.98 tell 192.168.0.125
22:10:43.361718 arp who-has 192.168.0.101 tell 192.168.0.5
22:10:43.368052 arp who-has 192.168.0.199 tell 192.168.0.1

8192 packets captured
17121 packets received by filter
736 packets dropped by kernel

星期一, 十一月 19, 2007

python further than "unittest by permutation"

python Permutation 和 unittest 耗时测试隔离 中,讨论了使用排列来进行更全面的一个测试。这种方法的一个问题是耗时太长,P(6, 6)=720 耗时 ~45s,最好的情况也需要 ~30s,则 P(7, 7) 则需要耗时最少 ~3.5min,>P(8, 8) 看上去就让人无法接受了。所以在代码中增加一个判断:
    cond_len = len(names)
if cond_len > 7:
raise "Too many tests by permutation will be too time consuming."
接下来,我需要对 Tree.__call__() 和 Tree._one_node_set() 方法进行测试,对 __call__(),主要是看 value = root.trunk.node() 这种操作能否正确取值,以及 root.trunk.node('set', value) 能否正确赋值;而对 _one_node_set() 此时它涉及到的测试就比较多了,因此不适合于使用排列方法来做全面的测试。

同时,因为已经使用排列方法对 attribute 和 item 类型的赋值和取值进行过测试了(TestSetAttr 和 TestKeyIndex),那么只有能够保证它们的验证结论可以在这里直接使用,就可以使用另外一种方法来解决"始终保证后面的操作不会对前面的已有的树节点造成破坏"这样一个需求。

那么首先定义一个基类:
class TestTreeBase(TestTree):
def setUp(self):
TestTree.setUp(self)
self._setBaseCase()

def assertNodeValue(self, node, value):
self.failUnless(isinstance(node, Tree))
self.failUnlessEqual(node(), value)
self.failUnless(node() is node._Tree__node_value)
# This identity test is very important !!!
# it proves the results of the previous TestCases
# can be applied directly to the latter tests from now on.

def assertNodeIndexValue(self, node, key, value):
try:
indexed_node = node[key]
except KeyError:
self.fail("__getitem__ operator raises an unexpected KeyError")
self.failUnless(node[key] is node._Tree__node_items[key])
# Important identity test !!!
self.assertNodeValue(node[key], value)
这里对 node() 进行验证,而不像 TestTree 那样对 node._Tree__node_value 进行验证,但需要保证 node() 就是 node._Tree__node_value,因此是一个 identity 检查。

对 __call__() 方法进行测试,因为其 test* 方法比较少,所以仍然可以使用排列来进行。但对于 TestOneNodeSet,必须使用新方法。

我最初想到的一个办法是在每次调用一个 _set* 的时候,先调用"前面"的那个 _set* 方法,并在每个 test* 方法中调用"前面"那个 test* 方法(使用引号是因为 PyUnit 的各个 tests 之间实际上并不存在顺序,顺序只能由自己来定义,因此我按照编辑上从上到下的顺序来排列)。则代码象这样:
class TestOneNodeSet(TestTreeBase):
...
def _setCreateAttr(self):
self.root._one_node_set(('node_1',), 2)
self.root._one_node_set(['node_2'], "2")

def testCreateAttr(self):
if self._setToggle:
self._setCreateAttr()
self.assertNodeValue(self.root.node_1, 2)
self.assertNodeValue(self.root.node_2, "2")
# If the target node is assigned a Tree instance
self.failUnlessRaises(TreeExc, self.root._one_node_set, ('assign_Tree_directly',), Tree("two"))
self.assertPrevious("testBaseCase")

def _setCreateAttrWhenParentExisted(self):
self._setCreateAttr()
self.root.existed_parent = "existed_parent"
self.root.existed_parent.node1 = "existed/node1"
self.root._one_node_set(('existed_parent', 'node2'), "existed/node2")

def testCreateAttrWhenParentExisted(self):
# The parent nodes should not be affected
if self._setToggle:
self._setCreateAttrWhenParentExisted()
self.assertNodeValue(self.root.existed_parent, "existed_parent")
# Make sure the previous nodes are not affected, because Tree._one_node_set() is a recursive operation
self.assertNodeValue(self.root.existed_parent.node1, "existed/node1")
self.assertNodeValue(self.root.existed_parent.node2, "existed/node2")
self.assertPrevious("testCreateAttr")
assertPrevious() 是在 TestTree 中定义的:
class TestTree(unittest.TestCase):
...
def assertPrevious(self, test_name):
self._setToggle = 0
test_method = getattr(self, test_name)
test_method()
self._setToggle = 1
这样有几个问题,一是测试方法之间的模块性就没有那么好了,另一方面,一个 test* 方法每次调用"前面"那个 test*,而那个 test* 又要调用"它自己前面"的那个 test*,则测试方法需要耗费的时间也是会越来越长,虽然还是在一个可接受的范围之类,但似乎每次都测前面的有点多余。

实际上,完全可以在 setUp() 中就先建立所有应该已存在节点,因为 node() 已经做了 is identity 检查,所以每次只需要测试这些节点,实际上就表示已经对所有已存在的树节点进行了检查。

编辑代码如下:
class TestOneNodeSet(TestTreeBase):
def _setBaseCase(self):
# Since the strategy is changed,
# build some previous nodes first:
TestTreeBase._setBaseCase(self)
self.root['base'] = "(base)"
self.root['base'].data = "(base)/data"
self.root['base']['x'] = "(base|x)"
self.root['base']['x'].extra = "(base|x)/extra"
self.root['base']['x']['y'] = "(base|x|y)"
self.root.base = "base"
self.root.base['x'] = "base(x)"
self.root.base['x'].data = "base(x)/data"
self.root.base['x']['y'] = "base(x|y)"
self.root.base['x']['y'].extra = "base(x|y)/extra"
self.root.base['x']['y']['z'] = "base(x|y|z)"

def testBaseCase(self):
if self._setToggle:
self._setBaseCase()
# TestTreeBase.testBaseCase(self)
self.assertNodeIndexValue(self.root, 'base', "(base)")
self.assertNodeValue(self.root['base'].data, "(base)/data")
self.assertNodeIndexValue(self.root['base'], 'x', "(base|x)")
self.assertNodeValue(self.root['base']['x'].extra, "(base|x)/extra")
self.assertNodeIndexValue(self.root['base']['x'], 'y', "(base|x|y)")
self.assertNodeValue(self.root.base, "base")
self.assertNodeIndexValue(self.root.base, 'x', "base(x)")
self.assertNodeValue(self.root.base['x'].data, "base(x)/data")
self.assertNodeIndexValue(self.root.base['x'], 'y', "base(x|y)")
self.assertNodeValue(self.root.base['x']['y'].extra, "base(x|y)/extra")
self.assertNodeIndexValue(self.root.base['x']['y'], 'z', "base(x|y|z)")

def _setCreateAttr(self):
self.root._one_node_set(('node_1',), 2)
self.root._one_node_set(['node_2'], "2")

def testCreateAttr(self):
if self._setToggle:
self._setCreateAttr()
self.assertNodeValue(self.root.node_1, 2)
self.assertNodeValue(self.root.node_2, "2")
# If the target node is assigned a Tree instance
self.failUnlessRaises(TreeExc, self.root._one_node_set, ('assign_Tree_directly',), Tree("two"))
self.assertPrevious("testBaseCase")

def _setCreateAttrWhenParentExisted(self):
# self._setCreateAttr()
self.root.existed_parent = "existed_parent"
self.root.existed_parent.node1 = "existed/node1"
self.root._one_node_set(('existed_parent', 'node2'), "existed/node2")

def testCreateAttrWhenParentExisted(self):
# The parent nodes should not be affected
if self._setToggle:
self._setCreateAttrWhenParentExisted()
self.assertNodeValue(self.root.existed_parent, "existed_parent")
# Make sure the previous nodes are not affected, because Tree._one_node_set() is a recursive operation
self.assertNodeValue(self.root.existed_parent.node1, "existed/node1")
self.assertNodeValue(self.root.existed_parent.node2, "existed/node2")
# self.assertPrevious("testCreateAttr")
self.assertPrevious("testBaseCase")

...
setUp() 方法是在 TestTreeBase 中定义的:
class TestTreeBase(TestTree):
def setUp(self):
TestTree.setUp(self)
self._setBaseCase()
此时,注意 def _setCreateAttrWhenParentExisted(self): 中 self._setCreateAttr() 已经被注释,而 def testCreateAttrWhenParentExisted(self): 中也不再使用 self.assertPrevious("testCreateAttr"),而代之以 self.assertPrevious("testBaseCase") 即可。

__package__ = "caxes"
__revision__ = [259:262]

symlink for Apache DocumentRoot/Directory

/home/httpd 是 /data/httpd 的符号链接。在 httpd.conf 中,一开始使用的都是 DocumentRoot /home/httpd/$site 这样的形式,这次配置一个简单的认证,采用如下方法:
Directory "/data/httpd/$site"
AuthUserFile /usr/local/apache2/conf/.htpasswd
AuthName "SimpleAuth"
AuthType Basic
require valid-user
Options None
AllowOverride None
Order Deny,Allow
Deny from all
Allow from $ipaddr
结果无法通过验证。

把 Directory "/data/httpd/$site" 改为 Directory "/home/httpd/$site" 就可以了。

如果 /data/httpd/$site/ 下的一个页面通过 url 连接到 http://$site/ 下的某个其他页面则仍会有问题,因为显然不能自动提交认证信息。

星期二, 十一月 13, 2007

python Permutation 和 unittest 耗时测试隔离

python unittest TestSuite 框架实践和几个问题中,提到"按照任意的顺序去调用 _set* 和 test* 方法,始终保证后面的操作不会对前面的已有结果造成破坏"这样一个需求,当时并没有给出一个结论。

实际上,这里涉及到一个组合数的问题。比如在 test_tree.TestSetAttr 中,有这样几个带 _set* 的测试:
["testCreateNew", "testAssignNonTreeToExisted", "testAssignTreeToExisted", "testReserveSomeAttrWhenReplaceNode"]。这里 TestSetAttr 虽然是从 TestTree 继承来的,包含 testBaseCase() 方法,但因为 testBaseCase() 是基础,在 setUp() 里面设定,所以应该排除。这样一来,就有 P(4, 4)=24 种组合情况。

为了实现这一点,首先编写一个简单的组合数函数:test/caxes/support.py:
def full_permutate(items):
if len(items) <=1:
yield items
else:
for P in full_permutate(items[1:]):
for i in range(len(P) + 1):
yield P[:i] + items[0:1] + P[i:]
可以定义相应的单元测试 test/caxes/test_support.py:
#!/usr/bin/python
# -*- encoding: utf-8 -*-

__author__ = "Roc Zhou #周鹏"
__date__ = "12 November 2007"
__version__ = "0.2"
__license__ = "GPL v2.0"

import unittest

from support import full_permutate

class TestPermute(unittest.TestCase):
def testFullPermutate(self):
self.failUnlessEqual([ x for x in full_permutate("") ], [""])
self.failUnlessEqual([ x for x in full_permutate([]) ], [[]])
self.failUnlessEqual([ x for x in full_permutate(['a']) ], [['a']])
self.failUnlessEqual([ x for x in full_permutate(['a', 'b']) ], [['a', 'b'], ['b', 'a']])
result = []
for x in full_permutate(['a', 'b', 'c']): result.append(x)
expect = [
['a', 'b', 'c'],
['a', 'c', 'b'],
['b', 'a', 'c'],
['b', 'c', 'a'],
['c', 'a', 'b'],
['c', 'b', 'a'] ]
result.sort()
expect.sort()
self.failUnlessEqual(result, expect)
result = []
for x in full_permutate(['a', 'b', 'c', 'd']): result.append(x)
expect = [
['a', 'b', 'c', 'd'],
['a', 'b', 'd', 'c'],
['a', 'c', 'b', 'd'],
['a', 'c', 'd', 'b'],
['a', 'd', 'b', 'c'],
['a', 'd', 'c', 'b'],
['b', 'a', 'c', 'd'],
['b', 'a', 'd', 'c'],
['b', 'c', 'a', 'd'],
['b', 'c', 'd', 'a'],
['b', 'd', 'a', 'c'],
['b', 'd', 'c', 'a'],
['c', 'a', 'b', 'd'],
['c', 'a', 'd', 'b'],
['c', 'b', 'a', 'd'],
['c', 'b', 'd', 'a'],
['c', 'd', 'a', 'b'],
['c', 'd', 'b', 'a'],
['d', 'a', 'b', 'c'],
['d', 'a', 'c', 'b'],
['d', 'b', 'a', 'c'],
['d', 'b', 'c', 'a'],
['d', 'c', 'a', 'b'],
['d', 'c', 'b', 'a'] ]
result.sort()
expect.sort()
self.failUnlessEqual(result, expect)

if __name__ == "__main__":
unittest.main()
关于 Python 的排列组合,有不少参考资料,讨论的人也很多。例如可以参考:
http://snippets.dzone.com/posts/show/753
http://www.pyzen.cn/subject/2966/
http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/474124

然后,在 test_tree.TestTree 中增加这样的定义:

from caxes import support

class TestTree(unittest.TestCase):
...
def _testPrevious(self, test_name):
self._setToggle = 0
test_method = getattr(self, test_name)
test_method()
self._setToggle = 1

def testPermutation(self):
names = []
for member in inspect.getmembers(self, inspect.ismethod):
name = member[0]
if name.startswith("_set"): names.append(name[4:])
names.remove("BaseCase")
cond_len = len(names)
for cond in support.full_permutate(names):
self.tearDown()
self.setUp()
previous_names = ["testBaseCase"]
for i in range(cond_len):
test_name = "test%s" % cond[0]
testMethod = getattr(self, test_name)
testMethod()
for pre_test_name in previous_names:
self._testPrevious(pre_test_name)
previous_names.append(test_name)

...
if __name__ == "__main__":
# #2:
import __main__
suite = unittest.TestLoader().loadTestsFromModule(__main__)
unittest.TextTestRunner(verbosity=2).run(suite
这样运行时可以的。但是有一个问题,在后面的 TestKeyIndex 测试中,_set* 方法有 6 个,则 P(6, 6)=720 中排列,这意味着需要花比较长的时间才能完成一次测试,在一台 2.4GHz*2 Xeon, 1G MEM 的主机上,花费了 ~45s。

《Pragmatic Unit Testing》中提到要讲耗时的测试隔离。那么在这里具体应该怎么做呢。设定条件从 tests list 删除实际上不太现实:
suite = unittest.TestLoader().loadTestsFromTestCase(TestKeyIndex)
# for test in suite: print test
# suite._tests.remove(TestKeyIndex("testUnordered"))
remove() 最终会抛出 IndexError。而且这样又会破坏正交性。

也许有很多解法。目前我使用一个类 TestTreeComplex 从 TestTree 集成,将 testPermutation() 只定义到 TestTreeComplex 中,同时定义 TestSetAttrComplex 和 TestKeyIndexComplex 从 TestTreeComplex 和 TestSetAttr/TestKeyIndex 双重集成:
sh# vi test/test_tree_complex.py
#!/usr/bin/python
# -*- encoding: utf-8 -*-

__author__ = "Roc Zhou #周鹏"
__date__ = "13 November 2007"
__version__ = "0.2"
__license__ = "GPL v2.0"

import unittest
import inspect

from caxes import support
import test_tree

import tree
from tree import Tree,TreeExc,TreeTypeExc,TreePathConvExc

class TestTreeComplex(test_tree.TestTree):
def _testPrevious(self, test_name):
self._setToggle = 0
test_method = getattr(self, test_name)
test_method()
self._setToggle = 1

def testPermutation(self):
names = []
for member in inspect.getmembers(self, inspect.ismethod):
name = member[0]
if name.startswith("_set"): names.append(name[4:])
names.remove("BaseCase")
cond_len = len(names)
for cond in support.full_permutate(names):
self.tearDown()
self.setUp()
previous_names = ["testBaseCase"]
for i in range(cond_len):
test_name = "test%s" % cond[0]
testMethod = getattr(self, test_name)
testMethod()
for pre_test_name in previous_names:
self._testPrevious(pre_test_name)
previous_names.append(test_name)

class TestSetAttrComplex(TestTreeComplex, test_tree.TestSetAttr):
pass

class TestKeyIndexComplex(TestTreeComplex, test_tree.TestKeyIndex):
pass

if __name__ == "__main__":
suite = unittest.TestSuite()
suite.addTest(TestSetAttrComplex("testPermutation"))
suite.addTest(TestKeyIndexComplex("testPermutation"))
unittest.TextTestRunner(verbosity=2).run(suite)
这里 TestSetAttrComplex 和 TestKeyIndexComplex 实际上不需要定义任何其他方法和属性。这样可以有效隔离耗时测试,将其放到项目自动化构建的每时或每日构建中。

__package__ = "caxes"
__revision__ = [258:~]

python distutils 调整目录结构后

项目自动化中谈到对整个目录结构进行调整以利于测试和项目自动化构建,则相应的,distutils 的 setup.py 也需要相应的进行调整。

现在以完成部分的目录结构是这样的:
trunk/
ChangeLog
lib/
tree.py
LICENSE
MANIFEST.in
README
setup.py
test/
caxes/
__init__.py
support.py
test_support.py
test_tree.py
test_tree_complex.py
test_tree.bk
则调整后的 setup.py 为:
#!/usr/bin/python
# -*- encoding: utf-8 -*-

__author__ = "Roc Zhou #周鹏"
__date__ = "13 November 2007"
__version__ = "0.2"
__license__ = "GPL v2.0"

from distutils.core import setup
from distutils import sysconfig

lib_prefix = sysconfig.get_python_lib()

setup(
name = 'caxes',
version = '0.2',
description = """
Some new Python data types such as Tree,
and configuration sharing mechanism implementation.
""",
long_description = """
Some new Python data structure,
can be afforded as APIs for new ways of configuration,
and configuration sharing mechanism implementation.

It's a subproject of uLFS.

uLFS means "Your Manageable, Automatable and Templated Reusable Linux From Scratch",
it's a set of tools to build your own customed Linux distribution with
more managability than raw LFS(Linux From Scratch). Include source package
manager, file system backup and realtime mirror apps, and some assistant data
structure such as Tree writen in Python, etc...
""",
author = "Roc Zhou",
author_email = 'chowroc.z@gmail.com',
platforms = "Platform Independent",
license = "GPL v2.0",
url = "http://crablfs.sourceforge.net",
download_url = "http://sourceforge.net/projects/crablfs",
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Console",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: GNU General Public License (GPL)",
"Natural Language :: English",
"Natural Language :: Chinese (Simplified)",
"Operating System :: POSIX",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python",
"Topic :: Software Development :: Libraries :: Python Modules"
],
py_modules = ["tree"],
package_dir = {"" : "lib", "caxes" : "lib/caxes"},
# data_files = [("%s/test" % lib_prefix, ["test/test_tree*.py", "test/support.py"])],
# data_files = [("%s/test" % lib_prefix, ["test/*.py"]), ("%s/test/caxes" % lib_prefix, ["test/caxes/*.py"])],
data_files = [("%s/test" % lib_prefix, ["test/*.py", "test/caxes"])],
# packages = ['caxes']
)
这里先说明一下 py_modules 和 package_dir 的调整。

之前的定义为:
py_modules = ["tree"]
package_dir = {"caxes" : "lib"}
因为目录结构为:
trunk/
tree.py
lib/
...
但当目录结构调整后,会提示找不到模块 tree 的文件 tree.py,因为这个文件已经不再 trunk 的根目录下了,而是移到了 lib/,此时应该调整 package_dir,增加 "" : "lib"。而原来的 lib/ 变成了 lib/caxes,所以 package_dir 也应该相应变动。

在下面定义 data_files,保证 test/test_tree*.py 被安装到 /usr/lib/python2.4/site-packages/test/,而 test/caxes/ 被拷贝成 /usr/lib/python2.4/site-packages/caxes/,忽略掉 *.bk 文件。此时必须记住要调整 trunk/ 下的 MANIFEST.in 文件:
include *.py
include test/*.py
recursive-include test/caxes *
include README
include ChangeLog
include LICENSE
注意 recursive-include 一行,否真 test/caxes 目录不会被拷贝。

有一点比较奇怪的是,当我使用:
data_files = [("%s/test" % lib_prefix, ["test/test_tree*.py", "test/caxes"])]
却提示找不到文件 test/test_tree*.py。这是为什么呢?

__package__ = "caxes"
__revision__ = [258:259]

星期六, 十一月 10, 2007

python unittest TestSuite 框架实践和几个问题

之前的 python Tree 实现中单元测试做的不好,在"Progmatic Unit Testing", 心得和自省中说过这一点。现在要重写单元测试。

为了保证所有测试的独立性和正交性,编写测试 setattr() 操作正确性的代码如下:
#!/usr/bin/env python
# -*- encoding: utf-8 -*-

__author__ = "Roc Zhou #周鹏"
__date__ = "09 November 2007"
__version__ = "0.2"
__license__ = "GPL v2.0"

"""Unittest for tree.Tree"""

from gettext import gettext as _

import os
import unittest
import inspect

import tree
from tree import Tree,TreeExc,TreeTypeExc,TreePathConvExc

# def _setAll(self):
# names = []
# for member in inspect.getmembers(self, inspect.ismethod):
# method_name = member[0]
# if method_name.startswith("_set"):
# names.append(method_name)
# names.remove("_setAll")
# for name in names:
# method = getattr(self, name)
# method()

# def testPreviousAll(self):
# # """The previous Tree nodes should not be affected by the latter operations"""
# self._setToggle = 0
# names = []
# for test_name in inspect.getmembers(self, inspect.ismethod):
# if test_name.startswith("test"):
# names.append(test_name)
# names.remove("testPreviousAll")
# # #1:
# suite = unittest.TestSuite(map(self.__class__, test_names))
# suite.run()
# # #2:
# # for name in names:
# # test_method = getattr(self, name)
# # test_method()

class TestTree(unittest.TestCase):
def setUp(self):
self._setToggle = 1
### self._setAll = _setAll
### self.testPreviousAll = testPreviousAll
### Why can't ??????

def tearDown(self):
self.root = None

def assertNodeSet(self, node, value):
self.failUnless(isinstance(node, Tree))
self.failUnlessEqual(node._Tree__node_value, value)

def assertNodeValue(self, node, value):
self.failUnless(isinstance(node, Tree))
self.failUnlessEqual(node(), value)

def assertNodeIndex(self, node, key, value):
self.failUnless(node._Tree__node_items.has_key(key))
self.failUnless(isinstance(node[key], Tree))
self.failUnlessEqual(node[key]._Tree__node_value, value)

def assertNotNodeIndex(self, node, key):
self.failIf(node._Tree__node_items.has_key(key))

def assertNodeIndexValue(self, node, key, value):
self.failUnless(node.has_key(key))
self.assertNodeValue(node[key], value)

def _setBaseCase(self):
self.root = Tree("root", data="root.data", extra="extra")

def testBaseCase(self):
# """The simplest assignment should create a Tree instance with several valid sub nodes"""
if self._setToggle:
self._setBaseCase()
self.assertNodeSet(self.root, "root")
self.assertNodeSet(self.root.data, "root.data")
self.assertNodeSet(self.root.extra, "extra")

def _setAll(self):
names = []
for member in inspect.getmembers(self, inspect.ismethod):
method_name = member[0]
if method_name.startswith("_set"):
names.append(method_name)
names.remove("_setAll")
for name in names:
method = getattr(self, name)
method()

def testPreviousAll(self):
# """The previous Tree nodes should not be affected by the latter operations"""
self._setToggle = 0
# self.tearDown()
# self.setUp()
self._setAll()
names = []
for member in inspect.getmembers(self, inspect.ismethod):
test_name = member[0]
if test_name.startswith("test"):
names.append(test_name)
names.remove("testPreviousAll")
# #1:
# suite = unittest.TestSuite(map(self.__class__, names))
# suite.run(suite)
# #2:
for name in names:
test_method = getattr(self, name)
test_method()

class TestSetAttr(TestTree):
def setUp(self):
TestTree.setUp(self)
self._setBaseCase()

def testNameReserved(self):
# """Assign to reserved names should be avoided"""
__used_names = object.__dict__.copy().keys() + [
'_Tree__path_stack', '_Tree__id_visited', '_Tree__used_names',
'_Tree__node_value', '_Tree__node_items',
'__path_stack', '__id_visited', '__used_names', '__node_value', '__node_items',
'__getattr__', '__setitem__', '__getitem__', '__call__',
'__add__', '__iadd__', '__cmp__',
'_one_node_set', '__traverse__', '__update__', '__copy__', '__search__'
]
for used_name in __used_names:
try:
setattr(self.root, used_name, 1)
self.fail(_("A reserved name '%s' should can not be reassignable" % used_name))
except TreeExc:
pass
except TypeError:
pass

def _setCreateNew(self):
self.root.trunk = 1
self.root.branch = Tree(None, data='branch/data', extra=('branch', 'extra'))

def testCreateNew(self):
# """If the sub node does not exist, assign it directly"""
if self._setToggle:
self._setCreateNew()
self.assertNodeSet(self.root.trunk, 1)
self.assertNodeSet(self.root.branch, None)
self.assertNodeSet(self.root.branch.data, "branch/data")
self.assertNodeSet(self.root.branch.extra, ('branch', 'extra'))

def _setAssignNonTreeToExisted(self):
self.root.nt_ex_simple = 1
self.root.nt_ex_complex = Tree(1, data="nt_ex_complex/data", extra="nt_ex_complex/extra")
# Create new first
self.root.nt_ex_simple = "one"
self.root.nt_ex_complex = "ONE"

def testAssignNonTreeToExisted(self):
# """If attribute is an existed Tree, and target is not a Tree, only the node value should be replaced"""
if self._setToggle:
self._setAssignNonTreeToExisted()
self.assertNodeSet(self.root.nt_ex_simple, "one")
self.assertNodeSet(self.root.nt_ex_complex, "ONE")
# If the node has childs, only value replacement:
self.assertNodeSet(self.root.nt_ex_complex.data, "nt_ex_complex/data")
self.assertNodeSet(self.root.nt_ex_complex.extra, "nt_ex_complex/extra")

def _setAssignTreeToExisted(self):
self.root.tr_ex_simple = 2
self.root.tr_ex_complex = Tree(2, data="tr_ex_complex/data")
self.root.tr_ex_simple_branch = '2'
self.root.tr_ex_complex_branch = Tree(2, data="tr_ex_complex_branch/data")
# Create first
self.root.tr_ex_simple = Tree("two")
self.root.tr_ex_complex = Tree("TWO")
self.root.tr_ex_simple_branch = Tree("_two", data="tr_ex_simple_branch/data")
self.root.tr_ex_complex_branch = Tree("_TWO", extra="tr_ex_complex_branch/extra")

def testAssignTreeToExisted(self):
# """If attribute is an existed Tree, and target is a Tree too, the node itself should be replaced"""
if self._setToggle:
self._setAssignTreeToExisted()
# (1) target Tree instance does not have attributes, and original Tree does not have sub nodes:
self.assertNodeSet(self.root.tr_ex_simple, "two")
# (2) target Tree instance does not have attributes, and original Tree have sub nodes:
self.assertNodeSet(self.root.tr_ex_complex, "TWO")
self.failIf(hasattr(self.root.tr_ex_complex, "data"))
# (3) target Tree instance has attributes, and original Tree does not have sub nodes:
self.assertNodeSet(self.root.tr_ex_simple_branch, "_two")
self.assertNodeSet(self.root.tr_ex_simple_branch.data, "tr_ex_simple_branch/data")
# (4) target Tree instance has attributes, and original Tree have sub nodes too:
self.assertNodeSet(self.root.tr_ex_complex_branch, "_TWO")
self.failIf(hasattr(self.root.tr_ex_complex_branch, "data"))
self.assertNodeSet(self.root.tr_ex_complex_branch.extra, "tr_ex_complex_branch/extra")

def _setReserveSomeAttrWhenReplaceNode(self):
self.root.replace_but_reserve_1 = ["Replace", "But", "Reserve", "Method", 1]
self.root.replace_but_reserve_1.br1 = 1
subtree = self.root.replace_but_reserve_1.br1
self.root.replace_but_reserve_1 = Tree("replaced_by_method_1", data="another_1")
self.root.replace_but_reserve_1.br1 = subtree

self.root.replace_but_reserve_2 = ["Replace", "But", "Reserve", "Method", 2]
self.root.replace_but_reserve_2.br2 = 2
self.root.replace_but_reserve_2 = Tree("replaced_by_method_2", br2=self.root.replace_but_reserve_2.br2, data="another_2")

def testReserveSomeAttrWhenReplaceNode(self):
if self._setToggle:
self._setReserveSomeAttrWhenReplaceNode()
self.assertNodeSet(self.root.replace_but_reserve_1, "replaced_by_method_1")
self.assertNodeSet(self.root.replace_but_reserve_1.br1, 1)
self.assertNodeSet(self.root.replace_but_reserve_1.data, "another_1")
self.assertNodeSet(self.root.replace_but_reserve_2, "replaced_by_method_2")
self.assertNodeSet(self.root.replace_but_reserve_2.br2, 2)
self.assertNodeSet(self.root.replace_but_reserve_2.data, "another_2")

......
这里首先从 TestCase 继承一个 TestTree 基类,并自定义一些 assert 测试方法来做一些基本的测试。这样测试 setattr() 的类 TestSetAttr 可以从 TestTree 继承并直接调用这些 assert 来完成更复杂的测试,这样也就提高了复用性。

每一个 test* 方法基本上和一个 _set* 方法对应,并只进行很简单很专门的测试,这样就可以将方法名定义得更具有可读性,而且方便其他测试方法来调用,test* 和 _set* 分开也是为了这个目的。

比如,上面在 TestTree 中定义的 testPreviousAll() 方法,就是为了测试之前所有的操作是否互相影响,比如对一个节点的子节点进行操作之后,它本身的值、它的兄弟节点和其他子节点都不应该受到影响,它会利用 inspect 模块提供的功能寻找所有自己这个测试类中的 test* 方法(排除自身),并逐一调用,以确保这一点。因为 TestSetAttr 是从 TestTree 继承的,所以它也会有这个 testPreviousAll() 方法。

要逐一调用这些方法方法,有两种想法,其一是利用 getattr() 得到这个方法的实例,并直接调用;另一种思路是利用 TestSuite 的构造方法得到一个 test suite,并调用 suite.run() 直接运行。

除了利用 testPreviousAll() 方法来做这件事外,我一开始的另一个思路是在后面调用 TextTestRunner().run(suite) 的时候调用对 TestSetAttr 前后调用两次,而不用使用 testPreviousAll() 方法。

无论使用那种方法,前后两次调用 test* 方法都需要分别设定 _setToggle 标志,保证第二次调用的时候,test* 方法不会去调用 _set* 重复设定各个节点,这样才能得到正确的测试结果。

但是这几种思路都有问题。逐一来讨论。

首先,更深入的了解一下 unittest 的框架。TestCase 为一个基本单元,由若干 TestCase 组成一个 TestSuite,是可以运行的单元,TestSuite 也可以包含 TestSuite。但必须通过调用 TestRunner 的实例来运行 TestSuite 的实例。例如:
suite = unittest.TestSuite()
suite.addTest(...)
...
unittest.TextTestRunner(verbosity=2).run(suite)
在 Python 的官方手册中,提到一个 test fixture,这个 fixture 事实上并没有相应的 class,只不过是一种概念,即可以将若干包含 TestSuite 的 TestSuite 看作一个 fixture,因为其环境比较复杂了。如何对这样一个 fixture 设置 setUp() 和 tearDown() 环境这里不讨论。

为了运行所有这些测试,最简单的办法是直接调用 unittest.main() 函数:
if __name__ == "__main__":
unittest.main()
它会自动去寻找当前模块里所有的 tests 组成 TestSuite 并运行之。因此这段代码的等效代码可以看作是:
if __name__ == "__main__":
import __main__
suite = unittest.TestLoader().loadTestsFromModule(__main__)
unittest.TextTestRunner(verbosity=2).run(suite)
所以问题的关键是如何得到这些 TestSuite 并将 tests 加入其中。

除了调用 unittest.main() 以外,显式地创建 test suite 有很多方法,除了上面的 TestLoader 之外,还可以逐个加入:
suite = unittest.TestSuite()
suite.addTest(TestSetAttr("testNameReserved"))
suite.addTest(TestSetAttr("testCreateNew"))
...
unittest.TextTestRunner(verbosity=2).run(suite)
通过阅读 unittest 的源代码可以知道,addTest() 的参数即为一个 test,也就是一个 TestCase 的 instance,并且这个 instance 的 testMothod() 方法指向传递给构造函数的方法名所表示的方法。

TestSuite.addTests() 的参数是 tuple list of tests。

另一种方法是:
test_names = [
"testNameReserved",
"testCreateNew",
"testAssignNonTreeToExisted",
"testAssignTreeToExisted",
"testReserveSomeAttrWhenReplaceNode",
"testPreviousAll" ]
suite = unittest.TestSuite(map(TestSetAttr, test_names))
因为 TestSetAttr 是 callable,所以 map(function, seq1, [seq2, ...]) 会将它作为一个函数调用,并将 test_names 依次作为其调用时的参数。这样就会依次构建所有的 tests instance。

回到这个测试的具体案例上来。如果采用 unittest.main() 或 unittest.TestLoader().loadTestsFromModules(__main__) 的做法,有一个问题就是 tests 不会按照你定义他们的时候的上下顺序来运行(而且即使按照定义的顺序来运行也没有用,下面谈到),但是在定义测试的时候,如最开始所描述的,必须要考虑到对 Previous 条件进行测试的需要。如果我不做 testPreviousAll(),那么这样运行没有什么问题,但问题就在于做 testPreviousAll() 是有必要的。

一开始,使用 unittest.main() 或 unittest.TestLoader().loadTestsFromModules(__main__) 这种方法,在 TestTree 这个基类中定义的方法如下:
def _setAll(self):
names = []
for member in inspect.getmembers(self, inspect.ismethod):
method_name = member[0]
if method_name.startswith("_set"):
names.append(method_name)
names.remove("_setAll")
for name in names:
method = getattr(self, name)
method()

def testPreviousAll(self):
self._setToggle = 0
self._setAll()
names = []
for member in inspect.getmembers(self, inspect.ismethod):
test_name = member[0]
if test_name.startswith("test"):
names.append(test_name)
names.remove("testPreviousAll")
# #1:
suite = unittest.TestSuite(map(self.__class__, names))
suite.run(suite)
# unittest.TextTestRunner(verbosity=2).run(suite)
# #2:
# for name in names:
# test_method = getattr(self, name)
# test_method()
然后运行:
 python test_tree.bk
testBaseCase (__main__.TestKeyIndex) ... ok
testDeepIndexedNodes (__main__.TestKeyIndex) ... ok
testIndexedParentReplacement (__main__.TestKeyIndex) ... ok
testInexistentNode (__main__.TestKeyIndex) ... ok
testNodeReplacement (__main__.TestKeyIndex) ... ok
testNodeWithKeys (__main__.TestKeyIndex) ... ok
testNodeWithoutKey (__main__.TestKeyIndex) ... ok
testOnlyValueReplacement (__main__.TestKeyIndex) ... ok
testPreviousAll (__main__.TestKeyIndex) ... ERROR
testUnhashable (__main__.TestKeyIndex) ... ok
testAssignNonTreeToExisted (__main__.TestSetAttr) ... ok
testAssignTreeToExisted (__main__.TestSetAttr) ... ok
testBaseCase (__main__.TestSetAttr) ... ok
testCreateNew (__main__.TestSetAttr) ... ok
testNameReserved (__main__.TestSetAttr) ... ok
testPreviousAll (__main__.TestSetAttr) ... ERROR
testReserveSomeAttrWhenReplaceNode (__main__.TestSetAttr) ... ok
testBaseCase (__main__.TestTree) ... ok
testPreviousAll (__main__.TestTree) ... ERROR

======================================================================
ERROR: testPreviousAll (__main__.TestKeyIndex)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_tree.bk", line 113, in testPreviousAll
suite.run(suite)
File "/usr/lib/python2.4/unittest.py", line 422, in run
if result.shouldStop:
AttributeError: 'TestSuite' object has no attribute 'shouldStop'

======================================================================
ERROR: testPreviousAll (__main__.TestSetAttr)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_tree.bk", line 113, in testPreviousAll
suite.run(suite)
File "/usr/lib/python2.4/unittest.py", line 422, in run
if result.shouldStop:
AttributeError: 'TestSuite' object has no attribute 'shouldStop'

======================================================================
ERROR: testPreviousAll (__main__.TestTree)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_tree.bk", line 113, in testPreviousAll
suite.run(suite)
File "/usr/lib/python2.4/unittest.py", line 422, in run
if result.shouldStop:
AttributeError: 'TestSuite' object has no attribute 'shouldStop'

----------------------------------------------------------------------
Ran 19 tests in 0.462s

FAILED (errors=3)
显然,你不能向 suite.run() 传递 suite 作为参数,你只能向 suite.run() 传递 TestResult 的 instance 作为参数,或者将 suite 作为参数传递给 TestRunner 的 instance,亦即在上面的定义中使用的另外一种方法:unittest.TextTestRunner(verbosity=2).run(suite)。但这样运行会导致你的输出很不规整,你可能会得到类似这样的结果:
sh# python test_tree.bk
testBaseCase (__main__.TestKeyIndex) ... ok
testDeepIndexedNodes (__main__.TestKeyIndex) ... ok
testIndexedParentReplacement (__main__.TestKeyIndex) ... ok
testInexistentNode (__main__.TestKeyIndex) ... ok
testNodeReplacement (__main__.TestKeyIndex) ... ok
testNodeWithKeys (__main__.TestKeyIndex) ... ok
testNodeWithoutKey (__main__.TestKeyIndex) ... ok
testOnlyValueReplacement (__main__.TestKeyIndex) ... ok
testPreviousAll (__main__.TestKeyIndex) ... testBaseCase (__main__.TestKeyIndex) ... ok
testDeepIndexedNodes (__main__.TestKeyIndex) ... ok
testIndexedParentReplacement (__main__.TestKeyIndex) ... ok
testInexistentNode (__main__.TestKeyIndex) ... ok
testNodeReplacement (__main__.TestKeyIndex) ... ok
testNodeWithKeys (__main__.TestKeyIndex) ... ok
testNodeWithoutKey (__main__.TestKeyIndex) ... ok
testOnlyValueReplacement (__main__.TestKeyIndex) ... ok
testUnhashable (__main__.TestKeyIndex) ... ok

----------------------------------------------------------------------
Ran 9 tests in 0.157s

OK
ok
testUnhashable (__main__.TestKeyIndex) ... ok
testAssignNonTreeToExisted (__main__.TestSetAttr) ... ok
testAssignTreeToExisted (__main__.TestSetAttr) ... ok
testBaseCase (__main__.TestSetAttr) ... ok
testCreateNew (__main__.TestSetAttr) ... ok
testNameReserved (__main__.TestSetAttr) ... ok
testPreviousAll (__main__.TestSetAttr) ... testAssignNonTreeToExisted (__main__.TestSetAttr) ... ok
testAssignTreeToExisted (__main__.TestSetAttr) ... ok
testBaseCase (__main__.TestSetAttr) ... ok
testCreateNew (__main__.TestSetAttr) ... ok
testNameReserved (__main__.TestSetAttr) ... ok
testReserveSomeAttrWhenReplaceNode (__main__.TestSetAttr) ... ok

----------------------------------------------------------------------
Ran 6 tests in 0.124s

OK
ok
testReserveSomeAttrWhenReplaceNode (__main__.TestSetAttr) ... ok
testBaseCase (__main__.TestTree) ... ok
testPreviousAll (__main__.TestTree) ... testBaseCase (__main__.TestTree) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.006s

OK
ok

----------------------------------------------------------------------
Ran 19 tests in 0.759s
这样显然是不利于测试的组织和代码的排错的。所以改用在上面的定义中的 #2 所指明的办法。

在上面 testPreviousAll() 的定义中调用了 self._setAll(),这看上去会降低正交性,但是不再其中定义却不行,你会得到这样的错误:

# python test_tree.bk
testBaseCase (__main__.TestKeyIndex) ... ok
testDeepIndexedNodes (__main__.TestKeyIndex) ... ok
testIndexedParentReplacement (__main__.TestKeyIndex) ... ok
testInexistentNode (__main__.TestKeyIndex) ... ok
testNodeReplacement (__main__.TestKeyIndex) ... ok
testNodeWithKeys (__main__.TestKeyIndex) ... ok
testNodeWithoutKey (__main__.TestKeyIndex) ... ok
testOnlyValueReplacement (__main__.TestKeyIndex) ... ok
testPreviousAll (__main__.TestKeyIndex) ... ERROR
testUnhashable (__main__.TestKeyIndex) ... ok
testAssignNonTreeToExisted (__main__.TestSetAttr) ... ok
testAssignTreeToExisted (__main__.TestSetAttr) ... ok
testBaseCase (__main__.TestSetAttr) ... ok
testCreateNew (__main__.TestSetAttr) ... ok
testNameReserved (__main__.TestSetAttr) ... ok
testPreviousAll (__main__.TestSetAttr) ... ERROR
testReserveSomeAttrWhenReplaceNode (__main__.TestSetAttr) ... ok
testBaseCase (__main__.TestTree) ... ok
testPreviousAll (__main__.TestTree) ... ERROR

======================================================================
ERROR: testPreviousAll (__main__.TestKeyIndex)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_tree.bk", line 118, in testPreviousAll
test_method()
File "test_tree.bk", line 360, in testDeepIndexedNodes
self.assertNodeSet(self.root.deep, "deep_indexed_prefix")
AttributeError: Tree instance has no attribute 'deep'

======================================================================
ERROR: testPreviousAll (__main__.TestSetAttr)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_tree.bk", line 118, in testPreviousAll
test_method()
File "test_tree.bk", line 168, in testAssignNonTreeToExisted
self.assertNodeSet(self.root.nt_ex_simple, "one")
AttributeError: Tree instance has no attribute 'nt_ex_simple'

======================================================================
ERROR: testPreviousAll (__main__.TestTree)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_tree.bk", line 118, in testPreviousAll
test_method()
File "test_tree.bk", line 84, in testBaseCase
self.assertNodeSet(self.root, "root")
AttributeError: 'TestTree' object has no attribute 'root'

----------------------------------------------------------------------
Ran 19 tests in 0.304s

FAILED (errors=3)
因为通过构造 TestSuite 的方法可以看出,每一次传递给它的构造函数的都是一个全新的 TestCase instance,并且每一个这样的 instance 中在运行的时候只有其中的一个 test* 方法被运行,就是 testMothod 所指向的那个方法。所以如果不先调用这个方法,那么任何节点都不会设置。

如果使用方法 #1,则似乎不会出现这个问题,所有测试都 OK 通过,但其实是不对的。因为在检查 PreviousAll 的时候,我必须避免对要检查的节点重复设置,所以在 test* 方法中使用了 _setToggle 标志来实现这一点。但因为使用 TestSuite() 创建了新的 tests,而这些 tests 的 _setToggle 必然是重新设置为 1 的,所以所有节点都会被重复设置,因此测出的结果是不正确的。如果将 _setToggle 定义为 class 的 static 变量呢?似乎可行,但显然会降低正交性,并且如前所述,测试的结果输出很不规范。

这也就是前面提到,即使按照顺序来调用这些 tests 也没有用的原因。如果要按照顺序来调用,可以这样:
fixture = unittest.TestSuite()
test_names = ["testBaseCase"]
suite = unittest.TestSuite(map(TestTree, test_names))
fixture.addTest(suite)
test_names = [
"testNameReserved",
"testCreateNew",
"testAssignNonTreeToExisted",
"testAssignTreeToExisted",
"testReserveSomeAttrWhenReplaceNode",
"testPreviousAll" ]
suite = unittest.TestSuite(map(TestSetAttr, test_names))
fixture.addTest(suite)
test_names = [
"testNodeWithoutKey",
"testNodeWithKeys",
"testInexistentNode",
"testOnlyValueReplacement",
"testNodeReplacement",
"testIndexedParentReplacement",
"testUnhashable",
"testDeepIndexedNodes",
"testPreviousAll" ]
suite = unittest.TestSuite(map(TestKeyIndex, test_names))
fixture.addTest(suite)
unittest.TextTestRunner(verbosity=3).run(fixture)
当然这样意义不大,而且显然增加了重复性。

即使是在 testPreviousAll() 中调用了 _setAll() 也仍然是有问题的,因为
for member in inspect.getmembers(self, inspect.ismethod)
这段代码也不会按照你定义的顺序去 inspect member methods,所以 _setBaseCase() 可能会在中间运行再次被运行(setUp() 里面运行过第一次,否则没有根节点,我这里实际上就在中间重新运行的),因此导致 root 节点被重新设置,结果是已经设置的其他节点被冲掉了,因此后面必然会抛出 AttributeError 异常。当然结果是不确定的,可能会出现这样的问题,也可能会巧合的按照定义顺序进行。

显然第一种思路是行不通了。那么第二种思路呢?
fixture = unittest.TestSuite()
suite = unittest.TestLoader().loadTestsFromTestCase(TestTree)
fixture.addTest(suite)
suite = unittest.TestLoader().loadTestsFromTestCase(TestSetAttr)
fixture.addTest(suite)
TestSetAttr._setToggle = 0
suite = unittest.TestLoader().loadTestsFromTestCase(TestSetAttr)
fixture.addTest(suite)
suite = unittest.TestLoader().loadTestsFromTestCase(TestKeyIndex)
fixture.addTest(suite)
TestKeyIndex._setToggle = 0
suite = unittest.TestLoader().loadTestsFromTestCase(TestKeyIndex)
fixture.addTest(suite)
unittest.TextTestRunner(verbosity=2).run(fixture)
这时候,不定义 testPrevious() 和 _setAll() 方法,并且 _setToggle 定义为类的 static 变量。结果当然仍然是不行,可以推想,因为所有的 tests 都是全新的 TestCase instance,相互之间没有关联,因此第二次产生的 suite 并不会使用第一次产生的 suite 的结果。在将 TestSetAttr._setToggle 设置为 0 后,调用 unittest.TestLoader().loadTestsFromTestCase(TestSetAttr) 产生的 tests,其 test* 将不再设置 _set* 方法,因此所有的节点都不会被设置,必然会抛出 AttributeError。实际运行的结果也是如此。

那么如何解决这个正交性测试独立性的问题呢?

因为同时必须要考虑到顺序的问题,但同时因为各个测试方法之间都是互相独立的,所以我接下来的一个思路就是在每一个 test* 方法中调用定义的前一个 test* 和 _set*。代码大概是这样(此时不再需要定义 testPreviousAll() 和 _setAll()):
class TestTree(unittest.TestCase):
...
def _testPrevious(self, test_name):
self._setToggle = 0
test_method = getattr(self, test_name)
test_method()
self._setToggle = 1

class TestSetAttr(TestTree):
def setUp(self):
TestTree.setUp(self)
self._setBaseCase()
...
def _setCreateNew(self):
self._setBaseCase()
self.root.trunk = 1
self.root.branch = Tree(None, data='branch/data', extra=('branch', 'extra'))

def testCreateNew(self):
# """If the sub node does not exist, assign it directly"""
if self._setToggle:
self._setCreateNew()
self.assertNodeSet(self.root.trunk, 1)
self.assertNodeSet(self.root.branch, None)
self.assertNodeSet(self.root.branch.data, "branch/data")
self.assertNodeSet(self.root.branch.extra, ('branch', 'extra'))
self._testPrevious("testBaseCase")
这样在运行 testCreateNew() 的时候,会接着运行 testBaseCase() 以确保前面设置的节点没有受到 CreateNew 操作的影响,并且因为关闭了 _setToggle,不会导致重复设置。

但如果把这个问题扩展一下呢?就是说,如果我按照任意的顺序去调用 _set* 方法,那么能否始终保证后面的操作不会对前面的已有结果造成破坏呢?

另外还有一个问题。在:
python assemble methods at runtime?

python unzip
中,曾提到运行时装配方法的技巧。但在这里似乎行不通,因为这时候这个 testPreviousAll() 方法好像根本就没有被检测到(使用 loadTestsFromModules(__main__)):
sh# python test_tree.bk
testBaseCase (__main__.TestKeyIndex) ... ok
testDeepIndexedNodes (__main__.TestKeyIndex) ... ok
testIndexedParentReplacement (__main__.TestKeyIndex) ... ok
testInexistentNode (__main__.TestKeyIndex) ... ok
testNodeReplacement (__main__.TestKeyIndex) ... ok
testNodeWithKeys (__main__.TestKeyIndex) ... ok
testNodeWithoutKey (__main__.TestKeyIndex) ... ok
testOnlyValueReplacement (__main__.TestKeyIndex) ... ok
testUnhashable (__main__.TestKeyIndex) ... ok
testAssignNonTreeToExisted (__main__.TestSetAttr) ... ok
testAssignTreeToExisted (__main__.TestSetAttr) ... ok
testBaseCase (__main__.TestSetAttr) ... ok
testCreateNew (__main__.TestSetAttr) ... ok
testNameReserved (__main__.TestSetAttr) ... ok
testReserveSomeAttrWhenReplaceNode (__main__.TestSetAttr) ... ok
testBaseCase (__main__.TestTree) ... ok

----------------------------------------------------------------------
Ran 16 tests in 0.280s

OK
看一下 unittest 的源代码,其调用关系是这样的:
loadTestsFromModules()
\--> loadTestsFromTestCase() # TestSetAttr
\--> getTestCaseNames(self, testCaseClass)

class TestLoader:
......
def loadTestsFromTestCase(self, testCaseClass):
"""Return a suite of all tests cases contained in testCaseClass"""
if issubclass(testCaseClass, TestSuite):
raise TypeError("Test cases should not be derived from TestSuite. Maybe you meant to derive from TestCase?")
testCaseNames = self.getTestCaseNames(testCaseClass)
if not testCaseNames and hasattr(testCaseClass, 'runTest'):
testCaseNames = ['runTest']
return self.suiteClass(map(testCaseClass, testCaseNames))

def loadTestsFromModule(self, module):
"""Return a suite of all tests cases contained in the given module"""
tests = []
for name in dir(module):
obj = getattr(module, name)
if (isinstance(obj, (type, types.ClassType)) and
issubclass(obj, TestCase)):
tests.append(self.loadTestsFromTestCase(obj))
return self.suiteClass(tests)

def getTestCaseNames(self, testCaseClass):
"""Return a sorted sequence of method names found within testCaseClass
"""
def isTestMethod(attrname, testCaseClass=testCaseClass, prefix=self.testMethodPrefix):
return attrname.startswith(prefix) and callable(getattr(testCaseClass, attrname))
testFnNames = filter(isTestMethod, dir(testCaseClass))
for baseclass in testCaseClass.__bases__:
for testFnName in self.getTestCaseNames(baseclass):
if testFnName not in testFnNames: # handle overridden methods
testFnNames.append(testFnName)
if self.sortTestMethodsUsing:
testFnNames.sort(self.sortTestMethodsUsing)
return testFnNames
filter 是 builtin 函数,显然在使用 dir(testCaseClass) 的时候是不会包含 testPreviousAll() 的,因为 testPreviousAll()只会在 TestCase instance 中存在。

所以应该不是在 setUp() 方法里面设置,而是应该定义 TestTree.testPreviousAll = testPreviousAll。

包含这些问题的代码在 caxes/trunk/test/test_tree.bk 中:
__package__ = "caxes"
__revision__ = [257:259]

星期三, 十一月 07, 2007

sourceforge vhost

绑定 VirtualHost。在 Project -> Admin -> Shell/DB/Web -> "Manage VHOSTs" 下,添加一个 vhost(Add New Virtual Host -> New Virtual Host),例如我原来的项目站点为 crablfs.sourceforge.net,这时增加一个 www.yourlfs.net。

然后设置 DNS。注册一个域名 yourlfs.net(¥50/年),并进行如下设置:
yourlfs.net      IN    A        66.35.250.210
www.yourlfs.net IN CNAME vhost.sourceforge.net.
svn.yourlfs.net IN CNAME crablfs.svn.sourceforge.net.

dig DNS 迭代查询以及双线 view 问题

默认情况下,DNS 都会使用递归查询。通过迭代查询,可以获得查询的路径,即对域名是如何被解析的得到一个直观印象。可以通过运行
sh$ dig +trace www.example.com (@server)
来进行查询。

我在 "变态"DNS中曾经讨论过为双线设置 DNS 的方法。但最近发现老是会被解析到网通的服务器,即使是电信的 DNS 服务器,也会得到网通的结果。用 dig +trace 也看不出所以然来。

后来发现网通的 slave DNS 的两个域文件(电信 .zone 和 网通 .cnc_zone)的内容完全一样。原来从服务器同步的时候,因为地址是网通的,所以主服务器只会返回网通的结果。这样看来,从主服务器之间也是通过 53 端口传递数据并且也受 acl view 规则的影响。

要解决这个问题,复杂一点的办法是从服务器绑定两个 IP 地址,主服务器的两个 view 设置不同的 allow-transfer {},从服务器的两个 view 设置两个 transfer-source $ipaddr,可参考:
http://www.chinalinuxpub.com/read.php?wid=1452

简单点的办法就是把两个都设置成主的 DNS 并手工同步。

python setup.py classifiers for pypi

通过编写 setup.py 中的 classfiers,以及相应的 url/download_url 和 description,可以直接从命令行上运行 python setup.py register 将包直接上传到 pypi,从而避免每次发布新版本的时候都要登录到 web 页面填写表单这样的重复劳动。例子如下:
#!/usr/bin/python
# -*- encoding: utf-8 -*-

# Author: Roc Zhou
# Date: 2007-11-07
# Email: chowroc.z@gmail.com

from distutils.core import setup
from distutils import sysconfig

lib_prefix = sysconfig.get_python_lib()

setup(
name = 'caxes',
version = '0.1.4'
description = """
Some new Python data structure,
can be afforded as APIs for new ways of configuration.

It's a subproject of uLFS.

uLFS means "Your Manageable, Automatable and Templated Reusable Linux From Scratch",
it's a set of tools to build your own customed Linux distribution with
more managability than raw LFS(Linux From Scratch). Include source package
manager, file system backup and realtime mirror apps, and some assistant data
structure such as Tree writen in Python, etc...
""",
author = "Roc Zhou",
author_email = 'chowroc.z@gmail.com',
platforms = "Platform Independent",
license = "GPL v2.0",
url = "http://crablfs.sourceforge.net",
download_url = "http://sourceforge.net/projects/crablfs",
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Console",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: GNU General Public License (GPL)",
"Natural Language :: English",
"Natural Language :: Chinese (Simplified)",
"Operating System :: POSIX",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python",
"Topic :: Software Development :: Libraries :: Python Modules"
],
py_modules = ['tree'],
# data_files = [('test', ['tree_ut.py'])],
data_files = [("%s/test" % lib_prefix, ['test_tree.py'])],
# package_dir = {'caxes' : 'lib'},
# packages = ['caxes']
)
不过不知道为什么 description 却和 web 页面中的 Description 不一样,却显示在了 Summary 下面。结果 Summary 信息很长。

星期日, 十一月 04, 2007

python distutils data_files lib_prefix

使用 distutils 的 setup() 的 data_files 参数,本来期望文件被拷贝到 /usr/lib/python2.4/site-packages/test 下面,结果却被拷贝到了 /usr/test(如果使用 upm 安装则没有权限),因为 prefix 为 /usr。

那么我希望改变这个设置。因为 /usr/lib/python2.4/site-packages 应该是在安装时的 LIB 目录设定下的,所以找到 lib_prefix 即可:
from distutils import sysconfig

lib_prefix = sysconfig.get_python_lib()

setup(
...
data_files = [("%s/test" % lib_prefix, ['tree_ut.py'])],
...

星期五, 十一月 02, 2007

SNMP access 的一个问题

SNMP Agent 的配置文件:
sh$ egrep -v '(^#|^$)' snmpd.conf
syslocation Unknown (edit /etc/snmp/snmpd.conf)
syscontact Root (configure /etc/snmp/snmp.local.conf)
pass .1.3.6.1.4.1.4413.4.1 /usr/bin/ucd5820stat
syscontact monitor@zovatech.com
disk / 36G
com2sec mynet 192.168.0.0/24 public
com2sec mynet 222.66.231.106 public
group mynet v1 mynet
group mynet v2c mynet
view system included .1
access mynet "" any noauth exact system none none
exec .1.3.6.1.4.1.2021.54 hdNum /usr/local/bin/snmpdiskio hdNum
exec .1.3.6.1.4.1.2021.55 hdIndex /usr/local/bin/snmpdiskio hdIndex
exec .1.3.6.1.4.1.2021.56 hdDescr /usr/local/bin/snmpdiskio hdDescr
exec .1.3.6.1.4.1.2021.57 hdInBlocks /usr/local/bin/snmpdiskio hdInBlocks
exec .1.3.6.1.4.1.2021.58 hdOutBlocks /usr/local/bin/snmpdiskio hdOutBlocks
本来没有设置 com2sec mynet 222.66.231.106 的。因为 SNMP client 这台主机有两个 IP,一个是内网的 192.168.0.1,还有一个是外网的 202.66.231.106,而 SNMP Agent 所在主机为 192.168.0.197。则在配置 SNMP access 的候,一个很直接的想法就是只要允许了所有内网的主机就应该可以了呀,但是在这里却不行,对于 192.168.0.1 这台主机,必须要使用外网(eth1)的那个 IP 来指定。

不知道原因是什么?

星期四, 十一月 01, 2007

项目自动化

《Pragmatic Project Automation》主要是针对 Java 项目的自动化建构和部署以及监控。我目前主要使用 Python,接着需要重新再学习 C,那么我需要考虑一些不同的情况。

首先,我不大可能使用象 Ant 和 CruiseControl 这样的工具,不过我想应该可以直接利用 Python 的 distutils 工具基本上也可以做这些。

首先,为了使测试能够自动化,并且编写单元测试的时候能够更加一致,也许应该调整一下目录结构。以 caxes 项目为例,目前的目录结构是:
ulfs/
caxes/
tree.py
test_tree.py
lib/
ctemplates.py
edconfig.py
sctmd.py
test/
test_ctemplates.py
test_edconfig.py
test_sctmd.py
......
但对于单元测试文件的存放就不一致了。另一个问题是,test_ctemplates.py 要 import ctemplates.py,在 sandbox 中和安装后的包路径会不一致,因为安装后显然只能是 caxes 包(或其他名字如 libcaxes,但不可能是 lib),虽然可以象这样:
try:
pwd = os.getcwd()
MODULE_PATH = os.path.dirname(pwd)
sys.path.insert(0, MODULE_PATH)
import mirrord,fs_info
sys.path.pop(0)
except ImportError:
from cutils import mirrord,fs_info
但这会导致重复增加。所以我想这样的目录结构也许更合理:
ulfs/
caxes/
lib/
tree.py
caxes/
ctemplates.py
edconfig.py
sctmd.py
test/
test_tree.py
caxes/
test_ctemplates.py
test_edconfig.py
test_sctmd.py
......
然后做一个构建脚本,在其中首先设置 PYTHONPATH,并调用 setup.py build --build-base=build/,并进入 build/ 目录自动运行所有的测试即可。例如:
#!/bin/sh
cd /opt/automated/
svn co http://crablfs.svn.sourceforge.net/svnroot/crablfs ulfs/
cd ulfs/caxes/
python setup.py build --build-base=build/
cd build/
export PYTHONPATH=`pwd`/lib
python test/test_ctemplates.py
python test/test_edconfig.py
python test/test_sctmd.py
...
当然应该能够写的更好?不过我不知道有没有 Python 下对应的工具?

另一个问题是如果将定时构建脚本的输出重定向到邮件、短信或信号灯,这应该属于监控的问题了。

另外,书中写到在安装和部署时使用诊断测试排除故障的技巧是比较有价值。

星期三, 十月 31, 2007

Linux 内存寻址 1GB(896MB) 限制问题

《Understanding the LINUX KERNEL》(《深入理解LINUX内核》)一书,P51 提到:
In theory, up to 4GB of RAM could be installed on such systems; in practice, due to the linear address space requirements of User Mode processes, the kernel cannot directly address more than 1GB of RAM, as we will see in the later secion "Paging in Linux."


然后,在 P70 中(Paging in Linux -> Kernel Page Tables)中首先说明的是"Final kernel Page Table when RAM size is less than 896MB"。这个 896MB 根据后面的说明,是 1GB - 128MB = 896MB,128MB 有别的用处。但这个 1GB 是怎么来的呢?

参考 P47 关于 "Regular Paging" 的说明,可以看到,实际上每个进程(Process)都有它自己的 Linear Address 空间,默认会分配一个 Page Directory 和 Page Table(因此最小内存占用量为 8KB),而在 "Paging in Linux" 中讲到,系统被分为 User Mode 和 Kernel Mode,如果将 kernel 看作一个进程(init),那么系统最多为它预留的内存只能是 1GB,否则它将不得不使用 User Mode 的内存作为内核之用。因为 User Mode 无法获得直接获得 Kernel Mode 的内容,而 Kernel Mode 则可以从 User Mode 中存取数据,所以以最通常的情况 4GB 最大寻址空间为例,则 3GB 作为 User Mode 使用,1GB 给 Kernel Mode 使用,而 Kernel Mode 的线性地址空间为:0xc0000000 到 0xffffffff。

这个 4GB 的最大寻址空间是指 32 位使用最简单的线性地址表示方法时可以映射的最大寻址空间,也就是只有一个 Page Directory 和 1024 个 Page Table 的情况,此时是一个两级的目录状态。为了支持 Intel 处理器的 PAE(Physical Address Extension)功能以支持超过 4GB 的物理内存,Linux 可以在 PD(称为 Page Gloabl Directory, PGD) 和 PT 之间增加 Page Upper Directory 和 Page Middle Directory,显然这增加了目录级数,也就意味着增加了寻址时间。所以如果不使用那么大的内存,Linux 会将 PUD 和 PMD 的长度设定为 0 并直接映射为 PGD 的一个条目。

但这个 Kernel Mode 和 Low Memory/High Memory 有联系吗?

"Permission Denied" when load php module (SELinux)

Cannot load /usr/local/apache2/modules/libphp4.so into server: /usr/local/apache2/modules/libphp4.so : cannot restore segment prot after reloc: Permission Denied
这个错误是因为打开了 RedHat 的 SELinux,关闭就可以了。

另外,在编译 php 时使用了 --enable-gd-native-ttf 和 --with-gd=/usr/local 参数,待同样报告类似的错误:
/usr/local/lib/libgd.so.2: cannot restore segment prot after reloc: Permission Denied
,同样也是 SELinux 造成的。

星期六, 十月 27, 2007

"Progmatic Unit Testing", 心得和自省

昨天花了一天时间,把《Progmatic Unit Testing》这本书基本上过了一遍。写的很不错,结合之前做项目时写单元测试的经验,有一些心得,也必须做一些反省。

最主要的一个方面是关于测试代码本身的质量。测试代码应该与产品代码有同样的质量,因此要遵循 DRY 和正交性、低耦合的设计原则,而这一点在我之前的测试中做的很不好,以 Python Tree 来说,本来我想测试应该保证逻辑上的尽可能简单,因此为了保证测试覆盖面足够,结果包含了很多重复的语句,尤其是有时侯需要对一个变量(Tree Node)的几个方面做检查的时候,这些检查语句都要反复写,这就很不好。

在书中提到,最好从 TestCase 继承一个类,然后所有的其他所有的测试类都从这个继承基类再继承,这样可以在这个基类里面做 setUp() 和 tearDown(),并且可以自定义 assert/fail 函数。例如,对 Python Tree,我大可以这样定义:
class TestTree(unittest.TestCase):
def assertValidTreeNode(node, value):
self.failUnlessEqual(node(), value)
...

class TestSetAttr(TestTree):
def testNotExisted():
self.root.branch = 1
self.assertValidTreeNode(self.root.branch, 1)
...
在 cutils 的 mirrord/fs_mirror 项目中也存在这个问题。这样导致我在单元测试上花费了太多的时间,特别是如果对产品代码做出改动,在单元测试中就需要改很多地方,也就是说,复用性不好!不够专业。

另一个重要的问题是”独立性“不好。这一点在做 cutils 的 mirrord/fs_mirror 的时候尤其明显。一个方面是对于环境的依赖,在 mirrord/fs_mirror 中,因为并发非常重要,所以在 test_mirrord 中需要测试这种不同的并发情况会产生的不同的结果,之前的做法就是调度一个实际的实例,然后进行一些 sleep/wait 来等待并发的状态变化。但因为并发状态会如何变化是不受控制的,在不同的主机上结果也可能完全不同,所以只能等的更久来保证状态一定会变化(实际上有时侯也难以完全保证),结果就是运行的时间很长。另外,这也导致了在编写的时候更难保持 DRY 和正交性、低耦合的原则,对进度很不利。

我当时还在找多线程/并发的单元测试方法,好像对于 Java 还有专门的这样的软件。但现在看来,其实并不需要,只要利用 Mock 对象,模拟出相同的接口,然后在里面可控的设置并发状态,这个问题应该很好解决。

在 caxes Tree 的顺序问题也反映了独立性不够,主要是必须保证以前的节点不会因为后面的操作而受到影响,之前的做法就是逐一检查,当然也就导致了重复,而且后面的操作受前面的影响,没有前面的操作,后面就无从谈起,但完全可以利用 fixture 做多个 TestSuite,每次使用 fixture 和 test case 的 setUp() 重建就好了,然后利用 fixture 和 test suites 重新调用前面的测试就可以了。

在 test_mirrord 也有顺序问题,因为并发状态不同,可能结果不同,那么测试 server thread 的时候和测试 monitor 的时候结果可能不同!我之前的做法显然是错误的,只是因为想不到好的办法,就将前面测试 monitor 的结果记录到文件中,在 测试 server thread 的时候调出来比较,这显然不正确,只不过一般机器的运行调度结果在一般情况下会一样,所以基本上不会出现 fail 而已。

关于测试的“彻底性”和"自动化"方面,应该做得还可以,不过因为复用性不好,比较烦琐。当然,书中提到的对测试覆盖和边界条件的检查的原则还是相当有价值的,我之前也很难说做的很好。

另外,使用数据文件的技巧也很有意思。

要重写了! '')

meizu miniplayer 死机

上次买了一个,除了听音乐之外,主要也是为了作为一种存储和学英语的工具。前天挂到 LFS 下面的时候,发现分区表全是乱的,当然挂不上来。没多想,mkfs 了一下,结果再起来就不行了,不再创建默认的目录了。

在 Windows 下格了几次,每次插入都提示要重新格式话,试了一下 fat 格式,结果就没办法再启动了,每次都死机!

搜了一下。发现可以长按"Enter"键关机,但再启动还是死机。但在"长"按"Power + >>"键后可以自己格式化,而不需要进入 Windows,屏幕提示 Formating...,不过必须在刚开机的时进行这个操作,否则死机后就不行了。再启动后发现可以了。

参考:
http://bbs.imp3.net/thread-384771-1-6.html

官方资料:
http://www.meizu.com/support/faq_bbs.asp?Type=2

不知道是不是低格,因为其中说到"Power + <<"是低格。

用 WinXP 再格成 Fat32,结果在进入 LFS 还是分区乱。原来没有在 Linux 内核里面编译 vfat 模块!看来忙得实在是太晕头了。

星期四, 十月 25, 2007

用 bc 做进制转换

sh$ echo "obase=16; ibase=10; 1024 * 4096" | bc -l
400000
obase 即 output base,ibase 即 input base,结果当然是 0x400000

星期二, 十月 23, 2007

Ian Murdock 见面会

周一晚上去参见了 CU 组织的 Debian 创始人 Ian Murdork 见面会,不过 Ian 现在在 Sun 工作,自然也少不了 Sun 公司的广告啦。不过因为人比较多,Ian 减少了演讲的时间,留了一个半小时给大家提问,这本来是件好事情,可惜主办组织者太不地道了。

我本来有一些技术上的问题想问问看,虽说口语不怎么样,还是准备了一下,至少写在纸上了,可惜举手一个半小时,也抢不到话筒,因为话筒根本就不是由主讲人 Ian 来点名的,而是主办方在下面"偷偷摸摸”递来递去,除非你什么也不管,站起来就说逼得主办方不得不把话筒递给你,否则话筒基本上只会递给他们熟脸的人。好像大家都在乎那本提问就发的书似的。结果常常搞得 Ian 还没搞清楚人在哪里,眼睛还看着东边,声音就从西边响起来了,真是一点礼貌都没有。

问的问题也是一些泛泛而谈,没有什么实质性的内容。什么“Sun 的战略”呀,什么“为什么 Solaris 要关注个人用户”“向 Linux 靠拢”呀,什么“Microsoft 也搞开源啦,那么开源运动的未来怎样”呀,什么“IBM 也有 openJDK,那么对 Sun 有什么冲突”呀,什么“为什么 xx 编程比赛没有 Java"呀,什么“开源对国防安全的意义”啦等等等等...简直是在浪费大家的时间嘛。又不是搞新闻发布会。

最后,没办法,只能在最后结束后,等那些签名、拍照的人都排完了,过去说了两句,递了一张纸条,把在做的项目和个人邮箱写在上面,并说希望他能够看一下我正在做的开源项目,给我一个 feedback 作为指导和建议,他看了一下,看出了其中那个 file system mirror 的项目,并说 OK。说起来,这好像还是我真正第一次与外国人对话吧。

当然,我不能指望一定会有回复,最终要走下去,只能靠自己。

路漫漫其修远兮,吾将上下而求索。
荃不察余之衷情兮,反信馋而其怒。
亦予心之所善兮,虽九死其尤未悔。

星期五, 十月 19, 2007

Linux zip compression

在 Linux 使用 zip 打包一个目录:
sh# find dir/ | zip dir.zip -i -@

星期二, 十月 16, 2007

google sitemap

今天花了点时间了解了一下 google sitemap_gen,并在项目的文档站点上部署了一下,主要是为了使其更利于搜索以促进项目的推广。

首先从 sourceforge 上下载 sitemap_gen,解开后,拷贝 example_config.xml 为 config.xml,拷贝 example_urllist.txt 为 ulfs_urllist.txt,编辑这两个文件。config.xml 文件我只保留了 URL 和 URLLIST 两种方法,指定 base_url 和 store_into 到站点根目录下的 sitemap.xml.gz 文件,这样 google 可以通过 web 访问到这个 sitemap 文件,urllist 指向到 ulfs_urllist.txt:
http://crablfs.sourceforge.net
http://crablfs.sourceforge.net/index.html lastmod=2007-10-14T22:48:00+01:00 changefreq=monthly priority=1.0
http://crablfs.sourceforge.net/sysadm_zh_CN.html lastmod=2007-09-25T22:17+01:00 changefreq=monthly priority=0.3
http://crablfs.sourceforge.net/tree.html lastmod=2007-07-05T03:32:00+01:00 changefreq=monthly priority=0.5
http://crablfs.sourceforge.net/ru_data_man_zh_CN.html lastmod=2007-10-15T01:14:00+01:00 changefreq=weekly priority=0.6
然后上传 3 个文件:config.xml, ulfs_urllist.txt 和 sitemap_gen.py 到站点根目录下。

接着使用 ssh 登录到站点,运行 sitemap_gen.py 命令:
[chowroc@pr-shellC htdocs]$ python sitemap_gen.py --config=config.xml
Reading configuration file: config.xml
Opened URLLIST file: ulfs_urllist.txt
[WARNING] Discarded URL for not starting with the base_url: http://crablfs.sourceforge.net
Sorting and normalizing collected URLs.
Writing Sitemap file "/home/groups/c/cr/crablfs/htdocs/sitemap.xml.gz" with 4 URLs
Notifying search engines.
Notifying: www.google.com
[WARNING] Cannot contact: www.google.com
Count of file extensions on URLs:
4 .html
Number of errors: 0
Number of warnings: 2
去掉 ulfs_urllist.txt 中的第一行,在运行:
[chowroc@pr-shellC htdocs]$ python sitemap_gen.py --config=config.xml
Reading configuration file: config.xml
Opened URLLIST file: ulfs_urllist.txt
Sorting and normalizing collected URLs.
Writing Sitemap file "/home/groups/c/cr/crablfs/htdocs/sitemap.xml.gz" with 4 URLs
Notifying search engines.
Notifying: www.google.com
[WARNING] Cannot contact: www.google.com
Count of file extensions on URLs:
4 .html
Number of errors: 0
Number of warnings: 1
仍然报告无法 contact www.google.com,那么可以登录到 google 的 Webmaster tools,手工添加 sitemap,在 "Sitemaps" 中提交:
http://crablfs.sourceforge.net/sitemap.xml.gz
等待一段时间之后,可以看到已经正确提交的显示。

关于 google sitemap_gen 的使用文档在:
https://www.google.com/webmasters/tools/docs/en/sitemap-generator.html

星期六, 十月 13, 2007

bind slave 同步中的一个问题

昨天发现辅 DNS 同步有问题,一些后来加入的域名没有被传输,重启主、辅服务器都没有用,但在 /var/log/messages 里面也没有显示报错信息。

主、辅服务器都是使用的数据 zone 文件,于是我在辅助服务器上删除了原来的数据文件,再重启辅助服务器,发现这次新的域名被同步过来了!

星期五, 十月 12, 2007

python *args and wrapped BDB's pop() default

>>> def f(x, **kwargs):
... print kwargs
...
>>> f(1)
{}
我在 cutils 项目的 mirrord/fs_mirror 中好几个地方都使用了 Berkeley DB,并且是包裹在一个模拟字典类的对象中的(hash table like),为使其 pop() 操作更接近于内置的 dict 对象,特别是在返回默认值的操作上能够保持一致的行为方式,编码如下:
def pop(self, key, *args):
if args:
try:
args_len = len(args)
if args_len > 1:
raise TypeError, "pop() takes exactly 2 arguments (%d given)" % args_len
default = args[0]
except KeyError:
pass
try:
self.dbfile.pop(key)
except KeyError, kexc:
try:
return default
except NameError:
raise KeyError, kexc

python iterator 的一种用法

>>> d = {'a' : 1, 'b' : 2, 'c' : 3, 'd' : 4}
>>> it = iter(d)
>>> for x in it: print x
...
a
c
b
d

开始使用百度的空间

以前的文章都帖在 google 的 blogspot 上面了,但我总是缺乏安全感(总之我活在这个现世里面就没有真正感到过安全),担心有一天如果 google 的帐号出问题访问不了了,那数据的损失就大了!

有一些备份的方法。对于 gmail,一般的邮件客户端没办法很好的备份,因为不在 Inbox 中的邮件无法下载,而且象 labels 这些东西也没有办法了;通过 filter forward 转到另一个邮箱的办法也不是太好,因为转过去不好分类,会很乱,而且对于同一个 session 的保存也不是很好,并且以前老的邮件也没办法处理。虽然有一个 libgmail 的 Python 模块,但是我当时使用了一下它的 demos/archive.py,效果不是太好,中间会退出,可能是中文的关系,而且它的界面也不是太好,交互终端的方式不是很灵活,我想有时间再研究它的基本原理和代码,然后自己写一个,可以直接提交 gmail 的查询指令就好了,然后在一个默认的交互式命令行里面操作——如果可能,再在上面包装终端和 GUI。

不过目前来说,没有时间,只能是用 filter forward 的办法了。但是对于其他的服务就不好办了。比如 blog——如果有一个程序能够一键搞定所有与这个帐号相关的数据的备份就好了,如果能够实现增量以及差分备份则更佳,不过目前我也没有发现这些东西,而且我现在实在也没有时间去做这些事情。所以对于以前的 blogspot,只能用一种土办法来备份了。一个备份脚本如下:

#!/bin/sh

day=`date -I`
backdir=/mnt/file/internet/google/$day
if ! [ -e $backdir ]; then
mkdir -p $backdir
elif ! [ -d $backdir ]; then
echo "Not a directory" >&2
exit 1
fi

wget http://chowroc.blogspot.com/search?max-results=999 -O $backdir/blog_all_public_posts.html
wget http://chowroc.blogspot.com/feeds/posts/default?max-results=999 -O $backdir/blog_all_xml_feeds.html
wget http://chowroc.blogspot.com/feeds/comments/default?max-results=999 -O $backdir/blog_all_comments.html

这个会生成 3 个 html 文件,包含所有文章。

不过,也是为了一种备份的需要,我决定现在开始使用百度空间,那么以后在 blogspot 上粘贴的文章,在这里也帖一份。不太敢使用博客搬家的功能,因为需要向服务器提交另一个 blog 地址的账户信息,那意味着这些程序将拥有完全的权限,甚至删掉以前的东西。事实上,好像有些博客搬家就是这么干的。我需要的实际上是博客镜像而不是搬家,所以我宁可麻烦一点也不去冒这个风险。

以前的文章不会再贴在这里,要访问以前的内容,到:
http://chowroc.blogspot.com/

因为朝廷构建的 GFW,一般情况下是没办法访问的,不过所幸我还知道一些方法可以比较快的访问。不过这里就不广泛传播了,一则意义不大,再则知道的人多了,气味太浓又要被鹰犬们嗅到了 :P

星期四, 十月 11, 2007

Some Distributed Design Principles

Given what we have covered so far, we can define some fundamental design principles which every distributed system designer and software engineer should know. Some of these may seem obvious, but it will be helpful as we proceed to have a good starting list.

* As Ken Arnold says: "You have to design distributed systems with the expectation of failure." Avoid making assumptions that any component in the system is in a particular state. A classic error scenario is for a process to send data to a process running on a second machine. The process on the first machine receives some data back and processes it, and then sends the results back to the second machine assuming it is ready to receive. Any number of things could have failed in the interim and the sending process must anticipate these possible failures.

* Explicitly define failure scenarios and identify how likely each one might occur. Make sure your code is thoroughly covered for the most likely ones.

* Both clients and servers must be able to deal with unresponsive senders/receivers.

* Think carefully about how much data you send over the network. Minimize traffic as much as possible.

* Latency is the time between initiating a request for data and the beginning of the actual data transfer. Minimizing latency sometimes comes down to a question of whether you should make many little calls/data transfers or one big call/data transfer. The way to make this decision is to experiment. Do small tests to identify the best compromise.

* Don't assume that data sent across a network (or even sent from disk to disk in a rack) is the same data when it arrives. If you must be sure, do checksums or validity checks on data to verify that the data has not changed.

* Caches and replication strategies are methods for dealing with state across components. We try to minimize stateful components in distributed systems, but it's challenging. State is something held in one place on behalf of a process that is in another place, something that cannot be reconstructed by any other component. If it can be reconstructed it's a cache. Caches can be helpful in mitigating the risks of maintaining state across components. But cached data can become stale, so there may need to be a policy for validating a cached data item before using it.

If a process stores information that can't be reconstructed, then problems arise. One possible question is, "Are you now a single point of failure?" I have to talk to you now - I can't talk to anyone else. So what happens if you go down? To deal with this issue, you could be replicated. Replication strategies are also useful in mitigating the risks of maintaining state. But there are challenges here too: What if I talk to one replicant and modify some data, then I talk to another? Is that modification guaranteed to have already arrived at the other? What happens if the network gets partitioned and the replicants can't talk to each other? Can anybody proceed?

There are a set of tradeoffs in deciding how and where to maintain state, and when to use caches and replication. It's more difficult to run small tests in these scenarios because of the overhead in setting up the different mechanisms.

* Be sensitive to speed and performance. Take time to determine which parts of your system can have a significant impact on performance: Where are the bottlenecks and why? Devise small tests you can do to evaluate alternatives. Profile and measure to learn more. Talk to your colleagues about these alternatives and your results, and decide on the best solution.

* Acks are expensive and tend to be avoided in distributed systems wherever possible.

* Retransmission is costly. It's important to experiment so you can tune the delay that prompts a retransmission to be optimal.

From:

http://code.google.com/edu/parallel/dsd-tutorial.html

星期二, 十月 09, 2007

数据库连接与 /etc/hosts 设置

上次数据库出问题后更换了主机,但之后时常出现"Can not connect to database"的错误,考虑了若干中可能,但最有效的还是先查看系统日志 /var/log/message:
Oct  9 10:49:31 shopex mysqld: gethostby*.getanswer: asked for "210.0.168.192.in-addr.arpa IN PTR", got type "A"
...
Oct 9 10:49:32 shopex mysqld: gethostby*.getanswer: asked for "210.0.168.192.in-addr.arpa IN PTR", got type "A"
这说明是地址解析的问题,mysqld 中设置了反向 DNS 解析。一般只需要在 /etc/hosts 中增加相应的记录即可:
192.168.0.210   www

星期六, 九月 29, 2007

福无双至,祸不单行

Prima 的那一套东西实在不敢恭维。本来软件的一个目的就是要和硬件分离,结果他们搞的许可证和硬件绑在一起,令人深受其害。而且其可管理性也不好,只能按单个主机的方式排列,如果想要实现真正的集群,那似乎是没有希望了。

昨天下午 wdwd 重启了一次(搬机器的时候掉电了),本来已经忙了一天了——早上肚子里还在翻江倒海,数据库的硬盘就坏了,想找个替手的人都没有(就我一个 SA),急急忙忙处理了备份恢复,检测确诊,写笔记,中间还时不时抛过来一些杂七杂八的事情,然后又要和机房联系,急急忙忙跑过去把损坏的硬盘拿出来,在机房磨了两个小时,做 ts,——晚上回去还没进门,同事打电话跟我说发现所有的新的订单都变成“待处理”,听说上线的消息已经在各大网站发布(反正没给我说过,也没有接到过消息,反正是他们这些老板出钱,又有关系,他们想怎么干就怎么干好了,何必问我们)。 Prima 没有其他报警机制,只好回去查后台和日志,发现"License not match"。但这个 license 是昨天刚刚申请的正式授权证书,之前是运行“正常”的!

我尝试重新上载那个授权证书,结果报告“序列号不符”。解开证书的压缩包,查看 license.info 中的 serial,确实与 /usr/prima/bin/print_serial 的结果不一样。再次重启这台主机,发现序列号又变了!

从昨天晚上折腾到今天下午(晚上别指望能找到人),最后查出来原因是主机每次启动的时候,板载的那块网卡会自动改变 MAC 地址!!!而 Prima 的许可证要根据所谓 CPU 个数、频率和网卡 MAC 地址来生成序列号,然后授权证书要用到这个序列号,一套很复杂的设置,晕 @_@~~~

最后,只好在系统 /etc/rc.d/rc.local 里面每次启动时自动改一下 MAC 地址:
/sbin/ifconfig eth0 hw ether 00:00:6C:92:4E:19
我后来问 Prima 是不是我更换网卡后把新网卡的 MAC 地址设置和以前一样就可以,回复说没有测过。没有测过!!!其实他们没有测过的东西也不少了,耗了我不少时间精力。我个人感觉 Prima 就是一个坑,一台主机什么都干的单主机排列的方式不利于实现集群和扩展,而且这种许可证方法使得高可用的代价更高了,和硬件绑定也不是什么好事 ... 商业软件,反而让人更不放心。

其实不仅他们没有严格测过,我们现在在生产线上的那些东西,其实大多也都没有严格测过,很多都是急急忙忙逼着上线,最多手工运行看看。大家都在急功近利,想着怎么尽快捞钱,成本就转嫁给下面的人去扛着就是了,用户在分担一点,他们自己就安心了。真正能静下心来做点事情的没几个。

星期五, 九月 28, 2007

trouble shooting for ext3fs with disk error

今天放数据库的主机不能正常运行了,因为以前出过文件系统错误,当时我把数据磁盘 umount,再做 fsck.ext3 /dev/sdb1 后就直接从 journal 恢复了。但今天却不行,当运行 fsck.ext3 /dev/sdb1 的时候,出现
(Attempt to read block from filesystem resulted in short read).  Ignore error?
这样的错误。

于是检查一下系统日志 /var/log/message
Sep 23 04:07:38 db kernel: EXT3-fs error (device sdb1) in start_transaction: Journal has aborted
Sep 23 04:09:31 db kernel: EXT3-fs error (device sdb1) in start_transaction: Journal has aborted
Sep 23 04:28:51 db kernel: EXT3-fs error (device sdb1) in start_transaction: Journal has aborted
Sep 23 04:31:38 db kernel: EXT3-fs error (device sdb1) in start_transaction: Journal has aborted
Sep 23 04:34:01 db kernel: EXT3-fs error (device sdb1) in start_transaction: Journal has aborted
Sep 23 04:37:40 db kernel: EXT3-fs error (device sdb1) in start_transaction: Journal has aborted
......
Sep 23 12:07:13 db kernel: EXT3-fs error (device sdb1) in start_transaction: Journal has aborted
Sep 23 12:07:36 db kernel: EXT3-fs error (device sdb1) in start_transaction: Journal has aborted
Sep 23 12:40:05 db kernel: __journal_remove_journal_head: freeing b_frozen_data
Sep 23 12:40:05 db kernel: __journal_remove_journal_head: freeing b_committed_data
Sep 23 12:40:05 db kernel: __journal_remove_journal_head: freeing b_frozen_data
Sep 23 12:40:59 db kernel: kjournald starting. Commit interval 5 seconds
Sep 23 12:40:59 db kernel: EXT3 FS on sdb1, internal journal
Sep 23 12:40:59 db kernel: EXT3-fs: mounted filesystem with ordered data mode.
Sep 27 02:34:00 db kernel: (scsi1:A:1:0): Unexpected busfree in Data-in phase
Sep 27 02:34:00 db kernel: SEQADDR == 0x82
Sep 27 02:34:00 db kernel: SCSI error : <1 0 1 0> return code = 0x10000
Sep 27 02:34:00 db kernel: end_request: I/O error, dev sdb, sector 272737767
Sep 27 02:34:01 db kernel: SCSI error : <1 0 1 0> return code = 0x10000
Sep 27 02:34:01 db kernel: end_request: I/O error, dev sdb, sector 272737775
Sep 27 02:34:01 db kernel: SCSI error : <1 0 1 0> return code = 0x10000
Sep 27 02:34:01 db kernel: end_request: I/O error, dev sdb, sector 272737783
Sep 27 02:34:01 db kernel: SCSI error : <1 0 1 0> return code = 0x10000
Sep 27 02:34:01 db kernel: end_request: I/O error, dev sdb, sector 272737791
Sep 27 02:34:01 db kernel: SCSI error : <1 0 1 0> return code = 0x10000
Sep 27 02:34:01 db kernel: end_request: I/O error, dev sdb, sector 272737799
Sep 27 02:34:02 db kernel: SCSI error : <1 0 1 0> return code = 0x10000
......
Sep 27 02:34:14 db kernel: SCSI error : <1 0 1 0> return code = 0x8000002
Sep 27 02:34:14 db kernel: Info fld=0x0, Current sdb: sense key Aborted Command
Sep 27 02:34:14 db kernel: end_request: I/O error, dev sdb, sector 272737767
Sep 27 02:34:14 db kernel: (scsi1:A:1): 160.000MB/s transfers (80.000MHz DT, offset 127, 16bit)
Sep 27 02:34:15 db kernel: (scsi1:A:1:0): Unexpected busfree in Data-in phase
Sep 27 02:34:15 db kernel: SEQADDR == 0x56
Sep 27 02:34:15 db kernel: SCSI error : <1 0 1 0> return code = 0x10000
Sep 27 02:34:15 db kernel: end_request: I/O error, dev sdb, sector 272872423
Sep 27 02:34:16 db kernel: SCSI error : <1 0 1 0> return code = 0x10000
Sep 27 02:34:16 db kernel: end_request: I/O error, dev sdb, sector 272872431
......
Sep 27 02:34:30 db kernel: SCSI error : <1 0 1 0> return code = 0x8000002
Sep 27 02:34:30 db kernel: Info fld=0x0, Current sdb: sense key Aborted Command
Sep 27 02:34:30 db kernel: end_request: I/O error, dev sdb, sector 272872639
Sep 27 02:34:30 db kernel: (scsi1:A:1): 160.000MB/s transfers (80.000MHz DT, offset 127, 16bit)
Sep 28 02:35:10 db kernel: (scsi1:A:1:0): Unexpected busfree in Data-in phase
Sep 28 02:35:10 db kernel: SEQADDR == 0x53
Sep 28 02:35:10 db kernel: scsi1:A:15: parity error detected while idle. SEQADDR(0x1) SCSIRATE(0xc2)
Sep 28 02:35:10 db kernel: No terminal CRC packet recevied
Sep 28 02:35:10 db kernel: SCSI error : <1 0 1 0> return code = 0x10000
Sep 28 02:35:10 db kernel: end_request: I/O error, dev sdb, sector 270637663
......
Sep 28 02:35:27 db kernel: SCSI error : <1 0 1 0> return code = 0x8000002
Sep 28 02:35:27 db kernel: Info fld=0x0, Current sdb: sense key Aborted Command
Sep 28 02:35:27 db kernel: end_request: I/O error, dev sdb, sector 120062031
Sep 28 02:35:27 db kernel: EXT3-fs error (device sdb1): ext3_get_inode_loc: unable to read inode block - inode=7503878, block=15007746
Sep 28 02:35:27 db kernel: Aborting journal on device sdb1.
Sep 28 02:35:27 db kernel: SCSI error : <1 0 1 0> return code = 0x8000002
Sep 28 02:35:27 db kernel: Info fld=0x0, Current sdb: sense key Aborted Command
Sep 28 02:35:27 db kernel: end_request: I/O error, dev sdb, sector 273146143
......
Sep 28 02:35:29 db kernel: SCSI error : <1 0 1 0> return code = 0x8000002
Sep 28 02:35:29 db kernel: Info fld=0x0, Current sdb: sense key Aborted Command
Sep 28 02:35:29 db kernel: end_request: I/O error, dev sdb, sector 63
Sep 28 02:35:29 db kernel: EXT3-fs error (device sdb1) in ext3_reserve_inode_write: IO failure
Sep 28 02:35:29 db kernel: SCSI error : <1 0 1 0> return code = 0x8000002
Sep 28 02:35:29 db kernel: Info fld=0x0, Current sdb: sense key Aborted Command
Sep 28 02:35:29 db kernel: end_request: I/O error, dev sdb, sector 272573927
Sep 28 02:35:29 db kernel: SCSI error : <1 0 1 0> return code = 0x8000002
Sep 28 02:35:29 db kernel: Info fld=0x0, Current sdb: sense key Aborted Command
Sep 28 02:35:29 db kernel: end_request: I/O error, dev sdb, sector 272574783
Sep 28 02:35:29 db kernel: ext3_abort called.
Sep 28 02:35:29 db kernel: EXT3-fs error (device sdb1): ext3_journal_start_sb: Detected aborted journal
Sep 28 02:35:29 db kernel: Remounting filesystem read-only
Sep 28 02:35:29 db kernel: EXT3-fs error (device sdb1) in start_transaction: Journal has aborted
Sep 28 02:35:29 db kernel: EXT3-fs error (device sdb1) in start_transaction: Journal has aborted
Sep 28 02:35:29 db kernel: (scsi1:A:1): 160.000MB/s transfers (80.000MHz DT, offset 127, 16bit)
Sep 28 02:35:29 db kernel: EXT3-fs error (device sdb1) in ext3_dirty_inode: IO failure
Sep 28 02:35:29 db kernel: EXT3-fs error (device sdb1) in start_transaction: Journal has aborted
Sep 28 02:35:29 db kernel: __journal_remove_journal_head: freeing b_frozen_data
Sep 28 02:35:29 db kernel: __journal_remove_journal_head: freeing b_frozen_data
Sep 28 02:35:55 db kernel: EXT3-fs error (device sdb1) in start_transaction: Journal has aborted
Sep 28 02:41:25 db kernel: EXT3-fs error (device sdb1) in start_transaction: Journal has aborted
Sep 28 03:10:40 db kernel: EXT3-fs error (device sdb1) in start_transaction: Journal has aborted
Sep 28 03:42:08 db kernel: EXT3-fs error (device sdb1) in start_transaction: Journal has aborted
Sep 28 03:51:37 db kernel: EXT3-fs error (device sdb1) in start_transaction: Journal has aborted
Sep 28 03:54:38 db kernel: EXT3-fs error (device sdb1) in start_transaction: Journal has aborted
......
基本上,可以肯定是磁盘损坏了,出现了坏道!

因为这里是没有使用 RAID 设施的,所以只能使用 rescue 主机替代,这属于高可用(HA: High Availability)的问题了,这里不讨论。回过头来,需要检查和确认这块磁盘的问题。

首先使用另外一个 IP 启动这台主机(因为 HA 已经使用了原来的 IP 地址)。在这里我遇到的情况是在重启时第二块磁盘(/dev/sdb)还是被“正常”挂载了(/dev/sdb1),这意味着你仍然可以从里面把数据拷贝出来,当然最好是只读使用!——运气不错,不是吗?但这并不意味着这块磁盘就没有问题了,需要先进行检查:
sh# fsck.ext3 /dev/sdb1
e2fsck 1.35 (28-Feb-2004)
/dev/sdb1: clean, 39195/17924096 files, 22706784/35843015 blocks
好像没有问题?但真的是这样吗?

应该使用其他的超级块检查一下,因为很可能这个超级块本身就有问题。从 Ext3 文件系统使用磁盘的基本原理上看,一个磁盘被分成若干 groups,每个 groups 包含很多物理 blocks(通常为 4KB,和内存的一个 page 的大小一致。注意,这里和使用 fdisk 是显示的 Blocks 是不一样的,后者是以 1KB 为单元的),通常一个 group 可能包含 8192/16384/32768 个 blocks,取决于磁盘的大小或创建分区文件系统时的设置,而每一个 group 上面都有一个超级块(super block),只有第一个 group 的超级块被实际使用,其他 group 的超级块都作为备用。因此,尝试使用另外一个超级块:
sh# fsck.ext3 -b 8192 /dev/sdb1
e2fsck 1.35 (28-Feb-2004)
fsck.ext3: Bad magic number in super-block while trying to open /dev/sdb1

The superblock could not be read or does not describe a correct ext2
filesystem. If the device is valid and it really contains an ext2
filesystem (and not swap or ufs or something else), then the superblock
is corrupt, and you might try running e2fsck with an alternate superblock:
e2fsck -b 8193

sh# fsck.ext3 -b 8193 /dev/sdb1
e2fsck 1.35 (28-Feb-2004)
fsck.ext3: Bad magic number in super-block while trying to open /dev/sdb1

The superblock could not be read or does not describe a correct ext2
filesystem. If the device is valid and it really contains an ext2
filesystem (and not swap or ufs or something else), then the superblock
is corrupt, and you might try running e2fsck with an alternate superblock:
e2fsck -b 8193
两者都不行!这说明一个 group 的大小不是 8192,第二个 super block 不在第 8193 个 block 上,那么可以通过 tune2fs 来获得文件系统的信息,进而找到下一个超级块。
sh# tune2fs -l /dev/sdb1
tune2fs 1.35 (28-Feb-2004)
Filesystem volume name:
Last mounted on:
Filesystem UUID: 204a2d09-12d5-4623-bb0d-75b2e340401d
Filesystem magic number: 0xEF53
Filesystem revision #: 1 (dynamic)
Filesystem features: has_journal resize_inode filetype sparse_super large_file
Default mount options: (none)
Filesystem state: clean
Errors behavior: Continue
Filesystem OS type: Linux
Inode count: 17924096
Block count: 35843015
Reserved block count: 1792150
Free blocks: 13136231
Free inodes: 17884901
First block: 0
Block size: 4096
Fragment size: 4096
Reserved GDT blocks: 1024
Blocks per group: 32768
Fragments per group: 32768
Inodes per group: 16384
Inode blocks per group: 512
Filesystem created: Sun Jan 7 15:50:06 2007
Last mount time: Fri Sep 28 10:05:06 2007
Last write time: Fri Sep 28 11:01:56 2007
Mount count: 1
Maximum mount count: 35
Last checked: Fri Sep 28 10:05:05 2007
Check interval: 15552000 (6 months)
Next check after: Wed Mar 26 10:05:05 2008
Reserved blocks uid: 0 (user root)
Reserved blocks gid: 0 (group root)
First inode: 11
Inode size: 128
Journal inode: 8
Default directory hash: tea
Directory Hash Seed: dd1084ed-a28f-489e-8e47-c4a3a49f5e6d
Journal backup: inode blocks
可以看出每个块组拥有 32768 个 blocks,所以应该指定 -b 32768 来进行检查:
sh# fsck.ext3 -b 32768 /dev/sdb1
e2fsck 1.35 (28-Feb-2004)
/dev/sdb1 contains a file system with errors, check forced.
Pass 1: Checking inodes, blocks, and sizes
Duplicate blocks found... invoking duplicate block passes.
Pass 1B: Rescan for duplicate/bad blocks
Duplicate/bad block(s) in inode 311389: 712136
Duplicate/bad block(s) in inode 311425: 680600 680633 680634 680635 680636 680637 680638 680639 680641 680642 680643 680644 680645 680646 680647 680665 680666 680667 680668 680669 680670 680671 680673 681752 682704 684104 684352 685688 686064
Duplicate/bad block(s) in inode 311560: 657856 658872 662192 665192 669600 686504 689824 694104 696144 696904 698816 703032
Duplicate/bad block(s) in inode 344144: 705776 706088 706784
Duplicate/bad block(s) in inode 344151: 708952
Duplicate/bad block(s) in inode 344170: 709200
Duplicate/bad block(s) in inode 377354: 810496
Error reading block 22872266 (Attempt to read block from filesystem resulted in short read). Ignore error?
已经报告出现了 bad blocks。

short read 通常意味着文件系统中的一个 i 节点指向一个不能再读取的块,或者关于文件系统的某个元数据位于一个或者几个不能读取的块。对于日志文件系统,如果文件系统的日志的任何一部分存储在一个坏块上,也会出现这个错误,因为不能读取事务。

如果运气不够好,磁盘分区完全不能挂载和读取,可以先尝试把日志删除,再尝试挂接和访问数据。先用 debugfs 来看看文件系统的特性:
sh# debugfs /dev/sdb1
debugfs 1.35 (28-Feb-2004)
debugfs: features
Filesystem features: has_journal resize_inode filetype sparse_super large_file
debugfs: open /dev/sdb1
open: Filesystem /dev/sdb1 is still open. Close it first.
debugfs: open /dev/sdb1
open: Filesystem /dev/sdb1 is still open. Close it first.
debugfs: close /dev/sdb1
close: Usage: close_filesys
debugfs:
debugfs: open /dev/sdb1
open: Filesystem /dev/sdb1 is still open. Close it first.
debugfs: quit
可以看到 has_journal 特性。使用 tune2fs 可以设置和清除文件系统特性。下面的命令删除日志:
sh# tune2fs -f -O ^has_journal /dev/sdb1
tune2fs 1.35 (28-Feb-2004)
sh# debugfs /dev/sdb1
debugfs 1.35 (28-Feb-2004)
debugfs: features
Filesystem features: resize_inode filetype sparse_super large_file
debugfs: open /dev/sdb1
open: Filesystem /dev/sdb1 is still open. Close it first.
debugfs: quit
has_journal 特性已经被清除。这时候,再次运行 fsck.ext3,不使用 -b 指定 super block,也可以看到报错信息:
e2fsck 1.35 (28-Feb-2004)
/dev/sdb1 contains a file system with errors, check forced.
Pass 1: Checking inodes, blocks, and sizes
Error reading block 35750122 (Attempt to read block from filesystem resulted in short read) while doing inode scan. Ignore error?
总之,要从一个损坏的磁盘上恢复数据总是没有保证的,所以备份至关重要——"热镜像 + 轮转拷贝"才是根本之道。

参考:《LINUX SERVER HACKS 卷二》, Hacks #94 "从崩溃的磁盘恢复数据"。

另外,我的项目 cutils包含了 mirrord/fs_mirror 镜像和轮转工具。