beaker-hcloud 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []