使用Ansible和Asdf在macOS和Ubuntu上自动安装Erlang、Elixir、Phoenix和Nerves的方法

当要在一台macOS或Ubuntu的PC上使用Asdf安装Erlang、Elixir、Phoenix和Nerves时,可以根据类似”M1 Mac上Elixir和Erlang的2021年12月最新安装指南”的文章手动安装。但是,如果要在两台或更多的PC上安装,可能会希望自动化这个过程。Ansible就是一个可以实现自动化的方法。

在本文中,我们将介绍如何使用 Ansible 和 Asdf 在 macOS 和 Ubuntu 的个人电脑上自动安装 Erlang、Elixir、Phoenix 和 Nerves。

这篇文章的中文译文是”使用Ansible和Asdf自动将Erlang、Elixir、Phoenix和Nerves安装到macOS和Ubuntu的机器上”。

这篇文章的前提

假设有一个主机和一个或多个目标机器。假设主机上已经安装了Ansible。目标机器可以是macOS或Ubuntu。同时,假设macOS目标机器上已经安装了Homebrew。假设所有目标机器可以使用公钥进行ssh登录,并且可以使用相同的密码在所有目标机器上sudo获取管理员权限。并且,目标机器的主机名分别是 target1, target2, …, target9。

库存清单.yml

将目标信息和共享变量写入 inventory/inventory.yml 文件中。

all:
  hosts:
    target[1:9]:
  vars:
    asdf: v0.8.1
    erlang: latest
    elixir: latest
    phoenix: latest
    nerves: latest

目标[1:9]表示target1、target2、…、target9,根据需要可以进行修改。可以分别指定Asdf、Erlang、Elixir、Phoenix、Nerves的版本。在这种情况下,Asdf的版本是v0.8.1,并且表明Asdf的其他版本是最新版。还可以将Erlang、Elixir、Phoenix、Nerves的版本指定为其他旧版本。

如果特别是在本地主机上安装的情况下,可以按照以下步骤进行。

all:
  hosts:
    localhost:
      ansible_host: "127.0.0.1"
  vars:
    asdf: v0.8.1
    erlang: latest
    elixir: latest
    phoenix: latest
    nerves: latest

如果要在本地主机上安装,需要设置以使其可以通过ssh登录到本地主机。

ansible的配置文件

为了抑制警告,可以按照以下方式编写ansible.cfg。

[defaults]
interpreter_python=/usr/bin/python3

任务

为了再利用的目的,可以将Ansible任务作为组件进行描述。

安装Asdf到Ubuntu

---
- block:
  - name: Install dependencies of asdf
    become: true
    apt:
      update_cache: yes
      cache_valid_time: 86400 # 1day
      name:
        - curl
        - git
      state: latest
  - name: Install asdf
    git:
      repo: https://github.com/asdf-vm/asdf.git
      dest: "{{ ansible_user_dir }}/.asdf"
      depth: 1
      version: "{{ asdf | quote }}"
    register: result
  - name: asdf update
    shell: "bash -lc 'cd {{ ansible_user_dir }}/.asdf && git pull'"
    ignore_errors: yes
    when: result is failed
  - name: set env vars
    lineinfile:
      dest: "{{ shrc }}"
      state: present
      line: "{{ item.line }}"
    with_items:
    - line: ". $HOME/.asdf/completions/asdf.{{ sh }}"
      regexp: '^ \. \$HOME/\.asdf/completions/asdf\.{{ sh }}'
    - line: '. $HOME/.asdf/asdf.sh'
      regexp: '^ \. \$HOME/\.asdf/asdf\.sh'
  when: ansible_system == 'Linux' and (ansible_distribution == 'Ubuntu' or ansible_distribution == 'Debian')
  vars:
    - shrc: "{{ ansible_user_dir | quote }}/.{{ ansible_user_shell | basename | quote }}rc"
    - sh: "{{ ansible_user_shell | basename | quote }}"

