背景故事:
我正在创建一个 Ansible 角色,为需要的主机创建 SSH 密钥,并使用最新信息自动更新我的配置文件; lan ip、用户名、密钥等。尽管在添加新块之前,我无法让 Ansible 正确删除现有块。然后,当它添加新的主机时,由于某种原因,它会覆盖其他主机块(所以最后,即使有 10 个主机,也只存在 1 个块。稍后我会修复后者,但我主要关心的是获取旧块已正确删除。
问题与博士:
有没有更好的方法来做到这一点,有谁明白为什么这个正则表达式在 Ansible 中不起作用,我的正则表达式可以简化/改进吗?
这是我的 Ansible 角色的相关任务
- name: Read existing SSH config
slurp:
src: "{{ ssh_config_dir }}/config"
register: ssh_config_file
- name: Decode existing SSH config
set_fact:
ssh_config_content: "{{ ssh_config_file.content | b64decode }}"
- name: Parse existing SSH config into lines
set_fact:
ssh_config_lines: "{{ ssh_config_content.split('\n') }}"
- name: Check if existing host entry matches
set_fact:
host_entry_valid: >
{{ ssh_config_lines | select('match', '^Host {{ inventory_hostname }}$') | list | length > 0 and
ssh_config_lines | select('match', '^\\s*Hostname {{ hostvars[inventory_hostname].ansible_host }}$') | list | length > 0 and
ssh_config_lines | select('match', '^\\s*User {{ ssh_remote_user }}$') | list | length > 0 and
ssh_config_lines | select('match', '^\\s*Port {{ ssh_port }}$') | list | length > 0 and
ssh_config_lines | select('match', '^\\s*IdentityFile {{ ssh_key_dir }}/{{ inventory_hostname }}{{ ssh_key_name_suffix }}$') | list | length > 0 }}
- name: Debug host entry validity
debug:
var: host_entry_valid
- name: Backup the existing SSH config
copy:
src: "{{ ssh_config_dir }}/config"
dest: "{{ ssh_config_dir }}/config.bak"
when: not host_entry_valid
- name: Define the regex pattern
set_fact:
my_regex: '^(\s+)?Host\s+{{ inventory_hostname }}(\s+)?$\n^(((\s+)?[A-Za-z0-9./_-]|#)+(\s+)?)$\n^(\s+)?$'
- name: Print regex pattern
debug:
msg: "{{ my_regex | quote }}"
- name: Remove existing host entry if it doesn't match
lineinfile:
path: "{{ ssh_config_dir }}/config"
state: absent
regexp: '^(\s+)?Host\s+{{ inventory_hostname }}(\s+)?$\n^(((\s+)?[A-Za-z0-9./_-]|#)+(\s+)?)$\n^(\s+)?$'
when: not host_entry_valid
Check if existing host entry matches
需要工作,但它应该触发我遇到问题的Remove existing host entry if it doesn't match
任务。正则表达式在 VSCode 中完美匹配一个块,但在 Sublime 中它匹配多个块,而 Ansible 似乎根本不起作用。
这最初是 ChatGPT 的创作,但我对其进行了一些调整,最终自己编写了正则表达式。
这适用于 VSCode:
^(\s+)?Host test(\s+)?$\n^(((\s+)?[A-Za-z0-9./_-]|#)+(\s+)?)$\n
但消极(?)的展望却不然。它在 Sublime 中确实有效,但是 sublime 由于某种原因匹配多个块。
^(\s+)?Host test(\s+)?$\n^(((\s+)?[A-Za-z0-9./_-]|#)+(\s+)?)$\n(?=Host)
的想法是匹配
Host hostname
,然后查找任何包含文本的行,直到出现空行;并使用负前瞻检查主机是否存在于空行之后,但我不熟悉使用前瞻,如果该块是文件中的最后一个块,则这将不起作用,因此我将删除它.
这是相关的 Ansible 输出,并且旧的 SSH 块不会被删除:
TASK [ssh-keys : Remove existing SSH agents and known hosts] ********************************************************************************************
included: /scripts/ansible/roles/ssh-keys/tasks/remove_existing_ssh_agents.yml for test
TASK [ssh-keys : Remove all SSH agents] *****************************************************************************************************************
skipping: [test]
TASK [ssh-keys : Remove host from known hosts] **********************************************************************************************************
skipping: [test]
TASK [ssh-keys : Copy SSH key to remote server] *********************************************************************************************************
included: /scripts/ansible/roles/ssh-keys/tasks/copy_key.yml for test
TASK [ssh-keys : Copy SSH key to remote server] *********************************************************************************************************
skipping: [test]
示例块:
Host test
HostName 10.0.0.4
User myuser
IdentityFile ...
Host test2
...
正如Zeitounator评论的那样,有一个 SSH 专用模块, 这比你自己做这一切要容易和安全得多。
但是关于你的问题,你的正则表达式模式可以是 重写如下:
^[\t ]*host[\t ]+(.+?)[\t ]*\n(?:(?!(?:^[\t ]*#.*\n)*[\t ]*host\b)[\t ]*(?:(\w+)\b(?:[\t ]*=[\t ]*|[\t ]+)(.*)|#.*)?(?:\n|\Z))+
在这里进行现场测试:https://regex101.com/r/LiGszL/2
(\s+)?
来匹配空格可以写成 \s*
(更清晰)。
但它仍然有一个问题,因为 \s
相当于
[\r\n\t\f\v ]
,它也将匹配新行。在
Perl/PCRE 风格,可以使用 \h
来匹配水平空格。
但似乎Python还没有得到它。所以我们将其替换为
[\t ]
(注意 2 个不同的空格字符:普通空格和
不间断空格(Unicode 中分别为 U+0020 和 U+00A0)。
可能只考虑正常空间就足够了。
\n
将匹配新行。这将工作正常,因为配置
文件是为 Unix 系统创建的,并且只有这个字符。
但如果我们有一个 Windows 文件,它将是 \r\n
。我们可以使用
\r?\n
甚至 (?:\r|\n|\r\n)
与旧的 Mac OS 系统确实使用
\r
过去的字符。为了清楚起见,我将坚持使用简单的\n
。
对于 Perl/PCRE 风格,可以使用 \R
来匹配任何类型
行分隔符。
上面的单线图案和这个评论的一样 原版:
# Host entry:
# Start of line followed by optional horizontal spaces,
# The word "Host" case-insensitive, followed by anything (captured) and a new line.
^[\t ]*host[\t ]+(.+?)[\t ]*\n
# A configuration line or comment, multiple times:
(?:
# Negative lookahead to avoid matching a new "Host" entry, but
# also with optional comment lines before it.
(?!(?:^[\t ]*\#.*\n)*[\t ]*host\b)
# Optional horizontal spaces.
[\t ]*
# Config line, comment or empty line (done with the ? at the end).
(?:
# A) A config line, capturing it (with space or equal sign).
(\w+)\b(?:[\t ]*=[\t ]*|[\t ]+)(.*) |
# B) Or a comment.
\#.*
)?
# New line or end of the config file.
(?:\n|\Z)
)+
查看实际操作并附有解释:https://regex101.com/r/LiGszL/1
注意中间部分匹配可以简化 配置行或注释。不需要做所有这些检查,因为我们 可以简单地匹配任何内容,因为我们有负面的前瞻 阻止我们。但这表明如何可以阅读 主机配置行或带有第二个正则的注释 表达。
完整示例,在 JavaScript 中:
const regexHostEntry = /^[\t ]*host[\t ]+(?<host>.+?)[\t ]*\n(?<config>(?:(?!(?:^[\t ]*#.*\n)*[\t ]*host\b)[\t ]*(?:(\w+)\b(?:[\t ]*=[\t ]*|[\t ]+)(.*)|#.*)?(?:\n|\Z))+)/gim;
const regexConfigLine = /^[\t ]*(\w+)\b(?:[\t ]*=[\t ]*|[\t ]+)(.*)/gim;
const input = `Host test
Hostname test.domain.com
User james
Port 22
# Comment
IdentityFile ~/.ssh/key.pub
# With 2 aliases
Host test2 test-2
Hostname test2.domain.com
User = james
Port=22
# Port 23
IdentityFile = ~/.ssh/key2.pub
# For all hosts except test2, activate compression and set log level:
Host * !test2
Compression yes
LogLevel INFO
IdentityFile ~/.ssh/id_rsa
Host *.sweet.home
Hostname 192.168.2.17
User tom
IdentityFile "~/.ssh/id tom.pub" # If has spaces, then quote it.
# With a lot of spaces between lines
Host localhost
Hostname 127.0.0.*
IdentityFile ~/.ssh/id_rsa
# Without empty lines between Host definitions:
Host dummy
Hostname ssh.dummy.com
User user
Host dummy2
Hostname ssh.dummy2.com
User user`;
let matches = input.matchAll(regexHostEntry);
if (matches) {
matches.forEach((match) => {
console.log(`Found match for Host ${match.groups.host}:`);
console.log([...match.groups.config.matchAll(regexConfigLine)]);
});
}