Skip to content

Instantly share code, notes, and snippets.

@brandt
Last active September 14, 2020 21:09
Show Gist options
  • Save brandt/657f001909b2046dea1a to your computer and use it in GitHub Desktop.
Save brandt/657f001909b2046dea1a to your computer and use it in GitHub Desktop.
Demonstration of a bug where a node is incorrectly indexed as having a role that it does not
#!/usr/bin/env ruby
# POC for: https://github.com/chef/chef-server/issues/718
# Demonstration of a bug where a node is incorrectly indexed as having a role that it does not.
#
# 1. chef-expander munges and flattens the node hash
#
# 2. if the node has "role" as a nested keys, it will be promoted and merged with the top
#
# 3. SOLR interprets this as the node having that role, so the node is incorrectly listed in the results of `knife search role:guest`
#
#
# The danger is if you happen to have a collision with a real role name. Then, a command like this would execute on unexpected servers:
#
# knife ssh "role:guest" "sudo service foo stop"
#
#
# The issue described above was probably the cause of: https://tickets.opscode.com/browse/CHEF-2902
#
require 'fast_xs' # gem install fast_xs -v 0.7.3
require 'json'
require 'pp'
module Chef
module Expander
# From: https://github.com/chef/chef-expander/blob/14b11a/lib/chef/expander/flattener.rb
# Flattens and expands nested Hashes representing Chef objects
# (e.g, Nodes, Roles, DataBagItems, etc.) into flat Hashes so the
# objects are suitable to be saved into Solr. This code is more or
# less copy-pasted from chef/solr/index which may or may not be a
# great idea, though that does minimize the dependencies and
# hopefully minimize the memory use of chef-expander.
class Flattener
UNDERSCORE = '_'
X = 'X'
X_CHEF_id_CHEF_X = 'X_CHEF_id_CHEF_X'
X_CHEF_database_CHEF_X = 'X_CHEF_database_CHEF_X'
X_CHEF_type_CHEF_X = 'X_CHEF_type_CHEF_X'
def initialize(item)
@item = item
end
def flattened_item
@flattened_item || flatten_and_expand
end
def flatten_and_expand
@flattened_item = Hash.new {|hash, key| hash[key] = []}
@item.each do |key, value|
flatten_each([key.to_s], value)
end
@flattened_item.each_value { |values| values.uniq! }
@flattened_item
end
def flatten_each(keys, values)
case values
when Hash
values.each do |child_key, child_value|
add_field_value(keys, child_key)
flatten_each(keys + [child_key.to_s], child_value)
end
when Array
values.each { |child_value| flatten_each(keys, child_value) }
else
add_field_value(keys, values)
end
end
def add_field_value(keys, value)
value = value.to_s
@flattened_item[keys.join(UNDERSCORE)] << value
@flattened_item[keys.last] << value
end
end
# Modified version of: https://github.com/chef/chef-expander/blob/14b11a/lib/chef/expander/solrizer.rb#L158-L208
class Solrizer
ADD = "add"
DELETE = "delete"
SKIP = "skip"
ITEM = "item"
ID = "id"
TYPE = "type"
DATABASE = "database"
ENQUEUED_AT = "enqueued_at"
DATA_BAG_ITEM = "data_bag_item"
DATA_BAG = "data_bag"
X_CHEF_id_CHEF_X = 'X_CHEF_id_CHEF_X'
X_CHEF_database_CHEF_X = 'X_CHEF_database_CHEF_X'
X_CHEF_type_CHEF_X = 'X_CHEF_type_CHEF_X'
CONTENT_TYPE_XML = {"Content-Type" => "text/xml"}
START_XML = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
ADD_DOC = "<add><doc>"
DELETE_DOC = "<delete>"
ID_OPEN = "<id>"
ID_CLOSE = "</id>"
END_ADD_DOC = "</doc></add>\n"
END_DELETE = "</delete>\n"
START_CONTENT = '<field name="content">'
CLOSE_FIELD = "</field>"
FLD_CHEF_ID_FMT = '<field name="X_CHEF_id_CHEF_X">%s</field>'
FLD_CHEF_DB_FMT = '<field name="X_CHEF_database_CHEF_X">%s</field>'
FLD_CHEF_TY_FMT = '<field name="X_CHEF_type_CHEF_X">%s</field>'
FLD_DATA_BAG = '<field name="data_bag">%s</field>'
KEYVAL_FMT = "%s__=__%s "
# Takes a flattened hash where the values are arrays and converts it into
# a dignified XML document suitable for POST to Solr.
# The general structure of the output document is like this:
# <?xml version="1.0" encoding="UTF-8"?>
# <add>
# <doc>
# <field name="content">
# key__=__value
# key__=__another_value
# other_key__=__yet another value
# </field>
# </doc>
# </add>
# The document as generated has minimal newlines and formatting, however.
def self.pointyize_add(flattened_object)
xml = ""
xml << START_XML << ADD_DOC
xml << (FLD_CHEF_ID_FMT % @obj_id)
xml << (FLD_CHEF_DB_FMT % @database)
xml << (FLD_CHEF_TY_FMT % @obj_type)
xml << START_CONTENT
content = ""
flattened_object.each do |field, values|
values.each do |v|
content << (KEYVAL_FMT % [field, v])
end
end
xml << content.fast_xs
xml << CLOSE_FIELD # ends content
xml << (FLD_DATA_BAG % @data_bag.fast_xs) if @data_bag
xml << END_ADD_DOC
# @xml_time = Time.now.to_f - @start_time
xml
end
end
end
end
obj = JSON.parse(DATA.read)
warn "After Flattener:"
warn "----------------------------------------"
e = Chef::Expander::Flattener.new(obj).flattened_item
pp e
warn ""
warn "After Solrizer (roughly):"
warn "----------------------------------------"
s = Chef::Expander::Solrizer.pointyize_add(e)
puts s
__END__
{
"name": "i-f09a939b",
"json_class": "Chef::Node",
"automatic": {
"domain": "opscode.us",
"os": "linux",
"virtualization": {
"role": "guest",
"emulator": "xen"
},
"hostname": "lb-prod-i-f09a939b",
"platform": "ubuntu"
},
"normal": {
"apache": {
"user": "www-data",
"keepalive": "On"
}
},
"chef_type": "node",
"default": {
"ha_role": "secondary",
"opscode_lb_heartbeat": {
"heartbeatsecret": "WIaVjtQurY0Q7sti"
},
"red_zookeeper_mp": {
"role": "zookeeper_mp"
},
"app_environment": "production"
},
"override": {
"filesystem": {},
"opscode_lb_type": "external",
"rabbitmq": {
"users": {
"chef": "Tat9THLuiOkdtyyH5VVf"
}
}
},
"run_list": [
"role[production]",
"role[ha-secondary]",
"recipe[aws]"
],
"_rev": "147-bb4f74122079d1feac675c1b2c57bfcc"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment