ansible初次试探

作为Python运维开发人员,老早就注意了ansible,saltstack。
以前专门去买腾讯云的机器来研究saltstack。现在是免费领取了华为的云机器来研究。忧伤~
使用ansible 2.4版本的,其它老版本不适合这种情况哦!把主控端A的公钥发送给被控端B :

sudo ssh-copy-id -i ~/.ssh/id_rsa.pub [email protected]  

观察B我们就发现了一个新的文件authorized_keys。

[root@fdafda .ssh]# pwd  
/root/.ssh  
[root@fdafda .ssh]# ls -l  
total 4  
-rw------- 1 root root 748 Jan 22 16:55 authorized_keys  
[root@fdafda .ssh]# cat authorized_keys  
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDFB98VspJGjtAaTQ3pJCgrl9MbOSgxz2AZZPQ9eIjzAnJfNsfLT7JClgIRa/plQjOwKAP1wUZ631mD2BzGenf05dicgFLYcfhd3g/68Fugymd6Oejbb4XciTJiHh4965vc0+P8vhQZ5wMT8ZGxQwOL8Uabw9HoKdzcckZtUol6axmP25Jv28+3kRXJov8VubahVgeVkNPYETBElKDb+lilm+yxLNRez1euzzBbYyb5ak6sDeMHuo8ZakyO1+lSl1Dv33a3E/eqLWSo0YpbVVhqkapTuFAklJ33vMo+Ejw2WjmHgpnLsevFfAb9Qc9pJoGVylREeZwEjLLjhEt9eW6el2MMxxj+8siNYA2zHnYp2YXZprDSj9SxpUgOp0A8P3AeIlARrnOF87QsXBx4V7z8x8itSrMs++OIbLhzCbBl8KbZtV8HvI2ITpfVaLdLBCoox0Bp/kZrXHyM81A0axskPhBrQ178uXY1rS3qKv7v73n93OCA9X4bHNtHhRlFeVQ7SpdHnkHvW2dNy+5gBdbOKigvtFGdMyvqyL2J5grL+K7zKfNt2PYg0wvSAbvvXBRGJQ+Pzi6azhlEdwdtA4HxYilFI22luIYmLEWKIi2d4cCfU1UWN94yMSdkVv4sYsXhXZeHslsis8L/Vm6TmChCTf7R2f8VeKugOuptQRbwFQ== [email protected]  

1、ansible模块因为随处可见,暂时不写
2、ansible playbook使用
demo:这是我的playbook.yml文件。example为hosts里面的组名

未分类

然后执行:

ansible-playbook ./playbook.yml  

结果:

未分类

目标机器上:

未分类

Ansible扩展

Ansible简介

Ansible是由Python开发的一个运维工具,因为工作需要接触到Ansible,经常会集成一些东西到Ansible,所以对Ansible的了解越来越多。

那Ansible到底是什么呢?在我的理解中,原来需要登录到服务器上,然后执行一堆命令才能完成一些操作。而Ansible就是来代替我们去执行那些命令。并且可以通过Ansible控制多台机器,在机器上进行任务的编排和执行,在Ansible中称为playbook。

那Ansible是如何做到的呢?简单点说,就是Ansible将我们要执行的命令生成一个脚本,然后通过sftp将脚本上传到要执行命令的服务器上,然后在通过ssh协议,执行这个脚本并将执行结果返回。

那Ansible具体是怎么做到的呢?下面从模块和插件来看一下Ansible是如何完成一个模块的执行

PS:下面的分析都是在对Ansible有一些具体使用经验之后,通过阅读源代码进一步得出的执行结论,所以希望在看本文时,是建立在对Ansible有一定了解的基础上,最起码对于Ansible的一些概念有了解,例如inventory,module,playbooks等

Ansible模块

模块是Ansible执行的最小单位,可以是由Python编写,也可以是Shell编写,也可以是由其他语言编写。模块中定义了具体的操作步骤以及实际使用过程中所需要的参数

执行的脚本就是根据模块生成一个可执行的脚本。

那Ansible是怎么样将这个脚本上传到服务器上,然后执行获取结果的呢?

Ansible插件

connection插件

连接插件,根据指定的ssh参数连接指定的服务器,并切提供实际执行命令的接口

shell插件

命令插件,根据sh类型,来生成用于connection时要执行的命令

strategy插件

执行策略插件,默认情况下是线性插件,就是一个任务接着一个任务的向下执行,此插件将任务丢到执行器去执行。

action插件

动作插件,实质就是任务模块的所有动作,如果ansible的模块没有特别编写的action插件,默认情况下是normal或者async(这两个根据模块是否async来选择),normal和async中定义的就是模块的执行步骤。例如,本地创建临时文件,上传临时文件,执行脚本,删除脚本等等,如果想在所有的模块中增加一些特殊步骤,可以通过增加action插件的方式来扩展。

Ansible执行模块流程

  • ansible命令实质是通过ansible/cli/adhoc.py来运行,同时会收集参数信息
    • 设置Play信息,然后通过TaskQueueManager进行run,
    • TaskQueueManager需要Inventory(节点仓库),variable_manager(收集变量),options(命令行中指定的参数),stdout_callback(回调函数)
  • 在task_queue_manager.py中找到run中
    • 初始化时会设置队列
    • 会根据options,,variable_manager,passwords等信息设置成一个PlayContext信息(playbooks/playcontext.py)
    • 设置插件(plugins)信息callback_loader(回调), strategy_loader(执行策略), module_loader(任务模块)
    • 通过strategy_loader(strategy插件)的run(默认的strategy类型是linear,线性执行),去按照顺序执行所有的任务(执行一个模块,可能会执行多个任务)
    • 在strategy_loader插件run之后,会判断action类型。如果是meta类型的话会单独执行(不是具体的ansible模块时),而其他模块时,会加载到队列_queue_task
    • 在队列中会调用WorkerProcess去处理,在workerproces实际的run之后,会使用TaskExecutor进行执行
    • 在TaskExecutor中会设置connection插件,并且根据task的类型(模块。或是include等)获取action插件,就是对应的模块,如果模块有自定义的执行,则会执行自定义的action,如果没有的会使用normal或者async,这个是根据是否是任务的async属性来决定
  • 在Action插件中定义着执行的顺序,及具体操作,例如生成临时目录,生成临时脚本,所以要在统一的模式下,集成一些额外的处理时,可以重写Action的方法
  • 通过Connection插件来执行Action的各个操作步骤

