Skip to content

Commit 2636165

Browse files
committed
Add appliance/container tests for ansible-runner
This commit introduces Bats tests for ansible-runner to allow testing the manageiq-ansible-venv installation. This tests cover ansible-runner CLI tests as well as tests running through the Ansible::Runner Ruby class, even without database access. This commit also includes changes to Ansible::Runner itself to allow for better detection of ansible and the manageiq-ansible-venv, allowing the tests to run on the old appliances with python3.9 as well as the new appliances with python3.12.
1 parent 9997c45 commit 2636165

File tree

7 files changed

+277
-21
lines changed

7 files changed

+277
-21
lines changed

config/brakeman.ignore

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,28 @@
11
{
22
"ignored_warnings": [
3+
{
4+
"warning_type": "Command Injection",
5+
"warning_code": 14,
6+
"fingerprint": "7ead8455ac1333e286f0ffe378b2ecff77f3abe3e942866471d598851e0ef89f",
7+
"check_name": "Execute",
8+
"message": "Possible command injection",
9+
"file": "lib/ansible/runner.rb",
10+
"line": 452,
11+
"link": "https://brakemanscanner.org/docs/warning_types/command_injection/",
12+
"code": "`#{(File.join(venv_bin_path, \"ansible\") or \"ansible\")} --version 2>/dev/null`",
13+
"render_path": null,
14+
"location": {
15+
"type": "method",
16+
"class": "Ansible::Runner",
17+
"method": "s(:self).ansible_python_version_raw"
18+
},
19+
"user_input": "File.join(venv_bin_path, \"ansible\")",
20+
"confidence": "Medium",
21+
"cwe_id": [
22+
77
23+
],
24+
"note": "This method is safe because it only uses hardcoded paths, which cannot be changed by user input."
25+
},
326
{
427
"warning_type": "Unmaintained Dependency",
528
"warning_code": 121,
@@ -22,18 +45,18 @@
2245
{
2346
"warning_type": "Command Injection",
2447
"warning_code": 14,
25-
"fingerprint": "9a58ac820e59b1edb4530e27646edc1f328915a7a356d987397659b48c52239e",
48+
"fingerprint": "a8efd255cf411737c44782259d6c4eff8e56c29a0249272a7ad317c2a5a2e816",
2649
"check_name": "Execute",
2750
"message": "Possible command injection",
2851
"file": "lib/ansible/runner.rb",
29-
"line": 430,
52+
"line": 446,
3053
"link": "https://brakemanscanner.org/docs/warning_types/command_injection/",
3154
"code": "`python#{version} -c 'import site; print(\":\".join(site.getsitepackages()))'`",
3255
"render_path": null,
3356
"location": {
3457
"type": "method",
3558
"class": "Ansible::Runner",
36-
"method": "s(:self).ansible_python_paths_raw"
59+
"method": "s(:self).ansible_python_path_raw"
3760
},
3861
"user_input": "version",
3962
"confidence": "Medium",
@@ -43,6 +66,6 @@
4366
"note": "This method is safe because it verifies that the version is in the form #.#."
4467
}
4568
],
46-
"updated": "2025-03-31 10:21:49 -0400",
69+
"updated": "2025-06-13 18:54:18 -0400",
4770
"brakeman_version": "6.2.2"
4871
}

lib/ansible/content.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ def initialize(path)
88
@path = Pathname.new(path)
99
end
1010

11-
def fetch_galaxy_roles
11+
def fetch_galaxy_roles(env = {})
1212
return true unless requirements_file.exist?
1313

1414
require "awesome_spawn"
15-
AwesomeSpawn.run!("ansible-galaxy", :params => ["install", {:roles_path= => roles_dir, :role_file= => requirements_file}])
15+
AwesomeSpawn.run!("ansible-galaxy", :env => env, :params => ["install", {:roles_path= => roles_dir, :role_file= => requirements_file}])
1616
end
1717

1818
def self.fetch_plugin_galaxy_roles

lib/ansible/runner.rb

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,8 @@ def run_via_cli(hosts, credentials, env_vars, extra_vars, tags: nil, ansible_run
221221

222222
params = runner_params(base_dir, ansible_runner_method, playbook_or_role_args, verbosity)
223223

224+
# puts "#{env_vars_hash.map { |k, v| "#{k}=#{v}" }.join(" ")} #{AwesomeSpawn.build_command_line("ansible-runner", params)}"
225+
224226
begin
225227
fetch_galaxy_roles(playbook_or_role_args)
226228

@@ -317,11 +319,11 @@ def fetch_galaxy_roles(playbook_or_role_args)
317319
return unless playbook_or_role_args[:playbook]
318320

319321
playbook_dir = File.dirname(playbook_or_role_args[:playbook])
320-
Ansible::Content.new(playbook_dir).fetch_galaxy_roles
322+
Ansible::Content.new(playbook_dir).fetch_galaxy_roles(runner_env)
321323
end
322324

323325
def runner_env
324-
{"PYTHONPATH" => python_path}.delete_nils
326+
{"PYTHONPATH" => runner_python_path, "PATH" => runner_path}.compact
325327
end
326328

327329
def credentials_info(credentials, base_dir)
@@ -408,20 +410,34 @@ def wait_for_listener_start(listener)
408410
end
409411
end
410412

411-
def python_path
412-
@python_path ||= [manageiq_venv_path, *ansible_python_paths].compact.join(File::PATH_SEPARATOR)
413+
def runner_python_path
414+
@runner_python_path ||= [venv_python_path, ansible_python_path].compact.join(File::PATH_SEPARATOR)
415+
end
416+
417+
def runner_path
418+
@runner_path ||= [venv_bin_path, ENV["PATH"].presence].compact.join(File::PATH_SEPARATOR)
419+
end
420+
421+
VENV_ROOT = "/var/lib/manageiq/venv".freeze
422+
423+
def venv_python_path
424+
Dir.glob(File.join(VENV_ROOT, "lib/python*/site-packages")).first
413425
end
414426

415-
def manageiq_venv_path
416-
Dir.glob("/var/lib/manageiq/venv/lib/python*/site-packages").first
427+
def venv_bin_path
428+
Dir.glob(File.join(VENV_ROOT, "bin")).first
417429
end
418430

419-
def ansible_python_paths
420-
ansible_python_paths_raw(ansible_python_version).chomp.split(":")
431+
def ansible_python_path
432+
ansible_python_path_raw(ansible_python_version).presence
433+
end
434+
435+
def ansible_python_version
436+
ansible_python_version_raw.match(/python version = (\d+\.\d+)\./)&.captures&.first
421437
end
422438

423439
# NOTE: This method is ignored by brakeman in the config/brakeman.ignore
424-
def ansible_python_paths_raw(version)
440+
def ansible_python_path_raw(version)
425441
return "" if version.blank?
426442

427443
# This check allows us to ignore the brakeman warning about command line injection
@@ -430,12 +446,10 @@ def ansible_python_paths_raw(version)
430446
`python#{version} -c 'import site; print(":".join(site.getsitepackages()))'`.chomp
431447
end
432448

433-
def ansible_python_version
434-
ansible_python_version_raw.match(/python version = (\d+\.\d+)\./)&.captures&.first
435-
end
436-
449+
# NOTE: This method is ignored by brakeman in the config/brakeman.ignore
437450
def ansible_python_version_raw
438-
`ansible --version 2>/dev/null`.chomp
451+
ansible = venv_bin_path ? File.join(venv_bin_path, "ansible") : "ansible"
452+
`#{ansible} --version 2>/dev/null`.chomp
439453
end
440454
end
441455
end

lib/tasks/test_security_helper.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def self.brakeman(format: "human")
2323
app_path = Rails.root.to_s
2424
engine_paths = Vmdb::Plugins.paths.except(ManageIQ::Schema::Engine).values
2525

26-
puts "** Running brakeman in #{app_path}"
26+
puts "** Running brakeman in #{app_path}#{" (interactive ignore)" if interactive_ignore}"
2727
puts "** engines:"
2828
puts "** - #{engine_paths.join("\n** - ")}"
2929

spec/lib/ansible/runner/data/aws.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
- name: Test aws collection
2+
hosts: localhost
3+
tasks:
4+
- name: Connect to aws
5+
amazon.aws.ec2_instance_info:
6+
access_key: 'access_key'
7+
secret_key: 'secret_key'
8+
region: us-east-1
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
- name: Test vmware collection
2+
hosts: localhost
3+
tasks:
4+
- name: Connect to vcenter
5+
community.vmware.vmware_vm_info:
6+
hostname: 'vcenter_hostname'
7+
username: 'vcenter_username'
8+
password: 'vcenter_password'
9+
validate_certs: false
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
#/usr/bin/env bats
2+
3+
# This test file is for testing ansible-runner on a production appliance to verify
4+
# that the real installation is working as expected. It is a duplicate of the tests
5+
# in runner_execution_spec.rb but without the rspec and rspec-rails overhead.
6+
#
7+
# This test requires the Bats test framework to be installed
8+
# macOS: brew install bats-core
9+
# appliance: dnf install bats
10+
# as well as the bats-support and bats-assert plugins installed
11+
# git clone https://github.com/bats-core/bats-support ~/.bats/libs/bats-support
12+
# git clone https://github.com/bats-core/bats-assert ~/.bats/libs/bats-assert
13+
14+
setup_file() {
15+
export BATS_LIB_PATH="$HOME/.bats/libs:$BATS_LIB_PATH"
16+
17+
export PYTHON_VERSION="3.12"
18+
19+
export SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")" >/dev/null 2>&1 && pwd)"
20+
export DATA_DIR="$SCRIPT_DIR/runner/data"
21+
export TEST_DIR="/tmp/ansible-runner-test"
22+
export ROLES_DIR="/tmp/ansible-runner-test-roles"
23+
export VAULT_FILE="$TEST_DIR/vault_password"
24+
}
25+
26+
setup() {
27+
bats_load_library 'bats-support'
28+
bats_load_library 'bats-assert'
29+
30+
rm -rf $TEST_DIR
31+
rm -rf $ROLES_DIR
32+
33+
mkdir -p $TEST_DIR
34+
}
35+
36+
teardown() {
37+
rm -rf $DATA_DIR/hello_world_with_requirements_github/roles/manageiq.example
38+
}
39+
40+
setup_roles_dir() {
41+
# In prod builds, ansible-galaxy lives in the venv, so set up the PATH temporarily to install the roles
42+
PATH="/var/lib/manageiq/venv/bin:$PATH"
43+
44+
roles_path="$1"
45+
source_role_file="${2:-$1/requirements.yml}"
46+
role_file="$roles_path/requirements.yml"
47+
if [ "$source_role_file" != "$role_file" ]; then
48+
mkdir -p $roles_path
49+
cp $source_role_file $role_file
50+
fi
51+
ansible-galaxy install --roles-path=$roles_path --role-file=$role_file
52+
53+
PATH="${PATH#*:}"
54+
}
55+
56+
################################################################################
57+
58+
exec_ansible_runner_cli() {
59+
PATH="/var/lib/manageiq/venv/bin:$PATH" \
60+
PYTHONPATH="/var/lib/manageiq/venv/lib/python${PYTHON_VERSION}/site-packages:/usr/local/lib64/python${PYTHON_VERSION}/site-packages:/usr/local/lib/python${PYTHON_VERSION}/site-packages:/usr/lib64/python${PYTHON_VERSION}/site-packages:/usr/lib/python${PYTHON_VERSION}/site-packages" \
61+
ansible-runner run $TEST_DIR --ident result --playbook $1 --project-dir $DATA_DIR
62+
}
63+
64+
exec_ansible_runner_cli_role() {
65+
PATH="/var/lib/manageiq/venv/bin:$PATH" \
66+
PYTHONPATH="/var/lib/manageiq/venv/lib/python${PYTHON_VERSION}/site-packages:/usr/local/lib64/python${PYTHON_VERSION}/site-packages:/usr/local/lib/python${PYTHON_VERSION}/site-packages:/usr/lib64/python${PYTHON_VERSION}/site-packages:/usr/lib/python${PYTHON_VERSION}/site-packages" \
67+
ansible-runner run $TEST_DIR --ident result --role $1 --roles-path $ROLES_DIR --role-skip-facts --hosts localhost
68+
}
69+
70+
@test "[ansible-runner] runs a playbook" {
71+
run exec_ansible_runner_cli hello_world.yml
72+
assert_success
73+
assert_output --partial '"msg": "Hello World!"'
74+
}
75+
76+
@test "[ansible-runner] runs a playbook with variables in a vars file" {
77+
run exec_ansible_runner_cli hello_world_vars_file.yml
78+
assert_success
79+
assert_output --partial '"msg": "Hello World! vars_file_1=vars_file_1_value, vars_file_2=vars_file_2_value"'
80+
}
81+
82+
@test "[ansible-runner] runs a playbook with vault encrypted variables" {
83+
echo -n "vault" >> $VAULT_FILE
84+
ANSIBLE_VAULT_PASSWORD_FILE=$VAULT_FILE run exec_ansible_runner_cli hello_world_vault_encrypted_vars.yml
85+
assert_success
86+
assert_output --partial '"msg": "Hello World! (NOTE: This message has been encrypted with ansible-vault)"'
87+
}
88+
89+
@test "[ansible-runner] runs a playbook with variables in a vault encrypted vars file" {
90+
echo -n "vault" >> $VAULT_FILE
91+
ANSIBLE_VAULT_PASSWORD_FILE=$VAULT_FILE run exec_ansible_runner_cli hello_world_vault_encrypted_vars_file.yml
92+
assert_success
93+
assert_output --partial '"msg": "Hello World! vars_file_1=vars_file_1_value, vars_file_2=vars_file_2_value"'
94+
}
95+
96+
@test "[ansible-runner] runs a playbook using roles from github" {
97+
setup_roles_dir $DATA_DIR/hello_world_with_requirements_github/roles
98+
99+
run exec_ansible_runner_cli hello_world_with_requirements_github/hello_world_with_requirements_github.yml
100+
assert_success
101+
assert_output --partial '"msg": "Hello World! example_var='\''example var value'\''"'
102+
}
103+
104+
@test "[ansible-runner] runs a role" {
105+
setup_roles_dir $ROLES_DIR $DATA_DIR/hello_world_with_requirements_github/roles/requirements.yml
106+
107+
run exec_ansible_runner_cli_role manageiq.example
108+
assert_success
109+
assert_output --partial '"msg": "Hello from manageiq.example role! example_var='\''example var value'\''"'
110+
}
111+
112+
@test "[ansible-runner] vmware collection" {
113+
if [ ! -d /var/lib/manageiq/venv ]; then
114+
skip "manageiq venv collections are not present"
115+
fi
116+
117+
run exec_ansible_runner_cli vmware.yml
118+
assert_failure # We expect to this to fail due to connecting to an unknown vcenter
119+
assert_output --partial '"msg": "Unknown error while connecting to vCenter or ESXi API at vcenter_hostname:443 : [Errno -2] Name or service not known"'
120+
}
121+
122+
@test "[ansible-runner] aws collection" {
123+
if [ ! -d /var/lib/manageiq/venv ]; then
124+
skip "manageiq venv collections are not present"
125+
fi
126+
127+
run exec_ansible_runner_cli aws.yml
128+
assert_failure # We expect to this to fail due to connecting with bad creds
129+
assert_output --partial '"msg": "Failed to describe instances: An error occurred (AuthFailure) when calling the DescribeInstances operation: AWS was not able to validate the provided access credentials"'
130+
}
131+
132+
################################################################################
133+
134+
exec_ansible_runner() {
135+
rails runner "resp = Ansible::Runner.run({}, {}, '$DATA_DIR/$1'); puts resp.human_stdout; exit resp.return_code"
136+
}
137+
138+
exec_ansible_runner_role() {
139+
rails runner "resp = Ansible::Runner.run_role({}, {}, '$1', roles_path: '$ROLES_DIR'); puts resp.human_stdout; exit resp.return_code"
140+
}
141+
142+
@test "[Ansible::Runner] runs a playbook" {
143+
run exec_ansible_runner hello_world.yml
144+
assert_success
145+
assert_output --partial '"msg": "Hello World!"'
146+
}
147+
148+
@test "[Ansible::Runner] runs a playbook with variables in a vars file" {
149+
run exec_ansible_runner hello_world_vars_file.yml
150+
assert_success
151+
assert_output --partial '"msg": "Hello World! vars_file_1=vars_file_1_value, vars_file_2=vars_file_2_value"'
152+
}
153+
154+
@test "[Ansible::Runner] runs a playbook with vault encrypted variables" {
155+
skip "requires database access"
156+
157+
run exec_ansible_runner hello_world_vault_encrypted_vars.yml
158+
assert_success
159+
assert_output --partial '"msg": "Hello World! (NOTE: This message has been encrypted with ansible-vault)"'
160+
}
161+
162+
@test "[Ansible::Runner] runs a playbook with variables in a vault encrypted vars file" {
163+
skip "requires database access"
164+
165+
run exec_ansible_runner hello_world_vault_encrypted_vars_file.yml
166+
assert_success
167+
assert_output --partial '"msg": "Hello World! vars_file_1=vars_file_1_value, vars_file_2=vars_file_2_value"'
168+
}
169+
170+
@test "[Ansible::Runner] runs a playbook using roles from github" {
171+
run exec_ansible_runner hello_world_with_requirements_github/hello_world_with_requirements_github.yml
172+
assert_success
173+
assert_output --partial '"msg": "Hello World! example_var='\''example var value'\''"'
174+
}
175+
176+
@test "[Ansible::Runner] runs a role" {
177+
setup_roles_dir $ROLES_DIR $DATA_DIR/hello_world_with_requirements_github/roles/requirements.yml
178+
179+
run exec_ansible_runner_role manageiq.example
180+
assert_success
181+
assert_output --partial '"msg": "Hello from manageiq.example role! example_var='\''example var value'\''"'
182+
}
183+
184+
@test "[Ansible::Runner] vmware collection" {
185+
if [ ! -d /var/lib/manageiq/venv ]; then
186+
skip "manageiq venv collections are not present"
187+
fi
188+
189+
run exec_ansible_runner vmware.yml
190+
assert_failure # We expect to this to fail due to connecting to an unknown vcenter
191+
assert_output --partial '"msg": "Unknown error while connecting to vCenter or ESXi API at vcenter_hostname:443 : [Errno -2] Name or service not known"'
192+
}
193+
194+
@test "[Ansible::Runner] aws collection" {
195+
if [ ! -d /var/lib/manageiq/venv ]; then
196+
skip "manageiq venv collections are not present"
197+
fi
198+
199+
run exec_ansible_runner aws.yml
200+
assert_failure # We expect to this to fail due to connecting with bad creds
201+
assert_output --partial '"msg": "Failed to describe instances: An error occurred (AuthFailure) when calling the DescribeInstances operation: AWS was not able to validate the provided access credentials"'
202+
}

0 commit comments

Comments
 (0)