埃拉托斯特尼筛法求素数

算法

要得到自然数n以内的全部素数,必须把不大于 的所有素数的倍数剔除,剩下的就是素数。 [1]
给出要筛数值的范围n,找出以内的素数。先用2去筛,即把2留下,把2的倍数剔除掉;再用下一个质数,也就是3筛,把3留下,把3的倍数剔除掉;接下去用下一个质数5筛,把5留下,把5的倍数剔除掉;不断重复下去……。

python实现

def print_prime(N):
    ret = list()
    for i in range(2, N):
        ret.append(i)
    idx = 0
    def remove_multiple(n):
        for v in ret:
            if v%n==0 and v//n > 1:
                ret.remove(v)
    while ret[idx]*ret[idx] <= N-1:
        remove_multiple(ret[idx])
        idx+=1
    print(ret)


if __name__ == '__main__':
    print_prime(10000)

参考

算法说明

C语言程序设计阅读笔记

  1. 第一章 导言
  2. 第二章 类型、运算与表达式
    1. 类型转换
    2. 运算符优先级与求值次序
  3. 第三章 控制流
  4. 第四章 函数与程序结构
    1. C预处理器
      1. 宏替换
  5. 第五章 指针与数组
    1. 指针与地址
    2. 指针与函数参数
    3. 指针与数组
    4. 地址算术运算
    5. 字符指针与函数
    6. 指针数组以及指向指针的指针
    7. 命令行参数
    8. 复杂声明
  6. 第六章 结构
    1. 结构的基本知识
    2. 结构与函数
    3. 结构数组
    4. 指向结构的指针
    5. 自引用结构
    6. 类型定义(typedef)
    7. 联合
    8. 位字段
  7. 第七章 输入输出
    1. 标准输入/输出
    2. 格式化输出 – printf函数
    3. 变长参数表
    4. 格式化输入 – scanf函数
    5. 文件访问
    6. 错误处理 – stderr 和 exit
    7. 行输入和行输出
    8. 其它函数

第一章 导言

浮点数知识:

  1. http://www.jb51.net/article/103412.htm 原码、反码、补码、移码的作用
  2. 浮点数结构 http://blog.csdn.net/whzhaochao/article/details/1288587
  3. https://jingyan.baidu.com/article/425e69e6e93ca9be15fc1626.html 小数转二进制

在程序中使用300、20等"幻数"并不是好习惯,它们几乎不能给以后阅读程序的人提供什么信息,而且使程序的修改更加困难。处理这类幻数的一种方法是赋与它们有意义的名字。
如果程序中的幻数都以符号常量的形式出现,对程序进行大量修改就会相对容易很多。

#define 指令行的未尾没有分号。

标准库提供的输入/输出模型非常简单:无论文本从何处输入,输出到何处,其输入/输出都是按照字符流的方式处理。

语句++nc比nc=nc+1更精炼一些,且通常效率更高一些。

%.0f强制不打印小数点和后面的小数部分,因此小数部分的位数为0。

单独的分号称为空语句。

单引号中的字符表示一个整型值,该值等于此字符在机器字符集中对应的数值,我们称之为字符常量。
但是,它只不过是小的整型数的另一种写法而已。

在兼有值与赋值两种功能的表达式中,赋值结合次序是由右至左。

char类型的字符是小整数,因此char类型的变量和常量在算术表达式中等价于int类型的变量和常量。

之所以getchar的返回值得是int 而不是char, 是因为它需要包含int类型的EOF。

程序中的每个局部变量只在函数被调用时存在,在函数执行完毕退出时消失,这也是其他语言通常把这类变量称为 自动变量 的原因。
如果 自动变量 没有赋值,则其中存放的是无效值。
外部变量 必须定义在所有函数之外,且只能定义一次,定义后编译程序将为它分配存储单元。在每个需要访问外部变量的函数中,必须声明相应的外部变量,此时声明其类型。
声明时可以用 extern 语句显示声明,也可以通过上下文隐式声明。
在源文件中,如果外部变量的定义出现在使用它的函数之前,那么在那个函数中就没有必要使用extern声明。
在通常的做法中,所有外部的变量的定义都是放在源文件中的开始处,这样就可以省略extern声明。
如果程序包含在多个源文件中,而某个变量在file1中定义,在file2和file3文件中使用,那么在文件file2与file3中就需要使用 extern 声明来建立该变量与其定义之间的联系。