扩展Ansible实例

执行节点Python环境扩展

实际需求中,我们扩展的一些Ansible模块需要使用三方库,但每个节点中安装这些库有些不易于管理。ansible执行模块的实质就是在节点的python环境下执行生成的脚本,所以我们采取的方案是,指定节点上的Python环境,将局域网内一个python环境作为nfs共享。通过扩展Action插件,增加节点上挂载nfs,待执行结束后再将节点上的nfs卸载。具体实施步骤如下:

扩展代码:

重写ActionBase的execute_module方法

# execute_module

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

import json
import pipes

from ansible.compat.six import text_type, iteritems

from ansible import constants as C
from ansible.errors import AnsibleError
from ansible.release import __version__

try:
    from __main__ import display
except ImportError:
    from ansible.utils.display import Display
    display = Display()


class MagicStackBase(object):

    def _mount_nfs(self, ansible_nfs_src, ansible_nfs_dest):
        cmd = ['mount',ansible_nfs_src, ansible_nfs_dest]
        cmd = [pipes.quote(c) for c in cmd]
        cmd = ' '.join(cmd)
        result = self._low_level_execute_command(cmd=cmd, sudoable=True)
        return result

    def _umount_nfs(self, ansible_nfs_dest):
        cmd = ['umount', ansible_nfs_dest]
        cmd = [pipes.quote(c) for c in cmd]
        cmd = ' '.join(cmd)
        result = self._low_level_execute_command(cmd=cmd, sudoable=True)
        return result

    def _execute_module(self, module_name=None, module_args=None, tmp=None, task_vars=None, persist_files=False, delete_remote_tmp=True):
        '''
        Transfer and run a module along with its arguments.
        '''

        # display.v(task_vars)

        if task_vars is None:
            task_vars = dict()

        # if a module name was not specified for this execution, use
        # the action from the task
        if module_name is None:
            module_name = self._task.action
        if module_args is None:
            module_args = self._task.args

        # set check mode in the module arguments, if required
        if self._play_context.check_mode:
            if not self._supports_check_mode:
                raise AnsibleError("check mode is not supported for this operation")
            module_args['_ansible_check_mode'] = True
        else:
            module_args['_ansible_check_mode'] = False

        # Get the connection user for permission checks
        remote_user = task_vars.get('ansible_ssh_user') or self._play_context.remote_user

        # set no log in the module arguments, if required
        module_args['_ansible_no_log'] = self._play_context.no_log or C.DEFAULT_NO_TARGET_SYSLOG

        # set debug in the module arguments, if required
        module_args['_ansible_debug'] = C.DEFAULT_DEBUG

        # let module know we are in diff mode
        module_args['_ansible_diff'] = self._play_context.diff

        # let module know our verbosity
        module_args['_ansible_verbosity'] = display.verbosity

        # give the module information about the ansible version
        module_args['_ansible_version'] = __version__

        # set the syslog facility to be used in the module
        module_args['_ansible_syslog_facility'] = task_vars.get('ansible_syslog_facility', C.DEFAULT_SYSLOG_FACILITY)

        # let module know about filesystems that selinux treats specially
        module_args['_ansible_selinux_special_fs'] = C.DEFAULT_SELINUX_SPECIAL_FS

        (module_style, shebang, module_data) = self._configure_module(module_name=module_name, module_args=module_args, task_vars=task_vars)
        if not shebang:
            raise AnsibleError("module (%s) is missing interpreter line" % module_name)

        # get nfs info for mount python packages
        ansible_nfs_src = task_vars.get("ansible_nfs_src", None)
        ansible_nfs_dest = task_vars.get("ansible_nfs_dest", None)

        # a remote tmp path may be necessary and not already created
        remote_module_path = None
        args_file_path = None
        if not tmp and self._late_needs_tmp_path(tmp, module_style):
            tmp = self._make_tmp_path(remote_user)

        if tmp:
            remote_module_filename = self._connection._shell.get_remote_filename(module_name)
            remote_module_path = self._connection._shell.join_path(tmp, remote_module_filename)
            if module_style in ['old', 'non_native_want_json']:
                # we'll also need a temp file to hold our module arguments
                args_file_path = self._connection._shell.join_path(tmp, 'args')

        if remote_module_path or module_style != 'new':
            display.debug("transferring module to remote")
            self._transfer_data(remote_module_path, module_data)
            if module_style == 'old':
                # we need to dump the module args to a k=v string in a file on
                # the remote system, which can be read and parsed by the module
                args_data = ""
                for k,v in iteritems(module_args):
                    args_data += '%s=%s ' % (k, pipes.quote(text_type(v)))
                self._transfer_data(args_file_path, args_data)
            elif module_style == 'non_native_want_json':
                self._transfer_data(args_file_path, json.dumps(module_args))
            display.debug("done transferring module to remote")

        environment_string = self._compute_environment_string()

        remote_files = None

        if args_file_path:
            remote_files = tmp, remote_module_path, args_file_path
        elif remote_module_path:
            remote_files = tmp, remote_module_path

        # Fix permissions of the tmp path and tmp files.  This should be
        # called after all files have been transferred.
        if remote_files:
            self._fixup_perms2(remote_files, remote_user)


        # mount nfs
        if ansible_nfs_src and ansible_nfs_dest:
            result = self._mount_nfs(ansible_nfs_src, ansible_nfs_dest)
            if result['rc'] != 0:
                raise AnsibleError("mount nfs failed!!! {0}".format(result['stderr']))

        cmd = ""
        in_data = None

        if self._connection.has_pipelining and self._play_context.pipelining and not C.DEFAULT_KEEP_REMOTE_FILES and module_style == 'new':
            in_data = module_data
        else:
            if remote_module_path:
                cmd = remote_module_path

        rm_tmp = None
        if tmp and "tmp" in tmp and not C.DEFAULT_KEEP_REMOTE_FILES and not persist_files and delete_remote_tmp:
            if not self._play_context.become or self._play_context.become_user == 'root':
                # not sudoing or sudoing to root, so can cleanup files in the same step
                rm_tmp = tmp

        cmd = self._connection._shell.build_module_command(environment_string, shebang, cmd, arg_path=args_file_path, rm_tmp=rm_tmp)
        cmd = cmd.strip()
        sudoable = True
        if module_name == "accelerate":
            # always run the accelerate module as the user
            # specified in the play, not the sudo_user
            sudoable = False


        res = self._low_level_execute_command(cmd, sudoable=sudoable, in_data=in_data)

        # umount nfs
        if ansible_nfs_src and ansible_nfs_dest:
            result = self._umount_nfs(ansible_nfs_dest)
            if result['rc'] != 0:
                raise AnsibleError("umount nfs failed!!! {0}".format(result['stderr']))

        if tmp and "tmp" in tmp and not C.DEFAULT_KEEP_REMOTE_FILES and not persist_files and delete_remote_tmp:
            if self._play_context.become and self._play_context.become_user != 'root':
                # not sudoing to root, so maybe can't delete files as that other user
                # have to clean up temp files as original user in a second step
                tmp_rm_cmd = self._connection._shell.remove(tmp, recurse=True)
                tmp_rm_res = self._low_level_execute_command(tmp_rm_cmd, sudoable=False)
                tmp_rm_data = self._parse_returned_data(tmp_rm_res)
                if tmp_rm_data.get('rc', 0) != 0:
                    display.warning('Error deleting remote temporary files (rc: {0}, stderr: {1})'.format(tmp_rm_res.get('rc'), tmp_rm_res.get('stderr', 'No error string available.')))

        # parse the main result
        data = self._parse_returned_data(res)

        # pre-split stdout into lines, if stdout is in the data and there
        # isn't already a stdout_lines value there
        if 'stdout' in data and 'stdout_lines' not in data:
            data['stdout_lines'] = data.get('stdout', u'').splitlines()

        display.debug("done with _execute_module (%s, %s)" % (module_name, module_args))
        return data

