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 = ×tamp 409 return meta, nil 410 }