diff --git a/README.md b/README.md index d48c21dd..7ad85c97 100644 --- a/README.md +++ b/README.md @@ -426,7 +426,7 @@ PVE version. They should be IPv4 or IPv6 addresses. For more information, refer to the [Cluster Manager][pvecm-network] chapter in the PVE Documentation. ``` -# pve_cluster_addr0: "{{ ansible_default_ipv4.address }}" +# pve_cluster_addr0: "{{ defaults to the default interface ipv4 or ipv6 if detected }}" # pve_cluster_addr1: "another interface's IP address or hostname" ``` diff --git a/defaults/main.yml b/defaults/main.yml index b9d4f7a0..873156a0 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -33,11 +33,12 @@ pve_ceph_crush_rules: [] pve_cluster_enabled: no pve_cluster_clustername: "{{ pve_group }}" pve_manage_hosts_enabled: yes -# pve_cluster_addr0: "{{ ansible_default_ipv4.address }}" +pve_cluster_addr0: "{{ ansible_default_ipv4.address if ansible_default_ipv4.address is defined else ansible_default_ipv6.address if ansible_default_ipv6.address is defined }}" # pve_cluster_addr1: "{{ ansible_eth1.ipv4.address }} pve_datacenter_cfg: {} pve_cluster_ha_groups: [] # additional roles for your cluster (f.e. for monitoring) +pve_pools: [] pve_roles: [] pve_groups: [] pve_users: [] diff --git a/handlers/main.yml b/handlers/main.yml index af6f0d96..98d3165b 100644 --- a/handlers/main.yml +++ b/handlers/main.yml @@ -10,9 +10,9 @@ name: pveproxy state: restarted -- name: reload sshd configuration - service: - name: sshd +- name: reload ssh server configuration + ansible.builtin.systemd: + name: ssh.service state: reloaded - name: restart watchdog-mux diff --git a/library/collect_kernel_info.py b/library/collect_kernel_info.py index b51a89be..d1151a73 100755 --- a/library/collect_kernel_info.py +++ b/library/collect_kernel_info.py @@ -1,11 +1,11 @@ #!/usr/bin/python import glob -import re import subprocess from ansible.module_utils.basic import AnsibleModule from ansible.module_utils._text import to_text + def main(): module = AnsibleModule( argument_spec = dict( @@ -16,52 +16,54 @@ def main(): params = module.params - # Much of the following is reimplemented from /usr/share/grub/grub-mkconfig_lib - kernels = [] - # Collect a list of possible installed kernels - for filename in glob.glob("/boot/vmlinuz-*") + glob.glob("/vmlinuz-*") + \ - glob.glob("/boot/kernel-*"): - if ".dpkg-" in filename: - continue - if filename.endswith(".rpmsave") or filename.endswith(".rpmnew"): - continue - kernels.append(filename) + # Collect a list of installed kernels + kernels = glob.glob("/lib/modules/*") + # Identify path to the latest kernel latest_kernel = "" - re_prefix = re.compile("[^-]*-") - re_attributes = re.compile("[._-](pre|rc|test|git|old|trunk)") for kernel in kernels: - right = re.sub(re_attributes, "~\1", re.sub(re_prefix, '', latest_kernel, count=1)) - if not right: + if not latest_kernel: latest_kernel = kernel continue - left = re.sub(re_attributes, "~\1", re.sub(re_prefix, '', kernel, count=1)) + # These splits remove the path and get the base directory name, which + # should be something like 5.4.78-1-pve, that we can compare + right = latest_kernel.split("/")[-1] + left = kernel.split("/")[-1] cmp_str = "gt" - if left.endswith(".old") and not right.endswith(".old"): - left = left[:-4] - if right.endswith(".old") and not left.endswith(".old"): - right = right[:-4] - cmp_str = "ge" if subprocess.call(["dpkg", "--compare-versions", left, cmp_str, right]) == 0: latest_kernel = kernel - # This will likely output a path that considers the boot partition as / - # e.g. /vmlinuz-4.4.44-1-pve - booted_kernel = to_text(subprocess.check_output(["grep", "-o", "-P", "(?<=BOOT_IMAGE=).*?(?= )", "/proc/cmdline"]).strip()) + booted_kernel = "/lib/modules/{}".format(to_text( + subprocess.run(["uname", "-r"], capture_output=True).stdout.strip)) booted_kernel_package = "" old_kernel_packages = [] - if params['lookup_packages']: for kernel in kernels: + # Identify the currently booted kernel and unused old kernels by + # querying which packages own directories in /lib/modules + try: + sp = subprocess.run(["dpkg-query", "-S", kernel], + check=True, capture_output=True) + except subprocess.CalledProcessError as e: + # Ignore errors about directories not associated with a package + if e.stderr.startswith(b"dpkg-query: no path found matching"): + continue + raise e if kernel.split("/")[-1] == booted_kernel.split("/")[-1]: - booted_kernel_package = to_text(subprocess.check_output(["dpkg-query", "-S", kernel])).split(":")[0] + booted_kernel_package = to_text(sp.stdout).split(":")[0] elif kernel != latest_kernel: - old_kernel_packages.append(to_text(subprocess.check_output(["dpkg-query", "-S", kernel])).split(":")[0]) + old_kernel_packages.append(to_text(sp.stdout).split(":")[0]) # returns True if we're not booted into the latest kernel new_kernel_exists = booted_kernel.split("/")[-1] != latest_kernel.split("/")[-1] - module.exit_json(changed=False, new_kernel_exists=new_kernel_exists, old_packages=old_kernel_packages, booted_package=booted_kernel_package) + module.exit_json( + changed=False, + new_kernel_exists=new_kernel_exists, + old_packages=old_kernel_packages, + booted_package=booted_kernel_package + ) + if __name__ == '__main__': main() diff --git a/library/proxmox_pool.py b/library/proxmox_pool.py new file mode 100644 index 00000000..4671e5ad --- /dev/null +++ b/library/proxmox_pool.py @@ -0,0 +1,175 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'status': ['stableinterface'], + 'supported_by': 'futuriste' +} + +DOCUMENTATION = ''' +--- +module: proxmox_pool + +short_description: Manages pools in Proxmox + +options: + name: + required: true + aliases: [ "pool", "poolid" ] + description: + - Name of the PVE pool to manage. + state: + required: false + default: "present" + choices: [ "present", "absent" ] + description: + - Specifies whether the pool should exist or not. + comment: + required: false + description: + - Optionally sets the pool's comment in PVE. + +author: + - Guiffo Joel (@futuriste) +''' + +EXAMPLES = ''' +- name: Create Administrators pool + proxmox_pool: + name: Administrators +- name: Create Dev Users pool's + proxmox_pool: + name: pool_dev + comment: Dev Users allowed to access on this pool. +''' + +RETURN = ''' +updated_fields: + description: Fields that were modified for an existing pool + type: list +pool: + description: Information about the pool fetched from PVE after this task completed. + type: json +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_text +from ansible.module_utils.pvesh import ProxmoxShellError +import ansible.module_utils.pvesh as pvesh + +class ProxmoxPool(object): + def __init__(self, module): + self.module = module + self.name = module.params['name'] + self.state = module.params['state'] + self.comment = module.params['comment'] + + def lookup(self): + try: + return pvesh.get("pools/{}".format(self.name)) + except ProxmoxShellError as e: + self.module.fail_json(msg=e.message, status_code=e.status_code, **result) + + def remove_pool(self): + try: + pvesh.delete("pools/{}".format(self.name)) + return (True, None) + except ProxmoxShellError as e: + return (False, e.message) + + def create_pool(self): + new_pool = {} + if self.comment is not None: + new_pool['comment'] = self.comment + + try: + pvesh.create("pools", poolid=self.name, **new_pool) + return (True, None) + except ProxmoxShellError as e: + return (False, e.message) + + def modify_pool(self): + lookup = self.lookup() + staged_pool = {} + + if self.comment is not None: + staged_pool['comment'] = self.comment + + updated_fields = [] + error = None + + for key in staged_pool: + staged_value = to_text(staged_pool[key]) if isinstance(staged_pool[key], str) else staged_pool[key] + if key not in lookup or staged_value != lookup[key]: + updated_fields.append(key) + + if self.module.check_mode: + self.module.exit_json(changed=bool(updated_fields), expected_changes=updated_fields) + + if not updated_fields: + # No changes necessary + return (updated_fields, error) + + try: + pvesh.set("pools/{}".format(self.name), **staged_pool) + except ProxmoxShellError as e: + error = e.message + + return (updated_fields, error) + +def main(): + # Refer to https://pve.proxmox.com/pve-docs/api-viewer/index.html + module = AnsibleModule( + argument_spec = dict( + name=dict(type='str', required=True, aliases=['pool', 'poolid']), + state=dict(default='present', choices=['present', 'absent'], type='str'), + comment=dict(default=None, type='str'), + ), + supports_check_mode=True + ) + + pool = ProxmoxPool(module) + + changed = False + error = None + result = {} + result['name'] = pool.name + result['state'] = pool.state + + if pool.state == 'absent': + if pool.lookup() is not None: + if module.check_mode: + module.exit_json(changed=True) + + (changed, error) = pool.remove_pool() + + if error is not None: + module.fail_json(name=pool.name, msg=error) + elif pool.state == 'present': + if not pool.lookup(): + if module.check_mode: + module.exit_json(changed=True) + + (changed, error) = pool.create_pool() + else: + # modify pool (note: this function is check mode aware) + (updated_fields, error) = pool.modify_pool() + + if updated_fields: + changed = True + result['updated_fields'] = updated_fields + + if error is not None: + module.fail_json(name=pool.name, msg=error) + + lookup = pool.lookup() + if lookup is not None: + result['pool'] = lookup + + result['changed'] = changed + + module.exit_json(**result) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/module_utils/pvesh.py b/module_utils/pvesh.py index 410085b7..9fd5dace 100644 --- a/module_utils/pvesh.py +++ b/module_utils/pvesh.py @@ -57,7 +57,7 @@ def run_command(handler, resource, **params): if handler == "get": if any(re.match(pattern, stderr[0]) for pattern in [ "^no such user \('.{3,64}?'\)$", - "^(group|role) '[A-Za-z0-9\.\-_]+' does not exist$", + "^(group|role|pool) '[A-Za-z0-9\.\-_]+' does not exist$", "^domain '[A-Za-z][A-Za-z0-9\.\-_]+' does not exist$"]): return {u"status": 404, u"message": stderr[0]} diff --git a/tasks/ceph.yml b/tasks/ceph.yml index 264e1cdb..8be61e53 100644 --- a/tasks/ceph.yml +++ b/tasks/ceph.yml @@ -39,11 +39,12 @@ - block: - name: Query for existing Ceph volumes pve_ceph_volume: + check_mode: no register: _ceph_volume_data - name: Generate a list of active OSDs ansible.builtin.set_fact: - _existing_ceph_osds: "{{ _ceph_volume_data.stdout | from_json | json_query('*[].devices[]') }}" + _existing_ceph_osds: "{{ _ceph_volume_data.stdout | from_json | json_query('*[].devices[]') | default([]) }}" - name: Generate list of unprovisioned OSDs ansible.builtin.set_fact: @@ -126,6 +127,7 @@ - name: List Ceph Pools command: ceph osd pool ls changed_when: false + check_mode: no register: _ceph_pools - name: Create Ceph Pools @@ -175,6 +177,7 @@ - name: List Ceph Filesystems command: ceph fs ls -f json changed_when: false + check_mode: no when: "pve_ceph_fs | length > 0" register: _ceph_fs @@ -192,6 +195,7 @@ - name: Get Ceph Filesystem pool CRUSH rules command: 'ceph -f json osd pool get {{ item.0.name }}_{{ item.1 }} crush_rule' changed_when: false + check_mode: no when: "pve_ceph_fs | length > 0" register: _ceph_fs_rule loop: '{{ pve_ceph_fs | product(["data", "metadata"]) | list }}' @@ -241,7 +245,7 @@ {{ loop.last | ternary("", ",") -}} {% endfor %}:/ fstype: 'ceph' - opts: 'name={{ item.name }},secretfile=/etc/ceph/{{ item.name }}.secret,_netdev' + opts: 'name={{ item.name }},secretfile=/etc/ceph/{{ item.name }}.secret,_netdev,fs={{ item.name }}' state: 'mounted' when: "item.mountpoint is defined" loop: '{{ pve_ceph_fs }}' diff --git a/tasks/kernel_module_cleanup.yml b/tasks/kernel_module_cleanup.yml index 3081f762..579fd030 100644 --- a/tasks/kernel_module_cleanup.yml +++ b/tasks/kernel_module_cleanup.yml @@ -5,7 +5,7 @@ state: absent when: > (pve_zfs_options is not defined) or - (pve_zfs_options is defined and not pve_zfs_options | bool) or + (pve_zfs_options is defined and not pve_zfs_options | length > 0) or (not pve_zfs_enabled | bool) - name: Disable loading of ZFS module on init diff --git a/tasks/load_variables.yml b/tasks/load_variables.yml index a5ec77f7..88e238c5 100644 --- a/tasks/load_variables.yml +++ b/tasks/load_variables.yml @@ -2,28 +2,9 @@ - name: Gather distribution specific variables include_vars: "debian-{{ ansible_distribution_release }}.yml" -- block: - # Per Proxmox documentation, bindnet_addr is expected to be an IP address and - # ring_addr can be either hostname or IP, but this role has always used an IP - # address. Thus, we're deprecating them. See below references. - # https://pve.proxmox.com/wiki/Separate_Cluster_Network#Setup_at_Cluster_Creation - # https://git.proxmox.com/?p=pve-cluster.git;a=blob;f=data/PVE/Corosync.pm;h=8b5c91e0da084da4e9ba7423176872a0c16ef5af;hb=refs/heads/stable-5#l209 - - name: LEGACY - Define pve_cluster_addr0 from link0_addr - set_fact: - pve_cluster_addr0: "{{ pve_cluster_link0_addr }}" - when: "pve_cluster_link0_addr is defined and ansible_distribution_release == 'buster'" - when: "pve_cluster_addr0 is not defined" - -- block: - - name: LEGACY - Define pve_cluster_addr1 from link1_addr - set_fact: - pve_cluster_addr1: "{{ pve_cluster_link1_addr }}" - when: "pve_cluster_link1_addr is defined and ansible_distribution_release == 'buster'" - when: "pve_cluster_addr1 is not defined" - -- name: Define pve_cluster_addr0 if not provided +- name: Ensure pve_cluster_addr0 is in the host facts set_fact: - pve_cluster_addr0: "{{ pve_cluster_addr0 | default(_pve_cluster_addr0) }}" + pve_cluster_addr0: "{{ pve_cluster_addr0 }}" - name: Calculate list of SSH addresses set_fact: diff --git a/tasks/main.yml b/tasks/main.yml index 6b3c887a..356aff48 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -26,7 +26,7 @@ when: - "pve_manage_ssh | bool and pve_cluster_enabled | bool" -- name: Run handlers if needed (sshd reload) +- name: Run handlers if needed (ssh server reload) meta: flush_handlers - name: Enumerate all cluster hosts within the hosts file @@ -202,6 +202,14 @@ - "pve_ceph_enabled | bool" - "inventory_hostname in groups[pve_ceph_nodes]" +- name: Configure Proxmox pools + proxmox_pool: + name: "{{ item.name }}" + state: "{{ item.state | default('present') }}" + comment: "{{ item.comment | default(omit) }}" + with_items: "{{ pve_pools }}" + when: "not pve_cluster_enabled or (pve_cluster_enabled | bool and inventory_hostname == groups[pve_group][0])" + - name: Configure Proxmox roles proxmox_role: name: "{{ item.name }}" diff --git a/tasks/ssh_cluster_config.yml b/tasks/ssh_cluster_config.yml index 5d814bfc..02d9a6df 100644 --- a/tasks/ssh_cluster_config.yml +++ b/tasks/ssh_cluster_config.yml @@ -48,7 +48,13 @@ {% endfor %} validate: "/usr/sbin/sshd -t -f %s" notify: - - reload sshd configuration + - reload ssh server configuration + +- name: Enable and start SSH server + ansible.builtin.systemd: + name: ssh.service + enabled: yes + state: started - name: Fetch a SSH public key to use for cluster joins ansible.builtin.slurp: diff --git a/tasks/zfs.yml b/tasks/zfs.yml index 676c4d95..6d3342c3 100644 --- a/tasks/zfs.yml +++ b/tasks/zfs.yml @@ -17,7 +17,7 @@ content: "options zfs {{ pve_zfs_options }}" dest: /etc/modprobe.d/zfs.conf mode: 0644 - when: "pve_zfs_options is defined and pve_zfs_options | bool" + when: "pve_zfs_options is defined and pve_zfs_options | length > 0" - name: Configure email address for ZFS event daemon notifications lineinfile: diff --git a/tests/vagrant/group_vars/all b/tests/vagrant/group_vars/all index 776dd74e..c25b4bea 100644 --- a/tests/vagrant/group_vars/all +++ b/tests/vagrant/group_vars/all @@ -12,6 +12,9 @@ pve_zfs_zed_email: root@localhost pve_cluster_enabled: yes pve_datacenter_cfg: console: xtermjs +pve_pools: + - name: customer01 + comment: Pool for customer01 pve_groups: - name: Admins comment: Administrators of this PVE cluster diff --git a/vars/main.yml b/vars/main.yml index b49eb989..871123b5 100644 --- a/vars/main.yml +++ b/vars/main.yml @@ -2,6 +2,3 @@ # vars file for ansible-role-proxmox pve_base_dir: "/etc/pve" pve_cluster_conf: "{{ pve_base_dir }}/corosync.conf" - -# defaults that need to be host facts -_pve_cluster_addr0: "{{ ansible_default_ipv4.address }}"