集成到normal.py和async.py中,记住要将这两个插件在ansible.cfg中进行配置

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

from ansible.plugins.action import ActionBase
from ansible.utils.vars import merge_hash

from common.ansible_plugins import MagicStackBase


class ActionModule(MagicStackBase, ActionBase):

    def run(self, tmp=None, task_vars=None):
        if task_vars is None:
            task_vars = dict()

        results = super(ActionModule, self).run(tmp, task_vars)
        # remove as modules might hide due to nolog
        del results['invocation']['module_args']
        results = merge_hash(results, self._execute_module(tmp=tmp, task_vars=task_vars))
        # Remove special fields from the result, which can only be set
        # internally by the executor engine. We do this only here in
        # the 'normal' action, as other action plugins may set this.
        #
        # We don't want modules to determine that running the module fires
        # notify handlers.  That's for the playbook to decide.
        for field in ('_ansible_notify',):
            if field in results:
                results.pop(field)

        return results
  • 配置ansible.cfg,将扩展的插件指定为ansible需要的action插件
  • 重写插件方法,重点是execute_module
  • 执行命令中需要指定Python环境,将需要的参数添加进去nfs挂载和卸载的参数
ansible 51 -m mysql_db -a "state=dump name=all target=/tmp/test.sql" -i hosts -u root -v -e "ansible_nfs_src=172.16.30.170:/web/proxy_env/lib64/python2.7/site-packages ansible_nfs_dest=/root/.pyenv/versions/2.7.10/lib/python2.7/site-packages ansible_python_interpreter=/root/.pyenv/versions/2.7.10/bin/python"
1
ansible 51 -m mysql_db -a "state=dump name=all target=/tmp/test.sql" -i hosts -u root -v -e "ansible_nfs_src=172.16.30.170:/web/proxy_env/lib64/python2.7/site-packages ansible_nfs_dest=/root/.pyenv/versions/2.7.10/lib/python2.7/site-packages ansible_python_interpreter=/root/.pyenv/versions/2.7.10/bin/python"

ansible笔记(1):ansible的基本概念

一些基础概念

ansible是什么?
它是一个”配置管理工具”,它是一个”自动化运维工具”,如果你没有使用过任何配置管理工具,不要害怕,看完这篇文章,你自然会对ansible有所了解。

ansible能做什么?
正如其他配置管理工具一样,ansible可以帮助我们完成一些批量任务,或者完成一些需要经常重复的工作。
比如:同时在100台服务器上安装nginx服务,并在安装后启动它们。
比如:将某个文件一次性拷贝到100台服务器上。
比如:每当有新服务器加入工作环境时,你都要为新服务器部署redis服务,也就是说你需要经常重复的完成相同的工作。
这些场景中我们都可以使用到ansible。

看到这里,你可能会说,我编写一些脚本,也能够满足上面的工作场景,为什么还要使用ansible呢?没错,使用脚本也可以完成这些工作,不过我还是推荐你使用ansible,因为ansible支持一些优秀的特性,比如”幂等性”,”幂等性”是什么意思呢?举个例子,你想把一个文件拷贝到目标主机的某个目录上,但是你不确定此目录中是否已经存在此文件,当你使用ansible完成这项任务时,就非常简单了,因为如果目标主机的对应目录中已经存在此文件,那么ansible则不会进行任何操作,如果目标主机的对应目录中并不存在此文件,ansible就会将文件拷贝到对应目录中,说白了,ansible是”以结果为导向的”,我们指定了一个”目标状态”,ansible会自动判断,”当前状态”是否与”目标状态”一致,如果一致,则不进行任何操作,如果不一致,那么就将”当前状态”变成”目标状态”,这就是”幂等性”,”幂等性”可以保证我们重复的执行同一项操作时,得到的结果是一样的,这种特性在很多场景中相对于脚本来说都有一定优势,单单这样说,可能并不容易理解,当你在后面真正使用到时,自然会有自己的体会,所以此处不用纠结,继续向下看。

如果你了解过其他的配置管理工具,比如puppet或者saltstack,那么你一定知道,如果我们想要使用puppet管理100台主机,就要在这100台主机上安装puppet对应的agent(客户端代理程序),而ansible则不同,ansible只需要依赖ssh即可正常工作,不用在受管主机上安装agent,也就是说,只要你能通过ssh连接到对应主机,你就可以通过ansible管理对应的主机。

