Skip to content

Commit 8ed7b73

Browse files
Implemented basic functionality
1 parent c99eeca commit 8ed7b73

File tree

4 files changed

+570
-11
lines changed

4 files changed

+570
-11
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*.exe
2+
*.so
3+
*.a

driver/driver.go

Lines changed: 324 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,55 @@
11
package driver
22

33
import (
4+
"fmt"
5+
"os"
6+
7+
"io/ioutil"
8+
9+
"net"
10+
11+
"time"
12+
413
"github.com/docker/machine/libmachine/drivers"
5-
"github.com/docker/machine/libmachine/ssh"
14+
"github.com/docker/machine/libmachine/log"
615
"github.com/docker/machine/libmachine/mcnflag"
16+
"github.com/docker/machine/libmachine/mcnutils"
17+
mcnssh "github.com/docker/machine/libmachine/ssh"
18+
"github.com/docker/machine/libmachine/state"
19+
"github.com/jonasprogrammer/docker-machine-driver-hetzner/driver/hetzner"
20+
"golang.org/x/crypto/ssh"
721
)
822

923
type Driver struct {
1024
*drivers.BaseDriver
1125

12-
SSHKeyPair *ssh.KeyPair
26+
AccessToken string
27+
Image string
28+
Type string
29+
Location string
30+
KeyID int
31+
IsExistingKey bool
32+
originalKey string
33+
ServerID int
1334
}
1435

