如何将 SSH 配置块与 Ansible 正则表达式匹配

问题描述 投票:0回答:1

背景故事:

我正在创建一个 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
  ...
regex ssh ansible ansible-2.x ssh-config
1个回答
0
投票

正如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)]);
  });
}

© www.soinside.com 2019 - 2024. All rights reserved.