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
@@ -1,32 +0,0 @@
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
- Array.class_eval do
29
- def collect_key(key)
30
- collect { |item| item.kind_of?(Hash) ? item[key] : nil }
31
- end
32
- end
@@ -1,48 +0,0 @@
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 'guignol/commands/create'
29
- require 'guignol/commands/kill'
30
- require 'guignol/commands/start'
31
- require 'guignol/commands/stop'
32
- require 'guignol/commands/help'
33
- require 'guignol/commands/list'
34
- require 'guignol/commands/uuid'
35
- require 'guignol/commands/fix_dns'
36
-
37
- module Guignol::Commands
38
- Map = {
39
- 'create' => Guignol::Commands::Create,
40
- 'kill' => Guignol::Commands::Kill,
41
- 'start' => Guignol::Commands::Start,
42
- 'stop' => Guignol::Commands::Stop,
43
- 'help' => Guignol::Commands::Help,
44
- 'list' => Guignol::Commands::List,
45
- 'uuid' => Guignol::Commands::UUID,
46
- 'fixdns' => Guignol::Commands::FixDNS,
47
- }
48
- end
@@ -1,77 +0,0 @@
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 'guignol/commands/base'
29
- require 'guignol/instance'
30
-
31
- module Guignol::Commands
32
- class Help
33
- def initialize(*argv)
34
- if help_for = argv.shift
35
- @command_class = Guignol::Commands::Map[help_for]
36
- @command_class.nil? and raise "no such command '#{help_for}'"
37
- end
38
- end
39
-
40
- def run
41
- if @command_class.respond_to?(:long_usage)
42
- puts @command_class.long_usage
43
- else
44
- usage
45
- end
46
- end
47
-
48
- def usage
49
- puts "usage: guignol <command> [options] [patterns]"
50
- puts "manipulate EC2 instances from your command line."
51
- puts
52
- puts "The commands are:"
53
- command_table =
54
- Guignol::Commands::Map.map { |command_name, command_class|
55
- usage = command_class.short_usage
56
- [command_name] + usage
57
- }
58
- puts format_table(command_table, :sep => " ")
59
- end
60
-
61
- def self.short_usage
62
- ["", "You're reading it !"]
63
- end
64
-
65
- private
66
-
67
- # Format a text table from an array of arrays. Rows separated by +sep+.
68
- def format_table(table, options={})
69
- sep = options.delete(:sep) || " | "
70
- columns = table.map { |row| row.size }.max
71
- widths = table.reduce([0] * columns) { |memo,row| row.map(&:size).zip(memo).map(&:max) }
72
- format = widths.map { |width| "%-#{width}s" }.join(sep)
73
- table.map { |row| format % row }.join("\n")
74
- end
75
- end
76
- end
77
-
@@ -1,270 +0,0 @@
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 'yaml'
29
- require 'fog'
30
- require 'md5'
31
- require 'active_support/core_ext/hash/slice'
32
- require 'guignol'
33
- require 'guignol/shared'
34
- require 'guignol/volume'
35
-
36
- module Guignol
37
- class Instance
38
- include Shared
39
-
40
- def initialize(options)
41
- @options = options.dup
42
- require_options :name, :uuid
43
-
44
- @options[:volumes] ||= []
45
- connection_options = DefaultConnectionOptions.dup.merge @options.slice(:region)
46
-
47
- @connection = Fog::Compute.new(connection_options)
48
-
49
- @subject = @connection.servers.
50
- select { |s| s.state != 'terminated' }.
51
- find { |s| s.tags['UUID'] == uuid }
52
- end
53
-
54
-
55
- def exist?
56
- !!@subject
57
- end
58
-
59
-
60
- def name
61
- @options[:name]
62
- end
63
-
64
-
65
- def domain
66
- @options[:domain]
67
- end
68
-
69
-
70
- def uuid
71
- @options[:uuid]
72
- end
73
-
74
-
75
- def fqdn
76
- name and domain and "#{name}.#{domain}"
77
- end
78
-
79
- def state
80
- exist? and @subject.state or 'nonexistent'
81
- end
82
-
83
- def create
84
- log "server already exists" and return self if exist?
85
-
86
- options = DefaultServerOptions.dup.merge @options.slice(:image_id, :flavor_id, :key_name, :security_group_ids, :user_data)
87
-
88
- # check for pre-existing volume(s). if any exist, add their AZ to the server's options
89
- zones = @options[:volumes].map { |volume_options| Volume.new(volume_options).availability_zone }.compact.uniq
90
- if zones.size > 1
91
- raise "pre-existing volumes volumes are in different availability zones"
92
- elsif zones.size == 1
93
- log "using AZ '#{zones.first}' since volumes already exist"
94
- options[:availability_zone] = zones.first
95
- end
96
-
97
- log "building server..."
98
- @subject = @connection.servers.create(options)
99
- setup
100
- log "created as #{@subject.dns_name}"
101
-
102
- return self
103
- rescue Exception => e
104
- log "error while creating (#{e.class.name})"
105
- destroy
106
- raise
107
- end
108
-
109
-
110
- def start
111
- log "server doesn't exist (ignoring)" and return unless exist?
112
- wait_for_state_transitions
113
-
114
- if @subject.state != "stopped"
115
- log "server #{@subject.state}."
116
- else
117
- log "starting server..."
118
- @subject.start
119
- setup
120
- log "server started"
121
- end
122
- return self
123
- end
124
-
125
- # shared between create and start
126
- def setup
127
- update_tags
128
- log "waiting for public dns to be set up..."
129
- wait_for { @subject.dns_name }
130
- update_dns
131
- update_volumes
132
- wait_for_state 'running'
133
- return self
134
- end
135
-
136
- def stop
137
- wait_for_state_transitions
138
- if !exist?
139
- log "server doesn't exist (ignoring)."
140
- elsif @subject.state != "running"
141
- log "server #{@subject.state}."
142
- else
143
- log "stopping server..."
144
- remove_dns
145
- @subject.stop
146
- wait_for_state 'stopped', 'terminated'
147
- end
148
- return self
149
- end
150
-
151
-
152
- def destroy
153
- if !exist?
154
- log "server doesn't exist (ignoring)."
155
- else
156
- log "tearing server down..."
157
- remove_dns
158
- @subject.destroy
159
- wait_for_state 'stopped', 'terminated'
160
- # FIXME: remove tags here
161
- @subject = nil
162
- end
163
- return self
164
- end
165
-
166
-
167
- def update_tags
168
- log "updating server tags"
169
- tags = { 'Name' => name, 'Domain' => domain, 'UUID' => uuid }
170
- response = @connection.create_tags(@subject.id, tags)
171
- raise "failed" unless response.status == 200
172
- return self
173
- end
174
-
175
-
176
- def update_root_volume_tags
177
- log "updating root volume tags"
178
- tags = { 'Name' => "#{name}-root", 'UUID' => uuid }
179
- response = @connection.create_tags(@subject.volumes.first.id, tags)
180
- raise "failed" unless response.status == 200
181
- return self
182
- end
183
-
184
-
185
- def update_volumes
186
- update_root_volume_tags
187
-
188
- @options[:volumes].each do |options|
189
- options[:availability_zone] = @subject.availability_zone
190
- Volume.new(options).attach(@subject.id)
191
- end
192
- end
193
-
194
-
195
- def update_dns
196
- return unless @options[:domain]
197
- log "updating dns zone"
198
-
199
- unless dns_zone
200
- log "dns zone does not exist"
201
- return self
202
- end
203
-
204
- if record = dns_record
205
- if dns_record_matches?(record)
206
- log "DNS record already exists"
207
- return self
208
- else
209
- log "warning, while creating, DNS record exists but points to wrong server (fixing)"
210
- record.destroy
211
- end
212
- end
213
-
214
- dns_zone.records.create(:name => fqdn, :type => 'CNAME', :value => @subject.dns_name, :ttl => 5)
215
- log "#{fqdn} -> #{@subject.dns_name}"
216
- return self
217
- end
218
-
219
-
220
- def remove_dns
221
- return unless @options[:domain]
222
- log "removing dns record"
223
-
224
- unless dns_zone
225
- log "dns zone does not exist"
226
- return self
227
- end
228
-
229
- if record = dns_record
230
- unless dns_record_matches?(record)
231
- log "warning, while removing, DNS record exist but does not point to the current server"
232
- end
233
- record.destroy
234
- end
235
-
236
- return self
237
- end
238
-
239
-
240
- private
241
-
242
-
243
- def wait_for_state_transitions
244
- return unless @subject
245
- return unless %w(stopping pending).include? @subject.state
246
- log "waiting for state transition from '#{@subject.state}' to complete"
247
- wait_for { @subject.state != 'pending' }
248
- wait_for { @subject.state != 'stopping' }
249
- end
250
-
251
-
252
- def dns_connection
253
- @dns_connection ||= Fog::DNS.new(:provider => :aws)
254
- end
255
-
256
-
257
- def dns_zone
258
- @dns_zone ||= dns_connection.zones.find { |zone| zone.domain == domain }
259
- end
260
-
261
- def dns_record
262
- dns_zone.records.find { |record| record.name == fqdn }
263
- end
264
-
265
- def dns_record_matches?(record)
266
- !!record and record.value.any? { |dns_name| dns_name == @subject.dns_name }
267
- end
268
-
269
- end
270
- end
@@ -1,80 +0,0 @@
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 'guignol/tty_spinner'
29
-
30
- module Guignol
31
- module Shared
32
-
33
-
34
- def log message
35
- stamp = Time.now.strftime('%Y-%m-%d %H:%M:%S')
36
- $stderr.write("[#{stamp}] #{name}: #{message}\n")
37
- $stderr.flush
38
- true
39
- end
40
-
41
-
42
- def subject_name
43
- self.class.name.gsub(/.*:/,'').downcase
44
- end
45
-
46
-
47
- def wait_for_state(*states)
48
- @subject or raise "#{subject_name} doesn't exist"
49
- original_state = @subject.state
50
- unless states.include?(original_state)
51
- log "waiting for #{subject_name} to become #{states.join(' or ')}..."
52
- wait_for do
53
- if @subject.state != original_state
54
- log "#{subject_name} now #{@subject.state}"
55
- original_state = @subject.state
56
- end
57
- states.include?(@subject.state)
58
- end
59
- end
60
- end
61
-
62
-
63
- def wait_for(&block)
64
- return unless @subject
65
- @subject.wait_for { TtySpinner.spin! ; block.call }
66
- end
67
-
68
- def confirm(message)
69
- puts "#{message} [y/n]"
70
- $stdin.gets =~ /^y$/i
71
- end
72
-
73
- def require_options(*required_options)
74
- required_options.each do |required_option|
75
- next if @options.include?(required_option)
76
- raise "option '#{required_option}' is mandatory for each #{subject_name}"
77
- end
78
- end
79
- end
80
- end