Skip to content

Commit 06cb388

Browse files
committed
(gh-26) Add configure task for initial openvox-agent configuration
This task provides very basic configuration needed for the initial agent run. * It allows generating a puppet.conf from a hash of parameters so that the server can set. * It allows generating a csr_attributes.yaml for autosigning and certificate extensions. * And it allows management of the puppet service.
1 parent d64d231 commit 06cb388

File tree

7 files changed

+391
-0
lines changed

7 files changed

+391
-0
lines changed

spec/tasks/configure_spec.rb

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
require_relative '../../tasks/configure'
5+
6+
# rubocop:disable RSpec/MessageSpies
7+
# rubocop:disable RSpec/StubbedMock
8+
describe 'openvox_bootstrap::configure' do
9+
let(:tmpdir) { Dir.mktmpdir('openvox_bootstrap-configure-spec') }
10+
let(:task) { OpenvoxBootstrap::Configure.new }
11+
12+
around do |example|
13+
example.run
14+
ensure
15+
FileUtils.remove_entry_secure(tmpdir)
16+
end
17+
18+
describe '#write_puppet_conf' do
19+
let(:puppet_conf) do
20+
{
21+
'main' => {
22+
'server' => 'puppet.spec',
23+
'certname' => 'agent.spec',
24+
},
25+
'agent' => {
26+
'environment' => 'test'
27+
}
28+
}
29+
end
30+
let(:puppet_conf_path) { File.join(tmpdir, 'puppet.conf') }
31+
let(:puppet_conf_contents) do
32+
<<~CONF
33+
[main]
34+
server = puppet.spec
35+
certname = agent.spec
36+
37+
[agent]
38+
environment = test
39+
CONF
40+
end
41+
42+
it 'writes a puppet.conf ini' do
43+
expect(task.write_puppet_conf(puppet_conf, tmpdir)).to(
44+
eq(
45+
{
46+
puppet_conf: {
47+
path: puppet_conf_path,
48+
contents: puppet_conf_contents
49+
}
50+
}
51+
)
52+
)
53+
expect(File.read(puppet_conf_path)).to eq(puppet_conf_contents)
54+
end
55+
56+
it 'does nothing if given an empty config' do
57+
expect(task.write_puppet_conf(nil)).to eq({})
58+
expect(task.write_puppet_conf({})).to eq({})
59+
end
60+
end
61+
62+
describe '#write_csr_attributes' do
63+
let(:csr_attributes) do
64+
{
65+
'custom_attributes' => {
66+
'1.2.840.113549.1.9.7' => 'bar',
67+
},
68+
'extension_requests' => {
69+
'pp_role' => 'spec'
70+
}
71+
}
72+
end
73+
let(:csr_attributes_path) { File.join(tmpdir, 'csr_attributes.yaml') }
74+
let(:csr_attributes_contents) do
75+
<<~YAML
76+
---
77+
custom_attributes:
78+
1.2.840.113549.1.9.7: bar
79+
extension_requests:
80+
pp_role: spec
81+
YAML
82+
end
83+
84+
it 'writes a csr_attributes.yaml file' do
85+
expect(task.write_csr_attributes(csr_attributes, tmpdir)).to(
86+
eq(
87+
{
88+
csr_attributes: {
89+
path: csr_attributes_path,
90+
contents: csr_attributes_contents,
91+
}
92+
}
93+
)
94+
)
95+
expect(File.read(csr_attributes_path)).to eq(csr_attributes_contents)
96+
expect(File.stat(csr_attributes_path).mode & 0o777).to eq(0o640)
97+
end
98+
99+
it 'does nothing if given an empty config' do
100+
expect(task.write_csr_attributes(nil)).to eq({})
101+
expect(task.write_csr_attributes({})).to eq({})
102+
end
103+
end
104+
105+
describe '#manage_puppet_service' do
106+
def status(code = 0)
107+
instance_double(Process::Status, exitstatus: code)
108+
end
109+
110+
it 'is successful for a 0 exit code' do
111+
command = [
112+
'puppet',
113+
'apply',
114+
'--detailed-exitcodes',
115+
'-e',
116+
%(service { 'puppet': ensure => running, enable => true, }),
117+
]
118+
expect(Open3).to receive(:capture2e).with(*command).and_return(['applied', status])
119+
120+
expect(task.manage_puppet_service(true, true)).to eq(
121+
{
122+
puppet_service: {
123+
command: command.join(' '),
124+
output: 'applied',
125+
successful: true,
126+
}
127+
}
128+
)
129+
end
130+
131+
it 'is successful for a 2 exit code' do
132+
expect(Open3).to receive(:capture2e).and_return(['applied', status(2)])
133+
134+
result = task.manage_puppet_service(true, true)
135+
expect(result.dig(:puppet_service, :successful)).to be true
136+
end
137+
138+
it 'fails for a non 0, 2 exit code' do
139+
expect(Open3).to receive(:capture2e).and_return(['applied', status(1)])
140+
141+
result = task.manage_puppet_service(true, true)
142+
expect(result.dig(:puppet_service, :successful)).to be false
143+
end
144+
end
145+
146+
describe '#task' do
147+
it 'returns a result hash if puppet service is managed successfully' do
148+
expect(task).to receive(:manage_puppet_service).and_return({ puppet_service: { successful: true } })
149+
150+
expect(task.task).to eq(
151+
{
152+
puppet_service: { successful: true },
153+
}
154+
)
155+
end
156+
157+
it 'prints results and exits 1 if puppet service fails' do
158+
expect(task).to(
159+
receive(:manage_puppet_service).
160+
and_return(
161+
{
162+
puppet_service: {
163+
successful: false,
164+
output: "apply failed\n",
165+
}
166+
}
167+
)
168+
)
169+
170+
expect { task.task }.to(
171+
raise_error(SystemExit).and(
172+
output(<<~EOM).to_stdout
173+
{
174+
"puppet_service": {
175+
"successful": false,
176+
"output": "apply failed\\n"
177+
}
178+
}
179+
180+
Failed managing the puppet service:
181+
182+
apply failed
183+
EOM
184+
).and(output('').to_stderr)
185+
)
186+
end
187+
end
188+
end
189+
# rubocop:enable RSpec/MessageSpies
190+
# rubocop:enable RSpec/StubbedMock

