summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorelijah <elijah@riseup.net>2012-10-09 00:05:44 -0700
committerelijah <elijah@riseup.net>2012-10-09 00:05:44 -0700
commit73b126976ad7843eb47a84944cf191bf05b14216 (patch)
tree918656f8d7c637e8c7a8f0c010eff55bfd98ae1b /lib
parent578ac2f5dc7432317d7a022bed9d869ab89ee45c (diff)
fixed paths
Diffstat (limited to 'lib')
-rw-r--r--lib/leap_cli.rb19
-rw-r--r--lib/leap_cli/commands/README101
-rw-r--r--lib/leap_cli/commands/compile.rb15
-rw-r--r--lib/leap_cli/commands/deploy.rb20
-rw-r--r--lib/leap_cli/commands/init.rb24
-rw-r--r--lib/leap_cli/commands/list.rb61
-rw-r--r--lib/leap_cli/commands/pre.rb38
-rw-r--r--lib/leap_cli/config.rb119
-rw-r--r--lib/leap_cli/config_list.rb77
-rw-r--r--lib/leap_cli/config_manager.rb200
-rw-r--r--lib/leap_cli/init.rb74
-rw-r--r--lib/leap_cli/log.rb44
-rw-r--r--lib/leap_cli/path.rb79
-rw-r--r--lib/leap_cli/version.rb3
14 files changed, 874 insertions, 0 deletions
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 <directory>
+
+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 '<node filter>'
+ 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 '<directory>'
+ 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 = <<EOS
+#
+# Global provider definition file.
+#
+{
+ "domain": "example.org"
+}
+EOS
+
+ COMMON_CONTENT = <<EOS
+#
+# Options put here are inherited by all nodes.
+#
+{
+ "domain": "example.org"
+}
+EOS
+
+end
diff --git a/lib/leap_cli/log.rb b/lib/leap_cli/log.rb
new file mode 100644
index 0000000..f51ca1e
--- /dev/null
+++ b/lib/leap_cli/log.rb
@@ -0,0 +1,44 @@
+module LeapCli
+
+ def self.log_level
+ @log_level
+ end
+
+ def self.log_level=(value)
+ @log_level = value
+ end
+
+end
+
+def log0(message=nil, &block)
+ if message
+ puts message
+ elsif block
+ puts yield(block)
+ end
+end
+
+def log1(message=nil, &block)
+ if LeapCli.log_level > 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