summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorelijah <elijah@riseup.net>2015-08-18 11:58:05 -0700
committerelijah <elijah@riseup.net>2015-08-18 11:58:05 -0700
commited604349a4035eed2bccefa9aa030d93ad4f6b58 (patch)
tree333a6c5335f25935be13c1ffed82b8cc7c293df8
parentcde4ae067e034e3969629f4d8da4f46daae3c351 (diff)
moved commands to leap_platform
-rwxr-xr-xbin/leap18
-rw-r--r--lib/leap_cli.rb2
-rw-r--r--lib/leap_cli/bootstrap.rb191
-rw-r--r--lib/leap_cli/commands/README101
-rw-r--r--lib/leap_cli/commands/ca.rb518
-rw-r--r--lib/leap_cli/commands/clean.rb16
-rw-r--r--lib/leap_cli/commands/common.rb61
-rw-r--r--lib/leap_cli/commands/compile.rb384
-rw-r--r--lib/leap_cli/commands/db.rb65
-rw-r--r--lib/leap_cli/commands/deploy.rb368
-rw-r--r--lib/leap_cli/commands/env.rb76
-rw-r--r--lib/leap_cli/commands/facts.rb100
-rw-r--r--lib/leap_cli/commands/inspect.rb144
-rw-r--r--lib/leap_cli/commands/list.rb132
-rw-r--r--lib/leap_cli/commands/new.rb6
-rw-r--r--lib/leap_cli/commands/node.rb165
-rw-r--r--lib/leap_cli/commands/node_init.rb169
-rw-r--r--lib/leap_cli/commands/pre.rb67
-rw-r--r--lib/leap_cli/commands/ssh.rb220
-rw-r--r--lib/leap_cli/commands/test.rb74
-rw-r--r--lib/leap_cli/commands/user.rb136
-rw-r--r--lib/leap_cli/commands/util.rb50
-rw-r--r--lib/leap_cli/commands/vagrant.rb197
-rw-r--r--lib/leap_cli/config/manager.rb9
-rw-r--r--lib/leap_cli/log.rb1
25 files changed, 272 insertions, 2998 deletions
diff --git a/bin/leap b/bin/leap
index 8da9356..3bb8975 100755
--- a/bin/leap
+++ b/bin/leap
@@ -15,7 +15,6 @@ else
end
LEAP_CLI_BASE_DIR = File.expand_path('..', File.dirname(File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__))
-ORIGINAL_ARGV = ARGV.dup
begin
# First, try to load the leap_cli code that is local to this `leap` command.
@@ -74,17 +73,13 @@ module LeapCli::Commands
program_desc LeapCli::SUMMARY
program_long_desc LeapCli::DESCRIPTION
+ LeapCli::Bootstrap::setup(ARGV)
+
# handle --version ourselves (and not GLI)
if ARGV.grep(/--version/).any?
- puts "leap #{LeapCli::VERSION}, ruby #{RUBY_VERSION}"
- begin
- commands_from('leap_cli/commands')
- initialize_leap_cli(false, {:verbose => 2})
- rescue StandardError => exc
- puts exc.to_s
- raise exc if DEBUG
- end
- exit(0)
+ LeapCli::Bootstrap::handle_version(self)
+ else
+ LeapCli::Bootstrap::load_libraries(self)
end
# disable GLI error catching
@@ -92,9 +87,6 @@ module LeapCli::Commands
def error_message(msg)
end
- # load commands
- commands_from('leap_cli/commands')
-
# run command
begin
exit_status = run(ARGV)
diff --git a/lib/leap_cli.rb b/lib/leap_cli.rb
index 0d95f5b..9052f8c 100644
--- a/lib/leap_cli.rb
+++ b/lib/leap_cli.rb
@@ -36,6 +36,7 @@ require 'leap_cli/util/secret'
require 'leap_cli/util/remote_command'
require 'leap_cli/util/x509'
require 'leap_cli/logger'
+require 'leap_cli/bootstrap'
require 'leap_cli/ssh_key'
require 'leap_cli/config/object'
@@ -54,7 +55,6 @@ require 'leap_cli/markdown_document_listener'
#
module LeapCli
Util.send(:extend, LeapCli::Log)
- Commands.send(:extend, LeapCli::Log)
Config::Manager.send(:include, LeapCli::Log)
extend LeapCli::Log
end
diff --git a/lib/leap_cli/bootstrap.rb b/lib/leap_cli/bootstrap.rb
new file mode 100644
index 0000000..c7df1a9
--- /dev/null
+++ b/lib/leap_cli/bootstrap.rb
@@ -0,0 +1,191 @@
+#
+# Initial bootstrap loading of all the necessary things that needed
+# for the `leap` command.
+#
+
+module LeapCli
+ module Bootstrap
+ extend LeapCli::Log
+ extend self
+
+ def setup(argv)
+ setup_logging(argv)
+ setup_leapfile(argv)
+ end
+
+ #
+ # print out the version string and exit.
+ # called from leap executable.
+ #
+ def handle_version(app)
+ puts "leap #{LeapCli::VERSION}, ruby #{RUBY_VERSION}"
+ begin
+ log_version
+ rescue StandardError => exc
+ puts exc.to_s
+ raise exc if DEBUG
+ end
+ exit(0)
+ end
+
+ #
+ # load the commands.
+ # called from leap executable.
+ #
+ def load_libraries(app)
+ if LeapCli.log_level >= 2
+ log_version
+ end
+ load_commands(app)
+ load_macros
+ end
+
+ #
+ # initialize the global options.
+ # called from pre.rb
+ #
+ def setup_global_options(app, global)
+ if global[:force]
+ global[:yes] = true
+ end
+ if Process::Sys.getuid == 0
+ Util.bail! "`leap` should not be run as root."
+ end
+ end
+
+ private
+
+ #
+ # Initial logging
+ #
+ # This is called very early by leap executable, because
+ # everything depends on the log file and log options
+ # being set correctly before any work is done.
+ #
+ # The Leapfile might later load additional logging
+ # options.
+ #
+ def setup_logging(argv)
+ options = parse_logging_options(argv)
+ verbose = (options[:verbose] || 1).to_i
+ if verbose
+ LeapCli.set_log_level(verbose)
+ end
+ if options[:log]
+ LeapCli.log_file = options[:log]
+ LeapCli::Util.log_raw(:log) { $0 + ' ' + argv.join(' ')}
+ end
+ unless options[:color].nil?
+ LeapCli.log_in_color = options[:color]
+ end
+ end
+
+ #
+ # load the leapfile and set the Path variables.
+ #
+ def setup_leapfile(argv)
+ LeapCli.leapfile.load
+ if LeapCli.leapfile.valid?
+ Path.set_platform_path(LeapCli.leapfile.platform_directory_path)
+ Path.set_provider_path(LeapCli.leapfile.provider_directory_path)
+ if !Path.provider || !File.directory?(Path.provider)
+ bail! { log :missing, "provider directory '#{Path.provider}'" }
+ end
+ if !Path.platform || !File.directory?(Path.platform)
+ bail! { log :missing, "platform directory '#{Path.platform}'" }
+ end
+ elsif !leapfile_optional?(argv)
+ puts
+ puts " ="
+ log :note, "There is no `Leapfile` in this directory, or any parent directory.\n"+
+ " = "+
+ "Without this file, most commands will not be available."
+ puts " ="
+ puts
+ end
+ end
+
+ #
+ # Add a log entry for the leap command and leap platform versions.
+ #
+ def log_version(force=false)
+ str = "leap command v#{LeapCli::VERSION}"
+ if Util.is_git_directory?(LEAP_CLI_BASE_DIR)
+ str << " (%s %s)" % [Util.current_git_branch(LEAP_CLI_BASE_DIR),
+ Util.current_git_commit(LEAP_CLI_BASE_DIR)]
+ else
+ str << " (%s)" % LEAP_CLI_BASE_DIR
+ end
+ log str
+ if LeapCli.leapfile.valid?
+ str = "leap platform v#{Leap::Platform.version}"
+ if Util.is_git_directory?(Path.platform)
+ str << " (%s %s)" % [Util.current_git_branch(Path.platform), Util.current_git_commit(Path.platform)]
+ end
+ log str
+ end
+ end
+
+ def parse_logging_options(argv)
+ argv = argv.dup
+ options = {:color => true, :verbose => 1}
+ loop do
+ current = argv.shift
+ case current
+ when '--verbose' then options[:verbose] = argv.shift;
+ when /-v[0-9]/ then options[:verbose] = current[-1];
+ when '--log' then options[:log] = argv.shift;
+ when '--no-color' then options[:color] = false;
+ when nil then break;
+ end
+ end
+ options
+ end
+
+ #
+ # Returns true if loading the Leapfile is optional.
+ #
+ # We could make the 'new' command skip the 'pre' command, and then load Leapfile
+ # from 'pre', but for various reasons we want the Leapfile loaded even earlier
+ # than that. So, we need a way to test to see if loading the leapfile is optional
+ # before any of the commands are loaded and the argument list is parsed by GLI.
+ # Yes, hacky.
+ #
+ def leapfile_optional?(argv)
+ if argv.include?('--version')
+ return true
+ else
+ without_flags = argv.select {|i| i !~ /^-/}
+ if without_flags.first == 'new'
+ return true
+ end
+ end
+ return false
+ end
+
+ #
+ # loads the GLI command definition files
+ #
+ def load_commands(app)
+ app.commands_from('leap_cli/commands')
+ if Path.platform
+ app.commands_from(Path.platform + '/lib/leap_cli/commands')
+ end
+ end
+
+ #
+ # loads the platform's macro definition files
+ #
+ def load_macros
+ if Path.platform
+ platform_macro_files = Dir[Path.platform + '/lib/leap_cli/macros/*.rb']
+ if platform_macro_files.any?
+ platform_macro_files.each do |macro_file|
+ require macro_file
+ end
+ end
+ end
+ end
+
+ end
+end
diff --git a/lib/leap_cli/commands/README b/lib/leap_cli/commands/README
index 00fcd84..fd731dd 100644
--- a/lib/leap_cli/commands/README
+++ b/lib/leap_cli/commands/README
@@ -1,101 +1,14 @@
-This directory contains ruby source files that define the available sub-commands of the `leap` executable.
+This directory contains ruby source files that define the available BUILT IN
+sub-commands of the `leap` executable.
For example, the command:
- leap init <directory>
+ leap new <directory>
-Lives in lib/leap_cli/commands/init.rb
+Lives in lib/leap_cli/commands/new.rb
+
+However, most commands for leap_cli are defined in the platform. This
+directory is `leap_platform/lib/leap_cli/commands`.
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/ca.rb b/lib/leap_cli/commands/ca.rb
deleted file mode 100644
index d5c6240..0000000
--- a/lib/leap_cli/commands/ca.rb
+++ /dev/null
@@ -1,518 +0,0 @@
-autoload :OpenSSL, 'openssl'
-autoload :CertificateAuthority, 'certificate_authority'
-autoload :Date, 'date'
-require 'digest/md5'
-
-module LeapCli; module Commands
-
- desc "Manage X.509 certificates"
- command :cert do |cert|
-
- cert.desc 'Creates two Certificate Authorities (one for validating servers and one for validating clients).'
- cert.long_desc 'See see what values are used in the generation of the certificates (like name and key size), run `leap inspect provider` and look for the "ca" property. To see the details of the created certs, run `leap inspect <file>`.'
- cert.command :ca do |ca|
- ca.action do |global_options,options,args|
- assert_config! 'provider.ca.name'
- generate_new_certificate_authority(:ca_key, :ca_cert, provider.ca.name)
- generate_new_certificate_authority(:client_ca_key, :client_ca_cert, provider.ca.name + ' (client certificates only!)')
- end
- end
-
- cert.desc 'Creates or renews a X.509 certificate/key pair for a single node or all nodes, but only if needed.'
- cert.long_desc 'This command will a generate new certificate for a node if some value in the node has changed ' +
- 'that is included in the certificate (like hostname or IP address), or if the old certificate will be expiring soon. ' +
- 'Sometimes, you might want to force the generation of a new certificate, ' +
- 'such as in the cases where you have changed a CA parameter for server certificates, like bit size or digest hash. ' +
- 'In this case, use --force. If <node-filter> is empty, this command will apply to all nodes.'
- cert.arg_name 'FILTER'
- cert.command :update do |update|
- update.switch 'force', :desc => 'Always generate new certificates', :negatable => false
- update.action do |global_options,options,args|
- update_certificates(manager.filter!(args), options)
- end
- end
-
- cert.desc 'Creates a Diffie-Hellman parameter file, needed for forward secret OpenVPN ciphers.' # (needed for server-side of some TLS connections)
- cert.command :dh do |dh|
- dh.action do |global_options,options,args|
- long_running do
- if cmd_exists?('certtool')
- log 0, 'Generating DH parameters (takes a long time)...'
- output = assert_run!('certtool --generate-dh-params --sec-param high')
- output.sub! /.*(-----BEGIN DH PARAMETERS-----.*-----END DH PARAMETERS-----).*/m, '\1'
- output << "\n"
- write_file!(:dh_params, output)
- else
- log 0, 'Generating DH parameters (takes a REALLY long time)...'
- output = OpenSSL::PKey::DH.generate(3248).to_pem
- write_file!(:dh_params, output)
- end
- end
- end
- end
-
- #
- # hints:
- #
- # inspect CSR:
- # openssl req -noout -text -in files/cert/x.csr
- #
- # generate CSR with openssl to see how it compares:
- # openssl req -sha256 -nodes -newkey rsa:2048 -keyout example.key -out example.csr
- #
- # validate a CSR:
- # http://certlogik.com/decoder/
- #
- # nice details about CSRs:
- # 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`, "+
- "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'
- assert_config! 'provider.default_language'
- assert_config! 'provider.ca.server_certificates.bit_size'
- assert_config! 'provider.ca.server_certificates.digest'
- domain = options[:domain] || provider.domain
-
- unless global_options[:force]
- assert_files_missing! [:commercial_key, domain], [:commercial_csr, domain],
- :msg => 'If you really want to create a new key and CSR, remove these files first or run with --force.'
- end
-
- server_certificates = provider.ca.server_certificates
-
- # RSA key
- keypair = CertificateAuthority::MemoryKeyMaterial.new
- 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
- 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
-
- # Sign using our own CA, for use in testing but hopefully not production.
- # It is not that commerical CAs are so secure, it is just that signing your own certs is
- # a total drag for the user because they must click through dire warnings.
- #if options[:sign]
- log :generating, "self-signed x509 server certificate for testing purposes" do
- cert = csr.to_cert
- cert.serial_number.number = cert_serial_number(domain)
- cert.not_before = yesterday
- cert.not_after = yesterday.advance(:years => 1)
- cert.parent = ca_root
- cert.sign! domain_test_signing_profile
- write_file! [:commercial_cert, domain], cert.to_pem
- log "please replace this file with the real certificate you get from a CA using #{Path.relative_path([:commercial_csr, domain])}"
- end
- #end
-
- # FAKE CA
- unless file_exists? :commercial_ca_cert
- log :using, "generated CA in place of commercial CA for testing purposes" do
- write_file! :commercial_ca_cert, read_file!(:ca_cert)
- log "please also replace this file with the CA cert from the commercial authority you use."
- end
- end
- end
- end
- end
-
- protected
-
- #
- # will generate new certificates for the specified nodes, if needed.
- #
- def update_certificates(nodes, options={})
- assert_files_exist! :ca_cert, :ca_key, :msg => 'Run `leap cert ca` to create them'
- assert_config! 'provider.ca.server_certificates.bit_size'
- assert_config! 'provider.ca.server_certificates.digest'
- assert_config! 'provider.ca.server_certificates.life_span'
- assert_config! 'common.x509.use'
-
- 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])
- elsif options[:force] || cert_needs_updating?(node)
- generate_cert_for_node(node)
- end
- end
- end
-
- private
-
- def generate_new_certificate_authority(key_file, cert_file, common_name)
- assert_files_missing! key_file, cert_file
- assert_config! 'provider.ca.name'
- assert_config! 'provider.ca.bit_size'
- assert_config! 'provider.ca.life_span'
-
- root = CertificateAuthority::Certificate.new
-
- # set subject
- root.subject.common_name = common_name
- possible = ['country', 'state', 'locality', 'organization', 'organizational_unit', 'email_address']
- provider.ca.keys.each do |key|
- if possible.include?(key)
- root.subject.send(key + '=', provider.ca[key])
- end
- end
-
- # set expiration
- root.not_before = yesterday
- root.not_after = yesterday_advance(provider.ca.life_span)
-
- # generate private key
- root.serial_number.number = 1
- root.key_material.generate_key(provider.ca.bit_size)
-
- # sign self
- root.signing_entity = true
- root.parent = root
- root.sign!(ca_root_signing_profile)
-
- # save
- write_file!(key_file, root.key_material.private_key.to_pem)
- write_file!(cert_file, root.to_pem)
- end
-
- #
- # returns true if the certs associated with +node+ need to be regenerated.
- #
- def cert_needs_updating?(node)
- if !file_exists?([:node_x509_cert, node.name], [:node_x509_key, node.name])
- return true
- else
- cert = load_certificate_file([:node_x509_cert, node.name])
- if cert.not_after < Time.now.advance(:months => 2)
- log :updating, "cert for node '#{node.name}' because it will expire soon"
- return true
- end
- if cert.subject.common_name != node.domain.full
- log :updating, "cert for node '#{node.name}' because domain.full has changed (was #{cert.subject.common_name}, now #{node.domain.full})"
- return true
- end
- cert.openssl_body.extensions.each do |ext|
- if ext.oid == "subjectAltName"
- ips = []
- dns_names = []
- ext.value.split(",").each do |value|
- value.strip!
- ips << $1 if value =~ /^IP Address:(.*)$/
- dns_names << $1 if value =~ /^DNS:(.*)$/
- end
- dns_names.sort!
- if ips.first != node.ip_address
- log :updating, "cert for node '#{node.name}' because ip_address has changed (from #{ips.first} to #{node.ip_address})"
- return true
- elsif dns_names != dns_names_for_node(node)
- log :updating, "cert for node '#{node.name}' because domain name aliases have changed\n from: #{dns_names.inspect}\n to: #{dns_names_for_node(node).inspect})"
- return true
- end
- end
- end
- end
- 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])
- path = Path.relative_path([:commercial_cert, domain])
- if cert.not_after < Time.now.utc
- log :error, "the commercial certificate '#{path}' has EXPIRED! " +
- "You should renew it with `leap cert csr --domain #{domain}`."
- elsif cert.not_after < Time.now.advance(:months => 2)
- log :warning, "the commercial certificate '#{path}' 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
-
- cert = CertificateAuthority::Certificate.new
-
- # set subject
- cert.subject.common_name = node.domain.full
- cert.serial_number.number = cert_serial_number(node.domain.full)
-
- # set expiration
- cert.not_before = yesterday
- cert.not_after = yesterday_advance(provider.ca.server_certificates.life_span)
-
- # generate key
- cert.key_material.generate_key(provider.ca.server_certificates.bit_size)
-
- # sign
- cert.parent = ca_root
- cert.sign!(server_signing_profile(node))
-
- # save
- write_file!([:node_x509_key, node.name], cert.key_material.private_key.to_pem)
- write_file!([:node_x509_cert, node.name], cert.to_pem)
- end
-
- #
- # yields client key and cert suitable for testing
- #
- def generate_test_client_cert(prefix=nil)
- cert = CertificateAuthority::Certificate.new
- cert.serial_number.number = cert_serial_number(provider.domain)
- cert.subject.common_name = [prefix, random_common_name(provider.domain)].join
- cert.not_before = yesterday
- cert.not_after = yesterday.advance(:years => 1)
- cert.key_material.generate_key(1024) # just for testing, remember!
- cert.parent = client_ca_root
- cert.sign! client_test_signing_profile
- 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)
- end
- end
-
- def client_ca_root
- @client_ca_root ||= begin
- load_certificate_file(:client_ca_cert, :client_ca_key)
- end
- end
-
- def load_certificate_file(crt_file, key_file=nil, password=nil)
- crt = read_file!(crt_file)
- openssl_cert = OpenSSL::X509::Certificate.new(crt)
- cert = CertificateAuthority::Certificate.from_openssl(openssl_cert)
- if key_file
- key = read_file!(key_file)
- cert.key_material.private_key = OpenSSL::PKey::RSA.new(key, password)
- end
- return cert
- end
-
- def ca_root_signing_profile
- {
- "extensions" => {
- "basicConstraints" => {"ca" => true},
- "keyUsage" => {
- "usage" => ["critical", "keyCertSign"]
- },
- "extendedKeyUsage" => {
- "usage" => []
- }
- }
- }
- end
-
- #
- # For keyusage, openvpn server certs can have keyEncipherment or keyAgreement.
- # Web browsers seem to break without keyEncipherment.
- # For now, I am using digitalSignature + keyEncipherment
- #
- # * digitalSignature -- for (EC)DHE cipher suites
- # "The digitalSignature bit is asserted when the subject public key is used
- # with a digital signature mechanism to support security services other
- # than certificate signing (bit 5), or CRL signing (bit 6). Digital
- # signature mechanisms are often used for entity authentication and data
- # origin authentication with integrity."
- #
- # * keyEncipherment ==> for plain RSA cipher suites
- # "The keyEncipherment bit is asserted when the subject public key is used for
- # key transport. For example, when an RSA key is to be used for key management,
- # then this bit is set."
- #
- # * keyAgreement ==> for used with DH, not RSA.
- # "The keyAgreement bit is asserted when the subject public key is used for key
- # agreement. For example, when a Diffie-Hellman key is to be used for key
- # management, then this bit is set."
- #
- # digest options: SHA512, SHA256, SHA1
- #
- def server_signing_profile(node)
- {
- "digest" => provider.ca.server_certificates.digest,
- "extensions" => {
- "keyUsage" => {
- "usage" => ["digitalSignature", "keyEncipherment"]
- },
- "extendedKeyUsage" => {
- "usage" => ["serverAuth", "clientAuth"]
- },
- "subjectAltName" => {
- "ips" => [node.ip_address],
- "dns_names" => dns_names_for_node(node)
- }
- }
- }
- end
-
- #
- # This is used when signing the main cert for the provider's domain
- # with our own CA (for testing purposes). Typically, this cert would
- # be purchased from a commercial CA, and not signed this way.
- #
- def domain_test_signing_profile
- {
- "digest" => "SHA256",
- "extensions" => {
- "keyUsage" => {
- "usage" => ["digitalSignature", "keyEncipherment"]
- },
- "extendedKeyUsage" => {
- "usage" => ["serverAuth"]
- }
- }
- }
- end
-
- #
- # This is used when signing a dummy client certificate that is only to be
- # used for testing.
- #
- def client_test_signing_profile
- {
- "digest" => "SHA256",
- "extensions" => {
- "keyUsage" => {
- "usage" => ["digitalSignature"]
- },
- "extendedKeyUsage" => {
- "usage" => ["clientAuth"]
- }
- }
- }
- end
-
- def dns_names_for_node(node)
- names = [node.domain.internal, node.domain.full]
- if node['dns'] && node.dns['aliases'] && node.dns.aliases.any?
- names += node.dns.aliases
- end
- names.compact!
- names.sort!
- names.uniq!
- return names
- end
-
- #
- # For cert serial numbers, we need a non-colliding number less than 160 bits.
- # md5 will do nicely, since there is no need for a secure hash, just a short one.
- # (md5 is 128 bits)
- #
- def cert_serial_number(domain_name)
- Digest::MD5.hexdigest("#{domain_name} -- #{Time.now}").to_i(16)
- end
-
- #
- # for the random common name, we need a text string that will be unique across all certs.
- # ruby 1.8 doesn't have a built-in uuid generator, or we would use SecureRandom.uuid
- #
- def random_common_name(domain_name)
- 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
- ##
- ## note: we use 'yesterday' instead of 'today', because times are in UTC, and some people on the planet
- ## are behind UTC.
- ##
-
- def yesterday
- t = Time.now - 24*24*60
- Time.utc t.year, t.month, t.day
- end
-
- def yesterday_advance(string)
- number, unit = string.split(' ')
- unless ['years', 'months', 'days', 'hours', 'minutes'].include? unit
- bail!("The time property '#{string}' is missing a unit (one of: years, months, days, hours, minutes).")
- end
- unless number.to_i.to_s == number
- bail!("The time property '#{string}' is missing a number.")
- end
- yesterday.advance(unit.to_sym => number.to_i)
- end
-
-end; end
diff --git a/lib/leap_cli/commands/clean.rb b/lib/leap_cli/commands/clean.rb
deleted file mode 100644
index a9afff5..0000000
--- a/lib/leap_cli/commands/clean.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-module LeapCli
- module Commands
-
- desc 'Removes all files generated with the "compile" command.'
- command :clean do |c|
- c.action do |global_options,options,args|
- Dir.glob(path([:hiera, '*'])).each do |file|
- remove_file! file
- end
- remove_file! path(:authorized_keys)
- remove_file! path(:known_hosts)
- end
- end
-
- end
-end \ No newline at end of file
diff --git a/lib/leap_cli/commands/common.rb b/lib/leap_cli/commands/common.rb
new file mode 100644
index 0000000..7bf49db
--- /dev/null
+++ b/lib/leap_cli/commands/common.rb
@@ -0,0 +1,61 @@
+#
+# Some common helpers available to all LeapCli::Commands
+#
+# This also includes utility methods, and makes all instance
+# methods available as class methods.
+#
+
+module LeapCli
+ module Commands
+
+ extend self
+ extend LeapCli::Log
+ extend LeapCli::Util
+ extend LeapCli::Util::RemoteCommand
+
+ protected
+
+ def path(name)
+ Path.named_path(name)
+ end
+
+ #
+ # keeps prompting the user for a numbered choice, until they pick a good one or bail out.
+ #
+ # block is yielded and is responsible for rendering the choices.
+ #
+ def numbered_choice_menu(msg, items, &block)
+ while true
+ say("\n" + msg + ':')
+ items.each_with_index &block
+ say("q. quit")
+ index = ask("number 1-#{items.length}> ")
+ if index.empty?
+ next
+ elsif index =~ /q/
+ bail!
+ else
+ i = index.to_i - 1
+ if i < 0 || i >= items.length
+ bail!
+ else
+ return i
+ end
+ end
+ end
+ end
+
+ def parse_node_list(nodes)
+ if nodes.is_a? Config::Object
+ Config::ObjectList.new(nodes)
+ elsif nodes.is_a? Config::ObjectList
+ nodes
+ elsif nodes.is_a? String
+ manager.filter!(nodes)
+ else
+ bail! "argument error"
+ end
+ end
+
+ end
+end \ No newline at end of file
diff --git a/lib/leap_cli/commands/compile.rb b/lib/leap_cli/commands/compile.rb
deleted file mode 100644
index a14c267..0000000
--- a/lib/leap_cli/commands/compile.rb
+++ /dev/null
@@ -1,384 +0,0 @@
-require 'socket'
-
-module LeapCli
- module Commands
-
- desc "Compile generated files."
- command [:compile, :c] 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|
- 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
- if manager.environment_names.include?(environment)
- compile_hiera_files(manager.filter([environment]), false)
- else
- bail! "There is no environment named `#{environment}`."
- end
- else
- clean_export = LeapCli.leapfile.environment.nil?
- compile_hiera_files(manager.filter, clean_export)
- end
- if file_exists?(:static_web_readme)
- compile_provider_json(environment)
- end
- end
- end
-
- c.desc "Compile a DNS zone file for your provider."
- c.command :zone do |zone|
- zone.action do |global_options, options, args|
- compile_zone_file
- end
- end
-
- c.desc "Compile provider.json bootstrap files for your provider."
- c.command 'provider.json' do |provider|
- provider.action do |global_options, options, args|
- compile_provider_json
- end
- end
-
- c.desc "Generate a list of firewall rules. These rules are already "+
- "implemented on each node, but you might want the list of all "+
- "rules in case you also have a restrictive network firewall."
- c.command :firewall do |zone|
- zone.action do |global_options, options, args|
- compile_firewall
- end
- end
-
- c.default_command :all
- end
-
- protected
-
- #
- # a "clean" export of secrets will also remove keys that are no longer used,
- # but this should not be done if we are not examining all possible nodes.
- #
- def compile_hiera_files(nodes, clean_export)
- update_compiled_ssh_configs # must come first
- sanity_check(nodes)
- manager.export_nodes(nodes)
- manager.export_secrets(clean_export)
- end
-
- def update_compiled_ssh_configs
- generate_monitor_ssh_keys
- update_authorized_keys
- update_known_hosts
- end
-
- def sanity_check(nodes)
- # confirm that every node has a unique ip address
- ips = {}
- nodes.pick_fields('ip_address').each do |name, ip_address|
- if ips.key?(ip_address)
- bail! {
- log(:fatal_error, "Every node must have its own IP address.") {
- log "Nodes `#{name}` and `#{ips[ip_address]}` are both configured with `#{ip_address}`."
- }
- }
- else
- ips[ip_address] = name
- end
- end
- # confirm that the IP address of this machine is not also used for a node.
- Socket.ip_address_list.each do |addrinfo|
- if !addrinfo.ipv4_private? && ips.key?(addrinfo.ip_address)
- ip = addrinfo.ip_address
- name = ips[ip]
- bail! {
- log(:fatal_error, "Something is very wrong. The `leap` command must only be run on your sysadmin machine, not on a provider node.") {
- log "This machine has the same IP address (#{ip}) as node `#{name}`."
- }
- }
- end
- end
- end
-
- ##
- ## SSH
- ##
-
- #
- # generates a ssh key pair that is used only by remote monitors
- # to connect to nodes and run certain allowed commands.
- #
- # every node has the public monitor key added to their authorized
- # keys, and every monitor node has a copy of the private monitor key.
- #
- def generate_monitor_ssh_keys
- priv_key_file = path(:monitor_priv_key)
- pub_key_file = path(:monitor_pub_key)
- unless file_exists?(priv_key_file, pub_key_file)
- ensure_dir(File.dirname(priv_key_file))
- ensure_dir(File.dirname(pub_key_file))
- cmd = %(ssh-keygen -N '' -C 'monitor' -t rsa -b 4096 -f '%s') % priv_key_file
- assert_run! cmd
- if file_exists?(priv_key_file, pub_key_file)
- log :created, priv_key_file
- log :created, pub_key_file
- else
- log :failed, 'to create monitor ssh keys'
- end
- end
- end
-
- #
- # Compiles the authorized keys file, which gets installed on every during init.
- # Afterwards, puppet installs an authorized keys file that is generated differently
- # (see authorized_keys() in macros.rb)
- #
- def update_authorized_keys
- buffer = StringIO.new
- keys = Dir.glob(path([:user_ssh, '*']))
- if keys.empty?
- bail! "You must have at least one public SSH user key configured in order to proceed. See `leap help add-user`."
- end
- if file_exists?(path(:monitor_pub_key))
- keys << path(:monitor_pub_key)
- end
- keys.sort.each do |keyfile|
- ssh_type, ssh_key = File.read(keyfile).strip.split(" ")
- buffer << ssh_type
- buffer << " "
- buffer << ssh_key
- buffer << " "
- buffer << Path.relative_path(keyfile)
- buffer << "\n"
- end
- write_file!(:authorized_keys, buffer.string)
- end
-
- #
- # generates the known_hosts file.
- #
- # we do a 'late' binding on the hostnames and ip part of the ssh pub key record in order to allow
- # for the possibility that the hostnames or ip has changed in the node configuration.
- #
- def update_known_hosts
- buffer = StringIO.new
- buffer << "#\n"
- buffer << "# This file is automatically generated by the command `leap`. You should NOT modify this file.\n"
- buffer << "# Instead, rerun `leap node init` on whatever node is causing SSH problems.\n"
- buffer << "#\n"
- manager.nodes.keys.sort.each do |node_name|
- node = manager.nodes[node_name]
- hostnames = [node.name, node.domain.internal, node.domain.full, node.ip_address].join(',')
- pub_key = read_file([:node_ssh_pub_key,node.name])
- if pub_key
- buffer << [hostnames, pub_key].join(' ')
- buffer << "\n"
- end
- end
- write_file!(:known_hosts, buffer.string)
- end
-
- ##
- ## provider.json
- ##
-
- #
- # generates static provider.json files that can put into place
- # (e.g. https://domain/provider.json) for the cases where the
- # webapp domain does not match the provider's domain.
- #
- def compile_provider_json(environments=nil)
- webapp_nodes = manager.nodes[:services => 'webapp']
- write_file!(:static_web_readme, STATIC_WEB_README)
- environments ||= manager.environment_names
- environments.each do |env|
- node = webapp_nodes[:environment => env].values.first
- if node
- env ||= 'default'
- write_file!(
- [:static_web_provider_json, env],
- node['definition_files']['provider']
- )
- write_file!(
- [:static_web_htaccess, env],
- HTACCESS_FILE % {:min_version => manager.env(env).provider.client_version['min']}
- )
- end
- end
- end
-
- HTACCESS_FILE = %[
- <Location /provider.json>
- Header set X-Minimum-Client-Version %{min_version}
- </Location>
-]
-
- STATIC_WEB_README = %[
-This directory contains statically rendered copies of the `provider.json` file
-used by the client to "bootstrap" configure itself for use with your service
-provider.
-
-There is a separate provider.json file for each environment, although you
-should only need 'production/provider.json' or, if you have no environments
-configured, 'default/provider.json'.
-
-To clarify, this is the public `provider.json` file used by the client, not the
-`provider.json` file that is used to configure the provider.
-
-The provider.json file must be available at `https://domain/provider.json`
-(unless this provider is included in the list of providers which are pre-
-seeded in client).
-
-This provider.json file can be served correctly in one of three ways:
-
-(1) If the property webapp.domain is not configured, then the web app will be
- installed at https://domain/ and it will handle serving the provider.json file.
-
-(2) If one or more nodes have the 'static' service configured for the provider's
- domain, then these 'static' nodes will correctly serve provider.json.
-
-(3) Otherwise, you must copy the provider.json file to your web
- server and make it available at '/provider.json'. The example htaccess
- file shows what header options should be sent by the web server
- with the response.
-
-This directory is needed for method (3), but not for methods (1) or (2).
-
-This directory has been created by the command `leap compile provider.json`.
-Once created, it will be kept up to date everytime you compile. You may safely
-remove this directory if you don't use it.
-]
-
- ##
- ##
- ## ZONE FILE
- ##
-
- def relative_hostname(fqdn)
- @domain_regexp ||= /\.?#{Regexp.escape(provider.domain)}$/
- fqdn.sub(@domain_regexp, '')
- end
-
- #
- # serial is any number less than 2^32 (4294967296)
- #
- def compile_zone_file
- hosts_seen = {}
- f = $stdout
- f.puts ZONE_HEADER % {:domain => provider.domain, :ns => provider.domain, :contact => provider.contacts.default.first.sub('@','.')}
- max_width = manager.nodes.values.inject(0) {|max, node| [max, relative_hostname(node.domain.full).length].max }
- put_line = lambda do |host, line|
- host = '@' if host == ''
- f.puts("%-#{max_width}s %s" % [host, line])
- end
-
- f.puts ORIGIN_HEADER
- # 'A' records for primary domain
- manager.nodes[:environment => '!local'].each_node do |node|
- if node.dns['aliases'] && node.dns.aliases.include?(provider.domain)
- put_line.call "", "IN A #{node.ip_address}"
- end
- end
-
- # NS records
- if provider['dns'] && provider.dns['nameservers']
- provider.dns.nameservers.each do |ns|
- put_line.call "", "IN NS #{ns}."
- end
- end
-
- # all other records
- manager.environment_names.each do |env|
- next if env == 'local'
- nodes = manager.nodes[:environment => env]
- next unless nodes.any?
- f.puts ENV_HEADER % (env.nil? ? 'default' : env)
- nodes.each_node do |node|
- if node.dns.public
- hostname = relative_hostname(node.domain.full)
- put_line.call relative_hostname(node.domain.full), "IN A #{node.ip_address}"
- end
- if node.dns['aliases']
- node.dns.aliases.each do |host_alias|
- if host_alias != node.domain.full && host_alias != provider.domain
- put_line.call relative_hostname(host_alias), "IN A #{node.ip_address}"
- end
- end
- end
- if node.services.include? 'mx'
- put_line.call relative_hostname(node.domain.full_suffix), "IN MX 10 #{relative_hostname(node.domain.full)}"
- end
- end
- end
- end
-
- ENV_HEADER = %[
-;;
-;; ENVIRONMENT %s
-;;
-
-]
-
- ZONE_HEADER = %[
-;;
-;; BIND data file for %{domain}
-;;
-
-$TTL 600
-$ORIGIN %{domain}.
-
-@ IN SOA %{ns}. %{contact}. (
- 0000 ; serial
- 7200 ; refresh ( 24 hours)
- 3600 ; retry ( 2 hours)
- 1209600 ; expire (1000 hours)
- 600 ) ; minimum ( 2 days)
-;
-]
-
- ORIGIN_HEADER = %[
-;;
-;; ZONE ORIGIN
-;;
-
-]
-
- ##
- ## FIREWALL
- ##
-
- def compile_firewall
- manager.nodes.each_node(&:evaluate)
-
- rules = [["ALLOW TO", "PORTS", "ALLOW FROM"]]
- manager.nodes[:environment => '!local'].values.each do |node|
- next unless node['firewall']
- node.firewall.each do |name, rule|
- if rule.is_a? Hash
- rules << add_rule(rule)
- elsif rule.is_a? Array
- rule.each do |r|
- rules << add_rule(r)
- end
- end
- end
- end
-
- max_to = rules.inject(0) {|max, r| [max, r[0].length].max}
- max_port = rules.inject(0) {|max, r| [max, r[1].length].max}
- max_from = rules.inject(0) {|max, r| [max, r[2].length].max}
- rules.each do |rule|
- puts "%-#{max_to}s %-#{max_port}s %-#{max_from}s" % rule
- end
- end
-
- private
-
- def add_rule(rule)
- [rule["to"], [rule["port"]].compact.join(','), rule["from"]]
- end
-
- end
-end \ No newline at end of file
diff --git a/lib/leap_cli/commands/db.rb b/lib/leap_cli/commands/db.rb
deleted file mode 100644
index e4fd385..0000000
--- a/lib/leap_cli/commands/db.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-module LeapCli; module Commands
-
- desc 'Database commands.'
- command :db do |db|
- db.desc 'Destroy one or more databases. If present, limit to FILTER nodes. For example `leap db destroy --db sessions,tokens testing`.'
- db.arg_name 'FILTER', :optional => true
- db.command :destroy do |destroy|
- destroy.flag :db, :arg_name => "DATABASES", :desc => 'Comma separated list of databases to destroy (no space). Use "--db all" to destroy all databases.', :optional => false
- destroy.action do |global_options,options,args|
- dbs = (options[:db]||"").split(',')
- bail!('No databases specified') if dbs.empty?
- nodes = manager.filter(args)
- if nodes.any?
- nodes = nodes[:services => 'couchdb']
- end
- if nodes.any?
- unless global_options[:yes]
- if dbs.include?('all')
- say 'You are about to permanently destroy all database data for nodes [%s].' % nodes.keys.join(', ')
- else
- say 'You are about to permanently destroy databases [%s] for nodes [%s].' % [dbs.join(', '), nodes.keys.join(', ')]
- end
- bail! unless agree("Continue? ")
- end
- if dbs.include?('all')
- destroy_all_dbs(nodes)
- else
- destroy_dbs(nodes, dbs)
- end
- say 'You must run `leap deploy` in order to create the databases again.'
- else
- say 'No nodes'
- end
- end
- end
- end
-
- private
-
- def destroy_all_dbs(nodes)
- ssh_connect(nodes) do |ssh|
- ssh.run('/etc/init.d/bigcouch stop && test ! -z "$(ls /opt/bigcouch/var/lib/ 2> /dev/null)" && rm -r /opt/bigcouch/var/lib/* && echo "db destroyed" || echo "db already destroyed"')
- ssh.run('grep ^seq_dir /etc/leap/tapicero.yaml | cut -f2 -d\" | xargs rm -rv')
- end
- end
-
- def destroy_dbs(nodes, dbs)
- nodes.each_node do |node|
- ssh_connect(node) do |ssh|
- dbs.each do |db|
- ssh.run(DESTROY_DB_COMMAND % {:db => db})
- end
- end
- end
- end
-
- DESTROY_DB_COMMAND = %{
-if [ 200 = `curl -ns -w "%%{http_code}" -X GET "127.0.0.1:5984/%{db}" -o /dev/null` ]; then
- echo "Result from DELETE /%{db}:" `curl -ns -X DELETE "127.0.0.1:5984/%{db}"`;
-else
- echo "Skipping db '%{db}': it does not exist or has already been deleted.";
-fi
-}
-
-end; end
diff --git a/lib/leap_cli/commands/deploy.rb b/lib/leap_cli/commands/deploy.rb
deleted file mode 100644
index c2a70af..0000000
--- a/lib/leap_cli/commands/deploy.rb
+++ /dev/null
@@ -1,368 +0,0 @@
-require 'etc'
-
-module LeapCli
- module Commands
-
- desc 'Apply recipes to a node or set of nodes.'
- long_desc 'The FILTER can be the name of a node, service, or tag.'
- arg_name 'FILTER'
- command [:deploy, :d] do |c|
-
- c.switch :fast, :desc => 'Makes the deploy command faster by skipping some slow steps. A "fast" deploy can be used safely if you recently completed a normal deploy.',
- :negatable => false
-
- c.switch :sync, :desc => "Sync files, but don't actually apply recipes.", :negatable => false
-
- c.switch :force, :desc => 'Deploy even if there is a lockfile.', :negatable => false
-
- c.switch :downgrade, :desc => 'Allows deploy to run with an older platform version.', :negatable => false
-
- c.switch :dev, :desc => "Development mode: don't run 'git submodule update' before deploy.", :negatable => false
-
- c.flag :tags, :desc => 'Specify tags to pass through to puppet (overriding the default).',
- :arg_name => 'TAG[,TAG]'
-
- c.flag :port, :desc => 'Override the default SSH port.',
- :arg_name => 'PORT'
-
- c.flag :ip, :desc => 'Override the default SSH IP address.',
- :arg_name => 'IPADDRESS'
-
- c.action do |global,options,args|
-
- if options[:dev] != true
- init_submodules
- end
-
- nodes = manager.filter!(args, :disabled => false)
- if nodes.size > 1
- say "Deploying to these nodes: #{nodes.keys.join(', ')}"
- if !global[:yes] && !agree("Continue? ")
- quit! "OK. Bye."
- end
- end
-
- environments = nodes.field('environment').uniq
- if environments.empty?
- environments = [nil]
- end
- environments.each do |env|
- check_platform_pinning(env, global)
- 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), false)
- # update server certificates if needed
- update_certificates(nodes)
-
- ssh_connect(nodes, connect_options(options)) do |ssh|
- ssh.leap.log :checking, 'node' do
- ssh.leap.check_for_no_deploy
- ssh.leap.assert_initialized
- end
- ssh.leap.log :synching, "configuration files" do
- sync_hiera_config(ssh)
- sync_support_files(ssh)
- end
- ssh.leap.log :synching, "puppet manifests" do
- sync_puppet_files(ssh)
- end
- unless options[:sync]
- ssh.leap.log :applying, "puppet" do
- ssh.puppet.apply(:verbosity => [LeapCli.log_level,5].min,
- :tags => tags(options),
- :force => options[:force],
- :info => deploy_info,
- :downgrade => options[:downgrade]
- )
- end
- end
- end
- if !Util.exit_status.nil? && Util.exit_status != 0
- log :warning, "puppet did not finish successfully."
- end
- end
- end
-
- desc 'Display recent deployment history for a set of nodes.'
- long_desc 'The FILTER can be the name of a node, service, or tag.'
- arg_name 'FILTER'
- command [:history, :h] do |c|
- c.flag :port, :desc => 'Override the default SSH port.',
- :arg_name => 'PORT'
- c.flag :ip, :desc => 'Override the default SSH IP address.',
- :arg_name => 'IPADDRESS'
- c.action do |global,options,args|
- nodes = manager.filter!(args)
- ssh_connect(nodes, connect_options(options)) do |ssh|
- ssh.leap.history
- end
- end
- end
-
- private
-
- def forcible_prompt(forced, msg, prompt)
- say(msg)
- if forced
- log :warning, "continuing anyway because of --force"
- else
- say "hint: use --force to skip this prompt."
- quit!("OK. Bye.") unless agree(prompt)
- end
- end
-
- #
- # 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, global_options)
- 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)
- forcible_prompt(
- global_options[:force],
- "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}.",
- "Do you really want to deploy from the wrong version? "
- )
- end
- end
-
- # check branch
- if provider.platform['branch']
- if !is_git_directory?(Path.platform)
- forcible_prompt(
- global_options[:force],
- "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.",
- "Do you really want to deploy anyway? "
- )
- end
- unless provider.platform.branch == current_git_branch(Path.platform)
- forcible_prompt(
- global_options[:force],
- "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}')",
- "Do you really want to deploy from the wrong branch? "
- )
- end
- end
-
- # check commit
- if provider.platform['commit']
- if !is_git_directory?(Path.platform)
- forcible_prompt(
- global_options[:force],
- "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.",
- "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
- forcible_prompt(
- global_options[:force],
- "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.",
- "Do you really want to deploy from the wrong commit? "
- )
- end
- end
- end
- end
-
- def sync_hiera_config(ssh)
- 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 + ':' + Leap::Platform.hiera_path
- {
- :source => hiera_file,
- :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 = Leap::Platform.files_dir
- custom_files = build_custom_file_list
- 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 += custom_files
- if files_to_sync.any?
- ssh.leap.log(files_to_sync.join(', ') + ' -> ' + node.name + ':' + dest_dir)
- {
- :chdir => Path.named_path(:files_dir),
- :source => ".",
- :dest => dest_dir,
- :excludes => "*",
- :includes => calculate_includes_from_files(files_to_sync, '/files'),
- :flags => "-rltp --chmod=u+rX,go-rwx --relative --delete --delete-excluded --copy-links"
- }
- else
- nil
- end
- end
- end
-
- def sync_puppet_files(ssh)
- ssh.rsync.update do |server|
- ssh.leap.log(Path.platform + '/[bin,tests,puppet] -> ' + server.host + ':' + Leap::Platform.leap_dir)
- {
- :dest => Leap::Platform.leap_dir,
- :source => '.',
- :chdir => Path.platform,
- :excludes => '*',
- :includes => ['/bin', '/bin/**', '/puppet', '/puppet/**', '/tests', '/tests/**'],
- :flags => "-rlt --relative --delete --copy-links"
- }
- 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"
- statuses.strip.split("\n").each do |status_line|
- if status_line =~ /^[\+-]/
- submodule = status_line.split(' ')[1]
- log "Updating submodule #{submodule}"
- assert_run! "git submodule update --init #{submodule}"
- end
- end
- end
- end
-
- #
- # 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 =~ /^\// ? file : '/' + file }
-
- # include all sub files of specified directories
- includes.size.times do |i|
- if includes[i] =~ /\/$/
- includes << includes[i] + '**'
- end
- end
-
- # include all parent directories (required because of --exclude '*')
- includes.size.times do |i|
- path = File.dirname(includes[i])
- while(path != '/')
- includes << path unless includes.include?(path)
- path = File.dirname(path)
- end
- end
-
- if prefix
- includes.map! {|path| path.sub(/^#{Regexp.escape(prefix)}\//, '/')}
- end
-
- return includes
- end
-
- def tags(options)
- if options[:tags]
- tags = options[:tags].split(',')
- else
- tags = Leap::Platform.default_puppet_tags.dup
- end
- tags << 'leap_slow' unless options[:fast]
- tags.join(',')
- end
-
- #
- # a provider might have various customization files that should be sync'ed to the server.
- # this method builds that list of files to sync.
- #
- def build_custom_file_list
- custom_files = []
- Leap::Platform.paths.keys.grep(/^custom_/).each do |path|
- if file_exists?(path)
- relative_path = Path.relative_path(path, Path.provider)
- if dir_exists?(path)
- custom_files << relative_path + '/' # rsync needs trailing slash
- else
- custom_files << relative_path
- end
- end
- end
- return custom_files
- end
-
- def deploy_info
- info = []
- info << "user: %s" % Etc.getpwuid(Process.euid).name
- if is_git_directory?(Path.platform) && current_git_branch(Path.platform) != 'master'
- info << "platform: %s (%s %s)" % [
- Leap::Platform.version,
- current_git_branch(Path.platform),
- current_git_commit(Path.platform)[0..4]
- ]
- else
- info << "platform: %s" % Leap::Platform.version
- end
- if is_git_directory?(LEAP_CLI_BASE_DIR)
- info << "leap_cli: %s (%s %s)" % [
- LeapCli::VERSION,
- current_git_branch(LEAP_CLI_BASE_DIR),
- current_git_commit(LEAP_CLI_BASE_DIR)[0..4]
- ]
- else
- info << "leap_cli: %s" % LeapCli::VERSION
- end
- info.join(', ')
- end
- end
-end
diff --git a/lib/leap_cli/commands/env.rb b/lib/leap_cli/commands/env.rb
deleted file mode 100644
index 80be217..0000000
--- a/lib/leap_cli/commands/env.rb
+++ /dev/null
@@ -1,76 +0,0 @@
-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, :e] do |c|
- c.desc "List the available environments. The pinned environment, if any, will be marked with '*'. Will also set the pin if run with an environment argument."
- c.arg_name 'ENVIRONMENT', :optional => true
- c.command :ls do |ls|
- ls.action do |global_options, options, args|
- environment = get_env_from_args(args)
- if environment
- pin(environment)
- LeapCli.leapfile.load
- end
- print_envs
- 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 = get_env_from_args(args)
- if environment
- pin(environment)
- else
- bail! "There is no environment `#{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
-
- def get_env_from_args(args)
- environment = args.first
- if environment == 'default' || (environment && manager.environment_names.include?(environment))
- return environment
- else
- return nil
- end
- end
-
- def pin(environment)
- LeapCli.leapfile.set('environment', environment)
- log 0, :saved, "~/.leaprc with environment set to #{environment}."
- end
-
- def print_envs
- 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
-end \ No newline at end of file
diff --git a/lib/leap_cli/commands/facts.rb b/lib/leap_cli/commands/facts.rb
deleted file mode 100644
index 11329cc..0000000
--- a/lib/leap_cli/commands/facts.rb
+++ /dev/null
@@ -1,100 +0,0 @@
-#
-# Gather facter facts
-#
-
-module LeapCli; module Commands
-
- desc 'Gather information on nodes.'
- command :facts do |facts|
- facts.desc 'Query servers to update facts.json.'
- facts.long_desc "Queries every node included in FILTER and saves the important information to facts.json"
- facts.arg_name 'FILTER'
- facts.command :update do |update|
- update.action do |global_options,options,args|
- update_facts(global_options, options, args)
- end
- end
- end
-
- protected
-
- def facter_cmd
- 'facter --json ' + Leap::Platform.facts.join(' ')
- end
-
- def remove_node_facts(name)
- if file_exists?(:facts)
- update_facts_file({name => nil})
- end
- end
-
- def update_node_facts(name, facts)
- update_facts_file({name => facts})
- end
-
- def rename_node_facts(old_name, new_name)
- if file_exists?(:facts)
- facts = JSON.parse(read_file(:facts) || {})
- facts[new_name] = facts[old_name]
- facts[old_name] = nil
- update_facts_file(facts, true)
- end
- end
-
- #
- # if overwrite = true, then ignore existing facts.json.
- #
- def update_facts_file(new_facts, overwrite=false)
- replace_file!(:facts) do |content|
- if overwrite || content.nil? || content.empty?
- old_facts = {}
- else
- old_facts = manager.facts
- end
- facts = old_facts.merge(new_facts)
- facts.each do |name, value|
- if value.is_a? String
- if value == ""
- value = nil
- else
- value = JSON.parse(value) rescue JSON::ParserError
- end
- end
- if value.is_a? Hash
- value.delete_if {|key,v| v.nil?}
- end
- facts[name] = value
- end
- facts.delete_if do |name, value|
- value.nil? || value.empty?
- end
- if facts.empty?
- "{}\n"
- else
- JSON.sorted_generate(facts) + "\n"
- end
- end
- end
-
- private
-
- def update_facts(global_options, options, args)
- nodes = manager.filter(args, :local => false, :disabled => false)
- new_facts = {}
- ssh_connect(nodes) do |ssh|
- ssh.leap.run_with_progress(facter_cmd) do |response|
- node = manager.node(response[:host])
- if node
- new_facts[node.name] = response[:data].strip
- else
- log :warning, 'Could not find node for hostname %s' % response[:host]
- end
- end
- end
- # 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
-
-end; end \ No newline at end of file
diff --git a/lib/leap_cli/commands/inspect.rb b/lib/leap_cli/commands/inspect.rb
deleted file mode 100644
index 20654fa..0000000
--- a/lib/leap_cli/commands/inspect.rb
+++ /dev/null
@@ -1,144 +0,0 @@
-module LeapCli; module Commands
-
- desc 'Prints details about a file. Alternately, the argument FILE can be the name of a node, service or tag.'
- arg_name 'FILE'
- command [:inspect, :i] do |c|
- c.switch 'base', :desc => 'Inspect the FILE from the provider_base (i.e. without local inheritance).', :negatable => false
- c.action do |global_options,options,args|
- object = args.first
- assert! object, 'A file path or node/service/tag name is required'
- method = inspection_method(object)
- if method && defined?(method)
- self.send(method, object, options)
- else
- log "Sorry, I don't know how to inspect that."
- end
- end
- end
-
- private
-
- FTYPE_MAP = {
- "PEM certificate" => :inspect_x509_cert,
- "PEM RSA private key" => :inspect_x509_key,
- "OpenSSH RSA public key" => :inspect_ssh_pub_key,
- "PEM certificate request" => :inspect_x509_csr
- }
-
- def inspection_method(object)
- if File.exists?(object)
- ftype = `file #{object}`.split(':').last.strip
- log 2, "file is of type '#{ftype}'"
- if FTYPE_MAP[ftype]
- FTYPE_MAP[ftype]
- elsif File.extname(object) == ".json"
- full_path = File.expand_path(object, Dir.pwd)
- if path_match?(:node_config, full_path)
- :inspect_node
- elsif path_match?(:service_config, full_path)
- :inspect_service
- elsif path_match?(:tag_config, full_path)
- :inspect_tag
- elsif path_match?(:provider_config, full_path) || path_match?(:provider_env_config, full_path)
- :inspect_provider
- elsif path_match?(:common_config, full_path)
- :inspect_common
- else
- nil
- end
- end
- elsif manager.nodes[object]
- :inspect_node
- elsif manager.services[object]
- :inspect_service
- elsif manager.tags[object]
- :inspect_tag
- elsif object == "common"
- :inspect_common
- elsif object == "provider"
- :inspect_provider
- else
- nil
- end
- end
-
- #
- # inspectors
- #
-
- def inspect_x509_key(file_path, options)
- assert_bin! 'openssl'
- puts assert_run! 'openssl rsa -in %s -text -check' % file_path
- end
-
- def inspect_x509_cert(file_path, options)
- assert_bin! 'openssl'
- puts assert_run! 'openssl x509 -in %s -text -noout' % file_path
- log 0, :"SHA256 fingerprint", X509.fingerprint("SHA256", file_path)
- end
-
- def inspect_x509_csr(file_path, options)
- assert_bin! 'openssl'
- puts assert_run! 'openssl req -text -noout -verify -in %s' % file_path
- end
-
- #def inspect_ssh_pub_key(file_path)
- #end
-
- def inspect_node(arg, options)
- inspect_json manager.nodes[name(arg)]
- end
-
- def inspect_service(arg, options)
- if options[:base]
- inspect_json manager.base_services[name(arg)]
- else
- inspect_json manager.services[name(arg)]
- end
- end
-
- def inspect_tag(arg, options)
- if options[:base]
- inspect_json manager.base_tags[name(arg)]
- else
- inspect_json manager.tags[name(arg)]
- end
- end
-
- def inspect_provider(arg, options)
- if options[:base]
- inspect_json manager.base_provider
- elsif arg =~ /provider\.(.*)\.json/
- inspect_json manager.env($1).provider
- else
- inspect_json manager.provider
- end
- end
-
- def inspect_common(arg, options)
- if options[:base]
- inspect_json manager.base_common
- else
- inspect_json manager.common
- end
- end
-
- #
- # helpers
- #
-
- def name(arg)
- File.basename(arg).sub(/\.json$/, '')
- end
-
- def inspect_json(config)
- if config
- puts JSON.sorted_generate(config)
- end
- end
-
- def path_match?(path_symbol, path)
- Dir.glob(Path.named_path([path_symbol, '*'])).include?(path)
- end
-
-end; end
diff --git a/lib/leap_cli/commands/list.rb b/lib/leap_cli/commands/list.rb
deleted file mode 100644
index c562b59..0000000
--- a/lib/leap_cli/commands/list.rb
+++ /dev/null
@@ -1,132 +0,0 @@
-require 'command_line_reporter'
-
-module LeapCli; module Commands
-
- desc 'List nodes and their classifications'
- long_desc 'Prints out a listing of nodes, services, or tags. ' +
- 'If present, the FILTER can be a list of names of nodes, services, or tags. ' +
- 'If the name is prefixed with +, this acts like an AND condition. ' +
- "For example:\n\n" +
- "`leap list node1 node2` matches all nodes named \"node1\" OR \"node2\"\n\n" +
- "`leap list openvpn +local` matches all nodes with service \"openvpn\" AND tag \"local\""
-
- arg_name 'FILTER', :optional => true
- command [:list,:ls] do |c|
- 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
- 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
- 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
- end
-
- private
-
- def self.print_node_properties(nodes, properties)
- properties = properties.split(',')
- max_width = nodes.keys.inject(0) {|max,i| [i.size,max].max}
- nodes.each_node do |node|
- value = properties.collect{|prop|
- prop_value = node[prop]
- if prop_value.nil?
- "null"
- elsif prop_value == ""
- "empty"
- elsif prop_value.is_a? LeapCli::Config::Object
- node[prop].dump_json(:compact) # TODO: add option of getting pre-evaluation values.
- else
- prop_value.to_s
- end
- }.join(', ')
- printf("%#{max_width}s %s\n", node.name, value)
- end
- puts
- end
-
- class TagTable
- include CommandLineReporter
- def initialize(heading, tag_list, colors)
- @heading = heading
- @tag_list = tag_list
- @colors = colors
- end
- def run
- 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
- column @heading, :align => 'right', :width => max_width
- 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(', ')
- end
- end
- end
- vertical_spacing
- end
- end
-
- #
- # might be handy: HighLine::SystemExtensions.terminal_size.first
- #
- class NodeTable
- include CommandLineReporter
- def initialize(node_list, colors)
- @node_list = node_list
- @colors = colors
- end
- def run
- rows = @node_list.keys.sort.collect do |node_name|
- [node_name, @node_list[node_name].services.sort.join(', '), @node_list[node_name].tags.sort.join(', ')]
- end
- unless rows.any?
- puts Paint["no results", :red]
- puts
- return
- end
- padding = 2
- max_node_width = [20, (rows.map{|i|i[0]} + ["NODES"] ).inject(0) {|max,i| [i.size,max].max}].max
- max_service_width = (rows.map{|i|i[1]} + ["SERVICES"]).inject(0) {|max,i| [i.size+padding+padding,max].max}
- max_tag_width = (rows.map{|i|i[2]} + ["TAGS"] ).inject(0) {|max,i| [i.size,max].max}
- table :border => false do
- row :color => @colors[0] do
- column "NODES", :align => 'right', :width => max_node_width
- column "SERVICES", :width => max_service_width, :padding => 2
- column "TAGS", :width => max_tag_width
- end
- rows.each do |r|
- row :color => @colors[1] do
- column r[0]
- column r[1]
- column r[2]
- end
- end
- end
- vertical_spacing
- end
- end
-
-end; end
diff --git a/lib/leap_cli/commands/new.rb b/lib/leap_cli/commands/new.rb
index 038e6e4..838b80e 100644
--- a/lib/leap_cli/commands/new.rb
+++ b/lib/leap_cli/commands/new.rb
@@ -4,7 +4,7 @@ module LeapCli; module Commands
desc 'Creates a new provider instance in the specified directory, creating it if necessary.'
arg_name 'DIRECTORY'
- skips_pre
+ #skips_pre
command :new do |c|
c.flag 'name', :desc => "The name of the provider." #, :default_value => 'Example'
c.flag 'domain', :desc => "The primary domain of the provider." #, :default_value => 'example.org'
@@ -12,6 +12,10 @@ module LeapCli; module Commands
c.flag 'contacts', :desc => "Default email address contacts." #, :default_value => 'root'
c.action do |global, options, args|
+ unless args.first
+ # this should not be needed, but GLI is not making it required.
+ bail! "Argument DIRECTORY is required."
+ end
directory = File.expand_path(args.first)
create_provider_directory(global, directory)
options[:domain] ||= ask_string("The primary domain of the provider: ") {|q| q.default = 'example.org'}
diff --git a/lib/leap_cli/commands/node.rb b/lib/leap_cli/commands/node.rb
deleted file mode 100644
index 12d6b49..0000000
--- a/lib/leap_cli/commands/node.rb
+++ /dev/null
@@ -1,165 +0,0 @@
-#
-# fyi: the `node init` command lives in node_init.rb,
-# but all other `node x` commands live here.
-#
-
-autoload :IPAddr, 'ipaddr'
-
-module LeapCli; module Commands
-
- ##
- ## COMMANDS
- ##
-
- desc 'Node management'
- command [:node, :n] do |node|
- node.desc 'Create a new configuration file for a node named NAME.'
- node.long_desc ["If specified, the optional argument SEED can be used to seed values in the node configuration file.",
- "The format is property_name:value.",
- "For example: `leap node add web1 ip_address:1.2.3.4 services:webapp`.",
- "To set nested properties, property name can contain '.', like so: `leap node add web1 ssh.port:44`",
- "Separeate multiple values for a single property with a comma, like so: `leap node add mynode services:webapp,dns`"].join("\n\n")
- node.arg_name 'NAME [SEED]' # , :optional => false, :multiple => false
- node.command :add do |add|
- add.switch :local, :desc => 'Make a local testing node (by automatically assigning the next available local IP address). Local nodes are run as virtual machines on your computer.', :negatable => false
- add.action do |global_options,options,args|
- # argument sanity checks
- name = args.first
- assert_valid_node_name!(name, options[:local])
- assert_files_missing! [:node_config, name]
-
- # create and seed new node
- node = Config::Node.new(manager)
- if options[:local]
- node['ip_address'] = pick_next_vagrant_ip_address
- end
- seed_node_data(node, args[1..-1])
- validate_ip_address(node)
- begin
- write_file! [:node_config, name], node.dump_json + "\n"
- node['name'] = name
- if file_exists? :ca_cert, :ca_key
- generate_cert_for_node(manager.reload_node!(node))
- end
- rescue LeapCli::ConfigError => exc
- remove_node_files(name)
- end
- end
- end
-
- node.desc 'Renames a node file, and all its related files.'
- node.arg_name 'OLD_NAME NEW_NAME'
- node.command :mv do |mv|
- mv.action do |global_options,options,args|
- node = get_node_from_args(args)
- new_name = args.last
- assert_valid_node_name!(new_name, node.vagrant?)
- ensure_dir [:node_files_dir, new_name]
- Leap::Platform.node_files.each do |path|
- rename_file! [path, node.name], [path, new_name]
- end
- remove_directory! [:node_files_dir, node.name]
- rename_node_facts(node.name, new_name)
- end
- end
-
- node.desc 'Removes all the files related to the node named NAME.'
- node.arg_name 'NAME' #:optional => false #, :multiple => false
- node.command :rm do |rm|
- rm.action do |global_options,options,args|
- node = get_node_from_args(args)
- remove_node_files(node.name)
- if node.vagrant?
- vagrant_command("destroy --force", [node.name])
- end
- remove_node_facts(node.name)
- end
- end
- end
-
- ##
- ## PUBLIC HELPERS
- ##
-
- def get_node_from_args(args, options={})
- node_name = args.first
- node = manager.node(node_name)
- if node.nil? && options[:include_disabled]
- node = manager.disabled_node(node_name)
- end
- assert!(node, "Node '#{node_name}' not found.")
- node
- end
-
- def seed_node_data(node, args)
- args.each do |seed|
- key, value = seed.split(':')
- value = format_seed_value(value)
- assert! key =~ /^[0-9a-z\._]+$/, "illegal characters used in property '#{key}'"
- if key =~ /\./
- key_parts = key.split('.')
- final_key = key_parts.pop
- current_object = node
- key_parts.each do |key_part|
- current_object[key_part] ||= Config::Object.new
- current_object = current_object[key_part]
- end
- current_object[final_key] = value
- else
- node[key] = value
- end
- end
- end
-
- def remove_node_files(node_name)
- (Leap::Platform.node_files + [:node_files_dir]).each do |path|
- remove_file! [path, node_name]
- end
- end
-
- #
- # conversions:
- #
- # "x,y,z" => ["x","y","z"]
- #
- # "22" => 22
- #
- # "5.1" => 5.1
- #
- def format_seed_value(v)
- if v =~ /,/
- v = v.split(',')
- v.map! do |i|
- i = i.to_i if i.to_i.to_s == i
- i = i.to_f if i.to_f.to_s == i
- i
- end
- else
- v = v.to_i if v.to_i.to_s == v
- v = v.to_f if v.to_f.to_s == v
- end
- return v
- end
-
- def validate_ip_address(node)
- IPAddr.new(node['ip_address'])
- rescue ArgumentError
- bail! do
- if node['ip_address']
- log :invalid, "ip_address #{node['ip_address'].inspect}"
- else
- log :missing, "ip_address"
- end
- end
- end
-
- def assert_valid_node_name!(name, local=false)
- assert! name, 'No <node-name> specified.'
- if local
- assert! name =~ /^[0-9a-z]+$/, "illegal characters used in node name '#{name}' (note: Vagrant does not allow hyphens or underscores)"
- else
- assert! name =~ /^[0-9a-z-]+$/, "illegal characters used in node name '#{name}' (note: Linux does not allow underscores)"
- end
- end
-
-end; end \ No newline at end of file
diff --git a/lib/leap_cli/commands/node_init.rb b/lib/leap_cli/commands/node_init.rb
deleted file mode 100644
index 33f6288..0000000
--- a/lib/leap_cli/commands/node_init.rb
+++ /dev/null
@@ -1,169 +0,0 @@
-#
-# Node initialization.
-# Most of the fun stuff is in tasks.rb.
-#
-
-module LeapCli; module Commands
-
- desc 'Node management'
- command :node do |node|
- node.desc 'Bootstraps a node or nodes, setting up SSH keys and installing prerequisite packages'
- node.long_desc "This command prepares a server to be used with the LEAP Platform by saving the server's SSH host key, " +
- "copying the authorized_keys file, installing packages that are required for deploying, and registering important facts. " +
- "Node init must be run before deploying to a server, and the server must be running and available via the network. " +
- "This command only needs to be run once, but there is no harm in running it multiple times."
- node.arg_name 'FILTER'
- node.command :init do |init|
- init.switch 'echo', :desc => 'If set, passwords are visible as you type them (default is hidden)', :negatable => false
- init.flag :port, :desc => 'Override the default SSH port.', :arg_name => 'PORT'
- init.flag :ip, :desc => 'Override the default SSH IP address.', :arg_name => 'IPADDRESS'
-
- init.action do |global,options,args|
- assert! args.any?, 'You must specify a FILTER'
- finished = []
- manager.filter!(args).each_node do |node|
- is_node_alive(node, options)
- save_public_host_key(node, global, options) unless node.vagrant?
- update_compiled_ssh_configs
- ssh_connect_options = connect_options(options).merge({:bootstrap => true, :echo => options[:echo]})
- ssh_connect(node, ssh_connect_options) do |ssh|
- if node.vagrant?
- ssh.install_insecure_vagrant_key
- end
- ssh.install_authorized_keys
- ssh.install_prerequisites
- unless node.vagrant?
- ssh.leap.log(:checking, "SSH host keys") do
- ssh.leap.capture(get_ssh_keys_cmd) do |response|
- update_local_ssh_host_keys(node, response[:data]) if response[:exitcode] == 0
- end
- end
- end
- ssh.leap.log(:updating, "facts") do
- ssh.leap.capture(facter_cmd) do |response|
- if response[:exitcode] == 0
- update_node_facts(node.name, response[:data])
- else
- log :failed, "to run facter on #{node.name}"
- end
- end
- end
- end
- finished << node.name
- end
- log :completed, "initialization of nodes #{finished.join(', ')}"
- end
- end
- end
-
- private
-
- ##
- ## PRIVATE HELPERS
- ##
-
- def is_node_alive(node, options)
- address = options[:ip] || node.ip_address
- port = options[:port] || node.ssh.port
- log :connecting, "to node #{node.name}"
- assert_run! "nc -zw3 #{address} #{port}",
- "Failed to reach #{node.name} (address #{address}, port #{port}). You can override the configured IP address and port with --ip or --port."
- end
-
- #
- # saves the public ssh host key for node into the provider directory.
- #
- # see `man sshd` for the format of known_hosts
- #
- def save_public_host_key(node, global, options)
- log :fetching, "public SSH host key for #{node.name}"
- address = options[:ip] || node.ip_address
- port = options[:port] || node.ssh.port
- host_keys = get_public_keys_for_ip(address, port)
- pub_key_path = Path.named_path([:node_ssh_pub_key, node.name])
-
- if Path.exists?(pub_key_path)
- if host_keys.include? SshKey.load(pub_key_path)
- log :trusted, "- Public SSH host key for #{node.name} matches previously saved key", :indent => 1
- else
- bail! do
- log :error, "The public SSH host keys we just fetched for #{node.name} doesn't match what we have saved previously.", :indent => 1
- log "Delete the file #{pub_key_path} if you really want to remove the trusted SSH host key.", :indent => 2
- end
- end
- else
- known_key = host_keys.detect{|k|k.in_known_hosts?(node.name, node.ip_address, node.domain.name)}
- if known_key
- log :trusted, "- Public SSH host key for #{node.name} is trusted (key found in your ~/.ssh/known_hosts)"
- else
- public_key = SshKey.pick_best_key(host_keys)
- if public_key.nil?
- bail!("We got back #{host_keys.size} host keys from #{node.name}, but we can't support any of them.")
- else
- say(" This is the SSH host key you got back from node \"#{node.name}\"")
- say(" Type -- #{public_key.bits} bit #{public_key.type.upcase}")
- say(" Fingerprint -- " + public_key.fingerprint)
- say(" Public Key -- " + public_key.key)
- if !global[:yes] && !agree(" Is this correct? ")
- bail!
- else
- known_key = public_key
- end
- end
- end
- puts
- write_file! [:node_ssh_pub_key, node.name], known_key.to_s
- end
- end
-
- #
- # Get the public host keys for a host using ssh-keyscan.
- # Return an array of SshKey objects, one for each key.
- #
- def get_public_keys_for_ip(address, port=22)
- assert_bin!('ssh-keyscan')
- 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
-
- if output =~ /No route to host/
- bail! :failed, 'ssh-keyscan: no route to %s' % address
- else
- keys = SshKey.parse_keys(output)
- if keys.empty?
- bail! "ssh-keyscan got zero host keys back (that we understand)! Output was: #{output}"
- else
- return keys
- end
- end
- end
-
- # run on the server to generate a string suitable for passing to SshKey.parse_keys()
- def get_ssh_keys_cmd
- "/bin/grep ^HostKey /etc/ssh/sshd_config | /usr/bin/awk '{print $2 \".pub\"}' | /usr/bin/xargs /bin/cat"
- end
-
- #
- # Sometimes the ssh host keys on the server will be better than what we have
- # stored locally. In these cases, ask the user if they want to upgrade.
- #
- def update_local_ssh_host_keys(node, remote_keys_string)
- remote_keys = SshKey.parse_keys(remote_keys_string)
- return unless remote_keys.any?
- current_key = SshKey.load(Path.named_path([:node_ssh_pub_key, node.name]))
- best_key = SshKey.pick_best_key(remote_keys)
- return unless best_key && current_key
- if current_key != best_key
- say(" One of the SSH host keys for node '#{node.name}' is better than what you currently have trusted.")
- say(" Current key: #{current_key.summary}")
- say(" Better key: #{best_key.summary}")
- if agree(" Do you want to use the better key? ")
- write_file! [:node_ssh_pub_key, node.name], best_key.to_s
- end
- else
- log(3, "current host key does not need updating")
- end
- end
-
-end; end
diff --git a/lib/leap_cli/commands/pre.rb b/lib/leap_cli/commands/pre.rb
index c531065..f4bf7bb 100644
--- a/lib/leap_cli/commands/pre.rb
+++ b/lib/leap_cli/commands/pre.rb
@@ -31,73 +31,8 @@ module LeapCli; module Commands
switch 'color', :negatable => true
pre do |global,command,options,args|
- if global[:force]
- global[:yes] = true
- end
- initialize_leap_cli(true, global)
+ Bootstrap.setup_global_options(self, global)
true
end
- protected
-
- #
- # available options:
- # :verbose -- integer log verbosity level
- # :log -- log file path
- # :color -- true or false, to log in color or not.
- #
- def initialize_leap_cli(require_provider, options={})
- if Process::Sys.getuid == 0
- bail! "`leap` should not be run as root."
- end
-
- # set verbosity
- options[:verbose] ||= 1
- LeapCli.set_log_level(options[:verbose].to_i)
-
- # load Leapfile
- LeapCli.leapfile.load
- if LeapCli.leapfile.valid?
- Path.set_platform_path(LeapCli.leapfile.platform_directory_path)
- Path.set_provider_path(LeapCli.leapfile.provider_directory_path)
- if !Path.provider || !File.directory?(Path.provider)
- bail! { log :missing, "provider directory '#{Path.provider}'" }
- end
- if !Path.platform || !File.directory?(Path.platform)
- bail! { log :missing, "platform directory '#{Path.platform}'" }
- end
- elsif require_provider
- bail! { log :missing, 'Leapfile in directory tree' }
- end
-
- # set log file
- LeapCli.log_file = options[:log] || LeapCli.leapfile.log
- LeapCli::Util.log_raw(:log) { $0 + ' ' + ORIGINAL_ARGV.join(' ')}
- log_version
- LeapCli.log_in_color = options[:color]
- end
-
- #
- # add a log entry for the leap command and leap platform versions
- #
- def log_version
- if LeapCli.log_level >= 2
- str = "leap command v#{LeapCli::VERSION}"
- if Util.is_git_directory?(LEAP_CLI_BASE_DIR)
- str << " (%s %s)" % [Util.current_git_branch(LEAP_CLI_BASE_DIR),
- Util.current_git_commit(LEAP_CLI_BASE_DIR)]
- else
- str << " (%s)" % LEAP_CLI_BASE_DIR
- end
- log 2, str
- if LeapCli.leapfile.valid?
- str = "leap platform v#{Leap::Platform.version}"
- if Util.is_git_directory?(Path.platform)
- str << " (%s %s)" % [Util.current_git_branch(Path.platform), Util.current_git_commit(Path.platform)]
- end
- log 2, str
- end
- end
- end
-
end; end
diff --git a/lib/leap_cli/commands/ssh.rb b/lib/leap_cli/commands/ssh.rb
deleted file mode 100644
index 1a81902..0000000
--- a/lib/leap_cli/commands/ssh.rb
+++ /dev/null
@@ -1,220 +0,0 @@
-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', :arg_name => 'SSH_PORT', :desc => 'Override default SSH port used when trying to connect to the server. Same as `--ssh "-p SSH_PORT"`.'
- c.action do |global_options,options,args|
- exec_ssh(:ssh, options, args)
- end
- end
-
- desc 'Log in to the specified node with an interactive shell using mosh (requires node to have mosh.enabled set to true).'
- arg_name 'NAME'
- command :mosh do |c|
- c.flag 'ssh', :desc => "Pass through raw options to ssh (e.g. `--ssh '-F ~/sshconfig'`)."
- c.flag 'port', :arg_name => 'SSH_PORT', :desc => 'Override default SSH port used when trying to connect to the server. Same as `--ssh "-p SSH_PORT"`.'
- c.action do |global_options,options,args|
- exec_ssh(:mosh, options, args)
- end
- end
-
- desc 'Creates an SSH port forward (tunnel) to the node NAME. REMOTE_PORT is the port on the remote node that the tunnel will connect to. LOCAL_PORT is the optional port on your local machine. For example: `leap tunnel couch1:5984`.'
- arg_name '[LOCAL_PORT:]NAME:REMOTE_PORT'
- command :tunnel do |c|
- c.flag 'ssh', :desc => "Pass through raw options to ssh (e.g. --ssh '-F ~/sshconfig')."
- c.flag 'port', :arg_name => 'SSH_PORT', :desc => 'Override default SSH port used when trying to connect to the server. Same as `--ssh "-p SSH_PORT"`.'
- c.action do |global_options,options,args|
- local_port, node, remote_port = parse_tunnel_arg(args.first)
- options[:ssh] = [options[:ssh], "-N -L 127.0.0.1:#{local_port}:0.0.0.0:#{remote_port}"].join(' ')
- log("Forward port localhost:#{local_port} to #{node.name}:#{remote_port}")
- if is_port_available?(local_port)
- exec_ssh(:ssh, options, [node.name])
- end
- end
- end
-
- desc 'Secure copy from FILE1 to FILE2. Files are specified as NODE_NAME:FILE_PATH. For local paths, omit "NODE_NAME:".'
- arg_name 'FILE1 FILE2'
- command :scp do |c|
- c.switch :r, :desc => 'Copy recursively'
- c.action do |global_options, options, args|
- if args.size != 2
- bail!('You must specificy both FILE1 and FILE2')
- end
- from, to = args
- if (from !~ /:/ && to !~ /:/) || (from =~ /:/ && to =~ /:/)
- bail!('One FILE must be remote and the other local.')
- end
- src_node_name = src_file_path = src_node = nil
- dst_node_name = dst_file_path = dst_node = nil
- if from =~ /:/
- src_node_name, src_file_path = from.split(':')
- src_node = get_node_from_args([src_node_name], :include_disabled => true)
- dst_file_path = to
- else
- dst_node_name, dst_file_path = to.split(':')
- dst_node = get_node_from_args([dst_node_name], :include_disabled => true)
- src_file_path = from
- end
- exec_scp(options, src_node, src_file_path, dst_node, dst_file_path)
- end
- end
-
- protected
-
- #
- # allow for ssh overrides of all commands that use ssh_connect
- #
- def connect_options(options)
- connect_options = {:ssh_options=>{}}
- if options[:port]
- connect_options[:ssh_options][:port] = options[:port]
- end
- if options[:ip]
- connect_options[:ssh_options][:host_name] = options[:ip]
- end
- return connect_options
- end
-
- def ssh_config_help_message
- puts ""
- puts "Are 'too many authentication failures' getting you down?"
- puts "Then we have the solution for you! Add something like this to your ~/.ssh/config file:"
- puts " Host *.#{manager.provider.domain}"
- puts " IdentityFile ~/.ssh/id_rsa"
- puts " IdentitiesOnly=yes"
- puts "(replace `id_rsa` with the actual private key filename that you use for this provider)"
- end
-
- require 'socket'
- def is_port_available?(port)
- TCPServer.open('127.0.0.1', port) {}
- true
- rescue Errno::EACCES
- bail!("You don't have permission to bind to port #{port}.")
- rescue Errno::EADDRINUSE
- bail!("Local port #{port} is already in use. Specify LOCAL_PORT to pick another.")
- rescue Exception => exc
- bail!(exc.to_s)
- end
-
- private
-
- def exec_ssh(cmd, cli_options, args)
- node = get_node_from_args(args, :include_disabled => true)
- port = node.ssh.port
- options = ssh_config(node)
- username = 'root'
- if LeapCli.log_level >= 3
- options << "-vv"
- elsif LeapCli.log_level >= 2
- options << "-v"
- end
- 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
- command = "MOSH_TITLE_NOPREFIX=1 mosh --ssh \"#{ssh}\" #{node.domain.full}"
- end
- log 2, command
-
- # exec the shell command in a subprocess
- pid = fork { exec "#{command}" }
-
- Signal.trap("SIGINT") do
- Process.kill("KILL", pid)
- Process.wait(pid)
- exit(0)
- end
-
- # wait for shell to exit so we can grab the exit status
- _, status = Process.waitpid2(pid)
-
- if status.exitstatus == 255
- ssh_config_help_message
- elsif status.exitstatus != 0
- exit(status.exitstatus)
- end
- end
-
- def exec_scp(cli_options, src_node, src_file_path, dst_node, dst_file_path)
- node = src_node || dst_node
- options = ssh_config(node)
- port = node.ssh.port
- username = 'root'
- options << "-r" if cli_options[:r]
- scp = "scp -P #{port} #{options.join(' ')}"
- if src_node
- command = "#{scp} #{username}@#{src_node.domain.full}:#{src_file_path} #{dst_file_path}"
- elsif dst_node
- command = "#{scp} #{src_file_path} #{username}@#{dst_node.domain.full}:#{dst_file_path}"
- end
- log 2, command
-
- # exec the shell command in a subprocess
- pid = fork { exec "#{command}" }
-
- Signal.trap("SIGINT") do
- Process.kill("KILL", pid)
- Process.wait(pid)
- exit(0)
- end
-
- # wait for shell to exit so we can grab the exit status
- _, status = Process.waitpid2(pid)
- exit(status.exitstatus)
- end
-
- #
- # SSH command line -o options. See `man ssh_config`
- #
- # NOTES:
- #
- # The option 'HostKeyAlias=#{node.name}' is oddly incompatible with ports in
- # known_hosts file, so we must not use this or non-standard ports break.
- #
- def ssh_config(node)
- options = [
- "-o 'HostName=#{node.ip_address}'",
- "-o 'GlobalKnownHostsFile=#{path(:known_hosts)}'",
- "-o 'UserKnownHostsFile=/dev/null'"
- ]
- if node.vagrant?
- options << "-i #{vagrant_ssh_key_file}" # use the universal vagrant insecure key
- options << "-o IdentitiesOnly=yes" # force the use of the insecure vagrant key
- options << "-o 'StrictHostKeyChecking=no'" # blindly accept host key and don't save it
- # (since userknownhostsfile is /dev/null)
- else
- options << "-o 'StrictHostKeyChecking=yes'"
- end
- if !node.supported_ssh_host_key_algorithms.empty?
- options << "-o 'HostKeyAlgorithms=#{node.supported_ssh_host_key_algorithms}'"
- end
- return options
- end
-
- def parse_tunnel_arg(arg)
- if arg.count(':') == 1
- node_name, remote = arg.split(':')
- local = nil
- elsif arg.count(':') == 2
- local, node_name, remote = arg.split(':')
- else
- bail!('Argument NAME:REMOTE_PORT required.')
- end
- node = get_node_from_args([node_name], :include_disabled => true)
- remote = remote.to_i
- local = local || remote
- local = local.to_i
- return [local, node, remote]
- end
-
-end; end \ No newline at end of file
diff --git a/lib/leap_cli/commands/test.rb b/lib/leap_cli/commands/test.rb
deleted file mode 100644
index 73207b3..0000000
--- a/lib/leap_cli/commands/test.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-module LeapCli; module Commands
-
- desc 'Run tests.'
- command [:test, :t] do |test|
- test.desc 'Run the test suit on FILTER nodes.'
- test.arg_name 'FILTER', :optional => true
- test.command :run do |run|
- run.switch 'continue', :desc => 'Continue over errors and failures (default is --no-continue).', :negatable => true
- run.action do |global_options,options,args|
- test_order = File.join(Path.platform, 'tests/order.rb')
- if File.exists?(test_order)
- require test_order
- end
- manager.filter!(args).names_in_test_dependency_order.each do |node_name|
- node = manager.nodes[node_name]
- begin
- ssh_connect(node) do |ssh|
- ssh.run(test_cmd(options))
- end
- rescue Capistrano::CommandError => exc
- if options[:continue]
- exit_status(1)
- else
- bail!
- end
- end
- end
- end
- end
-
- test.desc 'Creates files needed to run tests.'
- test.command :init do |init|
- init.action do |global_options,options,args|
- generate_test_client_openvpn_configs
- end
- end
-
- test.default_command :run
- end
-
- private
-
- def test_cmd(options)
- if options[:continue]
- "#{Leap::Platform.leap_dir}/bin/run_tests --continue"
- else
- "#{Leap::Platform.leap_dir}/bin/run_tests"
- end
- end
-
- #
- # generates a whole bunch of openvpn configs that can be used to connect to different openvpn gateways
- #
- def generate_test_client_openvpn_configs
- assert_config! 'provider.ca.client_certificates.unlimited_prefix'
- assert_config! 'provider.ca.client_certificates.limited_prefix'
- template = read_file! Path.find_file(:test_client_openvpn_template)
- manager.environment_names.each do |env|
- vpn_nodes = manager.nodes[:environment => env][:services => 'openvpn']['openvpn.allow_limited' => true]
- if vpn_nodes.any?
- generate_test_client_cert(provider.ca.client_certificates.limited_prefix) do |key, cert|
- write_file! [:test_openvpn_config, [env, 'limited'].compact.join('_')], Util.erb_eval(template, binding)
- end
- end
- vpn_nodes = manager.nodes[:environment => env][:services => 'openvpn']['openvpn.allow_unlimited' => true]
- if vpn_nodes.any?
- generate_test_client_cert(provider.ca.client_certificates.unlimited_prefix) do |key, cert|
- write_file! [:test_openvpn_config, [env, 'unlimited'].compact.join('_')], Util.erb_eval(template, binding)
- end
- end
- end
- end
-
-end; end
diff --git a/lib/leap_cli/commands/user.rb b/lib/leap_cli/commands/user.rb
deleted file mode 100644
index 480e9a9..0000000
--- a/lib/leap_cli/commands/user.rb
+++ /dev/null
@@ -1,136 +0,0 @@
-
-#
-# perhaps we want to verify that the key files are actually the key files we expect.
-# we could use 'file' for this:
-#
-# > file ~/.gnupg/00440025.asc
-# ~/.gnupg/00440025.asc: PGP public key block
-#
-# > file ~/.ssh/id_rsa.pub
-# ~/.ssh/id_rsa.pub: OpenSSH RSA public key
-#
-
-module LeapCli
- module Commands
-
- desc 'Adds a new trusted sysadmin by adding public keys to the "users" directory.'
- arg_name 'USERNAME' #, :optional => false, :multiple => false
- command :'add-user' do |c|
-
- c.switch 'self', :desc => 'Add yourself as a trusted sysadin by choosing among the public keys available for the current user.', :negatable => false
- c.flag 'ssh-pub-key', :desc => 'SSH public key file for this new user'
- c.flag 'pgp-pub-key', :desc => 'OpenPGP public key file for this new user'
-
- c.action do |global_options,options,args|
- username = args.first
- if !username.any?
- if options[:self]
- username ||= `whoami`.strip
- else
- help! "Either USERNAME argument or --self flag is required."
- end
- end
- if Leap::Platform.reserved_usernames.include? username
- bail! %(The username "#{username}" is reserved. Sorry, pick another.)
- end
-
- ssh_pub_key = nil
- pgp_pub_key = nil
-
- if options['ssh-pub-key']
- ssh_pub_key = read_file!(options['ssh-pub-key'])
- end
- if options['pgp-pub-key']
- pgp_pub_key = read_file!(options['pgp-pub-key'])
- end
-
- if options[:self]
- ssh_pub_key ||= pick_ssh_key.to_s
- pgp_pub_key ||= pick_pgp_key
- end
-
- assert!(ssh_pub_key, 'Sorry, could not find SSH public key.')
-
- if ssh_pub_key
- write_file!([:user_ssh, username], ssh_pub_key)
- end
- if pgp_pub_key
- write_file!([:user_pgp, username], pgp_pub_key)
- end
-
- update_authorized_keys
- end
- end
-
- #
- # let the the user choose among the ssh public keys that we encounter, or just pick the key if there is only one.
- #
- def pick_ssh_key
- ssh_keys = []
- Dir.glob("#{ENV['HOME']}/.ssh/*.pub").each do |keyfile|
- ssh_keys << SshKey.load(keyfile)
- end
-
- if `which ssh-add`.strip.any?
- `ssh-add -L 2> /dev/null`.split("\n").compact.each do |line|
- key = SshKey.load(line)
- if key
- key.comment = 'ssh-agent'
- ssh_keys << key unless ssh_keys.include?(key)
- end
- end
- end
- ssh_keys.compact!
-
- assert! ssh_keys.any?, 'Sorry, could not find any SSH public key for you. Have you run ssh-keygen?'
-
- if ssh_keys.length > 1
- key_index = numbered_choice_menu('Choose your SSH public key', ssh_keys.collect(&:summary)) do |line, i|
- say("#{i+1}. #{line}")
- end
- else
- key_index = 0
- end
-
- return ssh_keys[key_index]
- end
-
- #
- # 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
- require 'gpgme'
- rescue LoadError
- log "Skipping OpenPGP setup because gpgme is not installed."
- 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
-
- 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|
- key_info = key.to_s.split("\n")[0..1].map{|line| line.sub(/^\s*(sec|uid)\s*/,'')}.join(' -- ')
- say("#{i+1}. #{key_info}")
- end
- else
- key_index = 0
- end
-
- key_id = secret_keys[key_index].sha
-
- # can't use this, it includes signatures:
- #puts GPGME::Key.export(key_id, :armor => true, :export_options => :export_minimal)
-
- # export with signatures removed:
- return `gpg --armor --export-options export-minimal --export #{key_id}`.strip
- end
-
- end
-end \ No newline at end of file
diff --git a/lib/leap_cli/commands/util.rb b/lib/leap_cli/commands/util.rb
deleted file mode 100644
index c1da570..0000000
--- a/lib/leap_cli/commands/util.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-module LeapCli; module Commands
-
- extend self
- extend LeapCli::Util
- extend LeapCli::Util::RemoteCommand
-
- def path(name)
- Path.named_path(name)
- end
-
- #
- # keeps prompting the user for a numbered choice, until they pick a good one or bail out.
- #
- # block is yielded and is responsible for rendering the choices.
- #
- def numbered_choice_menu(msg, items, &block)
- while true
- say("\n" + msg + ':')
- items.each_with_index &block
- say("q. quit")
- index = ask("number 1-#{items.length}> ")
- if index.empty?
- next
- elsif index =~ /q/
- bail!
- else
- i = index.to_i - 1
- if i < 0 || i >= items.length
- bail!
- else
- return i
- end
- end
- end
- end
-
-
- def parse_node_list(nodes)
- if nodes.is_a? Config::Object
- Config::ObjectList.new(nodes)
- elsif nodes.is_a? Config::ObjectList
- nodes
- elsif nodes.is_a? String
- manager.filter!(nodes)
- else
- bail! "argument error"
- end
- end
-
-end; end
diff --git a/lib/leap_cli/commands/vagrant.rb b/lib/leap_cli/commands/vagrant.rb
deleted file mode 100644
index 27c739b..0000000
--- a/lib/leap_cli/commands/vagrant.rb
+++ /dev/null
@@ -1,197 +0,0 @@
-autoload :IPAddr, 'ipaddr'
-require 'fileutils'
-
-module LeapCli; module Commands
-
- desc "Manage local virtual machines."
- long_desc "This command provides a convient way to manage Vagrant-based virtual machines. If FILTER argument is missing, the command runs on all local virtual machines. The Vagrantfile is automatically generated in 'test/Vagrantfile'. If you want to run vagrant commands manually, cd to 'test'."
- command [:local, :l] do |local|
- local.desc 'Starts up the virtual machine(s)'
- local.arg_name 'FILTER', :optional => true #, :multiple => false
- local.command :start do |start|
- start.flag(:basebox,
- :desc => "The basebox to use. This value is passed to vagrant as the "+
- "`config.vm.box` option. The value here should be the name of an installed box or a "+
- "shorthand name of a box in HashiCorp's Atlas.",
- :arg_name => 'BASEBOX',
- :default_value => 'LEAP/wheezy'
- )
- start.action do |global_options,options,args|
- vagrant_command(["up", "sandbox on"], args, options)
- end
- end
-
- local.desc 'Shuts down the virtual machine(s)'
- local.arg_name 'FILTER', :optional => true #, :multiple => false
- local.command :stop do |stop|
- stop.action do |global_options,options,args|
- if global_options[:yes]
- vagrant_command("halt --force", args)
- else
- vagrant_command("halt", args)
- end
- end
- end
-
- local.desc 'Destroys the virtual machine(s), reclaiming the disk space'
- local.arg_name 'FILTER', :optional => true #, :multiple => false
- local.command :destroy do |destroy|
- destroy.action do |global_options,options,args|
- if global_options[:yes]
- vagrant_command("destroy --force", args)
- else
- vagrant_command("destroy", args)
- end
- end
- end
-
- local.desc 'Print the status of local virtual machine(s)'
- local.arg_name 'FILTER', :optional => true #, :multiple => false
- local.command :status do |status|
- status.action do |global_options,options,args|
- vagrant_command("status", args)
- end
- end
-
- local.desc 'Saves the current state of the virtual machine as a new snapshot'
- local.arg_name 'FILTER', :optional => true #, :multiple => false
- local.command :save do |status|
- status.action do |global_options,options,args|
- vagrant_command("sandbox commit", args)
- end
- end
-
- local.desc 'Resets virtual machine(s) to the last saved snapshot'
- local.arg_name 'FILTER', :optional => true #, :multiple => false
- local.command :reset do |reset|
- reset.action do |global_options,options,args|
- vagrant_command("sandbox rollback", args)
- end
- end
- end
-
- public
-
- #
- # returns the path to a vagrant ssh key file.
- #
- # if the vagrant.key file is owned by root or ourselves, then
- # we need to make sure that it owned by us and not world readable.
- #
- def vagrant_ssh_key_file
- file_path = File.expand_path('../../../vendor/vagrant_ssh_keys/vagrant.key', File.dirname(__FILE__))
- Util.assert_files_exist! file_path
- uid = File.new(file_path).stat.uid
- if uid == 0 || uid == Process.euid
- FileUtils.install file_path, '/tmp/vagrant.key', :mode => 0600
- file_path = '/tmp/vagrant.key'
- end
- return file_path
- end
-
- protected
-
- def vagrant_command(cmds, args, options={})
- vagrant_setup(options)
- cmds = cmds.to_a
- if args.empty?
- nodes = [""]
- else
- nodes = manager.filter(args)[:environment => "local"].field(:name)
- end
- if nodes.any?
- vagrant_dir = File.dirname(Path.named_path(:vagrantfile))
- exec = ["cd #{vagrant_dir}"]
- cmds.each do |cmd|
- nodes.each do |node|
- exec << "vagrant #{cmd} #{node}"
- end
- end
- execute exec.join('; ')
- else
- bail! "No nodes found. This command only works on nodes with ip_address in the network #{LeapCli.leapfile.vagrant_network}"
- end
- end
-
- private
-
- def vagrant_setup(options)
- assert_bin! 'vagrant', 'Vagrant is required for running local virtual machines. Run "sudo apt-get install vagrant".'
-
- if vagrant_version <= Gem::Version.new('1.0.0')
- gem_path = assert_run!('vagrant gem which sahara')
- if gem_path.nil? || gem_path.empty? || gem_path =~ /^ERROR/
- log :installing, "vagrant plugin 'sahara'"
- assert_run! 'vagrant gem install sahara -v 0.0.13'
- end
- else
- unless assert_run!('vagrant plugin list | grep sahara | cat').chars.any?
- log :installing, "vagrant plugin 'sahara'"
- assert_run! 'vagrant plugin install sahara'
- end
- end
- create_vagrant_file(options)
- end
-
- def vagrant_version
- @vagrant_version ||= Gem::Version.new(assert_run!('vagrant --version').split(' ')[1])
- end
-
- def execute(cmd)
- log 2, :run, cmd
- exec cmd
- end
-
- def create_vagrant_file(options)
- lines = []
- netmask = IPAddr.new('255.255.255.255').mask(LeapCli.leapfile.vagrant_network.split('/').last).to_s
-
- basebox = options[:basebox] || 'LEAP/wheezy'
-
- if vagrant_version <= Gem::Version.new('1.1.0')
- lines << %[Vagrant::Config.run do |config|]
- manager.each_node do |node|
- if node.vagrant?
- lines << %[ config.vm.define :#{node.name} do |config|]
- lines << %[ config.vm.box = "#{basebox}"]
- lines << %[ config.vm.network :hostonly, "#{node.ip_address}", :netmask => "#{netmask}"]
- lines << %[ config.vm.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]]
- lines << %[ config.vm.customize ["modifyvm", :id, "--name", "#{node.name}"]]
- lines << %[ #{leapfile.custom_vagrant_vm_line}] if leapfile.custom_vagrant_vm_line
- lines << %[ end]
- end
- end
- else
- lines << %[Vagrant.configure("2") do |config|]
- manager.each_node do |node|
- if node.vagrant?
- lines << %[ config.vm.define :#{node.name} do |config|]
- lines << %[ config.vm.box = "#{basebox}"]
- lines << %[ config.vm.network :private_network, ip: "#{node.ip_address}"]
- lines << %[ config.vm.provider "virtualbox" do |v|]
- lines << %[ v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]]
- lines << %[ v.name = "#{node.name}"]
- lines << %[ end]
- lines << %[ #{leapfile.custom_vagrant_vm_line}] if leapfile.custom_vagrant_vm_line
- lines << %[ end]
- end
- end
- end
-
- lines << %[end]
- lines << ""
- write_file! :vagrantfile, lines.join("\n")
- end
-
- def pick_next_vagrant_ip_address
- taken_ips = manager.nodes[:environment => "local"].field(:ip_address)
- if taken_ips.any?
- highest_ip = taken_ips.map{|ip| IPAddr.new(ip)}.max
- new_ip = highest_ip.succ
- else
- new_ip = IPAddr.new(LeapCli.leapfile.vagrant_network).succ.succ
- end
- return new_ip.to_s
- end
-
-end; end
diff --git a/lib/leap_cli/config/manager.rb b/lib/leap_cli/config/manager.rb
index aee2ed2..9b452f3 100644
--- a/lib/leap_cli/config/manager.rb
+++ b/lib/leap_cli/config/manager.rb
@@ -20,15 +20,6 @@ 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
diff --git a/lib/leap_cli/log.rb b/lib/leap_cli/log.rb
index 0915151..6589ad4 100644
--- a/lib/leap_cli/log.rb
+++ b/lib/leap_cli/log.rb
@@ -83,6 +83,7 @@ module LeapCli
when :fatal_error then ['fatal error:', :red, :bold]
when :warning then ['warning:', :yellow, :bold]
when :info then ['info', :cyan, :bold]
+ when :note then ['NOTE:', :cyan, :bold]
when :updated then ['updated', :cyan, :bold]
when :updating then ['updating', :cyan, :bold]
when :created then ['created', :green, :bold]