"定义"表示创建变量或分配存储单元,而"声明"指的是说明变量的性质,但并不分配存储单元。
有赋值语句叫定义,没有赋值说句叫声明.

int a=5;//定义
int b;//声明

过分依赖外部变量会导致一定的风险,因为它会使程序中的数据关系模糊不清–外部变量的值可能会被意外地或不经意地修改,而程序的修改又变得十分困难。

第二章 类型、运算与表达式

标准函数strlen(s)可以返回字符串参数s的长度,但长度不包括末尾的'\0'。

枚举为建立常量值与名字之间的关联提供了一种便利的方式。相对于#define语句来说,它的优势在于常量值可以自动生成。

字符常量'\0'表示值为0的字符,也就是空字符(null)。我们通常用'\0'的形式代替0,以强调某些表达式的字符属性,但其数字值是0。

所有变量都必须先声明后使用,尽管某些变量可以通过上下文隐式地声明。

如果变量不是自动变量,则只能进行一次初始化操作,从概念上讲,应该是在程序开始执行之前进行,并且初始化表达式必须为常量表达式。
默认情况下,外部变量与静态变量将被初始化为0。未经显式初始化的自动变量的值为未定义的(即无效值)。

对数组而言,const限定符指定数组所有元素的值都不能被修改。
const限定符也可配合数组参数使用,它表明函数不能修改数组元素的值。
如果试图修改const限定的值,其结果取决于具体的实现。

逻辑非运算符!的作用是将非0操作数转换为0,将操作数0转换为1。

需要注意的是, C语言没有指定同一运算符中多个操作数据的计算顺序。(&&、|| 、?:和,运算符除外).
类似的, C语言也没有指定函数各参数的求值顺序。
像下面的语句:

a[i] = i++;//数组下标i是引用的旧值还是新值,编译器会有不同的解释,因为最佳的求值顺序同机器结构有很大的关系。。实际开发中,需要避免这样的代码。

在任何一种编程语言中,如果代码的执行结果与求值顺序相关,则都是不好的程序设计风格。

类型转换

一般来说,自动转换是指把"比较窄"操作数转换为"比较宽的"操作数,并且不丢失信息的转换。

C语言没有指定char类型的变量是无符号变量还是有符号变量。
C语言定义保证了机器的标准打印字符集中的字符不会是负值,因此,在表达式中这些字符总是正值。但是,存储在字符变量中的位模式在某些机器中可能是负的,
而在另一些机器中可能是正的。为了保证程序的可移植性,如果要在char类型的变量中存储非字符数据,最好指定signed或unsigned限定符。

注意,表达式中的float类型的操作数不会自动转换为double类型,这一点与最初的定义有所不同。一般来说,数学函数使用double的变量。
使用float类型主要是为了在使用较大的数组时节省存储空间,有时也是为了节省机器执行时间(双精度算术运算特别费时)。

带符号与无符号值之间的比较运算是与机器相关的,因为它们取决于不同整数类型的大小。譬如-1L<1U, -1L>1UL(-1L被转换成UL,转成了正数)

当把较长的整数转换为较短的整数或char类型时,超出的高位部分将被丢弃。

当被double类型转换为float类型时,是进行四舍五入还是截取取决于具体的实现。

即使调用函数的参数为char或float类型, 我们也把函数参数声明为int或者double类型。(在没有函数原型的情况下,char与short类型者将被转换为
int类型, float类型将被转换为double类型。)

通常情况下,参数是通过函数原型声明的。

在对signed类型的带符号值进行右移时,某些机器将对左边空出的部分用符号位填补(即“算术移位”),而另一些机器则对左边空出来的部分用0填补(即“逻辑位移”)

x = x & ~077