经过上述描述,我想你应该对ansible已经有了一个初步的、大概的印象:
ansible是一个配置管理工具,可以帮助我们完成一些批量工作或者重复性工作,ansible通过ssh管理其他受管主机,并且具有一些特性,比如幂等性、剧本、模板,角色等,我们会慢慢的介绍这些特性以及怎样使用ansible。

怎样使用ansible呢?我们通过一条简单的命令开始认识它吧,命令如下
注:执行如下命令前,需要进行一些配置,如下命令才能正常执行,后文中会对这些操作进行描述,此处先行略过
Shell

ansible 10.1.1.60 -m ping

上述命令表示,使用ansible去ping 10.1.1.60这台主机,很容易理解吧。
“ping”是ansible中的一个模块,这个模块的作用就是ping对应的主机,ansible调用ping模块,就相当于我们手动执行ping命令一样,上述命令中的”-m ping”表示调用ping模块,当然,ansible肯定不止这一个模块,它有很多模块,不同的模块可以帮助我们完成不同的工作,你应该已经猜到了,我们在实际使用时,会使用到各种模块,ansible是基于这些模块完成实际任务的。

刚才,我们使用了一个简单的ansible命令作为示例,但是如果想要让上述命令正常执行,则必须同时满足两个最基本的条件,如下
条件一、ansible所在的主机可以通过ssh连接到受管主机。
条件二、受管主机的IP地址等信息已经添加到ansible的”管理清单”中。

之前说过,ansible不用在受管主机上安装agent,但是它需要依赖ssh,所以,条件一并不难理解,但是,在满足条件一的情况下,还要同时满足条件二,也就是说,即使ansible所在的主机能够通过ssh连接到受管主机,仍然需要将受管主机的IP地址、ssh端口号等信息添加到一个被称作为”清单(Inventory)”的配置文件中,如果对应的主机信息在ansible的”清单”中不存在,那么ansible则无法操作对应主机,后文会详细的介绍怎样配置ansible的”清单”。

好了,基本概念先了解到这里,现在需要动动手了。

一些基础配置

我们首先要做的就是安装ansible。
但是在安装之前,先介绍一下我的演示环境。
我有四台主机,IP地址分别如下

10.1.1.71

10.1.1.70

10.1.1.61

10.1.1.60

我将主机10.1.1.71(后文中简称71)作为配置管理主机,所以我们需要在71上安装ansible,剩下的主机作为受管主机,主机71和主机70的的操作系统版本为centos7.4,主机61和主机60的操作系统版本为centos6.9。

我使用yum源的方式安装ansible,因为安装ansible需要epel源,所以我配置了阿里的epel源和centos7系统镜像源,yum源配置如下
Shell

# pwd
/etc/yum.repos.d

# cat aliBase.repo
[aliBase]
name=aliBase
baseurl=https://mirrors.aliyun.com/centos/$releasever/os/$basearch/
enabled=1
gpgcheck=1
gpgkey=https://mirrors.aliyun.com/centos/$releasever/os/$basearch/RPM-GPG-KEY-CentOS-$releasever

# cat aliEpel.repo
[aliEpel]
name=aliEpel
baseurl=https://mirrors.aliyun.com/epel/$releaseverServer/$basearch/
enabled=1
gpgcheck=0

yum源配置完成后,安装ansible

yum install ansible

此时yum源中对应的版本为ansible-2.4.2.0-1

安装完毕,不过别急,我们还需要做一些其他的基本配置,在介绍ansible的概念时,我们说过,如果想要通过ansible管理某主机,还需要将对应主机的信息添加到ansible的”配置清单”中,清单中没有的主机无法通过ansible进行配置管理,现在,我们就来介绍一下ansible的”清单”,当安装完ansible以后,ansible会提供一个默认的”清单”,这个清单就是/etc/ansible/hosts,打开此文件,你会看到一些配置示例,没错,还是熟悉的配方,还是熟悉的味道,此文件使用的就是INI的配置风格,那么,我们一起来看看怎样进行配置吧。

以我们的演示环境为例,我们想要通过ansible主机管理60主机,所以,最直接的方式就是将它的IP地址写入到/etc/ansible/hosts文件中,配置如下,在/etc/ansible/hosts文件底部写入如下IP
10.1.1.60
就是这么简单,那么,完成上述配置,就能够通过ansible主机管理10.1.1.60这台主机了吗?我们来动手试试,看看会发生什么情况。

执行之前的示例命令:ansible 10.1.1.60 -m ping
使用ansible去ping主机10.1.1.60,返回结果如下

未分类

从命令的返回信息中可以看到,10.1.1.60不可达,也就是说,ansible无法通过ssh连接到主机60。
返回上述信息是正常的,因为ansible主机并不知道10.1.1.60这台主机的用户名和密码,所以ansible无法通过ssh连接到它。
所以,我们还需要在清单中,配置10.1.1.60主机的ssh信息,才能够进行正确的进行连接,配置示例如下:

未分类

修改清单文件,在之前的主机IP后加入ssh的相关配置信息,如上图所示
ansible_port 用于配置对应主机上的sshd服务端口号,在实际的生产环境中,各个主机的端口号通常不会使用默认的22号端口,所以用此参数指定对应端口。
ansible_user 用于配置连接到对应主机时所使用的用户名称。
ansible_ssh_pass 用于配置对应用户的连接密码。
所以,上图中的配置表示,10.1.1.60这台主机的sshd服务监听在22号端口,当ansible通过ssh连接到主机60时,会使用主机60的root用户进行连接,主机60的root用户的密码为123123
好了,主机60的ssh信息已经配置完毕,我们再来尝试一下,看看之前的命令能不能正常执行,如下

未分类

可以看到,上述命令已经正常执行了,ansible主机成功的ping通了10.1.1.60,从此以后,我们就可以通过ansible主机,管理10.1.1.60这台主机了。

其实,为了更加方便的使用,ansible还支持对主机添加别名,当主机存在别名时,我们可以通过主机的”别名”管理对应主机。
比如,我们想要将10.1.1.60这台主机的别名命名为test60,那么,我们在配置清单时,可以进行如下配置