在macOS上安装Asdf。

---
- block:
  - name: install asdf by Homebrew
    community.general.homebrew:
      update_homebrew: true
      name:
        - asdf
  - name: set env vars (bash)
    lineinfile:
      dest: "{{ shprofile }}"
      state: present
      line: "{{ item.line }}"
    with_items:
    - line: ".  $(brew --prefix asdf)/etc/bash_completion.d/asdf.bash"
      regexp: '^ \. \$(brew --prefix asdf)/etc/bash_completion\.d/asdf\.bash'
    - line: '. $(brew --prefix asdf)/libexec/asdf.sh'
      regexp: '^ \. \$(brew --prefix asdf)/libexec/asdf\.sh'
    when: sh == 'bash'
  - name: set env vars (zsh)
    lineinfile:
      dest: "{{ shrc }}"
      state: present
      line: "{{ item.line }}"
    with_items:
    - line: ". $(brew --prefix)/share/zsh/site-functions"
      regexp: '^ \. \$(brew --prefix)/share/zsh/site-functions'
    - line: '. $(brew --prefix asdf)/libexec/asdf.sh'
      regexp: '^ \. \$(brew --prefix asdf)/libexec/asdf\.sh'
    when: sh == 'zsh'
  when: ansible_system == 'Darwin'
  vars:
    - shprofile: "{{ ansible_user_dir }}/.{{ ansible_user_shell | basename | regex_replace('$', '_') | regex_replace('zsh_', 'z') }}profile"
    - shrc: "{{ ansible_user_dir }}/.{{ ansible_user_shell | basename }}rc"
    - sh: "{{ ansible_user_shell | basename | quote }}"

在Ubuntu上安装Erlang的前提库。

---
- block:
  - name: install prerequisite libraries for erlang 
    become: true
    apt:
      update_cache: yes
      cache_valid_time: 86400 # 1day
      state: latest
      name:
      - build-essential
      - autoconf
      - m4
      - libncurses5-dev
      - libwxgtk3.0-gtk3-dev
      - libgl1-mesa-dev
      - libglu1-mesa-dev
      - libpng-dev
      - libssh-dev
      - unixodbc-dev
      - xsltproc
      - fop
      - libxml2-utils
      - libncurses-dev
      - openjdk-11-jdk
  when: ansible_system == 'Linux' and (ansible_distribution == 'Ubuntu' or ansible_distribution == 'Debian')

在macOS上安装Erlang的前提库

---
- block:
  - name: install prerequisite libraries for erlang 
    community.general.homebrew:
      update_homebrew: true
      name:
        - autoconf
        - openssl@1.1
        - openssl@3
        - wxwidgets
        - libxslt
        - fop
        - openjdk
  when: ansible_system == 'Darwin'

安装Ubuntu上的Nerves前提库。

---
- block:
  - name: install prerequisite libraries for nerves
    become: true
    apt:
      update_cache: yes
      cache_valid_time: 86400 # 1day
      state: latest
      name:
      - automake
      - autoconf
      - git
      - squashfs-tools
      - ssh-askpass
      - pkg-config
      - curl
      - libssl-dev
      - libncurses5-dev
      - bc
      - m4
      - unzip
      - cmake
      - python
  when: ansible_system == 'Linux' and (ansible_distribution == 'Ubuntu' or ansible_distribution == 'Debian')

在macOS上安装Nerves的前置库。

---
- block:
  - name: install prerequisite libraries for nerves 
    community.general.homebrew:
      update_homebrew: true
      name:
        - fwup 
        - squashfs
        - coreutils
        - xz
        - pkg-config
  when: ansible_system == 'Darwin'

安装Erlang插件

