lakitu 1.0.2 → 1.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/Gemfile +9 -0
- data/README.md +39 -19
- data/bin/lakitu +6 -0
- data/lakitu.gemspec +1 -1
- data/lib/lakitu.rb +59 -3
- data/lib/lakitu/configurer.rb +19 -0
- data/lib/lakitu/file_operator.rb +70 -0
- data/lib/lakitu/generator.rb +50 -0
- data/lib/lakitu/options.rb +78 -0
- data/lib/lakitu/provider.rb +11 -0
- data/lib/lakitu/providers/aws.rb +43 -0
- data/lib/lakitu/version.rb +2 -2
- data/spec/configurer_spec.rb +29 -0
- data/spec/file_operator_spec.rb +57 -0
- data/spec/generator_spec.rb +50 -0
- data/spec/lakitu_spec.rb +28 -0
- data/spec/options_spec.rb +34 -0
- data/spec/providers/aws_spec.rb +22 -0
- data/spec/spec_helper.rb +152 -0
- metadata +36 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a39c34d03f0b881eb80772851d0d81bd5b290525
|
4
|
+
data.tar.gz: 22a3bb290adbaec68dfeb32b8077a51b8c92be62
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3dd6856ef1ca80e7f1f553aa055a742d157597e698a09c01e3e93f199403b8e5c333466e487bbc6e103be4afc3277cc45d630257ef733765560744a4154a61e3
|
7
|
+
data.tar.gz: 608987d97b1e3539c6d1e613b649223cb263ba4eb98ca18b1f5fe1cdd8321f47ba43dafa845bef6c755efff930e0cde6cd8eda15ad3cb136f363af9bb864fd21
|
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.0.0-p598
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,31 +1,51 @@
|
|
1
1
|
# Lakitu
|
2
|
+
![CircleCI build status](https://circleci.com/gh/bkconrad/lakitu.png?style=shield&circle-token=a622a5a71cc7a5ad0662d05e164a67f8ddbdaff1)
|
2
3
|
|
3
|
-
|
4
|
+
Generate an SSH config from AWS (or potentially other cloud provider) APIs.
|
4
5
|
|
5
|
-
##
|
6
|
+
## Prework
|
7
|
+
To use Lakitu effectively, you will need the following:
|
6
8
|
|
7
|
-
|
9
|
+
1. A modern Ruby (2.0+). You may need RVM for this.
|
10
|
+
2. A modern `bash-completion` package installed via `brew`, `apt-get`, or `yum`.
|
11
|
+
3. AWS profiles configured according to the [official documentation](http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html#cli-multiple-profiles) (or via `aws configure`).
|
8
12
|
|
9
|
-
|
10
|
-
gem 'lakitu'
|
11
|
-
```
|
12
|
-
|
13
|
-
And then execute:
|
13
|
+
If you don't have 1-3 above, you will not get the most out of this project (and you are missing out on some other really cool stuff).
|
14
14
|
|
15
|
-
|
15
|
+
## Usage
|
16
|
+
```
|
17
|
+
$ gem install lakitu
|
18
|
+
$ lakitu generate
|
19
|
+
$ lakitu configure
|
20
|
+
$ lakitu edit
|
21
|
+
```
|
16
22
|
|
17
|
-
|
23
|
+
## Periodic Refresh
|
24
|
+
Simply add the following line to your `.bashrc`
|
25
|
+
```
|
26
|
+
lakitu generate &>~/.lakitu.log &
|
27
|
+
```
|
18
28
|
|
19
|
-
|
29
|
+
If you installed lakitu via RVM, you should add the following to your shell profile instead:
|
30
|
+
```
|
31
|
+
rvm default do lakitu generate &>~/.lakitu.log &
|
32
|
+
```
|
20
33
|
|
21
|
-
|
34
|
+
`lakitu generate` waits for a refresh interval to pass before re-generating the SSH config. This means you can safely add it to your `.bashrc` or equivalent, and your ssh config will refresh in the background only when that interval has passed.
|
22
35
|
|
23
|
-
|
36
|
+
## Another One?
|
37
|
+
There are several projects that will generate an SSH config from AWS instance data. This one aims to provide the following advantages:
|
24
38
|
|
25
|
-
|
39
|
+
- **Ease of use**: With a properly configured environment, it will detect your configured profiles for supported providers.
|
40
|
+
- **Configurability**: Easily edit a simple config file to control host alias formatting, profiles to skip, etc.
|
41
|
+
- **Automation**: Periodically refreshes the ssh config (via bashrc), and takes no action unless the config appears to be stale.
|
42
|
+
- **Extensibility**: A simple Provider API allows easy support for new providers.
|
43
|
+
- **Reliability**: A robust test suite ensures that Lakitu probably won't cause your machine to combust unexpectedly.
|
26
44
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
45
|
+
## Other Options
|
46
|
+
You could also check out the folowing if this is too much hubbub for you:
|
47
|
+
- https://rubygems.org/gems/knife-ec2-ssh-config
|
48
|
+
- https://rubygems.org/gems/ec2-ssh
|
49
|
+
- https://rubygems.org/gems/aws_ssh
|
50
|
+
- https://rubygems.org/gems/aws-ssh
|
51
|
+
- [An SSH tip for modern AWS patrons](http://codeinthehole.com/writing/an-ssh-tip-for-modern-aws-patrons/)
|
data/bin/lakitu
ADDED
data/lakitu.gemspec
CHANGED
@@ -5,7 +5,7 @@ require 'lakitu/version'
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
7
|
spec.name = "lakitu"
|
8
|
-
spec.version =
|
8
|
+
spec.version = LakituVersion::VERSION
|
9
9
|
spec.authors = ["Bryan Conrad"]
|
10
10
|
spec.email = ["bkconrad@gmail.com"]
|
11
11
|
spec.summary = %q{Ride the clouds}
|
data/lib/lakitu.rb
CHANGED
@@ -1,5 +1,61 @@
|
|
1
|
-
|
1
|
+
$LOAD_PATH << File.dirname(File.expand_path(__FILE__))
|
2
|
+
require "logger"
|
3
|
+
require "yaml"
|
4
|
+
require "thor"
|
5
|
+
|
6
|
+
class Lakitu < Thor
|
7
|
+
LOCAL_SSHCONFIG_PATH = File.expand_path '~/.ssh/local.sshconfig'
|
8
|
+
MANAGED_SSH_CONFIG_TOKEN = "# Managed by Lakitu"
|
9
|
+
OPTIONS_FILE_PATH = File.expand_path "~/.lakitu.yml"
|
10
|
+
SSH_PATH = File.expand_path '~/.ssh'
|
11
|
+
SSHCONFIG_PATH = File.expand_path '~/.ssh/config'
|
12
|
+
EDIT_FILE_COMMAND = "$EDITOR #{OPTIONS_FILE_PATH}"
|
13
|
+
DEFAULT_FORMAT = "%{profile}-%{name}-%{id}"
|
14
|
+
EDIT_LOCAL_CONFIG_COMMAND = "$EDITOR #{LOCAL_SSHCONFIG_PATH}"
|
15
|
+
|
16
|
+
class_options %w( force -f ) => :boolean
|
17
|
+
class_options %w( verbose -v ) => :boolean
|
18
|
+
|
19
|
+
desc "generate [options]", "Generate the ssh config"
|
20
|
+
def generate
|
21
|
+
Lakitu::Options.merge options
|
22
|
+
Lakitu::FileOperator.backup_ssh_config!
|
23
|
+
Lakitu::FileOperator.write_ssh_config! Lakitu::Generator.generate if Lakitu::FileOperator::should_overwrite
|
24
|
+
end
|
25
|
+
|
26
|
+
desc "configure [options]", "Open Lakitu's config file in the system editor"
|
27
|
+
def configure
|
28
|
+
Lakitu::Options.merge options
|
29
|
+
Lakitu::Configurer.find_or_create_config
|
30
|
+
Lakitu::Configurer.edit
|
31
|
+
end
|
2
32
|
|
3
|
-
|
4
|
-
|
33
|
+
desc "edit", "edit #{Lakitu::LOCAL_SSHCONFIG_PATH} and generate config after"
|
34
|
+
def edit
|
35
|
+
Lakitu::Options.options[:force] = true
|
36
|
+
invoke :generate if Lakitu::Configurer.edit_local
|
37
|
+
end
|
38
|
+
|
39
|
+
@@logger = nil
|
40
|
+
def self.logger
|
41
|
+
unless @@logger
|
42
|
+
@@logger = ::Logger.new STDOUT
|
43
|
+
logger.level = Lakitu::Options.options.verbose ? ::Logger::DEBUG : ::Logger::INFO
|
44
|
+
logger.formatter = proc do |severity, datetime, progname, msg|
|
45
|
+
"#{severity}: #{msg}\n"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
@@logger
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.logger= arg
|
52
|
+
@@logger = arg
|
53
|
+
end
|
5
54
|
end
|
55
|
+
|
56
|
+
require "lakitu/version"
|
57
|
+
require "lakitu/configurer"
|
58
|
+
require "lakitu/file_operator"
|
59
|
+
require "lakitu/generator"
|
60
|
+
require "lakitu/options"
|
61
|
+
require "lakitu/providers/aws"
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
module Lakitu::Configurer
|
3
|
+
def self.find_or_create_config
|
4
|
+
unless Lakitu::FileOperator.lakitu_config_exists?
|
5
|
+
Lakitu.logger.debug "Creating new config file"
|
6
|
+
Lakitu::FileOperator.write_lakitu_config Lakitu::Options.default_config
|
7
|
+
end
|
8
|
+
|
9
|
+
Lakitu::FileOperator.read_lakitu_config
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.edit
|
13
|
+
system Lakitu::EDIT_FILE_COMMAND
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.edit_local
|
17
|
+
system Lakitu::EDIT_LOCAL_CONFIG_COMMAND
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'ostruct'
|
3
|
+
module Lakitu::FileOperator
|
4
|
+
def self.key_path key_name
|
5
|
+
expected_key_path = File.join Lakitu::SSH_PATH, "#{key_name}.pem"
|
6
|
+
return nil unless File.exist? expected_key_path
|
7
|
+
Lakitu.logger.debug "Found key at #{expected_key_path}"
|
8
|
+
expected_key_path
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.write_ssh_config! content
|
12
|
+
raise RuntimeError.new("Won't overwrite unmanaged ssh config") unless ssh_config_is_managed? or !File.exist?(Lakitu::SSHCONFIG_PATH)
|
13
|
+
Lakitu.logger.info "Writing ssh config to #{Lakitu::SSHCONFIG_PATH}"
|
14
|
+
File.write Lakitu::SSHCONFIG_PATH, content
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.local_ssh_config
|
18
|
+
return File.read(Lakitu::LOCAL_SSHCONFIG_PATH) if File.exist?(Lakitu::LOCAL_SSHCONFIG_PATH)
|
19
|
+
return ""
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.should_overwrite
|
23
|
+
return true unless ssh_config_is_managed?
|
24
|
+
ssh_config_is_stale?
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.backup_ssh_config!
|
28
|
+
return unless File.exist? Lakitu::SSHCONFIG_PATH
|
29
|
+
unless ssh_config_is_managed?
|
30
|
+
Lakitu.logger.debug "SSH config is unmanaged"
|
31
|
+
if File.exist? Lakitu::LOCAL_SSHCONFIG_PATH
|
32
|
+
Lakitu.logger.fatal "Can't back up unmanaged ssh config: #{Lakitu::LOCAL_SSHCONFIG_PATH} already exists."
|
33
|
+
exit 1
|
34
|
+
return
|
35
|
+
end
|
36
|
+
|
37
|
+
Lakitu.logger.info "Moving #{Lakitu::SSHCONFIG_PATH} to #{Lakitu::LOCAL_SSHCONFIG_PATH}"
|
38
|
+
FileUtils.mv Lakitu::SSHCONFIG_PATH, Lakitu::LOCAL_SSHCONFIG_PATH
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.ssh_config_is_managed?
|
43
|
+
return false unless File.exist? Lakitu::SSHCONFIG_PATH
|
44
|
+
File.read(Lakitu::SSHCONFIG_PATH).include? Lakitu::MANAGED_SSH_CONFIG_TOKEN
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.ssh_config_is_stale?
|
48
|
+
ssh_config_age_minutes = (Time.now - File.mtime(Lakitu::SSHCONFIG_PATH)) / 60
|
49
|
+
effectively_stale = (ssh_config_age_minutes > options.refresh_interval_minutes or options.force)
|
50
|
+
Lakitu.logger.debug "SSH Config modified #{ssh_config_age_minutes} minutes ago, threshold: #{options.refresh_interval_minutes}"
|
51
|
+
Lakitu.logger.info "SSH config is still fresh, use --force to force generation" unless effectively_stale
|
52
|
+
return !!effectively_stale
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.lakitu_config_exists?
|
56
|
+
File.exist? Lakitu::OPTIONS_FILE_PATH
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.read_lakitu_config
|
60
|
+
File.read Lakitu::OPTIONS_FILE_PATH
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.write_lakitu_config content
|
64
|
+
File.write Lakitu::OPTIONS_FILE_PATH, content
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.options
|
68
|
+
Lakitu::Options.options
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'ostruct'
|
3
|
+
module Lakitu::Generator
|
4
|
+
CLAUSE_TEMPLATE=<<-EOF
|
5
|
+
Host <%= host %><% if keyfile %>
|
6
|
+
IdentityFile <%= keyfile %>
|
7
|
+
<% end %>
|
8
|
+
HostName <%= public_ip || private_ip %>
|
9
|
+
EOF
|
10
|
+
|
11
|
+
def self.generate
|
12
|
+
([ Lakitu::MANAGED_SSH_CONFIG_TOKEN, Lakitu::FileOperator::local_ssh_config ] + instances.map do |instance|
|
13
|
+
instance[:host] = format_for(instance[:provider], instance[:profile]) % instance
|
14
|
+
key_path = Lakitu::FileOperator.key_path instance[:key]
|
15
|
+
instance[:keyfile] = key_path if key_path
|
16
|
+
ERB.new(CLAUSE_TEMPLATE).result(OpenStruct.new(instance).instance_eval { binding })
|
17
|
+
end).join("\n")
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.instances
|
21
|
+
result = Lakitu::Provider.providers.map do |provider_class|
|
22
|
+
Lakitu.logger.debug "Getting instances for #{provider_class.name}"
|
23
|
+
get_instances(provider_class.new)
|
24
|
+
end.flatten
|
25
|
+
Lakitu.logger.info "Found #{result.length} instances"
|
26
|
+
result
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def self.get_instances provider
|
32
|
+
provider.profiles.map do |profile|
|
33
|
+
Lakitu.logger.debug "Profile: #{profile}"
|
34
|
+
provider.regions.map do |region|
|
35
|
+
Lakitu.logger.debug " Region: #{region}"
|
36
|
+
result = provider.instances(profile, region)
|
37
|
+
Lakitu.logger.debug " Found #{(result.length rescue 0)} instances"
|
38
|
+
result
|
39
|
+
end
|
40
|
+
end.flatten
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.format_for provider, profile
|
44
|
+
(options[:providers][provider.to_sym][profile.to_sym][:format] rescue nil) || Lakitu::DEFAULT_FORMAT
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.options
|
48
|
+
Lakitu::Options::options
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
module Lakitu::Options
|
3
|
+
DEFAULTS = {
|
4
|
+
refresh_interval_minutes: 10
|
5
|
+
}
|
6
|
+
|
7
|
+
PROFILE_DEFAULTS = {
|
8
|
+
ignore: false,
|
9
|
+
format: "%{profile}-%{name}-%{id}"
|
10
|
+
}
|
11
|
+
|
12
|
+
@@options = nil
|
13
|
+
|
14
|
+
def self.options
|
15
|
+
unless @@options
|
16
|
+
@@options = OpenStruct.new(DEFAULTS.merge config_options)
|
17
|
+
end
|
18
|
+
@@options
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.options= arg
|
22
|
+
@@options = arg ? OpenStruct.new(arg) : arg
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.merge arg
|
26
|
+
@@options = OpenStruct.new(options.to_h.merge arg)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.config_options
|
30
|
+
return { } unless File.exist?(Lakitu::OPTIONS_FILE_PATH)
|
31
|
+
deep_symbolize_keys(::YAML::load(File.read(Lakitu::OPTIONS_FILE_PATH)) || {})
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.default_config
|
35
|
+
create_provider_defaults
|
36
|
+
YAML.dump(deep_stringify_keys(options.to_h))
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def self.deep_stringify_keys object
|
41
|
+
case object
|
42
|
+
when Hash
|
43
|
+
object.each_with_object({}) do |(key, value), result|
|
44
|
+
result[key.to_s] = deep_stringify_keys(value)
|
45
|
+
end
|
46
|
+
when Array
|
47
|
+
object.map {|e| deep_stringify_keys(e) }
|
48
|
+
else
|
49
|
+
object
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.deep_symbolize_keys object
|
54
|
+
case object
|
55
|
+
when Hash
|
56
|
+
object.each_with_object({}) do |(key, value), result|
|
57
|
+
result[key.to_sym] = deep_symbolize_keys(value)
|
58
|
+
end
|
59
|
+
when Array
|
60
|
+
object.map {|e| deep_symbolize_keys(e) }
|
61
|
+
else
|
62
|
+
object
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.create_provider_defaults
|
67
|
+
options[:providers] ||= {}
|
68
|
+
Lakitu::Provider.providers.each do |provider_class|
|
69
|
+
provider_key = provider_class.name.downcase.split('::').last.to_sym
|
70
|
+
next if options[:providers][provider_key]
|
71
|
+
result = provider_class.new.profiles.inject({}) do |memo, profile|
|
72
|
+
memo[profile.to_sym] = PROFILE_DEFAULTS.dup
|
73
|
+
memo
|
74
|
+
end
|
75
|
+
options[:providers][provider_key] = result
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'aws-sdk-resources'
|
2
|
+
require 'iniparse'
|
3
|
+
require 'lakitu/provider'
|
4
|
+
|
5
|
+
class Lakitu::Provider::Aws < Lakitu::Provider
|
6
|
+
def profiles
|
7
|
+
IniParse.parse(File.read(File.expand_path('~/.aws/credentials'))).to_hash.keys.reject() do |x| x == '__anonymous__' end
|
8
|
+
end
|
9
|
+
|
10
|
+
def instances profile, region
|
11
|
+
result = ec2(profile, region)
|
12
|
+
.instances
|
13
|
+
.to_a
|
14
|
+
.select { |x| x.state.name == "running" }
|
15
|
+
.map { |x| to_hash x }
|
16
|
+
|
17
|
+
result.each do |x|
|
18
|
+
x[:profile] = profile
|
19
|
+
x[:region] = region
|
20
|
+
x[:provider] = 'aws'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def regions
|
25
|
+
[ 'us-east-1', 'us-west-1', 'us-west-2' ]
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def ec2 profile, region
|
31
|
+
::Aws::EC2::Resource.new(region: region, profile: profile)
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_hash instance
|
35
|
+
{
|
36
|
+
id: instance.id,
|
37
|
+
name: (instance.tags.select do |x| x.key == 'Name' end.first.value rescue 'blank'),
|
38
|
+
key: instance.key_name,
|
39
|
+
private_ip: instance.private_ip_address,
|
40
|
+
public_ip: instance.public_ip_address
|
41
|
+
}
|
42
|
+
end
|
43
|
+
end
|
data/lib/lakitu/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
|
2
|
-
VERSION = "1.0.
|
1
|
+
class LakituVersion
|
2
|
+
VERSION = "1.0.3"
|
3
3
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
describe Lakitu::Configurer do
|
2
|
+
subject { described_class }
|
3
|
+
before :each do
|
4
|
+
mock_config
|
5
|
+
allow(File).to receive(:write).and_return(true)
|
6
|
+
end
|
7
|
+
|
8
|
+
it "creates config file when it doesn't exist" do
|
9
|
+
mock_no_options
|
10
|
+
expect(File).to receive(:write).with(Lakitu::OPTIONS_FILE_PATH, Lakitu::Options.default_config).and_return(true)
|
11
|
+
expect(File).to receive(:read).with(Lakitu::OPTIONS_FILE_PATH).and_return(Lakitu::Options.default_config)
|
12
|
+
expect(subject.find_or_create_config).to eq Lakitu::Options.default_config
|
13
|
+
end
|
14
|
+
|
15
|
+
it "uses existing config file when it exists" do
|
16
|
+
mock_options
|
17
|
+
expect(subject.find_or_create_config).to eq OPTIONS_CONTENT
|
18
|
+
end
|
19
|
+
|
20
|
+
it "opens config file in default editor" do
|
21
|
+
expect(subject).to receive(:system).with(Lakitu::EDIT_FILE_COMMAND)
|
22
|
+
subject.edit
|
23
|
+
end
|
24
|
+
|
25
|
+
it "opens local config file in default editor" do
|
26
|
+
expect(subject).to receive(:system).with(Lakitu::EDIT_LOCAL_CONFIG_COMMAND)
|
27
|
+
subject.edit_local
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
describe Lakitu::FileOperator do
|
2
|
+
subject { described_class }
|
3
|
+
before :each do
|
4
|
+
allow(File).to receive(:write).and_return(true)
|
5
|
+
end
|
6
|
+
|
7
|
+
it "moves unmanaged ssh configs to local.sshconfig" do
|
8
|
+
mock_no_ssh_keys
|
9
|
+
mock_unmanaged_sshconfig
|
10
|
+
mock_no_local_sshconfig
|
11
|
+
expect(FileUtils).to receive(:mv).with(Lakitu::SSHCONFIG_PATH, Lakitu::LOCAL_SSHCONFIG_PATH)
|
12
|
+
subject.backup_ssh_config!
|
13
|
+
end
|
14
|
+
|
15
|
+
context "when config is managed" do
|
16
|
+
before :each do mock_managed_sshconfig end
|
17
|
+
|
18
|
+
context "and ssh config is fresh" do
|
19
|
+
before :each do mock_fresh_sshconfig end
|
20
|
+
it "will not overwrite the config" do expect(subject.should_overwrite).to be false end
|
21
|
+
|
22
|
+
context "and force is true" do
|
23
|
+
before :each do subject.send(:options)[:force] = true end
|
24
|
+
it "will overwrite the config" do expect(subject.should_overwrite).to be true end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context "and ssh config is stale" do
|
29
|
+
before :each do mock_stale_sshconfig end
|
30
|
+
it "will overwrite the config" do expect(subject.should_overwrite).to be true end
|
31
|
+
end
|
32
|
+
|
33
|
+
it "does not move ssh configs to local.sshconfig" do
|
34
|
+
expect(FileUtils).not_to receive(:mv).with(Lakitu::SSHCONFIG_PATH, Lakitu::LOCAL_SSHCONFIG_PATH)
|
35
|
+
subject.backup_ssh_config!
|
36
|
+
end
|
37
|
+
|
38
|
+
it "writes the result to ~/.ssh/config" do
|
39
|
+
expect(FileUtils).not_to receive(:mv).with(Lakitu::SSHCONFIG_PATH, Lakitu::LOCAL_SSHCONFIG_PATH)
|
40
|
+
expect(File).to receive(:write).with(Lakitu::SSHCONFIG_PATH, "test")
|
41
|
+
subject.write_ssh_config! "test"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context "when config is unmanaged" do
|
46
|
+
before :each do mock_unmanaged_sshconfig end
|
47
|
+
it "will overwrite the config" do expect(subject.should_overwrite).to be true end
|
48
|
+
|
49
|
+
it "exits when local.sshconfig exists" do
|
50
|
+
mock_no_ssh_keys
|
51
|
+
mock_local_sshconfig_exists
|
52
|
+
expect(FileUtils).not_to receive(:mv).with(Lakitu::SSHCONFIG_PATH, Lakitu::LOCAL_SSHCONFIG_PATH)
|
53
|
+
expect(subject).to receive(:exit).with(1)
|
54
|
+
subject.backup_ssh_config!
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
describe Lakitu::Generator do
|
2
|
+
subject { described_class }
|
3
|
+
before :each do
|
4
|
+
stub_aws
|
5
|
+
mock_config
|
6
|
+
allow(File).to receive(:write).and_return(true)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "invokes all providers, for all regions, over all profiles" do
|
10
|
+
expect(Lakitu::Provider.providers.length).to be > 0
|
11
|
+
Lakitu::Provider.providers.each do |provider_class|
|
12
|
+
provider = provider_class.new
|
13
|
+
provider.profiles.each do |profile|
|
14
|
+
provider.regions.each do |region|
|
15
|
+
expect_any_instance_of(provider_class).to receive(:instances).with(profile, region).exactly(:once)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
subject.instances
|
21
|
+
end
|
22
|
+
|
23
|
+
it "merges instance lists from the providers" do
|
24
|
+
WRANGLED_INSTANCE_DATA_COMPLETE.each do |x|
|
25
|
+
expect(subject.instances).to include x
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
it "generates ssh config content from templates" do
|
30
|
+
mock_no_local_sshconfig
|
31
|
+
mock_no_ssh_keys
|
32
|
+
mock_options
|
33
|
+
Lakitu::Options.options = nil
|
34
|
+
result = subject.generate
|
35
|
+
expect(result).to include SSH_CONFIG_RESULT
|
36
|
+
expect(result).to include "Host formattest"
|
37
|
+
end
|
38
|
+
|
39
|
+
it "reads ~/.ssh/*.sshconfig files" do
|
40
|
+
mock_no_ssh_keys
|
41
|
+
mock_local_sshconfig
|
42
|
+
expect(subject.generate).to include LOCAL_SSHCONFIG
|
43
|
+
end
|
44
|
+
|
45
|
+
it "looks up keys in ~/.ssh/" do
|
46
|
+
mock_ssh_keys
|
47
|
+
mock_no_local_sshconfig
|
48
|
+
expect(subject.generate).to include "IdentityFile #{File.expand_path("~/.ssh/testkey.pem")}"
|
49
|
+
end
|
50
|
+
end
|
data/spec/lakitu_spec.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
describe Lakitu do
|
2
|
+
subject { described_class.new }
|
3
|
+
it "delegates generate!" do
|
4
|
+
expect(Lakitu::FileOperator).to receive(:backup_ssh_config!).and_return(nil)
|
5
|
+
expect(Lakitu::Generator).to receive(:generate).and_return("test")
|
6
|
+
expect(Lakitu::FileOperator).to receive(:write_ssh_config!).and_return(nil)
|
7
|
+
expect(Lakitu::FileOperator).to receive(:should_overwrite).and_return(true)
|
8
|
+
subject.generate
|
9
|
+
end
|
10
|
+
|
11
|
+
it "delegates configure" do
|
12
|
+
expect(Lakitu::Configurer).to receive(:find_or_create_config).and_return(true)
|
13
|
+
expect(Lakitu::Configurer).to receive(:edit).and_return("test")
|
14
|
+
subject.configure
|
15
|
+
end
|
16
|
+
|
17
|
+
it "delegates edit" do
|
18
|
+
expect(Lakitu::Configurer).to receive(:edit_local).and_return(true)
|
19
|
+
expect(subject).to receive(:invoke).with(:generate).and_return(true)
|
20
|
+
subject.edit
|
21
|
+
end
|
22
|
+
|
23
|
+
it "creates a logger" do
|
24
|
+
Lakitu.logger = nil
|
25
|
+
Lakitu::Options.options.verbose = false
|
26
|
+
expect(Lakitu.logger).to be_a(Logger)
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
describe Lakitu::Options do
|
2
|
+
subject { described_class }
|
3
|
+
before :each do
|
4
|
+
Lakitu::Options.options= nil
|
5
|
+
allow(File).to receive(:write).and_return(true)
|
6
|
+
end
|
7
|
+
|
8
|
+
it "provides default values for options" do
|
9
|
+
mock_no_options
|
10
|
+
expect(described_class.options.refresh_interval_minutes).to eq 10
|
11
|
+
end
|
12
|
+
|
13
|
+
it "overwrites defaults with config values" do
|
14
|
+
mock_options
|
15
|
+
expect(described_class.options.refresh_interval_minutes).to eq 5
|
16
|
+
end
|
17
|
+
|
18
|
+
it "overwrites config values with command line arguments" do
|
19
|
+
mock_options
|
20
|
+
Lakitu::Options.merge refresh_interval_minutes: 1
|
21
|
+
expect(described_class.options.refresh_interval_minutes).to eq 1
|
22
|
+
end
|
23
|
+
|
24
|
+
it "includes providers and profiles in default config" do
|
25
|
+
mock_no_options
|
26
|
+
mock_config
|
27
|
+
stub_aws
|
28
|
+
result = YAML.load(subject.default_config)
|
29
|
+
expect(result).to be_a Hash
|
30
|
+
expect(result['refresh_interval_minutes']).to eql 10
|
31
|
+
expect(result['providers']).to be_a Hash
|
32
|
+
expect(result['providers']['aws']['client1']['ignore']).to be false
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
RSpec.describe Lakitu::Provider::Aws do
|
2
|
+
subject { Lakitu::Provider::Aws.new }
|
3
|
+
context "with a valid credentials config" do
|
4
|
+
before :each do
|
5
|
+
mock_config
|
6
|
+
end
|
7
|
+
|
8
|
+
it "gets a list of profiles" do
|
9
|
+
expect(subject.profiles).to eq ['default', 'client1']
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
context "with a given list of instances" do
|
14
|
+
before do
|
15
|
+
stub_aws
|
16
|
+
end
|
17
|
+
|
18
|
+
it "gets a list of instances for a given profile" do
|
19
|
+
expect(subject.instances('default', 'us-east-1')).to eq(WRANGLED_INSTANCE_DATA_COMPLETE)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'simplecov'
|
3
|
+
SimpleCov.start
|
4
|
+
|
5
|
+
require './lib/lakitu.rb'
|
6
|
+
|
7
|
+
# silence the logger
|
8
|
+
RSpec.configure do |config|
|
9
|
+
config.before do
|
10
|
+
Lakitu.logger = Logger.new(false)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def stub_aws
|
15
|
+
Aws.config[:ec2] = {
|
16
|
+
stub_responses: {
|
17
|
+
describe_instances: {
|
18
|
+
reservations: [{
|
19
|
+
instances: [
|
20
|
+
{ instance_id: 'i-abcd1234', public_ip_address: '1.2.3.3', key_name: 'testkey', state: { name: 'stopped' }, tags: [ { key: 'Name', value: 'deadman' } ] },
|
21
|
+
{ instance_id: 'i-deadbeef', public_ip_address: '1.2.3.4', key_name: 'testkey', state: { name: 'running' }, tags: [ { key: 'Name', value: 'test' } ] },
|
22
|
+
{ instance_id: 'i-beeff00d', private_ip_address: '1.2.3.5', key_name: 'testkey2', state: { name: 'running' }, tags: [ { key: 'Name', value: 'test2' } ] }
|
23
|
+
]}
|
24
|
+
]
|
25
|
+
}
|
26
|
+
}
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
WRANGLED_INSTANCE_DATA_COMPLETE=[
|
31
|
+
{
|
32
|
+
id: 'i-deadbeef',
|
33
|
+
name: 'test',
|
34
|
+
key: 'testkey',
|
35
|
+
profile: 'default',
|
36
|
+
region: 'us-east-1',
|
37
|
+
provider: 'aws',
|
38
|
+
public_ip: '1.2.3.4',
|
39
|
+
private_ip: nil
|
40
|
+
},
|
41
|
+
{
|
42
|
+
id: 'i-beeff00d',
|
43
|
+
name: 'test2',
|
44
|
+
key: 'testkey2',
|
45
|
+
profile: 'default',
|
46
|
+
region: 'us-east-1',
|
47
|
+
provider: 'aws',
|
48
|
+
public_ip: nil,
|
49
|
+
private_ip: '1.2.3.5'
|
50
|
+
}
|
51
|
+
]
|
52
|
+
|
53
|
+
AWS_CREDENTIALS_CONTENT=<<EOF
|
54
|
+
[default]
|
55
|
+
aws_access_key_id = ACCESSKEY
|
56
|
+
aws_secret_access_key = SECRETKEY
|
57
|
+
region = us-west-2
|
58
|
+
|
59
|
+
[client1]
|
60
|
+
aws_access_key_id = ACCESSKEY
|
61
|
+
aws_secret_access_key = SECRETKEY
|
62
|
+
region = us-east-1
|
63
|
+
EOF
|
64
|
+
|
65
|
+
def mock_config
|
66
|
+
allow(File).to receive(:read).and_call_original
|
67
|
+
allow(File).to receive(:read).with(File.expand_path('~/.aws/credentials')).and_return(AWS_CREDENTIALS_CONTENT).at_least(:once)
|
68
|
+
end
|
69
|
+
|
70
|
+
LOCAL_SSHCONFIG=<<EOF
|
71
|
+
User myusername
|
72
|
+
EOF
|
73
|
+
|
74
|
+
def mock_local_sshconfig_exists
|
75
|
+
expect(File).to receive(:exist?).with(File.expand_path('~/.ssh/local.sshconfig')).and_return(true).at_least(:once)
|
76
|
+
end
|
77
|
+
|
78
|
+
def mock_local_sshconfig
|
79
|
+
expect(File).to receive(:exist?).with(File.expand_path('~/.ssh/local.sshconfig')).and_return(true).at_least(:once)
|
80
|
+
expect(File).to receive(:read).with(File.expand_path('~/.ssh/local.sshconfig')).and_return(LOCAL_SSHCONFIG).at_least(:once)
|
81
|
+
end
|
82
|
+
|
83
|
+
def mock_no_local_sshconfig
|
84
|
+
expect(File).to receive(:exist?).with(File.expand_path('~/.ssh/local.sshconfig')).and_return(false).at_least(:once)
|
85
|
+
end
|
86
|
+
|
87
|
+
SSH_CONFIG_RESULT=<<EOF
|
88
|
+
Host default-test-i-deadbeef
|
89
|
+
HostName 1.2.3.4
|
90
|
+
|
91
|
+
Host default-test2-i-beeff00d
|
92
|
+
HostName 1.2.3.5
|
93
|
+
EOF
|
94
|
+
|
95
|
+
UNMANAGED_SSH_CONFIG=<<EOF
|
96
|
+
Host default-test-i-deadbeef
|
97
|
+
HostName 1.2.3.4
|
98
|
+
EOF
|
99
|
+
|
100
|
+
def mock_unmanaged_sshconfig
|
101
|
+
expect(File).to receive(:exist?).with(File.expand_path('~/.ssh/config')).and_return(true).at_least(:once)
|
102
|
+
expect(File).to receive(:read).with(File.expand_path('~/.ssh/config')).and_return(UNMANAGED_SSH_CONFIG).at_least(:once)
|
103
|
+
end
|
104
|
+
|
105
|
+
MANAGED_SSH_CONFIG=<<EOF
|
106
|
+
# Managed by Lakitu
|
107
|
+
Host default-test-i-deadbeef
|
108
|
+
HostName 1.2.3.4
|
109
|
+
EOF
|
110
|
+
|
111
|
+
def mock_managed_sshconfig
|
112
|
+
expect(File).to receive(:exist?).with(File.expand_path('~/.ssh/config')).and_return(true).at_least(:once)
|
113
|
+
expect(File).to receive(:read).with(File.expand_path('~/.ssh/config')).and_return(MANAGED_SSH_CONFIG).at_least(:once)
|
114
|
+
end
|
115
|
+
|
116
|
+
def mock_fresh_sshconfig
|
117
|
+
expect(File).to receive(:mtime).with(Lakitu::SSHCONFIG_PATH).and_return(Time.now).at_least(:once)
|
118
|
+
end
|
119
|
+
|
120
|
+
def mock_stale_sshconfig
|
121
|
+
expect(File).to receive(:mtime).with(Lakitu::SSHCONFIG_PATH).and_return(Time.now - 60*60*24).at_least(:once)
|
122
|
+
end
|
123
|
+
|
124
|
+
def mock_ssh_keys
|
125
|
+
expect(File).to receive(:exist?).with(File.expand_path('~/.ssh/testkey.pem')).and_return(true).at_least(:once)
|
126
|
+
expect(File).to receive(:exist?).with(File.expand_path('~/.ssh/testkey2.pem')).and_return(true).at_least(:once)
|
127
|
+
end
|
128
|
+
|
129
|
+
def mock_no_ssh_keys
|
130
|
+
allow(File).to receive(:exist?).with(File.expand_path('~/.ssh/testkey.pem')).and_return(false).at_least(:once)
|
131
|
+
allow(File).to receive(:exist?).with(File.expand_path('~/.ssh/testkey2.pem')).and_return(false).at_least(:once)
|
132
|
+
end
|
133
|
+
|
134
|
+
OPTIONS_CONTENT=<<EOF
|
135
|
+
---
|
136
|
+
verbose: true
|
137
|
+
force: true
|
138
|
+
refresh_interval_minutes: 5
|
139
|
+
providers:
|
140
|
+
aws:
|
141
|
+
client1:
|
142
|
+
format: "formattest"
|
143
|
+
EOF
|
144
|
+
def mock_options
|
145
|
+
allow(File).to receive(:exist?).and_call_original
|
146
|
+
expect(File).to receive(:exist?).with(Lakitu::OPTIONS_FILE_PATH).and_return(true).at_least(:once)
|
147
|
+
expect(File).to receive(:read).with(Lakitu::OPTIONS_FILE_PATH).and_return(OPTIONS_CONTENT).at_least(:once)
|
148
|
+
end
|
149
|
+
|
150
|
+
def mock_no_options
|
151
|
+
expect(File).to receive(:exist?).with(Lakitu::OPTIONS_FILE_PATH).and_return(false).at_least(:once)
|
152
|
+
end
|
metadata
CHANGED
@@ -1,58 +1,75 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lakitu
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bryan Conrad
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-04-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - ~>
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '1.7'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - ~>
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '1.7'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rake
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- -
|
31
|
+
- - ~>
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '10.0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- -
|
38
|
+
- - ~>
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '10.0'
|
41
41
|
description: ''
|
42
42
|
email:
|
43
43
|
- bkconrad@gmail.com
|
44
|
-
executables:
|
44
|
+
executables:
|
45
|
+
- lakitu
|
45
46
|
extensions: []
|
46
47
|
extra_rdoc_files: []
|
47
48
|
files:
|
48
|
-
-
|
49
|
+
- .gitignore
|
50
|
+
- .rspec
|
51
|
+
- .ruby-version
|
49
52
|
- Gemfile
|
50
53
|
- LICENSE.txt
|
51
54
|
- README.md
|
52
55
|
- Rakefile
|
56
|
+
- bin/lakitu
|
53
57
|
- lakitu.gemspec
|
54
58
|
- lib/lakitu.rb
|
59
|
+
- lib/lakitu/configurer.rb
|
60
|
+
- lib/lakitu/file_operator.rb
|
61
|
+
- lib/lakitu/generator.rb
|
62
|
+
- lib/lakitu/options.rb
|
63
|
+
- lib/lakitu/provider.rb
|
64
|
+
- lib/lakitu/providers/aws.rb
|
55
65
|
- lib/lakitu/version.rb
|
66
|
+
- spec/configurer_spec.rb
|
67
|
+
- spec/file_operator_spec.rb
|
68
|
+
- spec/generator_spec.rb
|
69
|
+
- spec/lakitu_spec.rb
|
70
|
+
- spec/options_spec.rb
|
71
|
+
- spec/providers/aws_spec.rb
|
72
|
+
- spec/spec_helper.rb
|
56
73
|
homepage: ''
|
57
74
|
licenses:
|
58
75
|
- MIT
|
@@ -63,19 +80,25 @@ require_paths:
|
|
63
80
|
- lib
|
64
81
|
required_ruby_version: !ruby/object:Gem::Requirement
|
65
82
|
requirements:
|
66
|
-
- -
|
83
|
+
- - '>='
|
67
84
|
- !ruby/object:Gem::Version
|
68
85
|
version: '0'
|
69
86
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
87
|
requirements:
|
71
|
-
- -
|
88
|
+
- - '>='
|
72
89
|
- !ruby/object:Gem::Version
|
73
90
|
version: '0'
|
74
91
|
requirements: []
|
75
92
|
rubyforge_project:
|
76
|
-
rubygems_version: 2.
|
93
|
+
rubygems_version: 2.4.3
|
77
94
|
signing_key:
|
78
95
|
specification_version: 4
|
79
96
|
summary: Ride the clouds
|
80
|
-
test_files:
|
81
|
-
|
97
|
+
test_files:
|
98
|
+
- spec/configurer_spec.rb
|
99
|
+
- spec/file_operator_spec.rb
|
100
|
+
- spec/generator_spec.rb
|
101
|
+
- spec/lakitu_spec.rb
|
102
|
+
- spec/options_spec.rb
|
103
|
+
- spec/providers/aws_spec.rb
|
104
|
+
- spec/spec_helper.rb
|