From e767aa460fc64a317551012f1781c2105c572158 Mon Sep 17 00:00:00 2001 From: elijah Date: Mon, 19 Dec 2016 15:23:57 -0800 Subject: feature: add troubleshooting info to `leap user ls` It is hard to get ssh key setup right. This change makes it much easier to debug what the problem is. --- lib/leap_cli/commands/user.rb | 25 +++++++++++++++++++++++++ lib/leap_cli/ssh/key.rb | 11 ++++++----- 2 files changed, 31 insertions(+), 5 deletions(-) (limited to 'lib/leap_cli') diff --git a/lib/leap_cli/commands/user.rb b/lib/leap_cli/commands/user.rb index 1ca92719..a10d5163 100644 --- a/lib/leap_cli/commands/user.rb +++ b/lib/leap_cli/commands/user.rb @@ -113,6 +113,20 @@ module LeapCli def do_list_users(global, options, args) require 'leap_cli/ssh' + ssh_keys = {} + Dir.glob("#{ENV['HOME']}/.ssh/*.pub").each do |keyfile| + key = SSH::Key.load(keyfile) + ssh_keys[key.fingerprint] = key if key + end + + ssh_agent_keys = {} + if !`which ssh-add`.empty? + `ssh-add -L`.split("\n").each do |keystring| + key = SSH::Key.load(keystring) + ssh_agent_keys[key.fingerprint] = key if key + end + end + Dir.glob(path([:user_ssh, '*'])).each do |keyfile| username = File.basename(File.dirname(keyfile)) log username, :color => :cyan do @@ -121,6 +135,14 @@ module LeapCli log 'SSH MD5 fingerprint: ' + key.fingerprint(:digest => :md5, :type => :ssh, :encoding => :hex) log 'SSH SHA256 fingerprint: ' + key.fingerprint(:digest => :sha256, :type => :ssh, :encoding => :base64) log 'DER MD5 fingerprint: ' + key.fingerprint(:digest => :md5, :type => :der, :encoding => :hex) + if ssh_keys[key.fingerprint] + log 'Matches local key: ' + ssh_keys[key.fingerprint].filename, color: :green + if ssh_agent_keys[key.fingerprint] + log 'Matches ssh-agent key: ' + ssh_agent_keys[key.fingerprint].summary(encoding: :base64), color: :green + else + log :error, 'No matching key in the ssh-agent' + end + end end end end @@ -154,6 +176,9 @@ module LeapCli end else key_index = 0 + log "Picking the only compatible ssh key: "+ ssh_keys[key_index].filename do + log ssh_keys[key_index].summary + end end return ssh_keys[key_index] diff --git a/lib/leap_cli/ssh/key.rb b/lib/leap_cli/ssh/key.rb index 76223b7e..108b6137 100644 --- a/lib/leap_cli/ssh/key.rb +++ b/lib/leap_cli/ssh/key.rb @@ -254,9 +254,9 @@ module LeapCli end if digest == "MD5" && encoding == :hex - return fp.scan(/../).join(':') + return fp.strip.scan(/../).join(':') else - return fp + return fp.strip end end @@ -267,11 +267,12 @@ module LeapCli Net::SSH::Buffer.from(:key, @key).to_s.split("\001\000").last.size * 8 end - def summary + def summary(type: :ssh, digest: :sha256, encoding: :hex) + fp = digest.to_s.upcase + ":" + self.fingerprint(type: type, digest: digest, encoding: encoding) if self.filename - "%s %s %s (%s)" % [self.type, self.bits, self.fingerprint, File.basename(self.filename)] + "%s %s %s (%s)" % [self.type, self.bits, fp, File.basename(self.filename)] else - "%s %s %s" % [self.type, self.bits, self.fingerprint] + "%s %s %s" % [self.type, self.bits, fp] end end -- cgit v1.2.3 From dc43b30079316ed41bf95eca902d5d65ba877888 Mon Sep 17 00:00:00 2001 From: elijah Date: Mon, 19 Dec 2016 15:28:11 -0800 Subject: bugfix: ensure let's encrypt errors make it to the user. --- lib/leap_cli/commands/ca.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'lib/leap_cli') diff --git a/lib/leap_cli/commands/ca.rb b/lib/leap_cli/commands/ca.rb index 3c5fc7d5..1c67ae67 100644 --- a/lib/leap_cli/commands/ca.rb +++ b/lib/leap_cli/commands/ca.rb @@ -281,11 +281,13 @@ module LeapCli; module Commands if status == 'valid' log 'authorized!', color: :green, style: :bold elsif status == 'error' - bail! :error, message + bail! :error, message.inspect elsif status == 'unauthorized' - bail!(:unauthorized, message, color: :yellow, style: :bold) do + bail!(:unauthorized, message.inspect, color: :yellow, style: :bold) do log 'You must first run `leap cert register` to register the account key with letsencrypt.org' end + else + bail!(:error, "unrecognized status: #{status.inspect}, #{message.inspect}") end log :fetching, "new certificate from letsencrypt.org" -- cgit v1.2.3 From c0f489c4226c924fa1d96d12cba7eb5f63ccaf64 Mon Sep 17 00:00:00 2001 From: elijah Date: Wed, 21 Dec 2016 11:06:48 -0800 Subject: add command `leap node disable` and `leap node enable` --- lib/leap_cli/commands/node.rb | 27 +++++++++++++++++++++++++++ lib/leap_cli/config/environment.rb | 30 ++++++++++++++++++++++++++---- lib/leap_cli/config/node.rb | 4 ++-- 3 files changed, 55 insertions(+), 6 deletions(-) (limited to 'lib/leap_cli') diff --git a/lib/leap_cli/commands/node.rb b/lib/leap_cli/commands/node.rb index 60540de9..9cde15bc 100644 --- a/lib/leap_cli/commands/node.rb +++ b/lib/leap_cli/commands/node.rb @@ -45,6 +45,23 @@ module LeapCli; module Commands do_node_rm(global_options, options, args) end end + + node.desc 'Mark a node as disabled.' + node.arg_name 'NAME' + node.command :disable do |cmd| + cmd.action do |global_options,options,args| + do_node_disable(global_options, options, args) + end + end + + node.desc 'Mark a node as enabled.' + node.arg_name 'NAME' + node.command :enable do |cmd| + cmd.action do |global_options,options,args| + do_node_enable(global_options, options, args) + end + end + end ## @@ -126,4 +143,14 @@ module LeapCli; module Commands remove_node_facts(node.name) end + def do_node_enable(global, options, args) + node = get_node_from_args(args, include_disabled: true) + node.update_json({}, remove: ["enabled"]) + end + + def do_node_disable(global, options, args) + node = get_node_from_args(args, include_disabled: true) + node.update_json("enabled" => false) + end + end; end diff --git a/lib/leap_cli/config/environment.rb b/lib/leap_cli/config/environment.rb index ce570839..0410ef5b 100644 --- a/lib/leap_cli/config/environment.rb +++ b/lib/leap_cli/config/environment.rb @@ -122,14 +122,25 @@ module LeapCli; module Config end # - # Alters the node's json config file. Unfortunately, doing this will - # strip out all the comments. + # Alters the node's json config file. As a side effect, all comments get + # moved to the top of the file. # - def update_node_json(node, new_values) + # NOTE: This does a shallow merge! In other words, a call like this... + # + # update_node_json(node, {"webapp" => {"domain" => "example.org"}) + # + # ...is probably not what you want, because it will entirely remove all + # existing entries under "webapp". + # + def update_node_json(node, new_values, options=nil) node_json_path = Path.named_path([:node_config, node.name]) + comments = read_comments(node_json_path) old_data = load_json(node_json_path, Config::Node) + options && options[:remove] && options[:remove].each do |key| + old_data.delete(key) + end new_data = old_data.merge(new_values) - new_contents = JSON.sorted_generate(new_data) + "\n" + new_contents = [comments, JSON.sorted_generate(new_data), "\n"].join Util::write_file! node_json_path, new_contents end @@ -152,6 +163,17 @@ module LeapCli; module Config results end + def read_comments(filename) + buffer = StringIO.new + File.open(filename, "rb", :encoding => 'UTF-8') do |f| + while (line = f.gets) + next unless line =~ /^\s*\/\// + buffer << line + end + end + return buffer.string.force_encoding('utf-8') + end + def load_json(filename, object_class, options={}) if !File.exist?(filename) return object_class.new(self) diff --git a/lib/leap_cli/config/node.rb b/lib/leap_cli/config/node.rb index 23abdee3..a7c5c1e4 100644 --- a/lib/leap_cli/config/node.rb +++ b/lib/leap_cli/config/node.rb @@ -169,8 +169,8 @@ module LeapCli; module Config # # modifies the config file nodes/NAME.json for this node. # - def update_json(new_values) - self.env.update_node_json(node, new_values) + def update_json(new_values, options=nil) + self.env.update_node_json(node, new_values, options) end # -- cgit v1.2.3 From 8ab553dfcaa1aff10123d15c908117b59d2d5b7d Mon Sep 17 00:00:00 2001 From: elijah Date: Wed, 21 Dec 2016 11:16:24 -0800 Subject: rename s/ca.rb/cert.rb/ --- lib/leap_cli/commands/ca.rb | 368 ------------------------------------------ lib/leap_cli/commands/cert.rb | 368 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 368 insertions(+), 368 deletions(-) delete mode 100644 lib/leap_cli/commands/ca.rb create mode 100644 lib/leap_cli/commands/cert.rb (limited to 'lib/leap_cli') diff --git a/lib/leap_cli/commands/ca.rb b/lib/leap_cli/commands/ca.rb deleted file mode 100644 index 1c67ae67..00000000 --- a/lib/leap_cli/commands/ca.rb +++ /dev/null @@ -1,368 +0,0 @@ -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 `.' - 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 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| - generate_dh - end - end - - 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.arg_name "DOMAIN" - 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| - generate_csr(global_options, options, args) - end - end - - cert.desc "Register an authorization key with the CA letsencrypt.org" - cert.long_desc "This only needs to be done once." - cert.command :register do |register| - register.action do |global, options, args| - do_register_key(global, options, args) - end - end - - cert.desc "Renews a certificate using the CA letsencrypt.org" - cert.arg_name "DOMAIN" - cert.command :renew do |renew| - renew.action do |global, options, args| - do_renew_cert(global, options, args) - end - end - - end - - protected - - # - # will generate new certificates for the specified nodes, if needed. - # - def update_certificates(nodes, options={}) - require 'leap_cli/x509' - 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| - node.warn_if_commercial_cert_will_soon_expire - if !node.x509.use - remove_file!([:node_x509_key, node.name]) - remove_file!([:node_x509_cert, node.name]) - elsif options[:force] || node.cert_needs_updating? - node.generate_cert - end - end - end - - # - # yields client key and cert suitable for testing - # - def generate_test_client_cert(prefix=nil) - require 'leap_cli/x509' - 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 = X509.yesterday - cert.not_after = X509.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 - - private - - def generate_new_certificate_authority(key_file, cert_file, common_name) - require 'leap_cli/x509' - 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 = X509.new_ca(provider.ca, common_name) - - write_file!(key_file, root.key_material.private_key.to_pem) - write_file!(cert_file, root.to_pem) - end - - def generate_dh - require 'leap_cli/x509' - 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 - - # - # 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 - # - def generate_csr(global_options, options, args) - require 'leap_cli/x509' - 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' - - server_certificates = provider.ca.server_certificates - options[:domain] ||= args.first || provider.domain - options[:organization] ||= provider.name[provider.default_language] - options[:country] ||= server_certificates['country'] - options[:state] ||= server_certificates['state'] - options[:locality] ||= server_certificates['locality'] - options[:bits] ||= server_certificates.bit_size - options[:digest] ||= server_certificates.digest - - unless global_options[:force] - assert_files_missing! [:commercial_key, options[:domain]], [:commercial_csr, options[:domain]], - :msg => 'If you really want to create a new key and CSR, remove these files first or run with --force.' - end - - X509.create_csr_and_cert(options) - end - - # - # letsencrypt.org - # - - def do_register_key(global, options, args) - require 'leap_cli/acme' - assert_config! 'provider.contacts.default' - contact = manager.provider.contacts.default.first - - if file_exists?(:acme_key) && !global[:force] - bail! do - log "the authorization key for letsencrypt.org already exists" - log "run with --force if you really want to register a new key." - end - else - private_key = Acme.new_private_key - registration = nil - - log(:registering, "letsencrypt.org authorization key using contact `%s`" % contact) do - acme = Acme.new(key: private_key) - registration = acme.register(contact) - if registration - log 'success!', :color => :green, :style => :bold - else - bail! "could not register authorization key." - end - end - - log :saving, "authorization key for letsencrypt.org" do - write_file!(:acme_key, private_key.to_pem) - write_file!(:acme_info, JSON.sorted_generate({ - id: registration.id, - contact: registration.contact, - key: registration.key, - uri: registration.uri - })) - log :warning, "keep key file private!" - end - end - end - - def assert_no_errors!(msg) - yield - rescue StandardError => exc - bail! :error, msg do - log exc.to_s - end - end - - def do_renew_cert(global, options, args) - require 'leap_cli/acme' - require 'leap_cli/ssh' - require 'socket' - require 'net/http' - - csr = nil - account_key = nil - cert = nil - acme = nil - - # - # sanity check the domain - # - domain = args.first - nodes = nodes_for_domain(domain) - domain_ready_for_acme!(domain) - - # - # load key material - # - assert_files_exist!([:commercial_key, domain], [:commercial_csr, domain], - :msg => 'Please create the CSR first with `leap cert csr %s`' % domain) - assert_no_errors!("Could not load #{path([:commercial_csr, domain])}") do - csr = Acme.load_csr(read_file!([:commercial_csr, domain])) - end - assert_files_exist!(:acme_key, - :msg => "Please run `leap cert register` first. This only needs to be done once.") - assert_no_errors!("Could not load #{path(:acme_key)}") do - account_key = Acme.load_private_key(read_file!(:acme_key)) - end - - # - # check authorization for this domain - # - log :checking, "authorization" - acme = Acme.new(domain: domain, key: account_key) - status, message = acme.authorize do |challenge| - log(:uploading, 'challenge to server %s' % domain) do - SSH.remote_command(nodes) do |ssh, host| - ssh.scripts.upload_acme_challenge(challenge.token, challenge.file_content) - end - end - log :waiting, "for letsencrypt.org to verify challenge" - end - if status == 'valid' - log 'authorized!', color: :green, style: :bold - elsif status == 'error' - bail! :error, message.inspect - elsif status == 'unauthorized' - bail!(:unauthorized, message.inspect, color: :yellow, style: :bold) do - log 'You must first run `leap cert register` to register the account key with letsencrypt.org' - end - else - bail!(:error, "unrecognized status: #{status.inspect}, #{message.inspect}") - end - - log :fetching, "new certificate from letsencrypt.org" - assert_no_errors!("could not renew certificate") do - cert = acme.get_certificate(csr) - end - log 'success', color: :green, style: :bold - write_file!([:commercial_cert, domain], cert.fullchain_to_pem) - log 'You should now run `leap deploy` to deploy the new certificate.' - end - - # - # Returns a hash of nodes that match this domain. It also checks: - # - # * a node configuration has this domain - # * the dns for the domain exists - # - # This method will bail if any checks fail. - # - def nodes_for_domain(domain) - bail! { log 'Argument DOMAIN is required' } if domain.nil? || domain.empty? - nodes = manager.nodes['dns.aliases' => domain] - if nodes.empty? - bail! :error, "There are no nodes configured for domain `%s`" % domain - end - begin - ips = Socket.getaddrinfo(domain, 'http').map {|record| record[2]}.uniq - nodes = nodes['ip_address' => ips] - if nodes.empty? - bail! do - log :error, "The domain `%s` resolves to [%s]" % [domain, ips.join(', ')] - log :error, "But there no nodes configured for this domain with these adddresses." - end - end - rescue SocketError - bail! :error, "Could not resolve the DNS for `#{domain}`. Without a DNS " + - "entry for this domain, authorization will not work." - end - return nodes - end - - # - # runs the following checks on the domain: - # - # * we are able to get /.well-known/acme-challenge/ok - # - # This method will bail if any checks fail. - # - def domain_ready_for_acme!(domain) - begin - uri = URI("https://#{domain}/.well-known/acme-challenge/ok") - options = { - use_ssl: true, - open_timeout: 5, - verify_mode: OpenSSL::SSL::VERIFY_NONE - } - Net::HTTP.start(uri.host, uri.port, options) do |http| - http.request(Net::HTTP::Get.new(uri)) do |response| - if !response.is_a?(Net::HTTPSuccess) - bail!(:error, "Could not GET %s" % uri) do - log "%s %s" % [response.code, response.message] - log "You may need to run `leap deploy`" - end - end - end - end - rescue Errno::ETIMEDOUT, Net::OpenTimeout - bail! :error, "Connection attempt timed out: %s" % uri - rescue Interrupt - bail! - rescue StandardError => exc - bail!(:error, "Could not GET %s" % uri) do - log exc.to_s - end - end - end - -end; end diff --git a/lib/leap_cli/commands/cert.rb b/lib/leap_cli/commands/cert.rb new file mode 100644 index 00000000..1c67ae67 --- /dev/null +++ b/lib/leap_cli/commands/cert.rb @@ -0,0 +1,368 @@ +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 `.' + 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 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| + generate_dh + end + end + + 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.arg_name "DOMAIN" + 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| + generate_csr(global_options, options, args) + end + end + + cert.desc "Register an authorization key with the CA letsencrypt.org" + cert.long_desc "This only needs to be done once." + cert.command :register do |register| + register.action do |global, options, args| + do_register_key(global, options, args) + end + end + + cert.desc "Renews a certificate using the CA letsencrypt.org" + cert.arg_name "DOMAIN" + cert.command :renew do |renew| + renew.action do |global, options, args| + do_renew_cert(global, options, args) + end + end + + end + + protected + + # + # will generate new certificates for the specified nodes, if needed. + # + def update_certificates(nodes, options={}) + require 'leap_cli/x509' + 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| + node.warn_if_commercial_cert_will_soon_expire + if !node.x509.use + remove_file!([:node_x509_key, node.name]) + remove_file!([:node_x509_cert, node.name]) + elsif options[:force] || node.cert_needs_updating? + node.generate_cert + end + end + end + + # + # yields client key and cert suitable for testing + # + def generate_test_client_cert(prefix=nil) + require 'leap_cli/x509' + 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 = X509.yesterday + cert.not_after = X509.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 + + private + + def generate_new_certificate_authority(key_file, cert_file, common_name) + require 'leap_cli/x509' + 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 = X509.new_ca(provider.ca, common_name) + + write_file!(key_file, root.key_material.private_key.to_pem) + write_file!(cert_file, root.to_pem) + end + + def generate_dh + require 'leap_cli/x509' + 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 + + # + # 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 + # + def generate_csr(global_options, options, args) + require 'leap_cli/x509' + 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' + + server_certificates = provider.ca.server_certificates + options[:domain] ||= args.first || provider.domain + options[:organization] ||= provider.name[provider.default_language] + options[:country] ||= server_certificates['country'] + options[:state] ||= server_certificates['state'] + options[:locality] ||= server_certificates['locality'] + options[:bits] ||= server_certificates.bit_size + options[:digest] ||= server_certificates.digest + + unless global_options[:force] + assert_files_missing! [:commercial_key, options[:domain]], [:commercial_csr, options[:domain]], + :msg => 'If you really want to create a new key and CSR, remove these files first or run with --force.' + end + + X509.create_csr_and_cert(options) + end + + # + # letsencrypt.org + # + + def do_register_key(global, options, args) + require 'leap_cli/acme' + assert_config! 'provider.contacts.default' + contact = manager.provider.contacts.default.first + + if file_exists?(:acme_key) && !global[:force] + bail! do + log "the authorization key for letsencrypt.org already exists" + log "run with --force if you really want to register a new key." + end + else + private_key = Acme.new_private_key + registration = nil + + log(:registering, "letsencrypt.org authorization key using contact `%s`" % contact) do + acme = Acme.new(key: private_key) + registration = acme.register(contact) + if registration + log 'success!', :color => :green, :style => :bold + else + bail! "could not register authorization key." + end + end + + log :saving, "authorization key for letsencrypt.org" do + write_file!(:acme_key, private_key.to_pem) + write_file!(:acme_info, JSON.sorted_generate({ + id: registration.id, + contact: registration.contact, + key: registration.key, + uri: registration.uri + })) + log :warning, "keep key file private!" + end + end + end + + def assert_no_errors!(msg) + yield + rescue StandardError => exc + bail! :error, msg do + log exc.to_s + end + end + + def do_renew_cert(global, options, args) + require 'leap_cli/acme' + require 'leap_cli/ssh' + require 'socket' + require 'net/http' + + csr = nil + account_key = nil + cert = nil + acme = nil + + # + # sanity check the domain + # + domain = args.first + nodes = nodes_for_domain(domain) + domain_ready_for_acme!(domain) + + # + # load key material + # + assert_files_exist!([:commercial_key, domain], [:commercial_csr, domain], + :msg => 'Please create the CSR first with `leap cert csr %s`' % domain) + assert_no_errors!("Could not load #{path([:commercial_csr, domain])}") do + csr = Acme.load_csr(read_file!([:commercial_csr, domain])) + end + assert_files_exist!(:acme_key, + :msg => "Please run `leap cert register` first. This only needs to be done once.") + assert_no_errors!("Could not load #{path(:acme_key)}") do + account_key = Acme.load_private_key(read_file!(:acme_key)) + end + + # + # check authorization for this domain + # + log :checking, "authorization" + acme = Acme.new(domain: domain, key: account_key) + status, message = acme.authorize do |challenge| + log(:uploading, 'challenge to server %s' % domain) do + SSH.remote_command(nodes) do |ssh, host| + ssh.scripts.upload_acme_challenge(challenge.token, challenge.file_content) + end + end + log :waiting, "for letsencrypt.org to verify challenge" + end + if status == 'valid' + log 'authorized!', color: :green, style: :bold + elsif status == 'error' + bail! :error, message.inspect + elsif status == 'unauthorized' + bail!(:unauthorized, message.inspect, color: :yellow, style: :bold) do + log 'You must first run `leap cert register` to register the account key with letsencrypt.org' + end + else + bail!(:error, "unrecognized status: #{status.inspect}, #{message.inspect}") + end + + log :fetching, "new certificate from letsencrypt.org" + assert_no_errors!("could not renew certificate") do + cert = acme.get_certificate(csr) + end + log 'success', color: :green, style: :bold + write_file!([:commercial_cert, domain], cert.fullchain_to_pem) + log 'You should now run `leap deploy` to deploy the new certificate.' + end + + # + # Returns a hash of nodes that match this domain. It also checks: + # + # * a node configuration has this domain + # * the dns for the domain exists + # + # This method will bail if any checks fail. + # + def nodes_for_domain(domain) + bail! { log 'Argument DOMAIN is required' } if domain.nil? || domain.empty? + nodes = manager.nodes['dns.aliases' => domain] + if nodes.empty? + bail! :error, "There are no nodes configured for domain `%s`" % domain + end + begin + ips = Socket.getaddrinfo(domain, 'http').map {|record| record[2]}.uniq + nodes = nodes['ip_address' => ips] + if nodes.empty? + bail! do + log :error, "The domain `%s` resolves to [%s]" % [domain, ips.join(', ')] + log :error, "But there no nodes configured for this domain with these adddresses." + end + end + rescue SocketError + bail! :error, "Could not resolve the DNS for `#{domain}`. Without a DNS " + + "entry for this domain, authorization will not work." + end + return nodes + end + + # + # runs the following checks on the domain: + # + # * we are able to get /.well-known/acme-challenge/ok + # + # This method will bail if any checks fail. + # + def domain_ready_for_acme!(domain) + begin + uri = URI("https://#{domain}/.well-known/acme-challenge/ok") + options = { + use_ssl: true, + open_timeout: 5, + verify_mode: OpenSSL::SSL::VERIFY_NONE + } + Net::HTTP.start(uri.host, uri.port, options) do |http| + http.request(Net::HTTP::Get.new(uri)) do |response| + if !response.is_a?(Net::HTTPSuccess) + bail!(:error, "Could not GET %s" % uri) do + log "%s %s" % [response.code, response.message] + log "You may need to run `leap deploy`" + end + end + end + end + rescue Errno::ETIMEDOUT, Net::OpenTimeout + bail! :error, "Connection attempt timed out: %s" % uri + rescue Interrupt + bail! + rescue StandardError => exc + bail!(:error, "Could not GET %s" % uri) do + log exc.to_s + end + end + end + +end; end -- cgit v1.2.3 From 6da8d11ec2f000353e952ff95abe27dd8c8381c8 Mon Sep 17 00:00:00 2001 From: elijah Date: Wed, 21 Dec 2016 13:32:29 -0800 Subject: added command `leap ping` --- lib/leap_cli/commands/ping.rb | 58 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 lib/leap_cli/commands/ping.rb (limited to 'lib/leap_cli') diff --git a/lib/leap_cli/commands/ping.rb b/lib/leap_cli/commands/ping.rb new file mode 100644 index 00000000..4283d9b3 --- /dev/null +++ b/lib/leap_cli/commands/ping.rb @@ -0,0 +1,58 @@ +module LeapCli; module Commands + + desc "Ping nodes to see if they are alive." + long_desc "Attempts to ping each node in the FILTER set." + arg_name "FILTER" + command :ping do |c| + c.flag 'timeout', :arg_name => "TIMEOUT", + :default_value => 2, :desc => 'Wait at most TIMEOUT seconds.' + c.flag 'count', :arg_name => "COUNT", + :default_value => 2, :desc => 'Ping COUNT times.' + c.action do |global, options, args| + do_ping(global, options, args) + end + end + + private + + def do_ping(global, options, args) + assert_bin!('ping') + + timeout = [options[:timeout].to_i, 1].max + count = [options[:count].to_i, 1].max + nodes = nil + + if args && args.any? + node = manager.disabled_node(args.first) + if node + nodes = Config::ObjectList.new + nodes.add(node.name, node) + end + end + + nodes ||= manager.filter! args + + threads = [] + nodes.each_node do |node| + threads << Thread.new do + cmd = "ping -i 0.2 -n -q -W #{timeout} -c #{count} #{node.ip_address} 2>&1" + log(2, cmd) + output = `#{cmd}` + if $?.success? + last = output.split("\n").last + times = last.split('=').last.strip + min, avg, max, mdev = times.split('/') + log("ping #{min} ms", host: node.name, color: :green) + else + log(:failed, "to ping #{node.ip_address}", host: node.name) + end + end + end + threads.map(&:join) + + log("done") + end + +end; end + + -- cgit v1.2.3 From dd189d2de941ec081261ced814a9c822e5ef02a1 Mon Sep 17 00:00:00 2001 From: elijah Date: Tue, 10 Jan 2017 10:45:36 -0800 Subject: bugfix: `leap user ls` now warns if the ssh keytype is unsupported --- lib/leap_cli/commands/user.rb | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) (limited to 'lib/leap_cli') diff --git a/lib/leap_cli/commands/user.rb b/lib/leap_cli/commands/user.rb index a10d5163..7fd5f52d 100644 --- a/lib/leap_cli/commands/user.rb +++ b/lib/leap_cli/commands/user.rb @@ -132,15 +132,21 @@ module LeapCli log username, :color => :cyan do log Path.relative_path(keyfile) key = SSH::Key.load(keyfile) - log 'SSH MD5 fingerprint: ' + key.fingerprint(:digest => :md5, :type => :ssh, :encoding => :hex) - log 'SSH SHA256 fingerprint: ' + key.fingerprint(:digest => :sha256, :type => :ssh, :encoding => :base64) - log 'DER MD5 fingerprint: ' + key.fingerprint(:digest => :md5, :type => :der, :encoding => :hex) - if ssh_keys[key.fingerprint] - log 'Matches local key: ' + ssh_keys[key.fingerprint].filename, color: :green - if ssh_agent_keys[key.fingerprint] - log 'Matches ssh-agent key: ' + ssh_agent_keys[key.fingerprint].summary(encoding: :base64), color: :green - else - log :error, 'No matching key in the ssh-agent' + if key.nil? + log :warning, "could not read ssh key #{keyfile}" do + log "currently, only these ssh key types are supported: " + SSH::Key::SUPPORTED_TYPES.join(", ") + end + else + log 'SSH MD5 fingerprint: ' + key.fingerprint(:digest => :md5, :type => :ssh, :encoding => :hex) + log 'SSH SHA256 fingerprint: ' + key.fingerprint(:digest => :sha256, :type => :ssh, :encoding => :base64) + log 'DER MD5 fingerprint: ' + key.fingerprint(:digest => :md5, :type => :der, :encoding => :hex) + if ssh_keys[key.fingerprint] + log 'Matches local key: ' + ssh_keys[key.fingerprint].filename, color: :green + if ssh_agent_keys[key.fingerprint] + log 'Matches ssh-agent key: ' + ssh_agent_keys[key.fingerprint].summary(encoding: :base64), color: :green + else + log :error, 'No matching key in the ssh-agent' + end end end end -- cgit v1.2.3 From cce9af1fce42c29bf062cccfc46ef356d83a6328 Mon Sep 17 00:00:00 2001 From: varac Date: Thu, 23 Feb 2017 11:42:47 +0100 Subject: [8144] Remove Haproxy We used haproxy because we had multiple bigcouch nodes but now with a single couchdb node this is not needed anymore. - Resolves: #8144 --- lib/leap_cli/macros/haproxy.rb | 73 ------------------------------------------ 1 file changed, 73 deletions(-) delete mode 100644 lib/leap_cli/macros/haproxy.rb (limited to 'lib/leap_cli') diff --git a/lib/leap_cli/macros/haproxy.rb b/lib/leap_cli/macros/haproxy.rb deleted file mode 100644 index 3fef24c4..00000000 --- a/lib/leap_cli/macros/haproxy.rb +++ /dev/null @@ -1,73 +0,0 @@ -# encoding: utf-8 - -## -## HAPROXY -## - -module LeapCli - module Macro - - # - # creates a hash suitable for configuring haproxy. the key is the node name of the server we are proxying to. - # - # * node_list - a hash of nodes for the haproxy servers - # * stunnel_client - contains the mappings to local ports for each server node. - # * non_stunnel_port - in case self is included in node_list, the port to connect to. - # - # 1000 weight is used for nodes in the same location. - # 100 otherwise. - # - def haproxy_servers(node_list, stunnel_clients, non_stunnel_port=nil) - default_weight = 10 - local_weight = 100 - - # record the hosts_file - hostnames(node_list) - - # create a simple map for node name -> local stunnel accept port - accept_ports = stunnel_clients.inject({}) do |hsh, stunnel_entry| - name = stunnel_entry.first.sub(/_[0-9]+$/, '') - hsh[name] = stunnel_entry.last['accept_port'] - hsh - end - - # if one the nodes in the node list is ourself, then there will not be a stunnel to it, - # but we need to include it anyway in the haproxy config. - if node_list[self.name] && non_stunnel_port - accept_ports[self.name] = non_stunnel_port - end - - # create the first pass of the servers hash - servers = node_list.values.inject(Config::ObjectList.new) do |hsh, node| - # make sure we have a port to talk to - unless accept_ports[node.name] - error "haproxy needs a local port to talk to when connecting to #{node.name}" - end - weight = default_weight - try { - weight = local_weight if self.location.name == node.location.name - } - hsh[node.name] = Config::Object[ - 'backup', false, - 'host', 'localhost', - 'port', accept_ports[node.name], - 'weight', weight - ] - if node.services.include?('couchdb') - hsh[node.name]['writable'] = node.couch.mode != 'mirror' - end - hsh - end - - # if there are some local servers, make the others backup - if servers.detect{|k,v| v.weight == local_weight} - servers.each do |k,server| - server['backup'] = server['weight'] == default_weight - end - end - - return servers - end - - end -end -- cgit v1.2.3 From 8c1c4c102936dd779c74d615763e7adef7033ec1 Mon Sep 17 00:00:00 2001 From: varac Date: Wed, 15 Mar 2017 00:56:47 +0100 Subject: Direct connection when couch runs locally --- lib/leap_cli/macros/stunnel.rb | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) (limited to 'lib/leap_cli') diff --git a/lib/leap_cli/macros/stunnel.rb b/lib/leap_cli/macros/stunnel.rb index 821bda38..59a38fad 100644 --- a/lib/leap_cli/macros/stunnel.rb +++ b/lib/leap_cli/macros/stunnel.rb @@ -87,6 +87,18 @@ module LeapCli } end + # + # what it the port of the couchdb we should connect to. + # host will always be localhost. + # + def couchdb_port + if services.include?('couchdb') + couch.port + else + stunnel.clients.couch_client.values.first.accept_port + end + end + private # @@ -103,4 +115,4 @@ module LeapCli end end -end \ No newline at end of file +end -- cgit v1.2.3 From 0b3aef03cb113e997c2a654ef2f7b1674a0a8877 Mon Sep 17 00:00:00 2001 From: elijah Date: Tue, 25 Apr 2017 12:18:04 -0700 Subject: bugfix: ensure that nodes only have one environment specified (closes #8778) --- lib/leap_cli/config/manager.rb | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) (limited to 'lib/leap_cli') diff --git a/lib/leap_cli/config/manager.rb b/lib/leap_cli/config/manager.rb index d69a5808..a9f1a85f 100644 --- a/lib/leap_cli/config/manager.rb +++ b/lib/leap_cli/config/manager.rb @@ -342,14 +342,25 @@ module LeapCli if node.vagrant? return self.env("local") else - environment = self.env(default_environment) + environment = nil if node['tags'] node['tags'].to_a.each do |tag| if self.environment_names.include?(tag) - environment = self.env(tag) + if environment.nil? + environment = self.env(tag) + else + LeapCli::Util.bail! do + LeapCli.log( + :error, + "The node '%s' is invalid, because it cannot have two environments ('%s' and '%s')." % + [node.name, environment.name, tag] + ) + end + end end end end + environment ||= self.env(default_environment) return environment end end -- cgit v1.2.3 From 82a9d9cd3b11b5278a1e06c61c0e0b548f533593 Mon Sep 17 00:00:00 2001 From: varac Date: Wed, 10 May 2017 20:46:38 +0200 Subject: Increase Vagrant defaut mem to 2gb --- lib/leap_cli/commands/vagrant.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib/leap_cli') diff --git a/lib/leap_cli/commands/vagrant.rb b/lib/leap_cli/commands/vagrant.rb index f8a75b61..78b2fede 100644 --- a/lib/leap_cli/commands/vagrant.rb +++ b/lib/leap_cli/commands/vagrant.rb @@ -132,10 +132,10 @@ module LeapCli; module Commands lines << %[ config.vm.provider "virtualbox" do |v|] lines << %[ v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]] lines << %[ v.name = "#{node.name}"] - lines << %[ v.memory = 1536] + lines << %[ v.memory = 2048] lines << %[ end] lines << %[ config.vm.provider "libvirt" do |v|] - lines << %[ v.memory = 1536] + lines << %[ v.memory = 2048] lines << %[ end] lines << %[ #{leapfile.custom_vagrant_vm_line}] if leapfile.custom_vagrant_vm_line lines << %[ end] -- cgit v1.2.3 From 1e463c6638a05a237d660f458f5a147353be3fc1 Mon Sep 17 00:00:00 2001 From: elijah Date: Fri, 26 May 2017 16:41:51 -0700 Subject: static - support for renewing certs with let's encrypt for static sites --- lib/leap_cli/commands/cert.rb | 54 +++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 22 deletions(-) (limited to 'lib/leap_cli') diff --git a/lib/leap_cli/commands/cert.rb b/lib/leap_cli/commands/cert.rb index 1c67ae67..81f45eb5 100644 --- a/lib/leap_cli/commands/cert.rb +++ b/lib/leap_cli/commands/cert.rb @@ -337,31 +337,41 @@ module LeapCli; module Commands # This method will bail if any checks fail. # def domain_ready_for_acme!(domain) - begin - uri = URI("https://#{domain}/.well-known/acme-challenge/ok") - options = { - use_ssl: true, - open_timeout: 5, - verify_mode: OpenSSL::SSL::VERIFY_NONE - } - Net::HTTP.start(uri.host, uri.port, options) do |http| - http.request(Net::HTTP::Get.new(uri)) do |response| - if !response.is_a?(Net::HTTPSuccess) - bail!(:error, "Could not GET %s" % uri) do - log "%s %s" % [response.code, response.message] - log "You may need to run `leap deploy`" - end + uri = URI("https://#{domain}/.well-known/acme-challenge/ok") + options = { + use_ssl: true, + open_timeout: 5, + verify_mode: OpenSSL::SSL::VERIFY_NONE + } + http_get(uri, options) + end + + private + + def http_get(uri, options, limit = 10) + raise ArgumentError, "HTTP redirect too deep (#{uri})" if limit == 0 + Net::HTTP.start(uri.host, uri.port, options) do |http| + http.request(Net::HTTP::Get.new(uri)) do |response| + case response + when Net::HTTPSuccess then + return response + when Net::HTTPRedirection then + return http_get(URI(response['location']), options, limit - 1) + else + bail!(:error, "Could not GET %s" % uri) do + log "%s %s" % [response.code, response.message] + log "You may need to run `leap deploy`" end end end - rescue Errno::ETIMEDOUT, Net::OpenTimeout - bail! :error, "Connection attempt timed out: %s" % uri - rescue Interrupt - bail! - rescue StandardError => exc - bail!(:error, "Could not GET %s" % uri) do - log exc.to_s - end + end + rescue Errno::ETIMEDOUT, Net::OpenTimeout + bail! :error, "Connection attempt timed out: %s" % uri + rescue Interrupt + bail! + rescue StandardError => exc + bail!(:error, "Could not GET %s" % uri) do + log exc.to_s end end -- cgit v1.2.3 From f365b914662491ab33e6af18e1b02046f6b99538 Mon Sep 17 00:00:00 2001 From: elijah Date: Wed, 28 Jun 2017 13:24:39 -0700 Subject: leap_cli - make fog gem optional --- lib/leap_cli/cloud.rb | 3 +-- lib/leap_cli/cloud/dependencies.rb | 47 ++++++++++++++++++-------------------- lib/leap_cli/commands/vm.rb | 5 +++- 3 files changed, 27 insertions(+), 28 deletions(-) (limited to 'lib/leap_cli') diff --git a/lib/leap_cli/cloud.rb b/lib/leap_cli/cloud.rb index 268cea38..b8e45b3b 100644 --- a/lib/leap_cli/cloud.rb +++ b/lib/leap_cli/cloud.rb @@ -1,4 +1,3 @@ - -require 'fog/aws' +require_relative 'cloud/dependencies.rb' require_relative 'cloud/cloud.rb' require_relative 'cloud/image.rb' diff --git a/lib/leap_cli/cloud/dependencies.rb b/lib/leap_cli/cloud/dependencies.rb index fd690e59..670d6134 100644 --- a/lib/leap_cli/cloud/dependencies.rb +++ b/lib/leap_cli/cloud/dependencies.rb @@ -1,40 +1,37 @@ # -# I am not sure this is a good idea, but it might be. Tricky, so disabled for now +# Ensure that the needed fog gems are installed # - -=begin module LeapCli class Cloud - def self.check_required_gems - begin - require "fog" - rescue LoadError - bail! do - log :error, "The 'vm' command requires the gem 'fog-core'. Please run `gem install fog-core` and try again." - end - end + SUPPORTED = { + 'aws' => {require: 'fog/aws', gem: 'fog-aws'} + }.freeze - fog_gems = @cloud.required_gems - if !options[:mock] && fog_gems.empty? - bail! do - log :warning, "no vm providers are configured in cloud.json." - log "You must have credentials for one of: #{@cloud.possible_apis.join(', ')}." + def self.check_dependencies!(config) + required_gem = map_api_to_gem(config['api']) + if required_gem.nil? + Util.bail! do + Util.log :error, "The API '#{config['api']}' specified in cloud.json is not one that I know how to speak. Try one of #{supported_list}." end end - fog_gems.each do |name, gem_name| - begin - require gem_name.sub('-','/') - rescue LoadError - bail! do - log :error, "The 'vm' command requires the gem '#{gem_name}' (because of what is configured in cloud.json)." - log "Please run `sudo gem install #{gem_name}` and try again." - end + begin + require required_gem[:require] + rescue LoadError + Util.bail! do + Util.log :error, "The 'vm' command requires the gem '#{required_gem[:gem]}'. Please run `gem install #{required_gem[:gem]}` and try again." + Util.log "(make sure you install the gem in the ruby #{RUBY_VERSION} environment)" end end end + def self.supported_list + SUPPORTED.keys.join(', ') + end + + def self.map_api_to_gem(api) + SUPPORTED[api] + end end end -=end \ No newline at end of file diff --git a/lib/leap_cli/commands/vm.rb b/lib/leap_cli/commands/vm.rb index 790774f1..6f97dbce 100644 --- a/lib/leap_cli/commands/vm.rb +++ b/lib/leap_cli/commands/vm.rb @@ -415,7 +415,6 @@ module LeapCli; module Commands config = manager.env.cloud name = nil if options[:mock] - Fog.mock! name = 'mock_aws' config['mock_aws'] = { "api" => "aws", @@ -451,6 +450,10 @@ module LeapCli; module Commands assert! entry['api'] == 'aws', "cloud.json: currently, only 'aws' is supported for `api`." assert! entry['vendor'] == 'aws', "cloud.json: currently, only 'aws' is supported for `vendor`." + LeapCli::Cloud::check_dependencies!(entry) + if options[:mock] + Fog.mock! + end return LeapCli::Cloud.new(name, entry, node) end -- cgit v1.2.3 From c0ddb0da43910e9a064e08acf424b2f2a0ccdd88 Mon Sep 17 00:00:00 2001 From: elijah Date: Fri, 30 Jun 2017 00:24:38 -0700 Subject: by default, new providers will now require invites. requires leap_cli 4173154a177b00c11a36b3168b1ce12af59f04af or later (>1.9.2). resolves #8474. create new invites with `leap run invite` --- lib/leap_cli/commands/run.rb | 53 +++++++++++++++++++++++++++++++++++--- lib/leap_cli/config/object_list.rb | 4 +++ 2 files changed, 54 insertions(+), 3 deletions(-) (limited to 'lib/leap_cli') diff --git a/lib/leap_cli/commands/run.rb b/lib/leap_cli/commands/run.rb index cad9b7a0..9149d594 100644 --- a/lib/leap_cli/commands/run.rb +++ b/lib/leap_cli/commands/run.rb @@ -3,13 +3,27 @@ module LeapCli; module Commands desc 'Run a shell command remotely' long_desc "Runs the specified command COMMAND on each node in the FILTER set. " + "For example, `leap run 'uname -a' webapp`" - arg_name 'COMMAND FILTER' command :run do |c| c.switch 'stream', :default => false, :desc => 'If set, stream the output as it arrives. (default: --stream for a single node, --no-stream for multiple nodes)' c.flag 'port', :arg_name => 'SSH_PORT', :desc => 'Override default SSH port used when trying to connect to the server.' - c.action do |global, options, args| - run_shell_command(global, options, args) + + c.desc 'Run an arbitrary shell command.' + c.arg_name 'FILTER', optional: true + c.command :command do |command| + command.action do |global, options, args| + run_shell_command(global, options, args) + end + end + + c.desc 'Generate one or more new invite codes.' + c.arg_name '[COUNT] [ENVIRONMENT]' + c.command :invite do |invite| + invite.action do |global_options,options,args| + run_new_invites(global_options, options, args) + end end + + c.default_command :command end private @@ -27,6 +41,39 @@ module LeapCli; module Commands end end + CMD_NEW_INVITES="cd /srv/leap/webapp; RAILS_ENV=production bundle exec rake \"generate_invites[NUM,USES]\"" + + def run_new_invites(global, options, args) + require 'leap_cli/ssh' + count = 1 + uses = 1 + env = nil + arg1 = args.shift + arg2 = args.shift + if arg1 && arg2 + env = manager.env(arg2) + count = arg1 + elsif arg1 + env = manager.env(arg1) + else + env = manager.env(nil) + end + unless env + bail! "Environment name you specified does not match one that is available. See `leap env ls` for the available names" + end + + env_name = env.name == 'default' ? nil : env.name + webapp_nodes = env.nodes[:environment => env_name][:services => 'webapp'].first + if webapp_nodes.empty? + bail! "Could not find a webapp node for the specified environment" + end + stream_command( + webapp_nodes, + CMD_NEW_INVITES.sub('NUM', count.to_s).sub('USES', uses.to_s), + options + ) + end + def capture_command(nodes, cmd, options) SSH.remote_command(nodes, options) do |ssh, host| output = ssh.capture(cmd, :log_output => false) diff --git a/lib/leap_cli/config/object_list.rb b/lib/leap_cli/config/object_list.rb index 80f89d92..815864e4 100644 --- a/lib/leap_cli/config/object_list.rb +++ b/lib/leap_cli/config/object_list.rb @@ -49,6 +49,10 @@ module LeapCli end end + def first + ObjectList.new(self.values.first) + end + def exclude(node) list = self.dup list.delete(node.name) -- cgit v1.2.3 From 61d8d9e0e35dc9759ec93b517b0a67df1c3506d3 Mon Sep 17 00:00:00 2001 From: elijah Date: Fri, 18 Aug 2017 16:05:19 -0700 Subject: Bug: allow `leap test --continue` to run on additional nodes if there was an ssh error. closes #8811 --- lib/leap_cli/commands/test.rb | 2 +- lib/leap_cli/ssh/backend.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'lib/leap_cli') diff --git a/lib/leap_cli/commands/test.rb b/lib/leap_cli/commands/test.rb index 70eb00fd..e2815aae 100644 --- a/lib/leap_cli/commands/test.rb +++ b/lib/leap_cli/commands/test.rb @@ -35,7 +35,7 @@ module LeapCli; module Commands SSH::remote_command(node, options) do |ssh, host| ssh.stream(test_cmd(options), :raise_error => true, :log_wrap => true) end - rescue LeapCli::SSH::ExecuteError + rescue LeapCli::SSH::TimeoutError, SSHKit::Runner::ExecuteError, SSHKit::Command::Failed if options[:continue] exit_status(1) else diff --git a/lib/leap_cli/ssh/backend.rb b/lib/leap_cli/ssh/backend.rb index 3894d815..599fc9a0 100644 --- a/lib/leap_cli/ssh/backend.rb +++ b/lib/leap_cli/ssh/backend.rb @@ -178,7 +178,7 @@ module LeapCli rescue StandardError => exc if exc.is_a?(SSHKit::Command::Failed) || exc.is_a?(SSHKit::Runner::ExecuteError) if @options[:raise_error] - raise LeapCli::SSH::ExecuteError, exc.to_s + raise exc elsif @options[:fail_msg] @logger.log(@options[:fail_msg], host: @host.hostname, :color => :red) else -- cgit v1.2.3 From 9679c7e1cd5d7b5824fa99b070dc0899779c92ec Mon Sep 17 00:00:00 2001 From: elijah Date: Sat, 19 Aug 2017 13:42:39 -0700 Subject: leap_cli: minor help wording correction --- lib/leap_cli/commands/compile.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/leap_cli') diff --git a/lib/leap_cli/commands/compile.rb b/lib/leap_cli/commands/compile.rb index 92c879d7..16dff3df 100644 --- a/lib/leap_cli/commands/compile.rb +++ b/lib/leap_cli/commands/compile.rb @@ -155,7 +155,7 @@ module LeapCli 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`." + bail! "You must have at least one public SSH user key configured in order to proceed. See `leap help user add`." end if file_exists?(path(:monitor_pub_key)) keys << path(:monitor_pub_key) -- cgit v1.2.3 From 437f28b2cbfedfc7d119dcf4e228c5626bb8a152 Mon Sep 17 00:00:00 2001 From: elijah Date: Sun, 27 Aug 2017 23:51:14 -0700 Subject: bugfix: fix `leap test init` --- lib/leap_cli/commands/cert.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'lib/leap_cli') diff --git a/lib/leap_cli/commands/cert.rb b/lib/leap_cli/commands/cert.rb index 81f45eb5..68fa9444 100644 --- a/lib/leap_cli/commands/cert.rb +++ b/lib/leap_cli/commands/cert.rb @@ -102,13 +102,13 @@ module LeapCli; module Commands def generate_test_client_cert(prefix=nil) require 'leap_cli/x509' 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.serial_number.number = X509.cert_serial_number(provider.domain) + cert.subject.common_name = [prefix, X509.random_common_name(provider.domain)].join cert.not_before = X509.yesterday cert.not_after = X509.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 + cert.parent = X509.client_ca_root + cert.sign! X509.client_test_signing_profile yield cert.key_material.private_key.to_pem, cert.to_pem end -- cgit v1.2.3