Linux上Raid卡的简单配置与使用

使用 RAID 可以提高服务器的性能。不同 RAID 的级别,性能会有所不同。它通过容错和高可用性来保存我们的数据。

下面记录在linux上配置和使用raid卡的过程。

资源

  1. Linux主机
  2. Raid卡(LSI MegaRAID SAS 9271-8i)
  3. 两块希捷1T机械硬盘

工具

  1. Raid工具:MegaCli64或storcli64
  2. fdisk(分区)
  3. mkfs(格式化分区文件系统)
  4. df(查看分区挂载情况)

Raid卡真容

未分类

Raid卡

图中蓝线框出的两个插口是MinSATA的插口,可以连接Mni SATA转接线,接硬盘数据口和备用电源。

未分类

Mini SATA接口线(不是我的手)

安装

未分类
安装结构图

RAID卡插在PCI的扩展槽上,Mini SATA转接线中的数据口接在硬盘上,同时硬盘接电源,转接线的电源线可以不用(备用电源线)。

至此硬件安装基本完成。
要想RAID卡工作还有必不可少的一步就是安装驱动,这里涉及到Linux的Kernel的编译问题。在较新的Kernel中大多已经集成了RAID驱动,这里我们从新编译一下。
cd到Kernel源码目录执行:

make menuconfig

要编译Raid驱动,依赖PCI模块,所以我们需要把PCI support 模块编译进内核。

未分类

进入PCI配置

未分类

按Y选择编译PCI support

之后在”Device Drivers” >>”SCSI device support”>>”SCSI low-level drivers”下在 “LSI Logic MegaRAID SAS RAID Module”上按Y,把此模块编译进kernel中。
最后执行以下命令编译内核

make -j 9

之后改一下grub配置,上传编译好的内核文件,改好启动引导之后就可以重启设备了,不出意外将会系统启动前看到Raid初始化的打印输出。
开机之后运行以下命令可以查看Raid卡参数:

storcli64 /c0 show all#或MegaCli -AdpAllInfo -aAll

双盘创建Raid1:

storcli /c0 add vd raid1 drives=252:5,7 pdperarray=2

创建好之后先用fdisk做好分区然后用mkfs命令格式化分区文件系统,之挂载分区就行了。

后记:

其实搞硬件,最重要的一步就是硬件设备安装的时候一定要装好,要不然….

Linux中grep命令的12个实践例子

你是否遇到过需要在文件中查找一个特定的字符串或者样式,但是不知道从哪儿开始?那么,就请grep来帮你吧。

未分类

grep是每个Linux发行版都预装的一个强有力的文件模式搜索工具。无论何种原因,如果你的系统没有预装它的话,你可以很容易的通过系统的包管理器来安装它(Debian/Ubuntu系中的apt-get和RHEl/CentOS/Fedora系中的yum)。

$ sudo apt-get install grep #Debian/Ubuntu $ sudo yum install grep #RHEL/CentOS/Fedora 

我发现使用现实世界中的真实例子让你投身其中是让你接触grep命令的最容易方式。

1. 搜索和寻找文件

假设你已经在你的电脑上安装了一个全新的Ubuntu,然后你打算卸载Python。你浏览网页寻找教程,但是你发现存在两个不同版本的Python在使用,而你不知道你的Ubuntu安装器到底在你的系统中安装了哪个版本的Python,也不知道它安装了哪些模块。解决这个烦恼只需简单的运行以下命令:

$ sudo dpkg -l | grep -i python 

输出例子

ii  python2.7         2.7.3-0ubuntu3.4 Interactive high-level object-oriented language (version 2.7)
   ii  python2.7-minimal 2.7.3-0ubuntu3.4 Minimal subset of the Python language (version 2.7)
   ii  python-openssl    0.12-1ubuntu2.1  Python wrapper around the OpenSSL library
   ii  python-pam        0.4.2-12.2ubuntu4 A Python interface to the PAM library

首先,我们运行dpkg -l列出你系统上安装的.deb包。接着,我们使用管道将输出结果传输给命令grep -i python,这一步可以简单解释为把结果传输给grep然后过滤出所有含有python的项,并返回结果。–i选项用于忽略大小写,因为 grep 是大小写敏感的。使用选项-i是个好习惯,除非你打算进行更细节的搜索。

2. 搜索和过滤文件

grep还可以在一个或多个文件里用于搜索和过滤。让我们来看一个这样的情景:

你的Apache网页服务器出现了问题,你不得不从许多专业网站里找一个发帖询问。好心回复你的人让你粘贴上来你的/etc/apache2/sites-available/default-ssl文件内容。假如你能移除掉所有的注释行,那么对你,对帮你的人,以及所有阅读该文件的人,不是更容易发现问题吗?你当然可以很容易的做到!只需这样做就可以了:

$ sudo grep -v "#" /etc/apache2/sites-available/default-ssl 

选项-v是告诉grep命令反转它的输出结果,意思就是不输出匹配的项,做相反的事,打印出所有不匹配的项。这个例子中,有#的是注释行(译注:其实这个命令并不准确,包含“#”的行不全是注释行。关于如何精确匹配注释行,可以了解更多的关于正则表达式的内容。)。

3. 找出所有的mp3文件

grep命令对于过滤来自于标准输出的结果非常有用。例如,假设你的一个文件夹里面全是各种格式的音乐文件。你要找出艺术家jayZ的所有mp3格式的音乐文件,里面也不要有任何混合音轨。使用find命令再结合管道使用grep就可以完成这个魔法:

$ sudo find . -name ".mp3" | grep -i JayZ | grep -vi "remix"" 

在这个例子中,我们使用find命令打印出所有以.mp3为后缀名的文件,接着将其使用管道传递给grep -i过滤和打印出名字为“JayZ”的文件,再使用管道传送给grep -vi以便过滤掉含有“remix”的项。

35个Linux中find命令的实践例子: http://www.tecmint.com/35-practical-examples-of-linux-find-command/

4. 在搜索字符串前面或者后面显示行号

另外两个选项是-A和-B之间的切换,是用以显示匹配的行以及行号,分别控制在字符串前或字符串后显示的行数。Man页给出了更加详细的解释,我发现一个记忆的小窍门:-A=after、-B=before。

$ sudo ifconfig | grep -A 4 etho $ sudo ifconfig | grep -B 2 UP

5. 在匹配字符串周围打印出行号

grep命令的-C选项和例4中的很相似,不过打印的并不是在匹配字符串的前面或后面的行,而是打印出两个方向都匹配的行(译注:同上面的记忆窍门一样:-C=center,以此为中心): $ sudo ifconfig | grep -C 2 lo

6. 计算匹配项的数目

这个功能类似于将grep输出的结果用管道传送给计数器(wc程序),grep内建的选项可以达到同样的目的:

$ sudo ifconfig | grep -c inet6 

7. 按给定字符串搜索文件中匹配的行号