注意,表达式x&~077与机器字长无关,它比形式为x&0177700的表达式要好,因为后者假定x是16位的数值。这种可移植的形式并没有增加额外开销,因为
~077是常量表达式,可以在编译时求值。

赋值语句具有值,且可以用在表达式中,在这类表达式中,赋值表达式的类型是它的左操作数的类型,
其值是赋值操作完成后的值。

运算符优先级与求值次序

*同大多数语言一样,C语言没有指定同一运算符中多个操作数的计算顺序。* 如 x=f()+g(), f()是先于g(),还是后于g()调用,是未定义的。
*类似地,C语言也没有指定函数各参数的求值顺序。* printf("%d %d\n", ++n, power(2,n)), 执行结果取决于编译器。
*注意*
a[i] = i++; 此问题,i是引用旧值,还是新值,C语言是未定义的,执行结果由编译器决定,因为最佳的求值顺序同机器结构有很大的关系。
在任何一种编程语言中,如果代码的执行结果与求值顺序相关,则都不是好的程序设计风格。

需要记忆的优先级
() [] . -> 从左至右
! ~ ++ – + – * & (type) sizeof 从右至左

第三章 控制流

标号可以位于对应的goto语句所在函数的任何语句的前面。标号的作用域是整个函数。

在深度潜逃的场景下,跳出循环。用goto语句还是有用的。但不应该滥用。

第四章 函数与程序结构

如果函数定义中省略了返回值类型,则默认为int类型。
程序可以看成是变量定义和函数定义的集合。函数之间的通信可以通过参数、函数返回值、外部变量进行。
return 表达式。
在必要时,表达式将被转换为函数的返回值类型。表达式两边通常加一对圆括号,此处的括号是可选的。

如果某个函数从一个地方返回时有返回值,而从另一个地方返回时没有返回值,该函数并不非法,但可能是一种出问题的征兆。
在任何情况下,如果函数没有成功地返回一个值,则它的"值"肯定是无用的。

如果没有函数原型,则函数将在第一次出现的表达式中被隐式声明。
例如:

 sum += atof(line)

atof被假设为返回值为int值,同时为了兼容旧版本,并不会对参数做假设。 并且,如果函数声明中不包含参数,编译程序不会对参数做任何假设,并会关闭所有的参数检查。

规范的做法是:
*如果函数带有参数,则要声明它们;如果没有参数,则使用void进行声明*

由于C语言不允许在一个函数中定义其他函数,因此函数本身是“外部”的。默认情况下,外部变量与函数具有下列性质:
通过同一个名字对外部变量的所有引用(即使这种引用来自于单独编译的不同函数)实际上都是引用的同一个对象(标准中把这一性质称为外部链接)

名字的作用域指的是程序中可以使用该名字的部分。
外部变量 或函数的作用域从声明它的地方开始,到其所在的(待编译)文件的末尾结束。

如果要在外部变量的定义之前使用该变量,或者外部变量的定义与声明不在同一个源文件中,则必须在相应的变量声明中强制性地使用关键字 **extern**。

变量声明用于说明变量的属性(主要是变量的类型),而变量定义除此以外还将引起存储器的分配。

在一个源程序的所有源文件中,一个外部变量只能在某个文件中定义一次,而其它文件可以通过extern声明来访问它。

static 声明限定外部变量与函数,可以将其后声明的对象的作用域限定为被编译文件的剩余部分。

static类型的内部变量是一种只能在某个特定函数中使用但一直占据存储空间的变量。

register 变量放在机器的寄存器中,这样可以使程序更小、执行速度更快。
register变量只适用于自动变量以及函数的形式参数。
无论寄存器变量实际上是不是存放在寄存器中,它的地址都是不能访问的。

在一个好的程序设计中,应该避免出现变量名隐藏外部作用域中相同名字的情况,否则,很可能引起混乱和错误。

在不进行显示初始化的情况下,外部变量与静态变量都将被初始化为0, 而自动变量及寄存器变量的初值则没有定义(即初值为无用的信息)
对于外部变量与静态变量来说,初始化表达式必须是常量表达式,且只初始化一次(从概念上讲是在程序开始执行前进行初始化)

