Skip to content

Commit

Permalink
Merge pull request #524 from kelvinfan001/pr/time-zones
Browse files Browse the repository at this point in the history
strategy/periodic: support configuring a time zone for reboot windows
  • Loading branch information
Luca Bruno authored Apr 29, 2021
2 parents 0a3d264 + 130c039 commit 541afbd
Show file tree
Hide file tree
Showing 14 changed files with 475 additions and 36 deletions.
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ tempfile = "^3.2"
thiserror = "1.0"
tokio = { version = "1.5", features = ["rt", "rt-multi-thread"] }
toml = "0.5"
tzfile = "0.1.3"
url = { version = "2.2", features = ["serde"] }
users = "0.11.0"
zbus = "1.9.1"
Expand Down
15 changes: 9 additions & 6 deletions docs/development/update-strategy-periodic.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ The agent supports a `periodic` strategy, which allows gating reboots based on "

This strategy is a port of [locksmith reboot windows][locksmith], with a few differences:

* all times and dates are UTC-based to avoid skews and ambiguities
* multiple disjoint reboot windows are supported
* multiple configuration entries are assembled into a single weekly calendar
* weekdays need to be specified, in either long or abbreviated form
Expand All @@ -26,19 +25,23 @@ In order to ease the case where the same time-window has to be applied on multip

The start of a reboot window is a single point in time, specified in 24h format with minutes granularity (e.g. `22:30`) via the `start_time` parameter.

A key part of this logic is that all times and dates are UTC-based, in order to guarantee the correctness of maintenance windows.
In particular, UTC times are needed to avoid:
By default, all times and dates are UTC-based.
UTC times must be used to avoid:

* skipping reboot windows due to Daylight Saving Time time-change
* overshooting reboot windows due to Daylight Saving Time time-change
* shortening or skipping reboot windows due to Daylight Saving Time time-change
* lengthening reboot windows due to Daylight Saving Time time-change
* mixups due to short-notice law changes in time-zone definitions
* errors due to stale `tzdata` entries
* human confusion on machines with different local-timezone configurations

Overall this strategy aims at guaranteeing that the total weekly length for reboot windows is respected, regardless of local timezone laws.
Overall, the use of the default UTC times guarantee that the total weekly length for reboot windows is respected, regardless of local time zone laws.

As a side-effect, this also helps when cross-checking configurations across multiple machines located in different places.

Nevertheless, user-specified non-UTC time zones can still be configured, but with [caveats][time-zone-caveats].

[time-zone-caveats]: ../usage/updates-strategy.md#time-zone-caveats

# Implementation details

Configuration fragments are merged into a single weekly calendar.
Expand Down
70 changes: 69 additions & 1 deletion docs/usage/updates-strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ The `periodic` strategy allows Zincati to only reboot for updates during certain
Outside of those maintenance windows, reboots are not automatically performed and auto-updates are staged and held until the next available window.

Reboot windows recur on a weekly basis, and can be defined in any arbitrary order and length. Their individual length must be greater than zero.
To avoid timezone-related skews in a fleet of machines, all maintenance windows are defined in UTC dates and times.
By default, all maintenance windows are defined in UTC dates and times. This is meant to avoid timezone-related skews in a fleet of machines, as well as possible side-effects of Daylight Savings Time (DST) policies.

Periodic reboot windows can be configured and enabled in the following way:

Expand Down Expand Up @@ -101,3 +101,71 @@ Reboot windows can be separately configured in multiple snippets, as long as eac
* `length_minutes`: non-zero window duration, in minutes

For convenience, multiple entries can be defined with overlapping times, and each window definition is allowed to cross day and week boundaries (wrapping to the next day).

## Time zone configuration

To configure a non-UTC time zone for all the reboot windows, specify the `time_zone` field in a `updates.periodic` entry. The specified time zone must be either `"localtime"` or a time zone name from the [IANA Time Zone Database][IANA_tz_db] (you can find an unofficial list of time zone names [here][wikipedia_tz_names]).

If using `"localtime"`, the system's [local time zone configuration file][localtime], `/etc/localtime`, is used. As such, `/etc/localtime` must either be a symlink to a valid `tzfile` entry in your system's local time zone database (under `/usr/share/zoneinfo/`), or not exist, in which case `UTC` is used.

Note that you can only specify a single time zone for _all_ reboot windows.

A time zone can be specified in the following way:

```toml
[updates]
strategy = "periodic"

[updates.periodic]
time_zone = "America/Panama"

[[updates.periodic.window]]
days = [ "Sat", "Sun" ]
start_time = "23:30"
length_minutes = 60

[[updates.periodic.window]]
days = [ "Mon" ]
start_time = "00:00"
length_minutes = 60
```

Since Panama does not have Daylight Savings Time and follows Eastern Standard Time (which has a fixed offset of UTC -5) all year, the above configuration would result in two maintenance windows during which Zincati is allowed to reboot the machine for updates:
* 60 minutes starting at 23:30 EST on Saturday night, and ending at 00:30 EST on Sunday morning
* 90 minutes starting at 23:30 EST on Sunday night, and ending at 01:00 EST on Monday morning

### Time zone caveats

:warning: **Reboot window lengths may vary.**

