claws 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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