kitchen-openstack 6.2.2 → 7.0.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 -2
- data/lib/kitchen/driver/openstack/clouds.rb +195 -0
- data/lib/kitchen/driver/openstack/config.rb +87 -0
- data/lib/kitchen/driver/openstack/helpers.rb +83 -0
- data/lib/kitchen/driver/openstack/networking.rb +129 -0
- data/lib/kitchen/driver/openstack/server_helper.rb +137 -0
- data/lib/kitchen/driver/openstack/volume.rb +3 -3
- data/lib/kitchen/driver/openstack.rb +30 -305
- data/lib/kitchen/driver/openstack_version.rb +3 -3
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 431a4cae8533390ccb721da8645f872bcf4703cf09b3c4fecdbab3b52bb2050a
|
|
4
|
+
data.tar.gz: 3e0718cd558cac16cbd62d206a4a701cb64ee178ee5215fbfd37cd3868191cc6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2e71554edd9c953b862d67a76b05474ceba4ebe5b083051a215301eaad9119484e497be16da6b16096218bdcfc716af6b47921a5823ca3a894227212bfe834dc
|
|
7
|
+
data.tar.gz: aecfb9894b8d57324b94e388d01d56e92d04c92ec811d3e0a0a977930a2f10ec9932e3e8e068dce03c97577a19121d829608c4f06313a8d4a9209028c75257c5
|
data/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Kitchen::OpenStack
|
|
2
2
|
|
|
3
3
|

|
|
4
|
-

