mirror of
https://github.com/ansible-collections/google.cloud.git
synced 2025-09-12 16:08:33 -07:00
Merge pull request #709 from thekad/feature/connection-plugin
Some checks are pending
Run integration tests for the cloud.google collection / integration (stable-2.16) (push) Waiting to run
Run integration tests for the cloud.google collection / integration (stable-2.17) (push) Waiting to run
Run integration tests for the cloud.google collection / integration (stable-2.18) (push) Waiting to run
Run integration tests for the cloud.google collection / integration (stable-2.19) (push) Waiting to run
Some checks are pending
Run integration tests for the cloud.google collection / integration (stable-2.16) (push) Waiting to run
Run integration tests for the cloud.google collection / integration (stable-2.17) (push) Waiting to run
Run integration tests for the cloud.google collection / integration (stable-2.18) (push) Waiting to run
Run integration tests for the cloud.google collection / integration (stable-2.19) (push) Waiting to run
IAP connection plugin
This commit is contained in:
commit
091d11fd63
11 changed files with 1064 additions and 1 deletions
73
plugins/connection/README.md
Normal file
73
plugins/connection/README.md
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
# Identity Aware Proxy Connection Plugin
|
||||||
|
|
||||||
|
This plugin uses the gcloud cli [start-iap-tunnel](https://cloud.google.com/sdk/gcloud/reference/compute/start-iap-tunnel)
|
||||||
|
method to prepare TCP forwarding to your compute instances, and then uses the
|
||||||
|
builtin ansible SSH connection plugin to communicate ansible commands to the
|
||||||
|
target nodes.
|
||||||
|
|
||||||
|
This makes it possible to start using ansible without the need to expose your
|
||||||
|
instances to the open web, or configure stringent firewall rules to ensure no
|
||||||
|
bad actors can potentially login to your infrastructure.
|
||||||
|
|
||||||
|
## Requisites
|
||||||
|
|
||||||
|
1. The [gcloud cli tool](https://cloud.google.com/sdk/gcloud?authuser=0) installed
|
||||||
|
2. Firewall rules in places for [IAP TCP Forwarding](https://cloud.google.com/iap/docs/using-tcp-forwarding)
|
||||||
|
|
||||||
|
## Configuring the connection plugin
|
||||||
|
|
||||||
|
The connection plugin can be configured by setting some values in the
|
||||||
|
`[gcloud]` section of your ansible.cfg, here's an example:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[gcloud]
|
||||||
|
account = my-service-account@my-project.iam.gserviceaccount.com
|
||||||
|
project = my-project
|
||||||
|
region = us-central1
|
||||||
|
zone = us-central1-a
|
||||||
|
```
|
||||||
|
|
||||||
|
With the above, you can now connect to all your instances in a single
|
||||||
|
`us-central1-a` zone via IAP.
|
||||||
|
|
||||||
|
You can also couple this with the GCP dynamic inventory like so:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
plugin: google.cloud.gcp_compute
|
||||||
|
zones:
|
||||||
|
- us-central1-a
|
||||||
|
- us-central1-b
|
||||||
|
- us-central1-c
|
||||||
|
- us-central1-f
|
||||||
|
projects:
|
||||||
|
- my-project
|
||||||
|
service_account_file: /path/to/my/service-account.json
|
||||||
|
auth_kind: serviceaccount
|
||||||
|
scopes:
|
||||||
|
- 'https://www.googleapis.com/auth/cloud-platform'
|
||||||
|
- 'https://www.googleapis.com/auth/compute.readonly'
|
||||||
|
|
||||||
|
# Create groups from labels e.g.
|
||||||
|
keyed_groups:
|
||||||
|
- prefix: gcp
|
||||||
|
key: labels.gcp_role
|
||||||
|
|
||||||
|
# inventory_hostname needs to be the actual name of the instance
|
||||||
|
hostnames:
|
||||||
|
- name
|
||||||
|
|
||||||
|
# fetch zone dynamically to feed IAP plugin
|
||||||
|
compose:
|
||||||
|
ansible_gcloud_zone: zone
|
||||||
|
|
||||||
|
# maybe add some filters
|
||||||
|
filters:
|
||||||
|
- 'status = RUNNING'
|
||||||
|
- 'labels.my-special-label:some-value'
|
||||||
|
```
|
||||||
|
|
||||||
|
with the above, you don't need to statically set the zone, they will be
|
||||||
|
populated accordingly.
|
||||||
|
|
||||||
|
The rest of the connection behavior can be configured just like the builtin SSH
|
||||||
|
ansible plugin, e.g. remote user, etc.
|
775
plugins/connection/iap.py
Normal file
775
plugins/connection/iap.py
Normal file
|
@ -0,0 +1,775 @@
|
||||||
|
# Copyright (c) 2025 Red Hat
|
||||||
|
# GNU General Public License v3.0+ https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
# I had to duplicate (almost?) all of the documentation found in the
|
||||||
|
# ansible.plugins.connection.ssh plugin, due to how ansible-doc and ansible-test sanity
|
||||||
|
# work, they look at the lexical structure of the code. I had initially done:
|
||||||
|
# 1. load ssh.py DOCUMENTATION string into a yaml object
|
||||||
|
# 2. modify the yaml object to change defaults / add my options
|
||||||
|
# 3. set this plugin's DOCUMENTATION to the yaml.dump() of the modified object
|
||||||
|
# but that doesn't work with how the AST evaluation is done.
|
||||||
|
# here are the changes from upstream:
|
||||||
|
# 1. Changed default private_key_file default to ~/.ssh/google_compute_engine
|
||||||
|
# 2. Make host_key_checking default to False
|
||||||
|
# 3. Added known_hosts_file option pointing to ~/.ssh/google_compute_known_hosts
|
||||||
|
DOCUMENTATION = """
|
||||||
|
name: iap
|
||||||
|
short_description: connect via SSH through Google Cloud's Identity Aware Proxy (IAP)
|
||||||
|
description:
|
||||||
|
- This connection plugin behaves almost like the stock SSH plugin, but it creates
|
||||||
|
a new IAP process per host in the inventory so connections are tunneled through
|
||||||
|
it.
|
||||||
|
author:
|
||||||
|
- Jorge A Gallegos (@thekad)
|
||||||
|
notes:
|
||||||
|
- This plugin requires you to have configured gcloud authentication prior to using
|
||||||
|
it. You can change the active configuration used, but the plugin won't auth
|
||||||
|
for you.
|
||||||
|
- This plugin is mostly a wrapper to the ``ssh`` CLI utility and the exact behavior
|
||||||
|
of the options depends on this tool. This means that the documentation provided
|
||||||
|
here is subject to be overridden by the CLI tool itself.
|
||||||
|
- Many options default to V(None) here but that only means we do not override the
|
||||||
|
SSH tool's defaults and/or configuration. For example, if you specify the port
|
||||||
|
in this plugin it will override any C(Port) entry in your C(.ssh/config).
|
||||||
|
- The ssh CLI tool uses return code 255 as a 'connection error', this can conflict
|
||||||
|
with commands/tools that also return 255 as an error code and will look like an
|
||||||
|
'unreachable' condition or 'connection error' to this plugin.
|
||||||
|
extends_documentation_fragment:
|
||||||
|
- connection_pipelining
|
||||||
|
options:
|
||||||
|
gcloud_executable:
|
||||||
|
description:
|
||||||
|
- Path to the gcloud executable, defaults to whatever is found in the PATH
|
||||||
|
environment variable.
|
||||||
|
type: string
|
||||||
|
vars:
|
||||||
|
- name: ansible_gcloud_executable
|
||||||
|
ini:
|
||||||
|
- section: gcloud
|
||||||
|
key: executable
|
||||||
|
gcloud_configuration:
|
||||||
|
description:
|
||||||
|
- If set, points to non-standard gcloud configuration.
|
||||||
|
type: string
|
||||||
|
vars:
|
||||||
|
- name: ansible_gcloud_configuration
|
||||||
|
ini:
|
||||||
|
- section: gcloud
|
||||||
|
key: configuration
|
||||||
|
env:
|
||||||
|
- name: CLOUDSDK_ACTIVE_CONFIG_NAME
|
||||||
|
gcloud_project:
|
||||||
|
description:
|
||||||
|
- The Google Cloud project ID to use for this invocation.
|
||||||
|
- If omitted, then the current active project is assumed.
|
||||||
|
type: string
|
||||||
|
vars:
|
||||||
|
- name: ansible_gcloud_project
|
||||||
|
ini:
|
||||||
|
- section: gcloud
|
||||||
|
key: project
|
||||||
|
env:
|
||||||
|
- name: CLOUDSDK_CORE_PROJECT
|
||||||
|
gcloud_account:
|
||||||
|
description:
|
||||||
|
- Google cloud account to use for invocation.
|
||||||
|
type: string
|
||||||
|
vars:
|
||||||
|
- name: ansible_gcloud_account
|
||||||
|
ini:
|
||||||
|
- section: gcloud
|
||||||
|
key: account
|
||||||
|
env:
|
||||||
|
- name: CLOUDSDK_CORE_ACCOUNT
|
||||||
|
gcloud_zone:
|
||||||
|
description:
|
||||||
|
- The Google Cloud zone to use for the instance(s).
|
||||||
|
type: string
|
||||||
|
vars:
|
||||||
|
- name: ansible_gcloud_zone
|
||||||
|
ini:
|
||||||
|
- section: gcloud
|
||||||
|
key: zone
|
||||||
|
env:
|
||||||
|
- name: CLOUDSDK_COMPUTE_ZONE
|
||||||
|
gcloud_access_token_file:
|
||||||
|
description:
|
||||||
|
- A file to read the access token from.
|
||||||
|
- The credentials of the active account (if exists) will be ignored.
|
||||||
|
type: string
|
||||||
|
vars:
|
||||||
|
- name: ansible_access_token_file
|
||||||
|
ini:
|
||||||
|
- section: gcloud
|
||||||
|
key: access_token_file
|
||||||
|
env:
|
||||||
|
- name: CLOUDSDK_AUTH_ACCESS_TOKEN_FILE
|
||||||
|
host:
|
||||||
|
description: Google Cloud instance name to connect to.
|
||||||
|
default: inventory_hostname
|
||||||
|
type: string
|
||||||
|
vars:
|
||||||
|
- name: inventory_hostname
|
||||||
|
- name: ansible_host
|
||||||
|
- name: ansible_gcloud_host
|
||||||
|
known_hosts_file:
|
||||||
|
description: Path to the UserKnownHosts file storing SSH fingerprints. Defaults
|
||||||
|
to the same file used by `gcloud compute ssh`
|
||||||
|
type: string
|
||||||
|
default: ~/.ssh/google_compute_known_hosts
|
||||||
|
ini:
|
||||||
|
- section: ssh_connection
|
||||||
|
key: known_hosts_file
|
||||||
|
vars:
|
||||||
|
- name: ansible_known_hosts_file
|
||||||
|
- name: ansible_ssh_known_hosts_file
|
||||||
|
host_key_checking:
|
||||||
|
description: Determines if SSH should reject or not a connection after checking
|
||||||
|
host keys.
|
||||||
|
default: false
|
||||||
|
type: boolean
|
||||||
|
ini:
|
||||||
|
- section: defaults
|
||||||
|
key: host_key_checking
|
||||||
|
- section: ssh_connection
|
||||||
|
key: host_key_checking
|
||||||
|
env:
|
||||||
|
- name: ANSIBLE_HOST_KEY_CHECKING
|
||||||
|
- name: ANSIBLE_SSH_HOST_KEY_CHECKING
|
||||||
|
vars:
|
||||||
|
- name: ansible_host_key_checking
|
||||||
|
- name: ansible_ssh_host_key_checking
|
||||||
|
password:
|
||||||
|
description:
|
||||||
|
- Authentication password for the O(remote_user). Can be supplied as CLI option.
|
||||||
|
type: string
|
||||||
|
vars:
|
||||||
|
- name: ansible_password
|
||||||
|
- name: ansible_ssh_pass
|
||||||
|
- name: ansible_ssh_password
|
||||||
|
password_mechanism:
|
||||||
|
description: Mechanism to use for handling ssh password prompt
|
||||||
|
type: string
|
||||||
|
default: ssh_askpass
|
||||||
|
choices:
|
||||||
|
- ssh_askpass
|
||||||
|
- sshpass
|
||||||
|
- disable
|
||||||
|
env:
|
||||||
|
- name: ANSIBLE_SSH_PASSWORD_MECHANISM
|
||||||
|
ini:
|
||||||
|
- section: ssh_connection
|
||||||
|
key: password_mechanism
|
||||||
|
vars:
|
||||||
|
- name: ansible_ssh_password_mechanism
|
||||||
|
sshpass_prompt:
|
||||||
|
description:
|
||||||
|
- Password prompt that C(sshpass)/C(SSH_ASKPASS) should search for.
|
||||||
|
- Supported by sshpass 1.06 and up when O(password_mechanism) set to V(sshpass).
|
||||||
|
- Defaults to C(Enter PIN for) when pkcs11_provider is set.
|
||||||
|
- Defaults to C(assword) when O(password_mechanism) set to V(ssh_askpass).
|
||||||
|
default: ''
|
||||||
|
type: string
|
||||||
|
ini:
|
||||||
|
- section: ssh_connection
|
||||||
|
key: 'sshpass_prompt'
|
||||||
|
env:
|
||||||
|
- name: ANSIBLE_SSHPASS_PROMPT
|
||||||
|
vars:
|
||||||
|
- name: ansible_sshpass_prompt
|
||||||
|
ssh_args:
|
||||||
|
description: arguments to pass to all ssh cli tools.
|
||||||
|
type: string
|
||||||
|
default: '-C -o controlmaster=auto -o controlpersist=60s'
|
||||||
|
ini:
|
||||||
|
- section: ssh_connection
|
||||||
|
key: ssh_args
|
||||||
|
env:
|
||||||
|
- name: ansible_ssh_args
|
||||||
|
vars:
|
||||||
|
- name: ansible_ssh_args
|
||||||
|
ssh_common_args:
|
||||||
|
description: common extra args for all ssh cli tools.
|
||||||
|
type: string
|
||||||
|
default: ''
|
||||||
|
ini:
|
||||||
|
- section: ssh_connection
|
||||||
|
key: ssh_common_args
|
||||||
|
env:
|
||||||
|
- name: ANSIBLE_SSH_COMMON_ARGS
|
||||||
|
vars:
|
||||||
|
- name: ansible_ssh_common_args
|
||||||
|
cli:
|
||||||
|
- name: ssh_common_args
|
||||||
|
ssh_executable:
|
||||||
|
description:
|
||||||
|
- This defines the location of the SSH binary. It defaults to V(ssh) which will
|
||||||
|
use the first SSH binary available in $PATH.
|
||||||
|
- This option is usually not required, it might be useful when access to system
|
||||||
|
SSH is restricted, or when using SSH wrappers to connect to remote hosts.
|
||||||
|
type: string
|
||||||
|
default: ssh
|
||||||
|
ini:
|
||||||
|
- key: ssh_executable
|
||||||
|
section: ssh_connection
|
||||||
|
env:
|
||||||
|
- name: ANSIBLE_SSH_EXECUTABLE
|
||||||
|
vars:
|
||||||
|
- name: ansible_ssh_executable
|
||||||
|
sftp_executable:
|
||||||
|
description:
|
||||||
|
- This defines the location of the sftp binary. It defaults to V(sftp) which
|
||||||
|
will use the first binary available in $PATH.
|
||||||
|
type: string
|
||||||
|
default: sftp
|
||||||
|
ini:
|
||||||
|
- key: sftp_executable
|
||||||
|
section: ssh_connection
|
||||||
|
env:
|
||||||
|
- name: ANSIBLE_SFTP_EXECUTABLE
|
||||||
|
vars:
|
||||||
|
- name: ansible_sftp_executable
|
||||||
|
scp_executable:
|
||||||
|
description:
|
||||||
|
- This defines the location of the scp binary. It defaults to V(scp) which will
|
||||||
|
use the first binary available in $PATH.
|
||||||
|
type: string
|
||||||
|
default: scp
|
||||||
|
ini:
|
||||||
|
- section: ssh_connection
|
||||||
|
key: scp_executable
|
||||||
|
env:
|
||||||
|
- name: ANSIBLE_SCP_EXECUTABLE
|
||||||
|
vars:
|
||||||
|
- name: ansible_scp_executable
|
||||||
|
scp_extra_args:
|
||||||
|
description: Extra exclusive to the C(scp) CLI
|
||||||
|
type: string
|
||||||
|
default: ''
|
||||||
|
ini:
|
||||||
|
- section: ssh_connection
|
||||||
|
key: scp_extra_args
|
||||||
|
env:
|
||||||
|
- name: ANSIBLE_SCP_EXTRA_ARGS
|
||||||
|
vars:
|
||||||
|
- name: ansible_scp_extra_args
|
||||||
|
cli:
|
||||||
|
- name: scp_extra_args
|
||||||
|
sftp_extra_args:
|
||||||
|
description: Extra exclusive to the C(sftp) CLI
|
||||||
|
type: string
|
||||||
|
default: ''
|
||||||
|
ini:
|
||||||
|
- section: ssh_connection
|
||||||
|
key: sftp_extra_args
|
||||||
|
vars:
|
||||||
|
- name: ansible_sftp_extra_args
|
||||||
|
env:
|
||||||
|
- name: ANSIBLE_SFTP_EXTRA_ARGS
|
||||||
|
cli:
|
||||||
|
- name: sftp_extra_args
|
||||||
|
ssh_extra_args:
|
||||||
|
description: Extra exclusive to the SSH CLI.
|
||||||
|
type: string
|
||||||
|
default: ''
|
||||||
|
vars:
|
||||||
|
- name: ansible_ssh_extra_args
|
||||||
|
env:
|
||||||
|
- name: ANSIBLE_SSH_EXTRA_ARGS
|
||||||
|
ini:
|
||||||
|
- section: ssh_connection
|
||||||
|
key: ssh_extra_args
|
||||||
|
cli:
|
||||||
|
- name: ssh_extra_args
|
||||||
|
reconnection_retries:
|
||||||
|
description:
|
||||||
|
- Number of attempts to connect.
|
||||||
|
- Ansible retries connections only if it gets an SSH error with a return code
|
||||||
|
of 255.
|
||||||
|
- Any errors with return codes other than 255 indicate an issue with program
|
||||||
|
execution.
|
||||||
|
type: integer
|
||||||
|
default: 0
|
||||||
|
env:
|
||||||
|
- name: ANSIBLE_SSH_RETRIES
|
||||||
|
ini:
|
||||||
|
- section: connection
|
||||||
|
key: retries
|
||||||
|
- section: ssh_connection
|
||||||
|
key: retries
|
||||||
|
vars:
|
||||||
|
- name: ansible_ssh_retries
|
||||||
|
port:
|
||||||
|
description: Remote port to connect to.
|
||||||
|
type: int
|
||||||
|
ini:
|
||||||
|
- section: defaults
|
||||||
|
key: remote_port
|
||||||
|
env:
|
||||||
|
- name: ANSIBLE_REMOTE_PORT
|
||||||
|
vars:
|
||||||
|
- name: ansible_port
|
||||||
|
- name: ansible_ssh_port
|
||||||
|
keyword:
|
||||||
|
- name: port
|
||||||
|
remote_user:
|
||||||
|
description:
|
||||||
|
- User name with which to login to the remote server, normally set by the
|
||||||
|
remote_user keyword.
|
||||||
|
- If no user is supplied, Ansible will let the SSH client binary choose the
|
||||||
|
user as it normally.
|
||||||
|
type: string
|
||||||
|
ini:
|
||||||
|
- section: defaults
|
||||||
|
key: remote_user
|
||||||
|
env:
|
||||||
|
- name: ANSIBLE_REMOTE_USER
|
||||||
|
vars:
|
||||||
|
- name: ansible_user
|
||||||
|
- name: ansible_ssh_user
|
||||||
|
cli:
|
||||||
|
- name: user
|
||||||
|
keyword:
|
||||||
|
- name: remote_user
|
||||||
|
private_key_file:
|
||||||
|
description:
|
||||||
|
- Path to private key file to use for authentication.
|
||||||
|
type: string
|
||||||
|
default: '~/.ssh/google_compute_engine'
|
||||||
|
ini:
|
||||||
|
- section: defaults
|
||||||
|
key: private_key_file
|
||||||
|
- section: gcloud
|
||||||
|
key: private_key_file
|
||||||
|
env:
|
||||||
|
- name: ANSIBLE_PRIVATE_KEY_FILE
|
||||||
|
vars:
|
||||||
|
- name: ansible_private_key_file
|
||||||
|
- name: ansible_ssh_private_key_file
|
||||||
|
- name: ansible_gcloud_private_key_file
|
||||||
|
cli:
|
||||||
|
- name: private_key_file
|
||||||
|
option: '--private-key'
|
||||||
|
private_key:
|
||||||
|
description:
|
||||||
|
- Private key contents in PEM format. Requires the C(SSH_AGENT) configuration
|
||||||
|
to be enabled.
|
||||||
|
type: string
|
||||||
|
env:
|
||||||
|
- name: ANSIBLE_PRIVATE_KEY
|
||||||
|
vars:
|
||||||
|
- name: ansible_private_key
|
||||||
|
- name: ansible_ssh_private_key
|
||||||
|
private_key_passphrase:
|
||||||
|
description:
|
||||||
|
- Private key passphrase, dependent on O(private_key).
|
||||||
|
- This does NOT have any effect when used with O(private_key_file).
|
||||||
|
type: string
|
||||||
|
env:
|
||||||
|
- name: ANSIBLE_PRIVATE_KEY_PASSPHRASE
|
||||||
|
vars:
|
||||||
|
- name: ansible_private_key_passphrase
|
||||||
|
- name: ansible_ssh_private_key_passphrase
|
||||||
|
control_path:
|
||||||
|
description:
|
||||||
|
- This is the location to save SSH's ControlPath sockets, it uses SSH's variable
|
||||||
|
substitution.
|
||||||
|
- Be aware that this setting is ignored if C(-o ControlPath) is set in ssh args.
|
||||||
|
type: string
|
||||||
|
env:
|
||||||
|
- name: ANSIBLE_SSH_CONTROL_PATH
|
||||||
|
ini:
|
||||||
|
- section: ssh_connection
|
||||||
|
key: control_path
|
||||||
|
vars:
|
||||||
|
- name: ansible_control_path
|
||||||
|
control_path_dir:
|
||||||
|
default: ~/.ansible/cp
|
||||||
|
description:
|
||||||
|
- This sets the directory to use for ssh control path if the control path
|
||||||
|
setting is null.
|
||||||
|
- Also, provides the ``%(directory)s`` variable for the control path setting.
|
||||||
|
type: string
|
||||||
|
env:
|
||||||
|
- name: ANSIBLE_SSH_CONTROL_PATH_DIR
|
||||||
|
ini:
|
||||||
|
- section: ssh_connection
|
||||||
|
key: control_path_dir
|
||||||
|
vars:
|
||||||
|
- name: ansible_control_path_dir
|
||||||
|
sftp_batch_mode:
|
||||||
|
description:
|
||||||
|
- When set to C(True), sftp will be run in batch mode, allowing detection of
|
||||||
|
transfer errors.
|
||||||
|
- When set to C(False), sftp will not be run in batch mode, preventing detection
|
||||||
|
of transfer errors.
|
||||||
|
type: bool
|
||||||
|
default: true
|
||||||
|
env:
|
||||||
|
- name: ANSIBLE_SFTP_BATCH_MODE
|
||||||
|
ini:
|
||||||
|
- section: ssh_connection
|
||||||
|
key: sftp_batch_mode
|
||||||
|
vars:
|
||||||
|
- name: ansible_sftp_batch_mode
|
||||||
|
ssh_transfer_method:
|
||||||
|
description: Preferred method to use when transferring files over ssh
|
||||||
|
type: string
|
||||||
|
default: smart
|
||||||
|
choices:
|
||||||
|
- sftp
|
||||||
|
- scp
|
||||||
|
- piped
|
||||||
|
- smart
|
||||||
|
env:
|
||||||
|
- name: ANSIBLE_SSH_TRANSFER_METHOD
|
||||||
|
ini:
|
||||||
|
- section: ssh_connection
|
||||||
|
key: transfer_method
|
||||||
|
vars:
|
||||||
|
- name: ansible_ssh_transfer_method
|
||||||
|
use_tty:
|
||||||
|
description: add -tt to ssh commands to force tty allocation.
|
||||||
|
type: bool
|
||||||
|
default: true
|
||||||
|
env:
|
||||||
|
- name: ANSIBLE_SSH_USETTY
|
||||||
|
ini:
|
||||||
|
- section: ssh_connection
|
||||||
|
key: usetty
|
||||||
|
vars:
|
||||||
|
- name: ansible_ssh_use_tty
|
||||||
|
timeout:
|
||||||
|
description:
|
||||||
|
- This is the default amount of time we will wait while establishing an SSH
|
||||||
|
connection.
|
||||||
|
- It also controls how long we can wait to access reading the connection once
|
||||||
|
established (select on the socket).
|
||||||
|
type: integer
|
||||||
|
default: 10
|
||||||
|
env:
|
||||||
|
- name: ANSIBLE_TIMEOUT
|
||||||
|
- name: ANSIBLE_SSH_TIMEOUT
|
||||||
|
ini:
|
||||||
|
- section: defaults
|
||||||
|
key: timeout
|
||||||
|
- section: ssh_connection
|
||||||
|
key: timeout
|
||||||
|
vars:
|
||||||
|
- name: ansible_ssh_timeout
|
||||||
|
cli:
|
||||||
|
- name: timeout
|
||||||
|
pkcs11_provider:
|
||||||
|
description:
|
||||||
|
- PKCS11 SmartCard provider such as opensc, e.g. /usr/local/lib/opensc-pkcs11.so
|
||||||
|
type: string
|
||||||
|
default: ''
|
||||||
|
env:
|
||||||
|
- name: ANSIBLE_PKCS11_PROVIDER
|
||||||
|
ini:
|
||||||
|
- section: ssh_connection
|
||||||
|
key: pkcs11_provider
|
||||||
|
vars:
|
||||||
|
- name: ansible_ssh_pkcs11_provider
|
||||||
|
verbosity:
|
||||||
|
description:
|
||||||
|
- Requested verbosity level for the SSH CLI.
|
||||||
|
default: 0
|
||||||
|
type: int
|
||||||
|
env:
|
||||||
|
- name: ANSIBLE_SSH_VERBOSITY
|
||||||
|
ini:
|
||||||
|
- section: ssh_connection
|
||||||
|
key: verbosity
|
||||||
|
vars:
|
||||||
|
- name: ansible_ssh_verbosity
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import pty
|
||||||
|
import shlex
|
||||||
|
import select
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import tempfile
|
||||||
|
import typing as T
|
||||||
|
from os import path as ospath
|
||||||
|
|
||||||
|
from ansible.plugins.connection import ssh as sshconn
|
||||||
|
from ansible import errors
|
||||||
|
from ansible.utils import display
|
||||||
|
|
||||||
|
D = display.Display()
|
||||||
|
DEFAULT_GCLOUD: T.Optional[str] = shutil.which("gcloud")
|
||||||
|
DEFAULT_SSH_PORT: int = 22
|
||||||
|
PORT_REGEX = re.compile(r"\d+")
|
||||||
|
|
||||||
|
|
||||||
|
class IAP:
|
||||||
|
host: str
|
||||||
|
local_port: int
|
||||||
|
remote_port: int
|
||||||
|
master_fd: int
|
||||||
|
up: bool = False
|
||||||
|
process: T.Optional[subprocess.Popen] = None
|
||||||
|
thread: T.Optional[threading.Thread] = None
|
||||||
|
ready: threading.Event = threading.Event()
|
||||||
|
output: T.List[str] = []
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
gcloud_bin: str,
|
||||||
|
host: str,
|
||||||
|
remote_port: int,
|
||||||
|
project: T.Optional[str],
|
||||||
|
account: T.Optional[str],
|
||||||
|
zone: T.Optional[str],
|
||||||
|
config: T.Optional[str] = None,
|
||||||
|
token_file: T.Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
self.host = host
|
||||||
|
self.remote_port = remote_port
|
||||||
|
cmd: T.List[str] = [
|
||||||
|
gcloud_bin,
|
||||||
|
"compute",
|
||||||
|
"start-iap-tunnel",
|
||||||
|
host,
|
||||||
|
str(self.remote_port),
|
||||||
|
]
|
||||||
|
if config is not None:
|
||||||
|
cmd.extend(
|
||||||
|
[
|
||||||
|
"--configuration",
|
||||||
|
shlex.quote(ospath.realpath(ospath.expanduser(config.strip()))),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
if project is not None:
|
||||||
|
cmd.extend(
|
||||||
|
[
|
||||||
|
"--project",
|
||||||
|
shlex.quote(project.strip()),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
if account is not None:
|
||||||
|
cmd.extend(
|
||||||
|
[
|
||||||
|
"--account",
|
||||||
|
shlex.quote(account.strip()),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
if zone is not None:
|
||||||
|
cmd.extend(
|
||||||
|
[
|
||||||
|
"--zone",
|
||||||
|
shlex.quote(zone.strip()),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
if token_file is not None:
|
||||||
|
cmd.extend(
|
||||||
|
[
|
||||||
|
"--access-token-file",
|
||||||
|
shlex.quote(token_file.strip()),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
D.vvv(f"IAP: CMD {' '.join(cmd)}", host=self.host)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# start-iap-tunnel prints 2 lines:
|
||||||
|
# - Picking local unused port [$PORT].
|
||||||
|
# - Testing if tunnel connection works.
|
||||||
|
# and only when the terminal is a pty, a 3rd line:
|
||||||
|
# - Listening on port [$PORT].
|
||||||
|
# The last line only displayed after the tunnel has been tested,
|
||||||
|
# that's why we use a PTY for the subprocess
|
||||||
|
self.master_fd, slave_fd = pty.openpty()
|
||||||
|
self.process = subprocess.Popen(
|
||||||
|
cmd, stdout=slave_fd, stderr=slave_fd, text=True, close_fds=True
|
||||||
|
)
|
||||||
|
os.close(slave_fd)
|
||||||
|
self.thread = threading.Thread(target=self._monitor, daemon=True)
|
||||||
|
self.thread.start()
|
||||||
|
D.vvvvv("started IAP thread", host=self.host)
|
||||||
|
except Exception as e:
|
||||||
|
self.process = None
|
||||||
|
raise Exception from e
|
||||||
|
|
||||||
|
def _monitor(self) -> None:
|
||||||
|
"""Monitor the thread handling the IAP subprocess until it is 'up'"""
|
||||||
|
|
||||||
|
while self.process is not None and self.process.poll() is None:
|
||||||
|
# pylint: disable=disallowed-name
|
||||||
|
rlist, _, _ = select.select([self.master_fd], [], [], 0.1)
|
||||||
|
if rlist is not None:
|
||||||
|
try:
|
||||||
|
output = os.read(self.master_fd, 1024).decode("utf-8")
|
||||||
|
if output:
|
||||||
|
for line in output.splitlines():
|
||||||
|
self.output.append(line)
|
||||||
|
if line.startswith("Listening on port"):
|
||||||
|
m = PORT_REGEX.search(line)
|
||||||
|
if m is not None:
|
||||||
|
self.local_port = int(m.group())
|
||||||
|
self.up = True
|
||||||
|
D.vvv(
|
||||||
|
f"IAP: LOCAL PORT {self.local_port}",
|
||||||
|
host=self.host,
|
||||||
|
)
|
||||||
|
except OSError: # pty is closed
|
||||||
|
break
|
||||||
|
|
||||||
|
if self.up: # no need to monitor if already up
|
||||||
|
break
|
||||||
|
|
||||||
|
if not self.ready.is_set():
|
||||||
|
self.ready.set()
|
||||||
|
|
||||||
|
os.close(self.master_fd)
|
||||||
|
|
||||||
|
def terminate(self) -> None:
|
||||||
|
"""Gracefully terminate the IAP subprocess"""
|
||||||
|
|
||||||
|
D.vvv("IAP: STOPPING TUNNEL", host=self.host)
|
||||||
|
if self.process is not None and self.process.poll() is None:
|
||||||
|
try:
|
||||||
|
self.process.terminate()
|
||||||
|
self.process.wait(timeout=5) # wait up to 5 seconds to terminate IAP
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
self.process.kill()
|
||||||
|
|
||||||
|
D.vvvvv("terminated/killed IAP", host=self.host)
|
||||||
|
|
||||||
|
if self.thread is not None and self.thread.is_alive():
|
||||||
|
self.thread.join(timeout=1) # joining thread back should be quick
|
||||||
|
|
||||||
|
|
||||||
|
class Connection(sshconn.Connection):
|
||||||
|
"""
|
||||||
|
This is pretty much the same as the upstream ssh plugin, just overloads
|
||||||
|
the connection handling to start/stop the IAP tunnel with gcloud as appropriate
|
||||||
|
"""
|
||||||
|
|
||||||
|
iaps: dict[str, IAP] = {}
|
||||||
|
lock: threading.Lock = threading.Lock()
|
||||||
|
|
||||||
|
gcloud_executable: T.Optional[str] = None
|
||||||
|
ssh_config: str
|
||||||
|
|
||||||
|
transport = "gcloud-iap" # type: ignore[override]
|
||||||
|
|
||||||
|
def __init__(self, *args: T.Any, **kwargs: T.Any) -> None:
|
||||||
|
|
||||||
|
super(Connection, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# If the gcloud binary isn't found/configured, bail out immediately
|
||||||
|
exec: T.Optional[str] = self.get_option("gcloud_executable")
|
||||||
|
if exec is None:
|
||||||
|
self.gcloud_executable = DEFAULT_GCLOUD
|
||||||
|
else:
|
||||||
|
self.gcloud_executable = exec
|
||||||
|
|
||||||
|
if self.gcloud_executable is None:
|
||||||
|
raise errors.AnsiblePluginError(
|
||||||
|
"Plugin Error: no gcloud binary found in $PATH and "
|
||||||
|
"no executable defined in ansible config"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _connect(self) -> Connection:
|
||||||
|
"""Upstream ssh is empty, overload with the stuff starting the IAP tunnel"""
|
||||||
|
|
||||||
|
host: T.Optional[str] = self.get_option("host")
|
||||||
|
project: T.Optional[str] = self.get_option("gcloud_project")
|
||||||
|
account: T.Optional[str] = self.get_option("gcloud_account")
|
||||||
|
zone: T.Optional[str] = self.get_option("gcloud_zone")
|
||||||
|
token_file: T.Optional[str] = self.get_option("gcloud_access_token_file")
|
||||||
|
config: T.Optional[str] = self.get_option("gcloud_configuration")
|
||||||
|
port: T.Optional[int] = self.get_option("port")
|
||||||
|
timeout: T.Optional[int] = self.get_option("timeout")
|
||||||
|
|
||||||
|
# this shouldn't happen, but still.
|
||||||
|
if host is None:
|
||||||
|
raise errors.AnsibleAssertionError("No host defined")
|
||||||
|
|
||||||
|
with self.lock:
|
||||||
|
if host not in self.iaps:
|
||||||
|
self.iaps[host] = IAP(
|
||||||
|
str(self.gcloud_executable),
|
||||||
|
host=host,
|
||||||
|
remote_port=int(port or DEFAULT_SSH_PORT),
|
||||||
|
project=project,
|
||||||
|
zone=zone,
|
||||||
|
account=account,
|
||||||
|
config=config,
|
||||||
|
token_file=token_file,
|
||||||
|
)
|
||||||
|
|
||||||
|
success = self.iaps[host].ready.wait(timeout=timeout)
|
||||||
|
is_up: bool = False
|
||||||
|
for _ in range(3): # pylint: disable=disallowed-name
|
||||||
|
is_up = self.iaps[host].up
|
||||||
|
if success and is_up:
|
||||||
|
D.vvv("IAP: TUNNEL IS UP", host=host)
|
||||||
|
is_up = True
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
if not is_up:
|
||||||
|
D.vvv("IAP: TUNNEL FAILURE", host=host)
|
||||||
|
for line in self.iaps[host].output:
|
||||||
|
D.vvvvv(line, host=host)
|
||||||
|
raise errors.AnsibleRuntimeError("Failure when starting IAP tunnel")
|
||||||
|
|
||||||
|
# override port with the random IAP port
|
||||||
|
self.set_option("port", self.iaps[host].local_port)
|
||||||
|
|
||||||
|
# read path to the supplied known hosts file
|
||||||
|
ukhf: str = ospath.abspath(
|
||||||
|
ospath.expanduser(str(self.get_option("known_hosts_file")))
|
||||||
|
)
|
||||||
|
# have to trick SSH to connect to localhost instead of the instances
|
||||||
|
fd, self.ssh_config = tempfile.mkstemp(
|
||||||
|
suffix="ssh_config", prefix="ansible_gcloud", text=True
|
||||||
|
)
|
||||||
|
with open(fd, "w") as fp:
|
||||||
|
fp.write("Host *\n")
|
||||||
|
fp.write(" HostName localhost\n") # trick
|
||||||
|
fp.write(" HostKeyAlias {}\n".format(host)) # avoid multiple entries
|
||||||
|
fp.write(" UserKnownHostsFile {}\n".format(ukhf)) # as defined in opts
|
||||||
|
|
||||||
|
# prepend our generated ssh config to all ssh_args if not already present
|
||||||
|
if self.ssh_config not in str(self.get_option("ssh_args")):
|
||||||
|
self.set_option(
|
||||||
|
"ssh_args", f"-F {self.ssh_config} " + str(self.get_option("ssh_args"))
|
||||||
|
)
|
||||||
|
|
||||||
|
self._connected = True
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
"""
|
||||||
|
Upstream only marks the connection as closed, we have to terminate
|
||||||
|
all IAP tunnels as well
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Terminate IAP
|
||||||
|
with self.lock:
|
||||||
|
for iap in self.iaps.values():
|
||||||
|
iap.terminate()
|
||||||
|
self.iaps.clear()
|
||||||
|
|
||||||
|
# remove ssh config
|
||||||
|
os.unlink(self.ssh_config)
|
||||||
|
|
||||||
|
self._connected = False
|
1
tests/integration/targets/connection_plugin/aliases
Normal file
1
tests/integration/targets/connection_plugin/aliases
Normal file
|
@ -0,0 +1 @@
|
||||||
|
cloud/gcp
|
|
@ -0,0 +1,66 @@
|
||||||
|
---
|
||||||
|
- name: Setup test suite
|
||||||
|
hosts: localhost
|
||||||
|
connection: local
|
||||||
|
gather_facts: false
|
||||||
|
vars_files:
|
||||||
|
- ../vars.yml
|
||||||
|
environment:
|
||||||
|
GCP_SERVICE_ACCOUNT_FILE: "{{ gcp_cred_file }}"
|
||||||
|
GCP_AUTH_KIND: "{{ gcp_cred_kind }}"
|
||||||
|
GCP_PROJECT: "{{ gcp_project }}"
|
||||||
|
tasks:
|
||||||
|
- name: SETUP | Create SSH key pair
|
||||||
|
community.crypto.openssh_keypair:
|
||||||
|
path: "{{ ansible_ssh_private_key_file }}"
|
||||||
|
type: ed25519
|
||||||
|
register: _keypair
|
||||||
|
|
||||||
|
- name: SETUP | Create network
|
||||||
|
google.cloud.gcp_compute_network:
|
||||||
|
name: "{{ prefix }}"
|
||||||
|
auto_create_subnetworks: true
|
||||||
|
state: present
|
||||||
|
register: _network
|
||||||
|
|
||||||
|
- name: SETUP | Allow SSH through IAP
|
||||||
|
google.cloud.gcp_compute_firewall:
|
||||||
|
name: all-iap
|
||||||
|
state: present
|
||||||
|
source_ranges:
|
||||||
|
- 35.235.240.0/20
|
||||||
|
allowed:
|
||||||
|
- ip_protocol: tcp
|
||||||
|
ports:
|
||||||
|
- 22
|
||||||
|
network: "{{ _network }}"
|
||||||
|
|
||||||
|
- name: SETUP | Create instances
|
||||||
|
google.cloud.gcp_compute_instance:
|
||||||
|
name: "{{ prefix }}-{{ item.name }}"
|
||||||
|
machine_type: "{{ gcp_machine_type }}"
|
||||||
|
disks:
|
||||||
|
- auto_delete: true
|
||||||
|
boot: true
|
||||||
|
initialize_params:
|
||||||
|
source_image: "{{ gcp_disk_image }}"
|
||||||
|
disk_type: pd-standard
|
||||||
|
network_interfaces:
|
||||||
|
- network: "{{ _network }}"
|
||||||
|
metadata:
|
||||||
|
ssh-keys: "{{ ansible_ssh_user }}:{{ _keypair.public_key }}"
|
||||||
|
labels: "{{ item.labels | default({}) }}"
|
||||||
|
hostname: "{{ item.hostname | default(omit) }}"
|
||||||
|
zone: "{{ gcp_zone }}"
|
||||||
|
state: present
|
||||||
|
loop: "{{ sut }}"
|
||||||
|
|
||||||
|
- name: SETUP | Render dynamic inventory file
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: ../test.gcp_compute.yml
|
||||||
|
content: "{{ lookup('template', '../templates/inventory.yml.j2') }}"
|
||||||
|
mode: preserve
|
||||||
|
|
||||||
|
- name: SETUP | Give time for instances to be up
|
||||||
|
ansible.builtin.pause:
|
||||||
|
seconds: 30
|
|
@ -0,0 +1,34 @@
|
||||||
|
---
|
||||||
|
- name: Teardown test suite
|
||||||
|
hosts: localhost
|
||||||
|
connection: local
|
||||||
|
gather_facts: false
|
||||||
|
vars_files:
|
||||||
|
- ../vars.yml
|
||||||
|
environment:
|
||||||
|
GCP_SERVICE_ACCOUNT_FILE: "{{ gcp_cred_file }}"
|
||||||
|
GCP_AUTH_KIND: "{{ gcp_cred_kind }}"
|
||||||
|
GCP_PROJECT: "{{ gcp_project }}"
|
||||||
|
tasks:
|
||||||
|
- name: TEARDOWN | Destroy instances # noqa: ignore-errors
|
||||||
|
google.cloud.gcp_compute_instance:
|
||||||
|
name: "{{ prefix }}-{{ item.name }}"
|
||||||
|
machine_type: "{{ gcp_machine_type }}"
|
||||||
|
zone: "{{ gcp_zone }}"
|
||||||
|
state: absent
|
||||||
|
loop: "{{ sut }}"
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: TEARDOWN | Remove IAP firewall rule # noqa: ignore-errors
|
||||||
|
google.cloud.gcp_compute_firewall:
|
||||||
|
name: all-iap
|
||||||
|
state: absent
|
||||||
|
network:
|
||||||
|
selfLink: "networks/{{ prefix }}"
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: TEARDOWN | Destroy network # noqa: ignore-errors
|
||||||
|
google.cloud.gcp_compute_network:
|
||||||
|
name: "{{ prefix }}"
|
||||||
|
state: absent
|
||||||
|
ignore_errors: true
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
- name: Test IAP connection plugin
|
||||||
|
hosts: gcp_cluster_web:gcp_cluster_db
|
||||||
|
connection: google.cloud.iap
|
||||||
|
gather_facts: false
|
||||||
|
vars_files:
|
||||||
|
- ../vars.yml
|
||||||
|
tasks:
|
||||||
|
- name: TEST | Ping
|
||||||
|
ansible.builtin.ping:
|
||||||
|
|
||||||
|
- name: TEST | Copy
|
||||||
|
ansible.builtin.copy:
|
||||||
|
content: "Test file test"
|
||||||
|
dest: "/tmp/{{ prefix }}.txt"
|
||||||
|
mode: "0644"
|
||||||
|
|
||||||
|
- name: TEST | Slurp
|
||||||
|
ansible.builtin.slurp:
|
||||||
|
src: "/tmp/{{ prefix }}.txt"
|
||||||
|
register: _content
|
||||||
|
|
||||||
|
- name: TEST | Debug
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "{{ _content['content'] | b64decode }}"
|
25
tests/integration/targets/connection_plugin/runme.sh
Executable file
25
tests/integration/targets/connection_plugin/runme.sh
Executable file
|
@ -0,0 +1,25 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -eux
|
||||||
|
|
||||||
|
# test infra
|
||||||
|
ansible-playbook playbooks/setup.yml "$@"
|
||||||
|
|
||||||
|
export ANSIBLE_INVENTORY=test.gcp_compute.yml
|
||||||
|
|
||||||
|
ansible-inventory --graph
|
||||||
|
|
||||||
|
RC=0
|
||||||
|
# we want to run teardown regardless of playbook exit status, so catch the
|
||||||
|
# exit code of ansible-playbook manually
|
||||||
|
set +e
|
||||||
|
ansible-playbook -vvvvv playbooks/test.yml "$@"
|
||||||
|
RC=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
unset ANSIBLE_INVENTORY
|
||||||
|
|
||||||
|
# delete test infra
|
||||||
|
ansible-playbook playbooks/teardown.yml "$@"
|
||||||
|
|
||||||
|
exit $RC
|
|
@ -0,0 +1,31 @@
|
||||||
|
---
|
||||||
|
plugin: google.cloud.gcp_compute
|
||||||
|
|
||||||
|
zones:
|
||||||
|
{{ gcp_zones | to_nice_yaml }}
|
||||||
|
|
||||||
|
projects:
|
||||||
|
- {{ gcp_project }}
|
||||||
|
|
||||||
|
auth_kind: {{ gcp_cred_kind }}
|
||||||
|
|
||||||
|
service_account_file: {{ gcp_cred_file }}
|
||||||
|
|
||||||
|
scopes:
|
||||||
|
- 'https://www.googleapis.com/auth/cloud-platform'
|
||||||
|
- 'https://www.googleapis.com/auth/compute.readonly'
|
||||||
|
|
||||||
|
keyed_groups:
|
||||||
|
- prefix: gcp
|
||||||
|
key: labels
|
||||||
|
|
||||||
|
filters:
|
||||||
|
- 'labels.test:{{ prefix }}'
|
||||||
|
|
||||||
|
hostnames:
|
||||||
|
- name
|
||||||
|
|
||||||
|
# set variables for the connection plugin
|
||||||
|
compose:
|
||||||
|
ansible_gcloud_zone: zone
|
||||||
|
ansible_gcloud_project: project
|
|
@ -0,0 +1 @@
|
||||||
|
# placeholder
|
32
tests/integration/targets/connection_plugin/vars.yml
Normal file
32
tests/integration/targets/connection_plugin/vars.yml
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
---
|
||||||
|
prefix: "{{ resource_prefix | default('d3adb33f') }}"
|
||||||
|
|
||||||
|
gcp_region: us-central1
|
||||||
|
gcp_zones:
|
||||||
|
- "{{ gcp_region }}-a"
|
||||||
|
- "{{ gcp_region }}-b"
|
||||||
|
- "{{ gcp_region }}-c"
|
||||||
|
- "{{ gcp_region }}-f"
|
||||||
|
gcp_zone: "{{ gcp_zones | last }}"
|
||||||
|
gcp_disk_image: projects/centos-cloud/global/images/family/centos-stream-9
|
||||||
|
gcp_machine_type: g1-small
|
||||||
|
|
||||||
|
sut:
|
||||||
|
- name: vm1
|
||||||
|
labels:
|
||||||
|
test: "{{ prefix }}"
|
||||||
|
cluster: web
|
||||||
|
- name: vm2
|
||||||
|
labels:
|
||||||
|
test: "{{ prefix }}"
|
||||||
|
cluster: web
|
||||||
|
- name: vm3
|
||||||
|
labels:
|
||||||
|
test: "{{ prefix }}"
|
||||||
|
cluster: db
|
||||||
|
|
||||||
|
ansible_python_interpreter: /usr/bin/python3
|
||||||
|
|
||||||
|
# these are only useful when connection != local
|
||||||
|
ansible_ssh_user: cloud-user
|
||||||
|
ansible_ssh_private_key_file: "{{ playbook_dir }}/ssh_key"
|
|
@ -6,7 +6,7 @@ gcp_zones:
|
||||||
- "{{ gcp_region }}-c"
|
- "{{ gcp_region }}-c"
|
||||||
- "{{ gcp_region }}-f"
|
- "{{ gcp_region }}-f"
|
||||||
gcp_zone: "{{ gcp_zones | first }}"
|
gcp_zone: "{{ gcp_zones | first }}"
|
||||||
gcp_disk_image: projects/centos-cloud/global/images/centos-stream-9-v20250513
|
gcp_disk_image: projects/centos-cloud/global/images/family/centos-stream-9
|
||||||
|
|
||||||
prefix: "{{ resource_prefix | default('d3adb33f') }}"
|
prefix: "{{ resource_prefix | default('d3adb33f') }}"
|
||||||
sut:
|
sut:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue