cmd/core_plugin/firstboot/firstboot.go (90 lines of code) (raw):
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package firstboot provides a module to setup instance id, generate host ssh
// keys and generate boto config file.
package firstboot
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/GoogleCloudPlatform/galog"
"github.com/GoogleCloudPlatform/google-guest-agent/cmd/core_plugin/manager"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/cfg"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/metadata"
"github.com/GoogleCloudPlatform/google-guest-agent/internal/utils/file"
)
const (
// firstbootModuleID is the ID of the iosched module.
firstbootModuleID = "firstboot"
)
// NewModule returns the first boot module for late stage registration.
func NewModule(context.Context) *manager.Module {
return &manager.Module{
ID: firstbootModuleID,
Setup: moduleSetup,
Description: "Set up instance id, generates host ssh keys and generates boto config file",
}
}
// moduleSetup sets up the firstboot module.
func moduleSetup(ctx context.Context, data any) error {
desc, ok := data.(*metadata.Descriptor)
if !ok {
return fmt.Errorf("firstboot module expects a metadata descriptor in the data pointer")
}
instanceID := desc.Instance().ID().String()
projectID := desc.Project().ID()
config := cfg.Retrieve()
if err := runFirstboot(ctx, instanceID, projectID, config); err != nil {
return fmt.Errorf("failed to run firstboot: %v", err)
}
return nil
}
// runFirstboot runs the firstboot module setup.
func runFirstboot(ctx context.Context, instanceID string, projectID string, config *cfg.Sections) error {
isFirstboot, err := firstbootRun(instanceID, projectID, config)
if err != nil {
return fmt.Errorf("failed to check if we are in firstboot: %v", err)
}
if !isFirstboot {
galog.Debug("Instance id is already set, no update required.")
return nil
}
// InstanceID writing path is common between linux and windows so have it done
// before platform specific setup.
if err := writeInstanceID(config.Instance.InstanceIDDir, instanceID); err != nil {
return err
}
if err := platformSetup(ctx, projectID, config); err != nil {
return fmt.Errorf("failed to setup instance id: %v", err)
}
return nil
}
// firstbootRun returns true if the instance is being booted for the first time,
// or in a broader sense if the instance id has changed.
func firstbootRun(instanceID string, projectID string, config *cfg.Sections) (bool, error) {
fPath := config.Instance.InstanceIDDir
var currentInstanceID string
// If the instance id file exists, read the current instance id from it.
if file.Exists(fPath, file.TypeFile) {
data, err := os.ReadFile(fPath)
if err != nil {
return false, fmt.Errorf("failed to read instance id file: %w", err)
}
currentInstanceID = strings.TrimSpace(string(data))
}
// If the current instance id is empty, use the instance id from the config.
// The instance id in the config file is the legacy instance id configuration
// method, we try to honor it if the current instance id is empty.
if currentInstanceID == "" {
currentInstanceID = config.Instance.InstanceID
}
// No need to update the instance id file if the current instance id is the
// same as the new one.
if currentInstanceID == instanceID {
galog.Infof("Instance id is already set, no update required.")
return false, nil
}
return true, nil
}
// writeInstanceID writes the instance id to the file.
func writeInstanceID(fPath string, newInstanceID string) error {
// Make parent directories if they don't exist.
if err := os.MkdirAll(filepath.Dir(fPath), 0755); err != nil {
return fmt.Errorf("failed to create instance id file: %w", err)
}
f, err := os.OpenFile(fPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return fmt.Errorf("failed to open instance id file: %w", err)
}
defer f.Close()
n, err := f.WriteString(newInstanceID)
if err != nil {
return fmt.Errorf("failed to write instance id file: %w", err)
}
if n != len(newInstanceID) {
return fmt.Errorf("failed to write instance id file: %w", err)
}
return nil
}