shell脚本进阶1——精读ansible+shell脚本

文章目录

  • 一、脚本规划思路
  • 二、主控机shell脚本
    • 2.1 脚本输出字体特效
    • 2.2 生成菜单栏对话框
    • 2.3 配置本地yum源仓库
    • 2.4 配置受控机yum源
    • 2.5 关闭防火墙和selinux
    • 2.6 把docker安装包给受控机
    • 2.7 安装docker-compose
    • 2.8 安装docker
    • 2.9 安装ansible
    • 2.10 安装pip
    • 2.11 主控本机免密登录
    • 2.12 安装运维工具包
    • 2.13 配置pip的docker依赖
    • 2.14 加载环境变量
    • 2.15 定义主函数
  • 三、受控机ansible脚本
    • 3.1 定义菜单栏
    • 3.2 定义子函数
    • 3.3 定义主函数
    • 3.4 playbook
      • 3.4.1 shell执行playbook
      • 3.4.2 编写playbook
      • 3.4.3 编写角色
      • 3.4.4 定义静态清单
      • 3.4.5 修改配置文件
      • 3.4.6 查看收集事实信息
    • 3.6 如何自测功能

一、脚本规划思路

  • 运维实施部署平台时,需要先对系统进行初始化,安装所有基础服务环境及依赖包,关闭防火墙和selinux等操作之后,才能开始正式部署第三方插件和微服务。
  • 那系统初始化具体是干什么的呢?这就包括设置防火墙、服务器免密、配置基础环境和yum源、时间同步等等。
  • 当服务器太多时,我们不可能一台台登录上去操作,这极为耗时,所以可以结合ansible来写脚本,选一台主控机即可完成所有操作。

脚本思路:

  • 第一步:确定主控机,其他机器作为受控端,把我们后面要写的ansible脚本放在主控机上进行操作。
  • 第二步:预想用户在使用脚本时的大概要操作哪些步骤。
    • 防火墙、免密登录、配置yum源等等,这么多步骤,我们是不是应该要分条目来进行?先干什么,再干什么,理个顺序出来。
    • 是不是得搞个对话框出来与用户交互,让用户输入主控机信息?若在脚本里把主控机信息写死,那换个部署环境就不能玩了,或者很麻烦需要修改主控机信息,这就只能算半自动化了,没达到目的。
  • 第三步:规划脚本整体框架。当执行脚本时,第一步应该是主控机先对自己做系统初始化,然后再对受控机做,等所有机器的基础环境都具备后,才能再做其他设置。
    1. 确定主控机系统初始化脚本。主控机初始化和受控机操作得分开,我们规划只在主控机上存放脚本,提供基础环境的服务包和源,尽量把第三方服务和微服务安装在受控机,做到层次分明。
    2. 那受控机的脚本要怎么做呢?可以结合ansbile来玩,让主控机对其操作,这就需要定义清单文件、默认变量文件、静态文件、jinja2模板文件、角色任务、playbook剧本,还可以定义需要修改的变量、handlers处理程序任务,最后调试时再根据业务实际情况修改ansible配置文件参数,比如多进程并发数、ssh第一次登录访问会检查key的设置参数,理清需要使用的模块等等。这都需要一点点去调试修改,无误后才能上线正式环境。
  • 第四步:细化脚本执行步骤。上面我们只是理了下脚本框架,那脚本具体要做什么步骤需要规划清楚。
    1. 脚本输出的字体需要考虑,美化用户使用观感,比如tput、printf、echo命令。
    2. 确定对话框效果,比如whiptail、dialog工具。
    3. 定义函数。可以将用户在交互的对话框页面选择的所有操作类目分别定义成一个个子函数,最后再搞个总函数对其引用,比如主控机系统初始化定义成第1个子函数,对受控机的操作定义成第2个子函数…,当用户选择操作类目时,主函数根据用户的选择类目执行对应子函数。那怎么才能让主函数精确的执行对应的子函数呢?可以使用If语句对用户选择的类目结果进行判断,也可以使用case语句根据选择类目结果直接匹配。
    4. 编写playbook剧本。playbook中的内容需尽量简化,什么意思?正常的一个playbook中可以定义任务名称、主机、变量、tasks任务。若playbook简短还行,像系统初始化这种需要操作很多东西的情况下,是需要用到很多个模块,搞多个任务的,一趟下来palybook是很冗长的,要避免这种情况,尽量简洁。
      • 可以把变量和tasks任务全部放在角色里,根据标签对角色进行操作。
      • 定义清单文件,填写受控主机信息。为什么只填受控机的呢?因为主控机是单独设计的脚本操作。
      • 编写角色任务,根据用户选择的菜单条目来写,一个角色对应一个条目。
      • 角色任务中会涉及到安装包、yum源、二进制文件等等,这种就可以当作静态文件。
    5. 确定受控机的yum源。再给受控机安装服务时,受控机的源要怎么配置?可以在主控机上安装一个nginx服务,把提前下载好的离线安装包塞进一个统一目录下,利用nginx将这个目录映射到服务器外部,以web页面的方式让受控机去挂载配置本地yum源。
  • 第五步:理清子函数条目。我们前面说,可以根据用户的选择条目来定义子函数。那用户在对话框菜单中能选择哪些条目?我们可以规划一下:
    1. 首先得有个菜单吧,可以把生成菜单的命令段定义成第1个子函数。
    2. 设置防火墙类目,将其命令段定义成第2个子函数。
    3. 服务器免密操作类目,定义成第3个子函数。
    4. 安装服务器依赖类目,定义成第4个子函数。
    5. 设置时间同步类目,定义成第5个子函数。
    6. 设置harbor仓库域名解析类目,定义成第6个子函数。
  • 第六步: 理清主控机初始化脚本步骤。
    1. 配置本地yum源仓库。
    2. 关闭防火墙以及SELINUX。刚开时部署服务时需要关闭,等最后调试完后在设置防火墙规则。
    3. 安装docker、docker-compose服务、ansible、python-pip,将docker设置为系统服务。
    4. 配置本机提供外部使用的yum仓库,供受控机使用为本地源。
    5. 主控机本机免密登录。
    6. 安装常用运维工具包。
    7. 配置pip的docker依赖。
    8. 配置环境变量。
    9. 最后就是优化脚本,包括脚本输出的显示内容、避免脚本是否会输出打印敏感信息。
  • 第七步:以上就是我们要写的ansible脚本整体规划,整个框架写完后,再一点点改进。

