黑帽联盟

 找回密码
 会员注册
查看: 820|回复: 0
打印 上一主题 下一主题

[基础服务] ansible笔记(42):角色

[复制链接]
yun 黑帽联盟官方人员 

920

主题

37

听众

1364

积分

超级版主

Rank: 8Rank: 8

  • TA的每日心情
    奋斗
    2019-10-18 11:20
  • 签到天数: 678 天

    [LV.9]以坛为家II

    本帖最后由 yun 于 2019-9-11 17:18 编辑

    ansible是一个系列文章,我们会尽量以通俗易懂的方式总结ansible的相关知识点。
    ansible系列博文直达链接:ansible轻松入门系列
    "ansible系列"中的每篇文章都建立在前文的基础之上,所以,请按照顺序阅读这些文章,否则有可能在阅读中遇到障碍。


    在介绍角色之前,我先讲讲自己的经历。

    我经常需要安装配置各种服务,比如,nginx、redis、mysql...等等,于是我决定,分别为这些服务编写对应的剧本,以便在需要的时候,能够根据已经编写好的playbook,快速的安装配置对应的软件,于是,我们编写了nginx.yml、redis.yml、mysql.yml等等playbook,当有一台新主机A需要安装nginx和redis时,我只需要将nginx.yml和redis.yml中的目标主机IP修改成A主机的IP,然后执行这两个playbook即可,有没有觉得很方便?一开始我是觉得挺方便的,但是很快,我发现了一些很明显的问题。

    第一个问题就是,今天我需要在A主机上安装redis,于是我把redis.yml的目标主机修改成了A主机的IP,但是明天,我又需要在B主机上安装redis,于是我又把redis.yml的目标主机修改成了B主机的IP,每次安装配置都需要修改redis.yml,这样做似乎是理所应当的, 但是仔细想想,能不能有更好的办法呢?能不能让redis.yml中的配置过程与目标主机的耦合度降到最低呢?因为我有可能需要在任意一台主机上安装配置redis,所以每次都因为目标主机的缘故修改redis.yml文件,不仅仅会觉得麻烦,逻辑的界线似乎也不是特别清晰,好吧,一时半会儿我也没有想到什么好的解决办法,于是我决定先把这个问题记录下来。

    刚开始的时候,我编写的nginx.yml和redis.yml都很简单,因为初次编写它们的时候,我考虑到的情况并不是很多,甚至都没有使用到任何模板文件,也没有使用什么变量,总之就是很简单的写了几个任务,但是经过不断的实践,我决定优化刚开始编写的playbook,让它适用于更多的情况,慢慢的,我开始编写默认的模板文件,开始将一些默认配置提取到变量文件中,之前如果想要配置nginx,只靠nginx.yml一个文件就够了,但是现在,我已经有了nginx.yml、nginx.conf.j2、nginx.defaultvars.yml三个文件,同理,redis.yml也慢慢扩展成了几个文件,其他服务对应的文件也变得越来越多,总之,我按照自己的规范,井然有序的工作着。

    直到有一天,公司来了一个名叫狗蛋的新同事,他也是做运维的,我俩一见如故,很快便开始交换自己的运维经验,我告诉他,我已经写好了一些playbook,以便针对不同的服务进行方便快捷的安装配置,狗蛋听我这么说,立马接话道:"英雄所见略同,我也是这么干的,而且我有自己的一套规范。",狗蛋告诉我,他在编写处理nginx的playbook时,会创建一个名叫nginx的文件夹,然后再在nginx的目录中创建一些子目录,比如,task目录、temp目录、default目录等等,然后将一些安装配置的任务剧本放在task目录中,将一些会用到的模板文件放到temp目录中,将一些默认设置变量文件放在default目录中,这样就显得非常简洁明了了,比如,当你需要找到nginx的相关模板文件时,只要去nginx的temp目录查找就行,同理,他在编写redis的playbook时,也会创建一个redis目录,然后在redis目录中创建task、temp、default等目录,然后将各个相关文件放到对应的目录中,所有服务都是按照同样的标准编写的,这样比较规范,方便自己编写,也方便在以后找到对应的各种文件,听了狗蛋的描述,我觉得他的方法似乎更加规范,于是我决定,向狗蛋看齐,学习他的那一套规范,并且把狗蛋的规范写成文档,以便公司以后的新同事可以按照文档规范的进行操作,这样所有人生产出的内容都是按照统一的规范来的,不仅风格统一,而且也方便其他人阅读自己编写的内容。

    我和狗蛋商量好了统一的规范以后,很快便形成了具体的文档,我俩自豪的把规范发布到了公司的wiki系统中,并且邀功似的向技术总监展示了我俩编写的"规范",总监打开规范文档,还没有仔细看两眼就笑了笑说:"你俩不要重复造轮子了,ansible官方早就制定了一套统一的规范,全世界使用ansible的老铁都是按照官方的规范来操作的,如果公司按照你俩制定的规范来,岂不是反而不符合大多数人的规范吗?",我和狗蛋本来是想嘚瑟一下,谁知道最后反而被总监教育了一番,总监告诉我们,ansible官方制定的那一套规范被称之为"角色(Roles)",让我俩赶紧去了解一下。

    我和狗蛋真是后知后觉,原来官方早就有"角色"这种东西,角色是一种解决问题的思想,也是一种通用的规范,比如,我们可以把nginx的相关配置过程抽象成一个nginx角色,以便下次需要进行同样的配置时,调用这个角色,同理,redis的配置过程也可以抽象成一个redis角色,不过,无论是哪种角色,都是按照ansible官方规范好的目录结构进行创建的,那么此处,我们就来了解一下ansible官方定义的这个"规范",也就是所谓的"角色"。

    为了让你更直观的认识角色的标准结构,我提前创建了一个示例角色,示例角色的目录结构如下:
    1.png
    如你所见,我创建的示例角色的角色名为demorole,demorole目录就代表了这个角色,此目录中包含了defaults 、files 、handlers 、meta  、tasks 、templates 、vars等子目录,而且在defaults 、handlers 、meta  、tasks 、vars等目录中,还都有一个名为"main.yml"的文件,那么这样的目录结构代表了什么含义呢?我们一起来了解一下,在角色中,上述目录结构的作用如下:
    注:此处先进行大致介绍,以便你有一个大概的印象,之后会有对应的示例,所以,如果有疑问请先保留。

    tasks目录:角色需要执行的主任务文件放置在此目录中,默认的主任务文件名为main.yml,当调用角色时,默认会执行main.yml文件中的任务,你也可以将其他需要执行的任务文件通过include的方式包含在tasks/main.yml文件中。
    handlers目录:当角色需要调用handlers时,默认会在此目录中的main.yml文件中查找对应的handler
    defaults目录:角色会使用到的变量可以写入到此目录中的main.yml文件中,通常,defaults/main.yml文件中的变量都用于设置默认值,以便在你没有设置对应变量值时,变量有默认的值可以使用,定义在defaults/main.yml文件中的变量的优先级是最低的。
    vars目录:角色会使用到的变量可以写入到此目录中的main.yml文件中,看到这里你肯定会有疑问,vars/main.yml文件和defaults/main.yml文件的区别在哪里呢?区别就是,defaults/main.yml文件中的变量的优先级是最低的,而vars/main.yml文件中的变量的优先级非常高,如果你只是想提供一个默认的配置,那么你可以把对应的变量定义在defaults/main.yml中,如果你想要确保别人在调用角色时,使用的值就是你指定的值,则可以将变量定义在vars/main.yml中,因为定义在vars/main.yml文件中的变量的优先级非常高,所以其值比较难以覆盖。
    meta目录:如果你想要赋予这个角色一些元数据,则可以将元数据写入到meta/main.yml文件中,这些元数据用于描述角色的相关属性,比如 作者信息、角色主要作用等等,你也可以在meta/main.yml文件中定义这个角色依赖于哪些其他角色,或者改变角色的默认调用设定,在之后会有一些实际的示例,此处不用纠结。
    templates目录: 角色相关的模板文件可以放置在此目录中,当使用角色相关的模板时,如果没有指定路径,会默认从此目录中查找对应名称的模板文件。
    files目录:角色可能会用到的一些其他文件可以放置在此目录中,比如,当你定义nginx角色时,需要配置https,那么相关的证书文件即可放置在此目录中。

    当然,上述目录并不全是必须的,也就是说,如果你的角色并没有相关的模板文件,那么角色目录中并不用包含templates目录,同理,其他目录也一样,一般情况下,都至少会有一个tasks目录。

    看完上述描述,你可能还是有一些小疑惑,不如我们来动手写一个简单的角色,这样就比较容易理解了。
    注:当前用于测试的ansible版本为2.7.0,以下所有示例基于此版本进行。

    为了熟悉角色的使用,我们一起来纯手动的编写一个用于测试的角色吧,这个角色的名字就叫"testrole"。
    首先,我们创建一个名为"testrole"的目录,这个目录就代表了"testrole"角色,执行如下命令即可:
    1. # mkdir testrole
    复制代码
    角色目录创建完毕,我决定先赋予testrole角色一个简单的功能,即输出"hello role"这句话,没错,通过debug模块可以输出信息,那么我们需要编写一个debug任务,之前提到过,调用角色时,角色会默认执行tasks/main.yml中的任务,那么我们就把debug任务写在tasks/main.yml文件中吧,首先,在testrole目录中创建tasks子目录,在tasks子目录中创建一个名为main.yml的文件。
    1. # mkdir tasks
    2. # touch tasks/main.yml
    复制代码
    在tasks/main.yml文件中写入如下内容
    1. - debug:
    2.     msg: "hello role!"
    复制代码
    如你所见,直接将我们需要执行的任务写在tasks/main.yml文件中即可。

    我们并不需要创建handlers、defaults等目录,因为目前我们编写的角色非常简单,用不到这些目录结构。

    我们的第一个测试角色编写完毕了~,就是这么简单,角色编写完毕后,就需要调用对应的角色了,那么怎样才能调用角色呢?其实也很简单,我们只需要在testrole的同级目录中编写一个简单的剧本即可,此例中,调用角色的剧本文件名为test.yml,它与testrole目录处于同级目录中,如下:
    1. # ls
    2. testrole  test.yml
    复制代码
    我们需要使用test.yml文件来调用testrole角色,test.yml文件的内容如下:
    1. # cat test.yml
    2. - hosts: test70
    3.   roles:
    4.   - testrole
    复制代码
    如上例所示,调用对应的角色时,需要使用roles关键字进行调用,使用"- hosts"指定目标主机,上例表示,在目标主机test70上执行testrole角色对应的任务,但是由于testrole并没有什么其他操作,只是输出了一句话,所以并不会对目标主机有什么实际动作。

    那么我们执行test.yml,看看这个测试角色能不能正常被调用,执行后结果如下:
    1. # ansible-playbook test.yml

    2. PLAY [test70] *************************************************

    3. TASK [Gathering Facts] *****************************************
    4. ok: [test70]

    5. TASK [testrole : debug] ****************************************
    6. ok: [test70] => {
    7.     "msg": "hello role!"
    8. }

    9. PLAY RECAP *************************************************
    10. test70                     : ok=2    changed=0    unreachable=0    failed=0
    复制代码
    如我们所愿,角色被正常调用了,"hello role!"输出了,没错,我们已经学会使用角色了。

    看到此处,你会不会觉得我把简单的问题复杂化了,如果你只是单纯的调用一个debug任务输出一句话,那么是有点复杂化了,但是如果你的配置过程慢慢变得丰富,文件越来越多,结构越来越复杂,那么使用角色会是更好的选择,它能让你的文件结构符合统一的标准,让任何一个懂得这个标准的人快速的阅读你的代码,并且为以后的扩展留出很大的空间,而且,通过刚才的调用过程,你应该已经明白了,我可以将testrole目录移至到任何ansible主机中进行调用,testrole目录中包含了这个角色所需的所有文件,它是一个独立的的结构,说到独立,它在逻辑上也是独立的,因为这个角色的配置过程与目标主机是分开的,虽然我们调用角色时,需要编写一个playbook指定对应的目标主机,但是我们并没有修改角色目录中的任何文件,这正好解决了本文开头提出的问题。

    刚才在调用角色时,我刻意的将test.yml文件写在了testrole目录的同级目录中,也就是说,调用testrole角色时,test.yml会从同级目录中查找与testrole角色同名的目录,其实,不仅仅是同级目录,还有一些其他的目录,在调用角色时,test.yml也会去查找,这些目录就是:
    同级目录中的roles目录中。
    当前系统用户的家目录中的.ansible/roles目录,即 ~/.ansible/roles目录中。
    也就是说,只要testrole目录处于上述三个目录中的任何一个目录中,都可以使用上述方法正常的调用。

    你也可修改ansible的配置文件,设置自己的角色搜索目录,编辑/etc/ansible/ansible.cfg配置文件,设置roles_path选项,此项默认是注释掉的,将注释符去掉,当你想要设置多个路径时,多个路径之间用冒号隔开,示例如下
    1. roles_path    = /etc/ansible/roles:/opt:/testdir
    复制代码
    即使你的角色目录不处于上述目录中的任何一个,也可以使用绝对路径的方式,调用对应的角色,示例如下:
    1. - hosts: test70
    2.   roles:
    3.   - "/testdir/ansible/testrole/"
    复制代码
    如上例所示,我直接使用了testrole的绝对路径,调用了testrole角色,其实,上述写法不是特别正规,标准的语法应该是如下模样:
    1. - hosts: test70
    2.   roles:
    3.   - role: "/testdir/ansible/testrole/"
    复制代码
    没错,在roles关键字中使用role关键字指定角色对应的绝对路径,也可以直接调用角色,即使不使用绝对路径,也可以使用同样的语法指定角色名,如下:
    1. - hosts: test70
    2.   roles:
    3.   - role: testrole
    复制代码
    除了上述调用角色的语法,还有一些其他的语法也可以调用角色,不过后文中再行总结,先往下聊。

    上述示例中,我们没有使用任何变量,那么我们来尝试一下在角色中使用变量。
    我们将tasks/main.yml中的内容改为如下内容:
    1. - debug:
    2.     msg: "hello {{ testvar }} !"
    复制代码
    如上例所示,我们在输出的信息中使用了testvar变量,那么,我们在调用这个角色时,则需要传入对应的变量,否则就会报错,调用上例角色的示例如下:
    1. - hosts: test70
    2.   roles:
    3.   - role: testrole
    4.     vars:
    5.       testvar: "www.cnblackhat.com"
    复制代码
    如上例所示,我们在调用角色时,传入了对应的testvar变量,以便对应的任务可以使用这个变量。
    执行上例playbook,最终输出的信息为"hello www.cnblackhat.com !"

    其实,我们也可以为testvar变量设置默认值,这样即使在调用角色时没有传入任何参数,也有默认的值可以使用,同时也不会在调用时因为没有传入对应变量而报错,所以,我们需要在testrole目录中创建一个defaults目录,并且创建defaults/main.yml文件,defaults/main.yml文件内容如下:
    1. # cat testrole/defaults/main.yml
    2. testvar: "role"
    复制代码
    如你所见,我们在defaults/main.yml文件中定义了testvar变量,默认值为"role"
    此刻,我们调用testrole时,即使不传入testvar变量,也可以正常的进行调用了,如果不传入testvar变量,则默认所使用"role"作为变量值。

    需要注意的是,在默认情况下,角色中的的变量是全局可访问的。
    这样说可能不容易理解,不如来看一个小示例,如下
    2.png
    如上图所示,我定义了两个示例角色,这两个示例角色中都使用了名为testvar的变量,而且在这两个角色中,testvar变量都有各自的默认值,在testrole角色中,testvar的默认值为"test",在demorole角色中,testvar的默认值为"demo",在test.yml文件中,我调用了这两个角色,在调用testrole角色时,我传入了testvar变量,其值为zsythink,但是在调用demorole角色时,没有传入testvar变量,按照正常的理解,当执行test.yml文件时,testrole应该使用"zsythink"作为testvar变量的值,demorole应该使用默认值"demo"作为testvar变量的值,那么我们来执行一下test.yml,看看结果与我们想象的是否相同,执行结果如下
    1. # ansible-playbook test.yml

    2. PLAY [test70] *************************************************

    3. TASK [Gathering Facts] *****************************************
    4. ok: [test70]

    5. TASK [testrole : debug] *****************************************
    6. ok: [test70] => {
    7.     "msg": "hello zsythink!"
    8. }

    9. TASK [demorole : debug] **************************************
    10. ok: [test70] => {
    11.     "msg": "hello zsythink!"
    12. }

    13. PLAY RECAP *************************************************
    14. test70                     : ok=3    changed=0    unreachable=0    failed=0
    复制代码
    如你所见,结果与我预想的并不相同,无论是testrole还是demorole,都使用了"zsythink"作为了testvar的变量值,出现上述状况的原因我们刚才已经提到过,原因是:在默认情况下,角色中的变量是全局可访问的,上例中,当将testvar变量的值设置为"zsythink"时,就表示将testrole和demorole中的testvar变量的值都设置成了"zsythink",所以最终输出信息时,两个角色的testvar变量都使用了相同的值。

    如果想要解决上述问题,则可以将变量的访问域变成角色所私有的,如果想要将变量变成角色私有的,则需要设置/etc/ansible/ansible.cfg文件,将private_role_vars的值设置为yes,默认情况下,"private_role_vars = yes"是被注释掉的,将前面的注释符去掉皆可,设置完成后,再次执行上例中的test.yml文件,输出结果如下:
    1. # ansible-playbook test.yml

    2. PLAY [test70] *************************************************

    3. TASK [Gathering Facts] *****************************************
    4. ok: [test70]

    5. TASK [testrole : debug] *****************************************
    6. ok: [test70] => {
    7.     "msg": "hello zsythink!"
    8. }

    9. TASK [demorole : debug] **************************************
    10. ok: [test70] => {
    11.     "msg": "hello demo!"
    12. }

    13. PLAY RECAP *************************************************
    14. test70                     : ok=3    changed=0    unreachable=0    failed=0
    复制代码
    默认情况下,我们无法多次调用同一个角色,也就是说,如下playbook只会调用一次testrole角色:
    1. # cat test.yml
    2. - hosts: test70
    3.   roles:
    4.   - role: testrole
    5.   - role: testrole
    复制代码
    执行上例playbook会发现,testrole的debug模块只输出了一次,如果想要多次调用同一个角色,有两种方法,如下:
    方法一:设置角色的allow_duplicates属性 ,让其支持重复的调用。
    方法二:调用角色时,传入的参数值不同。

    方法一需要为角色设置allow_duplicates属性,而此属性需要设置在meta/main.yml文件中,所以我们需要在testrole中创建meta/main.yml文件,写入如下内容:
    1. # cat testrole/meta/main.yml
    2. allow_duplicates: true
    复制代码
    如上例所示,我们将allow_duplicates属性设置为true,表示可以重复调用同一个角色。
    属性设置完毕后,执行如下playbook尝试两次调用同一个角色,是完全可以正常执行的。
    1. # cat test.yml
    2. - hosts: test70
    3.   roles:
    4.   - role: testrole
    5.   - role: testrole
    复制代码
    说完方法一,现在来说说方法二,当调用角色需要传参时,如果参数的值不同,则可以连续调用多次
    下例中,两次调用了testrole角色,两次调用都传入了testvar变量,但是testvar变量的值不同。
    1. # cat test.yml
    2. - hosts: test70
    3.   roles:
    4.   - role: testrole
    5.     vars:
    6.       testvar: "zsythink"
    7.   - role: testrole
    8.     vars:
    9.       testvar: "zsythink.net"
    复制代码
    使用上例的方法,也可以对同一角色调用多次。

    在上述示例中,我们已经学会了如何在defaults/main.yml文件中定义变量的默认值,不过我们还没有使用过vars/main.yml文件,也没有在其中定义过任何变量,之前提到过,定义在vars/main.yml文件中的变量优先级比较高,难以被覆盖,那我们就来动手试试,看看定义在这个文件中的变量的优先级到底有多高,为了使效果更加明显,我们在defaults/main.yml文件和vars/main.yml文件中同时定义testvar变量,并为其赋值不同的值,如下:
    1. # cat testrole/defaults/main.yml
    2. testvar: "test"
    3. # cat testrole/vars/main.yml
    4. testvar: "testvar_in_vars_directory"
    复制代码
    同时,我们在调用testrole时,仍然传入testvar变量,看看testvar变量到底会使用哪个值作为最终的值,示例如下:
    1. # cat test.yml
    2. - hosts: test70
    3.   roles:
    4.   - role: testrole
    5.     vars:
    6.       testvar: "zsythink"
    复制代码
    执行上例playbook,最终结果如下:
    1. # ansible-playbook test.yml

    2. PLAY [test70] *************************************************

    3. TASK [Gathering Facts] *****************************************
    4. ok: [test70]

    5. TASK [testrole : debug] ****************************************
    6. ok: [test70] => {
    7.     "msg": "hello testvar_in_vars_directory!"
    8. }

    9. PLAY RECAP *************************************************
    10. test70                     : ok=2    changed=0    unreachable=0    failed=0
    复制代码
    从上述信息可以看出,即使在调用角色的时候传入对应的变量,也无法覆盖定义在vars/main.yml文件中的值,那么我们可以利用这个特性,将你想要确保使用的值定义在vars/main.yml中,以便别人在调用角色时,使用的值就是你定义的值,当然,如果你强烈推荐的值别人压根不想使用,也是有办法灵活的进行覆盖的,比如在调用playbook时使用"-e"选项传入参数,示例如下:
    1. # ansible-playbook -e testvar='usethis' test.yml

    2. PLAY [test70] *************************************************

    3. TASK [Gathering Facts] *****************************************
    4. ok: [test70]

    5. TASK [testrole : debug] ****************************************
    6. ok: [test70] => {
    7.     "msg": "hello usethis!"
    8. }

    9. PLAY RECAP *************************************************
    10. test70                     : ok=2    changed=0    unreachable=0    failed=0
    复制代码
    除了使用"-e"传入的变量的优先级,其他变量(包括主机变量)的优先级均低于vars/main.yml中变量的优先级。

    假设现在testrole需要使用一些模板,那么也可以直接将模板文件放到templates目录中。
    比如,testrole中需要使用一个名为test.conf.j2的模板文件,那么我们就将test.conf.j2文件放置在testrole/templates/目录中,test.conf.j2文件内容如下
    1. # cat testrole/templates/test.conf.j2
    2. something in template;
    3. {{ template_var }}
    复制代码
    模板文件中使用到了 template_var变量,我们可以为 template_var变量定义一个默认变量
    1. # cat testrole/defaults/main.yml
    2. testvar: "test"
    3. template_var: "template"
    复制代码
    然后在testrole中,直接使用这个模板文件
    1. # cat testrole/tasks/main.yml
    2. - debug:
    3.     msg: "hello {{ testvar }}!"
    4. - template:
    5.     src: test.conf.j2
    6.     dest: /opt/test.conf
    复制代码
    如上例所示,我们在使用template任务时,src直接指定了对应的模板文件的名称,并没有指定任何路径,这代表角色会默认去templates子目录中查找对应的文件。

    如果你想要在角色中使用一些handlers以便进行触发,则可以直接将对应的handler任务写入到handlers/main.yml文件中,示例如下:
    1. # cat testrole/handlers/main.yml
    2. - name: test_handler
    3.   debug:
    4.     msg: "this is a test handler"
    复制代码
    我直接在handlers/main.yml文件中写入了一个名为"test_handler"任务,以便随时进行触发。
    为了能够更加简单的触发对应的handler,我直接将tasks/main.yml中的debug任务的状态强行设置为"changed",示例如下:
    注:前文已经总结了changed_when的用法,此处不再赘述。
    1. # cat testrole/tasks/main.yml
    2. - debug:
    3.     msg: "hello testrole!"
    4.   changed_when: true
    5.   notify: test_handler
    复制代码
    如上例所示,当需要notify对应handler时,直接写入handler对应的名称即可,角色会自动去handlers/main.yml文件中查找对应的handler。

    经过上述描述,你肯定已经对"角色"有了初步的认识,不过我们还有很多关于角色的话题没有聊,比如使用ansible-galaxy命令快速创建角色、下载角色、管理角色,比如其他引用角色的方法,等等,这些就放在后面的文章中进行总结吧,这篇文章先写到这里,希望能够对你有所帮助~

    帖子永久地址: 

    黑帽联盟 - 论坛版权1、本主题所有言论和图片纯属会员个人意见,与本论坛立场无关
    2、本站所有主题由该帖子作者发表,该帖子作者与黑帽联盟享有帖子相关版权
    3、其他单位或个人使用、转载或引用本文时必须同时征得该帖子作者和黑帽联盟的同意
    4、帖子作者须承担一切因本文发表而直接或间接导致的民事或刑事法律责任
    5、本帖部分内容转载自其它媒体,但并不代表本站赞同其观点和对其真实性负责
    6、如本帖侵犯到任何版权问题,请立即告知本站,本站将及时予与删除并致以最深的歉意
    7、黑帽联盟管理员和版主有权不事先通知发贴者而删除本文

    您需要登录后才可以回帖 登录 | 会员注册

    发布主题 !fastreply! 收藏帖子 返回列表 搜索
    回顶部