自动化部署工具
学习了:http://blog.csdn.net/qq_25711251/article/details/72869682
自动化部署工具
学习了:http://blog.csdn.net/qq_25711251/article/details/72869682
1.简述
pssh是一个可以在多台服务器上执行命令的工具,同时支持拷贝文件,是同类工具中很出色的。使用是必须在各个服务器上配置好密钥认证访问。其实在自动化部署工具世界里有不少的出名的家伙,比如puppet、chef、ansible、slatstack等等以及本文要介绍的pssh。
2.平台信息
假设我们的有两台主机并且都能连接网络,因为等下要下载pssh的安装包,拿本次搭建环境的信息为例,主机系统都为Centos7.1,如下:
IP | 主机名 | 备注 |
---|---|---|
10.63.39.38 | lijiaze-master | 本地主机(或者说控制端) |
10.63.39.39 | lijiaze-node | 远程主机(或者说受控端) |
3.1 下载pssh的安装包
# 在/home目录下新建一个目录用来存放pssh软件压缩包,并进入该该目录。
# mkdir -p /home/pssh
# 下载v2.3.1版本的pssh软件压缩包
# cd /home/pssh
# wget http://parallel-ssh.googlecode.com/files/pssh-2.3.1.tar.gz
3.2 安装pssh
# 解压安装包
# tar -zxvf pssh-2.3.1.tar.gz
# 进入软件包pssh-2.3.1目录
# cd pssh-2.3.1
# 安装pssh
# python setup.py install
这样就安装完pssh,so easy!下面我们再来测试一下能不能用!
想要对远程主机进行控制,必须要将本地主机的密钥传到远程主机上。所以首先生成本地主机的密钥,然后传送到远程主机上。
# 本地主机生成密钥(注意中途不需要输入任何东西,一路按回车键即可)
# ssh-keygen
# 将刚刚生成的密钥传送到远程主机(这时输入yes以及10.63.39.38这台主机的root用户的密码即可)
# ssh-copy-id -i root@10.63.39.38
经过了上面的操作,我们就可以对远程主机进行操作了,当远程主机很多时,比如我们公司产品上线前进行测试时,有几百台主机要进行控制,这时我就会将这些主机IP弄成一个txt文本,当然,一台也是可以的!所以为了方便,在这里统一用文本形式。
# 新建一个txt文本,并将10.63.39.39这个IP号添加到这个文本里
# touch lijiaze.txt
# echo 10.63.39.39 >> lijiaze.txt
这样我们就可以使用pssh对远程主机进行控制了!pssh有五个常用的命令,下面分别一一简述。
# 1 pssh 多主机上并行执行命令,比如我要查看一下远程主机root目录下有那些文件,就可以用下面的命令
# pssh -h lijiaze.txt -i 'ls -al'
# 2 pscp 把文件并行地复制到多个主机上,比如在我的root目录下有一个lijiaze.tar.gz的压缩包要传到远程主机的/home/lijiaze目录下,就可以使用下面的命令
# pscp -h lijiaze.txt /root/lijiaze.tar.gz /home/lijiaze
# 3 pslurp 从多台远程机器拷贝文件到本地,和pscp刚好相反。比如在远程主机的root目录下有一个lijiaze.sh脚本要传回本地主机/home/lijiaze目录下,就可以使用下面的命令
# pslurp -r -h lijiaze.txt /root/lijiaze.sh /home/lijiaze /home/lijiaze
# 4 pnuke 并行在远程主机杀进程,比如在远程主机有一个docker服务,我想把它给终止掉,就可以使用下面的命令
# pnuke -h lijiaze.txt docker
# 5 prsync 使用rsync协议从本地计算机同步到远程主机,这个很少用到,至少我还没用过多少次,所以有需要的请自行看帮助文档。
基础说明
在日常运维过程上经常需要安装weblogic环境,常规的思路是通过安装VNC至操作系统,再通过界面安装JDK + weblogic软件。
这次通过shell + jython的方式实现weblogic环境的自动化部署环境信息
Linux
JDK1.8 ,tar.gz版本
weblogic 12c(12.1.3.0)自动化部署工具
JDK的自动安装
默认安装目录为:/usr/java
通过交互式提示,输入安装包的绝对路径,然后默认安装到/usr/java目录下,并配置环境变量,并生效变量代码具体实现:jdkInstaller.sh
#!/bin/bash #======== # JDK installer 安装JDK到/usr/java目录下 # 配置/etc/profile中JAVA_HOME环境变量,并生效 source /etc/profile # 此脚本只需要执行一次,如果安装,则需要提前删除/usr/java安装的jdk # 示例安装文件:jdk-8u211-linux-x64.tar.gz function jdkInstaller() { echo ' ################ JDK自动安装工作 工具默认目录:安装JDK到/usr/java目录下 配置/etc/profile中JAVA_HOME环境变量,并生效 source /etc/profile 注意: 本工具需要使用root权限执行 本工具需要使用jdk的tar.gz格式的源文件 ' echo '请输入安装包文件位置(绝对路径):' read input if [[ ! -f $input ]]; then #statements echo "$input 文件不存在,推出程序" fi # 配置一些参数 j_dir="/usr/java" p_file="/etc/profile" #j_dir="/app/install/wls/testdir" #p_file="/app/install/wls/1.txt" mkdir -p $j_dir #last_dir_name=`echo $input|awk -F '/' '{print $NF}'|cut -d '.' -f1` last_dir_name=`tar -ztf $input|head -1|sed 's/.$//'` JAVA_HOME="$j_dir/$last_dir_name" #将文件解压到指定目录 tar zxvf $input -C $j_dir # 获取java_home目录 chown -R root:root $JAVA_HOME #设定环境变量至/etc/profile echo "export JAVA_HOME=$JAVA_HOME" >> $p_file echo "export PATH=$PATH:$JAVA_HOME/bin" >> $p_file source $p_file } jdkInstaller
weblogic 12c自动化部署和配置
通过交互式操作完成weblogic软件环境安装
在执行脚本前,在脚本的环境变量部分需要配置一些使用到的基本信息
如安装目录、domain目录、控制台用户密码、创建的server名和使用端口信息等,然后执行wlsInstaller.sh来实现自动安装代码实现:
wlsInstaller.sh#!/bin/bash # weblogic12c自助安装 clear ####安装目录配置 wls_install_home=/app/weblogic inventory_loc=$wls_install_home/oraInventory inst_group=oinstall oraInst=$inventory_loc/oraInst.doc # for silent mode responseFile=$inventory_loc/wls.rsp mv_home=$wls_install_home/Oracle/Middleware/Oracle_Home ##### # domain_name AND weblogic_password 后期可以做成交互式输入参数 # for create domain wlst_scripts=$mv_home/oracle_common/common/bin/wlst.sh domain_py_template="create_domain.py" domain_name="fr_domain" # pass the domain_name to py template for creating domain domain_home=$mv_home/user_projects/domains/ # default domain_home weblogic_password="weblogic123" jar_template_name=$mv_home/wlserver/common/templates/wls/wls.jar host_ip="10.1.111.26" ########### # nodemanger config使用这一部分环境变量 # 配置使用nmRoll注册nodemanager home和domain_home machine_name=`hostname` osuser="weblogic" osuser_group="oinstall" admin_user="weblogic" admin_pasword=$weblogic_password host_ip="10.1.111.26" admin_url="t3://$host_ip:7001" my_domain=$domain_home$domain_name/ py_config_nm="configNodeManager.py" #### # 创建server1的部分 #### sv_name="fr_server2" port="7007" mem_args="-Xms4096m -Xmx4096m -XX:MaxPermSize=256M" #请注意通过脚本只能设置这三个参数大小值 py_create_sv="createServer.py" # check dirs function checkDirs() { # check install dirs if [[ ! -d $wls_install_home ]]; then #statements mkdir -p $wls_install_home mkdir -p $inventory_loc fi } # 1·配置oraInst.doc文件 function createOraInst() { cat /dev/null > $oraInst echo "inventory_loc=$inventory_loc" >> $oraInst echo "inst_group=$inst_group">> $oraInst echo "oraInst.doc 的位置 : $oraInst" echo "oraInst.doc文件内容:" cat $oraInst } # 2.配置response文件 function createResponseFile() { cat /dev/null > $responseFile echo "[ENGINE]" >> $responseFile echo "Response File Version=1.0.0.0.0" >> $responseFile echo "[GENERIC]" >> $responseFile echo "DECLINE_AUTO_UPDATES=true" >> $responseFile echo "MOS_USERNAME=" >> $responseFile echo "MOS_PASSWORD=<SECURE VALUE>" >> $responseFile echo "AUTO_UPDATES_LOCATION=" >> $responseFile echo "SOFTWARE_UPDATES_PROXY_SERVER=" >> $responseFile echo "SOFTWARE_UPDATES_PROXY_PORT=" >> $responseFile echo "SOFTWARE_UPDATES_PROXY_USER=" >> $responseFile echo "SOFTWARE_UPDATES_PROXY_PASSWORD=<SECURE VALUE>" >> $responseFile echo "ORACLE_HOME=$wls_install_home/Oracle/Middleware/Oracle_Home" >> $responseFile echo "FEDERATED_ORACLE_HOMES=" >> $responseFile echo "INSTALL_TYPE=WebLogic Server" >> $responseFile echo "responseFile响应文件位置: $responseFile" echo "显示响应文件的内容:" cat $responseFile } # 执行默认安装weblogic软件 function instll_wls() { echo "请输入wls jar安装包位置:" read input java -jar $input -silent -responseFile $responseFile -invPtrLoc $oraInst } # create Domain use create_domian.py function createDomain() { d_array[0]=$domain_name d_array[1]=$domain_home d_array[2]=$weblogic_password d_array[3]=$jar_template_name d_array[4]=$host_ip # 打印参数输出 echo "Print the create DomainParas:" for i in ${d_array[*]} do echo $i done # 临时使用 rm -rf $domain_home$domain_name # create domian use wlst.sh $wlst_scripts $domain_py_template ${d_array[@]} if [[ $? -eq 0 ]]; then #statements echo "Create Domain : $domain_name SUCSESS!!" fi # (yes or no)? to start AdminServer echo "We try to start AdminServer, Please input:(yes or no)?" while read input do #statements case $input in yes ) echo "Input value: $input . Started AdminServer" cd $domain_home$domain_name nohup ./startWebLogic.sh & break ;; no ) echo "Input value: $input . Not Start AdminServer, Exit" break ;; * ) echo "输入错误,请重新输入 yes or no" continue ;; esac done } # function configNodeManger() { # machine_name=`hostname` # osuser="weblogic" # osuser_group="oinstall" # admin_url="t3://localhost:7001" d_array[0]=$machine_name d_array[1]=$osuser d_array[2]=$osuser_group d_array[3]=$admin_user d_array[4]=$admin_pasword d_array[5]=$admin_url d_array[6]=$my_domain d_array[7]=$host_ip # 先修改nodemanager配置文件参数,方便后续使用wlst启动 # nm_pros=$my_domain/nodemanager/nodemanager.properties echo $nm_pros sed -i 's/SecureListener=true/SecureListener=false/g' $nm_pros # cat $nm_props|grep SecureListener # 使用for打印数组 for i in ${d_array[@]} ; do echo $i ; done echo "$wlst_scripts $py_config_nm ${d_array[@]}" #执行创建部分 echo "### Config NM start: running ...." $wlst_scripts $py_config_nm ${d_array[@]} echo "### Config NM stop: ended." # 通过脚本启动nodemanager echo "We try to start node manager, Please input:(yes or no)?" while read input do #statements case $input in yes ) echo "Input value: $input . Started NodeManager" cd $domain_home$domain_name/bin nohup ./startNodeManager.sh & break ;; no ) echo "Input value: $input . Not Start NodeManager, Exit" break ;; * ) echo "输入错误,请重新输入 yes or no" continue ;; esac done # nmEnroll('$my_domain','$my_domain/nodemanager') } # 按照模板创建server1,并指定到当前hostname的machine function createServer() { echo " ########### 按照模板创建单个 $sv_name 默认计算机: $machine_name IP地址:$host_ip 默认端口:$port 启动参数:$mem_args ########### " d_array[0]=$sv_name d_array[1]=$machine_name d_array[2]=$host_ip d_array[3]=$port #d_array[4]=$mem_args # for : connect to admin control d_array[4]=$admin_user d_array[5]=$admin_pasword d_array[6]=$admin_url d_array[7]=$mem_args echo $mem_args $wlst_scripts $py_create_sv ${d_array[@]} echo "完成创建$sv_name .." } showmenu() { echo ' ********************************** weblogic12c 自动安装工具 (0) 自动安装weblogic12c (1) 创建domain (2) 配置nodemanager (3) 创建app server (9) Exit 请输入执行步骤: ' } # 主程序部分 while showmenu read input do #statements case $input in 0) echo "开始自动安装" checkDirs createOraInst createResponseFile instll_wls continue ;; 1) echo "创建domain" # $wlst_scripts $domain_template $domain_name $domain_dir $weblogic_password createDomain continue ;; 2) echo "配置nodemanager" configNodeManger continue ;; 3) echo "创建app server" createServer continue ;; 9) echo "退出程序" exit 0 ;; *) echo "其它错误选项,请重新选择" continue ;; esac done
创建domain的jython文件:
create_domain.pyimport os import time import sys # Create the Domain Dir # domain_name="fr_domain" # 传递参数 domain_name # this file parameter IS passed for A shell ARRAY domain_name = sys.argv[1] domain_home = sys.argv[2] weblogic_password= sys.argv[3] jar_template = sys.argv[4] host_ip=sys.argv[5] # added for set adminserver # domain_dir= "/app/weblogic/Oracle/Middleware/Oracle_Home/user_projects/domains/"+domain_name # 其实创建目录的时候,可以放到linux shell的部分处理 domain_dir = domain_home + domain_name print "domain_dir: " + domain_dir if os.path.exists(domain_dir) == False: os.makedirs(domain_dir) print "### Create " + domain_name + " StartTime : " print time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time())) #======================================================================================= # Open a domain template. #======================================================================================= #template_name = "/app/weblogic/Oracle/Middleware/Oracle_Home/wlserver/common/templates/wls/wls.jar" template_name = jar_template #template_name = "/app/weblogic/Oracle/Middleware/Oracle_Home/wlserver/common/templates/wls/wls.jar" readTemplate(template_name) #======================================================================================= # Configure the Administration Server and SSL port. # # To enable access by both local and remote processes, you should not set the # listen address for the server instance (that is, it should be left blank or not set). # In this case, the server instance will determine the address of the machine and # listen on it. #======================================================================================= cd('Servers/AdminServer') set('ListenAddress','') set('ListenPort', 7001) # #### we do not enable SSL for AdminServer #create('AdminServer','SSL') #cd('SSL/AdminServer') #set('Enabled', 'True') #set('ListenPort', 7002) #======================================================================================= # Define the user password for weblogic. #======================================================================================= cd('/') cd('Security/base_domain/User/weblogic') # Please set password here before using this script, e.g. cmo.setPassword('value') cmo.setPassword(weblogic_password) # ==================================== # Saved Configuration setOption('OverwriteDomain', 'true') writeDomain(domain_dir) closeTemplate() # End Time print "### Create Domain " + domain_name +" End Time:" print time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time())) #======================================================================================= # Exit WLST. #======================================================================================= exit()
配置nodemanager:
configNodeManager.pyimport sys import time import os # 获取传递过来的环境变量 machine_name = sys.argv[1] osuser = sys.argv[2] osgroup = sys.argv[3] admin_user=sys.argv[4] admin_password=sys.argv[5] admin_url=sys.argv[6] my_domain=sys.argv[7] host_ip=sys.argv[8] def nmRegisterToDomain(): # connect(admin_user,admin_password,admin_url) cd('/') nmEnroll(my_domain,my_domain + "nodemanager") def createMachine(): cd('/') cmo.createUnixMachine(machine_name) cd("/Machines/" + machine_name + "/NodeManager/" + machine_name) cmo.setNMType('Plain') cmo.setListenAddress('localhost') cmo.setListenPort(5556) cmo.setDebugEnabled(false) #绑定machine的用户和用户组 cd("/Machines/" + machine_name) cmo.setPostBindGIDEnabled(true) cmo.setPostBindUID(osuser) cmo.setPostBindGID(osgroup) cmo.setPostBindUIDEnabled(true) ############### # main 函数执行部分 connect(admin_user,admin_password,'t3://localhost:7001') cd('/') edit() startEdit() print my_domain print my_domain + "nodemanager" nmRegisterToDomain() #启动nodemanger # startNodeManager(my_domain + "nodemanager") createMachine() activate()
创建server:
createServer.pyimport os import time import sys ##### # 获取参数 sv_name=sys.argv[1] machine_name=sys.argv[2] host_ip=sys.argv[3] port=sys.argv[4] #mem_args=sys.argv[5] admin_user=sys.argv[5] admin_password=sys.argv[6] admin_url=sys.argv[7] minHeapSize=sys.argv[8] maxHeapSize=sys.argv[9] maxHeapPermSize=sys.argv[10] print "port: " + port print "admin_user: :" + admin_user print "admin_password: :" + admin_password print "admin_url: :" + admin_url def createServer(): cd('/') cmo.createServer(sv_name) cd("/Servers/" + sv_name) cmo.setListenAddress(host_ip) cmo.setListenPort(int(port)) cmo.setListenPortEnabled(true) cmo.setClientCertProxyEnabled(false) cmo.setJavaCompiler('javac') cmo.setMachine(getMBean("/Machines/" + machine_name)) cmo.setCluster(None) cd("/Servers/"+ sv_name + "/SSL/" + sv_name) cmo.setEnabled(false) # cd('/Servers/server1/ServerDiagnosticConfig/server1') # cmo.setWLDFDiagnosticVolume('Low') cd("/Servers/" + sv_name + "/ServerStart/" + sv_name) #cmo.setClassPath('-Xms4096m -Xmx4096m -XX:MaxPermSize=256M') cmo.setClassPath(maxHeapPermSize + " " + maxHeapSize + " " + maxHeapPermSize) connect(admin_user,admin_password,admin_url) cd('/') edit() startEdit() createServer() save() activate()
开源自动化部署工具
编者注:本文最初于2016年3月发布,现已更新,以包括其他选项和信息。
自从我们上一次在2016年发布有关家庭自动化工具的评论文章以来,物联网不仅仅是一个时髦的词,而且这个事实正在Swift扩展。2017年, 美国有26.5%的家庭已经在使用某种类型的智能家居技术。 在五年内,该百分比有望翻倍。
但是与此同时,许多用户担心将新设备带入家中会带来安全性和隐私问题,这是非常现实和认真的考虑 。 他们想控制谁可以访问控制设备的重要系统,并记录他们的日常生活。 可以理解的是:在一个时代,即使您的冰箱现在可能已经是一个智能设备,您是否不想知道冰箱是否在打电话给您? 您是否不希望获得一些基本保证,即使您授予设备允许与外部进行通信的权限,但只有明确授权的人员才能访问该设备?
安全问题是开源对连接设备的未来至关重要的众多原因之一。 能够完全理解控制您的家庭的程序意味着您可以查看并在必要时修改设备本身上运行的源代码。
尽管连接的设备通常包含专有组件,但将开源引入家庭自动化系统的一个很好的第一步是确保将设备绑定在一起的设备(并为您提供与其连接的接口(“集线器”))是开放的资源。 幸运的是,有很多选择,并且可以在从永远在线的个人计算机到Raspberry Pi的所有设备上运行。
这只是我们的一些最爱。
卡劳斯
Calaos被设计为一个完整的家庭自动化平台,包括服务器应用程序,触摸屏界面,Web应用程序,适用于iOS和Android的本机移动应用程序以及可在其下运行的预配置Linux操作系统。 Calaos项目来自一家法国公司,因此其支持论坛主要是法语的,尽管大多数教学材料和文档已翻译成英语。
Calaos已获得GPL版本3的许可 ,您可以在GitHub上查看其源代码。
多莫奇兹
Domoticz是一个家庭自动化系统,具有相当广泛的受支持设备库,从气象站到烟雾探测器再到远程控制,并且在该项目的网站上记录了许多其他第三方集成 。 它采用HTML5前端设计,可从桌面浏览器和大多数现代智能手机访问,并且重量轻,可在Raspberry Pi等许多低功耗设备上运行。
Domoticz主要是在GPLv3下用C / C ++编写的,其源代码可以在GitHub上浏览。
家庭助理
Home Assistant是一个开源的家庭自动化平台,旨在从Raspberry Pi到网络连接的存储(NAS)设备,几乎可以在几乎所有可以运行Python 3的计算机上轻松部署,甚至还附带Docker容器以进行部署在其他系统上轻而易举。 它与大量开放源代码和商业产品集成在一起,允许您链接例如IFTTT,天气信息或您的Amazon Echo设备,以控制从锁到灯的硬件。
Home Assistant是根据MIT许可发布的,其源代码可以从GitHub下载。
先生之家
自2016年以来, MisterHouse取得了很多进展,当时我们在此列表中将其称为“另一种可供考虑的选择”。 它使用Perl脚本监视计算机可以查询的所有内容或控制任何可以远程控制的内容。 它响应语音命令,一天中的时间,天气,位置和其他事件以打开灯,唤醒您,录制您喜欢的电视节目,宣布电话呼叫,警告您的前门打开,报告您的儿子多长时间一直在线,告诉您您女儿的车是否在超速行驶等等。 它可以在Linux,macOS和Windows计算机上运行,并且可以从各种设备进行读/写,包括安全系统,气象站,呼叫者ID,路由器,车辆定位系统等等。
MisterHouse获得GPLv2许可,您可以在GitHub上查看其源代码。
OpenHAB
OpenHAB (开放式家庭自动化总线的简称)是开源发烧友中最著名的家庭自动化工具之一,拥有大量的用户社区,并且拥有大量受支持的设备和集成。 openHAB用Java编写,可以在大多数主要操作系统上移植,甚至可以在Raspberry Pi上很好地运行。 openHAB支持数百种设备,与设备无关,旨在使开发人员更轻松地将自己的设备或插件添加到系统中。 OpenHAB还提供用于设备控制的iOS和Android应用程序以及设计工具,因此您可以为家庭系统创建自己的UI。
您可以在Eclipse Public License许可的 GitHub上找到openHAB的源代码 。
OpenMotics
OpenMotics是一个家庭自动化系统,其硬件和软件均已获得开源许可。 它旨在提供一个用于控制设备的综合系统,而不是将来自不同提供商的许多设备拼接在一起。 与其他许多主要为易于改装而设计的系统不同,OpenMotics专注于硬接线解决方案。 有关更多信息,请参见OpenMotics后端开发人员Frederick Ryckbosch的完整文章 。
OpenMotics的源代码已在GPLv2下获得许可,可从GitHub上下载。
当然,这些不是唯一可用的选项。 许多家庭自动化发烧友采用了不同的解决方案,甚至决定推出自己的解决方案。 其他用户选择使用单个智能家居设备,而不将其集成到单个综合系统中。
如果以上解决方案无法满足您的需求,则可以考虑以下一些替代方案:
- EventGhost是仅在Microsoft Windows PC上运行的开源( GPL v2 )家庭影院自动化工具。 它允许用户通过使用触发宏的插件或编写自定义Python脚本来控制媒体PC和连接的硬件。
- ioBroker是基于JavaScript的IoT平台,可以控制灯光,锁,恒温器,媒体,网络摄像头等 。 它可以在运行Node.js的任何硬件上运行,包括Windows,Linux和macOS,并在MIT许可下开源。
- Jeedom是一个家庭自动化平台,由开源软件( GPL v2 )组成,用于控制灯光,锁,媒体等。 它包括一个移动应用程序(Android和iOS),并且可以在Linux PC上运行。 该公司还出售集线器,据说该集线器可提供用于设置家庭自动化的即用型解决方案。
- LinuxMCE称自己为媒体和所有电器之间的“数字粘合剂 ”。 它运行在Linux(包括Raspberry Pi)上,已在Pluto开源许可证下发布,可用于家庭安全,电信(VoIP和语音邮件),A / V设备,家庭自动化以及(唯一)播放视频。游戏。
- 与该类别中的其他解决方案一样, OpenNetHome是用于控制灯,警报,设备等的开源软件。它基于Java和Apache Maven,可在Windows,macOS和Linux(包括Raspberry Pi)上运行,并在GPLv3下发布。
- Smarthomatic是一个开源的家庭自动化框架,它专注于硬件设备和软件,而不是用户界面。 它已获得GPLv3的许可,可用于控制灯光,设备和空气湿度,测量环境温度以及记住给植物浇水之类的东西。
现在轮到您了:您已经有一个开源的家庭自动化系统吗? 也许您正在研究创建一个的选项。 您对家庭自动化的新手有什么建议?您会推荐什么系统?
您是否有兴趣阅读更多此类文章? 订阅我们的每周电子邮件通讯 。
开源自动化部署工具