二、主控机shell脚本

  • 先把主控机系统初始化脚本写出来,再写对受控机的操作脚本。

2.1 脚本输出字体特效

1.定义字体输出特效,提高用户使用观感。每种颜色特效都定义成一个子函数,当我想知道用户选择的条目执行结果时,就可以再这个条目的命令段最后引用颜色函数。

set +e
set -o noglob
bold=$(tput bold)
underline=$(tput sgr 0 1)
reset=$(tput sgr0)
red=$(tput setaf 1)
green=$(tput setaf 2)
yellow=$(tput setaf 3)
blue=$(tput setaf 4)
magenta=$(tput setaf 5)
cyan=$(tput setaf 6)
white=$(tput setaf 7)
underline() {
    printf "${underline}${bold}%s${reset}\n" "$@"
}
h1() {
    printf "\n${underline}${bold}${cyan}%s${reset}\n" "$@"
}
h2() {
    printf "\n${underline}${bold}${white}%s${reset}\n" "$@"
}
debug() {
    printf "${white}%s${reset}\n" "$@"
}
info() {
    printf "${white}-> %s${reset}\n" "$@"
}
success() {
    printf "${bold}${green}✔ %s${reset}\n" "$@"
}
error() {
    printf "${red}✖ %s${reset}\n" "$@"
}
warn() {
    printf "${yellow}%s${reset}\n" "$@"
}
bold() {
    printf "${bold}%s${reset}\n" "$@"
}
note() {
    printf "\n${underline}${bold}${blue}Note:${reset} ${blue}%s${reset}\n" "$@"
}

2.定义数组,写要安装的运维工具包。

set -e
set +o noglob
item=1
base_dir=$(readlink -f "$(dirname "$0")")    ##定位出符号链接所指向的位置。
list=(
    bash-completion
    bind-utils
    chrony
    cifs-utils
    cockpit
    cockpit-dashboard
    createrepo
    dstat
    expect
    ftp
    haveged
    iotop
    lrzsz
    lsof
    net-tools
    nfs-utils
    open
    rsync
    screen
    sysstat
    tcpdump
    telnet
    tree
    unzip
    vim
    wget
    zip
    traceroute
    jq
)

2.2 生成菜单栏对话框

基本了解:

  • 当执行脚本后,得设计成与用户交互的功能,提供一个菜单供用户输入主控机信息,可以使用whiptail来实现。
  • 让用户输入ip和输入密码的两段命令段逻辑相同,下面拿输入ip命令段做解析。

代码解读:

  1. 新手先从while循环内容看,里面的核心是两个whiptail语句和一个if判断语句,其他内容都是定义变量被whiptail语句引用,最后再输出了第2个whiptail语句结果,是主控机Ip,供后面使用。
  2. while循环是个死循环,当用户用户输入信息错误时应该再重新输出,而不是直接退出。
  3. 第一个whiptail语句是个表单输入对话框,让用户输入主控机ip,若返回结果不等于0则退出,若输出的内容为exit则退出。
  4. 第二个whiptail语句也是表单输入对话框,是让用户再次输入主控机ip,用于信息核对确认。
  5. 往下有个if判断,对第一个whiptail和第2个whiptail输出结果进行判断,若输入值为空,或两次输入内容不等都会退出脚本
  6. continue语句遇错跳过本次循环,执行后面的;break语句遇错直接退出本次循环,不再执行后面的。
  7. 最后再把第2次输入的结果定义成变量。

1.第一步,输入主控机ip。

local_address() {
    local address1=""
    local address2=""
    title_box="获取本机IP地址"
    while true; do
        reg_box="请输入本机本机IP地址:"
        address1=$(whiptail --title "${title_box}" --inputbox "${reg_box}" 8 78 3>&1 1>&2 2>&3)
        [ $? != 0 ] && exit
        [ "${address1,,}" == "exit" ] && exit
        reg2_box="请再次输入本机本机IP地址:"
        address2=$(whiptail --title "${title_box}" --inputbox "${reg2_box}" 8 78 3>&1 1>&2 2>&3)
        [ $? != 0 ] && exit
        [ "${address2,,}" == "exit" ] && exit
        if [ -z "${address1}" ] || [ -z "${address2}" ] || [ "${address1}" != "${address2}" ]; then
            msg_box="两次输入不一致,请重新设置"
            whiptail --title "${title_box}" --msgbox "${msg_box}" 8 78
            [ $? != 0 ] && exit
            [ "${address,,}" == "exit" ] && exit
            continue
        fi
        break
    done
    address=${address2}
}

2.输入主控机密码。