tasks/configure.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"description": "Provides initial configuration for a freshly installed openvox-agent.",
3+
"input_method": "stdin",
4+
"parameters": {
5+
"puppet_conf": {
6+
"description": "Hash of puppet configuration settings to write to the puppet.conf ini file. NOTE: This will completely overwrite any existing settings in the file; there is no merging behavior in this task.",
7+
"type": "Optional[OpenvoxBootstrap::IniFile]"
8+
},
9+
"csr_attributes": {
10+
"description": "Hash of CSR attributes (custom_attributes and extension_requests) to write to the csr_attributes.yaml file. NOTE: This will completely overwrite any existing settings in the file; there is no merging behavior in this task.",
11+
"type": "Optional[OpenvoxBootstrap::CsrAttributes]"
12+
},
13+
"puppet_service_running": {
14+
"description": "Whether the Puppet service should be running after this task completes. Defaults to true.",
15+
"type": "Boolean",
16+
"default": true
17+
},
18+
"puppet_service_enabled": {
19+
"description": "Whether the Puppet service should be enabled to start on boot after this task completes. Defaults to true.",
20+
"type": "Boolean",
21+
"default": true
22+
}
23+
}
24+
}

tasks/configure.rb

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
#! /opt/puppetlabs/puppet/bin/ruby
2+
# frozen_string_literal: true
3+
4+
require_relative '../lib/openvox_bootstrap/task'
5+
require 'open3'
6+
require 'yaml'
7+
8+
module OpenvoxBootstrap
9+
class Configure < Task
10+
# Overwrite puppet.conf with the values in the puppet_conf hash.
11+
#
12+
# Does nothing if given an empty or nil puppet_conf.
13+
#
14+
# @param puppet_conf [Hash<String,Hash<String,String>>] A hash of
15+
# sections and settings to write to the puppet.conf file.
16+
# @return [Hash]
17+
def write_puppet_conf(puppet_conf, etc_puppet_path = '/etc/puppetlabs/puppet')
18+
return {} if puppet_conf.nil? || puppet_conf.empty?
19+
20+
conf_path = File.join(etc_puppet_path, 'puppet.conf')
21+
sections = puppet_conf.map do |section, settings|
22+
"[#{section}]\n" +
23+
settings.map { |key, value| "#{key} = #{value}" }.join("\n")
24+
end
25+
puppet_conf_contents = "#{sections.join("\n\n")}\n"
26+
27+
File.open(conf_path, 'w', perm: 0o644) do |f|
28+
f.write(puppet_conf_contents)
29+
end
30+
31+
{
32+
puppet_conf: {
33+
path: conf_path,
34+
contents: puppet_conf_contents,
35+
}
36+
}
37+
end
38+
39+
# Overwrite the csr_attributes.yaml file with the given
40+
# csr_attributes hash.
41+
#
42+
# Does nothing if given an empty or nil csr_attributes.
43+
#
44+
# The file will be mode 640.
45+
#
46+
# @param csr_attributes [Hash] A hash of custom_attributes
47+
# and extension_requests to write to the csr_attributes.yaml
48+
# file.
49+
# @return [Hash]
50+
def write_csr_attributes(csr_attributes, etc_puppet_path = '/etc/puppetlabs/puppet')
51+
return {} if csr_attributes.nil? || csr_attributes.empty?
52+
53+
csr_attributes_path = File.join(etc_puppet_path, 'csr_attributes.yaml')
54+
csr_attributes_contents = csr_attributes.to_yaml
55+
File.open(csr_attributes_path, 'w', perm: 0o640) do |f|
56+
f.write(csr_attributes_contents)
57+
end
58+
59+
{
60+
csr_attributes: {
61+
path: csr_attributes_path,
62+
contents: csr_attributes_contents,
63+
}
64+
}
65+
end
66+
67+
# Manage the puppet service using puppet apply.
68+
def manage_puppet_service(running, enabled)
69+
manifest = <<~MANIFEST
70+
service { 'puppet':
71+
ensure => #{running ? 'running' : 'stopped'},
72+
enable => #{enabled},
73+
}
74+
MANIFEST
75+
76+
command = [
77+
'puppet',
78+
'apply',
79+
'--detailed-exitcodes',
80+
'-e',
81+
manifest.gsub(%r{\n}, ' ').strip,
82+
]
83+
84+
output, status = Open3.capture2e(*command)
85+
success = [0, 2].include?(status.exitstatus)
86+
87+
{
88+
puppet_service: {
89+
command: command.join(' '),
90+
output: output,
91+
successful: success,
92+
}
93+
}
94+
end
95+
96+
def task(
97+
puppet_conf: {},
98+
csr_attributes: {},
99+
puppet_service_running: true,
100+
puppet_service_enabled: true
101+
)
102+
puppet_conf_result = write_puppet_conf(puppet_conf)
103+
csr_result = write_csr_attributes(csr_attributes)
104+
puppet_service_result = manage_puppet_service(puppet_service_running, puppet_service_enabled)
105+
106+
results = puppet_conf_result.merge(
107+
csr_result
108+
).merge(puppet_service_result)
109+
110+
success = results[:puppet_service][:successful]
111+
if success
112+
results
113+
else
114+
puts JSON.pretty_generate(results)
115+
puts "\nFailed managing the puppet service:\n\n"
116+
puts results[:puppet_service][:output]
117+
exit 1
118+
end
119+
end
120+
end
121+
end
122+
123+
OpenvoxBootstrap::Configure.run if __FILE__ == $PROGRAM_NAME

