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