github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/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  	"os"
    13  	"time"
    14  
    15  	"github.com/juju/errors"
    16  	"github.com/juju/utils/filestorage"
    17  
    18  	"github.com/juju/juju/version"
    19  )
    20  
    21  // checksumFormat identifies how to interpret the checksum for a backup
    22  // generated with this version of juju.
    23  const checksumFormat = "SHA-1, base64 encoded"
    24  
    25  // Origin identifies where a backup archive came from.  While it is
    26  // more about where and Metadata about what and when, that distinction
    27  // does not merit special consideration.  Instead, Origin exists
    28  // separately from Metadata due to its use as an argument when
    29  // requesting the creation of a new backup.
    30  type Origin struct {
    31  	Environment string
    32  	Machine     string
    33  	Hostname    string
    34  	Version     version.Number
    35  }
    36  
    37  // UnknownString is a marker value for string fields with unknown values.
    38  const UnknownString = "<unknown>"
    39  
    40  // UnknownVersion is a marker value for version fields with unknown values.
    41  var UnknownVersion = version.MustParse("9999.9999.9999")
    42  
    43  // UnknownOrigin returns a new backups origin with unknown values.
    44  func UnknownOrigin() Origin {
    45  	return Origin{
    46  		Environment: UnknownString,
    47  		Machine:     UnknownString,
    48  		Hostname:    UnknownString,
    49  		Version:     UnknownVersion,
    50  	}
    51  }
    52  
    53  // Metadata contains the metadata for a single state backup archive.
    54  type Metadata struct {
    55  	*filestorage.FileMetadata
    56  
    57  	// Started records when the backup was started.
    58  	Started time.Time
    59  	// Finished records when the backup was complete.
    60  	Finished *time.Time
    61  	// Origin identifies where the backup was created.
    62  	Origin Origin
    63  	// Notes is an optional user-supplied annotation.
    64  	Notes string
    65  }
    66  
    67  // NewMetadata returns a new Metadata for a state backup archive.  Only
    68  // the start time and the version are set.
    69  func NewMetadata() *Metadata {
    70  	return &Metadata{
    71  		FileMetadata: filestorage.NewMetadata(),
    72  		Started:      time.Now().UTC(),
    73  		Origin: Origin{
    74  			Version: version.Current.Number,
    75  		},
    76  	}
    77  }
    78  
    79  // NewMetadataState composes a new backup metadata with its origin
    80  // values set.  The environment UUID comes from state.  The hostname is
    81  // retrieved from the OS.
    82  func NewMetadataState(db DB, machine string) (*Metadata, error) {
    83  	// hostname could be derived from the environment...
    84  	hostname, err := os.Hostname()
    85  	if err != nil {
    86  		// If os.Hostname() is not working, something is woefully wrong.
    87  		// Run for the hills.
    88  		return nil, errors.Annotate(err, "could not get hostname (system unstable?)")
    89  	}
    90  
    91  	meta := NewMetadata()
    92  	meta.Origin.Environment = db.EnvironTag().Id()
    93  	meta.Origin.Machine = machine
    94  	meta.Origin.Hostname = hostname
    95  	return meta, nil
    96  }
    97  
    98  // MarkComplete populates the remaining metadata values.  The default
    99  // checksum format is used.
   100  func (m *Metadata) MarkComplete(size int64, checksum string) error {
   101  	if size == 0 {
   102  		return errors.New("missing size")
   103  	}
   104  	if checksum == "" {
   105  		return errors.New("missing checksum")
   106  	}
   107  	format := checksumFormat
   108  	finished := time.Now().UTC()
   109  
   110  	if err := m.SetFileInfo(size, checksum, format); err != nil {
   111  		return errors.Annotate(err, "unexpected failure")
   112  	}
   113  	m.Finished = &finished
   114  
   115  	return nil
   116  }
   117  
   118  type flatMetadata struct {
   119  	ID string
   120  
   121  	// file storage
   122  
   123  	Checksum       string
   124  	ChecksumFormat string
   125  	Size           int64
   126  	Stored         time.Time
   127  
   128  	// backup
   129  
   130  	Started     time.Time
   131  	Finished    time.Time
   132  	Notes       string
   133  	Environment string
   134  	Machine     string
   135  	Hostname    string
   136  	Version     version.Number
   137  }
   138  
   139  // TODO(ericsnow) Move AsJSONBuffer to filestorage.Metadata.
   140  
   141  // AsJSONBuffer returns a bytes.Buffer containing the JSON-ified metadata.
   142  func (m *Metadata) AsJSONBuffer() (io.Reader, error) {
   143  	flat := flatMetadata{
   144  		ID: m.ID(),
   145  
   146  		Checksum:       m.Checksum(),
   147  		ChecksumFormat: m.ChecksumFormat(),
   148  		Size:           m.Size(),
   149  
   150  		Started:     m.Started,
   151  		Notes:       m.Notes,
   152  		Environment: m.Origin.Environment,
   153  		Machine:     m.Origin.Machine,
   154  		Hostname:    m.Origin.Hostname,
   155  		Version:     m.Origin.Version,
   156  	}
   157  
   158  	stored := m.Stored()
   159  	if stored != nil {
   160  		flat.Stored = *stored
   161  	}
   162  
   163  	if m.Finished != nil {
   164  		flat.Finished = *m.Finished
   165  	}
   166  
   167  	var outfile bytes.Buffer
   168  	if err := json.NewEncoder(&outfile).Encode(flat); err != nil {
   169  		return nil, errors.Trace(err)
   170  	}
   171  	return &outfile, nil
   172  }
   173  
   174  // NewMetadataJSONReader extracts a new metadata from the JSON file.
   175  func NewMetadataJSONReader(in io.Reader) (*Metadata, error) {
   176  	var flat flatMetadata
   177  	if err := json.NewDecoder(in).Decode(&flat); err != nil {
   178  		return nil, errors.Trace(err)
   179  	}
   180  
   181  	meta := NewMetadata()
   182  	meta.SetID(flat.ID)
   183  
   184  	err := meta.SetFileInfo(flat.Size, flat.Checksum, flat.ChecksumFormat)
   185  	if err != nil {
   186  		return nil, errors.Trace(err)
   187  	}
   188  
   189  	if !flat.Stored.IsZero() {
   190  		meta.SetStored(&flat.Stored)
   191  	}
   192  
   193  	meta.Started = flat.Started
   194  	if !flat.Finished.IsZero() {
   195  		meta.Finished = &flat.Finished
   196  	}
   197  	meta.Notes = flat.Notes
   198  	meta.Origin = Origin{
   199  		Environment: flat.Environment,
   200  		Machine:     flat.Machine,
   201  		Hostname:    flat.Hostname,
   202  		Version:     flat.Version,
   203  	}
   204  
   205  	return meta, nil
   206  }
   207  
   208  func fileTimestamp(fi os.FileInfo) time.Time {
   209  	timestamp := creationTime(fi)
   210  	if !timestamp.IsZero() {
   211  		return timestamp
   212  	}
   213  	// Fall back to modification time.
   214  	return fi.ModTime()
   215  }
   216  
   217  // BuildMetadata generates the metadata for a backup archive file.
   218  func BuildMetadata(file *os.File) (*Metadata, error) {
   219  
   220  	// Extract the file size.
   221  	fi, err := file.Stat()
   222  	if err != nil {
   223  		return nil, errors.Trace(err)
   224  	}
   225  	size := fi.Size()
   226  
   227  	// Extract the timestamp.
   228  	timestamp := fileTimestamp(fi)
   229  
   230  	// Get the checksum.
   231  	hasher := sha1.New()
   232  	_, err = io.Copy(hasher, file)
   233  	if err != nil {
   234  		return nil, errors.Trace(err)
   235  	}
   236  	rawsum := hasher.Sum(nil)
   237  	checksum := base64.StdEncoding.EncodeToString(rawsum)
   238  
   239  	// Build the metadata.
   240  	meta := NewMetadata()
   241  	meta.Started = time.Time{}
   242  	meta.Origin = UnknownOrigin()
   243  	err = meta.MarkComplete(size, checksum)
   244  	if err != nil {
   245  		return nil, errors.Trace(err)
   246  	}
   247  	meta.Finished = &timestamp
   248  	return meta, nil
   249  }