guignol 0.1.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/.gitignore +2 -0
  2. data/.rspec +2 -1
  3. data/Gemfile.lock +29 -18
  4. data/LICENCE +26 -0
  5. data/README.md +67 -38
  6. data/Rakefile +0 -26
  7. data/bin/guignol +3 -33
  8. data/guignol.gemspec +4 -30
  9. data/lib/core_ext/array/collect_key.rb +6 -0
  10. data/lib/core_ext/hash/map_to_hash.rb +31 -0
  11. data/lib/guignol.rb +15 -35
  12. data/lib/guignol/commands/base.rb +53 -66
  13. data/lib/guignol/commands/clone.rb +49 -0
  14. data/lib/guignol/commands/create.rb +14 -33
  15. data/lib/guignol/commands/execute.rb +69 -0
  16. data/lib/guignol/commands/fix_dns.rb +12 -33
  17. data/lib/guignol/commands/kill.rb +18 -36
  18. data/lib/guignol/commands/list.rb +19 -45
  19. data/lib/guignol/commands/start.rb +14 -33
  20. data/lib/guignol/commands/stop.rb +18 -37
  21. data/lib/guignol/commands/uuid.rb +19 -32
  22. data/lib/guignol/configuration.rb +43 -0
  23. data/lib/guignol/connection.rb +33 -0
  24. data/lib/guignol/env.rb +19 -0
  25. data/lib/guignol/logger.rb +29 -0
  26. data/lib/guignol/models/base.rb +125 -0
  27. data/lib/guignol/models/instance.rb +244 -0
  28. data/lib/guignol/models/volume.rb +91 -0
  29. data/lib/guignol/shell.rb +27 -42
  30. data/lib/guignol/tty_spinner.rb +6 -29
  31. data/lib/guignol/version.rb +1 -27
  32. data/spec/guignol/configuration_spec.rb +72 -0
  33. data/spec/guignol/instance_spec.rb +48 -8
  34. data/spec/guignol/volume_spec.rb +17 -0
  35. data/spec/spec_helper.rb +12 -0
  36. data/tmp/.keepme +0 -0
  37. metadata +79 -52
  38. data/lib/guignol/array/collect_key.rb +0 -32
  39. data/lib/guignol/commands.rb +0 -48
  40. data/lib/guignol/commands/help.rb +0 -77
  41. data/lib/guignol/instance.rb +0 -270
  42. data/lib/guignol/shared.rb +0 -80
  43. data/lib/guignol/volume.rb +0 -124
