diff --git a/roles/miniforge/README.md b/roles/miniforge/README.md new file mode 100644 index 0000000..de805c6 --- /dev/null +++ b/roles/miniforge/README.md @@ -0,0 +1,46 @@ +Role Name +========= + +Install the Miniforge distribution for each supplied user and, optionally, deploy Mamba environment from provided YAML files. + +Requirements +------------ + +None + +Role Variables +-------------- + +`miniforge_release`: Miniforge release version +`miniforge_script_path`: where to download the installation script +`miniforge_release_base_url`: base URL +`miniforge_create_envs`: whethere to create environments for each user or not + +`miniforge_users`: a list of dictionaries representing user accounts that need Miniforge +`manifest_dir`: path to folder on the Ansible controller host where environment files are kept +`miniforge_condarc`: a dictionary that will be YAMLified into the condarc file + +NOTE: `miniforge_users` items must have `name` string attribute and, optionally, a `mamba_files` list attribute (filenames must be available under the `manifest_dir`). + +Dependencies +------------ + +None + +Example Playbook +---------------- + +See `molecule` > `default` > `converge.yml` + +---------------- + +License +------- + +BSD + +Author Information +------------------ + +corvus-migratorius@proton.me +masayganova@gmail.com diff --git a/roles/miniforge/defaults/main.yml b/roles/miniforge/defaults/main.yml new file mode 100644 index 0000000..91e4818 --- /dev/null +++ b/roles/miniforge/defaults/main.yml @@ -0,0 +1,5 @@ +--- +miniforge_release: "26.3.2-2" +miniforge_script_path: "/tmp/{{ miniforge_install_script_name }}.sh" +miniforge_release_base_url: "https://github.com/conda-forge/miniforge/releases/download/" +miniforge_create_envs: false diff --git a/roles/miniforge/handlers/main.yml b/roles/miniforge/handlers/main.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/miniforge/handlers/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/miniforge/meta/main.yml b/roles/miniforge/meta/main.yml new file mode 100644 index 0000000..e903d3c --- /dev/null +++ b/roles/miniforge/meta/main.yml @@ -0,0 +1,17 @@ +--- +galaxy_info: + role_name: miniforge + namespace: genlab + author: Alexander Gorelyshev + company: Genlab, LLC + description: Deploy the Miniforge distribution of the Mamba package manager + license: MIT + min_ansible_version: "2.1" + + platforms: + - name: "Ubuntu" + versions: ["jammy", "noble"] + + galaxy_tags: [] + +dependencies: [] diff --git a/roles/miniforge/molecule/default/converge.yml b/roles/miniforge/molecule/default/converge.yml new file mode 100644 index 0000000..2b49dbf --- /dev/null +++ b/roles/miniforge/molecule/default/converge.yml @@ -0,0 +1,29 @@ +--- +# you can use {{all_users | selectattr('name', 'in', mamba_users)}} to narrow down a larger list +- name: Converge + hosts: all + vars: + mamba_users: + - name: alice + mamba_files: [project-alpha.yml, project-beta.yml] + - name: bob + mamba_files: [project-alpha.yml, project-beta.yml] + - name: charlie + + + pre_tasks: + - name: "Create test users" + loop: [alice, bob, charlie] + ansible.builtin.user: + name: "{{ item }}" + state: present + create_home: true + shell: /bin/bash + + roles: + - role: genlab.common.miniforge + miniforge_users: "{{ mamba_users }}" + manifest_dir: "manifests" + miniforge_create_envs: true + miniforge_condarc: + channels: [conda-forge] diff --git a/roles/miniforge/molecule/default/manifests/project-alpha.yml b/roles/miniforge/molecule/default/manifests/project-alpha.yml new file mode 100644 index 0000000..d3a5f05 --- /dev/null +++ b/roles/miniforge/molecule/default/manifests/project-alpha.yml @@ -0,0 +1,10 @@ +--- +name: env_alpha +channels: + - conda-forge +dependencies: + - python~=3.12 + - pip>=22.2 + - stress-ng==0.17.03 + - pip: + - humanfriendly diff --git a/roles/miniforge/molecule/default/manifests/project-beta.yml b/roles/miniforge/molecule/default/manifests/project-beta.yml new file mode 100644 index 0000000..6ab1fe1 --- /dev/null +++ b/roles/miniforge/molecule/default/manifests/project-beta.yml @@ -0,0 +1,9 @@ +--- +name: cucubeta +channels: + - conda-forge +dependencies: + - python~=3.10 + - pip>=22.2 + - pip: + - arrow diff --git a/roles/miniforge/molecule/default/molecule.yml b/roles/miniforge/molecule/default/molecule.yml new file mode 100644 index 0000000..fd2f06d --- /dev/null +++ b/roles/miniforge/molecule/default/molecule.yml @@ -0,0 +1,27 @@ +--- +dependency: + name: galaxy + +driver: + name: docker + +platforms: + - name: ubuntu + image: geerlingguy/docker-${MOLECULE_DISTRO:-ubuntu2204}-ansible:latest + pre_build_image: true + command: ${MOLECULE_DOCKER_COMMAND:-""} + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + cgroupns_mode: host + privileged: true + +provisioner: + name: ansible + +verifier: + name: ansible + +lint: | + set -e + yamllint . + ansible-lint . diff --git a/roles/miniforge/molecule/default/verify.yml b/roles/miniforge/molecule/default/verify.yml new file mode 100644 index 0000000..20efe2d --- /dev/null +++ b/roles/miniforge/molecule/default/verify.yml @@ -0,0 +1,70 @@ +--- +- name: Verify + hosts: all + gather_facts: false + any_errors_fatal: true + + pre_tasks: + - name: "Verify | include default vars" + ansible.builtin.include_vars: + dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/defaults/" + extensions: ['yml'] + + tasks: + - name: "Verify | get the version of Miniforge installed" + become: true + become_user: alice + changed_when: false + register: miniforge_installed_version + ansible.builtin.command: + cmd: ~/miniforge3/bin/conda --version + + - name: "Verify | check the installed version of Miniforge" + ansible.builtin.assert: + that: "'{{ miniforge_release | split('-') | first }}' in miniforge_installed_version.stdout" + success_msg: "OK, expected version installed: '{{ miniforge_installed_version.stdout }}'" + fail_msg: "FAIL: unexpected version installed: '{{ miniforge_installed_version.stdout }}'" + + - name: "Verify | check Mamba installing correct tools from channels" + become: true + become_user: alice + changed_when: false + vars: + env: env_alpha + register: miniforge_tool_version + ansible.builtin.command: + cmd: ~/miniforge3/envs/{{ env }}/bin/stress-ng --version + + - name: "Verify | check the version of the installed tool" + ansible.builtin.assert: + that: "'stress-ng, version 0.17.03' in miniforge_tool_version.stdout" + success_msg: "OK, expected version installed: '{{ miniforge_tool_version.stdout }}'" + fail_msg: "FAIL: unexpected version installed: '{{ miniforge_tool_version.stdout }}'" + + - name: "Verify | check the deployed environments for Alice (pip)" + become: true + become_user: alice + loop: + - { env: 'env_alpha', module: 'humanfriendly' } + - { env: 'cucubeta', module: 'arrow' } + changed_when: false + ansible.builtin.command: + cmd: ~/miniforge3/envs/{{ item.env }}/bin/python -c 'import {{ item.module }}' + + - name: "Verify | check the deployed environments for Bob" + become: true + become_user: bob + loop: + - { env: 'cucubeta', module: 'arrow' } + changed_when: false + ansible.builtin.command: + cmd: ~/miniforge3/envs/{{ item.env }}/bin/python -c 'import {{ item.module }}' + + - name: "Verify | check that no environments were deployed for Charlie" + become: true + become_user: charlie + register: miniforge_charlie_envs + failed_when: miniforge_charlie_envs.stdout_lines | length > 0 + changed_when: false + ansible.builtin.command: + cmd: "ls ~/miniforge3/envs" diff --git a/roles/miniforge/tasks/main.yml b/roles/miniforge/tasks/main.yml new file mode 100644 index 0000000..678a863 --- /dev/null +++ b/roles/miniforge/tasks/main.yml @@ -0,0 +1,15 @@ +--- +- name: "Download miniforge from '{{ miniforge_release_base_url }}'" + retries: 3 + delay: 1 + ansible.builtin.get_url: + timeout: 10 + url: "{{ miniforge_release_base_url }}/{{ miniforge_release }}/{{ miniforge_install_script_name }}.sh" + dest: "{{ miniforge_script_path }}" + mode: "0664" + +- name: "Deploy Mamba in a user loop" + loop: "{{ miniforge_users }}" + loop_control: + loop_var: user + ansible.builtin.include_tasks: "miniforge.yml" diff --git a/roles/miniforge/tasks/manifests.yml b/roles/miniforge/tasks/manifests.yml new file mode 100644 index 0000000..c8d7cbb --- /dev/null +++ b/roles/miniforge/tasks/manifests.yml @@ -0,0 +1,24 @@ +--- +- name: "Manifests | get contents of the manifest file" + register: miniforge_manifest_content + ansible.builtin.slurp: + src: "{{ miniforge_homedir }}/{{ file }}" + +# Since the file can be named whatever, we need to extract the environment name +# as that is what the actual environment folder name will be +- name: "Manifests | extract environment name from manifest file" + ansible.builtin.set_fact: + miniforge_env_name: '{{ (miniforge_manifest_content.content | b64decode | from_yaml).name }}' + +- name: "Manifests | display the extracted environment name" + ansible.builtin.debug: + msg: "{{ miniforge_env_name }}" + +- name: "Manifests | create environment: {{ miniforge_env_name }}" + become: true + become_user: "{{ user.name }}" + ansible.builtin.shell: + cmd: "{{ miniforge_homedir }}/miniforge3/bin/mamba env create -f {{ miniforge_homedir }}/{{ file }}" + executable: /bin/bash + # ensures that the task runs only when the environment doesn't exist + creates: "{{ miniforge_homedir }}/miniforge3/envs/{{ miniforge_env_name }}" diff --git a/roles/miniforge/tasks/miniforge.yml b/roles/miniforge/tasks/miniforge.yml new file mode 100644 index 0000000..5c74a6b --- /dev/null +++ b/roles/miniforge/tasks/miniforge.yml @@ -0,0 +1,101 @@ +--- +# These variables are registered as host vars, for whatever deep reason +- name: "Miniforge | get current user entity info ({{ user.name }})" + ansible.builtin.getent: + database: passwd + key: "{{ user.name }}" + split: ":" + +- name: "Miniforge | get current user home directory ({{ user.name }})" + ansible.builtin.set_fact: + miniforge_homedir: "{{ getent_passwd[user.name][4] }}" + +- name: "Miniforge | set miniforge dir ({{ user.name }})" + ansible.builtin.set_fact: + miniforge_dir: "{{ miniforge_homedir }}/miniforge3" + +- name: "Miniforge | build miniforge bin path ({{ user.name }})" + ansible.builtin.set_fact: + miniforge_mamba_bin_path: "{{ miniforge_dir }}/bin/mamba" + +- name: "Miniforge | display miniforge script path ({{ user.name }})" + ansible.builtin.debug: + msg: "{{ miniforge_script_path }}" + +- name: "Miniforge | install miniforge ({{ user.name }})" + become: true + become_user: "{{ user.name }}" + vars: + allow_world_readable_tmpfiles: true + block: + - name: "Miniforge | check Mamba availability ({{ user.name }})" + changed_when: false + ansible.builtin.command: + cmd: "{{ miniforge_mamba_bin_path }} info" + rescue: + - name: "Miniforge | install miniforge in a non-interactive mode ({{ user.name }})" + ansible.builtin.command: + cmd: "/bin/bash {{ miniforge_script_path }} -b" + creates: "{{ miniforge_homedir }}/miniforge3" + + - name: "Miniforge | initialize Mamba/Conda ({{ user.name }})" + changed_when: false + ansible.builtin.command: + cmd: "{{ miniforge_mamba_bin_path }} shell init" + + - name: "Miniforge | check if Mamba has become available ({{ user.name }})" + changed_when: false + ansible.builtin.command: + cmd: "{{ miniforge_mamba_bin_path }} info" + + +- name: "Miniforge | deploy default .condarc if it doesn't exist already ({{ user.name }})" + become: true + become_user: "{{ user.name }}" + block: + - name: "Miniforge | check if .condarc already exists ({{ user.name }})" + register: miniforge_condarc_stat + failed_when: miniforge_condarc_stat.stat.exists is false + ansible.builtin.stat: + path: "{{ miniforge_homedir }}/.condarc" + + rescue: + - name: "Miniforge | set default .condarc from external file if provided ({{ user.name }})" + when: condarc_file is defined and condarc_file != "" + ansible.builtin.copy: + src: "{{ condarc_file }}" + dest: "{{ miniforge_homedir }}/.condarc" + mode: "0600" + + - name: "Miniforge | set default content via .condarc ({{ user.name }})" + when: condarc_file is not defined or condarc_file == "" + ansible.builtin.copy: + dest: "{{ miniforge_homedir }}/.condarc" + content: "{{ miniforge_condarc | to_nice_yaml }}" + mode: "0600" + + +- name: "Miniforge | deploy manifest files for user: {{ user.name }}" + when: + - "manifest_dir is defined" + - "user.mamba_files is defined" + loop: "{{ user.mamba_files }}" + become: true + become_user: "{{ user.name }}" + ansible.builtin.copy: + src: "{{ manifest_dir }}/{{ item }}" + dest: "{{ miniforge_homedir }}/{{ item }}" + owner: "{{ user.name }}" + group: "{{ user.name }}" + mode: "0660" + + +# Variables from the outer scope (like 'user') will get passed down to the included tasks +- name: "Miniforge | create environments from deployed manifests for user: {{ user.name }}" + when: + - "miniforge_create_envs is true" + - "user.mamba_files is defined" + loop: "{{ user.mamba_files }}" + loop_control: + loop_var: file + ansible.builtin.include_tasks: "manifests.yml" diff --git a/roles/miniforge/vars/main.yml b/roles/miniforge/vars/main.yml new file mode 100644 index 0000000..0e869be --- /dev/null +++ b/roles/miniforge/vars/main.yml @@ -0,0 +1,2 @@ +--- +miniforge_install_script_name: "Miniforge3-{{ ansible_system }}-{{ ansible_architecture }}"