使用 Python-LDAP 操作 LDAP

周末看到那些排队血拼的人们,不用走进 shopping mall、不用看到那些五颜六色的打折和视觉冲击就能感受到 “节日要到了!”。一年又快结束了,这周完成备份、升级之类的收尾工作,接下来就是6周的假期,没啥大安排,假期第1周去南非德班参加高性能计算会议,回来后和家人短途旅行,然后圣诞节在家休息学点新东西,比如修车什么的,几次痛苦经历告诉我出来玩迟早是要坏的,对于 hiking/camping/road trip/4×4 这几个关键字的爱好者来说懂点维修常识是必须的。废话留到假期再说吧,接下来六周可能没有技术方面的博客更新~

最近对 LDAP 服务器上面的数据做处理,有机会接触了一下 Python-LDAP 这个库和 LDAP/Kerberos. 去除所有打印和错误处理的代码后,用 Python-LDAP 操作 LDAP 的骨干代码其实很简单,就这么几行,唯一遇到的一个小麻烦就是折腾了一个多小时才知道 ‘TRUE’ 要大写(后面有说到)。

安装 Python-LDAP

在 Ubuntu/Debian 下安装 python-ldap 模块:

$ sudo apt-get install python-ldap

在 CentOS/RHEL 下安装 python-ldap 模块:

# yum install python-ldap

创建

创建一条 LDAP 新纪录。有个要注意的地方,我们的 LDAP 有个属性 active,用来判断用户帐号是否是激活的 attrs['active'] = ‘TRUE’,这里的 ‘TRUE’ 不能用小写的 ‘true’,刚开始被 LDAP 管理工具上的小写 ‘true’ 误导,老以为 Python 程序里也应该用小写,结果总报错。

phpLDAPadmin

def ldap_add(firstname, lastname, username):
    l = ldap.open(LDAP_HOST)
    l.protocol_version = ldap.VERSION3
    l.simple_bind(LDAP_BIND, LDAP_PASS)

    cn = firstname + ' ' + lastname
    addDN = "cn=%s,ou=People,dc=vpsee,dc=com" % cn
    attrs = {}
    attrs['objectclass'] = ['top','person','inetOrgPerson','posixAccount','vpseeAccount']
    attrs['cn'] = cn
    attrs['givenName'] = firstname
    attrs['homeDirectory'] = '/home/people/%s' % username
    attrs['loginShell'] = '/bin/bash'
    attrs['sn'] = lastname
    attrs['uid'] = username
    attrs['uidNumber'] = ldap_newuid()
    attrs['gidNumber'] = ldap_getgid()
    attrs['active'] = 'TRUE'
    ldif = modlist.addModlist(attrs)
    l.add_s(addDN, ldif)
    l.unbind_s()

查找和读取

查找和读取一条 LDAP 纪录,比如根据 username 查找出 cn:

def ldap_getcn(username):
    try:
        l = ldap.open(LDAP_HOST)
        l.protocol_version = ldap.VERSION3
        l.simple_bind(LDAP_BIND, LDAP_PASS)

        searchScope = ldap.SCOPE_SUBTREE
        searchFilter = "uid=*" + username + "*"
        resultID = l.search(LDAP_BASE, searchScope, searchFilter, None)
        result_set = []
        while 1:
            result_type, result_data = l.result(resultID, 0)
            if (result_data == []):
                break
            else:
                if result_type == ldap.RES_SEARCH_ENTRY:
                    result_set.append(result_data)
        return result_set[0][0][1]['cn'][0]
    except ldap.LDAPError, e:
        print e

更新

更新一条 LDAP 纪录,比如更新用户状态 active 为 false:

def ldap_deactive(username):
    try:
        l = ldap.open(LDAP_HOST)
        l.protocol_version = ldap.VERSION3
        l.simple_bind(LDAP_BIND, LDAP_PASS)

        deactiveDN = ("cn=%s," + LDAP_BASE) % ldap_getcn(username)
        old = {'active':'TRUE'}
        new = {'active':'FALSE'}
        ldif = modlist.modifyModlist(old, new)
        l.modify_s(deactiveDN, ldif)
        l.unbind_s()
    except ldap.LDAPError, e:
        print e

