Skip to content

Commit e4288c7

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 e4288c7

File tree

7 files changed

+278
-21
lines changed

7 files changed

+278
-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

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: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
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 ansible-runner run $TEST_DIR --ident result --playbook hello_world_vars_file.yml --project-dir $DATA_DIR
78+
run exec_ansible_runner_cli hello_world_vars_file.yml
79+
assert_success
80+
assert_output --partial '"msg": "Hello World! vars_file_1=vars_file_1_value, vars_file_2=vars_file_2_value"'
81+
}
82+
83+
@test "[ansible-runner] runs a playbook with vault encrypted variables" {
84+
echo -n "vault" >> $VAULT_FILE
85+
ANSIBLE_VAULT_PASSWORD_FILE=$VAULT_FILE run exec_ansible_runner_cli hello_world_vault_encrypted_vars.yml
86+
assert_success
87+
assert_output --partial '"msg": "Hello World! (NOTE: This message has been encrypted with ansible-vault)"'
88+
}
89+
90+
@test "[ansible-runner] runs a playbook with variables in a vault encrypted vars file" {
91+
echo -n "vault" >> $VAULT_FILE
92+
ANSIBLE_VAULT_PASSWORD_FILE=$VAULT_FILE run exec_ansible_runner_cli hello_world_vault_encrypted_vars_file.yml
93+
assert_success
94+
assert_output --partial '"msg": "Hello World! vars_file_1=vars_file_1_value, vars_file_2=vars_file_2_value"'
95+
}
96+
97+
@test "[ansible-runner] runs a playbook using roles from github" {
98+
setup_roles_dir $DATA_DIR/hello_world_with_requirements_github/roles
99+
100+
run exec_ansible_runner_cli hello_world_with_requirements_github/hello_world_with_requirements_github.yml
101+
assert_success
102+
assert_output --partial '"msg": "Hello World! example_var='\''example var value'\''"'
103+
}
104+
105+
@test "[ansible-runner] runs a role" {
106+
setup_roles_dir $ROLES_DIR $DATA_DIR/hello_world_with_requirements_github/roles/requirements.yml
107+
108+
run exec_ansible_runner_cli_role manageiq.example
109+
assert_success
110+
assert_output --partial '"msg": "Hello from manageiq.example role! example_var='\''example var value'\''"'
111+
}
112+
113+
@test "[ansible-runner] vmware collection" {
114+
if [ ! -d /var/lib/manageiq/venv ]; then
115+
skip "manageiq venv collections are not present"
116+
fi
117+
118+
run exec_ansible_runner_cli vmware.yml
119+
assert_failure # We expect to this to fail due to connecting to an unknown vcenter
120+
assert_output --partial '"msg": "Unknown error while connecting to vCenter or ESXi API at vcenter_hostname:443 : [Errno -2] Name or service not known"'
121+
}
122+
123+
@test "[ansible-runner] aws collection" {
124+
if [ ! -d /var/lib/manageiq/venv ]; then
125+
skip "manageiq venv collections are not present"
126+
fi
127+
128+
run exec_ansible_runner_cli aws.yml
129+
assert_failure # We expect to this to fail due to connecting with bad creds
130+
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"'
131+
}
132+
133+
################################################################################
134+
135+
exec_ansible_runner() {
136+
rails runner "resp = Ansible::Runner.run({}, {}, '$DATA_DIR/$1'); puts resp.human_stdout; exit resp.return_code"
137+
}
138+
139+
exec_ansible_runner_role() {
140+
rails runner "resp = Ansible::Runner.run_role({}, {}, '$1', roles_path: '$ROLES_DIR'); puts resp.human_stdout; exit resp.return_code"
141+
}
142+
143+
@test "[Ansible::Runner] runs a playbook" {
144+
run exec_ansible_runner hello_world.yml
145+
assert_success
146+
assert_output --partial '"msg": "Hello World!"'
147+
}
148+
149+
@test "[Ansible::Runner] runs a playbook with variables in a vars file" {
150+
run exec_ansible_runner hello_world_vars_file.yml
151+
assert_success
152+
assert_output --partial '"msg": "Hello World! vars_file_1=vars_file_1_value, vars_file_2=vars_file_2_value"'
153+
}
154+
155+
@test "[Ansible::Runner] runs a playbook with vault encrypted variables" {
156+
skip "requires database access"
157+
158+
run exec_ansible_runner hello_world_vault_encrypted_vars.yml
159+
assert_success
160+
assert_output --partial '"msg": "Hello World! (NOTE: This message has been encrypted with ansible-vault)"'
161+
}
162+
163+
@test "[Ansible::Runner] runs a playbook with variables in a vault encrypted vars file" {
164+
skip "requires database access"
165+
166+
run exec_ansible_runner hello_world_vault_encrypted_vars_file.yml
167+
assert_success
168+
assert_output --partial '"msg": "Hello World! vars_file_1=vars_file_1_value, vars_file_2=vars_file_2_value"'
169+
}
170+
171+
@test "[Ansible::Runner] runs a playbook using roles from github" {
172+
run exec_ansible_runner hello_world_with_requirements_github/hello_world_with_requirements_github.yml
173+
assert_success
174+
assert_output --partial '"msg": "Hello World! example_var='\''example var value'\''"'
175+
}
176+
177+
@test "[Ansible::Runner] runs a role" {
178+
setup_roles_dir $ROLES_DIR $DATA_DIR/hello_world_with_requirements_github/roles/requirements.yml
179+
180+
run exec_ansible_runner_role manageiq.example
181+
assert_success
182+
assert_output --partial '"msg": "Hello from manageiq.example role! example_var='\''example var value'\''"'
183+
}
184+
185+
@test "[Ansible::Runner] vmware collection" {
186+
if [ ! -d /var/lib/manageiq/venv ]; then
187+
skip "manageiq venv collections are not present"
188+
fi
189+
190+
run exec_ansible_runner vmware.yml
191+
assert_failure # We expect to this to fail due to connecting to an unknown vcenter
192+
assert_output --partial '"msg": "Unknown error while connecting to vCenter or ESXi API at vcenter_hostname:443 : [Errno -2] Name or service not known"'
193+
}
194+
195+
@test "[Ansible::Runner] aws collection" {
196+
if [ ! -d /var/lib/manageiq/venv ]; then
197+
skip "manageiq venv collections are not present"
198+
fi
199+
200+
run exec_ansible_runner aws.yml
201+
assert_failure # We expect to this to fail due to connecting with bad creds
202+
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"'
203+
}

0 commit comments

Comments
 (0)