beaker-hcloud 1.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.
data/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # beaker-hcloud
2
+
3
+ [![License](https://img.shields.io/github/license/voxpupuli/beaker-hcloud.svg)](https://github.com/voxpupuli/beaker-hcloud/blob/master/LICENSE)
4
+ [![Test](https://github.com/voxpupuli/beaker-hcloud/actions/workflows/test.yml/badge.svg)](https://github.com/voxpupuli/beaker-hcloud/actions/workflows/test.yml)
5
+ [![Release](https://github.com/voxpupuli/beaker-hcloud/actions/workflows/release.yml/badge.svg)](https://github.com/voxpupuli/beaker-hcloud/actions/workflows/release.yml)
6
+ [![RubyGem Version](https://img.shields.io/gem/v/beaker-hcloud.svg)](https://rubygems.org/gems/beaker-hcloud)
7
+ [![RubyGem Downloads](https://img.shields.io/gem/dt/beaker-hcloud.svg)](https://rubygems.org/gems/beaker-hcloud)
8
+
9
+ A [beaker](https://github.com/voxpupuli/beaker) extension for provision Hetzner Cloud instances.
10
+
11
+ ## Installation
12
+
13
+ Include this gem alongside Beaker in your Gemfile or project.gemspec. E.g.
14
+
15
+ ```ruby
16
+ # Gemfile
17
+ gem 'beaker', '~> 5.0'
18
+ gem 'beaker-hcloud'
19
+
20
+ # project.gemspec
21
+ s.add_runtime_dependency 'beaker', '~> 5.0'
22
+ s.add_runtime_dependency 'beaker-hcloud'
23
+ ```
24
+
25
+ ## Authentication
26
+
27
+ You need to create an API token using Hetzner's cloud console. Make
28
+ sure to create the token in the correct project.
29
+
30
+ `beaker-hcloud` expects the token to be in the `BEAKER_HCLOUD_TOKEN`
31
+ environment variable.
32
+
33
+ ## Configuration
34
+
35
+ Some options can be set to influence how and where server instances
36
+ are being created:
37
+
38
+
39
+ | configuration option | required | default | description |
40
+ | -------------------- | -------- | ------- | ----------- |
41
+ | `image` | true | | The name of one of Hetzner's provided images, e.g. `ubuntu-20.04`, or a custom one, i.e. a snapshot in your account. |
42
+ | `server_type` | false | `cx11` | Hetzner cloud server type |
43
+ | `location` | false | `nbg1` | One of Hetzner's datacenter locations |
44
+
45
+ # Cleanup
46
+
47
+ In cases where the beaker process is killed before finishing, it may leave resources in Hetzner cloud. These will need to be manually deleted.
48
+
49
+ Look for servers in your project named exactly as the ones in your beaker host configuration and SSH keys with names beginning with `Beaker-`.
50
+
51
+ # Contributing
52
+
53
+ Please refer to voxpupuli/beaker's [contributing](https://github.com/voxpupuli/beaker/blob/master/CONTRIBUTING.md) guide.
data/Rakefile ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/core/rake_task'
4
+
5
+ begin
6
+ require 'voxpupuli/rubocop/rake'
7
+ rescue LoadError
8
+ # the voxpupuli-rubocop gem is optional
9
+ end
10
+
11
+ begin
12
+ require 'rubygems'
13
+ require 'github_changelog_generator/task'
14
+
15
+ GitHubChangelogGenerator::RakeTask.new :changelog do |config|
16
+ config.header = "# Changelog\n\nAll notable changes to this project will be documented in this file."
17
+ config.exclude_labels = %w[duplicate question invalid wontfix wont-fix skip-changelog modulesync github_actions]
18
+ config.user = 'voxpupuli'
19
+ config.project = 'beaker-hcloud'
20
+ config.future_release = Gem::Specification.load("#{config.project}.gemspec").version
21
+ end
22
+ rescue LoadError # rubocop:disable Lint/SuppressedException
23
+ end
24
+
25
+ RSpec::Core::RakeTask.new(:spec)
26
+ task default: %i[spec]
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift File.expand_path('lib', __dir__)
4
+ require 'beaker-hcloud/version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = 'beaker-hcloud'
8
+ s.version = BeakerHcloud::VERSION
9
+ s.summary = 'Hetzner Library for beaker acceptance testing framework'
10
+ s.description = 'Another gem that extends beaker'
11
+ s.authors = ['Tim Meusel', 'Vox Pupuli']
12
+ s.email = 'voxpupuli@groups.io'
13
+ s.files = `git ls-files`.split("\n")
14
+ s.homepage = 'https://github.com/voxpupuli/beaker-hcloud'
15
+ s.license = 'AGPL-3.0'
16
+
17
+ s.required_ruby_version = '>= 2.7'
18
+
19
+ # Testing dependencies
20
+ s.add_development_dependency 'rake', '~> 13.0', '>= 13.0.6'
21
+ s.add_development_dependency 'rspec', '~> 3.12'
22
+ s.add_development_dependency 'voxpupuli-rubocop', '~> 2.0.0'
23
+
24
+ s.add_runtime_dependency 'bcrypt_pbkdf', '~> 1.0'
25
+ s.add_runtime_dependency 'beaker', '~> 5.4'
26
+ s.add_runtime_dependency 'ed25519', '~> 1.2'
27
+ s.add_runtime_dependency 'hcloud', '>= 1.0.3', '< 2.0.0'
28
+ s.add_runtime_dependency 'ssh_data', '~> 1.3'
29
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'hcloud'
4
+ require 'ed25519'
5
+ require 'bcrypt_pbkdf'
6
+
7
+ require_relative '../../beaker-hcloud/ssh_data_patches'
8
+
9
+ module Beaker
10
+ # beaker extension to manage cloud instances from https://www.hetzner.com/cloud
11
+ class Hcloud < Beaker::Hypervisor
12
+ # @param [Host, Array<Host>, String, Symbol] hosts One or more hosts to act upon, or a role (String or Symbol) that identifies one or more hosts.
13
+ # @param [Hash{Symbol=>String}] options Options to pass on to the hypervisor
14
+ def initialize(hosts, options) # rubocop:disable Lint/MissingSuper
15
+ @options = options
16
+ @logger = options[:logger] || Beaker::Logger.new
17
+ @hosts = hosts
18
+
19
+ raise 'You need to pass a token as BEAKER_HCLOUD_TOKEN environment variable' unless ENV['BEAKER_HCLOUD_TOKEN']
20
+
21
+ @client = ::Hcloud::Client.new(token: ENV.fetch('BEAKER_HCLOUD_TOKEN'))
22
+ end
23
+
24
+ def provision
25
+ @logger.notify 'Provisioning hcloud'
26
+ create_ssh_key
27
+ @hosts.each do |host|
28
+ create_server(host)
29
+ end
30
+ @logger.notify 'Done provisioning hcloud'
31
+ end
32
+
33
+ def cleanup
34
+ @logger.notify 'Cleaning up hcloud'
35
+ @hosts.each do |host|
36
+ @logger.debug("Deleting hcloud server #{host.name}")
37
+ @client.servers.find(host[:hcloud_id]).destroy
38
+ end
39
+ @logger.notify 'Deleting hcloud SSH key'
40
+ @client.ssh_keys.find(@options[:ssh][:hcloud_id]).destroy
41
+ File.unlink(@key_file.path)
42
+ @logger.notify 'Done cleaning up hcloud'
43
+ end
44
+
45
+ private
46
+
47
+ def ssh_key_name
48
+ safe_hostname = Socket.gethostname.tr('.', '-')
49
+ [
50
+ 'Beaker',
51
+ ENV.fetch('USER', nil),
52
+ safe_hostname,
53
+ @options[:aws_keyname_modifier],
54
+ @options[:timestamp].strftime('%F_%H_%M_%S_%N'),
55
+ ].join('-')
56
+ end
57
+
58
+ def create_ssh_key
59
+ @logger.notify 'Generating SSH keypair'
60
+ ssh_key = SSHData::PrivateKey::ED25519.generate
61
+ @key_file = Tempfile.create(ssh_key_name)
62
+ File.write(@key_file.path, ssh_key.openssh(comment: ssh_key_name))
63
+ @logger.notify 'Creating hcloud SSH key'
64
+ hcloud_ssh_key = @client.ssh_keys.create(
65
+ name: ssh_key_name,
66
+ public_key: ssh_key.public_key.openssh(comment: ssh_key_name),
67
+ )
68
+ @options[:ssh][:hcloud_id] = hcloud_ssh_key.id
69
+ hcloud_ssh_key
70
+ end
71
+
72
+ def create_server(host)
73
+ @logger.notify "provisioning #{host.name}"
74
+ location = host[:location] || 'nbg1'
75
+ server_type = host[:server_type] || 'cx11'
76
+ action, server = @client.servers.create(
77
+ name: host.hostname,
78
+ location: location,
79
+ server_type: server_type,
80
+ image: host[:image],
81
+ ssh_keys: [ssh_key_name],
82
+ )
83
+ while action.status == 'running'
84
+ sleep 5
85
+ action = @client.actions.find(action.id)
86
+ server = @client.servers.find(server.id)
87
+ end
88
+ host[:ip] = server.public_net['ipv4']['ip']
89
+ host[:vmhostname] = server.public_net['ipv4']['dns_ptr']
90
+ host[:hcloud_id] = server.id
91
+ host.options[:ssh][:keys] = [@key_file.path]
92
+ server
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ed25519'
4
+ require 'ssh_data'
5
+
6
+ # Patches for the 'ssh_data' gem to allow serialization of
7
+ # ed25519 private keys in OpenSSH format.
8
+ module BeakerHcloud
9
+ module SSHDataPatches
10
+ # Add encoding methods for OpenSSH's PEM-like format to
11
+ # store private keys.
12
+ module EncodingPatch
13
+ def encode_pem(data, type)
14
+ encoded_data = Base64.strict_encode64(data)
15
+ .scan(/.{1,70}/m)
16
+ .join("\n")
17
+ .chomp
18
+ <<~PEM
19
+ -----BEGIN #{type}-----
20
+ #{encoded_data}
21
+ -----END #{type}-----
22
+ PEM
23
+ end
24
+
25
+ def encode_openssh_private_key(private_key, comment = '')
26
+ public_key = private_key.public_key
27
+ private_key_data = [
28
+ (SecureRandom.random_bytes(4) * 2),
29
+ public_key.rfc4253,
30
+ encode_string(private_key.ed25519_key.seed + public_key.ed25519_key.to_str),
31
+ encode_string(comment),
32
+ ].join
33
+ unpadded = private_key_data.bytesize % 8
34
+ private_key_data << Array(1..(8 - unpadded)).pack('c*') unless unpadded.zero?
35
+ [
36
+ ::SSHData::Encoding::OPENSSH_PRIVATE_KEY_MAGIC,
37
+ encode_string('none'),
38
+ encode_string('none'),
39
+ encode_string(''),
40
+ encode_uint32(1),
41
+ encode_string(public_key.rfc4253),
42
+ encode_string(private_key_data),
43
+ ].join
44
+ end
45
+ end
46
+
47
+ # Add method to emit OpenSSH-encoded string
48
+ module Ed25519PrivateKeyPatch
49
+ def openssh(comment: '')
50
+ encoded_key = ::SSHData::Encoding.encode_openssh_private_key(
51
+ self,
52
+ comment,
53
+ )
54
+ ::SSHData::Encoding.encode_pem(
55
+ encoded_key,
56
+ 'OPENSSH PRIVATE KEY',
57
+ )
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ if defined?(SSHData)
64
+ SSHData::Encoding.extend BeakerHcloud::SSHDataPatches::EncodingPatch
65
+ SSHData::PrivateKey::ED25519.prepend BeakerHcloud::SSHDataPatches::Ed25519PrivateKeyPatch
66
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BeakerHcloud
4
+ VERSION = '1.0.0'
5
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ # rubocop:disable RSpec/MultipleMemoizedHelpers, RSpec/VerifiedDoubles, RSpec/FilePath
6
+ describe Beaker::Hcloud do
7
+ let(:logger_double) do
8
+ double(:logger).as_null_object
9
+ end
10
+ let(:options) do
11
+ opts = Beaker::Options::Presets.new
12
+ opts.presets
13
+ .merge(opts.env_vars)
14
+ .merge({
15
+ logger: logger_double,
16
+ timestamp: Time.now,
17
+ })
18
+ end
19
+ let(:host1_hash) do
20
+ {
21
+ hypervisor: 'hcloud',
22
+ image: 'ubuntu-20.04',
23
+ }
24
+ end
25
+ let(:host2_hash) do
26
+ {
27
+ hypervisor: 'hcloud',
28
+ image: 'custom image',
29
+ location: 'custom location',
30
+ server_type: 'custom type',
31
+ }
32
+ end
33
+ let(:hosts) do
34
+ [[1, host1_hash], [2, host2_hash]].map do |number, host_hash|
35
+ host_hash = Beaker::Options::OptionsHash.new.merge(host_hash)
36
+ Beaker::Host.create("Server #{number}", host_hash, options)
37
+ end
38
+ end
39
+ let(:server1) do
40
+ double(:server1,
41
+ id: 1,
42
+ public_net: {
43
+ 'ipv4' => {
44
+ 'ip' => '192.168.0.1',
45
+ 'dns_ptr' => 'server1.example.com',
46
+ },
47
+ },
48
+ destroy: true)
49
+ end
50
+ let(:server2) do
51
+ double(:server2,
52
+ id: 2,
53
+ public_net: {
54
+ 'ipv4' => {
55
+ 'ip' => '192.168.0.2',
56
+ 'dns_ptr' => 'server2.example.com',
57
+ },
58
+ },
59
+ destroy: true)
60
+ end
61
+ let(:action_double) do
62
+ double(:action, status: 'success')
63
+ end
64
+ let(:actions_double) do
65
+ double(:actions, find: action_double)
66
+ end
67
+ let(:servers_double) do
68
+ servers_double = double(:servers)
69
+ allow(servers_double).to receive(:create)
70
+ .and_return([action_double, server1], [action_double, server2])
71
+ allow(servers_double).to receive(:find)
72
+ .and_return(server1, server2)
73
+ servers_double
74
+ end
75
+ let(:key_double) do
76
+ double(:key, id: 23, destroy: true)
77
+ end
78
+ let(:ssh_keys_double) do
79
+ double(:ssh_keys, create: key_double, find: key_double)
80
+ end
81
+ let(:hcloud_client) do
82
+ double(:hcloud_client,
83
+ actions: actions_double,
84
+ servers: servers_double,
85
+ ssh_keys: ssh_keys_double)
86
+ end
87
+ let(:hcloud) do
88
+ described_class.new(hosts, options)
89
+ end
90
+
91
+ before do
92
+ ENV['BEAKER_HCLOUD_TOKEN'] = 'abc'
93
+ allow(Hcloud::Client).to receive(:new).and_return(hcloud_client)
94
+ end
95
+
96
+ describe '#provision', :aggregate_failures do
97
+ subject(:provision) { hcloud.provision }
98
+
99
+ before { provision }
100
+
101
+ after { hcloud.cleanup }
102
+
103
+ it 'uploads an ssh key using the hcloud client' do
104
+ expect(ssh_keys_double).to have_received(:create)
105
+ end
106
+
107
+ # rubocop:disable RSpec/ExampleLength
108
+ it 'creates one server for each host via the hcloud client' do
109
+ expect(servers_double).to have_received(:create)
110
+ .with(hash_including({
111
+ name: 'Server 1',
112
+ location: 'nbg1',
113
+ server_type: 'cx11',
114
+ image: 'ubuntu-20.04',
115
+ }))
116
+ expect(servers_double).to have_received(:create)
117
+ .with(hash_including({
118
+ name: 'Server 2',
119
+ location: 'custom location',
120
+ server_type: 'custom type',
121
+ image: 'custom image',
122
+ }))
123
+ end
124
+ # rubocop:enable RSpec/ExampleLength
125
+
126
+ it "saves ip and dns name to the host's settings" do
127
+ host1, host2 = hosts
128
+ expect(host1[:ip]).to eq '192.168.0.1'
129
+ expect(host1[:vmhostname]).to eq 'server1.example.com'
130
+ expect(host2[:ip]).to eq '192.168.0.2'
131
+ expect(host2[:vmhostname]).to eq 'server2.example.com'
132
+ end
133
+
134
+ it "saves hcloud's server id with the host's settings" do
135
+ host1, host2 = hosts
136
+ expect(host1[:hcloud_id]).to eq 1
137
+ expect(host2[:hcloud_id]).to eq 2
138
+ end
139
+
140
+ it "saves the path to the temporary ssh key with the host's options" do
141
+ host1, host2 = hosts
142
+ expect(host1[:ssh][:keys].size).to eq 1
143
+ expect(host2[:ssh][:keys].size).to eq 1
144
+ end
145
+ end
146
+
147
+ describe '#cleanup' do
148
+ subject(:cleanup) { hcloud.cleanup }
149
+
150
+ before do
151
+ hcloud.provision
152
+ cleanup
153
+ end
154
+
155
+ it 'destroys first server' do
156
+ expect(server1).to have_received(:destroy)
157
+ end
158
+
159
+ it 'destroys last server' do
160
+ expect(server2).to have_received(:destroy)
161
+ end
162
+
163
+ it 'destroys the temporary ssh key' do
164
+ expect(key_double).to have_received(:destroy)
165
+ end
166
+ end
167
+ end
168
+ # rubocop:enable RSpec/MultipleMemoizedHelpers, RSpec/VerifiedDoubles, RSpec/FilePath
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ if ENV['COVERAGE'] == 'yes'
4
+ require 'simplecov'
5
+ require 'simplecov-console'
6
+ require 'codecov'
7
+
8
+ SimpleCov.start do
9
+ track_files 'lib/**/*.rb'
10
+
11
+ add_filter '/spec'
12
+
13
+ enable_coverage :branch
14
+
15
+ # do not track vendored files
16
+ add_filter '/vendor'
17
+ add_filter '/.vendor'
18
+ end
19
+
20
+ SimpleCov.formatters = [
21
+ SimpleCov::Formatter::Console,
22
+ SimpleCov::Formatter::Codecov,
23
+ ]
24
+ end
25
+ $LOAD_PATH.unshift File.expand_path('lib', __dir__)
26
+
27
+ require 'beaker'
28
+ require 'beaker-hcloud/version'
29
+ require 'beaker/hypervisor/hcloud'
metadata ADDED
@@ -0,0 +1,185 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: beaker-hcloud
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Tim Meusel
8
+ - Vox Pupuli
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2023-09-22 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '13.0'
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 13.0.6
24
+ type: :development
25
+ prerelease: false
26
+ version_requirements: !ruby/object:Gem::Requirement
27
+ requirements:
28
+ - - "~>"
29
+ - !ruby/object:Gem::Version
30
+ version: '13.0'
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 13.0.6
34
+ - !ruby/object:Gem::Dependency
35
+ name: rspec
36
+ requirement: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.12'
41
+ type: :development
42
+ prerelease: false
43
+ version_requirements: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.12'
48
+ - !ruby/object:Gem::Dependency
49
+ name: voxpupuli-rubocop
50
+ requirement: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 2.0.0
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 2.0.0
62
+ - !ruby/object:Gem::Dependency
63
+ name: bcrypt_pbkdf
64
+ requirement: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
69
+ type: :runtime
70
+ prerelease: false
71
+ version_requirements: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.0'
76
+ - !ruby/object:Gem::Dependency
77
+ name: beaker
78
+ requirement: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '5.4'
83
+ type: :runtime
84
+ prerelease: false
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '5.4'
90
+ - !ruby/object:Gem::Dependency
91
+ name: ed25519
92
+ requirement: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.2'
97
+ type: :runtime
98
+ prerelease: false
99
+ version_requirements: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.2'
104
+ - !ruby/object:Gem::Dependency
105
+ name: hcloud
106
+ requirement: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: 1.0.3
111
+ - - "<"
112
+ - !ruby/object:Gem::Version
113
+ version: 2.0.0
114
+ type: :runtime
115
+ prerelease: false
116
+ version_requirements: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: 1.0.3
121
+ - - "<"
122
+ - !ruby/object:Gem::Version
123
+ version: 2.0.0
124
+ - !ruby/object:Gem::Dependency
125
+ name: ssh_data
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '1.3'
131
+ type: :runtime
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '1.3'
138
+ description: Another gem that extends beaker
139
+ email: voxpupuli@groups.io
140
+ executables: []
141
+ extensions: []
142
+ extra_rdoc_files: []
143
+ files:
144
+ - ".github/dependabot.yml"
145
+ - ".github/workflows/ci.yml"
146
+ - ".github/workflows/codeql-analysis.yml"
147
+ - ".github/workflows/release.yml"
148
+ - ".gitignore"
149
+ - ".rubocop.yml"
150
+ - ".rubocop_todo.yml"
151
+ - CHANGELOG.md
152
+ - Gemfile
153
+ - LICENSE
154
+ - README.md
155
+ - Rakefile
156
+ - beaker-hcloud.gemspec
157
+ - lib/beaker-hcloud/ssh_data_patches.rb
158
+ - lib/beaker-hcloud/version.rb
159
+ - lib/beaker/hypervisor/hcloud.rb
160
+ - spec/beaker/hypervisor/hcloud_spec.rb
161
+ - spec/spec_helper.rb
162
+ homepage: https://github.com/voxpupuli/beaker-hcloud
163
+ licenses:
164
+ - AGPL-3.0
165
+ metadata: {}
166
+ post_install_message:
167
+ rdoc_options: []
168
+ require_paths:
169
+ - lib
170
+ required_ruby_version: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - ">="
173
+ - !ruby/object:Gem::Version
174
+ version: '2.7'
175
+ required_rubygems_version: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - ">="
178
+ - !ruby/object:Gem::Version
179
+ version: '0'
180
+ requirements: []
181
+ rubygems_version: 3.2.33
182
+ signing_key:
183
+ specification_version: 4
184
+ summary: Hetzner Library for beaker acceptance testing framework
185
+ test_files: []