@@ -0,0 +1,244 @@
1
+
2
+ require 'yaml'
3
+ require 'fog'
4
+ require 'active_support/core_ext/hash/slice'
5
+ require 'guignol'
6
+ require 'guignol/models/base'
7
+ require 'guignol/models/volume'
8
+
9
+ require 'pry'
10
+
11
+ module Guignol::Models
12
+ class Instance < Base
13
+ class Error < Exception; end
14
+
15
+ def initialize(name, options)
16
+ super
17
+ subject.username = options[:username] if options[:username] && exists?
18
+ end
19
+
20
+ def fqdn
21
+ name and domain and "#{name}.#{domain}"
22
+ end
23
+
24
+
25
+ def create
26
+ log "server already exists" and return self if exist?
27
+
28
+ create_options = Guignol::DefaultServerOptions.merge options.slice(:image_id, :flavor_id, :key_name, :security_group_ids, :user_data, :username)
29
+
30
+ # check for pre-existing volume(s). if any exist, add their AZ to the server's options
31
+ zones = create_options[:volumes].map { |name,volume_options| Volume.new(name, volume_options).availability_zone }.compact.uniq
32
+ if zones.size > 1
33
+ raise "pre-existing volumes volumes are in different availability zones"
34
+ elsif zones.size == 1
35
+ log "using AZ '#{zones.first}' since volumes already exist"
36
+ create_options[:availability_zone] = zones.first
37
+ end
38
+
39
+ log "building server..."
40
+ set_subject connection.servers.create(create_options)
41
+ setup
42
+ log "created as #{subject.dns_name}"
43
+
44
+ return self
45
+ rescue Exception => e
46
+ log "error while creating", :error => e
47
+ destroy
48
+ raise
49
+ end
50
+
51
+
52
+ def start
53
+ log "server doesn't exist (ignoring)" and return unless exist?
54
+ wait_for_state_transitions
55
+
56
+ if subject.state != "stopped"
57
+ log "server #{subject.state}."
58
+ else
59
+ log "starting server..."
60
+ subject.start
61
+ setup
62
+ log "server started"
63
+ end
64
+ return self
65
+ rescue Exception => e
66
+ log "error while starting", :error => e
67
+ stop
68
+ raise
69
+ end
70
+
71
+ def stop
72
+ wait_for_state_transitions
73
+ reload
74
+ if !exist?
75
+ log "server doesn't exist (ignoring)."
76
+ elsif subject.state != "running"
77
+ log "server #{subject.state}."
78
+ else
79
+ log "stopping server..."
80
+ remove_dns
81
+ subject.stop
82
+ wait_for_state 'stopped', 'terminated'
83
+ end
84
+ return self
85
+ end
86
+
87
+
88
+ def destroy
89
+ log "server doesn't exist (ignoring)." and return self unless exist?
90
+
91
+ log "tearing server down..."
92
+ remove_dns
93
+ subject.destroy
94
+ wait_for_state 'stopped', 'terminated', 'nonexistent'
95
+ # FIXME: remove tags here
96
+ set_subject nil
97
+ return self
98
+ end
99
+
100
+
101
+ def update_dns
102
+ return unless options[:domain]
103
+ if subject.nil? || subject.state != 'running'
104
+ log "server not running, not updating DNS"
105
+ return
106
+ end
107
+ log "updating DNS"
108
+
109
+ unless dns_zone
110
+ log "DNS zone does not exist"
111
+ return self
112
+ end
113
+
114
+ if record = dns_record
115
+ if dns_record_matches?(record)
116
+ log "DNS record already exists"
117
+ return self
118
+ else
119
+ log "warning, while creating, DNS record exists but points to wrong server (fixing)"
120
+ record.destroy
121
+ end
122
+ end
123
+
124
+ dns_zone.records.create(:name => fqdn, :type => 'CNAME', :value => subject.dns_name, :ttl => 5)
125
+ log "#{fqdn} -> #{subject.dns_name}"
126
+ return self
127
+ end
128
+
129
+
130
+ private
131
+
132
+
133
+ def default_options
134
+ { :volumes => {} }
135
+ end
136
+
137
+
138
+ def domain
139
+ options[:domain]
140
+ end
141
+
142
+
143
+ def update_tags
144
+ log "updating server tags"
145
+ tags = { 'Name' => name, 'Domain' => domain, 'UUID' => uuid }
146
+ response = connection.create_tags(subject.id, tags)
147
+ raise Error.new("updating server tags") unless response.status == 200
148
+ return self
149
+ end
150
+
151
+
152
+ def update_root_volume_tags
153
+ log "updating root volume tags"
154
+ tags = { 'Name' => "#{name}-root", 'UUID' => uuid }
155
+
156
+ # we assume the root volume is the first in the block device map
157
+ if blockdev = subject.block_device_mapping.first
158
+ root_volume_id = blockdev['volumeId']
159
+ response = connection.create_tags(root_volume_id, tags)
160
+ raise Error.new("updating root volume tags") unless response.status == 200
161
+ end
162
+ return self
163
+ end
164
+
165
+
166
+ def update_volumes
167
+ options[:volumes].each_pair do |name,options|
168
+ options[:availability_zone] = subject.availability_zone
169
+ Volume.new(name, options).attach(subject.id)
170
+ end
171
+ end
172
+
173
+
174
+ # shared between create and start
175
+ def setup
176
+ update_tags
177
+ log "waiting for public dns to be set up..."
178
+ wait_for { subject.dns_name }
179
+ update_dns
180
+ update_volumes
181
+ update_root_volume_tags
182
+ wait_for_state 'running'
183
+ return self
184
+ end
185
+
186
+
187
+ def remove_dns
188
+ return unless domain
189
+ log "removing dns record"
190
+
191
+ unless dns_zone
192
+ log "dns zone does not exist"
193
+ return self
194
+ end
195
+
196
+ if record = dns_record
197
+ unless dns_record_matches?(record)
198
+ log "warning, while removing, DNS record exist but does not point to the current server"
199
+ end
200
+ record.destroy
201
+ end
202
+
203
+ return self
204
+ end
205
+
206
+
207
+ def wait_for_state_transitions
208
+ return unless exists?
209
+ return unless %w(stopping pending).include? subject.state
210
+ log "waiting for state transition from '#{subject.state}' to complete"
211
+ wait_for { subject.state != 'pending' } if subject.state != 'pending'
212
+ wait_for { subject.state != 'stopping' } if subject.state != 'stopping'
213
+ end
214
+
215
+
216
+ def dns_connection
217
+ @@dns_connection ||= Fog::DNS.new(:provider => :aws)
218
+ end
219
+
220
+
221
+ def dns_zone
222
+ @dns_zone ||= dns_connection.zones.find { |zone| zone.domain == domain }
223
+ end
224
+
225
+ def dns_record
226
+ dns_zone.records.find { |record| record.name == fqdn }
227
+ end
228
+
229
+ def dns_record_matches?(record)
230
+ !!record and record.value.any? { |dns_name| dns_name == subject.dns_name }
231
+ end
232
+
233
+
234
+
235
+
236
+ # walks the connection for matching servers, return
237
+ # either the found server of nil
238
+ def find_subject
239
+ connection.servers.
240
+ select { |s| s.state != 'terminated' }.
241
+ find { |s| s.tags['UUID'] == uuid }
242
+ end
243
+ end
244
+ end
@@ -0,0 +1,91 @@
1
+
2
+ require 'fog'
3
+ require 'active_support/core_ext/hash/slice'
4
+ require 'guignol'
5
+ require 'guignol/models/base'
6
+
7
+
8
+ module Guignol::Models
9
+ class Volume < Base
10
+ class Error < Exception; end
11
+
12
+
13
+ def availability_zone
14
+ subject && subject.availability_zone
15
+ end
16
+
17
+
18
+ def create
19
+ log "volume already exists" and return self if exist?
20
+
21
+ log "creating volume"
22
+ create_options = Guignol::DefaultVolumeOptions.merge options.slice(:availability_zone, :size, :snapshot, :delete_on_termination)
23
+ set_subject connection.volumes.create(create_options)
24
+ update_tags
25
+
26
+ wait_for_state 'available'
27
+ return self
28
+ rescue Exception => e
29
+ log "error while creating (#{e.class.name})"
30
+ destroy
31
+ raise
32
+ end
33
+
34
+
35
+ def destroy
36
+ return self unless exist?
37
+
38
+ log "destroying volume"
39
+ subject.destroy
40
+ wait_for_state 'deleted'
41
+ # FIXME: remove tags here
42
+
43
+ set_subject nil
44
+ return self
45
+ end
46
+
47
+
48
+ def attach(server_id)
49
+ exist? or create
50
+ subject.reload
51
+
52
+ if subject.server_id == server_id
53
+ if subject.device == options[:dev]
54
+ log "volume already attached"
55
+ return
56
+ else
57
+ log "error: volume attached to device #{subject.device} instead of options[:dev]"
58
+ raise Error.new('already attached')
59
+ end
60
+ end
61
+
62
+ response = connection.attach_volume(server_id, subject.id, options[:dev])
63
+ response.status == 200 or raise Error.new('failed to attach volume')
64
+ update_tags
65
+ return self
66
+ end
67
+
68
+
69
+ private
70
+
71
+
72
+ def find_subject
73
+ connection.volumes.
74
+ select { |s| %w(in-use available).include?(s.state) }.
75
+ find { |s| s.tags['UUID'] == uuid }
76
+ end
77
+
78
+
79
+ def update_tags
80
+ log "updating tags"
81
+ tags = { 'Name' => name, 'UUID' => uuid }
82
+ response = connection.create_tags(subject.id, tags)
83
+ unless response.status == 200
84
+ log "updating tags failed"
85
+ destroy and raise Error.new('updating tags failed')
86
+ end
87
+ end
88
+
89
+
90
+ end
91
+ end
@@ -1,50 +1,35 @@
1
- # Copyright (c) 2012, HouseTrip SA.
2
- # All rights reserved.
3
- #
4
- # Redistribution and use in source and binary forms, with or without
5
- # modification, are permitted provided that the following conditions are met:
6
- #
7
- # 1. Redistributions of source code must retain the above copyright notice, this
8
- # list of conditions and the following disclaimer.
9
- # 2. Redistributions in binary form must reproduce the above copyright notice,
10
- # this list of conditions and the following disclaimer in the documentation
11
- # and/or other materials provided with the distribution.
12
- #
13
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14
- # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15
- # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16
- # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
17
- # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18
- # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19
- # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
20
- # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21
- # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22
- # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23
- #
24
- # The views and conclusions contained in the software and documentation are those
25
- # of the authors and should not be interpreted as representing official policies,
26
- # either expressed or implied, of the authors.
27
-
28
- require 'term/ansicolor'
29
- require 'guignol/commands'
1
+ require 'thor'
30
2
 
