Skip to content
Open
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
2 changes: 2 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ Style/SymbolArray:
Lint/HandleExceptions:
Exclude:
- 'spec/palletjack-tool_spec.rb'
Style/Not:
Enabled: false
42 changes: 14 additions & 28 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Lint/AssignmentInCondition:
# SupportedStylesAlignWith: either, start_of_block, start_of_line
Lint/BlockAlignment:
Exclude:
- 'tools/exe/palletjack2ipahost'
- 'tools/spec/palletjack2kea_spec.rb'

# Offense count: 1
Expand All @@ -36,6 +37,10 @@ Lint/NonLocalExitFromIterator:
Exclude:
- 'lib/palletjack/keytransformer.rb'

Lint/ShadowingOuterLocalVariable:
Exclude:
- 'tools/exe/palletjack2ipahost'

# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
Expand All @@ -60,6 +65,7 @@ Lint/UselessAccessModifier:
Lint/UselessAssignment:
Exclude:
- 'lib/palletjack/tool.rb'
- 'tools/exe/palletjack2ipahost'
- 'tools/exe/palletjack2unbound'

# Offense count: 16
Expand Down Expand Up @@ -157,6 +163,7 @@ Style/BlockDelimiters:
- 'tools/exe/create_ipv4_interface'
- 'tools/exe/create_system'
- 'tools/exe/dump_pallet'
- 'tools/exe/palletjack2ipahost'
- 'tools/exe/palletjack2unbound'
- 'tools/lib/palletjack2zones.rb'
- 'tools/spec/palletjack2kea_spec.rb'
Expand Down Expand Up @@ -223,15 +230,8 @@ Style/Documentation:
- 'lib/palletjack/pallet/identity.rb'
- 'lib/palletjack/tool.rb'
- 'lib/traceable.rb'
- 'tools/exe/create_domain'
- 'tools/exe/create_ipv4_interface'
- 'tools/exe/create_system'
- 'tools/exe/dump_pallet'
- 'tools/exe/palletjack2kea'
- 'tools/exe/palletjack2pxelinux'
- 'tools/exe/palletjack2salt'
- 'tools/exe/palletjack2unbound'
- 'tools/lib/palletjack2zones.rb'
- 'tools/exe/*'
- 'tools/lib/*'

# Offense count: 1
# Cop supports --auto-correct.
Expand Down Expand Up @@ -283,6 +283,7 @@ Layout/FirstParameterIndentation:
- 'tools/exe/create_domain'
- 'tools/exe/create_ipv4_interface'
- 'tools/exe/create_system'
- 'tools/exe/palletjack2ipahost'
- 'tools/exe/palletjack2kea'
- 'tools/exe/palletjack2pxelinux'
- 'tools/exe/palletjack2salt'
Expand Down Expand Up @@ -318,6 +319,7 @@ Style/GlobalVars:
Style/GuardClause:
Exclude:
- 'lib/traceable.rb'
- 'tools/exe/palletjack2ipahost'
- 'tools/exe/palletjack2salt'

# Offense count: 3
Expand All @@ -336,17 +338,7 @@ Style/HashSyntax:
# Configuration parameters: MaxLineLength.
Style/IfUnlessModifier:
Exclude:
- 'tools/exe/create_domain'
- 'tools/exe/create_ipv4_interface'
- 'tools/exe/create_system'
- 'tools/exe/dump_pallet'
- 'tools/exe/palletjack2kea'
- 'tools/exe/palletjack2knot1'
- 'tools/exe/palletjack2knot2'
- 'tools/exe/palletjack2pxelinux'
- 'tools/exe/palletjack2salt'
- 'tools/exe/palletjack2unbound'
- 'tools/lib/palletjack2zones.rb'
- 'tools/exe/*'

# Offense count: 16
# Cop supports --auto-correct.
Expand All @@ -357,6 +349,7 @@ Layout/IndentAssignment:
- 'tools/exe/create_domain'
- 'tools/exe/create_ipv4_interface'
- 'tools/exe/create_system'
- 'tools/exe/palletjack2ipahost'
- 'tools/exe/palletjack2kea'
- 'tools/exe/palletjack2pxelinux'
- 'tools/exe/palletjack2salt'
Expand Down Expand Up @@ -398,14 +391,6 @@ Style/MultilineIfModifier:
Exclude:
- 'tools/exe/palletjack2unbound'

# Offense count: 5
# Cop supports --auto-correct.
Style/MultilineIfThen:
Exclude:
- 'lib/palletjack/keytransformer.rb'
- 'lib/palletjack/tool.rb'
- 'tools/exe/palletjack2unbound'

