所有由bjmayor发布的文章

关于bjmayor

程序员,码农,php,python,ios,java,android,产品经理,创业,自由职业,兼职。

利用aws服务布署gitlab高可用服务

通过docker搭建镜像

前提是你已经有了docker服务,此处略过。

拉取镜像

sudo docker pull gitlab/gitlab-ce:latest

创建容器

sudo docker run -d -p 8443:443 -p 8081:80 -p 8022:22 \
--name gitlab --restart always \
--volume /home/ubuntu/dockerData/gitlab/config:/etc/gitlab \
--volume /home/ubuntu/dockerData/gitlab/logs:/var/log/gitlab \
--volume /home/ubuntu/dockerData/gitlab/data:/var/opt/gitlab \
gitlab/gitlab-ce:latest

说明:

  1. 因为我用了nginx使用了80端口,所以起了新端口以mapping 容器内的端口。
  2. /home/ubuntu/dockerData/gitlab/config 等三个目录需要事先创建好。

配置nginx

新建/etc/nginx/conf.d/gitlab.conf文件

server {
        listen 80;
        server_name  gitlab.xxxx.com;
        charset utf-8;

        access_log  /var/log/nginx/gitlab.access_log;
        error_log  /var/log/nginx/gitlab.error_log;
        if ($http_x_forwarded_proto = 'http') {
            return 301 https://$server_name$request_uri;
        }
        location / {
                proxy_pass http://127.0.0.1:8081;
                proxy_http_version 1.1;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection 'upgrade';
                proxy_set_header Host $host;
                proxy_cache_bypass $http_upgrade;
    }
}

测试

直接访问gitlab.xxxx.com就已经好了,刚进来要求设置管理员密码。管理员是root
不过这里有个问题,就是数据库配置都是默认配置,使用的容器里给你创建好的数据库,如redis、postgresql等。
为了服务的稳定性,现在都要求把web和数据库分离。下面就是相关的配置说明

自定义相关配置

配置文件都在/etc/gitlab/gitlab.rb文件下。对应的宿主机文件是/home/ubuntu/dockerData/gitlab/config/gitlab.rb
修改完了,在docker中运行gitlab-ctl reconfigure即可。

配置域名

创建了一个test项目,结果发现clone地址是http://78638653e348/root/test.git
这显明有问题。

这里需要修改配置文件, 改配置:

external_url "http://gitlab.example.com"

然后reconfigure就好了。
如果你的域名是http的,到此就结束了。

如果你的域名是https的,可能会和我一样遇到502错误。
针对https的配置是

external_url "https://gitlab.example.com"
nginx['listen_port'] = 80
nginx['listen_https'] = false

同样是reconfigure就好了。

邮件发送服务

搜索smtp即可找到对应的配置项。由于我们主要用的aws服务,他提供了SES发送邮件服务。
我们用的是SES相关的配置。其它如QQ邮箱等,可以查看官方文档

gitlab_rails['smtp_enable'] = true
gitlab_rails['smtp_address'] = "email-smtp.region-1.amazonaws.com"
gitlab_rails['smtp_port'] = 587
gitlab_rails['smtp_user_name'] = "IAMmailerKey"
gitlab_rails['smtp_password'] = "IAMmailerSecret"
gitlab_rails['smtp_domain'] = "yourdomain.com"
gitlab_rails['smtp_authentication'] = "login"
gitlab_rails['smtp_enable_starttls_auto'] = true
gitlab_rails['gitlab_email_from'] = 'gitlab@example.com'
gitlab_rails['gitlab_email_reply_to'] = 'noreply@example.com'

说明:
1. 我在配置过程中一开始没有配置gitlab_email_reply_to选项,结果一直报SMTP错误, 554 Transaction failed: Invalid domain name: '78638653e348' 因为我的gitlab默认的gitlab_email_reply_tonoreply@78638653e348。明确指定之后就正常了。
2. 最后发现78638653e348是做为域名配置在/etc/hosts中的。

保存完后:

1. 进入容器

sudo docker exec -it gitlab /bin/bash

2. 使配置生效

gitlab-ctl reconfigure

3. 测试邮件功能

gitlab-rails console

irb(main):003:0> Notify.test_email('xxxx@xxx.com', 'Message Subject', 'Message Body').deliver_now

使用S3存储数据

S3是由aws提供的分布式存储方案。

搜索store可以迅速定位到相关的配置项

gitlab_rails['artifacts_enabled'] = true
gitlab_rails['artifacts_object_store_enabled'] = true
gitlab_rails['artifacts_object_store_remote_directory'] = "artifacts"
gitlab_rails['artifacts_object_store_connection'] = {
  'provider' => 'AWS',
  'region' => 'us-east-1',
  'use_iam_profile' => true
}

测试
在容器中执行命令: gitlab-rake gitlab:artifacts:migrate

Git LFSGitLab uploads按类似设置,都上传到S3。

备份

备份配置文件

配置文件都在/etc/gitlab目录。由于我们把这个目录mapping到了宿主机的/home/ubuntu/dockerData/gitlab/config目录。

创建在/home/ubuntu/dockerData/gitlab/目录上创建backup.sh

backup=$(date "+etc-gitlab-%s.tar")
sudo sh -c 'umask 0077; tar -cf $backup  config';
# backup to S3
aws s3 cp  $backup s3://yourbucket/backups/config/

每次修改了配置文件后,执行sh backup.sh即可。

备份应用数据

此处我把备份直接备份到S3上。

gitlab_rails['backup_keep_time'] = 604800

 gitlab_rails['backup_upload_connection'] = {
   'provider' => 'AWS',
   'region' => 'us-east-1',
   'aws_access_key_id' => 'aws_access_key_id',
   'aws_secret_access_key' => 'aws_secret_access_key'
 }
 gitlab_rails['backup_upload_remote_directory'] = 'yourbucket/backups/data'

执行docker exec -t gitlab gitlab-rake gitlab:backup:create即可创建一份备份

恢复

恢复配置文件

1. 备份下现有的/etc/gitlab配置文件。

