diff --git a/src/molecule_plugins/gce/playbooks/tasks/create_linux_instance.yml b/src/molecule_plugins/gce/playbooks/tasks/create_linux_instance.yml index a5efeb33..416502b2 100644 --- a/src/molecule_plugins/gce/playbooks/tasks/create_linux_instance.yml +++ b/src/molecule_plugins/gce/playbooks/tasks/create_linux_instance.yml @@ -29,6 +29,7 @@ selfLink: "{{ gcp_snet }}" access_configs: "{{ [{'name': 'instance_ip', 'type': 'ONE_TO_ONE_NAT'}] if molecule_yml.driver.external_access else [] }}" tags: "{{ item.tags | default(omit) }}" + labels: "{{ item.labels | default(omit) }}" zone: "{{ item.zone | default(molecule_yml.driver.region + '-b') }}" project: "{{ gcp_project_id }}" scopes: "{{ molecule_yml.driver.scopes | default(['https://www.googleapis.com/auth/compute'], True) }}" diff --git a/src/molecule_plugins/gce/playbooks/tasks/create_windows_instance.yml b/src/molecule_plugins/gce/playbooks/tasks/create_windows_instance.yml index 86e2a20b..b134d759 100644 --- a/src/molecule_plugins/gce/playbooks/tasks/create_windows_instance.yml +++ b/src/molecule_plugins/gce/playbooks/tasks/create_windows_instance.yml @@ -21,6 +21,7 @@ selfLink: "{{ gcp_snet }}" access_configs: "{{ [{'name': 'instance_ip', 'type': 'ONE_TO_ONE_NAT'}] if molecule_yml.driver.external_access else [] }}" tags: "{{ item.tags | default(omit) }}" + labels: "{{ item.labels | default(omit) }}" zone: "{{ item.zone | default(molecule_yml.driver.region + '-b') }}" project: "{{ gcp_project_id }}" scopes: "{{ molecule_yml.driver.scopes | default(['https://www.googleapis.com/auth/compute'], True) }}" diff --git a/test/gce/scenarios/label-verify/INSTALL.md b/test/gce/scenarios/label-verify/INSTALL.md new file mode 100644 index 00000000..2851c820 --- /dev/null +++ b/test/gce/scenarios/label-verify/INSTALL.md @@ -0,0 +1,19 @@ +# Google Cloud Engine driver installation guide + +## Requirements + +- A GCE credentials rc file + +## Install + +Please refer to the [Virtual environment][] documentation for +installation best practices. If not using a virtual environment, please +consider passing the widely recommended ['--user' flag][] when invoking +`pip`. + +```bash +pip install 'molecule_gce' +``` + +[Virtual environment]: https://virtualenv.pypa.io/en/latest/ +['--user' flag]: https://packaging.python.org/tutorials/installing-packages/#installing-to-the-user-site diff --git a/test/gce/scenarios/label-verify/create.yml b/test/gce/scenarios/label-verify/create.yml new file mode 100644 index 00000000..ba72e2c3 --- /dev/null +++ b/test/gce/scenarios/label-verify/create.yml @@ -0,0 +1,59 @@ +--- +- name: Create + hosts: localhost + connection: local + gather_facts: false + no_log: "{{ molecule_no_log }}" + vars: + ssh_identity_file: "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}/ssh_key" + gcp_project_id: "{{ molecule_yml.driver.project_id | default(lookup('env', 'GCE_PROJECT_ID')) }}" + + tasks: + - name: Make sure if linux or windows either specified + ansible.builtin.assert: + that: + - molecule_yml.driver.instance_os_type | lower == "linux" or molecule_yml.driver.instance_os_type | lower == "windows" + fail_msg: instance_os_type is possible only to specify linux or windows either + + - name: Get network info + google.cloud.gcp_compute_network_info: + filters: + - name = "{{ molecule_yml.driver.network_name | default('default') }}" + project: "{{ molecule_yml.driver.vpc_host_project | default(gcp_project_id) }}" + service_account_email: "{{ molecule_yml.driver.service_account_email | default(omit, true) }}" + service_account_file: "{{ molecule_yml.driver.service_account_file | default(omit, true) }}" + auth_kind: "{{ molecule_yml.driver.auth_kind | default(omit, true) }}" + register: my_network + + - name: Get subnetwork info + google.cloud.gcp_compute_subnetwork_info: + filters: + - name = "{{ molecule_yml.driver.subnetwork_name | default('default') }}" + project: "{{ molecule_yml.driver.vpc_host_project | default(gcp_project_id) }}" + region: "{{ molecule_yml.driver.region }}" + service_account_email: "{{ molecule_yml.driver.service_account_email | default(omit, true) }}" + service_account_file: "{{ molecule_yml.driver.service_account_file | default(omit, true) }}" + auth_kind: "{{ molecule_yml.driver.auth_kind | default(omit, true) }}" + register: my_subnetwork + + - name: Set external access config + ansible.builtin.set_fact: + external_access_config: + - access_configs: + - name: External NAT + type: ONE_TO_NAT + when: molecule_yml.driver.external_access + + - name: Include create_linux_instance tasks + ansible.builtin.include_tasks: tasks/create_linux_instance.yml + when: + - molecule_yml.driver.instance_os_type | lower == "linux" + + - name: Include create_windows_instance tasks + ansible.builtin.include_tasks: tasks/create_windows_instance.yml + when: + - molecule_yml.driver.instance_os_type | lower == "windows" + + handlers: + - name: Import main handler tasks + ansible.builtin.import_tasks: handlers/main.yml diff --git a/test/gce/scenarios/label-verify/destroy.yml b/test/gce/scenarios/label-verify/destroy.yml new file mode 100644 index 00000000..d9a8fb91 --- /dev/null +++ b/test/gce/scenarios/label-verify/destroy.yml @@ -0,0 +1,38 @@ +--- +- name: Destroy + hosts: localhost + connection: local + gather_facts: false + no_log: "{{ molecule_no_log }}" + + tasks: + - name: Destroy molecule instance(s) + google.cloud.gcp_compute_instance: + name: "{{ item.name }}" + state: absent + zone: "{{ item.zone | default(molecule_yml.driver.region + '-b') }}" + project: "{{ molecule_yml.driver.project_id | default(lookup('env', 'GCE_PROJECT_ID')) }}" + scopes: "{{ molecule_yml.driver.scopes | default(['https://www.googleapis.com/auth/compute'], True) }}" + service_account_email: "{{ molecule_yml.driver.service_account_email | default(omit, true) }}" + service_account_file: "{{ molecule_yml.driver.service_account_file | default(omit, true) }}" + auth_kind: "{{ molecule_yml.driver.auth_kind | default(omit, true) }}" + register: async_results + loop: "{{ molecule_yml.platforms }}" + async: 7200 + poll: 0 + notify: + - Wipe out instance config + - Dump instance config + + - name: Wait for instance(s) deletion to complete + ansible.builtin.async_status: + jid: "{{ item.ansible_job_id }}" + register: server + until: server.finished + retries: 300 + delay: 10 + loop: "{{ async_results.results }}" + + handlers: + - name: Import main handler tasks + ansible.builtin.import_tasks: handlers/main.yml diff --git a/test/gce/scenarios/label-verify/files/windows_auth.py b/test/gce/scenarios/label-verify/files/windows_auth.py new file mode 100755 index 00000000..5dd89558 --- /dev/null +++ b/test/gce/scenarios/label-verify/files/windows_auth.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python + +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import base64 +import copy +import datetime +import json +import time + +# PyCrypto library: https://pypi.python.org/pypi/pycrypto +from Crypto.Cipher import PKCS1_OAEP +from Crypto.PublicKey import RSA +from Crypto.Util.number import long_to_bytes +from googleapiclient.discovery import build + +# Google API Client Library for Python: +# https://developers.google.com/api-client-library/python/start/get_started +from oauth2client.client import GoogleCredentials + + +def GetCompute(): + """Get a compute object for communicating with the Compute Engine API.""" + credentials = GoogleCredentials.get_application_default() + compute = build("compute", "v1", credentials=credentials) + return compute + + +def GetInstance(compute, instance, zone, project): + """Get the data for a Google Compute Engine instance.""" + cmd = compute.instances().get(instance=instance, project=project, zone=zone) + return cmd.execute() + + +def GetKey(): + """Get an RSA key for encryption.""" + # This uses the PyCrypto library + key = RSA.generate(2048) + return key + + +def GetModulusExponentInBase64(key): + """Return the public modulus and exponent for the key in bas64 encoding.""" + mod = long_to_bytes(key.n) + exp = long_to_bytes(key.e) + + modulus = base64.b64encode(mod) + exponent = base64.b64encode(exp) + + return modulus, exponent + + +def GetExpirationTimeString(): + """Return an RFC3339 UTC timestamp for 5 minutes from now.""" + utc_now = datetime.datetime.utcnow() + # These metadata entries are one-time-use, so the expiration time does + # not need to be very far in the future. In fact, one minute would + # generally be sufficient. Five minutes allows for minor variations + # between the time on the client and the time on the server. + expire_time = utc_now + datetime.timedelta(minutes=5) + return expire_time.strftime("%Y-%m-%dT%H:%M:%SZ") + + +def GetJsonString(user, modulus, exponent, email): + """Return the JSON string object that represents the windows-keys entry.""" + converted_modulus = modulus.decode("utf-8") + converted_exponent = exponent.decode("utf-8") + + expire = GetExpirationTimeString() + data = { + "userName": user, + "modulus": converted_modulus, + "exponent": converted_exponent, + "email": email, + "expireOn": expire, + } + + return json.dumps(data) + + +def UpdateWindowsKeys(old_metadata, metadata_entry): + """Return updated metadata contents with the new windows-keys entry.""" + # Simply overwrites the "windows-keys" metadata entry. Production code may + # want to append new lines to the metadata value and remove any expired + # entries. + new_metadata = copy.deepcopy(old_metadata) + new_metadata["items"] = [{"key": "windows-keys", "value": metadata_entry}] + return new_metadata + + +def UpdateInstanceMetadata(compute, instance, zone, project, new_metadata): + """Update the instance metadata.""" + cmd = compute.instances().setMetadata( + instance=instance, + project=project, + zone=zone, + body=new_metadata, + ) + return cmd.execute() + + +def GetSerialPortFourOutput(compute, instance, zone, project): + """Get the output from serial port 4 from the instance.""" + # Encrypted passwords are printed to COM4 on the windows server: + port = 4 + cmd = compute.instances().getSerialPortOutput( + instance=instance, + project=project, + zone=zone, + port=port, + ) + output = cmd.execute() + return output["contents"] + + +def GetEncryptedPasswordFromSerialPort(serial_port_output, modulus): + """Find and return the correct encrypted password, based on the modulus.""" + # In production code, this may need to be run multiple times if the output + # does not yet contain the correct entry. + + converted_modulus = modulus.decode("utf-8") + + output = serial_port_output.split("\n") + for line in reversed(output): + try: + entry = json.loads(line) + if converted_modulus == entry["modulus"]: + return entry["encryptedPassword"] + except ValueError: + pass + return None + + +def DecryptPassword(encrypted_password, key): + """Decrypt a base64 encoded encrypted password using the provided key.""" + decoded_password = base64.b64decode(encrypted_password) + cipher = PKCS1_OAEP.new(key) + password = cipher.decrypt(decoded_password) + return password + + +def Arguments(): + # Create the parser + args = argparse.ArgumentParser(description="List the content of a folder") + + # Add the arguments + args.add_argument( + "--instance", + metavar="instance", + type=str, + help="compute instance name", + ) + + args.add_argument("--zone", metavar="zone", type=str, help="compute zone") + + args.add_argument("--project", metavar="project", type=str, help="gcp project") + + args.add_argument("--username", metavar="username", type=str, help="username") + + args.add_argument("--email", metavar="email", type=str, help="email") + + return args.parse_args() + + +def main(): + config_args = Arguments() + + # Setup + compute = GetCompute() + key = GetKey() + modulus, exponent = GetModulusExponentInBase64(key) + + # Get existing metadata + instance_ref = GetInstance( + compute, + config_args.instance, + config_args.zone, + config_args.project, + ) + old_metadata = instance_ref["metadata"] + # Create and set new metadata + metadata_entry = GetJsonString( + config_args.username, + modulus, + exponent, + config_args.email, + ) + new_metadata = UpdateWindowsKeys(old_metadata, metadata_entry) + + # Get Serial output BEFORE the modification + serial_port_output = GetSerialPortFourOutput( + compute, + config_args.instance, + config_args.zone, + config_args.project, + ) + + UpdateInstanceMetadata( + compute, + config_args.instance, + config_args.zone, + config_args.project, + new_metadata, + ) + + # Get and decrypt password from serial port output + # Monitor changes from output to get the encrypted password as soon as it's generated, will wait for 30 seconds + i = 0 + new_serial_port_output = serial_port_output + while i <= 30 and serial_port_output == new_serial_port_output: + new_serial_port_output = GetSerialPortFourOutput( + compute, + config_args.instance, + config_args.zone, + config_args.project, + ) + i += 1 + time.sleep(1) + + enc_password = GetEncryptedPasswordFromSerialPort(new_serial_port_output, modulus) + + password = DecryptPassword(enc_password, key) + converted_password = password.decode("utf-8") + + # Display only the password + print(format(converted_password)) # noqa: T201 + + +if __name__ == "__main__": + main() diff --git a/test/gce/scenarios/label-verify/handlers/main.yml b/test/gce/scenarios/label-verify/handlers/main.yml new file mode 100644 index 00000000..935a17a5 --- /dev/null +++ b/test/gce/scenarios/label-verify/handlers/main.yml @@ -0,0 +1,49 @@ +--- +- name: Populate instance config dict Linux + ansible.builtin.set_fact: + instance_conf_dict: + instance: "{{ instance_info.name }}" + address: + "{{ instance_info.networkInterfaces.0.accessConfigs.0.natIP if molecule_yml.driver.external_access else instance_info.networkInterfaces.0.networkIP + }}" + user: "{{ lookup('env', 'USER') }}" + port: "22" + identity_file: "{{ ssh_identity_file }}" + instance_os_type: "{{ molecule_yml.driver.instance_os_type }}" + loop: "{{ server.results }}" + loop_control: + loop_var: instance_info + no_log: true + register: instance_conf_dict + +- name: Populate instance config dict Windows + ansible.builtin.set_fact: + instance_conf_dict: + instance: "{{ instance_info.name }}" + address: + "{{ instance_info.networkInterfaces.0.accessConfigs.0.natIP if molecule_yml.driver.external_access else instance_info.networkInterfaces.0.networkIP + }}" + user: molecule_usr + password: "{{ instance_info.password }}" + port: "{{ instance_info.winrm_port | default(5986) }}" + winrm_transport: "{{ molecule_yml.driver.winrm_transport | default('ntlm') }}" + winrm_server_cert_validation: "{{ molecule_yml.driver.winrm_server_cert_validation | default('ignore') }}" + instance_os_type: "{{ molecule_yml.driver.instance_os_type }}" + loop: "{{ win_instances }}" + loop_control: + loop_var: instance_info + no_log: true + register: instance_conf_dict + +- name: Wipe out instance config + ansible.builtin.set_fact: + instance_conf: {} +- name: Convert instance config dict to a list + ansible.builtin.set_fact: + instance_conf: "{{ instance_conf_dict.results | map(attribute='ansible_facts.instance_conf_dict') | list }}" + +- name: Dump instance config + ansible.builtin.copy: + content: "{{ instance_conf }}" + dest: "{{ molecule_instance_config }}" + mode: "0600" diff --git a/test/gce/scenarios/label-verify/molecule/default/converge.yml b/test/gce/scenarios/label-verify/molecule/default/converge.yml new file mode 100644 index 00000000..2b212831 --- /dev/null +++ b/test/gce/scenarios/label-verify/molecule/default/converge.yml @@ -0,0 +1,7 @@ +--- +- name: Converge + hosts: all + tasks: + - name: Skip Converge + ansible.builtin.debug: + msg: "Skipping converge; labels are tested in verify." diff --git a/test/gce/scenarios/label-verify/molecule/default/molecule.yml b/test/gce/scenarios/label-verify/molecule/default/molecule.yml new file mode 100644 index 00000000..da1fc54f --- /dev/null +++ b/test/gce/scenarios/label-verify/molecule/default/molecule.yml @@ -0,0 +1,23 @@ +--- +dependency: + name: galaxy +driver: + name: gce + project_id: change-me # if not set, will default to env GCE_PROJECT_ID + auth_kind: # set to machineaccount or serviceaccount or application - if set to null will read env GCP_AUTH_KIND + region: us-central1 # REQUIRED. example: us-central1 + external_access: false # chose whether to create a public IP for the VM or not - default is private IP only + instance_os_type: linux # will be considered linux by default, but can be explicitly set to windows +platforms: + - name: linuxgce-createdbymolecule # is an instance name + machine_type: n1-standard-1 # define your machine type + preemptible: true # Define if it should be a preemptive or not + #zone: # example: us-west1-b, will default to zone b of driver.region + image: projects/debian-cloud/global/images/family/debian-12 + labels: + env: ci + role: under-test +provisioner: + name: ansible +verifier: + name: ansible diff --git a/test/gce/scenarios/label-verify/molecule/default/prepare.yml b/test/gce/scenarios/label-verify/molecule/default/prepare.yml new file mode 100644 index 00000000..076f259d --- /dev/null +++ b/test/gce/scenarios/label-verify/molecule/default/prepare.yml @@ -0,0 +1,7 @@ +--- +- name: Prepare + hosts: all + gather_facts: false + tasks: + - name: Wait 600 seconds for target connection to become reachable/usable + ansible.builtin.wait_for_connection: diff --git a/test/gce/scenarios/label-verify/molecule/default/verify.yml b/test/gce/scenarios/label-verify/molecule/default/verify.yml new file mode 100644 index 00000000..90e3e545 --- /dev/null +++ b/test/gce/scenarios/label-verify/molecule/default/verify.yml @@ -0,0 +1,14 @@ +--- +- name: Verify + hosts: localhost + gather_facts: false + vars: + gcp_project_id: "{{ molecule_yml.driver.project_id | default(lookup('env', 'GCE_PROJECT_ID')) }}" + gcp_zone_default: "{{ molecule_yml.driver.region ~ '-b' }}" + + tasks: + - name: Verify labels for each platform + ansible.builtin.include_tasks: verify_main_logic.yml + loop: "{{ molecule_yml.platforms }}" + loop_control: + loop_var: plat diff --git a/test/gce/scenarios/label-verify/molecule/default/verify_main_logic.yml b/test/gce/scenarios/label-verify/molecule/default/verify_main_logic.yml new file mode 100644 index 00000000..b7c695f5 --- /dev/null +++ b/test/gce/scenarios/label-verify/molecule/default/verify_main_logic.yml @@ -0,0 +1,37 @@ +--- +# Runs once per platform (variable: plat) + +- name: Fetch instance info (retry for eventual consistency) + google.cloud.gcp_compute_instance_info: + project: "{{ gcp_project_id }}" + zone: "{{ plat.zone | default(gcp_zone_default) }}" + filters: + - "name = {{ plat.name }}" + register: info + retries: 6 + delay: 5 + until: > + (info.resources is defined and (info.resources | length) > 0) + or + (info.items is defined and (info.items | length) > 0) + +- name: Pick the instance object + set_fact: + inst: >- + {{ + (info.resources | default([]) | first) + if (info.resources is defined and (info.resources | length) > 0) + else (info.items | default([]) | first) + }} + +- name: Assert each label key/value matches + when: plat.labels is defined and (plat.labels | length) > 0 + loop: "{{ plat.labels | dict2items }}" + loop_control: + loop_var: label + ansible.builtin.assert: + that: + - inst.labels is defined + - inst.labels.get(label.key) == label.value + success_msg: "Label {{ label.key }}={{ label.value }} present on {{ plat.name }}" + fail_msg: "Missing/incorrect label {{ label.key }} on {{ plat.name }} (got={{ inst.labels | default({}) }})" diff --git a/test/gce/scenarios/label-verify/requirements.yml b/test/gce/scenarios/label-verify/requirements.yml new file mode 100644 index 00000000..ac6a3656 --- /dev/null +++ b/test/gce/scenarios/label-verify/requirements.yml @@ -0,0 +1,2 @@ +collections: + - name: google.cloud diff --git a/test/gce/scenarios/label-verify/tasks/create_linux_instance.yml b/test/gce/scenarios/label-verify/tasks/create_linux_instance.yml new file mode 100644 index 00000000..9ebdc80f --- /dev/null +++ b/test/gce/scenarios/label-verify/tasks/create_linux_instance.yml @@ -0,0 +1,61 @@ +--- +- name: Create ssh keypair + community.crypto.openssh_keypair: + comment: "{{ lookup('env', 'USER') }} user for Molecule" + path: "{{ ssh_identity_file }}" + register: keypair + +- name: Create molecule Linux instance(s) + google.cloud.gcp_compute_instance: + state: present + name: "{{ item.name }}" + machine_type: "{{ item.machine_type | default('n1-standard-1') }}" + metadata: + ssh-keys: "{{ lookup('env', 'USER') }}:{{ keypair.public_key }}" + labels: "{{ item.labels | default(omit) }}" + scheduling: + preemptible: "{{ item.preemptible | default(false) }}" + disks: + - auto_delete: true + boot: true + initialize_params: + disk_size_gb: "{{ item.disk_size_gb | default(omit) }}" + source_image: "{{ item.image | default('projects/debian-cloud/global/images/family/debian-10') }}" + source_image_encryption_key: + raw_key: "{{ item.image_encryption_key | default(omit) }}" + network_interfaces: + "{{ [ { 'network': my_network.resources.0 | default(omit), 'subnetwork': my_subnetwork.resources.0 | default(omit) } | combine(external_access_config + | default([]) ) ] }}" + zone: "{{ item.zone | default(molecule_yml.driver.region + '-b') }}" + project: "{{ gcp_project_id }}" + scopes: "{{ molecule_yml.driver.scopes | default(['https://www.googleapis.com/auth/compute'], True) }}" + service_account_email: "{{ molecule_yml.driver.service_account_email | default(omit, true) }}" + service_account_file: "{{ molecule_yml.driver.service_account_file | default(omit, true) }}" + auth_kind: "{{ molecule_yml.driver.auth_kind | default(omit, true) }}" + register: async_results + loop: "{{ molecule_yml.platforms }}" + loop_control: + pause: 3 + async: 7200 + poll: 0 + +- name: Wait for instance(s) creation to complete + ansible.builtin.async_status: + jid: "{{ item.ansible_job_id }}" + loop: "{{ async_results.results }}" + register: server + until: server.finished + retries: 300 + delay: 10 + notify: + - Populate instance config dict Linux + - Convert instance config dict to a list + - Dump instance config + +- name: Wait for SSH + ansible.builtin.wait_for: + port: 22 + host: "{{ item.networkInterfaces.0.accessConfigs.0.natIP if molecule_yml.driver.external_access else item.networkInterfaces.0.networkIP }}" + search_regex: SSH + delay: 10 + loop: "{{ server.results }}" diff --git a/test/gce/scenarios/label-verify/tasks/create_windows_instance.yml b/test/gce/scenarios/label-verify/tasks/create_windows_instance.yml new file mode 100644 index 00000000..41765c4f --- /dev/null +++ b/test/gce/scenarios/label-verify/tasks/create_windows_instance.yml @@ -0,0 +1,70 @@ +--- +- name: Create molecule Windows instance(s) + google.cloud.gcp_compute_instance: + state: present + name: "{{ item.name }}" + machine_type: "{{ item.machine_type | default('n1-standard-1') }}" + labels: "{{ item.labels | default(omit) }}" + scheduling: + preemptible: "{{ item.preemptible | default(false) }}" + disks: + - auto_delete: true + boot: true + initialize_params: + disk_size_gb: "{{ item.disk_size_gb | default(omit) }}" + source_image: "{{ item.image | default('projects/windows-cloud/global/images/family/windows-2019') }}" + source_image_encryption_key: + raw_key: "{{ item.image_encryption_key | default(omit) }}" + network_interfaces: + "{{ [ { 'network': my_network.resources.0 | default(omit), 'subnetwork': my_subnetwork.resources.0 | default(omit) } | combine(external_access_config + | default([])) ] }}" + zone: "{{ item.zone | default(molecule_yml.driver.region + '-b') }}" + project: "{{ gcp_project_id }}" + scopes: "{{ molecule_yml.driver.scopes | default(['https://www.googleapis.com/auth/compute'], True) }}" + service_account_email: "{{ molecule_yml.driver.service_account_email | default(omit, true) }}" + service_account_file: "{{ molecule_yml.driver.service_account_file | default(omit, true) }}" + auth_kind: "{{ molecule_yml.driver.auth_kind | default(omit, true) }}" + register: async_results + loop: "{{ molecule_yml.platforms }}" + async: 7200 + poll: 0 + +- name: Wait for instance(s) creation to complete + ansible.builtin.async_status: + jid: "{{ item.ansible_job_id }}" + loop: "{{ async_results.results }}" + register: server + until: server.finished + retries: 300 + delay: 10 + notify: + - Populate instance config dict Windows + - Convert instance config dict to a list + - Dump instance config + +- name: Wait for WinRM + ansible.builtin.wait_for: + port: 5986 + host: "{{ item.networkInterfaces.0.accessConfigs.0.natIP if molecule_yml.driver.external_access else item.networkInterfaces.0.networkIP }}" + delay: 10 + loop: "{{ server.results }}" + +- name: Prepare Windows User + ansible.builtin.script: + ./files/windows_auth.py --instance {{ item.name }} --zone {{ item.zone | default(molecule_yml.driver.region + '-b') }} --project {{ gcp_project_id + }} --username molecule_usr + args: + executable: python3 + loop: "{{ molecule_yml.platforms }}" + changed_when: + - password.rc == 0 + - password.stdout + register: password + retries: 10 + delay: 10 + +- name: Add password for instances in server list + ansible.builtin.set_fact: + win_instances: "{{ win_instances | default([]) + [dict(item[0], password=item[1].stdout_lines | last)] }}" + loop: "{{ server.results | zip(password.results) | list }}" + no_log: true