---
- block:
  - name: sh env
    ansible.builtin.shell:
    args:
      cmd: "{{ shenv_cmd }}"
      chdir: '{{ ansible_user_dir }}/'
    register: shenv
  - name: asdf plugin add erlang
    ansible.builtin.shell: |
      {{ source }} 
      asdf plugin add erlang
    args:
      executable: '{{ ansible_user_shell }}'
    register: result
    failed_when: result.rc != 0 and result.stderr | regex_search('(Plugin named .* already added)') == '' 
    vars: 
      source: "{{ shenv.stdout_lines | map('regex_replace', '(^)', '. ') | join('\n') }}"
  vars:
    - asdfsh: "{{ ansible_user_dir | quote }}/.asdf/asdf.sh"
    - profile: "{{ ansible_user_dir | quote }}/.profile"
    - shprofile: "{{ ansible_user_dir }}/.{{ ansible_user_shell | basename | regex_replace('$', '_') | regex_replace('zsh_', 'z') }}profile"
    - shrc: "{{ ansible_user_dir | quote }}/.{{ ansible_user_shell | basename  | quote }}rc"
    - shenv_cmd: "if [ -e {{ asdfsh }} ]; then echo '{{ asdfsh }}'; fi; if [ -e {{ shprofile }} ]; then echo '{{ shprofile }}'; fi; if [ -e {{ profile }} ]; then echo '{{ profile }}'; fi; if [ -e {{ shrc }} ]; then echo '{{ shrc }}'; fi"

安装Elixir插件。

---
- block:
  - name: sh env
    ansible.builtin.shell:
    args:
      cmd: "{{ shenv_cmd }}"
      chdir: '{{ ansible_user_dir }}/'
    register: shenv
  - name: asdf plugin add elixir
    ansible.builtin.shell: |
      {{ source }} 
      asdf plugin add elixir
    args:
      executable: '{{ ansible_user_shell }}'
    register: result
    failed_when: result.rc != 0 and result.stderr | regex_search('(Plugin named .* already added)') == '' 
    vars: 
      source: "{{ shenv.stdout_lines | map('regex_replace', '(^)', '. ') | join('\n') }}"
  vars:
    - asdfsh: "{{ ansible_user_dir | quote }}/.asdf/asdf.sh"
    - profile: "{{ ansible_user_dir }}/.profile"
    - shprofile: "{{ ansible_user_dir }}/.{{ ansible_user_shell | basename | regex_replace('$', '_') | regex_replace('zsh_', 'z') }}profile"
    - shrc: "{{ ansible_user_dir }}/.{{ ansible_user_shell | basename }}rc"
    - shenv_cmd: "if [ -e {{ asdfsh }} ]; then echo '{{ asdfsh }}'; fi; if [ -e {{ shprofile }} ]; then echo '{{ shprofile }}'; fi; if [ -e {{ profile }} ]; then echo '{{ profile }}'; fi; if [ -e {{ shrc }} ]; then echo '{{ shrc }}'; fi"

安装 Erlang

