From 1985aec67b691339dd4b3a94cde46345986b59af Mon Sep 17 00:00:00 2001 From: Bruno Travouillon Date: Wed, 23 Feb 2022 23:41:56 -0500 Subject: [PATCH] feat: add new role to manage LXC containers --- README.md | 152 ++++++++++++++++++++ galaxy.yml | 66 +++++++++ roles/container/tasks/create.yml | 57 ++++++++ roles/container/tasks/destroy.yml | 32 +++++ roles/container/tasks/install_templates.yml | 18 +++ roles/container/tasks/main.yml | 8 ++ roles/vm/.gitkeep | 0 7 files changed, 333 insertions(+) create mode 100644 README.md create mode 100644 galaxy.yml create mode 100644 roles/container/tasks/create.yml create mode 100644 roles/container/tasks/destroy.yml create mode 100644 roles/container/tasks/install_templates.yml create mode 100644 roles/container/tasks/main.yml create mode 100644 roles/vm/.gitkeep diff --git a/README.md b/README.md new file mode 100644 index 0000000..9c1fe6e --- /dev/null +++ b/README.md @@ -0,0 +1,152 @@ +# Ansible Collection - mila.proxmox + +The purpose of this collection is to manage containers and virtual machines in +the [Proxmox VE](https://www.proxmox.com/en/proxmox-ve) virtualization +management platform. + +NOTE: The collection is still in its early stages of development, things might +change quickly and without notice while version is still 0.x.y. + +## Requirements + +The below requirements are needed on the host that executes this collection: + + - collection `community.general` + - jmespath + - proxmoxer + - requests + +## Roles + +### container + +To manage containers, use the role `mila.proxmox.container`. This role makes use +of the Ansible modules `community.general.proxmox_template` and +`community.general.proxmox`. + +The containers are created from a list defined in `proxmox_container`. Each item +in the list defines one container. + +Example for a container: + + proxmox_container: + - hostname: 'container1' + node: 'pve-1' + ostemplate: 'local:vztmpl/debian-11-standard_11.0-1_amd64.tar.gz' + disk: 'local:2' + cores: 2 + nameserver: '10.10.0.1' + netif: '{"net0":"name=eth0,gw=10.10.0.254,hwaddr=42:4d:69:6c:61:00,ip=dhcp,bridge=vmbr0"}' + unprivileged: True + features: + - nesting=1 # For systemd, otherwise takes a lot of time to open ssh session + +The role will parse `ostemplate` to ensure the required template is available on +all hosts. + +Most of the parameters supported by the module `community.general.proxmox` can +be defined in the list. The following parameters are not supported yet, but +this might change in future development: `api_password`, `ip_address`, +`mounts`, `password`, `pool`, `purge`, `searchdomain` and `storage`. Note that +`proxmox_default_behavior` is enforced to `no_defaults`. + +To authenticate to the REST API of your Proxmox VE cluster, you first need to +create an [API token][pve_api_tokens], then you must define the 4 **mandatory** +variables below: + + - `proxmox_api_host`: the hostname or IP address of the Proxmox VE API server + - `proxmox_api_user`: the user name to authenticate (e.g. `ansible@pam`) + - `proxmox_api_token_id`: the token name (e.g. `prod`) + - `proxmox_api_token_secret`: the token secret provided by ProxmoxVE + +[pve_api_tokens]: https://pve.proxmox.com/pve-docs/chapter-pveum.html#pveum_tokens + +It is also possible to define the user, token id or token secret on a per +container basis for use cases where the management of a container requires +different access privileges. + + proxmox_container: + - hostname: 'container2' + node: 'pve-1' + api_user: 'root@pam' + api_token_secret: 'MySecretShouldBeInAVault ;)' + features: + - keyctl=1 # This feature needs root access in ProxmoxVE + [...] + +In the example above, `item.api_user` and `item.api_token_secret` will +respectively replace the values of `proxmox_api_user` and +`proxmox_api_token_secret`. + +The example below creates two containers that will serve as DHCP servers. The +configuration of the DHCP service is not managed by this role. The variable +`proxmox_container_dhcp` is defined as a play variable. It is then passed to +`proxmox_container` as a role parameter. It is advised to define it in your +Ansible inventory. + + - name: Instanciate DHCP servers in PVE + hosts: localhost + vars: + proxmox_api_token_secret: "{{ lookup('env','PROXMOX_API_TOKEN') }}" + proxmox_container_dhcp: + - hostname: 'dhcp1' + node: 'pve-1' + ostemplate: 'local:vztmpl/debian-11-standard_11.0-1_amd64.tar.gz' + disk: 'local:2' + cores: 2 + nameserver: '10.10.0.1' + netif: '{"net0":"name=eth0,gw=10.10.0.254,ip=10.10.0.2/24,bridge=vmbr0"}' + unprivileged: True + features: + - nesting=1 # For systemd, otherwise takes a lot of time to open ssh session + - hostname: 'dhcp2' + node: 'pve-2' + ostemplate: 'local:vztmpl/debian-11-standard_11.0-1_amd64.tar.gz' + disk: 'local:2' + cores: 2 + nameserver: '10.10.0.1' + netif: '{"net0":"name=eth0,gw=10.10.0.254,ip=10.10.0.3/24,bridge=vmbr0"}' + unprivileged: True + features: + - nesting=1 # For systemd, otherwise takes a lot of time to open ssh session + roles: + - name: Download proxmox appliance container template + role: mila.proxmox.container + vars: + proxmox_container: "{{ proxmox_container_dhcp }}" + + +Once a container exists, some parameters cannot be changed with the +`community.general.proxmox` module. To circumvent this limitation, it is +possible to destroy the containers and recreate these. For this, include the +role `mila.proxmox.container` in the pre_tasks of your playbook, with +`tasks_from: destroy`. + + - name: Instanciate containers + hosts: localhost + pre_tasks: + - name: Ensure existing containers are removed + block: + - name: Remove SSH public host keys + ansible.builtin.known_hosts: + name: "{{ item }}" + state: absent + loop: + - 10.10.0.2 + - 10.10.0.3 + - name: Cleanup containers + ansible.builtin.include_role: + name: mila.proxmox.container + tasks_from: destroy + vars: + proxmox_container: "{{ proxmox_container_list }}" + when: proxmox_container_destroy_first is defined and proxmox_container_destroy_first + roles: + - name: Instanciate containers in Proxmox VE cluster + role: mila.proxmox.container + vars: + proxmox_container: "{{ proxmox_container_list }}" + +In the example above, ansible-playbook must run with `-e +proxmox_container_destroy_first=True` for the destruction of the containers to +be effective. diff --git a/galaxy.yml b/galaxy.yml new file mode 100644 index 0000000..961e116 --- /dev/null +++ b/galaxy.yml @@ -0,0 +1,66 @@ +### REQUIRED +# The namespace of the collection. This can be a company/brand/organization or product namespace under which all +# content lives. May only contain alphanumeric lowercase characters and underscores. Namespaces cannot start with +# underscores or numbers and cannot contain consecutive underscores +namespace: mila + +# The name of the collection. Has the same character restrictions as 'namespace' +name: proxmox + +# The version of the collection. Must be compatible with semantic versioning +version: 0.1.0 + +# The path to the Markdown (.md) readme file. This path is relative to the root of the collection +readme: README.md + +# A list of the collection's content authors. Can be just the name or in the format 'Full Name (url) +# @nicks:irc/im.site#channel' +authors: +- Bruno Travouillon + + +### OPTIONAL but strongly recommended +# A short summary description of the collection +description: Ansible Collection to manage containers and virtual machines with Proxmox VE + +# Either a single license or a list of licenses for content inside of a collection. Ansible Galaxy currently only +# accepts L(SPDX,https://spdx.org/licenses/) licenses. This key is mutually exclusive with 'license_file' +license: +- MIT + +# The path to the license file for the collection. This path is relative to the root of the collection. This key is +# mutually exclusive with 'license' +license_file: '' + +# A list of tags you want to associate with the collection for indexing/searching. A tag name has the same character +# requirements as 'namespace' and 'name' +tags: + - proxmox + - virtualization + - container + - vm + +# Collections that this collection requires to be installed for it to be usable. The key of the dict is the +# collection label 'namespace.name'. The value is a version range +# L(specifiers,https://python-semanticversion.readthedocs.io/en/latest/#requirement-specification). Multiple version +# range specifiers can be set and are separated by ',' +dependencies: {community.general: '>4.0.0'} + +# The URL of the originating SCM repository +repository: https://github.com/mila-iqia/ansible-collection-proxmox + +# The URL to any online docs +# documentation: http://docs.example.com + +# The URL to the homepage of the collection/project +# homepage: http://example.com + +# The URL to the collection issue tracker +issues: https://github.com/mila-iqia/ansible-collection-proxmox/issues + +# A list of file glob-like patterns used to filter any files or directories that should not be included in the build +# artifact. A pattern is matched from the relative path of the file or directory of the collection directory. This +# uses 'fnmatch' to match the files or directories. Some directories and files like 'galaxy.yml', '*.pyc', '*.retry', +# and '.git' are always filtered +build_ignore: [] + diff --git a/roles/container/tasks/create.yml b/roles/container/tasks/create.yml new file mode 100644 index 0000000..0a2fe22 --- /dev/null +++ b/roles/container/tasks/create.yml @@ -0,0 +1,57 @@ +--- +- name: Setup LXC Containers + community.general.proxmox: + proxmox_default_behavior: 'no_defaults' + api_host: "{{ proxmox_api_host }}" + api_token_id: "{{ item.api_token_id | default(proxmox_api_token_id) }}" + api_token_secret: "{{ item.api_token_secret | default(proxmox_api_token_secret) }}" + api_user: "{{ item.api_user | default(proxmox_api_user) }}" + cores: "{{ item.cores | default(1) }}" + cpus: "{{ item.cpus | default(1) }}" + cpuunits: "{{ item.cpuunits | default(1000) }}" + description: "{{ item.description | default(omit) }}" + disk: "{{ item.disk }}" + features: "{{ item.features | default(omit) }}" + force: "{{ item.force | default(omit) | bool }}" + hookscript: "{{ item.hookscript | default(omit) }}" + hostname: "{{ item.hostname }}" + memory: "{{ item.memory | default(512) }}" + nameserver: "{{ item.nameserver | default(omit) }}" + # Remember to create bridge first on hypervisor + # https://pve.proxmox.com/wiki/Install_Proxmox_VE_on_Debian_11_Bullseye#Create_a_Linux_Bridge + netif: "{{ item.netif }}" + node: "{{ item.node }}" + onboot: "{{ item.onboot | default(False) }}" + ostemplate: "{{ item.ostemplate }}" + pubkey: "{{ item.pubkey | default(proxmox_pubkey) | default(omit) }}" + state: "{{ item.state | default('present') }}" + swap: "{{ item.swap | default(0) }}" + timeout: "{{ item.timeout | default(120) }}" + unprivileged: "{{ item.unprivileged | default(omit) }}" + validate_certs: "{{ item.validate_certs | default(omit) | bool }}" + vmid: "{{ item.vmid | default(omit) }}" + loop: "{{ proxmox_container }}" + register: proxmox_container_setup + +- name: Pause for 10 seconds to finish LXC setup + pause: + seconds: 10 + when: proxmox_container_setup.changed + +- name: Start Containers + community.general.proxmox: + proxmox_default_behavior: 'no_defaults' + api_host: "{{ proxmox_api_host }}" + api_user: "{{ proxmox_api_user }}" + api_token_id: "{{ proxmox_api_token_id }}" + api_token_secret: "{{ proxmox_api_token_secret }}" + node: "{{ item.node }}" + hostname: "{{ item.hostname }}" + state: started + loop: "{{ proxmox_container }}" + register: proxmox_container_start + +- name: Pause for 20 seconds to finish LXC start + pause: + seconds: 20 + when: proxmox_container_start.changed diff --git a/roles/container/tasks/destroy.yml b/roles/container/tasks/destroy.yml new file mode 100644 index 0000000..5b761cc --- /dev/null +++ b/roles/container/tasks/destroy.yml @@ -0,0 +1,32 @@ +--- +- name: Stop Containers + community.general.proxmox: + proxmox_default_behavior: 'no_defaults' + #proxmox_default_behavior: 'compatibility' + api_host: "{{ proxmox_api_host }}" + api_user: "{{ proxmox_api_user }}" + api_token_id: "{{ proxmox_api_token_id }}" + api_token_secret: "{{ proxmox_api_token_secret }}" + node: "{{ item.node }}" + hostname: "{{ item.hostname }}" + state: stopped + loop: "{{ proxmox_container }}" + register: proxmox_container_stop + +- name: Pause for 10 seconds to finish LXC stop + pause: + seconds: 10 + when: proxmox_container_stop.changed + +- name: Destroy Containers + community.general.proxmox: + proxmox_default_behavior: 'no_defaults' + #proxmox_default_behavior: 'compatibility' + api_host: "{{ proxmox_api_host }}" + api_user: "{{ proxmox_api_user }}" + api_token_id: "{{ proxmox_api_token_id }}" + api_token_secret: "{{ proxmox_api_token_secret }}" + node: "{{ item.node }}" + hostname: "{{ item.hostname }}" + state: absent + loop: "{{ proxmox_container }}" diff --git a/roles/container/tasks/install_templates.yml b/roles/container/tasks/install_templates.yml new file mode 100644 index 0000000..6ca85b4 --- /dev/null +++ b/roles/container/tasks/install_templates.yml @@ -0,0 +1,18 @@ +--- +- name: Set facts to install OS templates + ansible.builtin.set_fact: + _ostemplate_regex: '^([a-z]+):([a-z]+)/(.*)' + _list_of_templates: "{{ proxmox_container | community.general.json_query('[*].ostemplate') | unique }}" + +- name: Download proxmox appliance container templates + community.general.proxmox_template: + api_host: "{{ proxmox_api_host }}" + api_user: "{{ proxmox_api_user }}" + api_token_id: "{{ proxmox_api_token_id }}" + api_token_secret: "{{ proxmox_api_token_secret }}" + node: "{{ item.0 }}" + storage: "{{ item.1 | regex_search(_ostemplate_regex, '\\1') | first }}" + content_type: "{{ item.1 | regex_search(_ostemplate_regex, '\\2') | first }}" + template: "{{ item.1 | regex_search(_ostemplate_regex, '\\3') | first }}" + timeout: 300 + loop: "{{ groups[pve_group] | product(_list_of_templates) | list }}" diff --git a/roles/container/tasks/main.yml b/roles/container/tasks/main.yml new file mode 100644 index 0000000..0becd0a --- /dev/null +++ b/roles/container/tasks/main.yml @@ -0,0 +1,8 @@ +--- +- name: Import tasks to install containers templates + ansible.builtin.include_tasks: + file: install_templates.yml + +- name: Import tasks to create containers + ansible.builtin.include_tasks: + file: create.yml diff --git a/roles/vm/.gitkeep b/roles/vm/.gitkeep new file mode 100644 index 0000000..e69de29