kitchen-cloudstack 0.23.3 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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