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.
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