当你在编译出错时需要调试时,grep命令的-n选项是个非常有用的功能。它能告诉你所搜索的内容在文件的哪一行:

$ sudo grep -n "main" setup.py 

8. 在所有目录里递归的搜索

假若你要在当前文件夹里搜索一个字符串,而当前文件夹里又有很多子目录,你可以指定一个-r选项以便于递归的搜索: $ sudo grep -r “function” *

9. 进行精确匹配搜索

传递-w选项给grep命令可以在字符串中进行精确匹配搜索(译注:包含要搜索的单词,而不是通配)。例如,像下面这样输入:

$ sudo ifconfig | grep -w “RUNNING” 

将打印出含有引号内匹配项的行。另外,你还可以试一下这个:

$ sudo ifconfig | grep -w “RUN” 

搜索这个匹配项时,若搜索的东西里面没有这样的一个单独的单词,将什么也不会返回。

10. 在Gzip压缩文件中搜索

我们还要关注一下grep的衍生应用。第一个是zgrep,这个与zcat很相似,可以用于gzip压缩过的文件。它有与grep相似的命令选项,使用方式也一样:

$ sudo zgrep -i error /var/log/syslog.2.gz 

11. 在文件中匹配正则表达式

egrep是另一个衍生应用,代表着“扩展全局正则表达式”。它可以识别更多的正则表达式元字符,例如at + ? | 和()。在搜索源代码文件时,egrep是一个非常有用的工具,还有其他的一些零碎代码文件的搜索需要,使得这样的搜索能力成为必需。可以在grep命令中使用选项-E来启用它。

$ sudo grep -E

12. 搜索一个固定匹配字符串

fgrep用于在一个文件或文件列表中搜索固定样式的字符串。功能与grep -F同。fgrep的一个通常用法为传递一个含有样式的文件给它:

$ sudo fgrep -f file_full_of_patterns.txt file_to_search.txt 

这仅仅是grep命令的开始,你可能已经注意到,它对于实现各种各样的需求简直是太有用了。除了这种我们运行的这种只有一行的命令,grep还可以写成cron任务或者自动的shell脚本去执行。保持好奇心,试验一下man页的各个选项,为实现你的目的写出一些grep表达式吧。

Linux 查看空间使用情况

在日常的Linux巡检中,我们会遇到文件系统目录使用空间很高的情况,例如如下利用”df -h “查看到根目录空间使用超过80%。而我们仅仅知道是根目录空间使用过高,这样是不够的。还需要知道是目录还是文件让根目录空间使用过高。通常我们使用的命令是”du -sh *”。

第一步:查看Linux系统的文件系统使用情况,如下可以看到根目录”/”已经使用81%。

[root@hostname ~]# df -h
Filesystem            Size  Used Avail Use% Mounted on
/dev/sda6              67G   51G   13G  81% /
tmpfs                  16G  152K   16G   1% /dev/shm
/dev/sda3              99G  188M   94G   1% /arch
/dev/sda1             197G  188M  187G   1% /u01
/dev/sda2             197G   50G  138G  27% /u02

第二步:进入根目录,利用du -sh 命令查询各个目录或者文件占用空间的情况,因为/u02是独立的文件系统,所以可以忽略。我们关注的是root这个目录,占用了空间47G。

[root@hostname ~]# cd /

[root@hostname /]# du -sh *
20K     arch
7.6M    bin
27M     boot
324K    dev
36M     etc
36K     home
136M    lib
26M     lib64
16K     lost+found
4.0K    media
0       misc
4.0K    mnt
0       net
8.0K    opt
4.0K    orbit-root
--省略/proc目录统计的报错
0       proc
47G     root    ##根目录文件系统使用最大的目录
15M     sbin
0       selinux
4.0K    srv
0       sys
80K     tmp
20K     u01
49G     u02

第三步:进入root目录,其实也是root用户的主目录。我们从ls命令可以猜到可能是【apache-tomcat-6.0.36-linux】这个tomcat的程序目录占用比较多。

[root@hostname /]# cd root
[root@hostname ~]# ls
anaconda-ks.cfg  apache-tomcat-6.0.36-linux  Desktop  Documents  Downloads  install.log  install.log.syslog  Music  Pictures  Public  Templates  Videos

第四步:通过统计发现,确实是【apache-tomcat-6.0.36-linux】这个目录占用了47G。但是到此问题还没有结束,因为既然是应用程序的目录,根据我的经验很可能是tomcat的日志文件占用空间比较多。

[root@hostname ~]# du -sh *
4.0K    anaconda-ks.cfg
47G     apache-tomcat-6.0.36-linux
4.0K    Desktop
4.0K    Documents
4.0K    Downloads
56K     install.log
12K     install.log.syslog
4.0K    Music
4.0K    Pictures
4.0K    Public
4.0K    Templates
4.0K    Videos

第五步:既然看到有tomcat的目录,很可能这个tomcat运行着,利用ps命令查看是否有tomcat的进程,一般可以过滤java这个关键词。如下,果然有tomcat在运行着。那么很可能占用空间最多就是tomcat的日志文件。

[root@hostname ~]# ps -ef|grep java
root      4879  4836  0 08:26 pts/0    00:00:00 grep java
root     11864     1 47 Aug16 ?        26-03:11:29 /usr/bin/java -Djava.util.logging.config.file=/root/apache-tomcat-6.0.36-linux/conf/logging.properties -Xms512m -Xmx1024m -XX:PermSize=128M -XX:MaxPermSize=256m -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djava.endorsed.dirs=/root/apache-tomcat-6.0.36-linux/endorsed -classpath /root/apache-tomcat-6.0.36-linux/bin/bootstrap.jar -Dcatalina.base=/root/apache-tomcat-6.0.36-linux -Dcatalina.home=/root/apache-tomcat-6.0.36-linux -Djava.io.tmpdir=/root/apache-tomcat-6.0.36-linux/temp org.apache.catalina.startup.Bootstrap start

第六步:进入tomcat的程序目录,查看哪个目录占用的空间最大。如下,发现是一个【fare】的目录占用了46G,而【logs】是日志目录却占用了23M。

[root@hostname ~]# cd apache-tomcat-6.0.36-linux
[root@hostname apache-tomcat-6.0.36-linux]# ls
backup  bin  conf  fare  lib  LICENSE  logs  NOTICE  RELEASE-NOTES  RUNNING.txt  temp  webapps  work
[root@hostname apache-tomcat-6.0.36-linux]# du -sh *
92K     backup
882M    bin
108K    conf
46G     fare
6.8M    lib
40K     LICENSE
23M     logs
4.0K    NOTICE
12K     RELEASE-NOTES
16K     RUNNING.txt
7.9M    temp
188M    webapps
7.1M    work

第七步:继续一探究竟【fare】目录里面到底存放的是什么类型的目录或者文件。如下看到有很多类似日志文件,如common.log和pricing.log。通过和开发人员沟通之后,确定确实日志文件。