# Offense count: 6
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
Expand Down Expand Up @@ -501,6 +486,7 @@ Layout/SpaceInsideBlockBraces:
- 'tools/exe/create_ipv4_interface'
- 'tools/exe/create_system'
- 'tools/exe/dump_pallet'
- 'tools/exe/palletjack2ipahost'
- 'tools/exe/palletjack2kea'
- 'tools/exe/palletjack2pxelinux'
- 'tools/exe/palletjack2salt'
Expand Down
20 changes: 18 additions & 2 deletions examples/warehouse/system/README
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,21 @@ physical machine, a virtual machine or a container.
The link "os" points to the operating system this system runs.

role.yaml contains, in the key "system.role", a list of roles which
this system skall have, which in the future will be made available to
the configuration management system.
this system skall have, which will be made available to the
configuration management system. It also contains, in the key
"system.ipa.hostgroups", a list of hostgroups the system should join
in a FreeIPA domain.

ipa.yaml is updated by the palletjack2ipahost tool and contains, in
the key "system.ipa.otp", a one-time password which can be used to
enroll in a FreeIPA domain.


Files:

system/<name>/architecture.yaml
system/<name>/role.yaml
system/<name>/ipa.yaml


Links:
Expand All @@ -46,6 +53,15 @@ role.yaml:
- role1
- role2
...
ipa:
hostgroups:
- group1
...

ipa.yaml:
system:
ipa:
otp: Auto-generated FreeIPA password


Transformed values:
Expand Down
4 changes: 4 additions & 0 deletions examples/warehouse/system/testvm/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@ system:
role:
- web-server
- ssh-server
- freeipa-client
ipa:
hostgroups:
- clients
5 changes: 2 additions & 3 deletions lib/palletjack/keytransformer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def synthesize_internal(param, dictionary, result = String.new)
case param
when String
rex = /#\[([[:alnum:]._-]+)\]/
if md = rex.match(param) then
if md = rex.match(param)
result << md.pre_match
return unless lookup = dictionary[md[1]]
result << lookup.to_s
Expand Down Expand Up @@ -249,9 +249,8 @@ def transform!(pallet)

transforms.each do |t|
transform, param = t.flatten
if self.respond_to?(transform.to_sym) then
if self.respond_to?(transform.to_sym)
if new_value = self.send(transform.to_sym, param, context)
then
new_value = TraceableString.new(new_value)
new_value.file = transform.file
new_value.line = transform.line
Expand Down
1 change: 0 additions & 1 deletion lib/palletjack/tool.rb
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,6 @@ def git_header(tool_name, comment_char: '#', include_id: false)
#{comment_char}#{comment_char} Repository: #{workdir}
#{comment_char}#{comment_char} Branch: #{branch}\n"
if include_id
then
header +=
"#{comment_char}#{comment_char} Commit ID: #{repo.head.target_id}\n"
end
Expand Down
192 changes: 192 additions & 0 deletions tools/exe/palletjack2ipahost
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
#!/usr/bin/env ruby

# Create host objects in a FreeIPA instance.
#
# Requires you to first have a valid Kerberos ticket for a FreeIPA
# administrator. Check if you have one with "klist" and if not, get
# one using "kinit <username>".
#
# Runs for a single system or, if none is given, every system in the
# warehouse with the role freeipa-client.
#
# Hostgroups are synchronized with the warehouse and any group not
# specified in the warehouse will be removed.
#
# Host objects will be created with a randomly generated password. The
# password is saved in the warehouse as system.ipa.otp, for later
# export to Salt.
#
# If the --reinstall parameter is specified, the existing host object
# will be removed and a new one created, allowing the machine be be
# reinstalled.

require 'palletjack/tool'
require 'ipa/client'
require 'yaml'
require 'hashie'

class PalletJack2IPAHost < PalletJack::Tool
def parse_options(opts)
opts.banner =
"Usage: #{$PROGRAM_NAME} -w <warehouse> -i <IPA server> [-s <system> [--reinstall]] [-v]

Synchronize systems in the warehouse to host objects in an IPA server.

Requres a valid Kerberos ticket for a FreeIPA administrator."

opts.on('-i SERVER', '--ipa-server SERVER', 'IPA server',
String) { |srv| options[:ipa] = srv }
opts.on('-s SYSTEM', '--system SYSTEM', 'system to synchronize',
String) { |system| options[:system] = system }
opts.on('-r', '--reinstall', 'reinstall the named system') { |reinstall|
required_option :system
options[:reinstall] = reinstall
}
opts.on('-v', '--verbose', 'verbose mode') { |verbose|
options[:verbose] = verbose
}

