github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/cmd/snap/cmd_snapshot.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2018 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package main 21 22 import ( 23 "fmt" 24 "io" 25 "os" 26 "strconv" 27 "strings" 28 29 "github.com/jessevdk/go-flags" 30 31 "github.com/snapcore/snapd/i18n" 32 "github.com/snapcore/snapd/strutil" 33 "github.com/snapcore/snapd/strutil/quantity" 34 ) 35 36 func fmtSize(size int64) string { 37 return quantity.FormatAmount(uint64(size), -1) + "B" 38 } 39 40 var ( 41 shortSavedHelp = i18n.G("List currently stored snapshots") 42 shortSaveHelp = i18n.G("Save a snapshot of the current data") 43 shortForgetHelp = i18n.G("Delete a snapshot") 44 shortCheckHelp = i18n.G("Check a snapshot") 45 shortRestoreHelp = i18n.G("Restore a snapshot") 46 shortExportSnapshotHelp = i18n.G("Export a snapshot") 47 shortImportSnapshotHelp = i18n.G("Import a snapshot") 48 ) 49 50 var longSavedHelp = i18n.G(` 51 The saved command displays a list of snapshots that have been created 52 previously with the 'save' command. 53 `) 54 var longSaveHelp = i18n.G(` 55 The save command creates a snapshot of the current user, system and 56 configuration data for the given snaps. 57 58 By default, this command saves the data of all snaps for all users. 59 Alternatively, you can specify the data of which snaps to save, or 60 for which users, or a combination of these. 61 62 If a snap is included in a save operation, excluding its system and 63 configuration data from the snapshot is not currently possible. This 64 restriction may be lifted in the future. 65 `) 66 var longForgetHelp = i18n.G(` 67 The forget command deletes a snapshot. This operation can not be 68 undone. 69 70 A snapshot contains archives for the user, system and configuration 71 data of each snap included in the snapshot. 72 73 By default, this command forgets all the data in a snapshot. 74 Alternatively, you can specify the data of which snaps to forget. 75 `) 76 var longCheckHelp = i18n.G(` 77 The check-snapshot command verifies the user, system and configuration 78 data of the snaps included in the specified snapshot. 79 80 The check operation runs the same data integrity verification that is 81 performed when a snapshot is restored. 82 83 By default, this command checks all the data in a snapshot. 84 Alternatively, you can specify the data of which snaps to check, or 85 for which users, or a combination of these. 86 87 If a snap is included in a check-snapshot operation, excluding its 88 system and configuration data from the check is not currently 89 possible. This restriction may be lifted in the future. 90 `) 91 var longRestoreHelp = i18n.G(` 92 The restore command replaces the current user, system and 93 configuration data of included snaps, with the corresponding data from 94 the specified snapshot. 95 96 By default, this command restores all the data in a snapshot. 97 Alternatively, you can specify the data of which snaps to restore, or 98 for which users, or a combination of these. 99 100 If a snap is included in a restore operation, excluding its system and 101 configuration data from the restore is not currently possible. This 102 restriction may be lifted in the future. 103 `) 104 105 var longExportSnapshotHelp = i18n.G(` 106 Export a snapshot to the given filename. 107 `) 108 109 var longImportSnapshotHelp = i18n.G(` 110 Import an exported snapshot set to the system. The snapshot is imported 111 with a new snapshot ID and can be restored using the restore command. 112 `) 113 114 type savedCmd struct { 115 clientMixin 116 durationMixin 117 ID snapshotID `long:"id"` 118 Positional struct { 119 Snaps []installedSnapName `positional-arg-name:"<snap>"` 120 } `positional-args:"yes"` 121 } 122 123 func (x *savedCmd) Execute([]string) error { 124 var setID uint64 125 var err error 126 if x.ID != "" { 127 setID, err = x.ID.ToUint() 128 if err != nil { 129 return err 130 } 131 } 132 snaps := installedSnapNames(x.Positional.Snaps) 133 list, err := x.client.SnapshotSets(setID, snaps) 134 if err != nil { 135 return err 136 } 137 if len(list) == 0 { 138 fmt.Fprintln(Stdout, i18n.G("No snapshots found.")) 139 return nil 140 } 141 142 w := tabWriter() 143 defer w.Flush() 144 145 fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", 146 // TRANSLATORS: 'Set' as in group or bag of things 147 i18n.G("Set"), 148 "Snap", 149 // TRANSLATORS: 'Age' as in how old something is 150 i18n.G("Age"), 151 i18n.G("Version"), 152 // TRANSLATORS: 'Rev' is an abbreviation of 'Revision' 153 i18n.G("Rev"), 154 i18n.G("Size"), 155 // TRANSLATORS: 'Notes' as in 'Comments' 156 i18n.G("Notes")) 157 for _, sg := range list { 158 for _, sh := range sg.Snapshots { 159 notes := []string{} 160 if sh.Auto { 161 notes = append(notes, "auto") 162 } 163 if sh.Broken != "" { 164 notes = append(notes, "broken: "+sh.Broken) 165 } 166 note := "-" 167 if len(notes) > 0 { 168 note = strings.Join(notes, ", ") 169 } 170 size := fmtSize(sh.Size) 171 age := x.fmtDuration(sh.Time) 172 fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%s\t%s\n", sg.ID, sh.Snap, age, sh.Version, sh.Revision, size, note) 173 } 174 } 175 return nil 176 } 177 178 type saveCmd struct { 179 waitMixin 180 durationMixin 181 Users string `long:"users"` 182 Positional struct { 183 Snaps []installedSnapName `positional-arg-name:"<snap>"` 184 } `positional-args:"yes"` 185 } 186 187 func (x *saveCmd) Execute([]string) error { 188 snaps := installedSnapNames(x.Positional.Snaps) 189 users := strutil.CommaSeparatedList(x.Users) 190 setID, changeID, err := x.client.SnapshotMany(snaps, users) 191 if err != nil { 192 return err 193 } 194 if _, err := x.wait(changeID); err != nil { 195 if err == noWait { 196 return nil 197 } 198 return err 199 } 200 201 y := &savedCmd{ 202 clientMixin: x.clientMixin, 203 durationMixin: x.durationMixin, 204 ID: snapshotID(strconv.FormatUint(setID, 10)), 205 } 206 return y.Execute(nil) 207 } 208 209 type forgetCmd struct { 210 waitMixin 211 Positional struct { 212 ID snapshotID `positional-arg-name:"<id>"` 213 Snaps []installedSnapName `positional-arg-name:"<snap>"` 214 } `positional-args:"yes" required:"yes"` 215 } 216 217 func (x *forgetCmd) Execute([]string) error { 218 setID, err := x.Positional.ID.ToUint() 219 if err != nil { 220 return err 221 } 222 snaps := installedSnapNames(x.Positional.Snaps) 223 changeID, err := x.client.ForgetSnapshots(setID, snaps) 224 if err != nil { 225 return err 226 } 227 _, err = x.wait(changeID) 228 if err == noWait { 229 return nil 230 } 231 if err != nil { 232 return err 233 } 234 235 if len(snaps) > 0 { 236 // TRANSLATORS: the %s is a comma-separated list of quoted snap names 237 fmt.Fprintf(Stdout, i18n.NG("Snapshot #%s of snap %s forgotten.\n", "Snapshot #%s of snaps %s forgotten.\n", len(snaps)), x.Positional.ID, strutil.Quoted(snaps)) 238 } else { 239 fmt.Fprintf(Stdout, i18n.G("Snapshot #%s forgotten.\n"), x.Positional.ID) 240 } 241 return nil 242 } 243 244 type checkSnapshotCmd struct { 245 waitMixin 246 Users string `long:"users"` 247 Positional struct { 248 ID snapshotID `positional-arg-name:"<id>"` 249 Snaps []installedSnapName `positional-arg-name:"<snap>"` 250 } `positional-args:"yes" required:"yes"` 251 } 252 253 func (x *checkSnapshotCmd) Execute([]string) error { 254 setID, err := x.Positional.ID.ToUint() 255 if err != nil { 256 return err 257 } 258 snaps := installedSnapNames(x.Positional.Snaps) 259 users := strutil.CommaSeparatedList(x.Users) 260 changeID, err := x.client.CheckSnapshots(setID, snaps, users) 261 if err != nil { 262 return err 263 } 264 _, err = x.wait(changeID) 265 if err == noWait { 266 return nil 267 } 268 if err != nil { 269 return err 270 } 271 272 // TODO: also mention the home archives that were actually checked 273 if len(snaps) > 0 { 274 // TRANSLATORS: the %s is a comma-separated list of quoted snap names 275 fmt.Fprintf(Stdout, i18n.G("Snapshot #%s of snaps %s verified successfully.\n"), 276 x.Positional.ID, strutil.Quoted(snaps)) 277 } else { 278 fmt.Fprintf(Stdout, i18n.G("Snapshot #%s verified successfully.\n"), x.Positional.ID) 279 } 280 return nil 281 } 282 283 type restoreCmd struct { 284 waitMixin 285 Users string `long:"users"` 286 Positional struct { 287 ID snapshotID `positional-arg-name:"<id>"` 288 Snaps []installedSnapName `positional-arg-name:"<snap>"` 289 } `positional-args:"yes" required:"yes"` 290 } 291 292 func (x *restoreCmd) Execute([]string) error { 293 setID, err := x.Positional.ID.ToUint() 294 if err != nil { 295 return err 296 } 297 snaps := installedSnapNames(x.Positional.Snaps) 298 users := strutil.CommaSeparatedList(x.Users) 299 changeID, err := x.client.RestoreSnapshots(setID, snaps, users) 300 if err != nil { 301 return err 302 } 303 _, err = x.wait(changeID) 304 if err == noWait { 305 return nil 306 } 307 if err != nil { 308 return err 309 } 310 311 // TODO: also mention the home archives that were actually restored 312 if len(snaps) > 0 { 313 // TRANSLATORS: the %s is a comma-separated list of quoted snap names 314 fmt.Fprintf(Stdout, i18n.G("Restored snapshot #%s of snaps %s.\n"), 315 x.Positional.ID, strutil.Quoted(snaps)) 316 } else { 317 fmt.Fprintf(Stdout, i18n.G("Restored snapshot #%s.\n"), x.Positional.ID) 318 } 319 return nil 320 } 321 322 func init() { 323 addCommand("saved", 324 shortSavedHelp, 325 longSavedHelp, 326 func() flags.Commander { 327 return &savedCmd{} 328 }, 329 durationDescs.also(map[string]string{ 330 // TRANSLATORS: This should not start with a lowercase letter. 331 "id": i18n.G("Show only a specific snapshot."), 332 }), 333 nil) 334 335 addCommand("save", 336 shortSaveHelp, 337 longSaveHelp, 338 func() flags.Commander { 339 return &saveCmd{} 340 }, durationDescs.also(waitDescs).also(map[string]string{ 341 // TRANSLATORS: This should not start with a lowercase letter. 342 "users": i18n.G("Snapshot data of only specific users (comma-separated) (default: all users)"), 343 }), nil) 344 345 addCommand("restore", 346 shortRestoreHelp, 347 longRestoreHelp, 348 func() flags.Commander { 349 return &restoreCmd{} 350 }, waitDescs.also(map[string]string{ 351 // TRANSLATORS: This should not start with a lowercase letter. 352 "users": i18n.G("Restore data of only specific users (comma-separated) (default: all users)"), 353 }), []argDesc{ 354 { 355 name: "<id>", 356 // TRANSLATORS: This should not start with a lowercase letter. 357 desc: i18n.G("Set id of snapshot to restore (see 'snap help saved')"), 358 }, { 359 name: "<snap>", 360 // TRANSLATORS: This should not start with a lowercase letter. 361 desc: i18n.G("The snap for which data will be restored"), 362 }, 363 }) 364 365 addCommand("forget", 366 shortForgetHelp, 367 longForgetHelp, 368 func() flags.Commander { 369 return &forgetCmd{} 370 }, waitDescs, []argDesc{ 371 { 372 name: "<id>", 373 // TRANSLATORS: This should not start with a lowercase letter. 374 desc: i18n.G("Set id of snapshot to delete (see 'snap help saved')"), 375 }, { 376 name: "<snap>", 377 // TRANSLATORS: This should not start with a lowercase letter. 378 desc: i18n.G("The snap for which data will be deleted"), 379 }, 380 }) 381 382 addCommand("check-snapshot", 383 shortCheckHelp, 384 longCheckHelp, 385 func() flags.Commander { 386 return &checkSnapshotCmd{} 387 }, waitDescs.also(map[string]string{ 388 // TRANSLATORS: This should not start with a lowercase letter. 389 "users": i18n.G("Check data of only specific users (comma-separated) (default: all users)"), 390 }), []argDesc{ 391 { 392 name: "<id>", 393 // TRANSLATORS: This should not start with a lowercase letter. 394 desc: i18n.G("Set id of snapshot to verify (see 'snap help saved')"), 395 }, { 396 name: "<snap>", 397 // TRANSLATORS: This should not start with a lowercase letter. 398 desc: i18n.G("The snap for which data will be verified"), 399 }, 400 }) 401 402 cmd := addCommand("export-snapshot", 403 shortExportSnapshotHelp, 404 longExportSnapshotHelp, 405 func() flags.Commander { 406 return &exportSnapshotCmd{} 407 }, nil, []argDesc{ 408 { 409 name: "<id>", 410 // TRANSLATORS: This should not start with a lowercase letter. 411 desc: i18n.G("Set id of snapshot to export"), 412 }, 413 { 414 // TRANSLATORS: This should retain < ... >. The file name is the name of an exported snapshot. 415 name: i18n.G("<filename>"), 416 // TRANSLATORS: This should not start with a lowercase letter. 417 desc: i18n.G("The filename of the export"), 418 }, 419 }) 420 // XXX: this command is hidden because import/export is not complete 421 cmd.hidden = true 422 423 cmd = addCommand("import-snapshot", 424 shortImportSnapshotHelp, 425 longImportSnapshotHelp, 426 func() flags.Commander { 427 return &importSnapshotCmd{} 428 }, nil, []argDesc{ 429 { 430 name: "<filename>", 431 // TRANSLATORS: This should not start with a lowercase letter. 432 desc: i18n.G("Name of the snapshot export file to use"), 433 }, 434 }) 435 // XXX: this command is hidden because import/export is not complete 436 cmd.hidden = true 437 } 438 439 type exportSnapshotCmd struct { 440 clientMixin 441 Positional struct { 442 ID snapshotID `positional-arg-name:"<id>"` 443 Filename string `long:"filename"` 444 } `positional-args:"yes" required:"yes"` 445 } 446 447 func (x *exportSnapshotCmd) Execute([]string) (err error) { 448 setID, err := x.Positional.ID.ToUint() 449 if err != nil { 450 return err 451 } 452 453 r, expectedSize, err := x.client.SnapshotExport(setID) 454 if err != nil { 455 return err 456 } 457 458 filename := x.Positional.Filename 459 f, err := os.Create(filename + ".part") 460 if err != nil { 461 return err 462 } 463 defer f.Close() 464 defer func() { 465 if err != nil { 466 os.Remove(filename + ".part") 467 } 468 }() 469 470 // Pre-allocate the disk space for the snapshot, if the file system supports this. 471 if err := maybeReserveDiskSpace(f, expectedSize); err != nil { 472 return fmt.Errorf(i18n.G("cannot reserve disk space for snapshot: %v"), err) 473 } 474 475 n, err := io.Copy(f, r) 476 if err != nil { 477 return err 478 } 479 if n != expectedSize { 480 return fmt.Errorf(i18n.G("unexpected size, got: %v but wanted %v"), n, expectedSize) 481 } 482 483 if err := os.Rename(filename+".part", filename); err != nil { 484 return err 485 } 486 487 // TRANSLATORS: the first argument is the identifier of the snapshot, the second one is the file name. 488 fmt.Fprintf(Stdout, i18n.G("Exported snapshot #%s into %q\n"), x.Positional.ID, x.Positional.Filename) 489 490 return nil 491 } 492 493 type importSnapshotCmd struct { 494 clientMixin 495 durationMixin 496 Positional struct { 497 Filename string `long:"filename"` 498 } `positional-args:"yes" required:"yes"` 499 } 500 501 func (x *importSnapshotCmd) Execute([]string) error { 502 filename := x.Positional.Filename 503 f, err := os.Open(filename) 504 if err != nil { 505 return fmt.Errorf("error accessing file: %v", err) 506 } 507 defer f.Close() 508 st, err := f.Stat() 509 if err != nil { 510 return fmt.Errorf("cannot stat file: %v", err) 511 } 512 513 importSet, err := x.client.SnapshotImport(f, st.Size()) 514 if err != nil { 515 return err 516 } 517 518 fmt.Fprintf(Stdout, i18n.G("Imported snapshot as #%d\n"), importSet.ID) 519 // Now display the details about this snapshot, re-use the 520 // "snap saved" command for this which displays details about 521 // the snapshot. 522 y := &savedCmd{ 523 clientMixin: x.clientMixin, 524 durationMixin: x.durationMixin, 525 ID: snapshotID(strconv.FormatUint(importSet.ID, 10)), 526 } 527 return y.Execute(nil) 528 }