int days[13] = {1,2}

如果初始化表达的个数比数组元素少,则对外部变量、静态变量和自动变量来说,没有初始化表达式的元素将被初始化为0。

递归的执行速度并不快,但递归代码比较紧凑,并且比相应的非递归的代码更易于编写与理解。

C预处理器

宏替换

###
如果在替换文本中,参数名以 # 作为前缀则结果将是被扩展 为 *由实际参数替换为该参数的带引号的字符串*。

#define dprint(expr) printf(#expr " = %g\n", expr)

使用语句
dprint(x/y);
调用该宏时,该宏将被扩展为:
printf("x/y" " = %g\n", expr) 等价于printf("x/y = %g\n", x/y)

如果替换文本中的参数与 ## 相邻,则该参数将被实际参数替换,##与前后的空白符将被删除,并对替换后的结果 *重新扫描*。

#define paste(front, back) front ## back // paste(name,1) 将建立记号name1, 可用作动态变量?

第五章 指针与数组

C语言中,指针使用很广泛:

  1. 指针常常是表达某个计算的唯一途径。
  2. 使用指针通常可以生成更高效、更紧凑的代码。

ANSI C使用类型void*(指向void的指针)代替char*作为通用指针的类型。

指针与地址

指针是能够存放一个地址的一组存储单元(通常是2个或4个字节)
地址运算符&只能应用于内存中的对象,即变量与数组元素。 它不能作用于表达式、常量或register类型的变量。
一元运算符*是间接寻址或间接引用运算符。当它作用于指针时,将访问指针所指向的对象。

int *ip;
上述声明语句表明*ip指向的对象类型是int。

每个指针都必须指向某个特定类型的数据类型。(void类型指针例外,但它不能间接引用其自身)

指针与函数参数

  1. 由于C语言是以传值的方式将参数值传递给被调用函数。因此,被调用函数不能直接修改主调函数中变量的值。如果修改形参的值,实际修改的是副本。
  2. 指针参数使得被调用函数能够访问和修改主调函数中对象的值。因为形参指向的地址和实参指向的地址一样。

指针与数组

通过数组下标所能完成的任何操作都可以通过指针来实现。一般来说,用指针编写的程序比用数组下标编写的程序执行速度快,但更难理解。

根据定义,数组类型的变量或表达式的值是该数组第0个元素的地址。因此pa = &a[0] 和 pa = a是相同的。

对数组元素a[i]的引用也可以写成*(a+i)这种形式。
在计算数组a[i]时,C语言实际上先将其转换为*(a+i)的形式,然后再进行求值,因此在程序中这两种形式是等价的。

数组名和指针之间有一个不同之处。
指针是一个变量,因此pa=a 和 pa++都合法。
但数组名不是变量,因此类型于a = pa 和a++形式的语句是非法的。

在函数定义中,形式参数

char s[];

char *s;

是等价的。更习惯用后一种。

地址算术运算

通常,对指针有意义的初始化只能是0或者是表示地址的表达式。

指针与整数之间不能相互转换,但0是惟一例外,因为把0定义为指针的一个特殊值,常用符号常量NULL代替常量0,表示指针还未指向合适的地址。

有效的指针运算有以下情况:

  1. 相同类型指针之间的赋值运算;
  2. 指针同整数之间的加法或减法运算;数组位移
  3. 指向相同数组中元素的两个指针间的减法或比较运算;位置关系
  4. 将指针赋值为0或指针与0之间的比较运算。 判断指针的值是否有效

字符指针与函数

C语言没有提供将整个字符串作为一个整体进行处理的运算符。

注意以下声明的区别:

char amessage[] = "now is the time";
char *pmessage = "now is the time";

amessage是一个仅仅足以存放初始化字符串以及空字符'\0'的一维数组。数组中的内容可以修改,但amessage始终指向同一个存储地址。
pmessage是一个指针,其初值指向一个字符串常量,之后它可以指向其它地址,但如果试图修改字符串的内容,结果是没有定义的

指针数组以及指向指针的指针

区分指针数组和数组指针:

int (*p)[n]

