github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/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  	jujuversion "github.com/juju/juju/version"
    17  	"github.com/juju/utils/filestorage"
    18  	"github.com/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  	Model    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  		Model:    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  
    60  	// Finished records when the backup was complete.
    61  	Finished *time.Time
    62  
    63  	// Origin identifies where the backup was created.
    64  	Origin Origin
    65  
    66  	// Notes is an optional user-supplied annotation.
    67  	Notes string
    68  
    69  	// TODO(wallyworld) - remove these ASAP
    70  	// These are only used by the restore CLI when re-bootstrapping.
    71  	// We will use a better solution but the way restore currently
    72  	// works, we need them and they are no longer available via
    73  	// bootstrap config. We will need to ifx how re-bootstrap deals
    74  	// with these keys to address the issue.
    75  
    76  	// CACert is the controller CA certificate.
    77  	CACert string
    78  
    79  	// CAPrivateKey is the controller CA private key.
    80  	CAPrivateKey string
    81  }
    82  
    83  // NewMetadata returns a new Metadata for a state backup archive.  Only
    84  // the start time and the version are set.
    85  func NewMetadata() *Metadata {
    86  	return &Metadata{
    87  		FileMetadata: filestorage.NewMetadata(),
    88  		// TODO(fwereade): 2016-03-17 lp:1558657
    89  		Started: time.Now().UTC(),
    90  		Origin: Origin{
    91  			Version: jujuversion.Current,
    92  		},
    93  	}
    94  }
    95  
    96  // NewMetadataState composes a new backup metadata with its origin
    97  // values set.  The model UUID comes from state.  The hostname is
    98  // retrieved from the OS.
    99  func NewMetadataState(db DB, machine string) (*Metadata, error) {
   100  	// hostname could be derived from the model...
   101  	hostname, err := os.Hostname()
   102  	if err != nil {
   103  		// If os.Hostname() is not working, something is woefully wrong.
   104  		// Run for the hills.
   105  		return nil, errors.Annotate(err, "could not get hostname (system unstable?)")
   106  	}
   107  
   108  	meta := NewMetadata()
   109  	meta.Origin.Model = db.ModelTag().Id()
   110  	meta.Origin.Machine = machine
   111  	meta.Origin.Hostname = hostname
   112  
   113  	si, err := db.StateServingInfo()
   114  	if err != nil {
   115  		return nil, errors.Annotate(err, "could not get server secrets")
   116  	}
   117  	cfg, err := db.ModelConfig()
   118  	if err != nil {
   119  		return nil, errors.Annotate(err, "could not get model config")
   120  	}
   121  	meta.CACert, _ = cfg.CACert()
   122  	meta.CAPrivateKey = si.CAPrivateKey
   123  	return meta, nil
   124  }
   125  
   126  // MarkComplete populates the remaining metadata values.  The default
   127  // checksum format is used.
   128  func (m *Metadata) MarkComplete(size int64, checksum string) error {
   129  	if size == 0 {
   130  		return errors.New("missing size")
   131  	}
   132  	if checksum == "" {
   133  		return errors.New("missing checksum")
   134  	}
   135  	format := checksumFormat
   136  	// TODO(fwereade): 2016-03-17 lp:1558657
   137  	finished := time.Now().UTC()
   138  
   139  	if err := m.SetFileInfo(size, checksum, format); err != nil {
   140  		return errors.Annotate(err, "unexpected failure")
   141  	}
   142  	m.Finished = &finished
   143  
   144  	return nil
   145  }
   146  
   147  type flatMetadata struct {
   148  	ID string
   149  
   150  	// file storage
   151  
   152  	Checksum       string
   153  	ChecksumFormat string
   154  	Size           int64
   155  	Stored         time.Time
   156  
   157  	// backup
   158  
   159  	Started     time.Time
   160  	Finished    time.Time
   161  	Notes       string
   162  	Environment string
   163  	Machine     string
   164  	Hostname    string
   165  	Version     version.Number
   166  
   167  	CACert       string
   168  	CAPrivateKey string
   169  }
   170  
   171  // TODO(ericsnow) Move AsJSONBuffer to filestorage.Metadata.
   172  
   173  // AsJSONBuffer returns a bytes.Buffer containing the JSON-ified metadata.
   174  func (m *Metadata) AsJSONBuffer() (io.Reader, error) {
   175  	flat := flatMetadata{
   176  		ID: m.ID(),
   177  
   178  		Checksum:       m.Checksum(),
   179  		ChecksumFormat: m.ChecksumFormat(),
   180  		Size:           m.Size(),
   181  
   182  		Started:      m.Started,
   183  		Notes:        m.Notes,
   184  		Environment:  m.Origin.Model,
   185  		Machine:      m.Origin.Machine,
   186  		Hostname:     m.Origin.Hostname,
   187  		Version:      m.Origin.Version,
   188  		CACert:       m.CACert,
   189  		CAPrivateKey: m.CAPrivateKey,
   190  	}
   191  
   192  	stored := m.Stored()
   193  	if stored != nil {
   194  		flat.Stored = *stored
   195  	}
   196  
   197  	if m.Finished != nil {
   198  		flat.Finished = *m.Finished
   199  	}
   200  
   201  	var outfile bytes.Buffer
   202  	if err := json.NewEncoder(&outfile).Encode(flat); err != nil {
   203  		return nil, errors.Trace(err)
   204  	}
   205  	return &outfile, nil
   206  }
   207  
   208  // NewMetadataJSONReader extracts a new metadata from the JSON file.
   209  func NewMetadataJSONReader(in io.Reader) (*Metadata, error) {
   210  	var flat flatMetadata
   211  	if err := json.NewDecoder(in).Decode(&flat); err != nil {
   212  		return nil, errors.Trace(err)
   213  	}
   214  
   215  	meta := NewMetadata()
   216  	meta.SetID(flat.ID)
   217  
   218  	err := meta.SetFileInfo(flat.Size, flat.Checksum, flat.ChecksumFormat)
   219  	if err != nil {
   220  		return nil, errors.Trace(err)
   221  	}
   222  
   223  	if !flat.Stored.IsZero() {
   224  		meta.SetStored(&flat.Stored)
   225  	}
   226  
   227  	meta.Started = flat.Started
   228  	if !flat.Finished.IsZero() {
   229  		meta.Finished = &flat.Finished
   230  	}
   231  	meta.Notes = flat.Notes
   232  	meta.Origin = Origin{
   233  		Model:    flat.Environment,
   234  		Machine:  flat.Machine,
   235  		Hostname: flat.Hostname,
   236  		Version:  flat.Version,
   237  	}
   238  
   239  	// TODO(wallyworld) - put these in a separate file.
   240  	meta.CACert = flat.CACert
   241  	meta.CAPrivateKey = flat.CAPrivateKey
   242  
   243  	return meta, nil
   244  }
   245  
   246  func fileTimestamp(fi os.FileInfo) time.Time {
   247  	timestamp := creationTime(fi)
   248  	if !timestamp.IsZero() {
   249  		return timestamp
   250  	}
   251  	// Fall back to modification time.
   252  	return fi.ModTime()
   253  }
   254  
   255  // BuildMetadata generates the metadata for a backup archive file.
   256  func BuildMetadata(file *os.File) (*Metadata, error) {
   257  
   258  	// Extract the file size.
   259  	fi, err := file.Stat()
   260  	if err != nil {
   261  		return nil, errors.Trace(err)
   262  	}
   263  	size := fi.Size()
   264  
   265  	// Extract the timestamp.
   266  	timestamp := fileTimestamp(fi)
   267  
   268  	// Get the checksum.
   269  	hasher := sha1.New()
   270  	_, err = io.Copy(hasher, file)
   271  	if err != nil {
   272  		return nil, errors.Trace(err)
   273  	}
   274  	rawsum := hasher.Sum(nil)
   275  	checksum := base64.StdEncoding.EncodeToString(rawsum)
   276  
   277  	// Build the metadata.
   278  	meta := NewMetadata()
   279  	meta.Started = time.Time{}
   280  	meta.Origin = UnknownOrigin()
   281  	err = meta.MarkComplete(size, checksum)
   282  	if err != nil {
   283  		return nil, errors.Trace(err)
   284  	}
   285  	meta.Finished = &timestamp
   286  	return meta, nil
   287  }