Written by: Robert R. Russell on Thursday, August 13, 2020.
Now that I can read a list of snapshots, I need to read a snapshot and transfer
it to the destination. The three functions that allow me to do that are
exec.StdinPipe(),
exec.StdoutPipe(),
and io.CopyBuffer().
The process consists of the following steps:
Create an exec.Cmd representing the zfs send command
Use exec.StdoutPipe() to connect a pipe to the output of the command
created in step 1.
Create an exec.Cmd representing the zfs receive command
Use exec.StdinPipe() to connect a pipe to the input of the command created
in step 3.
Start both commands
Use io.CopyBuffer() to read from the snapshot to the receiver.
Written by: Robert R. Russell on Monday, August 10, 2020.
When I rebooted my blog, yet again, I gave myself the personal challenge of
writing at least one article per day. Except for August 3rd, 2020, I have met
that challenge. Let’s see it continues.
Written by: Robert R. Russell on Sunday, August 9, 2020.
Which software to use?
With the ZFS backup tool, I want to host the code for it here on my website
instead of GitHub. What options are available? If I want to host the bare repo,
I can use ssh for write access and add a virtual host for apache so you can
have read access. If I want a nice web interface, though, I need a different
setup.
A bit of online searching shows four major self-hosted Git web frontends. They
are GitLab, Gitea,
GitBucket, and Gogs.
GitLab and GitBucket are out because they require a lot of extra software to
support the service. GitLab could almost qualify as its own Linux distro with
a bit more work. GitBucket is nearly as bad. That leaves the two clones, Gogs
and Gitea. Gitea is a fork of Gogs with more maintainers. The increase in
maintainers gives Gitea a faster issue resolution, so I chose it.
System requirements
Gitea has very moderate system requirements. Golang, about 256MB of RAM,
and optionally MariaDB, MySQL, or PostgreSQL. An external database is a
recommendation for large sites. I will use MariaDB because I am already using
it and have a working scheduled backup of my entire database server.
Written by: Robert R. Russell on Saturday, August 8, 2020.
Mustie1 does small
engine repair videos. Most of his videos start with something simple that
someone overlooked with the “dead” engine. He fixes that and usually cleans the
engine as well.
Here are three videos where he fixed a forklift that someone abandoned because
two previous mechanics wouldn’t follow their troubleshooting workflow to the
end.
Written by: Robert R. Russell on Friday, August 7, 2020.
Welcome to Part 4 of my series on my tool for backing up ZFS Snapshots to an
external device. In this part, I am discussing how to exec a command and read
its output.
To deal with external commands in Go, you use
the os/exec package. The
primary pieces of the package that I need for now are
exec.Command() and
CombinedOutput().
exec.Command() sets up the Command structure with the command and any
arguments that I am passing to it.
var listCommand = exec.Command("zfs", "list", "-Hrt", "snapshot", "dpool")
That code creates a variable called listCommand, which is ready to run
the command zfs with the arguments list, -Hrt, and snapshot as individual
arguments.
var snapList, err = listCommand.CombinedOutput()
That line of code runs the command I previously prepared, puts both its
Standard Output and Standard Error in a slice of bytes. If the command exited
with an error code other than 0, CombinedOutput sets err to a non-nil value.
snapList will have the Standard Error of the executed command, so printing
snapList’s contents will be useful for debugging.
var snapScanner = bufio.NewScanner(bytes.NewReader(snapList))
if err != nil {
fmt.Println(listCommand)
fmt.Println("Error trying to list snapshots:", err.Error())
for snapScanner.Scan() {
fmt.Println(snapScanner.Text())
}
}
I will need to use the more complicated IO redirection tools provided in the
os/exec package for the zfs send and zfs receive commands. However, for a test
run today, I can use a modification of the loop I used to print the output from
zfs if it errored.
for snapScanner.Scan() {
if snapshotLineRegex.MatchString(snapScanner.Text()) {
var temp = strings.SplitN(snapScanner.Text(), "\t", 2)
var snapshot = ParseSnapshot(temp[0])
if snapshot != nil {
fmt.Println("I found snapshot", snapshot.Name(), "at", snapshot.Path())
}
}
}
}
Written by: Robert R. Russell on Thursday, August 6, 2020.
Today’s project is parsing a snapshot into a custom datatype that gives us
more accessible options to manipulate snapshots. First, the regular expression
strings need to be moved into separate files so I can reference them across
other files.
The essential parts of a snapshot are:
the pool name
the filesystem tree
the Interval
the TimeStamp
To parse a snapshot out of a string.
Confirm that the string matches our regular expression for snapshots and only
contains the regular expression for a snapshot.
If it does not return an error otherwise continue
Split the input string into the path and the snapshot name
Parse the snapshot name into the interval and timestamp fields
Split the path into the pool name and any filesystem tree portions.
Below is a function that implements the listed requirements.
/*
ParseSnapshot parses a string into a Snapshot.
It returns nil on error.
*/funcParseSnapshot(input string) *Snapshot {
var snapshotOnly, err = regexp.Compile("^" + PoolNameRegex + "@" + ZfsSnapshotNameRegex + "$")
if err != nil {
returnnil }
if !snapshotOnly.MatchString(input) {
returnnil }
var snapshotPieces []string = snapshotOnly.FindStringSubmatch(input)
var theSnapshot = Snapshot{}
theSnapshot.Interval = intervalStringToUInt(snapshotPieces[1])
var year, month, day, hour, minute int year, err = strconv.Atoi(snapshotPieces[2])
if err != nil {
returnnil }
month, err = strconv.Atoi(snapshotPieces[3])
if err != nil {
returnnil }
day, err = strconv.Atoi(snapshotPieces[4])
if err != nil {
returnnil }
hour, err = strconv.Atoi(snapshotPieces[5])
if err != nil {
returnnil }
minute, err = strconv.Atoi(snapshotPieces[6])
if err != nil {
returnnil }
theSnapshot.TimeStamp = time.Date(year, time.Month(month), day, hour, minute, 0, 0, time.UTC)
var splitInput []string = strings.Split(input, "@")
iflen(splitInput) != 2 {
returnnil }
var paths []string = strings.Split(splitInput[0], "/")
theSnapshot.pool = paths[0]
iflen(paths) > 1 {
copy(theSnapshot.fsTree, paths[1:])
}
return &theSnapshot
}
funcintervalStringToUInt(input string) uint64 {
switch input {
case"yearly":
return 0
case"monthly":
return 1
case"weekly":
return 2
case"daily":
return 3
case"hourly":
return 4
}
return 5
}
Now that I can create a Snapshot structure I need some utility methods for them.
Read the pool snapshot and file system tree as a single string.
Compare two snapshots by date and interval
Get the snapshot name
Get the full snapshot string. <path>@<snapshot name>
The following code will implement those utility methods.
/*
Path returns a string containing the path of the snapshot
*/func (s Snapshot) Path() string {
var temp strings.Builder
temp.WriteString(s.pool)
iflen(s.fsTree) > 0 {
for _, v := range s.fsTree {
temp.WriteString("/" + v)
}
}
return temp.String()
}
/*
Name returns a string containing the full name of snapshot
*/func (s Snapshot) Name() string {
var temp strings.Builder
temp.WriteString("zfs-auto-snap_")
temp.WriteString(intervalUIntToString(s.Interval) + "-")
fmt.Fprintf(&temp, "%d-%d-%d-%d%d", s.TimeStamp.Year(), s.TimeStamp.Month(), s.TimeStamp.Day(), s.TimeStamp.Hour(), s.TimeStamp.Minute())
return temp.String()
}
funcintervalUIntToString(x uint64) string {
switch x {
case 0:
return"yearly"case 1:
return"monthly"case 2:
return"weekly"case 3:
return"daily"case 4:
return"hourly" }
return"frequent"}
/*
String returns a string equal to s.Path() + "@" + s.Name() for Snapshot s
*/func (s Snapshot) String() string {
return s.Path() + "@" + s.Name()
}
/*
CompareSnapshotDates returns -2 if x occured before y and would include y in its interval
returns -1 if x occured before y
returns 0 if x and y are the same snapshot
returns +1 if y occured after x
err is non nill if the snapshots do not have the same path
*/funcCompareSnapshotDates(x Snapshot, y Snapshot) (int, error) {
if x.Path() != y.Path() {
return 0, errors.New("Can only compare snapshots with the same path")
}
if x.Interval == y.Interval {
if x.TimeStamp.Equal(y.TimeStamp) {
return 0, nil }
if x.TimeStamp.Before(y.TimeStamp) {
return -1, nil }
return 1, nil }
if x.Interval < y.Interval { // y is from a more frequent backup interval than x
var interval time.Time
switch x.Interval {
case 0:
interval = x.TimeStamp.AddDate(-1, 0, 0)
case 1:
interval = x.TimeStamp.AddDate(0, -1, 0)
case 2:
interval = x.TimeStamp.AddDate(0, 0, -7)
case 3:
interval = x.TimeStamp.AddDate(0, 0, -1)
case 4:
interval = x.TimeStamp.Add(time.Hour * -1)
case 5:
interval = x.TimeStamp.Add(time.Minute * -15)
}
if x.TimeStamp.Before(y.TimeStamp) {
return 1, nil }
if interval.Before(y.TimeStamp) {
return -2, nil }
return -1, nil }
// y is from a less frequent backup interval than x
if x.TimeStamp.Before(y.TimeStamp) {
return -1, nil }
if x.TimeStamp.After(y.TimeStamp) {
return 1, nil }
return 0, nil}
You can get the entire source code for the tool below.
I will start with the snapshot names. The part after the @. Those start with
zfs-auto-snap so “zfs-auto-snap” will match it.
The next section is which timer made the snapshot. This section can also be
called the increment. The valid timers are yearly, monthly, weekly, daily,
hourly, and frequent for a default install of zfs-auto-snapshot. The regex
“yearly|monthly|weekly|daily|hourly|frequent” will match these timers. However,
I would like to get which timer created the snapshot without further parsing.
That is the perfect job for a capturing sub match. After adding the capturing
sub match, the regex looks like
“(?P<increment>yearly|monthly|weekly|daily|hourly|frequent)”.
The final section is the timestamp of the snapshot. Like with the timer section,
it is useful not to have to parse this data a second time. With the sub matches
“(?P<year>[[:digit:]]{4})-(?P<month>[[:digit:]]{2})-(?P<day>[[:digit:]]{2})-(?P<hour>[[:digit:]]{2})(?P<minute>[[:digit:]]{2})”
will work.
With the snapshot names completed, I need to capture the zfs tree structure
before the @ symbol. I haven’t found a reliable regular expression that will
capture that tree but “(?:[[:word:]-.]+)+(?:/?[[:word:]-.]+)*” will recognize
a subset of all valid zfs trees. Avoid using anything it won’t recognize, or you
may end up with inaccessible files.
Including some test code the tool’s source code looks like this so far.
Written by: Robert R. Russell on Tuesday, August 4, 2020.
I haven’t seen a lot of tools that are designed to backup ZFS snapshots to
removable media. So, I am writing my own. I am going to document the process
here.
The basic loop for a backup tool is
Read a list of snapshots on the source.
Read a list of snapshots on the destination.
Find the list of snapshots on the source that are not on the destination.
These are the non backed up snapshots.
Find the list of snapshots on the destination that are not on the source.
These are the aged out snapshots.
Copy all non backed up snapshots to the destination, preferably one at a time
to make recovery from IO failure easier.
Remove the aged out snapshots.
I am designing this tool to only backup snapshots taken by zfs-auto-snapshot.
These are named
<pool|filesystem>@zfs-auto-snap_<time interval>-<year>-<month>-<day>-<hour><minute>.
The command zfs -Hrt snapshot <source poolname> will generate a list of
all snapshots in a pool in a machine parseable format.
Issuing the command zfs send -ci <old snapshot> <pool|filesystem>@<new snapshot>
will send an incremental snapshot from old to new to the commands standard
output. I can estimate the amount of data to be transferred by replacing
-ci with -cpvni in the zfs send command.
Issuing the command zfs receive -u <backup location> will store a snapshot
from its standard input to the backup location.
Snapshots are removed by zfs destroy -d <pool|filesystem>@<snapshot name>.
The snapshot name is the portion of the snapshot pattern mentioned above after
the @ symbol.