%w( v1 ).each do |lib|
require "scooter/httpdispatchers/classifier/v1/#{lib}"
end
module Scooter
module HttpDispatchers
# Methods added here are not representative of endpoints, but are more
# generalized to be helpers to to acquire/transform data, such as getting
# the uuid of a node group based on the name. Be cautious about using
# these methods if you are utilizing a dispatcher with credentials;
# the user is not guaranteed to have privileges for all the methods
# defined here, or the user may not be signed in. If you have a method
# defined here that is using the connection object directly, you should
# probably be using a method defined in the version module instead.
module Classifier
include Scooter::HttpDispatchers::Classifier::V1
include Scooter::Utilities
Rootuuid = '00000000-0000-4000-8000-000000000000'
def set_classifier_path(connection=self.connection)
set_url_prefix
connection.url_prefix.path = '/classifier-api'
end
# This returns a tree-like hash of all node groups in the classifier; each
# key is a uuid, each value is an array of direct children. If no direct
# children are found, the value is an empty array. This representation
# is useful for iterating over specific children of known node groups,
# and is using primarily in delete_node_group_descendents and
# delete_tree_recursion.
# === Example return hash for default PE Installation
# { "00000000-0000-4000-8000-000000000000" => # root group with one child
# ["6d37be98-42ee-400d-a66e-ebced989546c"],
# "6d37be98-42ee-400d-a66e-ebced989546c" => # node group with 5 kids
# ["42e385ca-8fb2-442d-a0af-3b14c86d321b",
# "32e6a36d-59ad-44ea-8708-feb910907058",
# "44ebbb50-501c-45f1-8c92-95d5b0313d24",
# "58448b7d-3175-4695-ac32-31cf4ee25754",
# "00350026-bfb6-4ce7-bd06-1a1cfea445f9",
# "b6400234-2a61-4417-b85e-c2dcc123686b"],
# "42e385ca-8fb2-442d-a0af-3b14c86d321b" => [], # childless node group
# "32e6a36d-59ad-44ea-8708-feb910907058" => [],
# "44ebbb50-501c-45f1-8c92-95d5b0313d24" => [],
# "58448b7d-3175-4695-ac32-31cf4ee25754" => [],
# "00350026-bfb6-4ce7-bd06-1a1cfea445f9" => [],
# "b6400234-2a61-4417-b85e-c2dcc123686b" => [] }
def get_node_group_trees_of_direct_descendents
node_groups = get_list_of_node_groups
groups = node_groups.map { |each| [each['id'], each['parent']] }
# Constants for the array of tuples just created.
id = 0
parent = 1
# Create root node and insert it into the tree hash.
rootindex = groups.find_index { |e| e[id] == e[parent] }
rootid = (groups.delete_at(rootindex))[id]
tree = Object::Hash.new
tree[rootid] = Object::Array.new
# Construct the rest of the tree as a hash of
# id => [ child1, child2,...] nodes.
groups.each do |g|
tree[g[id]] = Object::Array.new
if tree.has_key?(g[parent]) then
tree[g[parent]] << g[id]
else
tree[g[parent]] = Object::Array.new
tree[g[parent]] << g[id]
end
end
tree
end
# This method deletes all the descendents of the Rootuuid; the root group
# can never be deleted. If you are looking to clean out a system entirely,
# consider using import_baseline_hierarchy instead, as this
# method doesn't clean out any classes or other settings the root group
# might have.
def delete_all_node_groups
delete_node_group_descendents(get_node_group(Rootuuid))
end
# This takes an optional hash of node group parameters, and auto-fills
# any required keys for node group generation. It returns the response
# body from the server.
def create_new_node_group_model(options={})
# name, classes, parent are the only required keys
name = options['name'] || RandomString.generate
classes = options['classes'] || {}
parent = options['parent'] || Rootuuid
rule = options['rule']
id = options['id']
environment = options['environment']
variables = options['variables']
description = options['description']
environment_trumps = options['environment_trumps']
hash = { "name" => name,
"parent" => parent,
"classes" => classes }
if environment_trumps
hash['environment_trumps'] = environment_trumps
end
if rule
hash['rule'] = rule
end
if environment
hash['environment'] = environment
end
if variables
hash['variables'] = variables
end
if description
hash['description'] = description
end
if id
hash['id'] = id
end
create_node_group(hash).env.body
end
alias_method :generate_node_group, :create_new_node_group_model
# This takes an optional hash of node group parameters. If a "name" option
# is provided it will attempt to find an existing node group with that
# name in the environment specified in the "environment" option (default:
# "production").
#
# If an existing node group is found, it will be updated according to the
# provided options (via `#replace_node_group_with_update_hash`).
#
# If no existing node group is found, the options hash will be used to
# create a new node group (via `#create_new_node_group_model`).
def find_or_create_node_group_model(options={})
options["environment"] ||= "production"
if options["name"] && existing = get_node_group_by_name(options["name"], options["environment"])
replace_node_group_with_update_hash(existing, options)
else
create_new_node_group_model(options)
end
end
# If for some reason your node group model is out of sync with the
# server's state for that node group, you can use this method to just
# update your model with the server state.
def refresh_node_group_model(node_group_model)
get_node_group(node_group_model['id'])
end
# This will delete anything that inherits from the node group specified,
# but not the actual node group itself.
def delete_node_group_descendents(node_group_model)
id = node_group_model['id']
tree = get_node_group_trees_of_direct_descendents
tree[id].each do |childid|
delete_tree_recursion(tree, childid)
end
end
# This will return a node group hash given the name of the node group. It
# defaults to production for the environment, since node names can be
# the same for different environments.
def get_node_group_by_name(name, environment='production')
nodes = get_list_of_node_groups
nodes.each do |node|
if node['name'] == name && node['environment'] == environment
return node
end
end
nil # return nil if no matching name found
end
# This will return the node group id given the name of the node group. It
# defaults to production for the environment, since node names can be
# the same for different environments.
def get_node_group_id_by_name(name, environment='production')
nodes = get_list_of_node_groups
nodes.each do |node|
if node['name'] == name && node['environment'] == environment
return node['id']
end
end
nil # return nil if no matching name found
end
# The tree parameter required here is generated from the method
# get_node_group_trees_with_direct_descendents. This method
# will also delete the node_group_id as well.
def delete_tree_recursion(tree, node_group_id)
tree[node_group_id].each do |childid|
delete_tree_recursion(tree, childid)
end
#protect against trying to delete the Rootuuid
delete_node_group(node_group_id) if node_group_id != Rootuuid
end
# This method imports a bare root group into the NC, cleaning out and
# deleting any node groups that might have been available. Consider
# using this or delete_all_node_groups at the beginning of your
# test, depending on requirements of the test.
def import_baseline_hierarchy
hierarchy = [{ "environment_trumps" => false,
"parent" => Rootuuid,
"name" => "default",
"rule" => ["and", ["~", "name", ".*"]],
"variables" => {},
"id" => Rootuuid,
"environment" => "production",
"classes" => {} }]
import_hierarchy(hierarchy)
end
# This doesn't have a home right now, so it will exist in this module
# until it has a proper home.
def deep_merge(group, update_hash)
# TODO : This doesn't work if v is ever an array. Needs to be
# reimplemented a la is_deep_subset?
update_hash.each do |k, v|
if v.is_a? Hash
group[k] ||= {}
deep_merge(group[k], update_hash[k])
else
group[k] = update_hash[k]
end
end
end
private :deep_merge
def remove_nil_values(hash)
hash.each do |k, v|
case v
when Hash
remove_nil_values(v)
else
hash.delete(k) if v == nil
end
end
end
private :remove_nil_values
# This uses a PUTs instead of a POST to update a node group; when using
# PUTs, it will delete and replace the entire node group instead of just
# updating the keys provided.
def replace_node_group_with_update_hash(node_group_model, update_hash)
merged_model = node_group_model.merge(update_hash)
replace_node_group(merged_model['id'], merged_model)
# no verification of the response for now, will need to write some code
# that verifies this and takes care of array ordering
end
# This uses the POST method to update a node group; when using POST, it
# will only send and update the specified keys.
def update_node_group_with_node_group_model(node_group_model, update_hash)
id = node_group_model['id']
response = update_node_group(id, update_hash)
deep_merge(node_group_model, update_hash)
node_group_model = remove_nil_values(node_group_model)
# check to see if the update hash had any class changes that require
# transforms to the groups['deleted'] object
if node_group_model['deleted'] && node_group_model['classes'] != {} && update_hash['classes']
update_hash['classes'].each do |classname, parameters|
if node_group_model['deleted'][classname] == nil
next
end
parameters.each do |parameter, value|
if value == nil
node_group_model['deleted'][classname].delete(parameter)
else
node_group_model['deleted'][classname][parameter]['value'] = value
end
end
end
end
# check to see if we need to delete any classes from the model's
# node_group_model{'deleted'] key
if node_group_model['deleted']
node_group_model['deleted'].each do |classname, parameters|
if node_group_model['classes'][classname] == nil || node_group_model['deleted'][classname].keys == ['puppetlabs.classifier/deleted']
node_group_model['deleted'].delete(classname)
end
end
node_group_model.delete('deleted') if node_group_model['deleted'] == {}
end
if node_group_model != response.env.body
raise "node_group_model did not match the server response:\n#{node_group_model}\n#{response.env.body}"
end
# If we got this far, return the "model" hash.
node_group_model
end
# Used to compare replica classifier to master. Raises exception if it does not match.
# @param [String] host_name
def classifier_database_matches_self?(replica_host)
original_host_name = host.host_hash[:vmhostname]
begin
host.host_hash[:vmhostname] = replica_host.hostname
other_nodes = get_list_of_nodes
other_classes = get_list_of_classes
other_environments = get_list_of_environments
other_groups = get_list_of_node_groups
ensure
host.host_hash[:vmhostname] = original_host_name
end
self_nodes = get_list_of_nodes
self_classes = get_list_of_classes
self_environments = get_list_of_environments
self_groups = get_list_of_node_groups
nodes_match = nodes_match?(other_nodes, self_nodes)
classes_match = classes_match?(other_classes, self_classes)
environments_match = environments_match?(other_environments, self_environments)
groups_match = groups_match?(other_groups, self_groups)
errors = ''
errors << "Nodes do not match\r\n" unless nodes_match
errors << "Classes do not match\r\n" unless classes_match
errors << "Environments do not match\r\n" unless environments_match
errors << "Groups do not match\r\n" unless groups_match
host.logger.warn(errors.chomp) unless errors.empty?
errors.empty?
end
# Check to see if all nodes match between two query responses
# @param [Object] other_nodes - response from get_list_of_nodes
# @param [Object] self_nodes - response from get_list_of_nodes
# @return [Boolean]
def nodes_match?(other_nodes, self_nodes=nil)
self_nodes = get_list_of_nodes if self_nodes.nil?
return other_nodes == self_nodes
end
# Check to see if all classes match between two query responses
# @param [Object] other_classes - response from get_list_of_classes
# @param [Object] self_classes - response from get_list_of_classes
# @return [Boolean]
def classes_match?(other_classes, self_classes=nil)
self_classes = get_list_of_classes if self_classes.nil?
return other_classes == self_classes
end
# Check to see if all groups match between two query responses
# @param [Object] other_groups - response from get_list_of_node_groups
# @param [Object] self_groups - response from get_list_of_node_groups
# @return [Boolean]
def groups_match?(other_groups, self_groups=nil)
self_groups = get_list_of_node_groups if self_groups.nil?
return other_groups == self_groups
end
# Check to see if all environments match between two query responses
# @param [Object] other_environments - response from get_list_of_environments
# @param [Object] self_environments - response from get_list_of_environments
# @return [Boolean]
def environments_match?(other_environments, self_environments=nil)
self_environments = get_list_of_environments if self_environments.nil?
return other_environments == self_environments
end
end
end
end