sudo mv /home/ubuntu/dockerData/gitlab/config/ /home/ubuntu/dockerData/gitlab/configgitlab.$(date +%s)

2. 用备份好的配置文件恢复配置文件

sudo tar -xf etc-gitlab-1399948539.tar -C /home/ubuntu/dockerData/gitlab/config/

3. 使新的配置文件生效

gitlab-ctl reconfigure

恢复应用数据

1. 把备份文件拷贝到备份目录。

sudo cp 11493107454_2018_04_25_10.6.4-ce_gitlab_backup.tar /home/ubuntu/dockerData/gitlab/data/backups/

2. 停掉连接了数据库的服务。只留下gitlab。

sudo gitlab-ctl stop unicorn
sudo gitlab-ctl stop sidekiq
# Verify
sudo gitlab-ctl status

3. 从备份文件中恢复数据

ls /var/opt/gitlab/backups/ #看下文件名
cd /var/opt/gitlab/backups/ #进入备份目录
sudo gitlab-rake gitlab:backup:restore BACKUP=1493107454_2018_04_25_10.6.4-ce #备份文件名中的_gitlab_backup.tar不需要填写。

4. 重启服务并检查

sudo gitlab-ctl restart
sudo gitlab-rake gitlab:check SANITIZE=true

参考

  1. 利用docker和gitLab搭建git私有服务器
  2. gitlab smtp配置
  3. 配置gitlab通过smtp发送邮件
  4. 使用对象存储
  5. 文件上传使用S3
  6. 使用外部数据库
  7. 备份
  8. 恢复
  9. 配置url
  10. external_url 502

解决 Android net::ERR_UNKNOWN_URL_SCHEME

问题背景

有天测试人员告诉我通过Android app无法打开h5页面。
我楞了一下,我明明测试过呀。。直到测试人员发个截图过来,我才愰然大悟。
net::ERR_UNKNOWN_URL_SCHEME

原因

Android 打开url时,默认只能识别http和https, 连系统自带的tel://都无法识别。
甚至还有第三方app自定义的url schema。
为了解决,必须在webview里加识别代码。

解决办法

@Override
 public boolean shouldOverrideUrlLoading(WebView view, String url) {
     try {
         if (!url.startsWith("http:") ||!url.startsWith("https:")) {
             Intent intent = new Intent(Intent.ACTION_VIEW,
                     Uri.parse(url));
             startActivity(intent);
             return true;
         }
     }
     catch (Exception e){
         return false;
     }

     view.loadUrl(url);
     return true;
 }

搞定!

sonar、jenkins构建代码检查

安装sonar

预置条件

1)已安装JAVA环境
    版本:JDK1.8
2)已安装有mysql数据库
    版本:mysql5.6以上