()的优先级最高,所以这说明定义的是一个指针。接下来是int [n], *p返回的是一个int[n], 即p指向一个整形的一维数组,它的长度是n。

int *p[n]

[]的优先级更高。所以这说明定义的是一个数组。接下来是int *p,说明这个数组存放的是整型指针。

命令行参数

ANSI标准要求argv[argc]的值必须为一空指针。

复杂声明

复杂的声明让人难以理解,原因在于:
C语言的声明不能从左至右阅读,而且使用了太多的圆括号。

规则如下:
dcl: 前面带有可选的*的direct-dcl
direct-dcl: name
(dcl)
direct-dcl()
direct-dcl[]

理解的方法有:右左方法。
几个关键运算符: *、 []、 ()。

从规则不难推导出右左法则。

  1. 首先找到name, 为direct-dcl

2.然后看右边是()还是[], 如果是(),说明声明的函数,如果是[],说明声明的数组。
3.看完右边看左边,如果左边是*,则说明内容是指针。
4.按此解析直到解析完。

第六章 结构

ANSI标准在结构方面的主要变化是字义了结构的赋值操作– **结构可以拷贝、赋值、传递给函数,函数也可以返回结构类型的返回值。
在ANSI标准中,自动结构和数组现在也可以进行初始化。
可以理解是一个复合类型, 高级语言里面的类和对象就是通过结构体实现的。

结构的基本知识

struct point {
    int x;
    int y;
}

关键字struct 引入结构声明。结构声明由包含在花括内的一系列声明组成。
struct 后面的名字是可选的,称为结构标识。结构标记用于为结构命名,在定义之后,结构标记就代表花括号内的声明,可以用它作为该声明的简写形式。
struct 声明定义了一种数据类型。

结构与函数

结构的合法操作只有几种:

  1. 作为一个整体复制和赋值
  2. 通过&运算符取地址。
  3. 访问其成员

如果传递给函数的结构很大,使用指针的方式的效率通常比复制整个结构的效率要高。

注意运算符的优先级和结合顺序:
在所有运算符中,结构运算符"." 和 "->"、用于函数调用的"()"以及用于下标的"[]",因此,它们同操作数之间的结合也最紧密。
注意,他们都是从左至右的结合顺序。

结构数组

条件编译语句#if中不能使用 sizeof, 因为预处理不对类型名进行分析。
但预处理器并不计算#define中的表达式,因此在#define中使用 sizeof是合法的。

指向结构的指针

自引用结构

struct tnode {
    char \*word;
    int count;
    struct tnode \*left;
    struct tnode \*right;
};

一个包括其自身实例的结构是非法的,但是,下列声明是合法的。
struct tnode *left;

类型定义(typedef)

typedef用来建立新的数据类型名。
从任何意义上讲,typedef并没有创建一个新类型,它只是为某个已存在的类型增加了一个新的名称而已。
typdef声明也并没有增加任何新的语义:通过这种方式声明的变量与通过普通声明方式声明的变量具有完全相同的属性。
实际上,typedef 类似于#define 语句,但由于typedef是由编译器解释的,因此它的文本替换功能要超过预处理器的能力。

除了表达方式更简洁之外,使用typedef还有另外两个重要原因。

  1. 它可以使程序参数化,以提高程序的可移值性。如果typdef声明的数据类型同机器有关,那么,当程序移植到其他机器上时,只需改变typedef类型定义就可以了。
  2. 为程序提供更好的说明性。

联合

联合是可以(在不同时刻)保存不同类型和长度的对象的变量,编译器负责跟踪对象的长度和对齐要求。
联合提供了一种方式,以在单块存储区中管理不同类型的数据,而不需要在程序中嵌入任何同机器有关的信息。

联合的目的:一个变量可以合法地保存多种数据类型中任何一种类型的对象。其语法基于结构,如下所示:

union tag {
    int ival;
    float fval;
    char *sval;
}

实际上,联合就是一个结构,它的所有成员相对于基地址的偏移量都为0, 此结构空间要大到足够容纳最"宽"的成员,并且,其对齐方式要适合于联合中所有类型的成员。
联合只能用其第一个成员类型的值进行初始化。

