Permalink
Browse files

Use tokens for discovery of host identity during installation

- fixes #1069
- fixes #1720
- refs #969
  • Loading branch information...
GregSutcliffe authored and ohadlevy committed Sep 21, 2012
1 parent 6099f48 commit 81159d4bf8355ab2fac1813127fccf60baf75fbc
@@ -84,35 +84,9 @@ def pxe_config
# if the host doesn't exists, it will return 404 and the requested method will not be reached.

def get_host_details
# find out ip info
if params.has_key? "spoof"
ip = params.delete("spoof")
@spoof = true
elsif (ip = request.env['REMOTE_ADDR']) =~ Regexp.new(Setting[:remote_addr])
ip = request.env["HTTP_X_FORWARDED_FOR"] unless request.env["HTTP_X_FORWARDED_FOR"].nil?
end

ip = ip.split(',').first # in cases where multiple nics/ips exists - see #1619

# search for a mac address in any of the RHN provisioning headers
# this section is kickstart only relevant
maclist = []
unless request.env['HTTP_X_RHN_PROVISIONING_MAC_0'].nil?
begin
request.env.keys.each do | header |
maclist << request.env[header].split[1].downcase.strip if header =~ /^HTTP_X_RHN_PROVISIONING_MAC_/
end
rescue => e
logger.info "unknown RHN_PROVISIONING header #{e}"
end
end

# we try to match first based on the MAC, falling back to the IP
conditions = maclist.empty? ? {:ip => ip} : [ "lower(mac) IN (?)", maclist.map(&:downcase) ]

@host = Host.first(:include => [:architecture, :medium, :operatingsystem, :domain], :conditions => conditions)
@host = find_host_by_spoof || find_host_by_token || find_host_by_ip_or_mac
unless @host
logger.info "#{controller_name}: unable to find ip/mac match for #{ip}"
logger.info "#{controller_name}: unable to find a host that matches the request from #{request.env['REMOTE_ADDR']}"
head(:not_found) and return
end
unless @host.operatingsystem
@@ -127,6 +101,48 @@ def get_host_details
logger.info "Found #{@host}"
end

def find_host_by_spoof
spoof = params.delete("spoof")
return nil if spoof.blank?
@spoof = true
Host.find_by_ip(spoof)
end

def find_host_by_token
token = params.delete("token")
return nil if token.blank?
Host.for_token(token).first
end

def find_host_by_ip_or_mac
# try to find host based on our client ip address
ip = request.env['REMOTE_ADDR']

# check if someone is asking on behave of another system (load balance etc)
if request.env['HTTP_X_FORWARDED_FOR'].present? and (ip =~ Regexp.new(Setting[:remote_addr]))
ip = request.env['HTTP_X_FORWARDED_FOR']
end

# in case we got back multiple ips (see #1619)
ip = ip.split(',').first

# search for a mac address in any of the RHN provisioning headers
# this section is kickstart only relevant
mac_list = []
if request.env['HTTP_X_RHN_PROVISIONING_MAC_0'].present?
begin
request.env.keys.each do |header|
mac_list << request.env[header].split[1].strip.downcase if header =~ /^HTTP_X_RHN_PROVISIONING_MAC_/
end
rescue => e
logger.info "unknown RHN_PROVISIONING header #{e}"
mac_list = []
end
end
# we try to match first based on the MAC, falling back to the IP
Host.where(mac_list.empty? ? { :ip => ip } : ["lower(mac) IN (?)", mac_list]).first
end

def allowed_to_install?
(@host.build or @spoof) ? true : head(:method_not_allowed)
end
@@ -14,6 +14,7 @@ class Host < Puppet::Rails::Host
belongs_to :sp_subnet, :class_name => "Subnet"
belongs_to :compute_resource
belongs_to :image
has_one :token, :dependent => :destroy, :conditions => Proc.new {"expires >= '#{Time.now.utc.to_s(:db)}'"}

include Hostext::Search
include HostCommon
@@ -122,6 +123,8 @@ class Jail < ::Safemode::Jail
end
}

scope :for_token, lambda { |token| joins(:token).where(:tokens => { :value => token }) }

