diff --git a/.gitignore b/.gitignore
index fbf62f942bb8168f7ee8cdab283e91df0611a71e..1fe042f6df9fbb1167becb3483faa0581f98c9a6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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 0000000000000000000000000000000000000000..5aca7d32bfffc4d35e7f6cfd8cf784be6380ac6b
--- /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"
diff --git a/README.md b/README.md
index 2ecc961e8395bff108fdb62e2eaba33db2ad1155..b543a886b47c264ecbf0bcbe0d53bacc4f628ed1 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/Rakefile b/Rakefile
index a51f8139afbc828b665efea6c5b41c4bff52b1d9..d4a61ea1ba1c8ba731012d291980cfb24bb7ddcf 100644
--- a/Rakefile
+++ b/Rakefile
@@ -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
 
diff --git a/bin/leap b/bin/leap
index 9cd3518f25a9af81f5ceebc67f3e8cccaef6d718..55ffb412f80bf24177030b68c84c14e8b4ef5212 100755
--- a/bin/leap
+++ b/bin/leap
@@ -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 beeb0a43fd33943ca2701428ac9e4e47f40b8e5a..8a38893ac16daad4d1d7a8c2b2e9610826e92e2e 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 9112ef35ae48283d958153645ba0edac58e4df6a..0000000000000000000000000000000000000000
--- 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 05633272cd0d7d143fabb90a8a4cbec449288689..c0e139e4b9e1b100fa2abd5be59e4f0aa965a497 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 b7bc8e9367ece62801e1807a53f0c507020410dd..75edf5b7f7a58f8dd97d966c1c48e9276a4be49f 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 7bf49dba5c46c5ab3e21b2ec6b9de9fbd6ca6081..3dab2a082b8f3297bd620a6dfeca686016ebd043 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 838b80eee3a3ac52165ff6c135ba09bcf743746c..6b60e7d0bd03aea49af8689cf710f762ce58a6a3 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 f4bf7bbc5cca1107619f48d1d6c12080e9507681..0b7e98b6751602e8f561be052104945aa70ee936 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 df4b56c9014a3254d84f3f1a8489231bef3309f7..0000000000000000000000000000000000000000
--- 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 2c80be809ac37fc9f5be531fc70666be7ae899cb..0000000000000000000000000000000000000000
--- 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 ecc59f3367c9998461ea67d5fcb10cdb0a614121..0000000000000000000000000000000000000000
--- 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 65735d52f68d202b65d88d267adf977507272d7b..0000000000000000000000000000000000000000
--- 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 b117c2f08598c6141de27fd80bf3b080f07dd0a5..0000000000000000000000000000000000000000
--- 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 f9299a611de0b9511757d1940da0c126e72295e3..0000000000000000000000000000000000000000
--- 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 0d8bc1f3f59dfce473eba4af8d46fadba71518e1..0000000000000000000000000000000000000000
--- 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 ca851c743718fe03c29da130663de29931ae5ae6..0000000000000000000000000000000000000000
--- 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 aee860de8c035883f749bf138865ac5e175ef653..0000000000000000000000000000000000000000
--- 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 6bd8d1e91366a03284d4f0817ecc8953961dfd3f..0000000000000000000000000000000000000000
--- 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 7df33b260e4309d9a9789148713e7c775d413ecd..4eb3af3d6fd19aff7a6b73bf4b95619b9028037f 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 9164d0a38c9c9b3135a2ac7cefd12fd7e951ef97..e526703d88544a59a4aaf8901d37df303e63c236 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 c46455f2da6e6124c92aa91c810143fb8a881aec..0000000000000000000000000000000000000000
--- 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 6589ad46fd90d550b253b82e7019f07c84d987c2..af2fae73d739cd4b132d7706a135d0ca625167a8 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 9e983212a1a0488dd9fbe0d118b6e900adc52ef1..0000000000000000000000000000000000000000
--- 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 fd2e3fc026c92de4633eb0ad73242e0ebdf5585b..a78dbd2c922f83448b814d6ec17784ed22209bb2 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 b48f4331ad7f04d4ff959d04ca8442d329aa7d1f..0000000000000000000000000000000000000000
--- 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 5a6e9081c5e0d02f5cdb38b87b0f4a59ef47289e..0000000000000000000000000000000000000000
--- 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 a6708f4d8494fd8736aa1d9b0f25ec93bfe53a2a..0000000000000000000000000000000000000000
--- 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 d08d19a25b99198662f51bbef346b3216f7af7bf..0000000000000000000000000000000000000000
--- 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 138f4444ec08f185fc35d47a922b3ca6c9635aa5..0000000000000000000000000000000000000000
--- 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 501423826dc0246522444d0684d2ee9c2bc104fa..ae737318232156e28912620c1e34f40b434e8fcf 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 10a5ca858ef6457e9c4b0d55a5f07289282dfdcd..0000000000000000000000000000000000000000
--- 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 749b95957c111cb22102e59ea393af68964d4a62..0000000000000000000000000000000000000000
--- 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 787fdfac7c410af904cc45964989a1e17bacbe80..0000000000000000000000000000000000000000
--- 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 475cdcc3710275fc9dc2a587335fceac37c0925e..bb8b57c3bde5395e4540f83567c4a50c1d5b3b87 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 abab946770123775fae230836c34c862fb27ade1..71af4f9211d65915cdfd4a66520ded60697a448e 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 0000000000000000000000000000000000000000..765b61dca5aaf4621481890f8d40ba96c8ba1289
--- /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 0000000000000000000000000000000000000000..b0a65500f47a992a20ad43bcda29ddfff9677c89
--- /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 0000000000000000000000000000000000000000..accc0cf00b62f5d0a690f20b145cf5ce2a319a56
--- /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 0000000000000000000000000000000000000000..f9e2f27d7964745109363eb5d9a2d17384508391
--- /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 f3aaae4f72c46c7ac0eff44b34094f36fcbeeca9..0000000000000000000000000000000000000000
--- 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 d106cb1127f3d7a8c24256ee1a41ebd199c1230a..0000000000000000000000000000000000000000
--- 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 877f7819aa8d4e782aa0f26bad0439da59823401..0000000000000000000000000000000000000000
--- 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 468941eaa8ef3716d4d84a8f0bf4ff2872cf0999..765b61dca5aaf4621481890f8d40ba96c8ba1289 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 0000000000000000000000000000000000000000..a863de4c815e299c0938edc3839b5e2145dedccb
--- /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 0000000000000000000000000000000000000000..5542c22f5e1febfc868058e18e02fa4300656c17
--- /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 0000000000000000000000000000000000000000..f6eedeca907c033619a918f97493554f21e5b82e
--- /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 0000000000000000000000000000000000000000..74854e54d09dd0e7f1cfce1a56b02a2eeddae4e4
--- /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 0000000000000000000000000000000000000000..47403ee6edc9cd532121510a8fcdca327e1afdbb
--- /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 0000000000000000000000000000000000000000..79e6d212a189c694cf1ef83adda2202666e3f13f
--- /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 0000000000000000000000000000000000000000..fa0106f5ac3d8cb1a2d5e4049527e97fcb78783f
--- /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 0000000000000000000000000000000000000000..2e4b38adef371051e75556068feab670c95c0816
--- /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 0000000000000000000000000000000000000000..c85cf7edaa38c208a724ce757804054b5a0c66d4
--- /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 0000000000000000000000000000000000000000..3003781fed6f90ac7eef721319ffe6b9ca3052fc
--- /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 0000000000000000000000000000000000000000..b74914c52ebfd292179c67ce4182693822ea8a18
--- /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 0000000000000000000000000000000000000000..5cbf7c36af2d29a96ca13a0093cc65000c25a4ff
--- /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 0000000000000000000000000000000000000000..36e3d44c6e803350e578c789d42adc625797284d
--- /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 0000000000000000000000000000000000000000..7ec04f5da68c12bdd2855580a5b0ffb34695f014
--- /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 0000000000000000000000000000000000000000..356ac6fed59308387dbf6bf39380f6ed1a8b6dd0
--- /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 0000000000000000000000000000000000000000..41f5c13a3edebae781b1e7019ae1f8341e2248ea
--- /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 0000000000000000000000000000000000000000..1ea72ad6c8539275ec0adea91e689a6fad919613
--- /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 0000000000000000000000000000000000000000..915a84ee4e2c95d05de680ba8a13fde4a2d52eb0
--- /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 0000000000000000000000000000000000000000..ecb248561bf5debe54cc6c1a32a5f6061c90489c
--- /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 d0f8abf923750e002e589ff606b95584d724ca08..f7c1df094ccd17b9085a3d5744ab9552f33fae9f 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 ffadc244069f41ebba3640ed8ac16583d802e87e..0000000000000000000000000000000000000000
--- 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 591f6142911c2dd3aaa84add0e007436882c14b4..0000000000000000000000000000000000000000
--- 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 f7ec6d9f9e96a991f3b22b4008f91a4e9a2001c7..cd856a37297efabda465dcf104b1b10acb599776 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 0b57ed0ffa0cb258ca46b848f53355680499b819..393bcf24eabf06bc3ebf780dc06e8d7b797f3994 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 9b6e09fb8fd567d4bc980dc90c212cce5b954e3f..042a742f91a837b7e79428f4febcac8708f6231e 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 54b45d146ac634377294f6a19237b05fee80807f..88e11e624277c2d943de1ccf9b1c2647227ddabe 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 0000000000000000000000000000000000000000..d26f9c85c1f687cfab51d9747876862c46555521
--- /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 25a36de7ca8b229777869b22dff6bec78593baf1..057e4b7dfddab6cf8ac25a96f75c8d15f84acf8b 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 0000000000000000000000000000000000000000..e0b10dfdba1e051efc85d14a48702244d8c20fc0
--- /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 0000000000000000000000000000000000000000..73b96b4863efeee3472fe037367b71706c6660ce
--- /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 0000000000000000000000000000000000000000..2047885ac03b98819d6a87187f8231595a728a76
--- /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 0000000000000000000000000000000000000000..b62d60c3f9744c2709aa4c042334ff2900f79dfb
--- /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 0000000000000000000000000000000000000000..7cc7a0a9d1c41b26aff613f95a688b7f43114e3b
--- /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 0000000000000000000000000000000000000000..801479e585de7cce4ddd322216e9b8ed0e19157c
--- /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 0000000000000000000000000000000000000000..6c68cc56531199d96162f7f8a01d8e7eb0e19396
--- /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 0000000000000000000000000000000000000000..8eae0c62bd7f578026f7776d021c2461ddb086b3
--- /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 0000000000000000000000000000000000000000..dfa5cdc935037ad44f23763742e7fe777362e834
--- /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 0000000000000000000000000000000000000000..2b356238be33af711108276ee56cb6c980e62433
--- /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 0000000000000000000000000000000000000000..21e29c9781ba0016e0cd468e053e145ad1d8cc6d
--- /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 0000000000000000000000000000000000000000..ad556889e79810223c627abd619d5b1482ea7e6b
--- /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 0000000000000000000000000000000000000000..9ca2e76936de4b8638c8203083c19677de38c589
--- /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 0000000000000000000000000000000000000000..ec92d471a89c94f4d72690245ab27d1b826529d3
--- /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 0000000000000000000000000000000000000000..c78c74ec5e5ac0fd6af429939f47999485b1fa51
--- /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 0000000000000000000000000000000000000000..543f43861b357a792737809ca518e1516d460f8a
--- /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 0000000000000000000000000000000000000000..4966091ad72efeb6772ef6afd523706fe3e17385
--- /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 0000000000000000000000000000000000000000..8f455f5dbf161583f8a442c6d598ccd16cdc302d
--- /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 0000000000000000000000000000000000000000..b7a4c1133c84e7442bb8a44be8a36cd4e9dc71aa
--- /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 0000000000000000000000000000000000000000..2e7d98cba149d4fcbda721d0bc27f88d594b5150
--- /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 0000000000000000000000000000000000000000..c989c12522dec58ea7fc6d374a9613d310c1556d
--- /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 0000000000000000000000000000000000000000..cdc04d96475b19fc6ccf248eb61b03cc3603fb74
--- /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 0000000000000000000000000000000000000000..4e84cea12bb29abcb48831c2860adcf6f200563e
--- /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 0000000000000000000000000000000000000000..4df2b1aed23ba1f0d2be79c5865bbe0e157d1d0d
--- /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 b7e86767b1173b522306a00fa9381561ab5f58ac..71ffb4a24a3ce41430d23c65b3694f8d413f818f 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 a697c1b705773745f87f95395f7e4182e5323f6e..c52e4b64d09837aac9bced47a08c62bc839002b3 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 496d91e6e955a593e594ef01f75c44b64cc8bd86..cdf432c9cf2c04a495fb2a3569f9ba1d9a8a3e66 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 c84d5884bd5aa9fa78a36f684468a748b5d94052..cb3aaf7b328693986849d2f910c9162c6dbf3d99 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 0000000000000000000000000000000000000000..0508f9a2278561088fe7c3fec42ce12cb9e99552
--- /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 32d9c1eaee77c4f1007807e4b922c4aa8540c497..3b83582cee7882dca76d66e746cefca88024805c 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 7bc4fabc9163e607434c41e9e34cea5b8c83ab0a..2b9478b128e54cc17a9da78abe4e1aa987b49596 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 1fd4dd920de8626b05bd2325219aec259e1fbb6b..ae3a530c97577c3f62ee20179be727b6135974a1 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 e101f9841b7402d9f3b2945f3416c8626825b9e6..0f2661c7dd74f4aef168422e3a464b510ad16eac 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 d4ebc4796ddff480ce79b67c5d33cdc5b54c29b3..8a83f0e6e7b2f0e4d52963b76e072d2293c0a378 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 b9a43cc2d224f229a5716713d8ad7975425744e7..99f30022dba84db72f5a7840a4e6f2a673697843 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 0000000000000000000000000000000000000000..a429c9683a3d1e4dfd6350452888f9c40b8dd718
--- /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 4b53a5c1481db26dbde2a39a2ca3cf48e0df8bf8..5e44845b17d6fa51434034fe0fb9e10fae216982 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 39e5945bf224d06b648d53d5e986ee865de33e15..bdcafe0a8d92299272a407d76fedd23f9b8407d0 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
@@ -46,13 +92,6 @@ class RsyncCommand
     end
   end
 
-  #
-  # returns true if last exec returned a failure
-  #
-  def failed?
-    @failures && @failures.any?
-  end
-
   #
   # build rsync command
   #
@@ -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.