[root@hostname apache-tomcat-6.0.36-linux]# cd fare
[root@hostname fare]# ls
common.log             common.log.2017-09-21  common.log.2017-10-03     pricing.log.2017-09-10  pricing.log.2017-09-22  pricing.log.2017-10-04
common.log.2017-09-10  common.log.2017-09-22  common.log.2017-10-04     pricing.log.2017-09-11  pricing.log.2017-09-23  pricing.log.2017-10-05
common.log.2017-09-11  common.log.2017-09-23  common.log.2017-10-05     pricing.log.2017-09-12  pricing.log.2017-09-24  pricing.log.2017-10-06
common.log.2017-09-12  common.log.2017-09-24  common.log.2017-10-06     pricing.log.2017-09-13  pricing.log.2017-09-25  pricing.log.2017-10-07
common.log.2017-09-13  common.log.2017-09-25  common.log.2017-10-07     pricing.log.2017-09-14  pricing.log.2017-09-26  pricing.log.2017-10-08
common.log.2017-09-14  common.log.2017-09-26  common.log.2017-10-08     pricing.log.2017-09-15  pricing.log.2017-09-27  pricing.log.2017-10-09
common.log.2017-09-15  common.log.2017-09-27  common.log.2017-10-09     pricing.log.2017-09-16  pricing.log.2017-09-28  spring.log
common.log.2017-09-16  common.log.2017-09-28  common.zip                pricing.log.2017-09-17  pricing.log.2017-09-29  struts2.log
common.log.2017-09-17  common.log.2017-09-29  framework.log             pricing.log.2017-09-18  pricing.log.2017-09-30  xwork2.log
common.log.2017-09-18  common.log.2017-09-30  hibernate.log             pricing.log.2017-09-19  pricing.log.2017-10-01
common.log.2017-09-19  common.log.2017-10-01  hibernate.log.2017-01-03  pricing.log.2017-09-20  pricing.log.2017-10-02
common.log.2017-09-20  common.log.2017-10-02  pricing.log               pricing.log.2017-09-21  pricing.log.2017-10-03

第八步:继续查看哪个目录或者文件占用的空间最大,发现都是common.log和pricing.log相关的文件占用的空间最大,都是700M以上。至此,已经查明占用根目录”/”空间最多的原因是tomcat的日志文件太多,而且每一个日志文件都很大。

[root@hostname fare]# du -sh *|sort -h
0       framework.log
0       hibernate.log.2017-01-03
0       spring.log
0       struts2.log
0       xwork2.log
4.0K    hibernate.log
17M     common.zip
215M    pricing.log
216M    common.log
667M    pricing.log.2017-10-08
668M    common.log.2017-10-08
674M    common.log.2017-10-05
674M    pricing.log.2017-10-05
678M    common.log.2017-10-07
678M    pricing.log.2017-10-07
679M    common.log.2017-10-06
679M    pricing.log.2017-10-06
683M    common.log.2017-10-04
683M    pricing.log.2017-10-04
690M    pricing.log.2017-09-10
691M    common.log.2017-09-10
711M    common.log.2017-09-12
711M    pricing.log.2017-09-12
719M    pricing.log.2017-09-11
720M    common.log.2017-09-11
737M    common.log.2017-09-30
737M    pricing.log.2017-09-28
737M    pricing.log.2017-09-30
738M    common.log.2017-09-28
742M    common.log.2017-09-29
742M    pricing.log.2017-09-29
744M    pricing.log.2017-10-09
745M    common.log.2017-10-09
751M    common.log.2017-10-01
751M    pricing.log.2017-10-01
754M    common.log.2017-09-13
754M    pricing.log.2017-09-13
760M    common.log.2017-10-03
760M    pricing.log.2017-10-03
783M    pricing.log.2017-10-02
784M    common.log.2017-10-02
791M    pricing.log.2017-09-18
792M    common.log.2017-09-18
797M    common.log.2017-09-27
797M    pricing.log.2017-09-27
804M    common.log.2017-09-17
804M    common.log.2017-09-19
804M    pricing.log.2017-09-17
804M    pricing.log.2017-09-19
808M    common.log.2017-09-26
808M    pricing.log.2017-09-26
815M    pricing.log.2017-09-24
816M    common.log.2017-09-24
821M    common.log.2017-09-14
821M    pricing.log.2017-09-14
821M    pricing.log.2017-09-23
822M    common.log.2017-09-23
826M    common.log.2017-09-25
826M    pricing.log.2017-09-25
827M    common.log.2017-09-16
827M    pricing.log.2017-09-15
827M    pricing.log.2017-09-16
828M    common.log.2017-09-15
831M    common.log.2017-09-22
831M    pricing.log.2017-09-22
851M    common.log.2017-09-21
851M    pricing.log.2017-09-21
860M    common.log.2017-09-20
860M    pricing.log.2017-09-20

第九步:从文件的命令规律,可以看出日志文件日期保留一个月,检查root用户是否有定时删除日志文件计划。如下命令查看所示,确实有每天凌晨03:15进行日志文件删除的计划。但是由于日志文件太多和根目录空间大小原因,导致根目录空间使用比例过高。在有部署自动监控工具,如zabbix等,会自动发送告警。

[root@hostname ~]# crontab -l
15 3 * * * /u02/shell/rm_log_pricing.sh
[root@hostname ~]# cat /u02/shell/rm_log_pricing.sh
find /root/apache-tomcat-6.0.36-linux/bin/fare/  -name "common.log.2*" -mtime +30 | xargs rm -f
find /root/apache-tomcat-6.0.36-linux/bin/fare/  -name "pricing.log.2*" -mtime +30 | xargs rm -f

总结:

从运维的角度,不建议tomcat等应用程序部署在和Linux操作系统相关的文件系统中,尤其是直接部署在根目录下。一般都建议使用独立的文件系统来部署应用程序,无论从IO性能监控,文件系统空间使用监控都是有利的。

Linux Loader 机制与内核模块修复

0x0 前言

在吃着月饼喝着茶的闲暇时间里,突然接到某单位的应急通知。赶往现场进行初步勘察后,发现攻击者手段非常暴力,直接删除系统相关关键模块(包括 PAM 模块、基础内核驱动模块等),导致后续用户无法登录操作系统。

在尝试恢复 PAM 模块后,正常登录系统,但由于缺少基础的核心模块,网络、USB 挂载等仍无法正常使用。在下载部分二进制驱动包进行安装后,重启开始出现问题(截图为后期复现):

未分类

确认 /lib/modules/ 下确实存在此文件,但重启依然出现此问题。

