claws 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md ADDED
@@ -0,0 +1,80 @@
1
+ ## Command Line AWS (CLAWS) [![Build Status](https://secure.travis-ci.org/wbailey/claws.png?branch=master)](http://travis-ci.org/wbailey/claws)
2
+
3
+ This tool provides a simple and powerful way to interact with the hosts in your Amazon Web Services
4
+ account via the command line.
5
+
6
+ It provides a configurable text based interface of the status of your hosts similar to the AWS web
7
+ console. An example of its usage appears below:
8
+
9
+ ![Instances](http://i.imgur.com/VEC8V.png)
10
+
11
+ It gives you a choice of which host to connect to and invokes the proper ssh command:
12
+
13
+ ![Connecting](http://i.imgur.com/b6ieS.png)
14
+
15
+ ### Installation
16
+
17
+ It is up on rubygems.org so add it to your bundle in the Gemfile
18
+
19
+ ```bash
20
+ gem 'claws'
21
+ ```
22
+
23
+ or do it the old fashioned way:
24
+
25
+ ```bash
26
+ gem install claws
27
+ ```
28
+
29
+ After you have the gem installed you will need to _initialize_ it to install a configuation file:
30
+
31
+ ```bash
32
+ claws --init
33
+ ```
34
+
35
+ Read more about the configuration file in this [wiki post](https://github.com/wbailey/claws/wiki/Configuration-file)
36
+
37
+ ### Usage
38
+
39
+ Once the initial configuration is completed you can simply run claws from the command line:
40
+
41
+ ```bash
42
+ claws
43
+ ```
44
+
45
+ The full list of options includes:
46
+
47
+ ```bash
48
+ Usage: claws [options]
49
+ -s, --status-only Display host status only and exit
50
+ -c, --choice N Enter the number of the host to automatically connect to
51
+ -i, --init Install the default configuration file for the application
52
+ ```
53
+
54
+ ### To Do
55
+
56
+ * Add filtering by regular expression for any field
57
+
58
+ * Integrate with your projects Capistrano definitions so that one can filter by environment and
59
+ roles.
60
+
61
+ * Add RDS and ELB support
62
+
63
+ ### License
64
+
65
+ Copyright (c) 2011-2012 Wes Bailey
66
+
67
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
68
+ associated documentation files (the "Software"), to deal in the Software without restriction,
69
+ including without limitation the rights to use, copy, modify, merge, publish, distribute,
70
+ sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
71
+ furnished to do so, subject to the following conditions:
72
+
73
+ The above copyright notice and this permission notice shall be included in all copies or substantial
74
+ portions of the Software.
75
+
76
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
77
+ NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
78
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
79
+ OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
80
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/bin/claws ADDED
@@ -0,0 +1,36 @@
1
+ #!/bin/env ruby
2
+ require 'claws'
3
+ require 'optparse'
4
+ require 'ostruct'
5
+ require 'yaml'
6
+
7
+ options = OpenStruct.new(
8
+ {
9
+ :config_file => nil,
10
+ :connect => true,
11
+ :initialize => false,
12
+ :selection => nil,
13
+ }
14
+ )
15
+
16
+ OptionParser.new do |opts|
17
+ opts.banner = 'Usage: claws [options]'
18
+
19
+ opts.on('-s', '--status-only', 'Display host status only and exit') do
20
+ options.connect = false
21
+ end
22
+
23
+ opts.on('-c', '--choice N', Float, 'Enter the number of the host to automatically connect to') do |n|
24
+ options.selection = n.to_i
25
+ end
26
+
27
+ opts.on('-i', '--init', 'Install the default configuration file for the application') do
28
+ options.initialize = true
29
+ end
30
+ end.parse!
31
+
32
+ if options.initialize
33
+ Claws::Command::Initialize.exec
34
+ else
35
+ Claws::Command::EC2.exec options
36
+ end
data/lib/claws.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'claws/options'
2
+ require 'claws/configuration'
3
+ require 'claws/collection/ec2'
4
+ require 'claws/report/ec2'
5
+ require 'claws/command/initialize'
6
+ require 'claws/command/ec2'
@@ -0,0 +1,63 @@
1
+ module Claws
2
+ class Capistrano
3
+ attr_accessor :home
4
+
5
+ def initialize(home = nil)
6
+ self.home = home || File.join('config', 'deploy')
7
+ end
8
+
9
+ def all_host_roles
10
+ @all_roles ||= begin
11
+ roles = Hash.new
12
+
13
+ Dir.glob(File.join(self.home, '**/*.rb')).each do |f|
14
+ environment = File.basename(f)[0..-4]
15
+ roles[environment.to_sym] = get_roles(environment)
16
+ end
17
+
18
+ roles
19
+ end
20
+ end
21
+
22
+ def roles(host)
23
+ self.all_host_roles.each do |env, hh|
24
+ hh.each do |k,v|
25
+ return v if k == host
26
+ end
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def get_roles(environment)
33
+ role_records = File.readlines(File.join(self.home, "#{environment}.rb")).select {|r| r.match(/^role/)}
34
+
35
+ # At this point we have an array of strings with:
36
+ #
37
+ # [
38
+ # "role :app, \"ec2-263-56-231-91.compute-1.amazonaws.com\", 'ec2-263-23-118-57.compute-1.amazonaws.com'",
39
+ # "role :web, \"ec2-263-56-231-91.compute-1.amazonaws.com\", 'ec2-263-23-118-57.compute-1.amazonaws.com', \"ec2-23-20-43-198.compute-1.amazonaws.com\"",
40
+ # ]
41
+ #
42
+ # and we want to convert that to:
43
+ #
44
+ # {
45
+ # 'ec2-263-56-231-91.compute-1.amazonaws.com' => ["app", "web"],
46
+ # 'ec2-263-23-118-57.compute-1.amazonaws.com' => ["app", "web"],
47
+ # 'ec2-23-20-43-198.compute-1.amazonaws.com' => ["web"],
48
+ # }
49
+
50
+ roles = Hash.new {|h,k| h[k] = Array.new}
51
+
52
+ role_records.each do |record|
53
+ role, *hosts = record.split(',').map {|v| v.strip.chomp.gsub(/"|'/, '')}
54
+
55
+ hosts.each do |host|
56
+ roles[host] << role.split(':')[1]
57
+ end
58
+ end
59
+
60
+ roles
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,19 @@
1
+ require 'aws-sdk'
2
+
3
+ module Claws
4
+ module Collection
5
+ class Base
6
+ def self.connect(credentials)
7
+ AWS.config(credentials)
8
+ AWS.start_memoizing
9
+ end
10
+
11
+ # Seems unnecessary
12
+ def self.build
13
+ collection = []
14
+ yield(collection)
15
+ collection
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ require 'aws-sdk'
2
+ require 'claws/collection/base'
3
+ require 'claws/presenter/ec2'
4
+
5
+ module Claws
6
+ module Collection
7
+ class EC2 < Claws::Collection::Base
8
+ def self.get(filters = {})
9
+ collection = []
10
+ AWS::EC2.new.instances.each do |instance|
11
+ collection << Claws::EC2::Presenter.new(instance)
12
+ end
13
+ collection
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,43 @@
1
+ module Claws
2
+ module Command
3
+ class EC2
4
+ def self.exec(options)
5
+ begin
6
+ config = Claws::Configuration.new( options.config_file )
7
+ rescue Claws::ConfigurationError => e
8
+ puts e.message
9
+ puts 'Use the --init option to create a configuration file.'
10
+ exit 1
11
+ end
12
+
13
+ Claws::Collection::EC2.connect( config.aws )
14
+
15
+ begin
16
+ instances = Claws::Collection::EC2.get
17
+ rescue Exception => e
18
+ puts e.message
19
+ end
20
+
21
+ Claws::Report::EC2.new( config, instances ).run
22
+
23
+ if options.connect
24
+ if instances.size == 1
25
+ puts
26
+ selection = 0
27
+ elsif options.selection
28
+ puts
29
+ selection = options.selection
30
+ else
31
+ print "Select server (enter q to quit): "
32
+ selection = gets.chomp
33
+ exit 0 if selection.match(/^q.*/i)
34
+ end
35
+
36
+ puts 'connecting to server...'
37
+
38
+ system "ssh #{config.ssh.user}@#{instances[selection.to_i].dns_name}"
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,63 @@
1
+ require 'yaml'
2
+
3
+ module Claws
4
+ module Command
5
+ class Initialize
6
+ def self.exec
7
+ h = {
8
+ 'capistrano' => {
9
+ 'home' => nil,
10
+ },
11
+ 'ssh' => {
12
+ 'user' => nil,
13
+ },
14
+ 'aws' => {
15
+ 'access_key_id' => nil,
16
+ 'secret_access_key' => nil,
17
+ },
18
+ 'ec2' => {
19
+ 'fields' => {
20
+ 'id' => {
21
+ 'width' => 10,
22
+ 'title' => 'ID',
23
+ },
24
+ 'name' => {
25
+ 'width' => 20,
26
+ 'title' => 'Name',
27
+ },
28
+ 'status' => {
29
+ 'width' => 8,
30
+ 'title' => 'Status',
31
+ },
32
+ 'dns_name' => {
33
+ 'width' => 42,
34
+ 'title' => 'DNS Name',
35
+ },
36
+ 'instance_type' => {
37
+ 'width' => 13,
38
+ 'title' => 'Instance Type',
39
+ },
40
+ 'public_ip_address' => {
41
+ 'width' => 16,
42
+ 'title' => 'Public IP',
43
+ },
44
+ 'private_ip_address' => {
45
+ 'width' => 16,
46
+ 'title' => 'Private IP',
47
+ },
48
+ 'tags' => {
49
+ 'width' => 30,
50
+ 'title' => 'tags',
51
+ },
52
+ },
53
+ }
54
+ }
55
+
56
+ conf = File.join(ENV['HOME'], '.claws.yml')
57
+ puts "Creating configuration file: #{conf}\n..."
58
+ File.open(conf, 'w').write(h.to_yaml)
59
+ puts 'Complete!'
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,25 @@
1
+ require 'yaml'
2
+ require 'ostruct'
3
+
4
+ module Claws
5
+ class ConfigurationError < StandardError; end
6
+
7
+ class Configuration
8
+ attr_accessor :path, :capistrano, :ssh, :aws, :ec2
9
+
10
+ def initialize(use_path = nil)
11
+ self.path = use_path || File.join(ENV['HOME'], '.claws.yml')
12
+
13
+ begin
14
+ yaml = YAML.load_file(path)
15
+ rescue Exception
16
+ raise ConfigurationError, "Unable to locate configuration file: #{self.path}"
17
+ end
18
+
19
+ self.capistrano = OpenStruct.new( yaml['capistrano'] )
20
+ self.ssh = OpenStruct.new( yaml['ssh'] )
21
+ self.aws = yaml['aws']
22
+ self.ec2 = OpenStruct.new( { :fields => yaml['ec2']['fields'] } )
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,37 @@
1
+ require 'ostruct'
2
+
3
+ module Claws
4
+ class Options
5
+ def self.parse
6
+ options = OpenStruct.new(
7
+ {
8
+ :connect => true,
9
+ :source => 'ec2',
10
+ :choice => nil,
11
+ }
12
+ )
13
+
14
+ OptionParser.new do |opts|
15
+ opts.banner = "Usage: script/aws [options] [environment] [role]"
16
+
17
+ opts.on('-d', '--display-only', 'display host information only and exit') do
18
+ options.connect = false
19
+ end
20
+
21
+ opts.on('-c', '--choice N', Float, 'enter the identity number of the host to automatically connect to') do |n|
22
+ options.choice = n.to_i
23
+ end
24
+
25
+ opts.on('-s', '--source', 'define the AWS source - default is ec2') do
26
+ options.source = 'elb'
27
+ options.connect = false
28
+ end
29
+ end.parse!
30
+
31
+ options.environment = ARGV.shift
32
+ options.role = ARGV.shift
33
+
34
+ options
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,43 @@
1
+ require 'claws/capistrano'
2
+ require 'claws/support'
3
+
4
+ module Claws
5
+ module EC2
6
+ class Presenter
7
+ attr_writer :roles
8
+
9
+ def initialize(instance, has_roles = [])
10
+ @ec2 = instance.extend(Claws::Support)
11
+ @roles = has_roles
12
+ freeze
13
+ end
14
+
15
+ def roles
16
+ @roles.empty? ? 'N/A' : @roles.join(', ')
17
+ end
18
+
19
+ def tags
20
+ @ec2.try(:tags) ? @ec2.tags.select {|k,v| [k,v] unless k.downcase == 'name'}.map{|k,v| "#{k}: #{v}"}.join(', ') : 'N/A'
21
+ end
22
+
23
+ def security_groups
24
+ @ec2.try(:security_groups) ? @ec2.security_groups.map {|sg| "#{sg.id}: #{sg.name}"}.join(', ') : 'N/A'
25
+ end
26
+
27
+ def method_missing(meth)
28
+ case meth
29
+ when :name
30
+ @ec2.send(:tags)['Name'] || 'N/A'
31
+ when @ec2.try(:tags) && @ec2.tags.has_key?(meth)
32
+ @ec2.tags[meth] || 'N/A'
33
+ else
34
+ begin
35
+ @ec2.send(meth)
36
+ rescue Exception
37
+ 'N/A'
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,54 @@
1
+ require 'command_line_reporter'
2
+
3
+ module Claws
4
+ module Report
5
+ class EC2
6
+ attr_accessor :config, :instances
7
+
8
+ include CommandLineReporter
9
+
10
+ def initialize config, instances
11
+ self.config = config
12
+ self.instances = instances
13
+ end
14
+
15
+ def run
16
+ table :border => true do
17
+ row :header => true do
18
+ column 'Choice', :width => 6, :color => 'blue', :bold => true, :align => 'right'
19
+
20
+ self.config.ec2.fields.each do |field, properties|
21
+ text = properties['title'] || field
22
+ width = properties['width'] || nil
23
+ column text, :width => width, :color => 'blue', :bold => true
24
+ end
25
+ end
26
+
27
+ choice = 0
28
+
29
+ self.instances.each do |i|
30
+ color = case i.status
31
+ when :running
32
+ 'green'
33
+ when :stopped
34
+ 'red'
35
+ else
36
+ 'white'
37
+ end
38
+
39
+ row do
40
+ column choice
41
+
42
+ self.config.ec2.fields.each do |field, properties|
43
+ props = ( field == 'status' ) ? {:color => color} : {}
44
+ column i.send( field ), props
45
+ end
46
+ end
47
+
48
+ choice += 1
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,12 @@
1
+ # Wish this was part of the ruby core
2
+ module Claws
3
+ module Support
4
+ def try(meth, *args, &block)
5
+ self.respond_to?(meth) ? self.send(meth, *args, &block) : nil
6
+ end
7
+ end
8
+ end
9
+
10
+ class Array
11
+ include Claws::Support
12
+ end
@@ -0,0 +1,3 @@
1
+ module Claws
2
+ VERSION = '1.0.0'
3
+ end
data/spec/base_spec.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'spec_helper'
2
+ require 'claws'
3
+
4
+ describe Claws do
5
+
6
+ end
@@ -0,0 +1,74 @@
1
+ require 'spec_helper'
2
+ require 'claws/capistrano'
3
+
4
+ describe Claws::Capistrano do
5
+ before :each do
6
+ @roles = {
7
+ :staging => [
8
+ %q{role :app, 'ec2-175-65-213-31.compute-1.amazonaws.com'},
9
+ %q{role :web, 'ec2-175-65-213-31.compute-1.amazonaws.com'},
10
+ %q{role :batch, "ec2-175-65-213-31.compute-1.amazonaws.com"},
11
+ %q{role :redis, "ec2-223-40-143-23.compute-1.amazonaws.com"},
12
+ %q{role :search, "ec2-147-32-151-54.compute-1.amazonaws.com"},
13
+ %q{role :search_slave, "ec2-147-32-151-54.compute-1.amazonaws.com"},
14
+ ],
15
+
16
+ :production => [
17
+ %q{role :app, "ec2-263-56-231-91.compute-1.amazonaws.com", 'ec2-263-23-118-57.compute-1.amazonaws.com'},
18
+ %q{role :web, "ec2-263-56-231-91.compute-1.amazonaws.com", 'ec2-263-23-118-57.compute-1.amazonaws.com', "ec2-23-20-43-198.compute-1.amazonaws.com"},
19
+ %q{role :batch, "ec2-23-20-43-198.compute-1.amazonaws.com"},
20
+ %q{role :redis, "ec2-23-20-43-198.compute-1.amazonaws.com"},
21
+ %q{role :search, "ec2-107-21-131-545.compute-1.amazonaws.com"},
22
+ %q{role :search_slave, "ec2-263-23-144-120.compute-1.amazonaws.com"},
23
+ ]
24
+ }
25
+ @mappings = {
26
+ :staging => {
27
+ 'ec2-175-65-213-31.compute-1.amazonaws.com' => %w{app web batch},
28
+ 'ec2-223-40-143-23.compute-1.amazonaws.com' => %w{redis},
29
+ 'ec2-147-32-151-54.compute-1.amazonaws.com' => %w{search search_slave},
30
+ },
31
+ :production => {
32
+ 'ec2-263-56-231-91.compute-1.amazonaws.com' => %w{app web},
33
+ 'ec2-263-23-118-57.compute-1.amazonaws.com' => %w{app web},
34
+ 'ec2-23-20-43-198.compute-1.amazonaws.com' => %w{web batch redis},
35
+ 'ec2-107-21-131-545.compute-1.amazonaws.com' => %w{search},
36
+ 'ec2-263-23-144-120.compute-1.amazonaws.com' => %w{search_slave},
37
+ },
38
+ }
39
+
40
+ @default_path = 'config/deploy'
41
+ end
42
+
43
+ context 'defines roles hash per environment per host' do
44
+ it 'from default path' do
45
+ Dir.should_receive(:glob).and_return(%W{#{@default_path}/staging.rb #{@default_path}/production.rb})
46
+ File.should_receive(:readlines).with("#{@default_path}/staging.rb").and_return(@roles[:staging])
47
+ File.should_receive(:readlines).with("#{@default_path}/production.rb").and_return(@roles[:production])
48
+
49
+ cap = Claws::Capistrano.new
50
+ cap.home.should == @default_path
51
+ cap.all_host_roles.should == @mappings
52
+ end
53
+
54
+ it 'from custom path' do
55
+ custom_path = '/home/app/config/deploy'
56
+ Dir.should_receive(:glob).and_return(%W{#{custom_path}/staging.rb #{custom_path}/production.rb})
57
+ File.should_receive(:readlines).with("#{custom_path}/staging.rb").and_return(@roles[:staging])
58
+ File.should_receive(:readlines).with("#{custom_path}/production.rb").and_return(@roles[:production])
59
+
60
+ cap = Claws::Capistrano.new(custom_path)
61
+ cap.home.should == custom_path
62
+ cap.all_host_roles.should == @mappings
63
+ end
64
+ end
65
+
66
+ it 'returns roles for host' do
67
+ Dir.should_receive(:glob).and_return(%W{#{@default_path}/staging.rb #{@default_path}/production.rb})
68
+ File.should_receive(:readlines).with("#{@default_path}/staging.rb").and_return(@roles[:staging])
69
+ File.should_receive(:readlines).with("#{@default_path}/production.rb").and_return(@roles[:production])
70
+
71
+ cap = Claws::Capistrano.new
72
+ cap.roles('ec2-263-56-231-91.compute-1.amazonaws.com').should == %w{app web}
73
+ end
74
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+ require 'claws/collection/base'
3
+ require 'claws/configuration'
4
+
5
+ describe Claws::Collection::Base do
6
+ subject { Claws::Collection::Base }
7
+
8
+ before :each do
9
+ @yaml = {"capistrano_home"=>"test", "access_key_id"=>"asdf", "secret_access_key"=>"qwer"}
10
+ @credentials = {:access_key_id => 'asdf', :secret_access_key => 'qwer'}
11
+
12
+ @config = double('Claws::Configuration', :aws_credentials => @credentials)
13
+ AWS.should_receive(:config).with(@credentials).and_return(true)
14
+ AWS.should_receive(:start_memoizing).and_return(nil)
15
+ end
16
+
17
+ it 'establishes a connection to the mothership' do
18
+ expect {
19
+ subject.connect(@config.aws_credentials)
20
+ }.to_not raise_exception
21
+ end
22
+
23
+ it 'builds a collection' do
24
+ subject.connect(@config.aws_credentials)
25
+
26
+ subject.build do |collection|
27
+ 10.times {|i| collection << i}
28
+ end.should == (0..9).to_a
29
+ end
30
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+ require 'aws-sdk'
3
+ require 'claws/collection/ec2'
4
+
5
+ describe Claws::Collection::EC2 do
6
+ subject { Claws::Collection::EC2 }
7
+
8
+ it 'gets all instances' do
9
+ subject.should_receive(:get).with(no_args).and_return(
10
+ [
11
+ double('AWS::EC2::Instance'),
12
+ double('AWS::EC2::Instance'),
13
+ ]
14
+ )
15
+
16
+ subject.get.size.should == 2
17
+ end
18
+ end
@@ -0,0 +1,179 @@
1
+ require 'spec_helper'
2
+
3
+ describe Claws::Command::EC2 do
4
+ subject { Claws::Command::EC2 }
5
+
6
+ describe '#exec' do
7
+ context 'configuration files' do
8
+ let(:options) { OpenStruct.new( { :config_file => '/doesnotexist', } ) }
9
+
10
+ it 'missing files' do
11
+ subject.should_receive(:puts).twice
12
+
13
+ expect {
14
+ subject.exec options
15
+ }.should raise_exception SystemExit
16
+ end
17
+
18
+ it 'invalid file' do
19
+ YAML.should_receive(:load_file).and_raise(Exception)
20
+
21
+ subject.should_receive(:puts).twice
22
+
23
+ expect {
24
+ subject.exec options
25
+ }.should raise_exception SystemExit
26
+ end
27
+ end
28
+
29
+ context 'valid config file' do
30
+ before :each do
31
+ Claws::Configuration.stub(:new).and_return(
32
+ OpenStruct.new(
33
+ {
34
+ :ssh => OpenStruct.new(
35
+ { :user => 'test' }
36
+ ),
37
+ :ec2 => OpenStruct.new(
38
+ :fields => {
39
+ :id => {
40
+ :width => 10,
41
+ :title => 'ID',
42
+ }
43
+ }
44
+ )
45
+ }
46
+ )
47
+ )
48
+ end
49
+
50
+ let(:options) { OpenStruct.new( { :config_file => nil, } ) }
51
+
52
+ context 'instance collections' do
53
+
54
+ it 'retrieves' do
55
+ Claws::Collection::EC2.should_receive(:connect).and_return(true)
56
+ Claws::Collection::EC2.should_receive(:get).and_return([])
57
+
58
+ capture_stdout {
59
+ subject.exec options
60
+ }
61
+ end
62
+
63
+ it 'handles errors retrieving' do
64
+ Claws::Collection::EC2.should_receive(:connect).and_return(true)
65
+ Claws::Collection::EC2.should_receive(:get).and_raise(Exception)
66
+ subject.should_receive(:puts).once
67
+
68
+ expect {
69
+ subject.exec options
70
+ }.should raise_exception
71
+ end
72
+ end
73
+
74
+ it 'performs report' do
75
+ Claws::Collection::EC2.should_receive(:connect).and_return(true)
76
+ Claws::Collection::EC2.should_receive(:get).and_return([])
77
+
78
+ expect {
79
+ capture_stdout {
80
+ subject.exec options
81
+ }
82
+ }.should_not raise_exception
83
+ end
84
+ end
85
+
86
+ context 'connect options' do
87
+ let(:options) { OpenStruct.new( { :config_file => nil, :connect => true } ) }
88
+
89
+ before :each do
90
+ Claws::Configuration.stub(:new).and_return(
91
+ OpenStruct.new(
92
+ {
93
+ :ssh => OpenStruct.new(
94
+ { :user => 'test' }
95
+ ),
96
+ :ec2 => OpenStruct.new(
97
+ :fields => {
98
+ :id => {
99
+ :width => 10,
100
+ :title => 'ID',
101
+ }
102
+ }
103
+ )
104
+ }
105
+ )
106
+ )
107
+ end
108
+
109
+ context 'single instance' do
110
+ let(:instances) do
111
+ [
112
+ double(AWS::EC2::Instance, :id => 'test', :status => 'running', :dns_name => 'test.com')
113
+ ]
114
+ end
115
+
116
+ it 'automatically connects to the server' do
117
+ Claws::Collection::EC2.should_receive(:connect).and_return(true)
118
+ Claws::Collection::EC2.should_receive(:get).and_return(instances)
119
+
120
+ subject.should_receive(:puts).twice
121
+ subject.should_receive(:system).with('ssh test@test.com').and_return(0)
122
+
123
+ capture_stdout {
124
+ subject.exec options
125
+ }
126
+ end
127
+ end
128
+
129
+ context 'multiple instances' do
130
+ let(:instances) do
131
+ [
132
+ double(AWS::EC2::Instance, :id => 'test1', :status => 'running', :dns_name => 'test1.com'),
133
+ double(AWS::EC2::Instance, :id => 'test2', :status => 'running', :dns_name => 'test2.com'),
134
+ double(AWS::EC2::Instance, :id => 'test3', :status => 'running', :dns_name => 'test3.com'),
135
+ ]
136
+ end
137
+
138
+ it 'handles user inputed selection from the command line' do
139
+ Claws::Collection::EC2.should_receive(:connect).and_return(true)
140
+ Claws::Collection::EC2.should_receive(:get).and_return(instances)
141
+
142
+ subject.should_receive(:puts).twice
143
+ subject.should_receive(:system).with('ssh test@test2.com').and_return(0)
144
+
145
+ capture_stdout {
146
+ subject.exec OpenStruct.new( {:selection => 1, :config_file => nil, :connect => true} )
147
+ }
148
+
149
+ end
150
+
151
+ it 'presents a selection and connects to the server' do
152
+ Claws::Collection::EC2.should_receive(:connect).and_return(true)
153
+ Claws::Collection::EC2.should_receive(:get).and_return(instances)
154
+
155
+ subject.should_receive(:gets).and_return('1\n')
156
+ subject.should_receive(:puts).once
157
+ subject.should_receive(:system).with('ssh test@test2.com').and_return(0)
158
+
159
+ capture_stdout {
160
+ subject.exec options
161
+ }
162
+ end
163
+
164
+ it 'presents a selection and allows a user to quit' do
165
+ Claws::Collection::EC2.should_receive(:connect).and_return(true)
166
+ Claws::Collection::EC2.should_receive(:get).and_return(instances)
167
+
168
+ subject.should_receive(:gets).and_return('q\n')
169
+
170
+ expect {
171
+ capture_stdout {
172
+ subject.exec options
173
+ }
174
+ }.should raise_exception SystemExit
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ describe Claws::Command::Initialize do
4
+ subject { Claws::Command::Initialize }
5
+
6
+ let(:config) { 'test/.test.yml' }
7
+
8
+ it 'works' do
9
+ File.should_receive(:join).and_return(config)
10
+ subject.should_receive(:puts)
11
+
12
+ fh = double( File, :write => true )
13
+
14
+ File.should_receive(:open).with(config, 'w').and_return(fh)
15
+ subject.should_receive(:puts)
16
+
17
+ subject.exec
18
+ end
19
+ end
@@ -0,0 +1,98 @@
1
+ require 'spec_helper'
2
+ require 'claws/configuration'
3
+
4
+ describe Claws::Configuration do
5
+ let (:yaml) do
6
+ {
7
+ 'capistrano' => {
8
+ 'home' => 'test',
9
+ },
10
+ 'aws' => {
11
+ 'access_key_id' => 'asdf',
12
+ 'secret_access_key' => 'qwer',
13
+ 'aws_user' => 'test',
14
+ },
15
+ 'ec2' => {
16
+ 'fields' => {
17
+ 'id' => {
18
+ 'width' => 10,
19
+ 'title' => 'ID'
20
+ },
21
+ 'name' => {
22
+ 'width' => 20,
23
+ 'title' => 'Name'
24
+ },
25
+ }
26
+ }
27
+ }
28
+ end
29
+
30
+ let (:config) { Claws::Configuration.new }
31
+
32
+ describe '#initialize' do
33
+ it 'defines default path' do
34
+ YAML.should_receive(:load_file).and_return( yaml )
35
+ c = Claws::Configuration.new
36
+ c.path.should == File.join(ENV['HOME'], '.claws.yml')
37
+ end
38
+
39
+ it 'defines a custom path' do
40
+ YAML.should_receive(:load_file).and_return( yaml )
41
+ c = Claws::Configuration.new '/home/test'
42
+ c.path.should == '/home/test'
43
+ end
44
+
45
+ it 'raises ConfigurationError' do
46
+ YAML.should_receive(:load_file).and_raise( Exception.new )
47
+
48
+ expect {
49
+ Claws::Configuration.new
50
+ }.should raise_exception Claws::ConfigurationError
51
+ end
52
+
53
+ context 'Capistrano' do
54
+ it 'defines home' do
55
+ YAML.should_receive(:load_file).and_return(yaml)
56
+ config.capistrano.home.should == 'test'
57
+ end
58
+ end
59
+
60
+ context 'AWS' do
61
+ before :each do
62
+ YAML.should_receive(:load_file).and_return(yaml)
63
+ end
64
+
65
+ it 'defines user' do
66
+ config.aws['aws_user'].should == 'test'
67
+ end
68
+
69
+ it 'defines secret access key' do
70
+ config.aws['secret_access_key'].should == 'qwer'
71
+ end
72
+
73
+ it 'defines access key id' do
74
+ config.aws['access_key_id'].should == 'asdf'
75
+ end
76
+ end
77
+
78
+ context 'EC2' do
79
+ before :each do
80
+ YAML.should_receive(:load_file).and_return(yaml)
81
+ end
82
+
83
+ context 'fields' do
84
+ it 'defines id hash' do
85
+ id = config.ec2.fields['id']
86
+ id['width'].should == 10
87
+ id['title'].should == 'ID'
88
+ end
89
+
90
+ it 'defines name hash' do
91
+ name = config.ec2.fields['name']
92
+ name['width'].should == 20
93
+ name['title'].should == 'Name'
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,73 @@
1
+ require 'spec_helper'
2
+ require 'claws/options'
3
+
4
+ ARGV.clear
5
+
6
+ def cli(args)
7
+ ARGV.push(*args)
8
+ yield
9
+ ARGV.clear
10
+ end
11
+
12
+ describe Claws::Options do
13
+ it 'defines default options' do
14
+ cli %w{production app} do
15
+ options = Claws::Options.parse
16
+ options.connect.should be_true
17
+ options.source.should == 'ec2'
18
+ options.choice.should be_nil
19
+ end
20
+ end
21
+
22
+ it 'accepts display only flag' do
23
+ # By default we want to connect to the instance
24
+ Claws::Options.parse.connect.should be_true
25
+
26
+ # Allow the user to override and just display information
27
+ cli %w{-d} do
28
+ Claws::Options.parse.connect.should be_false
29
+ end
30
+
31
+ cli %w{--display-only} do
32
+ Claws::Options.parse.connect.should be_false
33
+ end
34
+ end
35
+
36
+ it 'accepts a choice flag' do
37
+ cli %w{-c 10} do
38
+ Claws::Options.parse.choice.should == 10
39
+ end
40
+
41
+ cli %w{--choice 10} do
42
+ Claws::Options.parse.choice.should == 10
43
+ end
44
+ end
45
+
46
+ it 'accepts a source flag' do
47
+ cli %w{-s elb} do
48
+ options = Claws::Options.parse
49
+ options.source.should == 'elb'
50
+ options.connect.should be_false
51
+ end
52
+
53
+ cli %w{--source elb} do
54
+ options = Claws::Options.parse
55
+ options.source.should == 'elb'
56
+ options.connect.should be_false
57
+ end
58
+ end
59
+
60
+ context 'capistrano' do
61
+ it 'defines the environment' do
62
+ cli %w{-s production app} do
63
+ Claws::Options.parse.environment.should == 'production'
64
+ end
65
+ end
66
+
67
+ it 'defines the role' do
68
+ cli %w{-s production app} do
69
+ Claws::Options.parse.role.should == 'app'
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,94 @@
1
+ require 'spec_helper'
2
+ require 'aws-sdk'
3
+ require 'claws/presenter/ec2'
4
+ require 'claws/capistrano'
5
+
6
+ describe Claws::EC2::Presenter do
7
+ subject { Claws::EC2::Presenter }
8
+
9
+ before :each do
10
+ host = 'ec2-263-56-231-91.compute-1.amazonaws.com'
11
+
12
+ full_instance = double('AWS::EC2',
13
+ :public_dns => host,
14
+ :security_groups => [
15
+ double(AWS::EC2::SecurityGroup, :name => 'search', :id => 'sg-0f0f0f0f'),
16
+ double(AWS::EC2::SecurityGroup, :name => 'mongo', :id => 'sg-0d0d0d0d'),
17
+ double(AWS::EC2::SecurityGroup, :name => 'app', :id => 'sg-0c0c0c0c'),
18
+ ],
19
+ :tags => double(AWS::EC2::ResourceTagCollection, :select => [
20
+ ['environment', 'production'],
21
+ ['function', 'master'],
22
+ ],
23
+ 'has_key?'.to_sym => true
24
+ ),
25
+ :elastic_ip => '11.111.111.111'
26
+ )
27
+
28
+ cap = double('Claws::Capistrano')
29
+ cap.stub(:roles).with(host).and_return(%w{app web})
30
+
31
+ @full_presenter = subject.new(full_instance, cap.roles(full_instance.public_dns))
32
+
33
+ less_instance = double(AWS::EC2, :tags => nil)
34
+ @less_presenter = subject.new(less_instance)
35
+ end
36
+
37
+ describe '#initialize' do
38
+ it 'requires a valid ec2 instance' do
39
+ expect {
40
+ subject.new
41
+ }.to raise_error =~ /ArgumentError/
42
+ end
43
+ end
44
+
45
+ describe '#roles' do
46
+ it 'can be defined' do
47
+ @full_presenter.roles.should == 'app, web'
48
+ end
49
+
50
+ it 'are not required' do
51
+ @less_presenter.roles.should == 'N/A'
52
+ end
53
+ end
54
+
55
+ describe '#tags' do
56
+ it 'present a string summary' do
57
+ @full_presenter.tags.should == 'environment: production, function: master'
58
+ end
59
+
60
+ it 'are not required' do
61
+ @less_presenter.tags.should == 'N/A'
62
+ end
63
+ end
64
+
65
+ describe '#security_groups' do
66
+ it 'presents summary of names' do
67
+ @full_presenter.security_groups.should == 'sg-0f0f0f0f: search, sg-0d0d0d0d: mongo, sg-0c0c0c0c: app'
68
+ end
69
+
70
+ it 'are not required' do
71
+ @less_presenter.security_groups.should == 'N/A'
72
+ end
73
+ end
74
+
75
+ describe '#public_dns' do
76
+ it 'displays when available' do
77
+ @full_presenter.public_dns.should == 'ec2-263-56-231-91.compute-1.amazonaws.com'
78
+ end
79
+
80
+ it 'is not required' do
81
+ @less_presenter.public_dns.should == 'N/A'
82
+ end
83
+ end
84
+
85
+ describe '#elastic_ip' do
86
+ it 'displays when available' do
87
+ @full_presenter.elastic_ip.should == '11.111.111.111'
88
+ end
89
+
90
+ it 'is not required' do
91
+ @less_presenter.elastic_ip.should == 'N/A'
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,4 @@
1
+ $: << File.join(File.dirname(__FILE__), '..', 'lib')
2
+
3
+ Dir[File.dirname(__FILE__) + "/../lib/**/*.rb"].each {|f| require f}
4
+ Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each {|f| require f}
@@ -0,0 +1,11 @@
1
+ require 'stringio'
2
+
3
+ def capture_stdout
4
+ $stdout = StringIO.new
5
+ $stdin = StringIO.new("y\n")
6
+ yield
7
+ $stdout.string.strip
8
+ ensure
9
+ $stdout = STDOUT
10
+ $stdin = STDIN
11
+ end
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+ require 'claws/support'
3
+
4
+ describe Claws::Support do
5
+ subject { Array.new }
6
+
7
+ describe '#try' do
8
+ it 'handles undefined methods sanely' do
9
+ subject.try('asdf').should be_nil
10
+ end
11
+
12
+ it 'performs defined methods' do
13
+ subject.try('push', 2)
14
+ subject.should == [2]
15
+ end
16
+ end
17
+ end
metadata ADDED
@@ -0,0 +1,120 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: claws
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Wes
9
+ - Bailey
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2012-05-24 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: bundler
17
+ requirement: &2156422520 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: 1.0.0
23
+ type: :development
24
+ prerelease: false
25
+ version_requirements: *2156422520
26
+ - !ruby/object:Gem::Dependency
27
+ name: command_line_reporter
28
+ requirement: &2156421940 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: 3.2.1
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: *2156421940
37
+ - !ruby/object:Gem::Dependency
38
+ name: aws-sdk
39
+ requirement: &2156420960 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ! '>='
43
+ - !ruby/object:Gem::Version
44
+ version: '1.0'
45
+ type: :runtime
46
+ prerelease: false
47
+ version_requirements: *2156420960
48
+ description: A command line tool that provides a configurable report on the status
49
+ of all of your EC2 hosts and provides trivial ssh access for connectivity. Never
50
+ copy and paste the public dns for a host again!
51
+ email: baywes@gmail.com
52
+ executables:
53
+ - claws
54
+ extensions: []
55
+ extra_rdoc_files: []
56
+ files:
57
+ - lib/claws/capistrano.rb
58
+ - lib/claws/collection/base.rb
59
+ - lib/claws/collection/ec2.rb
60
+ - lib/claws/command/ec2.rb
61
+ - lib/claws/command/initialize.rb
62
+ - lib/claws/configuration.rb
63
+ - lib/claws/options.rb
64
+ - lib/claws/presenter/ec2.rb
65
+ - lib/claws/report/ec2.rb
66
+ - lib/claws/support.rb
67
+ - lib/claws/version.rb
68
+ - lib/claws.rb
69
+ - README.md
70
+ - spec/base_spec.rb
71
+ - spec/capistrano_spec.rb
72
+ - spec/collection/base_spec.rb
73
+ - spec/collection/ec2_spec.rb
74
+ - spec/command/ec2_spec.rb
75
+ - spec/command/initialize_spec.rb
76
+ - spec/configuration_spec.rb
77
+ - spec/options_spec.rb
78
+ - spec/presenter/ec2_spec.rb
79
+ - spec/spec_helper.rb
80
+ - spec/support/helpers/stdout_helper.rb
81
+ - spec/support_spec.rb
82
+ - !binary |-
83
+ YmluL2NsYXdz
84
+ homepage: http://github.com/wbailey/claws
85
+ licenses: []
86
+ post_install_message:
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ! '>='
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ requirements: []
103
+ rubyforge_project:
104
+ rubygems_version: 1.8.15
105
+ signing_key:
106
+ specification_version: 3
107
+ summary: A Command Line Tool For Amazon Web Services
108
+ test_files:
109
+ - spec/base_spec.rb
110
+ - spec/capistrano_spec.rb
111
+ - spec/collection/base_spec.rb
112
+ - spec/collection/ec2_spec.rb
113
+ - spec/command/ec2_spec.rb
114
+ - spec/command/initialize_spec.rb
115
+ - spec/configuration_spec.rb
116
+ - spec/options_spec.rb
117
+ - spec/presenter/ec2_spec.rb
118
+ - spec/spec_helper.rb
119
+ - spec/support/helpers/stdout_helper.rb
120
+ - spec/support_spec.rb