删除

删除一条 LDAP 纪录:

def ldap_delete(username):
    try:
        l = ldap.open(LDAP_HOST)
        l.protocol_version = ldap.VERSION3
        l.simple_bind(LDAP_BIND, LDAP_PASS)

        deleteDN = ("cn=%s," + LDAP_BASE) % ldap_getcn(username)
        l.delete_s(deleteDN)
    except ldap.LDAPError, e:
        print e

如何判断 Linux 是否运行在虚拟机上

在 WebHostingTalk 论坛上有些国外奸商会把虚拟机当作独立服务器卖,去年7月份的时候就有一位中国同胞上当受骗,并在 WHT 上发帖声讨,证据确凿,甚至连服务商自己也承认,回帖达355篇。这家独立服务器/VPS 提供商 HostATree.com 居然大胆的把 OpenVZ VPS 这种一看就知道是虚拟机的虚拟机当作独立服务器卖,晕,至少也要弄个 VMWare/KVM/Xen HVM 吧(更难发现是虚拟机),用 OpenVZ 这种容器也太欺负人了:)昨天恰好收到网友一封邮件问到了如何判断自己买的是独立服务器还是虚拟机的问题。这里 VPSee 简单介绍一下市面上常用虚拟技术(包括容器技术)的判别小技巧。

判断 OpenVZ/Xen PV/UML

判断 OpenVZ/Xen PV/UML 是最容易的,直接检查 /proc 下的相关目录和文件就可以知道,比如 OpenVZ VPS 上会有 /proc/vz 这个文件;Xen PV 虚拟机上会有 /proc/xen/ 这个目录,并且目录下有一些东西;UML 上打印 /proc/cpuinfo 会找到 UML 标志。写了一个简单的 Python 脚本来检测:

#!/usr/bin/python
# check if a linux system running on a virtual machine (openvz/xen pv/uml)
# written by http://www.vpsee.com

import sys, os

def main():
    if os.getuid() != 0:
        print "must be run as root"
        sys.exit(0)

    # check OpenVZ/Virtuozzo
    if os.path.exists("/proc/vz"):
        if not os.path.exists("/proc/bc"):
            print "openvz container"
        else:
            print "openvz node"

    # check Xen
    if os.path.exists("/proc/xen/capabilities"):
        if (os.path.getsize("/proc/xen/capabilities") > 0):
            print "xen dom0"
        else:
            print "xen domU"

    # check User Mode Linux (UML)
    f = open("/proc/cpuinfo", "r"); t = f.read(); f.close()
    if (t.find("UML") > 0):
        print "uml"

if __name__=="__main__":
    main()

判断 VMware/Xen HVM/KVM

如果使用的是 VMware/Xen HVM/KVM 这样的全虚拟就更难判断一些,最准确的办法是读取 CPUID 来判断,Xen 源代码下面有一段检测是否是 Xen 的 C 语言代码 tools/misc/xen-detect.c,这段代码提供了一个很好的例子,VPSee 重写了代码,用宏替代了函数,增加了对 VMware 和 KVM 的识别,用 gcc 编译后就可以运行:

/*
 * check if a linux system running on a virtual machine (vmware/xen hvm/kvm)
 * written by http://www.vpsee.com
 */
#include stdio.h
#include string.h

#define HYPERVISOR_INFO 0x40000000

#define CPUID(idx, eax, ebx, ecx, edx) \
    asm volatile ( \
        "test %1,%1 ; jz 1f ; ud2a ; .ascii \"xen\" ; 1: cpuid" \
        : "=b" (*ebx), "=a" (*eax), "=c" (*ecx), "=d" (*edx) \
        : "0" (idx) );

