bfire 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +0 -0
- data/README.md +90 -0
- data/bin/bfire +120 -0
- data/examples/benchmark.rb +18 -0
- data/examples/dag.rb +26 -0
- data/examples/elasticity.rb +105 -0
- data/examples/ibbt.rb +125 -0
- data/examples/mine.rb +40 -0
- data/examples/modules/apache2/manifests/init.pp +44 -0
- data/examples/modules/app/files/app/app.rb +29 -0
- data/examples/modules/app/files/app/config.ru +2 -0
- data/examples/modules/app/files/app.phtml +4 -0
- data/examples/modules/app/manifests/init.pp +19 -0
- data/examples/modules/common/manifests/init.pp +8 -0
- data/examples/modules/haproxy/files/default +4 -0
- data/examples/modules/haproxy/files/haproxy.rsyslog.conf +2 -0
- data/examples/modules/haproxy/manifests/init.pp +21 -0
- data/examples/modules/mysql/manifests/init.pp +40 -0
- data/examples/modules/rsyslog/files/rsyslog.conf +116 -0
- data/examples/modules/rsyslog/manifests/init.pp +15 -0
- data/examples/modules/sinatra/manifests/init.pp +9 -0
- data/examples/modules/web/files/monitor/app.rb +55 -0
- data/examples/modules/web/files/monitor/config.ru +2 -0
- data/examples/modules/web/files/monitor/haproxy.cfg.erb +50 -0
- data/examples/modules/web/manifests/init.pp +26 -0
- data/examples/simple.rb +58 -0
- data/lib/bfire/aggregator/zabbix.rb +55 -0
- data/lib/bfire/engine.rb +546 -0
- data/lib/bfire/group.rb +241 -0
- data/lib/bfire/metric.rb +36 -0
- data/lib/bfire/provider/puppet.rb +58 -0
- data/lib/bfire/pub_sub/publisher.rb +40 -0
- data/lib/bfire/rule.rb +110 -0
- data/lib/bfire/template.rb +142 -0
- data/lib/bfire/version.rb +3 -0
- data/lib/bfire.rb +10 -0
- metadata +241 -0
data/lib/bfire/group.rb
ADDED
@@ -0,0 +1,241 @@
|
|
1
|
+
require 'bfire/template'
|
2
|
+
require 'bfire/rule'
|
3
|
+
|
4
|
+
module Bfire
|
5
|
+
class Group
|
6
|
+
include Enumerable
|
7
|
+
include PubSub::Publisher
|
8
|
+
|
9
|
+
attr_reader :engine
|
10
|
+
attr_reader :name
|
11
|
+
attr_reader :dependencies
|
12
|
+
attr_reader :templates
|
13
|
+
# A free-form text tag to add to every compute name of this group.
|
14
|
+
attr_reader :tag
|
15
|
+
|
16
|
+
def initialize(engine, name, options = {})
|
17
|
+
@engine = engine
|
18
|
+
@name = name
|
19
|
+
@tag = options.delete(:tag)
|
20
|
+
raise Error, "Tag name can't contain two or more consecutive dashes" if @tag && @tag =~ /-{2,}/
|
21
|
+
@options = options
|
22
|
+
@listeners = {}
|
23
|
+
@dependencies = []
|
24
|
+
|
25
|
+
@templates = []
|
26
|
+
@default_template = Template.new(self, :default)
|
27
|
+
@current_template = @default_template
|
28
|
+
|
29
|
+
raise Error, "Group name can only contain [a-zA-Z0-9] characters" if name !~ /[a-z0-9]+/i
|
30
|
+
|
31
|
+
on(:error) {|group| Thread.current.group.list.each{|t|
|
32
|
+
t[:ko] = true
|
33
|
+
t.kill
|
34
|
+
}
|
35
|
+
}
|
36
|
+
on(:ready) {|group|
|
37
|
+
group.engine.logger.info "#{group.banner}All VMs are now READY: #{computes.map{|vm|
|
38
|
+
[vm['name'], (vm['nic'] || []).map{|n| n['ip']}.inspect].join("=")
|
39
|
+
}.join("; ")}"
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def launch_initial_resources
|
44
|
+
merge_templates!
|
45
|
+
engine.logger.debug "#{banner}Merged templates=#{templates.inspect}"
|
46
|
+
check!
|
47
|
+
if rule.launch_initial_resources
|
48
|
+
trigger :launched
|
49
|
+
true
|
50
|
+
else
|
51
|
+
trigger :error
|
52
|
+
false
|
53
|
+
end
|
54
|
+
rescue Exception => e
|
55
|
+
engine.logger.error "#{banner}#{e.class.name}: #{e.message}"
|
56
|
+
engine.logger.debug e.backtrace.join("; ")
|
57
|
+
trigger :error
|
58
|
+
end
|
59
|
+
|
60
|
+
def monitor
|
61
|
+
rule.manage(computes)
|
62
|
+
rule.monitor
|
63
|
+
rescue Exception => e
|
64
|
+
engine.logger.error "#{banner}#{e.class.name}: #{e.message}"
|
65
|
+
engine.logger.debug e.backtrace.join("; ")
|
66
|
+
trigger :error
|
67
|
+
end
|
68
|
+
|
69
|
+
def provision!(vms)
|
70
|
+
return true if provider.nil?
|
71
|
+
engine.logger.info "#{banner}Provisioning..."
|
72
|
+
vms.all?{|vm|
|
73
|
+
provisioned = false
|
74
|
+
ip = vm['nic'][0]['ip']
|
75
|
+
engine.ssh(ip, 'root') {|s|
|
76
|
+
provisioned = unless provider.install(s)
|
77
|
+
engine.logger.error "Failed to install provider on #{vm.inspect} (IP=#{ip})."
|
78
|
+
false
|
79
|
+
else
|
80
|
+
result = provider.run(s) do |stream|
|
81
|
+
engine.logger.info "#{banner}[#{ip}] #{stream}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
}
|
85
|
+
provisioned
|
86
|
+
}
|
87
|
+
end
|
88
|
+
|
89
|
+
# Delegates every unknown method to the current Template, except #conf.
|
90
|
+
def method_missing(method, *args, &block)
|
91
|
+
if method == :conf
|
92
|
+
engine.send(method, *args, &block)
|
93
|
+
else
|
94
|
+
@current_template.send(method, *args, &block)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# ======================
|
99
|
+
# = Group-only methods =
|
100
|
+
# ======================
|
101
|
+
|
102
|
+
def rule
|
103
|
+
@rule ||= Rule.new(self, :initial => 1, :range => 1..1)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Defines the scaling rule for this group
|
107
|
+
def scale(range, options = {})
|
108
|
+
@rule = Rule.new(self, options.merge(:range => range))
|
109
|
+
end
|
110
|
+
|
111
|
+
def at(location, &block)
|
112
|
+
t = template(location)
|
113
|
+
@current_template = t
|
114
|
+
instance_eval(&block) unless block.nil?
|
115
|
+
@current_template = @default_template
|
116
|
+
end
|
117
|
+
|
118
|
+
def depends_on(group_name, &block)
|
119
|
+
@dependencies.push [group_name, block]
|
120
|
+
end
|
121
|
+
|
122
|
+
# Define the provider to use to provision the compute resources
|
123
|
+
# (Puppet, Chef...).
|
124
|
+
# If <tt>selected_provider</tt> is nil, returns the current provider.
|
125
|
+
def provider(selected_provider = nil, options = {})
|
126
|
+
return @provider if selected_provider.nil?
|
127
|
+
options[:modules] = engine.path_to(options[:modules]) if options[:modules]
|
128
|
+
@provider = Provider::Puppet.new(options)
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
# ===========
|
133
|
+
# = Helpers =
|
134
|
+
# ===========
|
135
|
+
|
136
|
+
def banner
|
137
|
+
"[#{name}] "
|
138
|
+
end
|
139
|
+
|
140
|
+
# Iterates over the collection of compute resources.
|
141
|
+
# Required for the Enumerable module.
|
142
|
+
def each(*args, &block)
|
143
|
+
computes.each(*args, &block)
|
144
|
+
end
|
145
|
+
|
146
|
+
def computes
|
147
|
+
templates.map{|t| t.instances}.flatten
|
148
|
+
end
|
149
|
+
|
150
|
+
# Return the first <tt>how_many</tt> compute resources of the group.
|
151
|
+
def take(how_many = :all)
|
152
|
+
case how_many
|
153
|
+
when :all
|
154
|
+
computes
|
155
|
+
when :first
|
156
|
+
computes[0]
|
157
|
+
else
|
158
|
+
raise ArgumentError, "You must pass :all, :first, or a Fixnum" unless how_many.kind_of?(Fixnum)
|
159
|
+
computes.take(how_many)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def inspect
|
164
|
+
s = "#<#{self.class.name}:0x#{object_id.to_s(16)}"
|
165
|
+
s << " #{banner}" if banner
|
166
|
+
s << "VMs: "
|
167
|
+
s << computes.map{|vm|
|
168
|
+
[vm['name'].inspect, (vm['nic'] || []).map{|n|
|
169
|
+
n['ip']
|
170
|
+
}.inspect].join("=")
|
171
|
+
}.join("; ")
|
172
|
+
s << ">"
|
173
|
+
end
|
174
|
+
|
175
|
+
def reload
|
176
|
+
each(&:reload)
|
177
|
+
end
|
178
|
+
|
179
|
+
def ssh_accessible?(vms)
|
180
|
+
vms.all?{|compute|
|
181
|
+
begin
|
182
|
+
ip = compute['nic'][0]['ip']
|
183
|
+
Timeout.timeout(30) do
|
184
|
+
engine.ssh(ip, 'root', :log => false) {|s|
|
185
|
+
s.exec!("hostname")
|
186
|
+
}
|
187
|
+
end
|
188
|
+
true
|
189
|
+
rescue Exception => e
|
190
|
+
engine.logger.debug "#{banner}Can't SSH yet to #{compute.signature} at IP=#{ip.inspect}. Reason: #{e.class.name}, #{e.message}. Will retry later."
|
191
|
+
false
|
192
|
+
end
|
193
|
+
}
|
194
|
+
end
|
195
|
+
|
196
|
+
def template(location)
|
197
|
+
t = @templates.find{|t| t.name == location}
|
198
|
+
if t.nil?
|
199
|
+
t = Template.new(
|
200
|
+
self,
|
201
|
+
location
|
202
|
+
)
|
203
|
+
@templates.push(t)
|
204
|
+
end
|
205
|
+
t
|
206
|
+
end
|
207
|
+
|
208
|
+
def check!
|
209
|
+
check_templates!
|
210
|
+
if provider && !provider.valid?
|
211
|
+
raise Error, "#{banner}#{provider.errors.map(&:inspect).join(", ")}"
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def merge_templates!
|
216
|
+
default = @default_template
|
217
|
+
if engine.conf[:authorized_keys]
|
218
|
+
default.context :authorized_keys => File.read(
|
219
|
+
File.expand_path(engine.conf[:authorized_keys])
|
220
|
+
)
|
221
|
+
end
|
222
|
+
if @templates.empty?
|
223
|
+
@templates.push template(:any)
|
224
|
+
end
|
225
|
+
templates.each{|t|
|
226
|
+
t.merge_defaults!(default).resolve!
|
227
|
+
}
|
228
|
+
end # def merge_templates!
|
229
|
+
|
230
|
+
protected
|
231
|
+
|
232
|
+
def check_templates!
|
233
|
+
errors = []
|
234
|
+
templates.each do |t|
|
235
|
+
t.valid? || errors.push({t.name => t.errors})
|
236
|
+
end
|
237
|
+
raise Error, "#{banner}#{errors.map(&:inspect).join(", ")}" unless errors.empty?
|
238
|
+
end # def check_templates!
|
239
|
+
|
240
|
+
end
|
241
|
+
end
|
data/lib/bfire/metric.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
class Array
|
2
|
+
def sum
|
3
|
+
inject(:+)
|
4
|
+
end
|
5
|
+
|
6
|
+
def avg
|
7
|
+
sum.to_f / size
|
8
|
+
end
|
9
|
+
|
10
|
+
def median
|
11
|
+
sorted = sort
|
12
|
+
(sorted[size/2] + sorted[(size+1)/2]) / 2
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module Bfire
|
17
|
+
class Metric
|
18
|
+
|
19
|
+
def initialize(name, results, opts = {})
|
20
|
+
@name = name
|
21
|
+
@results = results
|
22
|
+
@opts = opts
|
23
|
+
end
|
24
|
+
|
25
|
+
def values
|
26
|
+
@results.map{|r|
|
27
|
+
case @opts[:type]
|
28
|
+
when :numeric
|
29
|
+
r['value'].to_f
|
30
|
+
else
|
31
|
+
r['value']
|
32
|
+
end
|
33
|
+
}.reverse
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Bfire
|
2
|
+
module Provider
|
3
|
+
class Puppet
|
4
|
+
attr_reader :classes
|
5
|
+
attr_reader :modules
|
6
|
+
attr_reader :options
|
7
|
+
attr_reader :errors
|
8
|
+
|
9
|
+
def initialize(opts = {})
|
10
|
+
@classes = opts.delete(:classes) || opts.delete("classes")
|
11
|
+
@modules = opts.delete(:modules) || opts.delete("modules")
|
12
|
+
@options = opts
|
13
|
+
@errors = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def install(ssh_session)
|
17
|
+
res = ssh_session.exec!("apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install curl puppet -y")
|
18
|
+
res = ssh_session.exec!("which puppet")
|
19
|
+
!res.nil? && !res.empty?
|
20
|
+
end
|
21
|
+
|
22
|
+
def run(ssh_session)
|
23
|
+
ssh_session.exec!("rm -rf /tmp/puppet && mkdir -p /tmp/puppet")
|
24
|
+
ssh_session.scp.upload!(
|
25
|
+
StringIO.new(manifest("vm")),
|
26
|
+
"/tmp/puppet/manifest.pp"
|
27
|
+
)
|
28
|
+
ssh_session.sftp.upload!(modules, "/tmp/puppet/modules")
|
29
|
+
ssh_session.exec!(
|
30
|
+
"puppet --modulepath /tmp/puppet/modules /tmp/puppet/manifest.pp"
|
31
|
+
) do |ch, stream, data|
|
32
|
+
yield "[#{stream.to_s.upcase}] #{data.chomp}"
|
33
|
+
end
|
34
|
+
true
|
35
|
+
end
|
36
|
+
|
37
|
+
def manifest(name)
|
38
|
+
content = <<MANIFEST
|
39
|
+
class #{name} {
|
40
|
+
#{classes.map{|klass| "include #{klass}"}.join("\n")}
|
41
|
+
}
|
42
|
+
|
43
|
+
include #{name}
|
44
|
+
MANIFEST
|
45
|
+
end
|
46
|
+
|
47
|
+
def valid?
|
48
|
+
@errors = []
|
49
|
+
if modules.nil?
|
50
|
+
@errors.push("You must pass a :modules option to `provider`")
|
51
|
+
elsif !File.directory?(modules)
|
52
|
+
@errors.push("#{modules} is not a valid directory")
|
53
|
+
end
|
54
|
+
@errors.empty?
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Bfire
|
2
|
+
module PubSub
|
3
|
+
module Publisher
|
4
|
+
# Notify all group listeners when event <tt>event</tt> occurs.
|
5
|
+
def trigger(event)
|
6
|
+
triggered_events.push(event)
|
7
|
+
engine.logger.info "#{banner}Triggering #{event.inspect} event..."
|
8
|
+
(hooks[event] || []).each{|block|
|
9
|
+
if block.arity == 1
|
10
|
+
block.call(self)
|
11
|
+
else
|
12
|
+
engine.instance_eval(&block)
|
13
|
+
end
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
# Defines a procedure (hook) to launch when event <tt>event</tt> occurs.
|
18
|
+
def on(event, &block)
|
19
|
+
hooks[event.to_sym] ||= []
|
20
|
+
hooks[event.to_sym] << block
|
21
|
+
end
|
22
|
+
|
23
|
+
def error?
|
24
|
+
triggered_events.include?(:error)
|
25
|
+
end
|
26
|
+
|
27
|
+
def hooks
|
28
|
+
@hooks ||= {}
|
29
|
+
end
|
30
|
+
|
31
|
+
def triggered_events
|
32
|
+
@triggered_events ||= []
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.included(mod)
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/bfire/rule.rb
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
module Bfire
|
2
|
+
class Rule
|
3
|
+
attr_reader :group
|
4
|
+
attr_reader :opts
|
5
|
+
|
6
|
+
include PubSub::Publisher
|
7
|
+
|
8
|
+
def initialize(group, opts = {})
|
9
|
+
@group = group
|
10
|
+
@opts = {:period => 5*60, :initial => 1, :range => 1..1}.merge(opts)
|
11
|
+
end
|
12
|
+
|
13
|
+
# we only support round-robin placement for now
|
14
|
+
def monitor
|
15
|
+
loop do
|
16
|
+
sleep opts[:period]
|
17
|
+
group.engine.logger.info "#{group.banner}Monitoring group elasticity rule..."
|
18
|
+
# this is blocking because we don't want the rule to be triggered
|
19
|
+
# too many times.
|
20
|
+
if scale_up?
|
21
|
+
group.engine.logger.info "#{group.banner}Scaling up!"
|
22
|
+
manage(scale(:up))
|
23
|
+
elsif scale_down?
|
24
|
+
group.engine.logger.info "#{group.banner}Scaling down!"
|
25
|
+
manage(scale(:down))
|
26
|
+
else
|
27
|
+
group.engine.logger.info "#{group.banner}..."
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def launch_initial_resources
|
33
|
+
scale(:up, opts[:initial])
|
34
|
+
end
|
35
|
+
|
36
|
+
def scale(up_or_down, count = 1)
|
37
|
+
new_computes = []
|
38
|
+
count.times do |i|
|
39
|
+
sorted_templates = group.templates.sort_by{|t| t.instances.length}
|
40
|
+
if up_or_down == :down
|
41
|
+
vm_to_delete = sorted_templates.last.instances[0]
|
42
|
+
if vm_to_delete.nil?
|
43
|
+
group.engine.logger.warn "#{group.banner}No resource to delete!"
|
44
|
+
else
|
45
|
+
group.engine.logger.info "#{group.banner}Removing compute #{vm_to_delete.signature}..."
|
46
|
+
if vm_to_delete.delete
|
47
|
+
sorted_templates.last.instances.delete vm_to_delete
|
48
|
+
group.trigger :scaled_down
|
49
|
+
end
|
50
|
+
end
|
51
|
+
else
|
52
|
+
template = sorted_templates.first
|
53
|
+
computes = group.engine.launch_compute(template)
|
54
|
+
template.instances.push(*computes)
|
55
|
+
new_computes.push(*computes)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
new_computes
|
59
|
+
end
|
60
|
+
|
61
|
+
def manage(vms)
|
62
|
+
return true if vms.empty?
|
63
|
+
group.engine.logger.info "#{group.banner}Monitoring VMs... IPs: #{vms.map{|vm| [vm['name'], (vm['nic'] || []).map{|n| n['ip']}.inspect].join("=")}.join("; ")}."
|
64
|
+
vms.each(&:reload)
|
65
|
+
if failed = vms.find{|compute| compute['state'] == 'FAILED'}
|
66
|
+
group.engine.logger.warn "#{group.banner}Compute #{failed.signature} is in a FAILED state."
|
67
|
+
if group.triggered_events.include?(:ready)
|
68
|
+
group.trigger :error
|
69
|
+
else
|
70
|
+
group.trigger :scale_error
|
71
|
+
end
|
72
|
+
elsif vms.all?{|compute| compute['state'] == 'ACTIVE'}
|
73
|
+
group.engine.logger.info "#{group.banner}All compute resources are ACTIVE"
|
74
|
+
if group.ssh_accessible?(vms)
|
75
|
+
group.engine.logger.info "#{group.banner}All compute resources are SSH-able"
|
76
|
+
provisioned = group.provision!(vms)
|
77
|
+
if group.triggered_events.include?(:ready)
|
78
|
+
if provisioned
|
79
|
+
group.trigger :scaled_up
|
80
|
+
else
|
81
|
+
group.trigger :scale_error
|
82
|
+
end
|
83
|
+
else
|
84
|
+
if provisioned
|
85
|
+
group.trigger :ready
|
86
|
+
else
|
87
|
+
group.trigger :error
|
88
|
+
end
|
89
|
+
end
|
90
|
+
monitor
|
91
|
+
else
|
92
|
+
sleep 20
|
93
|
+
manage(vms)
|
94
|
+
end
|
95
|
+
else
|
96
|
+
group.engine.logger.info "#{group.banner}Some compute resources are still PENDING"
|
97
|
+
sleep 10
|
98
|
+
manage(vms)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def scale_up?
|
103
|
+
opts[:up] && group.computes.length < opts[:range].end && opts[:up].call(group.engine)
|
104
|
+
end
|
105
|
+
|
106
|
+
def scale_down?
|
107
|
+
opts[:down] && group.computes.length > opts[:range].begin && opts[:down].call(group.engine)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
module Bfire
|
2
|
+
class Template
|
3
|
+
# Return the list of nics defined.
|
4
|
+
attr_reader :nics
|
5
|
+
# Return the list of disks defined.
|
6
|
+
attr_reader :disks
|
7
|
+
# Return the template name (i.e. the location name).
|
8
|
+
attr_reader :name
|
9
|
+
# Return the properties defined for this template (instance_type, etc.).
|
10
|
+
attr_reader :properties
|
11
|
+
# Return the list of metrics defined.
|
12
|
+
attr_reader :metrics
|
13
|
+
attr_reader :context
|
14
|
+
attr_reader :instances
|
15
|
+
|
16
|
+
# Return an Array of error messages in case this template is not valid.
|
17
|
+
attr_reader :errors
|
18
|
+
# Return the group this template belongs to.
|
19
|
+
attr_reader :group
|
20
|
+
|
21
|
+
def initialize(group, location_name = nil)
|
22
|
+
@group = group
|
23
|
+
@location_name = location_name
|
24
|
+
@name = location_name
|
25
|
+
@nics = []
|
26
|
+
@disks = []
|
27
|
+
@errors = []
|
28
|
+
@metrics = []
|
29
|
+
@properties = {}
|
30
|
+
@context = {}
|
31
|
+
@instances = []
|
32
|
+
end
|
33
|
+
|
34
|
+
def location
|
35
|
+
@location ||= if @location_name == :default
|
36
|
+
# noop
|
37
|
+
else
|
38
|
+
group.engine.fetch_location(@location_name)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def context(opts = {})
|
43
|
+
if opts.empty?
|
44
|
+
@context
|
45
|
+
else
|
46
|
+
@context.merge!(opts)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Define the instance type to use.
|
51
|
+
def instance_type(instance_type)
|
52
|
+
@properties[:instance_type] = instance_type.to_s
|
53
|
+
end
|
54
|
+
|
55
|
+
# Define the image to deploy on the compute resources.
|
56
|
+
def deploy(storage, options = {})
|
57
|
+
props = options.merge(
|
58
|
+
:storage => storage
|
59
|
+
)
|
60
|
+
if @disks.empty?
|
61
|
+
@disks.push props
|
62
|
+
else
|
63
|
+
@disks[0] = props
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def connect_to(network, options = {})
|
68
|
+
@nics.push options.merge(
|
69
|
+
:network => network
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Merge this template with another one.
|
74
|
+
# nics, disks, and metrics will be added, while other properties will be
|
75
|
+
# merged.
|
76
|
+
def merge_defaults!(template)
|
77
|
+
@properties = template.properties.merge(@properties)
|
78
|
+
@context = template.context.merge(@context)
|
79
|
+
template.nics.each do |nic|
|
80
|
+
@nics.unshift nic.clone
|
81
|
+
end
|
82
|
+
template.disks.each do |disk|
|
83
|
+
@disks.unshift disk.clone
|
84
|
+
end
|
85
|
+
template.metrics.each do |metric|
|
86
|
+
@metrics.unshift metric.clone
|
87
|
+
end
|
88
|
+
self
|
89
|
+
end
|
90
|
+
|
91
|
+
# Returns true if valid, false otherwise
|
92
|
+
def valid?
|
93
|
+
@errors = []
|
94
|
+
@errors.push("You must specify an instance_type") unless properties[:instance_type]
|
95
|
+
@errors.push("You must specify at least one disk image") if @disks.empty?
|
96
|
+
@errors.push("You must specify at least one network attachment") if @nics.empty?
|
97
|
+
@errors.empty?
|
98
|
+
end
|
99
|
+
|
100
|
+
# Resolve the networks and storages required for the template to be valid.
|
101
|
+
def resolve!
|
102
|
+
nics.each{|nic|
|
103
|
+
nic[:network] = group.engine.fetch_network(
|
104
|
+
nic[:network],
|
105
|
+
location
|
106
|
+
) || raise(Error, "Can't find network #{nic[:network].inspect} at #{location["name"].inspect}")
|
107
|
+
}
|
108
|
+
disks.each{|disk|
|
109
|
+
disk[:storage] = group.engine.fetch_storage(
|
110
|
+
disk[:storage],
|
111
|
+
location
|
112
|
+
) || raise(Error, "Can't find storage #{disk[:storage].inspect} at #{location["name"].inspect}")
|
113
|
+
}
|
114
|
+
self
|
115
|
+
end
|
116
|
+
|
117
|
+
# Register a metric on the compute resources.
|
118
|
+
def register(metric_name, options = {})
|
119
|
+
@metrics.push options.merge(:name => metric_name)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Exports the template to a ruby Hash, which conforms to what is expected
|
123
|
+
# by Restfully to submit a resource.
|
124
|
+
def to_h
|
125
|
+
h = {}
|
126
|
+
h.merge!(@properties)
|
127
|
+
h['name'] = "#{group.name}--#{name}--#{SecureRandom.hex(4)}"
|
128
|
+
h['name'] << "-#{group.tag}" if group.tag
|
129
|
+
h['nic'] = nics
|
130
|
+
h['disk'] = disks
|
131
|
+
h['location'] = location
|
132
|
+
h['context'] = @context
|
133
|
+
h['context']['metrics'] = XML::Node.new_cdata(metrics.map{|m|
|
134
|
+
"<metric>"+[m[:name], m[:command]].join(",")+"</metric>"
|
135
|
+
}.join("")) unless metrics.empty?
|
136
|
+
group.dependencies.each{|gname,block|
|
137
|
+
h['context'].merge!(block.call(group.engine.group(gname)))
|
138
|
+
}
|
139
|
+
h
|
140
|
+
end
|
141
|
+
end # class Template
|
142
|
+
end # module Bup
|