From: <jh...@us...> - 2010-10-20 18:37:30
|
Revision: 218 http://etch.svn.sourceforge.net/etch/?rev=218&view=rev Author: jheiss Date: 2010-10-20 18:37:23 +0000 (Wed, 20 Oct 2010) Log Message: ----------- Add --debug flag Ensure timezone is always set to UTC, see comments in script for details. Add support for a 'badfwd' state where we roll the client forward to the next good/non-bad tag, rather than rolling them back to the previous good/non-bad tag. Switch to nVentory's new get_objects syntax. Update with the actual field name (config_mgmt_tag) used in nVentory. Add logic to handle "temporary bump to trunk" tags from nVentory. Modified Paths: -------------- trunk/etchserver-samples/nventory/nodetagger Modified: trunk/etchserver-samples/nventory/nodetagger =================================================================== --- trunk/etchserver-samples/nventory/nodetagger 2010-10-05 00:23:29 UTC (rev 217) +++ trunk/etchserver-samples/nventory/nodetagger 2010-10-20 18:37:23 UTC (rev 218) @@ -4,11 +4,45 @@ # http://etch.wiki.sourceforge.net/ControlledDeployment ############################################################################## +require 'optparse' require 'nventory' -name = ARGV[0] or abort "No hostname passed" +# +# Parse the command line options +# +@debug = false + +opts = OptionParser.new +opts.banner = "Usage: #{File.basename($0)} [--debug] <hostname>" +opts.on('--debug', "Print messages about how the client's tag is determined") do |opt| + @debug = opt +end + +opts.on_tail("-h", "--help", "Show this message") do + puts opts + exit +end + +name = nil +leftovers = opts.parse(ARGV) +if leftovers.size == 1 + name = leftovers.first +else + puts opts + exit +end + # +# This script normally runs only on the etch servers and thus doesn't need to +# worry about timezone differences under standard operation. But for +# development and troubleshooting users might run it elsewhere. In which case +# it needs to execute with the same timezone setting as the etch servers. +# + +ENV['TZ'] = 'UTC' + +# # Load the tag state data # # This allows users to mark tags as good or bad. Here's a scenario to @@ -27,46 +61,89 @@ # implement a human review or change management process where only known-good # tags are allowed to propagate to production. # +# The rollback behavior doesn't work well when the bad change is a new +# file and the last not-bad tag doesn't contain any configuration for +# that file so the bad configuration remains in place until the fixed +# configuration catches up, which could be 5 hours. In those cases you +# can mark the bad tags with 'badfwd' if you want to have clients roll +# forward right away to the next not-bad tag so they pick up the fixed +# configuration right away. +# @tagstate = {} +valid_states = ['good', 'bad', 'badfwd'] tagstatefile = File.join(File.dirname(__FILE__), 'tagstate') if File.exist?(tagstatefile) IO.foreach(tagstatefile) do |line| next if line =~ /^\s*$/ # Skip blank lines next if line =~ /^\s*#/ # Skip comments tag, state = line.split - if state == 'good' || state == 'bad' + if valid_states.include?(state) @tagstate[tag] = state else - warn "Ignoring state #{state} for tag #{tag}, it's not 'good' or 'bad'" + warn "Ignoring state #{state} for tag #{tag}, it's not one of the valid states: #{valid_states.join(',')}" end end end -# This finds an autotag that is at least 'hoursago' old, isn't marked as bad, -# and is marked as good if 'needgoodtag' is true +# Shared with the repo_update script +def convert_tagtime_to_unixtime(tagdate, tagtime) + year, month, day = tagdate.unpack('A4A2A2') + hour, minute = tagtime.unpack('A2A2') + unixtime = Time.local(year, month, day, hour, minute, 0, 0) + unixtime +end + +def tagforhours(hours) + Time.at(Time.now - hours * 60 * 60).strftime('etchautotag-%Y%m%d-%H00') +end + def findautotag(hoursago, needgoodtag=false) tag = nil - hourcounter = hoursago - # Check back up to three days for an acceptable tag. The three day - # limit is arbitrary, but we need something so that we avoid going - # into an infinite loop if there simply isn't an acceptable tag. - while tag.nil? && hourcounter < 24*3 - proposedtag = Time.at(Time.now - hourcounter * 60 * 60).strftime('etchautotag-%Y%m%d-%H00') + + # Check the state for the 'hoursago' tag. If it is 'badfwd' then look + # forward for the next not-bad or good tag (depending on + # 'needgoodtag'). Otherwise check from that point backwards. + hoursagotag = tagforhours(hoursago) + hours_to_check = [] + if @tagstate[hoursagotag] == 'badfwd' + puts "Tag #{hoursagotag} is badfwd, looking forward rather than backward" if (@debug) + (hoursago-1).downto(0) do |hour| + hours_to_check << hour + end + else + # Check back up to three days for an acceptable tag. The three day + # limit is arbitrary, but we need something so that we avoid going + # into an infinite loop if there simply isn't an acceptable tag. + hoursago.upto(24*3) do |hour| + hours_to_check << hour + end + end + + hours_to_check.each do |hour| + proposedtag = tagforhours(hour) + puts "Checking tag #{proposedtag} for #{hour} hours ago" if (@debug) # If we need a 'good' tag then check that the proposed tag is # marked as 'good'. - if needgoodtag && - !@tagstate[proposedtag].nil? && - @tagstate[proposedtag] == 'good' - tag = proposedtag - end + if needgoodtag + if @tagstate[proposedtag] == 'good' + tag = proposedtag + puts "Need good tag, proposed tag is good" if (@debug) + break + else + puts "Need good tag, proposed tag is not good" if (@debug) + end + else # If we don't need a 'good' tag then check that either the # proposed tag has no state (unknown, and presumed good in this # case), or has a state that isn't 'bad'. - if !needgoodtag && - (@tagstate[proposedtag].nil? || @tagstate[proposedtag] != 'bad') - tag = proposedtag + if @tagstate[proposedtag].nil? || @tagstate[proposedtag] !~ /^bad/ + tag = proposedtag + puts "Need !bad tag, proposed tag is #{@tagstate[proposedtag]} and thus acceptable" if (@debug) + break + else + puts "Need !bad tag, proposed tag is #{@tagstate[proposedtag]} and thus not acceptable" if (@debug) + end end - hourcounter += 1 end if tag.nil? @@ -82,45 +159,74 @@ # nvclient = NVentory::Client.new -results = nvclient.get_objects('nodes', {}, { 'name' => name }, {}, {}, ['node_groups']) +results = nvclient.get_objects(:objecttype => 'nodes', :exactget => { 'name' => name }, :includes => ['node_groups']) -tag = '' +tag = nil DEFAULT_HOURS = 4 hours = DEFAULT_HOURS if !results.empty? && !results[name].nil? - if !results[name]['config_management_tag'].nil? && - !results[name]['config_management_tag'].empty? - tag = results[name]['config_management_tag'] - else - if !results[name]['node_groups'].nil? - node_group_names = results[name]['node_groups'].collect { |ng| ng['name'] } - case - when node_group_names.include?('dev') || node_group_names.include?('int') - hours = 0 - when node_group_names.include?('qa') - hours = 1 - when node_group_names.include?('stg') - hours = 2 - end + if !results[name]['node_groups'].nil? + node_group_names = results[name]['node_groups'].collect { |ng| ng['name'] } + case + when node_group_names.include?('dev') || node_group_names.include?('int') + hours = 0 + when node_group_names.include?('qa') + hours = 1 + when node_group_names.include?('stg') + hours = 2 end - - # For production nodes we want to divide them based on our - # failover/BCP strategy so that we deploy changes in such a way that - # a bad change doesn't take out all failover groups at once. With - # multiple data centers and global load balancing this could mean - # deploying to one data center and then the other. Or you could base - # it on other node groups. In other words this section should set about - # half your machines to hours = 3, and then the remaining systems will - # get the default number of hours below. - if hours == DEFAULT_HOURS - # nVentory query for DC goes here + end + + # For production nodes we want to divide them based on our + # failover/BCP strategy so that we deploy changes in such a way that + # a bad change doesn't take out all failover groups at once. With + # multiple data centers and global load balancing this could mean + # deploying to one data center and then the other. Or you could base + # it on other node groups. In other words this section should set about + # half your machines to hours = 3, and then the remaining systems will + # get the default number of hours below. + if hours == DEFAULT_HOURS + if false # <-- YOU NEED TO PUT SITE-APPROPRIATE LOGIC HERE + hours = 3 end end + + puts "Based on node groups and location this node gets hours #{hours}" if (@debug) + + if !results[name]['config_mgmt_tag'].nil? && + !results[name]['config_mgmt_tag'].empty? + nvtag = results[name]['config_mgmt_tag'] + puts "Tag from nVentory: #{nvtag}" if (@debug) + # Tags starting with trunk- are used to temporarily bump clients to trunk + if nvtag =~ /^trunk-(\d{8})-(\d{4})$/ + tagunixtime = convert_tagtime_to_unixtime($1, $2) + puts "nVentory tag looks like a temporary bump to trunk tag" if (@debug) + puts "tagunixtime #{tagunixtime}" if (@debug) + # If the timestamp is less than "hours" old the client gets trunk, + # otherwise the client gets its normal tag based on "hours". Ignore + # timestamps in the future too, so if someone inserts something way in + # the future the client isn't stuck on trunk forever. + timelimit = Time.at(Time.now - 60 * 60 * hours) + puts "timelimit #{timelimit}" if (@debug) + puts "now #{Time.now}" if (@debug) + if tagunixtime > timelimit && tagunixtime <= Time.now + puts "Temporary bump to trunk tag is within acceptable time window" if (@debug) + tag = 'trunk' + else + puts "Temporary bump to trunk tag is outside acceptable time window" if (@debug) + end + else + puts "nVentory tag not a temporary bump to trunk tag, using tag as-is" if (@debug) + tag = nvtag + end + else + puts "No tag found in nVentory for this node" if (@debug) + end end if tag.nil? || tag.empty? - tag = findautotag(hours) + tag = File.join('tags', findautotag(hours)) end -puts File.join('tags', tag) +puts tag This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |