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 +4 -4
- data/README.md +121 -116
- data/kitchen-cloudstack.gemspec +30 -30
- data/lib/kitchen/driver/cloudstack.rb +466 -461
- data/lib/kitchen/driver/cloudstack_version.rb +26 -26
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a8a29963edd6f603cfbd1ce31bcba93e2e10c4245b40a167d80cbffbe8bd0673
|
|
4
|
+
data.tar.gz: 451194fffeb3387191e0c526108f039ba1ef34a1531ca07d0016bd5598196aab
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
* `
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
[license]
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
[
|
|
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/
|
data/kitchen-cloudstack.gemspec
CHANGED
|
@@ -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.
|
|
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['
|
|
61
|
-
options['
|
|
62
|
-
options['
|
|
63
|
-
options[
|
|
64
|
-
|
|
65
|
-
options =
|
|
66
|
-
|
|
67
|
-
options[
|
|
68
|
-
options[:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if config[:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
#
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
debug("
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
debug("
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
'
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
'
|
|
374
|
-
'
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
end
|
|
431
|
-
|
|
432
|
-
def
|
|
433
|
-
compute.
|
|
434
|
-
.select{|e| e['id'] ==
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
.fetch('
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|