前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用结构化数据管理 SSH 配置:SSH Config Tool

使用结构化数据管理 SSH 配置:SSH Config Tool

作者头像
soulteary
发布2024-10-18 17:10:59
1050
发布2024-10-18 17:10:59
举报
文章被收录于专栏:为了不折腾而去折腾的那些事

随着折腾的设备和云服务器越来越多,我们本地的 SSH Config 配置越来越复杂,为了解决这个问题,最近做了一些简单的尝试。

写在前面

本文提到的开源软件 soulteary/ssh-config[1],为了让使用更放心,项目代码的单元测试覆盖 100%,确保数据转换是幂等、可靠的,能够放心的长时间使用。感兴趣可以自取,欢迎一键三连。

SSH Config Tool

熟悉我的朋友知道我换电子设备比较勤快[2],这几年敲字使用的 Mac 应该也已经更换了四五台左右。但是,不论我的“打字机”换成了哪一款,我都会在当前设备中用 Git 初始化一个 ~/.ssh 目录,导入包含了我的 RSA 密钥和绝大多数的服务器 SSH 连接配置。

这个目录的大致情况如下(主要配置包含config 文件和 config.d/* 文件目录中的文件):

代码语言:javascript
复制
# tree -L 4 ~/.ssh/       

/Users/soulteary/.ssh/
├── config
├── config.d
│   ├── aliyun
│   ├── discard
│   ├── ...
│   ├── homelab
│   └── tencent
├── keys
│   ├── github
│   │   ├── id_rsa_github
│   │   └── id_rsa_github.pub
│   └── devices
│       ├── devices-2018
│       ├── devices-2018.pub
│       ├── devices-2020
│       ├── devices-2020.pub
│       ├── devices-2022
│       ├── devices-2022.pub
│       ├── devices-2024
│       └── devices-2024.pub
├── known_hosts
├── logo.png
└── template.d
    └── keep-connect.conf

随着时间推移,折腾的 HomeLab 设备、虚拟机和云服务器越来越多,这个 .ssh 目录中的配置文件也越来越冗余。

如果使用命令行来统计行数,配置内容有 500 多行的内容:

代码语言:javascript
复制
# cat ~/.ssh/config ~/.ssh/config.d/* | wc -l

528

而配置中的内容,其实非常的无聊,都是模版化的服务器连接参数堆叠:

代码语言:javascript
复制
# cat ~/.ssh/config                                             

Host *
  HostkeyAlgorithms +ssh-rsa
  PubkeyAcceptedAlgorithms +ssh-rsa

# 服务器 1
Host server1
  Hostname 123.123.123.123
  Port 1234
  IdentityFile ~/.ssh/keys/your-key
  ControlPath     ~/.ssh/server1-%r@%h:%p
  ControlPersist  yes
  TCPKeepAlive    yes
  Compression     yes
  ForwardAgent    yes

# 服务器 2
Host server2
  Hostname 123.234.234.234
  Port 1234
  IdentityFile ~/.ssh/keys/your-key
  ControlPath     ~/.ssh/server2-%r@%h:%p
  ControlPersist  yes
  TCPKeepAlive    yes
  Compression     yes
  ForwardAgent    yes

# 其他配置
Include config.d/homelab
Include config.d/aliyun
Include config.d/tencent
...

为了解决这个问题,我写了一个简单的命令行工具 ssh-config,在将工具转换后的配置进行简单的调整后,具备清晰明了结构的 YAML 配置文件行数缩短到了之前的 25%(还有进一步优化空间)。从这个 YAML 配置文件转换出的 OpenSSH 使用的 SSH Config 行数,也只有 140 行左右。

代码语言:javascript
复制
# cat test.yaml | wc -l

130

# cat test.yaml | ssh-config -to-ssh | wc -l

143

随便举个例子,如果我们想管理三台两两之间有相似配置的服务器,这个配置将类似这样,而非充斥大量冗余配置的 SSH Config,而这个配置可以 100% 稳定转换为幂等的 SSH Config:

代码语言:javascript
复制
global:
  HostKeyAlgorithms: +ssh-rsa
  PubkeyAcceptedAlgorithms: +ssh-rsa

default:
  Compression: "yes"
  ControlPersist: "yes"
  ForwardAgent: "yes"
  Port: "1234"
  TCPKeepAlive: "yes"

Group server1:
  Common:
    ControlPath: ~/.ssh/server-1-%r@%h:%p
  Hosts:
    server1:
      Notes: your notes here
      config:
        HostName: 123.123.123.123
        IdentityFile: ~/.ssh/keys/your-key1
    server3:
      Notes: new server
      config:
        HostName: 123.124.123.124
        IdentityFile: ~/.ssh/keys/your-key3

Group server2:
  Hosts:
    server2:
      config:
        ControlPath: ~/.ssh/server-2-%r@%h:%p
        HostName: 123.234.123.234
        IdentityFile: ~/.ssh/keys/your-key2
        User: ubuntu

接下来,我将分别聊聊这个工具如何使用,以及制作过程中的一些有趣的事情。

SSH Config 配置管理工具的使用

首先是软件的获取,如果你熟悉 Docker,那么我推荐你使用 Docker。如果你是极简主义者,也可以从 GitHub 发布页面[3]下载合适你的系统、CPU 架构的二进制文件。

软件的使用有两种模式,第一种是普通的命令行加参数:

代码语言:javascript
复制
ssh-config [options] <input_file> <output_file>

如果我们想要把 ~/.ssh/ 目录中所有的配置都转换为 YAML 格式,并保存到当前目录的 test.yaml 文件中:

代码语言:javascript
复制
ssh-config -to-yaml -src ~/.ssh/ -dest test.yaml

如果你不想保存到某个文件中,而是想直接查看转换结果,只需要去掉 -dest 参数,转换文件的内容就直接展示出来啦:

代码语言:javascript
复制
# ssh-config -to-yaml -src /.ssh/

global:
  HostKeyAlgorithms: +ssh-rsa
  PubkeyAcceptedAlgorithms: +ssh-rsa
Group server1:
  Hosts:
    server1:
      Notes: your notes here
      config:
        Compression: "yes"
        ControlPath: ~/.ssh/server-1-%r@%h:%p
        ControlPersist: "yes"
        ForwardAgent: "yes"
        HostName: 123.123.123.123
        IdentityFile: ~/.ssh/keys/your-key1
        Port: "1234"
        TCPKeepAlive: "yes"
Group server2:
  Hosts:
    server2:
      config:
        Compression: "yes"
        ControlPath: ~/.ssh/server-2-%r@%h:%p
        ControlPersist: "yes"
        ForwardAgent: "yes"
        HostName: 123.234.123.234
        IdentityFile: ~/.ssh/keys/your-key2
        Port: "1234"
        TCPKeepAlive: "yes"
        User: ubuntu

当然,如果你喜欢 Linux 管道,那么你可以使用下面的方式,来实现上面的命令相同的功能:

代码语言:javascript
复制
# 保存到某个文件
cat ~/.ssh/config ~/.ssh/config.d/* | ssh-config -to-yaml > test.yaml

# 直接查看转换结果
cat ~/.ssh/config ~/.ssh/config.d/* | ssh-config -to-yaml

上面的配置是不是看起来有一些冗余?小学的时候,我们就学过一个技巧,叫做“提取公因数”,这个技巧,我们可以用来改进这个配置文件。

优化转换后的配置

上面的配置中,我们能够看到程序将每一个 Hosts 都转换为了一个单独的分组。每个分组中都有一些共同的配置,我们可以将它们挪到全局使用的配置 default 字段中。

代码语言:javascript
复制
global:
  HostKeyAlgorithms: +ssh-rsa
  PubkeyAcceptedAlgorithms: +ssh-rsa

default:
  Compression: "yes"
  ControlPersist: "yes"
  ForwardAgent: "yes"
  Port: "1234"
  TCPKeepAlive: "yes"

Group server1:
  Hosts:
    server1:
      Notes: your notes here
      config:
        ControlPath: ~/.ssh/server-1-%r@%h:%p
        HostName: 123.123.123.123
        IdentityFile: ~/.ssh/keys/your-key1

Group server2:
  Hosts:
    server2:
      config:
        ControlPath: ~/.ssh/server-2-%r@%h:%p
        HostName: 123.234.123.234
        IdentityFile: ~/.ssh/keys/your-key2
        User: ubuntu

当我们将文件保存为 test.yaml 后,执行转换命令:

代码语言:javascript
复制
cat test.yaml| ./ssh-config -to-ssh

就能够得到下面的配置转换结果啦:

代码语言:javascript
复制
Host *
    HostKeyAlgorithms +ssh-rsa
    PubkeyAcceptedAlgorithms +ssh-rsa

# your notes here
Host server1
    Compression yes
    ControlPath ~/.ssh/server-1-%r@%h:%p
    ControlPersist yes
    ForwardAgent yes
    HostName 123.123.123.123
    IdentityFile ~/.ssh/keys/your-key1
    Port 1234
    TCPKeepAlive yes

Host server2
    Compression yes
    ControlPath ~/.ssh/server-2-%r@%h:%p
    ControlPersist yes
    ForwardAgent yes
    HostName 123.234.123.234
    IdentityFile ~/.ssh/keys/your-key2
    Port 1234
    TCPKeepAlive yes
    User ubuntu

你或许会想问,如果我想给每个分组都有自己的额外的“共同配置”,那么该如何处理呢?举个例子,我们在上面的配置中,再添加一台服务器:

代码语言:javascript
复制
global:
  HostKeyAlgorithms: +ssh-rsa
  PubkeyAcceptedAlgorithms: +ssh-rsa

default:
  Compression: "yes"
  ControlPersist: "yes"
  ForwardAgent: "yes"
  Port: "1234"
  TCPKeepAlive: "yes"

Group server1:
  Hosts:
    server1:
      Notes: your notes here
      config:
        ControlPath: ~/.ssh/server-1-%r@%h:%p
        HostName: 123.123.123.123
        IdentityFile: ~/.ssh/keys/your-key1
    server3:
      Notes: new server
      config:
        ControlPath: ~/.ssh/server-1-%r@%h:%p
        HostName: 123.124.123.124
        IdentityFile: ~/.ssh/keys/your-key3

Group server2:
  Hosts:
    server2:
      config:
        ControlPath: ~/.ssh/server-2-%r@%h:%p
        HostName: 123.234.123.234
        IdentityFile: ~/.ssh/keys/your-key2
        User: ubuntu

上面的配置中,我们能够看到 “Group server 1” 有两台服务器连接配置,“server 1” 和 “server 3” 中有一些相似配置。这些配置和其他分组的服务器不一样,和上文一样,使用全局配置管理,有一些不合适。

我们可以在分组下的 Common 字段中,将小组内的“公因数”提取出来:

代码语言:javascript
复制
global:
  HostKeyAlgorithms: +ssh-rsa
  PubkeyAcceptedAlgorithms: +ssh-rsa

default:
  Compression: "yes"
  ControlPersist: "yes"
  ForwardAgent: "yes"
  Port: "1234"
  TCPKeepAlive: "yes"

Group server1:
  Common:
    ControlPath: ~/.ssh/server-1-%r@%h:%p
  Hosts:
    server1:
      Notes: your notes here
      config:
        HostName: 123.123.123.123
        IdentityFile: ~/.ssh/keys/your-key1
    server3:
      Notes: new server
      config:
        HostName: 123.124.123.124
        IdentityFile: ~/.ssh/keys/your-key3

Group server2:
  Hosts:
    server2:
      config:
        ControlPath: ~/.ssh/server-2-%r@%h:%p
        HostName: 123.234.123.234
        IdentityFile: ~/.ssh/keys/your-key2
        User: ubuntu

再次执行命令,转换为 SSH 配置看看?

代码语言:javascript
复制
# cat test.yaml| ./ssh-config -to-ssh

Host *
    HostKeyAlgorithms +ssh-rsa
    PubkeyAcceptedAlgorithms +ssh-rsa

# new server
Host server3
    Compression yes
    ControlPath ~/.ssh/server-1-%r@%h:%p
    ControlPersist yes
    ForwardAgent yes
    HostName 123.124.123.124
    IdentityFile ~/.ssh/keys/your-key3
    Port 1234
    TCPKeepAlive yes

# your notes here
Host server1
    Compression yes
    ControlPath ~/.ssh/server-1-%r@%h:%p
    ControlPersist yes
    ForwardAgent yes
    HostName 123.123.123.123
    IdentityFile ~/.ssh/keys/your-key1
    Port 1234
    TCPKeepAlive yes

Host server2
    Compression yes
    ControlPath ~/.ssh/server-2-%r@%h:%p
    ControlPersist yes
    ForwardAgent yes
    HostName 123.234.123.234
    IdentityFile ~/.ssh/keys/your-key2
    Port 1234
    TCPKeepAlive yes
    User ubuntu

符合预期的配置就出现啦,是不是还蛮简单的呢?

通过这个方式,我们就可以更清晰明了的管理我们的服务器连接配置啦。

Docker 使用

如果你使用 Docker ,可以使用下面的方式来下载软件的镜像:

代码语言:javascript
复制
docker pull soulteary/ssh-config:v1.1.0
# or
docker pull ghcr.io/soulteary/ssh-config:v1.1.0

和上文一样,使用的方式有直接操作文件,或者使用 Linux 管道。

如果你想直接操作文件,那么你需要将本地的文件映射到容器中,然后使用工具来处理文件即可:

代码语言:javascript
复制
docker run --rm -it -v `pwd`:/ssh soulteary/ssh-config:v1.1.0 ssh-config -to-yaml -src /ssh/test.yaml -dest /ssh/abc.yaml

如果你想使用管道来操作文件,我个人推荐先进入 Docker 交互式命令行环境,然后再使用工具操作配置文件:

代码语言:javascript
复制
docker run --rm -it -v `pwd`:/ssh soulteary/ssh-config:v1.1.0 bash

cat /ssh/test.yaml | ssh-config -to-yaml

工具制作背后的事情

在制作工具之前,第一件事是看能否不制作这个工具:或许开源社区有类似需求的人,实现了这样一个小工具。

虽然有一些工具,但是距离我的理想型,总归是差了一些。

SSH 配置管理工具方案

在进行了一番查找,以及群友的推荐下,大概可以使用下面四个路径来完成 SSH 配置文件的管理:

  • • OpenSSH Client 的替代品:trzsz-ssh ( tssh ) [4]。
  • • 基于编辑配置文件思路的一些软件,比如:SSH Config Editor[5](Brew[6])、终端中做一些界面的思路:karlot/sshclick[7]、部署工具的模版思路:Ansible template[8]。
  • • 让我开心又有一些失望的软件:bencromwell/sshush[9]

确定制作工具路径

我更换设备的频率保持在每年或最长每两年(有时候是半年),基于这个情况我越来越倾向使用最常见、最普适的高市场占比的软件,OpenSSH Client 是市占率非常高的家伙,除了路由器这类设备外,能够使用 SSH 进行交互的设备,几乎都在使用 OpenSSH 相关软件。所以,使用替代品,不一定是折腾后能够省心的事情。

因为我的服务器和设备数量并不少,基于可视化的界面方案在呈现效果和使用效率上来说,都会有一些体验上的损失。并且,除了 Web 技术栈之外的 “可视化” 方案在跨系统平台、CPU 架构、多设备的体验中需要付出额外的代价。即使我订阅的 Setapp 中有这个 SSH Config Editor 软件,考虑到上面的因素,我倾向选择不使用带 Web 界面之外的软件。

至于类似 Ansible 类似的持续集成常客的方案,管理配置不是不行,但是太过麻烦,上文中提到的功能,光看文档的时间就足够写完整个程序了。进行幂等的文件部署,是合适的,细粒度管理和更新配置,这类软件其实并不擅长。

让我开心的,但是又失望的软件,是一款 CLI 命令行软件。

开心的是,这款软件能够将 YAML 格式的 SSH Config 转换为 OpenSSH 能够使用的格式,定义了几乎就是我想要的清晰明了、很少冗余信息的配置。但,失望的是:

  1. 1. 软件只能够从 YAML 结构,转换出 SSH Config,是单向处理。
  2. 2. 软件的核心转换过程,没有任何单元测试覆盖保障正确性。
  3. 3. 软件明明只有一个单一转换的功能,但是引用了一堆外部依赖。
  4. 4. 软件或许是受到了 1.x 版本的 Python 程序的编写影响,实现的有一些不好修改,上面三个问题想要提 PR 改进,稍微有点难度。

为了解决这几个原因,我决定参考这个软件配置的思路,重新实现一个能够支持 SSH Config 多种格式保存、转换功能,核心逻辑 100% 测试覆盖,不需要复杂外部依赖的小工具。

更加稳定、可靠、小巧、高效,确保使用这个工具处理数据的结果是准确的、体验是舒服的。

实践经验分享

这个程序的本质是字符串处理工具,主要处理的字符串类型有下面三种:

  • • OpenSSH Client 使用的 SSH Config
  • • 通用的 YAML 格式
  • • 通用的 JSON 格式

因为工具设计里,这三种类型之间可以自由转换,所以,我们需要关注一种数据格式,或者抽象一种更简单的,可以横跨三个数据格式中的中间格式(类似“中间代码(IR)”)。

我定义的基础的中间代码类似下面这样,一个包含了所有 OpenSSH Client 支持的配置项的数据结构(源代码soulteary/ssh-config/internal/parser/define.go[10],120 行+):

代码语言:javascript
复制
type HostConfig struct {
 Host  string `yaml:"Host,omitempty"`
 Match string `yaml:"Match,omitempty"`
...
 Ciphers                     string `yaml:"Ciphers,omitempty"`
 ClearAllForwardings         string `yaml:"ClearAllForwardings,omitempty"`
...
 HostName                    string `yaml:"HostName,omitempty"`
...
 Port                     string `yaml:"Port,omitempty"`
...
}

上面这个数据结构就是三种配置“数据流通”过程中的硬通货。由于三种格式的使用场景和客观要求原因,三种格式的最终数据结构是有一些差异的,所以我们还需要为三种数据格式定义新的数据结构(源代码 soulteary/ssh-config/internal/define/define.go[11]):

代码语言:javascript
复制
...
// ssh config
type HostConfig struct {
 Name   string `yaml:"Name,omitempty"`
 Notes  string `yaml:"Notes,omitempty"`
 Config map[string]string
 Extra  HostExtraConfig `yaml:"Extra,omitempty"`
}

// json
type HostConfigDataForJSON map[string]string

type HostConfigForJSON struct {
 Name  string                `json:"Name,omitempty"`
 Notes string                `json:"Notes,omitempty"`
 Data  HostConfigDataForJSON `json:"Data,omitempty"`
}

// yaml
type GroupConfig struct {
 Prefix string                `yaml:"Prefix,omitempty"`
 Common map[string]string     `yaml:"Common,omitempty"`
 Hosts  map[string]HostConfig `yaml:"Hosts,omitempty"`
}
type YAMLOutput struct {
 Global  map[string]string      `yaml:"global,omitempty"`
 Default map[string]string      `yaml:"default,omitempty"`
 Groups  map[string]GroupConfig `yaml:",inline"`
}

在定义了“硬通货”和“最终数据格式”之后,我们就能够快速的展开工作了,只需要:

  • • 完成不同格式到中间数据格式的相互转换,工具就能够支持多种文件格式的转换了。
  • • 完成基础的 IO 操作,工具就能够实现跟进需求读取配置文件,转换并保存新的配置文件的功能了。
  • • 完成标准管道操作,工具就能够支持 Linux 标准的 Pipeline 管道操作了。

虽然每个功能都很简单,但是因为我们可以选择某个目录或者某个文件文件作为数据源,可以选择设置保存文件路径,也可以不设置路径,可以将数据转换为 YAML、转换为 JSON、转换为 SSH 配置等等,加上各种异常判断,各种组合下来,想要写一个能跑的程序简单,想要写一个 100% bugfree 的软件,其实还是要花一些时间的。

而这里,能够减少时间花费的秘诀,就是优先完成函数的设计和单元测试覆盖。 100% 的单元测试覆盖,可以让你在使用软件的时候拥有非常强的安全感,在重构或者为软件新增功能的时候,花最少的时间去完成功能的添加和验证,而不需要担心引入额外的风险。

最后

最近的事情比较多,既然工具已经分享出来啦,那么这篇文章就先写到这里吧。

我们下篇文章再见。

--EOF

引用链接

[1] soulteary/ssh-config: https://github.com/soulteary/ssh-config [2] 比较勤快: https://github.com/soulteary/Home-Network-Note [3] GitHub 发布页面: https://github.com/soulteary/ssh-config/releases [4] trzsz-ssh ( tssh ) : https://github.com/trzsz/trzsz-ssh [5] SSH Config Editor: https://www.hejki.org/ssheditor/ [6] Brew: https://formulae.brew.sh/cask/ssh-config-editor [7] karlot/sshclick: https://github.com/karlot/sshclick [8] Ansible template: https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_templating.html [9] bencromwell/sshush: https://github.com/bencromwell/sshush [10] soulteary/ssh-config/internal/parser/define.go: https://github.com/soulteary/ssh-config/blob/main/internal/parser/define.go [11] soulteary/ssh-config/internal/define/define.go: https://github.com/soulteary/ssh-config/blob/main/internal/define/define.go


如果你觉得内容还算实用,欢迎点赞分享给你的朋友,在此谢过。

如果你想更快的看到后续内容的更新,请戳 “点赞”、“分享”、“喜欢” ,这些免费的鼓励将会影响后续有关内容的更新速度。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-10-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 折腾技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写在前面
  • SSH Config 配置管理工具的使用
    • 优化转换后的配置
    • Docker 使用
    • 工具制作背后的事情
      • SSH 配置管理工具方案
        • 确定制作工具路径
          • 实践经验分享
            • 引用链接
        • 最后
        相关产品与服务
        云服务器
        云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档