ps:不知道怎么用。能节省能内存,但真正使用的时候,还得知道最后存的值是什么类型,管理也挺麻烦的。
关于对齐,参考文章: http://blog.sina.com.cn/s/blog_715de2f50100pgs3.html

位字段

多个对象保存在一个机器字中,取和设置的时候只操作字的部分位。
这其实可以通过移位运算、屏蔽运算、补码运算进行简单的位操作来实现。

#define EXTERNAL 02
#define STATIC   04

利用|来实现置1操作

flags |= EXTERNAL | STATIC;

利用&来实现置0操作

flags &= ~(EXTERNAL | STATIC);

相对于以上方法,C语言提供了另一种可替代的方法,即直接定义和访问一个字中位字段的能力,而不需要通过按位逻辑运算符。
位字段,或简称字段,是"字"中相邻位的集合。"字"是单个的存储单元,它同具体的实现有关。
示例如下:

struct {
    unsigned int iskeyword : 1;
    unsigned int isextern  : 1;
    unsigned int isstatic  : 1;
}

字段的所有属性几乎都同具体的实现有关。字段是否能覆盖边界由具体的实现定义。
字段可以不命名,无名字段(只有一个冒号和宽度)起填充作用。特殊宽度0可以用来强制在下一个字边界上对齐。

某些机器 上字段的分配是从字的左端至右端进行的,而某些机器则相反。

位字段不是数组,并且没有地址,因此对它们不能使用&运算符。

第七章 输入输出

标准库函数并不是C语言本身的组成部分。
ANSI标准精确定义了这些库函数,所以,只用库函数完成的程序是可移植的。

标准输入/输出

文本流由一系列的行组成,每一行的未尾是一个换行符。
如果系统没有遵循这种模式,则标准库将通过一些措施使得该系统适应这种模式。

格式化输出 – printf函数

printf转换说明:
负号,用于指定被转换的参数按照左对齐的形式输出。

变长参数表

中包含了一组宏定义,它们对如何遍历参数表进行了定义。该文件的实现因不同的机器而不同。
使用流程一致:

  1. va_list ap;//声明
  2. va_start(ap, 最后一个有名参数);//初始化,将ap指向第一个无名参数
  3. value = va_arg(ap,参数类型);//获取参数的值,需要把类型传进行,才可以取得正确的值以及正确地移动到下一个无名参数。
  4. va_end(ap);//释放资源

格式化输入 – scanf函数

两个注意点:

  1. 输入字段定义为一个不包括空白符的字符串,其边界定义为下一个空白符或达到指定的字段宽度。
    这表明scanf函数将越过行边界读取输入,因为换行符也是空白符。(空白符包括空格符、横向制表符、纵向制表符、换行符、回车符、换页符)
  2. 如果转换说明中有赋值禁止字符*,则跳过该输入字段,不进行赋值。

文件访问

关于linux的标准输入、标准输出以及重定向。

0 表示标准输入(默认键盘
1 表示标准输出 (默认屏幕)
2表示标准错误。(默认屏幕)
通过重定向,可以控制以上3个的默认值。

cat file > fil2 等同于 cat file 1>fil2, 输出重定 。1>中的1可以省略不写,默认就是标准输出。

&[n] 代表已经存在的文件描述符。&1代表输出,&2代表错误。&-表示关闭与它绑定的描述符。
所以有下面两种方式放弃标准输出和标准错误。
cat file > /dev/null 2>&1
cat file 1>&- 2>&-
/dev/null是黑洞文件,所有到它的输出都会丢弃。

错误处理 – stderr 和 exit

在main程序中return xx; 相当于exit(xx);

尽管输出错误很少出现,但还是存在(例如磁盘满时);

行输入和行输出

库函数gets在读取输入时,会把换行符'\n'删除。
puts则会在行尾添加换行符。

其它函数

int ungetc(char c, FILE \*fp), 每个文件只能有一个写回字符。

system(char \*s)执行命令。

利用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源码,或者关注接下来后面几篇的内容!