feat: log output

This commit is contained in:
Julian Tölle 2024-04-30 23:48:59 +02:00
parent 4f57df5b66
commit 904e5e0bed
8 changed files with 133 additions and 23 deletions

View file

@ -3,12 +3,12 @@ package hcloud_upload_image
import (
"context"
"fmt"
"log"
"time"
"github.com/hetznercloud/hcloud-go/v2/hcloud"
"golang.org/x/crypto/ssh"
"github.com/apricote/hcloud-upload-image/util/contextlogger"
"github.com/apricote/hcloud-upload-image/util/control"
"github.com/apricote/hcloud-upload-image/util/randomid"
"github.com/apricote/hcloud-upload-image/util/sshkey"
@ -50,15 +50,21 @@ type snapshotClient struct {
}
func (s snapshotClient) Upload(ctx context.Context, options UploadOptions) (*hcloud.Image, error) {
logger := contextlogger.From(ctx).With(
"library", "hcloud-upload-image",
"method", "upload",
)
id, err := randomid.Generate()
if err != nil {
return nil, err
}
logger = logger.With("run-id", id)
// For simplicity, we use the name random name for SSH Key + Server
resourceName := resourcePrefix + id
// 1. Create SSH Key
log.Print("Step 1: Generating SSH Key")
logger.InfoContext(ctx, "# Step 1: Generating SSH Key")
publicKey, privateKey, err := sshkey.GenerateKeyPair()
if err != nil {
return nil, fmt.Errorf("failed to generate temporary ssh key pair: %w", err)
@ -72,25 +78,35 @@ func (s snapshotClient) Upload(ctx context.Context, options UploadOptions) (*hcl
if err != nil {
return nil, fmt.Errorf("failed to submit temporary ssh key to API: %w", err)
}
logger.DebugContext(ctx, "Uploaded ssh key", "ssh-key-id", key.ID)
defer func() {
// Cleanup SSH Key
if options.DebugSkipResourceCleanup {
logger.InfoContext(ctx, "Cleanup: Skipping cleanup of temporary ssh key")
return
}
logger.InfoContext(ctx, "Cleanup: Deleting temporary ssh key")
_, err := s.client.SSHKey.Delete(ctx, key)
if err != nil {
logger.WarnContext(ctx, "Cleanup: ssh key could not be deleted", "error", err)
// TODO
}
}()
// 2. Create Server
log.Print("Step 2: Creating Server")
logger.InfoContext(ctx, "# Step 2: Creating Server")
serverType, ok := serverTypePerArchitecture[options.Architecture]
if !ok {
return nil, fmt.Errorf("unknown architecture %q, valid options: %q, %q", options.Architecture, hcloud.ArchitectureX86, hcloud.ArchitectureARM)
}
logger.DebugContext(ctx, "creating server with config",
"image", defaultImage.Name,
"location", defaultLocation.Name,
"serverType", serverType.Name,
)
serverCreateResult, _, err := s.client.Server.Create(ctx, hcloud.ServerCreateOpts{
Name: resourceName,
ServerType: serverType,
@ -108,27 +124,34 @@ func (s snapshotClient) Upload(ctx context.Context, options UploadOptions) (*hcl
if err != nil {
return nil, fmt.Errorf("creating the temporary server failed: %w", err)
}
logger = logger.With("server", serverCreateResult.Server.ID)
logger.DebugContext(ctx, "Created Server")
_, err = s.client.Action.WaitForActions(ctx, append(serverCreateResult.NextActions, serverCreateResult.Action)...)
logger.DebugContext(ctx, "waiting on actions")
err = s.client.Action.WaitFor(ctx, append(serverCreateResult.NextActions, serverCreateResult.Action)...)
if err != nil {
return nil, fmt.Errorf("creating the temporary server failed: %w", err)
}
logger.DebugContext(ctx, "actions finished")
server := serverCreateResult.Server
defer func() {
// Cleanup Server
if options.DebugSkipResourceCleanup {
logger.InfoContext(ctx, "Cleanup: Skipping cleanup of temporary server")
return
}
logger.InfoContext(ctx, "Cleanup: Deleting temporary server")
_, _, err := s.client.Server.DeleteWithResult(ctx, server)
if err != nil {
// TODO
logger.WarnContext(ctx, "Cleanup: server could not be deleted", "error", err)
}
}()
// 3. Activate Rescue System
log.Print("Step 3: Activating Rescue System")
logger.InfoContext(ctx, "# Step 4: Activating Rescue System")
enableRescueResult, _, err := s.client.Server.EnableRescue(ctx, server, hcloud.ServerEnableRescueOpts{
Type: defaultRescueType,
SSHKeys: []*hcloud.SSHKey{key},
@ -137,25 +160,31 @@ func (s snapshotClient) Upload(ctx context.Context, options UploadOptions) (*hcl
return nil, fmt.Errorf("enabling the rescue system on the temporary server failed: %w", err)
}
_, err = s.client.Action.WaitForAction(ctx, enableRescueResult.Action)
logger.DebugContext(ctx, "rescue system requested, waiting on action")
err = s.client.Action.WaitFor(ctx, enableRescueResult.Action)
if err != nil {
return nil, fmt.Errorf("enabling the rescue system on the temporary server failed: %w", err)
}
logger.DebugContext(ctx, "action finished, rescue system enabled")
// 4. Boot Server
log.Print("Step 4: Booting Server")
logger.InfoContext(ctx, "# Step 4: Booting Server")
powerOnAction, _, err := s.client.Server.Poweron(ctx, server)
if err != nil {
return nil, fmt.Errorf("starting the temporary server failed: %w", err)
}
_, err = s.client.Action.WaitForAction(ctx, powerOnAction)
logger.DebugContext(ctx, "boot requested, waiting on action")
err = s.client.Action.WaitFor(ctx, powerOnAction)
if err != nil {
return nil, fmt.Errorf("starting the temporary server failed: %w", err)
}
logger.DebugContext(ctx, "action finished, server is booting")
// 5. Open SSH Session
log.Print("Step 5: Opening SSH Connection")
logger.InfoContext(ctx, "# Step 5: Opening SSH Connection")
signer, err := ssh.ParsePrivateKey(privateKey)
if err != nil {
return nil, fmt.Errorf("parsing the automatically generated temporary private key failed: %w", err)
@ -174,18 +203,23 @@ func (s snapshotClient) Upload(ctx context.Context, options UploadOptions) (*hcl
// the server needs some time until its properly started and ssh is available
var sshClient *ssh.Client
err = control.Retry(ctx, 10, func() error {
var err error
sshClient, err = ssh.Dial("tcp", server.PublicNet.IPv4.IP.String()+":ssh", sshClientConfig)
return err
})
err = control.Retry(
contextlogger.New(ctx, logger.With("operation", "ssh")),
10,
func() error {
var err error
logger.DebugContext(ctx, "trying to connect to server", "ip", server.PublicNet.IPv4.IP)
sshClient, err = ssh.Dial("tcp", server.PublicNet.IPv4.IP.String()+":ssh", sshClientConfig)
return err
},
)
if err != nil {
return nil, fmt.Errorf("failed to ssh into temporary server: %w", err)
}
defer sshClient.Close()
// 6. SSH On Server: Download Image, Decompress, Write to Root Disk
log.Print("Step 6: Downloading image and writing to disk")
logger.InfoContext(ctx, "# Step 6: Downloading image and writing to disk")
decompressionCommand := ""
if options.ImageCompression != CompressionNone {
switch options.ImageCompression {
@ -196,22 +230,26 @@ func (s snapshotClient) Upload(ctx context.Context, options UploadOptions) (*hcl
}
}
output, err := sshsession.Run(sshClient, fmt.Sprintf("wget --no-verbose -O - %q %s | dd of=/dev/sda && sync", options.ImageURL.String(), decompressionCommand))
log.Print(string(output))
fullCmd := fmt.Sprintf("wget --no-verbose -O - %q %s | dd of=/dev/sda bs=4M && sync", options.ImageURL.String(), decompressionCommand)
logger.DebugContext(ctx, "running download, decompress and write to disk command", "cmd", fullCmd)
output, err := sshsession.Run(sshClient, fullCmd)
logger.InfoContext(ctx, "# Step 6: Finished writing image to disk")
logger.DebugContext(ctx, string(output))
if err != nil {
return nil, fmt.Errorf("failed to download and write the image: %w", err)
}
// 7. SSH On Server: Shutdown
log.Print("Step 7: Shutting down server")
logger.InfoContext(ctx, "# Step 7: Shutting down server")
_, err = sshsession.Run(sshClient, "shutdown now")
if err != nil {
// TODO Verify if shutdown error, otherwise return
fmt.Printf("shutdown error: %+v", err)
logger.WarnContext(ctx, "shutdown returned error", "err", err)
}
// 8. Create Image from Server
log.Print("Step 7: Creating Image")
logger.InfoContext(ctx, "# Step 8: Creating Image")
createImageResult, _, err := s.client.Server.CreateImage(ctx, server, &hcloud.ServerCreateImageOpts{
Type: hcloud.ImageTypeSnapshot,
Description: options.Description,
@ -220,12 +258,16 @@ func (s snapshotClient) Upload(ctx context.Context, options UploadOptions) (*hcl
if err != nil {
return nil, fmt.Errorf("failed to create snapshot: %w", err)
}
_, err = s.client.Action.WaitForAction(ctx, createImageResult.Action)
logger.DebugContext(ctx, "image creation requested, waiting on action")
err = s.client.Action.WaitFor(ctx, createImageResult.Action)
if err != nil {
return nil, fmt.Errorf("failed to create snapshot: %w", err)
}
logger.DebugContext(ctx, "action finished, image was created")
image := createImageResult.Image
logger.InfoContext(ctx, "# Image was created", "image", image.ID)
// Resource cleanup is happening in `defer`
return image, nil