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