而由于对 Linux Loader 机制的不熟悉,现场并未能解决此问题(菜。同时在不确定攻击者是否也删除了正常业务程序组件的情况下,恢复的成本较大,于是备份关键数据,提取相关样本与日志后,对系统进行重装,恢复正常业务。

0x1 Linux 启动与内核加载

1.1 bootloader

首先,还是从我们的老朋友 BIOS 进行加电自检(POST,Power-On Self-Test)后,开始读取 MBR 记录。MBR 为 512 字节大小的扇区,其中前 446 字节硬编码着 bootloader 程序。BIOS 从 MBR 记录读取 bootloader 程序,将其加载到内存,并运行该程序。

对于不同的操作系统,有不同的 bootloader 程序,一般在安装操作系统时,安装程序会向 MBR 写入 bootloader 程序。如 Windows 操作系统,使用自带的 bootloader 程序。对于 Linux 发行版,一般使用新版的 GRUB2 来生成并写入 bootloader。

bootloader 的工作便是载入内核文件,使操作系统正常运行。

1.2 内核文件

在 Linux 中,由 bootloader 载入的内核文件位于 /boot 节点下:

[root@centos boot]# ls --format=single-column /boot
config-2.6.32-696.el6.x86_64 # 内核编译时启用的配置信息与模块设定
efi # EFI 引导程序
grub # GRUB 引导程序支持
initramfs-2.6.32-696.el6.x86_64.img # 虚拟文件系统,主角 ;-)
symvers-2.6.32-696.el6.x86_64.gz # 驱动模块符号表
System.map-2.6.32-696.el6.x86_64 # 内核符号表
vmlinuz-2.6.32-696.el6.x86_64 # 内核文件

如上,vmlinuz 便是 bootloader 载入的 Linux 内核文件,内核文件主要负责检测基础的硬件设施,并载入相关驱动,这些驱动一般位于 /lib/modules/<内核版本>/ 目录下。

那么 initramfs 是干嘛的?当内核启动时,需要往 /lib/modudles 下读取驱动文件,以支持 SATA、USB 等接口的设备,但是明显 /lib/modules 是存储于 SATA 磁盘中,没有 SATA 驱动的情况下也无法读取挂载,这里就出现了先有鸡还是先有蛋的问题。

而 initramfs 便是来解决这个问题,initramfs 事实上是一个具有根目录结构的文件,其中包含基础的应用程序,以及核心的内核模块,例如 SCSI、SATA、USB 等,用于支持最基础的外部设备。

1.3 解剖 initramfs

新建目录,将 initramfs 拷贝到目标目录下:

[root@centos ~]# mkdir /tmp/initramfs
[root@centos ~]# cp /boot/initramfs-2.6.32-696.el6.x86_64.img /tmp/initramfs/initramfs.img

initramfs 使用 gzip 进行压缩,将其解压:

[root@centos ~]# cd /tmp/initramfs/
[root@centos initramfs]# file initramfs.img 
initramfs.img: gzip compressed data, from Unix, last modified: Wed Oct  4 17:17:17 2017, max compression
[root@centos initramfs]# mv initramfs.img initramfs.gz
[root@centos initramfs]# gzip -d initramfs.gz 

在解压完成后,实际上文件使用 cpio 进行归档,最后我们使用 cpio 将其解压出来:

[root@centos initramfs]# file initramfs
initramfs: ASCII cpio archive (SVR4 with no CRC)

# -i 解压文件
# -d 必要时创建目录
# -H newc 指定类型为 SVR4 的归档文件
# --no-absolute-filenames 不要将文件解压覆盖到绝对路径
[root@centos initramfs]# cpio -i -d -H newc --no-absolute-filenames < initramfs
108638 blocks

从最后的目录结构可以看到 initramfs 的真实内容:

[root@centos initramfs]# ll
total 54432
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 bin
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 cmdline
drwxr-xr-x. 3 root root     4096 Oct  5 22:50 dev
-rw-r--r--. 1 root root       23 Oct  5 22:50 dracut-004-409.el6_8.2
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 emergency
drwxr-xr-x. 7 root root     4096 Oct  5 22:50 etc
-rwxr-xr-x. 1 root root     8989 Oct  5 22:50 init
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 initqueue
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 initqueue-finished
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 initqueue-settled
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 initqueue-timeout
drwxr-xr-x. 7 root root     4096 Oct  5 22:50 lib
drwxr-xr-x. 3 root root     4096 Oct  5 22:50 lib64
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 mount
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 netroot
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 pre-mount
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 pre-pivot
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 pre-trigger
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 pre-udev
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 proc
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 sbin
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 sys
drwxr-xr-x. 2 root root     4096 Oct  5 22:50 sysroot
drwxrwxrwt. 2 root root     4096 Oct  5 22:50 tmp
drwxr-xr-x. 7 root root     4096 Oct  5 22:50 usr
drwxr-xr-x. 4 root root     4096 Oct  5 22:50 var

列一下 lib/modules/<内核版本>/kernel/drivers 下的驱动文件:

[root@centos initramfs]# ls lib/modules/2.6.32-696.el6.x86_64/kernel/drivers/
acpi
ata
atm
auxdisplay
bcma
block
bluetooth
cdrom
<...snippets...>

initramfs 中包含最核心的驱动程序,bootloader 在启动时,将 initramfs 临时挂载到根目录,在内核文件正常加载驱动后,对其进行卸载,并挂载真正的根目录节点。

最后,内核程序启动 init / systemd 对系统服务进行加载。

1.4 bootloader 配置

在 Linux 发行版下,bootloader 通常由 GRUB 程序进行管理,而 bootloader 又负责加载内核文件,较新的发行版一般使用新的 GRUB2 进行引导。

由于笔者使用的是 CentOS 6.X,所以此时默认还是旧版的 GRUB 引导管理程序,但不同版本配置上大同小异,所以以旧版为例。

默认情况下,/boot/grub 下(新版为/boot/grub2)存放 GRUB 的配置文件:

[root@centos ~]# ls /boot/grub
grub.conf # GRUB 的引导菜单配置文件
stage1
stage2
<...snippets...>

可以注意到目录下有 stage1 和 stage2 文件,这个其实便是 bootloader 程序。由于 MBR 区域只有可怜的 512 字节,而 bootloader 程序相对都会比较大,所以 GRUB 将启动分为两个阶段,真正存在于 MBR 处的代码便是 stage1 程序,再由此加载真正的 bootloader 程序 stage2。

再看看主配置文件 /boot/grub/grub.conf 的内容:

# grub.conf generated by anaconda

default=0
timeout=5

title CentOS 6 (2.6.32-696.el6.x86_64)
    root (hd0,0)
    kernel /vmlinuz-2.6.32-696.el6.x86_64 ro root=/dev/mapper/vg_centos-lv_root nomodeset rd_NO_LUKS LANG=en_US.UTF-8 rd_LVM_LV=vg_centos/lv_swap rd_NO_MD SYSFONT=latarcyrheb-sun16 crashkernel=auto rd_LVM_LV=vg_centos/lv_root  KEYBOARDTYPE=pc KEYTABLE=us rd_NO_DM rhgb quiet
    initrd /initramfs-2.6.32-696.el6.x86_64.img

通过配置项名称应该都可以猜到相关的功能,例如 default 设置默认的启动菜单为 Centos 6 项,默认超时为 5 秒钟。

在 title 中,则定义了菜单项:

root (hd0,0)
kernel /vmlinuz-2.6.32-696.el6.x86_64 ro root=/dev/mapper/vg_centos-lv_root nomodeset rd_NO_LUKS LANG=en_US.UTF-8 rd_LVM_LV=vg_centos/lv_swap rd_NO_MD SYSFONT=latarcyrheb-sun16 crashkernel=auto rd_LVM_LV=vg_centos/lv_root KEYBOARDTYPE=pc KEYTABLE=us rd_NO_DM rhgb quiet
initrd /initramfs-2.6.32-696.el6.x86_64.img

root 指定 /boot 节点的位置,如果是多个磁盘,则从 hd0、hd1 等依次排列。后面的 0 代表所在的分区位置。

GRUB 从 0 开始索引分区,新版 GRUB2 则从 1 开始索引分区,磁盘索引一致,都是从 0 开始。

kernel 指令负责载入内核文件,包括载入的根目录位置、语言、键盘等参数。

initrd 负责挂载 initramfs 虚拟文件系统。

0x2 内核模块依赖重建

2.1 内核模块

Linux 中的内核模块通常与内核进行分离,作为独立的模块进行加载,当然也可以将其编译到内核中。独立的内核模块存放于 /lib/modules/<内核版本> 下。

该目录下的文件 modules.dep,记录了内核模块位置、依赖等信息。对于现在各种各样的设备驱动,手工配置依赖信息非常繁琐,所以当加入驱动后,可以通过 depmod 自动生成依赖信息:

depmod -a
依赖信息会被写入到 modules.dep 文件中,内核通过该文件对驱动进行正确加载。

2.2 问题重现

从引导过程中可以发现,vmlinuz 内核主要依赖 /lib/modules 下的内核驱动来对外部设备提供支持。

而在这起事件中,攻击者恶意删除了/lib/modules 下的所有文件。

于是在后续安装驱动中,/lib/modules 的内容被同步到 initramfs 中,但此时目录下已然没有完整的驱动信息。所以内核在挂载 initramfs 后,无法正常加载驱动程序,提示无法找到 modules.dep 文件,此时并非读取真正根目录下的 /lib/modules :-)。