36+
const (
37+
defaultImage = "debian-9"
38+
defaultType = "g2-local"
39+
40+
flagApiToken = "hetzner-api-token"
41+
flagImage = "hetzner-image"
42+
flagType = "hetzner-server-type"
43+
flagLocation = "hetzner-server-location"
44+
flagExKeyId = "hetzner-existing-key-id"
45+
flagExKeyPath = "hetzner-existing-key-path"
46+
)
47+
1548
func NewDriver() *Driver {
1649
return &Driver{
50+
Image: defaultImage,
51+
Type: defaultType,
52+
IsExistingKey: false,
1753
BaseDriver: &drivers.BaseDriver{
1854
SSHUser: drivers.DefaultSSHUser,
1955
SSHPort: drivers.DefaultSSHPort,
@@ -25,19 +61,297 @@ func (d *Driver) DriverName() string {
2561
return "hetzner"
2662
}
2763

28-
const (
29-
defaultImage = "debian-9"
30-
defaultType = "g2-local"
31-
defaultLocation = "fsn1"
32-
)
33-
3464
func (d *Driver) GetCreateFlags() []mcnflag.Flag {
3565
return []mcnflag.Flag{
3666
mcnflag.StringFlag{
3767
EnvVar: "HETZNER_API_TOKEN",
38-
Name: "hetzner-api-token",
39-
Usage: "Your project-specific Hetzner API token",
68+
Name: flagApiToken,
69+
Usage: "Project-specific Hetzner API token",
70+
Value: "",
71+
},
72+
73+
mcnflag.StringFlag{
74+
EnvVar: "HETZNER_IMAGE",
75+
Name: flagImage,
76+
Usage: "Image to use for server creation",
77+
Value: defaultImage,
78+
},
79+
80+
mcnflag.StringFlag{
81+
EnvVar: "HETZNER_TYPE",
82+
Name: flagType,
83+
Usage: "Server type to create",
84+
Value: defaultType,
85+
},
86+
87+
mcnflag.StringFlag{
88+
EnvVar: "HETZNER_LOCATION",
89+
Name: flagLocation,
90+
Usage: "Location to create machine at",
91+
Value: "",
92+
},
93+
94+
mcnflag.IntFlag{
95+
EnvVar: "HETZNER_EXISTING_KEY_ID",
96+
Name: flagExKeyId,
97+
Usage: "Existing key ID to use for server; requires --hetzner-existing-key-path",
98+
Value: 0,
99+
},
100+
101+
mcnflag.StringFlag{
102+
EnvVar: "HETZNER_EXISTING_KEY_PATH",
103+
Name: flagExKeyPath,
104+
Usage: "Path to existing key (new public key will be created unless --hetzner-existing-key-id is specified)",
40105
Value: "",
41106
},
42107
}
43108
}
109+
110+
func (d *Driver) SetConfigFromFlags(opts drivers.DriverOptions) error {
111+
d.AccessToken = opts.String(flagApiToken)
112+
d.Image = opts.String(flagImage)
113+
d.Location = opts.String(flagLocation)
114+
d.Type = opts.String(flagType)
115+
d.KeyID = opts.Int(flagExKeyId)
116+
d.IsExistingKey = d.KeyID != 0
117+
d.originalKey = opts.String(flagExKeyPath)
118+
119+
d.SetSwarmConfigFromFlags(opts)
120+
121+
if d.AccessToken == "" {
122+
return fmt.Errorf("hetnzer erquires --%v to be set", flagApiToken)
123+
}
124+
125+
return nil
126+
}
127+
128+
func (d *Driver) PreCreateCheck() error {
129+
// TODO: Validate location, type and image to exist
130+
131+
if d.IsExistingKey {
132+
if d.originalKey == "" {
133+
return fmt.Errorf("specifing an existing key ID requires the existing key path to be set as well")
134+
}
135+
136+
key, err := d.getClient().GetSSHKey(d.KeyID)
137+
138+
if err != nil {
139+
return err
140+
}
141+
142+
buf, err := ioutil.ReadFile(d.originalKey + ".pub")
143+
if err != nil {
144+
return err
145+
}
146+
147+
pubk, err := ssh.ParsePublicKey(buf)
148+
149+
if key.Fingerprint != ssh.FingerprintLegacyMD5(pubk) &&
150+
key.Fingerprint != ssh.FingerprintSHA256(pubk) {
151+
return fmt.Errorf("remote key %d does not fit with local key %s", d.KeyID, d.originalKey)
152+
}
153+
}
154+
155+
return nil
156+
}
157+
158+
func (d *Driver) Create() error {
159+
if d.KeyID == 0 {
160+
if d.originalKey != "" {
161+
log.Debugf("Copying SSH key...")
162+
if err := d.copySSHKeyPair(d.originalKey); err != nil {
163+
return err
164+
}
165+
} else {
166+
log.Debugf("Generating SSH key...")
167+
if err := mcnssh.GenerateSSHKey(d.GetSSHKeyPath()); err != nil {
168+
return err
169+
}
170+
}
171+
172+
log.Infof("Creating SSH key...")
173+
174+
buf, err := ioutil.ReadFile(d.GetSSHKeyPath() + ".pub")
175+
if err != nil {
176+
return err
177+
}
178+
179+
key, err := d.getClient().CreateSSHKey(d.GetMachineName(), string(buf))
180+
if err != nil {
181+
return err
182+
}
183+
184+
d.KeyID = key.Id
185+
}
186+
187+
log.Infof("Creating Hetzner server...")
188+
189+
srv, act, err := d.getClient().CreateServer(d.GetMachineName(), d.Type, d.Image, d.Location, d.KeyID)
190+
191+
if err != nil {
192+
return err
193+
}
194+
195+
log.Debugf(" -> Creating action %d, server %s[%d]", act.Id, srv.Name, srv.Id)
196+
197+
for {
198+
act, err = d.getClient().GetAction(act.Id)
199+
200+
if err != nil {
201+
return err
202+
}
203+
204+
if act.Status == "success" {
205+
log.Debugf(" -> Finished create action %d", act.Id)
206+
break
207+
} else if act.Status == "running" {
208+
log.Debugf(" -> Create action[%d]: %d %%", act.Id, act.Progress)
209+
} else if act.Status == "error" {
210+
if act.Error != nil {
211+
return fmt.Errorf("create action %d %s: %s", act.Id, act.Error.Code, act.Error.Message)
212+
} else {
213+
return fmt.Errorf("create action %d: failed for unknown reason", act.Id)
214+
}
215+
}
216+
217+
time.Sleep(1 * time.Second)
218+
}
219+
220+
d.ServerID = srv.Id
221+
log.Infof(" -> Server %s[%d]: Waiting to come up...", srv.Name, srv.Id)
222+
223+
for {
224+
srvstate, err := d.GetState()
225+
226+
if err != nil {
227+
return err
228+
}
229+
230+
if srvstate == state.Running {
231+
break
232+
}
233+
234+
time.Sleep(1 * time.Second)
235+
}
236+
237+
log.Debugf(" -> Server %s[%d] ready", srv.Name, srv.Id)
238+
d.IPAddress = srv.PublicNet.IPv4.IP
239+
240+
return nil
241+
}
242+
243+
func (d *Driver) GetSSHHostname() (string, error) {
244+
return d.GetIP()
245+
}
246+
247+
func (d *Driver) GetURL() (string, error) {
248+
if err := drivers.MustBeRunning(d); err != nil {
249+
return "", err
250+
}
251+
252+
ip, err := d.GetIP()
253+
if err != nil {
254+
return "", err
255+
}
256+
257+
return fmt.Sprintf("tcp://%s", net.JoinHostPort(ip, "2376")), nil
258+
}
259+
260+
func (d *Driver) GetState() (state.State, error) {
261+
srv, err := d.getClient().GetServer(d.ServerID)
262+
263+
if err != nil {
264+
return state.None, err
265+
}
266+
267+
switch srv.Status {
268+
case "initializing":
269+
case "starting":
270+
return state.Starting, nil
271+
case "running":
272+
return state.Running, nil
273+
case "stopping":
274+
return state.Stopping, nil
275+
case "off":
276+
return state.Stopped, nil
277+
}
278+
return state.None, nil
279+
}
280+
281+
func (d *Driver) Kill() error {
282+
panic("implement me")
283+
}
284+
285+
func (d *Driver) Remove() error {
286+
act, err := d.getClient().DeleteServer(d.ServerID)
287+
288+
if err != nil {
289+
return err
290+
}
291+
292+
log.Infof(" -> Destroying server %d, action %d...", d.ServerID, act.Id)
293+
294+
for {
295+
act, err = d.getClient().GetAction(act.Id)
296+
297+
if err != nil {
298+
return err
299+
}
300+
301+
if act.Status == "success" {
302+
log.Infof(" -> Finished destroy action %d", act.Id)
303+
break
304+
} else if act.Status == "running" {
305+
log.Infof(" -> Destroy action[%d]: %d %%", act.Id, act.Progress)
306+
} else if act.Status == "error" {
307+
if act.Error != nil {
308+
return fmt.Errorf("destroy action %d %s: %s", act.Id, act.Error.Code, act.Error.Message)
309+
} else {
310+
return fmt.Errorf("destroy action %d: failed for unknown reason", act.Id)
311+
}
312+
}
313+
314+
time.Sleep(1 * time.Second)
315+
}
316+
317+
if !d.IsExistingKey {
318+
log.Infof(" -> Destroying SSHkey %d...", d.KeyID)
319+
if err := d.getClient().DeleteSSHKey(d.KeyID); err != nil {
320+
return err
321+
}
322+
}
323+
324+
return nil
325+
}
326+
327+
func (d *Driver) Restart() error {
328+
panic("implement me")
329+
}
330+
331+
func (d *Driver) Start() error {
332+
panic("implement me")
333+
}
334+
335+
func (d *Driver) Stop() error {
336+
panic("implement me")
337+
}
338+
339+
func (d *Driver) getClient() *hetzner.Client {
340+
return hetzner.NewClient(d.AccessToken)
341+
}
342+
343+
func (d *Driver) copySSHKeyPair(src string) error {
344+
if err := mcnutils.CopyFile(src, d.GetSSHKeyPath()); err != nil {
345+
return fmt.Errorf("unable to copy ssh key: %s", err)
346+
}
347+
348+
if err := mcnutils.CopyFile(src+".pub", d.GetSSHKeyPath()+".pub"); err != nil {
349+
return fmt.Errorf("unable to copy ssh public key: %s", err)
350+
}
351+
352+
if err := os.Chmod(d.GetSSHKeyPath(), 0600); err != nil {
353+
return fmt.Errorf("unable to set permissions on the ssh key: %s", err)
354+
}
355+
356+
return nil
357+
}

driver/hetzner.go

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)