未分类

如上图所示,当为主机配置别名时,主机的IP地址必须使用anible_host关键字进行指明,否则ansible将无法正确的识别对应的主机。
主机的别名配置完成后,则可以使用主机的别名管理对应主机,示例如下。

未分类

不过,如果你只使用了上述方式配置了主机,则无法通过主机的IP进行管理了,除非你同时使用了别名的方式与IP的方式配置两个主机条目。

注意:上述配置参数都是ansible2.0版本以后的写法,2.0版本之前,应遵从如下写法

ansible_port应该写成ansible_ssh_port
ansible_user应该写成ansible_ssh_user
ansible_host应该写成ansible_ssh_host

因为当前演示环境的ansible版本为2.4,所以,我们使用新的写法进行演示,2.4版本同时也兼容之前的语法。

上述参数,其实都是为了创建ssh连接所使用的,而说到ssh,我们都知道,创建ssh连接时,可以基于密码进行认证,也可以基于密钥进行认证,而在生产环境中,为了提高安全性,我们通常会基于密钥进行ssh认证,甚至会禁用密码认证,那么,当ansible主机需要与受管主机建立ssh连接时,能够基于密钥进行认证码?必须能的。
其实,在实际的使用环境中,我们通常会在”配置管理机(ansible主机)”中生成密钥,然后通过公钥认证的方式连接到对应的受管主机中,如果你对基于密钥认证的方式还不是特别了解,则可以参考如下文章,此处不再对相应配置进行详细的描述:

http://www.zsythink.net/archives/2375

那么,我们就在ansible主机中生成密钥,并进行相应的配置吧。
首先,生成默认格式的密钥对,私钥与公钥。
Shell

# ssh-keygen

然后将生成的公钥加入到10.1.1.60的认证列表

# ssh-copy-id -i /root/.ssh/id_rsa.pub [email protected]

好了,公钥认证的相关操作配置完成,此刻,我们已经可以通过ansible主机免密码连接到主机60中了。

因为配置了密钥认证,所以可以实现免密码创建ssh连接,既然已经能够免密码创建ssh连接,那么在配置”主机清单”时,就没有必要再提供对应主机的用户名与密码了,所以,在完成了密钥认证的相关配置后,我们可以将清单中的配置精简为如下格式。

未分类

或者使用别名的格式

未分类

当然,如果你的受管服务器中的sshd服务使用了默认的22号端口,上述配置中的ansible_port也是可以省略的,为了方便演示,演示环境中的所有受管主机均使用默认的sshd端口号。

如果你的ansible主机上同时存在多对密钥,有可能需要通过不同的密钥连接不同的受管主机,这个时候,你可以通过ssh-agent帮助我们管理密钥,如果你还不了解ssh-agent,那么可以参考如下文章:

http://www.zsythink.net/archives/2407

如果你不想使用ssh-agent管理密钥,也可以通过ansible_ssh_private_key_file参数,指定连接对应主机时所使用的私钥,由于演示环境中并没有同时使用多对密钥,所以此处不再赘述。

在今后的演示中,默认使用密钥认证的方式连接到对应主机,我会提前配置好各个受管主机的密钥认证,后文中将不再对密钥认证的配置过程进行描述。
好了,说了这么多,我想你应该已经了解了ansible的基本概念,以及ansible的一些最基本的配置,在之后的文章中,我们会徐徐渐进,慢慢的介绍ansible的。

GitLab 接入 OpenLDAP和AD域

今天调试GitLab接入AD域控,找了很多资料才解决了问题,在此处做记录。

GitLab有自己的用户机制,但其也兼容ldap的用户验证机制(本质上OpenLDAP和AD域控都是基于LDAP机制),由于公司内部的统一账户机制正在调整,故需要将GitLab进行配置,试验下来社区版的GitLab经过修改配置文件后可以使用OpenLDAP和微软AD域控作为其用户机制。

版本:gitlab 社区版 9.3.4(汉化)

gitlab目录:/etc/gitlab

gitlab配置文件:/etc/gitlab/gitlab.rb(社区版初始该文件内容为空)

一、接入openldap

1.1 修改配置文件gitlab.rb

在gitlab.rb文件中添加下面配置,其中:

  1. host,是搭建的openldap的ip,根据实际情况修改;

  2. uid,可以配置cn,也可以配置uid,都能识别(具体为什么没深入研究);

  3. bind_dn,openldap的管理员账号,根据实际情况修改;

  4. password,openldap的管理员,根据实际情况修改;

  5. active_directory,似乎是针对连接是否是ad域控的标示,因为这部分是openldap的配置,故为false;

  6. allow_username_or_email_login,用户登录是否用户名和邮箱都可以,方便用户故配置true;

  7. base,用户列表所在的目录,因为新增的用户都在openldap的People下,故这么配置,根据实际情况修改;

# 指明服务的地址
external_url = 'http://localhost'

# 开启ldap
gitlab_rails['ldap_enabled'] = true
gitlab_rails['ldap_servers'] = YAML.load <<-'EOS' ###! **remember to close this block with 'EOS' below**
main: # 'main' is the GitLab 'provider ID' of this LDAP server
    label: 'LDAP'
    host: '192.168.6.4'
    port: 389
    uid: 'uid'
    method: 'plain' # "tls" or "ssl" or "plain"
    bind_dn: 'cn=Manager,dc=abc,dc=cn'
    password: '123456' 
    active_directory: false
    allow_username_or_email_login: true
    block_auto_created_users: false
    base: 'ou=People,dc=abc,dc=cn'
    user_filter: ''
EOS

1.2 重新加载新配置

# gitlab-ctl reconfigure

运行后最后一行为“gitlab Reconfigured!”才说明是加载新配置成功,否则就是根据报错信息找错误。

1.3 查看是否能正常获取用户列表

# gitlab-rake gitlab:ldap:check

正常连接的情况下执行之后能看到用户的列表

1.4 重启gitlab

# gitlab-ctl restart

未分类

重启成功后,gitlab的界面就会变成下面这样。

未分类

openldap的用户通过“LDAP”页进入,管理员root通过“标准”页进入。

二、接入AD

