Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ansible_runner] Add NetworkCredential for Ansible::Runner lib #19007

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class ManageIQ::Providers::EmbeddedAnsible::AutomationManager::NetworkCredential
# rubocop:disable Layout/AlignHash
#
# looks better to align the nested keys to the same distance, instead of
# scope just for the hash in question (which is what rubocop does.
# scope just for the hash in question (which is what rubocop does).
EXTRA_ATTRIBUTES = {
:authorize => {
:type => :boolean,
Expand Down Expand Up @@ -62,9 +62,9 @@ def self.display_name(number = 1)
def self.params_to_attributes(params)
attrs = params.dup

attrs[:auth_key] = attrs.delete(:ssh_key_data)
attrs[:auth_key_password] = attrs.delete(:ssh_key_unlock)
attrs[:become_password] = attrs.delete(:authorize_password)
attrs[:auth_key] = attrs.delete(:ssh_key_data)
attrs[:auth_key_password] = attrs.delete(:ssh_key_unlock)
attrs[:become_password] = attrs.delete(:authorize_password)

if attrs[:authorize]
attrs[:options] = { :authorize => attrs.delete(:authorize) }
Expand Down
4 changes: 4 additions & 0 deletions lib/ansible/runner/credential.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ def write_config_files

private

def initialize_password_data
File.exist?(password_file) ? YAML.load_file(password_file) : {}
end

def password_file
File.join(env_dir, "passwords")
end
Expand Down
12 changes: 7 additions & 5 deletions lib/ansible/runner/credential/machine_credential.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ def become_args
}
end

SSH_KEY = "^SSH [pP]assword:".freeze
BECOME_KEY = "^BECOME [pP]assword:".freeze
SSH_UNLOCK_KEY = "^Enter passphrase for [a-zA-Z0-9\-\/]+\/ssh_key_data:".freeze
def write_password_file
password_hash = {
"^SSH [pP]assword:" => auth.password,
"^BECOME [pP]assword:" => auth.become_password,
"^Enter passphrase for [a-zA-Z0-9\-\/]+\/ssh_key_data:" => auth.ssh_key_unlock
}.delete_blanks
password_hash = initialize_password_data
password_hash[SSH_KEY] = auth.password if auth.password
password_hash[BECOME_KEY] = auth.become_password if auth.become_password
password_hash[SSH_UNLOCK_KEY] = auth.ssh_key_unlock if auth.ssh_key_unlock

File.write(password_file, password_hash.to_yaml) if password_hash.present?
end
Expand Down
48 changes: 48 additions & 0 deletions lib/ansible/runner/credential/network_credential.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
module Ansible
class Runner
class NetworkCredential < Credential
def self.auth_type
"ManageIQ::Providers::EmbeddedAnsible::AutomationManager::NetworkCredential"
end

# Modeled off of awx codebase:
#
# https://github.com/ansible/awx/blob/1242ee2b/awx/main/tasks.py#L1432-L1443
#
def env_vars
env = {
"ANSIBLE_NET_USERNAME" => auth.userid || "",
"ANSIBLE_NET_PASSWORD" => auth.password || "",
"ANSIBLE_NET_AUTHORIZE" => auth.authorize ? "1" : "0"
}

env["ANSIBLE_NET_AUTH_PASS"] = auth.become_password || "" if auth.authorize
env["ANSIBLE_NET_SSH_KEYFILE"] = network_ssh_key_file if auth.auth_key
env
end

def write_config_files
write_password_file
write_network_ssh_key_file if auth.auth_key
end

private

SSH_UNLOCK_KEY = "^Enter passphrase for [a-zA-Z0-9\-\/]+\/ssh_key_data:".freeze
def write_password_file
password_data = initialize_password_data
password_data[SSH_UNLOCK_KEY] ||= auth.ssh_key_unlock || ""
File.write(password_file, password_data.to_yaml)
end

def write_network_ssh_key_file
File.write(network_ssh_key_file, auth.auth_key)
File.chmod(0o0400, network_ssh_key_file)
end

def network_ssh_key_file
File.join(env_dir, "network_ssh_key")
end
end
end
end
34 changes: 34 additions & 0 deletions spec/lib/ansible/runner/credential/machine_credential_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,40 @@ def password_hash

expect(password_hash["^SSH [pP]assword:"]).to eq(password)
end

context "with an existing password_file" do
let(:ssh_unlock_key) { "^Enter passphrase for [a-zA-Z0-9\-\/]+\/ssh_key_data:" }
def existing_env_password_file(data)
cred # initialize the dir
File.write(password_file, data.to_yaml)
end

it "clobbers existing ssh key unlock keys" do
existing_data = { ssh_unlock_key => "hunter2" }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ummmm... shouldn't hunter2 be ******* ? Or do I only see it because it's my password, but you see *******? 😆

expected_data = {
"^SSH [pP]assword:" => "secret",
"^BECOME [pP]assword:" => "othersecret",
ssh_unlock_key => "keypass"
}
existing_env_password_file(existing_data)
cred.write_config_files

expect(password_hash).to eq(expected_data)
end

it "appends data if not setting ssh_unlock_key" do
auth.update!(:auth_key_password => nil)
existing_data = { ssh_unlock_key => "hunter2" }
added_data = {
"^SSH [pP]assword:" => "secret",
"^BECOME [pP]assword:" => "othersecret"
}
existing_env_password_file(existing_data)
cred.write_config_files

expect(password_hash).to eq(existing_data.merge(added_data))
end
end
end
end
end
186 changes: 186 additions & 0 deletions spec/lib/ansible/runner/credential/network_credential_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
require 'ansible/runner'
require 'ansible/runner/credential'

RSpec.describe Ansible::Runner::NetworkCredential do
it ".auth_type is the correct Authentication sub-class" do
expect(described_class.auth_type).to eq("ManageIQ::Providers::EmbeddedAnsible::AutomationManager::NetworkCredential")
end

context "with a credential object" do
around do |example|
Dir.mktmpdir("ansible-runner-credential-test") do |dir|
@base_dir = dir
example.run
end
end

let(:auth) { FactoryBot.create(:embedded_ansible_network_credential, auth_attributes) }
let(:cred) { described_class.new(auth.id, @base_dir) }
let(:key_file) { File.join(@base_dir, "env", "network_ssh_key") }
let(:auth_attributes) do
{
:userid => "manageiq-network",
:password => "network_secret"
}
end

describe "#command_line" do
it "returns an empty hash" do
expect(cred.command_line).to eq({})
end
end

# Modeled off of awx codebase:
#
# https://github.com/ansible/awx/blob/1242ee2b/awx/main/tasks.py#L1432-L1443
#
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this comment belong in the tests? (expected in the code, but not the tests)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have had it in both in every other credential provider.

describe "#env_vars" do
it "sets ANSIBLE_NET_USERNAME, ANSIBLE_NET_PASSWORD, and ANSIBLE_NET_AUTHORIZE" do
expected = {
"ANSIBLE_NET_USERNAME" => "manageiq-network",
"ANSIBLE_NET_PASSWORD" => "network_secret",
"ANSIBLE_NET_AUTHORIZE" => "0",
}
expect(cred.env_vars).to eq(expected)
end

context "with an auth_key" do
let(:auth_attributes) do
{
:userid => "",
:password => "",
:auth_key => "key_data"
}
end

it "sets ANSIBLE_NET_SSH_KEYFILE to the network_ssh_key_file location" do
expected = {
"ANSIBLE_NET_USERNAME" => "",
"ANSIBLE_NET_PASSWORD" => "",
"ANSIBLE_NET_AUTHORIZE" => "0",
"ANSIBLE_NET_SSH_KEYFILE" => key_file
}
expect(cred.env_vars).to eq(expected)
end
end

context "with authorize set" do
let(:auth_attributes) do
{
:userid => "user",
:password => "pass",
:options => { :authorize => true }
}
end

it "sets ANSIBLE_NET_AUTHORIZE to '1'" do
expected = {
"ANSIBLE_NET_USERNAME" => "user",
"ANSIBLE_NET_PASSWORD" => "pass",
"ANSIBLE_NET_AUTHORIZE" => "1",
"ANSIBLE_NET_AUTH_PASS" => ""
}
expect(cred.env_vars).to eq(expected)
end

it "defines ANSIBLE_NET_AUTH_PASS if it is present" do
auth.update!(:become_password => "auth_pass")
expected = {
"ANSIBLE_NET_USERNAME" => "user",
"ANSIBLE_NET_PASSWORD" => "pass",
"ANSIBLE_NET_AUTHORIZE" => "1",
"ANSIBLE_NET_AUTH_PASS" => "auth_pass"
}
expect(cred.env_vars).to eq(expected)
end
end
end

describe "#extra_vars" do
it "returns an empty hash" do
expect(cred.extra_vars).to eq({})
end
end

describe "#write_config_files" do
let(:password_file) { File.join(@base_dir, "env", "passwords") }

def password_hash
YAML.load_file(password_file)
end

context "with an auth_key" do
let(:auth_attributes) { { :auth_key => "key_data" } }

it "writes the network_ssh_key_file" do
cred.write_config_files
expect(File.read(key_file)).to eq("key_data")
expect(File.stat(key_file).mode).to eq(0o100400)
end
end

context "without an auth_key" do
it "writes the network_ssh_key_file" do
cred.write_config_files
expect(File.exist?(key_file)).to be_falsey
end
end

context "with authorize set" do
let(:ssh_unlock_key) { "^Enter passphrase for [a-zA-Z0-9\-\/]+\/ssh_key_data:" }
let(:auth_attributes) do
{
:userid => "user",
:password => "pass",
:auth_key_password => "key_pass",
:options => { :authorize => true }
}
end

it "writes the password file" do
cred.write_config_files

expect(password_hash).to eq(ssh_unlock_key => "key_pass")
end

it "defaults auth_key_password to ''" do
auth.update!(:auth_key_password => nil)
cred.write_config_files

expect(password_hash).to eq(ssh_unlock_key => "")
end

context "and an existing password file" do
def existing_env_password_file(data)
cred # initialize the dir
File.write(password_file, data.to_yaml)
end

it "without the existing ssh unlock key adds the password to the file" do
existing_data = {
"^SSH [pP]assword:" => "hunter2",
"^BECOME [pP]assword:" => "hunter3"
}
expected_data = existing_data.merge(ssh_unlock_key => "key_pass")
existing_env_password_file(existing_data)
cred.write_config_files

expect(password_hash).to eq(expected_data)
end

it "with the existing data including the ssh unlock does nothing" do
existing_data = {
"^SSH [pP]assword:" => "hunter2",
"^BECOME [pP]assword:" => "hunter3",
ssh_unlock_key => "hunter4...really?"
}
existing_env_password_file(existing_data)
cred.write_config_files

expect(password_hash).to eq(existing_data)
end
end
end
end
end
end