int main(void)
{
        unsigned int eax, ebx, ecx, edx;
        char string[13];

        CPUID(HYPERVISOR_INFO, &eax, &ebx, &ecx, &edx);
        *(unsigned int *)(string+0) = ebx;
        *(unsigned int *)(string+4) = ecx;
        *(unsigned int *)(string+8) = edx;

        string[12] = 0;
        if (strncmp(string, "XenVMMXenVMM", 12) == 0) {
                printf("xen hvm\n");
        } else if (strncmp(string, "VMwareVMware", 12) == 0) {
                printf("vmware\n");
        } else if (strncmp(string, "KVMKVMKVM", 12) == 0) {
                printf("kvm\n");
        } else
                printf("bare hardware\n");

        return 0;
}

判断 VirtualBox/Virtual PC

什么?这种家用桌面虚拟机自己装的还会不知道?!如果不知道的话也有办法,在 Linux 下运行 dmidecode 工具然后查找 Manufacturer: innotek GmbH, Manufacturer: Microsoft Corporation 关键字就能对应上 VirtualBox 和 Virtual PC.

用 iptables 屏蔽来自某个国家的 IP

星期六我们一位客户受到攻击,我们的网络监测显示有连续6小时的巨大异常流量,我们立即联系了客户,没有得到回应,我们修改和限制了客户的 VPS,使得个别 VPS 受攻击不会对整个服务器和其他 VPS 用户造成任何影响,我们一直保持这个 VPS 为开通状态(尽管一直受攻击),攻击又持续了24小时,星期天攻击仍在继续,我们忍无可忍,但是仍然无法联系到客户,我们向客户网站的另一负责人询问是否需要我们介入来帮助解决,这位负责人答应后我们立即投入到与 DDoS 的战斗中(我们动态扫描屏蔽坏 IP,现在客户网站已恢复。整个过程很有意思,以后有时间再写一篇博客来描述)。登录到客户 VPS 第一件事情就是查当前连接和 IP,来自中国的大量 IP 不断侵占80端口,典型的 DDoS. 所以第一件事是切断攻击源,既然攻击只攻80端口,那有很多办法可以切断,直接关闭网站服务器、直接用防火墙/iptables 切断80端口或者关闭所有连接、把 VPS 网络关掉、换一个 IP,⋯,等等。因为攻击源在国内,所以我们决定切断来自国内的所有访问,这样看上去网站好像是被墙了而不是被攻击了,有助于维护客户网站的光辉形象:D,那么如何屏蔽来自某个特定国家的 IP 呢?

方法很容易,先到 IPdeny 下载以国家代码编制好的 IP 地址列表,比如下载 cn.zone:

# wget http://www.ipdeny.com/ipblocks/data/countries/cn.zone

有了国家的所有 IP 地址,要想屏蔽这些 IP 就很容易了,直接写个脚本逐行读取 cn.zone 文件并加入到 iptables 中:

#!/bin/bash
# Block traffic from a specific country
# written by vpsee.com

COUNTRY="cn"
IPTABLES=/sbin/iptables
EGREP=/bin/egrep

if [ "$(id -u)" != "0" ]; then
   echo "you must be root" 1>&2
   exit 1
fi

resetrules() {
$IPTABLES -F
$IPTABLES -t nat -F
$IPTABLES -t mangle -F
$IPTABLES -X
}

resetrules

for c in $COUNTRY
do
        country_file=$c.zone

        IPS=$($EGREP -v "^#|^$" $country_file)
        for ip in $IPS
        do
           echo "blocking $ip"
           $IPTABLES -A INPUT -s $ip -j DROP
        done
done

exit 0

好 IP 和坏 IP 都被屏蔽掉了,这种办法当然不高明,屏蔽 IP 也没有解决被攻击的问题,但是是解决问题的第一步,屏蔽了攻击源以后我们才有带宽、时间和心情去检查 VPS 的安全问题。公布一份我们客户被攻击的网络流量图,在18点到0点所有带宽都被攻击流量占用,这时候客户无法登录 VPS,访问者也无法访问网站:

ddos

随机生成 Xen 虚拟机的 MAC 地址

