From 73b126976ad7843eb47a84944cf191bf05b14216 Mon Sep 17 00:00:00 2001 From: elijah Date: Tue, 9 Oct 2012 00:05:44 -0700 Subject: fixed paths --- lib/leap_cli.rb | 19 ++++ lib/leap_cli/commands/README | 101 ++++++++++++++++++++ lib/leap_cli/commands/compile.rb | 15 +++ lib/leap_cli/commands/deploy.rb | 20 ++++ lib/leap_cli/commands/init.rb | 24 +++++ lib/leap_cli/commands/list.rb | 61 ++++++++++++ lib/leap_cli/commands/pre.rb | 38 ++++++++ lib/leap_cli/config.rb | 119 +++++++++++++++++++++++ lib/leap_cli/config_list.rb | 77 +++++++++++++++ lib/leap_cli/config_manager.rb | 200 +++++++++++++++++++++++++++++++++++++++ lib/leap_cli/init.rb | 74 +++++++++++++++ lib/leap_cli/log.rb | 44 +++++++++ lib/leap_cli/path.rb | 79 ++++++++++++++++ lib/leap_cli/version.rb | 3 + 14 files changed, 874 insertions(+) create mode 100644 lib/leap_cli.rb create mode 100644 lib/leap_cli/commands/README create mode 100644 lib/leap_cli/commands/compile.rb create mode 100644 lib/leap_cli/commands/deploy.rb create mode 100644 lib/leap_cli/commands/init.rb create mode 100644 lib/leap_cli/commands/list.rb create mode 100644 lib/leap_cli/commands/pre.rb create mode 100644 lib/leap_cli/config.rb create mode 100644 lib/leap_cli/config_list.rb create mode 100644 lib/leap_cli/config_manager.rb create mode 100644 lib/leap_cli/init.rb create mode 100644 lib/leap_cli/log.rb create mode 100644 lib/leap_cli/path.rb create mode 100644 lib/leap_cli/version.rb (limited to 'lib') diff --git a/lib/leap_cli.rb b/lib/leap_cli.rb new file mode 100644 index 0000000..b935e35 --- /dev/null +++ b/lib/leap_cli.rb @@ -0,0 +1,19 @@ +module LeapCli; end + +unless defined?(LeapCli::VERSION) + # ^^ I am not sure why this is needed. + require 'leap_cli/version.rb' +end + +require 'leap_cli/init' +require 'leap_cli/path' +require 'leap_cli/log' +require 'leap_cli/config' +require 'leap_cli/config_list' +require 'leap_cli/config_manager' + +unless String.method_defined?(:to_a) + class String + def to_a; [self]; end + end +end \ No newline at end of file diff --git a/lib/leap_cli/commands/README b/lib/leap_cli/commands/README new file mode 100644 index 0000000..00fcd84 --- /dev/null +++ b/lib/leap_cli/commands/README @@ -0,0 +1,101 @@ +This directory contains ruby source files that define the available sub-commands of the `leap` executable. + +For example, the command: + + leap init + +Lives in lib/leap_cli/commands/init.rb + +These files use a DSL (called GLI) for defining command suites. +See https://github.com/davetron5000/gli for more information. + + + c.command + c.commands + c.default_command + c.default_value + c.get_default_command + c.commands + c.commands_declaration_order + + c.flag + c.flags + c.switch + c.switches + + c.long_desc + + c.default_desc + c.default_description + c.desc + c.description + c.long_description + c.context_description + c.usage + + c.arg_name + c.arguments_description + c.arguments_options + + c.skips_post + c.skips_pre + c.skips_around + + c.action + + c.copy_options_to_aliases + c.nodoc + c.aliases + c.execute + c.names + + +#desc 'Describe some switch here' +#switch [:s,:switch] + +#desc 'Describe some flag here' +#default_value 'the default' +#arg_name 'The name of the argument' +#flag [:f,:flagname] + +# desc 'Describe deploy here' +# arg_name 'Describe arguments to deploy here' +# command :deploy do |c| +# c.action do |global_options,options,args| +# puts "deploy command ran" +# end +# end + +# desc 'Describe dryrun here' +# arg_name 'Describe arguments to dryrun here' +# command :dryrun do |c| +# c.action do |global_options,options,args| +# puts "dryrun command ran" +# end +# end + +# desc 'Describe add-node here' +# arg_name 'Describe arguments to add-node here' +# command :"add-node" do |c| +# c.desc 'Describe a switch to init' +# c.switch :s +# +# c.desc 'Describe a flag to init' +# c.default_value 'default' +# c.flag :f +# c.action do |global_options,options,args| +# puts "add-node command ran" +# end +# end + +# post do |global,command,options,args| +# # Post logic here +# # Use skips_post before a command to skip this +# # block on that command only +# end + +# on_error do |exception| +# # Error logic here +# # return false to skip default error handling +# true +# end diff --git a/lib/leap_cli/commands/compile.rb b/lib/leap_cli/commands/compile.rb new file mode 100644 index 0000000..6b38de5 --- /dev/null +++ b/lib/leap_cli/commands/compile.rb @@ -0,0 +1,15 @@ +module LeapCli + module Commands + + desc 'Compile json files to hiera configs' + command :compile do |c| + c.action do |global_options,options,args| + manager = ConfigManager.new + manager.load(Path.provider) + Path.ensure_dir(Path.hiera) + manager.export(Path.hiera) + end + end + + end +end \ No newline at end of file diff --git a/lib/leap_cli/commands/deploy.rb b/lib/leap_cli/commands/deploy.rb new file mode 100644 index 0000000..3694a38 --- /dev/null +++ b/lib/leap_cli/commands/deploy.rb @@ -0,0 +1,20 @@ +module LeapCli + module Commands + + desc 'Apply recipes to a node or set of nodes' + long_desc 'The node filter can be the name of a node, service, or tag.' + arg_name '' + command :deploy do |c| + c.action do |global_options,options,args| + nodes = ConfigManager.filter(args) + say "Deploying to these nodes: #{nodes.keys.join(', ')}" + if agree "Continue? " + say "deploy not yet implemented" + else + say "OK. Bye." + end + end + end + + end +end \ No newline at end of file diff --git a/lib/leap_cli/commands/init.rb b/lib/leap_cli/commands/init.rb new file mode 100644 index 0000000..75cc876 --- /dev/null +++ b/lib/leap_cli/commands/init.rb @@ -0,0 +1,24 @@ +module LeapCli + module Commands + desc 'Creates a new provider configuration directory.' + arg_name '' + skips_pre + command :init do |c| + c.action do |global_options,options,args| + directory = args.first + unless directory && directory.any? + help_now! "Directory name is required." + end + directory = File.expand_path(directory) + if File.exists?(directory) + raise "#{directory} already exists." + end + if agree("Create directory '#{directory}'? ") + LeapCli.init(directory) + else + puts "OK, bye." + end + end + end + end +end \ No newline at end of file diff --git a/lib/leap_cli/commands/list.rb b/lib/leap_cli/commands/list.rb new file mode 100644 index 0000000..a186049 --- /dev/null +++ b/lib/leap_cli/commands/list.rb @@ -0,0 +1,61 @@ +module LeapCli + module Commands + + def self.print_config_table(type, config_list) + style = {:border_x => '-', :border_y => ':', :border_i => '-', :width => 60} + + if type == :services + t = table do + self.style = style + self.headings = ['SERVICE', 'NODES'] + list = config_list.keys.sort + list.each do |name| + add_row [name, config_list[name].nodes.keys.join(', ')] + add_separator unless name == list.last + end + end + puts t + puts "\n\n" + elsif type == :tags + t = table do + self.style = style + self.headings = ['TAG', 'NODES'] + list = config_list.keys.sort + list.each do |name| + add_row [name, config_list[name].nodes.keys.join(', ')] + add_separator unless name == list.last + end + end + puts t + puts "\n\n" + elsif type == :nodes + t = table do + self.style = style + self.headings = ['NODE', 'SERVICES', 'TAGS'] + list = config_list.keys.sort + list.each do |name| + add_row [name, config_list[name].services.to_a.join(', '), config_list[name].tags.to_a.join(', ')] + add_separator unless name == list.last + end + end + puts t + end + end + + desc 'List nodes and their classifications' + long_desc 'Prints out a listing of nodes, services, or tags.' + arg_name 'filter' + command :list do |c| + c.action do |global_options,options,args| + if args.any? + print_config_table(:nodes, ConfigManager.filter(args)) + else + print_config_table(:services, ConfigManager.services) + print_config_table(:tags, ConfigManager.tags) + print_config_table(:nodes, ConfigManager.nodes) + end + end + end + + end +end diff --git a/lib/leap_cli/commands/pre.rb b/lib/leap_cli/commands/pre.rb new file mode 100644 index 0000000..ae58fc8 --- /dev/null +++ b/lib/leap_cli/commands/pre.rb @@ -0,0 +1,38 @@ + +# +# check to make sure we can find the root directory of the platform +# +module LeapCli + module Commands + + desc 'Verbosity level 0..2' + arg_name 'level' + default_value '0' + flag [:v, :verbose] + + desc 'Specify the root directory' + arg_name 'path' + default_value Path.root + flag [:root] + + pre do |global,command,options,args| + # + # set verbosity + # + LeapCli.log_level = global[:verbose].to_i + + # + # require a root directory + # + if global[:root] + Path.set_root(global[:root]) + end + if Path.ok? + true + else + exit_now!("Could not find the root directory. Change current working directory or try --root") + end + end + + end +end diff --git a/lib/leap_cli/config.rb b/lib/leap_cli/config.rb new file mode 100644 index 0000000..44e66be --- /dev/null +++ b/lib/leap_cli/config.rb @@ -0,0 +1,119 @@ +module LeapCli + # + # This class represents the configuration for a single node, service, or tag. + # + class Config < Hash + + def initialize(config_type, manager) + @manager = manager + @type = config_type + end + + # + # lazily eval dynamic values when we encounter them. + # + def [](key) + value = fetch(key, nil) + if value.is_a? Array + value + elsif value.nil? + nil + else + if value =~ /^= (.*)$/ + value = eval($1) + self[key] = value + end + value + end + end + + # + # make the type appear to be a normal Hash in yaml. + # + def to_yaml_type + "!map" + end + + # + # just like Hash#to_yaml, but sorted + # + def to_yaml(opts = {}) + YAML::quick_emit(self, opts) do |out| + out.map(taguri, to_yaml_style) do |map| + keys.sort.each do |k| + v = self.fetch(k) + map.add(k, v) + end + end + end + end + + # + # make obj['name'] available as obj.name + # + def method_missing(method, *args, &block) + if has_key?(method.to_s) + self[method.to_s] + else + super + end + end + + # + # convert self into a plain hash, but only include the specified keys + # + def to_h(*keys) + keys.map(&:to_s).inject({}) do |hsh, key| + if has_key?(key) + hsh[key] = self[key] + end + hsh + end + end + + def nodes + if @type == :node + @manager.nodes + else + @nodes ||= ConfigList.new + end + end + + def services + if @type == :node + self['services'] || [] + else + @manager.services + end + end + + def tags + if @type == :node + self['tags'] || [] + else + @manager.tags + end + end + + private + + ## + ## MACROS + ## these are methods used when eval'ing a value in the .json configuration + ## + + # + # inserts the contents of a file + # + def file(filename) + filepath = Path.find_file(name, filename) + if filepath + File.read(filepath) + else + log0('no such file, "%s"' % filename) + "" + end + end + + end # class +end # module \ No newline at end of file diff --git a/lib/leap_cli/config_list.rb b/lib/leap_cli/config_list.rb new file mode 100644 index 0000000..c8ff23b --- /dev/null +++ b/lib/leap_cli/config_list.rb @@ -0,0 +1,77 @@ +module LeapCli + class ConfigList < Hash + + def initialize(config=nil) + if config + self << config + end + end + + # + # if the key is a hash, we treat it as a condition and filter all the configs using the condition + # + # for example: + # + # nodes[:public_dns => true] + # + # will return a ConfigList with node configs that have public_dns set to true + # + def [](key) + if key.is_a? Hash + results = ConfigList.new + field, match_value = key.to_a.first + field = field.is_a?(Symbol) ? field.to_s : field + match_value = match_value.is_a?(Symbol) ? match_value.to_s : match_value + each do |name, config| + value = config[field] + if !value.nil? + if value.is_a? Array + if value.includes?(match_value) + results[name] = config + end + else + if value == match_value + results[name] = config + end + end + end + end + results + else + super + end + end + + def <<(config) + if config.is_a? ConfigList + self.merge!(config) + else + self[config['name']] = config + end + end + + # + # converts the hash of configs into an array of hashes, with ONLY the specified fields + # + def fields(*fields) + result = [] + keys.sort.each do |name| + result << self[name].to_h(*fields) + end + result + end + + # + # like fields(), but returns an array of values instead of an array of hashes. + # + def field(field) + field = field.to_s + result = [] + keys.sort.each do |name| + result << self[name][field] + end + result + end + + end +end diff --git a/lib/leap_cli/config_manager.rb b/lib/leap_cli/config_manager.rb new file mode 100644 index 0000000..d383cc1 --- /dev/null +++ b/lib/leap_cli/config_manager.rb @@ -0,0 +1,200 @@ +require 'oj' +require 'yaml' + +module LeapCli + + class ConfigManager + + attr_reader :services, :tags, :nodes + + ## + ## IMPORT EXPORT + ## + + # + # load .json configuration files + # + def load(dir) + @services = load_all_json("#{dir}/services/*.json") + @tags = load_all_json("#{dir}/tags/*.json") + @common = load_all_json("#{dir}/common.json")['common'] + @nodes = load_all_json("#{dir}/nodes/*.json", :node) + @nodes.each do |name, node| + apply_inheritance(node) + end + @nodes.each do |name, node| + node.each {|key,value| node[key] } # force evaluation of dynamic values + end + end + + # + # save compiled hiera .yaml files + # + def export(dir) + Dir.glob(dir + '/*.yaml').each do |f| + File.unlink(f) + end + @nodes.each do |name, node| + File.open("#{dir}/#{name}.#{node.domain_internal}.yaml", 'w') do |f| + f.write node.to_yaml + end + end + end + + ## + ## FILTERING + ## + + # + # returns a node list consisting only of nodes that satisfy the filter criteria. + # + # filter: condition [condition] [condition] [+condition] + # condition: [node_name | service_name | tag_name] + # + # if conditions is prefixed with +, then it works like an AND. Otherwise, it works like an OR. + # + def filter(filters) + if filters.empty? + return nodes + end + if filters[0] =~ /^\+/ + # don't let the first filter have a + prefix + filters[0] = filters[0][1..-1] + end + + node_list = ConfigList.new + filters.each do |filter| + if filter =~ /^\+/ + keep_list = nodes_for_filter(filter[1..-1]) + node_list.delete_if do |name, node| + if keep_list[name] + false + else + true + end + end + else + node_list << nodes_for_filter(filter) + end + end + return node_list + end + + ## + ## CLASS METHODS + ## + + def self.manager + @manager ||= begin + manager = ConfigManager.new + manager.load(Path.provider) + manager + end + end + + def self.filter(filters); manager.filter(filters); end + def self.nodes; manager.nodes; end + def self.services; manager.services; end + def self.tags; manager.tags; end + + private + + def load_all_json(pattern, config_type = :class) + results = ConfigList.new + Dir.glob(pattern).each do |filename| + obj = load_json(filename, config_type) + if obj + name = File.basename(filename).sub(/\.json$/,'') + obj['name'] = name + results[name] = obj + end + end + results + end + + def load_json(filename, config_type) + log2 { filename.sub(/^#{Regexp.escape(Path.root)}/,'') } + + # + # read file, strip out comments + # (File.read(filename) would be faster, but we like ability to have comments) + # + buffer = StringIO.new + File.open(filename) do |f| + while (line = f.gets) + next if line =~ /^\s*#/ + buffer << line + end + end + + # parse json, and flatten hash + begin + hash = Oj.load(buffer.string) || {} + rescue SyntaxError => exc + log0 'Error in file "%s":' % filename + log0 exc.to_s + return nil + end + return flatten_hash(hash, Config.new(config_type, self)) + end + + # + # remove all the nesting from a hash. + # + def flatten_hash(input = {}, output = {}, options = {}) + input.each do |key, value| + key = options[:prefix].nil? ? "#{key}" : "#{options[:prefix]}#{options[:delimiter]||"_"}#{key}" + if value.is_a? Hash + flatten_hash(value, output, :prefix => key, :delimiter => options[:delimiter]) + else + output[key] = value + end + end + output + end + + # + # makes this node inherit options from the common, service, and tag json files. + # + def apply_inheritance(node) + new_node = Config.new(:node, self) + new_node.merge!(@common) + if node['services'] + node['services'].sort.each do |node_service| + service = @services[node_service] + if service.nil? + log0('Error in node "%s": the service "%s" does not exist.' % [node['name'], node_service]) + else + new_node.merge!(service) + service.nodes << node # this is odd, but we want the node pointer, not new_node pointer. + end + end + end + if node['tags'] + node['tags'].sort.each do |node_tag| + tag = @tags[node_tag] + if tag.nil? + log0('Error in node "%s": the tag "%s" does not exist.' % [node['name'], node_tag]) + else + new_node.merge!(tag) + tag.nodes << node + end + end + end + new_node.merge!(node) + node.replace(new_node) + end + + def nodes_for_filter(filter) + if node = self.nodes[filter] + ConfigList.new(node) + elsif service = self.services[filter] + service.nodes + elsif tag = self.tags[filter] + tag.nodes + end + end + + end + +end diff --git a/lib/leap_cli/init.rb b/lib/leap_cli/init.rb new file mode 100644 index 0000000..bebede7 --- /dev/null +++ b/lib/leap_cli/init.rb @@ -0,0 +1,74 @@ +require 'fileutils' + +module LeapCli + # + # creates new provider directory + # + def self.init(directory) + dirs = [directory] + mkdirs(dirs, false, false) + + Dir.chdir(directory) do + dirs = ["nodes", "services", "keys", "tags"] + mkdirs(dirs, false, false) + + #puts "Creating .provider" + #FileUtils.touch('.provider') + + mkfile("provider.json", PROVIDER_CONTENT) + mkfile("common.json", COMMON_CONTENT) + end + end + + def self.mkfile(filename, content) + puts "Creating #{filename}" + File.open(filename, 'w') do |f| + f.write content + end + end + + def self.mkdirs(dirs,force,dry_run) + exists = false + if !force + dirs.each do |dir| + if File.exist? dir + raise "#{dir} exists; use --force to override" + exists = true + end + end + end + if !exists + dirs.each do |dir| + puts "Creating #{dir}/" + if dry_run + puts "dry-run; #{dir} not created" + else + FileUtils.mkdir_p dir + end + end + else + puts "Exiting..." + return false + end + true + end + + PROVIDER_CONTENT = < 0 + if message + puts message + elsif block + puts yield(block) + end + end +end + +def log2(message=nil, &block) + if LeapCli.log_level > 1 + if message + puts message + elsif block + puts yield(block) + end + end +end + +def help!(message=nil) + ENV['GLI_DEBUG'] = "false" + help_now!(message) +end diff --git a/lib/leap_cli/path.rb b/lib/leap_cli/path.rb new file mode 100644 index 0000000..5dc8fe8 --- /dev/null +++ b/lib/leap_cli/path.rb @@ -0,0 +1,79 @@ +require 'fileutils' + +module LeapCli + module Path + + def self.root + @root ||= File.expand_path("#{provider}/..") + end + + def self.platform + @platform ||= File.expand_path("#{root}/leap_platform") + end + + def self.provider + @provider ||= if @root + File.expand_path("#{root}/provider") + else + find_in_directory_tree('provider.json') + end + end + + def self.hiera + @hiera ||= "#{provider}/hiera" + end + + def self.files + @files ||= "#{provider}/files" + end + + def self.ok? + provider != '/' + end + + def self.set_root(root_path) + @root = File.expand_path(root_path) + raise "No such directory '#{@root}'" unless File.directory?(@root) + end + + def self.ensure_dir(dir) + unless File.directory?(dir) + if File.exists?(dir) + raise 'Unable to create directory "%s", file already exists.' % dir + else + FileUtils.mkdir_p(dir) + end + end + end + + def self.find_file(name, filename) + path = [Path.files, filename].join('/') + return path if File.exists?(path) + path = [Path.files, name, filename].join('/') + return path if File.exists?(path) + path = [Path.files, 'nodes', name, filename].join('/') + return path if File.exists?(path) + path = [Path.files, 'services', name, filename].join('/') + return path if File.exists?(path) + path = [Path.files, 'tags', name, filename].join('/') + return path if File.exists?(path) + + # give up + return nil + end + + private + + def self.find_in_directory_tree(filename) + search_dir = Dir.pwd + while search_dir != "/" + Dir.foreach(search_dir) do |f| + return search_dir if f == filename + end + search_dir = File.dirname(search_dir) + end + return search_dir + end + + end +end diff --git a/lib/leap_cli/version.rb b/lib/leap_cli/version.rb new file mode 100644 index 0000000..c272647 --- /dev/null +++ b/lib/leap_cli/version.rb @@ -0,0 +1,3 @@ +module LeapCli + VERSION = '0.0.1' +end -- cgit v1.2.3