2.1 修改配置文件gitlab.rb

在gitlab.rb文件中添加下面配置,其中:

  1. host,是搭建的微软域服务器的ip,根据实际情况修改;

  2. uid,必须配sAMAccountName才能识别;

  3. bind_dn,域服务器的管理员账号,根据实际情况修改;

  4. password,域服务器的管理员,根据实际情况修改;

  5. active_directory,似乎是针对连接是否是ad域控的标示,因为这部分是域服务器的配置,故为true;

  6. allow_username_or_email_login,用户登录是否用户名和邮箱都可以,方便用户故配置true;

  7. base,用户列表所在的目录,因为新增的用户都在People下(试验了用户放在User时gitlab检测不到用户,故新建了一个目录People,并将新建的用户移入了该目录下),故这么配置,根据实际情况修改;

external_url = 'http://localhost'

gitlab_rails['ldap_enabled'] = true
gitlab_rails['ldap_servers'] = YAML.load <<-'EOS' ###! **remember to close this block with 'EOS' below**
main: # 'main' is the GitLab 'provider ID' of this LDAP server
  label: 'LDAP'
  host: '192.168.6.1'
  port: 389
  uid: 'sAMAccountName'
  method: 'plain' # "tls" or "ssl" or "plain"
  bind_dn: 'cn=Administrator,cn=users,dc=myad,dc=cn'
  password: '123456'
  active_directory: true
  allow_username_or_email_login: true
  block_auto_created_users: false
  base: 'ou=People,dc=myad,dc=cn'
  user_filter: ''
EOS 

2.2 重新加载新配置

# gitlab-ctl reconfigure

运行后最后一行为“gitlab Reconfigured!”才说明是加载新配置成功,否则就是根据报错信息找错误。

2.3 查看是否能正常获取用户列表

# gitlab-rake gitlab:ldap:check

正常连接的情况下执行之后能看到用户的列表

2.4 重启gitlab

# gitlab-ctl restart

未分类

重启成功后,gitlab的界面就会变成下面这样。

未分类

openldap的用户通过“LDAP”页进入,管理员root通过“标准”页进入。

2.5 确认该用户不处于特殊状态

  1. 不处于“禁用账户”的状态;

  2. 不处于“需要首次登录修改密码”的状态;

(处于上面任意一种状态的账户在登录gitlab时都会报“Cloud not authenticate you from Ldapmain because “Invalid creadentials”.”)

搭建 gitlab 服务器

apt-get install gitlab-ce 失败

问题 log: gem devise-two-factor 的依赖关系得不到满足。