2.3 修复内核模块

对于 /lib/modules 下的内核模块,正常通过编译 Linux 内核来生成,当然也可以用过 yum / apt 等在线包管理工具来重新安装 Linux 内核。

在机器与网络隔离的情况下,无法通过网络下载编译完的二进制文件,也许只能通过自行编译内核来修复。

但编译内核也需要对应的编译工具、环境、源码包等。另一种更简单的方式,可以下载对应的发行版的镜像,来进行修复。

首先通过镜像引导,进入 rescue 救援模式终端,由于发行版的版本、内核型号都相同,同时镜像也是跑在相同的机器上,所以载入的驱动也是一致。

通常进入救援模式时,都会自动帮你挂载原机的系统分区,例如 CentOS 将其挂在到 /mnt/sysimage 下。

1)先将所有驱动模块拷贝到原本的系统节点:

cp -R /lib/modules/2.6.32-696.el6.x86_64 /mnt/sysimage/lib/modules/

2)切换 /mnt/sysimage 为根目录:

chroot /mnt/sysimage

此时访问 / 下的任何内容,实际上为 /mnt/sysimage 。

3)进入 /boot 节点,重构 initramfs 镜像,记得备份:

cd /boot
cp initramfs-2.6.32-696.el6.x86_64.img initramfs-2.6.32-696.el6.x86_64.org

# -f 覆盖原始 initramfs 镜像
# -v 显示详细信息
dracut -f -v initramfs-2.6.32-696.el6.x86_64.img 2.6.32-696.el6.x86_64

dracut 用于重构 initramfs 镜像,最后的内核版本号要对应 /lib/modules 下的版本。

4)如果过程中修改了其他文件(例如修改了 shadow 等文件),由于 SELinux 的缘故,需要加入 SELinux 重建标识:

touch /.autorelabel

重启,首次进入时,等待 SELinux 重新建立策略:

未分类

再次重启后,正常进入系统,基本模块修复:

未分类

0x4 总结

  • 内核通过 initramfs 载入核心驱动为基础的外部设备提供支持
  • 内核模块位于 /lib/modules/<内核版本> 下,modules.dep 记录模块索引,使用 depmod -a 可以自动建立依赖信息
  • 使用 dracut 可以对 initramfs 进行重建,将 /lib/modules/ 下的核心模块打包到虚拟文件系统当中
  • 在 SELinux 的上下文下,需要建立 /.autorelabel 通知 SELinux 重新建立策略

总体而言都是在干运维的活,但是 Loader 的加载机制还是十分有趣。

mysql手动删除BINLOG的方法

在MySQL中执行以下命令:

PURGE {MASTER | BINARY} LOGS TO ‘log_name'

PURGE {MASTER | BINARY} LOGS BEFORE ‘date'

PURGE {MASTER|BINARY} LOGS BEFORE DATE_SUB(NOW(),INTERVAL 7 DAY);

例如:

mysql>PURGE MASTER LOGS TO ‘mysql-bin.010′;

mysql>PURGE MASTER LOGS BEFORE ‘2008-06-22 13:00:00′;

mysql>PURGE MASTER LOGS BEFORE DATE_SUB( NOW( ), INTERVAL 7 DAY);

Mac下安装Python虚拟环境Virtualenv

virtualenv官方文档对virtualenv的解释是:

virtualenv is a tool to create isolated Python environments.

virtualenv可以创建一个独立的 Python 环境,每个项目都可以有一个专属环境,避免了不同各种包安装冲突以及版本要求问题,可以让你更方便快捷的切换不同 Python 环境,更高效的开发。

pip是 Python 自带的包管理工具。

安装 virtualenv

$ sudo pip install virtualenv

测试virtualenv是否安装成功:

$ mkdir ~/Pyenv
$ cd ~/Pyenv
$ mkvirtualenv env1

安装 virtualenvwrapper

Virtaulenvwrapper是对virtualenv的封装,可以更方便地管理虚拟环境。

$ sudo easy_install virtualenvwrapper

第一次安装完成后需要,先设置WORKON_HOME,即环境的存储路径,并且运行source /usr/local/bin/virtualenvwrapper.sh

$ export WORKON_HOME=~/Pyenv
$ source /usr/local/bin/virtualenvwrapper.sh

把export命令和source命令加入到~/.bash_profile中,每次打开终端就无需初始化了。

$ vim ~/.bash_profile
$ export WORKON_HOME=~/Pyenv
$ source /usr/local/bin/virtualenvwrapper.sh

