From 5b0cedc231f677d6e8ff2579c8f8a8bb13c47236 Mon Sep 17 00:00:00 2001 From: Allar Viik Date: Mon, 22 Jun 2026 10:29:42 +0300 Subject: [PATCH 01/18] Added Google Cloud Platform support to `machine_operations` & `connection` role --- nova/core/roles/accounts/tasks/main.yml | 11 ++- nova/core/roles/connection/tasks/aws.yml | 2 +- nova/core/roles/connection/tasks/azure.yml | 2 +- nova/core/roles/connection/tasks/external.yml | 2 +- nova/core/roles/connection/tasks/google.yml | 13 +++ nova/core/roles/connection/tasks/linode.yml | 2 +- nova/core/roles/connection/tasks/main.yml | 20 ++-- .../core/roles/connection/tasks/openstack.yml | 2 +- nova/core/roles/connection/tasks/proxmox.yml | 2 +- nova/core/roles/connection/tasks/vsphere.yml | 4 +- nova/core/roles/deploy_vars/defaults/main.yml | 21 ++++ nova/core/roles/deploy_vars/tasks/main.yml | 2 +- .../machine_operations/defaults/main.yml | 28 ++++++ .../machine_operations/tasks/aws/create.yml | 2 +- .../tasks/google/create.yml | 75 +++++++++++++++ .../tasks/google/create_firewall_rules.yml | 14 +++ .../tasks/google/create_networks.yml | 34 +++++++ .../machine_operations/tasks/google/main.yml | 57 +++++++++++ .../tasks/google/remove.yml | 20 ++++ .../roles/machine_operations/tasks/main.yml | 18 ++-- .../templates/Configure-CloudWindows.ps1 | 95 ++++++++++--------- .../templates/google_cloud_interfaces.yml | 24 +++++ .../google_cloud_linux_startup_script.yml | 19 ++++ nova/core/roles/powerstate/google/main.yml | 47 +++++++++ 24 files changed, 446 insertions(+), 70 deletions(-) create mode 100644 nova/core/roles/connection/tasks/google.yml create mode 100644 nova/core/roles/machine_operations/tasks/google/create.yml create mode 100644 nova/core/roles/machine_operations/tasks/google/create_firewall_rules.yml create mode 100644 nova/core/roles/machine_operations/tasks/google/create_networks.yml create mode 100644 nova/core/roles/machine_operations/tasks/google/main.yml create mode 100644 nova/core/roles/machine_operations/tasks/google/remove.yml create mode 100644 nova/core/roles/machine_operations/templates/google_cloud_interfaces.yml create mode 100644 nova/core/roles/machine_operations/templates/google_cloud_linux_startup_script.yml create mode 100644 nova/core/roles/powerstate/google/main.yml diff --git a/nova/core/roles/accounts/tasks/main.yml b/nova/core/roles/accounts/tasks/main.yml index 0b466c4be..7ebdc145a 100644 --- a/nova/core/roles/accounts/tasks/main.yml +++ b/nova/core/roles/accounts/tasks/main.yml @@ -104,7 +104,7 @@ when: ansible_facts.system | default('') == "Darwin" or ansible_custom_facts.system | default('') == "Darwin" - # Cloud temp keys removal + # Cloud temp SSH keys removal - name: Removing {{ infra_env }} temp ssh key for {{ inventory_hostname }}... ansible.builtin.file: path: "{{ lookup('vars', 'machine_operations_' ~ infra_env ~ '_temp_ssh_key_path') | default('') }}" @@ -113,6 +113,15 @@ become: false when: lookup('vars', 'machine_operations_' ~ infra_env ~ '_temp_ssh_key_path') is defined + # Cloud temp SSHD config removal + - name: Removing {{ infra_env }} temp SSHD config for {{ inventory_hostname }}... + ansible.builtin.file: + path: "{{ lookup('vars', 'machine_operations_' ~ infra_env ~ '_temp_sshd_config_path') | default('') }}" + state: absent + when: + - ansible_facts.system | default('') in ["FreeBSD", "Linux"] + - lookup('vars', 'machine_operations_' ~ infra_env ~ '_temp_sshd_config_path') is defined + - name: Including following accounts post-roles... ansible.builtin.include_role: name: "{{ account_post_role }}" diff --git a/nova/core/roles/connection/tasks/aws.yml b/nova/core/roles/connection/tasks/aws.yml index afcd2f221..405291d73 100644 --- a/nova/core/roles/connection/tasks/aws.yml +++ b/nova/core/roles/connection/tasks/aws.yml @@ -11,7 +11,7 @@ - name: Connecting as {{ aws_template_username | default(template_username) if fresh_deploy else ansible_deployer_username }} to {{ inventory_hostname }} using {{ ansible_connection | default('ssh') | upper - }} over {{ connection_address_custom | default(connection_address) }}... # noqa: name[template] + }} over {{ connection_address_custom | default(connection_address) }} # noqa: name[template] ansible.builtin.set_fact: ansible_host: "{{ connection_address_custom | default(connection_address) }}" ansible_user: "{{ aws_template_username | default(template_username) if fresh_deploy else ansible_deployer_username }}" diff --git a/nova/core/roles/connection/tasks/azure.yml b/nova/core/roles/connection/tasks/azure.yml index 2c0c1c02d..26beff7ef 100644 --- a/nova/core/roles/connection/tasks/azure.yml +++ b/nova/core/roles/connection/tasks/azure.yml @@ -3,7 +3,7 @@ Connecting as {{ azure_template_username if fresh_deploy else ansible_deployer_username }} to {{ inventory_hostname }} using {{ ansible_connection | default('ssh') | upper }} - over {{ connection_address_custom | default(connection_address) }}... + over {{ connection_address_custom | default(connection_address) }} ansible.builtin.set_fact: ansible_host: "{{ connection_address_custom | default(connection_address) }}" ansible_user: "{{ azure_template_username if fresh_deploy else ansible_deployer_username }}" diff --git a/nova/core/roles/connection/tasks/external.yml b/nova/core/roles/connection/tasks/external.yml index 19f13abb2..b5bd18420 100644 --- a/nova/core/roles/connection/tasks/external.yml +++ b/nova/core/roles/connection/tasks/external.yml @@ -16,7 +16,7 @@ - name: Connecting as {{ ansible_deployer_username }} to {{ inventory_hostname }} using {{ ansible_connection | default('ssh') | upper }} - over {{ connection_address_custom | default(connection_address) }}... + over {{ connection_address_custom | default(connection_address) }} ansible.builtin.set_fact: ansible_host: "{{ connection_address_custom | default(connection_address) }}" ansible_user: "{{ ansible_deployer_username }}" diff --git a/nova/core/roles/connection/tasks/google.yml b/nova/core/roles/connection/tasks/google.yml new file mode 100644 index 000000000..23a72f71a --- /dev/null +++ b/nova/core/roles/connection/tasks/google.yml @@ -0,0 +1,13 @@ +--- +- name: + Connecting as {{ template_username | default(admin_account) if fresh_deploy else ansible_deployer_username }} + to {{ inventory_hostname }} using {{ ansible_connection | default('ssh') | upper }} + over {{ connection_address_custom | default(connection_address) }} + ansible.builtin.set_fact: + ansible_host: "{{ connection_address_custom | default(connection_address) }}" + ansible_user: "{{ template_username | default(admin_account) if fresh_deploy else ansible_deployer_username }}" + ansible_password: "{{ template_password if fresh_deploy else omit if connection_no_password else ansible_deployer_password }}" + + # Initial connection will forced to password to bypass default SSHD config + ansible_ssh_extra_args: "{{ connection_custom_ansible_ssh_extra_args + | default('-o PreferredAuthentications=password' if fresh_deploy else omit) }}" diff --git a/nova/core/roles/connection/tasks/linode.yml b/nova/core/roles/connection/tasks/linode.yml index 2e619732c..691375182 100644 --- a/nova/core/roles/connection/tasks/linode.yml +++ b/nova/core/roles/connection/tasks/linode.yml @@ -2,7 +2,7 @@ - name: Connecting as {{ template_username if fresh_deploy else ansible_deployer_username }} to {{ inventory_hostname }} using {{ ansible_connection | default('ssh') | upper }} - over {{ connection_address_custom | default(connection_address) }}... + over {{ connection_address_custom | default(connection_address) }} ansible.builtin.set_fact: ansible_host: "{{ connection_address_custom | default(connection_address) }}" ansible_user: "{{ template_username if fresh_deploy else ansible_deployer_username }}" diff --git a/nova/core/roles/connection/tasks/main.yml b/nova/core/roles/connection/tasks/main.yml index 3a3d78c9e..a2aa92a6b 100644 --- a/nova/core/roles/connection/tasks/main.yml +++ b/nova/core/roles/connection/tasks/main.yml @@ -1,7 +1,7 @@ --- - name: Configuring VM connection in vSphere environment... ansible.builtin.include_tasks: vsphere.yml - when: infra_env == 'vsphere' + when: infra_env == "vsphere" - name: Configuring VM connection in VMware Workstation environment... ansible.builtin.include_tasks: vmware_workstation.yml @@ -9,31 +9,35 @@ - name: Configuring connection for your host... # The laptop or desktop you are running Ansible from ansible.builtin.include_tasks: my_host.yml - when: infra_env == 'my_host' + when: infra_env == "my_host" - name: Configuring connection in AWS environment... ansible.builtin.include_tasks: aws.yml - when: infra_env == 'aws' + when: infra_env == "aws" - name: Configuring connection in Azure environment... ansible.builtin.include_tasks: azure.yml - when: infra_env == 'azure' + when: infra_env == "azure" - name: Configuring connection in Linode environment... ansible.builtin.include_tasks: linode.yml - when: infra_env == 'linode' + when: infra_env == "linode" - name: Configuring connection in Proxmox environment... ansible.builtin.include_tasks: proxmox.yml - when: infra_env == 'proxmox' + when: infra_env == "proxmox" - name: Configuring connection in OpenStack environment... ansible.builtin.include_tasks: openstack.yml - when: infra_env == 'openstack' + when: infra_env == "openstack" + +- name: Configuring connection in Google Cloud environment... + ansible.builtin.include_tasks: google.yml + when: infra_env == "google" - name: Configuring connection for external or pre-existing machine... ansible.builtin.include_tasks: external.yml - when: infra_env == 'external' + when: infra_env == "external" # Setting PanOS connection parameters here because the correct ansible_host is calculated no matter what infra_env is used - name: Setting connection parameters for PanOS {{ inventory_hostname }}... diff --git a/nova/core/roles/connection/tasks/openstack.yml b/nova/core/roles/connection/tasks/openstack.yml index 03ab3de80..a41bbb784 100644 --- a/nova/core/roles/connection/tasks/openstack.yml +++ b/nova/core/roles/connection/tasks/openstack.yml @@ -2,7 +2,7 @@ - name: Connecting as {{ openstack_template_username | default(template_username) if fresh_deploy else ansible_deployer_username }} to {{ inventory_hostname }} using {{ ansible_connection | default('ssh') | upper - }} over {{ connection_address_custom | default(connection_address) }}... # noqa: name[template] + }} over {{ connection_address_custom | default(connection_address) }} # noqa: name[template] ansible.builtin.set_fact: ansible_host: "{{ connection_address_custom | default(connection_address) }}" ansible_user: "{{ openstack_template_username | default(template_username) if fresh_deploy else ansible_deployer_username }}" diff --git a/nova/core/roles/connection/tasks/proxmox.yml b/nova/core/roles/connection/tasks/proxmox.yml index faff7325b..e82470dc5 100644 --- a/nova/core/roles/connection/tasks/proxmox.yml +++ b/nova/core/roles/connection/tasks/proxmox.yml @@ -80,7 +80,7 @@ - name: Connecting as {{ template_username if fresh_deploy else ansible_deployer_username }} to {{ inventory_hostname }} using {{ ansible_connection | default('ssh') | upper }} - over {{ connection_address_custom | default(connection_address) }}... # noqa: name[template] + over {{ connection_address_custom | default(connection_address) }} # noqa: name[template] ansible.builtin.set_fact: ansible_host: "{{ connection_address_custom | default(connection_address) }}" ansible_user: "{{ template_username if fresh_deploy else ansible_deployer_username }}" diff --git a/nova/core/roles/connection/tasks/vsphere.yml b/nova/core/roles/connection/tasks/vsphere.yml index 6a7254a19..2fcf96eae 100644 --- a/nova/core/roles/connection/tasks/vsphere.yml +++ b/nova/core/roles/connection/tasks/vsphere.yml @@ -32,7 +32,7 @@ Connecting as {{ template_username if fresh_deploy else ansible_deployer_username }} to {{ inventory_hostname }} using {{ ansible_connection | default('ssh') | upper }} - over {{ connection_address_custom | default(detect_dhcp.instance.guest.ipAddress) | default(connection_address) }}... + over {{ connection_address_custom | default(detect_dhcp.instance.guest.ipAddress) | default(connection_address) }} ansible.builtin.set_fact: ansible_host: "{{ connection_address_custom | default(detect_dhcp.instance.guest.ipAddress) | default(connection_address) }}" ansible_user: "{{ template_username if fresh_deploy else ansible_deployer_username }}" @@ -41,7 +41,7 @@ - name: Connecting to {{ inventory_hostname }} using {{ ansible_connection | default('ssh') | upper }} - over {{ connection_address_custom | default(detect_dhcp.instance.guest.ipAddress) | default(connection_address) }}... # noqa: name[template] jinja[invalid] + over {{ connection_address_custom | default(detect_dhcp.instance.guest.ipAddress) | default(connection_address) }} # noqa: name[template] jinja[invalid] ansible.builtin.set_fact: ansible_host: "{{ '[' ~ connection_address_custom | default(detect_dhcp.instance.guest.ipAddress) | default(connection_address) ~ ']' diff --git a/nova/core/roles/deploy_vars/defaults/main.yml b/nova/core/roles/deploy_vars/defaults/main.yml index 2060b331f..7818a87cd 100644 --- a/nova/core/roles/deploy_vars/defaults/main.yml +++ b/nova/core/roles/deploy_vars/defaults/main.yml @@ -305,6 +305,27 @@ openstack_defaults: | default(openstack_project_domain_name) | default('Default') }}" +################ +# Google Cloud # +################ + +# The content of the service account is the base64 encoded in the json file downloaded from Google Cloud Console. +# Use base64 -w 0 to encode the file content to base64 and then save it to Ansible Vault +google_cloud_service_account: + "{{ lookup('vars', environment_name | default('') ~ '_gc_service_account') + | default(lookup('vars', project_fullname | default('') ~ '_gc_service_account')) + | default(lookup('vars', environment_name | default('') ~ '_' ~ project_fullname | default('') ~ '_gc_service_account')) + | default(gc_service_account) | default('') }}" + +# Zone and Region are not in module_defaults since not all modules require them and these modules fail if they are in defaults +google_cloud_zone: europe-north1-b +google_cloud_region: europe-north1 + +google_cloud_defaults: + auth_kind: serviceaccount + project: "{{ google_cloud_project_id | default('') }}" + service_account_contents: "{{ google_cloud_service_account | b64decode }}" + ########### # FortiOS # ########### diff --git a/nova/core/roles/deploy_vars/tasks/main.yml b/nova/core/roles/deploy_vars/tasks/main.yml index afff877c9..7857199b6 100644 --- a/nova/core/roles/deploy_vars/tasks/main.yml +++ b/nova/core/roles/deploy_vars/tasks/main.yml @@ -14,8 +14,8 @@ when: deploy_vars_pre_roles != [] - name: Delegating required tasks to localhost... - delegate_to: localhost become: false + delegate_to: localhost block: - name: Using {{ deploy_vars_vault_auth_method | upper }} to get auth token for {{ vault_address }}... community.hashi_vault.vault_login: diff --git a/nova/core/roles/machine_operations/defaults/main.yml b/nova/core/roles/machine_operations/defaults/main.yml index 0fe57ff8c..158787d7d 100644 --- a/nova/core/roles/machine_operations/defaults/main.yml +++ b/nova/core/roles/machine_operations/defaults/main.yml @@ -246,3 +246,31 @@ machine_operations_openstack_floating_ip_nat_destination: {} # Set to true to inject the generated keypair to the instance using cloud-init machine_operations_openstack_use_keypair: false + +################ +# Google Cloud # +################ + +# Assign a public IP to the instances connection interface +machine_operations_google_assign_public_ip: true + +machine_operations_google_temp_sshd_config_path: /etc/ssh/sshd_config.d/00-{{ infra_env }}-first-run.conf + +# Will try and pre-create the network for a VM based on the interfaces variable +# Set to false if network(s) are already pre-created by other means +machine_operations_google_create_networks: true + +# Will create firewalls for each network created +# Needs the interfaces variable to not be empty +# Set to false if firewalls are already pre-created by other means +machine_operations_google_create_firewall_rules: true + +# Default firewall rules that will be added to each network created +# Pass the machine_operations_google_firewall_rules variable in your host_vars/group_vars to override the defaults +# or disable network and firewall creation to fully manage the networks and firewalls by other means +# Refer to the module documentation for more details: +# https://docs.ansible.com/projects/ansible/latest/collections/google/cloud/gcp_compute_firewall_module.html +machine_operations_google_firewall_rule: + name: "{{ item.cloud_id }}-allow-all" # The cloud_id comes from the interfaces list + allowed: + - ip_protocol: all diff --git a/nova/core/roles/machine_operations/tasks/aws/create.yml b/nova/core/roles/machine_operations/tasks/aws/create.yml index 8d9823f90..ca7a7be7c 100644 --- a/nova/core/roles/machine_operations/tasks/aws/create.yml +++ b/nova/core/roles/machine_operations/tasks/aws/create.yml @@ -158,5 +158,5 @@ port: 22 state: started timeout: 300 - delegate_to: localhost become: false + delegate_to: localhost diff --git a/nova/core/roles/machine_operations/tasks/google/create.yml b/nova/core/roles/machine_operations/tasks/google/create.yml new file mode 100644 index 000000000..a0c6d3e44 --- /dev/null +++ b/nova/core/roles/machine_operations/tasks/google/create.yml @@ -0,0 +1,75 @@ +--- +- name: Creating Google Cloud instance... + become: false + delegate_to: localhost + block: + - name: Running fresh deploy tasks for Google Cloud... + when: fresh_deploy + block: + - name: Finding latest {{ google_cloud_vm_image.name }} image... + google.cloud.gcp_compute_image_info: + project: "{{ google_cloud_vm_image.project }}" + filters: + - name eq .*{{ google_cloud_vm_image.name }}.* + register: google_image_info + + - name: Setting template_password as fact... + ansible.builtin.set_fact: + template_password: "{{ lookup('password', '/dev/null length=17 chars=ascii_lowercase,ascii_uppercase,hexdigits,digits') }}" + + - name: FAILED TO FIND IMAGE + ansible.builtin.fail: + msg: | + Failed to find image matching {{ google_cloud_vm_image.name }} in {{ google_cloud_vm_image.project }} project + Verify the image lookup parameters *google_cloud_vm_image* variable: + {{ google_cloud_vm_image | to_nice_yaml }} + when: google_image_info.resources == [] + + - name: Creating a static public IP address for {{ custom_vm_name | default(vm_name) }}... + google.cloud.gcp_compute_address: + name: "{{ custom_vm_name | default(vm_name) }}-public-ip" + region: "{{ google_cloud_region }}" + state: present + register: google_public_address + when: machine_operations_google_assign_public_ip + + # TODO Find a way to clear metadata after deployment to avoid noise in the console + - name: Creating {{ custom_vm_name | default(vm_name) }}... + google.cloud.gcp_compute_instance: + name: "{{ custom_vm_name | default(vm_name) }}" + machine_type: "{{ google_cloud_machine_type }}" + disks: + - auto_delete: true + boot: true + initialize_params: + disk_size_gb: "{{ os_disk_size | default(hardware_primary_disk_size) | default(omit) }}" + disk_type: "{{ google_cloud_disk_type | default(omit) }}" + source_image: "{{ google_cloud_vm_image.path ~ '/' ~ google_image_info.resources[-1].name | default(omit) }}" + network_interfaces: "{{ omit if not fresh_deploy else lookup('ansible.builtin.template', 'google_cloud_interfaces.yml') | from_yaml }}" + metadata: + "{{ omit if not fresh_deploy else lookup('ansible.builtin.template', 'Configure-CloudWindows.ps1') | from_yaml if 'os_windows' in group_names + else lookup('ansible.builtin.template', 'google_cloud_linux_startup_script.yml') | from_yaml }}" + zone: "{{ google_cloud_zone }}" + register: google_instance + + - name: Installing SSH... + ansible.builtin.debug: + msg: | + Installing and enabling SSH for {{ custom_vm_name | default(vm_name) }}... + This may take a few minutes to complete. + when: + - fresh_deploy + - ("os_windows" in group_names) + + - name: Setting connection address to {{ google_instance.networkInterfaces[0].accessConfigs[0].natIP | default('') }} + ansible.builtin.set_fact: + connection_address: "{{ google_instance.networkInterfaces[0].accessConfigs[0].natIP }}" + when: machine_operations_google_assign_public_ip + + - name: Waiting until SSH is up for {{ custom_vm_name | default(vm_name) }} over {{ connection_address }}... + ansible.builtin.wait_for: + host: "{{ connection_address }}" + port: 22 + state: started + timeout: "{{ 600 if 'os_windows' in group_names else 300 }}" + when: fresh_deploy diff --git a/nova/core/roles/machine_operations/tasks/google/create_firewall_rules.yml b/nova/core/roles/machine_operations/tasks/google/create_firewall_rules.yml new file mode 100644 index 000000000..0aab8fd24 --- /dev/null +++ b/nova/core/roles/machine_operations/tasks/google/create_firewall_rules.yml @@ -0,0 +1,14 @@ +--- +- name: Creating following firewall rules... + google.cloud.gcp_compute_firewall: + name: "{{ machine_operations_google_firewall_rule.name }}" + allowed: "{{ machine_operations_google_firewall_rule.allowed }}" + network: + selfLink: "{{ create_network_result.results[loop_index].selfLink }}" + state: present + loop_control: + index_var: loop_index + label: "{{ machine_operations_google_firewall_rule.name }}" + loop: "{{ interfaces }}" + become: false + delegate_to: localhost diff --git a/nova/core/roles/machine_operations/tasks/google/create_networks.yml b/nova/core/roles/machine_operations/tasks/google/create_networks.yml new file mode 100644 index 000000000..d514e5606 --- /dev/null +++ b/nova/core/roles/machine_operations/tasks/google/create_networks.yml @@ -0,0 +1,34 @@ +--- +- name: Creating networks for GCP VMs... + become: false + delegate_to: localhost + block: + - name: Create following virtual networks... + google.cloud.gcp_compute_network: + name: "{{ item.cloud_id }}" + auto_create_subnetworks: false + routing_config: + routing_mode: REGIONAL + state: present + loop: "{{ interfaces | selectattr('cloud_id') }}" + loop_control: + label: "{{ item.cloud_id }}" + register: create_network_result + retries: 3 # Retries a request for the case where no network exists yet and multiple machines are being created in parallel + delay: 20 + + - name: Creating following subnets... + google.cloud.gcp_compute_subnetwork: + name: "{{ item.cloud_id }}-subnet" + network: + selfLink: "{{ create_network_result.results[subnet_index].selfLink }}" + ip_cidr_range: "{{ item.addresses | map(attribute='address') | map('ansible.utils.ipaddr', 'network/prefix') | list | first }}" + region: "{{ google_cloud_region }}" + state: present + loop: "{{ interfaces | selectattr('cloud_id') }}" + loop_control: + label: "{{ item.cloud_id }}-subnet" + index_var: subnet_index + register: create_subnet_result + retries: 3 # Retries a request for the case where no network exists yet and multiple machines are being created in parallel + delay: 20 diff --git a/nova/core/roles/machine_operations/tasks/google/main.yml b/nova/core/roles/machine_operations/tasks/google/main.yml new file mode 100644 index 000000000..bd0569929 --- /dev/null +++ b/nova/core/roles/machine_operations/tasks/google/main.yml @@ -0,0 +1,57 @@ +--- +- name: MISSING SECRETS + ansible.builtin.fail: + msg: | + You are missing following secrets: + + {% for variable in required_variables %} + {% if vars[variable] is defined %} + {% set evaluated = lookup('vars', variable) %} + {% if evaluated is ansible.builtin.falsy %} + - {{ variable }} + {% endif %} + {% else %} + - {{ variable }} + {% endif %} + {% endfor %} + + You can add them to your Ansible Vault file or define external lookups in your group_vars/host_vars files. + vars: + required_variables: + - gc_service_account + when: google_cloud_service_account is ansible.builtin.falsy + +- name: Checking if {{ custom_vm_name | default(vm_name) }} exists... + google.cloud.gcp_compute_instance_info: + filters: + - name = {{ custom_vm_name | default(vm_name) }} + zone: "{{ google_cloud_zone }}" + become: false + delegate_to: localhost + register: google_vm_info + +- name: Setting fresh_deploy as fact... + ansible.builtin.set_fact: + fresh_deploy: true + when: google_vm_info.resources == [] or deploy_mode == "redeploy" + +- name: Including {{ custom_vm_name | default(vm_name) }} removal tasks... + ansible.builtin.include_tasks: remove.yml + when: deploy_mode in ['undeploy', 'redeploy'] + +- name: Including networks creation tasks... + ansible.builtin.include_tasks: create_networks.yml + when: + - interfaces != [] + - machine_operations_google_create_networks + - single_role is not defined + +- name: Including firewall rules creation tasks... + ansible.builtin.include_tasks: create_firewall_rules.yml + when: + - interfaces != [] + - machine_operations_google_create_firewall_rules + - single_role is not defined + +- name: Including {{ custom_vm_name | default(vm_name) }} creation tasks... + ansible.builtin.include_tasks: create.yml diff --git a/nova/core/roles/machine_operations/tasks/google/remove.yml b/nova/core/roles/machine_operations/tasks/google/remove.yml new file mode 100644 index 000000000..c663d44b3 --- /dev/null +++ b/nova/core/roles/machine_operations/tasks/google/remove.yml @@ -0,0 +1,20 @@ +--- +- name: Deleting {{ custom_vm_name | default(vm_name) | replace('_', '-') }}... + google.cloud.gcp_compute_instance: + name: "{{ custom_vm_name | default(vm_name) | replace('_', '-') }}" + zone: "{{ google_cloud_zone }}" + state: absent + become: false + delegate_to: localhost + +- name: Deleting a static public IP address for {{ custom_vm_name | default(vm_name) }}... + google.cloud.gcp_compute_address: + name: "{{ custom_vm_name | default(vm_name) }}-public-ip" + region: "{{ google_cloud_region }}" + state: absent + become: false + delegate_to: localhost + +- name: Stopping play... + ansible.builtin.meta: end_host + when: deploy_mode == 'undeploy' diff --git a/nova/core/roles/machine_operations/tasks/main.yml b/nova/core/roles/machine_operations/tasks/main.yml index c60b22df6..8211dc235 100644 --- a/nova/core/roles/machine_operations/tasks/main.yml +++ b/nova/core/roles/machine_operations/tasks/main.yml @@ -35,7 +35,7 @@ block: - name: Including vSphere environment tasks... ansible.builtin.include_tasks: vsphere/main.yml - when: infra_env == 'vsphere' + when: infra_env == "vsphere" - name: Including VMware Workstation environment tasks... ansible.builtin.include_tasks: vmware_workstation/main.yml @@ -43,27 +43,31 @@ - name: Including AWS EC2 environment tasks... ansible.builtin.include_tasks: aws/main.yml - when: infra_env == 'aws' + when: infra_env == "aws" - name: Including Linode environment tasks... ansible.builtin.include_tasks: linode/main.yml - when: infra_env == 'linode' + when: infra_env == "linode" - name: Including Azure environment tasks... ansible.builtin.include_tasks: azure/main.yml - when: infra_env == 'azure' + when: infra_env == "azure" - name: Including Proxmox environment tasks... ansible.builtin.include_tasks: proxmox/main.yml - when: infra_env == 'proxmox' + when: infra_env == "proxmox" - name: Including OpenStack environment tasks... ansible.builtin.include_tasks: openstack/main.yml - when: infra_env == 'openstack' + when: infra_env == "openstack" + + - name: Including Google Cloud environment tasks... + ansible.builtin.include_tasks: google/main.yml + when: infra_env == "google" - name: Including external environment tasks... ansible.builtin.include_tasks: external/main.yml - when: infra_env == 'external' + when: infra_env == "external" - name: Including following machine_operations post-roles... ansible.builtin.include_role: diff --git a/nova/core/roles/machine_operations/templates/Configure-CloudWindows.ps1 b/nova/core/roles/machine_operations/templates/Configure-CloudWindows.ps1 index 056acb8d4..7a94326e6 100644 --- a/nova/core/roles/machine_operations/templates/Configure-CloudWindows.ps1 +++ b/nova/core/roles/machine_operations/templates/Configure-CloudWindows.ps1 @@ -1,70 +1,77 @@ {% if infra_env == "aws" %} {% endif %} -$ErrorActionPreference = 'Stop' +{% if infra_env == "google" %} +windows-startup-script-ps1: | +{% endif %} + $ErrorActionPreference = 'Stop' + + $LogPath = "C:\Windows\Temp\install.log" + $RemoteConnectivityHost = "google.com" + $OS = Get-WmiObject -Class Win32_OperatingSystem + $version = [version]$OS.Version + $SSHDownloadLink = "https://github.com/PowerShell/Win32-OpenSSH/releases/download/10.0.0.0p2-Preview/OpenSSH-Win64-v10.0.0.0.msi" -$LogPath = "C:\Windows\Temp\install.log" -$RemoteConnectivityHost = "google.com" -$OS = Get-WmiObject -Class Win32_OperatingSystem -$version = [version]$OS.Version -$SSHDownloadLink = "https://github.com/PowerShell/Win32-OpenSSH/releases/download/10.0.0.0p2-Preview/OpenSSH-Win64-v10.0.0.0.msi" + #--------------------End of variables, Start of Script--------------------# -#--------------------End of variables, Start of Script--------------------# + Start-Transcript -Path $LogPath -Start-Transcript -Path $LogPath +{% if infra_env in ["aws", "google"] %} + Write-Host "Setting Administrator password" + $Password = "{{ template_password }}" + $SecurePassword = ConvertTo-SecureString $Password -AsPlainText -Force + Set-LocalUser -Name "Administrator" -Password $SecurePassword + Enable-LocalUser -Name "Administrator" +{% endif %} {% if infra_env == "aws" %} -Write-Host "Setting Administrator password" -$Password = "{{ template_password }}" -$SecurePassword = ConvertTo-SecureString $Password -AsPlainText -Force -Set-LocalUser -Name "Administrator" -Password $SecurePassword - -Write-Host "Setting temporary SSH key for Administrator user" -$TempSSHKey = "{{ slurped_aws_temp_ssh_key.content | b64decode }}" -Set-Content -Path "C:\ProgramData\ssh\administrators_authorized_keys" -Value $TempSSHKey -Force + Write-Host "Setting temporary SSH key for Administrator user" + $TempSSHKey = "{{ slurped_aws_temp_ssh_key.content | b64decode }}" + Set-Content -Path "C:\ProgramData\ssh\administrators_authorized_keys" -Value $TempSSHKey -Force {% endif %} {% if infra_env == "azure" %} -Rename-LocalUser -Name "azureadmin" -NewName "Administrator" + Rename-LocalUser -Name "azureadmin" -NewName "Administrator" + Enable-LocalUser -Name "Administrator" {% endif %} -Write-Host "Checking that $RemoteConnectivityHost is reachable" -while($true) { - $ping = Test-Connection -ComputerName $RemoteConnectivityHost -Count 1 -Quiet - if($ping) { - Write-Host "$RemoteConnectivityHost is reachable." - break - } else { - Write-Host "Cannot reach $RemoteConnectivityHost. Retrying..." - Start-Sleep -Seconds 3 + Write-Host "Checking that $RemoteConnectivityHost is reachable" + while($true) { + $ping = Test-Connection -ComputerName $RemoteConnectivityHost -Count 1 -Quiet + if($ping) { + Write-Host "$RemoteConnectivityHost is reachable." + break + } else { + Write-Host "Cannot reach $RemoteConnectivityHost. Retrying..." + Start-Sleep -Seconds 3 + } } -} -Write-Host "Setting PowerShell as default SSH shell" -reg add HKLM\Software\OpenSSH /v DefaultShell /t REG_SZ /d C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe /f + Write-Host "Setting PowerShell as default SSH shell" + reg add HKLM\Software\OpenSSH /v DefaultShell /t REG_SZ /d C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe /f -if ($version.Major -ge 10) { + if ($version.Major -ge 10) { - Write-Host "Removing native OpenSSH Client" - Remove-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0 + Write-Host "Removing native OpenSSH Client" + Remove-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0 -} + } -Write-Host "Downloading and installing latest OpenSSH" -[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; Invoke-WebRequest -Uri $SSHDownloadLink -OutFile "C:\Windows\Temp\OpenSSH.msi" -msiexec /i "C:\Windows\Temp\OpenSSH.msi" /qn + Write-Host "Downloading and installing latest OpenSSH" + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; Invoke-WebRequest -Uri $SSHDownloadLink -OutFile "C:\Windows\Temp\OpenSSH.msi" + msiexec /i "C:\Windows\Temp\OpenSSH.msi" /qn -while(-not (Get-NetFirewallRule | Where-object DisplayName -like "*OpenSSH*")) { - Start-Sleep -Seconds 1 - Write-Host "Waiting until SSH firewall rule is created" -} + while(-not (Get-NetFirewallRule | Where-object DisplayName -like "*OpenSSH*")) { + Start-Sleep -Seconds 1 + Write-Host "Waiting until SSH firewall rule is created" + } -Write-Host "Allowing OpenSSH through firewall with all profiles" -Get-NetFirewallRule | Where-object DisplayName -like "*OpenSSH*" | Set-NetFirewallRule -Profile Any + Write-Host "Allowing OpenSSH through firewall with all profiles" + Get-NetFirewallRule | Where-object DisplayName -like "*OpenSSH*" | Set-NetFirewallRule -Profile Any -Remove-item C:\Windows\Temp\OpenSSH.msi -Force + Remove-item C:\Windows\Temp\OpenSSH.msi -Force -Stop-Transcript + Stop-Transcript {% if infra_env == "aws" %} {% endif %} \ No newline at end of file diff --git a/nova/core/roles/machine_operations/templates/google_cloud_interfaces.yml b/nova/core/roles/machine_operations/templates/google_cloud_interfaces.yml new file mode 100644 index 000000000..84f46b507 --- /dev/null +++ b/nova/core/roles/machine_operations/templates/google_cloud_interfaces.yml @@ -0,0 +1,24 @@ +#jinja2: lstrip_blocks: True, trim_blocks: True +{% if interfaces != [] %} +{% for interface in interfaces | selectattr('cloud_id') %} +- network: + selfLink: "{{ create_network_result.results[loop.index0].selfLink }}" + network_ip: "{{ interface.addresses | map(attribute='address') | ansible.utils.ipaddr('address') | list | first }}" + subnetwork: + selfLink: "{{ create_subnet_result.results[loop.index0].selfLink }}" + {% if machine_operations_google_assign_public_ip and interface.connection %} + access_configs: + - name: External NAT + type: ONE_TO_ONE_NAT + nat_ip: + address: "{{ google_public_address.address }}" + {% endif %} +{% endfor %} +{% endif %} +{% if interfaces == [] and machine_operations_google_assign_public_ip %} +- access_configs: + - name: External NAT + type: ONE_TO_ONE_NAT + nat_ip: + address: "{{ google_public_address.address }}" +{% endif %} \ No newline at end of file diff --git a/nova/core/roles/machine_operations/templates/google_cloud_linux_startup_script.yml b/nova/core/roles/machine_operations/templates/google_cloud_linux_startup_script.yml new file mode 100644 index 000000000..9e892c7ac --- /dev/null +++ b/nova/core/roles/machine_operations/templates/google_cloud_linux_startup_script.yml @@ -0,0 +1,19 @@ +# Using startup-script since not all images have cloud-init pre-installed +startup-script: | + #!/bin/bash + + cat <<'EOF' >{{ machine_operations_google_temp_sshd_config_path }} + PermitRootLogin yes + PasswordAuthentication yes + MaxAuthTries 30 + EOF + systemctl restart ssh + echo root:{{ template_password }} | chpasswd + + # Needed during os_configuration role + apt install locales -y + + # Changing sudo for Ubuntu 26.04 since the sudo.rs does not currently work with become in Ansible for GCP images + if grep -q resolute /etc/os-release; then + update-alternatives --set sudo /usr/bin/sudo.ws + fi diff --git a/nova/core/roles/powerstate/google/main.yml b/nova/core/roles/powerstate/google/main.yml new file mode 100644 index 000000000..0c20548b6 --- /dev/null +++ b/nova/core/roles/powerstate/google/main.yml @@ -0,0 +1,47 @@ +--- +- name: UNSUPPORTED POWERSTATE MODE + ansible.builtin.debug: + msg: Google Cloud does not support the suspend using Ansible, skipping powerstate change. + when: suspend | bool + +- name: Checking and setting Google Cloud VM powerstate... + become: false + delegate_to: localhost + when: not suspend + block: + - name: Checking if {{ custom_vm_name | default(vm_name) }} exists... + google.cloud.gcp_compute_instance_info: + filters: + - name = {{ custom_vm_name | default(vm_name) }} + zone: "{{ google_cloud_zone }}" + become: false + delegate_to: localhost + register: google_vm_info + + - name: MISSING INSTANCE + ansible.builtin.debug: + msg: Instance {{ custom_vm_name | default(vm_name) }} does not exist, skipping powerstate change. + when: google_vm_info.resources == [] + + - name: Running instance powerstate tasks... + when: google_vm_info.resources != [] + block: + - name: Restarting {{ custom_vm_name | default(vm_name) }} Google Cloud... + google.cloud.gcp_compute_instance: + name: "{{ custom_vm_name | default(vm_name) }}" + status: TERMINATED + zone: "{{ google_cloud_zone }}" + when: restart | bool + + - name: + Setting {{ custom_vm_name | default(vm_name) }} Google Cloud instance state - {{ + 'started' if poweron | bool else + 'restarted' if restart | bool else + 'stopped' if (poweroff or shutdown) | bool }} + google.cloud.gcp_compute_instance: + name: "{{ custom_vm_name | default(vm_name) }}" + status: "{{ + 'RUNNING' if (poweron or restart) | bool else + 'TERMINATED' if (poweroff or shutdown) | bool + }}" + zone: "{{ google_cloud_zone }}" From 868e79164cf9b3a4610c80ea5eab75ed2463b5b4 Mon Sep 17 00:00:00 2001 From: Allar Viik Date: Mon, 22 Jun 2026 14:09:18 +0300 Subject: [PATCH 02/18] Added Google Cloud support to `powerstate` role --- nova/core/roles/powerstate/tasks/main.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/nova/core/roles/powerstate/tasks/main.yml b/nova/core/roles/powerstate/tasks/main.yml index 11e347637..2a0ec3f6f 100644 --- a/nova/core/roles/powerstate/tasks/main.yml +++ b/nova/core/roles/powerstate/tasks/main.yml @@ -4,7 +4,7 @@ block: - name: Including vSphere powerstate tasks... ansible.builtin.include_tasks: vsphere/main.yml - when: infra_env == 'vsphere' + when: infra_env == "vsphere" - name: Including VMware Workstation powerstate task... ansible.builtin.include_tasks: vmware_workstation/main.yml @@ -12,12 +12,16 @@ - name: Including AWS EC2 powerstate tasks... ansible.builtin.include_tasks: aws/main.yml - when: infra_env == 'aws' + when: infra_env == "aws" - name: Including Linode powerstate tasks... ansible.builtin.include_tasks: linode/main.yml - when: infra_env == 'linode' + when: infra_env == "linode" - name: Including Proxmox powerstate tasks... ansible.builtin.include_tasks: proxmox/main.yml - when: infra_env == 'proxmox' + when: infra_env == "proxmox" + + - name: Including Google Cloud powerstate tasks... + ansible.builtin.include_tasks: google/main.yml + when: infra_env == "google" From 0b32af9d1c8d238cd7718761bf9f24bb5874a223 Mon Sep 17 00:00:00 2001 From: Allar Viik Date: Thu, 25 Jun 2026 21:08:24 +0300 Subject: [PATCH 03/18] Using custom heatlcheck for `gitlab` to really wait for it to be running --- nova/core/roles/gitlab/tasks/install.yml | 2 +- nova/core/roles/gitlab/templates/gitlab.yml | 5 ++--- nova/core/roles/updates/tasks/debian_family.yml | 1 + 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nova/core/roles/gitlab/tasks/install.yml b/nova/core/roles/gitlab/tasks/install.yml index 486e46b65..d21c4c23a 100644 --- a/nova/core/roles/gitlab/tasks/install.yml +++ b/nova/core/roles/gitlab/tasks/install.yml @@ -90,5 +90,5 @@ status_code: 200 register: gitlab_uri until: gitlab_uri.status == 200 - retries: 18 + retries: 12 delay: 10 diff --git a/nova/core/roles/gitlab/templates/gitlab.yml b/nova/core/roles/gitlab/templates/gitlab.yml index 7cf63bf1f..9c40e01b2 100644 --- a/nova/core/roles/gitlab/templates/gitlab.yml +++ b/nova/core/roles/gitlab/templates/gitlab.yml @@ -20,10 +20,9 @@ services: # GitLab keeps custom certs in this folder https://docs.gitlab.com/omnibus/settings/ssl.html#troubleshooting - "/usr/local/share/ca-certificates:/etc/gitlab/trusted-certs" - # Overriding the default healthcheck values to allow longer startup times on low end machines - # https://gitlab.com/gitlab-org/omnibus-gitlab/-/blob/master/docker/Dockerfile#L81-82 + # Overriding the default healthcheck from the GitLab image to be more lenient and allow for longer startup times healthcheck: - test: /opt/gitlab/bin/gitlab-healthcheck --fail + test: curl --fail --silent --output /dev/null http://localhost/projects interval: 10s retries: 90 timeout: 30s diff --git a/nova/core/roles/updates/tasks/debian_family.yml b/nova/core/roles/updates/tasks/debian_family.yml index 26af62460..2d9d04759 100644 --- a/nova/core/roles/updates/tasks/debian_family.yml +++ b/nova/core/roles/updates/tasks/debian_family.yml @@ -81,6 +81,7 @@ mode: "0644" loop_control: loop_var: update_disable + label: "{{ update_disable.line }}" loop: - regexp: .*Unattended-Upgrade line: APT::Periodic::Unattended-Upgrade "0"; From 08f96681f3f5dd5a466ba7f3ee43037d84fe7ccf Mon Sep 17 00:00:00 2001 From: Allar Viik Date: Fri, 26 Jun 2026 12:16:37 +0300 Subject: [PATCH 04/18] Set WSL version to 2.7.10 in `docker` role --- nova/core/roles/docker/defaults/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/core/roles/docker/defaults/main.yml b/nova/core/roles/docker/defaults/main.yml index 2dc03cce0..0c3a1d227 100644 --- a/nova/core/roles/docker/defaults/main.yml +++ b/nova/core/roles/docker/defaults/main.yml @@ -63,6 +63,6 @@ docker_non_default_debian_os_list: # Windows # ########### -docker_wsl_version: 2.7.8 +docker_wsl_version: 2.7.10 docker_wsl_download_link: https://github.com/microsoft/WSL/releases/download/{{ docker_wsl_version }}/wsl.{{ docker_wsl_version }}.0.x64.msi docker_desktop_autostart: true From d30efb6f033fe57848604ce03844a1704c0a4aa3 Mon Sep 17 00:00:00 2001 From: Allar Viik Date: Fri, 26 Jun 2026 12:17:01 +0300 Subject: [PATCH 05/18] Set `nexus` version to 3.93.2 --- nova/core/roles/nexus/defaults/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/core/roles/nexus/defaults/main.yml b/nova/core/roles/nexus/defaults/main.yml index 5a1112449..c7b553e5d 100644 --- a/nova/core/roles/nexus/defaults/main.yml +++ b/nova/core/roles/nexus/defaults/main.yml @@ -9,7 +9,7 @@ nexus_configure_ldap: false # Set to true to also configure LDAP after installat nexus_create_repos: false # Set to true to also create default repositories after installation nexus_allow_anonymous_access: true # Set to false to disable anonymous access nexus_active_encryption_key_id: Primary Encryption Key # Name of the active encryption key that comes with this role -nexus_version: 3.93.0 # Nexus version to install +nexus_version: 3.93.2 # Nexus version to install # Default is the built-in Nexus admin user. If set to a different value than admin, the role will create the user. nexus_admin_username: admin From a713ee7c20fcc3e9337f5d7ff9aea2f3f27c6d5a Mon Sep 17 00:00:00 2001 From: Allar Viik Date: Sun, 28 Jun 2026 13:05:18 +0300 Subject: [PATCH 06/18] Set `keycloak` version to 26.6.4 --- nova/core/roles/keycloak/defaults/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/core/roles/keycloak/defaults/main.yml b/nova/core/roles/keycloak/defaults/main.yml index 995f00dd3..f0a2e0ea2 100644 --- a/nova/core/roles/keycloak/defaults/main.yml +++ b/nova/core/roles/keycloak/defaults/main.yml @@ -1,6 +1,6 @@ --- # The Keycloak version and the Keycloak Config CLI version should be compatible -keycloak_version: 26.6.3 +keycloak_version: 26.6.4 # https://github.com/adorsys/keycloak-config-cli # https://hub.docker.com/r/adorsys/keycloak-config-cli/tags From 92efd5a1b208187d1c9b11e3fc32af1309bc6262 Mon Sep 17 00:00:00 2001 From: Allar Viik Date: Mon, 29 Jun 2026 22:57:31 +0300 Subject: [PATCH 07/18] Redsesigned the Providentia inventory plugin creds lookup so the most unique one (env_project prefix) would be used first --- nova/core/plugins/inventory/providentia_v3.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/nova/core/plugins/inventory/providentia_v3.py b/nova/core/plugins/inventory/providentia_v3.py index 81a9dd2ea..3b906cfb0 100644 --- a/nova/core/plugins/inventory/providentia_v3.py +++ b/nova/core/plugins/inventory/providentia_v3.py @@ -230,21 +230,22 @@ async def fetch_from_providentia(self, endpoint=""): def fetch_creds(self): """ Retrieve deployer credentials from Ansible Vault in the following order of precedence: - 1. Environment specific credentials eg. dev_providentia_api_token or dev_deployer_username and dev_deployer_password + 1. Environment and project specific credentials eg. dev_projectA_providentia_api_token or dev_projectA_deployer_username and dev_projectA_deployer_password 2. Project specific credentials eg. projectA_providentia_api_token or projectA_deployer_username and projectA_deployer_password - 3. Environment and project specific credentials eg. dev_projectA_providentia_api_token or dev_projectA_deployer_username and dev_projectA_deployer_password + 3. Environment specific credentials eg. dev_providentia_api_token or dev_deployer_username and dev_deployer_password + 4. Default credentials eg. providentia_api_token or deployer_username and deployer_password API token is preferred over username/password when both are available. """ # Build prefixes in precedence order prefixes = [] - if self.environment: - prefixes.append((f"{self.environment}_", 'env_')) - if self.project: - prefixes.append((f"{self.project}_", 'project_')) if self.environment and self.project: prefixes.append((f"{self.environment}_{self.project}_", 'project_')) + if self.project: + prefixes.append((f"{self.project}_", 'project_')) + if self.environment: + prefixes.append((f"{self.environment}_", 'env_')) # Check for API token first (preferred over username/password) for option_prefix, inv_prefix in prefixes: From 4b7f6936a690a10501b2b28b76e488f646104317 Mon Sep 17 00:00:00 2001 From: Allar Viik Date: Mon, 29 Jun 2026 22:58:30 +0300 Subject: [PATCH 08/18] Removed unneeded OPNsense auth configuration tasks from `template_os_configuration` role --- .../tasks/opnsense.yml | 35 +------------------ 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/nova/core/roles/template_os_configuration/tasks/opnsense.yml b/nova/core/roles/template_os_configuration/tasks/opnsense.yml index 6ab83a583..3149c7cd8 100644 --- a/nova/core/roles/template_os_configuration/tasks/opnsense.yml +++ b/nova/core/roles/template_os_configuration/tasks/opnsense.yml @@ -2,45 +2,12 @@ - name: Including vSphere specific configuration tasks... when: infra_env == "vsphere" block: - # Configuring OPNsense in localhost to avoid installing required Python modules on the remote host - - name: Downloading config.xml from {{ inventory_hostname }}... - ansible.builtin.fetch: - dest: /tmp/{{ project_fullname | default('') }}_{{ inventory_hostname }}_opnsense_config.xml - src: /conf/config.xml - flat: true - - - name: Disabling integrated authentication... # Required for auth to work over VMTools https://github.com/opnsense/core/issues/1399 - community.general.xml: - path: /tmp/{{ project_fullname | default('') }}_{{ inventory_hostname }}_opnsense_config.xml - xpath: /opnsense/system/disableintegratedauth - value: "1" - pretty_print: true - delegate_to: localhost - become: false - - - name: Uploading config.xml to {{ inventory_hostname }}... - ansible.builtin.copy: - src: /tmp/{{ project_fullname | default('') }}_{{ inventory_hostname }}_opnsense_config.xml - dest: /conf/config.xml - mode: "0644" - owner: root - group: wheel - - - name: Removing local config.xml file... - ansible.builtin.file: - path: /tmp/{{ project_fullname | default('') }}_{{ inventory_hostname }}_opnsense_config.xml - state: absent - delegate_to: localhost - become: false - - # Something has changed with the PAM module in OPNsense 25 that requires this change - # Otherwise, the VMware Tools authentication will not work + # Needed for VMware Tools authentication to work - name: Allowing VMware Tools authentication... ansible.builtin.lineinfile: path: /usr/local/etc/pam.d/vmtoolsd regexp: ^auth.* line: auth sufficient pam_permit.so - when: group_names | select('match', '^os_opnsense_25.*') | length > 0 # SSHD Configuration # This to avoid errors when someone has more that 6 SSH keys in their agent From ce13296e10d0e08cd2555aba21d25ae0d830e436 Mon Sep 17 00:00:00 2001 From: Allar Viik Date: Mon, 29 Jun 2026 22:58:44 +0300 Subject: [PATCH 09/18] Added OPNsense and pfSense support to `updates` role --- nova/core/roles/updates/tasks/main.yml | 8 +++++++- nova/core/roles/updates/tasks/sense.yml | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 nova/core/roles/updates/tasks/sense.yml diff --git a/nova/core/roles/updates/tasks/main.yml b/nova/core/roles/updates/tasks/main.yml index aafdcf65a..6ba0a8bc5 100644 --- a/nova/core/roles/updates/tasks/main.yml +++ b/nova/core/roles/updates/tasks/main.yml @@ -23,7 +23,13 @@ - name: Including FreeBSD update tasks... ansible.builtin.include_tasks: freebsd.yml - when: ansible_facts.system | default('') == "FreeBSD" + when: + - ansible_facts.system | default('') == "FreeBSD" + - ansible_network_os is not defined + +- name: Including OPN/pfSense update tasks... + ansible.builtin.include_tasks: sense.yml + when: ansible_network_os | default('') in ["opnsense", "pfsense"] - name: Including MacOS update tasks... ansible.builtin.include_tasks: macos.yml diff --git a/nova/core/roles/updates/tasks/sense.yml b/nova/core/roles/updates/tasks/sense.yml new file mode 100644 index 000000000..e8efec97e --- /dev/null +++ b/nova/core/roles/updates/tasks/sense.yml @@ -0,0 +1,17 @@ +--- +- name: Updating {{ inventory_hostname }}... + ansible.builtin.command: opnsense-update -bkp + register: opnsense_update + changed_when: opnsense_update.stdout is search("Installing.*") + when: ansible_network_os | default('') == "opnsense" + +- name: Updating {{ inventory_hostname }}... + ansible.builtin.command: pfSense-upgrade -d -y + register: pfsense_update + changed_when: pfsense_update.stdout is search("Installing.*") + when: ansible_network_os | default('') == "pfsense" + +- name: Rebooting... + ansible.builtin.reboot: + when: ("Please reboot." in opnsense_update.stdout_lines | default('')) + or ("Please reboot." in pfsense_update.stdout_lines | default('')) From 44383adec4e340324871c6c1248d9e6b39ca0a09 Mon Sep 17 00:00:00 2001 From: Allar Viik Date: Tue, 30 Jun 2026 12:22:12 +0300 Subject: [PATCH 10/18] Added support to new kali.sources format to `configure_package_mirrors` --- nova/core/galaxy.yml | 2 +- .../configure_package_mirrors/tasks/kali.yml | 66 ++++++++++++++----- .../templates/kali.sources | 7 ++ 3 files changed, 57 insertions(+), 18 deletions(-) create mode 100644 nova/core/roles/configure_package_mirrors/templates/kali.sources diff --git a/nova/core/galaxy.yml b/nova/core/galaxy.yml index b4a78842f..4c5a73933 100644 --- a/nova/core/galaxy.yml +++ b/nova/core/galaxy.yml @@ -1,6 +1,6 @@ namespace: nova name: core -version: 11.0.1 +version: 11.0.2 readme: README.md authors: - https://github.com/novateams diff --git a/nova/core/roles/configure_package_mirrors/tasks/kali.yml b/nova/core/roles/configure_package_mirrors/tasks/kali.yml index cbbc11656..287b75a6d 100644 --- a/nova/core/roles/configure_package_mirrors/tasks/kali.yml +++ b/nova/core/roles/configure_package_mirrors/tasks/kali.yml @@ -1,26 +1,58 @@ --- -- name: Checking if sources.list backup exists... +- name: Checking if kali.sources is used... ansible.builtin.stat: - path: /etc/apt/sources.list_backup - register: apt_backup_file + path: /etc/apt/sources.list.d/kali.sources + register: kali_sources -- name: Backing up existing sources.list... - ansible.builtin.copy: - src: /etc/apt/sources.list - dest: /etc/apt/sources.list_backup - remote_src: true - mode: "0644" - when: not apt_backup_file.stat.exists +- name: Configuring kali.sources... + when: kali_sources.stat.exists + block: + - name: Checking if /etc/apt/kali.sources_backup exists... + ansible.builtin.stat: + path: /etc/apt/kali.sources_backup + register: kali_sources_backup -- name: Templating custom kali.list to {{ inventory_hostname }}... - ansible.builtin.template: - src: kali-sources.list - dest: /etc/apt/sources.list - mode: "0644" - register: apt_sources + - name: Backing up existing kali.sources... + ansible.builtin.copy: + src: /etc/apt/sources.list.d/kali.sources + dest: /etc/apt/kali.sources_backup + remote_src: true + mode: "0644" + when: not kali_sources_backup.stat.exists + + - name: Templating custom kali.sources to {{ inventory_hostname }}... + ansible.builtin.template: + src: kali.sources + dest: /etc/apt/sources.list.d/kali.sources + mode: "0644" + register: kali_sources_templated + +- name: Configuring sources.list... + when: not kali_sources.stat.exists + block: + - name: Checking if sources.list backup exists... + ansible.builtin.stat: + path: /etc/apt/sources.list_backup + register: apt_backup_file + + - name: Backing up existing sources.list... + ansible.builtin.copy: + src: /etc/apt/sources.list + dest: /etc/apt/sources.list_backup + remote_src: true + mode: "0644" + when: not apt_backup_file.stat.exists + + - name: Templating custom kali.list to {{ inventory_hostname }}... + ansible.builtin.template: + src: kali-sources.list + dest: /etc/apt/sources.list + mode: "0644" + register: list_apt_sources - name: Running apt update... # noqa: no-handler ansible.builtin.apt: update_cache: true retries: 2 - when: apt_sources.changed + when: list_apt_sources.changed | default(false) + or kali_sources_templated.changed | default(false) diff --git a/nova/core/roles/configure_package_mirrors/templates/kali.sources b/nova/core/roles/configure_package_mirrors/templates/kali.sources new file mode 100644 index 000000000..c86f4cfdf --- /dev/null +++ b/nova/core/roles/configure_package_mirrors/templates/kali.sources @@ -0,0 +1,7 @@ +# The original kali.sources is backed up as /etc/apt/kali.sources_backup + +Types: deb +URIs: {{ package_mirror_kali_uri.general }} +Suites: kali-rolling +Components: main contrib non-free non-free-firmware +Signed-By: /usr/share/keyrings/kali-archive-keyring.gpg From b41d0664f5e60534b562fdec1c244653039c7192 Mon Sep 17 00:00:00 2001 From: Allar Viik Date: Tue, 30 Jun 2026 13:14:58 +0300 Subject: [PATCH 11/18] Set WSL version to 2.9.3 in `docker` role --- nova/core/roles/docker/defaults/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/core/roles/docker/defaults/main.yml b/nova/core/roles/docker/defaults/main.yml index 0c3a1d227..8238fbb32 100644 --- a/nova/core/roles/docker/defaults/main.yml +++ b/nova/core/roles/docker/defaults/main.yml @@ -63,6 +63,6 @@ docker_non_default_debian_os_list: # Windows # ########### -docker_wsl_version: 2.7.10 +docker_wsl_version: 2.9.3 docker_wsl_download_link: https://github.com/microsoft/WSL/releases/download/{{ docker_wsl_version }}/wsl.{{ docker_wsl_version }}.0.x64.msi docker_desktop_autostart: true From b27e23b67ec79388011f247664e241b81c51fa55 Mon Sep 17 00:00:00 2001 From: Allar Viik Date: Tue, 30 Jun 2026 16:19:55 +0300 Subject: [PATCH 12/18] Fixed an issue where `nova.core.addresses` did not always report correct addresses --- nova/core/plugins/filter/addresses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/core/plugins/filter/addresses.py b/nova/core/plugins/filter/addresses.py index 5f9051469..5ebd6d592 100644 --- a/nova/core/plugins/filter/addresses.py +++ b/nova/core/plugins/filter/addresses.py @@ -185,7 +185,7 @@ def get_mgmt_addresses(self, interfaces, interface_type, mode): for interface in interfaces: if interface.get(interface_type): for address in interface.get('addresses', []): - if self.validate_mgmt_address(address, mode) and re.match(r'^mgmt-.*', address['pool_id']): + if self.validate_mgmt_address(address, mode): address_val = address['address'] addresses.append(address_val if address_val else 'null') # Append 'null' if address is an empty string return addresses @@ -208,7 +208,7 @@ def get_connection_address_info(self, interfaces): return address_info def validate_address(self, address, mode): - return address.get('mode') == mode and address.get('address') and address.get('pool_id').startswith('default-') + return address.get('mode') == mode and address.get('address') def validate_mgmt_address(self, address, mode): return address.get('mode') == mode and address.get('address') and address.get('pool_id').startswith('mgmt-') From a066fc611907f588e9c2c40149d66547602d9960 Mon Sep 17 00:00:00 2001 From: Allar Viik Date: Wed, 1 Jul 2026 23:37:41 +0300 Subject: [PATCH 13/18] Printing an error when EC2 image is not found --- nova/core/roles/machine_operations/tasks/aws/create.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nova/core/roles/machine_operations/tasks/aws/create.yml b/nova/core/roles/machine_operations/tasks/aws/create.yml index ca7a7be7c..fe32c6f4d 100644 --- a/nova/core/roles/machine_operations/tasks/aws/create.yml +++ b/nova/core/roles/machine_operations/tasks/aws/create.yml @@ -60,6 +60,11 @@ architecture: x86_64 register: found_amis + - name: NO OS IMAGES FOUND + ansible.builtin.fail: + msg: No OS images found check the *ami_owner_id* and *ami_name_search_string* variables + when: found_amis.images == [] + - name: Setting latest found image as instance image... ansible.builtin.set_fact: ec2_instance_image: "{{ (found_amis.images | sort(attribute='creation_date'))[-1] }}" From 2099bc34ae0e798fd250e59d48aec978f3fe393a Mon Sep 17 00:00:00 2001 From: Allar Viik Date: Thu, 2 Jul 2026 01:01:14 +0300 Subject: [PATCH 14/18] Using a raw reboot command when cleaning up stale MacOS accounts --- .../roles/accounts/tasks/macos_remove.yml | 31 ++++++------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/nova/core/roles/accounts/tasks/macos_remove.yml b/nova/core/roles/accounts/tasks/macos_remove.yml index 9ebb5b89d..acc5cd3c5 100644 --- a/nova/core/roles/accounts/tasks/macos_remove.yml +++ b/nova/core/roles/accounts/tasks/macos_remove.yml @@ -12,7 +12,8 @@ # Filtering out empty lines - name: Getting profiles not defined in accounts list... ansible.builtin.set_fact: - user_profiles_to_remove: "{{ home_profiles.stdout_lines | select('string') | select('regex', '.+') + user_profiles_to_remove: + "{{ home_profiles.stdout_lines | select('string') | select('regex', '.+') | difference(local_accounts_with_password | map(attribute='username')) }}" - name: Removing stale user accounts and profiles... @@ -28,28 +29,14 @@ rescue: - name: Handling reboot before removing accounts... when: accounts_reboot_before_remove | bool - delegate_to: localhost - become: false block: - - name: Restarting {{ custom_vm_name | default(vm_name) }} VM... - ansible.builtin.include_role: - name: nova.core.powerstate - vars: - restart: true - - - name: Waiting until ssh is down for {{ inventory_hostname }}... - ansible.builtin.wait_for: - host: "{{ ansible_host }}" - port: 22 - state: stopped - timeout: 300 - - - name: Waiting until ssh is up for {{ inventory_hostname }}... - ansible.builtin.wait_for: - host: "{{ ansible_host }}" - port: 22 - state: started - timeout: 300 + - name: Rebooting + ansible.builtin.raw: reboot + changed_when: true + # Running reboot from ansible.builtin.raw can cause a race condition where the task is marked as failed + # because the connection is lost before the reboot command completes. + # To avoid this, we can ignore unreachable hosts after issuing the reboot command. + ignore_unreachable: true - name: Removing following accounts... ansible.builtin.raw: | From 5c5cd1e1828e3ca551f31cbe107511e676238e72 Mon Sep 17 00:00:00 2001 From: Allar Viik Date: Thu, 2 Jul 2026 17:44:10 +0300 Subject: [PATCH 15/18] Allowing custom UPN to be set for AD user with the `accounts` role --- nova/core/galaxy.yml | 2 +- nova/core/roles/accounts/tasks/windows_create.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/core/galaxy.yml b/nova/core/galaxy.yml index 4c5a73933..4b2d37f55 100644 --- a/nova/core/galaxy.yml +++ b/nova/core/galaxy.yml @@ -1,6 +1,6 @@ namespace: nova name: core -version: 11.0.2 +version: 11.0.3 readme: README.md authors: - https://github.com/novateams diff --git a/nova/core/roles/accounts/tasks/windows_create.yml b/nova/core/roles/accounts/tasks/windows_create.yml index 7fa4edfff..edf690b15 100644 --- a/nova/core/roles/accounts/tasks/windows_create.yml +++ b/nova/core/roles/accounts/tasks/windows_create.yml @@ -71,7 +71,7 @@ spn: "{{ item.spn | default(omit) }}" # https://docs.ansible.com/ansible/latest/collections/microsoft/ad/user_module.html#parameter-spn state: present update_password: "{{ item.update_password | default('always') }}" # always, on_create or when_changed - upn: "{{ item.username }}@{{ ad_domain_name }}" + upn: "{{ item.upn | default(item.username ~ '@' ~ ad_domain_name) }}" groups: add: "{{ item.groups | default(windows_domain_admin_groups if item.username in admin_accounts | map(attribute='username') else omit) }}" register: win_domain_user From ccdcc9ffdb536c457376aa5cb122d8fa19a8a664 Mon Sep 17 00:00:00 2001 From: Allar Viik Date: Fri, 3 Jul 2026 20:50:10 +0300 Subject: [PATCH 16/18] Fixed an issue where `trusted_certificates` failed for MacOS when src was an http address --- .../roles/os_configuration/tasks/macos.yml | 4 ++-- .../roles/trusted_certificates/tasks/macos.yml | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/nova/core/roles/os_configuration/tasks/macos.yml b/nova/core/roles/os_configuration/tasks/macos.yml index dfe99e6f3..7fc374ad5 100644 --- a/nova/core/roles/os_configuration/tasks/macos.yml +++ b/nova/core/roles/os_configuration/tasks/macos.yml @@ -1,5 +1,5 @@ --- -- name: Set hostname +- name: Setting hostname to {{ hostname }}... ansible.builtin.raw: scutil --set {{ item }} changed_when: true with_items: @@ -7,7 +7,7 @@ - LocalHostName {{ hostname }} - ComputerName {{ hostname }} -- name: Lineinfile_raw ( /etc/hosts ) +- name: Updating {{ inventory_hostname }} /etc/hosts file with {{ fqdn }} and {{ hostname }}... ansible.builtin.raw: | line="127.0.1.1 {{ fqdn }} {{ hostname }}" file=/etc/hosts diff --git a/nova/core/roles/trusted_certificates/tasks/macos.yml b/nova/core/roles/trusted_certificates/tasks/macos.yml index f5ae2dd48..5d818d3fa 100644 --- a/nova/core/roles/trusted_certificates/tasks/macos.yml +++ b/nova/core/roles/trusted_certificates/tasks/macos.yml @@ -1,7 +1,7 @@ --- - name: Getting trusted certificates... - delegate_to: localhost become: false + delegate_to: localhost block: - name: Reading trusted certificates list... ansible.builtin.slurp: @@ -31,12 +31,12 @@ - name: Reading downloaded certificates... ansible.builtin.slurp: - src: "{{ crt_result.dest }}" + src: "{{ crt.dest }}" loop: "{{ download_trusted_certificates.results }}" loop_control: - loop_var: crt_result - label: "{{ crt_result.dest | default('') }}" - when: crt_result.dest is defined + loop_var: crt + label: "{{ crt.dest | default('') }}" + when: crt.dest is defined register: slurp_downloaded_certificates - name: Creating the /usr/local/etc/certs/ directory... @@ -44,12 +44,12 @@ changed_when: true - name: Saving certificates to {{ inventory_hostname }}... - ansible.builtin.raw: echo "{{ read_cert.content | b64decode }}" > /usr/local/etc/certs/{{ read_cert.crt.name | default('') }}.crt + ansible.builtin.raw: echo "{{ read_cert.content | b64decode }}" > /usr/local/etc/certs/{{ read_cert.crt.crt.name | default(read_cert.crt.name) }}.crt changed_when: true loop: "{{ slurp_trusted_certificates.results | default([]) + slurp_downloaded_certificates.results | default([]) }}" loop_control: loop_var: read_cert - label: /usr/local/etc/certs/{{ read_cert.crt.name | default('') }}.crt + label: /usr/local/etc/certs/{{ read_cert.crt.crt.name | default(read_cert.crt.name) }}.crt register: save_local_certs when: read_cert.content is defined @@ -60,11 +60,11 @@ - name: Manually run the following commands from an interactive session on {{ inventory_hostname }} to add the trusted certificates... ansible.builtin.debug: msg: sudo security add-trusted-cert -d -r trustRoot -k - /Library/Keychains/System.keychain /usr/local/etc/certs/{{ read_cert.crt.name | default('') }}.crt + /Library/Keychains/System.keychain /usr/local/etc/certs/{{ read_cert.crt.crt.name | default(read_cert.crt.name) }}.crt loop: "{{ slurp_trusted_certificates.results | default([]) + slurp_downloaded_certificates.results | default([]) }}" loop_control: loop_var: read_cert - label: /usr/local/etc/certs/{{ read_cert.crt.name | default('') }}.crt + label: /usr/local/etc/certs/{{ read_cert.crt.crt.name | default(read_cert.crt.name) }}.crt when: read_cert.content is defined - name: Giving time to read the above commands... From 3af83be4da7ebc80fff904dce11abf6da2cc9500 Mon Sep 17 00:00:00 2001 From: Allar Viik Date: Tue, 7 Jul 2026 12:00:14 +0300 Subject: [PATCH 17/18] Added extra up check for `gitlab` to improve fresh deploy stability --- nova/core/roles/gitlab/tasks/general-settings.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/nova/core/roles/gitlab/tasks/general-settings.yml b/nova/core/roles/gitlab/tasks/general-settings.yml index 7377ebcf7..3320a15e5 100644 --- a/nova/core/roles/gitlab/tasks/general-settings.yml +++ b/nova/core/roles/gitlab/tasks/general-settings.yml @@ -10,6 +10,7 @@ gitlab_product_usage_data_enabled: "{{ gitlab_general_settings_product_usage_data_enabled | default(false) }}" return_content: true status_code: [200, 201] + timeout: 120 # For some reason this sometimes returns -1 on fresh deploy and update when: gitlab_general_settings_product_usage_data_enabled is defined - name: Set version check ping settings.. From 535190ec3fb1af71b3257e8f2ac08e7053faf832 Mon Sep 17 00:00:00 2001 From: Allar Viik Date: Tue, 7 Jul 2026 14:49:32 +0300 Subject: [PATCH 18/18] Trying to run updates again on rescue when installing Gnome or Xfce on cloud Linux hosts --- nova/core/roles/os_configuration/tasks/linux_cloud.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/nova/core/roles/os_configuration/tasks/linux_cloud.yml b/nova/core/roles/os_configuration/tasks/linux_cloud.yml index ef31b870f..2c48ed0de 100644 --- a/nova/core/roles/os_configuration/tasks/linux_cloud.yml +++ b/nova/core/roles/os_configuration/tasks/linux_cloud.yml @@ -28,6 +28,10 @@ vars: restart: true + - name: Including nova.core.updates role... + ansible.builtin.include_role: + name: nova.core.updates + - name: Retrying Gnome installation... ansible.builtin.apt: name: ubuntu-gnome-desktop @@ -57,6 +61,10 @@ vars: restart: true + - name: Including nova.core.updates role... + ansible.builtin.include_role: + name: nova.core.updates + - name: Retrying Xfce installation... ansible.builtin.apt: name: kali-desktop-xfce