local_password() {
    local passwd1=""
    local passwd2=""
    title_box="获取本机ROOT用户密码"
    while true; do
        reg_box="请输入本机ROOT用户密码:"
        passwd1=$(whiptail --title "${title_box}" --passwordbox "${reg_box}" 8 78 3>&1 1>&2 2>&3)
        [ $? != 0 ] && exit
        [ "${passwd1,,}" == "exit" ] && exit
        reg2_box="请再次输入本机ROOT用户密码:"
        passwd2=$(whiptail --title "${title_box}" --passwordbox "${reg2_box}" 8 78 3>&1 1>&2 2>&3)
        [ $? != 0 ] && exit
        [ "${passwd2,,}" == "exit" ] && exit
        if [ -z "${passwd1}" ] || [ -z "${passwd2}" ] || [ "${passwd1}" != "${passwd2}" ]; then
            msg_box="两次输入不一致,请重新设置"
            whiptail --title "${title_box}" --msgbox "${msg_box}" 8 78
            [ $? != 0 ] && exit
            [ "${address,,}" == "exit" ] && exit
            continue
        fi
        break
    done
    passwd=${passwd2}
}

2.3 配置本地yum源仓库

基本了解:

  • 本地yum源仓库的服务包需要运维提前下载打包好,塞进同一目录,最后直接挂载这个目录即可。

脚本解读:

  1. 引用h1函数,先打印一行内容“step 1 配置本地YUM源仓库”,作为提示,属于优化性质。
  2. 让tiem+1,当面后面引用item变量时自动+1,让用户知道脚本当前执行进度。
  3. if判断yum源备份目录是否存在,若不存在则创建备份目录。随后把系统自带的yum源移入到备份目录。
  4. 自定义本地yum源,挂载到/opt/tools目录,该目录下存放的是离线包和源,需要运维提前下载准备好。
  5. 引用success函数,打印一行内容“YUM源配置完毕”,作为提示,属于优化性质。
