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,73 @@
1
+ #
2
+ # Create a plan
3
+ #
4
+ # Author: Mark Heily <mark.heily@bronto.com>
5
+ #
6
+ class Architect
7
+ class Designer
8
+
9
+ require 'yaml'
10
+ require 'pp'
11
+
12
+ def self.create_plan
13
+ questions = {
14
+ 'environment' => 'Puppet environment',
15
+ 'hostgroup' => 'Foreman hostgroup',
16
+ 'domain' => 'DNS domain',
17
+ 'subnet' => 'Foreman subnet',
18
+ 'network_interface' => 'libvirt network interface',
19
+ 'cpus' => 'Number of virtual CPUs',
20
+ 'memory' => 'Amount of memory',
21
+ 'disk_capacity' => 'Disk sizes; separate multiple disks with a comma',
22
+ 'storage_pool' => 'libvirt storage pool',
23
+ 'owner' => 'Foreman owner',
24
+ }
25
+
26
+ defaults = {
27
+ 'environment' => 'staging',
28
+ 'hostgroup' => 'staging/generic',
29
+ 'domain' => 'brontolabs.local',
30
+ 'subnet' => 'staging_200',
31
+ 'network_interface' => 'vnet0.200',
32
+ 'cpus' => '1',
33
+ 'memory' => '1G',
34
+ 'disk_capacity' => '20G,20G',
35
+ 'storage_pool' => 'gvol',
36
+ 'owner' => 'Systems Engineering',
37
+ }
38
+
39
+ puts "\n------\n\nPlease provide default settings for the instances to be created.\n"
40
+
41
+ answers = { 'version' => 1, 'defaults' => {} }
42
+ questions.each do |k,question|
43
+ puts "\n" + question + " (default: " + defaults[k] + ")\n"
44
+ printf ": "
45
+ res = STDIN.readline.chomp
46
+ res = defaults[k] if res.empty?
47
+ answers['defaults'][k] = res
48
+ end
49
+
50
+ puts "\n------\n\nEnter the name(s) of the instances to be created. Separate multiple instances with a comma. To specify a range, use (x..y) notation.\n"
51
+
52
+ printf ": "
53
+ answers['instances'] = []
54
+ STDIN.readline.chomp.split(/\s*,\s*/).sort.each do |tok|
55
+ # Expand (x..y) notation
56
+ # This assumes a host naming convention of foo-prod-(x..y)
57
+ if tok =~ /\((\d+)\.\.(\d+)\)/
58
+ prefix = $`
59
+ r = Range.new($1,$2)
60
+ puts prefix
61
+ r.each { |x| answers['instances'].push(prefix + sprintf("%03d", x)) }
62
+ else
63
+ answers['instances'].push tok
64
+ end
65
+ end
66
+
67
+
68
+ puts "\n\nHere is the plan you requested:\n\n"
69
+ puts answers.to_yaml
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1,28 @@
1
+ class Architect
2
+ class Log
3
+ require 'logger'
4
+
5
+ @@log = Logger.new(STDOUT)
6
+ @@log.level = Logger::WARN
7
+
8
+ def self.log
9
+ @@log
10
+ end
11
+
12
+ def self.level=(level)
13
+ @@log.level = level
14
+ end
15
+
16
+ def self.debug(message)
17
+ @@log.debug message
18
+ end
19
+
20
+ def self.info(message)
21
+ @@log.info message
22
+ end
23
+
24
+ def self.warn(message)
25
+ @@log.warn message
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,41 @@
1
+ class Architect
2
+ class Plan
3
+
4
+ class InvalidPlanException < Exception
5
+ end
6
+
7
+ require 'yaml'
8
+
9
+ def initialize(path)
10
+ case path.kind_of?
11
+ when String
12
+ @yaml = YAML.load_file(path)
13
+ when Hash
14
+ @yaml = path
15
+ else
16
+ raise ArgumentError
17
+ end
18
+
19
+ validate
20
+ end
21
+
22
+ def to_s
23
+ @yaml
24
+ end
25
+
26
+ def instances
27
+ @yaml['instances']
28
+ end
29
+
30
+ private
31
+
32
+ def validate
33
+ problems = []
34
+ problems.push 'Version number missing' unless @yaml.has_key? 'version'
35
+ problems.push 'Wrong version number' unless @yaml['version'] == 1
36
+ unless problems.empty?
37
+ raise InvalidPlanException, "Errors found in the plan:\n * " + problems.join("\n * ")
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,67 @@
1
+ class Architect
2
+ # When the main Architect executable runs, it calls out to various
3
+ # Plugin objects to do additional work at various stages.
4
+ class Plugin
5
+ require 'architect/log'
6
+
7
+ # The name of the plugin
8
+ attr_reader :name
9
+ # The configuration settings for the plugin
10
+ attr_reader :config
11
+
12
+ # Called when the plugin is registered
13
+ def register
14
+ nil
15
+ end
16
+
17
+ # Parse a [+yaml+] configuration file
18
+ def configure(yaml)
19
+ nil
20
+ end
21
+
22
+ # Check to see if any actions need to be taken, and return
23
+ # a list of Architect::ChangeRequest objects for each proposed
24
+ # action
25
+ def check
26
+ []
27
+ end
28
+
29
+ # Ask questions during the design of a new plan; i.e. when
30
+ # architect --design is called.
31
+ def design
32
+ nil
33
+ end
34
+
35
+ # Parse a YAML plan file and perform validation.
36
+ def plan(yaml)
37
+ nil
38
+ end
39
+
40
+ # Execute the proposed actions based on the current plan.
41
+ def execute
42
+ nil
43
+ end
44
+
45
+ # A single instance of a machine (physical, virtual, or container)
46
+ class MachineInstance
47
+ def initialize(spec)
48
+ raise 'Method not implemented'
49
+ end
50
+
51
+ # Destroy the instance
52
+ def destroy
53
+ raise 'Method not implemented'
54
+ end
55
+
56
+ # Change the name of an instance to [+newname+]
57
+ def rename(newname)
58
+ raise 'Method not implemented'
59
+ end
60
+
61
+ # Return true if the instance exists within the realm of whatever the Plugin manages.
62
+ def exist?
63
+ raise 'Method not implemented'
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,46 @@
1
+ require 'architect/plugin'
2
+
3
+ # A sample plugin to say "hello world" at every opportunity
4
+ class HelloWorldPlugin < Architect::Plugin
5
+
6
+ def initialize
7
+ @name = 'hello_world'
8
+ end
9
+
10
+ def configure(config_hash)
11
+ @config = OpenStruct.new({ hello: 'world', quiet: false }.merge(config_hash))
12
+ @quiet = @config[:quiet]
13
+ say 'hello_world: configured'
14
+ end
15
+
16
+ # Check to see if any actions need to be taken, and return
17
+ # a list of Architect::ChangeRequest objects for each proposed
18
+ # action
19
+ def check
20
+ say 'hello_world: checking'
21
+ []
22
+ end
23
+
24
+ # Ask questions during the design of a new plan; i.e. when
25
+ # architect --design is called.
26
+ def design
27
+ say 'hello_world: designing'
28
+ end
29
+
30
+ # Parse a YAML plan file and perform validation.
31
+ def plan(yaml)
32
+ say 'hello_world: planning'
33
+ end
34
+
35
+ # Execute the proposed actions based on the current plan.
36
+ def execute
37
+ say 'hello_world: executing'
38
+ end
39
+
40
+ private
41
+
42
+ # Print a message to the screen
43
+ def say(message)
44
+ puts message unless @quiet
45
+ end
46
+ end
@@ -0,0 +1,114 @@
1
+ require 'architect/plugin'
2
+
3
+ # Manage host membership in the LDAP 'Security Netgroup' subtree
4
+ class LDAPNetgroupPlugin < Architect::Plugin
5
+ require 'net/ldap'
6
+ require 'ostruct'
7
+
8
+ attr_accessor :log
9
+
10
+ def initialize
11
+ @name = 'ldap_netgroup'
12
+ @log = Architect::Log.log
13
+ #log.level = Logger::DEBUG
14
+ end
15
+
16
+ def configure(config_hash)
17
+ @config = OpenStruct.new({
18
+ host: nil,
19
+ port: nil,
20
+ bind_dn: nil,
21
+ bind_password: nil,
22
+ base_dn: nil,
23
+ nis_domain: nil,
24
+ }.merge(config_hash))
25
+
26
+ bind_to_server
27
+ end
28
+
29
+ # Delete a [+fqdn+] from all netgroups
30
+ def instance_delete(fqdn)
31
+ shortname = fqdn.gsub(/\..*/, '')
32
+ match = '(' + [shortname, '', config.nis_domain].join(',') + ')'
33
+ treebase = config.base_dn
34
+ filter = Net::LDAP::Filter.eq( 'nisnetgrouptriple', match )
35
+ attrs = [ "nisnetgrouptriple" ]
36
+
37
+ log.debug "searching for #{match}"
38
+ ldap.search(base: treebase, filter: filter, attributes: attrs, return_result: false) do |entry|
39
+ log.debug "deleting #{shortname} from #{entry.dn}"
40
+ dn = entry.dn
41
+ ops = [[:delete, :nisNetgroupTriple, match]]
42
+ ldap.modify :dn => dn, :operations => ops
43
+ end
44
+ end
45
+
46
+ # Rename an instance
47
+ def instance_rename(old_fqdn, new_fqdn)
48
+ instance_delete old_fqdn
49
+ instance_create new_fqdn
50
+ end
51
+
52
+ # Add a [+fqdn+] to netgroups
53
+ def instance_create(fqdn)
54
+ value = '(' + [fqdn.gsub(/\..*/, ''), '', config.nis_domain].join(',') + ')'
55
+ netgroup_membership(fqdn).each do |dn|
56
+ log.debug "adding #{value} to #{dn}"
57
+ ops = [[:add, :nisNetgroupTriple, value]]
58
+ ldap.modify :dn => dn, :operations => ops
59
+ check_operation_result
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ attr_accessor :ldap, :config
66
+
67
+ # Check the result of the previous LDAP operation
68
+ def check_operation_result
69
+ result = ldap.get_operation_result
70
+ return if result.code == 0
71
+ log.warn "LDAP operation failed with error code #{result.code}: " +
72
+ result.message
73
+ end
74
+
75
+ # Given a [+fqdn+] return a list of netgroups it should be a member of
76
+ # This relies on the 'description' field of the netgroup being populated
77
+ # with information like: "role=foo,bar,baz", and assumes that the role
78
+ # is the first part of the FQDN.
79
+ #
80
+ def netgroup_membership(fqdn)
81
+ role = fqdn.gsub /\-.*/, ''
82
+ filter = Net::LDAP::Filter.eq( "description", "*role=*" )
83
+ log.debug "searching netgroups for role=#{role}"
84
+
85
+ # Put everything in the AllServers tree by default
86
+ result = ['cn=AllServers,' + config.base_dn]
87
+ ldap.search( :base => config.base_dn, :filter => filter ) do |entry|
88
+ begin
89
+ # Parse something like "role=foo,bar" out of the description
90
+ entry.description[0] =~ /role=([A-Za-z0-9_,]+)/
91
+ roles = $1.split /,/
92
+ log.debug "netgroup #{entry.dn} has roles #{roles.inspect}"
93
+ result.push entry.dn if roles.include? role
94
+ rescue => e
95
+ log.warn "unable to parse #{entry.description} for #{entry.dn}"
96
+ log.warn "#{e.inspect}"
97
+ end
98
+ end
99
+ result
100
+ end
101
+
102
+ def bind_to_server
103
+ ldap = Net::LDAP.new
104
+ ldap.host = config.host
105
+ ldap.port = config.port
106
+ ldap.auth config.bind_dn, config.bind_password
107
+ if ldap.bind
108
+ log.debug "bound to #{ldap.host}:#{ldap.port} as #{config.bind_dn}"
109
+ @ldap = ldap
110
+ else
111
+ raise "Unable to connect to the LDAP server"
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,64 @@
1
+ class Architect
2
+ class PluginManager
3
+ require 'ostruct'
4
+ require 'architect/plugin'
5
+
6
+ # The path to the configuration directory for plugins.
7
+ # Each plugin gets it's own configuration file with the same
8
+ # name as the plugin.
9
+ attr_accessor :pluginconfigdir
10
+
11
+ attr_reader :plugins
12
+
13
+ def initialize(config)
14
+ @config = config.to_hash
15
+ @pluginconfigdir = '/etc/architect/plugin.d/'
16
+ @log = Architect::Log.log
17
+ #log.level = Logger::DEBUG
18
+ @plugins = OpenStruct.new
19
+ plugindir = File.dirname(__FILE__) + '/plugin'
20
+ log.debug "registering plugins in #{plugindir}"
21
+ Dir.glob("#{plugindir}/*.rb").each do |pluginfile|
22
+ load pluginfile
23
+ end
24
+ register_all_plugins
25
+ plugins.each do |plugin|
26
+ conffile = "#{pluginconfigdir}/#{plugin.name}.yaml"
27
+ if File.exist? conffile
28
+ plugin.configure Architect::Config.symbolize_hash YAML.load(conffile)
29
+ end
30
+ end
31
+ end
32
+
33
+ # Before the program exist, each plugin is unregistered.
34
+ # This gives it a chance to clean up.
35
+ def unregister_all
36
+ raise 'FIXME - STUB'
37
+ end
38
+
39
+ private
40
+
41
+ attr_accessor :log
42
+
43
+ def load(pluginfile)
44
+ log.debug "loading #{pluginfile}"
45
+ begin
46
+ require pluginfile
47
+ rescue
48
+ log.warn "plugin failed to load: #{pluginfile}"
49
+ end
50
+ end
51
+
52
+ # Instantiate all plugins
53
+ def register_all_plugins
54
+ ObjectSpace.each_object(Class) do |klass|
55
+ if klass < Architect::Plugin
56
+ plugin = klass.new
57
+ name = plugin.name.to_sym
58
+ @plugins[name] = plugin
59
+ log.debug "registered #{klass} as #{name}"
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,67 @@
1
+ class Architect
2
+ class Report
3
+
4
+ require 'ForemanVM'
5
+ require 'pp'
6
+
7
+ include FVMUtil
8
+
9
+ def initialize
10
+ @fvm = ForemanVM.new
11
+ end
12
+
13
+ # Show the current capacity of the hypervisor cluster
14
+ def capacity
15
+ summary = {
16
+ :host => {},
17
+ :cluster => {
18
+ :cpu_count => 0,
19
+ :vcpu_count => 0,
20
+ :allocated_memory => 0,
21
+ :total_memory => 0,
22
+ }
23
+ }
24
+ @fvm.cluster.members2.each do |host|
25
+ total_memory = host.memory
26
+ cpu_count = host.cpus
27
+ allocated_memory = 0
28
+ vcpu_count = 0
29
+
30
+ host.domains2.each do |domain|
31
+ vcpu_count += domain.vcpu_count
32
+ allocated_memory += domain.memory
33
+ end
34
+
35
+ summary[:host][shortname(host.hostname)] = {
36
+ :allocated_memory => allocated_memory,
37
+ :total_memory => total_memory,
38
+ :vcpu_count => vcpu_count,
39
+ :cpu_count => cpu_count,
40
+ }
41
+ end
42
+
43
+ # Compute the cluster-wide totals
44
+ summary[:host].values.each do |host|
45
+ summary[:cluster].keys.each do |item|
46
+ summary[:cluster][item] += host[item]
47
+ end
48
+ end
49
+ #pp summary[:cluster] ; raise 'DEBUG'
50
+
51
+ cluster = summary[:cluster]
52
+ used_mem_pct = 100 * (cluster[:allocated_memory].to_f / cluster[:total_memory].to_f)
53
+ free_mem = cluster[:total_memory] - cluster[:allocated_memory]
54
+ printf "Memory: %d%% allocated, %s free, %s total\n",
55
+ used_mem_pct,
56
+ gigabytes(free_mem),
57
+ gigabytes(cluster[:total_memory])
58
+
59
+ used_cpu_pct = 100 * (cluster[:vcpu_count].to_f / cluster[:cpu_count].to_f)
60
+ printf "CPU: %d%% allocated, %d vCPUs running on %d CPUs\n",
61
+ used_cpu_pct,
62
+ cluster[:vcpu_count],
63
+ cluster[:cpu_count]
64
+ end
65
+
66
+ end
67
+ end