创建虚拟环境

$ mkvirtualenv env2

环境创建之后,会自动进入该目录,并激活该环境,当前路径前面就会有 (env2)。

列出虚拟环境:

$ lsvirtualenv -b
env1
env2

切换虚拟环境:

$ workon env1

查看环境里安装了哪些包:

$ lssitepackages

复制虚拟环境:

$ cpvirtualenv env1 env3
Copying env1 as env3...

退出虚拟环境:

$ deactivate

删除虚拟环境:

$ rmvirtualenv env2
Removing env2...

至此,Python虚拟环境Virtualenv安装流程完毕,你可以在你自己的虚拟环境下随意安装各种包,不同项目间也不会相互影响了。

使用flask来做一个小应用

提示:博主默认你已经具备了Python的基础知识,已经能够很顺畅的编写一些Python脚本,否则接下来你会比较难看懂。

旧版

这里先给出旧版本的一些使用截图,初始化的时候的样子

未分类

模糊查询

未分类

精确查询

未分类

技术

这个应用比较简单,所使用的技术也比较少,主要有以下技术要点

  • requests模拟请求
  • 正则匹配关键字
  • web.py搭建web环境
  • vue.js做数据自动绑定

是不是很简单?

Flask Web开发基于Python的Web应用开发实战pdf http://www.gooln.com/document/283768.html

在这个小应用中使用web.py的时候目录结构是这样的

未分类

其中static目录里面存放的是静态资源

未分类

结构相当简单

python代码

这里给出全部的Python代码

#!/usr/bin/env python
# coding=utf-8

import requests
import json
import web
import sys
import re

reload(sys)
sys.setdefaultencoding('utf8')

urls = (
    "/", "index",
    "/query", "Query"
)


render = web.template.render('static', cache=False)


class index:
    def GET(self):
        return render.index('static')


