foreman_cpp_cloudstack 0.1.5
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.
- checksums.yaml +7 -0
- data/LICENSE +619 -0
- data/README.md +48 -0
- data/Rakefile +7 -0
- data/app/models/concerns/fog_extensions/cloudstack/flavor.rb +15 -0
- data/app/models/concerns/fog_extensions/cloudstack/server.rb +88 -0
- data/app/models/concerns/foreman_cpp_cloudstack/compute.rb +28 -0
- data/app/models/foreman_cpp_cloudstack/cloudstack.rb +238 -0
- data/app/views/api/v1/compute_resources/cloudstack.json.rabl +1 -0
- data/app/views/api/v2/compute_resources/cloudstack.json.rabl +1 -0
- data/app/views/compute_resources/form/_cloudstack.html.erb +15 -0
- data/app/views/compute_resources/show/_cloudstack.html.erb +4 -0
- data/app/views/compute_resources_vms/form/_cloudstack.html.erb +9 -0
- data/app/views/compute_resources_vms/index/_cloudstack.html.erb +20 -0
- data/app/views/compute_resources_vms/show/_cloudstack.html.erb +18 -0
- data/app/views/images/form/_cloudstack.html.erb +3 -0
- data/lib/foreman_cpp_cloudstack.rb +3 -0
- data/lib/foreman_cpp_cloudstack/engine.rb +40 -0
- data/lib/foreman_cpp_cloudstack/version.rb +3 -0
- data/lib/tasks/foreman_cpp_cloudstack_tasks.rake +4 -0
- data/locale/Makefile +6 -0
- data/test/buildhost.sh +29 -0
- data/test/buildhostds.sh +29 -0
- data/test/foreman_cpp_cloudstack_test.rb +7 -0
- data/test/test_helper.rb +15 -0
- metadata +100 -0
data/README.md
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# Foreman Cloudstack Plugin
|
2
|
+
|
3
|
+
This plugin enables provisioning and managing a Cloudstack Server in Foreman.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
The only way I can get this to work today is unzipping the source code here on top of foreman or by using the included Vagrantfile. The typical gem installation is what I want to support but does not work yet.
|
8
|
+
|
9
|
+
Please see the Foreman manual for appropriate instructions:
|
10
|
+
|
11
|
+
* [Foreman: How to Install a Plugin](http://theforeman.org/manuals/latest/index.html#6.1InstallaPlugin)
|
12
|
+
|
13
|
+
The gem name is "foreman_cpp_cloudstack".
|
14
|
+
|
15
|
+
## Compatibility
|
16
|
+
|
17
|
+
| Foreman Version | Plugin Version |
|
18
|
+
| ---------------:| --------------:|
|
19
|
+
| >= 1.7 | 0.1.4 |
|
20
|
+
|
21
|
+
## Latest code
|
22
|
+
|
23
|
+
You can get the develop branch of the plugin by specifying your Gemfile in this way:
|
24
|
+
|
25
|
+
gem 'foreman_cpp_cloudstack', :git => "https://github.com/bytemine/foreman-cloudstack.git"
|
26
|
+
|
27
|
+
## Limitations
|
28
|
+
|
29
|
+
Only advanced networking is supported
|
30
|
+
|
31
|
+
All user data is gzipped
|
32
|
+
|
33
|
+
# Copyright
|
34
|
+
|
35
|
+
Copyright (c) 2014 Citrix
|
36
|
+
|
37
|
+
This program is free software: you can redistribute it and/or modify
|
38
|
+
it under the terms of the GNU General Public License as published by
|
39
|
+
the Free Software Foundation, either version 3 of the License, or
|
40
|
+
(at your option) any later version.
|
41
|
+
|
42
|
+
This program is distributed in the hope that it will be useful,
|
43
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
44
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
45
|
+
GNU General Public License for more details.
|
46
|
+
|
47
|
+
You should have received a copy of the GNU General Public License
|
48
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
data/Rakefile
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
module FogExtensions
|
2
|
+
module Cloudstack
|
3
|
+
module Server
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
#included do
|
7
|
+
# alias_method_chain :security_groups, :no_id
|
8
|
+
# attr_writer :security_group, :network # floating IP
|
9
|
+
#end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
name
|
13
|
+
end
|
14
|
+
|
15
|
+
def ip_address
|
16
|
+
logger.info "BG inspect nics:"
|
17
|
+
logger.info nics.inspect
|
18
|
+
return nics[0]["ipaddress"]
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_method
|
22
|
+
nics[0]["ipaddress"]
|
23
|
+
end
|
24
|
+
|
25
|
+
def ip_addresses
|
26
|
+
logger.info "BG inspect nics:"
|
27
|
+
logger.info nics.inspect
|
28
|
+
nics.map { |n| n.ipaddress }
|
29
|
+
end
|
30
|
+
|
31
|
+
def start
|
32
|
+
if state.downcase == 'paused'
|
33
|
+
service.unpause_server(id)
|
34
|
+
else
|
35
|
+
service.resume_server(id)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def stop
|
40
|
+
service.suspend_server(id)
|
41
|
+
end
|
42
|
+
|
43
|
+
def pause
|
44
|
+
service.pause_server(id)
|
45
|
+
end
|
46
|
+
|
47
|
+
def tenant
|
48
|
+
service.tenants.detect{|t| t.id == tenant_id }
|
49
|
+
end
|
50
|
+
|
51
|
+
def flavor_with_object
|
52
|
+
service.flavors.get attributes[:flavor]['id']
|
53
|
+
end
|
54
|
+
|
55
|
+
def created_at
|
56
|
+
Time.parse attributes['created']
|
57
|
+
end
|
58
|
+
|
59
|
+
# the original method requires a server ID, however we want to be able to call this method on new instances too
|
60
|
+
def security_groups_with_no_id
|
61
|
+
return [] if id.nil?
|
62
|
+
|
63
|
+
security_groups_without_no_id
|
64
|
+
end
|
65
|
+
|
66
|
+
def network
|
67
|
+
return @network if @network # in case we didnt submitting the form again after an error.
|
68
|
+
return networks.try(:first).try(:name) if persisted?
|
69
|
+
nil
|
70
|
+
end
|
71
|
+
|
72
|
+
def security_group
|
73
|
+
return @security_group if @security_group # in case we didnt submitting the form again after an error.
|
74
|
+
return security_groups.try(:first).try(:name) if persisted?
|
75
|
+
nil
|
76
|
+
end
|
77
|
+
|
78
|
+
def reset
|
79
|
+
reboot('HARD')
|
80
|
+
end
|
81
|
+
|
82
|
+
def vm_description
|
83
|
+
""
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'timeout'
|
3
|
+
require 'zlib'
|
4
|
+
|
5
|
+
module ForemanCPPCloudstack::Compute
|
6
|
+
extend Orchestration::Compute
|
7
|
+
|
8
|
+
def setUserData
|
9
|
+
logger.info "Rendering UserData template for #{name}"
|
10
|
+
template = configTemplate(:kind => "user_data")
|
11
|
+
@host = self
|
12
|
+
# For some reason this renders as 'built' in spoof view but 'provision' when
|
13
|
+
# actually used. For now, use foreman_url('built') in the template
|
14
|
+
if self.provider.downcase == "cloudstack"
|
15
|
+
logger.info "computing cloudstack userdata"
|
16
|
+
wio = StringIO.new("w")
|
17
|
+
w_gz = Zlib::GzipWriter.new(wio)
|
18
|
+
w_gz.write(unattended_render(template.template))
|
19
|
+
w_gz.close
|
20
|
+
self.compute_attributes[:user_data] = Base64.strict_encode64(wio.string)
|
21
|
+
else
|
22
|
+
self.compute_attributes[:user_data] = unattended_render(template.template)
|
23
|
+
end
|
24
|
+
self.handle_ca
|
25
|
+
return false if errors.any?
|
26
|
+
logger.info "Revoked old certificates and enabled autosign for UserData"
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,238 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module ForemanCPPCloudstack
|
4
|
+
class Cloudstack < ComputeResource
|
5
|
+
has_one :key_pair, :foreign_key => :compute_resource_id, :dependent => :destroy
|
6
|
+
after_create :setup_key_pair
|
7
|
+
after_destroy :destroy_key_pair
|
8
|
+
delegate :flavors, :to => :client
|
9
|
+
delegate :disk_offerings, :to => :client
|
10
|
+
attr_accessor :zone, :hypervisor
|
11
|
+
#alias_attribute :subnet_id, :network_ids
|
12
|
+
|
13
|
+
validates :url, :user, :password, :presence => true
|
14
|
+
|
15
|
+
# add additional params to the 'new' method
|
16
|
+
def initialize(params)
|
17
|
+
super
|
18
|
+
if params
|
19
|
+
attrs[:zone_id] = params[:zone]
|
20
|
+
attrs[:hypervisor] = params[:hypervisor]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def domains
|
25
|
+
return [] if url.blank? or user.blank? or password.blank?
|
26
|
+
domainsobj = client.list_domains
|
27
|
+
|
28
|
+
domains_array = []
|
29
|
+
zonesobj["listdomainsresponse"]["domain"].each do |domain|
|
30
|
+
z = domain["name"]
|
31
|
+
domains_array.push(z)
|
32
|
+
end
|
33
|
+
logger.info(domainsobj)
|
34
|
+
logger.info(domains_array)
|
35
|
+
return domains_array
|
36
|
+
end
|
37
|
+
|
38
|
+
def zones
|
39
|
+
return [] if url.blank? or user.blank? or password.blank?
|
40
|
+
zonesobj = client.list_zones
|
41
|
+
|
42
|
+
zones_array = []
|
43
|
+
zonesobj["listzonesresponse"]["zone"].each do |zone|
|
44
|
+
z = {:name => zone["name"], :id => zone["id"]}
|
45
|
+
zones_array.push(z)
|
46
|
+
end
|
47
|
+
return zones_array
|
48
|
+
end
|
49
|
+
|
50
|
+
# save the zone
|
51
|
+
def set_zone=(zone_id)
|
52
|
+
self.attrs[:zone_id] = zone_id
|
53
|
+
end
|
54
|
+
|
55
|
+
def get_zone
|
56
|
+
self.attrs[:zone_id]
|
57
|
+
end
|
58
|
+
|
59
|
+
def hypervisors
|
60
|
+
return [] if url.blank? or user.blank? or password.blank?
|
61
|
+
hypervisorsobj = client.list_hypervisors
|
62
|
+
|
63
|
+
hypervisors_array = []
|
64
|
+
hypervisorsobj["listhypervisorsresponse"]["hypervisor"].each do |hypervisor|
|
65
|
+
hypervisors_array.push(hypervisor["name"])
|
66
|
+
end
|
67
|
+
return hypervisors_array
|
68
|
+
end
|
69
|
+
|
70
|
+
def domain
|
71
|
+
attrs[:domain]
|
72
|
+
end
|
73
|
+
|
74
|
+
def zone
|
75
|
+
attrs[:zone]
|
76
|
+
end
|
77
|
+
|
78
|
+
def zone_id
|
79
|
+
return client.list_zones["listzonesresponse"]["zone"][0]["id"]
|
80
|
+
end
|
81
|
+
|
82
|
+
def hypervisor
|
83
|
+
attrs[:hypervisor]
|
84
|
+
end
|
85
|
+
|
86
|
+
def set_hypervisor=(hypervisor)
|
87
|
+
self.attrs[:hypervisor] = hypervisor
|
88
|
+
end
|
89
|
+
|
90
|
+
def get_hypervisor
|
91
|
+
self.attrs[:hypervisor]
|
92
|
+
end
|
93
|
+
|
94
|
+
def provided_attributes
|
95
|
+
super.merge({ :ip => :test_method })
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.model_name
|
99
|
+
ComputeResource.model_name
|
100
|
+
end
|
101
|
+
|
102
|
+
def image_param_name
|
103
|
+
:image_ref
|
104
|
+
end
|
105
|
+
|
106
|
+
def capabilities
|
107
|
+
[:image]
|
108
|
+
end
|
109
|
+
|
110
|
+
def networks
|
111
|
+
fog_ntwrks = []
|
112
|
+
networks_array = client.list_networks["listnetworksresponse"]["network"]
|
113
|
+
networks_array.each do |network|
|
114
|
+
ntwrk = Fog::Compute::Cloudstack::Address.new
|
115
|
+
ntwrk.id = network["id"]
|
116
|
+
ntwrk.network_id = network["name"]
|
117
|
+
fog_ntwrks.push(ntwrk)
|
118
|
+
end
|
119
|
+
return fog_ntwrks
|
120
|
+
end
|
121
|
+
|
122
|
+
def test_connection options = {}
|
123
|
+
client
|
124
|
+
errors[:url].empty? and errors[:user].empty? and errors[:password].empty? and zones
|
125
|
+
rescue Fog::Compute::Cloudstack::Error => e
|
126
|
+
errors[:base] << e.message
|
127
|
+
end
|
128
|
+
|
129
|
+
def available_images
|
130
|
+
client.images
|
131
|
+
end
|
132
|
+
|
133
|
+
def create_vm(args = {})
|
134
|
+
args[:security_group_ids] = nil
|
135
|
+
args[:network_ids] = [args[:network_ids]] if args[:network_ids]
|
136
|
+
args[:network_ids] = [args[:subnet_id]] if args[:subnet_id]
|
137
|
+
args[:network_ids] = nil
|
138
|
+
|
139
|
+
args[:zone_id] = get_zone
|
140
|
+
|
141
|
+
args[:display_name] = args[:name]
|
142
|
+
# name has to be hostname without domain: no dots allowed
|
143
|
+
name = args[:name].split(/\.(?=[\w])/).first || args[:name]
|
144
|
+
args[:name] = name
|
145
|
+
|
146
|
+
options = vm_instance_defaults.merge(args.to_hash.symbolize_keys)
|
147
|
+
vm = client.servers.create options
|
148
|
+
vm.wait_for { nics.present? }
|
149
|
+
logger.warn "captured ipaddress"
|
150
|
+
logger.warn vm.nics[0]["ipaddress"]
|
151
|
+
logger.warn vm.inspect
|
152
|
+
vm
|
153
|
+
rescue => e
|
154
|
+
message = JSON.parse(e.response.body)['badRequest']['message'] rescue (e.to_s)
|
155
|
+
logger.warn "failed to create vm: #{message}"
|
156
|
+
destroy_vm vm.id if vm
|
157
|
+
raise message
|
158
|
+
end
|
159
|
+
|
160
|
+
def destroy_vm uuid
|
161
|
+
find_vm_by_uuid(uuid).destroy
|
162
|
+
rescue ActiveRecord::RecordNotFound
|
163
|
+
# if the VM does not exists, we don't really care.
|
164
|
+
true
|
165
|
+
end
|
166
|
+
|
167
|
+
def console(uuid)
|
168
|
+
vm = find_vm_by_uuid(uuid)
|
169
|
+
vm.console.body.merge({'timestamp' => Time.now.utc})
|
170
|
+
end
|
171
|
+
|
172
|
+
def associated_host(vm)
|
173
|
+
Host.authorized(:view_hosts, Host).where(:ip => [vm.nics[0]["ipaddress"], vm.floating_ip_address, vm.private_ip_address]).first
|
174
|
+
end
|
175
|
+
|
176
|
+
def ip_address uuid
|
177
|
+
vm = find_vm_by_uuid(uuid)
|
178
|
+
vm.nics[0]["ipaddress"]
|
179
|
+
end
|
180
|
+
|
181
|
+
def flavor_name(flavor_ref)
|
182
|
+
client.flavors.get(flavor_ref).try(:name)
|
183
|
+
end
|
184
|
+
|
185
|
+
def provider_friendly_name
|
186
|
+
"Cloudstack"
|
187
|
+
end
|
188
|
+
|
189
|
+
private
|
190
|
+
|
191
|
+
def client
|
192
|
+
results = /^(https|http):\/\/(\S+):(\d+)(\/\S+)/.match(url)
|
193
|
+
scheme = results[1]
|
194
|
+
path = results[4]
|
195
|
+
host = results[2]
|
196
|
+
port = results[3]
|
197
|
+
|
198
|
+
@client = Fog::Compute.new(
|
199
|
+
:provider => 'cloudstack',
|
200
|
+
:cloudstack_api_key => user,
|
201
|
+
:cloudstack_host => host,
|
202
|
+
:cloudstack_port => port,
|
203
|
+
:cloudstack_path => path,
|
204
|
+
:cloudstack_scheme => scheme,
|
205
|
+
:cloudstack_secret_access_key => password
|
206
|
+
)
|
207
|
+
end
|
208
|
+
|
209
|
+
def setup_key_pair
|
210
|
+
result = client.create_ssh_key_pair("foreman-#{id}#{Foreman.uuid}")
|
211
|
+
private_key = result["createsshkeypairresponse"]["keypair"]["privatekey"]
|
212
|
+
name = result["createsshkeypairresponse"]["keypair"]["name"]
|
213
|
+
KeyPair.create! :name => name, :compute_resource_id => self.id, :secret => private_key
|
214
|
+
rescue => e
|
215
|
+
logger.warn "failed to generate key pair"
|
216
|
+
destroy_key_pair
|
217
|
+
raise
|
218
|
+
end
|
219
|
+
|
220
|
+
def destroy_key_pair
|
221
|
+
return unless key_pair
|
222
|
+
logger.info "removing CloudStack key #{key_pair.name}"
|
223
|
+
result = client.delete_ssh_key_pair(key_pair.name)
|
224
|
+
key.destroy if key
|
225
|
+
key_pair.destroy
|
226
|
+
true
|
227
|
+
rescue => e
|
228
|
+
logger.warn "failed to delete key pair from CloudStack, you might need to cleanup manually : #{e}"
|
229
|
+
end
|
230
|
+
|
231
|
+
def vm_instance_defaults
|
232
|
+
super.merge(
|
233
|
+
:key_name => key_pair.name
|
234
|
+
)
|
235
|
+
end
|
236
|
+
|
237
|
+
end
|
238
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
attributes :user, :tenant
|
@@ -0,0 +1 @@
|
|
1
|
+
attributes :user, :tenant
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<%= text_f f, :url, :size => "col-md-8", :help_block => _("e.g. http://managementserver.example.com:8080/client/api") %>
|
2
|
+
<%= text_f f, :user, :label => _("API Key") %>
|
3
|
+
<%= password_f f, :password, :label => _("Secret Key") %>
|
4
|
+
|
5
|
+
<% zones = f.object.zones rescue [] %>
|
6
|
+
<%= selectable_f(f, :zone, zones.map { |z| [z[:name], z[:id]] }, {}, {:label => _('Zone'), :disabled => zones.empty?,
|
7
|
+
:help_inline => link_to_function(zones.empty? ? _("Load Zones") : _("Test Connection"), "testConnection(this)",
|
8
|
+
:class => "btn + #{zones.empty? ? "btn-default" : "btn-success"}",
|
9
|
+
:'data-url' => test_connection_compute_resources_path) + image_tag('/assets/spinner.gif', :id => 'test_connection_indicator', :class => 'hide').html_safe }) %>
|
10
|
+
|
11
|
+
<% hypervisors = f.object.hypervisors rescue [] %>
|
12
|
+
<%= selectable_f(f, :hypervisor, hypervisors.map { |h| [h, h] }, {}, {:label => _('Hypervisor'), :disabled => hypervisors.empty?,
|
13
|
+
:help_inline => link_to_function(hypervisors.empty? ? _("Load Hypervisors") : _("Test Connection"), "testConnection(this)",
|
14
|
+
:class => "btn + #{hypervisors.empty? ? "btn-default" : "btn-success"}",
|
15
|
+
:'data-url' => test_connection_compute_resources_path) + image_tag('/assets/spinner.gif', :id => 'test_connection_indicator', :class => 'hide').html_safe }) %>
|