├── README.md ├── cmd-file.example.txt ├── config.ini ├── demo.sh ├── host-file.example.txt ├── main.go └── utils ├── concurrency.go ├── sftp.go ├── ssh.go ├── usage.go └── utils.go /README.md: -------------------------------------------------------------------------------- 1 | # ssgo 2 | ssgo是一个基于SSH协议开发的小工具,面向系统管理员,主要用于在远程主机上执行命令、脚本或传输文件 3 | # 安装 4 | ## 获取源码 5 | ``` bash 6 | go get github.com/JeffreySE/ssgo 7 | ``` 8 | ## 打包下载最新版本 9 | 10 | * **For Windows x64**: 11 | [ssgo.1.0.3.zip](https://github.com/JeffreySE/ssgo/files/2371010/ssgo.1.0.3.zip) 12 | * **For Linux x64**: 13 | [ssgo.1.0.3.tar.gz](https://github.com/JeffreySE/ssgo/files/2371009/ssgo.1.0.3.tar.gz) 14 | 15 | ## 小特性 16 | * 默认并发执行 17 | * 支持单条、多条命令、脚本执行(直接在远程主机执行本地脚本,可以接受脚本参数) 18 | * 对于复杂场景,可以像Ansible那样指定一个仓库主机清单文件,包含主机组,对应主机组的登录用户、密码、端口 19 | * 支持指定主机清单文件(一个包含主机IP地址的文件) 20 | * 支持格式化输出结果,目前支持简单样式(默认)和表格格式、json格式 21 | * 可以在某一个命令后指定`--example`获取该命令的使用案例 22 | * 输出结果包含颜色,兼容Linux & Windows平台 23 | * 可用IP地址支持以下形式: 24 | * 单个IP地址:`192.168.100.2` 25 | * IP地址段:`192.168.100.2-5` 或 `192.168.100.2-192.168.100.5` 都是可以的 26 | * 包含子网掩码的IP地址范围:`192.168.100.0/28`或`192.168.100.0/255.255.255.240` 都是可以的 27 | * 组合形式:`192.168.100.2,192.168.100.3-192.168.100.5,192.168.100.0/29` 只要以英文逗号分隔开即可 28 | * 重复主机检测:当ssgo执行操作时,会从主机清单中检测重复IP地址的存在,防止在主机上进行重复操作 29 | * 支持输出命令执行结果到日志文件 30 | 31 | ### 一点背景 32 | 作为运维人员经常需要和远程主机(好吧 有时候是一大堆)打交道,执行脚本、执行命令、传输文件是三个最基本的诉求了,前期有接触过Ansible 使用已经很方便了,但是不够轻量;也使用Python基于paramiko库编写过小轮子,无奈有些虚机、系统上竟然没有预装paramiko组件,更可恨的是还要解决一堆依赖关系,对系统进行修改。。。 33 | 为了解决这些痛点,重新使用Go语言造了一个轮子 基本满足这些需求,初学Go语言,开发过程中学到了很多,当然啦,代码比较烂,请见谅 34 | 灵感来源:[https://github.com/shanghai-edu/multissh](https://github.com/shanghai-edu/multissh)非常感谢 35 | 36 | ### 使用帮助 37 | ``` bash 38 | λ go run main.go 39 | usage: ssgo [] [ ...] 40 | 41 | A SSH-based command line tool for operating remote hosts. 42 | 43 | Flags: 44 | -h, --help Show context-sensitive help (also try --help-long 45 | and --help-man). 46 | -e, --example Show examples of ssgo's command. 47 | -i, --inventory=INVENTORY For advanced use case, you can specify a host 48 | warehouse .ini file (Default is 'config.ini' file 49 | in current directory.) 50 | -g, --group=GROUP Remote host group name in the inventory file, 51 | which must be used with '-i' or '--inventory' 52 | argument! 53 | --host-file=HOST-FILE A file contains remote host or host range IP 54 | Address.(e.g. 'hosts.example.txt' in current 55 | directory.) 56 | --host-list=HOST-LIST Remote host or host range IP Address. e.g. 57 | 192.168.10.100,192.168.10.101-192.168.10.103,192.168.20.100/28,192.168.30.11-15 58 | -p, --pass=PASS The SSH login password for remote hosts. 59 | -u, --user="root" The SSH login user for remote hosts. default is 60 | 'root' 61 | -P, --port=22 The SSH login port for remote hosts. default is 62 | '22' 63 | -n, --maxExecuteNum=20 Set Maximum concurrent count of hosts. 64 | -o, --output=OUTPUT Output result'log to a file.(Be default if your 65 | input is "log",ssgo will output logs like 66 | "ssgo-%s.log") 67 | -F, --format="simple" For pretty look in terminal,you can format the 68 | result with table,simple,json or other 69 | style.(Default is simple) 70 | --json-raw By default, the json data will be formatted and 71 | output by the console. You can specify the 72 | --json-raw parameter to output raw json 73 | data.(Default is false) 74 | -w, --maxTableCellWidth=40 For pretty look,you can set the printed table's 75 | max cell width in terminal.(Default is 40) 76 | -v, --version Show application version. 77 | 78 | Commands: 79 | help [...] 80 | Show help. 81 | 82 | list 83 | List available remote hosts from your input. 84 | 85 | run [] 86 | Run commands on remote hosts. 87 | 88 | copy --action=ACTION --src=SRC [] 89 | Transfer files between local machine and remote hosts. 90 | ``` 91 | ### 核心组件 92 | * **ssgo list** 93 | 用于从用户输入的参数中获取可用IP地址清单(好吧,这个功能比较鸡肋) 94 | * **ssgo run** 95 | 用于在远程主机上执行命令或脚本 96 | * **ssgo copy** 97 | 用于在本地和远程主机之间传输文件 98 | 99 | 100 | ### 文件示例 101 | **config.ini文件** 102 | ``` ini 103 | # 2. ssgo tools use ini config file for advanced usage 104 | # 3. Important Tips: you can't use 'all' as a host group name, cause 'all' will be identified as all host in your config.ini file. 105 | 106 | [dc] 107 | user = root 108 | pass = root 109 | port = 22 110 | hosts = 192.168.100.1 111 | 112 | [web] 113 | user = root 114 | pass = root 115 | port = 22 116 | hosts = 192.168.100.2,192.168.100.3-192.168.100.4,192.168.100.8 117 | 118 | [db] 119 | user = root 120 | pass = root 121 | port = 22 122 | hosts = 192.168.100.5-6 123 | 124 | [docker] 125 | user = root 126 | pass = root 127 | port = 22 128 | hosts = """ 129 | 192.168.100.7 130 | 192.168.100.9 131 | 192.168.100.10 132 | 192.168.100.1-192.168.100.3 133 | """ 134 | ``` 135 | **host-file.example.txt文件** 136 | **备注**:如果某一个IP地址开头包含了“#”ssgo默认会忽略它 137 | 138 | ``` text 139 | 192.168.100.1,192.168.100.2-192.168.100.4 140 | #192.168.100.5 #host-list strings with "#" prefix will be ignored! 141 | 192.168.100.6 142 | 192.168.100.7-10 143 | ``` 144 | **cmd-file.example.txt文件** 145 | **备注**:如果该文件内某一行命令执行失败,在该主机上后续命令不会继续被执行 146 | 147 | ``` text 148 | echo -e "***************************************" 149 | hostname 150 | ssh -V 151 | df -Th 152 | cd /opt/ 153 | ls -l 154 | cat /etc/rsyslog.conf | grep "#kern.*" 155 | cat /etc/passwd | awk -F ':' '{print $1}' 156 | echo -e "***************************************" 157 | ``` 158 | **bash.sh文件** 159 | **备注**:可以在远程主机直接执行本地Shell脚本,也可以接受脚本参数 160 | ``` bash 161 | #!/bin/bash 162 | 163 | echo "HostName:$(hostname)" 164 | echo "I am a test Shell script running on the remote server!" 165 | echo "Script Args \$1: $1" 166 | echo "Script Args \$2: $2" 167 | echo "What happens if an exception occurs during script execution?" 168 | ls ThisFileIsNotExist 169 | ``` 170 | 171 | # 使用示例 172 | ## ssgo run 执行命令 173 | ### `--host-list` 参数相关 174 | **`ssgo run --host-list`单个命令** 175 | ``` bash 176 | ➜ ./ssgo run --host-list 192.168.100.1 -u root -p root -c "hostname" 177 | 178 | Tips:Process running start: 2018-04-20 17:13:25 179 | >>> No.1, Host:192.168.100.1, Status:success, Results: 180 | 42f432e85ab6 181 | 182 | Tips: Process running done. 183 | End Time: 2018-04-20 17:13:25 184 | Cost Time: 128.218018ms 185 | Total Hosts Running: 1(Success) + 0(Failed) = 1(Total) 186 | ``` 187 | 188 | **`ssgo run --host-list` 多个主机 执行单个命令,以表格样式输出结果** 189 | **备注** : -F, --format 格式化命令执行后的输出结果 190 | ``` bash 191 | ➜ ./ssgo run --host-list 192.168.100.1,192.168.100.2-4 -u root -p root -c "hostname" -F table 192 | 193 | Tips:Process running start: 2018-04-20 17:15:11 194 | INFO: Success hosts 195 | +---+---------------+---------+--------------+ 196 | | # | Host | Status | Result | 197 | +---+---------------+---------+--------------+ 198 | | 1 | 192.168.100.1 | success | 42f432e85ab6 | 199 | +---+---------------+---------+--------------+ 200 | | 2 | 192.168.100.2 | success | 39beb225f669 | 201 | +---+---------------+---------+--------------+ 202 | | 3 | 192.168.100.3 | success | dfc9aed2f3ce | 203 | +---+---------------+---------+--------------+ 204 | | 4 | 192.168.100.4 | success | 8080c8c88026 | 205 | +---+---------------+---------+--------------+ 206 | 207 | Tips: Process running done. 208 | End Time: 2018-04-20 17:15:11 209 | Cost Time: 129.434302ms 210 | Total Hosts Running: 4(Success) + 0(Failed) = 4(Total) 211 | ``` 212 | 213 | **`ssgo run --host-list` 多个主机 执行多个命令** 214 | **备注**:执行多个命令时,就不建议使用表格样式输出结果了,那样会不好看的,你懂的 215 | ``` bash 216 | ➜ ./ssgo run --host-list 192.168.100.1,192.168.100.2-4 -u root -p root -c "hostname;pwd;date" 217 | 218 | Tips:Process running start: 2018-04-20 17:17:33 219 | >>> No.1, Host:192.168.100.1, Status:success, Results: 220 | 42f432e85ab6 221 | /root 222 | Fri Apr 20 09:17:34 UTC 2018 223 | 224 | >>> No.2, Host:192.168.100.2, Status:success, Results: 225 | 39beb225f669 226 | /root 227 | Fri Apr 20 09:17:34 UTC 2018 228 | 229 | >>> No.3, Host:192.168.100.3, Status:success, Results: 230 | dfc9aed2f3ce 231 | /root 232 | Fri Apr 20 09:17:34 UTC 2018 233 | 234 | >>> No.4, Host:192.168.100.4, Status:success, Results: 235 | 8080c8c88026 236 | /root 237 | Fri Apr 20 09:17:34 UTC 2018 238 | 239 | Tips: Process running done. 240 | End Time: 2018-04-20 17:17:34 241 | Cost Time: 301.24909ms 242 | Total Hosts Running: 4(Success) + 0(Failed) = 4(Total) 243 | ``` 244 | 245 | ### ` --host-file` 参数相关 246 | **备注**:`--host-file` 参数只需指定一个主机清单文件即可,表格样式会比较容易比对输出结果,对吧 247 | 248 | ``` bash 249 | ➜ ./ssgo run --host-file host-file.example.txt -u root -p root -c "date" -F table 250 | Tips:Process running start: 2018-04-20 17:21:15 251 | INFO: Success hosts 252 | +---+----------------+---------+------------------------------+ 253 | | # | Host | Status | Result | 254 | +---+----------------+---------+------------------------------+ 255 | | 1 | 192.168.100.1 | success | Fri Apr 20 09:21:15 UTC 2018 | 256 | +---+----------------+---------+------------------------------+ 257 | | 2 | 192.168.100.10 | success | Fri Apr 20 09:21:15 UTC 2018 | 258 | +---+----------------+---------+------------------------------+ 259 | | 3 | 192.168.100.2 | success | Fri Apr 20 09:21:15 UTC 2018 | 260 | +---+----------------+---------+------------------------------+ 261 | | 4 | 192.168.100.3 | success | Fri Apr 20 09:21:15 UTC 2018 | 262 | +---+----------------+---------+------------------------------+ 263 | | 5 | 192.168.100.4 | success | Fri Apr 20 09:21:15 UTC 2018 | 264 | +---+----------------+---------+------------------------------+ 265 | | 6 | 192.168.100.6 | success | Fri Apr 20 09:21:15 UTC 2018 | 266 | +---+----------------+---------+------------------------------+ 267 | | 7 | 192.168.100.7 | success | Fri Apr 20 09:21:15 UTC 2018 | 268 | +---+----------------+---------+------------------------------+ 269 | | 8 | 192.168.100.8 | success | Fri Apr 20 09:21:15 UTC 2018 | 270 | +---+----------------+---------+------------------------------+ 271 | | 9 | 192.168.100.9 | success | Fri Apr 20 09:21:15 UTC 2018 | 272 | +---+----------------+---------+------------------------------+ 273 | 274 | Tips: Process running done. 275 | End Time: 2018-04-20 17:21:15 276 | Cost Time: 172.372373ms 277 | Total Hosts Running: 9(Success) + 0(Failed) = 9(Total) 278 | ``` 279 | 280 | ### `-i, --inventory`和 `-g, --group`参数相关 281 | **备注**:`-i, --inventory`和 `-g, --group`参数需要组合使用,`-i`指定config.ini主机仓库文件,`-g`指定主机组名称,如果是`-g`的参数为`all`,则该主机仓库中所有的主机会被识别,用来执行操作 282 | ``` bash 283 | ➜ ./ssgo run -i config.ini -g docker -c "date" -F table 284 | 285 | 286 | Tips:Process running start: 2018-04-20 17:25:21 287 | INFO: Success hosts 288 | +---+----------------+---------+------------------------------+ 289 | | # | Host | Status | Result | 290 | +---+----------------+---------+------------------------------+ 291 | | 1 | 192.168.100.1 | success | Fri Apr 20 09:25:21 UTC 2018 | 292 | +---+----------------+---------+------------------------------+ 293 | | 2 | 192.168.100.10 | success | Fri Apr 20 09:25:21 UTC 2018 | 294 | +---+----------------+---------+------------------------------+ 295 | | 3 | 192.168.100.2 | success | Fri Apr 20 09:25:21 UTC 2018 | 296 | +---+----------------+---------+------------------------------+ 297 | | 4 | 192.168.100.3 | success | Fri Apr 20 09:25:21 UTC 2018 | 298 | +---+----------------+---------+------------------------------+ 299 | | 5 | 192.168.100.7 | success | Fri Apr 20 09:25:21 UTC 2018 | 300 | +---+----------------+---------+------------------------------+ 301 | | 6 | 192.168.100.9 | success | Fri Apr 20 09:25:21 UTC 2018 | 302 | +---+----------------+---------+------------------------------+ 303 | 304 | Tips: Process running done. 305 | End Time: 2018-04-20 17:25:21 306 | Cost Time: 134.046557ms 307 | Total Hosts Running: 6(Success) + 0(Failed) = 6(Total) 308 | ``` 309 | 310 | ### 使用ssgo run命令在远程主机执行本地脚本 311 | **备注**:`ssgo run`的`-s,--script`命令用于指定要执行的本地脚本路径,`-a,--args`参数可选,用于指定该脚本的参数,建议执行脚本时,不再指定-F table以表格输出结果,因输出内容包含多余换行标识会导致输出表格内容错乱 312 | 313 | ``` bash 314 | ➜ ./ssgo run -i config.ini -g web -s demo.sh -a "tiger rabbit" 315 | >>> Group Name: [web] 316 | Tips:Process running start: 2018-04-27 14:27:56 317 | >>> No.1, Host:192.168.100.2, Status:failed, Results: 318 | HostName:39beb225f669 319 | I am a test Shell script running on the remote server! 320 | Script Args $1: tiger 321 | Script Args $2: rabbit 322 | What happens if an exception occurs during script execution? 323 | ls: cannot access 'ThisFileIsNotExist': No such file or directory 324 | ERROR: while running script (demo.sh) on host 192.168.100.2, an error occured Process exited with status 2 325 | 326 | >>> No.2, Host:192.168.100.3, Status:failed, Results: 327 | HostName:dfc9aed2f3ce 328 | I am a test Shell script running on the remote server! 329 | Script Args $1: tiger 330 | Script Args $2: rabbit 331 | What happens if an exception occurs during script execution? 332 | ls: cannot access 'ThisFileIsNotExist': No such file or directory 333 | ERROR: while running script (demo.sh) on host 192.168.100.3, an error occured Process exited with status 2 334 | 335 | >>> No.3, Host:192.168.100.4, Status:failed, Results: 336 | HostName:8080c8c88026 337 | I am a test Shell script running on the remote server! 338 | Script Args $1: tiger 339 | Script Args $2: rabbit 340 | What happens if an exception occurs during script execution? 341 | ls: cannot access 'ThisFileIsNotExist': No such file or directory 342 | ERROR: while running script (demo.sh) on host 192.168.100.4, an error occured Process exited with status 2 343 | 344 | >>> No.4, Host:192.168.100.8, Status:failed, Results: 345 | HostName:b1bda3a80a08 346 | I am a test Shell script running on the remote server! 347 | Script Args $1: tiger 348 | Script Args $2: rabbit 349 | What happens if an exception occurs during script execution? 350 | ls: cannot access 'ThisFileIsNotExist': No such file or directory 351 | ERROR: while running script (demo.sh) on host 192.168.100.8, an error occured Process exited with status 2 352 | 353 | WARNING: Failed hosts, please confirm! 354 | +---+---------------+--------+ 355 | | # | Host | Status | 356 | +---+---------------+--------+ 357 | | 1 | 192.168.100.2 | failed | 358 | +---+---------------+--------+ 359 | | 2 | 192.168.100.3 | failed | 360 | +---+---------------+--------+ 361 | | 3 | 192.168.100.4 | failed | 362 | +---+---------------+--------+ 363 | | 4 | 192.168.100.8 | failed | 364 | +---+---------------+--------+ 365 | 366 | Tips: Process running done. 367 | Start Time: 2018-04-27 14:27:56 368 | End Time: 2018-04-27 14:27:56 369 | Cost Time: 701.917888ms 370 | Total Hosts Running: 0(Success) + 4(Failed) = 4(Total) 371 | 372 | ``` 373 | 374 | ## `ssgo copy`上传文件 375 | **备注**: 376 | 377 | * `ssgo copy`命令下载文件需要制定`-a` 或`--action` 参数为`upload` 378 | * 当进行上传或下载操作时,当`-d, --dst`的参数为`""`空白时,默认文件将会被上传或下载至本地或远程主机的当前工作目录 379 | * 示例:向远程主机192.168.100.1,192.168.100.2,192.168.100.3,192.168.100.4上上传本地demo.sh文件,并已表格返回命令执行结果 380 | 381 | ``` bash 382 | ➜ ./ssgo copy -a upload --host-list 192.168.100.1-4 -u root -p root -s demo.sh -d "/root/" -F table 383 | Tips:Process running start: 2018-04-27 15:51:09 384 | INFO: Success hosts 385 | +---+---------------+---------+------------+-----------------+--------------------+ 386 | | # | Host | Status | SourcePath | DestinationPath | Result | 387 | +---+---------------+---------+------------+-----------------+--------------------+ 388 | | 1 | 192.168.100.1 | success | demo.sh | /root/ | Upload finished!:) | 389 | +---+---------------+---------+------------+-----------------+--------------------+ 390 | | 2 | 192.168.100.2 | success | demo.sh | /root/ | Upload finished!:) | 391 | +---+---------------+---------+------------+-----------------+--------------------+ 392 | | 3 | 192.168.100.3 | success | demo.sh | /root/ | Upload finished!:) | 393 | +---+---------------+---------+------------+-----------------+--------------------+ 394 | | 4 | 192.168.100.4 | success | demo.sh | /root/ | Upload finished!:) | 395 | +---+---------------+---------+------------+-----------------+--------------------+ 396 | 397 | Tips: Process running done. 398 | Start Time: 2018-04-27 15:51:09 399 | End Time: 2018-04-27 15:51:09 400 | Cost Time: 133.040933ms 401 | Total Hosts Running: 4(Success) + 0(Failed) = 4(Total) 402 | 403 | ``` 404 | 405 | ## `ssgo copy`下载文件 406 | 407 | **备注**: 408 | 409 | * `ssgo copy`命令下载文件需要制定`-a` 或`--action` 参数为`download` 410 | * 当进行上传或下载操作时,当`-d, --dst`的参数为`""`空白时,默认文件将会被上传或下载至本地或远程主机的当前工作目录 411 | * **注意:**ssgo默认所有从远程主机下载的文件下载到本地目录后会在原文件名上添加对应文件所在主机IP地址前缀 412 | 413 | ``` bash 414 | ➜ ./ssgo copy -a download -i config.ini -g docker -s "demo.sh" -d /tmp/temp/ -F table 415 | 416 | Tips:Process running start: 2018-04-23 10:08:27 417 | INFO: Success hosts 418 | +---+----------------+---------+-------------+------------------+----------------------+ 419 | | # | Host | Status | Source Path | Destination Path | Result | 420 | +---+----------------+---------+-------------+------------------+----------------------+ 421 | | 1 | 192.168.100.1 | success | demo.sh | /tmp/temp/ | Download finished!:) | 422 | +---+----------------+---------+-------------+------------------+----------------------+ 423 | | 2 | 192.168.100.10 | success | demo.sh | /tmp/temp/ | Download finished!:) | 424 | +---+----------------+---------+-------------+------------------+----------------------+ 425 | | 3 | 192.168.100.2 | success | demo.sh | /tmp/temp/ | Download finished!:) | 426 | +---+----------------+---------+-------------+------------------+----------------------+ 427 | | 4 | 192.168.100.3 | success | demo.sh | /tmp/temp/ | Download finished!:) | 428 | +---+----------------+---------+-------------+------------------+----------------------+ 429 | | 5 | 192.168.100.7 | success | demo.sh | /tmp/temp/ | Download finished!:) | 430 | +---+----------------+---------+-------------+------------------+----------------------+ 431 | | 6 | 192.168.100.9 | success | demo.sh | /tmp/temp/ | Download finished!:) | 432 | +---+----------------+---------+-------------+------------------+----------------------+ 433 | 434 | Tips: Process running done. 435 | End Time: 2018-04-23 10:08:27 436 | Cost Time: 142.740437ms 437 | Total Hosts Running: 6(Success) + 0(Failed) = 6(Total) 438 | 439 | ``` 440 | **下载后的文件** 441 | 442 | ``` bash 443 | ➜ ll /tmp/temp/ 444 | 总用量 24 445 | -rw-r--r-- 1 root root 1024 4月 23 10:08 192.168.100.10_demo.sh 446 | -rw-r--r-- 1 root root 1024 4月 23 10:08 192.168.100.1_demo.sh 447 | -rw-r--r-- 1 root root 1024 4月 23 10:08 192.168.100.2_demo.sh 448 | -rw-r--r-- 1 root root 1024 4月 23 10:08 192.168.100.3_demo.sh 449 | -rw-r--r-- 1 root root 1024 4月 23 10:08 192.168.100.7_demo.sh 450 | -rw-r--r-- 1 root root 1024 4月 23 10:08 192.168.100.9_demo.sh 451 | ``` 452 | 453 | 454 | ## `ssgo list`获取可用IP地址清单 455 | * 从`--host-list`获取可用IP地址清单(友好提示是否过滤重复主机) 456 | ``` shell 457 | ➜ ./ssgo list --host-list 192.168.100.1,192.168.100.3-5,192.168.100.5,192.168.100.1 458 | Duplicate IP Address Found, input 'yes or y' to remove duplicate IP Address, Input 'no or n' will keep duplicate IP Address, 459 | nothing will do by default! (y/n) y 460 | >>> Available Hosts: 461 | +---+---------------+ 462 | | # | Host | 463 | +---+---------------+ 464 | | 1 | 192.168.100.1 | 465 | +---+---------------+ 466 | | 2 | 192.168.100.3 | 467 | +---+---------------+ 468 | | 3 | 192.168.100.4 | 469 | +---+---------------+ 470 | | 4 | 192.168.100.5 | 471 | +---+---------------+ 472 | 473 | ``` 474 | 475 | * 从`--host-file`获取可用IP地址清单 476 | ``` bash 477 | ➜ ./ssgo list --host-file host-file.example.txt 478 | ``` 479 | * 从config.ini文件获取可用IP地址清单 480 | **示例:** 获取config.ini主机仓库文件中,主机群组为docker的主机清单 481 | ``` shell 482 | ➜ ./ssgo list -i config.ini -g docker 483 | 484 | >>> Group Name: [docker] 485 | >>> Hosts From: 486 | 192.168.100.7 487 | 192.168.100.9 488 | 192.168.100.10 489 | 192.168.100.1-192.168.100.3 490 | 491 | >>> Available Hosts: 492 | +---+----------------+------------+ 493 | | # | Host | Group Name | 494 | +---+----------------+------------+ 495 | | 1 | 192.168.100.1 | docker | 496 | +---+----------------+------------+ 497 | | 2 | 192.168.100.10 | docker | 498 | +---+----------------+------------+ 499 | | 3 | 192.168.100.2 | docker | 500 | +---+----------------+------------+ 501 | | 4 | 192.168.100.3 | docker | 502 | +---+----------------+------------+ 503 | | 5 | 192.168.100.7 | docker | 504 | +---+----------------+------------+ 505 | | 6 | 192.168.100.9 | docker | 506 | +---+----------------+------------+ 507 | 508 | ``` 509 | 510 | # 其他 511 | ## 使用json格式化命令执行结果 512 | **示例:** 将本地demo.sh文件上传至config.ini主机仓库文件主机群组为docker的主机`/tmp/`目录下,并将结果格式化为Json格式 513 | ``` bash 514 | ➜ ./ssgo copy -a upload -i config.ini -g docker -s demo.sh -d "/tmp/" -F json 515 | [ 516 | { 517 | "StartTime": "2018-04-27 14:33:34", 518 | "HostGroup": "docker", 519 | "SuccessHosts": [ 520 | { 521 | "Host": "192.168.100.1", 522 | "Status": "success", 523 | "SourcePath": "demo.sh", 524 | "DestinationPath": "/tmp/", 525 | "Result": "Upload finished!:)" 526 | }, 527 | { 528 | "Host": "192.168.100.10", 529 | "Status": "success", 530 | "SourcePath": "demo.sh", 531 | "DestinationPath": "/tmp/", 532 | "Result": "Upload finished!:)" 533 | }, 534 | { 535 | "Host": "192.168.100.2", 536 | "Status": "success", 537 | "SourcePath": "demo.sh", 538 | "DestinationPath": "/tmp/", 539 | "Result": "Upload finished!:)" 540 | }, 541 | { 542 | "Host": "192.168.100.3", 543 | "Status": "success", 544 | "SourcePath": "demo.sh", 545 | "DestinationPath": "/tmp/", 546 | "Result": "Upload finished!:)" 547 | }, 548 | { 549 | "Host": "192.168.100.7", 550 | "Status": "success", 551 | "SourcePath": "demo.sh", 552 | "DestinationPath": "/tmp/", 553 | "Result": "Upload finished!:)" 554 | }, 555 | { 556 | "Host": "192.168.100.9", 557 | "Status": "success", 558 | "SourcePath": "demo.sh", 559 | "DestinationPath": "/tmp/", 560 | "Result": "Upload finished!:)" 561 | } 562 | ], 563 | "ErrorHosts": null, 564 | "EndTime": "2018-04-27 14:33:34", 565 | "CostTime": "129.689291ms", 566 | "TotalHostsInfo": "6(Success) + 0(Failed) = 6(Total)" 567 | } 568 | ] 569 | ``` 570 | 571 | ### 输出命令执行结果为原始json数据 572 | **示例:** 查询192.168.100.1,192.168.100.2,192.168.100.3,192.168.100.4主机的时间,并将原始json结果数据输出 573 | ``` bash 574 | ➜ ./ssgo run --host-list 192.168.100.1-4 -u root -p root -c date -F json --json-raw 575 | [{"StartTime":"2018-04-27 14:44:36","HostGroup":"from list (192.168.100.1-4)","SuccessHosts":[{"Host":"192.168.100.1","Status":"success","Result":"Fri Apr 27 06:44:36 UTC 2018"},{"Host":"192.168.100.2","Status":"success","Result":"Fri Apr 27 06:44:36 UTC 2018"},{"Host":"192.168.100.3","Status":"success","Result":"Fri Apr 27 06:44:36 UTC 2018"},{"Host":"192.168.100.4","Status":"success","Result":"Fri Apr 27 06:44:36 UTC 2018"}],"ErrorHosts":null,"EndTime":"2018-04-27 14:44:36","CostTime":"98.982031ms","TotalHostsInfo":"4(Success) + 0(Failed) = 4(Total)"}] 576 | ``` 577 | 578 | 579 | ## 输出日志文件 580 | ## 关于`-n, --maxExecuteNum`参数 581 | 考虑到并发控制,当主机比较多的情况下,可以适当提高并发数,默认为20 582 | ## 关于`-w, --maxTableCellWidth`参数 583 | `-w, --maxTableCellWidth` 用于美化表格输出列的最大宽度,当发现表格输出列的最大宽度不足以完美存放单行文字时,可以适当调整该数值,默认大小为40 584 | 585 | # License 586 | Apache License 2.0 587 | -------------------------------------------------------------------------------- /cmd-file.example.txt: -------------------------------------------------------------------------------- 1 | echo -e "***************************************" 2 | hostname 3 | ssh -V 4 | df -Th 5 | cd /opt/ 6 | ls -l 7 | cat /etc/rsyslog.conf | grep "#kern.*" 8 | cat /etc/passwd | awk -F ':' '{print $1}' 9 | echo -e "***************************************" -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | # ssgo use ini config file for advanced usage 2 | # Important Tips: you can't use 'all' as a host group name, cause 'all' will be identified as all host in your config.ini file. 3 | 4 | [dc] 5 | user = root 6 | pass = root 7 | port = 22 8 | hosts = 192.168.100.1 9 | 10 | [web] 11 | user = root 12 | pass = root 13 | port = 22 14 | hosts = 192.168.100.2,192.168.100.3-192.168.100.4,192.168.100.8 15 | 16 | [db] 17 | user = root 18 | pass = root 19 | port = 22 20 | hosts = 192.168.100.5-6 21 | 22 | [docker] 23 | user = root 24 | pass = root 25 | port = 22 26 | hosts = """ 27 | 192.168.100.7 28 | 192.168.100.9 29 | 192.168.100.10 30 | 192.168.100.1-192.168.100.3 31 | """ 32 | -------------------------------------------------------------------------------- /demo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "HostName:$(hostname)" 4 | echo "I am a test Shell script running on the remote server!" 5 | echo "Script Args \$1: $1" 6 | echo "Script Args \$2: $2" 7 | echo "What happens if an exception occurs during script execution?" 8 | ls ThisFileIsNotExist -------------------------------------------------------------------------------- /host-file.example.txt: -------------------------------------------------------------------------------- 1 | 192.168.100.1,192.168.100.2-192.168.100.4 2 | #192.168.100.5 #host-list strings with "#" prefix will be ignored! 3 | 192.168.100.6 4 | 192.168.100.7-10 5 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/JeffreySE/ssgo/utils" 6 | "github.com/go-ini/ini" 7 | "gopkg.in/alecthomas/kingpin.v2" 8 | "os" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | var ( 14 | app = kingpin.New("ssgo", "A SSH-based command line tool for operating remote hosts.") 15 | _ = app.HelpFlag.Short('h') 16 | example = app.Flag("example", "Show examples of ssgo's command.").Short('e').Default("false").Bool() 17 | inventory = app.Flag("inventory", "For advanced use case, you can specify a host warehouse .ini file (Default is 'config.ini' file in current directory.)").Short('i').ExistingFile() 18 | group = app.Flag("group", "Remote host group name in the inventory file, which must be used with '-i' or '--inventory' argument!").Short('g').String() 19 | hostFile = app.Flag("host-file", "A file contains remote host or host range IP Address.(e.g. 'hosts.example.txt' in current directory.)").ExistingFile() 20 | hostList = app.Flag("host-list", "Remote host or host range IP Address. e.g. 192.168.10.100,192.168.10.101-192.168.10.103,192.168.20.100/28,192.168.30.11-15").String() 21 | password = app.Flag("pass", "The SSH login password for remote hosts.").Short('p').String() 22 | user = app.Flag("user", "The SSH login user for remote hosts. default is 'root'").Short('u').Default("root").String() 23 | port = app.Flag("port", "The SSH login port for remote hosts. default is '22'").Short('P').Default("22").Int() 24 | //timeout = app.Flag("timeout", "Set ssh connection timeout.").Short('t').Default("10s").Duration() 25 | maxExecuteNum = app.Flag("maxExecuteNum", "Set Maximum concurrent count of hosts.").Short('n').Default("20").Int() 26 | output = app.Flag("output", "Output result'log to a file.(Be default if your input is \"log\",ssgo will output logs like \"ssgo-%s.log\")").Short('o').String() 27 | formatMode = app.Flag("format", "For pretty look in terminal,you can format the result with table,simple,json or other style.(Default is simple)").Short('F').Default("simple").String() 28 | jsonRaw = app.Flag("json-raw", "By default, the json data will be formatted and output by the console. You can specify the --json-raw parameter to output raw json data.(Default is false)").Default("false").Bool() 29 | maxTableCellWidth = app.Flag("maxTableCellWidth", "For pretty look,you can set the printed table's max cell width in terminal.(Default is 40)").Short('w').Default("40").Int() 30 | 31 | list = app.Command("list", "List available remote hosts from your input. ") 32 | 33 | run = app.Command("run", "Run commands on remote hosts.") 34 | scriptFile = run.Flag("script", "Want execute script on remote hosts ? Just specify the path of your script.").PlaceHolder("shell-script.sh").Short('s').ExistingFile() 35 | scriptArgs = run.Flag("args", "Shell script arguments,use this flag with --script if you need.").Short('a').Default("").String() 36 | cmdArgs = run.Flag("cmd", "Specify the commands or command file you want execute on remote hosts. By default will run 'echo pong' command if nothing is specified!").Short('c').Default("").String() 37 | 38 | sshCopy = app.Command("copy", "Transfer files between local machine and remote hosts.") 39 | copyAction = sshCopy.Flag("action", "ssgo's copy command do upload or download operations(only accept \"upload\" or \"download\" action)").Required().Short('a').String() 40 | sourcePath = sshCopy.Flag("src", "Source file or directory path on the local machine or remote hosts").Short('s').Required().String() 41 | destinationPath = sshCopy.Flag("dst", "Destination file or directory path on the remote host or local machine.").Short('d').Default("").String() 42 | ) 43 | 44 | var ( 45 | allResultLogs []utils.ResultLogs 46 | ) 47 | 48 | func main() { 49 | app.Version("1.0.3") 50 | app.VersionFlag.Short('v') 51 | switch kingpin.MustParse(app.Parse(os.Args[1:])) { 52 | case list.FullCommand(): 53 | if *example != false { 54 | utils.ShowListCommandUsage() 55 | } else if *inventory != "" && *group != "" { 56 | cfg, err := utils.Cfg(*inventory) 57 | if err != nil { 58 | utils.ColorPrint("ERROR", ">>>", "ERROR: ", err, "\n") 59 | } 60 | if *group == "all" { 61 | //get all hosts in config.ini file 62 | for _, s := range cfg.Sections() { 63 | if s.Name() == "DEFAULT" { 64 | continue 65 | } 66 | listCommandAction(s) 67 | } 68 | } else { 69 | s, _ := cfg.GetSection(*group) 70 | listCommandAction(s) 71 | } 72 | return 73 | } else if *hostFile != "" { 74 | hosts, err := utils.GetAvailableIPFromFile(*hostFile) 75 | if err != nil { 76 | utils.ColorPrint("ERROR", "", "ERROR:", err, "\n") 77 | return 78 | } 79 | utils.PrintListHosts(hosts, *maxTableCellWidth) 80 | return 81 | } else if *hostList != "" { 82 | hosts, err := utils.GetAvailableIP(*hostList) 83 | if err != nil { 84 | utils.ColorPrint("ERROR", "", "ERROR:", err, "\n") 85 | } 86 | utils.PrintListHosts(hosts, *maxTableCellWidth) 87 | return 88 | } else { 89 | utils.ShowListCommandUsage() 90 | } 91 | case run.FullCommand(): 92 | if *example != false { 93 | utils.ShowRunCommandUsage() 94 | } else if *inventory != "" && *group != "" { 95 | var hosts []string 96 | cfg, err := utils.Cfg(*inventory) 97 | if err != nil { 98 | utils.ColorPrint("ERROR", ">>>", "ERROR: ", err, "\n") 99 | } 100 | if *group == "all" { 101 | //get all hosts in config.ini file 102 | isFinished := false 103 | for index, s := range cfg.Sections() { 104 | if s.Name() == "DEFAULT" { 105 | continue 106 | } 107 | 108 | if s.HasKey("hosts") { 109 | h, _ := s.GetKey("hosts") 110 | resHosts, err := utils.GetAvailableIPFromMultiLines(h.String()) 111 | if err != nil { 112 | utils.ColorPrint("ERROR", ">>>", "ERROR: ", err, "\n") 113 | return 114 | } 115 | hosts = resHosts 116 | userName := s.Key("user").String() 117 | password := s.Key("pass").String() 118 | port := s.Key("port").MustInt() 119 | 120 | cmds, err := checkCommandArgs() 121 | if err != nil { 122 | utils.ColorPrint("ERROR", "", "ERROR:", err, "\n") 123 | return 124 | } 125 | if *formatMode != "json" { 126 | utils.ColorPrint("INFO", ">>> Group Name: ", "["+s.Name()+"]\n") 127 | } 128 | if index == len(cfg.Sections())-1 { 129 | isFinished = true 130 | } 131 | if *scriptFile != "" { 132 | doSSHCommands(userName, password, fmt.Sprintf("from hostgroup %s@%s file", s.Name(), *inventory), "", port, hosts, []string{}, *scriptFile, *scriptArgs, "script", isFinished) 133 | } 134 | if *cmdArgs != "" { 135 | doSSHCommands(userName, password, fmt.Sprintf("from hostgroup %s@%s file", s.Name(), *inventory), "", port, hosts, cmds, "", "", "cmd", isFinished) 136 | } 137 | 138 | } 139 | } 140 | return 141 | } else { 142 | s, _ := cfg.GetSection(*group) 143 | if s.HasKey("hosts") { 144 | h, _ := s.GetKey("hosts") 145 | resHosts, err := utils.GetAvailableIPFromMultiLines(h.String()) 146 | if err != nil { 147 | utils.ColorPrint("ERROR", ">>>", "ERROR: ", err, "\n") 148 | return 149 | } 150 | hosts = resHosts 151 | userName := s.Key("user").String() 152 | password := s.Key("pass").String() 153 | port := s.Key("port").MustInt() 154 | cmds, err := checkCommandArgs() 155 | if err != nil { 156 | utils.ColorPrint("ERROR", "", "ERROR:", err, "\n") 157 | return 158 | } 159 | if *formatMode != "json" { 160 | utils.ColorPrint("INFO", ">>> Group Name: ", "["+s.Name()+"]\n") 161 | } 162 | if *scriptFile != "" { 163 | doSSHCommands(userName, password, fmt.Sprintf("from hostgroup %s@%s file", s.Name(), *inventory), "", port, hosts, []string{}, *scriptFile, *scriptArgs, "script", true) 164 | } 165 | if *cmdArgs != "" { 166 | doSSHCommands(userName, password, fmt.Sprintf("from hostgroup %s@%s file", s.Name(), *inventory), "", port, hosts, cmds, "", "", "cmd", true) 167 | } 168 | } 169 | } 170 | return 171 | } else if *hostFile != "" { 172 | hosts, err := utils.GetAvailableIPFromFile(*hostFile) 173 | if err != nil { 174 | utils.ColorPrint("ERROR", "", "ERROR:", err, "\n") 175 | return 176 | } 177 | cmds, err := checkCommandArgs() 178 | if err != nil { 179 | utils.ColorPrint("ERROR", "", "ERROR:", err, "\n") 180 | return 181 | } 182 | if *scriptFile != "" { 183 | doSSHCommands(*user, *password, fmt.Sprintf("from file (%s)", *hostFile), "", *port, hosts, []string{}, *scriptFile, *scriptArgs, "script", true) 184 | return 185 | } 186 | if *cmdArgs != "" { 187 | doSSHCommands(*user, *password, fmt.Sprintf("from file (%s)", *hostFile), "", *port, hosts, cmds, "", "", "cmd", true) 188 | } 189 | return 190 | } else if *hostList != "" { 191 | hosts, err := utils.GetAvailableIP(*hostList) 192 | if err != nil { 193 | utils.ColorPrint("ERROR", "", "ERROR:", err, "\n") 194 | return 195 | } 196 | cmds, err := checkCommandArgs() 197 | if err != nil { 198 | utils.ColorPrint("ERROR", "", "ERROR:", err, "\n") 199 | return 200 | } 201 | if *scriptFile != "" { 202 | doSSHCommands(*user, *password, fmt.Sprintf("from list (%s)", *hostList), "", *port, hosts, []string{}, *scriptFile, *scriptArgs, "script", true) 203 | return 204 | } 205 | if *cmdArgs != "" { 206 | doSSHCommands(*user, *password, fmt.Sprintf("from list (%s)", *hostList), "", *port, hosts, cmds, "", "", "cmd", true) 207 | } 208 | return 209 | } else { 210 | utils.ShowRunCommandUsage() 211 | } 212 | case sshCopy.FullCommand(): 213 | if *example != false { 214 | utils.ShowFileTransferUsage() 215 | } else if *inventory != "" && *group != "" { 216 | var hosts []string 217 | cfg, err := utils.Cfg(*inventory) 218 | if err != nil { 219 | utils.ColorPrint("ERROR", ">>>", "ERROR: ", err, "\n") 220 | return 221 | } 222 | if *group == "all" { 223 | //get all hosts in config.ini file 224 | isFinished := false 225 | for index, s := range cfg.Sections() { 226 | if s.Name() == "DEFAULT" { 227 | continue 228 | } 229 | if s.HasKey("hosts") { 230 | h, _ := s.GetKey("hosts") 231 | resHosts, err := utils.GetAvailableIPFromMultiLines(h.String()) 232 | if err != nil { 233 | utils.ColorPrint("ERROR", ">>>", "ERROR: ", err, "\n") 234 | } 235 | hosts = resHosts 236 | userName := s.Key("user").String() 237 | password := s.Key("pass").String() 238 | port := s.Key("port").MustInt() 239 | utils.ColorPrint("INFO", ">>> Group Name: ", "["+s.Name()+"]\n") 240 | if index == len(cfg.Sections())-1 { 241 | isFinished = true 242 | } 243 | if *copyAction == "upload" { 244 | doSFTPFileTransfer(userName, password, s.Name(), "", port, hosts, *sourcePath, *destinationPath, "upload", isFinished) 245 | } else if *copyAction == "download" { 246 | doSFTPFileTransfer(userName, password, s.Name(), "", port, hosts, *sourcePath, *destinationPath, "download", isFinished) 247 | } else { 248 | utils.ShowFileTransferUsage() 249 | } 250 | } 251 | } 252 | } else { 253 | s, _ := cfg.GetSection(*group) 254 | if s.HasKey("hosts") { 255 | h, _ := s.GetKey("hosts") 256 | resHosts, err := utils.GetAvailableIPFromMultiLines(h.String()) 257 | if err != nil { 258 | utils.ColorPrint("ERROR", ">>>", "ERROR: ", err, "\n") 259 | return 260 | } 261 | hosts = resHosts 262 | userName := s.Key("user").String() 263 | password := s.Key("pass").String() 264 | port := s.Key("port").MustInt() 265 | if *copyAction == "upload" { 266 | doSFTPFileTransfer(userName, password, s.Name(), "", port, hosts, *sourcePath, *destinationPath, "upload", true) 267 | } else if *copyAction == "download" { 268 | doSFTPFileTransfer(userName, password, s.Name(), "", port, hosts, *sourcePath, *destinationPath, "download", true) 269 | } else { 270 | utils.ShowFileTransferUsage() 271 | } 272 | } 273 | } 274 | return 275 | } else if *hostFile != "" { 276 | hosts, err := utils.GetAvailableIPFromFile(*hostFile) 277 | if err != nil { 278 | utils.ColorPrint("ERROR", "", "ERROR:", err, "\n") 279 | return 280 | } 281 | if *copyAction == "upload" { 282 | doSFTPFileTransfer(*user, *password, "from-file", "", *port, hosts, *sourcePath, *destinationPath, "upload", true) 283 | } else if *copyAction == "download" { 284 | doSFTPFileTransfer(*user, *password, "from-file", "", *port, hosts, *sourcePath, *destinationPath, "download", true) 285 | } else { 286 | utils.ShowFileTransferUsage() 287 | } 288 | return 289 | } else if *hostList != "" { 290 | hosts, err := utils.GetAvailableIP(*hostList) 291 | if err != nil { 292 | utils.ColorPrint("ERROR", "", "ERROR:", err, "\n") 293 | return 294 | } 295 | if *copyAction == "upload" { 296 | doSFTPFileTransfer(*user, *password, "from-list", "", *port, hosts, *sourcePath, *destinationPath, "upload", true) 297 | } else if *copyAction == "download" { 298 | doSFTPFileTransfer(*user, *password, "from-list", "", *port, hosts, *sourcePath, *destinationPath, "download", true) 299 | } else { 300 | utils.ShowFileTransferUsage() 301 | } 302 | return 303 | } else { 304 | utils.ShowFileTransferUsage() 305 | } 306 | } 307 | } 308 | 309 | func listCommandAction(sec *ini.Section) { 310 | utils.ColorPrint("INFO", "", ">>> Group Name: ", "["+sec.Name()+"]\n") 311 | if sec.HasKey("hosts") { 312 | h, _ := sec.GetKey("hosts") 313 | utils.ColorPrint("INFO", "", ">>> Hosts From: ", h.String(), "\n") 314 | hosts, err := utils.GetAvailableIPFromMultiLines(h.String()) 315 | if err != nil { 316 | utils.ColorPrint("ERROR", "", "ERROR:", err, "\n") 317 | return 318 | 319 | } 320 | utils.PrintListHosts(hosts, *maxTableCellWidth, sec.Name()) 321 | } 322 | } 323 | 324 | func checkCommandArgs() ([]string, error) { 325 | var cmds []string 326 | if *cmdArgs != "" { 327 | strPath, err := utils.GetRealPath(*cmdArgs) 328 | if err != nil { 329 | // not real path 330 | if !strings.Contains(*cmdArgs, ";") { 331 | cmds = append(cmds, *cmdArgs) 332 | } 333 | cmds = strings.Split(*cmdArgs, ";") 334 | } else { 335 | // cmd file path, just get the commands content 336 | cmdFileContent, err := utils.GetFileContent(strPath) 337 | cmds = cmdFileContent 338 | if err != nil { 339 | fmt.Println("ERROR:", err) 340 | return cmds, err 341 | } 342 | } 343 | } 344 | return cmds, nil 345 | } 346 | 347 | func doSSHCommands(user, password, hostGroupName, key string, port int, todoHosts, cmds []string, scriptFilePath, scriptArgs, action string, isFinished bool) { 348 | var resultLog utils.ResultLogs 349 | if len(cmds) == 0 { 350 | cmds = append(cmds, "echo pong") 351 | } 352 | todoHosts, err := utils.DuplicateIPAddressCheck(todoHosts) 353 | if err != nil { 354 | fmt.Println(err) 355 | return 356 | } 357 | pool := utils.NewPool(*maxExecuteNum, len(todoHosts)) 358 | startTime := time.Now() 359 | resultLog.StartTime = startTime.Format("2006-01-02 15:04:05") 360 | resultLog.HostGroup = hostGroupName 361 | switch *formatMode { 362 | case "simple", "table": 363 | utils.ColorPrint("INFO", "", "Tips:", fmt.Sprintf("Process running start: %s\n", resultLog.StartTime)) 364 | } 365 | if *output != "" { 366 | utils.WriteAndAppendFile(*output, fmt.Sprintf("Tips: process running start: %s", resultLog.StartTime)) 367 | } 368 | chres := make([]chan interface{}, len(todoHosts)) 369 | for i, host := range todoHosts { 370 | chres[i] = make(chan interface{}, 1) 371 | go func(h string, a string, chr chan interface{}) { 372 | pool.AddOne() 373 | switch a { 374 | case "script": 375 | utils.SSHRunShellScript(user, password, h, key, scriptFilePath, scriptArgs, port, chr) 376 | case "cmd": 377 | utils.DoSSHRunFast(user, password, h, key, cmds, port, chr) 378 | } 379 | pool.DelOne() 380 | }(host, action, chres[i]) 381 | if *formatMode == "simple" || *output != "" { 382 | res := <-chres[i] 383 | if res.(utils.SSHResult).Status == "failed" { 384 | resultLog.ErrorHosts = append(resultLog.ErrorHosts, res) 385 | } else { 386 | resultLog.SuccessHosts = append(resultLog.SuccessHosts, res) 387 | } 388 | utils.FormatResultWithBasicStyle(i, res.(utils.SSHResult)) 389 | if *output != "" { 390 | utils.LogSSHResultToFile(i, res.(utils.SSHResult), *output) 391 | } 392 | } 393 | } 394 | switch *formatMode { 395 | case "simple": 396 | utils.FormatResultLogWithSimpleStyle(resultLog, startTime, *maxTableCellWidth, []string{"Result"}) 397 | case "table": 398 | utils.FormatResultLogWithTableStyle(chres, resultLog, startTime, *maxTableCellWidth) 399 | case "json": 400 | if *inventory != "" && *group == "all" { 401 | log := utils.GetAllResultLog(chres, resultLog, startTime) 402 | allResultLogs = append(allResultLogs, log) 403 | if isFinished { 404 | utils.FormatResultToJson(allResultLogs, *jsonRaw) 405 | } 406 | } else { 407 | utils.FormatResultLogWithJsonStyle(chres, resultLog, startTime, *jsonRaw) 408 | } 409 | } 410 | if *output != "" { 411 | utils.ResultLogInfo(resultLog, startTime, true, *output) 412 | } 413 | pool.Wg.Wait() 414 | } 415 | 416 | func doSFTPFileTransfer(user, password, hostGroupName, key string, port int, todoHosts []string, sourcePath, destinationPath, action string, isFinished bool) { 417 | var resultLog utils.ResultLogs 418 | todoHosts, err := utils.DuplicateIPAddressCheck(todoHosts) 419 | if err != nil { 420 | fmt.Println(err) 421 | return 422 | } 423 | pool := utils.NewPool(*maxExecuteNum, len(todoHosts)) 424 | startTime := time.Now() 425 | resultLog.StartTime = startTime.Format("2006-01-02 15:04:05") 426 | resultLog.HostGroup = hostGroupName 427 | if *formatMode == "simple" || *formatMode == "table" { 428 | utils.ColorPrint("INFO", "", "Tips:", fmt.Sprintf("Process running start: %s\n", resultLog.StartTime)) 429 | } 430 | if *output != "" { 431 | utils.WriteAndAppendFile(*output, fmt.Sprintf("Tips: process running start: %s", resultLog.StartTime)) 432 | } 433 | chres := make([]chan interface{}, len(todoHosts)) 434 | for i, host := range todoHosts { 435 | chres[i] = make(chan interface{}, 1) 436 | go func(h string, a, s, d string, chr chan interface{}) { 437 | pool.AddOne() 438 | switch a { 439 | case "upload": 440 | utils.SFTPUpload(user, password, h, key, port, s, d, chr) 441 | case "download": 442 | utils.SFTPDownload(user, password, h, key, port, s, d, chr) 443 | } 444 | pool.DelOne() 445 | }(host, action, sourcePath, destinationPath, chres[i]) 446 | if *formatMode == "simple" || *output != "" { 447 | res := <-chres[i] 448 | if res.(utils.SFTPResult).Status == "failed" { 449 | resultLog.ErrorHosts = append(resultLog.ErrorHosts, res) 450 | } else { 451 | resultLog.SuccessHosts = append(resultLog.SuccessHosts, res) 452 | } 453 | utils.SFTPFormatResultWithBasicStyle(i, res.(utils.SFTPResult)) 454 | if *output != "" { 455 | utils.LogSFTPResultToFile(i, res.(utils.SFTPResult), *output) 456 | } 457 | } 458 | } 459 | switch *formatMode { 460 | case "simple": 461 | utils.FormatResultLogWithSimpleStyle(resultLog, startTime, *maxTableCellWidth, []string{}) 462 | case "table": 463 | utils.FormatResultLogWithTableStyle(chres, resultLog, startTime, *maxTableCellWidth) 464 | case "json": 465 | if *inventory != "" && *group == "all" { 466 | log := utils.GetAllResultLog(chres, resultLog, startTime) 467 | allResultLogs = append(allResultLogs, log) 468 | if isFinished { 469 | utils.FormatResultToJson(allResultLogs, *jsonRaw) 470 | } 471 | } else { 472 | utils.FormatResultLogWithJsonStyle(chres, resultLog, startTime, *jsonRaw) 473 | } 474 | } 475 | if *output != "" { 476 | utils.ResultLogInfo(resultLog, startTime, true, *output) 477 | } 478 | pool.Wg.Wait() 479 | } 480 | -------------------------------------------------------------------------------- /utils/concurrency.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "sync" 4 | 5 | // https://www.golangtc.com/t/559e97d6b09ecc22f6000053 6 | // thank you very much 7 | 8 | type Pool struct { 9 | queue chan int 10 | Wg *sync.WaitGroup 11 | Size int // pool size 12 | } 13 | 14 | func NewPool(cap, total int) *Pool { 15 | if cap < 1 { 16 | cap = 1 17 | } 18 | p := &Pool{ 19 | queue: make(chan int, cap), 20 | Wg: new(sync.WaitGroup), 21 | } 22 | p.Wg.Add(total) 23 | p.Size = 0 24 | return p 25 | } 26 | func (p *Pool) AddOne() { 27 | p.queue <- 1 28 | p.Size++ 29 | } 30 | func (p *Pool) DelOne() { 31 | <-p.queue 32 | p.Wg.Done() 33 | p.Size-- 34 | } 35 | -------------------------------------------------------------------------------- /utils/sftp.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "github.com/pkg/sftp" 6 | "golang.org/x/crypto/ssh" 7 | "io" 8 | "io/ioutil" 9 | "net" 10 | "os" 11 | "path" 12 | "path/filepath" 13 | "time" 14 | ) 15 | 16 | type SFTPResult struct { 17 | Host string 18 | Status string 19 | SourcePath string 20 | DestinationPath string 21 | Result string 22 | } 23 | 24 | // coped from https://github.com/shanghai-edu/multissh (thank you very much) 25 | func sftpConnect(user, password, host, key string, port int) (*sftp.Client, error) { 26 | var ( 27 | auth []ssh.AuthMethod 28 | addr string 29 | clientConfig *ssh.ClientConfig 30 | sshClient *ssh.Client 31 | config ssh.Config 32 | sftpClient *sftp.Client 33 | err error 34 | ) 35 | // get auth method 36 | auth = make([]ssh.AuthMethod, 0) 37 | if key == "" { 38 | auth = append(auth, ssh.Password(password)) 39 | } else { 40 | pemBytes, err := ioutil.ReadFile(key) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | var signer ssh.Signer 46 | if password == "" { 47 | signer, err = ssh.ParsePrivateKey(pemBytes) 48 | } else { 49 | signer, err = ssh.ParsePrivateKeyWithPassphrase(pemBytes, []byte(password)) 50 | } 51 | if err != nil { 52 | return nil, err 53 | } 54 | auth = append(auth, ssh.PublicKeys(signer)) 55 | } 56 | 57 | clientConfig = &ssh.ClientConfig{ 58 | User: user, 59 | Auth: auth, 60 | Timeout: 30 * time.Second, 61 | Config: config, 62 | HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { 63 | return nil 64 | }, 65 | } 66 | 67 | // connect to ssh 68 | addr = fmt.Sprintf("%s:%d", host, port) 69 | 70 | if sshClient, err = ssh.Dial("tcp", addr, clientConfig); err != nil { 71 | return nil, err 72 | } 73 | 74 | // create sftp client 75 | if sftpClient, err = sftp.NewClient(sshClient); err != nil { 76 | return nil, err 77 | } 78 | 79 | return sftpClient, nil 80 | } 81 | 82 | func SFTPSimpleUpload(user, password, host, key string, port int, sourcePath, destinationPath string) SFTPResult { 83 | var ( 84 | err error 85 | sftpClient *sftp.Client 86 | sftpResult SFTPResult 87 | ) 88 | sftpResult.Host = host 89 | sftpResult.SourcePath = sourcePath 90 | sftpResult.DestinationPath = destinationPath 91 | sftpClient, err = sftpConnect(user, password, host, key, port) 92 | if err != nil { 93 | sftpResult.Status = "failed" 94 | sftpResult.Result = fmt.Sprintf("ERROR: sftp connect to %s failed, error message:%s", sftpResult.Host, err.Error()) 95 | return sftpResult 96 | } 97 | defer sftpClient.Close() 98 | 99 | srcFile, err := os.Open(sourcePath) 100 | if err != nil { 101 | sftpResult.Status = "failed" 102 | sftpResult.Result = fmt.Sprintf("ERROR: os open file \"%s\" failed, error message:%s", sftpResult.Host, err.Error()) 103 | return sftpResult 104 | } 105 | defer srcFile.Close() 106 | 107 | if destinationPath == "" { 108 | currWorkDir, _ := sftpClient.Getwd() 109 | sftpResult.DestinationPath = currWorkDir 110 | } 111 | 112 | var remoteFileName = filepath.Base(sourcePath) 113 | dstFile, err := sftpClient.Create(path.Join(destinationPath, remoteFileName)) 114 | if err != nil { 115 | sftpResult.Status = "failed" 116 | sftpResult.Result = fmt.Sprintf("ERROR: while upload file \"%s\" to remote path \"%s\" ,error message:%s ", sftpResult.SourcePath, sftpResult.DestinationPath, err.Error()) 117 | return sftpResult 118 | } 119 | defer dstFile.Close() 120 | 121 | buf := make([]byte, 1024) 122 | for { 123 | n, err := srcFile.Read(buf) 124 | if err != nil && err != io.EOF { 125 | sftpResult.Status = "failed" 126 | sftpResult.Result = fmt.Sprintf("ERROR: while upload file \"%s\" to remote path \"%s\" ,error message:%s ", sftpResult.SourcePath, sftpResult.DestinationPath, err.Error()) 127 | return sftpResult 128 | } 129 | if n == 0 { 130 | break 131 | } 132 | dstFile.Write(buf[0:n]) 133 | } 134 | 135 | sftpResult.Status = "success" 136 | sftpResult.Result = fmt.Sprintf("Upload finished!:)") 137 | return sftpResult 138 | } 139 | 140 | func SFTPUpload(user, password, host, key string, port int, sourcePath, destinationPath string, chr chan interface{}) { 141 | var ( 142 | err error 143 | sftpClient *sftp.Client 144 | sftpResult SFTPResult 145 | ) 146 | sftpResult.Host = host 147 | sftpResult.SourcePath = sourcePath 148 | sftpResult.DestinationPath = destinationPath 149 | sftpClient, err = sftpConnect(user, password, host, key, port) 150 | if err != nil { 151 | sftpResult.Status = "failed" 152 | sftpResult.Result = fmt.Sprintf("ERROR: sftp connect to %s failed, error message:%s", sftpResult.Host, err.Error()) 153 | chr <- sftpResult 154 | return 155 | } 156 | defer sftpClient.Close() 157 | if destinationPath == "" { 158 | currWorkDir, _ := sftpClient.Getwd() 159 | sftpResult.DestinationPath = currWorkDir 160 | } 161 | srcFile, err := os.Open(sourcePath) 162 | if err != nil { 163 | sftpResult.Status = "failed" 164 | sftpResult.Result = fmt.Sprintf("ERROR: os open file \"%s\" failed, error message:%s", sftpResult.Host, err.Error()) 165 | chr <- sftpResult 166 | return 167 | } 168 | defer srcFile.Close() 169 | var remoteFileName = filepath.Base(sourcePath) 170 | var remoteFilePath = path.Join(destinationPath, remoteFileName) 171 | dstFile, err := sftpClient.Create(remoteFilePath) 172 | if err != nil { 173 | sftpResult.Status = "failed" 174 | sftpResult.Result = fmt.Sprintf("ERROR: while upload file \"%s\" to remote path \"%s\" ,error message:%s ", sftpResult.SourcePath, sftpResult.DestinationPath, err.Error()) 175 | chr <- sftpResult 176 | return 177 | } 178 | defer dstFile.Close() 179 | 180 | buf := make([]byte, 1024) 181 | for { 182 | n, err := srcFile.Read(buf) 183 | if err != nil && err != io.EOF { 184 | sftpResult.Status = "failed" 185 | sftpResult.Result = fmt.Sprintf("ERROR: while upload file \"%s\" to remote path \"%s\" ,error message:%s ", sftpResult.SourcePath, sftpResult.DestinationPath, err.Error()) 186 | return 187 | } 188 | if n == 0 { 189 | break 190 | } 191 | dstFile.Write(buf[0:n]) 192 | } 193 | 194 | sftpResult.Status = "success" 195 | sftpResult.Result = fmt.Sprintf("Upload finished!:)") 196 | chr <- sftpResult 197 | return 198 | } 199 | 200 | func SFTPDownload(user, password, host, key string, port int, sourcePath, destinationPath string, chr chan interface{}) { 201 | var ( 202 | err error 203 | sftpClient *sftp.Client 204 | sftpResult SFTPResult 205 | ) 206 | sftpResult.Host = host 207 | sftpResult.SourcePath = sourcePath 208 | sftpResult.DestinationPath = destinationPath 209 | sftpClient, err = sftpConnect(user, password, host, key, port) 210 | if err != nil { 211 | sftpResult.Status = "failed" 212 | sftpResult.Result = fmt.Sprintf("ERROR: sftp connect to %s failed, error message:%s", sftpResult.Host, err.Error()) 213 | chr <- sftpResult 214 | return 215 | } 216 | defer sftpClient.Close() 217 | 218 | srcFile, err := sftpClient.Open(sourcePath) 219 | if err != nil { 220 | sftpResult.Status = "failed" 221 | sftpResult.Result = fmt.Sprintf("ERROR: sftp open file failed %s, error message:%s", sftpResult.SourcePath, err.Error()) 222 | chr <- sftpResult 223 | return 224 | } 225 | defer srcFile.Close() 226 | if destinationPath == "" { 227 | currWorkDir, _ := os.Getwd() 228 | sftpResult.DestinationPath = currWorkDir 229 | } 230 | fileInfo, _ := srcFile.Stat() 231 | var localFileName = fmt.Sprintf("%s_%s", sftpResult.Host, fileInfo.Name()) 232 | dstFile, err := os.Create(filepath.Join(destinationPath, localFileName)) 233 | if err != nil { 234 | sftpResult.Status = "failed" 235 | sftpResult.Result = fmt.Sprintf("ERROR: while download file \"%s\" to local path \"%s\" ,error message: %s", sftpResult.SourcePath, sftpResult.DestinationPath, err.Error()) 236 | chr <- sftpResult 237 | return 238 | } 239 | defer dstFile.Close() 240 | 241 | if _, err := srcFile.WriteTo(dstFile); err != nil { 242 | sftpResult.Status = "failed" 243 | sftpResult.Result = fmt.Sprintf("ERROR: while download file \"%s\" to local path \"%s\" ,error message:%s ", sftpResult.SourcePath, sftpResult.DestinationPath, err.Error()) 244 | chr <- sftpResult 245 | return 246 | } 247 | 248 | sftpResult.Status = "success" 249 | sftpResult.Result = fmt.Sprintf("Download finished!:)") 250 | chr <- sftpResult 251 | return 252 | } 253 | -------------------------------------------------------------------------------- /utils/ssh.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "golang.org/x/crypto/ssh" 7 | "io/ioutil" 8 | "net" 9 | "path/filepath" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | type SSHResult struct { 15 | Host string 16 | Status string 17 | Result string 18 | } 19 | 20 | // coped from https://github.com/shanghai-edu/multissh (thank you very much) 21 | func connect(user, password, host, key string, port int) (*ssh.Session, error) { 22 | var ( 23 | auth []ssh.AuthMethod 24 | addr string 25 | clientConfig *ssh.ClientConfig 26 | client *ssh.Client 27 | config ssh.Config 28 | session *ssh.Session 29 | err error 30 | ) 31 | // get auth method 32 | auth = make([]ssh.AuthMethod, 0) 33 | if key == "" { 34 | auth = append(auth, ssh.Password(password)) 35 | } else { 36 | pemBytes, err := ioutil.ReadFile(key) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | var signer ssh.Signer 42 | if password == "" { 43 | signer, err = ssh.ParsePrivateKey(pemBytes) 44 | } else { 45 | signer, err = ssh.ParsePrivateKeyWithPassphrase(pemBytes, []byte(password)) 46 | } 47 | if err != nil { 48 | return nil, err 49 | } 50 | auth = append(auth, ssh.PublicKeys(signer)) 51 | } 52 | 53 | clientConfig = &ssh.ClientConfig{ 54 | User: user, 55 | Auth: auth, 56 | Timeout: 5 * time.Second, 57 | Config: config, 58 | HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { 59 | return nil 60 | }, 61 | } 62 | 63 | // connect to ssh 64 | addr = fmt.Sprintf("%s:%d", host, port) 65 | 66 | if client, err = ssh.Dial("tcp", addr, clientConfig); err != nil { 67 | return nil, err 68 | } 69 | 70 | // create session 71 | if session, err = client.NewSession(); err != nil { 72 | return nil, err 73 | } 74 | 75 | modes := ssh.TerminalModes{ 76 | ssh.ECHO: 0, // disable echoing 77 | ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud 78 | ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud 79 | } 80 | 81 | if err := session.RequestPty("xterm", 40, 80, modes); err != nil { 82 | return nil, err 83 | } 84 | 85 | return session, nil 86 | } 87 | 88 | func SSHRunShellScript(user, password, host, key, scriptFilePath, scriptArgs string, port int, chr chan interface{}) { 89 | var sshResult SSHResult 90 | var cmds []string 91 | sshResult.Host = host 92 | session, err := connect(user, password, host, key, port) 93 | if err != nil { 94 | sshResult.Status = "failed" 95 | sshResult.Result = fmt.Sprintf("ERROR: while connecting host %s, an error occured,error message: %s", sshResult.Host, err) 96 | chr <- sshResult 97 | return 98 | } 99 | defer session.Close() 100 | var outBuffer, errBuffer bytes.Buffer 101 | session.Stdout = &outBuffer 102 | session.Stderr = &errBuffer 103 | 104 | resSftpResult := SFTPSimpleUpload(user, password, host, key, port, scriptFilePath, "") 105 | if resSftpResult.Status == "false" { 106 | sshResult.Status = "failed" 107 | sshResult.Result = fmt.Sprintf("ERROR: copy local Shell script %s to host %s failed, error message: %s", scriptFilePath, sshResult.Host, err.Error()) 108 | chr <- sshResult 109 | return 110 | } 111 | 112 | scriptFileRemotePath := resSftpResult.DestinationPath + "/" + filepath.Base(scriptFilePath) 113 | executeScriptCmd := fmt.Sprintf("%s %s %s", "/bin/sh", scriptFileRemotePath, scriptArgs) 114 | //removeScriptBeforeExitCmd := fmt.Sprintf("ls %s", scriptFileRemotePath) 115 | removeScriptBeforeExitCmd := fmt.Sprintf("rm -rf %s", scriptFileRemotePath) 116 | cmds = append(cmds, executeScriptCmd, removeScriptBeforeExitCmd, "exit") 117 | cmd := strings.Join(cmds, " && ") 118 | err = session.Run(cmd) 119 | if err != nil { 120 | sshResult.Status = "failed" 121 | res := outBuffer.String() 122 | res = strings.TrimSpace(res) 123 | sshResult.Result = fmt.Sprintf("%s\nERROR: while running script (%s) on host %s, an error occured %s", res, scriptFilePath, sshResult.Host, err.Error()) 124 | chr <- sshResult 125 | return 126 | } 127 | if errBuffer.String() != "" { 128 | sshResult.Status = "failed" 129 | sshResult.Result = errBuffer.String() 130 | chr <- sshResult 131 | } else { 132 | sshResult.Status = "success" 133 | sshResult.Result = outBuffer.String() 134 | chr <- sshResult 135 | } 136 | chr <- sshResult 137 | return 138 | } 139 | func DoSSHRunFast(user, password, host, key string, cmdList []string, port int, chr chan interface{}) { 140 | var sshResult SSHResult 141 | sshResult.Host = host 142 | session, err := connect(user, password, host, key, port) 143 | if err != nil { 144 | sshResult.Status = "failed" 145 | sshResult.Result = fmt.Sprintf("ERROR: while connecting host %s, an error occured %s", sshResult.Host, err) 146 | chr <- sshResult 147 | return 148 | } 149 | defer session.Close() 150 | 151 | var outBuffer, errBuffer bytes.Buffer 152 | session.Stdout = &outBuffer 153 | session.Stderr = &errBuffer 154 | 155 | newCmd := strings.Join(cmdList, " && ") 156 | err = session.Run(newCmd) 157 | if err != nil { 158 | sshResult.Status = "failed" 159 | res := outBuffer.String() 160 | res = strings.TrimSpace(res) 161 | 162 | sshResult.Result = fmt.Sprintf("%s\nERROR: while running one or more command failed on host %s, an error occured %s", res, sshResult.Host, err.Error()) 163 | chr <- sshResult 164 | return 165 | } 166 | if errBuffer.String() != "" { 167 | sshResult.Status = "failed" 168 | res := errBuffer.String() 169 | res = strings.TrimSpace(res) 170 | sshResult.Result = res 171 | chr <- sshResult 172 | } else { 173 | sshResult.Status = "success" 174 | res := outBuffer.String() 175 | res = strings.TrimSpace(res) 176 | sshResult.Result = res 177 | chr <- sshResult 178 | } 179 | return 180 | } 181 | -------------------------------------------------------------------------------- /utils/usage.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "fmt" 4 | 5 | func ShowListCommandUsage() { 6 | ColorPrint("INFO", "", "Tips: ", "ssgo's run command is used for list available IP address from your input, for more help information,input this:\n") 7 | fmt.Printf("# %s", "ssgo list -h\n") 8 | fmt.Printf("# %s", "ssgo list --help\n\n") 9 | ColorPrint("INFO", "", "Example 1", ": Use an ini file instead of specifying login user, password in the command line.\n") 10 | fmt.Println("(1) when usage a ini file, -i and -g flag must be specified together.") 11 | fmt.Println("(2) -i flag's argument must be a .ini file and -g flag's argument must be all or a host group name in the ini file you specified. e.g.") 12 | fmt.Println(" a) -i config.ini -g all") 13 | fmt.Println(" b) -i config.ini -g web") 14 | fmt.Println("(3) Of course you can also specify an own ini file like ssgo's config.ini") 15 | fmt.Printf("# %s", "ssgo list -i example.ini -g all\n") 16 | fmt.Printf("# %s", "ssgo list -i example.ini -g web\n\n") 17 | ColorPrint("INFO", "", "Example 2", ": Use --host-list flag.\n") 18 | fmt.Println("(1) --host-list flag's argument support IP address range expression,e.g.") 19 | fmt.Println("(2) --host-list 192.168.100.1") 20 | fmt.Println("(3) --host-list 192.168.100.1-192.168.100.3,192.168.100.4-6") 21 | fmt.Println("(4) --host-list 192.168.100.0/24") 22 | fmt.Println("(5) --host-list 192.168.100.0/24,192.168.200.10,192.168.200.10-30") 23 | fmt.Printf("# %s", "ssgo list --host-list 192.168.10.110\n") 24 | fmt.Printf("# %s", "ssgo list --host-list 192.168.10.100,192.168.10.101-192.168.10.103\n") 25 | fmt.Printf("# %s", "ssgo list --host-list 192.168.10.0/24\n\n") 26 | ColorPrint("INFO", "", "Example 3", ": Use --host-file flag.\n") 27 | fmt.Println("(1) --host-file flag's argument should be a file contains IP address range expression,e.g. host-file.example.txt") 28 | fmt.Println("(2) in this case,IP Address in the line 4(#192.168.100.101) and line 5(#192.168.200.201) with \"#\" prefix will be ignored.") 29 | fmt.Println("(3) the duplicate IP Address in host-file.example.txt also will be ignored.") 30 | fmt.Println("###########host-file.example.txt##########") 31 | fmt.Println("192.168.100.1") 32 | fmt.Println("192.168.100.1-192.168.100.3,192.168.100.4-6") 33 | fmt.Println("192.168.100.0/24") 34 | fmt.Println("#192.168.100.101") 35 | fmt.Println("192.168.100.0/24,#192.168.200.201,192.168.200.10-30") 36 | fmt.Println("###########host-file.example.txt##########") 37 | fmt.Printf("# %s", "ssgo list --host-file host-file.example.txt\n\n") 38 | return 39 | } 40 | 41 | func ShowRunCommandUsage() { 42 | ColorPrint("INFO", "", "Tips: ", "ssgo's run command is used for execute commands or Shell script on remote hosts,for more help information,input this:\n") 43 | fmt.Printf("# %s", "ssgo run -h\n") 44 | fmt.Printf("# %s", "ssgo run --help\n\n") 45 | ColorPrint("INFO", "", "Example 1", ": Use an ini file instead of specifying login user, password in the command line.\n") 46 | fmt.Println("(1) when usage a ini file, -i and -g flag must be specified together.") 47 | fmt.Println("(2) -i flag's argument must be a .ini file and -g flag's argument must be all or a host group name in the ini file you specified. e.g.") 48 | fmt.Println(" a) -i config.ini -g all") 49 | fmt.Println(" b) -i config.ini -g web") 50 | fmt.Println("(3) Of course you can also specify an own ini file like ssgo's config.ini") 51 | fmt.Println("(4) -s,--script flag's argument should be a Shell script file; -a,--args is optional for Shell script's arguments.") 52 | fmt.Println("(5) -c,--cmd flag argument should be Shell command,it also support multiple comma-separated commands.") 53 | fmt.Printf("# %s", "ssgo run -i example.ini -g all -c \"hostname\"\n") 54 | fmt.Printf("# %s", "ssgo run -i example.ini -g web -c \"hostname;date\"\n") 55 | fmt.Printf("# %s", "ssgo run -i example.ini -g web -s demo.sh -a \"arg1 arg2\"\n\n") 56 | ColorPrint("INFO", "", "Example 2", ": Use --host-list flag by specifying login user, password in the command line.\n") 57 | fmt.Println("(1) --host-list flag's argument support IP address range expression,e.g.") 58 | fmt.Println("(2) --host-list 192.168.100.1") 59 | fmt.Println("(3) --host-list 192.168.100.1-192.168.100.3,192.168.100.4-6") 60 | fmt.Println("(4) --host-list 192.168.100.0/24") 61 | fmt.Println("(5) --host-list 192.168.100.0/24,192.168.200.10,192.168.200.10-30") 62 | fmt.Printf("# %s", "ssgo run --host-list 192.168.10.110 -g all -u root -p root -s demo.sh -a \"arg1 arg2\"\n") 63 | fmt.Printf("# %s", "ssgo run --host-list 192.168.10.100,192.168.10.101-192.168.10.103 -g all -u root -p root -c \"hostname;date\"\n") 64 | fmt.Printf("# %s", "ssgo run --host-list 192.168.10.0/24 -g all -u root -p root -c \"hostname\"\n\n") 65 | ColorPrint("INFO", "", "Example 3", ": Use --host-file flag by specifying login user, password in the command line.\n") 66 | fmt.Println("(1) --host-file flag's argument should be a file contains IP address range expression,e.g. host-file.example.txt") 67 | fmt.Println("(2) in this case,IP Address in the line 4(#192.168.100.101) and line 5(#192.168.200.201) with \"#\" prefix will be ignored.") 68 | fmt.Println("(3) the duplicate IP Address in host-file.example.txt also will be ignored.") 69 | fmt.Println("-----------host-file.example.txt-----------") 70 | fmt.Println("192.168.100.1") 71 | fmt.Println("192.168.100.1-192.168.100.3,192.168.100.4-6") 72 | fmt.Println("192.168.100.0/24") 73 | fmt.Println("#192.168.100.101") 74 | fmt.Println("192.168.100.0/24,#192.168.200.201,192.168.200.10-30") 75 | fmt.Println("-----------host-file.example.txt-----------") 76 | fmt.Printf("# %s", "ssgo run --host-file host-file.example.txt -u root -p root -s demo.sh -a \"arg1 arg2\"\n") 77 | fmt.Printf("# %s", "ssgo run --host-file host-file.example.txt -u root -p root -c \"hostname;date\"\n") 78 | fmt.Printf("# %s", "ssgo run --host-file host-file.example.txt -u root -p root -c \"hostname\"\n\n") 79 | ColorPrint("INFO", "", "Example 4", ": like ssgo's run command with -F flag you can also format the result in 'simple','table','json' or other styles.\n") 80 | fmt.Println("(1) By now -F flag only support 'simple','table' and 'json' style, default is 'simple' style(you don't need specific the simple style).") 81 | fmt.Println(" a) -F table will format result with table style") 82 | fmt.Println(" b) -F json will format result with json style, by default json strings will format for better looks,you can specify the --json-raw flag, if you need the original json strings") 83 | fmt.Println(" c) better not use \"-F table\" when running multiple comma-separated commands or Shell scripts.") 84 | fmt.Println("(2) difference between 'simple' and 'table' style:") 85 | fmt.Println(" a) the simple style: the execution result will be printed after each host finished executing the command.") 86 | fmt.Println(" b) the table style: the execution result will be printed after all hosts finished executing the command.") 87 | fmt.Printf("# %s", "ssgo run --host-list 192.168.10.110 -u root -p root -c \"date\" -F table\n") 88 | fmt.Printf("# %s", "ssgo run --host-list 192.168.10.110 -u root -p root -c \"date\" -F json\n") 89 | fmt.Printf("# %s", "ssgo run --host-list 192.168.10.110 -u root -p root -c \"date\" -F json --json-raw\n\n") 90 | ColorPrint("INFO", "", "Example 5", ": output result logs.\n") 91 | fmt.Printf("# %s", "ssgo run --host-list 192.168.10.110 -u root -p root -c \"date\" -o log \n") 92 | fmt.Printf("# %s", "ssgo run --host-list 192.168.10.110 -u root -p root -c \"date\" -o demo.log\n") 93 | fmt.Println("(1) description:") 94 | fmt.Printf(" a) -o, --output, By default if your input is \"log\",the output logs filename will contain the current date numbers like \"ssgo-%s.log\"\n", GetCurrentDateNumbers()) 95 | fmt.Println(" b) otherwise, the log file'name with be the argument you specified.") 96 | return 97 | } 98 | 99 | func ShowFileTransferUsage() { 100 | ColorPrint("INFO", "", "Tips: ", "ssgo's copy command is used for transfer file between local machine and remote hosts,for more help information,input this:\n") 101 | fmt.Printf("# %s", "ssgo copy -h\n") 102 | fmt.Printf("# %s", "ssgo copy --help\n\n") 103 | ColorPrint("INFO", "", "Example 1", ": Use an ini file instead of specifying login user, password in the command line.\n") 104 | fmt.Println("(1) when usage a ini file, -i and -g flag must be specified together.") 105 | fmt.Println("(2) -i flag's argument must be a .ini file and -g flag's argument must be all or a host group name in the ini file you specified. e.g.") 106 | fmt.Println(" a) -i config.ini -g all") 107 | fmt.Println(" b) -i config.ini -g web") 108 | fmt.Println("(3) Of course you can also specify an own ini file like ssgo's config.ini") 109 | fmt.Println("(4) if -d is empty or -d \"\" file will be upload or download to current work directory on local machine or remote host.") 110 | fmt.Printf("# %s", "ssgo copy -a upload -i example.ini -g web -s demo.txt -d \"/root\"\n") 111 | fmt.Printf("# %s", "ssgo copy -a upload -i example.ini -g web -s demo.txt -d \"\"\n") 112 | fmt.Printf("# %s", "ssgo copy -a download -i example.ini -g web -s /root/demo.txt -d \"e:\\temp\"\n\n") 113 | ColorPrint("INFO", "", "Example 2", ": Use --host-list flag by specifying login user, password in the command line.\n") 114 | fmt.Println("(1) --host-list flag's argument support IP address range expression,e.g.") 115 | fmt.Println("(2) --host-list 192.168.100.1") 116 | fmt.Println("(3) --host-list 192.168.100.1-192.168.100.3,192.168.100.4-6") 117 | fmt.Println("(4) --host-list 192.168.100.0/24") 118 | fmt.Println("(5) --host-list 192.168.100.0/24,192.168.200.10,192.168.200.10-30") 119 | fmt.Printf("# %s", "ssgo copy -a upload --host-list 192.168.10.110 -u root -p root -s demo.txt -d \"/root\"\n") 120 | fmt.Printf("# %s", "ssgo copy -a upload --host-list 192.168.10.100,192.168.10.101-192.168.10.103 -u root -p root -s demo.txt -d \"\"\n") 121 | fmt.Printf("# %s", "ssgo copy -a download --host-list 192.168.10.0/24 -u root -p root -s /root/demo.txt -d \"e:\\temp\"\n\n") 122 | ColorPrint("INFO", "", "Example 3", ": Use --host-file flag by specifying login user, password in the command line.\n") 123 | fmt.Println("(1) --host-file flag's argument should be a file contains IP address range expression,e.g. host-file.example.txt") 124 | fmt.Println("(2) in this case,IP Address in the line 4(#192.168.100.101) and line 5(#192.168.200.201) with \"#\" prefix will be ignored.") 125 | fmt.Println("(3) the duplicate IP Address in host-file.example.txt also will be ignored.") 126 | fmt.Println("-----------host-file.example.txt-----------") 127 | fmt.Println("192.168.100.1") 128 | fmt.Println("192.168.100.1-192.168.100.3,192.168.100.4-6") 129 | fmt.Println("192.168.100.0/24") 130 | fmt.Println("#192.168.100.101") 131 | fmt.Println("192.168.100.0/24,#192.168.200.201,192.168.200.10-30") 132 | fmt.Println("-----------host-file.example.txt-----------") 133 | fmt.Printf("# %s", "ssgo copy -a upload --host-file host-file.example.txt -u root -p root -s demo.txt -d \"/root\"\n") 134 | fmt.Printf("# %s", "ssgo copy -a upload --host-file host-file.example.txt -u root -p root -s demo.txt -d \"\"\n") 135 | fmt.Printf("# %s", "ssgo copy -a download --host-file host-file.example.txt -u root -p root -s /root/demo.txt -d \"e:\\temp\"\n\n") 136 | ColorPrint("INFO", "", "Example 4", ": like ssgo's run command with -F flag you can also format the result in 'simple','table','json' or other styles.\n") 137 | fmt.Println("(1) By now -F flag only support 'simple','table' and 'json' style, default is'simple' style(you don't need specific the simple style).") 138 | fmt.Println(" a) -F table will format result with table style") 139 | fmt.Println(" b) -F json will format result with json style, by default json strings will format for better looks,you can specific the --json-raw flag, if you need the original json strings") 140 | fmt.Println(" c) better not use \"-F table\" when running multiple comma-separated commands or Shell scripts.") 141 | fmt.Println("(2) difference between 'simple' and 'table' style:") 142 | fmt.Println(" a) the simple style: the execution result will be printed after each host finished executing the command.") 143 | fmt.Println(" b) the table style: the execution result will be printed after all hosts finished executing the command.") 144 | fmt.Printf("# %s", "ssgo copy -a upload --host-list 192.168.10.110 -u root -p root -s demo.txt -d \"/root\" -F table\n") 145 | fmt.Printf("# %s", "ssgo copy -a upload --host-list 192.168.10.110 -u root -p root -s demo.txt -d \"/root\" -F json\n\n") 146 | fmt.Printf("# %s", "ssgo copy -a upload --host-list 192.168.10.110 -u root -p root -s demo.txt -d \"/root\" -F json --json-raw\n\n") 147 | ColorPrint("INFO", "", "Example 5", ": output result logs.\n") 148 | fmt.Printf("# %s", "ssgo copy -a upload --host-list 192.168.10.110 -u root -p root -s demo.txt -d \"/root\" -o log\n") 149 | fmt.Printf("# %s", "ssgo copy -a upload --host-list 192.168.10.110 -u root -p root -s demo.txt -d \"/root\" -o demo.log\n") 150 | fmt.Println("(1) description:") 151 | fmt.Printf(" a) -o, --output, By default if your input is \"log\",the output logs filename will contain the current date numbers like \"ssgo-%s.log\"\n", GetCurrentDateNumbers()) 152 | fmt.Println(" b) otherwise, the log file'name with be the argument you specified.") 153 | return 154 | } 155 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "github.com/bndr/gotabulate" 10 | "github.com/daviddengcn/go-colortext" 11 | "github.com/go-ini/ini" 12 | "io/ioutil" 13 | "net" 14 | "os" 15 | "path/filepath" // cross platform for windows & linux 16 | "reflect" 17 | "sort" 18 | "strconv" 19 | "strings" 20 | "time" 21 | ) 22 | 23 | type ResultLogs struct { 24 | StartTime string 25 | HostGroup string 26 | SuccessHosts []interface{} 27 | ErrorHosts []interface{} 28 | EndTime string 29 | CostTime string 30 | TotalHostsInfo string 31 | } 32 | 33 | // 检测文件或文件夹是否存在 34 | func IsPathExists(path string) (bool, error) { 35 | _, err := os.Stat(path) 36 | 37 | if err == nil { 38 | return true, nil 39 | } 40 | if os.IsNotExist(err) { 41 | return false, err 42 | } 43 | return false, err 44 | } 45 | 46 | //获取当前路径 47 | func GetCurrentDir() string { 48 | pwd, _ := os.Getwd() 49 | return pwd 50 | } 51 | 52 | // 根据输入路径获取该路径的物理路径,如果是相对路径,则附加在当前目录下,并判断附加后的路径是否存在 53 | func GetRealPath(strPath string) (string, error) { 54 | if !filepath.IsAbs(strPath) { 55 | pwd := filepath.Join(GetCurrentDir(), strPath) 56 | absStrPath, _ := filepath.Abs(pwd) 57 | strPath = absStrPath 58 | } 59 | checkPath, err := IsPathExists(strPath) 60 | if !checkPath { 61 | return "", err 62 | } 63 | return strPath, nil 64 | } 65 | 66 | //检测当前目录下是否存在某一文件或路径 67 | func IsPathExistInCurrentPath(path string) (bool, error) { 68 | pwd := GetCurrentDir() 69 | strPath := filepath.Join(pwd, path) 70 | rst, err := IsPathExists(strPath) 71 | if !rst { 72 | return false, err 73 | } 74 | return true, nil 75 | } 76 | 77 | //检测ssgo默认配置文件config.ini文件是否存在 78 | func CheckDefaultINIFile(name string) (bool, error) { 79 | r, err := IsPathExistInCurrentPath(name) 80 | 81 | if !r { 82 | return false, err 83 | } 84 | return true, nil 85 | } 86 | 87 | // functions for parsing ip address 88 | //检测IP地址,返回true or false 89 | func CheckIp(strIPAddress string) bool { 90 | strIPAddress = strings.TrimSpace(strIPAddress) 91 | if net.ParseIP(strIPAddress) == nil { 92 | return false 93 | } 94 | return true 95 | } 96 | 97 | // 将IP地址的掩码转换为CIDR格式的掩码,比如,255.255.255.0 转换为 24 98 | func IPMaskToCIDRMask(netmask string) (bool, string) { 99 | netMasks := strings.Split(netmask, ".") 100 | var ms []int 101 | for _, v := range netMasks { 102 | intV, err := strconv.Atoi(v) 103 | if err != nil { 104 | return false, "ERROR: '" + netmask + "' is not a valid subnet mask, subnet mask should be numbers,please check the subnet mask form!" 105 | } 106 | ms = append(ms, intV) 107 | } 108 | ipMask := net.IPv4Mask(byte(ms[0]), byte(ms[1]), byte(ms[2]), byte(ms[3])) 109 | ones, _ := ipMask.Size() 110 | if ones == 0 { 111 | return false, "ERROR: '" + netmask + "' is not a valid subnet mask,please check the subnet mask form!" 112 | } 113 | return true, strconv.Itoa(ones) 114 | } 115 | 116 | //获取可用IP地址 默认以逗号分隔符来根据IP地址表示形式来解析可用IP地址清单 117 | //比如:192.168.100.200-192.168.100.204,#192.168.100.204,192.168.100.272 118 | // 可以是单个IP地址、IP地址段、也可以用逗号分隔多个IP地址或IP地址段 119 | func GetAvailableIP(strIPList string) ([]string, error) { 120 | var availableIPs []string 121 | strIPList = strings.TrimSpace(strIPList) 122 | if !strings.Contains(strIPList, ",") { 123 | ips, err := GetAvailableIPList(strIPList) 124 | if err != nil { 125 | return availableIPs, err 126 | } 127 | availableIPs = ips 128 | return availableIPs, nil 129 | } 130 | strIPs := strings.Split(strIPList, ",") 131 | for _, strIP := range strIPs { 132 | ips, err := GetAvailableIPList(strIP) 133 | if err != nil { 134 | continue 135 | } 136 | availableIPs = append(availableIPs, ips...) 137 | } 138 | if len(availableIPs) == 0 { 139 | return availableIPs, fmt.Errorf("ERROR: no valid IP Address found, please check your input") 140 | } 141 | 142 | return availableIPs, nil 143 | } 144 | 145 | // get file content 146 | func GetFileContent(strFilePath string) ([]string, error) { 147 | var fileContent []string 148 | strFilePath = strings.TrimSpace(strFilePath) 149 | path, err := GetRealPath(strFilePath) 150 | if err != nil { 151 | return fileContent, err 152 | } 153 | buf, err := ioutil.ReadFile(path) 154 | if err != nil { 155 | return fileContent, err 156 | } 157 | strContent := string(buf) 158 | for _, lineStr := range strings.Split(strContent, "\n") { 159 | lineStr = strings.TrimSpace(lineStr) 160 | if lineStr == "" { 161 | continue 162 | } 163 | fileContent = append(fileContent, lineStr) 164 | } 165 | return fileContent, nil 166 | } 167 | 168 | // 从文件中获取可用IP地址清单 169 | // ipAddresslist.example.txt 内容如下: 170 | //192.168.100.200-192.168.100.204,#192.168.100.204,192.168.100.272,192.168.100.203,192.168.100.204,192.168.100.200" 171 | //192.168.100.200-192.168.100.204" 172 | //192.168.100.208" 173 | // 174 | func GetAvailableIPFromFile(strFilePath string) ([]string, error) { 175 | var availableIPs []string 176 | strContent, err := GetFileContent(strFilePath) 177 | if err != nil { 178 | return availableIPs, err 179 | } 180 | if len(strContent) == 0 { 181 | return availableIPs, errors.New("ERROR: Nothing found in '" + strFilePath + "' please check your input file content!") 182 | } 183 | for _, strIps := range strContent { 184 | ips, err := GetAvailableIP(strIps) 185 | if err != nil { 186 | continue 187 | } 188 | availableIPs = append(availableIPs, ips...) 189 | } 190 | if len(availableIPs) == 0 { 191 | return availableIPs, errors.New("ERROR: no valid IP Address found, please check your input file, '" + strFilePath + "'") 192 | } 193 | retIPs, err := DuplicateIPAddressCheck(availableIPs) 194 | if err != nil { 195 | return availableIPs, err 196 | } 197 | return retIPs, nil 198 | } 199 | 200 | // 检测解析后的IP地址清单中是否包含重复IP地址,并接收用户输入以确定是否移除这些重复IP地址 201 | func DuplicateIPAddressCheck(ips []string) ([]string, error) { 202 | sort.Strings(ips) 203 | if len(ips) > len(Duplicate(ips)) { 204 | LabelConfirm: 205 | ok, err := Confirm("Duplicate IP Address Found, input 'yes or y' to remove duplicate IP Address, Input 'no or n' will keep duplicate IP Address,\nnothing will do by default! (y/n) ") 206 | if err != nil { 207 | goto LabelConfirm 208 | } 209 | if ok { 210 | // remove duplicate IP Address 211 | sort.Strings(ips) 212 | retAvailableIPs, err := DuplicateToStringSlice(ips) 213 | if err != nil { 214 | return retAvailableIPs, err 215 | } 216 | return retAvailableIPs, nil 217 | } 218 | // else did nothing keep duplicate IP Address, and return 219 | return ips, nil 220 | } 221 | return ips, nil 222 | } 223 | 224 | //从多行文本获取可用IP地址 225 | //多行IP,通常从配置文件或IP地址清单文件中解析,比如: 226 | //输入: 227 | // ` 228 | //192.168.100.200-192.168.100.204,#192.168.100.204,192.168.100.272,192.168.100.203,192.168.100.204,192.168.100.200" 229 | //192.168.100.200-192.168.100.204" 230 | //192.168.100.208" 231 | // ` 232 | func GetAvailableIPFromMultiLines(multiLines string) ([]string, error) { 233 | var availableIPs []string 234 | multiLines = strings.TrimSpace(multiLines) 235 | if len(multiLines) == 0 { 236 | return availableIPs, fmt.Errorf("ERROR: empty text, no valid IP Address found") 237 | } 238 | 239 | ipLists := strings.Split(multiLines, "\n") 240 | for _, strIps := range ipLists { 241 | ips, err := GetAvailableIP(strIps) 242 | if err != nil { 243 | continue 244 | } 245 | availableIPs = append(availableIPs, ips...) 246 | } 247 | if len(availableIPs) == 0 { 248 | return availableIPs, fmt.Errorf("ERROR: no valid IP Address found, please check your input") 249 | } 250 | return availableIPs, nil 251 | } 252 | 253 | // 支持从如下IP地址标示形式获取可用IP地址清单,比如: 254 | // GetAvailableIPFromSingleIP 支持解析单个IP地址:192.168.100.100 255 | // GetAvailableIPRangeWithDelimiter 支持解析包含分隔符范围的IP地址段:192.168.100.100-105,192, 192.168.100.106-192.168.100.108 256 | // GetAvailableIPWithMask 支持解析包含子网掩码的IP地址段:192.168.100.100/28, 192.168.100.106/255.255.255.240 257 | func GetAvailableIPList(strIP string) ([]string, error) { 258 | var availableIPs []string 259 | strIP = strings.TrimSpace(strIP) 260 | if !strings.HasPrefix(strIP, "#") { 261 | // 如果IP地址钱包含# 默认跳过该条目,代表注释 262 | if ips, err := GetAvailableIPFromSingleIP(strIP); err == nil { 263 | availableIPs = append(availableIPs, ips...) 264 | } else if ips, err := GetAvailableIPRangeWithDelimiter(strIP, "-"); err == nil { 265 | availableIPs = append(availableIPs, ips...) 266 | } else if ips, err := GetAvailableIPWithMask(strIP); err == nil { 267 | availableIPs = append(availableIPs, ips...) 268 | } else { 269 | return availableIPs, fmt.Errorf("ERROR: no valid IP Address found, please check") 270 | } 271 | } 272 | 273 | return availableIPs, nil 274 | } 275 | 276 | // 使用分隔符 获取可用IP地址范围,输出可用IP地址切片, 277 | // 比如:192.168.1.100-192.168.1.103 返回[192.168.1.100 192.168.1.101 192.168.1.102 192.168.1.103] 278 | // 比如:192.168.1.100-102 返回[192.168.1.100 192.168.1.101 192.168.1.102] 279 | func GetAvailableIPRangeWithDelimiter(strIPRanges string, strDelimiter string) ([]string, error) { 280 | var availableIPs []string 281 | if strDelimiter == "." || strDelimiter == ":" || strDelimiter == "" { 282 | return availableIPs, errors.New("ERROR: strings like '.' or ':' or space con't used for split a IP Address strings") 283 | } 284 | if !strings.Contains(strIPRanges, strDelimiter) { 285 | return availableIPs, errors.New("ERROR: can't find " + strDelimiter + "' in '" + strIPRanges + "' please check!") 286 | } 287 | strIPlist := strings.Split(strIPRanges, strDelimiter) 288 | startIP := strings.TrimSpace(strIPlist[0]) 289 | endIP := strings.TrimSpace(strIPlist[1]) 290 | if CheckIp(startIP) == false { 291 | return availableIPs, errors.New("ERROR: Start IP Address is not a valid IP Address range strings, e.g. 192.168.1.100-192.168.1.110") 292 | } 293 | _, startIPPrefix, startIPNo := GetIPAddressPrefixAndEndNo(startIP) 294 | var endIPPrefix []string 295 | var endIPNo int 296 | if CheckIp(endIP) == false { 297 | if v, ok := strconv.Atoi(endIP); ok != nil { 298 | return availableIPs, errors.New("ERROR: END IP Address is not a valid IP Address range strings, e.g. 192.168.1.100-192.168.1.110") 299 | } else { 300 | endIPNo = v 301 | endIPPrefix = startIPPrefix 302 | } 303 | } else { 304 | _, p, n := GetIPAddressPrefixAndEndNo(endIP) 305 | endIPPrefix = p 306 | endIPNo = n 307 | } 308 | // 检测 起始IP地址和终止IP地址的前三位是否相同 309 | if startIPPrefix[0] != endIPPrefix[0] || startIPPrefix[1] != endIPPrefix[1] || startIPPrefix[2] != endIPPrefix[2] { 310 | return availableIPs, errors.New("ERROR: the Start IP Address and END IP Address first three section are not same, Please confirm!, e.g. 192.168.1.100-192.168.1.110") 311 | } 312 | flag := endIPNo - startIPNo 313 | switch { 314 | case flag < 0: 315 | return availableIPs, errors.New("ERROR: the End IP Address must bigger than the Start IP Address, Please confirm!, e.g. 192.168.1.100-192.168.1.110") 316 | case flag == 0: 317 | availableIPs = append(availableIPs, startIP) 318 | return availableIPs, nil 319 | case flag > 0: 320 | for i := 0; i <= flag; i++ { 321 | ips := startIPPrefix 322 | ips = append(ips, strconv.Itoa(i+startIPNo)) 323 | newIP := strings.Join(ips, ".") 324 | availableIPs = append(availableIPs, newIP) 325 | } 326 | } 327 | return availableIPs, nil 328 | } 329 | 330 | // 给定一个IP地址 返回该IP地址的前3位切片,并返回该IP地址的末尾 331 | func GetIPAddressPrefixAndEndNo(strIP string) (bool, []string, int) { 332 | var strIPAddressPrefix []string 333 | strIP = strings.TrimSpace(strIP) 334 | if CheckIp(strIP) == false { 335 | return false, strIPAddressPrefix, 0 336 | } 337 | iplist := strings.Split(strIP, ".") 338 | strIPAddressPrefix = iplist[0 : len(iplist)-1] 339 | ipAddressEndNo, _ := strconv.Atoi(iplist[len(iplist)-1]) 340 | return true, strIPAddressPrefix, ipAddressEndNo 341 | } 342 | 343 | // 判断一个字符串是否为IP地址,并返回可用IP地址切片 344 | func GetAvailableIPFromSingleIP(ipAddress string) ([]string, error) { 345 | var availableIPs []string 346 | ipAddress = strings.TrimSpace(ipAddress) 347 | if !CheckIp(ipAddress) { 348 | return availableIPs, errors.New("ERROR: '" + ipAddress + "' is not a valid IP Address, please confirm!>>>") 349 | } 350 | availableIPs = append(availableIPs, ipAddress) 351 | return availableIPs, nil 352 | } 353 | 354 | // 引用函数 355 | // 将ip/mask形式 统一转换为ip/cidrmask形式 356 | // 比如:输入 192.168.1.100 输出 192.168.1.100/32 357 | // 比如:输入 192.168.1.100/24 输出 192.168.1.100/24 358 | // 比如:输入 192.168.1.100/255.255.255.0 输出 192.168.1.100/24 359 | func IPAddressToCIDR(ipAddress string) (string, error) { 360 | ipAddress = strings.TrimSpace(ipAddress) 361 | if strings.Contains(ipAddress, "/") == true { 362 | ipAndMask := strings.Split(ipAddress, "/") 363 | ip := ipAndMask[0] 364 | if CheckIp(ip) == false { 365 | return "", errors.New("ERROR: '" + ip + "' is not a valid IP Address, please confirm!!!") 366 | } 367 | mask := ipAndMask[1] 368 | 369 | if strings.Contains(mask, ".") == true { 370 | ok, cidrMask := IPMaskToCIDRMask(mask) 371 | if !ok { 372 | return "", errors.New(cidrMask) 373 | } 374 | mask = cidrMask 375 | } else { 376 | intMask, err := strconv.Atoi(mask) 377 | if err != nil { 378 | return "", errors.New("ERROR: '" + mask + "' is not a valid network mask, please confirm!!!") 379 | } 380 | var cidrMaskNos = map[int]int{24: 24, 25: 25, 26: 26, 27: 27, 28: 28, 29: 29, 30: 30, 31: 31, 32: 32} 381 | if _, ok := cidrMaskNos[intMask]; !ok { 382 | return "", errors.New("ERROR: '" + mask + "' is not a valid network mask,for CIDR form masks, valid mask number should be one of [24,25,26,27,28,29,30,31,32] please confirm!") 383 | } 384 | mask = strconv.Itoa(intMask) 385 | } 386 | return ip + "/" + mask, nil 387 | } else { 388 | if net.ParseIP(ipAddress) == nil { 389 | return "", errors.New("ERROR: '" + ipAddress + "' is not a valid IP Address, please check!") 390 | } 391 | return fmt.Sprintf("%s/%d", ipAddress, 32), nil 392 | } 393 | } 394 | 395 | func GetAvailableIPWithMask(ipAndMask string) ([]string, error) { 396 | var availableIPs []string 397 | 398 | ipAndMask = strings.TrimSpace(ipAndMask) 399 | ipAndCIDRMask, err := IPAddressToCIDR(ipAndMask) 400 | if err != nil { 401 | return availableIPs, err 402 | } 403 | _, ipNet, _ := net.ParseCIDR(ipAndCIDRMask) 404 | 405 | firstIP, _ := networkRange(ipNet) 406 | ipNum := ipToInt(firstIP) 407 | size := networkSize(ipNet.Mask) 408 | pos := int32(1) 409 | max := size - 2 // -1 for the broadcast address, -1 for the gateway address 410 | 411 | var newNum int32 412 | for attempt := int32(0); attempt < max; attempt++ { 413 | newNum = ipNum + pos 414 | pos = pos%max + 1 415 | availableIPs = append(availableIPs, intToIP(newNum).String()) 416 | } 417 | return availableIPs, nil 418 | } 419 | 420 | // Calculates the first and last IP addresses in an IPNet 421 | func networkRange(network *net.IPNet) (net.IP, net.IP) { 422 | netIP := network.IP.To4() 423 | firstIP := netIP.Mask(network.Mask) 424 | lastIP := net.IPv4(0, 0, 0, 0).To4() 425 | for i := 0; i < len(lastIP); i++ { 426 | lastIP[i] = netIP[i] | ^network.Mask[i] 427 | } 428 | return firstIP, lastIP 429 | } 430 | 431 | // Given a netmask, calculates the number of available hosts 432 | func networkSize(mask net.IPMask) int32 { 433 | m := net.IPv4Mask(0, 0, 0, 0) 434 | for i := 0; i < net.IPv4len; i++ { 435 | m[i] = ^mask[i] 436 | } 437 | return int32(binary.BigEndian.Uint32(m)) + 1 438 | } 439 | 440 | // Converts a 4 bytes IP into a 32 bit integer 441 | func ipToInt(ip net.IP) int32 { 442 | return int32(binary.BigEndian.Uint32(ip.To4())) 443 | } 444 | 445 | // Converts 32 bit integer into a 4 bytes IP address 446 | func intToIP(n int32) net.IP { 447 | b := make([]byte, 4) 448 | binary.BigEndian.PutUint32(b, uint32(n)) 449 | return net.IP(b) 450 | } 451 | 452 | // go语言实现去重,可以接受任何类型 453 | func Duplicate(a interface{}) (ret []interface{}) { 454 | va := reflect.ValueOf(a) 455 | for i := 0; i < va.Len(); i++ { 456 | if i > 0 && reflect.DeepEqual(va.Index(i-1).Interface(), va.Index(i).Interface()) { 457 | continue 458 | } 459 | ret = append(ret, va.Index(i).Interface()) 460 | } 461 | return ret 462 | } 463 | 464 | //如果确定接收的是字符串类型的接口,去除字符串切片中的重复元素 465 | func DuplicateToStringSlice(fromInterface interface{}) ([]string, error) { 466 | var str []string 467 | ret := Duplicate(fromInterface) 468 | 469 | for _, v := range ret { 470 | t := reflect.TypeOf(v) 471 | if t.String() == "string" { 472 | str = append(str, reflect.ValueOf(v).String()) 473 | } else { 474 | return str, fmt.Errorf("ERROR: Interface Type convert to Strings Type Failed") 475 | } 476 | } 477 | return str, nil 478 | } 479 | 480 | // Get difference items between two slices 481 | func DiffStringSlices(slice1 []string, slice2 []string) []string { 482 | var diffStr []string 483 | m := map[string]int{} 484 | for _, s1Val := range slice1 { 485 | m[s1Val] = 1 486 | } 487 | for _, s2Val := range slice2 { 488 | m[s2Val] = m[s2Val] + 1 489 | } 490 | for mKey, mVal := range m { 491 | if mVal == 1 { 492 | diffStr = append(diffStr, mKey) 493 | } 494 | } 495 | 496 | return diffStr 497 | } 498 | 499 | // 接受用户输入,确认是否继续下一步操作 500 | func Confirm(str string) (bool, error) { 501 | var isTrue string 502 | fmt.Printf(str) 503 | fmt.Scanln(&isTrue) 504 | trueOrFalse, err := ParseBool(isTrue) 505 | if err != nil { 506 | return false, err 507 | } 508 | return trueOrFalse, nil 509 | } 510 | 511 | //从用户输入内容中解析 布尔值 true or false 512 | func ParseBool(str string) (value bool, err error) { 513 | switch str { 514 | case "1", "t", "T", "true", "TRUE", "True", "YES", "yes", "Yes", "y", "ON", "on", "On": 515 | return true, nil 516 | case "0", "f", "F", "false", "FALSE", "False", "NO", "no", "No", "n", "OFF", "off", "Off": 517 | return false, nil 518 | } 519 | return false, fmt.Errorf("Parsing ERROR: \"%s\" can't convert to 'true' or 'false'", str) 520 | } 521 | 522 | // 控制台输出颜色控制,兼容Windows & Linux 523 | func ColorPrint(logLevel string, textBefore interface{}, colorText string, textAfter ...interface{}) { 524 | color := ct.None 525 | switch logLevel { 526 | case "INFO": 527 | color = ct.Green 528 | case "WARNING": 529 | color = ct.Yellow 530 | case "ERROR": 531 | color = ct.Red 532 | } 533 | fmt.Printf("%s", textBefore) 534 | ct.Foreground(color, true) 535 | fmt.Printf("%s", colorText) 536 | ct.ResetColor() 537 | for _, v := range textAfter { 538 | fmt.Printf("%s", v) 539 | } 540 | } 541 | 542 | // 根据默认config.ini 543 | func Cfg(iniFilePath string) (*ini.File, error) { 544 | var cf *ini.File 545 | if _, err := GetRealPath(iniFilePath); err != nil { 546 | return cf, fmt.Errorf("%s not exist! please check", iniFilePath) 547 | } 548 | isConfigINIExist, _ := CheckDefaultINIFile("config.ini") 549 | if !isConfigINIExist { 550 | fmt.Println("Default config.ini not exist") 551 | return cf, fmt.Errorf("default config.ini not exist, please confirm") 552 | } 553 | iniFile, _ := filepath.Abs(iniFilePath) 554 | cfg, err := ini.LoadSources(ini.LoadOptions{IgnoreInlineComment: true}, iniFile) 555 | cfg.BlockMode = false 556 | 557 | if err != nil { 558 | return cf, fmt.Errorf("failed to read config file,please check:%v", err) 559 | } 560 | return cfg, nil 561 | } 562 | 563 | func PrintResultInTable(headers []string, data [][]string, maxTableCellWidth int) { 564 | tabulate := gotabulate.Create(data) 565 | tabulate.SetHeaders(headers) 566 | tabulate.SetAlign("center") 567 | tabulate.SetMaxCellSize(maxTableCellWidth) 568 | tabulate.SetWrapStrings(true) 569 | fmt.Println(tabulate.Render("grid")) 570 | } 571 | 572 | func PrintListHosts(hosts []string, maxTableCellWidth int, groupName ...string) { 573 | var headers []string 574 | var data [][]string 575 | headers = append(headers, "#", "Host") 576 | 577 | if len(groupName) != 0 { 578 | headers = append(headers, "Group Name") 579 | } 580 | todoHosts, err := DuplicateIPAddressCheck(hosts) 581 | if err != nil { 582 | fmt.Println(err) 583 | return 584 | } 585 | for i, v := range todoHosts { 586 | if len(groupName) != 0 { 587 | data = append(data, []string{strconv.Itoa(i + 1), v, groupName[0]}) 588 | } else { 589 | data = append(data, []string{strconv.Itoa(i + 1), v}) 590 | } 591 | } 592 | ColorPrint("INFO", "", ">>> Available Hosts", ":\n") 593 | PrintResultInTable(headers, data, maxTableCellWidth) 594 | } 595 | 596 | // format result with table style, supports output of the contents of the specified column 597 | func FormatResultWithTableStyle(res []interface{}, maxTableCellWidth int, notIncludedFields []string) { 598 | var header = []string{"#"} 599 | var headerAll, headerNotInclude, headerInclude []string 600 | var data [][]string 601 | if len(res) > 0 { 602 | iRes := res[0] 603 | typeName := reflect.TypeOf(iRes) 604 | if len(notIncludedFields) > 0 { 605 | for i := 0; i < typeName.NumField(); i++ { 606 | for _, f := range notIncludedFields { 607 | if len(f) == 0 { 608 | continue 609 | } 610 | if f == typeName.Field(i).Name { 611 | headerNotInclude = append(headerNotInclude, typeName.Field(i).Name) 612 | } 613 | } 614 | headerAll = append(headerAll, typeName.Field(i).Name) 615 | headerInclude = DiffStringSlices(headerNotInclude, headerAll) 616 | } 617 | header = append(header, headerInclude...) 618 | } else { 619 | for i := 0; i < typeName.NumField(); i++ { 620 | header = append(header, typeName.Field(i).Name) 621 | } 622 | } 623 | } 624 | for i, v := range res { 625 | value := reflect.ValueOf(v) 626 | typeName := reflect.TypeOf(v) 627 | var row []string 628 | index := strconv.Itoa(i + 1) 629 | row = append(row, index) 630 | 631 | for i := 0; i < value.NumField(); i++ { 632 | if len(notIncludedFields) > 0 { 633 | var r []string 634 | for _, f := range headerInclude { 635 | if f == typeName.Field(i).Name { 636 | r = append(r, value.Field(i).String()) 637 | } else { 638 | continue 639 | } 640 | } 641 | row = append(row, r...) 642 | } else { 643 | row = append(row, value.Field(i).String()) 644 | } 645 | } 646 | data = append(data, row) 647 | } 648 | PrintResultInTable(header, data, maxTableCellWidth) 649 | } 650 | 651 | func FormatResultWithBasicStyle(i int, res SSHResult) { 652 | ColorPrint("INFO", "", ">>> ", fmt.Sprintf("No.%d, ", i+1)) 653 | ColorPrint("INFO", "", "Host:", fmt.Sprintf("%s,", res.Host)) 654 | if res.Status == "success" { 655 | ColorPrint("INFO", " Status:", fmt.Sprintf("%s", res.Status)) 656 | } else { 657 | ColorPrint("ERROR", " Status:", fmt.Sprintf("%s", res.Status)) 658 | } 659 | ColorPrint("INFO", ", Results:\n", "", fmt.Sprintf("%s\n\n", res.Result)) 660 | } 661 | 662 | func LogSSHResultToFile(i int, res SSHResult, filePath string) { 663 | WriteAndAppendFile(filePath, fmt.Sprintf(">>> No.%d, Host: %s, Status: %s", i+1, res.Host, res.Status)) 664 | WriteAndAppendFile(filePath, fmt.Sprintf("Result: %s", res.Result)) 665 | } 666 | 667 | func SFTPFormatResultWithBasicStyle(i int, res SFTPResult) { 668 | ColorPrint("INFO", "", ">>> ", fmt.Sprintf("No.%d, ", i+1)) 669 | ColorPrint("INFO", "", "Host:", fmt.Sprintf("%s,", res.Host)) 670 | if res.Status == "success" { 671 | ColorPrint("INFO", " Status:", fmt.Sprintf("%s", res.Status)) 672 | } else { 673 | ColorPrint("ERROR", " Status:", fmt.Sprintf("%s", res.Status)) 674 | } 675 | ColorPrint("INFO", "", ", Source Path:", fmt.Sprintf("%s,", res.SourcePath)) 676 | ColorPrint("INFO", "", " Destination Path:", fmt.Sprintf("%s,", res.DestinationPath)) 677 | ColorPrint("INFO", " Results:\n", "", fmt.Sprintf("%s\n\n", res.Result)) 678 | } 679 | func LogSFTPResultToFile(i int, res SFTPResult, filePath string) { 680 | WriteAndAppendFile(filePath, fmt.Sprintf(">>> No.%d, Host: %s, Status: %s", i+1, res.Host, res.Status)) 681 | WriteAndAppendFile(filePath, fmt.Sprintf("Source Path: %s, Destination Path: %s", res.SourcePath, res.DestinationPath)) 682 | WriteAndAppendFile(filePath, fmt.Sprintf("Result: %s", res.Result)) 683 | } 684 | 685 | // ============================================================= 686 | // ResultLog format functions 687 | func ResultLogInfo(resultLog ResultLogs, startTime time.Time, logToFile bool, logFilePath string) { 688 | endTime := time.Now() 689 | resultLog.StartTime = startTime.Format("2006-01-02 15:04:05") 690 | resultLog.EndTime = endTime.Format("2006-01-02 15:04:05") 691 | resultLog.CostTime = endTime.Sub(startTime).String() 692 | resultLog.TotalHostsInfo = fmt.Sprintf("%d(Success) + %d(Failed) = %d(Total)", len(resultLog.SuccessHosts), len(resultLog.ErrorHosts), len(resultLog.SuccessHosts)+len(resultLog.ErrorHosts)) 693 | if logToFile { 694 | WriteAndAppendFile(logFilePath, fmt.Sprintf("Tips: process running done.")) 695 | WriteAndAppendFile(logFilePath, fmt.Sprintf("\nStart Time: %s\nEnd Time: %s\nCost Time: %s\nTotal Hosts Running: %s\n", resultLog.StartTime, resultLog.EndTime, resultLog.CostTime, resultLog.TotalHostsInfo)) 696 | } else { 697 | ColorPrint("INFO", "", "Tips: ", fmt.Sprintf("Process running done.\n")) 698 | fmt.Printf("Start Time: %s\nEnd Time: %s\nCost Time: %s\nTotal Hosts Running: %s\n", resultLog.StartTime, resultLog.EndTime, resultLog.CostTime, resultLog.TotalHostsInfo) 699 | } 700 | } 701 | func FormatResultLogWithSimpleStyle(resultLog ResultLogs, startTime time.Time, maxTableCellWidth int, notIncludeTableFields []string) { 702 | if len(resultLog.ErrorHosts) > 0 { 703 | ColorPrint("ERROR", "", "WARNING: ", "Failed hosts, please confirm!\n") 704 | FormatResultWithTableStyle(resultLog.ErrorHosts, maxTableCellWidth, notIncludeTableFields) 705 | } 706 | ResultLogInfo(resultLog, startTime, false, "") 707 | } 708 | 709 | // format result log with table style 710 | func FormatResultLogWithTableStyle(chs []chan interface{}, resultLog ResultLogs, startTime time.Time, maxTableCellWidth int) { 711 | resultStatus := "" 712 | for _, resCh := range chs { 713 | result := <-resCh 714 | switch reflect.TypeOf(result).String() { 715 | case "utils.SSHResult": 716 | resultStatus = result.(SSHResult).Status 717 | case "utils.SFTPResult": 718 | resultStatus = result.(SFTPResult).Status 719 | } 720 | if resultStatus == "failed" { 721 | resultLog.ErrorHosts = append(resultLog.ErrorHosts, result) 722 | 723 | } else { 724 | resultLog.SuccessHosts = append(resultLog.SuccessHosts, result) 725 | } 726 | } 727 | 728 | if len(resultLog.SuccessHosts) > 0 { 729 | ColorPrint("INFO", "", "INFO: ", "Success hosts\n") 730 | FormatResultWithTableStyle(resultLog.SuccessHosts, maxTableCellWidth, []string{}) 731 | } 732 | if len(resultLog.ErrorHosts) > 0 { 733 | ColorPrint("ERROR", "", "WARNING: ", "Failed hosts, please confirm!\n") 734 | FormatResultWithTableStyle(resultLog.ErrorHosts, maxTableCellWidth, []string{}) 735 | } 736 | ResultLogInfo(resultLog, startTime, false, "") 737 | } 738 | 739 | func GetAllResultLog(chs []chan interface{}, resultLog ResultLogs, startTime time.Time) ResultLogs { 740 | resultStatus := "" 741 | for _, resCh := range chs { 742 | result := <-resCh 743 | switch reflect.TypeOf(result).String() { 744 | case "utils.SSHResult": 745 | resultStatus = result.(SSHResult).Status 746 | case "utils.SFTPResult": 747 | resultStatus = result.(SFTPResult).Status 748 | } 749 | if resultStatus == "failed" { 750 | resultLog.ErrorHosts = append(resultLog.ErrorHosts, result) 751 | 752 | } else { 753 | resultLog.SuccessHosts = append(resultLog.SuccessHosts, result) 754 | } 755 | } 756 | endTime := time.Now() 757 | resultLog.StartTime = startTime.Format("2006-01-02 15:04:05") 758 | resultLog.EndTime = endTime.Format("2006-01-02 15:04:05") 759 | resultLog.CostTime = endTime.Sub(startTime).String() 760 | resultLog.TotalHostsInfo = fmt.Sprintf("%d(Success) + %d(Failed) = %d(Total)", len(resultLog.SuccessHosts), len(resultLog.ErrorHosts), len(resultLog.SuccessHosts)+len(resultLog.ErrorHosts)) 761 | 762 | return resultLog 763 | } 764 | 765 | // format result log with json style 766 | func FormatResultToJson(logs []ResultLogs, isJsonRaw bool) { 767 | b, err := json.Marshal(logs) 768 | if err != nil { 769 | fmt.Println("json err:", err) 770 | return 771 | } 772 | if isJsonRaw != false { 773 | fmt.Println(string(b)) 774 | return 775 | } 776 | var out bytes.Buffer 777 | err = json.Indent(&out, b, "", " ") 778 | if err != nil { 779 | fmt.Println("Json Format ERROR:", err) 780 | } 781 | out.WriteTo(os.Stdout) 782 | return 783 | } 784 | func FormatResultLogWithJsonStyle(chs []chan interface{}, resultLog ResultLogs, startTime time.Time, isJsonRaw bool) { 785 | var allResultLog []ResultLogs 786 | resLog := GetAllResultLog(chs, resultLog, startTime) 787 | allResultLog = append(allResultLog, resLog) 788 | FormatResultToJson(allResultLog, isJsonRaw) 789 | return 790 | } 791 | 792 | func WriteAndAppendFile(filePath, strContent string) { 793 | strTime := GetCurrentDateNumbers 794 | if filePath == "log" { 795 | filePath = fmt.Sprintf("ssgo-%s.log", strTime) 796 | } 797 | f, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) 798 | if err != nil { 799 | fmt.Println("ERROR: write file failed:", err) 800 | return 801 | } 802 | appendTime := time.Now().Format("2006-01-02 15:04:05") 803 | fileContent := strings.Join([]string{appendTime, strContent, "\n"}, " ") 804 | buf := []byte(fileContent) 805 | f.Write(buf) 806 | f.Close() 807 | } 808 | 809 | func GetCurrentDateNumbers() (strTime string) { 810 | currTime := time.Now() 811 | strFormatTime := currTime.Format("2006-01-02") 812 | strTime = strings.Replace(strFormatTime, "-", "", -1) 813 | return 814 | } 815 | --------------------------------------------------------------------------------