required_option :ipa
end

def output
# Do all the processing here in #output, since we're always
# modifying state on an external FreeIPA server and there is no
# local preprocessing to do in #process.

@ipa_boxes = {}

connect_to_freeipa(options[:ipa])

if options[:system]
system = jack.fetch(kind: 'system', name: options[:system])
unless system['system.role'].include? 'freeipa-client'
raise "System \"#{options[:system]}\" does not have the role \"freeipa-client\""
end
sync_system system
else
jack.each(kind: 'system') do |system|
next unless system['system.role'].include? 'freeipa-client'
sync_system system
end
end

@ipa_boxes.each do |system, content|
pallet_box 'system', system, 'ipa' do
{ system: { ipa: content } }
end
end
end

private

def connect_to_freeipa(host)
@ipa = IPA::Client.new(host: host)
end

# Update all FreeIPA server state to match the +system+ from the warehouse
#
# If reinstalling, remove the old host object. Then, create a new
# host object and sync hostgroups.
def sync_system(system)
puts "Syncing #{system['system.name']}" if options[:verbose]

box = system['system.ipa'] || {}

if options[:reinstall]
puts 'Removing host object' if options[:verbose]
del_host = @ipa.host_del(hostname: system['net.dns.fqdn'])
check_api_error(del_host, 'deleting host')
end

unless @ipa.host_exists? system['net.dns.fqdn']
puts 'Creating host object' if options[:verbose]
box['otp'] = create_host_object(system)
end

sync_hostgroups(system)

@ipa_boxes[system['system.name']] = box
end

# Create a host object in FreeIPA representing a +system+ from the
# warehouse. The host object may not already exist.
def create_host_object(system)
# Generate a random password
add_host_params = { 'random' => true }

ip_address = nil
system.children(kind: 'ipv4_interface') do |interface|
# Find an interface in the same domain as the host and set as
# primary IP address in FreeIPA
if interface['net.dns.domain'] == system['net.dns.domain']
ip_address = interface['net.ipv4.address']
end
end

if ip_address
add_host_params['ip_address'] = ip_address
puts "Using IP address #{ip_address}" if options[:verbose]
else
raise "No IP address in domain #{system['net.dns.domain']} found for #{system['system.name']}"
end

add_host = @ipa.host_add(hostname: system['net.dns.fqdn'], params: add_host_params)
check_api_error(add_host, 'adding host')

add_host['result']['result']['randompassword']
end

# Sync hostgroups from the warehouse to FreeIPA
#
# Join the system to the groups listed in the warehouse, and leave
# all groups not listed in the warehouse.
def sync_hostgroups(system)
host_show = @ipa.host_show(hostname: system['net.dns.fqdn'], all: true)
check_api_error(host_show, 'getting host information')
host_data = host_show['result']['result']

server_hostgroups = host_data['memberof_hostgroup'] || []
warehouse_hostgroups = system['system.ipa.hostgroups'] || []

warehouse_hostgroups.each do |group|
next if server_hostgroups.include? group
puts "Adding hostgroup #{group}" if options[:verbose]
ipa_mod = @ipa.api_post(method: 'hostgroup_add_member', item: group,
params: { 'host' => [system['net.dns.fqdn']] })
check_api_error(ipa_mod, 'adding host to group')
end

server_hostgroups.each do |group|
next if warehouse_hostgroups.include? group
puts "Removing hostgroup #{group}" if options[:verbose]
ipa_mod = @ipa.api_post(method: 'hostgroup_remove_member', item: group,
params: { 'host' => [system['net.dns.fqdn']] })
check_api_error(ipa_mod, 'removing host from group')
end
end

# Check if the return value +ret+ from the +ipa/client+ API is an
# error or a failure, and if so, raise a RuntimeError including the
# string +action+.
def check_api_error(ret, action)
if ret['error']
raise "Error #{action}: #{ret['error']['code']} #{ret['error']['name']}: #{ret['error']['message']}"
end

# result.failed contains a complicated and variable structure with
# its leaves being lists of the requested operations that failed.
# If all operations were successful, all these lists are empty.
failed = ret['result']['failed']
return unless failed
failed.extend Hashie::Extensions::DeepLocate
failures = failed.deep_locate lambda { |_, value, _|
value.is_a?(Array) && !value.empty?
}
raise "Failed #{action}: #{ret['result']['failed']}" unless failures.empty?
end
end

if PalletJack2IPAHost.standalone?(__FILE__)
PalletJack2IPAHost.run
end
Loading