foreman-architect 0.1.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/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
|