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
    openvpn
    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 <<EOF
version: '3'
services:
  nginx:
    container_name: nginx-yum-62633
    image: nginx:latest
    restart: always
    ports:
      - "62633:80"
    volumes:
      - "/opt/tools:/usr/share/nginx/html"
      - "/opt/nginx_yum/default.conf:/etc/nginx/conf.d/default.conf"
EOF
    if docker ps |grep nginx-yum-62633 >/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 <<EOF
set timeout 20
spawn ssh-copy-id -i /root/.ssh/id_rsa.pub  $address
expect {
        "(yes/no)" {send "yes\r"; exp_continue}
        "*password:" {send "$passwd\r"}
}
expect eof
EOF
    fi &
    wait 
    success "本机免密配置完成!!!"
}

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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.查看刚才收集的受控机信息。
在这里插入图片描述
2.使用setup模块查看。
在这里插入图片描述

3.6 如何自测功能

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/30193.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

利用谷歌DevTool解决web网页内存泄漏问题

目录 web网页内存泄漏 主要的内存泄漏来源 利用谷歌DevTool定位内存泄漏问题 性能Performance 主要功能 Performance insights性能数据分析 Memory内存 三种模式 相关概念 解决内存泄漏问题 第一步 &#xff1a;是否内存泄漏&#xff1a;js堆直增不降&#xff1b;降…

Unity3d_Cut\Clipping sphere\CSG(boolean)(裁剪模型重合部分)总结

1、https://liu-if-else.github.io/stencil-buffers-uses-in-unity3d/ 下载&#xff1a;https://github.com/liu-if-else/UnityStencilBufferUses 2、手动切割 Unity 模型切割工具,CSG,任意图案,任意切割_unity csg_唐沢的博客-CSDN博客 3、 Shader Unity Shader学习&#x…

CVPR 2023 | 计算机视觉顶会亮点前瞻

在知识和技术都迅速更新迭代的计算机领域中&#xff0c;国际计算机视觉与模式识别会议&#xff08;CVPR&#xff09;是计算机视觉方向的“顶级流量”&#xff0c;引领着学科及相关领域的研究潮流。今天我们为大家带来5篇微软亚洲研究院被 CVPR 2023 收录的论文&#xff0c;主题…

JVM知识点梳理

什么是JVM&#xff1f; JVM是java虚拟机的缩写 &#xff0c;也是java程式可以实现跨平台的关键。 JVM部分需要知道什么东西&#xff1f; JVM的结构和功能、参数配置、GC回收机制、GC回收器极其优缺点。 JVM结构&#xff08;栈&#xff0c;程序计数器&#xff0c;方法区&#xf…

基于深度学习的高精度打电话检测识别系统(PyTorch+Pyside6+YOLOv5模型)

摘要&#xff1a;基于深度学习的高精度打电话检测识别系统可用于日常生活中或野外来检测与定位打电话目标&#xff0c;利用深度学习算法可实现图片、视频、摄像头等方式的打电话目标检测识别&#xff0c;另外支持结果可视化与图片或视频检测结果的导出。本系统采用YOLOv5目标检…

《C# 教程》菜鸟教程学习笔记

学习地址 ######C#有用的网站 C# Programming Guide - 介绍了有关关键的 C# 语言特征以及如何通过 .NET 框架访问 C# 的详细信息。Visual Studio - 下载作为 C# 集成开发环境的 Visual Studio 的最新版本。Go Mono - Mono 是一个允许开发人员简单地创建跨平台应用程序的软件平台…

【每日算法】【203. 移除链表元素】

☀️博客主页&#xff1a;CSDN博客主页 &#x1f4a8;本文由 我是小狼君 原创&#xff0c;首发于 CSDN&#x1f4a2; &#x1f525;学习专栏推荐&#xff1a;面试汇总 ❗️游戏框架专栏推荐&#xff1a;游戏实用框架专栏 ⛅️点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd;&…

Docker基本介绍

一、定义 Docker 是一个开源的应用容器引擎&#xff0c;让开发者可以打包他们的应用以及依赖包到一个可移植的容器中&#xff0c;然后发布到任何流行的 Linux 机器上&#xff0c;也可以实现虚拟化。 解决了运行环境和配置问题的软件容器&#xff0c; 方便做持续集成并有助于整…

UnityVR--UIManager--UI管理1

目录 前言 UI节点的结构 需要用到的组件 1. CanvasGroup 2. Button等控件的OnClick()监听 3. EventTrigger 建立UI工具集 1. 管理UI节点 2. UIBase包含了以下的工具 建立分面板的管理工具——以主面板MainUi为例 前言 UI在项目中的重要性不言而喻&#xff0c;并且UI控件的…