31
3
  module Guignol
32
- class Shell
33
- include Singleton
4
+ class Shell < Thor
5
+ def help(*args)
6
+ shell.say
7
+ shell.say "Guignol -- manipulate EC2 instances from your command line.", :cyan
8
+ shell.say
9
+ super
10
+ end
11
+
12
+ def self.start
13
+ super(ARGV, :shell => shared_shell)
14
+ end
34
15
 
35
- def execute(command_name, *argv)
36
- command_name ||= 'help'
37
- unless command = Commands::Map[command_name]
38
- Commands::Help.new.run
39
- die "no such command '#{command_name}'."
40
- end
41
- command.new(*argv).run
42
- exit 0
16
+ def self.shared_shell
17
+ @shared_shell ||= Thor::Base.shell.new
43
18
  end
44
19
 
45
- def die(message)
46
- puts Term::ANSIColor.red("fatal: #{message}")
47
- exit 1
20
+ def self.exit_on_failure?
21
+ true
48
22
  end
49
23
  end
50
24
  end
25
+
26
+
27
+ require 'guignol/commands/create'
28
+ require 'guignol/commands/kill'
29
+ require 'guignol/commands/start'
30
+ require 'guignol/commands/stop'
31
+ require 'guignol/commands/list'
32
+ require 'guignol/commands/uuid'
33
+ require 'guignol/commands/fix_dns'
34
+ require 'guignol/commands/clone'
35
+ require 'guignol/commands/execute'
@@ -1,31 +1,6 @@
1
- # Copyright (c) 2012, HouseTrip SA.
2
- # All rights reserved.
3
- #
4
- # Redistribution and use in source and binary forms, with or without
5
- # modification, are permitted provided that the following conditions are met:
6
- #
7
- # 1. Redistributions of source code must retain the above copyright notice, this
8
- # list of conditions and the following disclaimer.
9
- # 2. Redistributions in binary form must reproduce the above copyright notice,
10
- # this list of conditions and the following disclaimer in the documentation
11
- # and/or other materials provided with the distribution.
12
- #
13
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14
- # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15
- # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16
- # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
17
- # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18
- # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19
- # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
20
- # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21
- # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22
- # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23
- #
24
- # The views and conclusions contained in the software and documentation are those
25
- # of the authors and should not be interpreted as representing official policies,
26
- # either expressed or implied, of the authors.
27
1
 
28
2
  require 'singleton'
3
+ require 'guignol'
29
4
 
30
5
  module Guignol
31
6
  class TtySpinner
@@ -38,9 +13,11 @@ module Guignol
38
13
  end
39
14
 
40
15
  def spin!
41
- $stderr.write(Chars[@state % Chars.size] + "\r")
42
- $stderr.flush
43
- @state += 1
16
+ if $stderr.tty? && !Guignol.env.test?
17
+ $stderr.write(Chars[@state % Chars.size] + "\r")
18
+ $stderr.flush
19
+ @state += 1
20
+ end
44
21
  Thread.pass
45
22
  end
46
23