github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/backups/metadata.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package backups
     5  
     6  import (
     7  	"bytes"
     8  	"crypto/sha1"
     9  	"encoding/base64"
    10  	"encoding/json"
    11  	"io"
    12  	"math"
    13  	"os"
    14  	"time"
    15  
    16  	"github.com/juju/errors"
    17  	"github.com/juju/names/v5"
    18  	"github.com/juju/utils/v3/filestorage"
    19  	"github.com/juju/version/v2"
    20  
    21  	"github.com/juju/juju/controller"
    22  	jujuversion "github.com/juju/juju/version"
    23  )
    24  
    25  // checksumFormat identifies how to interpret the checksum for a backup
    26  // generated with this version of juju.
    27  const checksumFormat = "SHA-1, base64 encoded"
    28  
    29  // Origin identifies where a backup archive came from.  While it is
    30  // more about where and Metadata about what and when, that distinction
    31  // does not merit special consideration.  Instead, Origin exists
    32  // separately from Metadata due to its use as an argument when
    33  // requesting the creation of a new backup.
    34  type Origin struct {
    35  	Model    string
    36  	Machine  string
    37  	Hostname string
    38  	Version  version.Number
    39  	Base     string
    40  }
    41  
    42  // UnknownString is a marker value for string fields with unknown values.
    43  const UnknownString = "<unknown>"
    44  
    45  // UnknownVersion is a marker value for version fields with unknown values.
    46  var UnknownVersion = version.MustParse("9999.9999.9999")
    47  
    48  // UnknownInt64 is a marker value for int64 fields with unknown values.
    49  var UnknownInt64 = int64(math.MaxInt64)
    50  
    51  // UnknownOrigin returns a new backups origin with unknown values.
    52  func UnknownOrigin() Origin {
    53  	return Origin{
    54  		Model:    UnknownString,
    55  		Machine:  UnknownString,
    56  		Hostname: UnknownString,
    57  		Version:  UnknownVersion,
    58  	}
    59  }
    60  
    61  // UnknownController returns a new backups origin with unknown values.
    62  func UnknownController() ControllerMetadata {
    63  	return ControllerMetadata{
    64  		UUID:              UnknownString,
    65  		MachineID:         UnknownString,
    66  		MachineInstanceID: UnknownString,
    67  		HANodes:           UnknownInt64,
    68  	}
    69  }
    70  
    71  // Metadata contains the metadata for a single state backup archive.
    72  type Metadata struct {
    73  	*filestorage.FileMetadata
    74  
    75  	// Started records when the backup was started.
    76  	Started time.Time
    77  
    78  	// Finished records when the backup was complete.
    79  	Finished *time.Time
    80  
    81  	// Origin identifies where the backup was created.
    82  	Origin Origin
    83  
    84  	// Notes is an optional user-supplied annotation.
    85  	Notes string
    86  
    87  	// FormatVersion stores format version of these metadata.
    88  	FormatVersion int64
    89  
    90  	// Controller contains metadata about the controller where the backup was taken.
    91  	Controller ControllerMetadata
    92  }
    93  
    94  // ControllerMetadata contains controller specific metadata.
    95  type ControllerMetadata struct {
    96  	// UUID contains the controller UUID.
    97  	UUID string
    98  
    99  	// MachineID contains controller machine id from which this backup is taken.
   100  	MachineID string
   101  
   102  	// MachineInstanceID contains controller machine's instance id from which this backup is taken.
   103  	MachineInstanceID string
   104  
   105  	// HANodes contains the number of nodes in this controller's HA configuration.
   106  	HANodes int64
   107  }
   108  
   109  // All un-versioned metadata is considered to be version 0,
   110  // so the versions start with 1.
   111  const currentFormatVersion = 1
   112  
   113  // NewMetadata returns a new Metadata for a state backup archive,
   114  // in the most current format.
   115  func NewMetadata() *Metadata {
   116  	return &Metadata{
   117  		FileMetadata: filestorage.NewMetadata(),
   118  		// TODO(fwereade): 2016-03-17 lp:1558657
   119  		Started: time.Now().UTC(),
   120  		Origin: Origin{
   121  			Version: jujuversion.Current,
   122  		},
   123  		FormatVersion: currentFormatVersion,
   124  		Controller:    ControllerMetadata{},
   125  	}
   126  }
   127  
   128  type backend interface {
   129  	ModelTag() names.ModelTag
   130  	ControllerConfig() (controller.Config, error)
   131  	StateServingInfo() (controller.StateServingInfo, error)
   132  }
   133  
   134  // NewMetadataState composes a new backup metadata based on the current Juju state.
   135  func NewMetadataState(db backend, machine, base string) (*Metadata, error) {
   136  	hostname, err := os.Hostname()
   137  	if err != nil {
   138  		// If os.Hostname() is not working, something is woefully wrong.
   139  		return nil, errors.Annotate(err, "could not get hostname (system unstable?)")
   140  	}
   141  
   142  	meta := NewMetadata()
   143  	meta.Origin.Model = db.ModelTag().Id()
   144  	meta.Origin.Machine = machine
   145  	meta.Origin.Hostname = hostname
   146  	meta.Origin.Base = base
   147  
   148  	controllerCfg, err := db.ControllerConfig()
   149  	if err != nil {
   150  		return nil, errors.Annotate(err, "could not get controller config")
   151  	}
   152  	meta.Controller.UUID = controllerCfg.ControllerUUID()
   153  	return meta, nil
   154  }
   155  
   156  // MarkComplete populates the remaining metadata values.  The default
   157  // checksum format is used.
   158  func (m *Metadata) MarkComplete(size int64, checksum string) error {
   159  	if size == 0 {
   160  		return errors.New("missing size")
   161  	}
   162  	if checksum == "" {
   163  		return errors.New("missing checksum")
   164  	}
   165  	format := checksumFormat
   166  	// TODO(fwereade): 2016-03-17 lp:1558657
   167  	finished := time.Now().UTC()
   168  
   169  	if err := m.SetFileInfo(size, checksum, format); err != nil {
   170  		return errors.Annotate(err, "unexpected failure")
   171  	}
   172  	m.Finished = &finished
   173  
   174  	return nil
   175  }
   176  
   177  // flatMetadataV0 contains old, un-versioned format of backup, aka v0.
   178  type flatMetadataV0 struct {
   179  	ID string
   180  
   181  	// file storage
   182  
   183  	Checksum       string
   184  	ChecksumFormat string
   185  	Size           int64
   186  	Stored         time.Time
   187  
   188  	// backup
   189  
   190  	Started     time.Time
   191  	Finished    time.Time
   192  	Notes       string
   193  	Environment string
   194  	Machine     string
   195  	Hostname    string
   196  	Version     version.Number
   197  	Base        string
   198  
   199  	CACert       string
   200  	CAPrivateKey string
   201  }
   202  
   203  func (flat *flatMetadataV0) inflate() (*Metadata, error) {
   204  	meta := NewMetadata()
   205  	meta.SetID(flat.ID)
   206  	meta.FormatVersion = 0
   207  	err := meta.SetFileInfo(flat.Size, flat.Checksum, flat.ChecksumFormat)
   208  	if err != nil {
   209  		return nil, errors.Trace(err)
   210  	}
   211  
   212  	if !flat.Stored.IsZero() {
   213  		meta.SetStored(&flat.Stored)
   214  	}
   215  
   216  	meta.Started = flat.Started
   217  	if !flat.Finished.IsZero() {
   218  		meta.Finished = &flat.Finished
   219  	}
   220  	meta.Notes = flat.Notes
   221  	meta.Origin = Origin{
   222  		Model:    flat.Environment,
   223  		Machine:  flat.Machine,
   224  		Hostname: flat.Hostname,
   225  		Version:  flat.Version,
   226  		Base:     flat.Base,
   227  	}
   228  	return meta, nil
   229  }
   230  
   231  // flatMetadata contains the latest format of the backup.
   232  // NOTE If any changes need to be made here, rename this struct to
   233  // reflect version 1, for example flatMetadataV1 and construct
   234  // new flatMetadata with desired modifications.
   235  type flatMetadata struct {
   236  	ID            string
   237  	FormatVersion int64
   238  
   239  	// file storage
   240  
   241  	Checksum       string
   242  	ChecksumFormat string
   243  	Size           int64
   244  	Stored         time.Time
   245  
   246  	// backup
   247  
   248  	Started                     time.Time
   249  	Finished                    time.Time
   250  	Notes                       string
   251  	ModelUUID                   string
   252  	Machine                     string
   253  	Hostname                    string
   254  	Version                     version.Number
   255  	Base                        string
   256  	ControllerUUID              string
   257  	HANodes                     int64
   258  	ControllerMachineID         string
   259  	ControllerMachineInstanceID string
   260  }
   261  
   262  func (m *Metadata) flat() flatMetadata {
   263  	flat := flatMetadata{
   264  		ID:                          m.ID(),
   265  		Checksum:                    m.Checksum(),
   266  		ChecksumFormat:              m.ChecksumFormat(),
   267  		Size:                        m.Size(),
   268  		Started:                     m.Started,
   269  		Notes:                       m.Notes,
   270  		ModelUUID:                   m.Origin.Model,
   271  		Machine:                     m.Origin.Machine,
   272  		Hostname:                    m.Origin.Hostname,
   273  		Version:                     m.Origin.Version,
   274  		Base:                        m.Origin.Base,
   275  		FormatVersion:               m.FormatVersion,
   276  		ControllerUUID:              m.Controller.UUID,
   277  		ControllerMachineID:         m.Controller.MachineID,
   278  		ControllerMachineInstanceID: m.Controller.MachineInstanceID,
   279  		HANodes:                     m.Controller.HANodes,
   280  	}
   281  	stored := m.Stored()
   282  	if stored != nil {
   283  		flat.Stored = *stored
   284  	}
   285  
   286  	if m.Finished != nil {
   287  		flat.Finished = *m.Finished
   288  	}
   289  	return flat
   290  }
   291  
   292  func (flat *flatMetadata) inflate() (*Metadata, error) {
   293  	meta := NewMetadata()
   294  	meta.SetID(flat.ID)
   295  	meta.FormatVersion = flat.FormatVersion
   296  
   297  	err := meta.SetFileInfo(flat.Size, flat.Checksum, flat.ChecksumFormat)
   298  	if err != nil {
   299  		return nil, errors.Trace(err)
   300  	}
   301  
   302  	if !flat.Stored.IsZero() {
   303  		meta.SetStored(&flat.Stored)
   304  	}
   305  
   306  	meta.Started = flat.Started
   307  	if !flat.Finished.IsZero() {
   308  		meta.Finished = &flat.Finished
   309  	}
   310  	meta.Notes = flat.Notes
   311  	meta.Origin = Origin{
   312  		Model:    flat.ModelUUID,
   313  		Machine:  flat.Machine,
   314  		Hostname: flat.Hostname,
   315  		Version:  flat.Version,
   316  		Base:     flat.Base,
   317  	}
   318  
   319  	meta.Controller = ControllerMetadata{
   320  		UUID:              flat.ControllerUUID,
   321  		MachineID:         flat.ControllerMachineID,
   322  		MachineInstanceID: flat.ControllerMachineInstanceID,
   323  		HANodes:           flat.HANodes,
   324  	}
   325  	return meta, nil
   326  }
   327  
   328  // AsJSONBuffer returns a bytes.Buffer containing the JSON-ified metadata.
   329  // This will always produce latest known format.
   330  func (m *Metadata) AsJSONBuffer() (io.Reader, error) {
   331  	var outfile bytes.Buffer
   332  	if err := json.NewEncoder(&outfile).Encode(m.flat()); err != nil {
   333  		return nil, errors.Trace(err)
   334  	}
   335  	return &outfile, nil
   336  }
   337  
   338  // NewMetadataJSONReader extracts a new metadata from the JSON file.
   339  func NewMetadataJSONReader(in io.Reader) (*Metadata, error) {
   340  	data, err := io.ReadAll(in)
   341  	if err != nil {
   342  		return nil, errors.Trace(err)
   343  	}
   344  	// We always want to decode into the most recent format version.
   345  	var flat flatMetadata
   346  	if err := json.Unmarshal(data, &flat); err != nil {
   347  		return nil, errors.Trace(err)
   348  	}
   349  
   350  	// Cater for old backup files, taken as version 0 or with no version.
   351  	switch flat.FormatVersion {
   352  	case 0:
   353  		{
   354  			var v0 flatMetadataV0
   355  			if err := json.Unmarshal(data, &v0); err != nil {
   356  				return nil, errors.Trace(err)
   357  			}
   358  			return v0.inflate()
   359  		}
   360  	case 1:
   361  		return flat.inflate()
   362  	default:
   363  		return nil, errors.NotSupportedf("backup format %d", flat.FormatVersion)
   364  	}
   365  }
   366  
   367  func fileTimestamp(fi os.FileInfo) time.Time {
   368  	timestamp := creationTime(fi)
   369  	if !timestamp.IsZero() {
   370  		return timestamp
   371  	}
   372  	// Fall back to modification time.
   373  	return fi.ModTime()
   374  }
   375  
   376  // BuildMetadata generates the metadata for a backup archive file.
   377  func BuildMetadata(file *os.File) (*Metadata, error) {
   378  
   379  	// Extract the file size.
   380  	fi, err := file.Stat()
   381  	if err != nil {
   382  		return nil, errors.Trace(err)
   383  	}
   384  	size := fi.Size()
   385  
   386  	// Extract the timestamp.
   387  	timestamp := fileTimestamp(fi)
   388  
   389  	// Get the checksum.
   390  	hasher := sha1.New()
   391  	_, err = io.Copy(hasher, file)
   392  	if err != nil {
   393  		return nil, errors.Trace(err)
   394  	}
   395  	rawsum := hasher.Sum(nil)
   396  	checksum := base64.StdEncoding.EncodeToString(rawsum)
   397  
   398  	// Build the metadata.
   399  	meta := NewMetadata()
   400  	meta.Started = time.Time{}
   401  	meta.Origin = UnknownOrigin()
   402  	meta.FormatVersion = UnknownInt64
   403  	meta.Controller = UnknownController()
   404  	err = meta.MarkComplete(size, checksum)
   405  	if err != nil {
   406  		return nil, errors.Trace(err)
   407  	}
   408  	meta.Finished = &timestamp
   409  	return meta, nil
   410  }