|
|
5
5
|
|
|
6
6
|
A Test Kitchen Driver for OpenStack.
|
|
7
7
|
|
|
@@ -11,7 +11,7 @@ Shamelessly copied from [Fletcher Nichol](https://github.com/fnichol)'s awesome
|
|
|
11
11
|
|
|
12
12
|
## Status
|
|
13
13
|
|
|
14
|
-
This software project is
|
|
14
|
+
This software project is actively maintained by the [OSU Open Source Lab](https://osuosl.org/).
|
|
15
15
|
|
|
16
16
|
## Requirements
|
|
17
17
|
|
|
@@ -47,6 +47,125 @@ gem install kitchen-openstack
|
|
|
47
47
|
|
|
48
48
|
See <https://kitchen.ci/docs/drivers/openstack/> for documentation.
|
|
49
49
|
|
|
50
|
+
### Using `clouds.yaml`
|
|
51
|
+
|
|
52
|
+
This driver supports OpenStack's standard
|
|
53
|
+
[`clouds.yaml`](https://docs.openstack.org/python-openstackclient/latest/configuration/index.html)
|
|
54
|
+
client configuration file. This allows you to use the same credentials and
|
|
55
|
+
endpoint configuration that other OpenStack tools (like the `openstack` CLI)
|
|
56
|
+
already use, instead of duplicating them in `kitchen.yml`.
|
|
57
|
+
|
|
58
|
+
The driver searches for `clouds.yaml` in the standard locations:
|
|
59
|
+
|
|
60
|
+
1. `OS_CLIENT_CONFIG_FILE` environment variable (if set)
|
|
61
|
+
2. `clouds_yaml_path` driver config option (if set)
|
|
62
|
+
3. Current directory (`./clouds.yaml`)
|
|
63
|
+
4. `~/.config/openstack/clouds.yaml`
|
|
64
|
+
5. `/etc/openstack/clouds.yaml`
|
|
65
|
+
|
|
66
|
+
The first file found is used. A `secure.yaml` file in the same search
|
|
67
|
+
locations is also loaded and merged, so you can split secrets out of
|
|
68
|
+
`clouds.yaml` following the
|
|
69
|
+
[standard convention](https://docs.openstack.org/openstacksdk/latest/user/config/configuration.html#splitting-secrets).
|
|
70
|
+
|
|
71
|
+
#### Selecting a cloud
|
|
72
|
+
|
|
73
|
+
Specify which cloud entry to use in one of two ways:
|
|
74
|
+
|
|
75
|
+
- Set `openstack_cloud` in `kitchen.yml` (takes precedence)
|
|
76
|
+
- Set the `OS_CLOUD` environment variable
|
|
77
|
+
|
|
78
|
+
#### Example `kitchen.yml`
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
driver:
|
|
82
|
+
name: openstack
|
|
83
|
+
openstack_cloud: mycloud
|
|
84
|
+
image_ref: ubuntu-22.04
|
|
85
|
+
flavor_ref: m1.small
|
|
86
|
+
key_name: my-keypair
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Or, relying entirely on `OS_CLOUD`:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
export OS_CLOUD=mycloud
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```yaml
|
|
96
|
+
driver:
|
|
97
|
+
name: openstack
|
|
98
|
+
image_ref: ubuntu-22.04
|
|
99
|
+
flavor_ref: m1.small
|
|
100
|
+
key_name: my-keypair
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Settings specified in `kitchen.yml` always take precedence over values from
|
|
104
|
+
`clouds.yaml`. For example, you can override just the region:
|
|
105
|
+
|
|
106
|
+
```yaml
|
|
107
|
+
driver:
|
|
108
|
+
name: openstack
|
|
109
|
+
openstack_cloud: mycloud
|
|
110
|
+
openstack_region: RegionTwo
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
#### Using `OS_*` environment variables
|
|
114
|
+
|
|
115
|
+
The driver recognizes the standard OpenStack `OS_*` environment variables
|
|
116
|
+
(e.g. from an `openrc` file). This means you can source your OpenStack
|
|
117
|
+
credentials and use them directly without any extra configuration in
|
|
118
|
+
`kitchen.yml`:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
source openrc.sh
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```yaml
|
|
125
|
+
driver:
|
|
126
|
+
name: openstack
|
|
127
|
+
image_ref: ubuntu-22.04
|
|
128
|
+
flavor_ref: m1.small
|
|
129
|
+
key_name: my-keypair
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The supported environment variables are:
|
|
133
|
+
|
|
134
|
+
| Env var | Maps to |
|
|
135
|
+
|---|---|
|
|
136
|
+
| `OS_AUTH_URL` | `openstack_auth_url` |
|
|
137
|
+
| `OS_USERNAME` | `openstack_username` |
|
|
138
|
+
| `OS_PASSWORD` | `openstack_api_key` |
|
|
139
|
+
| `OS_PROJECT_NAME` | `openstack_project_name` |
|
|
140
|
+
| `OS_PROJECT_ID` | `openstack_project_id` |
|
|
141
|
+
| `OS_USER_DOMAIN_NAME` | `openstack_user_domain` |
|
|
142
|
+
| `OS_USER_DOMAIN_ID` | `openstack_user_domain_id` |
|
|
143
|
+
| `OS_PROJECT_DOMAIN_NAME` | `openstack_project_domain` |
|
|
144
|
+
| `OS_PROJECT_DOMAIN_ID` | `openstack_project_domain_id` |
|
|
145
|
+
| `OS_DOMAIN_ID` | `openstack_domain_id` |
|
|
146
|
+
| `OS_DOMAIN_NAME` | `openstack_domain_name` |
|
|
147
|
+
| `OS_REGION_NAME` | `openstack_region` |
|
|
148
|
+
| `OS_INTERFACE` | `openstack_endpoint_type` |
|
|
149
|
+
| `OS_IDENTITY_API_VERSION` | `openstack_identity_api_version` |
|
|
150
|
+
| `OS_APPLICATION_CREDENTIAL_ID` | `openstack_application_credential_id` |
|
|
151
|
+
| `OS_APPLICATION_CREDENTIAL_SECRET` | `openstack_application_credential_secret` |
|
|
152
|
+
| `OS_CACERT` | `ssl_ca_file` |
|
|
153
|
+
|
|
154
|
+
#### Configuration precedence
|
|
155
|
+
|
|
156
|
+
The driver follows the upstream OpenStack SDK precedence order:
|
|
157
|
+
|
|
158
|
+
1. **`kitchen.yml`** — explicit driver config always wins
|
|
159
|
+
2. **`OS_*` env vars** — override `clouds.yaml` values
|
|
160
|
+
3. **`clouds.yaml`** (merged with `secure.yaml`) — base configuration
|
|
161
|
+
|
|
162
|
+
#### New driver config options
|
|
163
|
+
|
|
164
|
+
| Option | Default | Description |
|
|
165
|
+
|---|---|---|
|
|
166
|
+
| `openstack_cloud` | `nil` | Name of the cloud entry in `clouds.yaml`. Falls back to the `OS_CLOUD` env var. |
|
|
167
|
+
| `clouds_yaml_path` | `nil` | Explicit path to a `clouds.yaml` file, inserted into the search path. |
|
|
168
|
+
|
|
50
169
|
## Development
|
|
51
170
|
|
|
52
171
|
Pull requests are very welcome! Make sure your patches are well tested.
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# Author:: Lance Albertson (<lance@osuosl.org>)
|
|
5
|
+
#
|
|
6
|
+
# Copyright:: (C) 2026, Oregon State University
|
|
7
|
+
#
|
|
8
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
9
|
+
# you may not use this file except in compliance with the License.
|
|
10
|
+
# You may obtain a copy of the License at
|
|
11
|
+
#
|
|
12
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
13
|
+
#
|
|
14
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
15
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
16
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
17
|
+
# See the License for the specific language governing permissions and
|
|
18
|
+
# limitations under the License.
|
|
19
|
+
|
|
20
|
+
require "yaml"
|
|
21
|
+
|
|
22
|
+
module Kitchen
|
|
23
|
+
module Driver
|
|
24
|
+
class Openstack < Kitchen::Driver::Base
|
|
25
|
+
# Support for OpenStack clouds.yaml client configuration
|
|
26
|
+
module Clouds
|
|
27
|
+
# Mapping of clouds.yaml auth keys to Fog OpenStack config keys
|
|
28
|
+
CLOUDS_YAML_AUTH_MAP = {
|
|
29
|
+
"auth_url" => :openstack_auth_url,
|
|
30
|
+
"username" => :openstack_username,
|
|
31
|
+
"password" => :openstack_api_key,
|
|
32
|
+
"project_name" => :openstack_project_name,
|
|
33
|
+
"project_id" => :openstack_project_id,
|
|
34
|
+
"user_domain_name" => :openstack_user_domain,
|
|
35
|
+
"user_domain_id" => :openstack_user_domain_id,
|
|
36
|
+
"project_domain_name" => :openstack_project_domain,
|
|
37
|
+
"project_domain_id" => :openstack_project_domain_id,
|
|
38
|
+
"domain_id" => :openstack_domain_id,
|
|
39
|
+
"domain_name" => :openstack_domain_name,
|
|
40
|
+
"application_credential_id" => :openstack_application_credential_id,
|
|
41
|
+
"application_credential_secret" => :openstack_application_credential_secret,
|
|
42
|
+
}.freeze
|
|
43
|
+
|
|
44
|
+
# Mapping of clouds.yaml top-level keys to Fog OpenStack config keys
|
|
45
|
+
CLOUDS_YAML_TOP_MAP = {
|
|
46
|
+
"region_name" => :openstack_region,
|
|
47
|
+
"interface" => :openstack_endpoint_type,
|
|
48
|
+
"identity_api_version" => :openstack_identity_api_version,
|
|
49
|
+
}.freeze
|
|
50
|
+
|
|
51
|
+
# Mapping of OS_* environment variables to Fog OpenStack config keys
|
|
52
|
+
ENV_VAR_MAP = {
|
|
53
|
+
"OS_AUTH_URL" => :openstack_auth_url,
|
|
54
|
+
"OS_USERNAME" => :openstack_username,
|
|
55
|
+
"OS_PASSWORD" => :openstack_api_key,
|
|
56
|
+
"OS_PROJECT_NAME" => :openstack_project_name,
|
|
57
|
+
"OS_PROJECT_ID" => :openstack_project_id,
|
|
58
|
+
"OS_USER_DOMAIN_NAME" => :openstack_user_domain,
|
|
59
|
+
"OS_USER_DOMAIN_ID" => :openstack_user_domain_id,
|
|
60
|
+
"OS_PROJECT_DOMAIN_NAME" => :openstack_project_domain,
|
|
61
|
+
"OS_PROJECT_DOMAIN_ID" => :openstack_project_domain_id,
|
|
62
|
+
"OS_DOMAIN_ID" => :openstack_domain_id,
|
|
63
|
+
"OS_DOMAIN_NAME" => :openstack_domain_name,
|
|
64
|
+
"OS_REGION_NAME" => :openstack_region,
|
|
65
|
+
"OS_INTERFACE" => :openstack_endpoint_type,
|
|
66
|
+
"OS_IDENTITY_API_VERSION" => :openstack_identity_api_version,
|
|
67
|
+
"OS_APPLICATION_CREDENTIAL_ID" => :openstack_application_credential_id,
|
|
68
|
+
"OS_APPLICATION_CREDENTIAL_SECRET" => :openstack_application_credential_secret,
|
|
69
|
+
"OS_CACERT" => :ssl_ca_file,
|
|
70
|
+
}.freeze
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# Merges external config sources into the driver config hash.
|
|
75
|
+
# Precedence: kitchen.yml > OS_* env vars > clouds.yaml
|
|
76
|
+
# Only sets keys that are currently nil so that kitchen.yml
|
|
77
|
+
# values always take precedence.
|
|
78
|
+
def apply_clouds_config
|
|
79
|
+
cc = load_clouds_config
|
|
80
|
+
env = load_env_vars
|
|
81
|
+
|
|
82
|
+
# env vars override clouds.yaml per upstream openstacksdk precedence
|
|
83
|
+
merged = cc.merge(env)
|
|
84
|
+
return if merged.empty?
|
|
85
|
+
|
|
86
|
+
merged.each do |key, value|
|
|
87
|
+
config[key] = value if config[key].nil?
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Apply SSL settings: env vars or clouds.yaml disabling verification
|
|
91
|
+
ssl_verify = env.key?(:ssl_verify_peer) ? env[:ssl_verify_peer] : cc[:ssl_verify_peer]
|
|
92
|
+
return unless ssl_verify == false && !config[:disable_ssl_validation]
|
|
93
|
+
|
|
94
|
+
config[:disable_ssl_validation] = true
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Reads OS_* environment variables and maps them to Fog config keys.
|
|
98
|
+
# Returns a hash of fog config symbols for any set env vars.
|
|
99
|
+
def load_env_vars
|
|
100
|
+
result = {}
|
|
101
|
+
ENV_VAR_MAP.each do |env_var, fog_key|
|
|
102
|
+
value = ENV[env_var]
|
|
103
|
+
result[fog_key] = value if value && !value.empty?
|
|
104
|
+
end
|
|
105
|
+
result
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Resolves the cloud name from config or the OS_CLOUD environment variable
|
|
109
|
+
def cloud_name
|
|
110
|
+
config[:openstack_cloud] || ENV["OS_CLOUD"]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Loads and merges clouds.yaml with secure.yaml, then translates the
|
|
114
|
+
# named cloud entry into Fog-compatible config keys.
|
|
115
|
+
# Returns a hash of fog config symbols, or empty hash if no cloud configured.
|
|
116
|
+
def load_clouds_config
|
|
117
|
+
name = cloud_name
|
|
118
|
+
return {} unless name
|
|
119
|
+
|
|
120
|
+
clouds_data = load_yaml_file("clouds.yaml", "OS_CLIENT_CONFIG_FILE")
|
|
121
|
+
secure_data = load_yaml_file("secure.yaml", "OS_CLIENT_SECURE_FILE")
|
|
122
|
+
|
|
123
|
+
cloud = extract_cloud(clouds_data, name)
|
|
124
|
+
secure = extract_cloud(secure_data, name)
|
|
125
|
+
|
|
126
|
+
cloud = deep_merge(cloud, secure)
|
|
127
|
+
translate_cloud_config(cloud)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Search standard OpenStack config file locations for the given filename
|
|
131
|
+
def load_yaml_file(filename, env_var)
|
|
132
|
+
paths = clouds_yaml_search_paths(filename, env_var)
|
|
133
|
+
path = paths.find { |p| File.exist?(p) }
|
|
134
|
+
return {} unless path
|
|
135
|
+
|
|
136
|
+
debug "Loading #{filename} from #{path}"
|
|
137
|
+
YAML.safe_load(File.read(path), permitted_classes: [Date]) || {} # rubocop: disable Style/YAMLFileRead
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def clouds_yaml_search_paths(filename, env_var)
|
|
141
|
+
paths = []
|
|
142
|
+
paths << ENV[env_var] if ENV[env_var]
|
|
143
|
+
paths << config[:clouds_yaml_path] if config[:clouds_yaml_path] && filename == "clouds.yaml"
|
|
144
|
+
paths << File.join(Dir.pwd, filename)
|
|
145
|
+
paths << File.join(Dir.home, ".config", "openstack", filename)
|
|
146
|
+
paths << File.join("/etc/openstack", filename)
|
|
147
|
+
paths
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def extract_cloud(data, name)
|
|
151
|
+
clouds = data["clouds"] || {}
|
|
152
|
+
cloud = clouds[name]
|
|
153
|
+
return {} unless cloud
|
|
154
|
+
|
|
155
|
+
cloud
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Deep merge two hashes (secure overrides clouds)
|
|
159
|
+
def deep_merge(base, override)
|
|
160
|
+
result = base.dup
|
|
161
|
+
override.each do |key, value|
|
|
162
|
+
result[key] = if result[key].is_a?(Hash) && value.is_a?(Hash)
|
|
163
|
+
deep_merge(result[key], value)
|
|
164
|
+
else
|
|
165
|
+
value
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
result
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Convert a clouds.yaml cloud entry into Fog-compatible config keys
|
|
172
|
+
def translate_cloud_config(cloud)
|
|
173
|
+
result = {}
|
|
174
|
+
|
|
175
|
+
# Map auth section
|
|
176
|
+
auth = cloud["auth"] || {}
|
|
177
|
+
CLOUDS_YAML_AUTH_MAP.each do |yaml_key, fog_key|
|
|
178
|
+
result[fog_key] = auth[yaml_key] if auth[yaml_key]
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Map top-level keys
|
|
182
|
+
CLOUDS_YAML_TOP_MAP.each do |yaml_key, fog_key|
|
|
183
|
+
result[fog_key] = cloud[yaml_key] if cloud[yaml_key]
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# SSL settings
|
|
187
|
+
result[:ssl_verify_peer] = cloud["verify"] if cloud.key?("verify")
|
|
188
|
+
result[:ssl_ca_file] = cloud["cacert"] if cloud["cacert"]
|
|
189
|
+
|
|
190
|
+
result
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# Author:: Jonathan Hartman (<j@p4nt5.com>)
|
|
5
|
+
# Author:: JJ Asghar (<jj@chef.io>)
|
|
6
|
+
# Author:: Lance Albertson (<lance@osuosl.org>)
|
|
7
|
+
#
|
|
8
|
+
# Copyright:: (C) 2013-2015, Jonathan Hartman
|
|
9
|
+
# Copyright:: (C) 2015-2020, Chef Software Inc.
|
|
10
|
+
# Copyright:: (C) 2026, Oregon State University
|
|
11
|
+
#
|
|
12
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
13
|
+
# you may not use this file except in compliance with the License.
|
|
14
|
+
# You may obtain a copy of the License at
|
|
15
|
+
#
|
|
16
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
17
|
+
#
|
|
18
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
19
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
20
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
21
|
+
# See the License for the specific language governing permissions and
|
|
22
|
+
# limitations under the License.
|
|
23
|
+
|
|
24
|
+
module Kitchen
|
|
25
|
+
module Driver
|
|
26
|
+
class Openstack < Kitchen::Driver::Base
|
|
27
|
+
# Server naming and configuration helpers
|
|
28
|
+
module Config
|
|
29
|
+
# Set the proper server name in the config
|
|
30
|
+
def config_server_name
|
|
31
|
+
return if config[:server_name]
|
|
32
|
+
|
|
33
|
+
config[:server_name] = if config[:server_name_prefix]
|
|
34
|
+
server_name_prefix(config[:server_name_prefix])
|
|
35
|
+
else
|
|
36
|
+
default_name
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# Generate what should be a unique server name up to 63 total chars
|
|
43
|
+
# Base name: 15
|
|
44
|
+
# Username: 15
|
|
45
|
+
# Hostname: 23
|
|
46
|
+
# Random string: 7
|
|
47
|
+
# Separators: 3
|
|
48
|
+
# ================
|
|
49
|
+
# Total: 63
|
|
50
|
+
def default_name
|
|
51
|
+
[
|
|
52
|
+
instance.name.gsub(/\W/, "")[0..14],
|
|
53
|
+
((Etc.getpwuid ? Etc.getpwuid.name : Etc.getlogin) || "nologin").gsub(/\W/, "")[0..14],
|
|
54
|
+
Socket.gethostname.gsub(/\W/, "")[0..22],
|
|
55
|
+
Array.new(7) { rand(36).to_s(36) }.join,
|
|
56
|
+
].join("-")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def server_name_prefix(server_name_prefix)
|
|
60
|
+
# Generate what should be a unique server name with given prefix
|
|
61
|
+
# of up to 63 total chars
|
|
62
|
+
#
|
|
63
|
+
# Provided prefix: variable, max 54
|
|
64
|
+
# Separator: 1
|
|
65
|
+
# Random string: 8
|
|
66
|
+
# ===================
|
|
67
|
+
# Max: 63
|
|
68
|
+
#
|
|
69
|
+
if server_name_prefix.length > 54
|
|
70
|
+
warn "Server name prefix too long, truncated to 54 characters"
|
|
71
|
+
server_name_prefix = server_name_prefix[0..53]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
server_name_prefix.gsub!(/\W/, "")
|
|
75
|
+
|
|
76
|
+
if server_name_prefix.empty?
|
|
77
|
+
warn "Server name prefix empty or invalid; using fully generated name"
|
|
78
|
+
default_name
|
|
79
|
+
else
|
|
80
|
+
random_suffix = ("a".."z").to_a.sample(8).join
|
|
81
|
+
server_name_prefix + "-" + random_suffix
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# Author:: Jonathan Hartman (<j@p4nt5.com>)
|
|
5
|
+
# Author:: JJ Asghar (<jj@chef.io>)
|
|
6
|
+
# Author:: Lance Albertson (<lance@osuosl.org>)
|
|
7
|
+
#
|
|
8
|
+
# Copyright:: (C) 2013-2015, Jonathan Hartman
|
|
9
|
+
# Copyright:: (C) 2015-2020, Chef Software Inc.
|
|
10
|
+
# Copyright:: (C) 2026, Oregon State University
|
|
11
|
+
#
|
|
12
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
13
|
+
# you may not use this file except in compliance with the License.
|
|
14
|
+
# You may obtain a copy of the License at
|
|
15
|
+
#
|
|
16
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
17
|
+
#
|
|
18
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
19
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
20
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
21
|
+
# See the License for the specific language governing permissions and
|
|
22
|
+
# limitations under the License.
|
|
23
|
+
|
|
24
|
+
require "ohai" unless defined?(Ohai::System)
|
|
25
|
+
|
|
26
|
+
module Kitchen
|
|
27
|
+
module Driver
|
|
28
|
+
class Openstack < Kitchen::Driver::Base
|
|
29
|
+
# Ohai hints, SSL handling, and server wait helpers
|
|
30
|
+
module Helpers
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def add_ohai_hint(state)
|
|
34
|
+
if bourne_shell?
|
|
35
|
+
info "Adding OpenStack hint for ohai"
|
|
36
|
+
mkdir_cmd = "sudo mkdir -p #{hints_path}"
|
|
37
|
+
touch_cmd = "sudo bash -c 'echo {} > #{hints_path}/openstack.json'"
|
|
38
|
+
instance.transport.connection(state).execute(
|
|
39
|
+
"#{mkdir_cmd} && #{touch_cmd}"
|
|
40
|
+
)
|
|
41
|
+
elsif windows_os?
|
|
42
|
+
info "Adding OpenStack hint for ohai"
|
|
43
|
+
touch_cmd = "New-Item #{hints_path}\\openstack.json"
|
|
44
|
+
touch_cmd_args = "-Value '{}' -Force -Type file"
|
|
45
|
+
instance.transport.connection(state).execute(
|
|
46
|
+
"#{touch_cmd} #{touch_cmd_args}"
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def hints_path
|
|
52
|
+
Ohai.config[:hints_path][0]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def disable_ssl_validation
|
|
56
|
+
require "excon" unless defined?(Excon)
|
|
57
|
+
Excon.defaults[:ssl_verify_peer] = false
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def wait_for_server(state)
|
|
61
|
+
if config[:server_wait]
|
|
62
|
+
info "Sleeping for #{config[:server_wait]} seconds to let your server start up..."
|
|
63
|
+
countdown(config[:server_wait])
|
|
64
|
+
end
|
|
65
|
+
info "Waiting for server to be ready..."
|
|
66
|
+
instance.transport.connection(state).wait_until_ready
|
|
67
|
+
rescue
|
|
68
|
+
error "Server #{state[:hostname]} (#{state[:server_id]}) not reachable. Destroying server..."
|
|
69
|
+
destroy(state)
|
|
70
|
+
raise
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def countdown(seconds)
|
|
74
|
+
date1 = Time.now + seconds
|
|
75
|
+
while Time.now < date1
|
|
76
|
+
Kernel.print "."
|
|
77
|
+
sleep 10
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# Author:: Jonathan Hartman (<j@p4nt5.com>)
|
|
5
|
+
# Author:: JJ Asghar (<jj@chef.io>)
|
|
6
|
+
# Author:: Lance Albertson (<lance@osuosl.org>)
|
|
7
|
+
#
|
|
8
|
+
# Copyright:: (C) 2013-2015, Jonathan Hartman
|
|
9
|
+
# Copyright:: (C) 2015-2020, Chef Software Inc.
|
|
10
|
+
# Copyright:: (C) 2026, Oregon State University
|
|
11
|
+
#
|
|
12
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
13
|
+
# you may not use this file except in compliance with the License.
|
|
14
|
+
# You may obtain a copy of the License at
|
|
15
|
+
#
|
|
16
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
17
|
+
#
|
|
18
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
19
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
20
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
21
|
+
# See the License for the specific language governing permissions and
|
|
22
|
+
# limitations under the License.
|
|
23
|
+
|
|
24
|
+
require "ipaddr" unless defined?(IPAddr)
|
|
25
|
+
|
|
26
|
+
module Kitchen
|
|
27
|
+
module Driver
|
|
28
|
+
class Openstack < Kitchen::Driver::Base
|
|
29
|
+
# Floating IP allocation and IP address resolution
|
|
30
|
+
module Networking
|
|
31
|
+
IP_POOL_LOCK = Mutex.new
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def attach_ip_from_pool(server, pool)
|
|
36
|
+
IP_POOL_LOCK.synchronize do
|
|
37
|
+
info "Attaching floating IP from <#{pool}> pool"
|
|
38
|
+
if config[:allocate_floating_ip]
|
|
39
|
+
network_id = network
|
|
40
|
+
.list_networks(
|
|
41
|
+
name: pool
|
|
42
|
+
).body["networks"][0]["id"]
|
|
43
|
+
resp = network.create_floating_ip(network_id)
|
|
44
|
+
ip = resp.body["floatingip"]["floating_ip_address"]
|
|
45
|
+
info "Created floating IP <#{ip}> from <#{pool}> pool"
|
|
46
|
+
config[:floating_ip] = ip
|
|
47
|
+
else
|
|
48
|
+
free_addrs = compute.addresses.map do |i|
|
|
49
|
+
i.ip if i.fixed_ip.nil? && i.instance_id.nil? && i.pool == pool
|
|
50
|
+
end.compact
|
|
51
|
+
if free_addrs.empty?
|
|
52
|
+
raise ActionFailed, "No available IPs in pool <#{pool}>"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
config[:floating_ip] = free_addrs[0]
|
|
56
|
+
end
|
|
57
|
+
attach_ip(server, config[:floating_ip])
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def attach_ip(server, ip)
|
|
62
|
+
info "Attaching floating IP <#{ip}>"
|
|
63
|
+
server.associate_address ip
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def get_public_private_ips(server)
|
|
67
|
+
begin
|
|
68
|
+
pub = server.public_ip_addresses
|
|
69
|
+
priv = server.private_ip_addresses
|
|
70
|
+
rescue Fog::OpenStack::Compute::NotFound, Excon::Errors::Forbidden
|
|
71
|
+
# See Fog issue: https://github.com/fog/fog/issues/2160
|
|
72
|
+
addrs = server.addresses
|
|
73
|
+
addrs["public"] && pub = addrs["public"].map { |i| i["addr"] }
|
|
74
|
+
addrs["private"] && priv = addrs["private"].map { |i| i["addr"] }
|
|
75
|
+
end
|
|
76
|
+
[pub, priv]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def get_ip(server)
|
|
80
|
+
if config[:floating_ip]
|
|
81
|
+
debug "Using floating ip: #{config[:floating_ip]}"
|
|
82
|
+
return config[:floating_ip]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# make sure we have the latest info
|
|
86
|
+
info "Waiting for network information to be available..."
|
|
87
|
+
begin
|
|
88
|
+
w = server.wait_for { !addresses.empty? }
|
|
89
|
+
debug "Waited #{w[:duration]} seconds for network information."
|
|
90
|
+
rescue Fog::Errors::TimeoutError
|
|
91
|
+
raise ActionFailed, "Could not get network information (timed out)"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# should also work for private networks
|
|
95
|
+
if config[:openstack_network_name]
|
|
96
|
+
debug "Using configured net: #{config[:openstack_network_name]}"
|
|
97
|
+
return filter_ips(server.addresses[config[:openstack_network_name]]).first["addr"]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
pub, priv = get_public_private_ips(server)
|
|
101
|
+
priv = server.ip_addresses if Array(pub).empty? && Array(priv).empty?
|
|
102
|
+
pub, priv = parse_ips(pub, priv)
|
|
103
|
+
pub[config[:public_ip_order].to_i] ||
|
|
104
|
+
priv[config[:private_ip_order].to_i] ||
|
|
105
|
+
raise(ActionFailed, "Could not find an IP")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def filter_ips(addresses)
|
|
109
|
+
if config[:use_ipv6]
|
|
110
|
+
addresses.select { |i| IPAddr.new(i["addr"]).ipv6? }
|
|
111
|
+
else
|
|
112
|
+
addresses.select { |i| IPAddr.new(i["addr"]).ipv4? }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def parse_ips(pub, priv)
|
|
117
|
+
pub = Array(pub)
|
|
118
|
+
priv = Array(priv)
|
|
119
|
+
if config[:use_ipv6]
|
|
120
|
+
[pub, priv].each { |n| n.select! { |i| IPAddr.new(i).ipv6? } }
|
|
121
|
+
else
|
|
122
|
+
[pub, priv].each { |n| n.select! { |i| IPAddr.new(i).ipv4? } }
|
|
123
|
+
end
|
|
124
|
+
[pub, priv]
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# Author:: Jonathan Hartman (<j@p4nt5.com>)
|
|
5
|
+
# Author:: JJ Asghar (<jj@chef.io>)
|
|
6
|
+
# Author:: Lance Albertson (<lance@osuosl.org>)
|
|
7
|
+
#
|
|
8
|
+
# Copyright:: (C) 2013-2015, Jonathan Hartman
|
|
9
|
+
# Copyright:: (C) 2015-2020, Chef Software Inc.
|
|
10
|
+
# Copyright:: (C) 2026, Oregon State University
|
|
11
|
+
#
|
|
12
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
13
|
+
# you may not use this file except in compliance with the License.
|
|
14
|
+
# You may obtain a copy of the License at
|
|
15
|
+
#
|
|
16
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
17
|
+
#
|
|
18
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
19
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
20
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
21
|
+
# See the License for the specific language governing permissions and
|
|
22
|
+
# limitations under the License.
|
|
23
|
+
|
|
24
|
+
module Kitchen
|
|
25
|
+
module Driver
|
|
26
|
+
class Openstack < Kitchen::Driver::Base
|
|
27
|
+
# Server creation and resource finders (image, flavor, network)
|
|
28
|
+
module ServerHelper
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def create_server
|
|
32
|
+
server_def = init_configuration
|
|
33
|
+
raise(ActionFailed, "Cannot specify both network_ref and network_id") if config[:network_id] && config[:network_ref]
|
|
34
|
+
|
|
35
|
+
if config[:network_id]
|
|
36
|
+
networks = [].push(config[:network_id])
|
|
37
|
+
server_def[:nics] = networks.flatten.map do |net_id|
|
|
38
|
+
{ "net_id" => net_id }
|
|
39
|
+
end
|
|
40
|
+
elsif config[:network_ref]
|
|
41
|
+
networks = [].push(config[:network_ref])
|
|
42
|
+
server_def[:nics] = networks.flatten.map do |net|
|
|
43
|
+
{ "net_id" => find_network(net).id }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
if config[:block_device_mapping]
|
|
48
|
+
server_def[:block_device_mapping] = get_bdm(config)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
%i{
|
|
52
|
+
security_groups
|
|
53
|
+
key_name
|
|
54
|
+
user_data
|
|
55
|
+
config_drive
|
|
56
|
+
metadata
|
|
57
|
+
}.each do |c|
|
|
58
|
+
server_def[c] = optional_config(c) if config[c]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if config[:cloud_config]
|
|
62
|
+
raise(ActionFailed, "Cannot specify both cloud_config and user_data") if config[:user_data]
|
|
63
|
+
|
|
64
|
+
server_def[:user_data] = YAML.dump(Kitchen::Util.stringified_hash(config[:cloud_config])).gsub(/^---\n/, "#cloud-config\n")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Can't use the Fog bootstrap and/or setup methods here; they require a
|
|
68
|
+
# public IP address that can't be guaranteed to exist across all
|
|
69
|
+
# OpenStack deployments (e.g. TryStack ARM only has private IPs).
|
|
70
|
+
compute.servers.create(server_def)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def init_configuration
|
|
74
|
+
raise(ActionFailed, "Cannot specify both image_ref and image_id") if config[:image_id] && config[:image_ref]
|
|
75
|
+
raise(ActionFailed, "Cannot specify both flavor_ref and flavor_id") if config[:flavor_id] && config[:flavor_ref]
|
|
76
|
+
|
|
77
|
+
{
|
|
78
|
+
name: config[:server_name],
|
|
79
|
+
image_ref: config[:image_id] || find_image(config[:image_ref]).id,
|
|
80
|
+
flavor_ref: config[:flavor_id] || find_flavor(config[:flavor_ref]).id,
|
|
81
|
+
availability_zone: config[:availability_zone],
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def optional_config(c)
|
|
86
|
+
case c
|
|
87
|
+
when :security_groups
|
|
88
|
+
config[c] if config[c].is_a?(Array)
|
|
89
|
+
when :user_data
|
|
90
|
+
File.read(config[c]) if File.exist?(config[c])
|
|
91
|
+
else
|
|
92
|
+
config[c]
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def find_image(image_ref)
|
|
97
|
+
image = find_matching(compute.images, image_ref)
|
|
98
|
+
raise(ActionFailed, "Image not found") unless image
|
|
99
|
+
|
|
100
|
+
debug "Selected image: #{image.id} #{image.name}"
|
|
101
|
+
image
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def find_flavor(flavor_ref)
|
|
105
|
+
flavor = find_matching(compute.flavors, flavor_ref)
|
|
106
|
+
raise(ActionFailed, "Flavor not found") unless flavor
|
|
107
|
+
|
|
108
|
+
debug "Selected flavor: #{flavor.id} #{flavor.name}"
|
|
109
|
+
flavor
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def find_network(network_ref)
|
|
113
|
+
net = find_matching(network.networks.all, network_ref)
|
|
114
|
+
raise(ActionFailed, "Network not found") unless net
|
|
115
|
+
|
|
116
|
+
debug "Selected net: #{net.id} #{net.name}"
|
|
117
|
+
net
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def find_matching(collection, name)
|
|
121
|
+
name = name.to_s
|
|
122
|
+
if name.start_with?("/") && name.end_with?("/")
|
|
123
|
+
regex = Regexp.new(name[1...-1])
|
|
124
|
+
# check for regex name match
|
|
125
|
+
collection.each { |single| return single if regex&.match?(single.name) }
|
|
126
|
+
else
|
|
127
|
+
# check for exact id match
|
|
128
|
+
collection.each { |single| return single if single.id == name }
|
|
129
|
+
# check for exact name match
|
|
130
|
+
collection.each { |single| return single if single.name == name }
|
|
131
|
+
end
|
|
132
|
+
nil
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
#
|
|
4
4
|
# Author:: Jonathan Hartman (<j@p4nt5.com>)
|
|
5
5
|
#
|
|
6
|
-
# Copyright (C) 2013-2015, Jonathan Hartman
|
|
6
|
+
# Copyright:: (C) 2013-2015, Jonathan Hartman
|
|
7
7
|
#
|
|
8
8
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
9
9
|
# you may not use this file except in compliance with the License.
|
|
@@ -28,7 +28,7 @@ module Kitchen
|
|
|
28
28
|
#
|
|
29
29
|
# @author Liam Haworth <liam.haworth@bluereef.com.au>
|
|
30
30
|
class Volume
|
|
31
|
-
|
|
31
|
+
DEFAULT_CREATION_TIMEOUT = 60
|
|
32
32
|
|
|
33
33
|
def initialize(logger)
|
|
34
34
|
@logger = logger
|
|
@@ -60,7 +60,7 @@ module Kitchen
|
|
|
60
60
|
vol_model = volume(os).volumes.first { |x| x.id == vol_id }
|
|
61
61
|
|
|
62
62
|
# Use default creation timeout or user supplied
|
|
63
|
-
creation_timeout =
|
|
63
|
+
creation_timeout = DEFAULT_CREATION_TIMEOUT
|
|
64
64
|
if bdm.key?(:creation_timeout)
|
|
65
65
|
creation_timeout = bdm[:creation_timeout]
|
|
66
66
|
end
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
#
|
|
4
4
|
# Author:: Jonathan Hartman (<j@p4nt5.com>)
|
|
5
5
|
# Author:: JJ Asghar (<jj@chef.io>)
|
|
6
|
+
# Author:: Lance Albertson (<lance@osuosl.org>)
|
|
6
7
|
#
|
|
7
|
-
# Copyright (C) 2013-2015, Jonathan Hartman
|
|
8
|
-
# Copyright (C) 2015-2020, Chef Software Inc.
|
|
8
|
+
# Copyright:: (C) 2013-2015, Jonathan Hartman
|
|
9
|
+
# Copyright:: (C) 2015-2020, Chef Software Inc.
|
|
10
|
+
# Copyright:: (C) 2026, Oregon State University
|
|
9
11
|
#
|
|
10
12
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
11
13
|
# you may not use this file except in compliance with the License.
|
|
@@ -21,21 +23,39 @@
|
|
|
21
23
|
|
|
22
24
|
require "kitchen"
|
|
23
25
|
require "fog/openstack"
|
|
24
|
-
require "ohai" unless defined?(Ohai::System)
|
|
25
26
|
require "yaml"
|
|
26
27
|
require_relative "openstack_version"
|
|
28
|
+
require_relative "openstack/clouds"
|
|
29
|
+
require_relative "openstack/config"
|
|
30
|
+
require_relative "openstack/helpers"
|
|
31
|
+
require_relative "openstack/networking"
|
|
32
|
+
require_relative "openstack/server_helper"
|
|
27
33
|
require_relative "openstack/volume"
|
|
28
34
|
|
|
29
35
|
module Kitchen
|
|
30
36
|
module Driver
|
|
31
37
|
# This takes from the Base Class and creates the OpenStack driver.
|
|
32
38
|
class Openstack < Kitchen::Driver::Base
|
|
33
|
-
|
|
39
|
+
include Clouds
|
|
40
|
+
include Config
|
|
41
|
+
include Helpers
|
|
42
|
+
include Networking
|
|
43
|
+
include ServerHelper
|
|
34
44
|
|
|
35
45
|
kitchen_driver_api_version 2
|
|
36
46
|
plugin_version Kitchen::Driver::OPENSTACK_VERSION
|
|
37
47
|
|
|
48
|
+
default_config :openstack_cloud, nil
|
|
49
|
+
default_config :clouds_yaml_path, nil
|
|
38
50
|
default_config :server_name, nil
|
|
51
|
+
|
|
52
|
+
# Merge clouds.yaml values into config so they are visible in
|
|
53
|
+
# `kitchen diagnose` and available to all driver methods.
|
|
54
|
+
def finalize_config!(instance)
|
|
55
|
+
super
|
|
56
|
+
apply_clouds_config
|
|
57
|
+
self
|
|
58
|
+
end
|
|
39
59
|
default_config :server_name_prefix, nil
|
|
40
60
|
default_config :key_name, nil
|
|
41
61
|
default_config :port, "22"
|
|
@@ -62,17 +82,6 @@ module Kitchen
|
|
|
62
82
|
default_config :write_timeout, 60
|
|
63
83
|
default_config :metadata, nil
|
|
64
84
|
|
|
65
|
-
# Set the proper server name in the config
|
|
66
|
-
def config_server_name
|
|
67
|
-
return if config[:server_name]
|
|
68
|
-
|
|
69
|
-
config[:server_name] = if config[:server_name_prefix]
|
|
70
|
-
server_name_prefix(config[:server_name_prefix])
|
|
71
|
-
else
|
|
72
|
-
default_name
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
|
|
76
85
|
def create(state)
|
|
77
86
|
config_server_name
|
|
78
87
|
if state[:server_id]
|
|
@@ -87,7 +96,10 @@ module Kitchen
|
|
|
87
96
|
debug "Waiting for a max time of:#{config[:glance_cache_wait_timeout]} seconds for OpenStack server to be in ACTIVE state"
|
|
88
97
|
server.wait_for(config[:glance_cache_wait_timeout]) do
|
|
89
98
|
sleep(1)
|
|
90
|
-
|
|
99
|
+
if failed?
|
|
100
|
+
raise(Kitchen::InstanceFailure,
|
|
101
|
+
"OpenStack server ID <#{state[:server_id]}> build failed to ERROR state")
|
|
102
|
+
end
|
|
91
103
|
|
|
92
104
|
ready?
|
|
93
105
|
end
|
|
@@ -101,8 +113,8 @@ module Kitchen
|
|
|
101
113
|
state[:hostname] = get_ip(server)
|
|
102
114
|
wait_for_server(state)
|
|
103
115
|
add_ohai_hint(state)
|
|
104
|
-
rescue Fog::Errors::Error, Excon::Errors::Error =>
|
|
105
|
-
raise ActionFailed,
|
|
116
|
+
rescue Fog::Errors::Error, Excon::Errors::Error => e
|
|
117
|
+
raise ActionFailed, e.message
|
|
106
118
|
end
|
|
107
119
|
|
|
108
120
|
def destroy(state)
|
|
@@ -172,293 +184,6 @@ module Kitchen
|
|
|
172
184
|
def get_bdm(config)
|
|
173
185
|
volume.get_bdm(config, openstack_server)
|
|
174
186
|
end
|
|
175
|
-
|
|
176
|
-
def create_server
|
|
177
|
-
server_def = init_configuration
|
|
178
|
-
raise(ActionFailed, "Cannot specify both network_ref and network_id") if config[:network_id] && config[:network_ref]
|
|
179
|
-
|
|
180
|
-
if config[:network_id]
|
|
181
|
-
networks = [].push(config[:network_id])
|
|
182
|
-
server_def[:nics] = networks.flatten.map do |net_id|
|
|
183
|
-
{ "net_id" => net_id }
|
|
184
|
-
end
|
|
185
|
-
elsif config[:network_ref]
|
|
186
|
-
networks = [].push(config[:network_ref])
|
|
187
|
-
server_def[:nics] = networks.flatten.map do |net|
|
|
188
|
-
{ "net_id" => find_network(net).id }
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
if config[:block_device_mapping]
|
|
193
|
-
server_def[:block_device_mapping] = get_bdm(config)
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
%i{
|
|
197
|
-
security_groups
|
|
198
|
-
key_name
|
|
199
|
-
user_data
|
|
200
|
-
config_drive
|
|
201
|
-
metadata
|
|
202
|
-
}.each do |c|
|
|
203
|
-
server_def[c] = optional_config(c) if config[c]
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
if config[:cloud_config]
|
|
207
|
-
raise(ActionFailed, "Cannot specify both cloud_config and user_data") if config[:user_data]
|
|
208
|
-
|
|
209
|
-
server_def[:user_data] = Kitchen::Util.stringified_hash(config[:cloud_config]).to_yaml.gsub(/^---\n/, "#cloud-config\n")
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
# Can't use the Fog bootstrap and/or setup methods here; they require a
|
|
213
|
-
# public IP address that can't be guaranteed to exist across all
|
|
214
|
-
# OpenStack deployments (e.g. TryStack ARM only has private IPs).
|
|
215
|
-
compute.servers.create(server_def)
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
def init_configuration
|
|
219
|
-
raise(ActionFailed, "Cannot specify both image_ref and image_id") if config[:image_id] && config[:image_ref]
|
|
220
|
-
raise(ActionFailed, "Cannot specify both flavor_ref and flavor_id") if config[:flavor_id] && config[:flavor_ref]
|
|
221
|
-
|
|
222
|
-
{
|
|
223
|
-
name: config[:server_name],
|
|
224
|
-
image_ref: config[:image_id] || find_image(config[:image_ref]).id,
|
|
225
|
-
flavor_ref: config[:flavor_id] || find_flavor(config[:flavor_ref]).id,
|
|
226
|
-
availability_zone: config[:availability_zone],
|
|
227
|
-
}
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
def optional_config(c)
|
|
231
|
-
case c
|
|
232
|
-
when :security_groups
|
|
233
|
-
config[c] if config[c].is_a?(Array)
|
|
234
|
-
when :user_data
|
|
235
|
-
File.open(config[c], &:read) if File.exist?(config[c])
|
|
236
|
-
else
|
|
237
|
-
config[c]
|
|
238
|
-
end
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
def find_image(image_ref)
|
|
242
|
-
image = find_matching(compute.images, image_ref)
|
|
243
|
-
raise(ActionFailed, "Image not found") unless image
|
|
244
|
-
|
|
245
|
-
debug "Selected image: #{image.id} #{image.name}"
|
|
246
|
-
image
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
def find_flavor(flavor_ref)
|
|
250
|
-
flavor = find_matching(compute.flavors, flavor_ref)
|
|
251
|
-
raise(ActionFailed, "Flavor not found") unless flavor
|
|
252
|
-
|
|
253
|
-
debug "Selected flavor: #{flavor.id} #{flavor.name}"
|
|
254
|
-
flavor
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
def find_network(network_ref)
|
|
258
|
-
net = find_matching(network.networks.all, network_ref)
|
|
259
|
-
raise(ActionFailed, "Network not found") unless net
|
|
260
|
-
|
|
261
|
-
debug "Selected net: #{net.id} #{net.name}"
|
|
262
|
-
net
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
# Generate what should be a unique server name up to 63 total chars
|
|
266
|
-
# Base name: 15
|
|
267
|
-
# Username: 15
|
|
268
|
-
# Hostname: 23
|
|
269
|
-
# Random string: 7
|
|
270
|
-
# Separators: 3
|
|
271
|
-
# ================
|
|
272
|
-
# Total: 63
|
|
273
|
-
def default_name
|
|
274
|
-
[
|
|
275
|
-
instance.name.gsub(/\W/, "")[0..14],
|
|
276
|
-
((Etc.getpwuid ? Etc.getpwuid.name : Etc.getlogin) || "nologin").gsub(/\W/, "")[0..14],
|
|
277
|
-
Socket.gethostname.gsub(/\W/, "")[0..22],
|
|
278
|
-
Array.new(7) { rand(36).to_s(36) }.join,
|
|
279
|
-
].join("-")
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
def server_name_prefix(server_name_prefix)
|
|
283
|
-
# Generate what should be a unique server name with given prefix
|
|
284
|
-
# of up to 63 total chars
|
|
285
|
-
#
|
|
286
|
-
# Provided prefix: variable, max 54
|
|
287
|
-
# Separator: 1
|
|
288
|
-
# Random string: 8
|
|
289
|
-
# ===================
|
|
290
|
-
# Max: 63
|
|
291
|
-
#
|
|
292
|
-
if server_name_prefix.length > 54
|
|
293
|
-
warn "Server name prefix too long, truncated to 54 characters"
|
|
294
|
-
server_name_prefix = server_name_prefix[0..53]
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
server_name_prefix.gsub!(/\W/, "")
|
|
298
|
-
|
|
299
|
-
if server_name_prefix.empty?
|
|
300
|
-
warn "Server name prefix empty or invalid; using fully generated name"
|
|
301
|
-
default_name
|
|
302
|
-
else
|
|
303
|
-
random_suffix = ("a".."z").to_a.sample(8).join
|
|
304
|
-
server_name_prefix + "-" + random_suffix
|
|
305
|
-
end
|
|
306
|
-
end
|
|
307
|
-
|
|
308
|
-
def attach_ip_from_pool(server, pool)
|
|
309
|
-
@@ip_pool_lock.synchronize do
|
|
310
|
-
info "Attaching floating IP from <#{pool}> pool"
|
|
311
|
-
if config[:allocate_floating_ip]
|
|
312
|
-
network_id = network
|
|
313
|
-
.list_networks(
|
|
314
|
-
name: pool
|
|
315
|
-
).body["networks"][0]["id"]
|
|
316
|
-
resp = network.create_floating_ip(network_id)
|
|
317
|
-
ip = resp.body["floatingip"]["floating_ip_address"]
|
|
318
|
-
info "Created floating IP <#{ip}> from <#{pool}> pool"
|
|
319
|
-
config[:floating_ip] = ip
|
|
320
|
-
else
|
|
321
|
-
free_addrs = compute.addresses.map do |i|
|
|
322
|
-
i.ip if i.fixed_ip.nil? && i.instance_id.nil? && i.pool == pool
|
|
323
|
-
end.compact
|
|
324
|
-
if free_addrs.empty?
|
|
325
|
-
raise ActionFailed, "No available IPs in pool <#{pool}>"
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
config[:floating_ip] = free_addrs[0]
|
|
329
|
-
end
|
|
330
|
-
attach_ip(server, config[:floating_ip])
|
|
331
|
-
end
|
|
332
|
-
end
|
|
333
|
-
|
|
334
|
-
def attach_ip(server, ip)
|
|
335
|
-
info "Attaching floating IP <#{ip}>"
|
|
336
|
-
server.associate_address ip
|
|
337
|
-
end
|
|
338
|
-
|
|
339
|
-
def get_public_private_ips(server)
|
|
340
|
-
begin
|
|
341
|
-
pub = server.public_ip_addresses
|
|
342
|
-
priv = server.private_ip_addresses
|
|
343
|
-
rescue Fog::OpenStack::Compute::NotFound, Excon::Errors::Forbidden
|
|
344
|
-
# See Fog issue: https://github.com/fog/fog/issues/2160
|
|
345
|
-
addrs = server.addresses
|
|
346
|
-
addrs["public"] && pub = addrs["public"].map { |i| i["addr"] }
|
|
347
|
-
addrs["private"] && priv = addrs["private"].map { |i| i["addr"] }
|
|
348
|
-
end
|
|
349
|
-
[pub, priv]
|
|
350
|
-
end
|
|
351
|
-
|
|
352
|
-
def get_ip(server)
|
|
353
|
-
if config[:floating_ip]
|
|
354
|
-
debug "Using floating ip: #{config[:floating_ip]}"
|
|
355
|
-
return config[:floating_ip]
|
|
356
|
-
end
|
|
357
|
-
|
|
358
|
-
# make sure we have the latest info
|
|
359
|
-
info "Waiting for network information to be available..."
|
|
360
|
-
begin
|
|
361
|
-
w = server.wait_for { !addresses.empty? }
|
|
362
|
-
debug "Waited #{w[:duration]} seconds for network information."
|
|
363
|
-
rescue Fog::Errors::TimeoutError
|
|
364
|
-
raise ActionFailed, "Could not get network information (timed out)"
|
|
365
|
-
end
|
|
366
|
-
|
|
367
|
-
# should also work for private networks
|
|
368
|
-
if config[:openstack_network_name]
|
|
369
|
-
debug "Using configured net: #{config[:openstack_network_name]}"
|
|
370
|
-
return filter_ips(server.addresses[config[:openstack_network_name]]).first["addr"]
|
|
371
|
-
end
|
|
372
|
-
|
|
373
|
-
pub, priv = get_public_private_ips(server)
|
|
374
|
-
priv = server.ip_addresses if Array(pub).empty? && Array(priv).empty?
|
|
375
|
-
pub, priv = parse_ips(pub, priv)
|
|
376
|
-
pub[config[:public_ip_order].to_i] ||
|
|
377
|
-
priv[config[:private_ip_order].to_i] ||
|
|
378
|
-
raise(ActionFailed, "Could not find an IP")
|
|
379
|
-
end
|
|
380
|
-
|
|
381
|
-
def filter_ips(addresses)
|
|
382
|
-
if config[:use_ipv6]
|
|
383
|
-
addresses.select { |i| IPAddr.new(i["addr"]).ipv6? }
|
|
384
|
-
else
|
|
385
|
-
addresses.select { |i| IPAddr.new(i["addr"]).ipv4? }
|
|
386
|
-
end
|
|
387
|
-
end
|
|
388
|
-
|
|
389
|
-
def parse_ips(pub, priv)
|
|
390
|
-
pub = Array(pub)
|
|
391
|
-
priv = Array(priv)
|
|
392
|
-
if config[:use_ipv6]
|
|
393
|
-
[pub, priv].each { |n| n.select! { |i| IPAddr.new(i).ipv6? } }
|
|
394
|
-
else
|
|
395
|
-
[pub, priv].each { |n| n.select! { |i| IPAddr.new(i).ipv4? } }
|
|
396
|
-
end
|
|
397
|
-
[pub, priv]
|
|
398
|
-
end
|
|
399
|
-
|
|
400
|
-
def add_ohai_hint(state)
|
|
401
|
-
if bourne_shell?
|
|
402
|
-
info "Adding OpenStack hint for ohai"
|
|
403
|
-
mkdir_cmd = "sudo mkdir -p #{hints_path}"
|
|
404
|
-
touch_cmd = "sudo bash -c 'echo {} > #{hints_path}/openstack.json'"
|
|
405
|
-
instance.transport.connection(state).execute(
|
|
406
|
-
"#{mkdir_cmd} && #{touch_cmd}"
|
|
407
|
-
)
|
|
408
|
-
elsif windows_os?
|
|
409
|
-
info "Adding OpenStack hint for ohai"
|
|
410
|
-
touch_cmd = "New-Item #{hints_path}\\openstack.json"
|
|
411
|
-
touch_cmd_args = "-Value '{}' -Force -Type file"
|
|
412
|
-
instance.transport.connection(state).execute(
|
|
413
|
-
"#{touch_cmd} #{touch_cmd_args}"
|
|
414
|
-
)
|
|
415
|
-
end
|
|
416
|
-
end
|
|
417
|
-
|
|
418
|
-
def hints_path
|
|
419
|
-
Ohai.config[:hints_path][0]
|
|
420
|
-
end
|
|
421
|
-
|
|
422
|
-
def disable_ssl_validation
|
|
423
|
-
require "excon" unless defined?(Excon)
|
|
424
|
-
Excon.defaults[:ssl_verify_peer] = false
|
|
425
|
-
end
|
|
426
|
-
|
|
427
|
-
def wait_for_server(state)
|
|
428
|
-
if config[:server_wait]
|
|
429
|
-
info "Sleeping for #{config[:server_wait]} seconds to let your server start up..."
|
|
430
|
-
countdown(config[:server_wait])
|
|
431
|
-
end
|
|
432
|
-
info "Waiting for server to be ready..."
|
|
433
|
-
instance.transport.connection(state).wait_until_ready
|
|
434
|
-
rescue
|
|
435
|
-
error "Server #{state[:hostname]} (#{state[:server_id]}) not reachable. Destroying server..."
|
|
436
|
-
destroy(state)
|
|
437
|
-
raise
|
|
438
|
-
end
|
|
439
|
-
|
|
440
|
-
def countdown(seconds)
|
|
441
|
-
date1 = Time.now + seconds
|
|
442
|
-
while Time.now < date1
|
|
443
|
-
Kernel.print "."
|
|
444
|
-
sleep 10
|
|
445
|
-
end
|
|
446
|
-
end
|
|
447
|
-
|
|
448
|
-
def find_matching(collection, name)
|
|
449
|
-
name = name.to_s
|
|
450
|
-
if name.start_with?("/") && name.end_with?("/")
|
|
451
|
-
regex = Regexp.new(name[1...-1])
|
|
452
|
-
# check for regex name match
|
|
453
|
-
collection.each { |single| return single if regex&.match?(single.name) }
|
|
454
|
-
else
|
|
455
|
-
# check for exact id match
|
|
456
|
-
collection.each { |single| return single if single.id == name }
|
|
457
|
-
# check for exact name match
|
|
458
|
-
collection.each { |single| return single if single.name == name }
|
|
459
|
-
end
|
|
460
|
-
nil
|
|
461
|
-
end
|
|
462
187
|
end
|
|
463
188
|
end
|
|
464
189
|
end
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
#
|
|
4
4
|
# Author:: Jonathan Hartman (<j@p4nt5.com>)
|
|
5
5
|
#
|
|
6
|
-
# Copyright (C) 2013-2015, Jonathan Hartman
|
|
7
|
-
# Copyright (C) 2015-2021, Chef Software Inc
|
|
6
|
+
# Copyright:: (C) 2013-2015, Jonathan Hartman
|
|
7
|
+
# Copyright:: (C) 2015-2021, Chef Software Inc
|
|
8
8
|
#
|
|
9
9
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
10
10
|
# you may not use this file except in compliance with the License.
|
|
@@ -23,6 +23,6 @@ module Kitchen
|
|
|
23
23
|
#
|
|
24
24
|
# @author Jonathan Hartman <j@p4nt5.com>
|
|
25
25
|
module Driver
|
|
26
|
-
OPENSTACK_VERSION = "
|
|
26
|
+
OPENSTACK_VERSION = "7.0.0"
|
|
27
27
|
end
|
|
28
28
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kitchen-openstack
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 7.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jonathan Hartman
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2026-
|
|
12
|
+
date: 2026-04-07 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: test-kitchen
|
|
@@ -69,6 +69,11 @@ extra_rdoc_files: []
|
|
|
69
69
|
files:
|
|
70
70
|
- README.md
|
|
71
71
|
- lib/kitchen/driver/openstack.rb
|
|
72
|
+
- lib/kitchen/driver/openstack/clouds.rb
|
|
73
|
+
- lib/kitchen/driver/openstack/config.rb
|
|
74
|
+
- lib/kitchen/driver/openstack/helpers.rb
|
|
75
|
+
- lib/kitchen/driver/openstack/networking.rb
|
|
76
|
+
- lib/kitchen/driver/openstack/server_helper.rb
|
|
72
77
|
- lib/kitchen/driver/openstack/volume.rb
|
|
73
78
|
- lib/kitchen/driver/openstack_version.rb
|
|
74
79
|
homepage: https://github.com/test-kitchen/kitchen-openstack
|