types/cer_short_names.pp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Certificate extension request short names.
2+
# These are the allowed short names documented for Puppet(TM)
3+
# extension requests per [csr_attributes.yaml](https://help.puppet.com/core/current/Content/PuppetCore/config_file_csr_attributes.htm)
4+
type OpenvoxBootstrap::CerShortNames = Enum[
5+
# 1.3.6.1.4.1.34380.1.1 range
6+
'pp_uuid',
7+
'pp_instance_id',
8+
'pp_image_name',
9+
'pp_preshared_key',
10+
'pp_cost_center',
11+
'pp_product',
12+
'pp_project',
13+
'pp_application',
14+
'pp_service',
15+
'pp_employee',
16+
'pp_created_by',
17+
'pp_environment',
18+
'pp_role',
19+
'pp_software_version',
20+
'pp_department',
21+
'pp_cluster',
22+
'pp_provisioner',
23+
'pp_region',
24+
'pp_datacenter',
25+
'pp_zone',
26+
'pp_network',
27+
'pp_securitypolicy',
28+
'pp_cloudplatform',
29+
'pp_apptier',
30+
'pp_hostname',
31+
# 1.3.6.1.4.1.34380.1.3 range
32+
'pp_authorization',
33+
'pp_auth_role',
34+
]

types/csr_attributes.pp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# [csr_attributes.yaml](https://help.puppet.com/core/current/Content/PuppetCore/config_file_csr_attributes.htm)
2+
type OpenvoxBootstrap::CsrAttributes = Struct[
3+
{
4+
Optional['custom_attributes'] => Hash[
5+
OpenvoxBootstrap::Oid,
6+
String
7+
],
8+
Optional['extension_requests'] => Hash[
9+
Variant[OpenvoxBootstrap::Oid,OpenvoxBootstrap::CerShortNames],
10+
String
11+
],
12+
}
13+
]

types/ini_file.pp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Simple type for data to be transformed to an INI file format.
2+
type OpenvoxBootstrap::IniFile = Hash[
3+
String, # Section name
4+
Hash[String, String] # Key-value pairs within the section
5+
]

types/oid.pp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Object Identifier per https://en.wikipedia.org/wiki/Object_identifier
2+
type OpenvoxBootstrap::Oid = Pattern[/\d+(\.\d+)*/]

0 commit comments

Comments
 (0)