如果不在创建 Xen 虚拟机(domU)的时候指定 MAC 地址的话,Xen 就会随机生成一个 MAC 地址给虚拟机,这样管理员就没办法知道虚拟机的 MAC 地址了,不利于以后带宽的统计和虚拟机的管理,所以最好就在 Xen 虚拟机配置文件中直接指明虚拟机的 MAC 地址,比如:

# vi /etc/xen/vpsuser1
vif = [ "mac=00:16:3e:0c:11:53,ip=172.16.16.200,bridge=xenbr0" ]

IEEE OUI 为 Xen domU 保留了一段 MAC 地址,前3段是 “00-16-3e”,后3段是随机的,其中第1个随机段的第1个 bit 是0(IEEE OUI 给 VMware 保留的前3段地址是 “00-0c-29”,后3段随机)。我们在为用户分配 MAC 地址时候不想有规律的分配(虽然也不会有什么安全问题),所以 VPSee 写了一个简单的 Python 脚本用来随机生成符合 Xen 虚拟机标准的 MAC 地址:

#!/usr/bin/python 
# generates a MAC address for Xen domU
# http://www.vpsee.com
#

import random

mac = [ 0x00, 0x16, 0x3e, random.randint(0x00, 0x7f), 
random.randint(0x00, 0xff), random.randint(0x00, 0xff) ]
s = []
for item in mac:
        s.append(str("%02x" % item))
print ':'.join(s)

逐行读取文本文件的 shell 脚本

网上有很多 shell script 读文本文件的例子,但是都没有讲出故事的全部,只说了一半。举个例子,比如从一个 testfile 文件中读取如下格式的文本行:

$ vi testfile
ls      -a -l /bin |  sort
ls      -a -l /bin |  sort | wc
ls      -a -l |  grep sh | wc
ls      -a -l
ls      -a -l |       sort      |    wc

最常见的一个 line by line 读取文件内容的例子就是:

$ vi readfile
#!/bin/sh

testfile=$1
while read -r line
do
    echo $line
done < $testfile

$ chmod +x readfile
$ ./readfile testfile
ls -a -l /bin | sort
ls -a -l /bin | sort | wc
ls -a -l | grep sh | wc
ls -a -l
ls -a -l | sort | wc

这个例子的问题是读取文本行后,文本格式发生了变化,和原来 testfile 文件的内容不完全一致,空格字符自动被删除了一些。为什么会这样呢?因为 IFS,如果在 shell script 里没有明确指定 IFS 的话,IFS 会默认用来分割空格、制表、换行等,所以上面文本行里多余的空格和换行都被自动缩进了。

如果想要输出 testfile 文件原有的格式,把每行(作为整体)原封不动的打印出来怎么办?这时需要指定 IFS 变量,告诉 shell 以 "行" 为单位读取。

$ vi readfile
#!/bin/sh

IFS="
"

testfile=$1
while read -r line
do
    echo $line
done < $testfile

$ ./readfile testfile
ls      -a -l /bin |  sort
ls      -a -l /bin |  sort | wc
ls      -a -l |  grep sh | wc
ls      -a -l
ls      -a -l |       sort      |    wc     

上面两种方法的输出不是差不多吗,有什么关系呢,第一种还美观一些?关系重大,VPSee 昨天写了一个模拟 shell 的 C 程序,然后又写了一个 shell script 来测试这个 C 程序,这个 script 需要从上面的 testfile 里读取完整一行传给 C 程序,如果按照上面的两种方法会得到两种不同的输入格式,意义完全不同:
$./mypipe ls -a -l | sort | wc
$./mypipe "ls -a -l | sort | wc "
显然我要的是第2种输入,把 "ls -a -l | sort | wc " 作为整体传给我的 mypipe,来测试我的 mypipe 能不能正确识别出字符串里面的各种命令。

如果不用 IFS 的话,还有一种方法可以得到上面第二种方法的效果:

#!/bin/sh