---
- block:
  - name: sh env
    ansible.builtin.shell:
    args:
      cmd: "{{ shenv_cmd }}"
      chdir: '{{ ansible_user_dir }}/'
    register: shenv
  - name: asdf install erlang (for Linux)
    ansible.builtin.shell: |
      {{ source }} 
      asdf install erlang {{ erlang | quote }}
    args:
      executable: '{{ ansible_user_shell }}'
    register: result
    when: ansible_system == 'Linux'
    vars: 
      source: "{{ shenv.stdout_lines | map('regex_replace', '(^)', '. ') | join('\n') }}"
  - name: show result
    debug:
      var: result
  - name: asdf install erlang (macOS OTP version 24.1.x or earlier)
    ansible.builtin.shell: |
      {{ source }} 
      {{ install_erlang_ssl_1_1 }}
    args:
      executable: '{{ ansible_user_shell }}'
    register: result
    when: (erlang != 'latest' and erlang is version_compare('24.2', '<')) and ansible_system == 'Darwin'
    vars: 
      source: "{{ shenv.stdout_lines | map('regex_replace', '(^)', '. ') | join('\n') }}"
  - name: show result
    debug:
      var: result
    when: (erlang != 'latest' and erlang is version_compare('24.2', '<')) and ansible_system == 'Darwin'
  - name: asdf install erlang (macOS OTP 24.2 or later)
    ansible.builtin.shell: |
      {{ source }} 
      {{ install_erlang_ssl_3 }}
    args:
      executable: '{{ ansible_user_shell }}'
    register: result
    when: (erlang == 'latest' or (erlang is version_compare('24.2', '>='))) and ansible_system == 'Darwin'
    vars: 
      source: "{{ shenv.stdout_lines | map('regex_replace', '(^)', '. ') | join('\n') }}"
  - name: show result
    debug:
      var: result
    when: (erlang == 'latest' or (erlang is version_compare('24.2', '>='))) and ansible_system == 'Darwin'
  - name: asdf global erlang
    ansible.builtin.shell: |
      {{ source }} 
      asdf global erlang {{ erlang | quote }}
    args:
      executable: '{{ ansible_user_shell }}'
    vars: 
      source: "{{ shenv.stdout_lines | map('regex_replace', '(^)', '. ') | join('\n') }}"
  vars:
    - asdfsh: "{{ ansible_user_dir | quote }}/.asdf/asdf.sh"
    - profile: "{{ ansible_user_dir }}/.profile"
    - shprofile: "{{ ansible_user_dir }}/.{{ ansible_user_shell | basename | regex_replace('$', '_') | regex_replace('zsh_', 'z') }}profile"
    - shrc: "{{ ansible_user_dir }}/.{{ ansible_user_shell | basename }}rc"
    - shenv_cmd: "if [ -e {{ asdfsh }} ]; then echo '{{ asdfsh }}'; fi; if [ -e {{ shprofile }} ]; then echo '{{ shprofile }}'; fi; if [ -e {{ profile }} ]; then echo '{{ profile }}'; fi; if [ -e {{ shrc }} ]; then echo '{{ shrc }}'; fi"
    - install_erlang_ssl_1_1: "KERL_CONFIGURE_OPTIONS=\"--with-ssl=$(brew --prefix openssl@1.1) --with-odbc=$(brew --prefix unixodbc)\" CC=\"/usr/bin/gcc -I$(brew --prefix unixodbc)/include\" LDFLAGS=-L$(brew --prefix unixodbc)/lib asdf install erlang {{ erlang | quote }}"
    - install_erlang_ssl_3: "KERL_CONFIGURE_OPTIONS=\"--with-ssl=$(brew --prefix openssl@3) --with-odbc=$(brew --prefix unixodbc)\" CC=\"/usr/bin/gcc -I$(brew --prefix unixodbc)/include\" LDFLAGS=-L$(brew --prefix unixodbc)/lib asdf install erlang {{ erlang | quote }}"

安装Elixir

---
- block:
  - name: sh env
    ansible.builtin.shell:
    args:
      cmd: "{{ shenv_cmd }}"
      chdir: '{{ ansible_user_dir }}/'
    register: shenv
  - name: asdf install elixir
    ansible.builtin.shell: |
      {{ source }}
      asdf install elixir {{ elixir | quote }}
    args:
      executable: '{{ ansible_user_shell }}'
    register: result
    vars: 
      source: "{{ shenv.stdout_lines | map('regex_replace', '(^)', '. ') | join('\n') }}"
  - name: show result
    debug:
      var: result
  - name: asdf install elixir
    ansible.builtin.shell: |
      {{ source }}
      asdf global elixir {{ elixir | quote }}
    args:
      executable: '{{ ansible_user_shell }}'
    vars: 
      source: "{{ shenv.stdout_lines | map('regex_replace', '(^)', '. ') | join('\n') }}"
  vars:
    - asdfsh: "{{ ansible_user_dir | quote }}/.asdf/asdf.sh"
    - profile: "{{ ansible_user_dir }}/.profile"
    - shprofile: "{{ ansible_user_dir }}/.{{ ansible_user_shell | basename | regex_replace('$', '_') | regex_replace('zsh_', 'z') }}profile"
    - shrc: "{{ ansible_user_dir }}/.{{ ansible_user_shell | basename }}rc"
    - shenv_cmd: "if [ -e {{ asdfsh }} ]; then echo '{{ asdfsh }}'; fi; if [ -e {{ shprofile }} ]; then echo '{{ shprofile }}'; fi; if [ -e {{ profile }} ]; then echo '{{ profile }}'; fi; if [ -e {{ shrc }} ]; then echo '{{ shrc }}'; fi"

