aboutsummaryrefslogtreecommitdiff
path: root/lib/leap_cli
diff options
context:
space:
mode:
Diffstat (limited to 'lib/leap_cli')
-rw-r--r--lib/leap_cli/commands/ca.rb106
-rw-r--r--lib/leap_cli/commands/compile.rb11
-rw-r--r--lib/leap_cli/commands/deploy.rb163
-rw-r--r--lib/leap_cli/commands/env.rb53
-rw-r--r--lib/leap_cli/commands/facts.rb4
-rw-r--r--lib/leap_cli/commands/inspect.rb2
-rw-r--r--lib/leap_cli/commands/list.rb18
-rw-r--r--lib/leap_cli/commands/node.rb44
-rw-r--r--lib/leap_cli/commands/pre.rb29
-rw-r--r--lib/leap_cli/commands/shell.rb17
-rw-r--r--lib/leap_cli/commands/test.rb4
-rw-r--r--lib/leap_cli/commands/user.rb16
-rw-r--r--lib/leap_cli/commands/vagrant.rb2
-rw-r--r--lib/leap_cli/config/filter.rb151
-rw-r--r--lib/leap_cli/config/macros.rb514
-rw-r--r--lib/leap_cli/config/manager.rb149
-rw-r--r--lib/leap_cli/config/object.rb113
-rw-r--r--lib/leap_cli/config/object_list.rb90
-rw-r--r--lib/leap_cli/config/tag.rb7
-rw-r--r--lib/leap_cli/constants.rb7
-rw-r--r--lib/leap_cli/exceptions.rb24
-rw-r--r--lib/leap_cli/leapfile.rb70
-rw-r--r--lib/leap_cli/log.rb2
-rw-r--r--lib/leap_cli/logger.rb2
-rw-r--r--lib/leap_cli/markdown_document_listener.rb2
-rw-r--r--lib/leap_cli/path.rb26
-rw-r--r--lib/leap_cli/remote/leap_plugin.rb4
-rw-r--r--lib/leap_cli/remote/puppet_plugin.rb2
-rw-r--r--lib/leap_cli/remote/rsync_plugin.rb2
-rw-r--r--lib/leap_cli/remote/tasks.rb2
-rw-r--r--lib/leap_cli/requirements.rb19
-rw-r--r--lib/leap_cli/ssh_key.rb1
-rw-r--r--lib/leap_cli/util.rb2
-rw-r--r--lib/leap_cli/util/remote_command.rb12
-rw-r--r--lib/leap_cli/util/secret.rb2
-rw-r--r--lib/leap_cli/util/x509.rb5
-rw-r--r--lib/leap_cli/version.rb4
37 files changed, 843 insertions, 838 deletions
diff --git a/lib/leap_cli/commands/ca.rb b/lib/leap_cli/commands/ca.rb
index 46e3494..579e305 100644
--- a/lib/leap_cli/commands/ca.rb
+++ b/lib/leap_cli/commands/ca.rb
@@ -1,6 +1,6 @@
-require 'openssl'
-require 'certificate_authority'
-require 'date'
+autoload :OpenSSL, 'openssl'
+autoload :CertificateAuthority, 'certificate_authority'
+autoload :Date, 'date'
require 'digest/md5'
module LeapCli; module Commands
@@ -36,6 +36,7 @@ module LeapCli; module Commands
nodes = manager.filter!(args)
nodes.each_node do |node|
+ warn_if_commercial_cert_will_soon_expire(node)
if !node.x509.use
remove_file!([:node_x509_key, node.name])
remove_file!([:node_x509_cert, node.name])
@@ -81,9 +82,19 @@ module LeapCli; module Commands
# http://www.redkestrel.co.uk/Articles/CSR.html
#
cert.desc "Creates a CSR for use in buying a commercial X.509 certificate."
- cert.long_desc "Unless specified, the CSR is created for the provider's primary domain. The properties used for this CSR come from `provider.ca.server_certificates`."
+ cert.long_desc "Unless specified, the CSR is created for the provider's primary domain. "+
+ "The properties used for this CSR come from `provider.ca.server_certificates`, "+
+ "but may be overridden here."
cert.command :csr do |csr|
csr.flag 'domain', :arg_name => 'DOMAIN', :desc => 'Specify what domain to create the CSR for.'
+ csr.flag ['organization', 'O'], :arg_name => 'ORGANIZATION', :desc => "Override default O in distinguished name."
+ csr.flag ['unit', 'OU'], :arg_name => 'UNIT', :desc => "Set OU in distinguished name."
+ csr.flag 'email', :arg_name => 'EMAIL', :desc => "Set emailAddress in distinguished name."
+ csr.flag ['locality', 'L'], :arg_name => 'LOCALITY', :desc => "Set L in distinguished name."
+ csr.flag ['state', 'ST'], :arg_name => 'STATE', :desc => "Set ST in distinguished name."
+ csr.flag ['country', 'C'], :arg_name => 'COUNTRY', :desc => "Set C in distinguished name."
+ csr.flag :bits, :arg_name => 'BITS', :desc => "Override default certificate bit length"
+ csr.flag :digest, :arg_name => 'DIGEST', :desc => "Override default signature digest"
csr.action do |global_options,options,args|
assert_config! 'provider.domain'
assert_config! 'provider.name'
@@ -97,24 +108,25 @@ module LeapCli; module Commands
# RSA key
keypair = CertificateAuthority::MemoryKeyMaterial.new
- log :generating, "%s bit RSA key" % server_certificates.bit_size do
- keypair.generate_key(server_certificates.bit_size)
+ bit_size = (options[:bits] || server_certificates.bit_size).to_i
+ log :generating, "%s bit RSA key" % bit_size do
+ keypair.generate_key(bit_size)
write_file! [:commercial_key, domain], keypair.private_key.to_pem
end
# CSR
dn = CertificateAuthority::DistinguishedName.new
- csr = CertificateAuthority::SigningRequest.new
- dn.common_name = domain
- dn.organization = provider.name[provider.default_language]
- dn.country = server_certificates['country'] # optional
- dn.state = server_certificates['state'] # optional
- dn.locality = server_certificates['locality'] # optional
-
- log :generating, "CSR with commonName => '%s', organization => '%s'" % [dn.common_name, dn.organization] do
- csr.distinguished_name = dn
- csr.key_material = keypair
- csr.digest = server_certificates.digest
+ dn.common_name = domain
+ dn.organization = options[:organization] || provider.name[provider.default_language]
+ dn.ou = options[:organizational_unit] # optional
+ dn.email_address = options[:email] # optional
+ dn.country = options[:country] || server_certificates['country'] # optional
+ dn.state = options[:state] || server_certificates['state'] # optional
+ dn.locality = options[:locality] || server_certificates['locality'] # optional
+
+ digest = options[:digest] || server_certificates.digest
+ log :generating, "CSR with #{digest} digest and #{print_dn(dn)}" do
+ csr = create_csr(dn, keypair, digest)
request = csr.to_x509_csr
write_file! [:commercial_csr, domain], csr.to_pem
end
@@ -191,7 +203,7 @@ module LeapCli; module Commands
return true
else
cert = load_certificate_file([:node_x509_cert, node.name])
- if cert.not_after < months_from_yesterday(1)
+ if cert.not_after < months_from_yesterday(2)
log :updating, "cert for node '#{node.name}' because it will expire soon"
return true
end
@@ -222,6 +234,18 @@ module LeapCli; module Commands
return false
end
+ def warn_if_commercial_cert_will_soon_expire(node)
+ dns_names_for_node(node).each do |domain|
+ if file_exists?([:commercial_cert, domain])
+ cert = load_certificate_file([:commercial_cert, domain])
+ if cert.not_after < months_from_yesterday(2)
+ log :warning, "the commercial certificate '#{Path.relative_path([:commercial_cert, domain])}' will expire soon. "+
+ "You should renew it with `leap cert csr --domain #{domain}`."
+ end
+ end
+ end
+ end
+
def generate_cert_for_node(node)
return if node.x509.use == false
@@ -262,6 +286,43 @@ module LeapCli; module Commands
yield cert.key_material.private_key.to_pem, cert.to_pem
end
+ #
+ # creates a CSR and returns it.
+ # with the correct extReq attribute so that the CA
+ # doens't generate certs with extensions we don't want.
+ #
+ def create_csr(dn, keypair, digest)
+ csr = CertificateAuthority::SigningRequest.new
+ csr.distinguished_name = dn
+ csr.key_material = keypair
+ csr.digest = digest
+
+ # define extensions manually (library doesn't support setting these on CSRs)
+ extensions = []
+ extensions << CertificateAuthority::Extensions::BasicConstraints.new.tap {|basic|
+ basic.ca = false
+ }
+ extensions << CertificateAuthority::Extensions::KeyUsage.new.tap {|keyusage|
+ keyusage.usage = ["digitalSignature", "keyEncipherment"]
+ }
+ extensions << CertificateAuthority::Extensions::ExtendedKeyUsage.new.tap {|extkeyusage|
+ extkeyusage.usage = [ "serverAuth"]
+ }
+
+ # convert extensions to attribute 'extReq'
+ # aka "Requested Extensions"
+ factory = OpenSSL::X509::ExtensionFactory.new
+ attrval = OpenSSL::ASN1::Set([OpenSSL::ASN1::Sequence(
+ extensions.map{|e| factory.create_ext(e.openssl_identifier, e.to_s, e.critical)}
+ )])
+ attrs = [
+ OpenSSL::X509::Attribute.new("extReq", attrval),
+ ]
+ csr.attributes = attrs
+
+ return csr
+ end
+
def ca_root
@ca_root ||= begin
load_certificate_file(:ca_cert, :ca_key)
@@ -406,6 +467,15 @@ module LeapCli; module Commands
cert_serial_number(domain_name).to_s(36)
end
+ # prints CertificateAuthority::DistinguishedName fields
+ def print_dn(dn)
+ fields = {}
+ [:common_name, :locality, :state, :country, :organization, :organizational_unit, :email_address].each do |attr|
+ fields[attr] = dn.send(attr) if dn.send(attr)
+ end
+ fields.inspect
+ end
+
##
## TIME HELPERS
##
diff --git a/lib/leap_cli/commands/compile.rb b/lib/leap_cli/commands/compile.rb
index 63c2047..644ce2a 100644
--- a/lib/leap_cli/commands/compile.rb
+++ b/lib/leap_cli/commands/compile.rb
@@ -5,9 +5,18 @@ module LeapCli
desc "Compile generated files."
command :compile do |c|
c.desc 'Compiles node configuration files into hiera files used for deployment.'
+ c.arg_name 'ENVIRONMENT', :optional => true
c.command :all do |all|
all.action do |global_options,options,args|
- compile_hiera_files
+ environment = args.first
+ if !LeapCli.leapfile.environment.nil? && !environment.nil? && environment != LeapCli.leapfile.environment
+ bail! "You cannot specify an ENVIRONMENT argument while the environment is pinned."
+ end
+ if environment && manager.environment_names.include?(environment)
+ compile_hiera_files(manager.filter([environment]))
+ else
+ compile_hiera_files(manager.filter)
+ end
end
end
diff --git a/lib/leap_cli/commands/deploy.rb b/lib/leap_cli/commands/deploy.rb
index 814407f..6589837 100644
--- a/lib/leap_cli/commands/deploy.rb
+++ b/lib/leap_cli/commands/deploy.rb
@@ -17,9 +17,12 @@ module LeapCli
# --force
c.switch :force, :desc => 'Deploy even if there is a lockfile.', :negatable => false
+ # --dev
+ c.switch :dev, :desc => "Development mode: don't run 'git submodule update' before deploy.", :negatable => false
+
# --tags
c.flag :tags, :desc => 'Specify tags to pass through to puppet (overriding the default).',
- :default_value => DEFAULT_TAGS.join(','), :arg_name => 'TAG[,TAG]'
+ :arg_name => 'TAG[,TAG]'
c.flag :port, :desc => 'Override the default SSH port.',
:arg_name => 'PORT'
@@ -28,9 +31,12 @@ module LeapCli
:arg_name => 'IPADDRESS'
c.action do |global,options,args|
- init_submodules
- nodes = filter_deploy_nodes(args)
+ if options[:dev] != true
+ init_submodules
+ end
+
+ nodes = manager.filter!(args)
if nodes.size > 1
say "Deploying to these nodes: #{nodes.keys.join(', ')}"
if !global[:yes] && !agree("Continue? ")
@@ -38,7 +44,16 @@ module LeapCli
end
end
- compile_hiera_files
+ environments = nodes.field('environment').uniq
+ if environments.empty?
+ environments = [nil]
+ end
+ environments.each do |env|
+ check_platform_pinning(env)
+ end
+ # compile hiera files for all the nodes in every environment that is
+ # being deployed and only those environments.
+ compile_hiera_files(manager.filter(environments))
ssh_connect(nodes, connect_options(options)) do |ssh|
ssh.leap.log :checking, 'node' do
@@ -58,39 +73,128 @@ module LeapCli
end
end
end
+
end
end
private
+ #
+ # The currently activated provider.json could have loaded some pinning
+ # information for the platform. If this is the case, refuse to deploy
+ # if there is a mismatch.
+ #
+ # For example:
+ #
+ # "platform": {
+ # "branch": "develop"
+ # "version": "1.0..99"
+ # "commit": "e1d6280e0a8c565b7fb1a4ed3969ea6fea31a5e2..HEAD"
+ # }
+ #
+ def check_platform_pinning(environment)
+ provider = manager.env(environment).provider
+ return unless provider['platform']
+
+ if environment.nil? || environment == 'default'
+ provider_json = 'provider.json'
+ else
+ provider_json = 'provider.' + environment + '.json'
+ end
+
+ # can we have json schema verification already?
+ unless provider.platform.is_a? Hash
+ bail!('`platform` attribute in #{provider_json} must be a hash (was %s).' % provider.platform.inspect)
+ end
+
+ # check version
+ if provider.platform['version']
+ if !Leap::Platform.version_in_range?(provider.platform.version)
+ say("The platform is pinned to a version range of '#{provider.platform.version}' "+
+ "by the `platform.version` property in #{provider_json}, but the platform "+
+ "(#{Path.platform}) has version #{Leap::Platform.version}.")
+ quit!("OK. Bye.") unless agree("Do you really want to deploy from the wrong version? ")
+ end
+ end
+
+ # check branch
+ if provider.platform['branch']
+ if !is_git_directory?(Path.platform)
+ say("The platform is pinned to a particular branch by the `platform.branch` property "+
+ "in #{provider_json}, but the platform directory (#{Path.platform}) is not a git repository.")
+ quit!("OK. Bye.") unless agree("Do you really want to deploy anyway? ")
+ end
+ unless provider.platform.branch == current_git_branch(Path.platform)
+ say("The platform is pinned to branch '#{provider.platform.branch}' by the `platform.branch` property "+
+ "in #{provider_json}, but the current branch is '#{current_git_branch(Path.platform)}' " +
+ "(for directory '#{Path.platform}')")
+ quit!("OK. Bye.") unless agree("Do you really want to deploy from the wrong branch? ")
+ end
+ end
+
+ # check commit
+ if provider.platform['commit']
+ if !is_git_directory?(Path.platform)
+ say("The platform is pinned to a particular commit range by the `platform.commit` property "+
+ "in #{provider_json}, but the platform directory (#{Path.platform}) is not a git repository.")
+ quit!("OK. Bye.") unless agree("Do you really want to deploy anyway? ")
+ end
+ current_commit = current_git_commit(Path.platform)
+ Dir.chdir(Path.platform) do
+ commit_range = assert_run!("git log --pretty='format:%H' '#{provider.platform.commit}'",
+ "The platform is pinned to a particular commit range by the `platform.commit` property "+
+ "in #{provider_json}, but git was not able to find commits in the range specified "+
+ "(#{provider.platform.commit}).")
+ commit_range = commit_range.split("\n")
+ if !commit_range.include?(current_commit) &&
+ provider.platform.commit.split('..').first != current_commit
+ say("The platform is pinned via the `platform.commit` property in #{provider_json} " +
+ "to a commit in the range #{provider.platform.commit}, but the current HEAD " +
+ "(#{current_commit}) is not in that range.")
+ quit!("OK. Bye.") unless agree("Do you really want to deploy from the wrong commit? ")
+ end
+ end
+ end
+ end
+
def sync_hiera_config(ssh)
- dest_dir = provider.hiera_sync_destination
ssh.rsync.update do |server|
node = manager.node(server.host)
hiera_file = Path.relative_path([:hiera, node.name])
- ssh.leap.log hiera_file + ' -> ' + node.name + ':' + dest_dir + '/hiera.yaml'
+ ssh.leap.log hiera_file + ' -> ' + node.name + ':' + Leap::Platform.hiera_path
{
:source => hiera_file,
- :dest => dest_dir + '/hiera.yaml',
+ :dest => Leap::Platform.hiera_path,
:flags => "-rltp --chmod=u+rX,go-rwx"
}
end
end
+ #
+ # sync various support files.
+ #
def sync_support_files(ssh)
- dest_dir = provider.hiera_sync_destination
+ dest_dir = Leap::Platform.files_dir
+ source_files = []
+ if Path.defined?(:custom_puppet_dir) && file_exists?(:custom_puppet_dir)
+ source_files += [:custom_puppet_dir, :custom_puppet_modules_dir, :custom_puppet_manifests_dir].collect{|path|
+ Path.relative_path(path, Path.provider) + '/' # rsync needs trailing slash
+ }
+ ensure_dir :custom_puppet_modules_dir
+ end
ssh.rsync.update do |server|
node = manager.node(server.host)
files_to_sync = node.file_paths.collect {|path| Path.relative_path(path, Path.provider) }
+ files_to_sync += source_files
if files_to_sync.any?
ssh.leap.log(files_to_sync.join(', ') + ' -> ' + node.name + ':' + dest_dir)
{
- :chdir => Path.provider,
+ :chdir => Path.named_path(:files_dir),
:source => ".",
:dest => dest_dir,
:excludes => "*",
- :includes => calculate_includes_from_files(files_to_sync),
- :flags => "-rltp --chmod=u+rX,go-rwx --relative --delete --delete-excluded --filter='protect hiera.yaml' --copy-links"
+ :includes => calculate_includes_from_files(files_to_sync, '/files'),
+ :flags => "-rltp --chmod=u+rX,go-rwx --relative --delete --delete-excluded --copy-links"
}
else
nil
@@ -100,9 +204,9 @@ module LeapCli
def sync_puppet_files(ssh)
ssh.rsync.update do |server|
- ssh.leap.log(Path.platform + '/[bin,tests,puppet] -> ' + server.host + ':' + LeapCli::PUPPET_DESTINATION)
+ ssh.leap.log(Path.platform + '/[bin,tests,puppet] -> ' + server.host + ':' + Leap::Platform.leap_dir)
{
- :dest => LeapCli::PUPPET_DESTINATION,
+ :dest => Leap::Platform.leap_dir,
:source => '.',
:chdir => Path.platform,
:excludes => '*',
@@ -112,7 +216,12 @@ module LeapCli
end
end
+ #
+ # ensure submodules are up to date, if the platform is a git
+ # repository.
+ #
def init_submodules
+ return unless is_git_directory?(Path.platform)
Dir.chdir Path.platform do
assert_run! "git submodule sync"
statuses = assert_run! "git submodule status"
@@ -126,11 +235,17 @@ module LeapCli
end
end
- def calculate_includes_from_files(files)
+ #
+ # converts an array of file paths into an array
+ # suitable for --include of rsync
+ #
+ # if set, `prefix` is stripped off.
+ #
+ def calculate_includes_from_files(files, prefix=nil)
return nil unless files and files.any?
# prepend '/' (kind of like ^ for rsync)
- includes = files.collect {|file| '/' + file}
+ includes = files.collect {|file| file =~ /^\// ? file : '/' + file }
# include all sub files of specified directories
includes.size.times do |i|
@@ -148,6 +263,10 @@ module LeapCli
end
end
+ if prefix
+ includes.map! {|path| path.sub(/^#{Regexp.escape(prefix)}\//, '/')}
+ end
+
return includes
end
@@ -155,23 +274,11 @@ module LeapCli
if options[:tags]
tags = options[:tags].split(',')
else
- tags = LeapCli::DEFAULT_TAGS.dup
+ tags = Leap::Platform.default_puppet_tags.dup
end
tags << 'leap_slow' unless options[:fast]
tags.join(',')
end
- #
- # for safety, we allow production deploys to be turned off in the Leapfile.
- #
- def filter_deploy_nodes(filter)
- nodes = manager.filter!(filter)
- if !leapfile.allow_production_deploy
- nodes = nodes[:environment => "!production"]
- assert! nodes.any?, "Skipping deploy because @allow_production_deploy is disabled."
- end
- nodes
- end
-
end
end
diff --git a/lib/leap_cli/commands/env.rb b/lib/leap_cli/commands/env.rb
new file mode 100644
index 0000000..b2f585d
--- /dev/null
+++ b/lib/leap_cli/commands/env.rb
@@ -0,0 +1,53 @@
+module LeapCli
+ module Commands
+
+ desc "Manipulate and query environment information."
+ long_desc "The 'environment' node property can be used to isolate sets of nodes into entirely separate environments. "+
+ "A node in one environment will never interact with a node from another environment. "+
+ "Environment pinning works by modifying your ~/.leaprc file and is dependent on the "+
+ "absolute file path of your provider directory (pins don't apply if you move the directory)"
+ command :env do |c|
+ c.desc "List the available environments. The pinned environment, if any, will be marked with '*'."
+ c.command :ls do |ls|
+ ls.action do |global_options, options, args|
+ envs = ["default"] + manager.environment_names.compact.sort
+ envs.each do |env|
+ if env
+ if LeapCli.leapfile.environment == env
+ puts "* #{env}"
+ else
+ puts " #{env}"
+ end
+ end
+ end
+ end
+ end
+
+ c.desc 'Pin the environment to ENVIRONMENT. All subsequent commands will only apply to nodes in this environment.'
+ c.arg_name 'ENVIRONMENT'
+ c.command :pin do |pin|
+ pin.action do |global_options,options,args|
+ environment = args.first
+ if environment == 'default' ||
+ (environment && manager.environment_names.include?(environment))
+ LeapCli.leapfile.set('environment', environment)
+ log 0, :saved, "~/.leaprc with environment set to #{environment}."
+ end
+ end
+ end
+
+ c.desc "Unpin the environment. All subsequent commands will apply to all nodes."
+ c.command :unpin do |unpin|
+ unpin.action do |global_options, options, args|
+ LeapCli.leapfile.unset('environment')
+ log 0, :saved, "~/.leaprc, removing environment property."
+ end
+ end
+
+ c.default_command :ls
+ end
+
+ protected
+
+ end
+end \ No newline at end of file
diff --git a/lib/leap_cli/commands/facts.rb b/lib/leap_cli/commands/facts.rb
index d607086..65eda61 100644
--- a/lib/leap_cli/commands/facts.rb
+++ b/lib/leap_cli/commands/facts.rb
@@ -91,7 +91,9 @@ module LeapCli; module Commands
end
end
end
- overwrite_existing = args.empty?
+ # only overwrite the entire facts file if and only if we are gathering facts
+ # for all nodes in all environments.
+ overwrite_existing = args.empty? && LeapCli.leapfile.environment.nil?
update_facts_file(new_facts, overwrite_existing)
end
diff --git a/lib/leap_cli/commands/inspect.rb b/lib/leap_cli/commands/inspect.rb
index 746a80c..e8f5caf 100644
--- a/lib/leap_cli/commands/inspect.rb
+++ b/lib/leap_cli/commands/inspect.rb
@@ -109,7 +109,7 @@ module LeapCli; module Commands
if options[:base]
inspect_json manager.base_provider
elsif arg =~ /provider\.(.*)\.json/
- inspect_json manager.providers[$1]
+ inspect_json manager.env($1).provider
else
inspect_json manager.provider
end
diff --git a/lib/leap_cli/commands/list.rb b/lib/leap_cli/commands/list.rb
index 5b84113..b8d7739 100644
--- a/lib/leap_cli/commands/list.rb
+++ b/lib/leap_cli/commands/list.rb
@@ -15,24 +15,25 @@ module LeapCli; module Commands
c.flag 'print', :desc => 'What attributes to print (optional)'
c.switch 'disabled', :desc => 'Include disabled nodes in the list.', :negatable => false
c.action do |global_options,options,args|
+ # don't rely on default manager(), because we want to pass custom options to load()
+ manager = LeapCli::Config::Manager.new
if global_options[:color]
colors = ['cyan', 'white']
else
colors = [nil, nil]
end
puts
- if options['disabled']
- manager.load(:include_disabled => true) # reload, with disabled nodes
- end
+ manager.load(:include_disabled => options['disabled'], :continue_on_error => true)
if options['print']
print_node_properties(manager.filter(args), options['print'])
else
if args.any?
NodeTable.new(manager.filter(args), colors).run
else
- TagTable.new('SERVICES', manager.services, colors).run
- TagTable.new('TAGS', manager.tags, colors).run
- NodeTable.new(manager.nodes, colors).run
+ environment = LeapCli.leapfile.environment || '_all_'
+ TagTable.new('SERVICES', manager.env(environment).services, colors).run
+ TagTable.new('TAGS', manager.env(environment).tags, colors).run
+ NodeTable.new(manager.filter(), colors).run
end
end
end
@@ -41,11 +42,9 @@ module LeapCli; module Commands
private
def self.print_node_properties(nodes, properties)
- node_list = manager.nodes
properties = properties.split(',')
max_width = nodes.keys.inject(0) {|max,i| [i.size,max].max}
nodes.each_node do |node|
- node.evaluate
value = properties.collect{|prop|
if node[prop].nil?
"null"
@@ -68,7 +67,7 @@ module LeapCli; module Commands
@colors = colors
end
def run
- tags = @tag_list.keys.sort
+ tags = @tag_list.keys.select{|tag| tag !~ /^_/}.sort # sorted list of tags, excluding _partials
max_width = [20, (tags+[@heading]).inject(0) {|max,i| [i.size,max].max}].max
table :border => false do
row :color => @colors[0] do
@@ -76,6 +75,7 @@ module LeapCli; module Commands
column "NODES", :width => HighLine::SystemExtensions.terminal_size.first - max_width - 2, :padding => 2
end
tags.each do |tag|
+ next if @tag_list[tag].node_list.empty?
row :color => @colors[1] do
column tag
column @tag_list[tag].node_list.keys.sort.join(', ')
diff --git a/lib/leap_cli/commands/node.rb b/lib/leap_cli/commands/node.rb
index 304d86b..f1e1cf8 100644
--- a/lib/leap_cli/commands/node.rb
+++ b/lib/leap_cli/commands/node.rb
@@ -1,6 +1,4 @@
-require 'net/ssh/known_hosts'
-require 'tempfile'
-require 'ipaddr'
+autoload :IPAddr, 'ipaddr'
module LeapCli; module Commands
@@ -194,18 +192,40 @@ module LeapCli; module Commands
end
end
+ #
+ # get the public host key for a host.
+ # return SshKey object representation of the key.
+ #
+ # Only supports ecdsa or rsa host keys. ecdsa is preferred if both are available.
+ #
def get_public_key_for_ip(address, port=22)
assert_bin!('ssh-keyscan')
- output = assert_run! "ssh-keyscan -p #{port} -t ecdsa #{address}", "Could not get the public host key from #{address}:#{port}. Maybe sshd is not running?"
- line = output.split("\n").grep(/^[^#]/).first
- if line =~ /No route to host/
- bail! :failed, 'ssh-keyscan: no route to %s' % address
- elsif line =~ /no hostkey alg/
- bail! :failed, 'ssh-keyscan: no hostkey alg (must be missing an ecdsa public host key)'
+ output = assert_run! "ssh-keyscan -p #{port} #{address}", "Could not get the public host key from #{address}:#{port}. Maybe sshd is not running?"
+ if output.empty?
+ bail! :failed, "ssh-keyscan returned empty output."
+ end
+
+ # key arrays [ip, key_type, public_key]
+ rsa_key = nil
+ ecdsa_key = nil
+
+ lines = output.split("\n").grep(/^[^#]/)
+ lines.each do |line|
+ if line =~ /No route to host/
+ bail! :failed, 'ssh-keyscan: no route to %s' % address
+ elsif line =~ / ssh-rsa /
+ rsa_key = line.split(' ')
+ elsif line =~ / ecdsa-sha2-nistp256 /
+ ecdsa_key = line.split(' ')
+ end
+ end
+
+ if rsa_key.nil? && ecdsa_key.nil?
+ bail! "ssh-keyscan got zero host keys back! Output was: #{output}"
+ else
+ key = ecdsa_key || rsa_key
+ return SshKey.load(key[2], key[1])
end
- assert! line, "Got zero host keys back!"
- ip, key_type, public_key = line.split(' ')
- return SshKey.load(public_key, key_type)
end
def is_node_alive(node, options)
diff --git a/lib/leap_cli/commands/pre.rb b/lib/leap_cli/commands/pre.rb
index 4b62b5b..7a64c15 100644
--- a/lib/leap_cli/commands/pre.rb
+++ b/lib/leap_cli/commands/pre.rb
@@ -21,7 +21,7 @@ module LeapCli; module Commands
switch :yes, :negatable => false
desc 'Enable debugging library (leap_cli development only)'
- switch :debug, :negatable => false
+ switch [:d, :debug], :negatable => false
desc 'Disable colors in output'
default_value true
@@ -31,12 +31,7 @@ module LeapCli; module Commands
#
# set verbosity
#
- LeapCli.log_level = global[:verbose].to_i
- if LeapCli.log_level > 1
- ENV['GLI_DEBUG'] = "true"
- else
- ENV['GLI_DEBUG'] = "false"
- end
+ LeapCli.set_log_level(global[:verbose].to_i)
#
# load Leapfile
@@ -53,13 +48,6 @@ module LeapCli; module Commands
bail! { log :missing, "platform directory '#{Path.platform}'" }
end
- if LeapCli.leapfile.platform_branch && LeapCli::Util.is_git_directory?(Path.platform)
- branch = LeapCli::Util.current_git_branch(Path.platform)
- if branch != LeapCli.leapfile.platform_branch
- bail! "Wrong branch for #{Path.platform}. Was '#{branch}', should be '#{LeapCli.leapfile.platform_branch}'. Edit Leapfile to disable this check."
- end
- end
-
#
# set log file
#
@@ -68,18 +56,7 @@ module LeapCli; module Commands
log_version
LeapCli.log_in_color = global[:color]
- #
- # load all the nodes everything
- #
- manager
-
- #
- # check requirements
- #
- REQUIREMENTS.each do |key|
- assert_config! key
- end
-
+ true
end
private
diff --git a/lib/leap_cli/commands/shell.rb b/lib/leap_cli/commands/shell.rb
index 2ccb3de..2138e9d 100644
--- a/lib/leap_cli/commands/shell.rb
+++ b/lib/leap_cli/commands/shell.rb
@@ -3,8 +3,10 @@ module LeapCli; module Commands
desc 'Log in to the specified node with an interactive shell.'
arg_name 'NAME' #, :optional => false, :multiple => false
command :ssh do |c|
+ c.flag 'ssh', :desc => "Pass through raw options to ssh (e.g. --ssh '-F ~/sshconfig')"
+ c.flag 'port', :desc => 'Override ssh port for remote host'
c.action do |global_options,options,args|
- exec_ssh(:ssh, args)
+ exec_ssh(:ssh, options, args)
end
end
@@ -12,7 +14,7 @@ module LeapCli; module Commands
arg_name 'NAME'
command :mosh do |c|
c.action do |global_options,options,args|
- exec_ssh(:mosh, args)
+ exec_ssh(:mosh, options, args)
end
end
@@ -44,8 +46,9 @@ module LeapCli; module Commands
private
- def exec_ssh(cmd, args)
+ def exec_ssh(cmd, cli_options, args)
node = get_node_from_args(args, :include_disabled => true)
+ port = node.ssh.port
options = [
"-o 'HostName=#{node.ip_address}'",
# "-o 'HostKeyAlias=#{node.name}'", << oddly incompatible with ports in known_hosts file, so we must not use this or non-standard ports break.
@@ -65,7 +68,13 @@ module LeapCli; module Commands
elsif LeapCli.log_level >= 2
options << "-v"
end
- ssh = "ssh -l #{username} -p #{node.ssh.port} #{options.join(' ')}"
+ if cli_options[:port]
+ port = cli_options[:port]
+ end
+ if cli_options[:ssh]
+ options << cli_options[:ssh]
+ end
+ ssh = "ssh -l #{username} -p #{port} #{options.join(' ')}"
if cmd == :ssh
command = "#{ssh} #{node.domain.full}"
elsif cmd == :mosh
diff --git a/lib/leap_cli/commands/test.rb b/lib/leap_cli/commands/test.rb
index 2584a69..2f146b7 100644
--- a/lib/leap_cli/commands/test.rb
+++ b/lib/leap_cli/commands/test.rb
@@ -33,9 +33,9 @@ module LeapCli; module Commands
def test_cmd(options)
if options[:continue]
- "#{PUPPET_DESTINATION}/bin/run_tests --continue"
+ "#{Leap::Platform.leap_dir}/bin/run_tests --continue"
else
- "#{PUPPET_DESTINATION}/bin/run_tests"
+ "#{Leap::Platform.leap_dir}/bin/run_tests"
end
end
diff --git a/lib/leap_cli/commands/user.rb b/lib/leap_cli/commands/user.rb
index d7c21db..6c33878 100644
--- a/lib/leap_cli/commands/user.rb
+++ b/lib/leap_cli/commands/user.rb
@@ -1,4 +1,3 @@
-require 'gpgme'
#
# perhaps we want to verify that the key files are actually the key files we expect.
@@ -75,8 +74,10 @@ module LeapCli
if `which ssh-add`.strip.any?
`ssh-add -L 2> /dev/null`.split("\n").compact.each do |line|
key = SshKey.load(line)
- key.comment = 'ssh-agent'
- ssh_keys << key unless ssh_keys.include?(key)
+ if key
+ key.comment = 'ssh-agent'
+ ssh_keys << key unless ssh_keys.include?(key)
+ end
end
end
ssh_keys.compact!
@@ -98,13 +99,20 @@ module LeapCli
# let the the user choose among the gpg public keys that we encounter, or just pick the key if there is only one.
#
def pick_pgp_key
+ begin
+ return unless `which gpg`.strip.any?
+ require 'gpgme'
+ rescue LoadError
+ return
+ end
+
secret_keys = GPGME::Key.find(:secret)
if secret_keys.empty?
log "Skipping OpenPGP setup because I could not find any OpenPGP keys for you"
return nil
end
- assert_bin! 'gpg'
+ secret_keys.select!{|key| !key.expired}
if secret_keys.length > 1
key_index = numbered_choice_menu('Choose your OpenPGP public key', secret_keys) do |key, i|
diff --git a/lib/leap_cli/commands/vagrant.rb b/lib/leap_cli/commands/vagrant.rb
index 5219161..41fda03 100644
--- a/lib/leap_cli/commands/vagrant.rb
+++ b/lib/leap_cli/commands/vagrant.rb
@@ -1,4 +1,4 @@
-require 'ipaddr'
+autoload :IPAddr, 'ipaddr'
require 'fileutils'
module LeapCli; module Commands
diff --git a/lib/leap_cli/config/filter.rb b/lib/leap_cli/config/filter.rb
new file mode 100644
index 0000000..123533f
--- /dev/null
+++ b/lib/leap_cli/config/filter.rb
@@ -0,0 +1,151 @@
+#
+# Many leap_cli commands accept a list of filters to select a subset of nodes for the command to
+# be applied to. This class is a helper for manager to run these filters.
+#
+# Classes other than Manager should not use this class.
+#
+
+module LeapCli
+ module Config
+ class Filter
+
+ #
+ # filter -- array of strings, each one a filter
+ # options -- hash, possible keys include
+ # :nopin -- disregard environment pinning
+ # :local -- if false, disallow local nodes
+ #
+ def initialize(filters, options, manager)
+ @filters = filters.nil? ? [] : filters.dup
+ @environments = []
+ @options = options
+ @manager = manager
+
+ # split filters by pulling out items that happen
+ # to be environment names.
+ if LeapCli.leapfile.environment.nil? || @options[:nopin]
+ @environments = []
+ else
+ @environments = [LeapCli.leapfile.environment]
+ end
+ @filters.select! do |filter|
+ filter_text = filter.sub(/^\+/,'')
+ if is_environment?(filter_text)
+ if filter_text == LeapCli.leapfile.environment
+ # silently ignore already pinned environments
+ elsif (filter =~ /^\+/ || @filters.first == filter) && !@environments.empty?
+ LeapCli::Util.bail! do
+ LeapCli::Util.log "Environments are exclusive: no node is in two environments." do
+ LeapCli::Util.log "Tried to filter on '#{@environments.join('\' AND \'')}' AND '#{filter_text}'"
+ end
+ end
+ else
+ @environments << filter_text
+ end
+ false
+ else
+ true
+ end
+ end
+
+ # don't let the first filter have a + prefix
+ if @filters[0] =~ /^\+/
+ @filters[0] = @filters[0][1..-1]
+ end
+ end
+
+ # actually run the filter, returns a filtered list of nodes
+ def nodes()
+ if @filters.empty?
+ return nodes_for_empty_filter
+ else
+ return nodes_for_filter
+ end
+ end
+
+ private
+
+ def nodes_for_empty_filter
+ node_list = @manager.nodes
+ if @environments.any?
+ node_list = node_list[ @environments.collect{|e|[:environment, env_to_filter(e)]} ]
+ end
+ if @options[:local] === false
+ node_list = node_list[:environment => '!local']
+ end
+ node_list
+ end
+
+ def nodes_for_filter
+ node_list = Config::ObjectList.new
+ @filters.each do |filter|
+ if filter =~ /^\+/
+ keep_list = nodes_for_name(filter[1..-1])
+ node_list.delete_if do |name, node|
+ if keep_list[name]
+ false
+ else
+ true
+ end
+ end
+ else
+ node_list.merge!(nodes_for_name(filter))
+ end
+ end
+ node_list
+ end
+
+ private
+
+ #
+ # returns a set of nodes corresponding to a single name,
+ # where name could be a node name, service name, or tag name.
+ #
+ # For services and tags, we only include nodes for the
+ # environments that are active
+ #
+ def nodes_for_name(name)
+ if node = @manager.nodes[name]
+ return Config::ObjectList.new(node)
+ elsif @environments.empty?
+ if @manager.services[name]
+ return @manager.env('_all_').services[name].node_list
+ elsif @manager.tags[name]
+ return @manager.env('_all_').tags[name].node_list
+ else
+ LeapCli::Util.log :warning, "filter '#{name}' does not match any node names, tags, services, or environments."
+ return Config::ObjectList.new
+ end
+ else
+ node_list = Config::ObjectList.new
+ if @manager.services[name]
+ @environments.each do |env|
+ node_list.merge!(@manager.env(env).services[name].node_list)
+ end
+ elsif @manager.tags[name]
+ @environments.each do |env|
+ node_list.merge!(@manager.env(env).tags[name].node_list)
+ end
+ else
+ LeapCli::Util.log :warning, "filter '#{name}' does not match any node names, tags, services, or environments."
+ end
+ return node_list
+ end
+ end
+
+ #
+ # when pinning, we use the name 'default' to specify nodes
+ # without an environment set, but when filtering, we need to filter
+ # on :environment => nil.
+ #
+ def env_to_filter(environment)
+ environment == 'default' ? nil : environment
+ end
+
+ def is_environment?(text)
+ text == 'default' || @manager.environment_names.include?(text)
+ end
+
+ end
+ end
+end
diff --git a/lib/leap_cli/config/macros.rb b/lib/leap_cli/config/macros.rb
deleted file mode 100644
index 59453b0..0000000
--- a/lib/leap_cli/config/macros.rb
+++ /dev/null
@@ -1,514 +0,0 @@
-# encoding: utf-8
-#
-# MACROS
-# these are methods available when eval'ing a value in the .json configuration
-#
-# This module is included in Config::Object
-#
-
-require 'base32'
-
-module LeapCli; module Config
- module Macros
- ##
- ## NODES
- ##
-
- #
- # the list of all the nodes
- #
- def nodes
- global.nodes
- end
-
- #
- # grab an environment appropriate provider
- #
- def provider
- global.env(@node.environment).provider
- end
-
- #
- # returns a list of nodes that match the same environment
- #
- # if @node.environment is not set, we return other nodes
- # where environment is not set.
- #
- def nodes_like_me
- nodes[:environment => @node.environment]
- end
-
- #
- # returns a list of nodes that match the location name
- # and environment of @node.
- #
- def nodes_near_me
- if @node['location'] && @node['location']['name']
- nodes_like_me['location.name' => @node.location.name]
- else
- nodes_like_me['location' => nil]
- end
- end
-
- #
- #
- # picks a node out from the node list in such a way that:
- #
- # (1) which nodes picked which nodes is saved in secrets.json
- # (2) when other nodes call this macro with the same node list, they are guaranteed to get a different node
- # (3) if all the nodes in the pick_node list have been picked, remaining nodes are distributed randomly.
- #
- # if the node_list is empty, an exception is raised.
- # if node_list size is 1, then that node is returned and nothing is
- # memorized via the secrets.json file.
- #
- # `label` is needed to distinguish between pools of nodes for different purposes.
- #
- # TODO: more evenly balance after all the nodes have been picked.
- #
- def pick_node(label, node_list)
- if node_list.any?
- if node_list.size == 1
- return node_list.values.first
- else
- secrets_key = "pick_node(:#{label},#{node_list.keys.sort.join(',')})"
- secrets_value = @manager.secrets.retrieve(secrets_key, @node.environment) || {}
- secrets_value[@node.name] ||= begin
- node_to_pick = nil
- node_list.each_node do |node|
- next if secrets_value.values.include?(node.name)
- node_to_pick = node.name
- end
- node_to_pick ||= secrets_value.values.shuffle.first # all picked already, so pick a random one.
- node_to_pick
- end
- picked_node_name = secrets_value[@node.name]
- @manager.secrets.set(secrets_key, secrets_value, @node.environment)
- return node_list[picked_node_name]
- end
- else
- raise ArgumentError.new('pick_node(node_list): node_list cannot be empty')
- end
- end
-
- ##
- ## FILES
- ##
-
- class FileMissing < Exception
- attr_accessor :path, :options
- def initialize(path, options={})
- @path = path
- @options = options
- end
- def to_s
- @path
- end
- end
-
- #
- # inserts the contents of a file
- #
- def file(filename, options={})
- if filename.is_a? Symbol
- filename = [filename, @node.name]
- end
- filepath = Path.find_file(filename)
- if filepath
- if filepath =~ /\.erb$/
- ERB.new(File.read(filepath, :encoding => 'UTF-8'), nil, '%<>').result(binding)
- else
- File.read(filepath, :encoding => 'UTF-8')
- end
- else
- raise FileMissing.new(Path.named_path(filename), options)
- ""
- end
- end
-
- #
- # like #file, but allow missing files
- #
- def try_file(filename)
- return file(filename)
- rescue FileMissing
- return nil
- end
-
- #
- # returns what the file path will be, once the file is rsynced to the server.
- # an internal list of discovered file paths is saved, in order to rsync these files when needed.
- #
- # notes:
- #
- # * argument 'path' is relative to Path.provider/files or Path.provider_base/files
- # * the path returned by this method is absolute
- # * the path stored for use later by rsync is relative to Path.provider
- # * if the path does not exist locally, but exists in provider_base, then the default file from
- # provider_base is copied locally. this is required for rsync to work correctly.
- #
- def file_path(path)
- if path.is_a? Symbol
- path = [path, @node.name]
- end
- actual_path = Path.find_file(path)
- if actual_path.nil?
- Util::log 2, :skipping, "file_path(\"#{path}\") because there is no such file."
- nil
- else
- if actual_path =~ /^#{Regexp.escape(Path.provider_base)}/
- # if file is under Path.provider_base, we must copy the default file to
- # to Path.provider in order for rsync to be able to sync the file.
- local_provider_path = actual_path.sub(/^#{Regexp.escape(Path.provider_base)}/, Path.provider)
- FileUtils.mkdir_p File.dirname(local_provider_path), :mode => 0700
- FileUtils.install actual_path, local_provider_path, :mode => 0600
- Util.log :created, Path.relative_path(local_provider_path)
- actual_path = local_provider_path
- end
- if File.directory?(actual_path) && actual_path !~ /\/$/
- actual_path += '/' # ensure directories end with /, important for building rsync command
- end
- relative_path = Path.relative_path(actual_path)
- @node.file_paths << relative_path
- @node.manager.provider.hiera_sync_destination + '/' + relative_path
- end
- end
-
- #
- # inserts a named secret, generating it if needed.
- #
- # manager.export_secrets should be called later to capture any newly generated secrets.
- #
- # +length+ is the character length of the generated password.
- #
- def secret(name, length=32)
- @manager.secrets.set(name, Util::Secret.generate(length), @node[:environment])
- end
-
- # inserts a base32 encoded secret
- def base32_secret(name, length=20)
- @manager.secrets.set(name, Base32.encode(Util::Secret.generate(length)), @node[:environment])
- end
-
- # Picks a random obfsproxy port from given range
- def rand_range(name, range)
- @manager.secrets.set(name, rand(range), @node[:environment])
- end
-
- #
- # inserts an hexidecimal secret string, generating it if needed.
- #
- # +bit_length+ is the bits in the secret, (ie length of resulting hex string will be bit_length/4)
- #
- def hex_secret(name, bit_length=128)
- @manager.secrets.set(name, Util::Secret.generate_hex(bit_length), @node[:environment])
- end
-
- #
- # return a fingerprint for a x509 certificate
- #
- def fingerprint(filename)
- "SHA256: " + X509.fingerprint("SHA256", Path.named_path(filename))
- end
-
- ##
- ## HOSTS
- ##
-
- #
- # records the list of hosts that are encountered for this node
- #
- def hostnames(nodes)
- @referenced_nodes ||= ObjectList.new
- nodes = listify(nodes)
- nodes.each_node do |node|
- @referenced_nodes[node.name] ||= node
- end
- return nodes.values.collect {|node| node.domain.name}
- end
-
- #
- # Generates entries needed for updating /etc/hosts on a node (as a hash).
- #
- # Argument `nodes` can be nil or a list of nodes. If nil, only include the
- # IPs of the other nodes this @node as has encountered (plus all mx nodes).
- #
- # Also, for virtual machines, we use the local address if this @node is in
- # the same location as the node in question.
- #
- # We include the ssh public key for each host, so that the hash can also
- # be used to generate the /etc/ssh/known_hosts
- #
- def hosts_file(nodes=nil)
- if nodes.nil?
- if @referenced_nodes && @referenced_nodes.any?
- nodes = @referenced_nodes
- nodes = nodes.merge(nodes_like_me[:services => 'mx']) # all nodes always need to communicate with mx nodes.
- end
- end
- return {} unless nodes
- hosts = {}
- my_location = @node['location'] ? @node['location']['name'] : nil
- nodes.each_node do |node|
- hosts[node.name] = {'ip_address' => node.ip_address, 'domain_internal' => node.domain.internal, 'domain_full' => node.domain.full}
- node_location = node['location'] ? node['location']['name'] : nil
- if my_location == node_location
- if facts = @node.manager.facts[node.name]
- if facts['ec2_public_ipv4']
- hosts[node.name]['ip_address'] = facts['ec2_public_ipv4']
- end
- end
- end
- host_pub_key = Util::read_file([:node_ssh_pub_key,node.name])
- if host_pub_key
- hosts[node.name]['host_pub_key'] = host_pub_key
- end
- end
- hosts
- end
-
- ##
- ## STUNNEL
- ##
-
- #
- # stunnel configuration for the client side.
- #
- # +node_list+ is a ObjectList of nodes running stunnel servers.
- #
- # +port+ is the real port of the ultimate service running on the servers
- # that the client wants to connect to.
- #
- # About ths stunnel puppet names:
- #
- # * accept_port is the port on localhost to which local clients
- # can connect. it is auto generated serially.
- # * connect_port is the port on the stunnel server to connect to.
- # it is auto generated from the +port+ argument.
- #
- # The network looks like this:
- #
- # |------ stunnel client ---------------| |--------- stunnel server -----------------------|
- # consumer app -> localhost:accept_port -> server:connect_port -> server:port -> service app
- #
- # generates an entry appropriate to be passed directly to
- # create_resources(stunnel::service, hiera('..'), defaults)
- #
- # local ports are automatically generated, starting at 4000
- # and incrementing in sorted order (by node name).
- #
- def stunnel_client(node_list, port, options={})
- @next_stunnel_port ||= 4000
- node_list = listify(node_list)
- hostnames(node_list) # record the hosts
- result = Config::ObjectList.new
- node_list.each_node do |node|
- if node.name != self.name || options[:include_self]
- result["#{node.name}_#{port}"] = Config::Object[
- 'accept_port', @next_stunnel_port,
- 'connect', node.domain.internal,
- 'connect_port', stunnel_port(port)
- ]
- @next_stunnel_port += 1
- end
- end
- result
- end
-
- #
- # generates a stunnel server entry.
- #
- # +port+ is the real port targeted service.
- #
- def stunnel_server(port)
- {"accept" => stunnel_port(port), "connect" => "127.0.0.1:#{port}"}
- end
-
- #
- # maps a real port to a stunnel port (used as the connect_port in the client config
- # and the accept_port in the server config)
- #
- def stunnel_port(port)
- port = port.to_i
- if port < 50000
- return port + 10000
- else
- return port - 10000
- end
- end
-
- ##
- ## HAPROXY
- ##
-
- #
- # creates a hash suitable for configuring haproxy. the key is the node name of the server we are proxying to.
- #
- # * node_list - a hash of nodes for the haproxy servers
- # * stunnel_client - contains the mappings to local ports for each server node.
- # * non_stunnel_port - in case self is included in node_list, the port to connect to.
- #
- # 1000 weight is used for nodes in the same location.
- # 100 otherwise.
- #
- def haproxy_servers(node_list, stunnel_clients, non_stunnel_port=nil)
- default_weight = 10
- local_weight = 100
-
- # record the hosts_file
- hostnames(node_list)
-
- # create a simple map for node name -> local stunnel accept port
- accept_ports = stunnel_clients.inject({}) do |hsh, stunnel_entry|
- name = stunnel_entry.first.sub /_[0-9]+$/, ''
- hsh[name] = stunnel_entry.last['accept_port']
- hsh
- end
-
- # if one the nodes in the node list is ourself, then there will not be a stunnel to it,
- # but we need to include it anyway in the haproxy config.
- if node_list[self.name] && non_stunnel_port
- accept_ports[self.name] = non_stunnel_port
- end
-
- # create the first pass of the servers hash
- servers = node_list.values.inject(Config::ObjectList.new) do |hsh, node|
- weight = default_weight
- if self['location'] && node['location']
- if self.location['name'] == node.location['name']
- weight = local_weight
- end
- end
- hsh[node.name] = Config::Object[
- 'backup', false,
- 'host', 'localhost',
- 'port', accept_ports[node.name] || 0,
- 'weight', weight
- ]
- hsh
- end
-
- # if there are some local servers, make the others backup
- if servers.detect{|k,v| v.weight == local_weight}
- servers.each do |k,server|
- server['backup'] = server['weight'] == default_weight
- end
- end
-
- return servers
- end
-
- ##
- ## SSH
- ##
-
- #
- # Creates a hash from the ssh key info in users directory, for use in
- # updating authorized_keys file. Additionally, the 'monitor' public key is
- # included, which is used by the monitor nodes to run particular commands
- # remotely.
- #
- def authorized_keys
- hash = {}
- keys = Dir.glob(Path.named_path([:user_ssh, '*']))
- keys.sort.each do |keyfile|
- ssh_type, ssh_key = File.read(keyfile, :encoding => 'UTF-8').strip.split(" ")
- name = File.basename(File.dirname(keyfile))
- hash[name] = {
- "type" => ssh_type,
- "key" => ssh_key
- }
- end
- ssh_type, ssh_key = File.read(Path.named_path(:monitor_pub_key), :encoding => 'UTF-8').strip.split(" ")
- hash[Leap::Platform.monitor_username] = {
- "type" => ssh_type,
- "key" => ssh_key
- }
- hash
- end
-
- #
- # this is not currently used, because we put key information in the 'hosts' hash.
- # see 'hosts_file()'
- #
- # def known_hosts_file(nodes=nil)
- # if nodes.nil?
- # if @referenced_nodes && @referenced_nodes.any?
- # nodes = @referenced_nodes
- # end
- # end
- # return nil unless nodes
- # entries = []
- # nodes.each_node do |node|
- # hostnames = [node.name, node.domain.internal, node.domain.full, node.ip_address].join(',')
- # pub_key = Util::read_file([:node_ssh_pub_key,node.name])
- # if pub_key
- # entries << [hostnames, pub_key].join(' ')
- # end
- # end
- # entries.join("\n")
- # end
-
- ##
- ## UTILITY
- ##
-
- class AssertionFailed < Exception
- attr_accessor :assertion
- def initialize(assertion)
- @assertion = assertion
- end
- def to_s
- @assertion
- end
- end
-
- def assert(assertion)
- if instance_eval(assertion)
- true
- else
- raise AssertionFailed.new(assertion)
- end
- end
-
- #
- # applies a JSON partial to this node
- #
- def apply_partial(partial_path)
- manager.partials(partial_path).each do |partial_data|
- self.deep_merge!(partial_data)
- end
- end
-
- #
- # If at first you don't succeed, then it is time to give up.
- #
- # try{} returns nil if anything in the block throws an exception.
- #
- # You can wrap something that might fail in `try`, like so.
- #
- # "= try{ nodes[:services => 'tor'].first.ip_address } "
- #
- def try(&block)
- yield
- rescue NoMethodError
- nil
- end
-
- private
-
- #
- # returns a node list, if argument is not already one
- #
- def listify(node_list)
- if node_list.is_a? Config::ObjectList
- node_list
- elsif node_list.is_a? Config::Object
- Config::ObjectList.new(node_list)
- else
- raise ArgumentError, 'argument must be a node or node list, not a `%s`' % node_list.class, caller
- end
- end
-
- end
-end; end
diff --git a/lib/leap_cli/config/manager.rb b/lib/leap_cli/config/manager.rb
index 7b3fb27..be95831 100644
--- a/lib/leap_cli/config/manager.rb
+++ b/lib/leap_cli/config/manager.rb
@@ -20,6 +20,16 @@ module LeapCli
def initialize
@environments = {} # hash of `Environment` objects, keyed by name.
+
+ # load macros and other custom ruby in provider base
+ platform_ruby_files = Dir[Path.provider_base + '/lib/*.rb']
+ if platform_ruby_files.any?
+ $: << Path.provider_base + '/lib'
+ platform_ruby_files.each do |rb_file|
+ require rb_file
+ end
+ end
+ Config::Object.send(:include, LeapCli::Macro)
end
##
@@ -54,9 +64,24 @@ module LeapCli
e
end
- def services; env('default').services; end
- def tags; env('default').tags; end
- def provider; env('default').provider; end
+ #
+ # The default accessors for services, tags, and provider.
+ # For these defaults, use 'default' environment, or whatever
+ # environment is pinned.
+ #
+ def services
+ env(default_environment).services
+ end
+ def tags
+ env(default_environment).tags
+ end
+ def provider
+ env(default_environment).provider
+ end
+
+ def default_environment
+ LeapCli.leapfile.environment
+ end
##
## IMPORT EXPORT
@@ -80,8 +105,8 @@ module LeapCli
@secrets = load_json( Path.named_path(:secrets_config, @provider_dir), Config::Secrets)
@common.inherit_from! @base_common
- # load provider services, tags, and provider.json, DEFAULT environment
- log 3, :loading, 'default environment.........'
+ # For the default environment, load provider services, tags, and provider.json
+ log 3, :loading, 'default environment...'
env('default') do |e|
e.services = load_all_json(Path.named_path([:service_config, '*'], @provider_dir), Config::Tag, :no_dots => true)
e.tags = load_all_json(Path.named_path([:tag_config, '*'], @provider_dir), Config::Tag, :no_dots => true)
@@ -92,17 +117,28 @@ module LeapCli
validate_provider(e.provider)
end
- # load provider services, tags, and provider.json, OTHER environments
+ # create a special '_all_' environment, used for tracking the union
+ # of all the environments
+ env('_all_') do |e|
+ e.services = Config::ObjectList.new
+ e.tags = Config::ObjectList.new
+ e.provider = Config::Provider.new
+ e.services.inherit_from! env('default').services
+ e.tags.inherit_from! env('default').tags
+ e.provider.inherit_from! env('default').provider
+ end
+
+ # For each defined environment, load provider services, tags, and provider.json.
environment_names.each do |ename|
next unless ename
- log 3, :loading, '%s environment.........' % ename
+ log 3, :loading, '%s environment...' % ename
env(ename) do |e|
e.services = load_all_json(Path.named_path([:service_env_config, '*', ename], @provider_dir), Config::Tag)
e.tags = load_all_json(Path.named_path([:tag_env_config, '*', ename], @provider_dir), Config::Tag)
e.provider = load_json( Path.named_path([:provider_env_config, ename], @provider_dir), Config::Provider)
- e.services.inherit_from! env.services
- e.tags.inherit_from! env.tags
- e.provider.inherit_from! env.provider
+ e.services.inherit_from! env('default').services
+ e.tags.inherit_from! env('default').tags
+ e.provider.inherit_from! env('default').provider
validate_provider(e.provider)
end
end
@@ -113,15 +149,21 @@ module LeapCli
@nodes[name] = apply_inheritance(node)
end
- # remove disabled nodes
- unless options[:include_disabled]
- remove_disabled_nodes
- end
+ # do some node-list post-processing
+ cleanup_node_lists(options)
# apply control files
@nodes.each do |name, node|
control_files(node).each do |file|
- node.instance_eval File.read(file), file, 1
+ begin
+ node.eval_file file
+ rescue ConfigError => exc
+ if options[:continue_on_error]
+ exc.log
+ else
+ raise exc
+ end
+ end
end
end
end
@@ -187,42 +229,19 @@ module LeapCli
# 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]
+ # condition: [node_name | service_name | tag_name | environment_name]
#
# if conditions is prefixed with +, then it works like an AND. Otherwise, it works like an OR.
#
+ # args:
+ # filter -- array of filter terms, one per item
+ #
# options:
# :local -- if :local is false and the filter is empty, then local nodes are excluded.
+ # :nopin -- if true, ignore environment pinning
#
- def filter(filters, options={})
- if filters.empty?
- if options[:local] === false
- return nodes[:environment => '!local']
- else
- return nodes
- end
- end
- if filters[0] =~ /^\+/
- # don't let the first filter have a + prefix
- filters[0] = filters[0][1..-1]
- end
-
- node_list = Config::ObjectList.new
- filters.each do |filter|
- if filter =~ /^\+/
- keep_list = nodes_for_name(filter[1..-1])
- node_list.delete_if do |name, node|
- if keep_list[name]
- false
- else
- true
- end
- end
- else
- node_list.merge!(nodes_for_name(filter))
- end
- end
- return node_list
+ def filter(filters=nil, options={})
+ Filter.new(filters, options, self).nodes()
end
#
@@ -396,7 +415,6 @@ module LeapCli
raise LeapCli::ConfigError.new(node, "error " + msg) if throw_exceptions
else
new_node.deep_merge!(service)
- self.services[node_service].node_list.add(name, new_node)
end
end
end
@@ -414,7 +432,6 @@ module LeapCli
raise LeapCli::ConfigError.new(node, "error " + msg) if throw_exceptions
else
new_node.deep_merge!(tag)
- self.tags[node_tag].node_list.add(name, new_node)
end
end
end
@@ -428,43 +445,35 @@ module LeapCli
apply_inheritance(node, true)
end
- def remove_disabled_nodes
+ #
+ # does some final clean at the end of loading nodes.
+ # this includes removing disabled nodes, and populating
+ # the services[x].node_list and tags[x].node_list
+ #
+ def cleanup_node_lists(options)
@disabled_nodes = Config::ObjectList.new
@nodes.each do |name, node|
- unless node.enabled
- log 2, :skipping, "disabled node #{name}."
- @nodes.delete(name)
- @disabled_nodes[name] = node
+ if node.enabled || options[:include_disabled]
if node['services']
node['services'].to_a.each do |node_service|
- self.services[node_service].node_list.delete(node.name)
+ env(node.environment).services[node_service].node_list.add(node.name, node)
+ env('_all_').services[node_service].node_list.add(node.name, node)
end
end
if node['tags']
node['tags'].to_a.each do |node_tag|
- self.tags[node_tag].node_list.delete(node.name)
+ env(node.environment).tags[node_tag].node_list.add(node.name, node)
+ env('_all_').tags[node_tag].node_list.add(node.name, node)
end
end
+ elsif !options[:include_disabled]
+ log 2, :skipping, "disabled node #{name}."
+ @nodes.delete(name)
+ @disabled_nodes[name] = node
end
end
end
-
- #
- # returns a set of nodes corresponding to a single name, where name could be a node name, service name, or tag name.
- #
- def nodes_for_name(name)
- if node = self.nodes[name]
- Config::ObjectList.new(node)
- elsif service = self.services[name]
- service.node_list
- elsif tag = self.tags[name]
- tag.node_list
- else
- {}
- end
- end
-
def validate_provider(provider)
# nothing yet.
end
diff --git a/lib/leap_cli/config/object.rb b/lib/leap_cli/config/object.rb
index 2392d1c..a0d402b 100644
--- a/lib/leap_cli/config/object.rb
+++ b/lib/leap_cli/config/object.rb
@@ -8,8 +8,6 @@ if $ruby_version < [1,9]
end
require 'ya2yaml' # pure ruby yaml
-require 'leap_cli/config/macros'
-
module LeapCli
module Config
#
@@ -20,8 +18,6 @@ module LeapCli
#
class Object < Hash
- include Config::Macros
-
attr_reader :node
attr_reader :manager
alias :global :manager
@@ -44,7 +40,7 @@ module LeapCli
#
def dump_yaml
evaluate(@node)
- ya2yaml(:syck_compatible => true)
+ sorted_ya2yaml(:syck_compatible => true)
end
#
@@ -68,6 +64,11 @@ module LeapCli
get(key)
end
+ # Overrride some default methods in Hash that are likely to
+ # be used as attributes.
+ alias_method :hkey, :key
+ def key; get('key'); end
+
#
# make hash addressable like an object (e.g. obj['name'] available as obj.name)
#
@@ -134,7 +135,18 @@ module LeapCli
#
def deep_merge!(object, prefer_self=false)
object.each do |key,new_value|
- old_value = self.fetch key, nil
+ if self.has_key?('+'+key)
+ mode = :add
+ old_value = self.fetch '+'+key, nil
+ self.delete('+'+key)
+ elsif self.has_key?('-'+key)
+ mode = :subtract
+ old_value = self.fetch '-'+key, nil
+ self.delete('-'+key)
+ else
+ mode = :normal
+ old_value = self.fetch key, nil
+ end
# clean up boolean
new_value = true if new_value == "true"
@@ -160,6 +172,18 @@ module LeapCli
elsif new_value.is_a?(Array) && !old_value.is_a?(Array)
(value = (new_value.dup << old_value).compact.uniq).delete('REQUIRED')
+ # merge two arrays
+ elsif old_value.is_a?(Array) && new_value.is_a?(Array)
+ if mode == :add
+ value = (old_value + new_value).sort.uniq
+ elsif mode == :subtract
+ value = new_value - old_value
+ elsif prefer_self
+ value = old_value
+ else
+ value = new_value
+ end
+
# catch errors
elsif type_mismatch?(old_value, new_value)
raise 'Type mismatch. Cannot merge %s (%s) with %s (%s). Key is "%s", name is "%s".' % [
@@ -168,7 +192,7 @@ module LeapCli
key, self.class
]
- # merge strings, numbers, and sometimes arrays
+ # merge simple strings & numbers
else
if prefer_self
value = old_value
@@ -206,6 +230,10 @@ module LeapCli
end
end
+ def eval_file(filename)
+ evaluate_ruby(filename, File.read(filename))
+ end
+
protected
#
@@ -246,45 +274,42 @@ module LeapCli
# (`key` is just passed for debugging purposes)
#
def evaluate_ruby(key, value)
- result = nil
- if LeapCli.log_level >= 2
- result = self.instance_eval(value)
- else
- begin
- result = self.instance_eval(value)
- rescue SystemStackError => exc
- Util::log 0, :error, "while evaluating node '#{self.name}'"
- Util::log 0, "offending key: #{key}", :indent => 1
- Util::log 0, "offending string: #{value}", :indent => 1
- Util::log 0, "STACK OVERFLOW, BAILING OUT. There must be an eval loop of death (variables with circular dependencies).", :indent => 1
- raise SystemExit.new(1)
- rescue FileMissing => exc
- Util::bail! do
- if exc.options[:missing]
- Util::log :missing, exc.options[:missing].gsub('$node', self.name).gsub('$file', exc.path)
- else
- Util::log :error, "while evaluating node '#{self.name}'"
- Util::log "offending key: #{key}", :indent => 1
- Util::log "offending string: #{value}", :indent => 1
- Util::log "error message: no file '#{exc}'", :indent => 1
- end
- end
- rescue AssertionFailed => exc
- Util.bail! do
- Util::log :failed, "assertion while evaluating node '#{self.name}'"
- Util::log 'assertion: %s' % exc.assertion, :indent => 1
- Util::log "offending key: #{key}", :indent => 1
- end
- rescue SyntaxError, StandardError => exc
- Util::bail! do
- Util::log :error, "while evaluating node '#{self.name}'"
- Util::log "offending key: #{key}", :indent => 1
- Util::log "offending string: #{value}", :indent => 1
- Util::log "error message: #{exc.inspect}", :indent => 1
- end
+ self.instance_eval(value, key, 1)
+ rescue ConfigError => exc
+ raise exc # pass through
+ rescue SystemStackError => exc
+ Util::log 0, :error, "while evaluating node '#{self.name}'"
+ Util::log 0, "offending key: #{key}", :indent => 1
+ Util::log 0, "offending string: #{value}", :indent => 1
+ Util::log 0, "STACK OVERFLOW, BAILING OUT. There must be an eval loop of death (variables with circular dependencies).", :indent => 1
+ raise SystemExit.new(1)
+ rescue FileMissing => exc
+ Util::bail! do
+ if exc.options[:missing]
+ Util::log :missing, exc.options[:missing].gsub('$node', self.name).gsub('$file', exc.path)
+ else
+ Util::log :error, "while evaluating node '#{self.name}'"
+ Util::log "offending key: #{key}", :indent => 1
+ Util::log "offending string: #{value}", :indent => 1
+ Util::log "error message: no file '#{exc}'", :indent => 1
end
+ raise exc if DEBUG
+ end
+ rescue AssertionFailed => exc
+ Util.bail! do
+ Util::log :failed, "assertion while evaluating node '#{self.name}'"
+ Util::log 'assertion: %s' % exc.assertion, :indent => 1
+ Util::log "offending key: #{key}", :indent => 1
+ raise exc if DEBUG
+ end
+ rescue SyntaxError, StandardError => exc
+ Util::bail! do
+ Util::log :error, "while evaluating node '#{self.name}'"
+ Util::log "offending key: #{key}", :indent => 1
+ Util::log "offending string: #{value}", :indent => 1
+ Util::log "error message: #{exc.inspect}", :indent => 1
+ raise exc if DEBUG
end
- return result
end
private
diff --git a/lib/leap_cli/config/object_list.rb b/lib/leap_cli/config/object_list.rb
index cd69d9b..33ca4dd 100644
--- a/lib/leap_cli/config/object_list.rb
+++ b/lib/leap_cli/config/object_list.rb
@@ -20,8 +20,6 @@ module LeapCli
# If the key is a hash, we treat it as a condition and filter all the Config::Objects using the condition.
# A new ObjectList is returned.
#
- # If the key is an array, it is treated as an array of node names
- #
# Examples:
#
# nodes['vpn1']
@@ -30,47 +28,22 @@ module LeapCli
# nodes[:public_dns => true]
# all nodes with public dns
#
- # nodes[:services => 'openvpn', :services => 'tor']
+ # nodes[:services => 'openvpn', 'location.country_code' => 'US']
+ # all nodes with services containing 'openvpn' OR country code of US
+ #
+ # Sometimes, you want to do an OR condition with multiple conditions
+ # for the same field. Since hash keys must be unique, you can use
+ # an array representation instead:
+ #
+ # nodes[[:services, 'openvpn'], [:services, 'tor']]
# nodes with openvpn OR tor service
#
# nodes[:services => 'openvpn'][:tags => 'production']
# nodes with openvpn AND are production
#
def [](key)
- if key.is_a? Hash
- results = Config::ObjectList.new
- key.each do |field, match_value|
- field = field.is_a?(Symbol) ? field.to_s : field
- match_value = match_value.is_a?(Symbol) ? match_value.to_s : match_value
- if match_value.is_a?(String) && match_value =~ /^!/
- operator = :not_equal
- match_value = match_value.sub(/^!/, '')
- else
- operator = :equal
- end
- each do |name, config|
- value = config[field]
- if value.is_a? Array
- if operator == :equal && value.include?(match_value)
- results[name] = config
- elsif operator == :not_equal && !value.include?(match_value)
- results[name] = config
- end
- else
- if operator == :equal && value == match_value
- results[name] = config
- elsif operator == :not_equal && value != match_value
- results[name] = config
- end
- end
- end
- end
- results
- elsif key.is_a? Array
- key.inject(Config::ObjectList.new) do |list, node_name|
- list[node_name] = super(node_name.to_s)
- list
- end
+ if key.is_a?(Hash) || key.is_a?(Array)
+ filter(key)
else
super key.to_s
end
@@ -88,15 +61,40 @@ module LeapCli
end
end
- # def <<(object)
- # if object.is_a? Config::ObjectList
- # self.merge!(object)
- # elsif object['name']
- # self[object['name']] = object
- # else
- # raise ArgumentError.new('argument must be a Config::Object or a Config::ObjectList')
- # end
- # end
+ #
+ # filters this object list, producing a new list.
+ # filter is an array or a hash. see []
+ #
+ def filter(filter)
+ results = Config::ObjectList.new
+ filter.each do |field, match_value|
+ field = field.is_a?(Symbol) ? field.to_s : field
+ match_value = match_value.is_a?(Symbol) ? match_value.to_s : match_value
+ if match_value.is_a?(String) && match_value =~ /^!/
+ operator = :not_equal
+ match_value = match_value.sub(/^!/, '')
+ else
+ operator = :equal
+ end
+ each do |name, config|
+ value = config[field]
+ if value.is_a? Array
+ if operator == :equal && value.include?(match_value)
+ results[name] = config
+ elsif operator == :not_equal && !value.include?(match_value)
+ results[name] = config
+ end
+ else
+ if operator == :equal && value == match_value
+ results[name] = config
+ elsif operator == :not_equal && value != match_value
+ results[name] = config
+ end
+ end
+ end
+ end
+ results
+ end
def add(name, object)
self[name] = object
diff --git a/lib/leap_cli/config/tag.rb b/lib/leap_cli/config/tag.rb
index e5e719d..31f4f76 100644
--- a/lib/leap_cli/config/tag.rb
+++ b/lib/leap_cli/config/tag.rb
@@ -13,6 +13,13 @@ module LeapCli; module Config
super(manager)
@node_list = Config::ObjectList.new
end
+
+ # don't copy the node list pointer when this object is dup'ed.
+ def initialize_copy(orig)
+ super
+ @node_list = Config::ObjectList.new
+ end
+
end
end; end
diff --git a/lib/leap_cli/constants.rb b/lib/leap_cli/constants.rb
deleted file mode 100644
index bf30df1..0000000
--- a/lib/leap_cli/constants.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-module LeapCli
-
- PUPPET_DESTINATION = '/srv/leap'
- INITIALIZED_FILE = "#{PUPPET_DESTINATION}/initialized"
- DEFAULT_TAGS = ['leap_base','leap_service']
-
-end \ No newline at end of file
diff --git a/lib/leap_cli/exceptions.rb b/lib/leap_cli/exceptions.rb
index cd27f14..24a0fa7 100644
--- a/lib/leap_cli/exceptions.rb
+++ b/lib/leap_cli/exceptions.rb
@@ -6,6 +6,30 @@ module LeapCli
@node = node
super(msg)
end
+ def log
+ Util.log(0, :error, "in node `#{@node.name}`: " + self.message)
+ end
+ end
+
+ class FileMissing < StandardError
+ attr_accessor :path, :options
+ def initialize(path, options={})
+ @path = path
+ @options = options
+ end
+ def to_s
+ @path
+ end
+ end
+
+ class AssertionFailed < StandardError
+ attr_accessor :assertion
+ def initialize(assertion)
+ @assertion = assertion
+ end
+ def to_s
+ @assertion
+ end
end
end \ No newline at end of file
diff --git a/lib/leap_cli/leapfile.rb b/lib/leap_cli/leapfile.rb
index bdf2c37..8895f4d 100644
--- a/lib/leap_cli/leapfile.rb
+++ b/lib/leap_cli/leapfile.rb
@@ -16,13 +16,28 @@ module LeapCli
attr_accessor :leap_version
attr_accessor :log
attr_accessor :vagrant_network
- attr_accessor :platform_branch
- attr_accessor :allow_production_deploy
+ attr_accessor :environment
def initialize
@vagrant_network = '10.5.5.0/24'
end
+ #
+ # The way the Leapfile handles pinning of environment (self.environment) is a little tricky.
+ # If self.environment is nil, then there is no pin. If self.environment is 'default', then
+ # there is a pin to the default environment. The problem is that an environment of nil
+ # is used to indicate the default environment in node properties.
+ #
+ # This method returns the environment tag as needed when filtering nodes.
+ #
+ def environment_filter
+ if self.environment == 'default'
+ nil
+ else
+ self.environment
+ end
+ end
+
def load(search_directory=nil)
directory = File.expand_path(find_in_directory_tree('Leapfile', search_directory))
if directory == '/'
@@ -33,7 +48,7 @@ module LeapCli
#
@provider_directory_path = directory
read_settings(directory + '/Leapfile')
- read_settings(ENV['HOME'] + '/.leaprc')
+ read_settings(leaprc_path)
@platform_directory_path = File.expand_path(@platform_directory_path || '../leap_platform', @provider_directory_path)
#
@@ -51,20 +66,55 @@ module LeapCli
"You need platform version #{LeapCli::COMPATIBLE_PLATFORM_VERSION.first} to #{LeapCli::COMPATIBLE_PLATFORM_VERSION.last}."
end
- #
- # set defaults
- #
- if @allow_production_deploy.nil?
- # by default, only allow production deploys from 'master' or if not a git repo
- @allow_production_deploy = !LeapCli::Util.is_git_directory?(@provider_directory_path) ||
- LeapCli::Util.current_git_branch(@provider_directory_path) == 'master'
+ unless @allow_production_deploy.nil?
+ Util::log 0, :warning, "in Leapfile: @allow_production_deploy is no longer supported."
+ end
+ unless @platform_branch.nil?
+ Util::log 0, :warning, "in Leapfile: @platform_branch is no longer supported."
end
return true
end
end
+ def set(property, value)
+ edit_leaprc(property, value)
+ end
+
+ def unset(property)
+ edit_leaprc(property)
+ end
+
private
+ #
+ # adds or removes a line to .leaprc for this particular provider directory.
+ # if value is nil, the line is removed. if not nil, it is added or replaced.
+ #
+ def edit_leaprc(property, value=nil)
+ file_path = leaprc_path
+ lines = []
+ if File.exists?(file_path)
+ regexp = /self\.#{Regexp.escape(property)} = .*? if @provider_directory_path == '#{Regexp.escape(@provider_directory_path)}'/
+ File.readlines(file_path).each do |line|
+ unless line =~ regexp
+ lines << line
+ end
+ end
+ end
+ unless value.nil?
+ lines << "self.#{property} = #{value.inspect} if @provider_directory_path == '#{@provider_directory_path}'\n"
+ end
+ File.open(file_path, 'w') do |f|
+ f.write(lines.join)
+ end
+ rescue Errno::EACCES, IOError => exc
+ Util::bail! :error, "trying to save ~/.leaprc (#{exc})."
+ end
+
+ def leaprc_path
+ File.join(ENV['HOME'], '.leaprc')
+ end
+
def read_settings(file)
if File.exists? file
Util::log 2, :read, file
diff --git a/lib/leap_cli/log.rb b/lib/leap_cli/log.rb
index f496b9a..c345107 100644
--- a/lib/leap_cli/log.rb
+++ b/lib/leap_cli/log.rb
@@ -15,7 +15,7 @@ module LeapCli
def log_level
@log_level ||= 1
end
- def log_level=(value)
+ def set_log_level(value)
@log_level = value
end
diff --git a/lib/leap_cli/logger.rb b/lib/leap_cli/logger.rb
index 954ffe2..cc23aa8 100644
--- a/lib/leap_cli/logger.rb
+++ b/lib/leap_cli/logger.rb
@@ -138,7 +138,7 @@ module LeapCli
# TESTS
{ :match => /^PASS: /, :color => :green, :priority => -20},
{ :match => /^(FAIL|ERROR): /, :color => :red, :priority => -20},
- { :match => /^SKIP: /, :color => :yellow, :priority => -20}
+ { :match => /^(SKIP|WARN): /, :color => :yellow, :priority => -20}
]
diff --git a/lib/leap_cli/markdown_document_listener.rb b/lib/leap_cli/markdown_document_listener.rb
index 60b012e..c25a243 100644
--- a/lib/leap_cli/markdown_document_listener.rb
+++ b/lib/leap_cli/markdown_document_listener.rb
@@ -11,7 +11,7 @@ require 'gli/commands/help_modules/arg_name_formatter'
module LeapCli
class MarkdownDocumentListener
- def initialize(global_options,options,arguments)
+ def initialize(global_options,options,arguments,app)
@io = File.new(File.basename($0) + ".md",'w')
@nest = ''
@commands = [File.basename($0)]
diff --git a/lib/leap_cli/path.rb b/lib/leap_cli/path.rb
index cd0e169..1f6726a 100644
--- a/lib/leap_cli/path.rb
+++ b/lib/leap_cli/path.rb
@@ -26,15 +26,29 @@ module LeapCli; module Path
end
#
- # tries to find a file somewhere
+ # Tries to find a file somewhere.
+ # Path can be a named path or a relative path.
+ #
+ # relative paths are checked against
+ # provider/<path>
+ # provider/files/<path>
+ # provider_base/<path>
+ # provider_base/files/<path>
+ #
#
def self.find_file(arg)
[Path.provider, Path.provider_base].each do |base|
- file_path = named_path(arg, base)
- return file_path if File.exists?(file_path)
- if arg.is_a? String
- file_path = base + '/files/' + arg
- return file_path if File.exists?(file_path)
+ if arg.is_a?(Symbol) || arg.is_a?(Array)
+ named_path(arg, base).tap {|path|
+ return path if File.exists?(path)
+ }
+ else
+ File.join(base, arg).tap {|path|
+ return path if File.exists?(path)
+ }
+ File.join(base, 'files', arg).tap {|path|
+ return path if File.exists?(path)
+ }
end
end
return nil
diff --git a/lib/leap_cli/remote/leap_plugin.rb b/lib/leap_cli/remote/leap_plugin.rb
index a284712..af88c2a 100644
--- a/lib/leap_cli/remote/leap_plugin.rb
+++ b/lib/leap_cli/remote/leap_plugin.rb
@@ -26,7 +26,7 @@ module LeapCli; module Remote; module LeapPlugin
#
def assert_initialized
begin
- test_initialized_file = "test -f #{INITIALIZED_FILE}"
+ test_initialized_file = "test -f #{Leap::Platform.init_path}"
check_required_packages = "! dpkg-query -W --showformat='${Status}\n' #{required_packages} 2>&1 | grep -q -E '(deinstall|no packages)'"
run "#{test_initialized_file} && #{check_required_packages} && echo ok"
rescue Capistrano::CommandError => exc
@@ -57,7 +57,7 @@ module LeapCli; module Remote; module LeapPlugin
end
def mark_initialized
- run "touch #{INITIALIZED_FILE}"
+ run "touch #{Leap::Platform.init_path}"
end
#
diff --git a/lib/leap_cli/remote/puppet_plugin.rb b/lib/leap_cli/remote/puppet_plugin.rb
index 9c41380..e3f6be2 100644
--- a/lib/leap_cli/remote/puppet_plugin.rb
+++ b/lib/leap_cli/remote/puppet_plugin.rb
@@ -6,7 +6,7 @@
module LeapCli; module Remote; module PuppetPlugin
def apply(options)
- run "#{PUPPET_DESTINATION}/bin/puppet_command set_hostname apply #{flagize(options)}"
+ run "#{Leap::Platform.leap_dir}/bin/puppet_command set_hostname apply #{flagize(options)}"
end
private
diff --git a/lib/leap_cli/remote/rsync_plugin.rb b/lib/leap_cli/remote/rsync_plugin.rb
index 48f82d3..a6708f4 100644
--- a/lib/leap_cli/remote/rsync_plugin.rb
+++ b/lib/leap_cli/remote/rsync_plugin.rb
@@ -3,7 +3,7 @@
# (see RemoteCommand::new_capistrano)
#
-require 'rsync_command'
+autoload :RsyncCommand, 'rsync_command'
module LeapCli; module Remote; module RsyncPlugin
diff --git a/lib/leap_cli/remote/tasks.rb b/lib/leap_cli/remote/tasks.rb
index e66b0a8..7fd8d64 100644
--- a/lib/leap_cli/remote/tasks.rb
+++ b/lib/leap_cli/remote/tasks.rb
@@ -34,7 +34,7 @@ BAD_APT_GET_UPDATE = /(BADSIG|NO_PUBKEY|KEYEXPIRED|REVKEYSIG|NODATA)/
task :install_prerequisites, :max_hosts => MAX_HOSTS do
apt_get = "DEBIAN_FRONTEND=noninteractive apt-get -q -y -o DPkg::Options::=--force-confold"
- leap.mkdirs LeapCli::PUPPET_DESTINATION
+ leap.mkdirs Leap::Platform.leap_dir
run "echo 'en_US.UTF-8 UTF-8' > /etc/locale.gen"
leap.log :updating, "package list" do
run "apt-get update" do |channel, stream, data|
diff --git a/lib/leap_cli/requirements.rb b/lib/leap_cli/requirements.rb
deleted file mode 100644
index f1f0952..0000000
--- a/lib/leap_cli/requirements.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# run 'rake update-requirements' to generate this file.
-module LeapCli
- REQUIREMENTS = [
- "provider.ca.name",
- "provider.ca.server_certificates.bit_size",
- "provider.ca.server_certificates.digest",
- "provider.ca.server_certificates.life_span",
- "common.x509.use",
- "provider.domain",
- "provider.name",
- "provider.ca.server_certificates.bit_size",
- "provider.ca.server_certificates.digest",
- "provider.ca.name",
- "provider.ca.bit_size",
- "provider.ca.life_span",
- "provider.ca.client_certificates.unlimited_prefix",
- "provider.ca.client_certificates.limited_prefix"
- ]
-end
diff --git a/lib/leap_cli/ssh_key.rb b/lib/leap_cli/ssh_key.rb
index a525128..bd5bf43 100644
--- a/lib/leap_cli/ssh_key.rb
+++ b/lib/leap_cli/ssh_key.rb
@@ -33,6 +33,7 @@ module LeapCli
end
end
return key
+ rescue StandardError => exc
end
def self.load_from_file(filename)
diff --git a/lib/leap_cli/util.rb b/lib/leap_cli/util.rb
index 0174158..07ffcec 100644
--- a/lib/leap_cli/util.rb
+++ b/lib/leap_cli/util.rb
@@ -30,7 +30,7 @@ module LeapCli
#
def bail!(*message)
if block_given?
- LeapCli.log_level = 3
+ LeapCli.set_log_level(3)
yield
elsif message
log 0, *message
diff --git a/lib/leap_cli/util/remote_command.rb b/lib/leap_cli/util/remote_command.rb
index 6b4d75f..6353e36 100644
--- a/lib/leap_cli/util/remote_command.rb
+++ b/lib/leap_cli/util/remote_command.rb
@@ -78,10 +78,20 @@ module LeapCli; module Util; module RemoteCommand
:keys_only => false, # Don't you dare change this.
:global_known_hosts_file => path(:known_hosts),
:user_known_hosts_file => '/dev/null',
- :paranoid => true
+ :paranoid => true,
+ :verbose => net_ssh_log_level
}
end
+ def net_ssh_log_level
+ case LeapCli.log_level
+ when 1 then 3
+ when 2 then 2
+ when 3 then 1
+ else 0
+ end
+ end
+
#
# For notes on advanced ways to set server-specific options, see
# http://railsware.com/blog/2011/11/02/advanced-server-definitions-in-capistrano/
diff --git a/lib/leap_cli/util/secret.rb b/lib/leap_cli/util/secret.rb
index a6bd7a3..837a0af 100644
--- a/lib/leap_cli/util/secret.rb
+++ b/lib/leap_cli/util/secret.rb
@@ -4,7 +4,7 @@
#
# Uses OpenSSL random number generator instead of Ruby's rand function
#
-require 'openssl'
+autoload :OpenSSL, 'openssl'
module LeapCli; module Util
class Secret
diff --git a/lib/leap_cli/util/x509.rb b/lib/leap_cli/util/x509.rb
index 9ecd92d..787fdfa 100644
--- a/lib/leap_cli/util/x509.rb
+++ b/lib/leap_cli/util/x509.rb
@@ -1,5 +1,6 @@
-require 'openssl'
-require 'certificate_authority'
+autoload :OpenSSL, 'openssl'
+autoload :CertificateAuthority, 'certificate_authority'
+
require 'digest'
require 'digest/md5'
require 'digest/sha1'
diff --git a/lib/leap_cli/version.rb b/lib/leap_cli/version.rb
index df0f87a..0248036 100644
--- a/lib/leap_cli/version.rb
+++ b/lib/leap_cli/version.rb
@@ -1,7 +1,7 @@
module LeapCli
unless defined?(LeapCli::VERSION)
- VERSION = '1.5.7'
- COMPATIBLE_PLATFORM_VERSION = '0.5.2'..'1.99'
+ VERSION = '1.6.1'
+ COMPATIBLE_PLATFORM_VERSION = '0.6.0'..'1.99'
SUMMARY = 'Command line interface to the LEAP platform'
DESCRIPTION = 'The command "leap" can be used to manage a bevy of servers running the LEAP platform from the comfort of your own home.'
LOAD_PATHS = ['lib', 'vendor/certificate_authority/lib', 'vendor/rsync_command/lib']