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.
@@ -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/>.
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ begin
4
+ require 'bundler/setup'
5
+ rescue LoadError
6
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
7
+ end
@@ -0,0 +1,15 @@
1
+ module FogExtensions
2
+ module Cloudstack
3
+ module Flavor
4
+ extend ActiveSupport::Concern
5
+
6
+ def to_label
7
+ "#{id} - #{name}"
8
+ end
9
+
10
+ def to_s
11
+ name
12
+ end
13
+ end
14
+ end
15
+ end
@@ -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 }) %>