standup 0.2.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/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ .DS_Store
2
+ .idea
3
+ pkg
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Ilia Ablamonov, Cloud Castle Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,8 @@
1
+ = Standup
2
+
3
+ Standup is an application deployment and infrastructure management tool for Rails and Amazon EC2.
4
+
5
+ == Copyright
6
+
7
+ Copyright (c) 2010 Ilia Ablamonov, Cloud Castle Inc.
8
+ See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,24 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = 'standup'
8
+ gem.summary = %Q{Standup is an application deployment and infrastructure management tool for Rails and Amazon EC2}
9
+ gem.description = %Q{}
10
+ gem.email = 'ilia@flamefork.ru'
11
+ gem.homepage = 'http://github.com/Flamefork/standup'
12
+ gem.authors = ['Ilia Ablamonov', 'Cloud Castle Inc.']
13
+ gem.add_dependency 'activesupport', '>= 3.0'
14
+ gem.add_dependency 'settingslogic', '>= 2.0'
15
+ gem.add_dependency 'amazon-ec2', '>= 0.9'
16
+ gem.add_dependency 'aws-s3', '>= 0.5'
17
+ gem.add_dependency 'net-ssh', '>= 2.0'
18
+ gem.add_dependency 'highline', '>= 1.5.2'
19
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
20
+ end
21
+ Jeweler::GemcutterTasks.new
22
+ rescue LoadError
23
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
24
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.0
@@ -0,0 +1,11 @@
1
+ module Kernel
2
+ def bright_p message, color = ''
3
+ puts "\e[1m#{color}#{message}\e[0m"
4
+ end
5
+
6
+ def bright_ask message, echo = true
7
+ require 'highline'
8
+ bright_p message, HighLine::GREEN
9
+ HighLine.new.ask('') {|q| q.echo = echo}
10
+ end
11
+ end
@@ -0,0 +1,61 @@
1
+ module Standup
2
+ module EC2
3
+ class Base
4
+ def initialize info = false
5
+ case info
6
+ when Hash
7
+ set_info info
8
+ when true
9
+ load_info
10
+ when false
11
+ # nothing
12
+ end
13
+ end
14
+
15
+ def self.info_reader *names
16
+ names.each do |name|
17
+ class_eval "def #{name}; read_info_field :#{name}; end", __FILE__, __LINE__
18
+ end
19
+ end
20
+
21
+ def exists?
22
+ read_info_field :exists
23
+ end
24
+
25
+ def load_info; end
26
+
27
+ protected
28
+
29
+ def read_info_field name
30
+ load_info unless instance_variable_defined?(:"@#{name}")
31
+ instance_variable_set(:"@#{name}", nil) unless instance_variable_defined?(:"@#{name}")
32
+ @exists = true
33
+ instance_variable_get(:"@#{name}")
34
+ rescue AWS::InvalidGroupNotFound
35
+ rescue AWS::InvalidInstanceIDNotFound
36
+ rescue AWS::InvalidVolumeIDNotFound
37
+ @exists = false
38
+ nil
39
+ end
40
+
41
+ def set_info info
42
+ info.each do |key, value|
43
+ instance_variable_set :"@#{key}", value
44
+ end
45
+ end
46
+
47
+ def list
48
+ self.class.list
49
+ end
50
+
51
+ def api
52
+ self.class.api
53
+ end
54
+
55
+ def self.api
56
+ @@api ||= AWS::EC2::Base.new :access_key_id => Settings.aws.access_key_id,
57
+ :secret_access_key => Settings.aws.secret_access_key
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,49 @@
1
+ module Standup
2
+ module EC2
3
+ class ElasticIP < Base
4
+ def initialize ip, info = false
5
+ @ip = ip
6
+ super info
7
+ end
8
+
9
+ info_reader :ip, :attached_to
10
+
11
+ def self.list reload = false
12
+ if !class_variable_defined?(:@@list) || reload
13
+ @@list = {}
14
+ result = api.describe_addresses
15
+ result.addressesSet.item.each do |item|
16
+ @@list[item.publicIp] = new item.publicIp, :attached_to => Instance.new(item.instanceId)
17
+ end if result.addressesSet
18
+ end
19
+ @@list
20
+ end
21
+
22
+ def self.create
23
+ ip = api.allocate_address.publicIp
24
+ list[ip] = ElasticIP.new ip
25
+ end
26
+
27
+ def destroy
28
+ api.release_address :public_ip => @ip
29
+ list.delete @ip
30
+ end
31
+
32
+ def attach_to instance
33
+ api.associate_address :instance_id => instance.id,
34
+ :public_ip => @ip
35
+ @attached_to = instance
36
+ end
37
+
38
+ def detach
39
+ api.disassociate_address :public_ip => @ip
40
+ @attached_to = nil
41
+ end
42
+
43
+ def load_info
44
+ result = api.describe_addresses :public_ip => @ip
45
+ @attached_to = Instance.new(result.addressesSet.item[0].instanceId)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,80 @@
1
+ module Standup
2
+ module EC2
3
+ class Instance < Base
4
+ def initialize id, info = false
5
+ @id = id
6
+ super info
7
+ end
8
+
9
+ info_reader :id, :external_ip, :internal_ip, :state, :security_groups, :architecture
10
+
11
+ def self.list reload = false
12
+ if !class_variable_defined?(:@@list) || reload
13
+ @@list = {}
14
+ result = api.describe_instances
15
+ result.reservationSet.item.each do |ritem|
16
+ ritem.instancesSet.item.each do |item|
17
+ @@list[item.instanceId] = new item.instanceId, build_info(ritem, item)
18
+ end if ritem.instancesSet
19
+ end if result.reservationSet
20
+ end
21
+ @@list
22
+ end
23
+
24
+ def self.group_running reload = false
25
+ result = {}
26
+ SecurityGroup.list(reload).each {|name, _| result[name] = []}
27
+ list(reload).each do |_, instance|
28
+ instance.security_groups.each do |sg|
29
+ result[sg.name] << instance
30
+ end unless [:terminated, :"shutting-down"].include?(instance.state)
31
+ end
32
+ result
33
+ end
34
+
35
+ def self.create image_id, instance_type, security_groups
36
+ response = api.run_instances :image_id => image_id,
37
+ :key_name => Settings.aws.keypair_name,
38
+ :instance_type => instance_type,
39
+ :security_group => security_groups.map(&:name),
40
+ :availability_zone => Settings.aws.availability_zone
41
+ id = response.instancesSet.item[0].instanceId
42
+ list[id] = Instance.new id
43
+ end
44
+
45
+ def terminate
46
+ api.terminate_instances :instance_id => @id
47
+ end
48
+
49
+ def reboot
50
+ api.reboot_instances :instance_id => @id
51
+ end
52
+
53
+ def wait_until timeout = 300
54
+ sleeping = 0
55
+ while yield(self) && sleeping < timeout
56
+ sleeping += sleep 5
57
+ STDOUT.print '.'
58
+ STDOUT.flush
59
+ load_info
60
+ end
61
+ print "\n"
62
+ end
63
+
64
+ def load_info
65
+ ritem = api.describe_instances(:instance_id => @id).reservationSet.item[0]
66
+ set_info self.class.build_info(ritem, ritem.instancesSet.item[0])
67
+ end
68
+
69
+ protected
70
+
71
+ def self.build_info ritem, item
72
+ return :external_ip => item.ipAddress,
73
+ :internal_ip => item.privateIpAddress,
74
+ :state => item.instanceState.name.to_sym,
75
+ :architecture => item.architecture,
76
+ :security_groups => ritem.groupSet.item.map{|i| SecurityGroup.new(i.groupId)}
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,95 @@
1
+ module Standup
2
+ module EC2
3
+ class SecurityGroup < Base
4
+ IPRule = Struct.new(:ip, :protocol, :from_port, :to_port)
5
+
6
+ def initialize name, info = false
7
+ @name = name
8
+ super info
9
+ end
10
+
11
+ info_reader :name, :description, :rules
12
+
13
+ def self.list reload = false
14
+ if !class_variable_defined?(:@@list) || reload
15
+ @@list = {}
16
+ result = api.describe_security_groups
17
+ result.securityGroupInfo.item.each do |gitem|
18
+ @@list[gitem.groupName] = new gitem.groupName, build_info(gitem)
19
+ end if result.securityGroupInfo
20
+ end
21
+ @@list
22
+ end
23
+
24
+ def self.create name, description = name
25
+ api.create_security_group :group_name => name,
26
+ :group_description => description
27
+ list[name] = SecurityGroup.new name,
28
+ :description => description,
29
+ :rules => []
30
+ end
31
+
32
+ def delete
33
+ api.delete_security_group :group_name => @name
34
+ list.delete @name
35
+ end
36
+
37
+ def add_rule rule
38
+ api.authorize_security_group_ingress build_rules_opts(rule)
39
+ rules << rule
40
+ end
41
+
42
+ def remove_rule rule
43
+ api.revoke_security_group_ingress build_rules_opts(rule)
44
+ rules.delete rule
45
+ end
46
+
47
+ def load_info
48
+ result = api.describe_security_groups :group_name => [@name]
49
+ set_info self.class.build_info(result.securityGroupInfo.item[0])
50
+ end
51
+
52
+ def hash
53
+ @name.hash
54
+ end
55
+
56
+ def eql? other
57
+ @name == other.name
58
+ end
59
+
60
+ private
61
+
62
+ def build_rules_opts rule
63
+ case rule
64
+ when SecurityGroup
65
+ return :group_name => @name,
66
+ :source_security_group_name => rule.name,
67
+ :source_security_group_owner_id => Settings.aws.account_id
68
+ when IPRule
69
+ return :group_name => @name,
70
+ :ip_protocol => rule.protocol,
71
+ :from_port => rule.from_port,
72
+ :to_port => rule.to_port,
73
+ :cidr_ip => rule.ip
74
+ end
75
+ end
76
+
77
+ def self.build_info gitem
78
+ rules = Set.new
79
+ gitem.ipPermissions.item.each do |pitem|
80
+ rules << if pitem.groups
81
+ SecurityGroup.new pitem.groups.item[0].groupName
82
+ else
83
+ IPRule.new pitem.ipRanges.item[0].cidrIp,
84
+ pitem.ipProtocol,
85
+ pitem.fromPort,
86
+ pitem.toPort
87
+ end
88
+ end if gitem.ipPermissions
89
+
90
+ return :description => gitem.groupDescription,
91
+ :rules => rules
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,57 @@
1
+ module Standup
2
+ module EC2
3
+ class Volume < Base
4
+ def initialize id, info = false
5
+ @id = id
6
+ super info
7
+ end
8
+
9
+ info_reader :id, :attached_to
10
+
11
+ def self.list reload = false
12
+ if !class_variable_defined?(:@@list) || reload
13
+ @@list = {}
14
+ response = api.describe_volumes
15
+ response.volumeSet.item.each do |item|
16
+ instance = item.attachmentSet ? Instance.new(item.attachmentSet.item[0].instanceId) : nil
17
+ @@list[item.volumeId] = Volume.new item.volumeId,
18
+ :status => item.status.to_sym,
19
+ :attached_to => instance
20
+ end if response.volumeSet
21
+ end
22
+ @@list
23
+ end
24
+
25
+ def self.create size
26
+ response = api.create_volume :size => size.to_s,
27
+ :availability_zone => Settings.aws.availability_zone
28
+ list[response.volumeId] = Volume.new response.volumeId
29
+ end
30
+
31
+ def destroy
32
+ api.delete_volume :volume_id => @id
33
+ list.delete @id
34
+ end
35
+
36
+ def attach_to instance, device
37
+ api.attach_volume :volume_id => @id,
38
+ :instance_id => instance.id,
39
+ :device => device
40
+ @attached_to = instance
41
+ end
42
+
43
+ def detach
44
+ api.detach_volume :volume_id => @id,
45
+ :force => 'true'
46
+ @attached_to = nil
47
+ end
48
+
49
+ def load_info
50
+ response = api.describe_volumes :volume_id => @id
51
+ item = response.volumeSet.item[0]
52
+ @status = item.status.to_sym
53
+ @attached_to = item.attachmentSet ? Instance.new(item.attachmentSet.item[0].instanceId) : nil
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,5 @@
1
+ require 'standup/ec2/base'
2
+ require 'standup/ec2/instance'
3
+ require 'standup/ec2/security_group'
4
+ require 'standup/ec2/elastic_ip'
5
+ require 'standup/ec2/volume'
@@ -0,0 +1,48 @@
1
+ module Standup
2
+ class Node
3
+ def initialize name
4
+ @name = name
5
+
6
+ @scripts = ActiveSupport::HashWithIndifferentAccess.new
7
+ Standup.scripts.each do |sname, script|
8
+ @scripts[sname] = script.new self
9
+ end
10
+ @remoting = nil
11
+ end
12
+
13
+ attr_reader :name, :scripts
14
+
15
+ def run_script script_name
16
+ scripts[script_name].titled_run
17
+ close_remoting
18
+ end
19
+
20
+ def instance
21
+ @instance ||= EC2::Instance.group_running[id_group].try(:first)
22
+ end
23
+
24
+ def ssh_string
25
+ return '' unless instance
26
+ "ssh -i #{Settings.aws.keypair_file} -q -o StrictHostKeyChecking=no #{params.ec2.ssh_user}@#{instance.external_ip}"
27
+ end
28
+
29
+ def params
30
+ Settings.nodes[@name]
31
+ end
32
+
33
+ def remoting
34
+ @remoting ||= Remoting.new self
35
+ end
36
+
37
+ def id_group
38
+ "standup_node_#{@name}"
39
+ end
40
+
41
+ protected
42
+
43
+ def close_remoting
44
+ @remoting.close if @remoting
45
+ @remoting = nil
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,10 @@
1
+ require 'standup'
2
+ require 'rails'
3
+
4
+ module Standup
5
+ class Railtie < Rails::Railtie
6
+ rake_tasks do
7
+ load 'tasks/standup.rake'
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,133 @@
1
+ module Standup
2
+ class Remoting
3
+ def initialize node
4
+ @node = node
5
+ @host = @node.instance.external_ip
6
+ @keypair_file = Settings.aws.keypair_file
7
+ @user = @node.params.ec2.ssh_user
8
+ @ssh = nil
9
+ @path = nil
10
+ end
11
+
12
+ def download *files
13
+ options = files.pop
14
+ rsync wrap_to_remote(files), options[:to], options[:sudo]
15
+ end
16
+
17
+ def upload *files
18
+ options = files.pop
19
+ rsync files, wrap_to_remote(options[:to]), options[:sudo]
20
+ end
21
+
22
+ def remote_update file, body, opts = {}
23
+ tmpfile = Tempfile.new('file')
24
+
25
+ download file,
26
+ :to => tmpfile.path,
27
+ :sudo => opts[:sudo]
28
+
29
+ opts[:delimiter] ||= '# standup remote_update fragment'
30
+
31
+ initial = File.read(tmpfile.path)
32
+
33
+ if initial.empty?
34
+ bright_p "error during file upload. skipping", Highline::RED
35
+ return
36
+ end
37
+
38
+ to_change = "#{opts[:delimiter]}\n#{body}#{opts[:delimiter]}\n"
39
+ changed = initial.gsub /#{opts[:delimiter]}.*#{opts[:delimiter]}\n?/m, to_change
40
+
41
+ File.open(tmpfile.path, 'w') {|f| f.write changed}
42
+
43
+ upload tmpfile.path,
44
+ :to => file,
45
+ :sudo => opts[:sudo]
46
+ end
47
+
48
+ def in_dir path
49
+ raise ArgumentError, 'Only absolute paths allowed' unless path[0,1] == '/'
50
+ old_path = @path
51
+ @path = path
52
+ result = yield
53
+ @path = old_path
54
+ result
55
+ end
56
+
57
+ def exec command
58
+ command = @path ? "cd #{@path} && #{command}" : command
59
+ bright_p command
60
+ ssh.exec! command do |ch, _, data|
61
+ ch[:result] ||= ""
62
+ ch[:result] << data
63
+ print data
64
+ STDOUT.flush
65
+ end
66
+ end
67
+
68
+ def sudo command
69
+ exec "sudo #{command}"
70
+ end
71
+
72
+ def in_temp_dir &block
73
+ tmp_dirname = "/tmp/standup_tmp_#{rand 10000}"
74
+ exec "mkdir #{tmp_dirname}"
75
+ result = in_dir tmp_dirname, &block
76
+ exec "rm -rf #{tmp_dirname}"
77
+ result
78
+ end
79
+
80
+ def file_exists? path
81
+ exec("if [ -e #{path} ]; then echo 'true'; fi") == "true\n"
82
+ end
83
+
84
+ def install_packages *packages
85
+ packages = [*packages].flatten.join(' ')
86
+ sudo "apt-get -qqy install #{packages}"
87
+ end
88
+ alias :install_package :install_packages
89
+
90
+ def install_gem name, version = nil
91
+ if version
92
+ unless exec("gem list | grep #{name}").try(:[], version)
93
+ sudo "gem install #{name} -v #{version} --no-ri --no-rdoc"
94
+ end
95
+ else
96
+ sudo "gem install #{name} --no-ri --no-rdoc"
97
+ end
98
+ end
99
+
100
+ def close
101
+ @ssh.close if @ssh
102
+ @ssh = nil
103
+ end
104
+
105
+ protected
106
+
107
+ def ssh
108
+ @ssh ||= Net::SSH.start @host, @user,
109
+ :keys => @keypair_file,
110
+ :paranoid => false,
111
+ :timeout => 10
112
+ end
113
+
114
+ def rsync source, destination, sudo
115
+ command = [
116
+ 'rsync -azP --delete',
117
+ "-e 'ssh -i #{@keypair_file} -q -o StrictHostKeyChecking=no'",
118
+ ("--rsync-path='sudo rsync'" if sudo),
119
+ [*source].join(' '),
120
+ destination
121
+ ].compact.join(' ')
122
+
123
+ 3.times do
124
+ bright_p command
125
+ break if system command
126
+ end
127
+ end
128
+
129
+ def wrap_to_remote files
130
+ [*files].map{|f| "#{@user}@#{@host}:#{f}"}.join(' ')
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,52 @@
1
+ require 'active_support/hash_with_indifferent_access'
2
+
3
+ module Standup
4
+ module Scripts
5
+ class Base
6
+ def initialize node
7
+ @node = node
8
+ @remoting = nil
9
+ @params = if node.params[name].is_a? Hash
10
+ ActiveSupport::HashWithIndifferentAccess.new self.class.default_params.merge(node.params[name])
11
+ else
12
+ node.params[name] || self.class.default_params
13
+ end
14
+ end
15
+
16
+ class_attribute :name
17
+
18
+ class_attribute :default_params
19
+ self.default_params = {}
20
+
21
+ class_attribute :description
22
+
23
+ delegate :instance, :open_port, :open_ports, :remoting, :scripts,
24
+ :to => :@node
25
+
26
+ delegate :download, :upload, :remote_update, :exec, :sudo, :in_dir, :in_temp_dir, :file_exists?, :install_package, :install_packages, :install_gem,
27
+ :to => :remoting
28
+
29
+ attr_accessor :node, :params
30
+
31
+ def name
32
+ self.class.name
33
+ end
34
+
35
+ def titled_run
36
+ bright_p "#{@node.name}:#{name}", HighLine::CYAN
37
+ run
38
+ end
39
+
40
+ def script_file filename
41
+ [Standup.local_scripts_path, Standup.gem_scripts_path].each do |dir|
42
+ next unless dir
43
+ path = File.expand_path("#{name}/#{filename}", dir)
44
+ return path if File.exists? path
45
+ end
46
+ nil
47
+ end
48
+
49
+ def run; end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,16 @@
1
+ module Standup
2
+ begin
3
+ class Settings < Settingslogic
4
+ source 'config/standup.yml'
5
+ load!
6
+
7
+ aws['account_id'].gsub!(/\D/, '')
8
+ # keypair_file default to ~/.ssh/keypair_name.pem
9
+ aws['keypair_file'] ||= "#{File.expand_path '~'}/.ssh/#{aws.keypair_name}.pem"
10
+ end
11
+ rescue
12
+ require 'active_support/hash_with_indifferent_access'
13
+ remove_const :Settings
14
+ const_set :Settings, ActiveSupport::HashWithIndifferentAccess.new('nodes' => {})
15
+ end
16
+ end