3)下载sonarQube与sonar-scanner
    版本:[sonarQube7.3](https://sonarsource.bintray.com/Distribution/sonarqube/sonarqube-7.3.zip)
    版本:[sonar-scanner3.2](https://sonarsource.bintray.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-3.2.0.1227-linux.zip)

创建数据库

创建用户sonar:  
CREATE DATABASE sonar CHARACTER SET utf8 COLLATE utf8_general_ci;  
CREATE USER 'sonar' IDENTIFIED BY 'sonar';  
GRANT ALL ON sonar.* TO 'sonar'@'%' IDENTIFIED BY 'sonar';  
GRANT ALL ON sonar.* TO 'sonar'@'localhost' IDENTIFIED BY 'sonar';  
FLUSH PRIVILEGES;

请不要用sonar用作密码,这里只是个示例!!!!

创建sonar专用用户

$ useradd sonar
$ passwd sonar

修改配置文件

将sonar-7.3.zip上传到服务器,放置到/home/sonar 目录,并解压到当前目录即可。
修改conf目录下的sonar.properties文件
配置参考:
修改数据库连接及用户名、密码和本机IP
sonar.jdbc.username=
sonar.jdbc.password=
sonar.jdbc.url=jdbc:mysql://IP:3306/sonar?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useConfigs=maxPerformance

sonar.web.host=

启动服务

启动sonar
切换到sonar安装目录下 /bin/linux-x86-64
#./sonar.sh start

现在通过9000端口就可以访问web服务了。
我这里启动出了问题。通过./sonar.sh console可以看到问题是Process exited with exit value [es]: 143另外在logs目录有更详细的错误信息。

解决es问题

  1. max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]
$ vim /etc/sysctl.conf
添加vm.max_map_count=262144
$ sysctl -p使配置生效。

配置自启动

创建自启动脚本文件/etc/init.d/sonar

$ vi /etc/init.d/sonar

添加如下内容

#!/bin/sh
#
# rc file for SonarQube
#
# chkconfig: 345 96 10
# description: SonarQube system (www.sonarsource.org)
#
### BEGIN INIT INFO
# Provides: sonar
# Required-Start: $network
# Required-Stop: $network
# Default-Start: 3 4 5
# Default-Stop: 0 1 2 6
# Short-Description: SonarQube system (www.sonarsource.org)
# Description: SonarQube system (www.sonarsource.org)
### END INIT INFO

/usr/bin/sonar $*

添加启动服务

$ ln -s $SONAR_HOME/bin/linux-x86-64/sonar.sh /usr/bin/sonar
$ chmod 755 /etc/init.d/sonar
$ sysv-rc-conf  sonar on

如果没有sysv-rc-conf,请安装apt-get install sysv-rc-conf

安装sonar-scanner

scanner在于jenkins 进行集成的作用是:jenkins通过scanner传入要分析的工程,scanner再将这些分析结果,传给sonargube 进行呈现

在sonar目录下直接解压zip文件
unzip sonar-scanner-cli-3.2.0.1227-linux.zip

配置

和sonar配置一样

sonar.host.url=http://192.168.136.144:9000
sonar.login=admin
sonar.password=admin
sonar.jdbc.username=sonar
sonar.jdbc.password=sonar123
sonar.jdbc.url=jdbc:mysql://192.168.136.144:3306/sonar?useUnicode=true&characterEncoding=utf8

加入PATH

修改.bash_profile文件

PATH=$PATH:$HOME/bin:/home/sonar/sonar-scanner-3.2.0.1227-linux/bin

source .bash_profile 后执行sonar-scanner -h 有输出就是好了。

配置jenkins

插件安装

系统管理->插件管理 搜索sonar, 可以看到SonarQube Scanner for Jenkins 安装即可。

配置插件

进入系统管理,会发现多了个SonarQube servers的配置项。先勾选Enable injection of SonarQube server configuration as build environment variables
填上名字和地址,token在刚进入sonar的Web页面时,会让你生成一个,就使用那个。

进入系统管理->Global Tool Configuration
新增SonarQube Scanner.
Name填上sonar-scanner-3.2
SONAR_RUNNER_HOME填上/home/sonar/sonar-scanner-3.2.0.1227-linux
保存。

使用sonar

目前公司的代码用的编程语言有.net, python, nodejs。
我以python为示测试的sonar。
只能选择在构建过程中增加。

配置好Anyalysis properties

sonar.projectKey=testSonar
sonar.projectName=testSonar
sonar.projectVersion=1.0
sonar.language=python
sonar.sources=.
sonar.login=admin
sonar.password=admin
sonar.exclusions=**/*View.java #用于忽略文件

遇到上传失败问题ERROR: Caused by: Broken pipe (Write failed)
看web.log日志发现是com.mysql.jdbc.PacketTooBigException: Packet for query is too large (19560234 > 16777216).
调整一下。

set global max_allowed_packet = 100*1024*1024;

然后重启mysql和sonar就好了。

sonar支持更多语言

下载对应的插件,直接复制到extensions/plugins目录下。
然后重启sonar即可。
我司用到了.netcore, 感觉这个稍微麻烦点。
直接用jenkins里构建步骤中的SonarQube Scanner for MSBuild - Begin AnalysisSonarQube Scanner for MSBuild - End Analysis没有成功。报错ERROR: SonarQube Scanner for MSBuild executable was not found for
安装全局工具dotnet tool install --global dotnet-sonarscanner,参考文档
最后在shell里用脚本成功了。

dotnet sonarscanner begin /k:key /n:name /v:1.0 /d:sonar.login=user /d:sonar.password=password
dotnet build
dotnet sonarscanner end /d:sonar.login=user /d:sonar.password=password

重置mysql密码遇到问题:mysqld_safe Directory ‘/var/run/mysqld’ for UNIX socket file don’t exists

问题背景

今天准备在服务器搭建sonar+jenkins做代码审查。跟着操作,需要安装mysql数据库。
结果安装的时候发现已经安装了,问了圈人,也不知道谁装的。不知道有没有用做它途。记得可以改密码的。操作了一番

  1. stop mysql

    systemctl stop mysql

  2. 跳过安全检查

    /usr/bin/mysqld_safe –skip-grant-tables &

呃。报错了。
mysqld_safe Directory ‘/var/run/mysqld’ for UNIX socket file don’t exists

解决办法

说没有就创建一个试试。

mkdir -p /var/run/mysqld
chown mysql:mysql /var/run/mysqld

然后再试下

/usr/bin/mysqld_safe –skip-grant-tables &

好了

接着修改密码去。

  1. 登陆

    mysql -u root mysql

  2. 更新密码

    update user set Password = PASSWORD(‘root’) where User =’root’; //5.6
    update user set authentication_string = PASSWORD(‘root’) where User =’root’; //5.7

  3. 刷新权限

    FLUSH PRIVILEGES;

搞定。

安装python+flask的博客系统blog_mini

安装python+flask的博客系统blog_mini

根据指引安装出了点问题。我的环境是mac+python3.5。参考博客
1. 报错,name reload not defined。

这个好解决。增加from importlib import reload(其实没有必要)

2.报错.sys has no attribute setdefaultencodeing。

这个是因为python3.5默认是utf-8了。这个函数干掉了。。所以直接把这行代码干掉。并且之前的reload也是python2.x中为了setdefaultencodeing服务的。所以from importlib import reload 和 reload(sys)也可以干掉了。

3.报错 ImportError: No module named ‘MySQLdb’。

这个其实也没有必要了。python3.中用pymysql替代了。
解决办法时,修改DATABASE_URL=mysql+pymysql://root@127.0.0.1/blog_mini。
增加’+pymysql’后就会用pymysql的api。不过你得安装pymysql,pip install pymysql。
这个比MySQLdb容易安装多了。那个不能通过pip安装。

我fork了一个。修改后的版本:https://github.com/bjmayor/Blog_mini
因为安装了两个项目。把DATABASE_URL改成BLOG_DATABASE_URL了。
另外我搞了个fab发布。可以参考修改下。fab主要是参考的廖雪峰的python教程。

我安装完成的网站: python.go2live.cn

感觉主要是参考的Flask Web开发 基于Python的Web应用开发实战
我会根据这本书的代码对这个代码再做些优化,譬如加上用户权限,加上测试用例,去掉bootstrap的文件,直接引用flask_bootstrap。

同时也会参考wordpress的数据库结构做些优化,再加上图片上传什么的。

慢慢优化吧。懒得从头搞起,就以这个blog_mini为起点了。

  1. 个人喜欢用markdonw语法。。所以第一个要把这个改掉。得支持markdown。

Python模块学习:tempfile

应用程序经常要保存一些临时的信息,这些信息不是特别重要,没有必要写在配置文件里,但又不能没有,这时候就可以把这些信息写到临时文件里。其实很多程序在运行的时候,都会产生一大堆临时文件,有些用于保存日志,有些用于保存一些临时数据,还有一些保存一些无关紧要的设置。在windows操作系统中,临时文件一般被保存在这个文件夹下:C:/Documents and Settings/User/Local Settings/Temp。其实我们最常用的IE浏览器在浏览网页的时候,会产生大量的临时文件,这些临时文件一般是我们浏览过的网页的本地副本。Python提供了一个tempfile模块,用来对临时数据进行操作。查阅Python手册,里面介绍了如下常用的方法:

tempfile.mkstemp([suffix=”[, prefix=’tmp'[, dir=None[, text=False]]]])

mkstemp方法用于创建一个临时文件。该方法仅仅用于创建临时文件,调用tempfile.mkstemp函数后,返回包含两个元素的元组,第一个元素指示操作该临时文件的安全级别,第二个元素指示该临时文件的路径。参数suffix和prefix分别表示临时文件名称的后缀和前缀;dir指定了临时文件所在的目录,如果没有指定目录,将根据系统环境变量TMPDIRTEMP或者TMP的设置来保存临时文件;参数text指定了是否以文本的形式来操作文件,默认为False,表示以二进制的形式来操作文件。

tempfile.mkdtemp([suffix=”[, prefix=’tmp'[, dir=None]]])

该函数用于创建一个临时文件夹。参数的意思与tempfile.mkdtemp一样。它返回临时文件夹的绝对路径。

tempfile.mktemp([suffix=”[, prefix=’tmp'[, dir=None]]])

mktemp用于返回一个临时文件的路径,但并不创建该临时文件。

tempfile.tempdir

该属性用于指定创建的临时文件(夹)所在的默认文件夹。如果没有设置该属性或者将其设为None,Python将返回以下环境变量TMPDIR, TEMP, TEMP指定的目录,如果没有定义这些环境变量,临时文件将被创建在当前工作目录。

tempfile.gettempdir()

gettempdir()则用于返回保存临时文件的文件夹路径。

tempfile.TemporaryFile([mode=’w+b'[, bufsize=-1[, suffix=”[, prefix=’tmp'[, dir=None]]]]])

该函数返回一个 类文件 对象(file-like)用于临时数据保存(实际上对应磁盘上的一个临时文件)。当文件对象被close或者被del的时候,临时文件将从磁盘上删除。mode、bufsize参数的单方与open()函数一样;suffix和prefix指定了临时文件名的后缀和前缀;dir用于设置临时文件默认的保存路径。返回的类文件对象有一个file属性,它指向真正操作的底层的file对象。

tempfile.NamedTemporaryFile([mode=’w+b'[, bufsize=-1[, suffix=”[, prefix=’tmp'[, dir=None[, delete=True]]]]]])

tempfile.NamedTemporaryFile函数的行为与tempfile.TemporaryFile类似,只不过它多了一个delete参数,用于指定类文件对象close或者被del之后,是否也一同删除磁盘上的临时文件(当delete = True的时候,行为与TemporaryFile一样)。

tempfile.SpooledTemporaryFile([max_size=0[, mode=’w+b'[, bufsize=-1[, suffix=”[, prefix=’tmp'[, dir=None]]]]]])

tempfile.SpooledTemporaryFile函数的行为与tempfile.TemporaryFile类似。不同的是向类文件对象写数据的时候,数据长度只有到达参数max_size指定大小时,或者调用类文件对象的fileno()方法,数据才会真正写入到磁盘的临时文件中。 蛮简单、实用的一个模块,不是吗?

python自动化工具从0到invork&ansible

原文出处: classam   译文出处:ictar

很久以前,当我第一次读到“程序员修炼(The Pragmatic Programmer)”时,我读了一些让我真真难以忘怀的建议。

“不要使用手工流程(Don’t Use Manual Procedures)”。

这在Ubiquitous Automation一节。总之,他们希望你将所有的事情都自动化。

麻烦的是,我对于如何真正的让任何东东都自动化并没有太多的想法。当时,我仍困于这样一个世界,其中所有的程序都是一个使用CLI的庞大的Java程序,而我的印象是,命令行界面无非是过去时代的一个垂死残余。

我的一些假装自动化之旅充满了陷阱和自我无能的尴尬故事,而我希望与大家分享这些故事。

bash脚本编程

与命令行系统交互的陷阱之一是,每一个复杂的交互使用一个同样复杂的命令 —— 而对于那些我可能会一次又一次运行的命令,每次我都将不得不记得精确的调用。

大多数时候,这个可以工作,虽然每次都要花不少的时间上谷歌或StackOverflow搜索。

关于幻想小说的一个持久的事情是,你总能看到追随者使用魔杖,但没有魔法书。这是因为他们都是愚蠢的。奇才携带魔法书。

所以,我开始写下我的命令,这样我就不会忘记它们。而不是将这些命令写到便利贴,或笔记本上,我将它们写到我的home目录下。

Python

1
2
3
$ cat work
ssh classam@workcomputer.blorf -P 8888 -p=hunter2

起初,我只想cat这个文件,然后将该命令复制回命令行。

Python

1
2
3
4
5
$ cat work
ssh classam@workcomputer.blorf -P 8888 -p=hunter2
$ ssh classam@workcomputer.blorf -P 8888 -p=hunter2
Welcome to Work Computer. Blorf.

我和你说了,我会分享我尴尬无能的故事。

我很快意识到,没有真的有打算,我写的是bash脚本 – 我只是没有明智地执行它们。如果我只是告诉系统我要像执行程序一样执行这个文件,那么它应该足够明智地工作。

Python

1
2
3
4
$ chmod u+x work
$ ./work
Welcome to Work Computer. Blorf.

 

Hashbang

仅凭这一点是不够好到让这个脚本正常工作的,但我们没有做很多的事来帮助系统弄清楚到底如何运行这个我们传给它的神秘的脚本。

让我们想象下我们创建了一个文件:

Python

1
2
print(“hello”)

这在Python 3中是有效的,但在Python 2中无效。如果我们将这个文件保存为’hello’,那么我们基本不知道怎样运行它。而如果我们尝试运行,计算机将会进行猜测,然后它会猜错。

Python

1
2
3
$ ./hello
idfk, man, that doesn’t look right to me

如果我们将文件命名为hello.py,给我们自己一个方便的线索以便提示自己这个是个怎么样的文件,那么我们可以使用Python解释器来执行它。

Python

1
2
3
4
5
# python hello.py
that’s python 3, man, I’m python 2. Whateva.
# python3 hello.py
hello

好吧,它工作了,但对于程序调用来说压力山大。七十年代的一个Unix创新可以消除这个问题:hashbang。在我们脚本的开头,我们可以准确的声明想使用哪个解释器。

Python

1
2
3
4
5
6
#!python3
print(“hello”)
#!bash
echo “hello”

这里唯一的规则是,编译器必须存在于系统的$PATH变量中。当你在本地运行该脚本时,是挺棒的,但如果你远程运行脚本而不使用一个伪终端会话集时,你或许并没有一个$PATH变量,这将导致你的hashbang声明失败。

解决方法?hashbang可以包含你打算用于程序的解释器的完整路径。

Python

1
2
3
4
5
6
#!/bin/python3
print(“hello”)
#!/bin/bash
echo “hello”

在很多现代编程中,你没有看到这个令人兴奋的技术,因为它并不是引人注目的可便携的 —— 你用来执行一个python程序的解释器的位置,甚至是名字,往往系统与系统之间并不相同。

虽然,bash通常位于/bin/bash,这就是为嘛sun下的每个bash程序都用#!/bin/bash打开。

Bashy Bash

所以,不久以后,我的home目录到处都开始充斥着名字诸如work.shtest.sh这样的有用的小的bash文件。

Alias

如果你在Django中编程,你可能还记得,你可以在Django环境中调用的每个命令都是通过直接引用程序manage.py开始的。

我也许像这样启动Django开发服务器:

Python

1
2
3
$ cd ~/code/django_project
$ ./manage.py runserver 0:8000

这很简单,但是,一天内我必须启动那个Django开发服务器无数次!

所有一切仅需对我的.bashrc文件稍微做个调整:

Python

1
2
alias dj=”/home/classam/code/django_project/manage.py”

突然之间,无论我当前的工作目录是什么,都可以用下面这个简单的命令启动服务器

Python

1
2
$ dj runserver 0:8000

这是一种全新的令人兴奋的感觉。alias工具会工作良好。

Make

随着我构建了越来越多微小的自动化步骤到我的编程环境,为每个我想要执行的命令使用一个单独的文件开始变得不方便。

来到Make工具。

现在,这完全不是什么化妆工具 – Make是一个构建工具!它像下面这样将一个项目的所有自动化全部安排在了一个make文件中,从而满足了我的需求:

Python

1
2
3
4
5
6
7
8
9
run:
    manage.py runserver 0:8000
shell:
    manage.py shell
test:
    nosetests /home/vagrant/code/things
lint:
    pyflakes /home/vagrant/code/things –no-bitching-about-long-lines

然后,我可以运行我那一群杂七杂八的任务,无需非得与一组分离的shell文件交互,像这样:

Python

1
2
$ make lint

归根结底,这不比shell文件稳定或便利得多,并且它为一堆的项目引入了一个奇怪的Make依赖,而一个Make依赖并不具备任何实际意义。

但它确实将我引入了我的旅途中的下一站:尝试使用一个task-runner来取代使用脚本。

Fabric

为嘛不用一个也存在于Python世界中的依赖来取代一个Make依赖呢?欢迎来到Fabric,一个优秀的Python命令运行器。如果比之Python,你对Ruby更加了解,那么你可能会记得Fabric邪恶的对手,Capistrano。

Fabric允许我像这样结构化我的测试运行:

Python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from fabric.api import run
def go():
    “””
    Documentation documentation documentation.
    “””
    run(‘manage.py runserver 0:8000’)
def test():
    run(‘nosetests /home/vagrant/code/things’)
def lint():
    run(‘pyflakes /home/vagrant/code/things –no-bitching-about-long-lines’)

然后,我可以这样调用:

Python

1
2
$ fab go

我们开始吧,我们所有的工具脚本都在同一个便利的地方。

这相当漂亮,而我开始使用它来做任何事。甚至是部署远程服务器这种棘手的问题 —— Fabric支持得相当好。

只要告诉它SSH到一个远程服务器,或者一堆远程服务器一次,然后fabric能够在所有它们上运行我的脚本。

我开始用Fabric来搭建服务器。新的服务器会需要PostgreSQL和nginx,而让脚本为我设置这些,会比必须手动处理所有繁琐的安装步骤棒得多。

Ansible

Fabric很棒,但有几件事它做的没那么漂亮。

在调试Fabric任务时,我在同一个服务器一遍又一遍的运行任务,而每一次它都会从头开始,运行整个脚本。如果有一个步骤很慢,那么它将会拖慢我的调试过程。

这意味着,当我使用它们时,我只会注释掉脚本的大块内容,测试脚本“头部”的同时留下尾部注释,这样我就可以推进我的自动化了。

有很多操作,第一次运行的时候会通过,但后续就全失败了,像“创建一个数据库”或者“创建一个目录”,这些操作第二次运行会失败,因为在我第二次运行的时候,该数据库或者目录已经存在了。

问题是啥?大多数的Fabric操作缺少”幂等性” – 一个五美元词,表示”每次运行同个操作总会产生相同的结果。”

另外一个问题是大量的配置文件。我发现自己经常要么编写丑陋的Bash操作来对大的配置文件进行小手术,要么使用字符串工具在python中构建配置文件,然后将它们放入到适当的位置。

最后一个问题?证书。为了让系统正常工作,证书需要放在哪里这个问题从来就还没搞得很清楚。我最终决定采取环境变量,但这意味着我建立的每个新系统,我都要马上立即提交三十几个变量到一个.bashrc文件中。

这仍然工作得很好,但我们可以做的更好。幸运的是,那里有一个框架,可以带这些属性运行操作,并且还让我们牢牢地处在Python的范畴之内。Ansible。

Ansible提供了一种YAML格式,它可以用来描述一个工作中的系统,然后它将通过一个SSH连接构建那个系统。一旦我熬过了YAML中固有的讨人厌的“编码”,这就很棒!除其他事项外,Ansible还包含一个全功能的模板系统,用于创建配置文件,以及带双向加密的密码存储,用以隐匿重要的配置凭据。

Ansible解决了很多问题。

Invoke

但是Ansible没有解决的一个问题是啥?它不是一个很好的任务运行器。

事实上,Ansible命令可以彻头彻尾的神秘运行。

在一组服务器上运行一个playbook看起来可能像这样:

Python

1
2
ansible-playbook -i inventory.yml mongo -u root –vault-password-file ~/.vaultpw mongo.yml

我不了解你,但这不是那种我可以轻松交给肌肉记忆的事情。

所以,再一次,我们需要一个任务运行器。我们可以回到Fabric,但是Fabric的维护者基本上已经放弃了它,转而支持一个正显示出一些第二系统效应的明确迹象的非常野心勃勃的2.0版本 —— 最明显的征兆是,它已经开发4年了,但还没有看到光明的一天。

所以Fabric现在不予考虑 —— 但同时Fabric 2.0在夹缝中生存,Fabric 2.0一个半完成的块已经出现了。虽然在功能上有所限制,它,额……比Fabric得多。

它称为Invoke,并且它只提供Fabric一半的”任务运行器(task runner)”,而不提供任何一点”ssh”或者”deployment”。但这就是我们想要的!如此完美!

所以我们可以像这样封装我们的ansible部署:

Python

1
2
3
4
5
6
from invoke import task, run
@task
def provision_mongo(ctx):
    ctx.run(‘ansible-playbook -i inventory.yml mongo -u root –vault-password-file ~/.vaultpw mongo.yml’)

然后像这样运行它:

Python

1
2
$ inv provision_mongo

我们可以用我们用来运行剩余应用的实用脚本来包含它。

Python

1
2
3
4
5
6
7
8
@task
def go(ctx):
    ctx.run(‘manage.py runserver 0:8000’)
@task
def test(ctx):
    ctx.run(‘nosetests /home/vagrant/code/things’)

Invoke具有更多的功能,但是涵盖所有将会超出这个已经过长的博文的范围了。

结论

显然的,我的可重复、有用的项目自动化之旅还有很长的路要走,但是我已经在invokeansible的交界处明确地发现了一些非常有用工具。

我们获得了Python所有的可组合性,Ansible所有的实用性,以及一个task runner所有的便利性。

P.S.

一个最后的牢骚:Invoke漂亮地支持Python 3,但是Ansible仍然绑在了Python 2的Python黑暗时代,所以为了同时运行Invoke和Python,我们必须接受一个次等的Python。

该死。

P.P.S.

据我所知,Javascript没有像这样的东东。我能找到最接近的东西是gulp。难道我将不得不满足于gulp吗?Yegh.

转载自演道,想查看更及时的互联网产品技术热点文章请点击http://go2live.cn

Python 中的枚举类型

枚举类型可以看作是一种标签或是一系列常量的集合,通常用于表示某些特定的有限集合,例如星期、月份、状态等。Python 的原生类型(Built-in types)里并没有专门的枚举类型,但是我们可以通过很多方法来实现它,例如字典、类等:

WEEKDAY = {
    'MON': 1,
    'TUS': 2,
    'WEN': 3,
    'THU': 4,
    'FRI': 5
}
class Color:
    RED   = 0
    GREEN = 1
    BLUE  = 2

上面两种方法可以看做是简单的枚举类型的实现,如果只在局部范围内用到了这样的枚举变量是没有问题的,但问题在于它们都是可变的(mutable),也就是说可以在其它地方被修改从而影响其正常使用:

WEEKDAY['MON'] = WEEKDAY['FRI']
print(WEEKDAY)

{‘FRI’: 5, ‘TUS’: 2, ‘MON’: 5, ‘WEN’: 3, ‘THU’: 4}

通过类定义的枚举甚至可以实例化,变得不伦不类:

c = Color()
print(c.RED)
Color.RED = 2
print(c.RED)

0
2

当然也可以使用不可变类型(immutable),例如元组,但是这样就失去了枚举类型的本意,将标签退化为无意义的变量:

COLOR = ('R', 'G', 'B')
print(COLOR[0], COLOR[1], COLOR[2])

R G B

为了提供更好的解决方案,Python 通过PEP 435 在 3.4 版本中添加了 enum标准库,3.4 之前的版本也可以通过 pip install enum 下载兼容支持的库。enum 提供了 Enum,IntEnum,unique三个工具,用法也非常简单,可以通过继承 Enum/IntEnum 定义枚举类型,其中 IntEnum 限定枚举成员必须为(或可以转化为)整数类型,而 unique 方法可以作为修饰器限定枚举成员的值不可重复:

from enum import Enum, IntEnum, unique
 
try:
    @unique
    class WEEKDAY(Enum):
        MON = 1
        TUS = 2
        WEN = 3
        THU = 4
        FRI = 1
except ValueError as e:
    print(e)

duplicate values found in : FRI -> MON

try:
    class Color(IntEnum):
        RED   = 0
        GREEN = 1
        BLUE  = 'b'
except ValueError as e:
    print(e)

invalid literal for int() with base 10: ‘b’

更有趣的是 Enum的成员均为单例(Singleton),并且不可实例化,不可更改:

class Color(Enum):
    R = 0
    G = 1
    B = 2

try:
    Color.R = 2
except AttributeError as e:
    print(e)

Cannot reassign members.

虽然不可实例化,但可以将枚举成员赋值给变量:

red = Color(0)
green = Color(1)
blue = Color(2)
print(red, green, blue)

Color.R Color.G Color.B

也可以进行比较判断:

print(red is Color.R)
print(red == Color.R)
print(red is blue)
print(green != Color.B)
print(red == 0) # 不等于任何非本枚举类的值

True
True
False
True
False

最后一点,由于枚举成员本身也是枚举类型,因此也可以通过枚举成员找到其它成员:

print(red.B)
print(red.B.G.R)

Color.B
Color.R

但是要谨慎使用这一特性,因为可能与成员原有的命名空间中的名称相冲突:

print(red.name, ':', red.value)
 
class Attr(Enum):
    name  = 'NAME'
    value = 'VALUE'
print(Attr.name.value, Attr.value.name)

R : 0
NAME value

总结
enum 模块的用法很简单,功能也很明确,但是其实现方式却非常值得学习。如果你想更深入了解更多 Python 中关于 ClassMetaclass的黑魔法,又不知道如何入手,那么不妨阅读一下 enum源码,或者关注接下来后面几篇的内容!

Python元类

Python元类

从某种意义上讲,元类只是扩展了装饰器的代码插入模式。
元类主要是针对那些构建API和工具供他人使用的程序员。

Python构建工具:

  1. 内省属性。如__class__、__dict__
  2. 运算符重载方法。如__str__、__add__
  3. 属性拦截方法。如__getattr__、__setattr__、__getattribute__。
  4. 类特性。内置函数property。拦截特定的属性。
  5. 类属性描述符。拦截特定的属性。特定只是定义根据访问自动运行函数的属性描述符的一种简洁方式。
  6. 函数和类装饰器。
  7. 元类。

元类允许我们在在一条class语句的末尾,插入当创建一个类对象的时候自动运行的逻辑。
这个逻辑不会把类名重新绑定到一个装饰器可调用对象,而是把类自身的创建指向特定的逻辑。

和类装饰器不同,它通常是添加实例创建时运行的逻辑,元类在类创建时运行。
同样的,它们都是通常用来管理或扩展类的钩子,而不是管理其实例。

通过声明一个元类,我们告诉Python把类对象的创建路由到我们所提供的另一个类:

由于创建类的时候,Python在class语句的末尾自动调用元类,因此它可以根据需要扩展、注册或管理类。

类也是某物的实例:

  • 在Python3.0中,用户定义的类对象是名为type的对象的实例,type本身是一个类。
  • 在Python2.6中,新式类继承自object,它是type的一个子类;传统类是type的一个实例,并且并不创建自一个类。
    实例创建自类,而类创建自type。
    类是类型,类型也是类
  • 类型由派生自type的类定义。
  • 用户定义的类是类型类的实例。
  • 用户定义的类是产生它们自己的实例的类型。

类根本不是一个独立的概念:它们就是用户定义的类型,并且type自身也是由一个类定义的。

由于类实际上是type类的实例,从type的定制的子类创建类允许我们实现各种定制的类。
在Python3.0中以及在Python2.6的新式类中:

  • type是产生用户定义的类的一个类。
  • 元类是type类的一个子类。
  • 类对象是type类的一个实例,或一个子类。
  • 实例对象产生自一个类。

换句话说,为了控制创建类以及扩展其行为的方式,我们所需要做的只是指定个用户定义的类创建自一个用户定义的元类,而不是常规的type类。

从技术上讲,Python遵从一个标准的协议来使这发生:在一条class语句的末尾,并且在运行了一个命名控件词典中的所有嵌套代码之后,它调用type对象来创建class对象:

class = type(classname, superclasses,attrbuteddict)

type对象反过来定义了一个__call__运算符重载方法,当调用type对象的时候,该方法运行两个其他方法:

type.__new__(typeclass, classname,superclasses,attributeddict)
type.__init__(class,classname,superclasses,attributedict)

__new__方法创建并返回了新的class对象,并且随后__init__方法初始化了新创建的对象。
这是type的元类子类通常用来定制类的钩子。

尽管重新定义type超类的__new____init__方法是元类向类对象创建过程插入逻辑的最常见方法,其他方案也是可能的。
实际上任何可调用对象都可以用作一个元类,只要它接收传递的参数并且返回与目标类兼容的一个对象。

另外,我们也可以重定义元类的__call__, 以拦截创建调用。

making class
In SuperMeta.call:
…Spam
…(<class ‘__main__.Eggs’>,)
…{‘meth’: , ‘data’: 1, ‘__new__’: , ‘__module__’: ‘__main__’, ‘__qualname__’: ‘Spam’, ‘__init__’: }
In SubMeta.new:
…Spam
…(<class ‘__main__.Eggs’>,)
…{‘meth’: , ‘data’: 1, ‘__new__’: , ‘__module__’: ‘__main__’, ‘__qualname__’: ‘Spam’, ‘__init__’: }
In SubMeta init:
…Spam
…(<class ‘__main__.Eggs’>,)
…{‘meth’: , ‘data’: 1, ‘__new__’: , ‘__module__’: ‘__main__’, ‘__qualname__’: ‘Spam’, ‘__init__’: }
…init class object: [‘meth’, ‘data’, ‘__new__’, ‘__doc__’, ‘__module__’, ‘__init__’]
making instance
data: 1

调用顺序是:
1. 调用元类的__call__。只调用一次。
2. 调用元类的__new__。创建一个type实例,只调用一次。
3. 调用元类的__init__。初始化type实例,只调用一次。
4. 调用自定义类的__new__。调用多次,每创建一个自定义实例,调用一次。
5. 调用自定义类的__init__。调用多次,每创建一个自定义实例,调用一次。

实例与继承的关系

  • 元类继承自type类。
  • 元类声明由子类继承。在用户定义的类中,metaclass=M声明由该类的子类继承,因此,对于在超类链中继承了这一声明的每个类的构建,该元类都将运行。
  • 元类属性没有由类实例继承。元类声明指定了一个实例关系,它和继承不同。由于类是元类的实例,所以元类中定义的行为应用于类,而不是类随后的实例。实例从它们的类和超类获取行为,但是,不是从任何元类获取行为。从技术上讲,实例属性查找通过只是搜索实例及期所有类的__dict__字典;元类不包含在实例查找中。

元类与类装饰器在功能上有重合。

  • 在class语句末尾,类装饰器把类名重新绑定到一个函数的结果。
  • 元类通过在一条class语句的末尾把类对象创建过程路由到一个对象来工作。

学习元类可参考Enum源码

  1. _EnumDict通过继承dict,并重写__setitem__,来实现枚举的name不重复。
  2. 通过重写元类EnumMeta的__delattr____setattr__来限制删除和修改枚举成员。

python之logging模块

python之logging模块

介绍

logging模块是用来做日志记录的。
logging.config用来配置。
logging.handlers用来处理日志,可以是屏幕输出,可以是写文件(文件又可以自动切割文件),可以是发邮件。

最简单的用法是:

import logging

logging.debug('This is debug message')
logging.info('This is info message')
logging.warning('This is warning message')
 
屏幕上打印:
WARNING:root:This is warning message

logging.basicConfig(**kwargs)

以默认格式创建一个StreamHandler,然后把它加到root logger,函数参数可以做些配置。
如果root logger没有定义handlers,则当函数debug(),info(), warning(), error() and critical()调用时会自动调用basicConfig()。

如果root logger已经定义好了handlers,则这个函数什么也不干。

import logging

logging.basicConfig(level=logging.DEBUG,
                format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
                datefmt='%a, %d %b %Y %H:%M:%S',
                filename='myapp.log',
                filemode='w')
    
logging.debug('This is debug message')
logging.info('This is info message')
logging.warning('This is warning message')
 
./myapp.log文件中内容为:
Sun, 24 May 2009 21:48:54 demo2.py[line:11] DEBUG This is debug message
Sun, 24 May 2009 21:48:54 demo2.py[line:12] INFO This is info message
Sun, 24 May 2009 21:48:54 demo2.py[line:13] WARNING This is warning message

logging.basicConfig函数各参数:
filename: 指定日志文件名
filemode: 和file函数意义相同,指定日志文件的打开模式,’w’或’a’
format: 指定输出的格式和内容,format可以输出很多有用信息,如上例所示:
%(levelno)s: 打印日志级别的数值
%(levelname)s: 打印日志级别名称
%(pathname)s: 打印当前执行程序的路径,其实就是sys.argv[0]
%(filename)s: 打印当前执行程序名
%(funcName)s: 打印日志的当前函数
%(lineno)d: 打印日志的当前行号
%(asctime)s: 打印日志的时间
%(thread)d: 打印线程ID
%(threadName)s: 打印线程名称
%(process)d: 打印进程ID
%(message)s: 打印日志信息
datefmt: 指定时间格式,同time.strftime()
level: 设置日志级别,默认为logging.WARNING
stream: 指定将日志的输出流,可以指定输出到sys.stderr,sys.stdout或者文件,默认输出到sys.stderr,当stream和filename同时指定时,stream被忽略

logging.handlers

handlers控制日志的落地。有下面这些方式。

logging.StreamHandler: 日志输出到流,可以是sys.stderr、sys.stdout或者文件
logging.FileHandler: 日志输出到文件
日志回滚方式,实际使用时用RotatingFileHandler和TimedRotatingFileHandler
logging.handlers.BaseRotatingHandler
logging.handlers.RotatingFileHandler
logging.handlers.TimedRotatingFileHandler
logging.handlers.SocketHandler: 远程输出日志到TCP/IP sockets
logging.handlers.DatagramHandler: 远程输出日志到UDP sockets
logging.handlers.SMTPHandler: 远程输出日志到邮件地址
logging.handlers.SysLogHandler: 日志输出到syslog
logging.handlers.NTEventLogHandler: 远程输出日志到Windows NT/2000/XP的事件日志
logging.handlers.MemoryHandler: 日志输出到内存中的制定buffer
logging.handlers.HTTPHandler: 通过”GET”或”POST”远程输出到HTTP服务器

将日志同时输同到文件和屏幕

import logging

logging.basicConfig(level=logging.DEBUG,
                format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
                datefmt='%a, %d %b %Y %H:%M:%S',
                filename='myapp.log',
                filemode='w')

#################################################################################################
#定义一个StreamHandler,将INFO级别或更高的日志信息打印到标准错误,并将其添加到当前的日志处理对象#
console = logging.StreamHandler()
console.setLevel(logging.INFO)
formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s')
console.setFormatter(formatter)
logging.getLogger('').addHandler(console)
#################################################################################################

logging.debug('This is debug message')
logging.info('This is info message')
logging.warning('This is warning message')
 
屏幕上打印:
root        : INFO     This is info message
root        : WARNING  This is warning message
./myapp.log文件中内容为:
Sun, 24 May 2009 21:48:54 demo2.py[line:11] DEBUG This is debug message
Sun, 24 May 2009 21:48:54 demo2.py[line:12] INFO This is info message
Sun, 24 May 2009 21:48:54 demo2.py[line:13] WARNING This is warning message

自动切割日志

import logging
from logging.handlers import RotatingFileHandler

#################################################################################################
#定义一个RotatingFileHandler,最多备份5个日志文件,每个日志文件最大10M
Rthandler = RotatingFileHandler('myapp.log', maxBytes=10*1024*1024,backupCount=5)
Rthandler.setLevel(logging.INFO)
formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s')
Rthandler.setFormatter(formatter)
logging.getLogger('').addHandler(Rthandler)
#######################################################################################

这个用的比较多。留下日志,1是可以跟踪性能信息,也可以跟踪出错信息。

通过文件配置日志

配置文件

#logger.conf
###############################################
[loggers]
keys=root,test01,test02
[logger_root]
level=DEBUG
handlers=hand01,hand02
[logger_test01]
handlers=hand01,hand02
qualname=test01
propagate=0
[logger_test02]
handlers=hand01,hand03
qualname=test02
propagate=0
###############################################
[handlers]
keys=hand01,hand02,hand03
[handler_hand01]
class=StreamHandler
level=INFO
formatter=form02
args=(sys.stderr,)
[handler_hand02]
class=FileHandler
level=DEBUG
formatter=form01
args=('myapp.log', 'a')
[handler_hand03]
class=handlers.RotatingFileHandler
level=INFO
formatter=form02
args=('myapp.log', 'a', 10*1024*1024, 5)
###############################################
[formatters]
keys=form01,form02
[formatter_form01]
format=%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s
datefmt=%a, %d %b %Y %H:%M:%S
[formatter_form02]
format=%(name)-12s: %(levelname)-8s %(message)s
datefmt=

代码使用配置文件


import logging
import logging.config

logging.config.fileConfig("logger.conf")
logger = logging.getLogger("test01")

logger.debug('This is debug message')
logger.info('This is info message')
logger.warning('This is warning message')