foreman-architect 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/bin/architect +147 -0
  2. data/bin/foreman-vm +50 -0
  3. data/bin/worker.rb +101 -0
  4. data/lib/architect.rb +49 -0
  5. data/lib/architect/builder/physical.rb +19 -0
  6. data/lib/architect/builder/virtual.rb +27 -0
  7. data/lib/architect/config.rb +64 -0
  8. data/lib/architect/designer.rb +73 -0
  9. data/lib/architect/log.rb +28 -0
  10. data/lib/architect/plan.rb +41 -0
  11. data/lib/architect/plugin.rb +67 -0
  12. data/lib/architect/plugin/hello_world.rb +46 -0
  13. data/lib/architect/plugin/ldap_netgroup.rb +114 -0
  14. data/lib/architect/plugin_manager.rb +64 -0
  15. data/lib/architect/report.rb +67 -0
  16. data/lib/architect/version.rb +3 -0
  17. data/lib/foreman_vm.rb +409 -0
  18. data/lib/foreman_vm/allocator.rb +49 -0
  19. data/lib/foreman_vm/buildspec.rb +48 -0
  20. data/lib/foreman_vm/cluster.rb +83 -0
  21. data/lib/foreman_vm/config.rb +55 -0
  22. data/lib/foreman_vm/console.rb +83 -0
  23. data/lib/foreman_vm/domain.rb +192 -0
  24. data/lib/foreman_vm/foreman_api.rb +78 -0
  25. data/lib/foreman_vm/getopt.rb +151 -0
  26. data/lib/foreman_vm/hypervisor.rb +96 -0
  27. data/lib/foreman_vm/storage_pool.rb +104 -0
  28. data/lib/foreman_vm/util.rb +18 -0
  29. data/lib/foreman_vm/volume.rb +70 -0
  30. data/lib/foreman_vm/workqueue.rb +58 -0
  31. data/test/architect/architect_test.rb +24 -0
  32. data/test/architect/product_service.yaml +33 -0
  33. data/test/architect/tc_builder_physical.rb +13 -0
  34. data/test/architect/tc_config.rb +20 -0
  35. data/test/architect/tc_log.rb +13 -0
  36. data/test/architect/tc_plugin_ldap_netgroup.rb +39 -0
  37. data/test/architect/tc_plugin_manager.rb +27 -0
  38. data/test/tc_allocator.rb +61 -0
  39. data/test/tc_buildspec.rb +45 -0
  40. data/test/tc_cluster.rb +20 -0
  41. data/test/tc_config.rb +12 -0
  42. data/test/tc_foreman_api.rb +20 -0
  43. data/test/tc_foremanvm.rb +20 -0
  44. data/test/tc_hypervisor.rb +37 -0
  45. data/test/tc_main.rb +19 -0
  46. data/test/tc_storage_pool.rb +28 -0
  47. data/test/tc_volume.rb +22 -0
  48. data/test/tc_workqueue.rb +35 -0
  49. data/test/ts_all.rb +13 -0
  50. metadata +226 -0
@@ -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
+
@@ -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
@@ -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
@@ -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