foreman-architect 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/architect +147 -0
- data/bin/foreman-vm +50 -0
- data/bin/worker.rb +101 -0
- data/lib/architect.rb +49 -0
- data/lib/architect/builder/physical.rb +19 -0
- data/lib/architect/builder/virtual.rb +27 -0
- data/lib/architect/config.rb +64 -0
- data/lib/architect/designer.rb +73 -0
- data/lib/architect/log.rb +28 -0
- data/lib/architect/plan.rb +41 -0
- data/lib/architect/plugin.rb +67 -0
- data/lib/architect/plugin/hello_world.rb +46 -0
- data/lib/architect/plugin/ldap_netgroup.rb +114 -0
- data/lib/architect/plugin_manager.rb +64 -0
- data/lib/architect/report.rb +67 -0
- data/lib/architect/version.rb +3 -0
- data/lib/foreman_vm.rb +409 -0
- data/lib/foreman_vm/allocator.rb +49 -0
- data/lib/foreman_vm/buildspec.rb +48 -0
- data/lib/foreman_vm/cluster.rb +83 -0
- data/lib/foreman_vm/config.rb +55 -0
- data/lib/foreman_vm/console.rb +83 -0
- data/lib/foreman_vm/domain.rb +192 -0
- data/lib/foreman_vm/foreman_api.rb +78 -0
- data/lib/foreman_vm/getopt.rb +151 -0
- data/lib/foreman_vm/hypervisor.rb +96 -0
- data/lib/foreman_vm/storage_pool.rb +104 -0
- data/lib/foreman_vm/util.rb +18 -0
- data/lib/foreman_vm/volume.rb +70 -0
- data/lib/foreman_vm/workqueue.rb +58 -0
- data/test/architect/architect_test.rb +24 -0
- data/test/architect/product_service.yaml +33 -0
- data/test/architect/tc_builder_physical.rb +13 -0
- data/test/architect/tc_config.rb +20 -0
- data/test/architect/tc_log.rb +13 -0
- data/test/architect/tc_plugin_ldap_netgroup.rb +39 -0
- data/test/architect/tc_plugin_manager.rb +27 -0
- data/test/tc_allocator.rb +61 -0
- data/test/tc_buildspec.rb +45 -0
- data/test/tc_cluster.rb +20 -0
- data/test/tc_config.rb +12 -0
- data/test/tc_foreman_api.rb +20 -0
- data/test/tc_foremanvm.rb +20 -0
- data/test/tc_hypervisor.rb +37 -0
- data/test/tc_main.rb +19 -0
- data/test/tc_storage_pool.rb +28 -0
- data/test/tc_volume.rb +22 -0
- data/test/tc_workqueue.rb +35 -0
- data/test/ts_all.rb +13 -0
- metadata +226 -0
data/bin/architect
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# The Architect - design and plan virtual machine environments
|
4
|
+
#
|
5
|
+
# Author: Mark Heily <mark.heily@bronto.com>
|
6
|
+
#
|
7
|
+
|
8
|
+
require 'optparse'
|
9
|
+
|
10
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib')
|
11
|
+
|
12
|
+
require 'rubygems'
|
13
|
+
require 'bundler/setup'
|
14
|
+
|
15
|
+
require 'logger'
|
16
|
+
require 'yaml'
|
17
|
+
require 'pp'
|
18
|
+
require 'architect'
|
19
|
+
|
20
|
+
log = Logger.new(STDERR)
|
21
|
+
log.level = Logger::WARN
|
22
|
+
|
23
|
+
# Parse command line options
|
24
|
+
options = {
|
25
|
+
:action => nil,
|
26
|
+
:dry_run => false,
|
27
|
+
:start => false, # If true, the VMs will be powered on after they are created
|
28
|
+
:verbose => 0,
|
29
|
+
}
|
30
|
+
op = OptionParser.new do |opts|
|
31
|
+
opts.banner = "Usage: architect [options] <path to plan>"
|
32
|
+
|
33
|
+
opts.on("--design", "Design a new plan") do
|
34
|
+
options[:action] = :design
|
35
|
+
end
|
36
|
+
|
37
|
+
opts.on("--execute", "Execute a plan") do
|
38
|
+
options[:action] = :execute
|
39
|
+
end
|
40
|
+
|
41
|
+
opts.on("--validate", "Validate a plan") do
|
42
|
+
options[:action] = :validate
|
43
|
+
end
|
44
|
+
|
45
|
+
opts.on("--dry-run", "Say what will be done, but do not make any changes") do
|
46
|
+
options[:dry_run] = true
|
47
|
+
end
|
48
|
+
|
49
|
+
opts.on("--[no-]start", "Start instances after they are created. [default: true]") do |val|
|
50
|
+
options[:start] = val
|
51
|
+
end
|
52
|
+
|
53
|
+
opts.on("--report-capacity", "Display a capacity report") do
|
54
|
+
options[:action] = :report
|
55
|
+
options[:report_type] = :capacity
|
56
|
+
end
|
57
|
+
|
58
|
+
opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
|
59
|
+
options[:verbose] = v
|
60
|
+
log.level = Logger::DEBUG
|
61
|
+
end
|
62
|
+
end
|
63
|
+
op.parse!
|
64
|
+
|
65
|
+
config = Architect::Config.new
|
66
|
+
|
67
|
+
case options[:action]
|
68
|
+
when :design
|
69
|
+
Architect::Designer.create_plan
|
70
|
+
when :validate
|
71
|
+
Architect::Plan.new(ARGV[0])
|
72
|
+
when :report
|
73
|
+
require 'architect/report'
|
74
|
+
report = Architect::Report.new
|
75
|
+
case options[:report_type]
|
76
|
+
when :capacity
|
77
|
+
puts report.capacity
|
78
|
+
else
|
79
|
+
raise ArgumentError, 'Invalid report type'
|
80
|
+
end
|
81
|
+
when :deploy
|
82
|
+
_deploy
|
83
|
+
else
|
84
|
+
puts "ERROR: You must specify a valid action"
|
85
|
+
system "#{$0} --help"
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
# TEMPORARY: refactor this into Plan#deploy
|
90
|
+
def _deploy
|
91
|
+
require 'ForemanVM'
|
92
|
+
|
93
|
+
if ARGV.empty?
|
94
|
+
puts "ERROR: You must specify at least one plan\n" + op.help
|
95
|
+
exit 1
|
96
|
+
end
|
97
|
+
|
98
|
+
# Build a list of all plans
|
99
|
+
plans = []
|
100
|
+
ARGV.each do |arg|
|
101
|
+
if File.directory? arg
|
102
|
+
plans.concat(Dir.glob(arg + '/*'))
|
103
|
+
else
|
104
|
+
plans.push arg
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
raise 'No plans found' if plans.empty?
|
109
|
+
|
110
|
+
# Process each plan
|
111
|
+
plans.each do |plan|
|
112
|
+
log.info "Processing #{plan}"
|
113
|
+
|
114
|
+
# Read the design specification
|
115
|
+
manifest = YAML.load(File.open(plan))
|
116
|
+
#pp manifest
|
117
|
+
|
118
|
+
# Create a virtual machine for each instance listed in the specification
|
119
|
+
manifest['instances'].each do |instance|
|
120
|
+
# Coerce scalar instances into hashes
|
121
|
+
if instance.kind_of? String
|
122
|
+
instance = { instance.dup => {} }
|
123
|
+
end
|
124
|
+
|
125
|
+
instance.each do |name, custom_spec|
|
126
|
+
|
127
|
+
# Create a new spec based on the 'defaults' section of the manifest,
|
128
|
+
# and then apply the custom fields for this instance.
|
129
|
+
spec = manifest['defaults'].dup
|
130
|
+
spec['name'] = name
|
131
|
+
spec.merge! custom_spec
|
132
|
+
spec['fqdn'] = spec['name'] + '.' + spec['domain']
|
133
|
+
spec['instance_type'] ||= 'virtual'
|
134
|
+
pp spec if log.level == Logger::DEBUG
|
135
|
+
|
136
|
+
if architect.instance_exists? fqdn
|
137
|
+
puts "An instance named #{fqdn} exists; skipping" if log.level == Logger::DEBUG
|
138
|
+
else
|
139
|
+
# Build the instance
|
140
|
+
architect.build(spec)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
exit 0
|
146
|
+
end
|
147
|
+
|
data/bin/foreman-vm
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
#!/usr/bin/ruby -w
|
2
|
+
#
|
3
|
+
# Build or rebuild a virtual machine using the Foreman API
|
4
|
+
#
|
5
|
+
# Author: Mark Heily <mark.heily@bronto.com>
|
6
|
+
#
|
7
|
+
|
8
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib')
|
9
|
+
|
10
|
+
require 'rubygems'
|
11
|
+
|
12
|
+
require 'ForemanVM'
|
13
|
+
|
14
|
+
STDOUT.sync = true
|
15
|
+
|
16
|
+
vm = ForemanVM.new
|
17
|
+
vm.parse_options
|
18
|
+
|
19
|
+
ARGV.each do |arg|
|
20
|
+
vm.name = arg
|
21
|
+
|
22
|
+
case vm.action
|
23
|
+
when 'rebuild'
|
24
|
+
vm.rebuild
|
25
|
+
when 'delete'
|
26
|
+
vm.delete
|
27
|
+
when 'create'
|
28
|
+
vm.create
|
29
|
+
when 'create-storage'
|
30
|
+
vm.create_storage
|
31
|
+
when 'stop'
|
32
|
+
vm.stop
|
33
|
+
when 'start'
|
34
|
+
vm.start
|
35
|
+
when 'enable-libgfapi'
|
36
|
+
vm.enable_libgfapi
|
37
|
+
when 'disable-libgfapi'
|
38
|
+
vm.disable_libgfapi
|
39
|
+
when 'dumpxml'
|
40
|
+
vm.dumpxml
|
41
|
+
when 'monitor-boot'
|
42
|
+
vm.monitor_boot
|
43
|
+
when 'console'
|
44
|
+
guest = arg
|
45
|
+
guest += vm.config.domain unless guest =~ /\./
|
46
|
+
vm.console.attach(guest)
|
47
|
+
else
|
48
|
+
raise 'Need to specify an action: rebuild, build, etc.'
|
49
|
+
end
|
50
|
+
end
|
data/bin/worker.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'yaml'
|
5
|
+
require 'pp'
|
6
|
+
require 'pathname'
|
7
|
+
|
8
|
+
require 'rubygems'
|
9
|
+
require 'beaneater'
|
10
|
+
require 'daemons'
|
11
|
+
|
12
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib')
|
13
|
+
|
14
|
+
require 'ForemanVM'
|
15
|
+
|
16
|
+
# Based on:
|
17
|
+
# http://www.jstorimer.com/blogs/workingwithcode/7766093-daemon-processes-in-ruby
|
18
|
+
def daemonize_app
|
19
|
+
exit if fork
|
20
|
+
Process.setsid
|
21
|
+
exit if fork
|
22
|
+
STDIN.reopen "/dev/null"
|
23
|
+
STDOUT.reopen "/dev/null", "a"
|
24
|
+
STDERR.reopen "/dev/null", "a"
|
25
|
+
end
|
26
|
+
|
27
|
+
#
|
28
|
+
# MAIN()
|
29
|
+
#
|
30
|
+
|
31
|
+
if ARGV.include? '--daemon'
|
32
|
+
daemonize_app
|
33
|
+
logfile = File.dirname(__FILE__) + '/../log/worker.log'
|
34
|
+
log = Logger.new(logfile, 10, 1024000)
|
35
|
+
log.info 'started a background worker process'
|
36
|
+
else
|
37
|
+
log = Logger.new(STDERR)
|
38
|
+
log.info 'started a foreground worker process'
|
39
|
+
end
|
40
|
+
|
41
|
+
if ARGV.include? '--debug'
|
42
|
+
log.level = Logger::DEBUG
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
# DEADWOOD: should use #clear method instead
|
47
|
+
### Kill any buried jobs
|
48
|
+
##if config['reap_buried_jobs']
|
49
|
+
## while tube.peek(:buried)
|
50
|
+
## log.info tube.stats
|
51
|
+
## tube.kick(1)
|
52
|
+
## job = tube.reserve
|
53
|
+
## log.info 'discarding job for ' + job.body
|
54
|
+
## end
|
55
|
+
##end
|
56
|
+
|
57
|
+
wq = ForemanAP::Workqueue.new
|
58
|
+
|
59
|
+
wq.process_all_jobs do |rec|
|
60
|
+
begin
|
61
|
+
|
62
|
+
raise 'user is required' unless rec.has_key?('user')
|
63
|
+
raise 'action is required' unless rec.has_key?('action')
|
64
|
+
raise 'invalid API version' unless rec['api_version'] == 1
|
65
|
+
raise 'invalid user' unless rec['user'] =~ /^([a-zA-Z0-9_.]+)$/m
|
66
|
+
raise 'invalid action' unless rec['action'] =~ /^([a-zA-Z0-9_.]+)$/m
|
67
|
+
|
68
|
+
hostname = rec['buildspec']['name'] or raise 'name is required'
|
69
|
+
domain = rec['buildspec']['domain'] or raise 'domain is required'
|
70
|
+
|
71
|
+
log.info("processing a #{rec['action']} job for hostname #{hostname}: #{rec.inspect}")
|
72
|
+
|
73
|
+
fvm = ForemanVM.new
|
74
|
+
case rec['action']
|
75
|
+
when 'create'
|
76
|
+
fvm.name = hostname
|
77
|
+
fvm.buildspec = rec['buildspec']
|
78
|
+
fvm.create
|
79
|
+
when 'rebuild'
|
80
|
+
fvm.name = hostname + '.' + domain
|
81
|
+
fvm.buildspec = rec['buildspec']
|
82
|
+
fvm.rebuild
|
83
|
+
when 'destroy'
|
84
|
+
fvm.name = hostname + '.' + domain
|
85
|
+
fvm.delete
|
86
|
+
when 'snapshot-create'
|
87
|
+
fvm.snapshot_create
|
88
|
+
when 'snapshot-revert'
|
89
|
+
fvm.snapshot_revert
|
90
|
+
else
|
91
|
+
log.error "invalid action: #{action}"
|
92
|
+
raise "invalid action: #{action}"
|
93
|
+
end
|
94
|
+
log.info("job for #{hostname} is complete")
|
95
|
+
puts "done"
|
96
|
+
rescue => e
|
97
|
+
log.error("job for #{hostname} failed")
|
98
|
+
log.error e.inspect
|
99
|
+
log.error e.backtrace
|
100
|
+
end
|
101
|
+
end
|
data/lib/architect.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
class Architect
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'architect/config'
|
4
|
+
require 'architect/designer'
|
5
|
+
require 'architect/log'
|
6
|
+
require 'architect/plan'
|
7
|
+
require 'architect/plugin_manager'
|
8
|
+
# FIXME: causes slowdown due to libvirt connection setup
|
9
|
+
#require 'architect/report'
|
10
|
+
|
11
|
+
require 'pp'
|
12
|
+
|
13
|
+
attr_accessor :config
|
14
|
+
|
15
|
+
def plugins
|
16
|
+
@plugins ||= Architect::PluginManager.new(@config.to_hash[:plugins])
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(opts = default_options)
|
20
|
+
@config = Architect::Config.new(opts[:conffile])
|
21
|
+
@plugins = nil # PluginManager, initialized on-demand
|
22
|
+
end
|
23
|
+
|
24
|
+
# Build an instance based on a specification
|
25
|
+
def build(spec)
|
26
|
+
builder(spec).build(spec)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Return the appriate Builder object for an instance
|
30
|
+
def builder(spec)
|
31
|
+
case spec[:instance_type]
|
32
|
+
when 'virtual'
|
33
|
+
require 'architect/builder/virtual' # Slow, so load it on demand
|
34
|
+
@vm_builder ||= VirtualMachineBuilder.new
|
35
|
+
when 'physical'
|
36
|
+
require 'architect/builder/physical'
|
37
|
+
@physical_builder ||= PhysicalMachineBuilder.new
|
38
|
+
else
|
39
|
+
pp spec
|
40
|
+
raise 'Unsupported instance type'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def default_options
|
45
|
+
return({
|
46
|
+
conffile: nil,
|
47
|
+
})
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class Architect
|
2
|
+
# Build a physical machine
|
3
|
+
class PhysicalMachineBuilder
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
end
|
7
|
+
|
8
|
+
# Return true if a physical machine named [+fqdn+] exists.
|
9
|
+
def exists?(fqdn)
|
10
|
+
raise 'FIXME'
|
11
|
+
end
|
12
|
+
|
13
|
+
# Build a physical machine
|
14
|
+
def build(spec)
|
15
|
+
pp spec
|
16
|
+
raise 'FIXME'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class Architect
|
2
|
+
# Build virtual machines
|
3
|
+
class VirtualMachineBuilder
|
4
|
+
require 'ForemanVM'
|
5
|
+
|
6
|
+
# Return true if a virtual machine named [+fqdn+] exists.
|
7
|
+
def self.exists?(fqdn)
|
8
|
+
ForemanVM.new.vm_exists? fqdn
|
9
|
+
end
|
10
|
+
|
11
|
+
# Build a virtual machine
|
12
|
+
def self.build(spec)
|
13
|
+
fqdn = spec['fqdn']
|
14
|
+
|
15
|
+
puts "Creating #{fqdn}"
|
16
|
+
if options[:dry_run]
|
17
|
+
puts '(skipping due to --dry-run)'
|
18
|
+
next
|
19
|
+
end
|
20
|
+
vm = ForemanVM.new
|
21
|
+
vm.name = spec['name']
|
22
|
+
vm.buildspec = spec
|
23
|
+
vm.create
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
class Architect
|
2
|
+
# Parse the configuration file and ARGV variables
|
3
|
+
class Config
|
4
|
+
require 'facter'
|
5
|
+
require 'pp'
|
6
|
+
|
7
|
+
def to_hash
|
8
|
+
@config
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(conffile = nil)
|
12
|
+
config = {
|
13
|
+
:domain => Facter['domain'].value,
|
14
|
+
:plugins => {},
|
15
|
+
}
|
16
|
+
if conffile.nil? or conffile.kind_of?(String)
|
17
|
+
conffile ||= File.dirname(__FILE__) + "/../../conf/architect.yaml"
|
18
|
+
conffile = File.realpath(conffile)
|
19
|
+
if File.exist?(conffile)
|
20
|
+
config.merge! parse(conffile)
|
21
|
+
else
|
22
|
+
raise "configuration file #{conffile} not found"
|
23
|
+
end
|
24
|
+
elsif conffile.kind_of?(Hash)
|
25
|
+
config.merge! symbolize(conffile)
|
26
|
+
else
|
27
|
+
raise ArgumentError
|
28
|
+
end
|
29
|
+
config.keys.each { |k| publish k }
|
30
|
+
@config = config
|
31
|
+
end
|
32
|
+
|
33
|
+
# TODO: find a better way to do this
|
34
|
+
def self.symbolize_hash(obj)
|
35
|
+
symbolize(obj)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
# Given a nested hash, convert all keys from String to Symbol type
|
41
|
+
# Based on http://stackoverflow.com/questions/800122/best-way-to-convert-strings-to-symbols-in-hash
|
42
|
+
#
|
43
|
+
def symbolize(obj)
|
44
|
+
return obj.inject({}){|memo,(k,v)| memo[k.to_sym] = symbolize(v); memo} if obj.is_a? Hash
|
45
|
+
return obj.inject([]){|memo,v | memo << symbolize(v); memo} if obj.is_a? Array
|
46
|
+
return obj
|
47
|
+
end
|
48
|
+
|
49
|
+
# Parse a configuration file and return a Hash
|
50
|
+
def parse(path)
|
51
|
+
# FIXME: want to disallow group-readable also
|
52
|
+
raise "Insecure permissions on #{path}; please chmod to 0600" \
|
53
|
+
if File.stat(path).world_readable?
|
54
|
+
symbolize(YAML.load_file(path))
|
55
|
+
end
|
56
|
+
|
57
|
+
# Given a key, create a read-only accessor method
|
58
|
+
def publish(key)
|
59
|
+
self.class.class_eval do
|
60
|
+
define_method(key.to_sym) { @config[key.to_sym] }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|