hetzner-cli 0.0.2
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/.gitignore +2 -0
- data/.rvmrc +6 -0
- data/Gemfile +7 -0
- data/README.md +53 -0
- data/Rakefile +6 -0
- data/bin/hetzner-cli +8 -0
- data/hetzner-cli.gemspec +30 -0
- data/lib/hetzner-cli.rb +3 -0
- data/lib/hetzner-cli/cli.rb +33 -0
- data/lib/hetzner-cli/command.rb +9 -0
- data/lib/hetzner-cli/command/distributions.rb +40 -0
- data/lib/hetzner-cli/command/kickstart.rb +116 -0
- data/lib/hetzner-cli/version.rb +3 -0
- metadata +179 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# Hetnzer CLI
|
2
|
+
## Purpose
|
3
|
+
[Hetzner](http://www.hetzner.de) is a hoster that allows you to get 'real' physical machines instead of all the 'virtualized' cloud stuff.
|
4
|
+
This is especially handy for testing virtualization stuff.
|
5
|
+
|
6
|
+
They provide a 'REST' like api that you can use to call separate tasks , like reset a server, install linux etc..
|
7
|
+
For most of the simple tasks,one could get away with using 'curl', but some tasks (like re-installing a server) require more coordination
|
8
|
+
|
9
|
+
1. purpose #1 : of this is to automate these 'combined' hetnzer tasks via CLI instead of using their webinterface.
|
10
|
+
2. purpose #2 : me to learn better their api and potentially put this into [fog](http://fog.io)
|
11
|
+
3. purpose #3 : extend [mccloud](http://github.com/jedi4ever/mccloud) to allow for Hetnzer support
|
12
|
+
|
13
|
+
## Tasks implemented
|
14
|
+
|
15
|
+
### Distributions
|
16
|
+
|
17
|
+
Usage:
|
18
|
+
hetzner-cli distributions IP --password=PASSWORD --user=USER
|
19
|
+
|
20
|
+
Options:
|
21
|
+
--user=USER # Hetzner Admin Username
|
22
|
+
--password=PASSWORD # Hetzner Admin Password
|
23
|
+
[--robot-url=ROBOT_URL] # URL to connect to hetzner robo service
|
24
|
+
# Default: https://robot-ws.your-server.de/
|
25
|
+
|
26
|
+
List availble distributions for IP
|
27
|
+
|
28
|
+
### Kickstart
|
29
|
+
The tasks of re-installing a server from scratch and putting an initial ssh key on it
|
30
|
+
|
31
|
+
Usage:
|
32
|
+
hetzner-cli kickstart IP --dist=DIST --password=PASSWORD --user=USER
|
33
|
+
|
34
|
+
Options:
|
35
|
+
[--lang=LANG] # Architecture to use
|
36
|
+
# Default: en
|
37
|
+
[--arch=ARCH] # Architecture to use (32|64)
|
38
|
+
# Default: 64
|
39
|
+
--user=USER # Hetzner Admin Username
|
40
|
+
--password=PASSWORD # Hetzner Admin Password
|
41
|
+
[--robot-url=ROBOT_URL] # URL to connect to hetzner robo service
|
42
|
+
# Default: https://robot-ws.your-server.de/
|
43
|
+
[--key-file=KEY_FILE] # SSH key to install as root user
|
44
|
+
# Default: /Users/patrick/.ssh/id_dsa.pub
|
45
|
+
--dist=DIST # Distribution to use
|
46
|
+
|
47
|
+
Re-install server with IP
|
48
|
+
|
49
|
+
## Todo
|
50
|
+
|
51
|
+
- obviously make it catch errors more and write tests
|
52
|
+
- potentially integrate the functionality into fog with a hetzner provider
|
53
|
+
- look into using the hetzner-api plugin to leverage all the API calls
|
data/Rakefile
ADDED
data/bin/hetzner-cli
ADDED
data/hetzner-cli.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path("../lib/hetzner-cli/version", __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "hetzner-cli"
|
6
|
+
s.version = HetznerCli::VERSION
|
7
|
+
s.platform = Gem::Platform::RUBY
|
8
|
+
s.authors = ["Patrick Debois"]
|
9
|
+
s.email = ["patrick.debois@jedi.be"]
|
10
|
+
s.homepage = "http://github.com/jedi4ever/hetzner-cli/"
|
11
|
+
s.summary = %q{Manage a Hetzner machine}
|
12
|
+
s.description = %q{Manage a Hetzner machine}
|
13
|
+
|
14
|
+
s.required_rubygems_version = ">= 1.3.6"
|
15
|
+
s.rubyforge_project = "hetzner-cli"
|
16
|
+
|
17
|
+
s.add_dependency "thor"
|
18
|
+
s.add_dependency "yajl-ruby"
|
19
|
+
s.add_dependency "json"
|
20
|
+
s.add_dependency "excon"
|
21
|
+
s.add_dependency "net-ssh"
|
22
|
+
s.add_dependency "system_timer"
|
23
|
+
|
24
|
+
s.add_development_dependency "bundler", ">= 1.0.0"
|
25
|
+
|
26
|
+
s.files = `git ls-files`.split("\n")
|
27
|
+
s.executables = `git ls-files`.split("\n").map{ |f| f =~ /^bin\/(.*)/ ? $1 : nil }.compact
|
28
|
+
s.require_path = 'lib'
|
29
|
+
end
|
30
|
+
|
data/lib/hetzner-cli.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
require 'hetzner-cli/command'
|
4
|
+
|
5
|
+
module HetznerCli
|
6
|
+
class CLI < Thor
|
7
|
+
|
8
|
+
include HetznerCli::Command
|
9
|
+
|
10
|
+
desc "kickstart IP", "Re-install server with IP"
|
11
|
+
method_option :robot_url , :default => 'https://robot-ws.your-server.de/', :desc => "URL to connect to hetzner robo service"
|
12
|
+
method_option :user, :desc => 'Hetzner Admin Username', :required => true
|
13
|
+
method_option :password, :desc => 'Hetzner Admin Password', :required => true
|
14
|
+
method_option :dist, :desc => "Distribution to use", :required => true
|
15
|
+
method_option :arch, :default => '64', :desc => "Architecture to use (32|64)"
|
16
|
+
method_option :key_file, :default => File.join(ENV['HOME'],'.ssh','id_dsa.pub'), :desc => "SSH key to install as root user"
|
17
|
+
method_option :lang, :default => 'en', :desc => "Architecture to use"
|
18
|
+
|
19
|
+
def kickstart(ip)
|
20
|
+
_kickstart(ip,options)
|
21
|
+
end
|
22
|
+
|
23
|
+
desc "distributions IP", "List availble distributions for IP"
|
24
|
+
method_option :robot_url , :default => 'https://robot-ws.your-server.de/', :desc => "URL to connect to hetzner robo service"
|
25
|
+
method_option :user, :desc => 'Hetzner Admin Username', :required => true
|
26
|
+
method_option :password, :desc => 'Hetzner Admin Password', :required => true
|
27
|
+
|
28
|
+
def distributions(ip)
|
29
|
+
_distributions(ip,options)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module HetznerCli
|
2
|
+
module Distributions
|
3
|
+
|
4
|
+
require 'pp'
|
5
|
+
require 'faraday'
|
6
|
+
require 'json'
|
7
|
+
require 'net/ssh'
|
8
|
+
|
9
|
+
def _distributions(ip,options)
|
10
|
+
user = options['user']
|
11
|
+
password = options['password']
|
12
|
+
robot_url = options['robot_url']
|
13
|
+
|
14
|
+
# Create connection
|
15
|
+
conn = Faraday.new(:url => robot_url) do |faraday|
|
16
|
+
faraday.request :url_encoded # form-encode POST params
|
17
|
+
#faraday.response :logger # log requests to STDOUT
|
18
|
+
faraday.adapter Faraday.default_adapter # make requests with Net::HTTP
|
19
|
+
end
|
20
|
+
|
21
|
+
# Set credentials
|
22
|
+
conn.basic_auth(user,password)
|
23
|
+
|
24
|
+
begin
|
25
|
+
# Get a list of available distributions
|
26
|
+
puts "[#{ip}] Available distributions:"
|
27
|
+
response = conn.get("/boot/#{ip}")
|
28
|
+
boot_info = JSON.parse(response.body)
|
29
|
+
distributions = boot_info['boot']['linux']['dist']
|
30
|
+
distributions.each do |distro|
|
31
|
+
puts "[#{ip}] - #{distro}"
|
32
|
+
end
|
33
|
+
rescue Faraday::Error::ConnectionFailed => ex
|
34
|
+
$stderr.puts "Error logging in #{ex}"
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module HetznerCli
|
2
|
+
module Kickstart
|
3
|
+
|
4
|
+
require 'pp'
|
5
|
+
require 'faraday'
|
6
|
+
require 'json'
|
7
|
+
require 'net/ssh'
|
8
|
+
|
9
|
+
def _kickstart(ip,options)
|
10
|
+
user = options['user']
|
11
|
+
password = options['password']
|
12
|
+
robot_url = options['robot_url']
|
13
|
+
dist = options['dist']
|
14
|
+
lang = options['lang']
|
15
|
+
arch = options['arch']
|
16
|
+
key_file = options['key_file']
|
17
|
+
key = ''
|
18
|
+
|
19
|
+
# Reading keyfile
|
20
|
+
begin
|
21
|
+
key = File.read(key_file)
|
22
|
+
rescue Error => ex
|
23
|
+
$stderr.puts "[#{ip}] Error reading key_file"
|
24
|
+
exit -1
|
25
|
+
end
|
26
|
+
|
27
|
+
# Create connection
|
28
|
+
conn = Faraday.new(:url => robot_url) do |faraday|
|
29
|
+
faraday.request :url_encoded # form-encode POST params
|
30
|
+
#faraday.response :logger # log requests to STDOUT
|
31
|
+
faraday.adapter Faraday.default_adapter # make requests with Net::HTTP
|
32
|
+
end
|
33
|
+
|
34
|
+
# Set credentials
|
35
|
+
conn.basic_auth(user,password)
|
36
|
+
|
37
|
+
# Check if new installation was already requested
|
38
|
+
unless installing?(conn,ip)
|
39
|
+
|
40
|
+
# Get a list of available distributions
|
41
|
+
puts "[#{ip}] Available distributions:"
|
42
|
+
response = conn.get("/boot/#{ip}")
|
43
|
+
boot_info = JSON.parse(response.body)
|
44
|
+
distributions = boot_info['boot']['linux']['dist']
|
45
|
+
distributions.each do |distro|
|
46
|
+
puts "[#{ip}] - #{distro}"
|
47
|
+
end
|
48
|
+
|
49
|
+
# Bail out if the distribution specified is not listed
|
50
|
+
unless distributions.include?(dist)
|
51
|
+
$stderr.puts "[#{ip}] The specified distribution '#{dist}' is not available for server with"
|
52
|
+
exit -1
|
53
|
+
else
|
54
|
+
puts "[#{ip}] Distribution selected: #{dist}"
|
55
|
+
end
|
56
|
+
|
57
|
+
# Trigger a new linux install on reboot
|
58
|
+
puts "[#{ip}] Activating new linux install: distribution '#{dist}', arch '#{arch}', lang '#{lang}'"
|
59
|
+
response = conn.post("/boot/#{ip}/linux", { :dist => dist , :arch => arch , :lang => lang})
|
60
|
+
linux_info = JSON.parse(response.body)
|
61
|
+
new_password = linux_info['linux']['password']
|
62
|
+
|
63
|
+
puts "[#{ip}] Sending hw reset"
|
64
|
+
# Hardware reboot the system
|
65
|
+
response = conn.post("/reset/#{ip}", { :type => 'hw'})
|
66
|
+
|
67
|
+
# Allowing the system to go down, saves us from checking bad authentication errors
|
68
|
+
puts "[#{ip}] New Password : #{new_password}"
|
69
|
+
|
70
|
+
begin
|
71
|
+
fully_booted = false
|
72
|
+
print "[#{ip}] Waiting for the linux install to finish and reboot: "
|
73
|
+
STDOUT.flush
|
74
|
+
4.times {
|
75
|
+
sleep 5
|
76
|
+
print '.'
|
77
|
+
STDOUT.flush
|
78
|
+
}
|
79
|
+
while !fully_booted do
|
80
|
+
sleep 5
|
81
|
+
print '.'
|
82
|
+
STDOUT.flush
|
83
|
+
begin
|
84
|
+
# Ignoring new key
|
85
|
+
Net::SSH.start(ip,'root',:password => new_password , :paranoid => false, :timeout => 5 ) do |ssh|
|
86
|
+
output = ssh.exec!("cat /root/.ssh/authorized_keys2")
|
87
|
+
if output.include?("No such file")
|
88
|
+
puts
|
89
|
+
puts "[#{ip}] Install is finished and system is available"
|
90
|
+
puts "[#{ip}] Installing root ssh key"
|
91
|
+
output = ssh.exec!("echo '#{key}' > /root/.ssh/authorized_keys")
|
92
|
+
fully_booted = true
|
93
|
+
end
|
94
|
+
end
|
95
|
+
rescue Net::SSH::Disconnect,Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ECONNABORTED, Errno::ECONNRESET, Errno::ENETUNREACH,Errno::ETIMEDOUT,IOError,Timeout::Error,Net::SSH::AuthenticationFailed
|
96
|
+
#Ignoring these errors
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
puts "[#{ip}] System is ready for login"
|
101
|
+
puts "[#{ip}] You might want to cleanup your old key:"
|
102
|
+
puts "[#{ip}] ssh-keygen -R #{ip}"
|
103
|
+
puts "[#{ip}] ssh-keyscan #{ip} >> $HOME/.ssh/known_hosts"
|
104
|
+
else
|
105
|
+
$stderr.puts "[#{ip}] Installation already in progress, aborting"
|
106
|
+
exit -1
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def installing?(conn,ip)
|
111
|
+
response = conn.get("/boot/#{ip}/linux")
|
112
|
+
linux_info = JSON.parse(response.body)
|
113
|
+
return linux_info['linux']['active'] == true
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
metadata
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hetzner-cli
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 2
|
10
|
+
version: 0.0.2
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Patrick Debois
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-08-15 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
22
|
+
none: false
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
hash: 3
|
27
|
+
segments:
|
28
|
+
- 0
|
29
|
+
version: "0"
|
30
|
+
type: :runtime
|
31
|
+
name: thor
|
32
|
+
version_requirements: *id001
|
33
|
+
prerelease: false
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
36
|
+
none: false
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
hash: 3
|
41
|
+
segments:
|
42
|
+
- 0
|
43
|
+
version: "0"
|
44
|
+
type: :runtime
|
45
|
+
name: yajl-ruby
|
46
|
+
version_requirements: *id002
|
47
|
+
prerelease: false
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
hash: 3
|
55
|
+
segments:
|
56
|
+
- 0
|
57
|
+
version: "0"
|
58
|
+
type: :runtime
|
59
|
+
name: json
|
60
|
+
version_requirements: *id003
|
61
|
+
prerelease: false
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
hash: 3
|
69
|
+
segments:
|
70
|
+
- 0
|
71
|
+
version: "0"
|
72
|
+
type: :runtime
|
73
|
+
name: excon
|
74
|
+
version_requirements: *id004
|
75
|
+
prerelease: false
|
76
|
+
- !ruby/object:Gem::Dependency
|
77
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
78
|
+
none: false
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
hash: 3
|
83
|
+
segments:
|
84
|
+
- 0
|
85
|
+
version: "0"
|
86
|
+
type: :runtime
|
87
|
+
name: net-ssh
|
88
|
+
version_requirements: *id005
|
89
|
+
prerelease: false
|
90
|
+
- !ruby/object:Gem::Dependency
|
91
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
92
|
+
none: false
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
hash: 3
|
97
|
+
segments:
|
98
|
+
- 0
|
99
|
+
version: "0"
|
100
|
+
type: :runtime
|
101
|
+
name: system_timer
|
102
|
+
version_requirements: *id006
|
103
|
+
prerelease: false
|
104
|
+
- !ruby/object:Gem::Dependency
|
105
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
hash: 23
|
111
|
+
segments:
|
112
|
+
- 1
|
113
|
+
- 0
|
114
|
+
- 0
|
115
|
+
version: 1.0.0
|
116
|
+
type: :development
|
117
|
+
name: bundler
|
118
|
+
version_requirements: *id007
|
119
|
+
prerelease: false
|
120
|
+
description: Manage a Hetzner machine
|
121
|
+
email:
|
122
|
+
- patrick.debois@jedi.be
|
123
|
+
executables:
|
124
|
+
- hetzner-cli
|
125
|
+
extensions: []
|
126
|
+
|
127
|
+
extra_rdoc_files: []
|
128
|
+
|
129
|
+
files:
|
130
|
+
- .gitignore
|
131
|
+
- .rvmrc
|
132
|
+
- Gemfile
|
133
|
+
- README.md
|
134
|
+
- Rakefile
|
135
|
+
- bin/hetzner-cli
|
136
|
+
- hetzner-cli.gemspec
|
137
|
+
- lib/hetzner-cli.rb
|
138
|
+
- lib/hetzner-cli/cli.rb
|
139
|
+
- lib/hetzner-cli/command.rb
|
140
|
+
- lib/hetzner-cli/command/distributions.rb
|
141
|
+
- lib/hetzner-cli/command/kickstart.rb
|
142
|
+
- lib/hetzner-cli/version.rb
|
143
|
+
homepage: http://github.com/jedi4ever/hetzner-cli/
|
144
|
+
licenses: []
|
145
|
+
|
146
|
+
post_install_message:
|
147
|
+
rdoc_options: []
|
148
|
+
|
149
|
+
require_paths:
|
150
|
+
- lib
|
151
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
152
|
+
none: false
|
153
|
+
requirements:
|
154
|
+
- - ">="
|
155
|
+
- !ruby/object:Gem::Version
|
156
|
+
hash: 3
|
157
|
+
segments:
|
158
|
+
- 0
|
159
|
+
version: "0"
|
160
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
161
|
+
none: false
|
162
|
+
requirements:
|
163
|
+
- - ">="
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
hash: 23
|
166
|
+
segments:
|
167
|
+
- 1
|
168
|
+
- 3
|
169
|
+
- 6
|
170
|
+
version: 1.3.6
|
171
|
+
requirements: []
|
172
|
+
|
173
|
+
rubyforge_project: hetzner-cli
|
174
|
+
rubygems_version: 1.8.12
|
175
|
+
signing_key:
|
176
|
+
specification_version: 3
|
177
|
+
summary: Manage a Hetzner machine
|
178
|
+
test_files: []
|
179
|
+
|