├── .gitignore ├── LICENSE ├── README.md ├── README_EN.md ├── generate_config.py ├── install.sh ├── requirements.txt ├── server_config.py ├── server_tag.py ├── settings.py ├── ssh_opt_parse.sh ├── template_config.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__ 3 | .DS_STORE 4 | .idea/ 5 | server_config.json 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Shawn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # server tag 3 | *server tag 是一个给服务器打标签的 iterm2 插件* 4 | 5 | ![GitHub](https://img.shields.io/github/license/shawn-bluce/server_tag) 6 | ![GitHub top language](https://img.shields.io/github/languages/top/shawn-bluce/server_tag) 7 | ![GitHub repo size](https://img.shields.io/github/repo-size/shawn-bluce/server_tag) 8 | 9 | **[English README.md](https://github.com/shawn-bluce/server_tag/blob/master/README_EN.md)** 10 | 11 | ## 功能 12 | 我们经常要登录到各种不同的服务器上,有些是自己个人的、有些是公司的测试环境、有些是公司的生产环境、有些又是开发环境等等。。。。当你使用的服务器多起来的时候就几乎不可避免的迷失在多个服务器之间,terminal 开了好多却分不清哪个是哪个,一不小心来了一手 `rm -rf` 当场人就没了。这个插件就是帮你实时的标记出当前操作的 tab 具体是连接到了哪台服务器上。 13 | 14 | ## 效果展示 15 | 这里可以看到 4 个 tab 分别有不同的颜色。具体的颜色和 terminal 右上角的内容(badge)都是可以自定义的,每一组服务器配置一个颜色,每一个 ip 配置一个badge。 16 | 17 | 图片可以点,是一段视频演示效果 18 | [![ScreenShot](https://raw.githubusercontent.com/shawn-bluce/pics_home/master/20200822140557.png)](https://www.bilibili.com/video/BV1r64y1c7su/) 19 | 20 | ## 快速开始 21 | 1. 检查 Python 版本和 pip 版本,该插件仅支持 Python3. 可以使用 `python3 --version && pip --version` 来检查版本 22 | 2. 安装 `iterm2` 的官方库,使用 `pip3 install iterm2 --user` 23 | 3. 克隆项目到本地,例如`/Users/shawn/Workstadion/server_tag/` 24 | 4. 运行 `install.sh` 脚本(不成功的话可以尝试手动安装) 25 | 26 | ## 手动安装 27 | 1. 检查 Python 版本和 pip 版本,该插件仅支持 Python3. 可以使用 `python3 --version && pip --version` 来检查版本 28 | 2. 安装 `iterm2` 的官方库,使用 `pip3 install iterm2 --user` 29 | 3. 克隆项目到本地,例如`/Users/shawn/Workstadion/server_tag/` 30 | 4. 按需编辑你的`.bashrc`或者`.zshrc`(看你用的是哪个 shell),添加`alias ssh="xxx/server_tag/server_tag.py"` 31 | 5. 如果想要保留原始的 ssh 命令,可以在上面的 alias 下添加`alias _ssh="/usr/bin/ssh"` (以后使用原始 ssh 就用 \_sshi 替代了) 32 | 6. 生成配置文件 `python3 generate_config.py` 33 | 7. 重启你的 iterm2, 然后尝试使用 ssh 登录一台服务器试试看🎉 34 | 35 | ## 管理服务器 36 | 服务器的配置是项目目录的`server_config.json`文件,如果没有则使用`python3 generate_config.py`创建。大家都是程序员,这里就不过多解释文件格式的问题了。 37 | 38 | ## 配置文件 39 | * json 文件最外层的是一组组的数据,就比如默认生成的配置文件里的 product 和 testing 40 | * 每一组服务器下的 color 就是指的 tab 和 badge 的颜色 41 | * 每一组服务器下的server 下层是一个个的服务器 42 | * 因为同功能的服务器可能有多台,所以一个名字下可以配置多个 ip 43 | 44 | ## 颜色 45 | 可以在这个地址方便的找到并生成需要的 RGB 颜色值:[https://www.w3schools.com/colors/colors_rgb.asp](https://www.w3schools.com/colors/colors_rgb.asp)。 46 | 47 | -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | # server tag 2 | *server tag is tag for server plugin on iterm2.* 3 | 4 | ![GitHub](https://img.shields.io/github/license/shawn-bluce/server_tag) 5 | ![GitHub top language](https://img.shields.io/github/languages/top/shawn-bluce/server_tag) 6 | ![GitHub repo size](https://img.shields.io/github/repo-size/shawn-bluce/server_tag) 7 | 8 | ## function 9 | Color your tab and show server name on your iterm, when you ssh to server. 10 | 11 | ## show 12 | [![ScreenShot](https://raw.githubusercontent.com/shawn-bluce/pics_home/master/20200822140557.png)](https://www.bilibili.com/video/BV1r64y1c7su/) 13 | 14 | ## quick start 15 | 1. check your python and pip using python3. **only support Python 3.** `python3 --version && pip3 --version` 16 | 2. install iterm2 library with python, `pip3 install iterm2 --user` 17 | 3. clone this project to your workspace 18 | 4. use `install.sh` to install. you can manual install if this script is not work. 19 | 20 | ## manual install 21 | 1. check your python and pip using python3. **only support Python 3.** `python3 --version && pip3 --version` 22 | 2. install iterm2 library with python, `pip3 install iterm2 --user` 23 | 3. clone this project to your workspace 24 | 4. edit .zshrc or .bashrc, add `alias ssh="xxx/server_tag/server_tag.py"` 25 | 5. edit .zshrc or .bashrc, add source `alias _ssh="/usr/bin/ssh"`(optional) 26 | 6. restart iterm2, test and check result for `ssh user@host` 27 | 28 | ## manage server 29 | Edit `server_config.json`. Outermost is server group, all server in same group use same color. 30 | 31 | ## color 32 | You can get RGB color on [https://www.w3schools.com/colors/colors_rgb.asp](https://www.w3schools.com/colors/colors_rgb.asp). 33 | -------------------------------------------------------------------------------- /generate_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # generate template file config.json, when it not exists. 3 | # run this command: `python3 generate_config.py` 4 | 5 | import os 6 | import json 7 | 8 | from utils import color_text 9 | from settings import config_file_name 10 | from template_config import config as template_config 11 | 12 | if __name__ == '__main__': 13 | file_content = json.dumps(template_config, indent=4) 14 | if os.path.exists(config_file_name): 15 | color_text.error('[error] {} is already exists, not generate.'.format(config_file_name)) 16 | exit(1) 17 | else: 18 | open(config_file_name, 'w').write(file_content) 19 | color_text.success('[success] {} is generated.'.format(config_file_name)) 20 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #/usr/bin/env bash 2 | 3 | # choice shell 4 | if [ "$SHELL" == "/bin/bash" ] 5 | then 6 | CONFIG_FILE="$HOME/.bashrc" 7 | elif [ "$SHELL" == "/bin/zsh" ] 8 | then 9 | CONFIG_FILE="$HOME/.zshrc" 10 | else 11 | echo "NOT SUPPORT THIS SHELL: $SHELL" 12 | exit 1 13 | fi 14 | 15 | EXEC_PY_FILE="$(pwd)/server_tag.py" 16 | 17 | # write config 18 | echo "write config" 19 | { 20 | echo "#server_tag alias config" 21 | echo "alias _ssh=\"/usr/bin/ssh\"" 22 | echo "alias ssh=\"$EXEC_PY_FILE\"" 23 | } >> "$CONFIG_FILE" 24 | echo "write done" 25 | 26 | echo "generate config to $(pwd)/server_config.json" 27 | python3 "$(pwd)/generate_config.py" 28 | echo "generate done" 29 | 30 | # finish 31 | echo "install SUCCESS, reopen your iterm2." -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | iterm2==1.18 2 | -------------------------------------------------------------------------------- /server_config.py: -------------------------------------------------------------------------------- 1 | 2 | import json 3 | import iterm2 4 | 5 | from utils import color_text 6 | from settings import config_file_name 7 | 8 | 9 | class ServerConfig: 10 | def __init__(self, name: str, host: str, r: int, g: int, b: int): 11 | self.name = name 12 | self.host = host 13 | self.color = iterm2.Color(r, g, b) 14 | 15 | def __str__(self): 16 | return 'id:{}, server_name:{}, host:{}, color:{}'.format( 17 | id(self), self.name, self.host, self.color 18 | ) 19 | 20 | 21 | def get_server_list(): 22 | server_list = [] 23 | try: 24 | content = open(config_file_name).read() 25 | config = json.loads(content) 26 | except FileNotFoundError: 27 | color_text.warning('[WARNING] use default config, have no config file, ' 28 | 'you can run `python3 generate_config.py` to create it.') 29 | return [] 30 | except json.JSONDecodeError: 31 | color_text.error('[ERROR] use default config, because {} is invalid json data'.format(config_file_name)) 32 | return [] 33 | 34 | for group_name in config: 35 | group = config.get(group_name) 36 | color = group.get('color') 37 | r = color.get('R') 38 | g = color.get('G') 39 | b = color.get('B') 40 | for server_name, host_list in group.get('server').items(): 41 | for server_host in host_list: 42 | server = ServerConfig( 43 | server_name, 44 | server_host, 45 | r, g, b 46 | ) 47 | server_list.append(server) 48 | 49 | return server_list 50 | -------------------------------------------------------------------------------- /server_tag.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import ipaddress 5 | import subprocess 6 | 7 | from settings import default_tab_color 8 | from utils import get_ip_by_host, color_text, parse_ssh_command 9 | 10 | try: 11 | import iterm2 12 | from server_config import get_server_list 13 | except (ModuleNotFoundError, ImportError) as e: 14 | color_text.error(e) 15 | exit() 16 | 17 | 18 | def ip_math_rule(ip_addr, server_host): 19 | """ 20 | check target ip math server_host rule 21 | :param ip_addr: target ip address 22 | :param server_host: server_config host rule 23 | :return: 24 | """ 25 | if '/' in server_host: 26 | ip_set = ipaddress.IPv4Network(server_host, strict=False) 27 | for ip in ip_set.hosts(): 28 | if ip_addr == ip.compressed: 29 | return True 30 | return False 31 | else: 32 | return ip_addr == server_host 33 | 34 | 35 | def get_host_config(host: str) -> tuple: 36 | """ 37 | :param host: domain or ip or ssh_config 38 | :return: tuple(host_name, iterm2_color) 39 | """ 40 | ip_addr = get_ip_by_host(host) 41 | for server in get_server_list(): 42 | if ip_math_rule(ip_addr, server.host): 43 | return server.name, server.color 44 | return host, default_tab_color 45 | 46 | 47 | async def main(connection): 48 | app = await iterm2.async_get_app(connection) 49 | session = app.current_terminal_window.current_tab.current_session 50 | change = iterm2.LocalWriteOnlyProfile() 51 | command = 'ssh ' + ' '.join(sys.argv[1:]) 52 | host = parse_ssh_command(full_command=command) 53 | 54 | alias, color = get_host_config(host) 55 | 56 | # set config 57 | change.set_badge_text(alias) 58 | change.set_tab_color(color) 59 | change.set_use_tab_color(True) 60 | change.set_badge_color(color) 61 | 62 | # apply new config for iterm2 and ssh to server 63 | await session.async_set_profile_properties(change) 64 | subprocess.call(command.split()) 65 | 66 | # revert config 67 | change.set_badge_text('') 68 | change.set_use_tab_color(False) 69 | await session.async_set_profile_properties(change) 70 | 71 | 72 | if __name__ == '__main__': 73 | iterm2.run_until_complete(main) 74 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | import iterm2 2 | 3 | 4 | default_tab_color = iterm2.Color(100, 149, 237) 5 | config_file_name = __file__.replace('settings.py', 'server_config.json') 6 | -------------------------------------------------------------------------------- /ssh_opt_parse.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # this script from https://github.com/greymd/ssh_opt_parse 3 | set -u 4 | 5 | ## Options work individually 6 | readonly SSH_SINGLE_OPTS="[1246AaCfGgKkMNnqsTtVvXxYy]" 7 | ## Options require its argument(s). 8 | readonly SSH_MULTI_OPTS="[bcDEeFIiJLlmOopQRSWw]" 9 | readonly SSH_CONFIG_FILE=${SSH_CONFIG_FILE:-${HOME}/.ssh/config} 10 | 11 | SSH_PORT= 12 | SSH_USER= 13 | SSH_HOST= 14 | SSH_COMMAND=() 15 | 16 | ssh_opt_show_result () { 17 | printf "SSH_PORT\\t%s\\n" "${SSH_PORT}" 18 | printf "SSH_USER\\t%s\\n" "${SSH_USER}" 19 | printf "SSH_HOST\\t%s\\n" "${SSH_HOST-}" 20 | printf "SSH_COMMAND\\t%s\\n" "${SSH_COMMAND[*]## }" 21 | } 22 | 23 | ssh_opt_parse_options () { 24 | while (( $# > 0 )) ;do 25 | case "$1" in 26 | --) 27 | break 28 | ;; 29 | 30 | -*) 31 | if [[ "$1" =~ -${SSH_SINGLE_OPTS}*p$ ]]; then 32 | SSH_PORT="$2" 33 | shift 34 | shift 35 | elif [[ "$1" =~ -${SSH_SINGLE_OPTS}*p. ]]; then 36 | SSH_PORT=$(sed "s/-${SSH_SINGLE_OPTS}*p//" <<<"$1") 37 | shift 38 | elif [[ "$1" =~ -${SSH_SINGLE_OPTS}*l$ ]] ;then 39 | SSH_USER="$2" 40 | shift 41 | shift 42 | elif [[ "$1" =~ -${SSH_SINGLE_OPTS}*l. ]] ;then 43 | SSH_USER=$(sed "s/-${SSH_SINGLE_OPTS}*l//" <<<"$1") 44 | shift 45 | elif [[ "$1" =~ -${SSH_SINGLE_OPTS}+$ ]] ;then 46 | shift 47 | elif [[ "$1" =~ -${SSH_SINGLE_OPTS}*${SSH_MULTI_OPTS}$ ]] ;then 48 | shift 49 | shift 50 | fi 51 | ;; 52 | 53 | *) 54 | if [[ -n "${SSH_HOST}" ]] ; then 55 | break 56 | fi 57 | if [[ -z "${SSH_HOST}" ]] && [[ "$1" =~ ^.*@.*$ ]] ; then 58 | SSH_USER="${1%%@*}" 59 | fi 60 | if [[ -z "${SSH_HOST}" ]] ;then 61 | SSH_HOST="${1##*@}" 62 | fi 63 | shift 64 | ;; 65 | esac 66 | done 67 | # Rest of arguments may be command line 68 | if (( $# > 0 )); then 69 | SSH_COMMAND=("$1") 70 | shift 71 | fi 72 | for _arg in "$@"; do 73 | SSH_COMMAND=("${SSH_COMMAND[@]-}" "$_arg") 74 | done 75 | 76 | if [[ -n "${SSH_HOST}" ]]; then 77 | if [[ -z "${SSH_USER}" ]] ;then 78 | SSH_USER=$(ssh_opt_get_value_from_config "${SSH_HOST}" "User") 79 | # If there is no record for the host in the ~/.ssh/config. 80 | if [[ $? -eq 1 ]]; then 81 | SSH_USER=$(whoami) 82 | fi 83 | fi 84 | 85 | if [[ -z "${SSH_PORT}" ]] ;then 86 | SSH_PORT=$(ssh_opt_get_value_from_config "${SSH_HOST}" "Port") 87 | # If there is no record for the host in the ~/.ssh/config. 88 | if [[ $? -eq 1 ]]; then 89 | SSH_PORT=22 90 | fi 91 | fi 92 | fi 93 | } 94 | 95 | ssh_opt_get_value_from_config () { 96 | local _host="$1" 97 | local _key="$2" 98 | perl -anpe 's/^\s+//' "${SSH_CONFIG_FILE}" \ 99 | | grep -v '^#' \ 100 | | perl -anle '$F[0] =~ /^(HOST|MATCH)$/i and $key=$F[1]; print "$key\t$_"' \ 101 | | perl -sanle '$F[0] eq $host and $F[1] =~ /^$key$/i and print $F[2]' -- -host="${_host}" -key="${_key}" \ 102 | | grep . 103 | return $? 104 | } 105 | 106 | ssh_opt_parse () { 107 | if ! [[ "$1" =~ ssh ]]; then 108 | return 109 | fi 110 | shift # remove beginning of "ssh" 111 | ssh_opt_parse_options "${1+"$@"}" 112 | ssh_opt_show_result 113 | } 114 | 115 | ## ------------------------- 116 | ## Entry point 117 | ## ------------------------- 118 | ssh_opt_parse "${1+"$@"}" -------------------------------------------------------------------------------- /template_config.py: -------------------------------------------------------------------------------- 1 | """ 2 | the template for server_tag config, don't edit this file. 3 | yourself config file is 'server_config.json', if you have no this file, 4 | can run this command: 'python3 generate_config.py' to create it. 5 | """ 6 | 7 | config = { 8 | 'product': { 9 | 'color': { 10 | 'R': 255, 11 | 'G': 0, 12 | 'B': 0 13 | }, 14 | 'server': { 15 | 'web': [ 16 | '192.168.1.1', 17 | '192.168.1.2' 18 | ], 19 | 'db': [ 20 | '192.168.1.3', 21 | '192.168.1.4' 22 | ] 23 | } 24 | }, 25 | 'testing': { 26 | 'color': { 27 | 'R': 0, 28 | 'G': 255, 29 | 'B': 0, 30 | }, 31 | 'server': { 32 | 'web': [ 33 | '192.168.1.5', 34 | '192.168.1.6' 35 | ] 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import socket 4 | import subprocess 5 | 6 | 7 | class ColorText: 8 | def __init__(self): 9 | self.warning_head = '\033[93m' 10 | self.success_head = '\033[92m' 11 | self.error_head = '\033[91m' 12 | self.end = '\033[0m' 13 | 14 | def success(self, text): 15 | print('{}{}{}'.format(self.success_head, text, self.end)) 16 | 17 | def warning(self, text): 18 | print('{}{}{}'.format(self.warning_head, text, self.end)) 19 | 20 | def error(self, text): 21 | print('{}{}{}'.format(self.error_head, text, self.end)) 22 | 23 | 24 | def analysis_ssh_config(host: str) -> str: 25 | """if host in ~/.ssh/config, get it real hostname""" 26 | home_path = os.environ.get('HOME') 27 | ssh_config_path = home_path + '/.ssh/config' 28 | 29 | if not os.path.exists(ssh_config_path): 30 | return host 31 | 32 | ssh_config_lines = open(ssh_config_path).read().split('\n') 33 | for index, line in enumerate(ssh_config_lines): 34 | if line.strip() == 'Host {}'.format(host): 35 | while line: 36 | line = ssh_config_lines[index + 1].strip() 37 | key, host = line.split() 38 | if key == 'HostName': 39 | return host 40 | 41 | 42 | def get_ip_by_host(host: str) -> str: 43 | """ 44 | :param host: domain or ip or ssh_config 45 | :return: ip address 46 | """ 47 | 48 | # try to get host from ssh_config, if has ssh_config 49 | host_from_ssh_config = analysis_ssh_config(host) 50 | host = host_from_ssh_config or host 51 | return socket.gethostbyname(host) 52 | 53 | 54 | def parse_ssh_command(full_command: str) -> str: 55 | awk_parm = '{print $2}' 56 | dir_path = os.path.dirname(os.path.realpath(__file__)) 57 | cmd = f"{dir_path}/ssh_opt_parse.sh {full_command} | grep 'SSH_HOST' | awk -F ' ' '{awk_parm}'" 58 | host = subprocess.check_output(cmd, shell=True).strip() 59 | if isinstance(host, bytes): 60 | host = host.decode('utf-8') 61 | return host 62 | 63 | 64 | color_text = ColorText() 65 | --------------------------------------------------------------------------------