bfire 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/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
|