软件设计师第4题

首先&#xff0c;我是备考2023年上半年的考试。 一、历年考试题 历年的考题如下&#xff0c;从表中分析可以看出&#xff0c;动态规划法、排序算法、回溯法、分治法是很大概率考察的算法&#xff0c;尤其是动态规划法&#xff0c;本身其理解难度较高&#xff0c;且可以出的题型…

【计网】第二章 物理层

文章目录 物理层一、物理层的基本概念二、数据通信的基础知识2.1 数据通信系统的模型2.2 有关信道的基本概念2.3 信道的极限容量2.3.1 奈奎斯特定理2.3.1 香农定理2.3.2 信噪比 三、物理层下面的传输媒体3.1 导引型传输媒体3.2 非导引型传输媒体 四、信道复用技术4.1 频分复用 …

【小沐学Python】Python实现在线电子书(Sphinx + readthedocs + github + Markdown)

文章目录 1、简介2、安装3、创建测试工程4、项目文件结构5、编译为本地文件6、编译为http服务7、更改样式主题8、支持markdown9、修改文档显示结构10、项目托管到github11、部署到ReadtheDocs结语 1、简介 Sphinx 是一个 文档生成器 &#xff0c;您也可以把它看成一种工具&…

STC15WProteus仿真HX711电子秤串口计价称重4x4键盘STC15W4K32S4

STC15WProteus仿真HX711电子秤串口计价称重4x4键盘STC15W4K32S4 Proteus仿真小实验&#xff1a; STC15WProteus仿真HX711电子秤串口计价称重4x4键盘STC15W4K32S4 功能&#xff1a; 硬件组成&#xff1a;STC15W4K32S4单片机 LCD12864显示器4x4矩阵键盘HX711电子秤 1.单片机通…

操作教程:EasyCVR视频融合平台如何配置平台级联?

EasyCVR视频融合平台基于云边端一体化架构&#xff0c;可支持多协议、多类型设备接入&#xff0c;在视频能力上&#xff0c;平台可实现视频直播、录像、回放、检索、云存储、告警上报、语音对讲、电子地图、集群、智能分析以及平台级联等。平台可拓展性强、开放度高、部署轻快&…

NUCLEO-F411RE RT-Thread 体验 (3) - GCC环境 uart驱动的移植以及console的使用

NUCLEO-F411RE RT-Thread 体验 (3) - GCC环境 uart驱动的移植以及console的使用 1、准备工作 在第一节里&#xff0c;我们用stm32cubemx将pa2 pa3管脚配置成usart2&#xff0c;用于跟st-link虚拟串口的打印用&#xff0c;那么我们先重定向printf函数&#xff0c;看这条通道是…

2009年iMac装64位windows7

前言&#xff1a;单位领导会花屏的iMac&#xff08;24寸 2009年初版&#xff09;我捡来用&#xff0c;应该大约是在2020年安装了32位windows7&#xff0c;发现不安装显卡驱动便不会花屏死机&#xff0c;于是就当简单的上网机用着&#xff0c;毕竟iMac的显示屏还是蛮不错的。现在…

windows系统安装显卡驱动软件和CUDA11.1的详细教程

深度学习目标检测框架在进行图像计算时需要GPU进行加速&#xff0c;需要用到硬件GPU显卡&#xff0c;目标检测框架和硬件GPU建立联系需要通过①显卡驱动软件&#xff1b;②CUDA软件依次建立联系。这两个软件&#xff0c;可直接从NVIDIA官网下载&#xff0c;版本没有非常严格的需…

python获取某乎热搜数据并保存成Excel

python获取知乎热搜数据 一、获取目标、准备工作二、开始编码三、总结 一、获取目标、准备工作 1、获取目标&#xff1a; 本次获取教程目标&#xff1a;某乎热搜 2、准备工作 环境python3.xrequestspandas requests跟pandas为本次教程所需的库&#xff0c;requests用于模拟h…

WinDbg安装入坑3(C#)

由于作者水平有限&#xff0c;如有写得不对的地方&#xff0c;请指正。 使用WinDbg的过程中&#xff0c;坑特别的多&#xff0c;对版本要求比较严格&#xff0c;如&#xff1a; 1 32位应用程序导出的Dump文件要用32位的WinDbg打开&#xff0c;想要没有那么多的问题&#xf…

SpringCloud Eureka注册服务提供者(七)

这里我们在原来的服务提供者项目 microservice-student-provider-1001 上面直接修改&#xff1a; 首先pom.xml修改&#xff0c;加上eureka客户端依赖&#xff1a; <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>…