Setting up ruby-paranoia (2.1.3-1) ...
Setting up gitlab (8.5.8+dfsg-5) ...
Creating/updating gitlab user account...
adduser: Warning: The home directory `/var/lib/gitlab' does not belong to the user you are currently creating.
Creating runtime directories for gitlab...
Updating file permissions...
Configuring hostname and email...
Registering /etc/gitlab/gitlab.yml via ucf

Creating config file /etc/gitlab/gitlab.yml with new version
Registering /etc/gitlab/gitlab-debian.conf via ucf

Creating config file /etc/gitlab/gitlab-debian.conf with new version

Creating config file /etc/nginx/sites-available/localhost with new version
Reloading nginx configuration...
Create database if not present
psql: FATAL:  database "gitlab_production" does not exist
psql: FATAL:  role "gitlab" does not exist
Create gitlab user with create database privillege...
CREATE ROLE
Make gitlab user owner of gitlab_production database...
ALTER DATABASE
Grant all privileges to gitlab user...
GRANT
Verifying we have all required libraries...
Could not find gem 'devise-two-factor (~> 2.0.0)' in any of the gem sources
listed in your Gemfile or available on this machine.
dpkg: error processing package gitlab (--configure):
 subprocess installed post-installation script returned error exit status 7
Setting up ruby-debug-inspector (0.0.2-1.1build3) ...
Setting up ruby-binding-of-caller (0.7.2+debian1-3) ...
Setting up ruby-bson (1.10.0-2) ...
Setting up ruby-bson-ext (1.10.0-2build5) ...
Setting up ruby-columnize (0.9.0-1) ...
Setting up ruby-byebug (5.0.0-1build3) ...
Setting up ruby2.3-dev:amd64 (2.3.1-2~16.04.2) ...
Setting up ruby-dev:amd64 (1:2.3.0+1) ...
Setting up ruby-ffi (1.9.10debian-1build2) ...
Setting up ruby-jbuilder (2.3.1-1) ...
Setting up ruby-libvirt (0.5.1-3build5) ...
Setting up ruby-rb-inotify (0.9.7-1) ...
Setting up ruby-listen (3.0.3-3) ...
Setting up ruby-msgpack (0.6.2-1build4) ...
Setting up ruby-rabl (0.11.4-2) ...
Setting up ruby-rabl-rails (0.4.1-1) ...
Setting up ruby-sdoc (0.4.1-1) ...
Setting up ruby-spring (1.3.6-2) ...
Setting up ruby-sqlite3 (1.3.11-2build1) ...
Setting up ruby-web-console (2.2.1-2) ...
Processing triggers for libc-bin (2.23-0ubuntu9) ...
Processing triggers for systemd (229-4ubuntu19) ...
Processing triggers for ureadahead (0.100.0-19) ...
Processing triggers for ufw (0.35-0ubuntu2) ...
Errors were encountered while processing:
 gitlab
E: Sub-process /usr/bin/dpkg returned an error code (1)
netadmin@kmc-b0232:~$

改用官方最新的 deb 包

详见 https://packages.gitlab.com/gitlab/gitlab-ce/install,

  • curl -s https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.deb.sh | sudo bash
  • sudo apt-get install gitlab-ce
netadmin@kmc-b0232:~$ curl -s https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.deb.sh | sudo bash
Detected operating system as Ubuntu/xenial.
Checking for curl...
Detected curl...
Running apt-get update... done.
Installing apt-transport-https... done.
Installing /etc/apt/sources.list.d/gitlab_gitlab-ce.list...done.
Importing packagecloud gpg key... done.
Running apt-get update... done.

The repository is setup! You can now install packages.
netadmin@kmc-b0232:~$

配置

目前还未配置邮件发送。

nginx

gitlab 默认使用 nginx。安装完后 nginx sites-enabled 有两个,我把 default 直接删了。
然后把另一个的 server_name 改成自己的 ip。修改完记得 sudo service nginx restart。

netadmin@kmc-b0232:/etc/nginx/sites-available$ ls ../sites-enabled/ -al
total 8
drwxr-xr-x 2 root root 4096 12月 13 19:42 .
drwxr-xr-x 6 root root 4096 12月 13 19:40 ..
lrwxrwxrwx 1 root root   36 12月 13 14:23 gitlab -> /etc/nginx/sites-available/localhost
netadmin@kmc-b0232:/etc/nginx/sites-available$ cat gitlab
## GitLab
##
## Lines starting with two hashes (##) are comments with information.
## Lines starting with one hash (#) are configuration parameters that can be uncommented.
##
##################################
##        CONTRIBUTING          ##
##################################
##
## If you change this file in a Merge Request, please also create
## a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests
##
###################################
##         configuration         ##
###################################
##
## See installation.md#using-https for additional HTTPS configuration details.

upstream gitlab-workhorse {
  server unix:/run/gitlab/gitlab-workhorse.socket fail_timeout=0;
}

## Normal HTTP host
server {
  ## Either remove "default_server" from the listen line below,
  ## or delete the /etc/nginx/sites-enabled/default file. This will cause gitlab
  ## to be served if you visit any address that your server responds to, eg.
  ## the ip address of the server (http://x.x.x.x/)n 0.0.0.0:80 default_server;
  listen 0.0.0.0:80;
  listen [::]:80;
  server_name your_url_or_your_ip; ## Replace this with something like gitlab.example.com
  server_tokens off; ## Don't show the nginx version number, a security best practice
  root /usr/share/gitlab/public;

  ## See app/controllers/application_controller.rb for headers set

  ## Individual nginx logs for this GitLab vhost
  access_log  /var/log/nginx/gitlab_access.log;
  error_log   /var/log/nginx/gitlab_error.log;

  location / {
    client_max_body_size 0;
    gzip off;

    ## https://github.com/gitlabhq/gitlabhq/issues/694
    ## Some requests take more than 30 seconds.
    proxy_read_timeout      300;
    proxy_connect_timeout   300;
    proxy_redirect          off;

    proxy_http_version 1.1;

    proxy_set_header    Host                $http_host;
    proxy_set_header    X-Real-IP           $remote_addr;
    proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
    proxy_set_header    X-Forwarded-Proto   $scheme;

    proxy_pass http://gitlab-workhorse;
  }
}
netadmin@kmc-b0232:/etc/nginx/sites-available$

gitlab 配置

在 /etc/gitlab 目录下,将以下两个文件各一处配置改成你的本机 IP。(我目前只想在局域网内访问 gitlab 服务器)

  • gitlab.rb:external_url ‘http://your_url_or_your_ip‘
  • gitlab.yml: host: your_ip

修改完后 sudo gitlab-ctl restart (还不清楚与 sudo gitlab-ctl reconfigure 的差异),即可通过 IP 访问到本地 gitlab 服务器。

Logstash采集网站的访问日志

最近又重新接触了一下elastisearch、logstash、kibana,蛮好用的一个日志框架。

同时好久没有更新网站内容、也没怎么关注,虽然有cnzz(umeng)的日志统计功能,但是毕竟是很小一段时间的。要是能够把日志都导出来,就可以用ELK来分析一下自己网站一年来文章的访问情况。

嗯,前阵子买了阿里云的一个VPN服务器,正好可以利用利用。把访问的日志情况通过http发送给logstash,然后存储下来,等过一段时间我们再回来分析分析这些日志。^^

启动Logstash收集服务

  • https://www.elastic.co/blog/introducing-logstash-input-http-plugin
  • https://discuss.elastic.co/t/post-data-to-logstash-using-http-input/69166/8
  • https://www.elastic.co/guide/en/logstash/current/plugins-filters-json.html
~/logstash-6.1.2/bin/logstash -e '
input { 
  http { 
    port => 20000 
    response_headers => {
      "Access-Control-Allow-Origin" => "*"
      "Content-Type" => "application/json"
      "Access-Control-Allow-Headers" => "Origin, X-Requested-With, Content-Type, Accept"
    }
  } 
} 
filter {
  if [message] =~ /^s*$/ {
    drop { }
  }

  json {
    source => "message"
  }
  json {
    source => "location"
    target => "location"
  }
  mutate {
    remove_field => [ "headers" ]
  }
}
output { 
  file { 
    path => "winse-accesslog-%{+YYYY-MM-dd}.log"
    codec => json_lines 
  } 
} 
'

页面发送访问日志记录

$.ajax({
  type: "POST",
  url: "http://SERVER:PORT",
  data: JSON.stringify({
    title: document.title,
    location: JSON.stringify(location),
    referrer: document.referrer,
    userAgent: navigator.userAgent
  }),
  contentType: "application/json; charset=utf-8",
  dataType: "json"
});

–END

redis主备同步配置方法

1. 配置主备

假设主机ip:10.136.16.146 port:6789
备机ip:10.136.30.144

我们有两种方式为其配置备机

方法1:修改备机配置文件

redis.conf中增加

daemonize yes
slaveof 10.136.16.146 6789
# 如果主机有密码,则修改下面一行即可
# masterauth <master-password>

在备机上启动redis

redis-server ./tmp/redis.conf

连接上备机reids,执行info replication, 可以看到下面的结果

127.0.0.1:9303> info replication
# Replication
role:slave
master_host:10.136.16.146
master_port:6789
master_link_status:up
master_last_io_seconds_ago:0
master_sync_in_progress:0

...

此时,我们的主备就已配好。数据会自动同步(包括主机之前的数据),如果之前主机中有数据,此时已可以在备机中读取。

连接主机,执行info replication,可以看到下面结果

10.136.16.146:6789> info replication
# Replication
role:master
connected_slaves:1
slave1:ip=10.136.30.144,port=9303,state=online,offset=40383758059,lag=0
master_repl_offset:40383983932

...

可以看到,主机上已连接一台备份redis,正是我们刚刚启动的那台。

方法2:动态修改备机配置

当然,有时你可能没有权限修改备机redis配置文件或重启redis-server。这时,如果你想配置主备,只需要连上备机redis,执行:

slaveof 10.136.16.146 6789

#主机有密码,还要执行这条
#config set masterauth <password>

同样可以达到配置主备的目的

2. 取消主备

有两种方法取消息主备

  1. 直接在配置文件中去掉slaveof的配置,然后重启redis-server
  2. 连入备机,执行
slaveof no one
  • 1

取消主备后,在备机上执行info replication, 会看到

127.0.0.1:9303> info replication
# Replication
role:master
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

当前机器的角色已不再是slave, 而是master

3. 几个可能用到的参数

  • slave-read-only:两个值,yes/no,用于控制slave是否为只读
  • slave-serve-stale-data: 用于控制当slave和master失去连接或正在进行同步时,slave的行为。
    • yes(默认):仍然正常处理客户端请求,但数据可能是旧的
    • no:向客户端回复SYNC with master in progress

4. 建议

如果配置了主备,建议master开启数据持久化,或者至少不要让master在挂掉后可以自动重启。

可以想象这样一个场景:master未开启持久化,master挂了后被监控重启。这时,master中没有任何数据,slave由于和master同步,上面的数据也将被清空!

Redis数据的备份和还原

未分类

redis属于内存数据库,速度很快,对于有键值对数据存储需求的是非常便利的。

redis数据库的备份和还原也非常的方便。

1、redis数据库备份(导出)

首先进入redis数据库的控制台。

redis-cli

然后输入备份命令

save

未分类

查看一下导出的文件(dumo.rdb)

未分类

2、redis数据库还原(导入)

首先进入redis数据库的控制台。

redis-cli

然后输入如下命令,获取redis的安装路径

CONFIG GET dir

然后在bin的同级目录下找到var,把备份的数据文件(dump.rdb)复制到var目录下,然后重启redis服务即可。

redis未授权访问漏洞

0x00 redis

Redis 是一个高性能的key-value数据库。它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set –有序集合)和hash(哈希类型)。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。
在此基础上,redis支持各种不同方式的排序。为了保证效率,数据都是缓存在内存中。redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave同步。

0x01 redis未授权访问

Redis因配置不当可以导致未授权访问,被攻击者恶意利用。当前流行的针对Redis未授权访问的一种新型攻击方式,在特定条件下,如果Redis以root身份运行,黑客可以给root账户写入SSH公钥文件,直接通过SSH登录受害服务器,可导致服务器权限被获取和数据删除、泄露或加密勒索事件发生,严重危害业务正常服务。

0x02 redis未授权访问漏洞产生的原因

Redis 默认情况下,会绑定在 0.0.0.0:6379,这样将会将 Redis 服务暴露到公网上,如果在没有开启认证的情况下,可以导致任意用户在可以访问目标服务器的情况下未授权访问 Redis 以及读取 Redis 的数据。攻击者在未授权访问 Redis 的情况下可以利用 Redis 的相关方法,可以成功在 Redis 服务器上写入公钥,进而可以使用对应私钥直接登录目标服务器。

0x03 漏洞重现和利用

首先在靶机安装redis并且不设置密码

sudo yum install redis* -y

在靶机安装ssh服务

sudo yum install openssh

在靶机配置redis配置文件,注释掉ip限制,并且设置安全设置为no,使其可以在公网被访问

vi /etc/redis.conf

未分类

未分类

重启redis服务

未分类

测试是否在公网开启

未分类

在靶机打开ssh服务

未分类

在主机本地生成ssh公钥

未分类

连接靶机redis服务,并利用redis未授权访问漏洞在目标服务器/root/.ssh文件夹的authotrized_keys写入刚才创建的ssh公钥

未分类

利用本机的ssh私钥登陆靶机服务器

未分类

0x04 使用openvas对该存在漏洞的系统进行扫描

通过openvas扫描靶机系统,可以得到以下结果:

未分类

未分类

0x05 危害

  • 数据库数据泄露
    Redis 作为数据库,保存着各种各样的数据,如果存在未授权访问的情况,将会导致数据的泄露,其中包含保存的用户信息等。

  • 代码执行
    Redis可以嵌套Lua脚本的特性将会导致代码执行, 危害同其他服务器端的代码执行
    一旦攻击者能够在服务器端执行任意代码, 攻击方式将会变得多且复杂, 这是非常危险的。
    通过Lua代码攻击者可以调用 redis.sha1hex() 函数,恶意利用 Redis 服务器进行 SHA-1 的破解。

  • 敏感信息泄露
    通过 Redis 的 INFO 命令, 可以查看服务器相关的参数和敏感信息, 为攻击者的后续渗透做铺垫。

0x06 应对方案

Redis未授权访问漏洞是一个出现广泛并且危害巨大的漏洞,会直接导致整个服务器沦陷,要预防则需要做到:

(1)禁止使用 root 权限启动 redis 服务;
(2)对 redis 访问启用密码认证,尤其不能使用空密码;
(3)添加 IP 访问限制;
(4)尽可能不对公网直接开放 SSH 服务。

Redis数据备份

Redis数据备份

实例

127.0.0.1:6379> bgsave
OK

这里为什么用bgsave而不使用save,请参考文章:

http://www.chenxm.cc/post/526.html

Redis恢复数据

1、 获取redis备份目录

127.0.0.1:6379> CONFIG GET dir
1) "dir"
2) "D:\software\Redis"
127.0.0.1:6379>

以上命令 CONFIG GET dir 输出的 redis 备份目录为 /usr/local/redis/bin。

2、 停止redis服务

src/redis-cli -p 6379 shutdown

src是redis安装目录

3、拷贝redis备份文件(dump.rdb)到 /usr/local/redis/bin目录下

4、重新启动redis服务

linux

src/redis-server redis.conf

windows

src/redis-server redis.windows.conf

实际上只要redis重启,会自动读取备份目录下dump.rdb文件,如果有该文件,会自动恢复数据。

5、查看是否redis恢复数据

$ src/redis-cli
127.0.0.1:6379> keys *
 1) "k1"
 2) "k2"
 3) "k3"
 4) "k4"
 5) "k5"