Because reboot window clock times are always obeyed, reboot windows may be lengthened or shortened due to shifts in clock time. For example, with the `US/Eastern` time zone which shifts between Eastern Standard Time and Eastern Daylight Time, on "fall back" day, a specified reboot window may be lengthened by up to one hour; on "spring forward" day, a specified reboot window may be shortened by up to one hour, or skipped entirely.

Example of varying length reboot windows using the `US/Eastern` time zone:

```toml
[updates]
strategy = "periodic"

[updates.periodic]
time_zone = "US/Eastern"

[[updates.periodic.window]]
days = [ "Sun" ]
start_time = "01:30"
length_minutes = 60
```

The above configuration will result in reboots being allowed at 1:30 AM to 2:30 AM on _every_ Sunday. This includes days when a Daylight Savings Shift occurs.

On the `US/Eastern` time zone's "fall back" day, where clocks are shifted back by one hour on a Sunday in Fall just before 3:00 AM, the thirty minutes between 2:00 AM and 2:30 AM will occur twice. As such, the reboot window will be lengthened by thirty minutes each year on "fall back" day.

On "spring forward" day, where clocks are shifted forward by one hour on a Sunday in Spring just before 2:00 AM, the thirty minutes between 2:00 AM and 2:30 AM will not occur. As such, the reboot window will be shortened by thirty minutes each year on "spring forward" day. Effectively, the reboot window on "spring forward" day will only be between 1:30 AM and 2:00 AM.

:warning: **Incorrect reboot times due to stale time zone database.**

Time zone data is read from the system's time zone database at `/usr/share/zoneinfo`. This directory and its contents are part of the `tzdata` RPM package; in the latest release of Fedora CoreOS, `tzdata` should be kept fairly up-to-date with the latest official release from the IANA.
However, if your system does not have the latest IANA time zone database, or there is a sudden policy change in the jurisdiction associated with your configured time zone, then reboots may happen at unexpected and incorrect times.

[IANA_tz_db]: https://www.iana.org/time-zones
[wikipedia_tz_names]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
[localtime]: https://www.freedesktop.org/software/systemd/man/localtime.html
6 changes: 6 additions & 0 deletions src/config/fragments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ pub(crate) struct UpdateFleetLock {
pub(crate) struct UpdatePeriodic {
/// A weekly window.
pub(crate) window: Option<Vec<UpdatePeriodicWindow>>,
/// A time zone in the IANA Time Zone Database (https://www.iana.org/time-zones)
/// or "localtime". If unset, UTC is used.
///
/// Examples: `America/Toronto`, `Europe/Rome`
pub(crate) time_zone: Option<String>,
}

/// Config fragment for a `periodic.window` entry.
Expand Down Expand Up @@ -138,6 +143,7 @@ mod tests {
length_minutes: 25,
},
]),
time_zone: Some("localtime".to_string()),
}),
}),
};
Expand Down
11 changes: 10 additions & 1 deletion src/config/inputs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,9 @@ pub(crate) struct FleetLockInput {
pub(crate) struct PeriodicInput {
/// Set of updates windows.
pub(crate) intervals: Vec<PeriodicIntervalInput>,
/// A time zone in the IANA Time Zone Database or "localtime".
/// Defaults to "UTC".
pub(crate) time_zone: String,
}

/// Update window for a "periodic" interval.
Expand All @@ -203,7 +206,10 @@ impl UpdateInput {
let mut fleet_lock = FleetLockInput {
base_url: String::new(),
};
let mut periodic = PeriodicInput { intervals: vec![] };
let mut periodic = PeriodicInput {
intervals: vec![],
time_zone: "UTC".to_string(),
};

for snip in fragments {
if let Some(a) = snip.allow_downgrade {
Expand All @@ -221,6 +227,9 @@ impl UpdateInput {
}
}
if let Some(w) = snip.periodic {
if let Some(tz) = w.time_zone {
periodic.time_zone = tz;
}
if let Some(win) = w.window {
for entry in win {
for day in entry.days {
Expand Down
10 changes: 8 additions & 2 deletions src/strategy/fleet_lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,10 @@ mod tests {
fleet_lock: FleetLockInput {
base_url: "https://example.com".to_string(),
},
periodic: PeriodicInput { intervals: vec![] },
periodic: PeriodicInput {
intervals: vec![],
time_zone: "UTC".to_string(),
},
};

let res = StrategyFleetLock::new(input, &id);
Expand All @@ -120,7 +123,10 @@ mod tests {
fleet_lock: FleetLockInput {
base_url: String::new(),
},
periodic: PeriodicInput { intervals: vec![] },
periodic: PeriodicInput {
intervals: vec![],
time_zone: "localtime".to_string(),
},
};

let res = StrategyFleetLock::new(input, &id);
Expand Down
9 changes: 3 additions & 6 deletions src/strategy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,9 @@ impl UpdateStrategy {
match self {
UpdateStrategy::FleetLock(_) => self.configuration_label().to_string(),
UpdateStrategy::Immediate(_) => self.configuration_label().to_string(),
UpdateStrategy::Periodic(p) => format!(
"{}, total schedule length {} minutes (next window {})",
self.configuration_label(),
p.schedule_length_minutes(),
p.human_remaining()
),
UpdateStrategy::Periodic(p) => {
format!("{}, {}", self.configuration_label(), p.calendar_summary(),)
}
}
}

Expand Down
Loading

0 comments on commit 541afbd

Please sign in to comment.