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