Last active
September 14, 2020 21:09
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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