testfile=$1
x=`wc -l $testfile |awk '{print $1}'`

i=1
while [ $i -le $x ]
do
    echo "`head -$i  $testfile | tail -1`"
    i=`expr $i + 1`
done

简单调试 Bash 脚本

用 Bash 写的脚本也可以进行调试,和 Python,Perl 等解释型语言一样。新建一个名为 servinfo 的脚本并增加可执行权限:

$ vi servinfo

#!/bin/bash

echo "Hostname: $(hostname)"
echo "Date: $(date)"
echo "Kernel: $(uname -mrs)"

$ chmod +x servinfo

用 bash -x 来调试上述脚本,Bash 先打印出每行脚本,再打印出每行脚本的执行结果:

$ bash -x servinfo
++ hostname
+ echo 'Hostname: vpsee'
Hostname: vpsee
++ date
+ echo 'Date: Thu Sep  3 19:33:48 SAST 2009'
Date: Thu Sep  3 19:33:48 SAST 2009
++ uname -mrs
+ echo 'Kernel: Linux 2.6.18-128.4.1.el5 i686'
Kernel: Linux 2.6.18-128.4.1.el5 i686

如果想同时打印行号的话,可以在脚本开头加上:

export PS4='+${BASH_SOURCE}:${LINENO}:${FUNCNAME[0]}: '

执行结果为:

$ bash -x servinfo
+ export 'PS4=+${BASH_SOURCE}:${LINENO}:${FUNCNAME[0]}: '
+ PS4='+${BASH_SOURCE}:${LINENO}:${FUNCNAME[0]}: '
++4:5:: hostname
+4:5:: echo 'Hostname: vpsee'
Hostname: vpsee
++4:6:: date
+4:6:: echo 'Date: Thu Sep  3 19:42:06 SAST 2009'
Date: Thu Sep  3 19:42:06 SAST 2009
++4:7:: uname -mrs
+4:7:: echo 'Kernel: Linux 2.6.18-128.4.1.el5 i686'
Kernel: Linux 2.6.18-128.4.1.el5 i686

如果只想调试其中几行脚本的话可以用 set -x 和 set +x 把要调试的部分包含进来:

#!/bin/bash

echo "Hostname: $(hostname)"
set -x
echo "Date: $(date)"
set +x
echo "Kernel: $(uname -mrs)"

这个时候可以直接运行脚本,不需要执行 bash -x 了:

$ ./servinfo
Hostname: vpsee
++ date
+ echo 'Date: Thu Sep  3 19:46:53 SAST 2009'
Date: Thu Sep  3 19:46:53 SAST 2009
+ set +x
Kernel: Linux 2.6.18-128.4.1.el5 i686

如果要调试一个非常复杂的 Bash 脚本的话,建议用专门的调试工具,比如:bashdb

简单调试 Python 程序

在 Python 中也可以像 gcc/gdb 那样调试程序,只要在运行 Python 程序时引入 pdb 模块(假设要调试的程序名为 d.py):

$ vi d.py
#!/usr/bin/python

def main():
        i, sum = 1, 0
        for i in xrange(100):
                sum = sum + i
        print sum

if __name__ == '__main__':
        main()

$ python -m pdb d.py

运行上面的命令后进入以下界面,可以输入类似 gdb 的命令来改变程序的执行流程:

$ python -m pdb 1.py 
> d.py(3)()
-> def main():
(Pdb) 

list 显示程序的最近代码段:

(Pdb) list
  1  	#!/usr/bin/python
  2  	
  3  ->	def main():
  4  		i, sum = 1, 0
  5  		for i in xrange(100):
  6  			sum = sum + i
  7  		print sum
  8  	 
  9  	if __name__ == '__main__':
 10  		main()
[EOF]

next 或者 n 执行下一行代码:

(Pdb) next
> d.py(9)()
-> if __name__ == '__main__':

用 break 在第6行设置一个断点:

(Pdb) break d.py:6
Breakpoint 1 at d.py:6

(Pdb) list
  1  	#!/usr/bin/python
  2  	
  3  	def main():
  4  		i, sum = 1, 0
  5  ->		for i in xrange(100):
  6 B			sum = sum + i
  7  		print sum
  8  	 
  9  	if __name__ == '__main__':
 10  		main()
[EOF]

如果想在函数处设置断点:

(Pdb) break d.main
d.py:3

(Pdb) list
  1  	#!/usr/bin/python
  2  	
  3 B	def main():
  4  ->		i, sum = 1, 0
  5  		for i in xrange(100):
  6  			sum = sum + i
  7  		print sum
  8  	 
  9  	if __name__ == '__main__':
 10  		main()
[EOF]

还可以给断点加条件,比如设置条件只有当 sum > 50 的时候才 break:

(Pdb) break d.py:6, sum > 50
Breakpoint 1 at d.py:6

如果想查看某个变量的值,可以用 pp 命令打印出来:

(Pdb) step
> d.py(5)main()
-> for i in xrange(100):
(Pdb) pp sum
0

可以直接在程序里使用 pdb 模块,import pdb 后 pdb.set_trace():

#!/usr/bin/python
import pdb

def main():
        i, sum = 1, 0
        for i in xrange(100):
                sum = sum + i
        pdb.set_trace()
        print sum

if __name__ == '__main__':
        main()

这样只要运行程序 ./d.py 就可以直接运行到 print sum 处:

$ ./d.py 
> d.py(9)main()
-> print sum
(Pdb) 

总结

命令 用途
break 或 b 设置断点
continue 或 c 继续执行程序
list 或 l 查看当前行的代码段
step 或 s 进入函数
return 或 r 执行代码直到从当前函数返回
exit 或 q 中止并退出
next 或 n 执行下一行
pp 打印变量的值
help 帮助

用 Python 做单词拼写检查

这几天在翻旧代码时发现以前写的注释部分有很多单词拼写错误,这些单词错得不算离谱,应该可以用工具自动纠错绝大部分。用 Python 写个拼写检查脚本很容易,如果能很好利用 aspell/ispell 这些现成的小工具就更简单了。

要点

1、输入一个拼写错误的单词,调用 aspell -a 后得到一些候选正确单词,然后用距离编辑进一步嗮选出更精确的词。比如运行 aspell -a,输入 ‘hella’ 后得到如下结果:
hell, Helli, hello, heal, Heall, he’ll, hells, Heller, Ella, Hall, Hill, Hull, hall, heel, hill, hula, hull, Helga, Helsa, Bella, Della, Mella, Sella, fella, Halli, Hally, Hilly, Holli, Holly, hallo, hilly, holly, hullo, Hell’s, hell’s

2、什么是距离编辑(Edit-Distance,也叫 Levenshtein algorithm)呢?就是说给定一个单词,通过多次插入、删除、交换、替换单字符的操作后枚举出所有可能的正确拼写,比如输入 ‘hella’,经过多次插入、删除、交换、替换单字符的操作后变成:
‘helkla’, ‘hjlla’, ‘hylla’, ‘hellma’, ‘khella’, ‘iella’, ‘helhla’, ‘hellag’, ‘hela’, ‘vhella’, ‘hhella’, ‘hell’, ‘heglla’, ‘hvlla’, ‘hellaa’, ‘ghella’, ‘hellar’, ‘heslla’, ‘lhella’, ‘helpa’, ‘hello’, …

3、综合上面2个集合的结果,并且考虑到一些理论知识可以提高拼写检查的准确度,比如一般来说写错单词都是无意的或者误打,完全错的单词可能性很小,而且单词的第一个字母一般不会拼错。所以可以在上面集合里去掉第一个字母不符合的单词,比如:’Sella’, ‘Mella’, khella’, ‘iella’ 等,这里 VPSee 不删除单词,而把这些单词从队列里取出来放到队列最后(优先级降低),所以实在匹配不了以 h 开头的单词才去匹配那些以其他字母开头的单词。

