11package driver
22
33import (
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
923type 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+
1548func 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-
3464func (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+ }
0 commit comments