guignol 0.1.2.1 → 0.3.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 +2 -0
- data/.rspec +2 -1
- data/Gemfile.lock +29 -18
- data/LICENCE +26 -0
- data/README.md +67 -38
- data/Rakefile +0 -26
- data/bin/guignol +3 -33
- data/guignol.gemspec +4 -30
- data/lib/core_ext/array/collect_key.rb +6 -0
- data/lib/core_ext/hash/map_to_hash.rb +31 -0
- data/lib/guignol.rb +15 -35
- data/lib/guignol/commands/base.rb +53 -66
- data/lib/guignol/commands/clone.rb +49 -0
- data/lib/guignol/commands/create.rb +14 -33
- data/lib/guignol/commands/execute.rb +69 -0
- data/lib/guignol/commands/fix_dns.rb +12 -33
- data/lib/guignol/commands/kill.rb +18 -36
- data/lib/guignol/commands/list.rb +19 -45
- data/lib/guignol/commands/start.rb +14 -33
- data/lib/guignol/commands/stop.rb +18 -37
- data/lib/guignol/commands/uuid.rb +19 -32
- data/lib/guignol/configuration.rb +43 -0
- data/lib/guignol/connection.rb +33 -0
- data/lib/guignol/env.rb +19 -0
- data/lib/guignol/logger.rb +29 -0
- data/lib/guignol/models/base.rb +125 -0
- data/lib/guignol/models/instance.rb +244 -0
- data/lib/guignol/models/volume.rb +91 -0
- data/lib/guignol/shell.rb +27 -42
- data/lib/guignol/tty_spinner.rb +6 -29
- data/lib/guignol/version.rb +1 -27
- data/spec/guignol/configuration_spec.rb +72 -0
- data/spec/guignol/instance_spec.rb +48 -8
- data/spec/guignol/volume_spec.rb +17 -0
- data/spec/spec_helper.rb +12 -0
- data/tmp/.keepme +0 -0
- metadata +79 -52
- data/lib/guignol/array/collect_key.rb +0 -32
- data/lib/guignol/commands.rb +0 -48
- data/lib/guignol/commands/help.rb +0 -77
- data/lib/guignol/instance.rb +0 -270
- data/lib/guignol/shared.rb +0 -80
- 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
|
data/lib/guignol/shell.rb
CHANGED
@@ -1,50 +1,35 @@
|
|
1
|
-
|
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
|
-
|
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
|
36
|
-
|
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
|
46
|
-
|
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'
|
data/lib/guignol/tty_spinner.rb
CHANGED
@@ -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.
|
42
|
-
|
43
|
-
|
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
|
|