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,78 @@
|
|
1
|
+
module ForemanAP
|
2
|
+
# Convenience methods for accessing the REST API of Foreman.
|
3
|
+
#
|
4
|
+
class ForemanAPI
|
5
|
+
|
6
|
+
require 'logger'
|
7
|
+
require 'net/https'
|
8
|
+
require 'json'
|
9
|
+
|
10
|
+
attr_accessor :log
|
11
|
+
|
12
|
+
# Create an object.
|
13
|
+
# [+uri+] The URI of the Foreman server.
|
14
|
+
# [+user+] The username to login with.
|
15
|
+
# [+password+] The password to login with.
|
16
|
+
def initialize(uri, user, password)
|
17
|
+
@uri = uri
|
18
|
+
@user = user
|
19
|
+
@password = password
|
20
|
+
@log = Logger.new(STDERR)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Send a request to the Foreman API.
|
24
|
+
# [+method+] The HTTP method; can be :post, :put, :get, or :delete
|
25
|
+
# [+path+] The path to the resource, underneath of /api/v2.
|
26
|
+
# [+payload+] The data to provide along with the request.
|
27
|
+
def request(method, path, payload)
|
28
|
+
uri = URI.parse(@uri)
|
29
|
+
path = '/api/v2/' + path
|
30
|
+
|
31
|
+
@log.debug "getting #{path}"
|
32
|
+
# Build the request
|
33
|
+
case method
|
34
|
+
when :post
|
35
|
+
req = Net::HTTP::Post.new(path, initheader = {'Content-Type' =>'application/json'})
|
36
|
+
when :put
|
37
|
+
req = Net::HTTP::Put.new(path, initheader = {'Content-Type' =>'application/json'})
|
38
|
+
when :get
|
39
|
+
req = Net::HTTP::Get.new(path, initheader = {'Content-Type' =>'application/json'})
|
40
|
+
when :delete
|
41
|
+
req = Net::HTTP::Delete.new(path, initheader = {'Content-Type' =>'application/json'})
|
42
|
+
else
|
43
|
+
raise 'Unsuported method'
|
44
|
+
end
|
45
|
+
|
46
|
+
req.basic_auth @user, @password
|
47
|
+
req.body = payload.to_json
|
48
|
+
|
49
|
+
# Submit the request
|
50
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
51
|
+
http.use_ssl = true
|
52
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE #XXX-FIXME insecure
|
53
|
+
response = http.start { |h| http.request(req) }
|
54
|
+
|
55
|
+
@log.debug "Response #{response.code} #{response.message}: #{response.body}"
|
56
|
+
|
57
|
+
if response.code != '200'
|
58
|
+
@log.error "Error #{response.code} calling the Foreman API: #{response.body}"
|
59
|
+
raise 'Error calling the Foreman API'
|
60
|
+
end
|
61
|
+
|
62
|
+
return response
|
63
|
+
end
|
64
|
+
|
65
|
+
# Get the ID of a resource in Foreman.
|
66
|
+
# [+path+] The path to the resource, underneath of /api/v2
|
67
|
+
# [+name+] The name to lookup
|
68
|
+
# [+key+] Either 'name' or 'title'. Don't ask why there is a difference.
|
69
|
+
#
|
70
|
+
def get_id(path, name, key='name')
|
71
|
+
raise 'name is required' if name.nil?
|
72
|
+
res = request(:get, "/#{path}?search=#{key}%20=%20\"#{URI.escape(name)}\"", {})
|
73
|
+
id = JSON.parse(res.body)['results'][0]['id']
|
74
|
+
raise 'Unable to get the ID for #{path}/#{key}=#{name}' if id.nil?
|
75
|
+
id
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
class ForemanVM
|
2
|
+
require 'optparse'
|
3
|
+
|
4
|
+
# Parse command line options
|
5
|
+
#
|
6
|
+
def parse_options
|
7
|
+
optparse = OptionParser.new do |opts|
|
8
|
+
|
9
|
+
opts.on( '--user USER', 'The username to login to Foreman with') do |arg|
|
10
|
+
@user = arg
|
11
|
+
end
|
12
|
+
|
13
|
+
opts.on( '--password PASSWORD', 'The password to login to Foreman with') do |arg|
|
14
|
+
@password = arg
|
15
|
+
end
|
16
|
+
|
17
|
+
opts.on( '--password-file FILE', 'The file containing your Foreman password') do |arg|
|
18
|
+
raise 'File does not exist' unless File.exists? arg
|
19
|
+
@password = `cat #{arg}`.chomp
|
20
|
+
end
|
21
|
+
|
22
|
+
opts.on( '--foreman-uri URI', 'The URI of the Foreman password') do |arg|
|
23
|
+
@foreman_uri = arg
|
24
|
+
#options['foreman_uri'] = arg
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
opts.separator ""
|
29
|
+
opts.separator "Specific options for building hosts:"
|
30
|
+
|
31
|
+
opts.on( '--console', 'Attach to the virtual machine console') do |arg|
|
32
|
+
@buildspec['console'] = true
|
33
|
+
@action = 'console' if @action.nil? #XXX-KLUDGE for testing
|
34
|
+
end
|
35
|
+
|
36
|
+
opts.on( '--cpus CPUS', 'The number of CPUs to assign to the VM') do |arg|
|
37
|
+
@buildspec['cpus'] = arg
|
38
|
+
end
|
39
|
+
|
40
|
+
opts.on( '--memory BYTES', 'The amount of memory, in bytes, to assign to the VM') do |arg|
|
41
|
+
@buildspec['memory'] = arg
|
42
|
+
end
|
43
|
+
|
44
|
+
opts.on( '--domain DOMAIN', 'The DNS domain name to use') do |arg|
|
45
|
+
@buildspec['domain'] = arg
|
46
|
+
end
|
47
|
+
|
48
|
+
opts.on( '--organization ORG', 'The organization name in Foreman') do |arg|
|
49
|
+
@buildspec['organization'] = arg
|
50
|
+
end
|
51
|
+
|
52
|
+
opts.on( '--owner OWNER', 'The owner name in Foreman') do |arg|
|
53
|
+
@buildspec['owner'] = arg
|
54
|
+
end
|
55
|
+
|
56
|
+
opts.on( '--hostgroup HOSTGROUP', 'The Foreman hostgroup') do |arg|
|
57
|
+
@buildspec['hostgroup'] = arg
|
58
|
+
end
|
59
|
+
|
60
|
+
opts.on( '--compute-resource RESOURCE', 'The Foreman compute resource') do |arg|
|
61
|
+
@buildspec['compute_resource'] = arg
|
62
|
+
end
|
63
|
+
|
64
|
+
opts.on( '--provision-method METHOD', 'The provisioning method (image or network)') do |arg|
|
65
|
+
@buildspec['provision_method'] = arg
|
66
|
+
end
|
67
|
+
|
68
|
+
opts.on( '--environment ENVIRONMENT', 'The Puppet environment') do |arg|
|
69
|
+
@buildspec['environment'] = arg
|
70
|
+
end
|
71
|
+
|
72
|
+
opts.on( '--network-interface INTERFACE', 'The network interface') do |arg|
|
73
|
+
@buildspec['network_interface'] = arg
|
74
|
+
end
|
75
|
+
|
76
|
+
opts.on( '--disk-capacity SIZE', 'The size of the first disk, using M or G suffixes') do |arg|
|
77
|
+
@buildspec['disk_capacity'] = arg
|
78
|
+
end
|
79
|
+
|
80
|
+
opts.on( '--disk-format FORMAT', 'The format of the disk. Can be raw or qcow2') do |arg|
|
81
|
+
@buildspec['disk_format'] = arg
|
82
|
+
end
|
83
|
+
|
84
|
+
opts.on( '--storage-pool POOL', 'The storage pool') do |arg|
|
85
|
+
@buildspec['storage_pool'] = arg
|
86
|
+
end
|
87
|
+
|
88
|
+
opts.on( '--subnet SUBNET', 'The subnet') do |arg|
|
89
|
+
@buildspec['subnet'] = arg
|
90
|
+
end
|
91
|
+
|
92
|
+
# TODO: some nice header thing
|
93
|
+
|
94
|
+
opts.separator ""
|
95
|
+
opts.separator "Actions:"
|
96
|
+
|
97
|
+
opts.on( '--rebuild', 'Rebuild the host') do |arg|
|
98
|
+
@action = 'rebuild'
|
99
|
+
end
|
100
|
+
|
101
|
+
opts.on( '--create', 'Create the host') do |arg|
|
102
|
+
@action = 'create'
|
103
|
+
end
|
104
|
+
|
105
|
+
opts.on( '--create-storage', 'Create storage and attach it to the host') do |arg|
|
106
|
+
@action = 'create-storage'
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
opts.on( '--delete', 'Delete the host') do |arg|
|
111
|
+
@action = 'delete'
|
112
|
+
end
|
113
|
+
|
114
|
+
opts.on( '--start', 'Power on the VM') do |arg|
|
115
|
+
@action = 'start'
|
116
|
+
end
|
117
|
+
|
118
|
+
opts.on( '--stop', 'Power off the VM, without doing a graceful shutdown') do |arg|
|
119
|
+
@action = 'stop'
|
120
|
+
end
|
121
|
+
|
122
|
+
opts.on( '--dumpxml', 'Show the libvirt XML virtual machine definition') do |arg|
|
123
|
+
@action = 'dumpxml'
|
124
|
+
end
|
125
|
+
|
126
|
+
opts.on( '--monitor-boot', 'Watch the console of a VM as it boots') do |arg|
|
127
|
+
@action = 'monitor-boot'
|
128
|
+
end
|
129
|
+
|
130
|
+
opts.on( '--enable-libgfapi', 'Enable GlusterFS libgfapi') do |arg|
|
131
|
+
@action = 'enable-libgfapi'
|
132
|
+
end
|
133
|
+
|
134
|
+
opts.on( '--disable-libgfapi', 'Disable GlusterFS libgfapi') do |arg|
|
135
|
+
@action = 'disable-libgfapi'
|
136
|
+
end
|
137
|
+
|
138
|
+
## Verbosity (DEBUG, WARN, INFO, etc.)
|
139
|
+
##log_level="DEBUG"
|
140
|
+
|
141
|
+
opts.on( '-h', '--help', 'Display this screen' ) do
|
142
|
+
puts opts
|
143
|
+
exit
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
optparse.parse!
|
148
|
+
|
149
|
+
ask_password if @password.nil?
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module ForemanAP
|
2
|
+
# A host in the cluster that runs a hypervisor.
|
3
|
+
class Hypervisor
|
4
|
+
require 'foreman_vm/storage_pool'
|
5
|
+
|
6
|
+
# The total number of CPUs (physical and virtual)
|
7
|
+
def cpus
|
8
|
+
nodeinfo.cpus
|
9
|
+
end
|
10
|
+
|
11
|
+
# The total amount of memory, in bytes.
|
12
|
+
def memory
|
13
|
+
nodeinfo.memory * 1024 # Convert from KiB
|
14
|
+
end
|
15
|
+
|
16
|
+
# The amount of free memory, in bytes.
|
17
|
+
def free_memory
|
18
|
+
@conn.node_free_memory
|
19
|
+
end
|
20
|
+
|
21
|
+
# Lookup a storage pool by name.
|
22
|
+
def storage_pool(name)
|
23
|
+
StoragePool.new(@conn, name)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Return the name of hypervisor
|
27
|
+
def hostname
|
28
|
+
@conn.hostname
|
29
|
+
end
|
30
|
+
|
31
|
+
# A guest running on the hypervisor
|
32
|
+
# [+fqdn+] the FQDN of the guest
|
33
|
+
def guest(fqdn)
|
34
|
+
ForemanAP::Domain.new(@conn, fqdn, @foreman_api)
|
35
|
+
end
|
36
|
+
|
37
|
+
# The names of all the domains defined on the hypervisor.
|
38
|
+
def domains
|
39
|
+
res = []
|
40
|
+
|
41
|
+
# Active domains
|
42
|
+
@conn.list_domains.each do |domid|
|
43
|
+
dom = @conn.lookup_domain_by_id(domid)
|
44
|
+
res.push dom.name
|
45
|
+
end
|
46
|
+
|
47
|
+
# Inactive domains
|
48
|
+
@conn.list_defined_domains.each do |domname|
|
49
|
+
res.push domname
|
50
|
+
end
|
51
|
+
|
52
|
+
res
|
53
|
+
end
|
54
|
+
|
55
|
+
# A list of handles to all the active domains on the hypervisor
|
56
|
+
# TODO - replace #domains with this
|
57
|
+
def domains2
|
58
|
+
res = []
|
59
|
+
|
60
|
+
# Active domains
|
61
|
+
@conn.list_domains.each do |domid|
|
62
|
+
dom = @conn.lookup_domain_by_id(domid)
|
63
|
+
res.push ForemanAP::Domain.new(@conn, dom.name, @foreman_api)
|
64
|
+
end
|
65
|
+
|
66
|
+
res
|
67
|
+
end
|
68
|
+
|
69
|
+
# Create an object.
|
70
|
+
#
|
71
|
+
# [+uri+] The libvirt URI. Example: qemu+tcp:///host/system
|
72
|
+
# [+user+] The username to login with.
|
73
|
+
# [+passphrase+] The passphrase to login with.
|
74
|
+
def initialize(uri, user, passphrase)
|
75
|
+
@uri = uri
|
76
|
+
@conn = Libvirt::open_auth(@uri,
|
77
|
+
[Libvirt::CRED_AUTHNAME, Libvirt::CRED_PASSPHRASE]) do |cred|
|
78
|
+
case cred["type"]
|
79
|
+
when Libvirt::CRED_AUTHNAME
|
80
|
+
user
|
81
|
+
when Libvirt::CRED_PASSPHRASE
|
82
|
+
passphrase
|
83
|
+
else
|
84
|
+
raise 'Unknown error'
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
# The 'nodeinfo' structure for the connection
|
92
|
+
def nodeinfo
|
93
|
+
@nodeinfo ||= @conn.node_get_info
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module ForemanAP
|
2
|
+
# A storage pool containing disk volumes.
|
3
|
+
class StoragePool
|
4
|
+
require 'foreman_vm/volume'
|
5
|
+
|
6
|
+
# Scan the pool for changes. This is useful for pools that are shared
|
7
|
+
# across multiple hosts in a cluster.
|
8
|
+
def refresh
|
9
|
+
@sph.refresh
|
10
|
+
true
|
11
|
+
end
|
12
|
+
|
13
|
+
# Lookup a volume by +name+, and return an object handle.
|
14
|
+
def volume(name)
|
15
|
+
Volume.new(@sph, name)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Create a new object.
|
19
|
+
#
|
20
|
+
# [+conn+] A libvirt connection handle.
|
21
|
+
# [+name+] The name of the storage pool.
|
22
|
+
#
|
23
|
+
def initialize(conn, name)
|
24
|
+
@sph = conn.lookup_storage_pool_by_name(name)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Create a new volume within the storage pool.
|
28
|
+
# [+path+] the full path to the volume
|
29
|
+
# [+capacity+] the disk capacity, in bytes
|
30
|
+
#
|
31
|
+
def create_volume(path,capacity)
|
32
|
+
xml = "<volume>
|
33
|
+
<name>#{File.basename(path)}</name>
|
34
|
+
<key>#{path}</key>
|
35
|
+
<source>
|
36
|
+
</source>
|
37
|
+
<capacity unit='bytes'>#{capacity}</capacity>
|
38
|
+
<allocation unit='bytes'>197120</allocation>
|
39
|
+
<target>
|
40
|
+
<path>#{path}</path>
|
41
|
+
<format type='raw'/>
|
42
|
+
<permissions>
|
43
|
+
<mode>0660</mode>
|
44
|
+
<owner>107</owner>
|
45
|
+
<group>107</group>
|
46
|
+
</permissions>
|
47
|
+
</target>
|
48
|
+
</volume>
|
49
|
+
"
|
50
|
+
#TODO: @log.debug "creating volume: #{xml}"
|
51
|
+
@sph.create_volume_xml(xml)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
#--
|
57
|
+
## Legacy code below here
|
58
|
+
|
59
|
+
class ForemanVM
|
60
|
+
|
61
|
+
# Copy an existing disk volume to reduce the build time
|
62
|
+
#
|
63
|
+
def copy_volume
|
64
|
+
delete_volume
|
65
|
+
|
66
|
+
virsh "vol-clone --pool #{@buildspec['storage_pool']} --vol #{@buildspec['_disk_backing_file']} --newname #{fqdn()}-disk1"
|
67
|
+
end
|
68
|
+
|
69
|
+
# Clone an existing disk volume to reduce the build time
|
70
|
+
#
|
71
|
+
def clone_volume
|
72
|
+
delete_volume
|
73
|
+
|
74
|
+
# BUG: We would like to do this, but it creates the file owned by root:root
|
75
|
+
#virsh "vol-create-as --pool #{@buildspec['storage_pool']} --name #{fqdn()}-disk1 --capacity 30G --format qcow2 --backing-vol #{@buildspec['_disk_backing_file']} --backing-vol-format qcow2"
|
76
|
+
#
|
77
|
+
# WORKAROUND: use an XML volume definition to set the owner/group
|
78
|
+
#
|
79
|
+
xml = "<volume>
|
80
|
+
<name>#{fqdn}-disk1</name>
|
81
|
+
<key>/gvol/images/#{fqdn}-disk1</key>
|
82
|
+
<source>
|
83
|
+
</source>
|
84
|
+
<capacity unit='bytes'>32212254720</capacity>
|
85
|
+
<allocation unit='bytes'>197120</allocation>
|
86
|
+
<target>
|
87
|
+
<path>/gvol/images/#{fqdn}-disk1</path>
|
88
|
+
<format type='qcow2'/>
|
89
|
+
<permissions>
|
90
|
+
<mode>0660</mode>
|
91
|
+
<owner>107</owner>
|
92
|
+
<group>107</group>
|
93
|
+
</permissions>
|
94
|
+
</target>
|
95
|
+
<backingStore>
|
96
|
+
<path>#{@buildspec['_disk_backing_file']}</path>
|
97
|
+
<format type='qcow2'/>
|
98
|
+
</backingStore>
|
99
|
+
</volume>
|
100
|
+
"
|
101
|
+
@log.debug "creating volume: #{xml}"
|
102
|
+
virsh("vol-create --pool gvol --file /dev/stdin >/dev/null", xml)
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# Miscellaneous utility functions
|
2
|
+
module FVMUtil
|
3
|
+
# Given an FQDN, return the shortname
|
4
|
+
def shortname(fqdn)
|
5
|
+
fqdn.sub(/\..*/, '')
|
6
|
+
end
|
7
|
+
|
8
|
+
# Given an amount in bytes, return the human-readable version in GiB
|
9
|
+
def gigabytes(s)
|
10
|
+
if s.to_i < 1073741824
|
11
|
+
mb = s.to_f / 1024 / 1024
|
12
|
+
sprintf '%0.0f MiB', mb
|
13
|
+
else
|
14
|
+
gb = s.to_f / 1024 / 1024 / 1024
|
15
|
+
sprintf '%0.0f GiB', gb
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|