安装Phoenix

-
- block:
  - name: sh env
    ansible.builtin.shell:
    args:
      cmd: "{{ shenv_cmd }}"
      chdir: '{{ ansible_user_dir }}/'
    register: shenv
  - name: install prerequisite
    ansible.builtin.shell: |
      {{ source }}
      mix local.rebar --force
      mix local.hex --force
    args:
      executable: '{{ ansible_user_shell }}'
    register: result
    vars: 
      source: "{{ shenv.stdout_lines | map('regex_replace', '(^)', '. ') | join('\n') }}"
  - name: install Phoenix (latest)
    ansible.builtin.shell: |
      {{ source }}
      mix archive.install hex phx_new --force
    args:
      executable: '{{ ansible_user_shell }}'
    register: result
    when: phoenix == 'latest'
    vars: 
      source: "{{ shenv.stdout_lines | map('regex_replace', '(^)', '. ') | join('\n') }}"
  - name: install Phoenix (not latest)
    ansible.builtin.shell: |
      {{ source }}
      mix archive.install hex phx_new {{ phoenix }} --force
    args:
      executable: '{{ ansible_user_shell }}'
    register: result
    when: phoenix != 'latest'
    vars: 
      source: "{{ shenv.stdout_lines | map('regex_replace', '(^)', '. ') | join('\n') }}"
  vars:
    - asdfsh: "{{ ansible_user_dir | quote }}/.asdf/asdf.sh"
    - profile: "{{ ansible_user_dir }}/.profile"
    - shprofile: "{{ ansible_user_dir }}/.{{ ansible_user_shell | basename | regex_replace('$', '_') | regex_replace('zsh_', 'z') }}profile"
    - shrc: "{{ ansible_user_dir }}/.{{ ansible_user_shell | basename }}rc"
    - shenv_cmd: "if [ -e {{ asdfsh }} ]; then echo '{{ asdfsh }}'; fi; if [ -e {{ shprofile }} ]; then echo '{{ shprofile }}'; fi; if [ -e {{ profile }} ]; then echo '{{ profile }}'; fi; if [ -e {{ shrc }} ]; then echo '{{ shrc }}'; fi"

安装Nerves

---
- block:
  - name: sh env
    ansible.builtin.shell:
    args:
      cmd: "{{ shenv_cmd }}"
      chdir: '{{ ansible_user_dir }}/'
    register: shenv
  - name: install Nerves (latest)
    ansible.builtin.shell: |
      {{ source }}
      mix local.rebar --force
      mix local.hex --force
      mix archive.install hex nerves_bootstrap --force
    args:
      executable: '{{ ansible_user_shell }}'
    register: result
    when: nerves == 'latest'
    vars: 
      source: "{{ shenv.stdout_lines | map('regex_replace', '(^)', '. ') | join('\n') }}"
  - name: install Nerves (not latest)
    ansible.builtin.shell: |
      {{ source }}
      mix local.rebar --force
      mix local.hex --force
      mix archive.install hex nerves_bootstrap {{ nerves }} --force
    args:
      executable: '{{ ansible_user_shell }}'
    register: result
    when: nerves != 'latest'
    vars: 
      source: "{{ shenv.stdout_lines | map('regex_replace', '(^)', '. ') | join('\n') }}"
  vars:
    - asdfsh: "{{ ansible_user_dir | quote }}/.asdf/asdf.sh"
    - profile: "{{ ansible_user_dir }}/.profile"
    - shprofile: "{{ ansible_user_dir }}/.{{ ansible_user_shell | basename | regex_replace('$', '_') | regex_replace('zsh_', 'z') }}profile"
    - shrc: "{{ ansible_user_dir }}/.{{ ansible_user_shell | basename }}rc"
    - shenv_cmd: "if [ -e {{ asdfsh }} ]; then echo '{{ asdfsh }}'; fi; if [ -e {{ shprofile }} ]; then echo '{{ shprofile }}'; fi; if [ -e {{ profile }} ]; then echo '{{ profile }}'; fi; if [ -e {{ shrc }} ]; then echo '{{ shrc }}'; fi"