4、程序中用到了外部工具 aspell,如何在 Python 里捕捉外部程序的输入和输出以便在 Python 程序里处理这些输入和输出呢?Python 2.4 以后引入了 subprocess 模块,可以用 subprocess.Popen 来处理。

5、Google 大牛 Peter Norvig 写了一篇 How to Write a Spelling Corrector 很值得一看,大牛就是大牛,21行 Python 就解决拼写问题,而且还不用外部工具,只需要事先读入一个词典文件。本文程序的 edits1 函数就是从牛人家那里 copy 的。

代码

 
#!/usr/bin/python
# A simple spell checker
# written by http://www.vpsee.com 

import os, sys, subprocess, signal

alphabet = 'abcdefghijklmnopqrstuvwxyz'

def found(word, args, cwd = None, shell = True):
    child = subprocess.Popen(args, 
        shell = shell,  
        stdin = subprocess.PIPE, 
        stdout = subprocess.PIPE, 
        cwd = cwd,  
        universal_newlines = True) 
    child.stdout.readline()
    (stdout, stderr) = child.communicate(word)
    if ": " in stdout:
        # remove \n\n
        stdout = stdout.rstrip("\n")
        # remove left part until :
        left, candidates = stdout.split(": ", 1) 
        candidates = candidates.split(", ")
        # making an error on the first letter of a word is less 
        # probable, so we remove those candidates and append them 
        # to the tail of queue, make them less priority
        for item in candidates:
            if item[0] != word[0]: 
                candidates.remove(item)
                candidates.append(item)
        return candidates
    else:
        return None

# copy from http://norvig.com/spell-correct.html
def edits1(word):
    n = len(word)
    return set([word[0:i]+word[i+1:] for i in range(n)] +                     
        [word[0:i]+word[i+1]+word[i]+word[i+2:] for i in range(n-1)] +
        [word[0:i]+c+word[i+1:] for i in range(n) for c in alphabet] +
        [word[0:i]+c+word[i:] for i in range(n+1) for c in alphabet])

def correct(word):
    candidates1 = found(word, 'aspell -a')
    if not candidates1:
        print "no suggestion"
        return  

    candidates2  = edits1(word)
    candidates  = []
    for word in candidates1:
        if word in candidates2:
            candidates.append(word)
    if not candidates:
        print "suggestion: %s" % candidates1[0]
    else:
        print "suggestion: %s" % max(candidates)

def signal_handler(signal, frame):
    sys.exit(0)

if __name__ == '__main__':
    signal.signal(signal.SIGINT, signal_handler)
    while True:
        input = raw_input()
        correct(input)

更简单的方法

当然直接在程序里调用相关模块最简单了,有个叫做 PyEnchant 的库支持拼写检查,安装 PyEnchant 和 Enchant 后就可以直接在 Python 程序里 import 了:

>>> import enchant
>>> d = enchant.Dict("en_US")
>>> d.check("Hello")
True
>>> d.check("Helo")
False
>>> d.suggest("Helo")
['He lo', 'He-lo', 'Hello', 'Helot', 'Help', 'Halo', 'Hell', 'Held', 'Helm', 'Hero', "He'll"]
>>>

如何查看进程 IO 读写情况?

Linux Kernel 2.6.20 以上的内核支持进程 IO 统计,可以用类似 iotop 这样的工具来监测每个进程对 IO 操作的情况,就像用 top 来实时查看进程内存、CPU 等占用情况那样。但是对于 2.6.20 以下的 Linux 内核版本就没那么幸运了,根据 Stack Overflow 的这篇回帖 给出的方法,VPSee 写了一个简单的 Python 脚本用来在 linux kernel < 2.6.20 下打印进程 IO 状况。

Kernel < 2.6.20

这个脚本的想法很简单,把 dmesg 的结果重定向到一个文件后再解析出来,每隔1秒钟打印一次进程 IO 读写的统计信息,执行这个脚本需要 root:

#!/usr/bin/python
# Monitoring per-process disk I/O activity
# written by http://www.vpsee.com 