# audit the changes to this model
audited :except => [:last_report, :puppet_status, :last_compile]
has_associated_audits
@@ -205,6 +208,9 @@ def clearFacts
# Build is cleared and the boot link and autosign entries are removed
# A site specific build script is called at this stage that can do site specific tasks
def built(installed = true)

# delete all expired tokens
expire_tokens
self.build = false
self.installed_at = Time.now.utc if installed
self.save
@@ -418,6 +424,10 @@ def populateFieldsFromFacts facts = self.facts_hash
def setBuild
clearFacts
clearReports
if Setting[:token_duration] != 0
self.create_token(:value => Foreman.uuid,
:expires => Time.now.utc + Setting[:token_duration].minutes)
end
self.build = true
self.save
errors.empty?
@@ -798,4 +808,9 @@ def set_certname
self.certname = Foreman.uuid if read_attribute(:certname).blank? or new_record?
end

def expire_tokens
# this clean up other hosts as well, but reduce the need for another task to cleanup tokens.
Token.delete_all(["expires < ? or host_id = ?", Time.now.utc.to_s(:db), id])
end

end
@@ -25,6 +25,8 @@ def media_path

#returns the URL for Foreman based on the required action
def foreman_url(action = "provision")
url_for :only_path => false, :controller => "unattended", :action => action, :host => request_url
url_for :only_path => false, :controller => "unattended",
:action => action, :host => request_url,
:token => (@host.token.value unless @host.token.nil?)
end
end
@@ -0,0 +1,19 @@
# == Schema Information
#
# Table name: tokens
#
# id :integer not null, primary key
# value :string(255)
# expires :datetime
# created_at :datetime
# updated_at :datetime
# host_id :integer
#

class Token < ActiveRecord::Base
attr_accessible :value, :expires
belongs_to :host

validates_presence_of :value, :host_id, :expires

end
@@ -0,0 +1,15 @@
class CreateTokens < ActiveRecord::Migration
def self.up
create_table :tokens do |t|
t.string :value
t.datetime :expires
t.integer :host_id
end
add_index :tokens, :value
add_index :tokens, :host_id
end

def self.down
drop_table :tokens
end
end
@@ -46,7 +46,8 @@ def load(reset=false)
set('manage_puppetca', "Should Foreman automate certificate signing upon provisioning new host", true),
set('ignore_puppet_facts_for_provisioning', "Does not update ipaddress and MAC values from Puppet facts", false),
set('query_local_nameservers', "Should Foreman query the locally configured name server or the SOA/NS authorities", false),
set('remote_addr', "If Foreman is running behind Passenger or a remote loadbalancer, the ip should be set here", "127.0.0")
set('remote_addr', "If Foreman is running behind Passenger or a remote loadbalancer, the ip should be set here", "127.0.0"),
set('token_duration', "Time in minutes installation tokens should be valid for, 0 to disable", 0)
].each { |s| create s.update(:category => "Provisioning")}