configure_local_yum() {
    h1 "[ step ${item} ]配置本地YUM源仓库"
    let item+=1
    if [ ! -d /etc/yum.repos.d/back ];then
        mkdir -p /etc/yum.repos.d/back
    fi
    mv /etc/yum.repos.d/*.repo  /etc/yum.repos.d/back
    cat >/etc/yum.repos.d/local.repo << EOF
[local]
name=server
baseurl=file:///opt/tools
enabled=1
gpgcheck=0
EOF
    yum repolist
    success "YUM源配置完毕"
}

2.4 配置受控机yum源

代码解读:

  1. 引用h1函数,打印一行内容作为提示。
  2. 进入images目录读取nginx离线镜像包。
  3. if判断是否存在/opt/nginx_yum目录,若不存在则创建该目录。目的是这个nginx是作为提供yum源的,单独创建一个目录存放它的相关文件。
  4. 随后第2个if判断这个目录下是否存在nginx配置文件,若没有则将config目录下的配置文件拷贝一份。
  5. 构建compose.yml文件,定义容器名称、使用镜像、重启规则、映射端口、将本地/opt/tools目录挂载到nginx的网页前端文件(最后访问nginx网页就显示/opt/tools目录下的内容)、将本地配置文件挂载到容器内。
  6. 随后又加了个if判断,若docker ps能看到nginx容器,则调用success函数;否则使用构建的yml文件启动nginx。

1.在主控机上安装一个nginx服务,将服务安装包目录暴露出来,供受控机挂载,成为受控机的yum仓库。

configure_yum_server() {
    h1 "[ step ${item} ] 配置本机提供外部使用的YUM仓库"
    let item+=1
    cd $base_dir/
    docker load -i ./images/nginx.tar.gz 
    if [ ! -d  /opt/nginx_yum ];then
        mkdir -p /opt/nginx_yum
    fi

    if [ ! -f /opt/nginx_yum/default.conf ];then
        cp ./config/default.conf /opt/nginx_yum
    fi

    cat >/opt/nginx_yum/docker-compose.yml </dev/null 2>&1 ;then
        success "已经安装过远程YUM源"
    else
        cd /opt/nginx_yum && docker-compose up -d
        [ $? == 0 ] && success "安装完成" 
    fi
}

2.暴露yum源,让受控机去挂载。

代码解读:

  1. 将宿主机ip写入一个文件里。
  2. 定义一个本地源,挂载路径就是我们前面安装的nginx服务的web页面,里卖弄就是离线包。将该文件放入一个角色的静态文件目录里,这个角色任务是给服务器安装依赖包的,执行该角色时候会引用这个静态文件。
write_address() {
    echo "$address" > ./config/control
    cat >$base_dir/roles/system_init/files/remote.repo << EOF
[local]
name=server
baseurl=http://$address:62633
enabled=1
gpgcheck=0
EOF
}

2.5 关闭防火墙和selinux

代码解读:

  1. 引用h1函数打印一行内容,作为提示。
  2. 执行getenforce命令,根据返回结果进行if判断,若返回值不是“Disabled”,说明selinux没有关闭,此时需要关闭。三条命令中,先是临时关闭,再是永久关闭,注意永久关闭需要重启服务器才能生效,临时关闭重启服务器会失效。
  3. 随后关闭防火墙。
  4. 最后再引用info函数打印一行内容作为提示。
stop_firewalld() {
    h1 "[ step ${item} ]关闭防火墙以及SELINUX"
    let item+=1
    cmd=`getenforce`
    if [ $cmd  != 'Disabled' ] ; then
        setenforce 0
        sed -i 's#SELINUX=enforcing#SELINUX=disabled#g' /etc/sysconfig/selinux
        sed -i 's#SELINUX=enforcing#SELINUX=disabled#g' /etc/selinux/config
    fi
    systemctl stop firewalld.service
    systemctl disable firewalld.service
    info "防火墙以及SELINUX已经关闭"  
}

2.6 把docker安装包给受控机

基本了解:

  • 受控机系统初始化时,也需要使用dokcer服务包,将主控机的docker安装包复制一份放到roles中,以及补全命令的脚本。

代码解读:

  1. 判断一个角色静态文件目录下是否有docker安装包,若没有,则拷贝一份。
  2. 判断 一个角色静态文件目录下是否有docker二进制文件,若没有,则拷贝一份。
  3. 判断 一个角色静态文件目录下是否有docker-compose二进制文件,若没有,则拷贝一份。
Copy_docker_package() {
    if [ ! -f  $base_dir/roles/system_init/files/docker-19.03.9.tar.gz ]; then
        cp $base_dir/package/docker-19.03.9.tar.gz  $base_dir/roles/system_init/files
    fi
    if [ ! -f $base_dir/roles/system_init/files/docker-compose-bash ]; then
        cp $base_dir/package/docker-compose-bash $base_dir/roles/system_init/files
    fi
    if [ ! -f $base_dir/roles/system_init/files/docker-bash ]; then
        cp $base_dir/package/docker-bash $base_dir/roles/system_init/files
    fi
}

2.7 安装docker-compose

代码解读:

  1. 引用h1函数打印一行内容,作为提示。
  2. if判断docker-compose二进制文件是否存在,若不存在,则拷贝一份并添加执行权限;否则引用函数error打印一行内容。
  3. 又引用第2个if判断是否存在docker-compose的命令补全,若没有,则拷贝一个环境变量脚本,并设置开机启动。
  4. 最后引用success函数打印一行内容。
check_docker_compose() {
    h1 "[ step ${item} ]安装docker-compose"
    let item+=1
    if [ ! -e /usr/local/bin/docker-compose ];then
        cp ./package/docker-compose  /usr/local/bin
        chmod a+x /usr/local/bin/docker-compose
        if [ ! -f /etc/bash_completion.d/docker-compose ]; then
            cp $base_dir/package/docker-compose-bash /etc/profile.d/docker-compose.sh
            echo "sh /etc/profile.d/docker-compose.sh" >> /root/.bash_profile
        fi
    else
        error "docker-compose已经安装"
    fi
    success "docker-cimpose安装完毕!!!"
}

2.8 安装docker

代码解读:

  1. 引用h1函数打印一行内容,作为提示。
  2. if判断docker是否已安装,若是,则引用success函数;否则进入package目录解压dokcer离线安装包,并修改属主属组为root,再将整个docker目录下内容拷贝一份到/usr/bin目录。再用第2个if判断是否设置了命令行补全,若没有则设置。
  3. 设置系统服务。
  4. 启动docker并设置开机自启,若成功则引用success函数;否则引用error函数,并返回自定义状态退出码-1。
install_docker() {
    h1 "[ step ${item} ] 安装docker"
    let item+=1
    if  docker version >/dev/null 2>&1 ;then
        success "docker已经安装"
    else
        cd $base_dir/package/ \
        &&  tar  -xvf  docker-19.03.9.tar.gz \
        &&  chown  root:root  -R docker \
        &&  \cp  -a docker/* /usr/bin/ 
        if [ ! -f /usr/share/bash-completion/completions/docker ]; then
            cp $base_dir/package/docker-bash /etc/profile.d/docker.sh
            echo "sh /etc/profile.d/docker.sh" >> /root/.bash_profile
        fi
        cat >> /usr/lib/systemd/system/docker.service<<'EOF'
[Unit]
Description=Docker Application Container Engine
Documentation=https://docs.docker.com
After=network-online.target firewalld.service
Wants=network-online.target

[Service]
Type=notify
ExecStart=/usr/bin/dockerd
ExecReload=/bin/kill -s HUP $MAINPID
LimitNOFILE=infinity
LimitNPROC=infinity
TimeoutStartSec=0
Delegate=yes
KillMode=process
Restart=on-failure
StartLimitBurst=3
StartLimitInterval=60s

[Install]
WantedBy=multi-user.target
EOF

        if systemctl start docker; then
            sleep 5
            systemctl enable docker
            success "docker 启动成功 已经添加开机自启任务"
        else
            error "dokcer启动失败"
            exit -1
        fi
    fi
}

2.9 安装ansible

代码解读:

  1. 引用h1函数打印一行内容,作为提示。
  2. if判断是否有ansible的rpm包,若有则引用error函数;否则安装ansible,并根据返回状态码判断是否成功安装,若返回为0,则引用success函数。
install_ansible() {
    h1 "[ step ${item} ]安装ansible"
    let item+=1
    if rpm -qa |grep ansible ;then
        error "ansible 已经安装过"
    else
        yum install ansible -y
        [ $? == 0 ] && success "ansible 安装完成"
    fi
}

2.10 安装pip

代码解读:

  1. 引用h1函数打印一行内容,作为提示。
  2. if判断是否有pip的rpm包,若有则引用error函数;否则安装pip,并根据返回状态码判断是否成功安装,若返回为0,则引用success函数。
install_pip() {
    h1 "[ step ${item} ]安装python-pip"
    let item+=1
    if pip -V >/dev/null 2>&1;then
        error "pip 已经安装过"
    else
        yum install python-pip -y
        [ $? == 0 ] && success "python-pip 安装完成"
    fi
}

2.11 主控本机免密登录

代码解读:

  1. 生成主控本机密钥。
  2. 引用h1函数打印一行内容,作为提示。
  3. if判断是否存在公钥,不存在则生成密钥对,引用函数success函数;否则直接引用success打印结果。
create_ssh_keygen() {
    h1 "[ step ${item} ] 生成本机密钥"
    let item+=1
    if [ ! -f /root/.ssh/id_rsa.pub ];then
        ssh-keygen -t rsa -N "" -f ~/.ssh/id_rsa
        success "密钥生成完毕"
    else
        success "密钥已经生成过"
    fi
}

代码解读:

  1. 给主控机自己做免密登录。
  2. 引用h1函数打印一行内容,作为提示。
  3. if判断/root/.ssh/known_hosts文件中是否有本机第一次ssh连接本机时返回的公钥,若存在则引用info函数;否则使用expect命令做免密登录。
  4. expect命令是专门应对免交互情景的,如下就是限制超过时间为20s,执行命令是将本机的公钥复制到本机authorized_keys文件中,此时会出现交互页面。当期望匹配到(yes/no)时,则send发送yes并回车继续;当期望匹配到*password时,则send发送前面用户在交互页面输入的主控机密码并回车,最后期望结束。
  5. 执行完毕,则引用success函数。
secret_free() {
    h1 "[ step ${item} ] 配置本机免密"
    let item+=1
    if cat /root/.ssh/known_hosts |grep  $address >/dev/null 2>&1 ; then
        info "已经配置过免密"
    else
        /usr/bin/expect <

2.12 安装运维工具包

代码解读:

  1. 引用h1函数打印一行内容,作为提示。
  2. 安装前面定义的数组list中的所有元素。
  3. 安装步骤最后一条命令返回码为0,则引用success函数。
  4. 设置开启自动haveged服务,用于补充熵池。
install_softword() {
    h1 "[ step ${item} ] 安装其他常用软件"
    let item+=1
    yum install "${list[@]}" -y 
    [ $? == 0 ] && success "常用软件安装完成" 
    # 开机自启动haveged
    systemctl  enable   haveged
}

2.13 配置pip的docker依赖

代码解读:

  1. 引用h1函数打印一行内容,作为提示。
  2. 取出 backports依赖包的大版本数。
  3. if判断是否安装了pip的 docker依赖库,若安装,则引用success函数;否则判断backports依赖包的大版本数是否小于5,若小于,则根据package/pip/backports/setup.py文件构建并安装,之后再安装/package/pip/目录下的所有whl压缩文件,若返回状态码为0,则引用success函数。
docker_pip() {
    h1 "[ step ${item} ] 配置pip的docker依赖"
    let item+=1
    pip_version=`pip freeze |grep backports |awk -F "=" '{print $3}' |awk -F "." '{print $2}'`
    if pip list |grep docker ;then
        success "pip中的docker已经安装"
    else
        if [ $pip_version -lt 5 ];then
            python $base_dir/package/pip/backports/setup.py build
            python $base_dir/package/pip/backports/setup.py install
        fi  
        pip install $base_dir/package/pip/*.whl
        [ $? == 0 ] && success "pip的docker依赖库已经安装完成"
    fi   
}

2.14 加载环境变量

代码解读:

  1. 引用h1函数打印一行内容,作为提示。
  2. 加载环境变量文件,并引用success函数打印提示信息。
load_var() {
        h1 "[ step ${item} ] 加载环境变量"
        let item+=1
        source /root/.bash_profile
        success "加载环境变量完毕"

}

2.15 定义主函数

代码解读:

  1. if判断是否存在package/tools.tar.gz文件,若不存在说明拿的脚本有问题,直接退出返回状态码1;再判断/opt/tools目录文件是否不存在,若是,则解压工具包到/opt下。
  2. 依次执行所有子函数。
main() {
    if [ ! -e ./package/tools.tar.gz ];then
        error "所需要的安装包不存在: tools.tar.gz, 联系管理员"
        exit 1
    elif [ ! -e /opt/tools ];then
        tar -xf ./package/tools.tar.gz -C /opt
    fi
    local_address
    local_password
    write_address
    configure_local_yum
    stop_firewalld
    Copy_docker_package
    check_docker_compose
    install_docker
    install_ansible
    install_pip
    configure_yum_server
    create_ssh_keygen
    install_softword
    secret_free
    docker_pip
    load_var
}
main

三、受控机ansible脚本

脚本思路:

  • 第一步:构思ansible脚本整体框架。基本和前面的shell脚本大差不差,同样要使用printf+tput组合优化脚本的打印内容,同样要定义子函数和主函数,唯一加的东西就是playbook。
  • 第二步:理清shell+ansible执行顺序流程。以shell脚本作为入口,当用户执行shell脚本后应该也是弹出对话框与之交互,对话框里也同样要设置菜单类目,同样把每个类目的实现命令段定义成子函数,最后再定义主函数对其引用。
  • 第三步:理清子函数类目。我们前面说主控机系统初始化完后,在对受控机进行操作,所以就分开写了2个脚本。那在实际应用场景中真的需要用户选择性执行脚本吗?这样会不会太麻烦了?可以将前面写好的主控机系统初始化脚本引用到ansible脚本主函数里,搞个判断,若是用户选择的菜单类目是主控机设置,则引用主控机初始化脚本,整体下来较为简单。
    1. 菜单服务器免密类目定义成第1个子函数。
    2. 菜单安装服务器依赖包类目定义成第2个子函数。
    3. 菜单服务器时间同步类目定义成第3个子函数。
    4. 菜单服务器防火墙操作类目定义成第4个子函数。
    5. 菜单服务器harbor域名解析类目定义成第5个子函数。
    6. 以上各个类目功能的实现过程应该用ansible来完成,此时可以把每个ansible-playbook执行命令定义子函数。根据用户选择的菜单类目引用上面这5个对应的子函数,根据返回结果判断要执行哪个playbook的子函数。
  • 第四步:规划playbook结构,有如下内容必须要考虑:
    1. 清单文件,定义主机组,填写主机相关信息。
    2. playbook剧本,根据标签引用角色任务。
    3. role角色任务。得有最基础的tasks任务目录主文件,涉及到有静态文件的可以搞个file目录,若有动态jinja2文件可以搞个template目录。
    4. 静态文件目录,存放被引用较多的配置文件、离线包或二进制文件。
    5. 采集各个受控机上的事实信息存放目录。
  • 第五步:总结shell+playbook套层关系。
    1. 应先是用户使用shell脚本一键执行。脚本中从总函数开始走,根据用户选择的类目结果让if判断选择要执行哪个子函数,执行的子函数正是我们提前定义好的ansible-playbook命令,该命令指定了playbook剧本、角色标签,并根据选择的条目功能适当传入参数。
    2. 接着开始读取playbook,其内定义了任务名称、要在哪些主机上执行(hosts)、使用什么用户执行(remote_user),机器根据tags标签执行对应的role角色。
    3. 接在再读取roles目录下的角色,该目录下有多个目录,每个目录下都存放的是角色任务和任务执行时要用到的默认变量、静态文件和动态文件等等,一个目录对应一个菜单栏选择的条目功能实现过程。比如roles/system_init目录下存放的任务,就是对应菜单栏中的“安装服务器依赖包”类目实现过程,以此类推。
    4. 当执行到角色任务时,也会考虑到默认变量和可修改变量,任务中可直接引用同层目录下的default目录下定义的默认变量,也可在同层目录下定义vars目录用于可覆盖变量。
    5. 当角色任务执行完时,说明用户在菜单栏选择的那个功能条目执行完了。

3.1 定义菜单栏

1.使用whiptail的菜单栏功能,如下是基本语法。

whiptail --title "系统初始化类目" --menu "选择你要操作的类目,使用上下键选择:" 15 60 4 "control_sys" "主控机设置" "secret_free" "服务器免密"    3>&1 1>&2 2>&3

shell脚本进阶1——精读ansible+shell脚本_第1张图片

2.优化下对话框效果,把菜单栏想要显示出来的类目全部加上去。

whiptail \
--fullbuttons \
--separate-output \
--ok-button "确定" \
--cancel-button "取消" \
--title "系统初始化类目" \
--menu "选择操作类目,使用上下键选择:" 0 0 0 \
"control_sys" "主控机设置" \
"secret_free" "服务器免密" \
"system_init" "安装依赖环境" \
"system_date" "时间同步" \
"system_fire"  "设置防火墙"  \
"system_regs" "harbor仓库域名解析" \
3>&1 1>&2 2>&3

shell脚本进阶1——精读ansible+shell脚本_第2张图片

3.定义子函数,引用函数观察效果。此时子函数的输出结果是${choice_name}的值,包括control_sys、secret_free、secret_init、secret_date、secret_fire、secret_regs。这个值会影响后面主函数的判断,决定要对哪个子函数执行。

[root@localhost ~]# cat qingjun.sh 
#!/bin/bash
select_menu() {
    choice=$(
        whiptail \
        --fullbuttons \
        --separate-output \
        --ok-button "确定" \
        --cancel-button "取消" \
        --title "系统初始化类目" \
        --menu "选择操作类目,使用上下键选择:" 0 0 0 \
          "control_sys" "主控机设置" \
          "secret_free" "服务器免密" \
          "system_init" "安装依赖环境" \
          "system_date" "时间同步" \
          "system_fire" "设置防火墙" \
          "system_regs" "harbor仓库域名解析" \
        3>&1 1>&2 2>&3)
    local choice
    choice_name="${choice}"
}
select_menu

shell脚本进阶1——精读ansible+shell脚本_第3张图片

4.优化代码,改成变量引用,再查看效果。

[root@localhost ~]# cat qingjun.sh 
#!/bin/bash
select_menu() {
    local ok="确认"
    local cancel="取消"
    local title_txt="系统初始化类目"
    local menu_txt1="上下键选择选择操作类目:"
    local menu_txt2=(
      control_sys  "    主控机设置"
      secret_free  "    服务器免密"
      system_init  "    安装依赖环境"
      system_date  "    时间同步"
      system_fire  "    设置防火墙"
      system_regs  "    harbor仓库域名解析"
    )
    local choice
    choice=$(
        whiptail \
        --fullbuttons \
        --separate-output \
        --ok-button ${ok} \
        --cancel-button ${cancel} \
        --title ${title_txt} \
        --menu ${menu_txt1} 0 0 0 \
        "${menu_txt2[@]}" \
        3>&1 1>&2 2>&3)
    service_name="${choice}"
}
select_menu

shell脚本进阶1——精读ansible+shell脚本_第4张图片

3.2 定义子函数

  • 根据用户选择的菜单类目返回结果,作为ansible执行的标签,去匹配tool.yml中对应标签的角色任务。
  • 以下所有子函数都是这个执行逻辑。

1.定义服务器免密子函数。

secret_free_server() {
        ansible-playbook -v -t ${service_name} ./tools.yml
}

2.服务器初始化子函数。

system_init() {
    ansible-playbook -v -t ${service_name} ./tools.yml
}

3.定义时间同步子函数。在ansible执行时-e传入了一个date_ip变量,变量值是用户输入的时间服务器ip,该变量在对应的角色任务中也会被引用,若在其他文件里定义了此变量,则最终会被-e参数指定的变量值所覆盖。

set_date_address() {
    local address1=""
    local address2=""
    title_box="设置时间服务器IP地址"
    while true; do
        date_box="请设置时间服务器的地址:"
        address1=$(whiptail --title "${title_box}" --inputbox "${date_box}" 8 78 3>&1 1>&2 2>&3)
        [ $? != 0 ] && exit
        [ "${address1,,}" == "exit" ] && exit
        date2_box="请确认时间服务器的地址:"
        address2=$(whiptail --title "${title_box}" --inputbox "${date2_box}" 8 78 3>&1 1>&2 2>&3)
        [ $? != 0 ] && exit
        [ "${address2,,}" == "exit" ] && exit
        if [ -z "${address1}" ] || [ -z "${address2}" ] || [ "${address1}" != "${address2}" ]; then
            msg_box="两次输入不一致,请重新设置"
            whiptail --title "${title_box}" --msgbox "${msg_box}" 8 78
            [ $? != 0 ] && exit
            [ "${address,,}" == "exit" ] && exit
            continue
        fi
        break
    done
    date_address=${address2}
}

set_date_ansible() {
    ansible-playbook -v -e date_ip=${date_address} -t ${service_name} ./tools.yml
}

4.定义设置防火墙子函数。这里的ansible执行时也同样传入了两个参数,与上同理。

set_reg_address() {
    local address1=""
    local address2=""
    title_box="输入Harbor仓库地址"
    while true; do
        reg_box="请输入Harbor仓库的地址:"
        address1=$(whiptail --title "${title_box}" --inputbox "${reg_box}" 8 78 3>&1 1>&2 2>&3)
        [ $? != 0 ] && exit
        [ "${address1,,}" == "exit" ] && exit
        reg2_box="请确认Harbor仓库的地址:"
        address2=$(whiptail --title "${title_box}" --inputbox "${reg2_box}" 8 78 3>&1 1>&2 2>&3)
        [ $? != 0 ] && exit
        [ "${address2,,}" == "exit" ] && exit
        if [ -z "${address1}" ] || [ -z "${address2}" ] || [ "${address1}" != "${address2}" ]; then
            msg_box="两次输入不一致,请重新设置"
            whiptail --title "${title_box}" --msgbox "${msg_box}" 8 78
            [ $? != 0 ] && exit
            [ "${address,,}" == "exit" ] && exit
            continue
        fi
        break
    done
    reg_address=${address2}
}

set_reg_ansible() {
    ansible-playbook -v -e num_reg=${num} -e reg_ip=${reg_address} -t ${service_name} ./tools.yml
}

5.定义防火墙操作子函数。

firewalld_menu() {
    local title_box="Operation Tools Menu"
    local menu_box="选择需要的工具:"
    local menu_options=(
        firewall_start "    开启防火墙"
        firewall_command "    批量执行防火墙命令"
    )
    local choice
    choice=$(
        whiptail \
            --fullbuttons \
            --separate-output \
            --title "${title_box}" \
            --menu "${menu_box}" 0 0 0 \
            "${menu_options[@]}" \
            3>&1 1>&2 2>&3
    )
    firewall_name="${choice}"
}

firewalld_cmd_menu() {
    local command=""
    title_box="批量输出防火墙命令窗口"
    date_box="请输入要执行的防火墙命令:\n示例 80/tcp 或 80/udp 或 80-81/tcp 或 80-81/udp 或 192.168.0.1 或 192.0.2.0/24"
    command=$(whiptail --title "${title_box}" --inputbox "${date_box}"  8 100 3>&1 1>&2 2>&3)
    [ $? != 0 ] && exit
    firewall_cmd=${command}

}

# 防火墙开启
firewall_start() {
    ansible-playbook -v -t ${firewall_name} ./tools.yml
}


# 防火墙批量操作命令
firewall_command() {
    ansible-playbook -v -t ${firewall_name} -e cmd=${firewall_cmd} ./tools.yml
}

3.3 定义主函数

代码解读:

  1. 先执行定义菜单的子函数。
  2. if判断开始,若用户选择的类目结果为secret_free,对应服务器免密操作,则执行secret_free_server子函数;
  3. 若选择的类目结果为system_date,对应时间同步操作,则依次执行set_date_address和set_date_ansible子函数;
  4. 若选择的类目结果为system_regs,对应harbor域名解析操作,则依次执行 set_reg_address和 set_reg_ansible子函数;
  5. 若选择的类目结果为system_init,对应受控机系统初始化操作,则执行system_init子函数;
  6. 若选择的类目结果为control_sys,对应主控机系统初始化,则执行主控机初始化脚本;
  7. 若选择的类目结果为system_fire,对应防火墙操作,根据用户选择类目结果再次判断执行顺序,若为firewall_start,对应开机防火墙,则执行firewall_start子函数;若为firewall_command,对应设置防火墙命令,值执行firewall_command子函数。
main() {
        menu_service
    if [[ ${service_name} == "secret_free" ]];then
        secret_free_server
    elif [[ ${service_name} == "system_date" ]];then
        set_date_address
        set_date_ansible
    elif [[ ${service_name} == "system_regs" ]];then
        set_reg_address
        set_reg_ansible
    elif [[ ${service_name} == "system_init" ]];then
        system_init
    elif [[ ${service_name} == "control_sys" ]];then
        sh ./control.sh
    elif [[ ${service_name} == "system_fire" ]];then
        firewalld_menu
        if  [[ ${firewall_name} == "firewall_start" ]];then
            firewall_start
        elif [[ ${firewall_name} == "firewall_command" ]];then
            firewalld_cmd_menu
            firewall_command
        else
            exit 1
        fi
    else
        exit 1
    fi
}
main

3.4 playbook

  • shell脚本的主体是以playbook来执行,所以需要定义清单文件、变量、静态文件、jinja2动态文件、角色、剧本。
  • 菜单栏的所有功能类目的实现顺序都是一样的,唯一不同的就是编写的任务不同,使用的模块和变量不同。下面以编写playbook到执行角色任务的顺序解读各文件内容,只要学会一个功能的实现逻辑,其他功能都可以应对自如。

3.4.1 shell执行playbook

  • 首先需要确定我们的playbook在哪儿执行的,它是在shell脚本中执行的,是定义成函数形式被主函数套层引用了。
  • 在shell脚本中应该只写执行playbook的命令,所有的变量和角色任务都应该放在其他文件被其引用,这样可以把shell和playbook隔离开,一方面排错时脉络清晰,一方面避免脚本太长影响阅读。
  • 将执行命令定义成子函数,-t指定只执行带该标签的任务,后面跟上剧本。
secret_free_server() {
        ansible-playbook -v -t ${service_name} ./tools.yml
}

3.4.2 编写playbook

[root@localhost system_tools-master]# cat tools.yml 
---
- name: Servers Secret free
  hosts: servers                ##清单文件里的主机组名称。
  remote_user: "{{ ansible_ssh_user }}"     ##使用受控机上的哪个用户执行任务,这里就是远程过去的用户root。
  roles:
    - role: secret_free            ##执行角色。
      tags: secret_free           ##标签。

3.4.3 编写角色

  • 编写角色最重要的就是根据各种模块的组合使用实现用户想要的功能,需要掌握常用模块,以及会查阅官方文档的模块使用。
  • 如下角色是给受控机做免密登录,分三个任务共同完成。

任务解读:

  • 任务一:先使用shell模块查看受控机上是否存在/root/.ssh/id_rsa.pub公钥,并将查询结果赋予变量名file作为第2个任务的判断条件。
  • 任务二: 根据第一个任务查询的结果中的rc值判断是否为真,rc类似$?,其值为0时说明为真。使用when语句定义一个条件判断,当取的rc值不等于0时候,说明没有公钥,此时执行block块任务,里面使用了shell模块执行了生成密钥对的命令。
  • 任务三:使用authorized_key模块将主控机上的公钥文件内容提取出来传给受控机。

注意事项:

  1. 每个任务中还有很多参数的运用,这都是起优化作用,比如执行遇错知否跳过执行下个任务、开启特权升级权限、是否设置为预操作,等等。参数众多不一一解释,大家自行查阅官方文档。
  2. 任务中还引用了变量"{{ ansible_ssh_user }}",这个变量是清单文件里的。
[root@localhost tasks]# cat main.yml 
---
- name: Judge whether the public key exists
  shell: "ls /root/.ssh/id_rsa.pub"
  args:
    warn: False
  ignore_errors: True
  register: file
  # 不打印找不到文件时的错误输出信息
  failed_when: False
  # 不打印改变之后的输出信息
  changed_when: False
  become: yes
  check_mode: no

- name: public key is not exit, create public key
  when: file.rc != 0
  block:
  - name: create server public key
    shell: "ssh-keygen -t rsa -N '' -f /root/.ssh/id_rsa"

- name: Configuring Passwordless login
  authorized_key:
    user: "{{ ansible_ssh_user }}"
    key: "{{ lookup('file', '/root/.ssh/id_rsa.pub') }}"
  ignore_errors: yes

3.4.4 定义静态清单

  • 角色任务中引用的变量就是从这来的,清单文件里的变量优先级最低,可被其他任意变量文件覆盖。
[root@localhost system_tools-master]# cat inventory.ini 
[servers]
192.168.247.100 ansible_ssh_user=root ansible_ssh_pass=111111 ansible_port=22

3.4.5 修改配置文件

  • 等我们调试完脚本功能后,就要开始优化脚本了,可以在修改ansible配置文件参数。
##指定清单文件。
inventory= ./inventory.ini
##并发数。
forks= 50

#表示默认收集 facts事实,但 facts 已有的情况下不会收集,即使用缓存 facts
gathering = smart

#首次连接是否检查key认证,False表示跳过。
host_key_checking = False

#默认情况下,角色中的变量将在全局变量中可见。
为了防止这种情况,设置此变量,只有角色中的任务和处理程序才能看到那里的变量。
private_role_vars = yes


#getfact缓存的主机信息存放方式
fact_caching = jsonfile

##存放文件地址。
fact_caching_connection=./facts-cache

3.4.6 查看收集事实信息

  • 所谓事实信息就是受控机的相关信息,内容较多,包括主机名称、内核版本、网络接口、IP地址、操作系统版本、各种环境变量、CPU数量、提供的或可用的内存、可用磁盘空间,等等。可以使用debug模块在任务中打印事实信息。
  • 事实信息有setup模块获取,ansible默认是开启状态,若用不到事实内容可关闭。
  • 事实信息内容也可以被定义输出到文件里,在ansible配置文件里有参数fact_caching 指定输出文件类型,fact_caching_connection参数指定输出文件存放目录。

1.查看刚才收集的受控机信息。
shell脚本进阶1——精读ansible+shell脚本_第5张图片
2.使用setup模块查看。
shell脚本进阶1——精读ansible+shell脚本_第6张图片

3.6 如何自测功能

1.先在清单文件中定义自测的受控机信息。
在这里插入图片描述
2.编写playbook。这里我就拿上面解释的服务器免密操作任务做演示,需要注意的是playbook中的层级关系。
shell脚本进阶1——精读ansible+shell脚本_第7张图片
3.执行playbook,查看结果。进入受控机查看/root/.ssh目录下是否生成了密钥对,是否有主控机的公钥。自此,一个小功能自测完成。
shell脚本进阶1——精读ansible+shell脚本_第8张图片

你可能感兴趣的:(shell脚本,ansible,自动化,运维,linux,ssh)