foreman-architect 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -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
|