[
@@ -14,7 +14,8 @@ def render_safe template, allowed_methods = [], allowed_vars = {}

#returns the URL for Foreman Built status (when a host has finished the OS installation)
def foreman_url(action = "built")
url_for :only_path => false, :controller => "unattended", :action => action
url_for :only_path => false, :controller => "unattended", :action => action,
:token => (@host.token.value unless @host.token.nil?)
end

# provide embedded snippets support as simple erb templates
@@ -0,0 +1,66 @@
# Locale, country and keyboard settings
d-i debian-installer/locale string en_US
d-i console-setup/ask_detect boolean false
d-i console-setup/modelcode string pc105
d-i console-setup/variant USA
d-i console-setup/layout USA
d-i console-setup/layoutcode string us

# Network configuration
d-i netcfg/choose_interface select auto
d-i netcfg/get_hostname string temp-01.yourdomain.net
d-i netcfg/get_domain string yourdomain.net
d-i netcfg/wireless_wep string

d-i hw-detect/load_firmware boolean true

# Mirror settings
d-i mirror/country string manual
d-i mirror/http/hostname string sg.archive.ubuntu.com:80
d-i mirror/http/directory string /
d-i mirror/http/proxy string
d-i mirror/codename string
d-i mirror/suite string
d-i mirror/udeb/suite string

# Time settings
d-i clock-setup/utc boolean true
d-i time/zone string UTC

# NTP
d-i clock-setup/ntp boolean true
d-i clock-setup/ntp-server string ntp

# Set alignment for automatic partitioning
# Choices: cylinder, minimal, optimal
#d-i partman/alignment select cylinder

d-i partman-auto/disk string /dev/sda\nd-i partman-auto/method string regular...

# Install different kernel
#d-i base-installer/kernel/image string linux-server

# User settings
d-i passwd/root-password-crypted password xybxa6JUkz63w
user-setup-udeb passwd/root-login boolean true
d-i passwd passwd/make-user boolean false
user-setup-udeb passwd/make-user boolean false

# Install minimal task set (see tasksel --task-packages minimal)
tasksel tasksel/first multiselect minimal

# Install some base packages
d-i pkgsel/include string puppet lsb-release openssh-server
d-i pkgsel/update-policy select unattended-upgrades

popularity-contest popularity-contest/participate boolean false

# Boot loader settings
#grub-pc grub-pc/hidden_timeout boolean false
#grub-pc grub-pc/timeout string 10
d-i grub-installer/only_debian boolean true
d-i grub-installer/with_other_os boolean true

d-i finish-install/reboot_in_progress note

d-i preseed/late_command string wget http://test.host/unattended/finish?token=aaaaaa -O /target/tmp/finish.sh && in-target chmod +x /tmp/finish.sh && in-target /tmp/finish.sh
@@ -124,3 +124,8 @@ attributes25:
category: Puppet
default: false
description: "Should Foreman use the new format (2.6.5+) to answer Puppet in its ENC yaml output?"
attribute26:
name: token_duration
category: Provisioning
default: 0
description: "Time in minutes installation tokens should be valid for, 0 to disable"
@@ -109,4 +109,21 @@ class UnattendedControllerTest < ActionController::TestCase
assert_response :not_found
end

test "hosts with unknown ip and valid token should render a template" do
Setting[:token_duration] = 30
@request.env["REMOTE_ADDR"] = '127.0.0.1'
hosts(:ubuntu).create_token(:value => "aaaaaa", :expires => Time.now + 5.minutes)
get :preseed, {'token' => hosts(:ubuntu).token.value }
assert_response :success
end

test "template should contain tokens when tokens enabled and present for the host" do
Setting[:token_duration] = 30
@request.env["REMOTE_ADDR"] = hosts(:ubuntu).ip
hosts(:ubuntu).create_token(:value => "aaaaaa", :expires => Time.now + 5.minutes)
template = get :preseed
expected = File.read(Pathname.new(__FILE__).parent.parent + "fixtures/sample_tokenised_template")
assert_equal template.body, expected
end

end
@@ -476,4 +476,39 @@ def setup_user_and_host
assert_equal "myhost1.mydomain.net", host.name
end

# Token tests

test "built should clean tokens" do
Setting[:token_duration] = 30
h = hosts(:one)
h.create_token(:value => "aaaaaa", :expires => Time.now)
assert_equal Token.all.size, 1
h.built(false)
assert_equal Token.all.size, 0
end

test "built should clean tokens even when tokens are disabled" do
Setting[:token_duration] = 0
h = hosts(:one)
h.create_token(:value => "aaaaaa", :expires => Time.now)
assert_equal Token.all.size, 1
h.built(false)
assert_equal Token.all.size, 0
end

test "hosts should be able to retrieve their token if one exists" do
Setting[:token_duration] = 30
h = hosts(:one)
assert_equal Token.first, h.token
end

test "token should return false when tokens are disabled or invalid" do
Setting[:token_duration] = 0
h = hosts(:one)
assert_equal h.token, nil
Setting[:token_duration] = 30
h = hosts(:one)
assert_equal h.token, nil
end

end
Oops, something went wrong.

0 comments on commit 81159d4

Please sign in to comment.