import sys, os, time, signal, re

class DiskIO:
    def __init__(self, pname=None, pid=None, reads=0, writes=0):
        self.pname = pname 
        self.pid = pid
        self.reads = 0
        self.writes = 0

def main():
    argc = len(sys.argv)
    if argc != 1:
        print "usage: ./iotop"
        sys.exit(0)

    if os.getuid() != 0:
        print "must be run as root"
        sys.exit(0)

    signal.signal(signal.SIGINT, signal_handler)
    os.system('echo 1 > /proc/sys/vm/block_dump')
    print "TASK              PID       READ      WRITE"
    while True:
        os.system('dmesg -c > /tmp/diskio.log')
        l = []  
        f = open('/tmp/diskio.log', 'r')
        line = f.readline()
        while line:
            m = re.match(\
                '^(\S+)\((\d+)\): (READ|WRITE) block (\d+) on (\S+)', line)
            if m != None:
                if not l:       
                    l.append(DiskIO(m.group(1), m.group(2)))
                    line = f.readline() 
                    continue            
                found = False   
                for item in l:  
                    if item.pid == m.group(2):
                        found = True            
                        if m.group(3) == "READ":
                            item.reads = item.reads + 1 
                        elif m.group(3) == "WRITE":
                            item.writes = item.writes + 1
                if not found:   
                    l.append(DiskIO(m.group(1), m.group(2)))
            line = f.readline()
        time.sleep(1)
        for item in l:
            print "%-10s %10s %10d %10d" % \
                (item.pname, item.pid, item.reads, item.writes)

def signal_handler(signal, frame):
    os.system('echo 0 > /proc/sys/vm/block_dump')
    sys.exit(0)

if __name__=="__main__":
    main()


继续阅读 »

自动监测和重启 FastCGI 服务

昨天有个服务器出了点小问题,PHP FastCGI 进程无缘无故就死在那里了,造成 Nginx 不能和 FastCGI 通信,不能解析 PHP 页面,只能看到 Nginx 的 默认 HTML 页面。登录到服务器检查日志也没有找到原因,重启 FastCGI 后恢复正常。服务器上装了 monit,如果服务关闭的话会自动重启,但是这里 FastCGI 服务并没有关闭,只不过由于某种原因不能和 Nginx 通信,所以 monit 误认为 FastCGI 运行正常没有执行重启 FastCGI 的命令。

今天写了一个 Python 脚本用来后台监测 nginx 的日志,如果在日志里发现 111: Connection refused 或者 104: Connection reset by peer 等错误就 kill 掉所有 php-cgi 进程后重启服务。程序有几个地方需要说明:

1、如果不先 kill 直接运行 /etc/init.d/php-cgi restart 重启服务有时候不管用,因为这时候 php-cgi 只是驻留在内存里的死进程而而已不能执行任何命令,所以需要 kill 全部 php-cgi 进程后再启动;
2、读 nginx 的 log 文件的时候要小心,log 文件通常很大,如果用 python 的普通读文件函数会把整个文件读出来后再处理,速度很慢,也会占用大量内存,所以最好用 tail 截取文件,我们只需要分析最后部分(也是最近)的记录即可;
3、程序通过 /var/run/php_cgi.pid 来判断 PHP FastCGI 是否正在运行,通过 /var/log/nginx/error.log 来判断 Nginx/FastCGI 是否工作正常,每600秒检查一次,如果工作不正常 sleep 2秒后重启 php-cgi;
4、本程序可扩展到重启其他服务,比如通过 /var/run/lighttpd.pid 来判断 lighttpd 是否需要重启;
5、程序中 daemonize 函数来自 Python Cookbook(O’Reilly ) 一书。

程序使用

拷贝下面代码做一定修改以后保存为 checkphpcgi,增加文件可执行权限后用 root 运行程序:

# chmod +x checkphpcgi

# ./checkphpcgi
usage: ./checkphpcgi start|stop|restart

# ./checkphpcgi start

如果想要停止程序:

继续阅读 »