kitchen-openstack 6.2.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2862ce2e9d5d93e2efa2518e740e277043158d400a0de9eb9e54aaee3c279452
4
- data.tar.gz: 43a3ed5923fd4334766c318efe85ad330180ca8846d246108f5224daab074d27
3
+ metadata.gz: 431a4cae8533390ccb721da8645f872bcf4703cf09b3c4fecdbab3b52bb2050a
4
+ data.tar.gz: 3e0718cd558cac16cbd62d206a4a701cb64ee178ee5215fbfd37cd3868191cc6
5
5
  SHA512:
6
- metadata.gz: ff7cdca79b5e5cb88cc941e14ca468cf16eb032ba83704491c65866a3c65c13ceba12414fad0ae5f2f022fdd8ef5144618cfd1c95b616dcb96502a6228e43ea5
7
- data.tar.gz: 96adba6d3c7a48639d62da0958856f0bcf51203aecca4d29bf31eb16d73adc65b66b7105083f129fd18292b49ef0d4ce7925b2a25e6f34651fc1a35f49a3fa15
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
  ![Gem Version](https://img.shields.io/gem/v/kitchen-openstack.svg)
4
- ![CI](https://github.com/test-kitchen/kitchen-openstack/workflows/CI/badge.svg)
4
+ ![CI](https://github.com/test-kitchen/kitchen-openstack/actions/workflows/lint.yml/badge.svg)
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 no longer under active development as it has no active maintainers. The software may continue to work for some or all use cases, but issues filed in GitHub will most likely not be triaged. If a new maintainer is interested in working on this project please come chat with us in #test-kitchen on Chef Community Slack.
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
- @@default_creation_timeout = 60
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 = @@default_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
- @@ip_pool_lock = Mutex.new
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
- raise(Kitchen::InstanceFailure, "OpenStack server ID <#{state[:server_id]}> build failed to ERROR state") if failed?
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 => ex
105
- raise ActionFailed, ex.message
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 = [].concat([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 = [].concat([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 = "6.2.1"
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: 6.2.1
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: 2024-06-21 00:00:00.000000000 Z
12
+ date: 2026-04-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: test-kitchen
@@ -20,7 +20,7 @@ dependencies:
20
20
  version: 1.4.1
21
21
  - - "<"
22
22
  - !ruby/object:Gem::Version
23
- version: '4'
23
+ version: '5'
24
24
  type: :runtime
25
25
  prerelease: false
26
26
  version_requirements: !ruby/object:Gem::Requirement
@@ -30,7 +30,7 @@ dependencies:
30
30
  version: 1.4.1
31
31
  - - "<"
32
32
  - !ruby/object:Gem::Version
33
- version: '4'
33
+ version: '5'
34
34
  - !ruby/object:Gem::Dependency
35
35
  name: fog-openstack
36
36
  requirement: !ruby/object:Gem::Requirement
@@ -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
@@ -90,7 +95,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
90
95
  - !ruby/object:Gem::Version
91
96
  version: '0'
92
97
  requirements: []
93
- rubygems_version: 3.4.10
98
+ rubygems_version: 3.5.9
94
99
  signing_key:
95
100
  specification_version: 4
96
101
  summary: A Test Kitchen OpenStack Nova driver