commit e0e0977b1e801555ba4f9382a8fc685700ac79ba Author: decentral1se Date: Mon May 30 12:59:44 2022 +0200 init diff --git a/.ansible-lint.yml b/.ansible-lint.yml new file mode 100644 index 0000000..14b5c9e --- /dev/null +++ b/.ansible-lint.yml @@ -0,0 +1,5 @@ +--- +skip_list: + - fqcn-builtins + - no-jinja-nesting + - experimental diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..bad7b7f --- /dev/null +++ b/.drone.yml @@ -0,0 +1,16 @@ +---- +kind: pipeline +name: default +steps: + - name: integration test + image: python:3.9-buster + environment: + REMOTE_USER: molecule + HCLOUD_TOKEN: + from_secret: HCLOUD_TOKEN + commands: + - apt update && apt install -y pwgen + - mkdir -p /root/.ansible/roles && ln -sr . /root/.ansible/roles/autonomic.new-hetzner + - export INSTANCE_UUID=$(pwgen 8 1) + - pip install -r requirements.txt + - molecule test diff --git a/.envrc.sample b/.envrc.sample new file mode 100644 index 0000000..8a266bf --- /dev/null +++ b/.envrc.sample @@ -0,0 +1,18 @@ +# Your username that you use for accounts on our machines. +export REMOTE_USER= +export ANSIBLE_USER=$REMOTE_USER + +# The path to our pass credentials store +export PASSWORD_STORE_DIR= + +# The Hetzner Cloud API token for managing our instances +# Uncomment the prod/test line below depending on what you're doing +# export HCLOUD_TOKEN=$(pass show logins/hetzner/prod/api_key) +# export HCLOUD_TOKEN=$(pass show logins/hetzner/test/api_key) +export HCLOUD_TOKEN=$(pass show logins/hetzner/cicd/api_key) + +# For molecule role testing +export INSTANCE_UUID=$RANDOM + +# So molecule will show credentials in the logs +export MOLECULE_NO_LOG=False diff --git a/.yamllint.yml b/.yamllint.yml new file mode 100755 index 0000000..456c99c --- /dev/null +++ b/.yamllint.yml @@ -0,0 +1,16 @@ +--- +extends: default + +yaml-files: + - "*.yaml" + - "*.yml" + +ignore: | + .venv + .drone.yml + +rules: + line-length: disable + braces: + max-spaces-inside: 1 + level: error diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fb71d97 --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ +autonomic.new-hetzner: Creates and runs "bootstrapping" roles against a new Hetzner Cloud server +Copyright (C) 2022 Autonomic Co-operative + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . diff --git a/README.md b/README.md new file mode 100644 index 0000000..77401f0 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# autonomic.new-hetzner + +[![Build Status](https://drone.autonomic.zone/api/badges/autonomic-cooperative/autonomic.new-hetzner/status.svg?ref=refs/heads/main)](https://drone.autonomic.zone/autonomic-cooperative/autonomic.new-hetzner) diff --git a/defaults/main.yml b/defaults/main.yml new file mode 100644 index 0000000..0a55b62 --- /dev/null +++ b/defaults/main.yml @@ -0,0 +1,4 @@ +--- +new_hetzner_backups_enabled: true +new_hetzner_delete_protection: true +new_hetzner_rebuild_protection: true diff --git a/meta/main.yml b/meta/main.yml new file mode 100644 index 0000000..eab7715 --- /dev/null +++ b/meta/main.yml @@ -0,0 +1,20 @@ +--- +dependencies: [] +galaxy_info: + role_name: new_hetzner + namespace: autonomic + author: autonomic + description: | + Creates and runs "bootstrapping" roles against a new Hetzner Cloud server. + The roles that are run are: autonomic.add-users, autonomic.sshd, + autonomic.ufw, autonomic.packages, autonomic.name and autonomic.motd. The + new user passwords are generated and stored in the password store. If + generating a new server, don't forget to add the details to the inventory + listing. + company: Autonomic + license: GPLv3 + min_ansible_version: 2.9 + platforms: + - name: Debian + versions: + - buster diff --git a/molecule/default/Dockerfile.j2 b/molecule/default/Dockerfile.j2 new file mode 100644 index 0000000..b8d463b --- /dev/null +++ b/molecule/default/Dockerfile.j2 @@ -0,0 +1,17 @@ +# Molecule managed + +{% if item.registry is defined %} +FROM {{ item.registry.url }}/{{ item.image }} +{% else %} +FROM {{ item.image }} +{% endif %} + +{% if item.env is defined %} +{% for var, value in item.env.items() %} +{% if value %} +ENV {{ var }} {{ value }} +{% endif %} +{% endfor %} +{% endif %} + +RUN apt-get update && apt-get install -y python sudo bash ca-certificates iproute2 && apt-get clean; diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml new file mode 100644 index 0000000..d7b62d6 --- /dev/null +++ b/molecule/default/converge.yml @@ -0,0 +1,25 @@ +--- +- name: Converge + hosts: all + vars: + - new_hetzner_server_name: autonomic.new-hetzner-molecule + - add_users_inventory_hostname: autonomic.new-hetzner-molecule + - new_hetzner_server_type: cx11 + - new_hetzner_server_image: debian-10 + - new_hetzner_delete_protection: false + - new_hetzner_rebuild_protection: false + tasks: + - name: Run the role under test + block: + - import_role: + name: autonomic.new-hetzner + always: + - name: Flush all handlers + meta: flush_handlers + ignore_errors: true + + - name: Ensure the server is deleted + hcloud_server: + name: "{{ new_hetzner_server_name }}" + api_token: "{{ lookup('env', 'HCLOUD_TOKEN') }}" + state: absent diff --git a/molecule/default/molecule.yml b/molecule/default/molecule.yml new file mode 100644 index 0000000..77fb312 --- /dev/null +++ b/molecule/default/molecule.yml @@ -0,0 +1,37 @@ +--- +dependency: + name: galaxy + +driver: + name: docker + +platforms: + - name: "autonomic.new-hetzner-${INSTANCE_UUID}" + image: debian:buster + +provisioner: + name: ansible + +lint: | + set -e + yamllint -c .yamllint.yml . + ansible-lint --exclude .drone.yml -c .ansible-lint.yml . + +scenario: + test_sequence: + - lint + - dependency + - cleanup + - destroy + - syntax + - create + - prepare + - converge + # TODO(decentral1se): Disabled for now since there are so many tasks which + # simply always report changed and I'd rather not patch this issue which is + # really something to do with Ansible and not Molecule. + # - idempotence + - side_effect + - verify + - cleanup + - destroy diff --git a/molecule/default/prepare.yml b/molecule/default/prepare.yml new file mode 100644 index 0000000..bb7ceae --- /dev/null +++ b/molecule/default/prepare.yml @@ -0,0 +1,16 @@ +--- +- name: Converge + hosts: all + tasks: + - name: Install python-pip + package: + name: + - python-apt + - python-pip + - openssh-client + - pass + state: present + - name: Install module dependencies + pip: + name: hcloud + state: present diff --git a/molecule/default/requirements.yml b/molecule/default/requirements.yml new file mode 100644 index 0000000..acc542a --- /dev/null +++ b/molecule/default/requirements.yml @@ -0,0 +1,36 @@ +--- + +roles: + - name: autonomic.add-users + src: https://git.autonomic.zone/autonomic-cooperative/autonomic.add-users + version: 0.1.0 + scm: git + + - name: autonomic.sshd + src: https://git.autonomic.zone/autonomic-cooperative/autonomic.sshd + version: 0.1.0 + scm: git + + - name: autonomic.ufw + src: https://git.autonomic.zone/autonomic-cooperative/autonomic.ufw + version: 0.1.0 + scm: git + + - name: autonomic.packages + src: https://git.autonomic.zone/autonomic-cooperative/autonomic.packages + version: 0.1.0 + scm: git + + - name: autonomic.name + src: https://git.autonomic.zone/autonomic-cooperative/autonomic.name + version: 0.1.0 + scm: git + + - name: autonomic.motd + src: https://git.autonomic.zone/autonomic-cooperative/autonomic.motd + version: 0.1.0 + scm: git + +collections: + - name: hetzner.hcloud + version: 1.6.0 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8a2482b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +ansible-lint==6.0.0 +ansible==5.4.0 +molecule-docker=1.1.0 +molecule-hetznercloud==1.3.0 +molecule==3.6.1 diff --git a/tasks/main.yml b/tasks/main.yml new file mode 100644 index 0000000..3eb1372 --- /dev/null +++ b/tasks/main.yml @@ -0,0 +1,143 @@ +--- +- name: Ensure mandatory variables are configured + assert: + that: "{{ item }} is defined" + fail_msg: "You must define the '{{ item }}' variable" + with_items: + - new_hetzner_server_name + - new_hetzner_server_type + - new_hetzner_server_image + +- name: Include resource variables + include_vars: "{{ role_path }}/../../resources/{{ lookup('env', 'MEMBERS_FILE') | default('members.yml', True) }}" + +# Note(decentral1se): gives root SSH access for all autonomic members +- name: Ensure all Autonomic member SSH keys are registered + hcloud_ssh_key: + name: "{{ item.email }}" + api_token: "{{ lookup('env', 'HCLOUD_TOKEN') }}" + public_key: "{{ item.ssh_key }}" + state: present + with_items: "{{ members }}" + +- name: Create new hetzner cloud instance + hcloud_server: + api_token: "{{ lookup('env', 'HCLOUD_TOKEN') }}" + image: "{{ new_hetzner_server_image }}" + name: "{{ new_hetzner_server_name }}" + server_type: "{{ new_hetzner_server_type }}" + ssh_keys: "{{ members | map(attribute='email') | list }}" + labels: + managed: ansible + backups: "{{ new_hetzner_backups_enabled }}" + delete_protection: "{{ new_hetzner_delete_protection }}" + rebuild_protection: "{{ new_hetzner_rebuild_protection }}" + state: present + register: new_instance + async: 7200 + poll: 0 + +- name: Wait for instance creation to complete + async_status: + jid: "{{ new_instance.ansible_job_id }}" + register: hetzner_job + until: hetzner_job.finished + retries: 300 + +# Note(decentral1se): before user accounts are created we connect as root. This +# is possible because we registered the SSH keys with Hetzner Cloud and when +# the new instance is created, those keys are in /root/authorized_keys +- name: Dynamically create root connection details for the new instance + add_host: + hostname: root-new-instance + ansible_host: "{{ hetzner_job.hcloud_server.ipv4_address }}" + ansible_ssh_extra_args: "-o StrictHostKeyChecking=no" + ansible_user: root + +- name: "Wait for SSH on {{ new_hetzner_server_name }} to come up on port 22" + wait_for: + port: 22 + host: "{{ hetzner_job.hcloud_server.ipv4_address }}" + search_regex: SSH + delay: 10 + +- name: Run the add-users role on the new instance + vars: + members: "../../../resources/members.yml" + delegate_to: root-new-instance + import_role: + name: autonomic.add-users + tags: + - molecule-notest + +- name: Run the sshd role on the new instance + delegate_to: root-new-instance + import_role: + name: autonomic.sshd + tags: + - molecule-notest + +- name: Run all service restart handlers + delegate_to: root-new-instance + meta: flush_handlers + tags: + - molecule-notest + +- name: "Wait for SSH to come up again on {{ sshd_port }}" + wait_for: + port: "{{ sshd_port }}" + host: "{{ hetzner_job.hcloud_server.ipv4_address }}" + search_regex: SSH + delay: 10 + tags: + - molecule-notest + +# Note(decentral1se): At this point we're connecting with our own new user +# account and using sudo based privilege escalation with the password generated +# by pass from the passwords repository. Dog fooding our own connection setup ensures it +# works +- name: "Dynamically create {{ lookup('env', 'REMOTE_USER') }} connection details for the new instance" + add_host: + hostname: user-new-instance + ansible_host: "{{ hetzner_job.hcloud_server.ipv4_address }}" + ansible_ssh_extra_args: "-o StrictHostKeyChecking=no" + ansible_user: "{{ ansible_user }}" + ansible_port: "{{ sshd_port }}" + + # Note(decentral1se): Ansible refuses to use the correct password without specifying both of these + ansible_sudo_pass: "{{ lookup('passwordstore', 'users/{{ ansible_user }}/sudo/{{ new_hetzner_server_name }}') }}" + ansible_become_password: "{{ lookup('passwordstore', 'users/{{ ansible_user }}/sudo/{{ new_hetzner_server_name }}') }}" + tags: + - molecule-notest + +- name: Run the ufw role on the new instance + delegate_to: user-new-instance + become: true + import_role: + name: autonomic.ufw + tags: + - molecule-notest + +- name: Run the packages role on the new instance + delegate_to: user-new-instance + become: true + import_role: + name: autonomic.packages + tags: + - molecule-notest + +- name: Run the name role on the new instance + delegate_to: user-new-instance + become: true + import_role: + name: autonomic.name + tags: + - molecule-notest + +- name: Run the motd role on the new instance + delegate_to: user-new-instance + become: true + import_role: + name: autonomic.motd + tags: + - molecule-notest