foreman_cpp_cloudstack 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- 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 }) %>
|