kitchen-cloudstack 0.23.3 → 0.24.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bb8a5089778d6b96cc77685f6abb732f03b8fc538e02f86b4e36d2f22521ee2d
4
- data.tar.gz: 793147c99f63a988659d5cc492ebc133a7fe4cf489db2d9f9d5a8b098da835d2
3
+ metadata.gz: a8a29963edd6f603cfbd1ce31bcba93e2e10c4245b40a167d80cbffbe8bd0673
4
+ data.tar.gz: 451194fffeb3387191e0c526108f039ba1ef34a1531ca07d0016bd5598196aab
5
5
  SHA512:
6
- metadata.gz: d7d98cd0424152196c9600380c5cfb657959eefd548a919103442f26d022f1ab2824038d460d0ca3cfc0b443671e27a21fa88e51cd8f5fcf0a654b72f3f0b81d
7
- data.tar.gz: aa32c1c00c61cbe0e9809e634687f817d88354360e493e36cbf88f200583f110583516039c76a79b101d9925a73b66b160c97211b0b16c421edbce27d45a621a
6
+ metadata.gz: 9a99208bdcc648135777a5de9a65dddc2df67f95e78c73fc4a57bf92d7654e2ca2808c6ebc2126fdf7ecc1829ba90b763dc08060d9d39d462c98bacc735e17d9
7
+ data.tar.gz: 36fbd486a5c91f8820ea687b91bce2952809167799e84898cd4b11734bbd26df23566bf778ad2d21a2371a8d90697aaeee97c975972be6dfde365b64a87cb937
data/README.md CHANGED
@@ -1,116 +1,121 @@
1
- # <a name="title"></a> Kitchen::CloudStack
2
-
3
- A Test Kitchen Driver for Apache CloudStack / Citrix CloudPlatform.
4
-
5
- ## <a name="requirements"></a> Requirements
6
-
7
- This Gem only requires FOG of a version greater than 1.3.1. However, as most of your knife plugins will be using newer
8
- versions of FOG, that shouldn't be an issue.
9
-
10
- ## <a name="installation"></a> Installation and Setup
11
-
12
- Please read the [Driver usage][driver_usage] page for more details.
13
-
14
- ## <a name="config"></a> Configuration
15
-
16
- Provide, at a minimum, the required driver options in your `.kitchen.yml` file:
17
-
18
- driver_plugin: cloudstack
19
- driver_config:
20
- cloudstack_api_key: [YOUR CLOUDSTACK API KEY]
21
- cloudstack_secret_key: [YOUR CLOUDSTACK SECRET KEY]
22
- cloudstack_api_url: [YOUR CLOUDSTACK API URL]
23
- require_chef_omnibus: latest (if you'll be using Chef)
24
- OPTIONAL
25
- cloudstack_expunge: [TRUE/FALSE] # Whether or not you want the instance to be expunged, default false.
26
- cloudstack_sync_time: [NUMBER OF SECONDS TO WAIT FOR CLOUD-SET-GUEST-PASSWORD/SSHKEY]
27
- keypair_search_directory: [PATH TO DIRECTORY (other than ~, ., and ~/.ssh) WITH KEYPAIR PEM FILE]
28
- cloudstack_project_id: [PROJECT_ID] # To deploy VMs into project.
29
- cloudstack_vm_public_ip: [PUBLIC_IP] # In case you use advanced networking and do static NAT manually.
30
- associate_public_ip: [TRUE/FALSE] # If you want kitchen to automatically associate a public IP, default false.
31
- cloudstack_create_firewall_rule: [TRUE/FALSE] # If you want Kitchen to automatically create firewall rule for public IP to reach SSH (port 22)
32
- cloudstack_userdata: "#cloud-config\npackages:\n - htop\n" # double quote required.
33
-
34
- Then to specify different OS templates,
35
-
36
- platforms:
37
- cloudstack_template_id: [INSTANCE TEMPLATE ID]
38
- cloudstack_serviceoffering_id: [INSTANCE SERVICE OFFERING ID]
39
- cloudstack_zone_id: [INSTANCE ZONE ID]
40
- OPTIONAL
41
- cloudstack_network_id: [NETWORK ID FOR ISOLATED OR VPC NETWORKS]
42
- cloudstack_security_group_id: [SECURITY GROUP ID FOR SHARED NETWORKS]
43
- cloudstack_diskoffering_id: [INSTANCE DISK OFFERING ID]
44
- cloudstack_ssh_keypair_name: [SSH KEY NAME]
45
- cloudstack_sync_time: [NUMBER OF SECONDS TO WAIT FOR CLOUD-SET-GUEST-PASSWORD/SSHKEY]
46
- To use the CloudStack public key provider, you need to have the .PEM file located in the same directory as
47
- your .kitchen.yml file, your home directory (~), your .ssh directory (~/.ssh/), or specify a directory (without any
48
- trailing slahses) as your "keypair_search_directory" and the file be named the same as the Keypair on CloudStack
49
- suffixed with .pem (e.g. the Keypair named "TestKey" should be located in one of the searched directories and named
50
- "TestKey.pem").
51
- This PEM file should be the PRIVATE key, not the PUBLIC key.
52
-
53
- By default, a unique server name will be generated and the randomly generated password will be used, though that
54
- behavior can be overridden with additional options (e.g., to specify a SSH private key):
55
-
56
- name: [A UNIQUE SERVER NAME]
57
- public_key_path: [PATH TO YOUR SSH PUBLIC KEY]
58
- username: [SSH USER]
59
- port: [SSH PORT]
60
-
61
- host_name setting is useful if you are facing ENAMETOOLONG exceptions in the
62
- chef run caused by long generated hostnames)
63
-
64
- host_name: [A UNIQUE HOST NAME]
65
-
66
- Only disable SSL cert validation if you absolutely know what you are doing,
67
- but are stuck with an CloudStack deployment without valid SSL certs.
68
-
69
- disable_ssl_validation: true
70
-
71
- ### <a name="config-require-chef-omnibus"></a> require\_chef\_omnibus
72
-
73
- Determines whether or not a Chef [Omnibus package][chef_omnibus_dl] will be
74
- installed. There are several different behaviors available:
75
-
76
- * `true` - the latest release will be installed. Subsequent converges
77
- will skip re-installing if chef is present.
78
- * `latest` - the latest release will be installed. Subsequent converges
79
- will always re-install even if chef is present.
80
- * `<VERSION_STRING>` (ex: `10.24.0`) - the desired version string will
81
- be passed the the install.sh script. Subsequent converges will skip if
82
- the installed version and the desired version match.
83
- * `false` or `nil` - no chef is installed.
84
-
85
- The default value is unset, or `nil`.
86
-
87
- ## <a name="development"></a> Development
88
-
89
- * Source hosted at [GitHub][repo]
90
- * Report issues/questions/feature requests on [GitHub Issues][issues]
91
-
92
- Pull requests are very welcome! Make sure your patches are well tested.
93
- Ideally create a topic branch for every separate change you make. For
94
- example:
95
-
96
- 1. Fork the repo
97
- 2. Create your feature branch (`git checkout -b my-new-feature`)
98
- 3. Commit your changes (`git commit -am 'Added some feature'`)
99
- 4. Push to the branch (`git push origin my-new-feature`)
100
- 5. Create new Pull Request
101
-
102
- ## <a name="authors"></a> Authors
103
-
104
- Created and maintained by [Jeff Moody][author] (<fifthecho@gmail.com>)
105
-
106
- ## <a name="license"></a> License
107
-
108
- Apache 2.0 (see [LICENSE][license])
109
-
110
-
111
- [author]: https://github.com/fifthecho
112
- [issues]: https://github.com/test-kitchen/kitchen-cloudstack/issues
113
- [license]: https://github.com/test-kitchen/kitchen-cloudstack/blob/master/LICENSE
114
- [repo]: https://github.com/test-kitchen/kitchen-cloudstack
115
- [driver_usage]: http://docs.kitchen-ci.org/drivers/usage
116
- [chef_omnibus_dl]: http://getchef.com/chef/install/
1
+ # <a name="title"></a> Kitchen::CloudStack
2
+
3
+ A Test Kitchen Driver for Apache CloudStack / Citrix CloudPlatform.
4
+
5
+ ## <a name="requirements"></a> Requirements
6
+
7
+ This Gem only requires FOG of a version greater than 1.3.1. However, as most of your knife plugins will be using newer
8
+ versions of FOG, that shouldn't be an issue.
9
+
10
+ ## <a name="installation"></a> Installation and Setup
11
+
12
+ Please read the [Driver usage][driver_usage] page for more details.
13
+
14
+ ## <a name="config"></a> Configuration
15
+
16
+ Provide, at a minimum, the required driver options in your `.kitchen.yml` file:
17
+
18
+ driver_plugin: cloudstack
19
+ driver_config:
20
+ cloudstack_api_key: [YOUR CLOUDSTACK API KEY]
21
+ cloudstack_secret_key: [YOUR CLOUDSTACK SECRET KEY]
22
+ cloudstack_api_url: [YOUR CLOUDSTACK API URL]
23
+ require_chef_omnibus: latest (if you'll be using Chef)
24
+ OPTIONAL
25
+ cloudstack_expunge: [TRUE/FALSE] # Whether or not you want the instance to be expunged, default false.
26
+ cloudstack_sync_time: [NUMBER OF SECONDS TO WAIT FOR CLOUD-SET-GUEST-PASSWORD/SSHKEY]
27
+ keypair_search_directory: [PATH TO DIRECTORY (other than ~, ., and ~/.ssh) WITH KEYPAIR PEM FILE]
28
+ cloudstack_project_id: [PROJECT_ID] # To deploy VMs into project.
29
+ cloudstack_vm_public_ip: [PUBLIC_IP] # In case you use advanced networking and do static NAT manually.
30
+ associate_public_ip: [TRUE/FALSE] # If you want kitchen to automatically associate a public IP, default false.
31
+ cloudstack_create_firewall_rule: [TRUE/FALSE] # If you want Kitchen to automatically create firewall rule for public IP to reach SSH (port 22)
32
+ cloudstack_userdata: "#cloud-config\npackages:\n - htop\n" # double quote required.
33
+
34
+ Then to specify different OS templates,
35
+
36
+ platforms:
37
+ cloudstack_template_id: [INSTANCE TEMPLATE ID]
38
+ cloudstack_serviceoffering_id: [INSTANCE SERVICE OFFERING ID]
39
+ cloudstack_zone_id: [INSTANCE ZONE ID]
40
+ OPTIONAL
41
+ cloudstack_network_id: [NETWORK ID FOR ISOLATED OR VPC NETWORKS]
42
+ cloudstack_security_group_id: [SECURITY GROUP ID FOR SHARED NETWORKS]
43
+ cloudstack_affinity_group_id: [AFFINITY GROUP ID FOR DEDICATED CLUSTER]
44
+ cloudstack_serviceoffering_cpu: [THE NUMBER OF CPU FOR A SERVICE OFFERING THAT DOES NOT SPECIFY CPU]
45
+ cloudstack_serviceoffering_cpuspeed: [THE SPEED OF EACH CPU FOR A SERVICE OFFERING THAT DOES NOT SPECIFY CPU]
46
+ cloudstack_serviceoffering_memory: [THE AMOUNT OF MEMORY IN MB FOR A SERVICE OFFERING THAT DOES NOT SPECIFY MEMORY]
47
+ cloudstack_diskoffering_id: [INSTANCE DISK OFFERING ID]
48
+ cloudstack_diskoffering_size: [INSTANCE DISK OFFERING SIZE IN GB]
49
+ cloudstack_ssh_keypair_name: [SSH KEY NAME]
50
+ cloudstack_sync_time: [NUMBER OF SECONDS TO WAIT FOR CLOUD-SET-GUEST-PASSWORD/SSHKEY]
51
+ To use the CloudStack public key provider, you need to have the .PEM file located in the same directory as
52
+ your .kitchen.yml file, your home directory (\~), your .ssh directory (\~/.ssh/), or specify a directory (without any
53
+ trailing slahses) as your "keypair_search_directory" and the file be named the same as the Keypair on CloudStack
54
+ suffixed with .pem (e.g. the Keypair named "TestKey" should be located in one of the searched directories and named
55
+ "TestKey.pem").
56
+ This PEM file should be the PRIVATE key, not the PUBLIC key.
57
+
58
+ By default, a unique server name will be generated and the randomly generated password will be used, though that
59
+ behavior can be overridden with additional options (e.g., to specify a SSH private key):
60
+
61
+ name: [A UNIQUE SERVER NAME]
62
+ public_key_path: [PATH TO YOUR SSH PUBLIC KEY]
63
+ username: [SSH USER]
64
+ port: [SSH PORT]
65
+
66
+ host_name setting is useful if you are facing ENAMETOOLONG exceptions in the
67
+ chef run caused by long generated hostnames)
68
+
69
+ host_name: [A UNIQUE HOST NAME]
70
+
71
+ Only disable SSL cert validation if you absolutely know what you are doing,
72
+ but are stuck with an CloudStack deployment without valid SSL certs.
73
+
74
+ disable_ssl_validation: true
75
+
76
+ ### <a name="config-require-chef-omnibus"></a> require\_chef\_omnibus
77
+
78
+ Determines whether or not a Chef [Omnibus package][chef_omnibus_dl] will be
79
+ installed. There are several different behaviors available:
80
+
81
+ * `true` - the latest release will be installed. Subsequent converges
82
+ will skip re-installing if chef is present.
83
+ * `latest` - the latest release will be installed. Subsequent converges
84
+ will always re-install even if chef is present.
85
+ * `<VERSION_STRING>` (ex: `10.24.0`) - the desired version string will
86
+ be passed the the install.sh script. Subsequent converges will skip if
87
+ the installed version and the desired version match.
88
+ * `false` or `nil` - no chef is installed.
89
+
90
+ The default value is unset, or `nil`.
91
+
92
+ ## <a name="development"></a> Development
93
+
94
+ * Source hosted at [GitHub][repo]
95
+ * Report issues/questions/feature requests on [GitHub Issues][issues]
96
+
97
+ Pull requests are very welcome! Make sure your patches are well tested.
98
+ Ideally create a topic branch for every separate change you make. For
99
+ example:
100
+
101
+ 1. Fork the repo
102
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
103
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
104
+ 4. Push to the branch (`git push origin my-new-feature`)
105
+ 5. Create new Pull Request
106
+
107
+ ## <a name="authors"></a> Authors
108
+
109
+ Created and maintained by [Jeff Moody][author] (<fifthecho@gmail.com>)
110
+
111
+ ## <a name="license"></a> License
112
+
113
+ Apache 2.0 (see [LICENSE][license])
114
+
115
+
116
+ [author]: https://github.com/fifthecho
117
+ [issues]: https://github.com/test-kitchen/kitchen-cloudstack/issues
118
+ [license]: https://github.com/test-kitchen/kitchen-cloudstack/blob/master/LICENSE
119
+ [repo]: https://github.com/test-kitchen/kitchen-cloudstack
120
+ [driver_usage]: http://docs.kitchen-ci.org/drivers/usage
121
+ [chef_omnibus_dl]: http://getchef.com/chef/install/
@@ -1,30 +1,30 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'kitchen/driver/cloudstack_version'
5
-
6
- Gem::Specification.new do |spec|
7
- spec.name = 'kitchen-cloudstack'
8
- spec.version = Kitchen::Driver::CLOUDSTACK_VERSION
9
- spec.authors = ['Jeff Moody']
10
- spec.email = ['fifthecho@gmail.com']
11
- spec.description = %q{A Test Kitchen Driver for Apache CloudStack}
12
- spec.summary = %q{Provides an interface for Test Kitchen to be able to run jobs against an Apache CloudStack cloud.}
13
- spec.homepage = 'https://github.com/test-kitchen/kitchen-cloudstack'
14
- spec.license = 'Apache-2.0'
15
-
16
- spec.files = `git ls-files`.split($/)
17
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
- spec.require_paths = ['lib']
19
-
20
- spec.add_dependency 'test-kitchen', '>= 1.0.0', "< 3"
21
- spec.add_dependency 'fog', '~> 1.23'
22
-
23
- spec.add_development_dependency 'bundler'
24
- spec.add_development_dependency 'rake'
25
-
26
- spec.add_development_dependency 'cane', '~> 2'
27
- spec.add_development_dependency 'tailor', '~> 1'
28
- spec.add_development_dependency 'countloc'
29
- spec.add_development_dependency 'pry'
30
- end
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'kitchen/driver/cloudstack_version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'kitchen-cloudstack'
8
+ spec.version = Kitchen::Driver::CLOUDSTACK_VERSION
9
+ spec.authors = ['Jeff Moody']
10
+ spec.email = ['fifthecho@gmail.com']
11
+ spec.description = %q{A Test Kitchen Driver for Apache CloudStack}
12
+ spec.summary = %q{Provides an interface for Test Kitchen to be able to run jobs against an Apache CloudStack cloud.}
13
+ spec.homepage = 'https://github.com/test-kitchen/kitchen-cloudstack'
14
+ spec.license = 'Apache-2.0'
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ['lib']
19
+
20
+ spec.add_dependency 'test-kitchen', '>= 1.0.0', "< 3"
21
+ spec.add_dependency 'fog-cloudstack', '~> 0.1.0'
22
+
23
+ spec.add_development_dependency 'bundler'
24
+ spec.add_development_dependency 'rake'
25
+
26
+ spec.add_development_dependency 'cane', '~> 2'
27
+ spec.add_development_dependency 'tailor', '~> 1'
28
+ spec.add_development_dependency 'countloc'
29
+ spec.add_development_dependency 'pry'
30
+ end
@@ -1,461 +1,466 @@
1
- # -*- encoding: utf-8 -*-
2
- #
3
- # Author:: Jeff Moody (<fifthecho@gmail.com>)
4
- #
5
- # Copyright (C) 2013, Jeff Moody
6
- #
7
- # Licensed under the Apache License, Version 2.0 (the "License");
8
- # you may not use this file except in compliance with the License.
9
- # You may obtain a copy of the License at
10
- #
11
- # http://www.apache.org/licenses/LICENSE-2.0
12
- #
13
- # Unless required by applicable law or agreed to in writing, software
14
- # distributed under the License is distributed on an "AS IS" BASIS,
15
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
- # See the License for the specific language governing permissions and
17
- # limitations under the License.
18
-
19
- require 'benchmark'
20
- require 'kitchen'
21
- require 'fog'
22
- require 'socket'
23
- require 'openssl'
24
- require 'base64'
25
-
26
- module Kitchen
27
- module Driver
28
- # Cloudstack driver for Kitchen.
29
- #
30
- # @author Jeff Moody <fifthecho@gmail.com>
31
- class Cloudstack < Kitchen::Driver::SSHBase
32
- default_config :name, nil
33
- default_config :username, 'root'
34
- default_config :port, '22'
35
- default_config :password, nil
36
- default_config :cloudstack_create_firewall_rule, false
37
-
38
- def compute
39
- cloudstack_uri = URI.parse(config[:cloudstack_api_url])
40
- connection = Fog::Compute.new(
41
- :provider => :cloudstack,
42
- :cloudstack_api_key => config[:cloudstack_api_key],
43
- :cloudstack_secret_access_key => config[:cloudstack_secret_key],
44
- :cloudstack_host => cloudstack_uri.host,
45
- :cloudstack_port => cloudstack_uri.port,
46
- :cloudstack_path => cloudstack_uri.path,
47
- :cloudstack_project_id => config[:cloudstack_project_id],
48
- :cloudstack_scheme => cloudstack_uri.scheme
49
- )
50
- end
51
-
52
- def create_server
53
- options = {}
54
-
55
- config[:server_name] ||= generate_name(instance.name)
56
-
57
- options['displayname'] = config[:server_name]
58
- options['networkids'] = config[:cloudstack_network_id]
59
- options['securitygroupids'] = config[:cloudstack_security_group_id]
60
- options['keypair'] = config[:cloudstack_ssh_keypair_name]
61
- options['diskofferingid'] = config[:cloudstack_diskoffering_id]
62
- options['name'] = config[:host_name]
63
- options[:userdata] = convert_userdata(config[:cloudstack_userdata]) if config[:cloudstack_userdata]
64
-
65
- options = sanitize(options)
66
-
67
- options[:templateid] = config[:cloudstack_template_id]
68
- options[:serviceofferingid] = config[:cloudstack_serviceoffering_id]
69
- options[:zoneid] = config[:cloudstack_zone_id]
70
-
71
- debug(options)
72
- compute.deploy_virtual_machine(options)
73
- end
74
-
75
- def create(state)
76
- if not config[:name]
77
- # Generate what should be a unique server name
78
- config[:name] = "#{instance.name}-#{Etc.getlogin}-" +
79
- "#{Socket.gethostname}-#{Array.new(8){rand(36).to_s(36)}.join}"
80
- end
81
- if config[:disable_ssl_validation]
82
- require 'excon'
83
- Excon.defaults[:ssl_verify_peer] = false
84
- end
85
-
86
- server = create_server
87
- debug(server)
88
-
89
- state[:server_id] = server['deployvirtualmachineresponse'].fetch('id')
90
- start_jobid = {
91
- 'jobid' => server['deployvirtualmachineresponse'].fetch('jobid')
92
- }
93
- info("CloudStack instance <#{state[:server_id]}> created.")
94
- debug("Job ID #{start_jobid}")
95
- # Cloning the original job id hash because running the
96
- # query_async_job_result updates the hash to include
97
- # more than just the job id (which I could work around, but I'm lazy).
98
- jobid = start_jobid.clone
99
-
100
- server_start = compute.query_async_job_result(jobid)
101
- # jobstatus of zero is a running job
102
- while server_start['queryasyncjobresultresponse'].fetch('jobstatus').to_i == 0
103
- debug("Job status: #{server_start}")
104
- print ". "
105
- sleep(10)
106
- debug("Running Job ID #{jobid}")
107
- debug("Start Job ID #{start_jobid}")
108
- # We have to reclone on each iteration, as the hash keeps getting updated.
109
- jobid = start_jobid.clone
110
- server_start = compute.query_async_job_result(jobid)
111
- end
112
- debug("Server_Start: #{server_start} \n")
113
-
114
- # jobstatus of 2 is an error response
115
- if server_start['queryasyncjobresultresponse'].fetch('jobstatus').to_i == 2
116
- errortext = server_start['queryasyncjobresultresponse']
117
- .fetch('jobresult')
118
- .fetch('errortext')
119
-
120
- error("ERROR! Job failed with #{errortext}")
121
-
122
- raise ActionFailed, "Could not create server #{errortext}"
123
- end
124
-
125
- # jobstatus of 1 is a succesfully completed async job
126
- if server_start['queryasyncjobresultresponse'].fetch('jobstatus').to_i == 1
127
- server_info = server_start['queryasyncjobresultresponse']['jobresult']['virtualmachine']
128
- debug(server_info)
129
- print "(server ready)"
130
-
131
- keypair = nil
132
- if config[:keypair_search_directory] and File.exist?(
133
- "#{config[:keypair_search_directory]}/#{config[:cloudstack_ssh_keypair_name]}.pem"
134
- )
135
- keypair = "#{config[:keypair_search_directory]}/#{config[:cloudstack_ssh_keypair_name]}.pem"
136
- debug("Keypair being used is #{keypair}")
137
- elsif File.exist?("./#{config[:cloudstack_ssh_keypair_name]}.pem")
138
- keypair = "./#{config[:cloudstack_ssh_keypair_name]}.pem"
139
- debug("Keypair being used is #{keypair}")
140
- elsif File.exist?("#{ENV["HOME"]}/#{config[:cloudstack_ssh_keypair_name]}.pem")
141
- keypair = "#{ENV["HOME"]}/#{config[:cloudstack_ssh_keypair_name]}.pem"
142
- debug("Keypair being used is #{keypair}")
143
- elsif File.exist?("#{ENV["HOME"]}/.ssh/#{config[:cloudstack_ssh_keypair_name]}.pem")
144
- keypair = "#{ENV["HOME"]}/.ssh/#{config[:cloudstack_ssh_keypair_name]}.pem"
145
- debug("Keypair being used is #{keypair}")
146
- elsif (!config[:cloudstack_ssh_keypair_name].nil?)
147
- info("Keypair specified but not found. Using password if enabled.")
148
- end
149
-
150
- if config[:associate_public_ip]
151
- info("Associating public ip...")
152
- state[:hostname] = associate_public_ip(state, server_info)
153
- info("Creating port forward...")
154
- create_port_forward(state, server_info['id'])
155
- else
156
- state[:hostname] = default_public_ip(server_info) unless config[:associate_public_ip]
157
- end
158
-
159
- if keypair
160
- debug("Using keypair: #{keypair}")
161
- info("SSH for #{state[:hostname]} with keypair #{config[:cloudstack_ssh_keypair_name]}.")
162
- ssh_key = File.read(keypair)
163
- if ssh_key.split[0] == "ssh-rsa" or ssh_key.split[0] == "ssh-dsa"
164
- error("SSH key #{keypair} is not a Private Key. Please modify your .kitchen.yml")
165
- end
166
-
167
- wait_for_sshd(state[:hostname], config[:username], {:keys => keypair})
168
- debug("SSH connectivity validated with keypair.")
169
-
170
- ssh = Fog::SSH.new(state[:hostname], config[:username], {:keys => keypair})
171
- debug("Connecting to : #{state[:hostname]} as #{config[:username]} using keypair #{keypair}.")
172
- elsif server_info.fetch('passwordenabled')
173
- password = server_info.fetch('password')
174
- config[:password] = password
175
- # Print out IP and password so you can record it if you want.
176
- info("Password for #{config[:username]} at #{state[:hostname]} is #{password}")
177
-
178
- wait_for_sshd(state[:hostname], config[:username], {:password => password})
179
- debug("SSH connectivity validated with cloudstack-set password.")
180
-
181
- ssh = Fog::SSH.new(state[:hostname], config[:username], {:password => password})
182
- debug("Connecting to : #{state[:hostname]} as #{config[:username]} using password #{password}.")
183
- elsif config[:password]
184
- info("Connecting with user #{config[:username]} with password #{config[:password]}")
185
-
186
- wait_for_sshd(state[:hostname], config[:username], {:password => config[:password]})
187
- debug("SSH connectivity validated with fixed password.")
188
-
189
- ssh = Fog::SSH.new(state[:hostname], config[:username], {:password => config[:password]})
190
- else
191
- info("No keypair specified (or file not found) nor is this a password enabled template. You will have to manually copy your SSH public key to #{state[:hostname]} to use this Kitchen.")
192
- end
193
-
194
- validate_ssh_connectivity(ssh)
195
-
196
- deploy_private_key(ssh)
197
- end
198
- end
199
-
200
- def destroy(state)
201
- return unless state[:server_id]
202
- if config[:associate_public_ip]
203
- delete_port_forward(state)
204
- release_public_ip(state)
205
- end
206
- debug("Destroying #{state[:server_id]}")
207
- server = compute.servers.get(state[:server_id])
208
- expunge =
209
- if !!config[:cloudstack_expunge] == config[:cloudstack_expunge]
210
- config[:cloudstack_expunge]
211
- else
212
- false
213
- end
214
- if server
215
- compute.destroy_virtual_machine(
216
- {
217
- 'id' => state[:server_id],
218
- 'expunge' => expunge
219
- }
220
- )
221
- end
222
- info("CloudStack instance <#{state[:server_id]}> destroyed.")
223
- state.delete(:server_id)
224
- state.delete(:hostname)
225
- end
226
-
227
- def validate_ssh_connectivity(ssh)
228
- rescue Errno::ETIMEDOUT
229
- debug("SSH connection timed out. Retrying.")
230
- sleep 2
231
- false
232
- rescue Errno::EPERM
233
- debug("SSH connection returned error. Retrying.")
234
- false
235
- rescue Errno::ECONNREFUSED
236
- debug("SSH connection returned connection refused. Retrying.")
237
- sleep 2
238
- false
239
- rescue Errno::EHOSTUNREACH
240
- debug("SSH connection returned host unreachable. Retrying.")
241
- sleep 2
242
- false
243
- rescue Errno::ENETUNREACH
244
- debug("SSH connection returned network unreachable. Retrying.")
245
- sleep 30
246
- false
247
- rescue Net::SSH::Disconnect
248
- debug("SSH connection has been disconnected. Retrying.")
249
- sleep 15
250
- false
251
- rescue Net::SSH::AuthenticationFailed
252
- debug("SSH authentication has failed. Password or Keys may not be in place yet. Retrying.")
253
- sleep 15
254
- false
255
- ensure
256
- sync_time = 0
257
- if (config[:cloudstack_sync_time])
258
- sync_time = config[:cloudstack_sync_time]
259
- end
260
- sleep(sync_time)
261
- debug("Connecting to host and running ls")
262
- ssh.run('ls')
263
- end
264
-
265
- def deploy_private_key(ssh)
266
- debug("Deploying user private key to server using connection #{ssh} to guarantee connectivity.")
267
- if File.exist?("#{ENV["HOME"]}/.ssh/id_rsa.pub")
268
- user_public_key = File.read("#{ENV["HOME"]}/.ssh/id_rsa.pub")
269
- elsif File.exist?("#{ENV["HOME"]}/.ssh/id_dsa.pub")
270
- user_public_key = File.read("#{ENV["HOME"]}/.ssh/id_dsa.pub")
271
- else
272
- debug("No public SSH key for user. Skipping.")
273
- end
274
-
275
- if user_public_key
276
- ssh.run([
277
- %{mkdir .ssh},
278
- %{echo "#{user_public_key}" >> ~/.ssh/authorized_keys}
279
- ])
280
- end
281
- end
282
-
283
- def generate_name(base)
284
- # Generate what should be a unique server name
285
- sep = '-'
286
- pieces = [
287
- base,
288
- Etc.getlogin,
289
- Socket.gethostname,
290
- Array.new(8) { rand(36).to_s(36) }.join
291
- ]
292
- until pieces.join(sep).length <= 64 do
293
- if pieces[2] && pieces[2].length > 24
294
- pieces[2] = pieces[2][0..-2]
295
- elsif pieces[1] && pieces[1].length > 16
296
- pieces[1] = pieces[1][0..-2]
297
- elsif pieces[0] && pieces[0].length > 16
298
- pieces[0] = pieces[0][0..-2]
299
- end
300
- end
301
- pieces.join sep
302
- end
303
-
304
- private
305
-
306
- def sanitize(options)
307
- options.reject { |k, v| v.nil? }
308
- end
309
-
310
- def convert_userdata(user_data)
311
- if user_data.match /^(?:[A-Za-z0-9+\/]{4}\n?)*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/
312
- user_data
313
- else
314
- Base64.encode64(user_data)
315
- end
316
- end
317
-
318
- def associate_public_ip(state, server_info)
319
- options = {
320
- 'zoneid' => config[:cloudstack_zone_id],
321
- 'vpcid' => get_vpc_id,
322
- 'networkid' => config[:cloudstack_network_id]
323
- }
324
- res = compute.associate_ip_address(options)
325
- job_status = compute.query_async_job_result(res['associateipaddressresponse']['jobid'])
326
- if job_status['queryasyncjobresultresponse'].fetch('jobstatus').to_i == 1
327
- save_ipaddress_id(state, job_status)
328
- ip_address = get_public_ip(res['associateipaddressresponse']['id'])
329
- else
330
- error(job_status['queryasyncjobresultresponse'].fetch('jobresult'))
331
- end
332
-
333
- if config[:cloudstack_create_firewall_rule]
334
- info("Creating firewall rule for SSH")
335
- # create firewallrule projectid=<project> cidrlist=<0.0.0.0/0 or your source> protocol=tcp startport=0 endport=65535 (or you can restrict to 22 if you want) ipaddressid=<public ip address id>
336
- options = {
337
- 'projectid' => config[:cloudstack_project_id],
338
- 'cidrlist' => '0.0.0.0/0',
339
- 'protocol' => 'tcp',
340
- 'startport' => 22,
341
- 'endport' => 22,
342
- 'ipaddressid' => state[:ipaddressid]
343
- }
344
- res = compute.create_firewall_rule(options)
345
- status = 0
346
- timeout = 10
347
- while status == 0
348
- job_status = compute.query_async_job_result(res['createfirewallruleresponse']['jobid'])
349
- status = job_status['queryasyncjobresultresponse'].fetch('jobstatus').to_i
350
- timeout -= 1
351
- error("Failed to create firewall rule by timeout") if timeout == 0
352
- sleep 1
353
- end
354
-
355
- if job_status['queryasyncjobresultresponse'].fetch('jobstatus').to_i == 1
356
- save_firewall_rule_id(state, job_status)
357
- info('Firewall rule successfully created')
358
- else
359
- error(job_status['queryasyncjobresultresponse'])
360
- end
361
- end
362
-
363
- ip_address
364
- end
365
-
366
- def create_port_forward(state, virtualmachineid)
367
- options = {
368
- 'ipaddressid' => state[:ipaddressid],
369
- 'privateport' => 22,
370
- 'protocol' => "TCP",
371
- 'publicport' => 22,
372
- 'virtualmachineid' => virtualmachineid,
373
- 'networkid' => config[:cloudstack_network_id],
374
- 'openfirewall' => false
375
- }
376
- res = compute.create_port_forwarding_rule(options)
377
- job_status = compute.query_async_job_result(res['createportforwardingruleresponse']['jobid'])
378
- unless job_status['queryasyncjobresultresponse'].fetch('jobstatus').to_i == 0
379
- error("Error creating port forwarding rules")
380
- end
381
- save_forwarding_port_rule_id(state, res['createportforwardingruleresponse']['id'])
382
- end
383
-
384
- def release_public_ip(state)
385
- info("Disassociating public ip...")
386
- begin
387
- res = compute.disassociate_ip_address(state[:ipaddressid])
388
- rescue Fog::Compute::Cloudstack::BadRequest => e
389
- error(e) unless e.to_s.match?(/does not exist/)
390
- else
391
- job_status = compute.query_async_job_result(res['disassociateipaddressresponse']['jobid'])
392
- unless job_status['queryasyncjobresultresponse'].fetch('jobstatus').to_i == 0
393
- error("Error disassociating public ip")
394
- end
395
- end
396
-
397
- if state[:firewall_rule_id]
398
- info("Removing firewall rule '#{state[:firewall_rule_id]}'")
399
-
400
- begin
401
- res = compute.delete_firewall_rule(state[:firewall_rule_id])
402
- rescue Fog::Compute::Cloudstack::BadRequest => e
403
- error(e) unless e.to_s.match?(/does not exist/)
404
- else
405
- job_status = compute.query_async_job_result(res['deletefirewallruleresponse']['jobid'])
406
- unless job_status['queryasyncjobresultresponse'].fetch('jobstatus').to_i == 0
407
- error("Error removing firewall rule '#{state[:firewall_rule_id]}'")
408
- end
409
- end
410
- end
411
- end
412
-
413
- def delete_port_forward(state)
414
- info("Deleting port forwarding rules...")
415
- begin
416
- res = compute.delete_port_forwarding_rule(state[:forwardingruleid])
417
- rescue Fog::Compute::Cloudstack::BadRequest => e
418
- error(e) unless e.to_s.match?(/does not exist/)
419
- else
420
- job_status = compute.query_async_job_result(res['deleteportforwardingruleresponse']['jobid'])
421
- unless job_status['queryasyncjobresultresponse'].fetch('jobstatus').to_i == 0
422
- error("Error deleting port forwarding rules")
423
- end
424
- end
425
- end
426
-
427
- def get_vpc_id
428
- compute.list_networks['listnetworksresponse']['network']
429
- .select{|e| e['id'] == config[:cloudstack_network_id]}.first['vpcid']
430
- end
431
-
432
- def get_public_ip(public_ip_uuid)
433
- compute.list_public_ip_addresses['listpublicipaddressesresponse']['publicipaddress']
434
- .select{|e| e['id'] == public_ip_uuid}
435
- .first['ipaddress']
436
- end
437
-
438
- def save_ipaddress_id(state, job_status)
439
- state[:ipaddressid] = job_status['queryasyncjobresultresponse']
440
- .fetch('jobresult')
441
- .fetch('ipaddress')
442
- .fetch('id')
443
- end
444
-
445
- def save_firewall_rule_id(state, job_status)
446
- state[:firewall_rule_id] = job_status['queryasyncjobresultresponse']
447
- .fetch('jobresult')
448
- .fetch('firewallrule')
449
- .fetch('id')
450
- end
451
-
452
- def save_forwarding_port_rule_id(state, uuid)
453
- state[:forwardingruleid] = uuid
454
- end
455
-
456
- def default_public_ip(server_info)
457
- config[:cloudstack_vm_public_ip] || server_info.fetch('nic').first.fetch('ipaddress')
458
- end
459
- end
460
- end
461
- end
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # Author:: Jeff Moody (<fifthecho@gmail.com>)
4
+ #
5
+ # Copyright (C) 2013, Jeff Moody
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+
19
+ require 'benchmark'
20
+ require 'kitchen'
21
+ require 'fog/cloudstack'
22
+ require 'socket'
23
+ require 'openssl'
24
+ require 'base64'
25
+
26
+ module Kitchen
27
+ module Driver
28
+ # Cloudstack driver for Kitchen.
29
+ #
30
+ # @author Jeff Moody <fifthecho@gmail.com>
31
+ class Cloudstack < Kitchen::Driver::SSHBase
32
+ default_config :name, nil
33
+ default_config :username, 'root'
34
+ default_config :port, '22'
35
+ default_config :password, nil
36
+ default_config :cloudstack_create_firewall_rule, false
37
+
38
+ def compute
39
+ cloudstack_uri = URI.parse(config[:cloudstack_api_url])
40
+ connection = Fog::Compute.new(
41
+ :provider => :cloudstack,
42
+ :cloudstack_api_key => config[:cloudstack_api_key],
43
+ :cloudstack_secret_access_key => config[:cloudstack_secret_key],
44
+ :cloudstack_host => cloudstack_uri.host,
45
+ :cloudstack_port => cloudstack_uri.port,
46
+ :cloudstack_path => cloudstack_uri.path,
47
+ :cloudstack_project_id => config[:cloudstack_project_id],
48
+ :cloudstack_scheme => cloudstack_uri.scheme
49
+ )
50
+ end
51
+
52
+ def create_server
53
+ options = {}
54
+
55
+ config[:server_name] ||= generate_name(instance.name)
56
+
57
+ options['displayname'] = config[:server_name]
58
+ options['networkids'] = config[:cloudstack_network_id]
59
+ options['securitygroupids'] = config[:cloudstack_security_group_id]
60
+ options['affinitygroupids'] = config[:cloudstack_affinity_group_id]
61
+ options['keypair'] = config[:cloudstack_ssh_keypair_name]
62
+ options['diskofferingid'] = config[:cloudstack_diskoffering_id]
63
+ options['size'] = config[:cloudstack_diskoffering_size]
64
+ options['name'] = config[:host_name]
65
+ options['details[0].cpuNumber'] = config[:cloudstack_serviceoffering_cpu]
66
+ options['details[0].cpuSpeed'] = config[:cloudstack_serviceoffering_cpuspeed]
67
+ options['details[0].memory'] = config[:cloudstack_serviceoffering_memory]
68
+ options[:userdata] = convert_userdata(config[:cloudstack_userdata]) if config[:cloudstack_userdata]
69
+
70
+ options = sanitize(options)
71
+
72
+ options[:templateid] = config[:cloudstack_template_id]
73
+ options[:serviceofferingid] = config[:cloudstack_serviceoffering_id]
74
+ options[:zoneid] = config[:cloudstack_zone_id]
75
+
76
+ debug(options)
77
+ compute.deploy_virtual_machine(options)
78
+ end
79
+
80
+ def create(state)
81
+ if not config[:name]
82
+ # Generate what should be a unique server name
83
+ config[:name] = "#{instance.name}-#{Etc.getlogin}-" +
84
+ "#{Socket.gethostname}-#{Array.new(8){rand(36).to_s(36)}.join}"
85
+ end
86
+ if config[:disable_ssl_validation]
87
+ require 'excon'
88
+ Excon.defaults[:ssl_verify_peer] = false
89
+ end
90
+
91
+ server = create_server
92
+ debug(server)
93
+
94
+ state[:server_id] = server['deployvirtualmachineresponse'].fetch('id')
95
+ start_jobid = {
96
+ 'jobid' => server['deployvirtualmachineresponse'].fetch('jobid')
97
+ }
98
+ info("CloudStack instance <#{state[:server_id]}> created.")
99
+ debug("Job ID #{start_jobid}")
100
+ # Cloning the original job id hash because running the
101
+ # query_async_job_result updates the hash to include
102
+ # more than just the job id (which I could work around, but I'm lazy).
103
+ jobid = start_jobid.clone
104
+
105
+ server_start = compute.query_async_job_result(jobid)
106
+ # jobstatus of zero is a running job
107
+ while server_start['queryasyncjobresultresponse'].fetch('jobstatus').to_i == 0
108
+ debug("Job status: #{server_start}")
109
+ print ". "
110
+ sleep(10)
111
+ debug("Running Job ID #{jobid}")
112
+ debug("Start Job ID #{start_jobid}")
113
+ # We have to reclone on each iteration, as the hash keeps getting updated.
114
+ jobid = start_jobid.clone
115
+ server_start = compute.query_async_job_result(jobid)
116
+ end
117
+ debug("Server_Start: #{server_start} \n")
118
+
119
+ # jobstatus of 2 is an error response
120
+ if server_start['queryasyncjobresultresponse'].fetch('jobstatus').to_i == 2
121
+ errortext = server_start['queryasyncjobresultresponse']
122
+ .fetch('jobresult')
123
+ .fetch('errortext')
124
+
125
+ error("ERROR! Job failed with #{errortext}")
126
+
127
+ raise ActionFailed, "Could not create server #{errortext}"
128
+ end
129
+
130
+ # jobstatus of 1 is a succesfully completed async job
131
+ if server_start['queryasyncjobresultresponse'].fetch('jobstatus').to_i == 1
132
+ server_info = server_start['queryasyncjobresultresponse']['jobresult']['virtualmachine']
133
+ debug(server_info)
134
+ print "(server ready)"
135
+
136
+ keypair = nil
137
+ if config[:keypair_search_directory] and File.exist?(
138
+ "#{config[:keypair_search_directory]}/#{config[:cloudstack_ssh_keypair_name]}.pem"
139
+ )
140
+ keypair = "#{config[:keypair_search_directory]}/#{config[:cloudstack_ssh_keypair_name]}.pem"
141
+ debug("Keypair being used is #{keypair}")
142
+ elsif File.exist?("./#{config[:cloudstack_ssh_keypair_name]}.pem")
143
+ keypair = "./#{config[:cloudstack_ssh_keypair_name]}.pem"
144
+ debug("Keypair being used is #{keypair}")
145
+ elsif File.exist?("#{ENV["HOME"]}/#{config[:cloudstack_ssh_keypair_name]}.pem")
146
+ keypair = "#{ENV["HOME"]}/#{config[:cloudstack_ssh_keypair_name]}.pem"
147
+ debug("Keypair being used is #{keypair}")
148
+ elsif File.exist?("#{ENV["HOME"]}/.ssh/#{config[:cloudstack_ssh_keypair_name]}.pem")
149
+ keypair = "#{ENV["HOME"]}/.ssh/#{config[:cloudstack_ssh_keypair_name]}.pem"
150
+ debug("Keypair being used is #{keypair}")
151
+ elsif (!config[:cloudstack_ssh_keypair_name].nil?)
152
+ info("Keypair specified but not found. Using password if enabled.")
153
+ end
154
+
155
+ if config[:associate_public_ip]
156
+ info("Associating public ip...")
157
+ state[:hostname] = associate_public_ip(state, server_info)
158
+ info("Creating port forward...")
159
+ create_port_forward(state, server_info['id'])
160
+ else
161
+ state[:hostname] = default_public_ip(server_info) unless config[:associate_public_ip]
162
+ end
163
+
164
+ if keypair
165
+ debug("Using keypair: #{keypair}")
166
+ info("SSH for #{state[:hostname]} with keypair #{config[:cloudstack_ssh_keypair_name]}.")
167
+ ssh_key = File.read(keypair)
168
+ if ssh_key.split[0] == "ssh-rsa" or ssh_key.split[0] == "ssh-dsa"
169
+ error("SSH key #{keypair} is not a Private Key. Please modify your .kitchen.yml")
170
+ end
171
+
172
+ wait_for_sshd(state[:hostname], config[:username], {:keys => keypair})
173
+ debug("SSH connectivity validated with keypair.")
174
+
175
+ ssh = Fog::SSH.new(state[:hostname], config[:username], {:keys => keypair})
176
+ debug("Connecting to : #{state[:hostname]} as #{config[:username]} using keypair #{keypair}.")
177
+ elsif server_info.fetch('passwordenabled')
178
+ password = server_info.fetch('password')
179
+ config[:password] = password
180
+ # Print out IP and password so you can record it if you want.
181
+ info("Password for #{config[:username]} at #{state[:hostname]} is #{password}")
182
+
183
+ wait_for_sshd(state[:hostname], config[:username], {:password => password})
184
+ debug("SSH connectivity validated with cloudstack-set password.")
185
+
186
+ ssh = Fog::SSH.new(state[:hostname], config[:username], {:password => password})
187
+ debug("Connecting to : #{state[:hostname]} as #{config[:username]} using password #{password}.")
188
+ elsif config[:password]
189
+ info("Connecting with user #{config[:username]} with password #{config[:password]}")
190
+
191
+ wait_for_sshd(state[:hostname], config[:username], {:password => config[:password]})
192
+ debug("SSH connectivity validated with fixed password.")
193
+
194
+ ssh = Fog::SSH.new(state[:hostname], config[:username], {:password => config[:password]})
195
+ else
196
+ info("No keypair specified (or file not found) nor is this a password enabled template. You will have to manually copy your SSH public key to #{state[:hostname]} to use this Kitchen.")
197
+ end
198
+
199
+ validate_ssh_connectivity(ssh)
200
+
201
+ deploy_private_key(ssh)
202
+ end
203
+ end
204
+
205
+ def destroy(state)
206
+ return unless state[:server_id]
207
+ if config[:associate_public_ip]
208
+ delete_port_forward(state)
209
+ release_public_ip(state)
210
+ end
211
+ debug("Destroying #{state[:server_id]}")
212
+ server = compute.servers.get(state[:server_id])
213
+ expunge =
214
+ if !!config[:cloudstack_expunge] == config[:cloudstack_expunge]
215
+ config[:cloudstack_expunge]
216
+ else
217
+ false
218
+ end
219
+ if server
220
+ compute.destroy_virtual_machine(
221
+ {
222
+ 'id' => state[:server_id],
223
+ 'expunge' => expunge
224
+ }
225
+ )
226
+ end
227
+ info("CloudStack instance <#{state[:server_id]}> destroyed.")
228
+ state.delete(:server_id)
229
+ state.delete(:hostname)
230
+ end
231
+
232
+ def validate_ssh_connectivity(ssh)
233
+ rescue Errno::ETIMEDOUT
234
+ debug("SSH connection timed out. Retrying.")
235
+ sleep 2
236
+ false
237
+ rescue Errno::EPERM
238
+ debug("SSH connection returned error. Retrying.")
239
+ false
240
+ rescue Errno::ECONNREFUSED
241
+ debug("SSH connection returned connection refused. Retrying.")
242
+ sleep 2
243
+ false
244
+ rescue Errno::EHOSTUNREACH
245
+ debug("SSH connection returned host unreachable. Retrying.")
246
+ sleep 2
247
+ false
248
+ rescue Errno::ENETUNREACH
249
+ debug("SSH connection returned network unreachable. Retrying.")
250
+ sleep 30
251
+ false
252
+ rescue Net::SSH::Disconnect
253
+ debug("SSH connection has been disconnected. Retrying.")
254
+ sleep 15
255
+ false
256
+ rescue Net::SSH::AuthenticationFailed
257
+ debug("SSH authentication has failed. Password or Keys may not be in place yet. Retrying.")
258
+ sleep 15
259
+ false
260
+ ensure
261
+ sync_time = 0
262
+ if (config[:cloudstack_sync_time])
263
+ sync_time = config[:cloudstack_sync_time]
264
+ end
265
+ sleep(sync_time)
266
+ debug("Connecting to host and running ls")
267
+ ssh.run('ls')
268
+ end
269
+
270
+ def deploy_private_key(ssh)
271
+ debug("Deploying user private key to server using connection #{ssh} to guarantee connectivity.")
272
+ if File.exist?("#{ENV["HOME"]}/.ssh/id_rsa.pub")
273
+ user_public_key = File.read("#{ENV["HOME"]}/.ssh/id_rsa.pub")
274
+ elsif File.exist?("#{ENV["HOME"]}/.ssh/id_dsa.pub")
275
+ user_public_key = File.read("#{ENV["HOME"]}/.ssh/id_dsa.pub")
276
+ else
277
+ debug("No public SSH key for user. Skipping.")
278
+ end
279
+
280
+ if user_public_key
281
+ ssh.run([
282
+ %{mkdir .ssh},
283
+ %{echo "#{user_public_key}" >> ~/.ssh/authorized_keys}
284
+ ])
285
+ end
286
+ end
287
+
288
+ def generate_name(base)
289
+ # Generate what should be a unique server name
290
+ sep = '-'
291
+ pieces = [
292
+ base,
293
+ Etc.getlogin,
294
+ Socket.gethostname,
295
+ Array.new(8) { rand(36).to_s(36) }.join
296
+ ]
297
+ until pieces.join(sep).length <= 64 do
298
+ if pieces[2] && pieces[2].length > 24
299
+ pieces[2] = pieces[2][0..-2]
300
+ elsif pieces[1] && pieces[1].length > 16
301
+ pieces[1] = pieces[1][0..-2]
302
+ elsif pieces[0] && pieces[0].length > 16
303
+ pieces[0] = pieces[0][0..-2]
304
+ end
305
+ end
306
+ pieces.join sep
307
+ end
308
+
309
+ private
310
+
311
+ def sanitize(options)
312
+ options.reject { |k, v| v.nil? }
313
+ end
314
+
315
+ def convert_userdata(user_data)
316
+ if user_data.match /^(?:[A-Za-z0-9+\/]{4}\n?)*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/
317
+ user_data
318
+ else
319
+ Base64.encode64(user_data)
320
+ end
321
+ end
322
+
323
+ def associate_public_ip(state, server_info)
324
+ options = {
325
+ 'zoneid' => config[:cloudstack_zone_id],
326
+ 'vpcid' => get_vpc_id,
327
+ 'networkid' => config[:cloudstack_network_id]
328
+ }
329
+ res = compute.associate_ip_address(options)
330
+ job_status = compute.query_async_job_result(res['associateipaddressresponse']['jobid'])
331
+ if job_status['queryasyncjobresultresponse'].fetch('jobstatus').to_i == 1
332
+ save_ipaddress_id(state, job_status)
333
+ ip_address = get_public_ip(res['associateipaddressresponse']['id'])
334
+ else
335
+ error(job_status['queryasyncjobresultresponse'].fetch('jobresult'))
336
+ end
337
+
338
+ if config[:cloudstack_create_firewall_rule]
339
+ info("Creating firewall rule for SSH")
340
+ # create firewallrule projectid=<project> cidrlist=<0.0.0.0/0 or your source> protocol=tcp startport=0 endport=65535 (or you can restrict to 22 if you want) ipaddressid=<public ip address id>
341
+ options = {
342
+ 'projectid' => config[:cloudstack_project_id],
343
+ 'cidrlist' => '0.0.0.0/0',
344
+ 'protocol' => 'tcp',
345
+ 'startport' => 22,
346
+ 'endport' => 22,
347
+ 'ipaddressid' => state[:ipaddressid]
348
+ }
349
+ res = compute.create_firewall_rule(options)
350
+ status = 0
351
+ timeout = 10
352
+ while status == 0
353
+ job_status = compute.query_async_job_result(res['createfirewallruleresponse']['jobid'])
354
+ status = job_status['queryasyncjobresultresponse'].fetch('jobstatus').to_i
355
+ timeout -= 1
356
+ error("Failed to create firewall rule by timeout") if timeout == 0
357
+ sleep 1
358
+ end
359
+
360
+ if job_status['queryasyncjobresultresponse'].fetch('jobstatus').to_i == 1
361
+ save_firewall_rule_id(state, job_status)
362
+ info('Firewall rule successfully created')
363
+ else
364
+ error(job_status['queryasyncjobresultresponse'])
365
+ end
366
+ end
367
+
368
+ ip_address
369
+ end
370
+
371
+ def create_port_forward(state, virtualmachineid)
372
+ options = {
373
+ 'ipaddressid' => state[:ipaddressid],
374
+ 'privateport' => 22,
375
+ 'protocol' => "TCP",
376
+ 'publicport' => 22,
377
+ 'virtualmachineid' => virtualmachineid,
378
+ 'networkid' => config[:cloudstack_network_id],
379
+ 'openfirewall' => false
380
+ }
381
+ res = compute.create_port_forwarding_rule(options)
382
+ job_status = compute.query_async_job_result(res['createportforwardingruleresponse']['jobid'])
383
+ unless job_status['queryasyncjobresultresponse'].fetch('jobstatus').to_i == 0
384
+ error("Error creating port forwarding rules")
385
+ end
386
+ save_forwarding_port_rule_id(state, res['createportforwardingruleresponse']['id'])
387
+ end
388
+
389
+ def release_public_ip(state)
390
+ info("Disassociating public ip...")
391
+ begin
392
+ res = compute.disassociate_ip_address(state[:ipaddressid])
393
+ rescue Fog::Compute::Cloudstack::BadRequest => e
394
+ error(e) unless e.to_s.match?(/does not exist/)
395
+ else
396
+ job_status = compute.query_async_job_result(res['disassociateipaddressresponse']['jobid'])
397
+ unless job_status['queryasyncjobresultresponse'].fetch('jobstatus').to_i == 0
398
+ error("Error disassociating public ip")
399
+ end
400
+ end
401
+
402
+ if state[:firewall_rule_id]
403
+ info("Removing firewall rule '#{state[:firewall_rule_id]}'")
404
+
405
+ begin
406
+ res = compute.delete_firewall_rule(state[:firewall_rule_id])
407
+ rescue Fog::Compute::Cloudstack::BadRequest => e
408
+ error(e) unless e.to_s.match?(/does not exist/)
409
+ else
410
+ job_status = compute.query_async_job_result(res['deletefirewallruleresponse']['jobid'])
411
+ unless job_status['queryasyncjobresultresponse'].fetch('jobstatus').to_i == 0
412
+ error("Error removing firewall rule '#{state[:firewall_rule_id]}'")
413
+ end
414
+ end
415
+ end
416
+ end
417
+
418
+ def delete_port_forward(state)
419
+ info("Deleting port forwarding rules...")
420
+ begin
421
+ res = compute.delete_port_forwarding_rule(state[:forwardingruleid])
422
+ rescue Fog::Compute::Cloudstack::BadRequest => e
423
+ error(e) unless e.to_s.match?(/does not exist/)
424
+ else
425
+ job_status = compute.query_async_job_result(res['deleteportforwardingruleresponse']['jobid'])
426
+ unless job_status['queryasyncjobresultresponse'].fetch('jobstatus').to_i == 0
427
+ error("Error deleting port forwarding rules")
428
+ end
429
+ end
430
+ end
431
+
432
+ def get_vpc_id
433
+ compute.list_networks['listnetworksresponse']['network']
434
+ .select{|e| e['id'] == config[:cloudstack_network_id]}.first['vpcid']
435
+ end
436
+
437
+ def get_public_ip(public_ip_uuid)
438
+ compute.list_public_ip_addresses['listpublicipaddressesresponse']['publicipaddress']
439
+ .select{|e| e['id'] == public_ip_uuid}
440
+ .first['ipaddress']
441
+ end
442
+
443
+ def save_ipaddress_id(state, job_status)
444
+ state[:ipaddressid] = job_status['queryasyncjobresultresponse']
445
+ .fetch('jobresult')
446
+ .fetch('ipaddress')
447
+ .fetch('id')
448
+ end
449
+
450
+ def save_firewall_rule_id(state, job_status)
451
+ state[:firewall_rule_id] = job_status['queryasyncjobresultresponse']
452
+ .fetch('jobresult')
453
+ .fetch('firewallrule')
454
+ .fetch('id')
455
+ end
456
+
457
+ def save_forwarding_port_rule_id(state, uuid)
458
+ state[:forwardingruleid] = uuid
459
+ end
460
+
461
+ def default_public_ip(server_info)
462
+ config[:cloudstack_vm_public_ip] || server_info.fetch('nic').first.fetch('ipaddress')
463
+ end
464
+ end
465
+ end
466
+ end