diff options
author | elijah <elijah@riseup.net> | 2016-10-05 14:35:56 -0700 |
---|---|---|
committer | elijah <elijah@riseup.net> | 2016-10-05 14:35:56 -0700 |
commit | 7abfbd6abae14fa6a72350f7b75268ff561354ee (patch) | |
tree | af5c969c905a8d2a95f2b2aa7c4dd6f4b8763126 | |
parent | cc57bc6c0ff99d88f3bfeff1b04297e9b91e6988 (diff) | |
parent | f95e08ef7d8defbde4a19e138b1ac4ebc9677669 (diff) |
Merge branch 'develop'
# Conflicts:
# lib/leap_cli/version.rb
113 files changed, 2369 insertions, 3268 deletions
@@ -1,11 +1,6 @@ Gemfile.lock pkg junk -test/provider/hiera -test/provider/files/nodes/ -test/provider/files/ca/ -test/provider/files/ssh/ -test/provider/files/users/ .vagrant Vagrantfile .reviewboardrc diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..5aca7d3 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,38 @@ +image: leapcode/ruby:2.1-slim + +stages: + - build + - test + - trigger + +build: + stage: build + script: + - "rake build" + - "gem install --user-install pkg/leap_cli-*.gem" + - export PATH="$PATH:$(ruby -e 'puts Gem.user_dir')/bin" + - leap + artifacts: + paths: + - pkg/leap_cli-*.gem + name: "leap_cli_${CI_BUILD_REF_NAME}_${CI_BUILD_REF}" + expire_in: 3 month + +test: + stage: test + script: +# - apt-get install --yes pkg-config +# - bundle config build.nokogiri --use-system-libraries + - apt-get install rake + - bundle install --path vendor/bundle --with test + - git clone https://leap.se/git/leap_platform.git -b develop + - chmod -R a+rwX test/provider + - useradd -ms /bin/bash testuser + - su -c "PLATFORM_DIR=$(readlink -e leap_platform) bundle exec rake test" testuser + +# trigger leap_platform pipeline +trigger: + stage: trigger + type: deploy + script: + - "curl -s -X POST -F token=${PLATFORM_TRIGGER_TOKEN} -F ref=develop https://0xacab.org/api/v3/projects/129/trigger/builds" @@ -1,14 +1,14 @@ About LEAP command line interface =================================================== -This gem installs an executable 'leap' that allows you to manage servers using the LEAP platform. You can read about the [platform on-line](https://leap.se). +This gem installs an executable 'leap' that allows you to manage servers using the LEAP platform. You can read about the [platform on-line](https://leap.se/docs). Installation =================================================== Install prerequisites: - sudo apt-get install git ruby ruby-dev rsync openssh-client openssl rake + sudo apt-get install git ruby ruby-dev rsync openssh-client openssl rake gcc make zlib1g-dev NOTE: leap_cli requires ruby 1.9 or later. @@ -16,31 +16,18 @@ Optionally install Vagrant in order to be able to test with local virtual machin sudo apt-get install vagrant virtualbox zlib1g-dev -NOTE: the packaged virtualbox and vagrant that comes with Debian and Ubuntu are rather ancient. Most people have better luck by downloading these packages from the upstream: +Install the `leap` command system-wide: -* https://downloads.vagrantup.com/ -* https://www.virtualbox.org/wiki/Downloads + sudo gem install leap_cli -Install the `leap` command: +Alternately, you can install just for your user: - sudo apt-get install rake - git clone https://leap.se/git/leap_cli.git - cd leap_cli - rake build - -Install as root user (recommended): - - sudo rake install - -Install as unprivileged user: - - rake install - # watch out for the directory leap is installed to, then i.e. - sudo ln -s ~/.gem/ruby/1.9.1/bin/leap /usr/local/bin/leap + gem install --user-install leap_cli + [ $(which ruby) ] && PATH="$PATH:$(ruby -e 'puts Gem.user_dir')/bin" -With both methods, you can use now /usr/local/bin/leap, which in most cases will be in your $PATH. +The `--user-install` option for `gem` will install gems to a location in your home directory (handy!) but this directory is not in your PATH (not handy!). Add the second line to your `.bashrc` file so that all your shells will have `leap` in PATH. -To run directly from a clone of the git repo, see "Development", below. +For other methods of installing `leap_cli`, see below. Usage =================================================== @@ -56,35 +43,42 @@ How to set up your environment for developing the ``leap`` command. Prerequisites --------------------------------------------------- -Debian Squeeze - - sudo apt-get install git ruby ruby-dev rubygems - sudo gem install bundler rake - export PATH=$PATH:/var/lib/gems/1.8/bin +Debian & Ubuntu -Debian Wheezy + sudo apt-get install git ruby ruby-dev rake bundler - sudo apt-get install git ruby ruby-dev bundler +Install from git +--------------------------------------------------- -Ubuntu +Download the source: - sudo apt-get install git ruby ruby-dev - sudo gem install bundler + cd leap_cli -Install from git +Installing from the source --------------------------------------------------- -Download the source: +Build the gem: - git clone https://github.com/leapcode/leap_cli.git + git clone https://leap.se/git/leap_cli.git cd leap_cli + rake build + +Install as root user: + + sudo rake install + +Alternately, install as unprivileged user: + + rake install + PATH="$PATH:$(ruby -e 'puts Gem.user_dir')/bin" -Running from the source directory +Running directly from the source directory --------------------------------------------------- To run the ``leap`` command directly from the source tree, you need to install the required gems using ``bundle`` and symlink ``bin/leap`` into your path: + git clone https://leap.se/git/leap_cli.git cd leap_cli bundle # install required gems ln -s `pwd`/bin/leap ~/bin # link executable somewhere in your bin path @@ -99,32 +93,3 @@ working directory is under leap_cli. Because the point is to be able to run ``le other places, it is easier to create the symlink. If you run ``leap`` directly, and not via the command launcher that rubygems installs, leap will run in a mode that simulates ``bundle exec leap`` (i.e. only gems included in Gemfile are allowed to be loaded). - -Changes -==================================================== - -1.7 - -* requires platform 0.7 -* deployment logging (see /var/log/leap) -* compatible with new tapicero -* selectively destroy some dbs with `leap db destroy` -* faster apt-get update -* added `leap scp` command -* bug fixes - -1.6.2 - -* auto generate certs on compile -* use internal ruby md5sum for compatibility on mac -* may override or customize tests by putting tests in `files/tests` -* bug fixes - -1.6.1 - -* requires platform 0.6 -* better `leap test run` -* added `leap tunnel` command -* only print stack trace if `--debug` flag was specified -* prompt user to upgrade host ssh key if a better one exists -* bug fixes @@ -1,4 +1,3 @@ -require "rubygems" require "pty" require "fileutils" require "rake/testtask" @@ -37,7 +36,7 @@ end desc "Build #{$spec.name}-#{$spec.version}.gem into the pkg directory" task 'build' do FileUtils.mkdir_p(File.join($base_dir, 'pkg')) - FileUtils.rm($gem_path) if File.exists?($gem_path) + FileUtils.rm($gem_path) if File.exist?($gem_path) run "gem build -V '#{$spec_path}'" file_name = File.basename(built_gem_path) FileUtils.mv(built_gem_path, 'pkg') @@ -46,7 +45,7 @@ end desc "Install #{$spec.name}-#{$spec.version}.gem into either system-wide or user gems" task 'install' do - if !File.exists?($gem_path) + if !File.exist?($gem_path) puts("Could not file #{$gem_path}. Try running 'rake build'") else options = '--verbose --conservative --no-rdoc --no-ri' @@ -83,6 +82,7 @@ end Rake::TestTask.new do |t| t.pattern = "test/unit/*_test.rb" + t.warning = false end task :default => :test @@ -14,6 +14,7 @@ else $VERBOSE=nil DEBUG=false end +TEST = false LEAP_CLI_BASE_DIR = File.expand_path('..', File.dirname(File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__)) @@ -27,8 +28,6 @@ rescue LoadError end require 'gli' -require 'highline' -require 'forwardable' require 'leap_cli/lib_ext/gli' # our custom extensions to gli # @@ -40,16 +39,6 @@ require 'leap_cli/lib_ext/gli' # our custom extensions to gli # module LeapCli::Commands extend GLI::App - extend Forwardable - - # delegate highline methods to make them available to sub-commands - @terminal = HighLine.new - def_delegator :@terminal, :ask, 'self.ask' - def_delegator :@terminal, :agree, 'self.agree' - def_delegator :@terminal, :choose, 'self.choose' - def_delegator :@terminal, :say, 'self.say' - def_delegator :@terminal, :color, 'self.color' - def_delegator :@terminal, :list, 'self.list' # make config manager available as 'manager' def self.manager @@ -90,6 +79,9 @@ module LeapCli::Commands # run command begin + if ARGV.any? + LeapCli.log_raw(:log, nil, "COMMAND") { 'leap ' + ARGV.join(' ') } + end exit_status = run(ARGV) exit(LeapCli::Util.exit_status || exit_status) rescue StandardError => exc @@ -102,6 +94,8 @@ module LeapCli::Commands end if DEBUG raise exc + else + exit(1) end end end diff --git a/leap_cli.gemspec b/leap_cli.gemspec index beeb0a4..8a38893 100644 --- a/leap_cli.gemspec +++ b/leap_cli.gemspec @@ -44,38 +44,25 @@ spec = Gem::Specification.new do |s| # test s.add_development_dependency('minitest', '~> 5.0') - - #s.add_development_dependency('rdoc') - #s.add_development_dependency('aruba') + s.add_development_dependency('rake', '~> 11.0') # console gems s.add_runtime_dependency('gli','~> 2.12', '>= 2.12.0') # note: gli version is also pinned in leap_cli.rb. - s.add_runtime_dependency('command_line_reporter', '~> 3.3') - s.add_runtime_dependency('highline', '~> 1.6') - s.add_runtime_dependency('paint', '~> 0.9') # network gems - s.add_runtime_dependency('net-ssh', '~> 2.7') - # ^^ we can upgrade once we get off broken capistrano - # https://github.com/net-ssh/net-ssh/issues/145 - s.add_runtime_dependency('capistrano', '~> 2.15') + s.add_runtime_dependency('sshkit', '~> 1.11') + s.add_runtime_dependency('fog-aws', '~> 0.11') # crypto gems - #s.add_runtime_dependency('certificate_authority', '>= 0.2.0') - # ^^ currently vendored # s.add_runtime_dependency('gpgme') # << does not build on debian jessie, so now optional. # also, there is a ruby-gpgme package anyway. + # acme-client is vendored for now, we need pre-lease version + # s.add_runtime_dependency('acme-client', '~> 0.4.2') + s.add_runtime_dependency('faraday', '~> 0.9', '>= 0.9.1') # for acme-client + # misc gems s.add_runtime_dependency('ya2yaml', '~> 0.31') # pure ruby yaml, so we can better control output. see https://github.com/afunai/ya2yaml s.add_runtime_dependency('json_pure', '~> 1.8') # pure ruby json, so we can better control output. - s.add_runtime_dependency('base32', '~> 0.3') # base32 encoding - - ## - ## DEPENDENCIES for VENDORED GEMS - ## - - # certificate_authority - s.add_runtime_dependency("activemodel", '~> 3.0', ">= 3.0.6") end diff --git a/lib/leap/platform.rb b/lib/leap/platform.rb deleted file mode 100644 index 9112ef3..0000000 --- a/lib/leap/platform.rb +++ /dev/null @@ -1,90 +0,0 @@ -module Leap - - class Platform - class << self - # - # configuration - # - - attr_reader :version - attr_reader :compatible_cli - attr_accessor :facts - attr_accessor :paths - attr_accessor :node_files - attr_accessor :monitor_username - attr_accessor :reserved_usernames - - attr_accessor :hiera_dir - attr_accessor :hiera_path - attr_accessor :files_dir - attr_accessor :leap_dir - attr_accessor :init_path - - attr_accessor :default_puppet_tags - - def define(&block) - # some defaults: - @reserved_usernames = [] - @hiera_dir = '/etc/leap' - @hiera_path = '/etc/leap/hiera.yaml' - @leap_dir = '/srv/leap' - @files_dir = '/srv/leap/files' - @init_path = '/srv/leap/initialized' - @default_puppet_tags = [] - - self.instance_eval(&block) - - @version ||= Gem::Version.new("0.0") - end - - def version=(version) - @version = Gem::Version.new(version) - end - - def compatible_cli=(range) - @compatible_cli = range - @minimum_cli_version = Gem::Version.new(range.first) - @maximum_cli_version = Gem::Version.new(range.last) - end - - # - # return true if the cli_version is compatible with this platform. - # - def compatible_with_cli?(cli_version) - cli_version = Gem::Version.new(cli_version) - cli_version >= @minimum_cli_version && cli_version <= @maximum_cli_version - end - - # - # return true if the platform version is within the specified range. - # - def version_in_range?(range) - if range.is_a? String - range = range.split('..') - end - minimum_platform_version = Gem::Version.new(range.first) - maximum_platform_version = Gem::Version.new(range.last) - @version >= minimum_platform_version && @version <= maximum_platform_version - end - - def major_version - if @version.segments.first == 0 - @version.segments[0..1].join('.') - else - @version.segments.first - end - end - - def method_missing(method, *args) - puts - puts "WARNING:" - puts " leap_cli is out of date and does not understand `#{method}`." - puts " called from: #{caller.first}" - puts " please upgrade to a newer leap_cli" - end - - end - - end - -end
\ No newline at end of file diff --git a/lib/leap_cli.rb b/lib/leap_cli.rb index 0563327..c0e139e 100644 --- a/lib/leap_cli.rb +++ b/lib/leap_cli.rb @@ -1,6 +1,6 @@ module LeapCli - module Commands; end # for commands in leap_cli/commands - module Macro; end # for macros in leap_platform/provider_base/lib/macros + module Commands; end # for commands in leap_platform/lib/leap_cli/commands + module Macro; end # for macros in leap_platform/lib/leap_cli/macros end $ruby_version = RUBY_VERSION.split('.').collect{ |i| i.to_i }.extend(Comparable) @@ -11,11 +11,8 @@ $:.unshift(File.expand_path('../leap_cli/override',__FILE__)) # for a few gems, things will break if using earlier versions. # enforce the compatible versions here: require 'rubygems' -gem 'net-ssh', '~> 2.7' gem 'gli', '~> 2.12', '>= 2.12.0' -require 'leap/platform' - require 'leap_cli/version' require 'leap_cli/exceptions' @@ -32,30 +29,13 @@ require 'leap_cli/core_ext/yaml' require 'leap_cli/log' require 'leap_cli/path' require 'leap_cli/util' -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' -require 'leap_cli/config/node' -require 'leap_cli/config/tag' -require 'leap_cli/config/provider' -require 'leap_cli/config/secrets' -require 'leap_cli/config/object_list' -require 'leap_cli/config/filter' -require 'leap_cli/config/environment' -require 'leap_cli/config/manager' - require 'leap_cli/markdown_document_listener' # # allow everyone easy access to log() command. # module LeapCli - Util.send(:extend, LeapCli::Log) - Config::Manager.send(:include, LeapCli::Log) - extend LeapCli::Log + extend LeapCli::LogCommand end diff --git a/lib/leap_cli/bootstrap.rb b/lib/leap_cli/bootstrap.rb index b7bc8e9..75edf5b 100644 --- a/lib/leap_cli/bootstrap.rb +++ b/lib/leap_cli/bootstrap.rb @@ -5,8 +5,8 @@ module LeapCli module Bootstrap - extend LeapCli::Log extend self + extend LeapCli::LogCommand # # the argument leapfile_path is only used for tests @@ -36,9 +36,11 @@ module LeapCli # called from leap executable. # def load_libraries(app) - if LeapCli.log_level >= 2 + if LeapCli.logger.log_level >= 2 log_version end + add_platform_lib_to_path + load_platform_libraries load_commands(app) load_macros end @@ -72,14 +74,14 @@ module LeapCli options = parse_logging_options(argv) verbose = (options[:verbose] || 1).to_i if verbose - LeapCli.set_log_level(verbose) + LeapCli.logger.log_level = verbose end if options[:log] - LeapCli.log_file = options[:log] - LeapCli::Util.log_raw(:log) { $0 + ' ' + argv.join(' ')} + LeapCli.logger.log_file = options[:log] + LeapCli.logger.log_raw(:log) { $0 + ' ' + argv.join(' ')} end unless options[:color].nil? - LeapCli.log_in_color = options[:color] + LeapCli.logger.log_in_color = options[:color] end end @@ -97,17 +99,16 @@ module LeapCli if !Path.platform || !File.directory?(Path.platform) bail! { log :missing, "platform directory '#{Path.platform}'" } end - if LeapCli.log_file.nil? && LeapCli.leapfile.log - LeapCli.log_file = LeapCli.leapfile.log + if LeapCli.logger.log_file.nil? && LeapCli.leapfile.log + LeapCli.logger.log_file = LeapCli.leapfile.log end elsif !leapfile_optional?(argv) puts puts " =" - log :note, "There is no `Leapfile` in this directory, or any parent directory.\n"+ - " = "+ + 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 @@ -158,7 +159,9 @@ module LeapCli # Yes, hacky. # def leapfile_optional?(argv) - if argv.include?('--version') + if TEST + return true + elsif argv.include?('--version') return true else without_flags = argv.select {|i| i !~ /^-/} @@ -193,5 +196,26 @@ module LeapCli end end + # + # makes all the ruby libraries in the leap_platform/lib directory + # available for inclusion. + # + def add_platform_lib_to_path + if Path.platform + path = File.join(Path.platform, 'lib') + $LOAD_PATH.unshift(path) unless $LOAD_PATH.include?(path) + end + end + + # + # loads libraries that live in the platform and should + # always be available. + # + def load_platform_libraries + if Path.platform + require 'leap_cli/load_libraries' + end + end + end end diff --git a/lib/leap_cli/commands/common.rb b/lib/leap_cli/commands/common.rb index 7bf49db..3dab2a0 100644 --- a/lib/leap_cli/commands/common.rb +++ b/lib/leap_cli/commands/common.rb @@ -1,61 +1,103 @@ -# -# Some common helpers available to all LeapCli::Commands -# -# This also includes utility methods, and makes all instance -# methods available as class methods. -# +require 'readline' -module LeapCli - module Commands +module LeapCli; module Commands - extend self - extend LeapCli::Log - extend LeapCli::Util - extend LeapCli::Util::RemoteCommand + extend LeapCli::LogCommand + extend LeapCli::Util - protected - - def path(name) - Path.named_path(name) - end + 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/ + # + # 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.nil? || index.empty? + next + elsif index =~ /q/ + bail! + else + i = index.to_i - 1 + if i < 0 || i >= items.length bail! else - i = index.to_i - 1 - if i < 0 || i >= items.length - bail! - else - return i - end + 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 + + def say(statement) + if ends_in_whitespace?(statement) + $stdout.print(statement) + $stdout.flush + else + $stdout.puts(statement) + end + end + + def ask(question, options={}) + default = options[:default] + if default + if ends_in_whitespace?(question) + question = question + "|" + default + "| " + else + question = question + "|" + default + "|" + end + end + response = Readline.readline(question, true) # set to false if ever reading passwords. + if response + response = response.strip + if response.empty? + return default + else + return response + end + else + return default + 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) + def agree(question, options={}) + while true + response = ask(question, options) + if response.nil? + say('Please enter "yes" or "no".') + elsif ["y","yes", "ye"].include?(response.downcase) + return true + elsif ["n", "no"].include?(response.downcase) + return false else - bail! "argument error" + say('Please enter "yes" or "no".') end end + end + private + + # true if str ends in whitespace before a color escape code. + def ends_in_whitespace?(str) + /[ \t](\e\[\d+(;\d+)*m)?\Z/ =~ str end -end
\ No newline at end of file + +end; end diff --git a/lib/leap_cli/commands/new.rb b/lib/leap_cli/commands/new.rb index 838b80e..6b60e7d 100644 --- a/lib/leap_cli/commands/new.rb +++ b/lib/leap_cli/commands/new.rb @@ -4,7 +4,6 @@ module LeapCli; module Commands desc 'Creates a new provider instance in the specified directory, creating it if necessary.' arg_name 'DIRECTORY' - #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,19 +11,7 @@ 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'} - options[:name] ||= ask_string("The name of the provider: ") {|q| q.default = 'Example'} - options[:platform] ||= ask_string("File path of the leap_platform directory: ") {|q| q.default = File.expand_path('../leap_platform', directory)} - options[:platform] = "./" + options[:platform] unless options[:platform] =~ /^\// - options[:contacts] ||= ask_string("Default email address contacts: ") {|q| q.default = 'root@' + options[:domain]} - options[:platform] = relative_path(options[:platform]) - create_initial_provider_files(directory, global, options) + new_provider_action(global, options, args) end end @@ -32,13 +19,33 @@ module LeapCli; module Commands DEFAULT_REPO = 'https://leap.se/git/leap_platform.git' + def new_provider_action(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: ", + default: 'example.org') + options[:name] ||= ask_string("The name of the provider: ", + default: 'Example') + options[:platform] ||= ask_string("File path of the leap_platform directory: ", + default: File.expand_path('../leap_platform', directory)) + options[:platform] = "./" + options[:platform] unless options[:platform] =~ /^\// + options[:contacts] ||= ask_string("Default email address contacts: ", + default: 'root@' + options[:domain]) + options[:platform] = relative_path(options[:platform]) + create_initial_provider_files(directory, global, options) + end + # # don't let the user specify any of the following: y, yes, n, no # they must actually input a real string # - def ask_string(str, &block) + def ask_string(str, options={}) while true - value = ask(str, &block) + value = ask(str, options) if value =~ /^(y|yes|n|no)$/i say "`#{value}` is not a valid value. Try again" else @@ -54,7 +61,7 @@ module LeapCli; module Commands unless directory && directory.any? help! "Directory name is required." end - unless File.exists?(directory) + unless File.exist?(directory) if global[:yes] || agree("Create directory #{directory}? ") ensure_dir directory else diff --git a/lib/leap_cli/commands/pre.rb b/lib/leap_cli/commands/pre.rb index f4bf7bb..0b7e98b 100644 --- a/lib/leap_cli/commands/pre.rb +++ b/lib/leap_cli/commands/pre.rb @@ -1,9 +1,11 @@ - # # check to make sure we can find the root directory of the platform # module LeapCli; module Commands + extend self # this is a trick to make all instance methods + # available as class methods. + desc 'Verbosity level 0..5' arg_name 'LEVEL' default_value '1' diff --git a/lib/leap_cli/config/environment.rb b/lib/leap_cli/config/environment.rb deleted file mode 100644 index df4b56c..0000000 --- a/lib/leap_cli/config/environment.rb +++ /dev/null @@ -1,180 +0,0 @@ -# -# All configurations files can be isolated into separate environments. -# -# Each config json in each environment inherits from the default environment, -# which in term inherits from the "_base_" environment: -# -# _base_ -- base provider in leap_platform -# '- default -- environment in provider dir when no env is set -# '- production -- example environment -# - -module LeapCli; module Config - - class Environment - # the String name of the environment - attr_accessor :name - - # the shared Manager object - attr_accessor :manager - - # hashes of {name => Config::Object} - attr_accessor :services, :tags, :partials - - # a Config::Provider - attr_accessor :provider - - # a Config::Object - attr_accessor :common - - # shared, non-inheritable - def nodes; @@nodes; end - def secrets; @@secrets; end - - def initialize(manager, name, search_dir, parent, options={}) - @@nodes ||= nil - @@secrets ||= nil - - @manager = manager - @name = name - - load_provider_files(search_dir, options) - - if parent - @services.inherit_from! parent.services, self - @tags.inherit_from! parent.tags , self - @partials.inherit_from! parent.partials, self - @common.inherit_from! parent.common - @provider.inherit_from! parent.provider - end - - if @provider - @provider.set_env(name) - @provider.validate! - end - end - - def load_provider_files(search_dir, options) - # - # load empty environment if search_dir doesn't exist - # - if search_dir.nil? || !Dir.exist?(search_dir) - @services = Config::ObjectList.new - @tags = Config::ObjectList.new - @partials = Config::ObjectList.new - @provider = Config::Provider.new - @common = Config::Object.new - return - end - - # - # inheritable - # - if options[:scope] - scope = options[:scope] - @services = load_all_json(Path.named_path([:service_env_config, '*', scope], search_dir), Config::Tag, options) - @tags = load_all_json(Path.named_path([:tag_env_config, '*', scope], search_dir), Config::Tag, options) - @partials = load_all_json(Path.named_path([:service_env_config, '_*', scope], search_dir), Config::Tag, options) - @provider = load_json( Path.named_path([:provider_env_config, scope], search_dir), Config::Provider, options) - @common = load_json( Path.named_path([:common_env_config, scope], search_dir), Config::Object, options) - else - @services = load_all_json(Path.named_path([:service_config, '*'], search_dir), Config::Tag, options) - @tags = load_all_json(Path.named_path([:tag_config, '*'], search_dir), Config::Tag, options) - @partials = load_all_json(Path.named_path([:service_config, '_*'], search_dir), Config::Tag, options) - @provider = load_json( Path.named_path(:provider_config, search_dir), Config::Provider, options) - @common = load_json( Path.named_path(:common_config, search_dir), Config::Object, options) - end - - # remove 'name' from partials, since partials get merged with nodes - @partials.values.each {|partial| partial.delete('name'); } - - # - # shared: currently non-inheritable - # load the first ones we find, and only those. - # - if @@nodes.nil? || @@nodes.empty? - @@nodes = load_all_json(Path.named_path([:node_config, '*'], search_dir), Config::Node, options) - end - if @@secrets.nil? || @@secrets.empty? - @@secrets = load_json(Path.named_path(:secrets_config, search_dir), Config::Secrets, options) - end - end - - # - # Loads a json template file as a Hash (used only when creating a new node .json - # file for the first time). - # - def template(template) - path = Path.named_path([:template_config, template], Path.provider_base) - if File.exists?(path) - return load_json(path, Config::Object) - else - return nil - end - end - - private - - def load_all_json(pattern, object_class, options={}) - results = Config::ObjectList.new - Dir.glob(pattern).each do |filename| - next if options[:no_dots] && File.basename(filename) !~ /^[^\.]*\.json$/ - obj = load_json(filename, object_class) - if obj - name = File.basename(filename).force_encoding('utf-8').sub(/^([^\.]+).*\.json$/,'\1') - obj['name'] ||= name - if options[:env] - obj.environment = options[:env] - end - results[name] = obj - end - end - results - end - - def load_json(filename, object_class, options={}) - if !File.exists?(filename) - return object_class.new(self) - end - - Util::log :loading, filename, 3 - - # - # Read a JSON file, strip out comments. - # - # UTF8 is the default encoding for JSON, but others are allowed: - # https://www.ietf.org/rfc/rfc4627.txt - # - buffer = StringIO.new - File.open(filename, "rb", :encoding => 'UTF-8') do |f| - while (line = f.gets) - next if line =~ /^\s*\/\// - buffer << line - end - end - - # - # force UTF-8 - # - if $ruby_version >= [1,9] - string = buffer.string.force_encoding('utf-8') - else - string = Iconv.conv("UTF-8//IGNORE", "UTF-8", buffer.string) - end - - # parse json - begin - hash = JSON.parse(string, :object_class => Hash, :array_class => Array) || {} - rescue SyntaxError, JSON::ParserError => exc - Util::log 0, :error, 'in file "%s":' % filename - Util::log 0, exc.to_s, :indent => 1 - return nil - end - object = object_class.new(self) - object.deep_merge!(hash) - return object - end - - end # end Environment - -end; end
\ No newline at end of file diff --git a/lib/leap_cli/config/filter.rb b/lib/leap_cli/config/filter.rb deleted file mode 100644 index 2c80be8..0000000 --- a/lib/leap_cli/config/filter.rb +++ /dev/null @@ -1,178 +0,0 @@ -# -# Many leap_cli commands accept a list of filters to select a subset of nodes for the command to -# be applied to. This class is a helper for manager to run these filters. -# -# Classes other than Manager should not use this class. -# -# Filter rules: -# -# * A filter consists of a list of tokens -# * A token may be a service name, tag name, environment name, or node name. -# * Each token may be optionally prefixed with a plus sign. -# * Multiple tokens with a plus are treated as an OR condition, -# but treated as an AND condition with the plus sign. -# -# For example -# -# * openvpn +development => all nodes with service 'openvpn' AND environment 'development' -# * openvpn seattle => all nodes with service 'openvpn' OR tag 'seattle'. -# -# There can only be one environment specified. Typically, there are also tags -# for each environment name. These name are treated as environments, not tags. -# -module LeapCli - module Config - class Filter - - # - # filter -- array of strings, each one a filter - # options -- hash, possible keys include - # :nopin -- disregard environment pinning - # :local -- if false, disallow local nodes - # - # A nil value in the filters array indicates - # the default environment. This is in order to support - # calls like `manager.filter(environments)` - # - def initialize(filters, options, manager) - @filters = filters.nil? ? [] : filters.dup - @environments = [] - @options = options - @manager = manager - - # split filters by pulling out items that happen - # to be environment names. - if LeapCli.leapfile.environment.nil? || @options[:nopin] - @environments = [] - else - @environments = [LeapCli.leapfile.environment] - end - @filters.select! do |filter| - if filter.nil? - @environments << nil unless @environments.include?(nil) - false - else - filter_text = filter.sub(/^\+/,'') - if is_environment?(filter_text) - if filter_text == LeapCli.leapfile.environment - # silently ignore already pinned environments - elsif (filter =~ /^\+/ || @filters.first == filter) && !@environments.empty? - LeapCli::Util.bail! do - LeapCli::Util.log "Environments are exclusive: no node is in two environments." do - LeapCli::Util.log "Tried to filter on '#{@environments.join('\' AND \'')}' AND '#{filter_text}'" - end - end - else - @environments << filter_text - end - false - else - true - end - end - end - - # don't let the first filter have a + prefix - if @filters[0] =~ /^\+/ - @filters[0] = @filters[0][1..-1] - end - end - - # actually run the filter, returns a filtered list of nodes - def nodes() - if @filters.empty? - return nodes_for_empty_filter - else - return nodes_for_filter - end - end - - private - - def nodes_for_empty_filter - node_list = @manager.nodes - if @environments.any? - node_list = node_list[ @environments.collect{|e|[:environment, env_to_filter(e)]} ] - end - if @options[:local] === false - node_list = node_list[:environment => '!local'] - end - if @options[:disabled] === false - node_list = node_list[:environment => '!disabled'] - end - node_list - end - - def nodes_for_filter - node_list = Config::ObjectList.new - @filters.each do |filter| - if filter =~ /^\+/ - keep_list = nodes_for_name(filter[1..-1]) - node_list.delete_if do |name, node| - if keep_list[name] - false - else - true - end - end - else - node_list.merge!(nodes_for_name(filter)) - end - end - node_list - end - - private - - # - # returns a set of nodes corresponding to a single name, - # where name could be a node name, service name, or tag name. - # - # For services and tags, we only include nodes for the - # environments that are active - # - def nodes_for_name(name) - if node = @manager.nodes[name] - return Config::ObjectList.new(node) - elsif @environments.empty? - if @manager.services[name] - return @manager.env('_all_').services[name].node_list - elsif @manager.tags[name] - return @manager.env('_all_').tags[name].node_list - else - LeapCli::Util.log :warning, "filter '#{name}' does not match any node names, tags, services, or environments." - return Config::ObjectList.new - end - else - node_list = Config::ObjectList.new - if @manager.services[name] - @environments.each do |env| - node_list.merge!(@manager.env(env).services[name].node_list) - end - elsif @manager.tags[name] - @environments.each do |env| - node_list.merge!(@manager.env(env).tags[name].node_list) - end - else - LeapCli::Util.log :warning, "filter '#{name}' does not match any node names, tags, services, or environments." - end - return node_list - end - end - - # - # when pinning, we use the name 'default' to specify nodes - # without an environment set, but when filtering, we need to filter - # on :environment => nil. - # - def env_to_filter(environment) - environment == 'default' ? nil : environment - end - - def is_environment?(text) - text == 'default' || @manager.environment_names.include?(text) - end - - end - end -end diff --git a/lib/leap_cli/config/manager.rb b/lib/leap_cli/config/manager.rb deleted file mode 100644 index ecc59f3..0000000 --- a/lib/leap_cli/config/manager.rb +++ /dev/null @@ -1,419 +0,0 @@ -# encoding: utf-8 - -require 'json/pure' - -if $ruby_version < [1,9] - require 'iconv' -end - -module LeapCli - module Config - - # - # A class to manage all the objects in all the configuration files. - # - class Manager - - def initialize - @environments = {} # hash of `Environment` objects, keyed by name. - Config::Object.send(:include, LeapCli::Macro) - end - - ## - ## ATTRIBUTES - ## - - # - # returns the Hash of the contents of facts.json - # - def facts - @facts ||= begin - content = Util.read_file(:facts) - if !content || content.empty? - content = "{}" - end - JSON.parse(content) - rescue SyntaxError, JSON::ParserError => exc - Util::bail! "Could not parse facts.json -- #{exc}" - end - end - - # - # returns an Array of all the environments defined for this provider. - # the returned array includes nil (for the default environment) - # - def environment_names - @environment_names ||= begin - [nil] + (env.tags.field('environment') + env.nodes.field('environment')).compact.uniq - end - end - - # - # Returns the appropriate environment variable - # - def env(env=nil) - @environments[env || 'default'] - end - - # - # The default accessors - # - # For these defaults, use 'default' environment, or whatever - # environment is pinned. - # - # I think it might be an error that these are ever used - # and I would like to get rid of them. - # - def services; env(default_environment).services; end - def tags; env(default_environment).tags; end - def partials; env(default_environment).partials; end - def provider; env(default_environment).provider; end - def common; env(default_environment).common; end - def secrets; env(default_environment).secrets; end - def nodes; env(default_environment).nodes; end - def template(*args) - self.env.template(*args) - end - - def default_environment - LeapCli.leapfile.environment - end - - ## - ## IMPORT EXPORT - ## - - def add_environment(args) - if args[:inherit] - parent = @environments[args.delete(:inherit)] - else - parent = nil - end - @environments[args[:name]] = Environment.new( - self, - args.delete(:name), - args.delete(:dir), - parent, - args - ) - end - - # - # load .json configuration files - # - def load(options = {}) - @provider_dir = Path.provider - - # load base - add_environment(name: '_base_', dir: Path.provider_base) - - # load provider - Util::assert_files_exist!(Path.named_path(:provider_config, @provider_dir)) - add_environment(name: 'default', dir: @provider_dir, - inherit: '_base_', no_dots: true) - - # create a special '_all_' environment, used for tracking - # the union of all the environments - add_environment(name: '_all_', inherit: 'default') - - # load environments - environment_names.each do |ename| - if ename - log 3, :loading, '%s environment...' % ename - add_environment(name: ename, dir: @provider_dir, - inherit: 'default', scope: ename) - end - end - - # apply inheritance - env.nodes.each do |name, node| - Util::assert! name =~ /^[0-9a-z-]+$/, "Illegal character(s) used in node name '#{name}'" - env.nodes[name] = apply_inheritance(node) - end - - # do some node-list post-processing - cleanup_node_lists(options) - - # apply control files - env.nodes.each do |name, node| - control_files(node).each do |file| - begin - node.eval_file file - rescue ConfigError => exc - if options[:continue_on_error] - exc.log - else - raise exc - end - end - end - end - end - - # - # save compiled hiera .yaml files - # - # if a node_list is specified, only update those .yaml files. - # otherwise, update all files, destroying files that are no longer used. - # - def export_nodes(node_list=nil) - updated_hiera = [] - updated_files = [] - existing_hiera = nil - existing_files = nil - - unless node_list - node_list = env.nodes - existing_hiera = Dir.glob(Path.named_path([:hiera, '*'], @provider_dir)) - existing_files = Dir.glob(Path.named_path([:node_files_dir, '*'], @provider_dir)) - end - - node_list.each_node do |node| - filepath = Path.named_path([:node_files_dir, node.name], @provider_dir) - hierapath = Path.named_path([:hiera, node.name], @provider_dir) - Util::write_file!(hierapath, node.dump_yaml) - updated_files << filepath - updated_hiera << hierapath - end - - if @disabled_nodes - # make disabled nodes appear as if they are still active - @disabled_nodes.each_node do |node| - updated_files << Path.named_path([:node_files_dir, node.name], @provider_dir) - updated_hiera << Path.named_path([:hiera, node.name], @provider_dir) - end - end - - # remove files that are no longer needed - if existing_hiera - (existing_hiera - updated_hiera).each do |filepath| - Util::remove_file!(filepath) - end - end - if existing_files - (existing_files - updated_files).each do |filepath| - Util::remove_directory!(filepath) - end - end - end - - def export_secrets(clean_unused_secrets = false) - if env.secrets.any? - Util.write_file!([:secrets_config, @provider_dir], env.secrets.dump_json(clean_unused_secrets) + "\n") - end - end - - ## - ## FILTERING - ## - - # - # returns a node list consisting only of nodes that satisfy the filter criteria. - # - # filter: condition [condition] [condition] [+condition] - # condition: [node_name | service_name | tag_name | environment_name] - # - # if conditions is prefixed with +, then it works like an AND. Otherwise, it works like an OR. - # - # args: - # filter -- array of filter terms, one per item - # - # options: - # :local -- if :local is false and the filter is empty, then local nodes are excluded. - # :nopin -- if true, ignore environment pinning - # - def filter(filters=nil, options={}) - Filter.new(filters, options, self).nodes() - end - - # - # same as filter(), but exits if there is no matching nodes - # - def filter!(filters, options={}) - node_list = filter(filters, options) - Util::assert! node_list.any?, "Could not match any nodes from '#{filters.join ' '}'" - return node_list - end - - # - # returns a single Config::Object that corresponds to a Node. - # - def node(name) - if name =~ /\./ - # probably got a fqdn, since periods are not allowed in node names. - # so, take the part before the first period as the node name - name = name.split('.').first - end - env.nodes[name] - end - - # - # returns a single node that is disabled - # - def disabled_node(name) - @disabled_nodes[name] - end - - # - # yields each node, in sorted order - # - def each_node(&block) - env.nodes.each_node &block - end - - def reload_node!(node) - env.nodes[node.name] = apply_inheritance!(node) - end - - ## - ## CONNECTIONS - ## - - class ConnectionList < Array - def add(data={}) - self << { - "from" => data[:from], - "to" => data[:to], - "port" => data[:port] - } - end - end - - def connections - @connections ||= ConnectionList.new - end - - ## - ## PRIVATE - ## - - private - - # - # makes a node inherit options from appropriate the common, service, and tag json files. - # - def apply_inheritance(node, throw_exceptions=false) - new_node = Config::Node.new(nil) - name = node.name - node_env = guess_node_env(node) - new_node.set_environment(node_env, new_node) - - # inherit from common - new_node.deep_merge!(node_env.common) - - # inherit from services - if node['services'] - node['services'].to_a.each do |node_service| - service = node_env.services[node_service] - if service.nil? - msg = 'in node "%s": the service "%s" does not exist.' % [node['name'], node_service] - log 0, :error, msg - raise LeapCli::ConfigError.new(node, "error " + msg) if throw_exceptions - else - new_node.deep_merge!(service) - end - end - end - - # inherit from tags - if node.vagrant? - node['tags'] = (node['tags'] || []).to_a + ['local'] - end - if node['tags'] - node['tags'].to_a.each do |node_tag| - tag = node_env.tags[node_tag] - if tag.nil? - msg = 'in node "%s": the tag "%s" does not exist.' % [node['name'], node_tag] - log 0, :error, msg - raise LeapCli::ConfigError.new(node, "error " + msg) if throw_exceptions - else - new_node.deep_merge!(tag) - end - end - end - - # inherit from node - new_node.deep_merge!(node) - return new_node - end - - def apply_inheritance!(node) - apply_inheritance(node, true) - end - - # - # Guess the environment of the node from the tag names. - # - # Technically, this is wrong: a tag that sets the environment might not be - # named the same as the environment. This code assumes that it is. - # - # Unfortunately, it is a chicken and egg problem. We need to know the nodes - # likely environment in order to apply the inheritance that will actually - # determine the node's properties. - # - def guess_node_env(node) - environment = self.env(default_environment) - if node['tags'] - node['tags'].to_a.each do |tag| - if self.environment_names.include?(tag) - environment = self.env(tag) - end - end - end - return environment - end - - # - # does some final clean at the end of loading nodes. - # this includes removing disabled nodes, and populating - # the services[x].node_list and tags[x].node_list - # - def cleanup_node_lists(options) - @disabled_nodes = Config::ObjectList.new - env.nodes.each do |name, node| - if node.enabled || options[:include_disabled] - if node['services'] - node['services'].to_a.each do |node_service| - env(node.environment).services[node_service].node_list.add(node.name, node) - env('_all_').services[node_service].node_list.add(node.name, node) - end - end - if node['tags'] - node['tags'].to_a.each do |node_tag| - env(node.environment).tags[node_tag].node_list.add(node.name, node) - env('_all_').tags[node_tag].node_list.add(node.name, node) - end - end - elsif !options[:include_disabled] - log 2, :skipping, "disabled node #{name}." - env.nodes.delete(name) - @disabled_nodes[name] = node - end - end - end - - # - # returns a list of 'control' files for this node. - # a control file is like a service or a tag JSON file, but it contains - # raw ruby code that gets evaluated in the context of the node. - # Yes, this entirely breaks our functional programming model - # for JSON generation. - # - def control_files(node) - files = [] - [Path.provider_base, @provider_dir].each do |provider_dir| - [['services', :service_config], ['tags', :tag_config]].each do |attribute, path_sym| - node[attribute].each do |attr_value| - path = Path.named_path([path_sym, "#{attr_value}.rb"], provider_dir).sub(/\.json$/,'') - if File.exists?(path) - files << path - end - end - end - end - return files - end - - end - end -end diff --git a/lib/leap_cli/config/node.rb b/lib/leap_cli/config/node.rb deleted file mode 100644 index 65735d5..0000000 --- a/lib/leap_cli/config/node.rb +++ /dev/null @@ -1,77 +0,0 @@ -# -# Configuration for a 'node' (a server in the provider's infrastructure) -# - -require 'ipaddr' - -module LeapCli; module Config - - class Node < Object - attr_accessor :file_paths - - def initialize(environment=nil) - super(environment) - @node = self - @file_paths = [] - end - - # - # returns true if this node has an ip address in the range of the vagrant network - # - def vagrant? - begin - vagrant_range = IPAddr.new LeapCli.leapfile.vagrant_network - rescue ArgumentError => exc - Util::bail! { Util::log :invalid, "ip address '#{@node.ip_address}' vagrant.network" } - end - - begin - ip_address = IPAddr.new @node.get('ip_address') - rescue ArgumentError => exc - Util::log :warning, "invalid ip address '#{@node.get('ip_address')}' for node '#{@node.name}'" - end - return vagrant_range.include?(ip_address) - end - - # - # Return a hash table representation of ourselves, with the key equal to the @node.name, - # and the value equal to the fields specified in *keys. - # - # Also, the result is flattened to a single hash, so a key of 'a.b' becomes 'a_b' - # - # compare to Object#pick(*keys). This method is the sames as Config::ObjectList#pick_fields, - # but works on a single node. - # - # Example: - # - # node.pick('domain.internal') => - # - # { - # 'node1': { - # 'domain_internal': 'node1.example.i' - # } - # } - # - def pick_fields(*keys) - {@node.name => self.pick(*keys)} - end - - # - # can be overridden by the platform. - # returns a list of node names that should be tested before this node - # - def test_dependencies - [] - end - - # returns a string list of supported ssh host key algorithms for this node. - # or an empty string if it could not be determined - def supported_ssh_host_key_algorithms - @host_key_algo ||= SshKey.supported_host_key_algorithms( - Util.read_file([:node_ssh_pub_key, @node.name]) - ) - end - - end - -end; end diff --git a/lib/leap_cli/config/object.rb b/lib/leap_cli/config/object.rb deleted file mode 100644 index b117c2f..0000000 --- a/lib/leap_cli/config/object.rb +++ /dev/null @@ -1,428 +0,0 @@ -# encoding: utf-8 - -require 'erb' -require 'json/pure' # pure ruby implementation is required for our sorted trick to work. - -if $ruby_version < [1,9] - $KCODE = 'UTF8' -end -require 'ya2yaml' # pure ruby yaml - -module LeapCli - module Config - - # - # This class represents the configuration for a single node, service, or tag. - # Also, all the nested hashes are also of this type. - # - # It is called 'object' because it corresponds to an Object in JSON. - # - class Object < Hash - - attr_reader :env - attr_reader :node - - def initialize(environment=nil, node=nil) - raise ArgumentError unless environment.nil? || environment.is_a?(Config::Environment) - @env = environment - # an object that is a node as @node equal to self, otherwise all the - # child objects point back to the top level node. - @node = node || self - end - - def manager - @env.manager - end - - # - # TODO: deprecate node.global() - # - def global - @env - end - - def environment=(e) - self.store('environment', e) - end - - def environment - self['environment'] - end - - def duplicate(env) - new_object = self.deep_dup - new_object.set_environment(env, new_object) - end - - # - # export YAML - # - # We use pure ruby yaml exporter ya2yaml instead of SYCK or PSYCH because it - # allows us greater compatibility regardless of installed ruby version and - # greater control over how the yaml is exported (sorted keys, in particular). - # - def dump_yaml - evaluate(@node) - sorted_ya2yaml(:syck_compatible => true) - end - - # - # export JSON - # - def dump_json(options={}) - evaluate(@node) - if options[:format] == :compact - return self.to_json - else - excluded = {} - if options[:exclude] - options[:exclude].each do |key| - excluded[key] = self[key] - self.delete(key) - end - end - json_str = JSON.sorted_generate(self) - if excluded.any? - self.merge!(excluded) - end - return json_str - end - end - - def evaluate(context=@node) - evaluate_everything(context) - late_evaluate_everything(context) - end - - ## - ## FETCHING VALUES - ## - - def [](key) - get(key) - end - - # Overrride some default methods in Hash that are likely to - # be used as attributes. - alias_method :hkey, :key - def key; get('key'); end - - # - # make hash addressable like an object (e.g. obj['name'] available as obj.name) - # - def method_missing(method, *args, &block) - get!(method) - end - - def get(key) - begin - get!(key) - rescue NoMethodError - nil - end - end - - # override behavior of #default() from Hash - def default - get!('default') - end - - # - # Like a normal Hash#[], except: - # - # (1) lazily eval dynamic values when we encounter them. (i.e. strings that start with "= ") - # - # (2) support for nested references in a single string (e.g. ['a.b'] is the same as ['a']['b']) - # the dot path is always absolute, starting at the top-most object. - # - def get!(key) - key = key.to_s - if self.has_key?(key) - fetch_value(key) - elsif key =~ /\./ - # for keys with with '.' in them, we start from the root object (@node). - keys = key.split('.') - value = self.get!(keys.first) - if value.is_a? Config::Object - value.get!(keys[1..-1].join('.')) - else - value - end - else - raise NoMethodError.new(key, "No method '#{key}' for #{self.class}") - end - end - - ## - ## COPYING - ## - - # - # A deep (recursive) merge with another Config::Object. - # - # If prefer_self is set to true, the value from self will be picked when there is a conflict - # that cannot be merged. - # - # Merging rules: - # - # - If a value is a hash, we recursively merge it. - # - If the value is simple, like a string, the new one overwrites the value. - # - If the value is an array: - # - If both old and new values are arrays, the new one replaces the old. - # - If one of the values is simple but the other is an array, the simple is added to the array. - # - def deep_merge!(object, prefer_self=false) - object.each do |key,new_value| - if self.has_key?('+'+key) - mode = :add - old_value = self.fetch '+'+key, nil - self.delete('+'+key) - elsif self.has_key?('-'+key) - mode = :subtract - old_value = self.fetch '-'+key, nil - self.delete('-'+key) - elsif self.has_key?('!'+key) - mode = :replace - old_value = self.fetch '!'+key, nil - self.delete('!'+key) - else - mode = :normal - old_value = self.fetch key, nil - end - - # clean up boolean - new_value = true if new_value == "true" - new_value = false if new_value == "false" - old_value = true if old_value == "true" - old_value = false if old_value == "false" - - # force replace? - if mode == :replace && prefer_self - value = old_value - - # merge hashes - elsif old_value.is_a?(Hash) || new_value.is_a?(Hash) - value = Config::Object.new(@env, @node) - old_value.is_a?(Hash) ? value.deep_merge!(old_value) : (value[key] = old_value if !old_value.nil?) - new_value.is_a?(Hash) ? value.deep_merge!(new_value, prefer_self) : (value[key] = new_value if !new_value.nil?) - - # merge nil - elsif new_value.nil? - value = old_value - elsif old_value.nil? - value = new_value - - # merge arrays when one value is not an array - elsif old_value.is_a?(Array) && !new_value.is_a?(Array) - (value = (old_value.dup << new_value).compact.uniq).delete('REQUIRED') - elsif new_value.is_a?(Array) && !old_value.is_a?(Array) - (value = (new_value.dup << old_value).compact.uniq).delete('REQUIRED') - - # merge two arrays - elsif old_value.is_a?(Array) && new_value.is_a?(Array) - if mode == :add - value = (old_value + new_value).sort.uniq - elsif mode == :subtract - value = new_value - old_value - elsif prefer_self - value = old_value - else - value = new_value - end - - # catch errors - elsif type_mismatch?(old_value, new_value) - raise 'Type mismatch. Cannot merge %s (%s) with %s (%s). Key is "%s", name is "%s".' % [ - old_value.inspect, old_value.class, - new_value.inspect, new_value.class, - key, self.class - ] - - # merge simple strings & numbers - else - if prefer_self - value = old_value - else - value = new_value - end - end - - # save value - self[key] = value - end - self - end - - def set_environment(env, node) - @env = env - @node = node - self.each do |key, value| - if value.is_a?(Config::Object) - value.set_environment(env, node) - end - end - end - - # - # like a reverse deep merge - # (self takes precedence) - # - def inherit_from!(object) - self.deep_merge!(object, true) - end - - # - # Make a copy of ourselves, except only including the specified keys. - # - # Also, the result is flattened to a single hash, so a key of 'a.b' becomes 'a_b' - # - def pick(*keys) - keys.map(&:to_s).inject(self.class.new(@manager)) do |hsh, key| - value = self.get(key) - if !value.nil? - hsh[key.gsub('.','_')] = value - end - hsh - end - end - - def eval_file(filename) - evaluate_ruby(filename, File.read(filename)) - end - - protected - - # - # walks the object tree, eval'ing all the attributes that are dynamic ruby (e.g. value starts with '= ') - # - def evaluate_everything(context) - keys.each do |key| - obj = fetch_value(key, context) - if is_required_value_not_set?(obj) - Util::log 0, :warning, "required property \"#{key}\" is not set in node \"#{node.name}\"." - elsif obj.is_a? Config::Object - obj.evaluate_everything(context) - end - end - end - - # - # some keys need to be evaluated 'late', after all the other keys have been evaluated. - # - def late_evaluate_everything(context) - if @late_eval_list - @late_eval_list.each do |key, value| - self[key] = context.evaluate_ruby(key, value) - if is_required_value_not_set?(self[key]) - Util::log 0, :warning, "required property \"#{key}\" is not set in node \"#{node.name}\"." - end - end - end - values.each do |obj| - if obj.is_a? Config::Object - obj.late_evaluate_everything(context) - end - end - end - - # - # evaluates the string `value` as ruby in the context of self. - # (`key` is just passed for debugging purposes) - # - def evaluate_ruby(key, value) - self.instance_eval(value, key, 1) - rescue ConfigError => exc - raise exc # pass through - rescue SystemStackError => exc - Util::log 0, :error, "while evaluating node '#{self.name}'" - Util::log 0, "offending key: #{key}", :indent => 1 - Util::log 0, "offending string: #{value}", :indent => 1 - Util::log 0, "STACK OVERFLOW, BAILING OUT. There must be an eval loop of death (variables with circular dependencies).", :indent => 1 - raise SystemExit.new(1) - rescue FileMissing => exc - Util::bail! do - if exc.options[:missing] - Util::log :missing, exc.options[:missing].gsub('$node', self.name).gsub('$file', exc.path) - else - Util::log :error, "while evaluating node '#{self.name}'" - Util::log "offending key: #{key}", :indent => 1 - Util::log "offending string: #{value}", :indent => 1 - Util::log "error message: no file '#{exc}'", :indent => 1 - end - raise exc if DEBUG - end - rescue AssertionFailed => exc - Util.bail! do - Util::log :failed, "assertion while evaluating node '#{self.name}'" - Util::log 'assertion: %s' % exc.assertion, :indent => 1 - Util::log "offending key: #{key}", :indent => 1 - raise exc if DEBUG - end - rescue SyntaxError, StandardError => exc - Util::bail! do - Util::log :error, "while evaluating node '#{self.name}'" - Util::log "offending key: #{key}", :indent => 1 - Util::log "offending string: #{value}", :indent => 1 - Util::log "error message: #{exc.inspect}", :indent => 1 - raise exc if DEBUG - end - end - - private - - # - # fetches the value for the key, evaluating the value as ruby if it begins with '=' - # - def fetch_value(key, context=@node) - value = fetch(key, nil) - if value.is_a?(String) && value =~ /^=/ - if value =~ /^=> (.*)$/ - value = evaluate_later(key, $1) - elsif value =~ /^= (.*)$/ - value = context.evaluate_ruby(key, $1) - end - self[key] = value - end - return value - end - - def evaluate_later(key, value) - @late_eval_list ||= [] - @late_eval_list << [key, value] - '<evaluate later>' - end - - # - # when merging, we raise an error if this method returns true for the two values. - # - def type_mismatch?(old_value, new_value) - if old_value.is_a?(Boolean) && new_value.is_a?(Boolean) - # note: FalseClass and TrueClass are different classes - # so we can't do old_value.class == new_value.class - return false - elsif old_value.is_a?(String) && old_value =~ /^=/ - # pass through macros, since we don't know what the type will eventually be. - return false - elsif new_value.is_a?(String) && new_value =~ /^=/ - return false - elsif old_value.class == new_value.class - return false - else - return true - end - end - - # - # returns true if the value has not been changed and the default is "REQUIRED" - # - def is_required_value_not_set?(value) - if value.is_a? Array - value == ["REQUIRED"] - else - value == "REQUIRED" - end - end - - end # class - end # module -end # module
\ No newline at end of file diff --git a/lib/leap_cli/config/object_list.rb b/lib/leap_cli/config/object_list.rb deleted file mode 100644 index f9299a6..0000000 --- a/lib/leap_cli/config/object_list.rb +++ /dev/null @@ -1,209 +0,0 @@ -require 'tsort' - -module LeapCli - module Config - # - # A list of Config::Object instances (internally stored as a hash) - # - class ObjectList < Hash - include TSort - - def initialize(config=nil) - if config - self.add(config['name'], config) - end - end - - # - # If the key is a string, the Config::Object it references is returned. - # - # If the key is a hash, we treat it as a condition and filter all the Config::Objects using the condition. - # A new ObjectList is returned. - # - # Examples: - # - # nodes['vpn1'] - # node named 'vpn1' - # - # nodes[:public_dns => true] - # all nodes with public dns - # - # nodes[:services => 'openvpn', 'location.country_code' => 'US'] - # all nodes with services containing 'openvpn' OR country code of US - # - # Sometimes, you want to do an OR condition with multiple conditions - # for the same field. Since hash keys must be unique, you can use - # an array representation instead: - # - # nodes[[:services, 'openvpn'], [:services, 'tor']] - # nodes with openvpn OR tor service - # - # nodes[:services => 'openvpn'][:tags => 'production'] - # nodes with openvpn AND are production - # - def [](key) - if key.is_a?(Hash) || key.is_a?(Array) - filter(key) - else - super key.to_s - end - end - - def exclude(node) - list = self.dup - list.delete(node.name) - return list - end - - def each_node(&block) - self.keys.sort.each do |node_name| - yield self[node_name] - end - end - - # - # filters this object list, producing a new list. - # filter is an array or a hash. see [] - # - def filter(filter) - results = Config::ObjectList.new - filter.each do |field, match_value| - field = field.is_a?(Symbol) ? field.to_s : field - match_value = match_value.is_a?(Symbol) ? match_value.to_s : match_value - if match_value.is_a?(String) && match_value =~ /^!/ - operator = :not_equal - match_value = match_value.sub(/^!/, '') - else - operator = :equal - end - each do |name, config| - value = config[field] - if value.is_a? Array - if operator == :equal && value.include?(match_value) - results[name] = config - elsif operator == :not_equal && !value.include?(match_value) - results[name] = config - end - else - if operator == :equal && value == match_value - results[name] = config - elsif operator == :not_equal && value != match_value - results[name] = config - end - end - end - end - results - end - - def add(name, object) - self[name] = object - end - - # - # converts the hash of configs into an array of hashes, with ONLY the specified fields - # - def fields(*fields) - result = [] - keys.sort.each do |name| - result << self[name].pick(*fields) - end - result - end - - # - # like fields(), but returns an array of values instead of an array of hashes. - # - def field(field) - field = field.to_s - result = [] - keys.sort.each do |name| - result << self[name].get(field) - end - result - end - - # - # pick_fields(field1, field2, ...) - # - # generates a Hash from the object list, but with only the fields that are picked. - # - # If there are more than one field, then the result is a Hash of Hashes. - # If there is just one field, it is a simple map to the value. - # - # For example: - # - # "neighbors" = "= nodes_like_me[:services => :couchdb].pick_fields('domain.full', 'ip_address')" - # - # generates this: - # - # neighbors: - # couch1: - # domain_full: couch1.bitmask.net - # ip_address: "10.5.5.44" - # couch2: - # domain_full: couch2.bitmask.net - # ip_address: "10.5.5.52" - # - # But this: - # - # "neighbors": "= nodes_like_me[:services => :couchdb].pick_fields('domain.full')" - # - # will generate this: - # - # neighbors: - # couch1: couch1.bitmask.net - # couch2: couch2.bitmask.net - # - def pick_fields(*fields) - self.values.inject({}) do |hsh, node| - value = self[node.name].pick(*fields) - if fields.size == 1 - value = value.values.first - end - hsh[node.name] = value - hsh - end - end - - # - # Applies inherit_from! to all objects. - # - # 'env' specifies what environment should be for - # each object in the list. - # - def inherit_from!(object_list, env) - object_list.each do |name, object| - if self[name] - self[name].inherit_from!(object) - else - self[name] = object.duplicate(env) - end - end - end - - # - # topographical sort based on test dependency - # - def tsort_each_node(&block) - self.each_key(&block) - end - - def tsort_each_child(node_name, &block) - if self[node_name] - self[node_name].test_dependencies.each do |test_me_first| - if self[test_me_first] # TODO: in the future, allow for ability to optionally pull in all dependencies. - # not just the ones that pass the node filter. - yield(test_me_first) - end - end - end - end - - def names_in_test_dependency_order - self.tsort - end - - end - end -end diff --git a/lib/leap_cli/config/provider.rb b/lib/leap_cli/config/provider.rb deleted file mode 100644 index 0d8bc1f..0000000 --- a/lib/leap_cli/config/provider.rb +++ /dev/null @@ -1,22 +0,0 @@ -# -# Configuration class for provider.json -# - -module LeapCli; module Config - class Provider < Object - attr_reader :environment - def set_env(e) - if e == 'default' - @environment = nil - else - @environment = e - end - end - def provider - self - end - def validate! - # nothing here yet :( - end - end -end; end diff --git a/lib/leap_cli/config/secrets.rb b/lib/leap_cli/config/secrets.rb deleted file mode 100644 index ca851c7..0000000 --- a/lib/leap_cli/config/secrets.rb +++ /dev/null @@ -1,87 +0,0 @@ -# encoding: utf-8 -# -# A class for the secrets.json file -# - -module LeapCli; module Config - - class Secrets < Object - attr_reader :node_list - - def initialize(manager=nil) - super(manager) - @discovered_keys = {} - end - - # we can't use fetch() or get(), since those already have special meanings - def retrieve(key, environment) - environment ||= 'default' - self.fetch(environment, {})[key.to_s] - end - - def set(*args, &block) - if block_given? - set_with_block(*args, &block) - else - set_without_block(*args) - end - end - - # searches over all keys matching the regexp, checking to see if the value - # has been already used by any of them. - def taken?(regexp, value, environment) - self.keys.grep(regexp).each do |key| - return true if self.retrieve(key, environment) == value - end - return false - end - - def set_without_block(key, value, environment) - set_with_block(key, environment) {value} - end - - def set_with_block(key, environment, &block) - environment ||= 'default' - key = key.to_s - @discovered_keys[environment] ||= {} - @discovered_keys[environment][key] = true - self[environment] ||= {} - self[environment][key] ||= yield - end - - # - # if clean is true, then only secrets that have been discovered - # during this run will be exported. - # - # if environment is also pinned, then we will clean those secrets - # just for that environment. - # - # the clean argument should only be used when all nodes have - # been processed, otherwise secrets that are actually in use will - # get mistakenly removed. - # - def dump_json(clean=false) - pinned_env = LeapCli.leapfile.environment - if clean - self.each_key do |environment| - if pinned_env.nil? || pinned_env == environment - env = self[environment] - if env.nil? - raise StandardError.new("secrets.json file seems corrupted. No such environment '#{environment}'") - end - env.each_key do |key| - unless @discovered_keys[environment] && @discovered_keys[environment][key] - self[environment].delete(key) - end - end - if self[environment].empty? - self.delete(environment) - end - end - end - end - super() - end - end - -end; end diff --git a/lib/leap_cli/config/sources.rb b/lib/leap_cli/config/sources.rb deleted file mode 100644 index aee860d..0000000 --- a/lib/leap_cli/config/sources.rb +++ /dev/null @@ -1,11 +0,0 @@ -# encoding: utf-8 -# -# A class for the sources.json file -# - -module LeapCli - module Config - class Sources < Object - end - end -end diff --git a/lib/leap_cli/config/tag.rb b/lib/leap_cli/config/tag.rb deleted file mode 100644 index 6bd8d1e..0000000 --- a/lib/leap_cli/config/tag.rb +++ /dev/null @@ -1,25 +0,0 @@ -# -# -# A class for node services or node tags. -# -# - -module LeapCli; module Config - - class Tag < Object - attr_reader :node_list - - def initialize(environment=nil) - super(environment) - @node_list = Config::ObjectList.new - end - - # don't copy the node list pointer when this object is dup'ed. - def initialize_copy(orig) - super - @node_list = Config::ObjectList.new - end - - end - -end; end diff --git a/lib/leap_cli/core_ext/hash.rb b/lib/leap_cli/core_ext/hash.rb index 7df33b2..4eb3af3 100644 --- a/lib/leap_cli/core_ext/hash.rb +++ b/lib/leap_cli/core_ext/hash.rb @@ -32,4 +32,23 @@ class Hash replace(deep_merge(other_hash)) end + # + # A recursive symbolize_keys + # + unless Hash.method_defined?(:symbolize_keys) + def symbolize_keys + self.inject({}) {|result, (key, value)| + new_key = case key + when String then key.to_sym + else key + end + new_value = case value + when Hash then symbolize_keys(value) + else value + end + result[new_key] = new_value + result + } + end + end end diff --git a/lib/leap_cli/leapfile.rb b/lib/leap_cli/leapfile.rb index 9164d0a..e526703 100644 --- a/lib/leap_cli/leapfile.rb +++ b/lib/leap_cli/leapfile.rb @@ -3,6 +3,8 @@ # # It is akin to a Gemfile, Rakefile, or Capfile (e.g. it is a ruby file that gets eval'ed) # +# Additional configuration options are defined in platform's leapfile_extensions.rb +# module LeapCli def self.leapfile @@ -10,17 +12,12 @@ module LeapCli end class Leapfile - attr_accessor :platform_directory_path - attr_accessor :provider_directory_path - attr_accessor :custom_vagrant_vm_line - attr_accessor :leap_version - attr_accessor :log - attr_accessor :vagrant_network - attr_accessor :vagrant_basebox - attr_accessor :environment + attr_reader :platform_directory_path + attr_reader :provider_directory_path + attr_reader :environment + attr_reader :valid def initialize - @vagrant_network = '10.5.5.0/24' end # @@ -61,25 +58,44 @@ module LeapCli # # load the platform # - platform_file = "#{@platform_directory_path}/platform.rb" - unless File.exists?(platform_file) + platform_class = "#{@platform_directory_path}/lib/leap/platform" + platform_definition = "#{@platform_directory_path}/platform.rb" + unless File.exist?(platform_definition) Util.bail! "ERROR: The file `#{platform_file}` does not exist. Please check the value of `@platform_directory_path` in `Leapfile` or `~/.leaprc`." end - require "#{@platform_directory_path}/platform.rb" - if !Leap::Platform.compatible_with_cli?(LeapCli::VERSION) || - !Leap::Platform.version_in_range?(LeapCli::COMPATIBLE_PLATFORM_VERSION) - Util.bail! "This leap command (v#{LeapCli::VERSION}) " + - "is not compatible with the platform #{@platform_directory_path} (v#{Leap::Platform.version}).\n " + - "You need either leap command #{Leap::Platform.compatible_cli.first} to #{Leap::Platform.compatible_cli.last} or " + - "platform version #{LeapCli::COMPATIBLE_PLATFORM_VERSION.first} to #{LeapCli::COMPATIBLE_PLATFORM_VERSION.last}" + begin + require platform_class + require platform_definition + rescue LoadError + Util.log "The leap_platform at #{platform_directory_path} is not compatible with this version of leap_cli" + Util.log "You can either:" do + Util.log "Upgrade leap_platform to version " + LeapCli::COMPATIBLE_PLATFORM_VERSION.first + Util.log "Or, downgrade leap_cli to version 1.8" + end + Util.bail! + rescue StandardError => exc + Util.bail! exc.to_s end - unless @allow_production_deploy.nil? - Util::log 0, :warning, "in Leapfile: @allow_production_deploy is no longer supported." + begin + Leap::Platform.validate!(LeapCli::VERSION, LeapCli::COMPATIBLE_PLATFORM_VERSION, self) + rescue StandardError => exc + Util.bail! exc.to_s end - unless @platform_branch.nil? - Util::log 0, :warning, "in Leapfile: @platform_branch is no longer supported." + leapfile_extensions = "#{@platform_directory_path}/lib/leap_cli/leapfile_extensions.rb" + if File.exist?(leapfile_extensions) + require leapfile_extensions end - @valid = true + + # + # validate + # + instance_variables.each do |var| + var = var.to_s.sub('@', '') + if !self.respond_to?(var) + LeapCli.log :warning, "the variable `#{var}` is set in .leaprc or Leapfile, but it is not supported." + end + end + @valid = validate return @valid end end @@ -105,7 +121,7 @@ module LeapCli def edit_leaprc(property, value=nil) file_path = leaprc_path lines = [] - if File.exists?(file_path) + if File.exist?(file_path) regexp = /self\.#{Regexp.escape(property)} = .*? if @provider_directory_path == '#{Regexp.escape(@provider_directory_path)}'/ File.readlines(file_path).each do |line| unless line =~ regexp @@ -128,10 +144,9 @@ module LeapCli end def read_settings(file) - if File.exists? file - Util::log 2, :read, file + if File.exist? file + LeapCli.log 2, :read, file instance_eval(File.read(file), file) - validate(file) end end @@ -146,11 +161,11 @@ module LeapCli return search_dir end - PRIVATE_IP_RANGES = /(^127\.0\.0\.1)|(^10\.)|(^172\.1[6-9]\.)|(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^192\.168\.)/ - - def validate(file) - Util::assert! vagrant_network =~ PRIVATE_IP_RANGES do - Util::log 0, :error, "in #{file}: vagrant_network is not a local private network" + def method_missing(method, *args) + if method =~ /=$/ + self.instance_variable_set('@' + method.to_s.sub('=',''), args.first) + else + self.instance_variable_get('@' + method.to_s) end end diff --git a/lib/leap_cli/lib_ext/capistrano_connections.rb b/lib/leap_cli/lib_ext/capistrano_connections.rb deleted file mode 100644 index c46455f..0000000 --- a/lib/leap_cli/lib_ext/capistrano_connections.rb +++ /dev/null @@ -1,16 +0,0 @@ -module Capistrano - class Configuration - module Connections - def failed!(server) - @failure_callback.call(server) if @failure_callback - Thread.current[:failed_sessions] << server - end - - def call_on_failure(&block) - @failure_callback = block - end - end - end -end - - diff --git a/lib/leap_cli/log.rb b/lib/leap_cli/log.rb index 6589ad4..af2fae7 100644 --- a/lib/leap_cli/log.rb +++ b/lib/leap_cli/log.rb @@ -1,58 +1,84 @@ -require 'paint' - ## ## LOGGING ## -## Ugh. This class does not work well with multiple threads! -## module LeapCli - extend self + module LogCommand + @@logger = nil - attr_accessor :log_in_color + def log(*args, &block) + logger.log(*args, &block) + end - # logging options - def log_level - @log_level ||= 1 - end - def set_log_level(value) - @log_level = value - end + def log_raw(*args, &block) + logger.log_raw(*args, &block) + end - def indent_level - @indent_level ||= 0 - end - def indent_level=(value) - @indent_level = value - end + # global shared logger + def logger + @@logger ||= LeapCli::LeapLogger.new + end - def log_file - @log_file - end - def log_file=(value) - @log_file = value - if @log_file - if !File.directory?(File.dirname(@log_file)) - Util.bail!('Invalid log file "%s", directory "%s" does not exist' % [@log_file, File.dirname(@log_file)]) - end - @log_output_stream = File.open(@log_file, 'a') + # thread safe logger + def new_logger + logger.dup end - end - def log_output_stream - @log_output_stream - end + # deprecated + def log_level + logger.log_level + end + + # + # These probably should have been part of the logger originally, + # but they are made available here for convenience: + # + + def bail!(*args, &block) + Util.bail!(*args, &block) + end + def assert!(*args, &block) + Util.assert!(*args, &block) + end + + end end module LeapCli - module Log + class LeapLogger # # these are log titles typically associated with files # - FILE_TITLES = [:updated, :created, :removed, :missing, :nochange, :loading] + FILE_TITLES = %w(updated created removed missing nochange loading) + + # TODO: use these + IMPORTANT = 0 + INFO = 1 + DEBUG = 2 + TRACE = 3 + attr_reader :log_output_stream, :log_file + attr_accessor :indent_level, :log_level, :log_in_color + + def initialize() + @log_level = 1 + @indent_level = 0 + @log_file = nil + @log_output_stream = nil + @log_in_color = true + end + + def log_file=(value) + @log_file = value + if @log_file + if !File.directory?(File.dirname(@log_file)) + Util.bail!('Invalid log file "%s", directory "%s" does not exist' % [@log_file, File.dirname(@log_file)]) + end + @log_output_stream = File.open(@log_file, 'a') + end + end # # master logging function. @@ -63,74 +89,95 @@ module LeapCli # * Integer: the log level (0, 1, 2) # * Symbol: the prefix title to colorize. may be one of # [:error, :warning, :info, :updated, :created, :removed, :no_change, :missing] - # * Hash: a hash of options. so far, only :indent is supported. + # * Hash: a hash of options. + # :wrap -- if true, appy intend to each line in message. + # :color -- apply color to message or prefix + # :style -- apply style to message or prefix # - def log(*args) level = args.grep(Integer).first || 1 title = args.grep(Symbol).first message = args.grep(String).first options = args.grep(Hash).first || {} - unless message && LeapCli.log_level >= level + host = options[:host] + if title + title = title.to_s + end + if @log_level < level || (title.nil? && message.nil?) return end - # prefix - clear_prefix = colored_prefix = "" - if title - prefix_options = case title - when :error then ['error', :red, :bold] - 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] - when :removed then ['removed', :red, :bold] - when :nochange then ['no change', :magenta] - when :loading then ['loading', :magenta] - when :missing then ['missing', :yellow, :bold] - when :skipping then ['skipping', :yellow, :bold] - when :run then ['run', :magenta] - when :failed then ['FAILED', :red, :bold] - when :completed then ['completed', :green, :bold] - when :ran then ['ran', :green, :bold] - when :bail then ['bailing out', :red, :bold] - when :invalid then ['invalid', :red, :bold] - else [title.to_s, :cyan, :bold] - end - if options[:host] - clear_prefix = "[%s] %s " % [options[:host], prefix_options[0]] - colored_prefix = "[%s] %s " % [Paint[options[:host], prefix_options[1], prefix_options[2]], prefix_options[0]] + # + # transform absolute path names + # + if title && FILE_TITLES.include?(title) && message =~ /^\// + message = LeapCli::Path.relative_path(message) + end + + # + # apply filters + # LogFilter will not be defined if no platform was loaded. + # + if defined?(LeapCli::LogFilter) + if title + title, filter_flags = LogFilter.apply_title_filters(title) else - clear_prefix = "%s " % prefix_options[0] - colored_prefix = "%s " % Paint[prefix_options[0], prefix_options[1], prefix_options[2]] + message, filter_flags = LogFilter.apply_message_filters(message) + return if message.nil? end - elsif options[:host] - clear_prefix = colored_prefix = "[%s] " % options[:host] + options = options.merge(filter_flags) end - # transform absolute path names - if title && FILE_TITLES.include?(title) && message =~ /^\// - message = LeapCli::Path.relative_path(message) + # + # set line prefix + # + if (host) + host = host.split('.').first end + prefix = prefix_str(host, title) - log_raw(:log, nil) { [clear_prefix, message].join } - if LeapCli.log_in_color - log_raw(:stdout, options[:indent]) { [colored_prefix, message].join } - else - log_raw(:stdout, options[:indent]) { [clear_prefix, message].join } + # + # write to the log file, always + # + log_raw(:log, nil, prefix) { message } + + # + # log to stdout, maybe in color + # + if @log_in_color + if options[:wrap] + message = message.split("\n") + end + if options[:color] + if host + host = colorize(host, options[:color], options[:style]) + elsif title + title = colorize(title, options[:color], options[:style]) + else + message = colorize(message, options[:color], options[:style]) + end + elsif title + title = colorize(title, :cyan, :bold) + end + # new colorized prefix: + prefix = prefix_str(host, title) end + log_raw(:stdout, options[:indent], prefix) { message } - # run block, if given + # + # run block indented, if given + # if block_given? - LeapCli.indent_level += 1 + @indent_level += 1 yield - LeapCli.indent_level -= 1 + @indent_level -= 1 end end + def debug(*args) + self.log(3, *args) + end + # # Add a raw log entry, without any modifications (other than indent). # Content to be logged is yielded by the block. @@ -139,23 +186,26 @@ module LeapCli # if mode == :stdout, output is sent to STDOUT. # if mode == :log, output is sent to log file, if present. # - def log_raw(mode, indent=nil, &block) - # NOTE: print message (using 'print' produces better results than 'puts' when multiple threads are logging) + def log_raw(mode, indent=nil, prefix=nil, &block) + # NOTE: using 'print' produces better results than 'puts' + # when multiple threads are logging) if mode == :log - if LeapCli.log_output_stream + if @log_output_stream messages = [yield].compact.flatten if messages.any? timestamp = Time.now.strftime("%b %d %H:%M:%S") messages.each do |message| - LeapCli.log_output_stream.print("#{timestamp} #{message}\n") + message = message.rstrip + next if message.empty? + @log_output_stream.print("#{timestamp} #{prefix} #{message}\n") end - LeapCli.log_output_stream.flush + @log_output_stream.flush end end elsif mode == :stdout messages = [yield].compact.flatten if messages.any? - indent ||= LeapCli.indent_level + indent ||= @indent_level indent_str = "" indent_str += " " * indent.to_i if indent.to_i > 0 @@ -163,12 +213,70 @@ module LeapCli else indent_str += ' = ' end + indent_str += prefix if prefix messages.each do |message| + message = message.rstrip + next if message.empty? STDOUT.print("#{indent_str}#{message}\n") end end end end + def colorize(str, color, style=nil) + codes = [FG_COLORS[color] || FG_COLORS[:default]] + if style + codes << EFFECTS[style] || EFFECTS[:nothing] + end + if str.is_a?(String) + ["\033[%sm" % codes.join(';'), str, NO_COLOR].join + elsif str.is_a?(Array) + str.map { |s| + ["\033[%sm" % codes.join(';'), s, NO_COLOR].join + } + end + end + + private + + def prefix_str(host, title) + prefix = "" + prefix += "[" + host + "] " if host + prefix += title + " " if title + prefix += " " if !prefix.empty? && prefix !~ / $/ + return prefix + end + + EFFECTS = { + :reset => 0, :nothing => 0, + :bright => 1, :bold => 1, + :underline => 4, + :inverse => 7, :swap => 7, + } + NO_COLOR = "\033[0m" + FG_COLORS = { + :black => 30, + :red => 31, + :green => 32, + :yellow => 33, + :blue => 34, + :magenta => 35, + :cyan => 36, + :white => 37, + :default => 39, + } + BG_COLORS = { + :black => 40, + :red => 41, + :green => 42, + :yellow => 43, + :blue => 44, + :magenta => 45, + :cyan => 46, + :white => 47, + :default => 49, + } + end -end
\ No newline at end of file +end + diff --git a/lib/leap_cli/logger.rb b/lib/leap_cli/logger.rb deleted file mode 100644 index 9e98321..0000000 --- a/lib/leap_cli/logger.rb +++ /dev/null @@ -1,237 +0,0 @@ -# -# A drop in replacement for Capistrano::Logger that integrates better with LEAP CLI. -# - -require 'capistrano/logger' - -# -# from Capistrano::Logger -# ========================= -# -# IMPORTANT = 0 -# INFO = 1 -# DEBUG = 2 -# TRACE = 3 -# MAX_LEVEL = 3 -# COLORS = { -# :none => "0", -# :black => "30", -# :red => "31", -# :green => "32", -# :yellow => "33", -# :blue => "34", -# :magenta => "35", -# :cyan => "36", -# :white => "37" -# } -# STYLES = { -# :bright => 1, -# :dim => 2, -# :underscore => 4, -# :blink => 5, -# :reverse => 7, -# :hidden => 8 -# } -# - -module LeapCli - class Logger < Capistrano::Logger - - def initialize(options={}) - @options = options - @level = options[:level] || 0 - @message_buffer = nil - end - - def log(level, message, line_prefix=nil, options={}) - if message !~ /\n$/ && level <= 2 && line_prefix.is_a?(String) - # in some cases, when the message doesn't end with a return, we buffer it and - # wait until we encounter the return before we log the message out. - @message_buffer ||= "" - @message_buffer += message - return - elsif @message_buffer - message = @message_buffer + message - @message_buffer = nil - end - - options[:level] ||= level - [:stdout, :log].each do |mode| - LeapCli::log_raw(mode) do - message_lines(mode, message, line_prefix, options) - end - end - end - - private - - def message_lines(mode, message, line_prefix, options) - formatted_message, formatted_prefix, message_options = apply_formatting(mode, message, line_prefix, options) - if message_options[:level] <= self.level && formatted_message && formatted_message.chars.any? - if formatted_prefix - formatted_message.lines.collect { |line| - "[#{formatted_prefix}] #{line.sub(/\s+$/, '')}" - } - else - formatted_message.lines.collect {|line| line.sub(/\s+$/, '')} - end - else - nil - end - end - - ## - ## FORMATTING - ## - - # - # options for formatters: - # - # :match => regexp for matching a log line - # :color => what color the line should be - # :style => what style the line should be - # :priority => what order the formatters are applied in. higher numbers first. - # :match_level => only apply filter at the specified log level - # :level => make this line visible at this log level or higher - # :replace => replace the matched text - # :exit => force the exit code to be this (does not interrupt program, just - # ensures a specific exit code when the program eventually exits) - # - @formatters = [ - # TRACE - { :match => /command finished/, :color => :white, :style => :dim, :match_level => 3, :priority => -10 }, - { :match => /executing locally/, :color => :yellow, :match_level => 3, :priority => -20 }, - - # DEBUG - #{ :match => /executing .*/, :color => :green, :match_level => 2, :priority => -10, :timestamp => true }, - #{ :match => /.*/, :color => :yellow, :match_level => 2, :priority => -30 }, - { :match => /^transaction:/, :level => 3 }, - - # INFO - { :match => /.*out\] (fatal:|ERROR:).*/, :color => :red, :match_level => 1, :priority => -10 }, - { :match => /Permission denied/, :color => :red, :match_level => 1, :priority => -20 }, - { :match => /sh: .+: command not found/, :color => :magenta, :match_level => 1, :priority => -30 }, - - # IMPORTANT - { :match => /^(E|e)rr ::/, :color => :red, :match_level => 0, :priority => -10, :exit => 1}, - { :match => /^ERROR:/, :color => :red, :priority => -10, :exit => 1}, - { :match => /.*/, :color => :blue, :match_level => 0, :priority => -20 }, - - # CLEANUP - { :match => /\s+$/, :replace => '', :priority => 0}, - - # DEBIAN PACKAGES - { :match => /^(Hit|Ign) /, :color => :green, :priority => -20}, - { :match => /^Err /, :color => :red, :priority => -20}, - { :match => /^W(ARNING)?: /, :color => :yellow, :priority => -20}, - { :match => /^E: /, :color => :red, :priority => -20}, - { :match => /already the newest version/, :color => :green, :priority => -20}, - { :match => /WARNING: The following packages cannot be authenticated!/, :color => :red, :level => 0, :priority => -10}, - - # PUPPET - { :match => /^(W|w)arning: Not collecting exported resources without storeconfigs/, :level => 2, :color => :yellow, :priority => -10}, - { :match => /^(W|w)arning: Found multiple default providers for vcsrepo:/, :level => 2, :color => :yellow, :priority => -10}, - { :match => /^(W|w)arning: .*is deprecated.*$/, :level => 2, :color => :yellow, :priority => -10}, - { :match => /^(W|w)arning: Scope.*$/, :level => 2, :color => :yellow, :priority => -10}, - { :match => /^(N|n)otice:/, :level => 1, :color => :cyan, :priority => -20}, - { :match => /^(N|n)otice:.*executed successfully$/, :level => 2, :color => :cyan, :priority => -15}, - { :match => /^(W|w)arning:/, :level => 0, :color => :yellow, :priority => -20}, - { :match => /^Duplicate declaration:/, :level => 0, :color => :red, :priority => -20}, - { :match => /Finished catalog run/, :level => 0, :color => :green, :priority => -10}, - { :match => /^APPLY COMPLETE \(changes made\)/, :level => 0, :color => :green, :priority => -10}, - { :match => /^APPLY COMPLETE \(no changes\)/, :level => 0, :color => :green, :priority => -10}, - - # PUPPET FATAL ERRORS - { :match => /^(E|e)rr(or|):/, :level => 0, :color => :red, :priority => -1, :exit => 1}, - { :match => /^Wrapped exception:/, :level => 0, :color => :red, :priority => -1, :exit => 1}, - { :match => /^Failed to parse template/, :level => 0, :color => :red, :priority => -1, :exit => 1}, - { :match => /^Execution of.*returned/, :level => 0, :color => :red, :priority => -1, :exit => 1}, - { :match => /^Parameter matches failed:/, :level => 0, :color => :red, :priority => -1, :exit => 1}, - { :match => /^Syntax error/, :level => 0, :color => :red, :priority => -1, :exit => 1}, - { :match => /^Cannot reassign variable/, :level => 0, :color => :red, :priority => -1, :exit => 1}, - { :match => /^Could not find template/, :level => 0, :color => :red, :priority => -1, :exit => 1}, - { :match => /^APPLY COMPLETE.*fail/, :level => 0, :color => :red, :priority => -1, :exit => 1}, - - # TESTS - { :match => /^PASS: /, :color => :green, :priority => -20}, - { :match => /^(FAIL|ERROR): /, :color => :red, :priority => -20}, - { :match => /^(SKIP|WARN): /, :color => :yellow, :priority => -20}, - { :match => /\d+ tests: \d+ passes, \d+ skips, 0 warnings, 0 failures, 0 errors/, :color => :blue, :priority => -20}, - - # LOG SUPPRESSION - { :match => /^(W|w)arning: You cannot collect without storeconfigs being set/, :level => 2, :priority => 10}, - { :match => /^(W|w)arning: You cannot collect exported resources without storeconfigs being set/, :level => 2, :priority => 10} - ] - - def self.sorted_formatters - # Sort matchers in reverse order so we can break if we found a match. - @sorted_formatters ||= @formatters.sort_by { |i| -(i[:priority] || i[:prio] || 0) } - end - - @prefix_formatters = [ - { :match => /(err|out) :: /, :replace => '', :priority => 0}, - { :match => /\s+$/, :replace => '', :priority => 0} - ] - def self.prefix_formatters; @prefix_formatters; end - - def apply_formatting(mode, message, line_prefix = nil, options={}) - message = message.dup - options = options.dup - if !line_prefix.nil? - if !line_prefix.is_a?(String) - line_prefix = line_prefix.to_s.dup - else - line_prefix = line_prefix.dup - end - end - color = options[:color] || :none - style = options[:style] - - if line_prefix - self.class.prefix_formatters.each do |formatter| - if line_prefix =~ formatter[:match] && formatter[:replace] - line_prefix.gsub!(formatter[:match], formatter[:replace]) - end - end - end - - self.class.sorted_formatters.each do |formatter| - if (formatter[:match_level] == level || formatter[:match_level].nil?) - if message =~ formatter[:match] - options[:level] = formatter[:level] if formatter[:level] - color = formatter[:color] if formatter[:color] - style = formatter[:style] || formatter[:attribute] # (support original cap colors) - - message.gsub!(formatter[:match], formatter[:replace]) if formatter[:replace] - message.replace(formatter[:prepend] + message) unless formatter[:prepend].nil? - message.replace(message + formatter[:append]) unless formatter[:append].nil? - message.replace(Time.now.strftime('%Y-%m-%d %T') + ' ' + message) if formatter[:timestamp] - - if formatter[:exit] - LeapCli::Util.exit_status(formatter[:exit]) - end - - # stop formatting, unless formatter was just for string replacement - break unless formatter[:replace] - end - end - end - - if color == :hide - return nil - elsif mode == :log || (color == :none && style.nil?) || !LeapCli.log_in_color - return [message, line_prefix, options] - else - term_color = COLORS[color] - term_style = STYLES[style] - if line_prefix.nil? - message.replace format(message, term_color, term_style) - else - line_prefix.replace format(line_prefix, term_color, term_style).strip # format() appends a \n - end - return [message, line_prefix, options] - end - end - - end -end diff --git a/lib/leap_cli/path.rb b/lib/leap_cli/path.rb index fd2e3fc..a78dbd2 100644 --- a/lib/leap_cli/path.rb +++ b/lib/leap_cli/path.rb @@ -3,7 +3,7 @@ require 'fileutils' module LeapCli; module Path def self.platform - @platform + @platform ||= nil end def self.provider_base @@ -40,14 +40,14 @@ module LeapCli; module Path [Path.provider, Path.provider_base].each do |base| if arg.is_a?(Symbol) || arg.is_a?(Array) named_path(arg, base).tap {|path| - return path if File.exists?(path) + return path if File.exist?(path) } else File.join(base, arg).tap {|path| - return path if File.exists?(path) + return path if File.exist?(path) } File.join(base, 'files', arg).tap {|path| - return path if File.exists?(path) + return path if File.exist?(path) } end end @@ -83,7 +83,7 @@ module LeapCli; module Path end def self.exists?(name, provider_dir=nil) - File.exists?(named_path(name, provider_dir)) + File.exist?(named_path(name, provider_dir)) end def self.defined?(name) diff --git a/lib/leap_cli/remote/leap_plugin.rb b/lib/leap_cli/remote/leap_plugin.rb deleted file mode 100644 index b48f433..0000000 --- a/lib/leap_cli/remote/leap_plugin.rb +++ /dev/null @@ -1,192 +0,0 @@ -# -# these methods are made available in capistrano tasks as 'leap.method_name' -# (see RemoteCommand::new_capistrano) -# - -module LeapCli; module Remote; module LeapPlugin - - def required_packages - "puppet rsync lsb-release locales" - end - - def log(*args, &block) - LeapCli::Util::log(*args, &block) - end - - # - # creates directories that are owned by root and 700 permissions - # - def mkdirs(*dirs) - raise ArgumentError.new('illegal dir name') if dirs.grep(/[\' ]/).any? - run dirs.collect{|dir| "mkdir -m 700 -p #{dir}; "}.join - end - - # - # echos "ok" if the node has been initialized and the required packages are installed, bails out otherwise. - # - def assert_initialized - begin - test_initialized_file = "test -f #{Leap::Platform.init_path}" - check_required_packages = "! dpkg-query -W --showformat='${Status}\n' #{required_packages} 2>&1 | grep -q -E '(deinstall|no packages)'" - run "#{test_initialized_file} && #{check_required_packages} && echo ok" - rescue Capistrano::CommandError => exc - LeapCli::Util.bail! do - exc.hosts.each do |host| - node = host.to_s.split('.').first - LeapCli::Util.log :error, "running deploy: node not initialized. Run 'leap node init #{node}'", :host => host - end - end - end - end - - # - # bails out the deploy if the file /etc/leap/no-deploy exists. - # This kind of sucks, because it would be better to skip over nodes that have no-deploy set instead - # halting the entire deploy. As far as I know, with capistrano, there is no way to close one of the - # ssh connections in the pool and make sure it gets no further commands. - # - def check_for_no_deploy - begin - run "test ! -f /etc/leap/no-deploy" - rescue Capistrano::CommandError => exc - LeapCli::Util.bail! do - exc.hosts.each do |host| - LeapCli::Util.log "Can't continue because file /etc/leap/no-deploy exists", :host => host - end - end - end - end - - # - # dumps debugging information - # # - def debug - run "#{Leap::Platform.leap_dir}/bin/debug.sh" - end - - # - # dumps the recent deploy history to the console - # - def history(lines) - command = "(test -s /var/log/leap/deploy-summary.log && tail -n #{lines} /var/log/leap/deploy-summary.log) || (test -s /var/log/leap/deploy-summary.log.1 && tail -n #{lines} /var/log/leap/deploy-summary.log.1) || (echo 'no history')" - run command - end - - # - # This is a hairy ugly hack, exactly the kind of stuff that makes ruby - # dangerous and too much fun for its own good. - # - # In most places, we run remote ssh without a current 'task'. This works fine, - # except that in a few places, the behavior of capistrano ssh is controlled by - # the options of the current task. - # - # We don't want to create an actual current task, because tasks are no fun - # and can't take arguments or return values. So, when we need to configure - # things that can only be configured in a task, we use this handy hack to - # fake the current task. - # - # This is NOT thread safe, but could be made to be so with some extra work. - # - def with_task(name) - task = @config.tasks[name] - @config.class.send(:alias_method, :original_current_task, :current_task) - @config.class.send(:define_method, :current_task, Proc.new(){ task }) - begin - yield - ensure - @config.class.send(:remove_method, :current_task) - @config.class.send(:alias_method, :current_task, :original_current_task) - end - end - - # - # similar to run(cmd, &block), but with: - # - # * exit codes - # * stdout and stderr are combined - # - def stream(cmd, &block) - command = '%s 2>&1; echo "exitcode=$?"' % cmd - run(command) do |channel, stream, data| - exitcode = nil - if data =~ /exitcode=(\d+)\n/ - exitcode = $1.to_i - data.sub!(/exitcode=(\d+)\n/,'') - end - yield({:host => channel[:host], :data => data, :exitcode => exitcode}) - end - end - - # - # like stream, but capture all the output before returning - # - def capture(cmd, &block) - command = '%s 2>&1; echo "exitcode=$?" 2>&1;' % cmd - host_data = {} - run(command) do |channel, stream, data| - host_data[channel[:host]] ||= "" - if data =~ /exitcode=(\d+)\n/ - exitcode = $1.to_i - data.sub!(/exitcode=(\d+)\n/,'') - host_data[channel[:host]] += data - yield({:host => channel[:host], :data => host_data[channel[:host]], :exitcode => exitcode}) - else - host_data[channel[:host]] += data - end - end - end - - # - # Run a command, with a nice status report and progress indicator. - # Only successful results are returned, errors are printed. - # - # For each successful run on each host, block is yielded with a hash like so: - # - # {:host => 'bluejay', :exitcode => 0, :data => 'shell output'} - # - def run_with_progress(cmd, &block) - ssh_failures = [] - exitcode_failures = [] - succeeded = [] - task = LeapCli.log_level > 1 ? :standard_task : :skip_errors_task - with_task(task) do - log :querying, 'facts' do - progress " " - call_on_failure do |host| - ssh_failures << host - progress 'F' - end - capture(cmd) do |response| - if response[:exitcode] == 0 - progress '.' - yield response - else - exitcode_failures << response - progress 'F' - end - end - end - end - puts "done" - if ssh_failures.any? - log :failed, 'to connect to nodes: ' + ssh_failures.join(' ') - end - if exitcode_failures.any? - log :failed, 'to run successfully:' do - exitcode_failures.each do |response| - log "[%s] exit %s - %s" % [response[:host], response[:exitcode], response[:data].strip] - end - end - end - rescue Capistrano::RemoteError => err - log :error, err.to_s - end - - private - - def progress(str='.') - print str - STDOUT.flush - end - -end; end; end diff --git a/lib/leap_cli/remote/puppet_plugin.rb b/lib/leap_cli/remote/puppet_plugin.rb deleted file mode 100644 index 5a6e908..0000000 --- a/lib/leap_cli/remote/puppet_plugin.rb +++ /dev/null @@ -1,26 +0,0 @@ -# -# these methods are made available in capistrano tasks as 'puppet.method_name' -# (see RemoteCommand::new_capistrano) -# - -module LeapCli; module Remote; module PuppetPlugin - - def apply(options) - run "#{Leap::Platform.leap_dir}/bin/puppet_command set_hostname apply #{flagize(options)}" - end - - private - - def flagize(hsh) - hsh.inject([]) {|str, item| - if item[1] === false - str - elsif item[1] === true - str << "--" + item[0].to_s - else - str << "--" + item[0].to_s + " " + item[1].inspect - end - }.join(' ') - end - -end; end; end diff --git a/lib/leap_cli/remote/rsync_plugin.rb b/lib/leap_cli/remote/rsync_plugin.rb deleted file mode 100644 index a6708f4..0000000 --- a/lib/leap_cli/remote/rsync_plugin.rb +++ /dev/null @@ -1,35 +0,0 @@ -# -# these methods are made available in capistrano tasks as 'rsync.method_name' -# (see RemoteCommand::new_capistrano) -# - -autoload :RsyncCommand, 'rsync_command' - -module LeapCli; module Remote; module RsyncPlugin - - # - # takes a block, yielded a server, that should return a hash with various rsync options. - # supported options include: - # - # {:source => '', :dest => '', :flags => '', :includes => [], :excludes => []} - # - def update - rsync = RsyncCommand.new(:logger => logger) - rsync.asynchronously(find_servers) do |server| - options = yield server - next unless options - remote_user = server.user || fetch(:user, ENV['USER']) - src = options[:source] - dest = {:user => remote_user, :host => server.host, :path => options[:dest]} - options[:ssh] = ssh_options.merge(server.options[:ssh_options]||{}) - options[:chdir] ||= Path.provider - rsync.exec(src, dest, options) - end - if rsync.failed? - LeapCli::Util.bail! do - LeapCli::Util.log :failed, "to rsync to #{rsync.failures.map{|f|f[:dest][:host]}.join(' ')}" - end - end - end - -end; end; end diff --git a/lib/leap_cli/remote/tasks.rb b/lib/leap_cli/remote/tasks.rb deleted file mode 100644 index d08d19a..0000000 --- a/lib/leap_cli/remote/tasks.rb +++ /dev/null @@ -1,51 +0,0 @@ -# -# This file is evaluated just the same as a typical capistrano "deploy.rb" -# For DSL manual, see https://github.com/capistrano/capistrano/wiki -# - -MAX_HOSTS = 10 - -task :install_authorized_keys, :max_hosts => MAX_HOSTS do - leap.log :updating, "authorized_keys" do - leap.mkdirs '/root/.ssh' - upload LeapCli::Path.named_path(:authorized_keys), '/root/.ssh/authorized_keys', :mode => '600' - end -end - -# -# for vagrant nodes, we install insecure vagrant key to authorized_keys2, since deploy -# will overwrite authorized_keys. -# -# why force the insecure vagrant key? -# if we don't do this, then first time initialization might fail if the user has many keys -# (ssh will bomb out before it gets to the vagrant key). -# and it really doesn't make sense to ask users to pin the insecure vagrant key in their -# .ssh/config files. -# -task :install_insecure_vagrant_key, :max_hosts => MAX_HOSTS do - leap.log :installing, "insecure vagrant key" do - leap.mkdirs '/root/.ssh' - upload LeapCli::Path.vagrant_ssh_pub_key_file, '/root/.ssh/authorized_keys2', :mode => '600' - end -end - -task :install_prerequisites, :max_hosts => MAX_HOSTS do - bin_dir = File.join(Leap::Platform.leap_dir, 'bin') - node_init_path = File.join(bin_dir, 'node_init') - - leap.log :running, "node_init script" do - leap.mkdirs bin_dir - upload LeapCli::Path.node_init_script, node_init_path, :mode => '500' - run node_init_path - end -end - -# -# just dummies, used to capture task options -# - -task :skip_errors_task, :on_error => :continue, :max_hosts => MAX_HOSTS do -end - -task :standard_task, :max_hosts => MAX_HOSTS do -end diff --git a/lib/leap_cli/ssh_key.rb b/lib/leap_cli/ssh_key.rb deleted file mode 100644 index 138f444..0000000 --- a/lib/leap_cli/ssh_key.rb +++ /dev/null @@ -1,195 +0,0 @@ -# -# A wrapper around OpenSSL::PKey::RSA instances to provide a better api for dealing with SSH keys. -# -# cipher 'ssh-ed25519' not supported yet because we are waiting for support in Net::SSH -# - -require 'net/ssh' -require 'forwardable' - -module LeapCli - class SshKey - extend Forwardable - - attr_accessor :filename - attr_accessor :comment - - # supported ssh key types, in order of preference - SUPPORTED_TYPES = ['ssh-rsa', 'ecdsa-sha2-nistp256'] - SUPPORTED_TYPES_RE = /(#{SUPPORTED_TYPES.join('|')})/ - - ## - ## CLASS METHODS - ## - - def self.load(arg1, arg2=nil) - key = nil - if arg1.is_a? OpenSSL::PKey::RSA - key = SshKey.new arg1 - elsif arg1.is_a? String - if arg1 =~ /^ssh-/ - type, data = arg1.split(' ') - key = SshKey.new load_from_data(data, type) - elsif File.exists? arg1 - key = SshKey.new load_from_file(arg1) - key.filename = arg1 - else - key = SshKey.new load_from_data(arg1, arg2) - end - end - return key - rescue StandardError => exc - end - - def self.load_from_file(filename) - public_key = nil - private_key = nil - begin - public_key = Net::SSH::KeyFactory.load_public_key(filename) - rescue NotImplementedError, Net::SSH::Exception, OpenSSL::PKey::PKeyError - begin - private_key = Net::SSH::KeyFactory.load_private_key(filename) - rescue NotImplementedError, Net::SSH::Exception, OpenSSL::PKey::PKeyError - end - end - public_key || private_key - end - - def self.load_from_data(data, type='ssh-rsa') - public_key = nil - private_key = nil - begin - public_key = Net::SSH::KeyFactory.load_data_public_key("#{type} #{data}") - rescue NotImplementedError, Net::SSH::Exception, OpenSSL::PKey::PKeyError - begin - private_key = Net::SSH::KeyFactory.load_data_private_key("#{type} #{data}") - rescue NotImplementedError, Net::SSH::Exception, OpenSSL::PKey::PKeyError - end - end - public_key || private_key - end - - # - # Picks one key out of an array of keys that we think is the "best", - # based on the order of preference in SUPPORTED_TYPES - # - # Currently, this does not take bitsize into account. - # - def self.pick_best_key(keys) - keys.select {|k| - SUPPORTED_TYPES.include?(k.type) - }.sort {|a,b| - SUPPORTED_TYPES.index(a.type) <=> SUPPORTED_TYPES.index(b.type) - }.first - end - - # - # takes a string with one or more ssh keys, one key per line, - # and returns an array of SshKey objects. - # - # the lines should be in one of these formats: - # - # 1. <hostname> <key-type> <key> - # 2. <key-type> <key> - # - def self.parse_keys(string) - keys = [] - lines = string.split("\n").grep(/^[^#]/) - lines.each do |line| - if line =~ / #{SshKey::SUPPORTED_TYPES_RE} / - # <hostname> <key-type> <key> - keys << line.split(' ')[1..2] - elsif line =~ /^#{SshKey::SUPPORTED_TYPES_RE} / - # <key-type> <key> - keys << line.split(' ') - end - end - return keys.map{|k| SshKey.load(k[1], k[0])} - end - - # - # takes a string with one or more ssh keys, one key per line, - # and returns a string that specified the ssh key algorithms - # that are supported by the keys, in order of preference. - # - # eg: ecdsa-sha2-nistp256,ssh-rsa,ssh-ed25519 - # - def self.supported_host_key_algorithms(string) - if string - self.parse_keys(string).map {|key| - key.type - }.join(',') - else - "" - end - end - - ## - ## INSTANCE METHODS - ## - - public - - def initialize(rsa_key) - @key = rsa_key - end - - def_delegator :@key, :fingerprint, :fingerprint - def_delegator :@key, :public?, :public? - def_delegator :@key, :private?, :private? - def_delegator :@key, :ssh_type, :type - def_delegator :@key, :public_encrypt, :public_encrypt - def_delegator :@key, :public_decrypt, :public_decrypt - def_delegator :@key, :private_encrypt, :private_encrypt - def_delegator :@key, :private_decrypt, :private_decrypt - def_delegator :@key, :params, :params - def_delegator :@key, :to_text, :to_text - - def public_key - SshKey.new(@key.public_key) - end - - def private_key - SshKey.new(@key.private_key) - end - - # - # not sure if this will always work, but is seems to for now. - # - def bits - Net::SSH::Buffer.from(:key, @key).to_s.split("\001\000").last.size * 8 - end - - def summary - if self.filename - "%s %s %s (%s)" % [self.type, self.bits, self.fingerprint, File.basename(self.filename)] - else - "%s %s %s" % [self.type, self.bits, self.fingerprint] - end - end - - def to_s - self.type + " " + self.key - end - - def key - [Net::SSH::Buffer.from(:key, @key).to_s].pack("m*").gsub(/\s/, "") - end - - def ==(other_key) - return false if other_key.nil? - return false if self.class != other_key.class - return self.to_text == other_key.to_text - end - - def in_known_hosts?(*identifiers) - identifiers.each do |identifier| - Net::SSH::KnownHosts.search_for(identifier).each do |key| - return true if self == key - end - end - return false - end - - end -end diff --git a/lib/leap_cli/util.rb b/lib/leap_cli/util.rb index 5014238..ae73731 100644 --- a/lib/leap_cli/util.rb +++ b/lib/leap_cli/util.rb @@ -10,6 +10,10 @@ module LeapCli @@exit_status = nil + def log(*args, &block) + LeapCli.log(*args, &block) + end + ## ## QUITTING ## @@ -36,15 +40,14 @@ module LeapCli # # exit with error code and with a message that we are bailing out. # - def bail!(*message) - if block_given? - LeapCli.set_log_level(3) - yield - elsif message - log 0, *message + def bail!(*message, &block) + LeapCli.logger.log_level = 3 if LeapCli.logger.log_level < 3 + if message.any? + log(0, *message, &block) + else + log(0, :bailing, "out", :color => :red, :style => :bold, &block) end - log 0, :bail, "" - raise SystemExit.new(@exit_status || 1) + raise SystemExit.new(exit_status || 1) end # @@ -52,7 +55,7 @@ module LeapCli # def quit!(message='') puts(message) - raise SystemExit.new(@exit_status || 0) + raise SystemExit.new(exit_status || 0) end # @@ -119,7 +122,7 @@ module LeapCli base = options[:base] || Path.provider file_list = files.collect { |file_path| file_path = Path.named_path(file_path, base) - File.exists?(file_path) ? Path.relative_path(file_path, base) : nil + File.exist?(file_path) ? Path.relative_path(file_path, base) : nil }.compact if file_list.length > 1 bail! do @@ -138,7 +141,7 @@ module LeapCli options = files.last.is_a?(Hash) ? files.pop : {} file_list = files.collect { |file_path| file_path = Path.named_path(file_path) - !File.exists?(file_path) ? Path.relative_path(file_path) : nil + !File.exist?(file_path) ? Path.relative_path(file_path) : nil }.compact if file_list.length > 1 bail! do @@ -157,7 +160,7 @@ module LeapCli def file_exists?(*files) files.each do |file_path| file_path = Path.named_path(file_path) - if !File.exists?(file_path) + if !File.exist?(file_path) return false end end @@ -233,7 +236,7 @@ module LeapCli # def replace_file!(filepath, &block) filepath = Path.named_path(filepath) - if !File.exists?(filepath) + if !File.exist?(filepath) content = yield(nil) unless content.nil? write_file!(filepath, content) @@ -258,7 +261,7 @@ module LeapCli def remove_file!(filepath) filepath = Path.named_path(filepath) - if File.exists?(filepath) + if File.exist?(filepath) if File.directory?(filepath) remove_directory!(filepath) else @@ -298,7 +301,7 @@ module LeapCli def write_file!(filepath, contents) filepath = Path.named_path(filepath) ensure_dir File.dirname(filepath) - existed = File.exists?(filepath) + existed = File.exist?(filepath) if existed if file_content_equals?(filepath, contents) log :nochange, filepath, 2 @@ -320,11 +323,11 @@ module LeapCli def rename_file!(oldpath, newpath) oldpath = Path.named_path(oldpath) newpath = Path.named_path(newpath) - if File.exists? newpath + if File.exist? newpath log :skipping, "#{Path.relative_path(newpath)}, file already exists" return end - if !File.exists? oldpath + if !File.exist? oldpath log :skipping, "#{Path.relative_path(oldpath)}, file is missing" return end @@ -425,11 +428,18 @@ module LeapCli end end + def is_git_subrepo?(dir) + Dir.chdir(dir) do + `ls .gitrepo 2>/dev/null` + return $? == 0 + end + end + def current_git_branch(dir) Dir.chdir(dir) do branch = `git symbolic-ref HEAD 2>/dev/null`.strip if branch.chars.any? - branch.sub /^refs\/heads\//, '' + branch.sub(/^refs\/heads\//, '') else nil end diff --git a/lib/leap_cli/util/remote_command.rb b/lib/leap_cli/util/remote_command.rb deleted file mode 100644 index 10a5ca8..0000000 --- a/lib/leap_cli/util/remote_command.rb +++ /dev/null @@ -1,158 +0,0 @@ -module LeapCli; module Util; module RemoteCommand - extend self - - # - # FYI - # Capistrano::Logger::IMPORTANT = 0 - # Capistrano::Logger::INFO = 1 - # Capistrano::Logger::DEBUG = 2 - # Capistrano::Logger::TRACE = 3 - # - def ssh_connect(nodes, options={}, &block) - options ||= {} - node_list = parse_node_list(nodes) - - cap = new_capistrano - cap.logger = LeapCli::Logger.new(:level => [LeapCli.log_level,3].min) - user = options[:user] || 'root' - cap.set :user, user - cap.set :ssh_options, ssh_options # ssh options common to all nodes - cap.set :use_sudo, false # we may want to change this in the future - - # Allow password authentication when we are bootstraping a single node - # (and key authentication fails). - if options[:bootstrap] && node_list.size == 1 - hostname = node_list.values.first.name - if options[:echo] - cap.set(:password) { ask "Root SSH password for #{user}@#{hostname}> " } - else - cap.set(:password) { Capistrano::CLI.password_prompt " * Typed password will be hidden (use --echo to make it visible)\nRoot SSH password for #{user}@#{hostname}> " } - end - end - - node_list.each do |name, node| - cap.server node.domain.full, :dummy_arg, node_options(node, options[:ssh_options]) - end - - yield cap - rescue Capistrano::ConnectionError => exc - # not sure if this will work if english is not the locale?? - if exc.message =~ /Too many authentication failures/ - at_exit {ssh_config_help_message} - end - raise exc - end - - private - - # - # For available options, see http://net-ssh.github.com/net-ssh/classes/Net/SSH.html#method-c-start - # - # Capistrano has some very evil behavior in it's ssh.rb: - # - # ssh_options = Net::SSH.configuration_for( - # server.host, ssh_options.fetch(:config, true) - # ).merge(ssh_options) - # # Once we've loaded the config, we don't need Net::SSH to do it again. - # ssh_options[:config] = false - # - # Net:SSH is supposed to call Net::SSH.configuration_for, but Capistrano is doing it - # in advance and then disabling loading of configs. - # - # The result of this is the following: if you have IdentityFile in your ~/.ssh/config - # file, then the above code will transform the ssh_options by reading ~/.ssh/config - # and adding the keys specified via IdentityFile to ssh_options... - # AND IT WILL SET :keys_only TO TRUE. - # - # The problem is that :keys_only will disable Net:SSH's ability to use ssh-agent. - # With :keys_only set to true, it will not consult the ssh-agent at all. - # - # So nice of capistrano to parse ~/.ssh/config for us, but then add flags to the - # ssh_options that prevent's these options from being useful. - # - # The current hackaround is to force :keys_only to be false. This allows the config - # to be read and also allows ssh-agent to still be used. - # - def ssh_options - { - :keys_only => false, # Don't you dare change this. - :global_known_hosts_file => path(:known_hosts), - :user_known_hosts_file => '/dev/null', - :paranoid => true, - :verbose => net_ssh_log_level - } - end - - def net_ssh_log_level - if DEBUG - case LeapCli.log_level - when 1 then 3 - when 2 then 2 - when 3 then 1 - else 0 - end - else - nil - end - end - - # - # For notes on advanced ways to set server-specific options, see - # http://railsware.com/blog/2011/11/02/advanced-server-definitions-in-capistrano/ - # - # if, in the future, we want to do per-node password options, it would be done like so: - # - # password_proc = Proc.new {Capistrano::CLI.password_prompt "Root SSH password for #{node.name}"} - # return {:password => password_proc} - # - def node_options(node, ssh_options_override=nil) - { - :ssh_options => { - # :host_key_alias => node.name, << incompatible with ports in known_hosts - :host_name => node.ip_address, - :port => node.ssh.port - }.merge(contingent_ssh_options_for_node(node)).merge(ssh_options_override||{}) - } - end - - def new_capistrano - # load once the library files - @capistrano_enabled ||= begin - require 'capistrano' - require 'capistrano/cli' - require 'leap_cli/lib_ext/capistrano_connections' - require 'leap_cli/remote/leap_plugin' - require 'leap_cli/remote/puppet_plugin' - require 'leap_cli/remote/rsync_plugin' - Capistrano.plugin :leap, LeapCli::Remote::LeapPlugin - Capistrano.plugin :puppet, LeapCli::Remote::PuppetPlugin - Capistrano.plugin :rsync, LeapCli::Remote::RsyncPlugin - true - end - - # create capistrano instance - cap = Capistrano::Configuration.new - - # add tasks to capistrano instance - cap.load File.dirname(__FILE__) + '/../remote/tasks.rb' - - return cap - end - - def contingent_ssh_options_for_node(node) - opts = {} - if node.vagrant? - opts[:keys] = [vagrant_ssh_key_file] - opts[:keys_only] = true # only use the keys specified above, and ignore whatever keys the ssh-agent is aware of. - opts[:paranoid] = false # we skip host checking for vagrant nodes, because fingerprint is different for everyone. - if LeapCli::log_level <= 1 - opts[:verbose] = :error # suppress all the warnings about adding host keys to known_hosts, since it is not actually doing that. - end - end - if !node.supported_ssh_host_key_algorithms.empty? - opts[:host_key] = node.supported_ssh_host_key_algorithms - end - return opts - end - -end; end; end
\ No newline at end of file diff --git a/lib/leap_cli/util/secret.rb b/lib/leap_cli/util/secret.rb deleted file mode 100644 index 749b959..0000000 --- a/lib/leap_cli/util/secret.rb +++ /dev/null @@ -1,55 +0,0 @@ -# encoding: utf-8 -# -# A simple secret generator -# -# Uses OpenSSL random number generator instead of Ruby's rand function -# -autoload :OpenSSL, 'openssl' - -module LeapCli; module Util - class Secret - CHARS = (('A'..'Z').to_a + ('a'..'z').to_a + ('0'..'9').to_a) - "i1loO06G".split(//u) - HEX = (0..9).to_a + ('a'..'f').to_a - - # - # generate a secret with with no ambiguous characters. - # - # +length+ is in chars - # - # Only alphanumerics are allowed, in order to make these passwords work - # for REST url calls and to allow you to easily copy and paste them. - # - def self.generate(length = 16) - seed - OpenSSL::Random.random_bytes(length).bytes.to_a.collect { |byte| - CHARS[ byte % CHARS.length ] - }.join - end - - # - # generates a hex secret, instead of an alphanumeric on. - # - # length is in bits - # - def self.generate_hex(length = 128) - seed - OpenSSL::Random.random_bytes(length/4).bytes.to_a.collect { |byte| - HEX[ byte % HEX.length ] - }.join - end - - private - - def self.seed - @pid ||= 0 - pid = $$ - if @pid != pid - now = Time.now - ary = [now.to_i, now.nsec, @pid, pid] - OpenSSL::Random.seed(ary.to_s) - @pid = pid - end - end - - end -end; end diff --git a/lib/leap_cli/util/x509.rb b/lib/leap_cli/util/x509.rb deleted file mode 100644 index 787fdfa..0000000 --- a/lib/leap_cli/util/x509.rb +++ /dev/null @@ -1,33 +0,0 @@ -autoload :OpenSSL, 'openssl' -autoload :CertificateAuthority, 'certificate_authority' - -require 'digest' -require 'digest/md5' -require 'digest/sha1' - -module LeapCli; module X509 - extend self - - # - # returns a fingerprint of a x509 certificate - # - def fingerprint(digest, cert_file) - if cert_file.is_a? String - cert = OpenSSL::X509::Certificate.new(Util.read_file!(cert_file)) - elsif cert_file.is_a? OpenSSL::X509::Certificate - cert = cert_file - elsif cert_file.is_a? CertificateAuthority::Certificate - cert = cert_file.openssl_body - end - digester = case digest - when "MD5" then Digest::MD5.new - when "SHA1" then Digest::SHA1.new - when "SHA256" then Digest::SHA256.new - when "SHA384" then Digest::SHA384.new - when "SHA512" then Digest::SHA512.new - end - digester.hexdigest(cert.to_der) - end - - -end; end diff --git a/lib/leap_cli/version.rb b/lib/leap_cli/version.rb index 475cdcc..bb8b57c 100644 --- a/lib/leap_cli/version.rb +++ b/lib/leap_cli/version.rb @@ -1,9 +1,14 @@ module LeapCli unless defined?(LeapCli::VERSION) - VERSION = '1.8.1' - COMPATIBLE_PLATFORM_VERSION = '0.8'..'0.8.99' + VERSION = '1.9' + COMPATIBLE_PLATFORM_VERSION = '0.9'..'0.99' SUMMARY = 'Command line interface to the LEAP platform' DESCRIPTION = 'The command "leap" can be used to manage a bevy of servers running the LEAP platform from the comfort of your own home.' - LOAD_PATHS = ['lib', 'vendor/certificate_authority/lib', 'vendor/rsync_command/lib'] + LOAD_PATHS = ['lib', + 'vendor/certificate_authority/lib', + 'vendor/rsync_command/lib', + 'vendor/base32/lib', + 'vendor/acme-client/lib' + ] end end diff --git a/test/provider/Leapfile b/test/provider/Leapfile index abab946..71af4f9 100644 --- a/test/provider/Leapfile +++ b/test/provider/Leapfile @@ -1 +1 @@ -@platform_directory_path = '../../../leap_platform'
\ No newline at end of file +@platform_directory_path = ENV['PLATFORM_DIR'] || '../../../leap_platform' diff --git a/test/provider/files/ca/ca.crt b/test/provider/files/ca/ca.crt new file mode 100644 index 0000000..765b61d --- /dev/null +++ b/test/provider/files/ca/ca.crt @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICbDCCAdWgAwIBAgIBATANBgkqhkiG9w0BAQ0FADBKMRAwDgYDVQQKDAdFeGFt +cGxlMRwwGgYDVQQLDBNodHRwczovL2V4YW1wbGUub3JnMRgwFgYDVQQDDA9FeGFt +cGxlIFJvb3QgQ0EwIBcNMTYwOTI4MDAwMDAwWhgPMjExNjA5MjgwMDAwMDBaMEox +EDAOBgNVBAoMB0V4YW1wbGUxHDAaBgNVBAsME2h0dHBzOi8vZXhhbXBsZS5vcmcx +GDAWBgNVBAMMD0V4YW1wbGUgUm9vdCBDQTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw +gYkCgYEA2qvO4cFgWRuMMgubaTP8L6ygeBPvHQrK0ZbM3MRJxtBUKfF0uT/+Y8CH +XtQ9Jz+uy4+0n/W/BvExOilY/A/S7cmdD/xdRl7IxSmpCpvQmuhtgV48wSvC9E1v +TB9pSZBJtfYiLqf6WXncSe/3AbjsF+z1id3Ye/4wcbY77MeSaRMCAwEAAaNgMF4w +HQYDVR0OBBYEFLWsA8r7sW8c87nGA4JQcSbYlZGBMA4GA1UdDwEB/wQEAwICBDAM +BgNVHRMEBTADAQH/MB8GA1UdIwQYMBaAFLWsA8r7sW8c87nGA4JQcSbYlZGBMA0G +CSqGSIb3DQEBDQUAA4GBAEeo1alEkXmugRJHjczC7od50zZxaoG/1fIfXqhTGPs7 +99FuXnlKKeFjQOUN1V0Ef5m3MOaTVk4Zx8zyZ9ybljriVS6Dwf2AdMUkOppYe4wp +soLvZ2y0bY//F279Xn/GvmHHLfA722mPJ/Z7csirWHaEhqp9prBXN5Fqin8mNCiF +-----END CERTIFICATE----- diff --git a/test/provider/files/ca/ca.key b/test/provider/files/ca/ca.key new file mode 100644 index 0000000..b0a6550 --- /dev/null +++ b/test/provider/files/ca/ca.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQDaq87hwWBZG4wyC5tpM/wvrKB4E+8dCsrRlszcxEnG0FQp8XS5 +P/5jwIde1D0nP67Lj7Sf9b8G8TE6KVj8D9LtyZ0P/F1GXsjFKakKm9Ca6G2BXjzB +K8L0TW9MH2lJkEm19iIup/pZedxJ7/cBuOwX7PWJ3dh7/jBxtjvsx5JpEwIDAQAB +AoGBAIbgKAf5RZtQsYWAwUf/h5JEUOofqYHpUTY7ZHrbG4JkpzUDuHI29YrDivvD +v0CBOChYqBlt83ittiZgsIEwpXFf36q9Xz80yySXHsjEhOVUPIdKZt95n0VZmwQn +y5PdsGgaWXkD58HSuSaa6CyMH0O50iwqJQiRy71VRr0A6LJZAkEA74dUYD/hJieH +OA0FDw0z5BaB6QTLtKZogOQ/g6Ju1PmhqcXvbMhv4hZ5DDXEwkVb/5qaFbAMmxL5 +QRkTw0Kx3wJBAOm1TWIauB0siNSGnESGSiZxsDGzfd6GbztC3E7E0tupAk0l+HuK +PA76vs76QgJPxRjLSn6A6mhGStwSnUk0N00CQHJ9/2jaX+Z68nlqT8a4Ctu1nnch +YbWB7WXetDVZiRyoDgw2npEi5cft8gJSGTC7MpRk8832DrB5S0dAk1+8G4UCQHQa +e90XBQyJSVi7nvpz9HZw2GV4lDluc+fu6V/AbDhwGBKXoIBPRlLywsQ0k4Jueq48 +oD+Eb+9prFr0bGsno6kCQQDqCigukRwPvpNyq5fMS4d7Rs0N5HlaSUdi0QYWQ38i +144eEq0NswCDQt025bEw/dzZZIqS3JSUbx3ZGOiUD3bp +-----END RSA PRIVATE KEY----- diff --git a/test/provider/files/ca/client_ca.crt b/test/provider/files/ca/client_ca.crt new file mode 100644 index 0000000..accc0cf --- /dev/null +++ b/test/provider/files/ca/client_ca.crt @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICpDCCAg2gAwIBAgIBATANBgkqhkiG9w0BAQ0FADBmMRAwDgYDVQQKDAdFeGFt +cGxlMRwwGgYDVQQLDBNodHRwczovL2V4YW1wbGUub3JnMTQwMgYDVQQDDCtFeGFt +cGxlIFJvb3QgQ0EgKGNsaWVudCBjZXJ0aWZpY2F0ZXMgb25seSEpMCAXDTE2MDky +ODAwMDAwMFoYDzIxMTYwOTI4MDAwMDAwWjBmMRAwDgYDVQQKDAdFeGFtcGxlMRww +GgYDVQQLDBNodHRwczovL2V4YW1wbGUub3JnMTQwMgYDVQQDDCtFeGFtcGxlIFJv +b3QgQ0EgKGNsaWVudCBjZXJ0aWZpY2F0ZXMgb25seSEpMIGfMA0GCSqGSIb3DQEB +AQUAA4GNADCBiQKBgQDrtlWEw/XMV0p4+R9fDEMKm4kBmN+F29qdA3cQssZkZBRj +UAbpwIk+wZXJuukwoQHY9bwobr85rEf6UiEi0e3sxD4yN4GEU+rX9JVHgGUmbi0f +Wmu6YHMRfnRKOu8IMu50Ry+oPIwHpzSek6IfYKI1D+484UBJ1sMESQyo3V47rQID +AQABo2AwXjAdBgNVHQ4EFgQUz+7haong5OegkFFugOHX4oRoJCowDgYDVR0PAQH/ +BAQDAgIEMAwGA1UdEwQFMAMBAf8wHwYDVR0jBBgwFoAUz+7haong5OegkFFugOHX +4oRoJCowDQYJKoZIhvcNAQENBQADgYEAzV/AUYmkxsnnbHExdePYceBeQ8mMGaqy +JQx4UstHEqUq5IXz346saQcXHELq2QHX/JgGC7crUsjICglqq1XeVJ7ULlmIRVoe +6iSG0sdAulji4sdIidXJ/AluBdUE9iPbmgKQWn9YD17j85QBQEa+M5G52gb0Ul8q +oMQRQRQ5X7I= +-----END CERTIFICATE----- diff --git a/test/provider/files/ca/client_ca.key b/test/provider/files/ca/client_ca.key new file mode 100644 index 0000000..f9e2f27 --- /dev/null +++ b/test/provider/files/ca/client_ca.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQDrtlWEw/XMV0p4+R9fDEMKm4kBmN+F29qdA3cQssZkZBRjUAbp +wIk+wZXJuukwoQHY9bwobr85rEf6UiEi0e3sxD4yN4GEU+rX9JVHgGUmbi0fWmu6 +YHMRfnRKOu8IMu50Ry+oPIwHpzSek6IfYKI1D+484UBJ1sMESQyo3V47rQIDAQAB +AoGAOuuDCQLq6D9RsFeljd7Ey1wBrVKHXTCNvv3kv1nQ2btilUil0bx9EiDVzm1Y +aP12NsOGWx0D0+jKvTnWapvLOw8e3F+XvkKCzhcyz/M2NlQh5bGlgtG1TBN/C/K6 +HuJk1GGDFei2dKPadkN18mHvq/2fFLtdJ+Z5Fczd6U3fXQkCQQD82sUY7uoN023q +smqxn60N3B+PN6DOaD+n2jOzrmkWvvY90X7WzxHRMWrV2a/Gov+MGOCebPNC9VLF +luxhU50LAkEA7qT5MSK3XfvmxcUfSMCjED2X4cf8VEBsYEHl/qQTxcXvo40dLinD +0za04iC6/NIUZaAhLzMsg/lByCkJZ09NJwJATR8Y4Kr2PnNPYjc67aRLLyAFjDQm +Wu5XBAY8oMBAk0x5ZI+CRVhxEcIl2MYFo+tRUFTCJfALHlAfB98ph+Ht0wJBANh7 +qV5MauEEES1ZC28Y6RNjfHMh0qGvK2EKhpQ/zXv8ec34xf7Jfk4M83uqS1XrUPt7 +jn7dwkUaCPWFXHVuN8MCQQCUkgXZRHjO+C9G9vKKhgiEWrDw/cx6+3o8sFELLqQn ++wgXov454z+ksILx9hxCFaDUDq1iqhVTK71njsIMZ1Gi +-----END RSA PRIVATE KEY----- diff --git a/test/provider/files/cert/bitmask.net.crt b/test/provider/files/cert/bitmask.net.crt deleted file mode 100644 index f3aaae4..0000000 --- a/test/provider/files/cert/bitmask.net.crt +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICZzCCAdCgAwIBAgIRAPF3nvtTiGL4Z/z8rrJ2OKAwDQYJKoZIhvcNAQELBQAw -SjEQMA4GA1UECgwHQml0bWFzazEcMBoGA1UECwwTaHR0cHM6Ly9iaXRtYXNrLm5l -dDEYMBYGA1UEAwwPQml0bWFzayBSb290IENBMB4XDTE2MDQwOTAwMDAwMFoXDTE3 -MDQwOTAwMDAwMFowKDEQMA4GA1UECgwHQml0bWFzazEUMBIGA1UEAwwLYml0bWFz -ay5uZXQwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMcuc0zp/JMOkZZXmaH/ -/ABBtc3i79OD90LRk4AEXZ7X46Ougw92qeHvX8worEHgpiPxzlj2QETrH25ljuqK -e/nDpHwO/43couFFliq3VnLLBDJvYzL5byTd5V0bs/q4tl5CUYt1j6Xg4ses/Hv3 -cHyNqNQKfVJuyeWdZhtNizhHAgMBAAGjbzBtMB0GA1UdDgQWBBTB0njg6dZRnf/Z -dO7EBRUy2+fBpTALBgNVHQ8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwCQYD -VR0TBAIwADAfBgNVHSMEGDAWgBQCuoulI/QMOR5z5nDOeXoOzkZtOjANBgkqhkiG -9w0BAQsFAAOBgQAQ9EWhZJqLKLwCTOG0AD5+KwpbAkhHgdO3BXcMJAqLhjezmd9c -cHQ/DZ/BSKmIm0eV6UsnxOBy9lZNIL1KqpazUyCgcCPDwDhd8Ihgk0x5ciNHgCFq -6rCQ3kQVPVJZ2S2gQLOKJz1a0muMBE5KmIEL0ZMgqpn97YHgrOMCIjoM9g== ------END CERTIFICATE----- diff --git a/test/provider/files/cert/bitmask.net.csr b/test/provider/files/cert/bitmask.net.csr deleted file mode 100644 index d106cb1..0000000 --- a/test/provider/files/cert/bitmask.net.csr +++ /dev/null @@ -1,11 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIIBpjCCAQ8CAQAwKDEQMA4GA1UECgwHQml0bWFzazEUMBIGA1UEAwwLYml0bWFz -ay5uZXQwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMcuc0zp/JMOkZZXmaH/ -/ABBtc3i79OD90LRk4AEXZ7X46Ougw92qeHvX8worEHgpiPxzlj2QETrH25ljuqK -e/nDpHwO/43couFFliq3VnLLBDJvYzL5byTd5V0bs/q4tl5CUYt1j6Xg4ses/Hv3 -cHyNqNQKfVJuyeWdZhtNizhHAgMBAAGgPjA8BgkqhkiG9w0BCQ4xLzAtMAkGA1Ud -EwQCMAAwCwYDVR0PBAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA0GCSqGSIb3 -DQEBCwUAA4GBAFnt0V7+qyPfQZQGF12DdCy0t3MRqFVQbcIegNPshKWP1GIruVMX -ltJmTB1oVqVQ8Pmj0lIAbCrudHBqblnUUt1tME1JmWgH9wQtDaP5jnATJ1DQGMl1 -bQJQdiSE3/VGSeHn3K/XY7Yk2kmWZ3mzf1AwCmpwrn4SxIPiGcYa+21U ------END CERTIFICATE REQUEST----- diff --git a/test/provider/files/cert/bitmask.net.key b/test/provider/files/cert/bitmask.net.key deleted file mode 100644 index 877f781..0000000 --- a/test/provider/files/cert/bitmask.net.key +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIICXwIBAAKBgQDHLnNM6fyTDpGWV5mh//wAQbXN4u/Tg/dC0ZOABF2e1+OjroMP -dqnh71/MKKxB4KYj8c5Y9kBE6x9uZY7qinv5w6R8Dv+N3KLhRZYqt1ZyywQyb2My -+W8k3eVdG7P6uLZeQlGLdY+l4OLHrPx793B8jajUCn1SbsnlnWYbTYs4RwIDAQAB -AoGBAKOKXh0+2aUdByi8EGbVOeI0EcRUmrm+1txEG6m26++qLzyL4wxlUCM0WiHV -G2qTu5Yzykt9FVQBAbOxK2EkB5mezLxGhnR24bPcpvDAqWy/dKBQ5t4hARKdgw4A -2iyhojno7aB/inP3ViTNvr/Kg77XyUgIq7fsLa8AsXJo0FAxAkEA5bye9XAYa29w -uK64rrtaflWcUqeejl9BQtrAKQmlRHC3uKxmWv260fn2OZzYwsNdD96y8YKeFS6g -65jj/eMPgwJBAN3znApBwUBDw4dX8ZLz2AC1P3ikQPGu+ySSf5+NJPUU3pgl6eL6 -pGaxplbDpFdvxgsfyxeSgNsFd/zmrD+v9O0CQQDjbTy3oIasJKAkU+NEJvjIxBuC -v6j5LFdAxakhdwkCnctiqFiTj0cYgyk7k4gKFrjT8xSWfUXdllF7qdlaByPdAkEA -t37+FKTERoM/lhepCxs6C2vNa8owPx+xVk0f4iLo2Q5F8Xf248bgQF7C7JyWtAse -qnfAil5+1ZSx3I5A/e5VCQJBALWoaVH/laZinIWgka9TngD0BtLPvYjoH7iLSpAK -STdh5IdwlcCKq/TzC+DpRYsEJM2wHEC+0nOLDp8xDwYPHfw= ------END RSA PRIVATE KEY----- diff --git a/test/provider/files/cert/commercial_ca.crt b/test/provider/files/cert/commercial_ca.crt index 468941e..765b61d 100644 --- a/test/provider/files/cert/commercial_ca.crt +++ b/test/provider/files/cert/commercial_ca.crt @@ -1,15 +1,15 @@ -----BEGIN CERTIFICATE----- -MIICbDCCAdWgAwIBAgIBATANBgkqhkiG9w0BAQ0FADBKMRAwDgYDVQQKDAdCaXRt -YXNrMRwwGgYDVQQLDBNodHRwczovL2JpdG1hc2submV0MRgwFgYDVQQDDA9CaXRt -YXNrIFJvb3QgQ0EwIBcNMTYwNDA5MDAwMDAwWhgPMjExNjA0MDkwMDAwMDBaMEox -EDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNVBAsME2h0dHBzOi8vYml0bWFzay5uZXQx -GDAWBgNVBAMMD0JpdG1hc2sgUm9vdCBDQTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEArDu+1XWnEHS9CsemL6wuFZ09vY59SpXcpkMEOYLl+H5HibLsjt7PkDCi -x4Bmf/0Mvlk5bft7VGHKtRbIe5/vIyA7IyIX76IHsX2iWASS4HaUE4ERtFTqE+2b -x5N0/r5mYJCIhRslZdcAvzVb6NbujsQHU7NSRMOjBofVk1oYn+8CAwEAAaNgMF4w -HQYDVR0OBBYEFAK6i6Uj9Aw5HnPmcM55eg7ORm06MA4GA1UdDwEB/wQEAwICBDAM -BgNVHRMEBTADAQH/MB8GA1UdIwQYMBaAFAK6i6Uj9Aw5HnPmcM55eg7ORm06MA0G -CSqGSIb3DQEBDQUAA4GBAD7cxb1nmhtfHfA4KnnK25dkHygMhqihj2xby3dLtAMO -BuataWvN4ssgrUs7XdZRdagI2W2jA7RyLX8hFo+F2A0CRzYNwHl+Ffa2GuZko6M9 -4Muo4aEs7/h20jsxVFLezTGwN7lcyA8FoueGkCUXMm8WAAL0Id1hk+3ek70ywewh +MIICbDCCAdWgAwIBAgIBATANBgkqhkiG9w0BAQ0FADBKMRAwDgYDVQQKDAdFeGFt +cGxlMRwwGgYDVQQLDBNodHRwczovL2V4YW1wbGUub3JnMRgwFgYDVQQDDA9FeGFt +cGxlIFJvb3QgQ0EwIBcNMTYwOTI4MDAwMDAwWhgPMjExNjA5MjgwMDAwMDBaMEox +EDAOBgNVBAoMB0V4YW1wbGUxHDAaBgNVBAsME2h0dHBzOi8vZXhhbXBsZS5vcmcx +GDAWBgNVBAMMD0V4YW1wbGUgUm9vdCBDQTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw +gYkCgYEA2qvO4cFgWRuMMgubaTP8L6ygeBPvHQrK0ZbM3MRJxtBUKfF0uT/+Y8CH +XtQ9Jz+uy4+0n/W/BvExOilY/A/S7cmdD/xdRl7IxSmpCpvQmuhtgV48wSvC9E1v +TB9pSZBJtfYiLqf6WXncSe/3AbjsF+z1id3Ye/4wcbY77MeSaRMCAwEAAaNgMF4w +HQYDVR0OBBYEFLWsA8r7sW8c87nGA4JQcSbYlZGBMA4GA1UdDwEB/wQEAwICBDAM +BgNVHRMEBTADAQH/MB8GA1UdIwQYMBaAFLWsA8r7sW8c87nGA4JQcSbYlZGBMA0G +CSqGSIb3DQEBDQUAA4GBAEeo1alEkXmugRJHjczC7od50zZxaoG/1fIfXqhTGPs7 +99FuXnlKKeFjQOUN1V0Ef5m3MOaTVk4Zx8zyZ9ybljriVS6Dwf2AdMUkOppYe4wp +soLvZ2y0bY//F279Xn/GvmHHLfA722mPJ/Z7csirWHaEhqp9prBXN5Fqin8mNCiF -----END CERTIFICATE----- diff --git a/test/provider/files/cert/example.org.crt b/test/provider/files/cert/example.org.crt new file mode 100644 index 0000000..a863de4 --- /dev/null +++ b/test/provider/files/cert/example.org.crt @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICZzCCAdCgAwIBAgIRAIm+g8LZXIiwbrNxIjkZUUgwDQYJKoZIhvcNAQELBQAw +SjEQMA4GA1UECgwHRXhhbXBsZTEcMBoGA1UECwwTaHR0cHM6Ly9leGFtcGxlLm9y +ZzEYMBYGA1UEAwwPRXhhbXBsZSBSb290IENBMB4XDTE2MDkyODAwMDAwMFoXDTE3 +MDkyODAwMDAwMFowKDEQMA4GA1UECgwHRXhhbXBsZTEUMBIGA1UEAwwLZXhhbXBs +ZS5vcmcwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAJ3cGk0Y1CCHqaqj8fJr +gAuINrofB/NXpVyLYCVhU4C+3xJEpOXSrOT0DqXHockChnaAusoBTrwN7jvqIBeU +6DmlC+kQqTTizF6c0Xna43ftjfuZAdIpehqA+7wQwKfilC+SNh+8U7V7VrDIrNfR +iluOWSA6jl+PMUA6atrzIWsLAgMBAAGjbzBtMB0GA1UdDgQWBBTNmHvqnul7KbX1 +uGrJs7Jh6VyEIzALBgNVHQ8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwCQYD +VR0TBAIwADAfBgNVHSMEGDAWgBS1rAPK+7FvHPO5xgOCUHEm2JWRgTANBgkqhkiG +9w0BAQsFAAOBgQAT6TUL9rYqEK7E4wCRbzyjaUc+7OTBtnYNVKCY4+jQzQR5r+wo +3fLbsQ5qd1a0BXp44rRlto0oj5ihHAauG/v0BVXbi4vshfV4pdlEWxsbHvRqat0w +gxNlEB9goapeMGdLjPo7uQiEtZhWEHcpyRBukve1aIxDPIHrogPftR0yMA== +-----END CERTIFICATE----- diff --git a/test/provider/files/cert/example.org.csr b/test/provider/files/cert/example.org.csr new file mode 100644 index 0000000..5542c22 --- /dev/null +++ b/test/provider/files/cert/example.org.csr @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBpjCCAQ8CAQAwKDEQMA4GA1UECgwHRXhhbXBsZTEUMBIGA1UEAwwLZXhhbXBs +ZS5vcmcwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAJ3cGk0Y1CCHqaqj8fJr +gAuINrofB/NXpVyLYCVhU4C+3xJEpOXSrOT0DqXHockChnaAusoBTrwN7jvqIBeU +6DmlC+kQqTTizF6c0Xna43ftjfuZAdIpehqA+7wQwKfilC+SNh+8U7V7VrDIrNfR +iluOWSA6jl+PMUA6atrzIWsLAgMBAAGgPjA8BgkqhkiG9w0BCQ4xLzAtMAkGA1Ud +EwQCMAAwCwYDVR0PBAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA0GCSqGSIb3 +DQEBCwUAA4GBAIF7mpeTKfvghlxuS+7CfOO24BNYS/uZu16XBNgzlHR+1eSHb8nU +EGzWCT7C8I7vWQHYXTOX4fMDUeHMw/w7rchgWd/7DikPJR5PAwotQnAVefNAjWpb ++l4rW2pqIJHzGZGoipFmTA2ISbD4AtGhzQOn5u/uu5H3Lo8tIA4iip2/ +-----END CERTIFICATE REQUEST----- diff --git a/test/provider/files/cert/example.org.key b/test/provider/files/cert/example.org.key new file mode 100644 index 0000000..f6eedec --- /dev/null +++ b/test/provider/files/cert/example.org.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQCd3BpNGNQgh6mqo/Hya4ALiDa6HwfzV6Vci2AlYVOAvt8SRKTl +0qzk9A6lx6HJAoZ2gLrKAU68De476iAXlOg5pQvpEKk04sxenNF52uN37Y37mQHS +KXoagPu8EMCn4pQvkjYfvFO1e1awyKzX0YpbjlkgOo5fjzFAOmra8yFrCwIDAQAB +AoGAODbrPs06rSLibpvXSwaxIGovYvQt9qAdiOkxId6Yx94wvean+hed7iJjHPIM +UPKPQ5/v5IO2sA0d60QijYM/dshqwNp/4eXNEceymGFzbqKvSi4xSdoEwDjTTHMl +YDLuAHDgn6s5AM5EvK8eOSb3mkR6kxOODUH6aidhdcDsRCECQQDPY4N7g7oCdwK/ +bkfAxheLh49gnFUi8EsQ3QgssPTN1vhs7zAWt+9ggenMybOgnKk3SY7f+rNErCjc +ZdINwYWTAkEAwtyTEboWOeArCxTJaT+1kZaon2GmDt5K7yu9+kly4r046bly7atD +GKRvttvgdDo59np6lIw/t5+dCmT7LiTvqQJAbW7fdI+f2akfBBCXQDvHNNNFbv9P +VW5izfU0WRDPPMbQs/rK71IDuHMVAgD1Di1chVYFVF8ftX762MHJw4R4jQJBAKU4 +gTq2ncHU4Ko0pdInwrv/ElqRYUuaD89bN2nQfSjjaC5En74FSI7MXiydomLqO9tR +Xj417JC1NWJq3M7zYoECQEN1tIObc0bQzQ+CqwW7M4xt5zzVL/qTvNnwgXkidE6p +WPigAjslZa9gJgJ7V4dYTCSie8baL3IdU824jSzZ10Q= +-----END RSA PRIVATE KEY----- diff --git a/test/provider/files/nodes/couch1/couch1.crt b/test/provider/files/nodes/couch1/couch1.crt new file mode 100644 index 0000000..74854e5 --- /dev/null +++ b/test/provider/files/nodes/couch1/couch1.crt @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICoDCCAgmgAwIBAgIQKGOYsoZwiYJpbIxjvubpATANBgkqhkiG9w0BAQsFADBK +MRAwDgYDVQQKDAdFeGFtcGxlMRwwGgYDVQQLDBNodHRwczovL2V4YW1wbGUub3Jn +MRgwFgYDVQQDDA9FeGFtcGxlIFJvb3QgQ0EwIBcNMTYwOTI4MDAwMDAwWhgPMjEx +NjA5MjgwMDAwMDBaMB0xGzAZBgNVBAMMEmNvdWNoMS5leGFtcGxlLm9yZzCBnzAN +BgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA55qmjlgU/9HCi/Ki9O2CNHbF38CCpVUd +/7dJaGVIxBgZI8a27TWNFHk3g+JPu+NCx9TT9bJYobnHe4UHnhhgjk1o6Z0Z9ele +nStVDqYwde3rG7uxwt6qmPrYVMYmujPDAZq3UF8ECl0t1nJtEKsaIu0AbCEqZDFC +sNhOnJflxn0CAwEAAaOBsTCBrjAdBgNVHQ4EFgQUjGqJowOEXygZJRnEUMTwlt1q +Rx0wNQYDVR0RBC4wLIIQY291Y2gxLmV4YW1wbGUuaYISY291Y2gxLmV4YW1wbGUu +b3JnhwQKBQUCMAsGA1UdDwQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB +BQUHAwIwCQYDVR0TBAIwADAfBgNVHSMEGDAWgBS1rAPK+7FvHPO5xgOCUHEm2JWR +gTANBgkqhkiG9w0BAQsFAAOBgQCNPzztMWdzDkTZHAxCv4ekZs+iiBR5R7hn92Xh +WJ7Gm9GtD+w8f6tYACdj7C+/+WGjuvl2xqN7qMv2FM1I/cQMuJumXXRjyJVKBYVb +VInoyy+0eFBAzLdx1CRY0zFytJfigBYD3Wq4HY3Dsm0W97xw8v7slYZ2fE0mEFqo +bXgDPg== +-----END CERTIFICATE----- diff --git a/test/provider/files/nodes/couch1/couch1.key b/test/provider/files/nodes/couch1/couch1.key new file mode 100644 index 0000000..47403ee --- /dev/null +++ b/test/provider/files/nodes/couch1/couch1.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQDnmqaOWBT/0cKL8qL07YI0dsXfwIKlVR3/t0loZUjEGBkjxrbt +NY0UeTeD4k+740LH1NP1slihucd7hQeeGGCOTWjpnRn16V6dK1UOpjB17esbu7HC +3qqY+thUxia6M8MBmrdQXwQKXS3Wcm0Qqxoi7QBsISpkMUKw2E6cl+XGfQIDAQAB +AoGANNxZU3fLIzBPBP4WL2zeIPdS5mTb7LxmomzE9mzXlNojMsUyDyX/00JvZ0yK +Ako2fcGXtyZDkHYEj66nNHA/6QueOiXmehC12GuElwWJirnZfVlxGg57FGZ6da39 +hBH2Ip/qnh7cE6j8jPz52MFhb0x5qN9TSaD4V6OS33thNgECQQD1bKkF6OggRwI4 +htBM5IESL9uQtjbeCa6QhFhNQjp0ZwXwp+5mNOBcja4FUReLtcsYc97Z4BCBXEsY +U+xVdlTBAkEA8ZWI2KCJ8tpz3qCbWOkZHhBbrBZbXrSDkHHU08Alh0ERo3eB2STU +r2VrzB1722jZhrILQlvNOwICjiH/8NI0vQJAM0gXMVLvXf84aZNR5x9AEQrK+Dv6 +zv566VueD9as3DHCvfx5BgY6c1xvZlEBeIHuBBgCEsiM6lrcniK7GUh2gQJBANDf +YBUkIIFnnNz0cbwatcvHiusr3U3xtvqxYLjAHfJmMPDrx8nNzVHk16IAL/FRIxoR +YCi8pKILJ9hpzxcRN+UCQQDML4DU0l7oYN2KMycOFuNub21UGuT3z164Fpmr7kbc +cPz84rHUKyzGjKpDWL2DL1po/HT4qBLxsRA4n0A0U4Dt +-----END RSA PRIVATE KEY----- diff --git a/test/provider/files/nodes/couch2/couch2.crt b/test/provider/files/nodes/couch2/couch2.crt new file mode 100644 index 0000000..79e6d21 --- /dev/null +++ b/test/provider/files/nodes/couch2/couch2.crt @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICoTCCAgqgAwIBAgIRAPmeg9lEoavLgBFgmRYVgUMwDQYJKoZIhvcNAQELBQAw +SjEQMA4GA1UECgwHRXhhbXBsZTEcMBoGA1UECwwTaHR0cHM6Ly9leGFtcGxlLm9y +ZzEYMBYGA1UEAwwPRXhhbXBsZSBSb290IENBMCAXDTE2MDkyODAwMDAwMFoYDzIx +MTYwOTI4MDAwMDAwWjAdMRswGQYDVQQDDBJjb3VjaDIuZXhhbXBsZS5vcmcwgZ8w +DQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBALfbx4JzrvYWinH3ZVg/tTMySiqNT5f2 +YHognrH/P9Ukys4nGA11gQbMpyQfXO/3dE592ReTnp2IVmJ2oVAKNkdQnbjk2Xx1 +3/6/AuaASdy68PZAqiWadw5MjAf0y6W0iDDqOQiXH+vEswK4HfP5rsfrsnKCh6U7 +drj+erU4JfVTAgMBAAGjgbEwga4wHQYDVR0OBBYEFLakLgOpq1j5EDHAHNFSKtjW +NA76MDUGA1UdEQQuMCyCEGNvdWNoMi5leGFtcGxlLmmCEmNvdWNoMi5leGFtcGxl +Lm9yZ4cESS1XCzALBgNVHQ8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG +AQUFBwMCMAkGA1UdEwQCMAAwHwYDVR0jBBgwFoAUtawDyvuxbxzzucYDglBxJtiV +kYEwDQYJKoZIhvcNAQELBQADgYEAlnRbf94YpFnqwLdCk3VBWEeGtwj2kEEJJjlC +R1WouYwz9tChUB8H26judnPsTafDN3f3gx3yAqooFXPXz8P0Gm5+XFPWDiL+GtIu +UngRlT6qunepEQ2BVlNuZO9Vd9ov5bD+rEAULASXkiJQhdaPW1Z+QjuMT+1Yykf5 +5zHts90= +-----END CERTIFICATE----- diff --git a/test/provider/files/nodes/couch2/couch2.key b/test/provider/files/nodes/couch2/couch2.key new file mode 100644 index 0000000..fa0106f --- /dev/null +++ b/test/provider/files/nodes/couch2/couch2.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQC328eCc672Fopx92VYP7UzMkoqjU+X9mB6IJ6x/z/VJMrOJxgN +dYEGzKckH1zv93ROfdkXk56diFZidqFQCjZHUJ245Nl8dd/+vwLmgEncuvD2QKol +mncOTIwH9MultIgw6jkIlx/rxLMCuB3z+a7H67JygoelO3a4/nq1OCX1UwIDAQAB +AoGAV9ciD5pTefE0/dQT0EDHwokBVCklYNXuLAsPprzrc1rbpfiZjjyYg3YdWK2/ +Skqwf5uyr4fwnRT5KJvC4Cmw2ju89qOQ1+WXprlM1o0Z4z5dj+LC8S4WlFZuHGB+ ++F5uKgEyO3zEvT5LF9V00IonsaXXpYeJlK0tXOA2ZXkmaXECQQDpCpQW+UZeNQaV +NBXIN7DXzdfsTfO7U4Sf7VB5hMlOwM75XgtXw7ekh0UHohsO2yzINj7QM7pJ5I0m +1FriJGHbAkEAyfjJBAFk4V/cmBcCzlKUV59w+GW+sgzHx4gnBbXu/JLIVaoAQZtS +kq6GHMMPiJK9KbRov9meaOI0wfsoRxw/6QJAMrIzbyABV+MvMGwpROoglYHZNDXt +DNZpZqUouZbSeEhnfkYgL5KLM8adlMCGJGA3yMJMPdzS7NpEfqr5rnJ9uwJBAJ1M +Tjn5X/kK8MHewgewVun7Oj+q9h6zR3CGAGY5MHyzUKUu9m4iKugkVjzWSiXCquJt +KFuqf+4NpqshEVh4jukCQEUVw/a4QvmVkQmsrde64fXm4EaELY+Ri48ibBLlc/qM +wfeyE8m2TEeXx6BPOBOFLeL6kYJkt3uDcXuQnmdsV5c= +-----END RSA PRIVATE KEY----- diff --git a/test/provider/files/nodes/ns1/ns1.crt b/test/provider/files/nodes/ns1/ns1.crt new file mode 100644 index 0000000..2e4b38a --- /dev/null +++ b/test/provider/files/nodes/ns1/ns1.crt @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIIClzCCAgCgAwIBAgIQEqzDOUwybJEK/8K5jRJNqTANBgkqhkiG9w0BAQsFADBK +MRAwDgYDVQQKDAdFeGFtcGxlMRwwGgYDVQQLDBNodHRwczovL2V4YW1wbGUub3Jn +MRgwFgYDVQQDDA9FeGFtcGxlIFJvb3QgQ0EwIBcNMTYwOTI4MDAwMDAwWhgPMjEx +NjA5MjgwMDAwMDBaMBoxGDAWBgNVBAMMD25zMS5leGFtcGxlLm9yZzCBnzANBgkq +hkiG9w0BAQEFAAOBjQAwgYkCgYEAxrvUSmtjXIvzxEAZlh/rJdqyyI706+DvNeha +BqtCHhT+iZ2+IdRXq2EhwoUWsTDBN0iw6wBk7qlvxGcpq782iCregwjQKJgMFHCs +UaTSnuQd9Apv7YyeopcXcD1d/Ee3wMyDUNH8rKksyi2gZfmn6HXsHjCQ8iEebwmD +yVAXg7cCAwEAAaOBqzCBqDAdBgNVHQ4EFgQUJ5qb5nFE1xTSCgvvnbWH6qO32cYw +LwYDVR0RBCgwJoINbnMxLmV4YW1wbGUuaYIPbnMxLmV4YW1wbGUub3JnhwQBAQEB +MAsGA1UdDwQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwCQYD +VR0TBAIwADAfBgNVHSMEGDAWgBS1rAPK+7FvHPO5xgOCUHEm2JWRgTANBgkqhkiG +9w0BAQsFAAOBgQAdwsJ6DhUj5IfsK/esWeCOgCdqAhdW61jABKAv0Y6BH5XqItEG +fsI4INYro2CzgEKVMdLzuK1dEHB17j2gYFowvAJ34KtPlXf3Ne++1Qbc5oJDCnxi +l3PQo4rHQC8V7sKnB/cEiQ15SG16u3B+kMwj8lU+QShh5osMbEG/n+Qs5A== +-----END CERTIFICATE----- diff --git a/test/provider/files/nodes/ns1/ns1.key b/test/provider/files/nodes/ns1/ns1.key new file mode 100644 index 0000000..c85cf7e --- /dev/null +++ b/test/provider/files/nodes/ns1/ns1.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDGu9RKa2Nci/PEQBmWH+sl2rLIjvTr4O816FoGq0IeFP6Jnb4h +1FerYSHChRaxMME3SLDrAGTuqW/EZymrvzaIKt6DCNAomAwUcKxRpNKe5B30Cm/t +jJ6ilxdwPV38R7fAzINQ0fysqSzKLaBl+afodeweMJDyIR5vCYPJUBeDtwIDAQAB +AoGAXEdVMOUicxOtMiBNgS77Ak3FnGj9AxYkHRTx0IzvG4bGFmJ/qbeuqa5lfaxM +uCQaY7BGLii1tThJ5Jm+eLhF+iyXoISzsepzmHflcjNXdu0W44Gb+hQz96DagAhm +dwMHAhquu7AQZ8iTIXzwuSp8B5WcyruPapoX4H+6AgqVlAECQQDurdhbFYN1Qlef +EU9htGaazx+DjSvncmcgFu7gRqUuKX2aPpA1yMbHZ7jTAKeqNBUgks4EwecP3Fxc +RZrMIsIBAkEA1SfiFr1936b2CUnT4KlIwwEcWTGM30tPM7fJ8oJk13eW+pIpLc4X +bFLvauAH15CLjYHkXMBUFVnXdMyhz9TVtwJABMVY08lETW28DqPr8EoI2wNU3+5M +eF3jDdMnhzgiSR/vMMwbWdffkVDTcvRKZa6Q1YvZrmKp2blP51BE3du8AQJARmE3 +1nhUwm73V9PHoKtkefa47H5e3C+ahCIQDQGe2EIFWNC/xf8BXuP3Z1t3W2a/nUah +JzrdyHr0l/0lBGFq+wJBAL59z6MACU4iLsULD5euJNDMtefeK2CvEhKsXJ7UlZ+q +a5eOtsjwEwtV4hSbd5yNR3FpW5grxxytnfVj7bYU584= +-----END RSA PRIVATE KEY----- diff --git a/test/provider/files/nodes/ns2/ns2.crt b/test/provider/files/nodes/ns2/ns2.crt new file mode 100644 index 0000000..3003781 --- /dev/null +++ b/test/provider/files/nodes/ns2/ns2.crt @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICmDCCAgGgAwIBAgIRANJEIfGpsriEvDXwPskuNbQwDQYJKoZIhvcNAQELBQAw +SjEQMA4GA1UECgwHRXhhbXBsZTEcMBoGA1UECwwTaHR0cHM6Ly9leGFtcGxlLm9y +ZzEYMBYGA1UEAwwPRXhhbXBsZSBSb290IENBMCAXDTE2MDkyODAwMDAwMFoYDzIx +MTYwOTI4MDAwMDAwWjAaMRgwFgYDVQQDDA9uczIuZXhhbXBsZS5vcmcwgZ8wDQYJ +KoZIhvcNAQEBBQADgY0AMIGJAoGBAJ8JECo3emqgpKCUESglWAfHljSCA0zMT8NN +zXvyeLTXwFLZvtDPBN6DcN1YBZN0tJHq222flkzHO+xyZs/qaDsTc3Y208FF7Fj2 +W8S/oP/bKvnOGI05jocmwR8Oso8KmzgjdrnEfGOVXFDnJH+oN2UeYxwph+ddJzUQ +JVVmc+xZAgMBAAGjgaswgagwHQYDVR0OBBYEFPwzZRN6d8nHri5XyzYSEcwnRtZ0 +MC8GA1UdEQQoMCaCDW5zMi5leGFtcGxlLmmCD25zMi5leGFtcGxlLm9yZ4cEAQEB +AjALBgNVHQ8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAkG +A1UdEwQCMAAwHwYDVR0jBBgwFoAUtawDyvuxbxzzucYDglBxJtiVkYEwDQYJKoZI +hvcNAQELBQADgYEAmMBg3ETAY8EfKrGOmghVjIKZLXDDdE8BDoJuIFvn0A1y52aq +YZqsv3R5oYJkrejwh/raE3opNOuTT4LdHE4W09cwwctz4TYOS2Xfy713qfp1QUVo +9q6fFwLbICpScECtk8e5c7JPpOi7utPDbX37gyE9VZ9varmBekQ//bflhSI= +-----END CERTIFICATE----- diff --git a/test/provider/files/nodes/ns2/ns2.key b/test/provider/files/nodes/ns2/ns2.key new file mode 100644 index 0000000..b74914c --- /dev/null +++ b/test/provider/files/nodes/ns2/ns2.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQCfCRAqN3pqoKSglBEoJVgHx5Y0ggNMzE/DTc178ni018BS2b7Q +zwTeg3DdWAWTdLSR6tttn5ZMxzvscmbP6mg7E3N2NtPBRexY9lvEv6D/2yr5zhiN +OY6HJsEfDrKPCps4I3a5xHxjlVxQ5yR/qDdlHmMcKYfnXSc1ECVVZnPsWQIDAQAB +AoGABHSQi146g7o0YntDb8h8CtvAjYAG76PZqDMJyqskToysyqVm/xqNnF46Tzkk +Dtl6JYxa0VtjLot2Vk1uK+z5NoMoN6J9pQkH6zVVAh5FnQdTWKCRSBLiC2FqSh3z +cbgn1ZwheeUo/Vc0zvJm4RGQ5gMGjBZEU89CHutzgkSxMzECQQDScrFtt+AWemCN +BlHSYJxcX6d7FS0ks2WVka2sXXj/1KolOfTV8NFbBhtagBxR7Orov9L6VtFfXrQK +tLBi71aTAkEAwXWA7BFZSGkDZEiym9wYEfvZ3Z9zlEghpHkhCW9Yd9/22hyyKLR+ +rgu69T3Wudnfukz19+sUYDumul1xHc444wJAFyif9d8CPfcBoQNNBcWz70Zne9f8 +u8kyKJ97aThwFFcm0inqk5CIuWeWowLuGuXjg/F4Gixrpf8Z+QOhVYHZGQJBALxO +1B71BCMnlNWYrcJoikV3EKpY+vfq/lRKU44Lg+Grb2z/YaudhXGEmYb9mnVtTgjZ +wNKBUGQbrD7bla+dfGECQQCDkDXPqK1UDxM0YIYG+gxW3BQr/q/3XUZs/2/X7PuU +aa0psnl5OcS3RkomavWKVXUpnwG3CSHBRQQ5xFNCPVNG +-----END RSA PRIVATE KEY----- diff --git a/test/provider/files/nodes/pcouch1/pcouch1.crt b/test/provider/files/nodes/pcouch1/pcouch1.crt new file mode 100644 index 0000000..5cbf7c3 --- /dev/null +++ b/test/provider/files/nodes/pcouch1/pcouch1.crt @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICozCCAgygAwIBAgIQd+WMYQcsfEJ7tKGgTQPhmzANBgkqhkiG9w0BAQsFADBK +MRAwDgYDVQQKDAdFeGFtcGxlMRwwGgYDVQQLDBNodHRwczovL2V4YW1wbGUub3Jn +MRgwFgYDVQQDDA9FeGFtcGxlIFJvb3QgQ0EwIBcNMTYwOTI4MDAwMDAwWhgPMjEx +NjA5MjgwMDAwMDBaMB4xHDAaBgNVBAMME3Bjb3VjaDEuZXhhbXBsZS5vcmcwgZ8w +DQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMDRNMvBNsLhOchxLHX6S+kTMZSRBbUD +bVV4QnVzXNlhOld42QcWh0sETjpBISsJe9se3qrbwBfQKCzbguJYCnLOa8q8sJhk +AM+VgYoSESAFRlWeu0CDXGH3FaLVHso7OSNFliq39h+LiPPAcCkli55rHwaTqWY/ +wrN8nU8CA0ynAgMBAAGjgbMwgbAwHQYDVR0OBBYEFN9u3kcthGftJHfo89tT+a/9 +ZVNoMDcGA1UdEQQwMC6CEXBjb3VjaDEuZXhhbXBsZS5pghNwY291Y2gxLmV4YW1w +bGUub3JnhwQLAAACMAsGA1UdDwQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYI +KwYBBQUHAwIwCQYDVR0TBAIwADAfBgNVHSMEGDAWgBS1rAPK+7FvHPO5xgOCUHEm +2JWRgTANBgkqhkiG9w0BAQsFAAOBgQBYajPrmFVBzJXxwBe7DN5giQ9VCM71XMVj +OMN2fAnrHKgozgnRxn2ZtyxI3vvMMal/n2ZUax0ku0XdFXJouZUhF3PNtu5fpFrJ +fngJnUeMY4bHneqG3iR4We6trkVIn1/b9CA8qqXsChF33LGQptCGnGe7x4zalBeX +b7xeGhepdw== +-----END CERTIFICATE----- diff --git a/test/provider/files/nodes/pcouch1/pcouch1.key b/test/provider/files/nodes/pcouch1/pcouch1.key new file mode 100644 index 0000000..36e3d44 --- /dev/null +++ b/test/provider/files/nodes/pcouch1/pcouch1.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQDA0TTLwTbC4TnIcSx1+kvpEzGUkQW1A21VeEJ1c1zZYTpXeNkH +FodLBE46QSErCXvbHt6q28AX0Cgs24LiWApyzmvKvLCYZADPlYGKEhEgBUZVnrtA +g1xh9xWi1R7KOzkjRZYqt/Yfi4jzwHApJYueax8Gk6lmP8KzfJ1PAgNMpwIDAQAB +AoGAf5ZvnxwdBltOhwoMZ4zWSkY/GpXT9vFrmZDYOSu7FsS1fEglJAGOSN9yfC24 +que9o0MMCHcc5yUAUJ54PxoO3rxFc9WRJFKT7jnPabGWjAwynFCEW/okM4gV6KBc +dw6jmQFLAAC2jRyUZhGP4zuDo9+P4zJ3D84J4mW8wh8MIsECQQD/SHehLFj/feLF +8kpXAF3bvNB+DK8iPDfbzgPoxhV3yX2/Jai7xhapiRLqekA66EVs4kwmJqlZdJAF +D8nZLHHXAkEAwVvUcQZzP5RJoogh7+LhbuiAC1HrY5qciIdJ+VbI+z3TSCYATeuw +IHVufX6u6jIyqYPqttbYfydgEmlhn4dBsQJBAPyOcDQXENFrdKBLLXrXVQQgz8/0 +sotXMhgWwE1ZM0H4KJykIEPtHNyLTRiG6+abhpvLYnTYCPEEXbt0PEjMLK8CQE4U +JtT9JcymtJVNI2ca1q1SdWIc0lCGPm9jMhvdT4skjAy2S6krYxO4V8WVQkyPuKV6 +/2yVlRbDb6f/pcwlcgECQQDx+bypDnhmlINXQIy6fktRDJsPrBNcRyrlrxcRNPMU +Qv/AcFVYpxhwf8Jg688RKcHhk00Ga1pkF6gKQooFTETR +-----END RSA PRIVATE KEY----- diff --git a/test/provider/files/nodes/pweb1/pweb1.crt b/test/provider/files/nodes/pweb1/pweb1.crt new file mode 100644 index 0000000..7ec04f5 --- /dev/null +++ b/test/provider/files/nodes/pweb1/pweb1.crt @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC0TCCAjqgAwIBAgIRALfSNQhp6ztK+6EupzNn0CowDQYJKoZIhvcNAQELBQAw +SjEQMA4GA1UECgwHRXhhbXBsZTEcMBoGA1UECwwTaHR0cHM6Ly9leGFtcGxlLm9y +ZzEYMBYGA1UEAwwPRXhhbXBsZSBSb290IENBMCAXDTE2MDkyODAwMDAwMFoYDzIx +MTYwOTI4MDAwMDAwWjAcMRowGAYDVQQDDBFwd2ViMS5leGFtcGxlLm9yZzCBnzAN +BgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAxfB90ytpi5oTo6vPq99HDg3Ci13wiegj +bBgqHMAo6jXoxT/3nq+1KE1ThqpNIvpuc6EgWCh5jfzK1hk8aFfWkXWLiPmGTelo +I84FKUa4zigQdJsaCU8aUj8oxT9eO0oXGR9Hv9Es8KVe5InFYBz54v/SwYbrunXS +vzXH1EpWIq0CAwEAAaOB4jCB3zAdBgNVHQ4EFgQUeAGn9QLkE31Y8tIC4H/XKLT/ +hDEwZgYDVR0RBF8wXYIPYXBpLmV4YW1wbGUub3JnggtleGFtcGxlLm9yZ4ITbmlj +a255bS5leGFtcGxlLm9yZ4IPcHdlYjEuZXhhbXBsZS5pghFwd2ViMS5leGFtcGxl +Lm9yZ4cECwAAATALBgNVHQ8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG +AQUFBwMCMAkGA1UdEwQCMAAwHwYDVR0jBBgwFoAUtawDyvuxbxzzucYDglBxJtiV +kYEwDQYJKoZIhvcNAQELBQADgYEAs9F+A9JOtU+7UHhhmf2DVFFGb+n7iTOaUzDv +/1nn3OyD1hY3kMsJWcZUuAiIGB0ZBNzStalFADNXy8QDI9xRwC1bt+if5I3XK8Ag +563xBpkSXtVp3IY7YHmxJu3j/6R/HOa3xIAkmpEryJ+r8XZgOF+gim+HmDOjpBOI +dKfZcFk= +-----END CERTIFICATE----- diff --git a/test/provider/files/nodes/pweb1/pweb1.key b/test/provider/files/nodes/pweb1/pweb1.key new file mode 100644 index 0000000..356ac6f --- /dev/null +++ b/test/provider/files/nodes/pweb1/pweb1.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQDF8H3TK2mLmhOjq8+r30cODcKLXfCJ6CNsGCocwCjqNejFP/ee +r7UoTVOGqk0i+m5zoSBYKHmN/MrWGTxoV9aRdYuI+YZN6WgjzgUpRrjOKBB0mxoJ +TxpSPyjFP147ShcZH0e/0SzwpV7kicVgHPni/9LBhuu6ddK/NcfUSlYirQIDAQAB +AoGADP18dHhb4+KHuW0UIvZzRlPW2aifmZ1XfceUM/DUfpJtJUzOZmann+57NdJF +X69JwmLnqYF2gL//W8+qLDrfhOzC5Qr3m4lFIvACmmh0Aj3u47k6W4pJryAp1B0f +Khiql1TZ006EzVRf+2h0pdVK2C1vGOEyhBMagytHXLFZCyECQQDicOqK25JL0n9J +t7+ZviZknLLAW15+P3I7oehZlUtN9CleBA0m/DrMX8oepKoPomK0tXs7pT6ZGZza ++8IxD88pAkEA38cfXPNRjm0YsXJZuIBblHt0tU0dajo0Ac2tKih0qtltnZoc8usj +p0ci9qRLg1Tp7Tu7DopSVtNOpphoeySb5QJBALGU5Bspvz1/QxvI4pXrrahRy01X +Wm+fyjJB8znt/zSPOrHkc3wTavlEVfpaIJRKQSZ+/Ln2CXV/xKdnsQ9Q2qECQQCk +KsHAgCTR1wlpjJlzuH73BEcPht5QgxiKRiiGqB1HBbHcECays3x5iL+Gr+tSEuZ2 +iv5k4WccmXK211K3HJldAkBrvf1NueFdH1OIAn7v1HFSXTzy8xPjUKLI6Ez6y2O5 +A6k4jqXnyCko2rJsae90xe5F5E2eW8W2h9+elBmGYTtJ +-----END RSA PRIVATE KEY----- diff --git a/test/provider/files/nodes/vpn1/vpn1.crt b/test/provider/files/nodes/vpn1/vpn1.crt new file mode 100644 index 0000000..41f5c13 --- /dev/null +++ b/test/provider/files/nodes/vpn1/vpn1.crt @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICmzCCAgSgAwIBAgIRANBnTjUGZeOrBzKeKeuhf+8wDQYJKoZIhvcNAQELBQAw +SjEQMA4GA1UECgwHRXhhbXBsZTEcMBoGA1UECwwTaHR0cHM6Ly9leGFtcGxlLm9y +ZzEYMBYGA1UEAwwPRXhhbXBsZSBSb290IENBMCAXDTE2MDkyODAwMDAwMFoYDzIx +MTYwOTI4MDAwMDAwWjAbMRkwFwYDVQQDDBB2cG4xLmV4YW1wbGUub3JnMIGfMA0G +CSqGSIb3DQEBAQUAA4GNADCBiQKBgQDADq6rtpObpScLStIREnPTxwOpqc71cPUa +OYy4C9gZIcqTxYcgAv8UF5DdV8dDBMLC2s2XdwcyGjDg2ElkVaKpqaGfPMKPnQ5u +ALtGQy+DFyQhfYRxUtlC3EATNLe3JJHTlRNI2VPzcVyOHpBBPa1PZuq/peq7HQ3V +hznHeTDZxQIDAQABo4GtMIGqMB0GA1UdDgQWBBSxyVFLKJYHJBbjl6hQePY6LNmS +nzAxBgNVHREEKjAogg52cG4xLmV4YW1wbGUuaYIQdnBuMS5leGFtcGxlLm9yZ4cE +CgUFAzALBgNVHQ8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC +MAkGA1UdEwQCMAAwHwYDVR0jBBgwFoAUtawDyvuxbxzzucYDglBxJtiVkYEwDQYJ +KoZIhvcNAQELBQADgYEApW4Vz9mqxw975Mw32S9FwrpkufC8+sGir4/xCP8Q1xg4 +pg8SvSaMdoPHKPkevHx8I3QY3H+l2XkZEtArgv8jjbpvPQkIvHVx9iUMRXWwxlTI +B5d1V4QCzow829JFyy8giRANK+dZF5pp4+G0+f0IJgQM52U+y6XKIL8DZvfarSQ= +-----END CERTIFICATE----- diff --git a/test/provider/files/nodes/vpn1/vpn1.key b/test/provider/files/nodes/vpn1/vpn1.key new file mode 100644 index 0000000..1ea72ad --- /dev/null +++ b/test/provider/files/nodes/vpn1/vpn1.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDADq6rtpObpScLStIREnPTxwOpqc71cPUaOYy4C9gZIcqTxYcg +Av8UF5DdV8dDBMLC2s2XdwcyGjDg2ElkVaKpqaGfPMKPnQ5uALtGQy+DFyQhfYRx +UtlC3EATNLe3JJHTlRNI2VPzcVyOHpBBPa1PZuq/peq7HQ3VhznHeTDZxQIDAQAB +AoGAF8x59os8RUg0y2BtIXJw6eg6WvbQz3c82BATkObe01ZtnNwYP24/n4TADb2H +0pUvcSfd3AwC10GJlwMWLRmzey07cbDSm77YSMoDNFuLNxxiSDJsR7kSeGjzTz92 +YEC59cNus8j5ExPCGnx4OVz264dpUyvbJKLbcX6hwO3cFc0CQQD9HvDe0kRVMug2 +2I1IUeF8QEt9B0lauq/mCapY1AWEJ2Y0rzg3nListOaHetTdNuLVsKc4UyGrF62D +MNK97UnXAkEAwj3ucfpdE7eZAiee4vwP4sg0C4HJEsAdhkhO/E6hKxRijTvVoWep +1Gkq/gwxO7qoAnaBzfLgBo840LUWKIXdwwJBAN2ykPQIpJMe8GbBSxVxqhZC1htf +G2+dHd1Uz9/XbDFwtMMmSQ3kdZfHJja5beGHZiwV+pCJt258YZwLUjnJsKcCQQCM +K17vlyklul7LJEZPLHBWSfzstNqiEkr8BSAiiKdbTBmWK7CNCh6O7tmcfLXmkVr+ +dABV20d41E++pH75/Sg7AkEA4yk7pPDe6A4IdWz5BEXOENG47qnQGbRgrtD8svee +J9yAujIm84up14Fv8WObuyHR7xVjhOhLBKo3cVbfnwY3Vg== +-----END RSA PRIVATE KEY----- diff --git a/test/provider/files/nodes/web1/web1.crt b/test/provider/files/nodes/web1/web1.crt new file mode 100644 index 0000000..915a84e --- /dev/null +++ b/test/provider/files/nodes/web1/web1.crt @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIICzjCCAjegAwIBAgIRAMoUUP0EOpqOEeOkAm6kbN0wDQYJKoZIhvcNAQELBQAw +SjEQMA4GA1UECgwHRXhhbXBsZTEcMBoGA1UECwwTaHR0cHM6Ly9leGFtcGxlLm9y +ZzEYMBYGA1UEAwwPRXhhbXBsZSBSb290IENBMCAXDTE2MDkyODAwMDAwMFoYDzIx +MTYwOTI4MDAwMDAwWjAbMRkwFwYDVQQDDBB3ZWIxLmV4YW1wbGUub3JnMIGfMA0G +CSqGSIb3DQEBAQUAA4GNADCBiQKBgQDS0Q+RQuBrFcVjaG44p6JBGYBtTeS1hpID +1yosGMftjVXnWYF8zi8XoNZr2Cp4g/BHb4OyC43C3f4sPx6qIU/Qt7fVfwKdV+A9 +c9PcvUE/RLhZMlzTu5UwBOWNOndQ2clkap/dyhfiRt0aAExh9IzfWyQJSDUiH9Ys +5jBE+5jMowIDAQABo4HgMIHdMB0GA1UdDgQWBBSSToilj03s0BIzdNb4p5WhAFpa +LDBkBgNVHREEXTBbgg9hcGkuZXhhbXBsZS5vcmeCC2V4YW1wbGUub3JnghNuaWNr +bnltLmV4YW1wbGUub3Jngg53ZWIxLmV4YW1wbGUuaYIQd2ViMS5leGFtcGxlLm9y +Z4cEBgYHBzALBgNVHQ8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUF +BwMCMAkGA1UdEwQCMAAwHwYDVR0jBBgwFoAUtawDyvuxbxzzucYDglBxJtiVkYEw +DQYJKoZIhvcNAQELBQADgYEAdcbYb1C0+thmBXyN7xcoSGvbHoIVbXvBYKi14hxT +6P/ZnI0zAQVWHhOliXXqTOGRCc5GWFUp6MufZtWd/yHhkxf1cCjSfnvqVAv7rtx5 +crECppsXCVFHyuLXvNfAS0y+FuqmK2pBZUdVXv1bXSYNN5ZcMwFacI0UGoOwN/65 +LOc= +-----END CERTIFICATE----- diff --git a/test/provider/files/nodes/web1/web1.key b/test/provider/files/nodes/web1/web1.key new file mode 100644 index 0000000..ecb2485 --- /dev/null +++ b/test/provider/files/nodes/web1/web1.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDS0Q+RQuBrFcVjaG44p6JBGYBtTeS1hpID1yosGMftjVXnWYF8 +zi8XoNZr2Cp4g/BHb4OyC43C3f4sPx6qIU/Qt7fVfwKdV+A9c9PcvUE/RLhZMlzT +u5UwBOWNOndQ2clkap/dyhfiRt0aAExh9IzfWyQJSDUiH9Ys5jBE+5jMowIDAQAB +AoGBAKEljXDMXh99FNVYDmjgOvboN3NWB2164EJvRp1OlATR9MhTctekA/tbxovJ +QS2+LP1uEI0Yp9Q9PP01gospy4erJTWdDmzKotrA3DSjw6Gr5EW1rVes93eM+Uqg +u70ETfZVwfp7+iB7OjUWZHrylt6ISPs2rbW8QSHPr9L4NUsBAkEA/2w/R+ph4XtN +D5j4PWtr6pRzcdno3jkWF3xrx6YM+tnN0qkuXoIEGsGWeGPPiGh12Ys2zwQwmTbA +CoBFPHeMKwJBANNLArQCBjA+UcCWMZoe5NoRp/hXBjPekmxiYyJ+XnGQCOEZWf0E +rMKUHNVe9TzfubhW/Cydiwag6CcQjCymbWkCQQDwhO09+i67llEle+VeaNZRKgNf +1VPcVqM/8HDJqsqUOR8A3UEFy6azz1GzAkH98GfxN4+f9xEQZacG/Gy2GNjLAkBj +Duash8po8b6YIJIOpG88QUzTY9E3niBdid7aPA6BBTr0dVM4COoJqzC9Y/BrYqQK +ZVWCgTW9nNBaCCr/f+MJAkEAxPVu3x0OL3WILkKhR37zAaFRoqWH4JZPM9LkKaYZ +Br7RdR3kBFzDAbxe7InXJ5/ZtWh4wFsqPtccHLuT4JcllA== +-----END RSA PRIVATE KEY----- diff --git a/test/provider/provider.json b/test/provider/provider.json index d0f8abf..f7c1df0 100644 --- a/test/provider/provider.json +++ b/test/provider/provider.json @@ -2,12 +2,12 @@ // General service provider configuration. // { - "domain": "bitmask.net", + "domain": "example.org", "name": { - "en": "Bitmask" + "en": "Example" }, "description": { - "en": "A demonstration service provider using the LEAP platform" + "en": "CI test" }, "languages": ["en"], "default_language": "en", diff --git a/test/provider/secrets.json b/test/provider/secrets.json deleted file mode 100644 index ffadc24..0000000 --- a/test/provider/secrets.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "default": { - "api_monitor_auth_token": "UrmuBDZkA9XTsfaq4kpjbtshHY5daUxX", - "couch_admin_password": "TDMmtYBmm4r5dI4VXPXnxXsKkLfFPEPR", - "couch_admin_password_salt": "8b2db5d295e54bdef430aae96b955845", - "couch_leap_mx_password": "YXhAyvm57XgwhIZNYqxF3g8ykzhkg4SF", - "couch_leap_mx_password_salt": "ef432b612887112fd227de859ab78521", - "couch_nickserver_password": "sjNIQ98ymFwaAHyIX4XJKraNmwdHgBw9", - "couch_nickserver_password_salt": "7b932afd1c2ffc42763d340e4e8b2bcd", - "couch_replication_password": "UZne4MrH5HzNAamMeYReHjW7LJLabDZJ", - "couch_replication_password_salt": "341d5e378e3a1bffaa709dcca9bcd465", - "couch_soledad_password": "wVLLKJCLzmbkPNfzhLbPy3gjWhhBMRhF", - "couch_soledad_password_salt": "e40a4751078ffa0f364a77a486d0dc4c", - "couch_webapp_password": "LRQUHweyjIFnELw4sQT8pveEUqKhIxLU", - "couch_webapp_password_salt": "fbb4fa950d30e524b10775c6aa712564", - "nagios_test_password": "4XpCbaFbcAAcfPqAqMtXMdMpUWengLEk", - "webapp_secret_token": "BzWmcgK4Xf7xgmkdYHZK2qKBM2YT2ffM" - }, - "local": { - "api_monitor_auth_token": "BUKNpTd9CPWcebeIXcSrmUmcXZZw3HEz", - "couch_admin_password": "mw2yxDQWw2HzTn5cIkBVnJhZJ5VXVEgZ", - "couch_admin_password_salt": "bbacf42821cee0af5a2fd638d014f939", - "couch_leap_mx_password": "Ray9PHuEUKscNQsIenpsfgbM2u2WBzPq", - "couch_leap_mx_password_salt": "d0dc07939c3f45a57954343f0e5fa13a", - "couch_nickserver_password": "pbXQcHXQ5cR9xwk9xsAwMCQ8mfLpvMmE", - "couch_nickserver_password_salt": "70cbc22a8603732bb6161f6e978d4abe", - "couch_replication_password": "aDgQI87unwHqkJWPxchayQpf7taUPTYe", - "couch_replication_password_salt": "6faaec5dc8c0ac5db9da91e01fc379a8", - "couch_soledad_password": "uEN8sfF3xXbhHg2WjpCVQyUy7LrkfTnA", - "couch_soledad_password_salt": "0db6d77f631df372bacc63dddea89e55", - "couch_webapp_password": "RT7D7KTjzuVdXXs5HDYTIMpdDFfJKeZu", - "couch_webapp_password_salt": "d8a7fb6c2f258137a4946ccb931d4e53", - "nagios_test_password": "FfbLyjPIQUBDvnHtVNCwHZsZ9UYfZdqa", - "scramblesuit_password_vpn1": "GJ2TSRLYKJLVAU2JKNNEIYSDKBKEGZ2R", - "scramblesuit_port_vpn1": 31531 - }, - "production": { - "api_monitor_auth_token": "TFkfYQHp5AMJmSY27YrPngg7sk5DtvBB", - "couch_admin_password": "Hqu7IhKmFHVpHU9pgTHffQYzh7ZWHc5B", - "couch_admin_password_salt": "8e7865b9e5263d06e1f74aea3dd44dd2", - "couch_leap_mx_password": "AMrrWcKnFbbhaBj4MxxgTFeHnNnHjQay", - "couch_leap_mx_password_salt": "2960d63958d067654be8c8d44131cd94", - "couch_nickserver_password": "WPUfpbEHu4d5FHTWgrefgrYHaKCsQKYX", - "couch_nickserver_password_salt": "983b745e70c31d811c876ca2c44d2ed0", - "couch_replication_password": "ImeBu2DIA3gRbrHcqHgzsFBYHkwbeJQS", - "couch_replication_password_salt": "54c09b42eb697972a4d7faabc9b4f2a6", - "couch_soledad_password": "fNbUdYdErwnfFCKZUHLBaLmYfnxIjEbW", - "couch_soledad_password_salt": "81cab24a5881de53ac79b4797b467d9f", - "couch_webapp_password": "8tFtJ84rYa59ECjrMbVUQVCjp4YhhK7F", - "couch_webapp_password_salt": "559eeeaa6ccd25169c9358c6c90eb24b", - "nagios_test_password": "8cuLRjYICKFPe4YaKwk22EytRsjQKP9X", - "webapp_secret_token": "4UQKXV94xqtFVkNSCqrphdNFJaPkQBx8" - } -} diff --git a/test/provider/users/duck/duck_ssh.pub b/test/provider/users/duck/duck_ssh.pub deleted file mode 100644 index 591f614..0000000 --- a/test/provider/users/duck/duck_ssh.pub +++ /dev/null @@ -1 +0,0 @@ -ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDezTqhg/zFkGOQia0QRVRaDUmgdc73CEXadwVgYN41PITesjQinyT4hMOO8BJZVV70W1dWWCtT2j3JTFWLvhpgbjlYdiG676i9UpARvHTdt1FTAmlWfEfKvhDTqPByFyUooYfXBbpcZtqw+5ChP/lIjfWmfUVS3phTm5LzMetWTXY//dmuF+sHU9ZAWvrkYVI+IuJvb3mxv+CEbpS5s9yTS56qPP2czETbANoXsbBa29Ag+x22X/OiEUZ/mAfEuqBGh2uKH+9I/HhjorXSflYcwVhgA5P6QAhZEKU+B/PprIX/dF0HZLayJ6Y+0E7uUzNKxHupHmPI03VbxRO74K9t duck@home diff --git a/test/test_helper.rb b/test/test_helper.rb index f7ec6d9..cd856a3 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -7,6 +7,7 @@ require 'gli' require 'fileutils' DEBUG = true +TEST = true module LeapCli::Commands extend GLI::App @@ -19,7 +20,7 @@ class Minitest::Test def initialize(*args) super(*args) - LeapCli::Bootstrap::setup([], test_provider_path) + LeapCli::Bootstrap::setup([], provider_path) LeapCli::Bootstrap::load_libraries(LeapCli::Commands) end @@ -43,33 +44,56 @@ class Minitest::Test end def leap_bin(*args) - `cd #{test_provider_path} && #{ruby_path} #{base_path}/bin/leap --no-color #{args.join ' '}` + cmd = "cd #{provider_path} && PLATFORM_DIR=#{platform_path} #{base_path}/bin/leap --debug --yes --no-color #{args.join ' '}" + `#{cmd}` end - def test_provider_path + def leap_bin!(*args) + output = leap_bin(*args) + exit_code = $? + assert_equal 0, exit_code, + "The command `leap #{args.join(' ')}` should have exited 0 " + + "(was #{exit_code}).\n" + + "Output was: #{output}" + output + end + + def provider_path "#{base_path}/test/provider" end + # + # for tests, we assume that the leap_platform code is + # in a sister directory to leap_cli. + # + def platform_path + ENV['PLATFORM_DIR'] || "#{base_path}/../leap_platform" + end + def cleanup_files(*args) - Dir.chdir(test_provider_path) do + Dir.chdir(provider_path) do args.each do |file| FileUtils.rm_r(file) if File.exist?(file) end end end + # + # we no longer support ruby 1.8, but this might be useful in the future + # def with_multiple_rubies(&block) - if ENV["RUBY"] - ENV["RUBY"].split(',').each do |ruby| - self.ruby_path = `which #{ruby}`.strip - next unless ruby_path.chars.any? - yield - end - else - self.ruby_path = `which ruby`.strip - yield - end - self.ruby_path = "" + yield + # if ENV["RUBY"] + # ENV["RUBY"].split(',').each do |ruby| + # self.ruby_path = `which #{ruby}`.strip + # next unless ruby_path.chars.any? + # yield + # end + # else + # self.ruby_path = `which ruby`.strip + # yield + # end + # self.ruby_path = "" end end diff --git a/test/unit/command_line_test.rb b/test/unit/command_line_test.rb index 0b57ed0..393bcf2 100644 --- a/test/unit/command_line_test.rb +++ b/test/unit/command_line_test.rb @@ -1,4 +1,4 @@ -require File.expand_path('../test_helper', __FILE__) +require_relative 'test_helper' class CommandLineTest < Minitest::Test @@ -13,7 +13,7 @@ class CommandLineTest < Minitest::Test with_multiple_rubies do output = leap_bin('list') assert_equal 0, $?, "list should exit 0" - assert output =~ /ns1 dns/m + assert output =~ /ns1 dns/m end end @@ -21,7 +21,8 @@ class CommandLineTest < Minitest::Test cleanup_files('nodes/banana.json', 'files/nodes/banana') output = leap_bin("node add banana tags:production "+ "services:openvpn ip_address:1.1.1.1 openvpn.gateway_address:2.2.2.2") - assert_match /created nodes\/banana\.json/, output + assert_match(/created nodes\/banana\.json/, output) + cleanup_files('nodes/banana.json', 'files/nodes/banana') end end diff --git a/test/unit/config_object_list_test.rb b/test/unit/config_object_list_test.rb index 9b6e09f..042a742 100644 --- a/test/unit/config_object_list_test.rb +++ b/test/unit/config_object_list_test.rb @@ -1,4 +1,4 @@ -require File.expand_path('../test_helper', __FILE__) +require_relative 'test_helper' class ConfigObjectListTest < Minitest::Test @@ -9,7 +9,6 @@ class ConfigObjectListTest < Minitest::Test end def test_complex_node_search - domain = provider.domain nodes = manager.nodes['location.country_code' => 'US'] assert nodes.size != manager.nodes.size, 'should not return all nodes' assert_equal 2, nodes.size, 'should be some nodes' diff --git a/test/unit/config_object_test.rb b/test/unit/config_object_test.rb index 54b45d1..88e11e6 100644 --- a/test/unit/config_object_test.rb +++ b/test/unit/config_object_test.rb @@ -1,4 +1,4 @@ -require File.expand_path('../test_helper', __FILE__) +require_relative 'test_helper' class ConfigObjectTest < Minitest::Test diff --git a/test/unit/quick_start_test.rb b/test/unit/quick_start_test.rb new file mode 100644 index 0000000..d26f9c8 --- /dev/null +++ b/test/unit/quick_start_test.rb @@ -0,0 +1,127 @@ +require_relative 'test_helper' + +# +# Runs all the commands in https://leap.se/quick-start +# + +Minitest.after_run { + FileUtils.rm_r(QuickStartTest::TMP_PROVIDER) +} + +class QuickStartTest < Minitest::Test + + # very reasonable to have ordered tests in this case, actually + i_suck_and_my_tests_are_order_dependent! + + TMP_PROVIDER = Dir.mktmpdir("test_leap_provider_") + + # + # use minimal bit sizes for our test. + # + PROVIDER_JSON = <<HERE +{ + "domain": "example.org", + "name": { + "en": "Example" + }, + "description": { + "en": "Example" + }, + "languages": ["en"], + "default_language": "en", + "enrollment_policy": "open", + "contacts": { + "default": "root@localhost" + }, + "ca": { + "bit_size": 1024, + "client_certificates": { + "bit_size": 1024, + "digest": "SHA1", + "life_span": "100 years" + }, + "life_span": "100 years", + "server_certificates": { + "bit_size": 1024, + "digest": "SHA1", + "life_span": "100 years" + } + } +} +HERE + + def provider_path + TMP_PROVIDER + end + + def test_01_new + output = leap_bin! "new --contacts me@example.org --domain example.org --name Example --platform='#{platform_path}' ." + assert_file "Leapfile" + assert_file "provider.json" + assert_dir "nodes" + File.write(File.join(provider_path, 'provider.json'), PROVIDER_JSON) + end + + def test_02_ca + leap_bin! "cert ca" + assert_dir "files/ca" + assert_file "files/ca/ca.crt" + assert_file "files/ca/ca.key" + end + + def test_03_csr + leap_bin! "cert csr" + assert_file "files/cert/example.org.csr" + assert_file "files/cert/example.org.crt" + assert_file "files/cert/example.org.key" + end + + def test_04_nodes + leap_bin! "node add wildebeest ip_address:1.1.1.2 services:webapp,couchdb" + leap_bin! "node add hippo ip_address:1.1.1.3 services:static" + assert_file "nodes/wildebeest.json" + assert_dir "files/nodes/wildebeest" + assert_file "files/nodes/wildebeest/wildebeest.crt" + assert_file "files/nodes/wildebeest/wildebeest.key" + end + + def test_05_compile + user_dir = File.join(provider_path, 'users', 'dummy') + user_key = File.join(user_dir, 'dummy_ssh.pub') + FileUtils.mkdir_p(user_dir) + File.write(user_key, 'ssh-rsa dummydummydummy') + + leap_bin! "compile" + assert_file "hiera/wildebeest.yaml" + assert_file "hiera/hippo.yaml" + end + + def test_06_rename + leap_bin! "node mv hippo hippopotamus" + assert_file "nodes/hippopotamus.json" + assert_dir "files/nodes/hippopotamus" + assert_file "files/nodes/hippopotamus/hippopotamus.key" + end + + def test_07_rm + leap_bin! "node rm hippopotamus" + assert_file_missing "nodes/hippopotamus.json" + assert_file_missing "files/nodes/hippopotamus/hippopotamus.key" + end + + def assert_file(path) + assert File.exist?(File.join(provider_path, path)), "The file `#{path}` should exist in #{provider_path}. Actual: \n#{provider_files}" + end + + def assert_file_missing(path) + assert !File.exist?(File.join(provider_path, path)), "The file `#{path}` should NOT exist in #{provider_path}." + end + + def assert_dir(path) + assert Dir.exist?(File.join(provider_path, path)), "The directory `#{path}` should exist in #{provider_path}. Actual: \n#{provider_files}" + end + + def provider_files + `cd #{provider_path} && find .` + end +end diff --git a/test/unit/test_helper.rb b/test/unit/test_helper.rb index 25a36de..057e4b7 100644 --- a/test/unit/test_helper.rb +++ b/test/unit/test_helper.rb @@ -1 +1 @@ -require File.expand_path('../../test_helper', __FILE__) +require_relative '../test_helper' diff --git a/vendor/acme-client/Gemfile b/vendor/acme-client/Gemfile new file mode 100644 index 0000000..e0b10df --- /dev/null +++ b/vendor/acme-client/Gemfile @@ -0,0 +1,12 @@ +source 'https://rubygems.org' +gemspec + +group :development, :test do + gem 'pry' + gem 'rubocop', '0.36.0' + gem 'ruby-prof', require: false + + if Gem::Version.new(RUBY_VERSION) <= Gem::Version.new('2.2.2') + gem 'activesupport', '~> 4.2.6' + end +end diff --git a/vendor/acme-client/LICENSE.txt b/vendor/acme-client/LICENSE.txt new file mode 100644 index 0000000..73b96b4 --- /dev/null +++ b/vendor/acme-client/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Charles Barbier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/acme-client/README.md b/vendor/acme-client/README.md new file mode 100644 index 0000000..2047885 --- /dev/null +++ b/vendor/acme-client/README.md @@ -0,0 +1,168 @@ +# Acme::Client +[![Build Status](https://travis-ci.org/unixcharles/acme-client.svg?branch=master)](https://travis-ci.org/unixcharles/acme-client) + +`acme-client` is a client implementation of the [ACME](https://letsencrypt.github.io/acme-spec) protocol in Ruby. + +You can find the ACME reference implementations of the [server](https://github.com/letsencrypt/boulder) in Go and the [client](https://github.com/letsencrypt/letsencrypt) in Python. + +ACME is part of the [Letsencrypt](https://letsencrypt.org/) project, which goal is to provide free SSL/TLS certificates with automation of the acquiring and renewal process. + +## Installation + +Via Rubygems: + + $ gem install acme-client + +Or add it to a Gemfile: + +```ruby +gem 'acme-client' +``` + +## Usage + +### Register client + +In order to authenticate our client, we have to create an account for it. + +```ruby +# We're going to need a private key. +require 'openssl' +private_key = OpenSSL::PKey::RSA.new(4096) + +# We need an ACME server to talk to, see github.com/letsencrypt/boulder +# WARNING: This endpoint is the production endpoint, which is rate limited and will produce valid certificates. +# You should probably use the staging endpoint for all your experimentation: +# endpoint = 'https://acme-staging.api.letsencrypt.org/' +endpoint = 'https://acme-v01.api.letsencrypt.org/' + +# Initialize the client +require 'acme-client' +client = Acme::Client.new(private_key: private_key, endpoint: endpoint, connection_options: { request: { open_timeout: 5, timeout: 5 } }) + +# If the private key is not known to the server, we need to register it for the first time. +registration = client.register(contact: 'mailto:contact@example.com') + +# You may need to agree to the terms of service (that's up the to the server to require it or not but boulder does by default) +registration.agree_terms +``` + +### Authorize for domain + +Before you are able to obtain certificates for your domain, you have to prove that you are in control of it. + +```ruby +authorization = client.authorize(domain: 'example.org') + +# If authorization.status returns 'valid' here you can already get a certificate +# and _must not_ try to solve another challenge. +authorization.status # => 'pending' + +# You can can store the authorization's URI to fully recover it and +# any associated challenges via Acme::Client#fetch_authorization. +authorization.uri # => '...' + +# This example is using the http-01 challenge type. Other challenges are dns-01 or tls-sni-01. +challenge = authorization.http01 + +# The http-01 method will require you to respond to a HTTP request. + +# You can retrieve the challenge token +challenge.token # => "some_token" + +# You can retrieve the expected path for the file. +challenge.filename # => ".well-known/acme-challenge/:some_token" + +# You can generate the body of the expected response. +challenge.file_content # => 'string token and JWK thumbprint' + +# You are not required to send a Content-Type. This method will return the right Content-Type should you decide to include one. +challenge.content_type + +# Save the file. We'll create a public directory to serve it from, and inside it we'll create the challenge file. +FileUtils.mkdir_p( File.join( 'public', File.dirname( challenge.filename ) ) ) + +# We'll write the content of the file +File.write( File.join( 'public', challenge.filename), challenge.file_content ) + +# Optionally save the challenge for use at another time (eg: by a background job processor) +File.write('challenge', challenge.to_h.to_json) + +# The challenge file can be served with a Ruby webserver. +# You can run a webserver in another console for that purpose. You may need to forward ports on your router. +# +# $ ruby -run -e httpd public -p 8080 --bind-address 0.0.0.0 + +# Load a saved challenge. This is only required if you need to reuse a saved challenge as outlined above. +challenge = client.challenge_from_hash(JSON.parse(File.read('challenge'))) + +# Once you are ready to serve the confirmation request you can proceed. +challenge.request_verification # => true +challenge.authorization.verify_status # => 'pending' + +# Wait a bit for the server to make the request, or just blink. It should be fast. +sleep(1) + +# Rely on authorization.verify_status more than on challenge.verify_status, +# if the former is 'valid' you can already issue a certificate and the status of +# the challenge is not relevant and in fact may never change from pending. +challenge.authorization.verify_status # => 'valid' +challenge.error # => nil + +# If authorization.verify_status is 'invalid', you can get at the error +# message only through the failed challenge. +authorization.verify_status # => 'invalid' +authorization.http01.error # => {"type" => "...", "detail" => "..."} +``` + +### Obtain a certificate + +Now that your account is authorized for the domain, you should be able to obtain a certificate for it. + +```ruby +# We're going to need a certificate signing request. If not explicitly +# specified, the first name listed becomes the common name. +csr = Acme::Client::CertificateRequest.new(names: %w[example.org www.example.org]) + +# We can now request a certificate. You can pass anything that returns +# a valid DER encoded CSR when calling to_der on it. For example an +# OpenSSL::X509::Request should work too. +certificate = client.new_certificate(csr) # => #<Acme::Client::Certificate ....> + +# Save the certificate and the private key to files +File.write("privkey.pem", certificate.request.private_key.to_pem) +File.write("cert.pem", certificate.to_pem) +File.write("chain.pem", certificate.chain_to_pem) +File.write("fullchain.pem", certificate.fullchain_to_pem) + +# Start a webserver, using your shiny new certificate +# ruby -r openssl -r webrick -r 'webrick/https' -e "s = WEBrick::HTTPServer.new( +# :Port => 8443, +# :DocumentRoot => Dir.pwd, +# :SSLEnable => true, +# :SSLPrivateKey => OpenSSL::PKey::RSA.new( File.read('privkey.pem') ), +# :SSLCertificate => OpenSSL::X509::Certificate.new( File.read('cert.pem') )); trap('INT') { s.shutdown }; s.start" +``` + +# Not implemented + +- Recovery methods are not implemented. + +# Requirements + +Ruby >= 2.1 + +## Development + +All the tests use VCR to mock the interaction with the server but if you +need to record new interation against the server simply clone boulder and +run it normally with `./start.py`. + +## Pull request? + +Yes. + +## License + +[MIT License](http://opensource.org/licenses/MIT) + diff --git a/vendor/acme-client/acme-client.gemspec b/vendor/acme-client/acme-client.gemspec new file mode 100644 index 0000000..b62d60c --- /dev/null +++ b/vendor/acme-client/acme-client.gemspec @@ -0,0 +1,27 @@ +# coding: utf-8 +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'acme/client/version' + +Gem::Specification.new do |spec| + spec.name = 'acme-client' + spec.version = Acme::Client::VERSION + spec.authors = ['Charles Barbier'] + spec.email = ['unixcharles@gmail.com'] + spec.summary = 'Client for the ACME protocol.' + spec.homepage = 'http://github.com/unixcharles/acme-client' + spec.license = 'MIT' + + spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } + spec.require_paths = ['lib'] + + spec.required_ruby_version = '>= 2.1.0' + + spec.add_development_dependency 'bundler', '~> 1.6', '>= 1.6.9' + spec.add_development_dependency 'rake', '~> 10.0' + spec.add_development_dependency 'rspec', '~> 3.3', '>= 3.3.0' + spec.add_development_dependency 'vcr', '~> 2.9', '>= 2.9.3' + spec.add_development_dependency 'webmock', '~> 1.21', '>= 1.21.0' + + spec.add_runtime_dependency 'faraday', '~> 0.9', '>= 0.9.1' +end diff --git a/vendor/acme-client/lib/acme-client.rb b/vendor/acme-client/lib/acme-client.rb new file mode 100644 index 0000000..7cc7a0a --- /dev/null +++ b/vendor/acme-client/lib/acme-client.rb @@ -0,0 +1 @@ +require 'acme/client' diff --git a/vendor/acme-client/lib/acme/client.rb b/vendor/acme-client/lib/acme/client.rb new file mode 100644 index 0000000..801479e --- /dev/null +++ b/vendor/acme-client/lib/acme/client.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +require 'faraday' +require 'json' +require 'openssl' +require 'digest' +require 'forwardable' +require 'base64' +require 'time' + +module Acme; end +class Acme::Client; end + +require 'acme/client/version' +require 'acme/client/certificate' +require 'acme/client/certificate_request' +require 'acme/client/self_sign_certificate' +require 'acme/client/crypto' +require 'acme/client/resources' +require 'acme/client/faraday_middleware' +require 'acme/client/error' + +class Acme::Client + DEFAULT_ENDPOINT = 'http://127.0.0.1:4000'.freeze + DIRECTORY_DEFAULT = { + 'new-authz' => '/acme/new-authz', + 'new-cert' => '/acme/new-cert', + 'new-reg' => '/acme/new-reg', + 'revoke-cert' => '/acme/revoke-cert' + }.freeze + + def initialize(private_key:, endpoint: DEFAULT_ENDPOINT, directory_uri: nil, connection_options: {}) + @endpoint, @private_key, @directory_uri, @connection_options = endpoint, private_key, directory_uri, connection_options + @nonces ||= [] + load_directory! + end + + attr_reader :private_key, :nonces, :operation_endpoints + + def register(contact:) + payload = { + resource: 'new-reg', contact: Array(contact) + } + + response = connection.post(@operation_endpoints.fetch('new-reg'), payload) + ::Acme::Client::Resources::Registration.new(self, response) + end + + def authorize(domain:) + payload = { + resource: 'new-authz', + identifier: { + type: 'dns', + value: domain + } + } + + response = connection.post(@operation_endpoints.fetch('new-authz'), payload) + ::Acme::Client::Resources::Authorization.new(self, response.headers['Location'], response) + end + + def fetch_authorization(uri) + response = connection.get(uri) + ::Acme::Client::Resources::Authorization.new(self, uri, response) + end + + def new_certificate(csr) + payload = { + resource: 'new-cert', + csr: Base64.urlsafe_encode64(csr.to_der) + } + + response = connection.post(@operation_endpoints.fetch('new-cert'), payload) + ::Acme::Client::Certificate.new(OpenSSL::X509::Certificate.new(response.body), response.headers['location'], fetch_chain(response), csr) + end + + def revoke_certificate(certificate) + payload = { resource: 'revoke-cert', certificate: Base64.urlsafe_encode64(certificate.to_der) } + endpoint = @operation_endpoints.fetch('revoke-cert') + response = connection.post(endpoint, payload) + response.success? + end + + def self.revoke_certificate(certificate, *arguments) + client = new(*arguments) + client.revoke_certificate(certificate) + end + + def connection + @connection ||= Faraday.new(@endpoint, **@connection_options) do |configuration| + configuration.use Acme::Client::FaradayMiddleware, client: self + configuration.adapter Faraday.default_adapter + end + end + + private + + def fetch_chain(response, limit = 10) + links = response.headers['link'] + if limit.zero? || links.nil? || links['up'].nil? + [] + else + issuer = connection.get(links['up']) + [OpenSSL::X509::Certificate.new(issuer.body), *fetch_chain(issuer, limit - 1)] + end + end + + def load_directory! + @operation_endpoints = if @directory_uri + response = connection.get(@directory_uri) + body = response.body + { + 'new-reg' => body.fetch('new-reg'), + 'new-authz' => body.fetch('new-authz'), + 'new-cert' => body.fetch('new-cert'), + 'revoke-cert' => body.fetch('revoke-cert'), + } + else + DIRECTORY_DEFAULT + end + end +end diff --git a/vendor/acme-client/lib/acme/client/certificate.rb b/vendor/acme-client/lib/acme/client/certificate.rb new file mode 100644 index 0000000..6c68cc5 --- /dev/null +++ b/vendor/acme-client/lib/acme/client/certificate.rb @@ -0,0 +1,30 @@ +class Acme::Client::Certificate + extend Forwardable + + attr_reader :x509, :x509_chain, :request, :private_key, :url + + def_delegators :x509, :to_pem, :to_der + + def initialize(certificate, url, chain, request) + @x509 = certificate + @url = url + @x509_chain = chain + @request = request + end + + def chain_to_pem + x509_chain.map(&:to_pem).join + end + + def x509_fullchain + [x509, *x509_chain] + end + + def fullchain_to_pem + x509_fullchain.map(&:to_pem).join + end + + def common_name + x509.subject.to_a.find { |name, _, _| name == 'CN' }[1] + end +end diff --git a/vendor/acme-client/lib/acme/client/certificate_request.rb b/vendor/acme-client/lib/acme/client/certificate_request.rb new file mode 100644 index 0000000..8eae0c6 --- /dev/null +++ b/vendor/acme-client/lib/acme/client/certificate_request.rb @@ -0,0 +1,111 @@ +class Acme::Client::CertificateRequest + extend Forwardable + + DEFAULT_KEY_LENGTH = 2048 + DEFAULT_DIGEST = OpenSSL::Digest::SHA256 + SUBJECT_KEYS = { + common_name: 'CN', + country_name: 'C', + organization_name: 'O', + organizational_unit: 'OU', + state_or_province: 'ST', + locality_name: 'L' + }.freeze + + SUBJECT_TYPES = { + 'CN' => OpenSSL::ASN1::UTF8STRING, + 'C' => OpenSSL::ASN1::UTF8STRING, + 'O' => OpenSSL::ASN1::UTF8STRING, + 'OU' => OpenSSL::ASN1::UTF8STRING, + 'ST' => OpenSSL::ASN1::UTF8STRING, + 'L' => OpenSSL::ASN1::UTF8STRING + }.freeze + + attr_reader :private_key, :common_name, :names, :subject + + def_delegators :csr, :to_pem, :to_der + + def initialize(common_name: nil, names: [], private_key: generate_private_key, subject: {}, digest: DEFAULT_DIGEST.new) + @digest = digest + @private_key = private_key + @subject = normalize_subject(subject) + @common_name = common_name || @subject[SUBJECT_KEYS[:common_name]] || @subject[:common_name] + @names = names.to_a.dup + normalize_names + @subject[SUBJECT_KEYS[:common_name]] ||= @common_name + validate_subject + end + + def csr + @csr ||= generate + end + + private + + def generate_private_key + OpenSSL::PKey::RSA.new(DEFAULT_KEY_LENGTH) + end + + def normalize_subject(subject) + @subject = subject.each_with_object({}) do |(key, value), hash| + hash[SUBJECT_KEYS.fetch(key, key)] = value.to_s + end + end + + def normalize_names + if @common_name + @names.unshift(@common_name) unless @names.include?(@common_name) + else + raise ArgumentError, 'No common name and no list of names given' if @names.empty? + @common_name = @names.first + end + end + + def validate_subject + validate_subject_attributes + validate_subject_common_name + end + + def validate_subject_attributes + extra_keys = @subject.keys - SUBJECT_KEYS.keys - SUBJECT_KEYS.values + return if extra_keys.empty? + raise ArgumentError, "Unexpected subject attributes given: #{extra_keys.inspect}" + end + + def validate_subject_common_name + return if @common_name == @subject[SUBJECT_KEYS[:common_name]] + raise ArgumentError, 'Conflicting common name given in arguments and subject' + end + + def generate + OpenSSL::X509::Request.new.tap do |csr| + csr.public_key = @private_key.public_key + csr.subject = generate_subject + csr.version = 2 + add_extension(csr) + csr.sign @private_key, @digest + end + end + + def generate_subject + OpenSSL::X509::Name.new( + @subject.map {|name, value| + [name, value, SUBJECT_TYPES[name]] + } + ) + end + + def add_extension(csr) + return if @names.size <= 1 + + extension = OpenSSL::X509::ExtensionFactory.new.create_extension( + 'subjectAltName', @names.map { |name| "DNS:#{name}" }.join(', '), false + ) + csr.add_attribute( + OpenSSL::X509::Attribute.new( + 'extReq', + OpenSSL::ASN1::Set.new([OpenSSL::ASN1::Sequence.new([extension])]) + ) + ) + end +end diff --git a/vendor/acme-client/lib/acme/client/crypto.rb b/vendor/acme-client/lib/acme/client/crypto.rb new file mode 100644 index 0000000..dfa5cdc --- /dev/null +++ b/vendor/acme-client/lib/acme/client/crypto.rb @@ -0,0 +1,98 @@ +class Acme::Client::Crypto + attr_reader :private_key + + def initialize(private_key) + @private_key = private_key + end + + def generate_signed_jws(header:, payload:) + header = { typ: 'JWT', alg: jws_alg, jwk: jwk }.merge(header) + + encoded_header = urlsafe_base64(header.to_json) + encoded_payload = urlsafe_base64(payload.to_json) + signature_data = "#{encoded_header}.#{encoded_payload}" + + signature = private_key.sign digest, signature_data + encoded_signature = urlsafe_base64(signature) + + { + protected: encoded_header, + payload: encoded_payload, + signature: encoded_signature + }.to_json + end + + def thumbprint + urlsafe_base64 digest.digest(jwk.to_json) + end + + def digest + OpenSSL::Digest::SHA256.new + end + + def urlsafe_base64(data) + Base64.urlsafe_encode64(data).sub(/[\s=]*\z/, '') + end + + private + + def jws_alg + { 'RSA' => 'RS256', 'EC' => 'ES256' }.fetch(jwk[:kty]) + end + + def jwk + @jwk ||= case private_key + when OpenSSL::PKey::RSA + rsa_jwk + when OpenSSL::PKey::EC + ec_jwk + else + raise ArgumentError, "Can't handle #{private_key} as private key, only OpenSSL::PKey::RSA and OpenSSL::PKey::EC" + end + end + + def rsa_jwk + { + e: urlsafe_base64(public_key.e.to_s(2)), + kty: 'RSA', + n: urlsafe_base64(public_key.n.to_s(2)) + } + end + + def ec_jwk + { + crv: curve_name, + kty: 'EC', + x: urlsafe_base64(coordinates[:x].to_s(2)), + y: urlsafe_base64(coordinates[:y].to_s(2)) + } + end + + def curve_name + { + 'prime256v1' => 'P-256', + 'secp384r1' => 'P-384', + 'secp521r1' => 'P-521' + }.fetch(private_key.group.curve_name) { raise ArgumentError, 'Unknown EC curve' } + end + + # rubocop:disable Metrics/AbcSize + def coordinates + @coordinates ||= begin + hex = public_key.to_bn.to_s(16) + data_len = hex.length - 2 + hex_x = hex[2, data_len / 2] + hex_y = hex[2 + data_len / 2, data_len / 2] + + { + x: OpenSSL::BN.new([hex_x].pack('H*'), 2), + y: OpenSSL::BN.new([hex_y].pack('H*'), 2) + } + end + end + # rubocop:enable Metrics/AbcSize + + def public_key + @public_key ||= private_key.public_key + end +end diff --git a/vendor/acme-client/lib/acme/client/error.rb b/vendor/acme-client/lib/acme/client/error.rb new file mode 100644 index 0000000..2b35623 --- /dev/null +++ b/vendor/acme-client/lib/acme/client/error.rb @@ -0,0 +1,16 @@ +class Acme::Client::Error < StandardError + class NotFound < Acme::Client::Error; end + class BadCSR < Acme::Client::Error; end + class BadNonce < Acme::Client::Error; end + class Connection < Acme::Client::Error; end + class Dnssec < Acme::Client::Error; end + class Malformed < Acme::Client::Error; end + class ServerInternal < Acme::Client::Error; end + class Acme::Tls < Acme::Client::Error; end + class Unauthorized < Acme::Client::Error; end + class UnknownHost < Acme::Client::Error; end + class Timeout < Acme::Client::Error; end + class RateLimited < Acme::Client::Error; end + class RejectedIdentifier < Acme::Client::Error; end + class UnsupportedIdentifier < Acme::Client::Error; end +end diff --git a/vendor/acme-client/lib/acme/client/faraday_middleware.rb b/vendor/acme-client/lib/acme/client/faraday_middleware.rb new file mode 100644 index 0000000..21e29c9 --- /dev/null +++ b/vendor/acme-client/lib/acme/client/faraday_middleware.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +class Acme::Client::FaradayMiddleware < Faraday::Middleware + attr_reader :env, :response, :client + + repo_url = 'https://github.com/unixcharles/acme-client' + USER_AGENT = "Acme::Client v#{Acme::Client::VERSION} (#{repo_url})".freeze + + def initialize(app, client:) + super(app) + @client = client + end + + def call(env) + @env = env + @env[:request_headers]['User-Agent'] = USER_AGENT + @env.body = crypto.generate_signed_jws(header: { nonce: pop_nonce }, payload: env.body) + @app.call(env).on_complete { |response_env| on_complete(response_env) } + rescue Faraday::TimeoutError + raise Acme::Client::Error::Timeout + end + + def on_complete(env) + @env = env + + raise_on_not_found! + store_nonce + env.body = decode_body + env.response_headers['Link'] = decode_link_headers + + return if env.success? + + raise_on_error! + end + + private + + def raise_on_not_found! + raise Acme::Client::Error::NotFound, env.url.to_s if env.status == 404 + end + + def raise_on_error! + raise error_class, error_message + end + + def error_message + if env.body.is_a? Hash + env.body['detail'] + else + "Error message: #{env.body}" + end + end + + def error_class + if error_name && !error_name.empty? && Acme::Client::Error.const_defined?(error_name) + Object.const_get("Acme::Client::Error::#{error_name}") + else + Acme::Client::Error + end + end + + def error_name + @error_name ||= begin + return unless env.body.is_a?(Hash) + return unless env.body.key?('type') + + env.body['type'].gsub('urn:acme:error:', '').split(/[_-]/).map(&:capitalize).join + end + end + + def decode_body + content_type = env.response_headers['Content-Type'] + + if content_type == 'application/json' || content_type == 'application/problem+json' + JSON.load(env.body) + else + env.body + end + end + + LINK_MATCH = /<(.*?)>;rel="([\w-]+)"/ + + def decode_link_headers + return unless env.response_headers.key?('Link') + link_header = env.response_headers['Link'] + + links = link_header.split(', ').map { |entry| + _, link, name = *entry.match(LINK_MATCH) + [name, link] + } + + Hash[*links.flatten] + end + + def store_nonce + nonces << env.response_headers['replay-nonce'] + end + + def pop_nonce + if nonces.empty? + get_nonce + else + nonces.pop + end + end + + def get_nonce + response = Faraday.head(env.url, nil, 'User-Agent' => USER_AGENT) + response.headers['replay-nonce'] + end + + def nonces + client.nonces + end + + def private_key + client.private_key + end + + def crypto + @crypto ||= Acme::Client::Crypto.new(private_key) + end +end diff --git a/vendor/acme-client/lib/acme/client/resources.rb b/vendor/acme-client/lib/acme/client/resources.rb new file mode 100644 index 0000000..ad55688 --- /dev/null +++ b/vendor/acme-client/lib/acme/client/resources.rb @@ -0,0 +1,5 @@ +module Acme::Client::Resources; end + +require 'acme/client/resources/registration' +require 'acme/client/resources/challenges' +require 'acme/client/resources/authorization' diff --git a/vendor/acme-client/lib/acme/client/resources/authorization.rb b/vendor/acme-client/lib/acme/client/resources/authorization.rb new file mode 100644 index 0000000..9ca2e76 --- /dev/null +++ b/vendor/acme-client/lib/acme/client/resources/authorization.rb @@ -0,0 +1,44 @@ +class Acme::Client::Resources::Authorization + HTTP01 = Acme::Client::Resources::Challenges::HTTP01 + DNS01 = Acme::Client::Resources::Challenges::DNS01 + TLSSNI01 = Acme::Client::Resources::Challenges::TLSSNI01 + + attr_reader :client, :uri, :domain, :status, :expires, :http01, :dns01, :tls_sni01 + + def initialize(client, uri, response) + @client = client + @uri = uri + assign_attributes(response.body) + end + + def verify_status + response = @client.connection.get(@uri) + + assign_attributes(response.body) + status + end + + private + + def assign_attributes(body) + @expires = Time.iso8601(body['expires']) if body.key? 'expires' + @domain = body['identifier']['value'] + @status = body['status'] + assign_challenges(body['challenges']) + end + + def assign_challenges(challenges) + challenges.each do |attributes| + challenge = case attributes.fetch('type') + when 'http-01' + @http01 ||= HTTP01.new(self) + when 'dns-01' + @dns01 ||= DNS01.new(self) + when 'tls-sni-01' + @tls_sni01 ||= TLSSNI01.new(self) + end + + challenge.assign_attributes(attributes) if challenge + end + end +end diff --git a/vendor/acme-client/lib/acme/client/resources/challenges.rb b/vendor/acme-client/lib/acme/client/resources/challenges.rb new file mode 100644 index 0000000..ec92d47 --- /dev/null +++ b/vendor/acme-client/lib/acme/client/resources/challenges.rb @@ -0,0 +1,6 @@ +module Acme::Client::Resources::Challenges; end + +require 'acme/client/resources/challenges/base' +require 'acme/client/resources/challenges/http01' +require 'acme/client/resources/challenges/dns01' +require 'acme/client/resources/challenges/tls_sni01' diff --git a/vendor/acme-client/lib/acme/client/resources/challenges/base.rb b/vendor/acme-client/lib/acme/client/resources/challenges/base.rb new file mode 100644 index 0000000..c78c74e --- /dev/null +++ b/vendor/acme-client/lib/acme/client/resources/challenges/base.rb @@ -0,0 +1,43 @@ +class Acme::Client::Resources::Challenges::Base + attr_reader :authorization, :status, :uri, :token, :error + + def initialize(authorization) + @authorization = authorization + end + + def client + authorization.client + end + + def verify_status + authorization.verify_status + + status + end + + def request_verification + response = client.connection.post(@uri, resource: 'challenge', type: challenge_type, keyAuthorization: authorization_key) + response.success? + end + + def assign_attributes(attributes) + @status = attributes.fetch('status', 'pending') + @uri = attributes.fetch('uri') + @token = attributes.fetch('token') + @error = attributes['error'] + end + + private + + def challenge_type + self.class::CHALLENGE_TYPE + end + + def authorization_key + "#{token}.#{crypto.thumbprint}" + end + + def crypto + @crypto ||= Acme::Client::Crypto.new(client.private_key) + end +end diff --git a/vendor/acme-client/lib/acme/client/resources/challenges/dns01.rb b/vendor/acme-client/lib/acme/client/resources/challenges/dns01.rb new file mode 100644 index 0000000..543f438 --- /dev/null +++ b/vendor/acme-client/lib/acme/client/resources/challenges/dns01.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class Acme::Client::Resources::Challenges::DNS01 < Acme::Client::Resources::Challenges::Base + CHALLENGE_TYPE = 'dns-01'.freeze + RECORD_NAME = '_acme-challenge'.freeze + RECORD_TYPE = 'TXT'.freeze + + def record_name + RECORD_NAME + end + + def record_type + RECORD_TYPE + end + + def record_content + crypto.urlsafe_base64(crypto.digest.digest(authorization_key)) + end +end diff --git a/vendor/acme-client/lib/acme/client/resources/challenges/http01.rb b/vendor/acme-client/lib/acme/client/resources/challenges/http01.rb new file mode 100644 index 0000000..4966091 --- /dev/null +++ b/vendor/acme-client/lib/acme/client/resources/challenges/http01.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class Acme::Client::Resources::Challenges::HTTP01 < Acme::Client::Resources::Challenges::Base + CHALLENGE_TYPE = 'http-01'.freeze + CONTENT_TYPE = 'text/plain'.freeze + + def content_type + CONTENT_TYPE + end + + def file_content + authorization_key + end + + def filename + ".well-known/acme-challenge/#{token}" + end +end diff --git a/vendor/acme-client/lib/acme/client/resources/challenges/tls_sni01.rb b/vendor/acme-client/lib/acme/client/resources/challenges/tls_sni01.rb new file mode 100644 index 0000000..8f455f5 --- /dev/null +++ b/vendor/acme-client/lib/acme/client/resources/challenges/tls_sni01.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class Acme::Client::Resources::Challenges::TLSSNI01 < Acme::Client::Resources::Challenges::Base + CHALLENGE_TYPE = 'tls-sni-01'.freeze + + def hostname + digest = crypto.digest.hexdigest(authorization_key) + "#{digest[0..31]}.#{digest[32..64]}.acme.invalid" + end + + def certificate + self_sign_certificate.certificate + end + + def private_key + self_sign_certificate.private_key + end + + private + + def self_sign_certificate + @self_sign_certificate ||= Acme::Client::SelfSignCertificate.new(subject_alt_names: [hostname]) + end +end diff --git a/vendor/acme-client/lib/acme/client/resources/registration.rb b/vendor/acme-client/lib/acme/client/resources/registration.rb new file mode 100644 index 0000000..b7a4c11 --- /dev/null +++ b/vendor/acme-client/lib/acme/client/resources/registration.rb @@ -0,0 +1,37 @@ +class Acme::Client::Resources::Registration + attr_reader :id, :key, :contact, :uri, :next_uri, :recover_uri, :term_of_service_uri + + def initialize(client, response) + @client = client + @uri = response.headers['location'] + assign_links(response.headers['Link']) + assign_attributes(response.body) + end + + def get_terms + return unless @term_of_service_uri + + @client.connection.get(@term_of_service_uri).body + end + + def agree_terms + return true unless @term_of_service_uri + + response = @client.connection.post(@uri, resource: 'reg', agreement: @term_of_service_uri) + response.success? + end + + private + + def assign_links(links) + @next_uri = links['next'] + @recover_uri = links['recover'] + @term_of_service_uri = links['terms-of-service'] + end + + def assign_attributes(body) + @id = body['id'] + @key = body['key'] + @contact = body['contact'] + end +end diff --git a/vendor/acme-client/lib/acme/client/self_sign_certificate.rb b/vendor/acme-client/lib/acme/client/self_sign_certificate.rb new file mode 100644 index 0000000..2e7d98c --- /dev/null +++ b/vendor/acme-client/lib/acme/client/self_sign_certificate.rb @@ -0,0 +1,60 @@ +class Acme::Client::SelfSignCertificate + attr_reader :private_key, :subject_alt_names, :not_before, :not_after + + extend Forwardable + def_delegators :certificate, :to_pem, :to_der + + def initialize(subject_alt_names:, not_before: default_not_before, not_after: default_not_after, private_key: generate_private_key) + @private_key = private_key + @subject_alt_names = subject_alt_names + @not_before = not_before + @not_after = not_after + end + + def certificate + @certificate ||= begin + certificate = generate_certificate + + extension_factory = generate_extension_factory(certificate) + subject_alt_name_entry = subject_alt_names.map { |d| "DNS: #{d}" }.join(',') + subject_alt_name_extension = extension_factory.create_extension('subjectAltName', subject_alt_name_entry) + certificate.add_extension(subject_alt_name_extension) + + certificate.sign(private_key, digest) + end + end + + private + + def generate_private_key + OpenSSL::PKey::RSA.new(2048) + end + + def default_not_before + Time.now - 3600 + end + + def default_not_after + Time.now + 30 * 24 * 3600 + end + + def digest + OpenSSL::Digest::SHA256.new + end + + def generate_certificate + certificate = OpenSSL::X509::Certificate.new + certificate.not_before = not_before + certificate.not_after = not_after + certificate.public_key = private_key.public_key + certificate.version = 2 + certificate + end + + def generate_extension_factory(certificate) + extension_factory = OpenSSL::X509::ExtensionFactory.new + extension_factory.subject_certificate = certificate + extension_factory.issuer_certificate = certificate + extension_factory + end +end diff --git a/vendor/acme-client/lib/acme/client/version.rb b/vendor/acme-client/lib/acme/client/version.rb new file mode 100644 index 0000000..c989c12 --- /dev/null +++ b/vendor/acme-client/lib/acme/client/version.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Acme + class Client + VERSION = '0.4.1'.freeze + end +end diff --git a/vendor/base32/LICENSE b/vendor/base32/LICENSE new file mode 100644 index 0000000..cdc04d9 --- /dev/null +++ b/vendor/base32/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2007-2011 Samuel Tesla + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/base32/base32.gemspec b/vendor/base32/base32.gemspec new file mode 100644 index 0000000..4e84cea --- /dev/null +++ b/vendor/base32/base32.gemspec @@ -0,0 +1,10 @@ +$:.push File.expand_path('../lib', __FILE__) + +Gem::Specification.new do |s| + s.name = 'base32' + s.version = '0.3.2' + s.authors = ['Samuel Tesla'] + s.email = 'samuel.tesla@gmail.com' + s.summary = 'Ruby extension for base32 encoding and decoding' + s.require_paths = ['lib'] +end diff --git a/vendor/base32/lib/base32.rb b/vendor/base32/lib/base32.rb new file mode 100644 index 0000000..4df2b1a --- /dev/null +++ b/vendor/base32/lib/base32.rb @@ -0,0 +1,67 @@ +require 'openssl' + +# Module for encoding and decoding in Base32 per RFC 3548 +module Base32 + TABLE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.freeze + + class Chunk + def initialize(bytes) + @bytes = bytes + end + + def decode + bytes = @bytes.take_while {|c| c != 61} # strip padding + n = (bytes.length * 5.0 / 8.0).floor + p = bytes.length < 8 ? 5 - (n * 8) % 5 : 0 + c = bytes.inject(0) {|m,o| (m << 5) + Base32.table.index(o.chr)} >> p + (0..n-1).to_a.reverse.collect {|i| ((c >> i * 8) & 0xff).chr} + end + + def encode + n = (@bytes.length * 8.0 / 5.0).ceil + p = n < 8 ? 5 - (@bytes.length * 8) % 5 : 0 + c = @bytes.inject(0) {|m,o| (m << 8) + o} << p + [(0..n-1).to_a.reverse.collect {|i| Base32.table[(c >> i * 5) & 0x1f].chr}, + ("=" * (8-n))] + end + end + + def self.chunks(str, size) + result = [] + bytes = str.bytes + while bytes.any? do + result << Chunk.new(bytes.take(size)) + bytes = bytes.drop(size) + end + result + end + + def self.encode(str) + chunks(str, 5).collect(&:encode).flatten.join + end + + def self.decode(str) + chunks(str, 8).collect(&:decode).flatten.join + end + + def self.random_base32(length=16, padding=true) + random = '' + OpenSSL::Random.random_bytes(length).each_byte do |b| + random << self.table[b % 32] + end + padding ? random.ljust((length / 8.0).ceil * 8, '=') : random + end + + def self.table=(table) + raise ArgumentError, "Table must have 32 unique characters" unless self.table_valid?(table) + @table = table + end + + def self.table + @table || TABLE + end + + def self.table_valid?(table) + table.bytes.to_a.size == 32 && table.bytes.to_a.uniq.size == 32 + end +end diff --git a/vendor/certificate_authority/certificate_authority.gemspec b/vendor/certificate_authority/certificate_authority.gemspec index b7e8676..71ffb4a 100644 --- a/vendor/certificate_authority/certificate_authority.gemspec +++ b/vendor/certificate_authority/certificate_authority.gemspec @@ -2,15 +2,17 @@ # DO NOT EDIT THIS FILE DIRECTLY # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' # -*- encoding: utf-8 -*- +# stub: certificate_authority 0.2.0 ruby lib Gem::Specification.new do |s| s.name = "certificate_authority" s.version = "0.2.0" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= + s.require_paths = ["lib"] s.authors = ["Chris Chandler"] - s.date = "2012-09-16" - s.email = "chris@flatterline.com" + s.date = "2016-06-21" + s.email = "squanderingtime@gmail.com" s.extra_rdoc_files = [ "README.rdoc" ] @@ -24,6 +26,7 @@ Gem::Specification.new do |s| "lib/certificate_authority.rb", "lib/certificate_authority/certificate.rb", "lib/certificate_authority/certificate_revocation_list.rb", + "lib/certificate_authority/core_extensions.rb", "lib/certificate_authority/distinguished_name.rb", "lib/certificate_authority/extensions.rb", "lib/certificate_authority/key_material.rb", @@ -33,6 +36,7 @@ Gem::Specification.new do |s| "lib/certificate_authority/serial_number.rb", "lib/certificate_authority/signing_entity.rb", "lib/certificate_authority/signing_request.rb", + "lib/certificate_authority/validations.rb", "lib/tasks/certificate_authority.rake", "spec/samples/certs/DigiCertHighAssuranceEVCA-1.pem", "spec/samples/certs/apple_wwdr_issued_cert.pem", @@ -63,27 +67,20 @@ Gem::Specification.new do |s| ] s.homepage = "https://github.com/cchandler/certificate_authority" s.licenses = ["MIT"] - s.require_paths = ["lib"] - s.rubygems_version = "1.8.15" + s.rubygems_version = "2.2.2" s.summary = "Ruby gem for managing the core functions outlined in RFC-3280 for PKI" if s.respond_to? :specification_version then - s.specification_version = 3 + s.specification_version = 4 if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then - s.add_runtime_dependency(%q<activemodel>, [">= 3.0.6"]) - s.add_runtime_dependency(%q<activesupport>, [">= 3.0.6"]) s.add_development_dependency(%q<rspec>, [">= 0"]) s.add_development_dependency(%q<jeweler>, [">= 1.5.2"]) else - s.add_dependency(%q<activemodel>, [">= 3.0.6"]) - s.add_dependency(%q<activesupport>, [">= 3.0.6"]) s.add_dependency(%q<rspec>, [">= 0"]) s.add_dependency(%q<jeweler>, [">= 1.5.2"]) end else - s.add_dependency(%q<activemodel>, [">= 3.0.6"]) - s.add_dependency(%q<activesupport>, [">= 3.0.6"]) s.add_dependency(%q<rspec>, [">= 0"]) s.add_dependency(%q<jeweler>, [">= 1.5.2"]) end diff --git a/vendor/certificate_authority/lib/certificate_authority.rb b/vendor/certificate_authority/lib/certificate_authority.rb index a697c1b..c52e4b6 100644 --- a/vendor/certificate_authority/lib/certificate_authority.rb +++ b/vendor/certificate_authority/lib/certificate_authority.rb @@ -2,11 +2,12 @@ $:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__)) || #Exterior requirements require 'openssl' -require 'active_model' #Internal modules +require 'certificate_authority/core_extensions' require 'certificate_authority/signing_entity' require 'certificate_authority/revocable' +require 'certificate_authority/validations' require 'certificate_authority/distinguished_name' require 'certificate_authority/serial_number' require 'certificate_authority/key_material' diff --git a/vendor/certificate_authority/lib/certificate_authority/certificate.rb b/vendor/certificate_authority/lib/certificate_authority/certificate.rb index 496d91e..cdf432c 100644 --- a/vendor/certificate_authority/lib/certificate_authority/certificate.rb +++ b/vendor/certificate_authority/lib/certificate_authority/certificate.rb @@ -1,6 +1,6 @@ module CertificateAuthority class Certificate - include ActiveModel::Validations + include Validations include Revocable attr_accessor :distinguished_name @@ -15,7 +15,7 @@ module CertificateAuthority attr_accessor :parent - validate do |certificate| + def validate errors.add :base, "Distinguished name must be valid" unless distinguished_name.valid? errors.add :base, "Key material must be valid" unless key_material.valid? errors.add :base, "Serial number must be valid" unless serial_number.valid? @@ -32,8 +32,8 @@ module CertificateAuthority self.distinguished_name = DistinguishedName.new self.serial_number = SerialNumber.new self.key_material = MemoryKeyMaterial.new - self.not_before = Time.now - self.not_after = Time.now + 60 * 60 * 24 * 365 # One year + self.not_before = Date.today.utc + self.not_after = Date.today.advance(:years => 1).utc self.parent = self self.extensions = load_extensions() diff --git a/vendor/certificate_authority/lib/certificate_authority/certificate_revocation_list.rb b/vendor/certificate_authority/lib/certificate_authority/certificate_revocation_list.rb index c84d588..cb3aaf7 100644 --- a/vendor/certificate_authority/lib/certificate_authority/certificate_revocation_list.rb +++ b/vendor/certificate_authority/lib/certificate_authority/certificate_revocation_list.rb @@ -1,20 +1,22 @@ module CertificateAuthority class CertificateRevocationList - include ActiveModel::Validations + include Validations attr_accessor :certificates attr_accessor :parent attr_accessor :crl_body attr_accessor :next_update + attr_accessor :last_update_skew_seconds - validate do |crl| - errors.add :next_update, "Next update must be a positive value" if crl.next_update < 0 - errors.add :parent, "A parent entity must be set" if crl.parent.nil? + def validate + errors.add :next_update, "Next update must be a positive value" if self.next_update < 0 + errors.add :parent, "A parent entity must be set" if self.parent.nil? end def initialize self.certificates = [] self.next_update = 60 * 60 * 4 # 4 hour default + self.last_update_skew_seconds = 0 end def <<(revocable) @@ -54,7 +56,7 @@ module CertificateAuthority end crl.version = 1 - crl.last_update = Time.now + crl.last_update = Time.now - self.last_update_skew_seconds crl.next_update = Time.now + self.next_update signing_cert = OpenSSL::X509::Certificate.new(self.parent.to_pem) diff --git a/vendor/certificate_authority/lib/certificate_authority/core_extensions.rb b/vendor/certificate_authority/lib/certificate_authority/core_extensions.rb new file mode 100644 index 0000000..0508f9a --- /dev/null +++ b/vendor/certificate_authority/lib/certificate_authority/core_extensions.rb @@ -0,0 +1,46 @@ +# +# ActiveSupport has these modifications. Now that we don't use ActiveSupport, +# these are added here as a kindness. +# + +require 'date' + +unless nil.respond_to?(:blank?) + class NilClass + def blank? + true + end + end +end + +unless String.respond_to?(:blank?) + class String + def blank? + self.empty? + end + end +end + +class Date + + def today + t = Time.now.utc + Date.new(t.year, t.month, t.day) + end + + def utc + self.to_datetime.to_time.utc + end + + unless Date.respond_to?(:advance) + def advance(options) + options = options.dup + d = self + d = d >> options.delete(:years) * 12 if options[:years] + d = d >> options.delete(:months) if options[:months] + d = d + options.delete(:weeks) * 7 if options[:weeks] + d = d + options.delete(:days) if options[:days] + d + end + end +end diff --git a/vendor/certificate_authority/lib/certificate_authority/distinguished_name.rb b/vendor/certificate_authority/lib/certificate_authority/distinguished_name.rb index 32d9c1e..3b83582 100644 --- a/vendor/certificate_authority/lib/certificate_authority/distinguished_name.rb +++ b/vendor/certificate_authority/lib/certificate_authority/distinguished_name.rb @@ -1,8 +1,12 @@ module CertificateAuthority class DistinguishedName - include ActiveModel::Validations + include Validations - validates_presence_of :common_name + def validate + if self.common_name.nil? || self.common_name.empty? + errors.add :common_name, 'cannot be blank' + end + end attr_accessor :common_name alias :cn :common_name diff --git a/vendor/certificate_authority/lib/certificate_authority/extensions.rb b/vendor/certificate_authority/lib/certificate_authority/extensions.rb index 7bc4fab..2b9478b 100644 --- a/vendor/certificate_authority/lib/certificate_authority/extensions.rb +++ b/vendor/certificate_authority/lib/certificate_authority/extensions.rb @@ -31,13 +31,20 @@ module CertificateAuthority OPENSSL_IDENTIFIER = "basicConstraints" include ExtensionAPI - include ActiveModel::Validations + include Validations attr_accessor :critical attr_accessor :ca attr_accessor :path_len - validates :critical, :inclusion => [true,false] - validates :ca, :inclusion => [true,false] + + def validate + unless [true, false].include? self.critical + errors.add :critical, 'must be true or false' + end + unless [true, false].include? self.ca + errors.add :ca, 'must be true or false' + end + end def initialize @critical = false diff --git a/vendor/certificate_authority/lib/certificate_authority/key_material.rb b/vendor/certificate_authority/lib/certificate_authority/key_material.rb index 1fd4dd9..ae3a530 100644 --- a/vendor/certificate_authority/lib/certificate_authority/key_material.rb +++ b/vendor/certificate_authority/lib/certificate_authority/key_material.rb @@ -38,7 +38,7 @@ module CertificateAuthority class MemoryKeyMaterial include KeyMaterial - include ActiveModel::Validations + include Validations attr_accessor :keypair attr_accessor :private_key @@ -47,11 +47,13 @@ module CertificateAuthority def initialize end - validates_each :private_key do |record, attr, value| - record.errors.add :private_key, "cannot be blank" if record.private_key.nil? - end - validates_each :public_key do |record, attr, value| - record.errors.add :public_key, "cannot be blank" if record.public_key.nil? + def validate + if private_key.nil? + errors.add :private_key, "cannot be blank" + end + if public_key.nil? + errors.add :public_key, "cannot be blank" + end end def is_in_hardware? @@ -80,10 +82,10 @@ module CertificateAuthority class SigningRequestKeyMaterial include KeyMaterial - include ActiveModel::Validations + include Validations - validates_each :public_key do |record, attr, value| - record.errors.add :public_key, "cannot be blank" if record.public_key.nil? + def validate + errors.add :public_key, "cannot be blank" if public_key.nil? end attr_accessor :public_key diff --git a/vendor/certificate_authority/lib/certificate_authority/ocsp_handler.rb b/vendor/certificate_authority/lib/certificate_authority/ocsp_handler.rb index e101f98..0f2661c 100644 --- a/vendor/certificate_authority/lib/certificate_authority/ocsp_handler.rb +++ b/vendor/certificate_authority/lib/certificate_authority/ocsp_handler.rb @@ -68,7 +68,7 @@ module CertificateAuthority ## DEPRECATED class OCSPHandler - include ActiveModel::Validations + include Validations attr_accessor :ocsp_request attr_accessor :certificate_ids @@ -78,10 +78,10 @@ module CertificateAuthority attr_accessor :ocsp_response_body - validate do |crl| + def validate errors.add :parent, "A parent entity must be set" if parent.nil? + all_certificates_available end - validate :all_certificates_available def initialize self.certificates = {} diff --git a/vendor/certificate_authority/lib/certificate_authority/pkcs11_key_material.rb b/vendor/certificate_authority/lib/certificate_authority/pkcs11_key_material.rb index d4ebc47..8a83f0e 100644 --- a/vendor/certificate_authority/lib/certificate_authority/pkcs11_key_material.rb +++ b/vendor/certificate_authority/lib/certificate_authority/pkcs11_key_material.rb @@ -1,8 +1,6 @@ module CertificateAuthority class Pkcs11KeyMaterial include KeyMaterial - include ActiveModel::Validations - include ActiveModel::Serialization attr_accessor :engine attr_accessor :token_id diff --git a/vendor/certificate_authority/lib/certificate_authority/serial_number.rb b/vendor/certificate_authority/lib/certificate_authority/serial_number.rb index b9a43cc..99f3002 100644 --- a/vendor/certificate_authority/lib/certificate_authority/serial_number.rb +++ b/vendor/certificate_authority/lib/certificate_authority/serial_number.rb @@ -2,12 +2,18 @@ require 'securerandom' module CertificateAuthority class SerialNumber - include ActiveModel::Validations + include Validations include Revocable attr_accessor :number - validates :number, :presence => true, :numericality => {:greater_than => 0} + def validate + if self.number.nil? + errors.add :number, "must not be empty" + elsif self.number.to_i <= 0 + errors.add :number, "must be greater than zero" + end + end def initialize self.number = SecureRandom.random_number(2**128-1) diff --git a/vendor/certificate_authority/lib/certificate_authority/validations.rb b/vendor/certificate_authority/lib/certificate_authority/validations.rb new file mode 100644 index 0000000..a429c96 --- /dev/null +++ b/vendor/certificate_authority/lib/certificate_authority/validations.rb @@ -0,0 +1,31 @@ +# +# This is a super simple replacement for ActiveSupport::Validations +# + +module CertificateAuthority + class Errors < Array + def add(symbol, msg) + self.push([symbol, msg]) + end + def full_messages + self.map {|i| i[0].to_s + ": " + i[1]}.join("\n") + end + end + + module Validations + def valid? + @errors = Errors.new + validate + errors.empty? + end + + # must be overridden + def validate + raise NotImplementedError + end + + def errors + @errors ||= Errors.new + end + end +end diff --git a/vendor/rsync_command/README.md b/vendor/rsync_command/README.md index 4b53a5c..5e44845 100644 --- a/vendor/rsync_command/README.md +++ b/vendor/rsync_command/README.md @@ -11,13 +11,15 @@ Installation Usage ------------------------------------ - rsync = RsyncCommand.new(:logger => logger, :ssh => {:auth_methods => 'publickey'}, :flags => '-a') - source = '/source/path' + rsync = RsyncCommand.new(:ssh => {:auth_methods => 'publickey'}, :flags => '-a') servers = ['red', 'green', 'blue'] - rsync.asynchronously(servers) do |server| - dest = {:user => 'root', :host => server, :path => '/dest/path'} - rsync.exec(source, dest) + rsync.asynchronously(servers) do |sync, server| + sync.user = 'root' + sync.host = server + sync.source = '/from' + sync.dest = '/to' + sync.exec end if rsync.failed? diff --git a/vendor/rsync_command/lib/rsync_command.rb b/vendor/rsync_command/lib/rsync_command.rb index 39e5945..bdcafe0 100644 --- a/vendor/rsync_command/lib/rsync_command.rb +++ b/vendor/rsync_command/lib/rsync_command.rb @@ -4,6 +4,44 @@ require "rsync_command/thread_pool" require 'monitor' +class RsyncRunner + attr_accessor :logger + attr_accessor :source, :dest, :flags, :includes, :excludes + attr_accessor :user, :host + attr_accessor :chdir, :ssh + def initialize(rsync_command) + @logger = nil + @source = "" + @dest = "" + @flags = "" + @includes = [] + @excludes = [] + @rsync_command = rsync_command + end + def log(*args) + @logger.log(*args) + end + def valid? + !@source.empty? || !@dest.empty? + end + def to_hash + fields = [:flags, :includes, :excludes, :logger, :ssh, :chdir] + fields.inject({}){|hsh, i| + hsh[i] = self.send(i); hsh + } + end + def exec + return unless valid? + dest = { + :user => self.user, + :host => self.host, + :path => self.dest + } + src = self.source + @rsync_command.exec_rsync(src, dest, self.to_hash) + end +end + class RsyncCommand attr_accessor :failures, :logger @@ -21,15 +59,23 @@ class RsyncCommand def asynchronously(array, &block) pool = ThreadPool.new array.each do |item| - pool.schedule(item, &block) + pool.schedule(RsyncRunner.new(self), item, &block) end pool.shutdown end # + # returns true if last exec returned a failure + # + def failed? + @failures && @failures.any? + end + + # # runs rsync, recording failures # - def exec(src, dest, options={}) + def exec_rsync(src, dest, options={}) + logger = options[:logger] || @logger @failures.synchronize do @failures.clear end @@ -37,7 +83,7 @@ class RsyncCommand if options[:chdir] rsync_cmd = "cd '#{options[:chdir]}'; #{rsync_cmd}" end - @logger.debug rsync_cmd if @logger + logger.debug rsync_cmd if logger ok = system(rsync_cmd) unless ok @failures.synchronize do @@ -47,13 +93,6 @@ class RsyncCommand end # - # returns true if last exec returned a failure - # - def failed? - @failures && @failures.any? - end - - # # build rsync command # def command(src, dest, options={}) @@ -70,8 +109,6 @@ class RsyncCommand "rsync #{flags.compact.join(' ')} #{src} #{dest}" end - private - # # Creates an rsync location if the +address+ is a hash with keys :user, :host, and :path # (each component is optional). If +address+ is a string, we just pass it through. |