剧本

您可以根据这些任务构建Playbook。本节中会提供一些示例。

安装Asdf

- name: install asdf
  hosts: all
  tasks:
    - include_tasks: ../tasks/0010_install_asdf_linux.yml
      when: ansible_system == 'Linux' and (ansible_distribution == 'Ubuntu' or ansible_distribution == 'Debian')
    - include_tasks: ../tasks/0010_install_asdf_macos.yml
      when: ansible_system == 'Darwin'

安装Erlang的前置库。

- name: install prerequisites of erlang
  hosts: all
  tasks:
    - include_tasks: ../tasks/0011_install_erlang_prerequisite_linux.yml
      when: ansible_system == 'Linux' and (ansible_distribution == 'Ubuntu' or ansible_distribution == 'Debian')
    - include_tasks: ../tasks/0011_install_erlang_prerequisite_macos.yml
      when: ansible_system == 'Darwin'

安装 Nerves 的前提库。

- name: install prerequisites of nerves
  hosts: all
  tasks:
    - include_tasks: ../tasks/0013_install_nerves_prerequisite_linux.yml
      when: ansible_system == 'Linux' and (ansible_distribution == 'Ubuntu' or ansible_distribution == 'Debian')
    - include_tasks: ../tasks/0013_install_nerves_prerequisite_macos.yml
      when: ansible_system == 'Darwin'

安装插件

- name: install erlang/elixir plugins for asdf
  hosts: all
  tasks:
    - include_tasks: ../tasks/0021_install_erlang_plugin.yml
    - include_tasks: ../tasks/0022_install_elixir_plugin.yml

安装Erlang和Elixir

- name: install erlang/elixir with asdf
  hosts: all
  tasks:
    - include_tasks: ../tasks/0101_install_erlang.yml
    - include_tasks: ../tasks/0102_install_elixir.yml

安装Phoenix

- name: install phoenix with asdf
  hosts: all
  tasks:
    - include_tasks: ../tasks/0201_install_phoenix.yml

安装Nerves

- name: install nerves with asdf
  hosts: all
  tasks:
    - include_tasks: ../tasks/0301_install_nerves.yml

怎么使用

按照以下步骤执行playbook。

ansible-playbook -f (ターゲット数) -i (inventoryファイル) (playbookファイル)

例如,要在target1、target2、…、target9上安装Erlang和Elixir,可以按如下步骤进行。

ansible-playbook -f 9 -i inventory/inventory.yml playbook/0100_install_erlang_elixir.yml

如果在执行playbook时需要管理员权限,请按照以下方式操作。

ansible-playbook -f (ターゲット数) -i (inventoryファイル) (playbookファイル) --ask-become-pass

例如,如果要将Asdf安装到target1、target2、…、target9之中,且目标中至少包含一台Ubuntu,则按照以下方式进行操作。

ansible-playbook -f 9 -i inventory/inventory.yml playbook/0010_install_asdf.yml --ask-become-pass
广告
将在 10 秒后关闭
bannerAds