class Query:
    def POST(self):
        keywords = str(web.input().get('shopname'))
        url_base = "https://list.tmall.com/search_product.htm?q="+keywords
        headers = {"User-Agent": "iphone7"}

        try:
            result_base = requests.get(url=url_base, headers=headers, timeout=15).content.replace('n', '').replace(' ','')
            infostr = re.findall(r'j_shop_moreshop_more">(.+?)
', result_base) shoplist = [] for item in infostr: scorelist = re.findall(r'">(.+?)<iclass="', item) thisShopname = re.findall(r'(.+?)', item)[0] shoplist.append('{"shopname": "'+ thisShopname +'" , "dsr": "'+scorelist[0]+'", "service": "'+scorelist[1].split('">')[1]+'","ship": "'+scorelist[2].split('">')[1]+'"}') return json.dumps({"code": 0, "rows":list(set(shoplist))}) except Exception, e: print e return json.dumps({"code": -1, "msg": "没查询到相关店铺"}) if __name__ == "__main__": app = web.application(urls, globals()) app.run()

前端HTML代码

$def with (urlbase)

<html lang="zh-CN">

        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="renderer" content="webkit">
        <meta name="viewport" content="width=device-width, initial-scale=1">



        <input type="text" name="shopname">
        <input type="button" value="提交" @click="query">
        <div class="info" v-for="item in shopes" style="border-bottom: #ccc 1px dashed">

店铺:{{ item.shopname }}



描述相符:{{ item.dsr }}
服务态度:{{ item.service }}
物流服务:{{ item.ship }}
<script type="text/javascript" src="$urlbase/jquery.min.js"> <script type="text/javascript" src="$urlbase/vue.js"> <script type="text/javascript" src="$urlbase/index.js">

js代码

var mainVM = new Vue({
    el: 'body',
    data: {
        shopes:[
            {
                shopname:'未查询',
                dsr:'未查询',
                service:'未查询',
                ship:'未查询'
            }
        ]
    },
    methods:{
        query:function(){
            var _self = this,keyword = $('input[name="shopname"]').val();
            $.post('/query',{"shopname":keyword},function (data) {
                if(data.code == 0){
                    _self.shopes = [];

                    for(var k in data.rows){
                        var thisdata = JSON.parse(data.rows[k]);
                        _self.shopes.push({
                            shopname:thisdata.shopname,
                            dsr:thisdata.dsr,
                            service:thisdata.service,
                            ship:thisdata.ship
                        })
                    }
                }else{
                    alert('查询出错,错误信息:'+data.msg);
                }
            },"json");
        }
    }
});

可以说代码部分也是相当简单,前端HTML和js的代码就不解释了,很容易看懂,这里只对app.py做简单的解释。

观察天猫的搜索页面,发现天猫pc端跟手机端页面都可以轻松抓取,但是使用手机端页面会更加快速方便,因为结构上更加清晰,而且数据量少,抓取速度更快

如何实现只抓取手机端页面的数据呢?很简单,这里我们只需要定义以下HTTP的请求头信息就可以了,也就是headers,如下定义

headers={“User-Agent”:”iphone7″}

  
天猫的搜索链接是使用的get请求,地址为

”https://list.tmall.com/search_product.htm?q=”+keywords

参数只需要传入一个关键字就可以了,前端使用ajax把数据POST给服务端,服务端接收使用下面的这句话

keywords=str(web.input().get(‘shopname’))

是不是马上就搞定了关键的几步了?接下来发起请求拿到数据就可以了

result_base=requests.get(url=url_base,headers=headers,timeout=15).content.replace(‘n’,”).replace(”,”)

注意,这里我把返回的结果中的换行跟空格都去掉了,因为我这里所需要的数据很简单,为了匹配方便我直接给替换成可空,也就是后面的这个

.replace(‘n’,”).replace(”,”)

然后根据正则匹配的字符串进行遍历组合成结果返回给前端就好了,前端直接使用vue.js进行数据的绑定,几乎不需要DOM操作就可以完成结果列表的渲染,棒!(这里强行安利一波vue.js)

前后端通信使用json进行数据交互,友好而且方便。

重写

上面给出了所需要的技术要点和关键代码,那么现在我需要使用flask重写一遍,当然了,关键部分还是不用变动,只是处理方式上稍微有些差异,如果会用web.py,那么使用flask上手应该是很快的。

1、web.py的处理方式

在使用web.py的时候我们启动一个web服务很简单,通常执行以下命令

python app.py

这样我们就启动了一个web服务,但是这样的话会有很多问题,主要有以下几点

  • 不能关闭终端窗口,否则应用结束,一般用于调试
  • 多个应用的时候公用Python环境会引起冲突

注意:

web.py并不适合高并发的应用,但是作为一般应用还是可以轻松应对的。

以上命令执行后web.py会在8080端口绑定一个web服务,如果你想创建多个应用,那么你应该在后面加上端口号

如果你使用了多个域名指向一台机器的多个应用,那么你应该使用nginx来转发请求,而不是直接输入域名加端口号

在远程vps上运行开发完成的应用时,你可以执行以下命令把web以后台服务的形式运行

nohup python app.py 

这种方式简单粗暴,但是仅仅作为临时方案是可行的,运行上述命令后你可以安心的关掉终端,而且web服务依然在运行,但是一旦重启了服务器,那么就得重新登录vps再次执行命令,不是很方便。

2、flask的处理方式

flask和web.py类似,它自带了一个web服务器,默认绑定在5000端口,但是它本身自带的web服务器并不是很好,安全性也不高,作为开发使用还是足够的,正式生产环境中不太建议直接使用flask自带的web服务。

好了,现在可以开始了,为了解决上面提到几个问题,这里咱们来使用一个新东西,上面说了多应用环境冲突的问题,在这儿可以使用一个叫做“虚拟环境”的东西解决。

“虚拟环境”就是直接复制一个Python的全局环境,但是是独立出来的,你可以在这个环境里面安装各种模块,而且不会影响到Python的全局环境,也就是说如果你把其中的一个“虚拟环境”给玩坏了,起不来了,那么你只需要删掉坏的“虚拟环境”重新创建一个就可以了,这些操作都不会对Python全局环境有任何的影响,安全又方便,下面咱们就来创建一个“虚拟环境”。

博主使用的开发环境是Ubuntu 16.04 并没有自带这个软件,使用下面的命令安装

sudo apt-get install python-virtualenv -y

安装完之后测试下是否安装成功

~$ virtualenv --version
15.0.1

接下来咱们创建一个叫 tmall 虚拟环境用于运行我们的应用

~$ virtualenv tmall
Running virtualenv with interpreter /usr/bin/python2
New python executable in /home/kbdancer/tmall/bin/python2
Also creating executable in /home/kbdancer/tmall/bin/python
Installing setuptools, pkg_resources, pip, wheel...done.

创建的时候会给出创建的位置,如果你需要在指定的目录下面创建虚拟环境,那么你得切换到目标目录,然后执行创建命令,博主这里直接在自己的用户目录下面执行的创建命令,自然就是在用户目录下面生成的一个 tmall 文件夹,文件夹下面自动生成了Python环境

未分类

安装完之后需要将这个环境激活才能使用,执行下面的命令进行激活

~$ source tmall/bin/activate
(tmall) :~$

接着在虚拟环境中安装flask环境(博主默认你的Python全局环境中已经有了easy_install或者pip),博主这里使用pip进行安装

~$ pip install flask

好了,所需要的环境配置完成,接下来就可以开始写小应用了。

3、开始编码

编码这个环节应该是快速而且高效的,上面我们已经给出了旧代码,关键部分直接复制过来就能用,稍微改改就可以跑起来了。

flask默认使用Jinja2作为模板引擎,Jinja2在进行模板渲染的时候通常会识别{{}}中的内容进行填充,但是这里博主遇到了一个尴尬的问题,Vue.js也是使用的{{}}作为标识符进行渲染,这就导致了冲突,访问页面的时候就会出现如图所示的错误

未分类

当然,解决方法还是有的,参考这篇文章进行配置 解决Jinja2与Vue.js的模板冲突 解决思路也比较简单,就是在需要Jinja2渲染的时候添加一个空格,而vue.js渲染的时候则不需要空格,python脚本如下

from flask import Flask, render_template

app = Flask(__name__)
app.jinja_env.variable_start_string = '{{ '
app.jinja_env.variable_end_string = ' }}'

前端HTML代码修改后就成了这样

<html lang="zh-CN">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="renderer" content="webkit">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Hello world</title>
    </head>
    <body>
        <input type="text" name="shopname">
        <input type="button" value="提交" @click="query">
        <div class="info" v-for="item in shopes" style="border-bottom: #ccc 1px dashed">
            <p>店铺:{{item.shopname}}</p>
            <p>描述相符:{{item.dsr}}<br>服务态度:{{item.service}}<br>物流服务:{{item.ship}}</p>
        </div>
        <script type="text/javascript" src="{{ url_for('static', filename='jquery.min.js') }}"></script>
        <script type="text/javascript" src="{{ url_for('static', filename='vue.js') }}"></script>
        <script type="text/javascript" src="{{ url_for('static', filename='index.js') }}"></script>
    </body>
</html>

Jinja2默认会在templates目录下面寻找模板文件,而静态文件比如css,js之类的默认存储在static目录下面,这里我们按照Jinja2的默认设置稍微进行修改,当然,如果你想自定义模板目录或者静态文件的目录也是可以的,只需要稍微的配置下就行了,博主这里按照默认的规则来设置。

很快,我们的小应用就跑起来了

未分类

这里还是需要提到几个关键点:

flask中接收前端传递过来的参数用到的是request对象,前端使用json把数据post到后端,后端使用下面这句进行接收

request.form.get('shopname')

更多详细使用方法参考这个地址 http://www.letiantian.me/2014-06-24-flask-process-post-data/ 接着测试下小应用能不能正常运行

未分类

未分类

OK,测试通过。

4、关于部署

由于这个小应用比较简单,部署起来可以按照常规的部署方式进行,但是并不适合生产环境,所以这里暂时不写如何部署,下次有大型网站案例的时候再详细写如何部署以及优化。

5、完整代码

python部分

#!/usr/bin/env python
# coding=utf-8

from flask import Flask, render_template, request
import requests
import json
import re

app = Flask(__name__)
app.jinja_env.variable_start_string = '{{ '
app.jinja_env.variable_end_string = ' }}'


@app.route('/')
def index():
    return render_template('index.html')


@app.route('/query', methods=['POST'])
def query():
    keywords = request.form.get('shopname')
    url_base = "https://list.tmall.com/search_product.htm?q=" + keywords
    headers = {"User-Agent": "iphone7"}

    try:
        result_base = requests.get(url=url_base, headers=headers, timeout=15).content.replace('n', '').replace(' ', '')
        infostr = re.findall(r'j_shop_moreshop_more">(.+?)
', result_base) shoplist = [] for item in infostr: scorelist = re.findall(r'">(.+?)<iclass="', item) thisShopname = re.findall(r'(.+?)', item)[0] shoplist.append('{"shopname": "' + thisShopname + '" , "dsr": "' + scorelist[0] + '", "service": "' + scorelist[1].split('">')[1] + '","ship": "' + scorelist[2].split('">')[1] + '"}') return json.dumps({"code": 0, "rows": list(set(shoplist))}) except Exception, e: print e return json.dumps({"code": -1, "msg": "没查询到相关店铺"}) if __name__ == "__main__": app.run(debug=True)

HTML部分

<html lang="zh-CN">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="renderer" content="webkit">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Hello world</title>
    </head>
    <body>
        <input type="text" name="shopname">
        <input type="button" value="提交" @click="query">
        <div class="info" v-for="item in shopes" style="border-bottom: #ccc 1px dashed">
            <p>店铺:{{item.shopname}}</p>
            <p>描述相符:{{item.dsr}}<br>服务态度:{{item.service}}<br>物流服务:{{item.ship}}</p>
        </div>
        <script type="text/javascript" src="{{ url_for('static', filename='jquery.min.js') }}"></script>
        <script type="text/javascript" src="{{ url_for('static', filename='vue.js') }}"></script>
        <script type="text/javascript" src="{{ url_for('static', filename='index.js') }}"></script>
    </body>
</html>

JS部分

没有做任何改动,就不贴出来了

总结

写这篇文章的目的一来是复习下flask的一些知识,二来是与web.py做个对比,再者就是给入门的朋友提供一个实战的例子,方便参考。

Flask 中模块化应用的实现

Flask是一个轻量级的Web框架。虽然是轻量级的,但是对于组件一个大型的、模块化应用也是能够实现的,“蓝图”就是这样一种实现。对于模块化应用的实现,在Flask 0.2版本中进行了设计。本文暂时不对“蓝图”做详细的介绍,而是先从0.2版本中的Module类的实现讲起。其实,“蓝图”的实现和Module类的实现很相似。

为什么实现模块化应用

对于大型应用而言,随着功能的不断增加,整个应用的规模也会扩大。按照一定的规则将应用的不同部分进行模块化,不仅能够使整个应用逻辑清晰,也易于维护。例如,在Flask中,你也许想像如下构建一个简单的项目:

/myapplication
    /__init__.py
    /views
        /__init__.py
        /admin.py
        /frontend.py

以上目录结构中,我们将之前的Flask单文件修改成了一个应用包,所有的视图函数都在views下,并且按照功能分为了admin和frontend两个部分。为了实现这种模块化应用的构建,在0.2版本中Flask实现了Module类。这个类实例可以通过注册的方式,在Flask应用创建后添加进应用。

Module类实现了一系列的方法:

  • route(rule, **options)

  • add_url_rule(rule, endpoint, view_func=None, **options)

  • before_request(f)

  • before_app_request(f)

  • after_request(f)

  • after_app_request(f)

  • context_processor(f)

  • app_context_processor(f)

  • _record(func)

以上方法除了add_url_rule和_record外,都可以作为装饰器在自己的模块中使用,这些装饰器都返回一个函数。通过调用_record方法,可以将装饰器返回的函数放到_register_events中。当Flask应用创建之后,通过运行_register_events列表中的函数,可以将这个模块注册到应用中去。

Flask应用怎么注册一个Module

以下我们以一个例子来说明Flask应用怎么注册一个Module。

1. 项目结构

这个简单的例子项目结构如下:

/myapplication
    /__init__.py
    /app.py
    /views
        /__init__.py
        /admin.py
        /blog.py

admin.py和blog.py两个模块的代码如下:

# admin.py
from flask import Module

admin = Module(__name__)

@admin.route('/')
def index():
    return "This is admin page!"

@admin.route('/profile')
def profile():
    return "This is profile page."
# blog.py
from flask import Module
blog = Module(__name__)
@blog.route('/')
def index():
    return "This is my blog!"
@blog.route('/article/<int:id>')
def article(id):
    return "The article id is %d." % id

以上两个模块中,我们首先分别创建了一个Module类,然后像写一般的视图函数一样,为每个模块增加一些规则。之后,可以在创建Flask应用的时候将这些模块引入,就可以注册了。

# app.py
from flask import Flask
from views.admin import admin
from views.blog import blog
app = Flask(__name__)
@app.route('/')
def index():
    return "This is my app."
app.register_module(blog, url_prefix='/blog')
app.register_module(admin, url_prefix='/admin')
if __name__ == '__main__':
    from werkzeug.serving import run_simple
    run_simple('localhost', 5000, app)

在app.py中:

  • 我们首先引入了admin和blog两个Module对象;

  • 之后,我们创建了一个Flask应用app,并且为这个应用增加了一个视图函数;

  • 为了注册模块,我们调用了应用的register_module方法;

  • 最后,从werkzeug.serving中我们调用run_simple方法,用来创建一个本地的服务器用于测试这个Flask应用。

根据以上的步骤,我们就可以测试这个应用。分别以/blog和/admin为URL前缀,就可以访问blog和admin两个模块了。

2. 注册Module时发生了什么

根据上面的例子,只要简单的调用Flask应用的register_module方法,就可以注册一个Module了。关于register_module方法的代码如下:

def register_module(self, module, **options):
    """Registers a module with this application.  The keyword argument
    of this function are the same as the ones for the constructor of the
    :class:`Module` class and will override the values of the module if
    provided.
    """
    options.setdefault('url_prefix', module.url_prefix)
    state = _ModuleSetupState(self, **options)
    for func in module._register_events:
        func(state)

通过以上代码可以发现:

  • 可以通过增加url_prefix来区分不同的Module,这在app注册admin和blog时我们已经看到了;

  • 在注册时,我们创建了一个_ModuleSetupState的类,这个类接收Flask应用和一些参数生成一个state实例。这个实例反映了当前Flask应用的状态。

  • 前面在讲到Module类的时候,我们讲到Module未注册时会将自己模块的一些功能实现都放在_register_events列表中,这些功能实现都是函数形式。当需要将模块注册到某一应用上时,只需要传递关于这个应用信息的参数即可,即就是上面的state实例。这样,通过运行函数,可以讲一些属性绑定到当前应用上去。

以上面例子中不同模块的URL绑定来讲,通过注册,应用app现形成了如下的URL“地图”:

>>> app.url_map
Map([<Rule '/admin/profile' (HEAD, GET) -> admin.profile>,
     <Rule '/admin/' (HEAD, GET) -> admin.index>,
     <Rule '/blog/' (HEAD, GET) -> blog.index>,
     <Rule '/' (HEAD, GET) -> index>,
     <Rule '/blog/article/<id>' (HEAD, GET) -> blog.article>,
     <Rule '/static/<filename>' (HEAD, GET) -> static>]
    )
>>> app.url_map._rules_by_endpoint
{'admin.index': [<Rule '/admin/' (HEAD, GET) -> admin.index>],
'admin.profile': [<Rule '/admin/profile' (HEAD, GET) -> admin.profile>],
'blog.article': [<Rule '/blog/article/<id>' (HEAD, GET) -> blog.article>],
'blog.index': [<Rule '/blog/' (HEAD, GET) -> blog.index>],
'index': [<Rule '/' (HEAD, GET) -> index>],
'static': [<Rule '/static/<filename>' (HEAD, GET) -> static>]
}
>>> app.view_functions
{'admin.index': <function views.admin.index>,
'admin.profile': <function views.admin.profile>,
'blog.article': <function views.blog.article>,
'blog.index': <function views.blog.index>,
'index': <function __main__.index>
}

这样,就可以把不同模块的URL规则放在一起,并在endpoint和视图函数之间形成对应关系。