Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ type ServiceInfo struct {
Startup ServiceStartup `json:"startup" yaml:"startup"`
Current ServiceStatus `json:"current" yaml:"current"`
CurrentSince time.Time `json:"current-since,omitzero" yaml:"current-since,omitempty"`
Scheduled time.Time `json:"scheduled,omitzero" yaml:"scheduled,omitempty"`
}

// ServiceStartup defines the different startup modes for a service.
Expand Down
1 change: 1 addition & 0 deletions docs/how-to/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ As your needs grow, you may want to use advanced Pebble features to run services
:maxdepth: 1

Run services reliably <run-services-reliably>
Run services on a schedule <schedule-services>
Manage service dependencies <service-dependencies>
Use layers <use-layers>
```
Expand Down
60 changes: 60 additions & 0 deletions docs/how-to/schedule-services.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# How to run services on a schedule

It can be useful to start a service periodically, either to ensure that a service
is no longer stopped or to perform some action.

For example, if you have a service that handles software updates, an administrator
might manually stop this service while they perform maintainence on a system. If
the administrator forgets to start the service again, important software updates
might be missed. By setting a schedule to start the service, we can ensure it
is running again.

Another example that is often seen with systems using TLS certificates, is that
the certificates are issued with a short validity period requiring renewal before
they expire. Using a tool like Certbot, we can fetch new certificates, but for
this to help, we need Certbot to run before the certificate expires.

(run-a-service-on-a-schedule)=
## Run a service on a schedule

To ensure a service is started, a schedule can be used to start the service.
This is useful to either delay the start of a service or to ensure the service
is started if it was manually stopped.

This is an example configuration for a scheduled start service that ensures the
service is running once per day:

```yaml
services:
svc1:
override: replace
command: daily.sh
startup: disabled
schedule: 0:00 # Once per day at 0:00.
```

(run-a-service-as-a-cron-job)=
## Run a service as a cron job

To run a service as a scheduled repeating one-shot service, it is required to
set the `on-success` field to ignore when the service process exits.
Since the schedule will eventually start the service again, it may be useful to
set the `on-failure` field to ignore the failure.

This is an example configuration for a cron job service:

```yaml
services:
svc1:
override: replace
command: ping -c 3 localhost
startup: disabled
schedule: 0:00-24:00/1440 # Once per minute.
on-success: ignore
on-failure: ignore
```

## See more

- [Layer specification](../reference/layer-specification)
- [Timer string format](../reference/timer-string-format)
16 changes: 16 additions & 0 deletions docs/reference/layer-specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,22 @@ services:
# Pebble starts or performs a 'replan' operation. Default is "disabled".
startup: enabled | disabled

# (Optional) A schedule of when to start the service automatically.
#
# By default, a service has no schedule by which it is started.
#
# See https://ubuntu.com/docs/pebble/reference/timer-string-format/ for
# the syntax of the timer string format, including more examples of its
# use.
#
# Examples:
# - `00:00-24:00/24` - Every hour on the hour
# - `00:00-24:00/48` - Every 30 minutes
# - `00:00-24:00/96` - Every 15 minutes
# - `12:00-13:00/12` - Every 5 minutes from 12:00 to 13:10
# - `23:00` - Every day at 23:00
schedule: <timer string>

# (Optional) A list of other services in the plan that this service
# should start after.
after:
Expand Down
153 changes: 153 additions & 0 deletions docs/reference/timer-string-format.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# Timer string format

Timer strings are used for configuring service `schedule`s.

See this [discourse thread](https://forum.snapcraft.io/t/refresh-scheduling-on-specific-days-of-the-month/1239/6) for details on how the syntax was conceived and evolved over time.

## Syntax

```
eventlist = eventset *( ",," eventset )
eventset = wdaylist / timelist / wdaylist "," timelist

wdaylist = wdayset *( "," wdayset )
wdayset = wday / wdaynumber / wdayspan
wday = ( "mon" / "tue" / "wed" / "thu" / "fri" / "sat" / "sun" )
wdaynumber = ( "sun" / "mon" / "tue" / "wed" / "thu" / "fri" / "sat" ) DIGIT
wdayspan = wday "-" wday / wdaynumber "-" wday / wday "-" wdaynumber
wspec = ( "1" / "2" / "3" / "4" / "5" )

timelist = timeset *( "," timeset )
timeset = time / timespan
time = 2DIGIT ":" 2DIGIT
timespan = time ( "-" / "~" ) time [ "/" count ]
count = n*DIGIT
```
Clock times are always specified in 24H format.

## Examples

* `00:00-24:00/24`</br>
Every hour on the hour

* `00:00-24:00/48`</br>
Every 30 minutes

* `00:00-24:00/96`</br>
Every 15 minutes

* `12:00-13:00/12`</br>
Every 5 minutes from 12:00 to 13:10

* `23:00`</br>
Every day at 23:00

More specific timer examples:

* `mon,10:00,,fri,15:00`</br>
Mondays at 10:00, Fridays at 15:10

* `mon,fri,10:00,15:00`</br>
Mondays at 10:00 and 15:00, Fridays at 10:00 and 15:00

* `mon-wed,fri,9:00-11:00/2`</br>
Monday to Wednesday and on Friday, twice between 9:00 and 11:00

* `mon,9:00~11:00,,wed,22:00~23:00`</br>
Mondays, some time between 9:00 and 11:00, and on Wednesdays, some time between 22:00 and 23:00

* `mon,wed`</br>
Monday and on Wednesday, at 0:00

* `mon2-wed,23:00-24:00`</br>
2nd Monday of the month through the following Wednesday, between 23:00 and 24:00

* `fri5,23:00-01:00`</br>
Last Friday of the month, from 23:00 to 1:00 the next day. Even in months with 4 Fridays, this schedule will still trigger on the last Friday.

## Semantics

A timer string is composed of one or more event sets, which are combined by using commas (`,,`) as separators.

Each event set defines the weekdays and the time windows in which events may occur. The next event will be scheduled inside the soonest opportunity that matches both one of the provided weekdays and one of the provided time windows. If no weekdays are provided, the default is every day. If no time windows are provided, the default is an arbitrary time in the day.

For example, consider the timer:

mon,fri,10:00,15:00

Assuming today is Sunday, the next 5 events are, in order:

Monday 10:00
Monday 15:00
Friday 10:00
Friday 15:00
Monday 10:00

Consider the following timer:

mon,10:00,,fri,15:00

The next 3 events in this case are:

Monday 10am
Friday 15pm
Monday 10am

All of these examples work on a weekly basis, but certain events are better scheduled on a monthly basis. To support that, weekdays may be suffixed by `wspec` entry that defines the week number inside the month. As an example, the following timer defines two events every month, on the first and third Mondays at 15:00:

mon1,mon3,15:00

As a special case, the 5th week is considered the last one to hold the given day, so that specifying an event on the last Friday of the month, for instance, is done simply as:

fri5

In addition to specifying precise weekdays, an interval may be used to define a larger span:

mon-fri,15:00

This represents an event per day at 15:00, Monday through Friday, every week.

The same interval syntax also works to define time spans, but the meaning is slightly different. For instance, consider this time span:

mon,14:00-16:00

It defines an event every Monday that will take place at the earliest chance between 14:100 and 16:00.

Weekday spans define an event **every day** within the span. In contrast, time spans define a **single event** inside the defined span.

That latter aspect may be changed via an explicit divisor, which may be specified as a `count`. For instance, consider this time span:

8:00-16:00/2

This represents two events every day, one in the morning between 8:00 and 12:00, and another one between 12:00 and 16:00.

While the following represents an hourly event, every day of the week:

0:00-24:00/24

All of the time spans defined so far work similarly in the sense that the start of the span defines the earliest chance in which the event may start, and the end of the span defines the latest chance for the event to have started. For various reasons, though, it’s often useful to introduce some level of randomization inside the time span so that events won’t all start at exactly the same time. This may be achieved by replacing the time span dash character (`-`) by a tilde (`~`). Consider the following time span:

0:00~24:00/4

It represents 4 events that will take place at a random time inside time windows of 6 hours each.

Week spans that need to start or end during a specific week in the month can be defined by appending the week number to either the start or end of a week span. Consider the following schedules:

mon1-fri
mon-fri1

The first example describes a week span that starts on the first Monday of the month and ends on the following Friday, while the second example defines a week span that starts on the Monday _before_ the first Friday of the month, which is when the span ends.

Consider the following calendar months of July and August 2019:

```
July August
Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa
1 2 3 4 5 6 1 2 3
7 8 9 10 11 12 13 4 5 6 7 8 9 10
14 15 16 17 18 19 20 11 12 13 14 15 16 17
21 22 23 24 25 26 27 18 19 20 21 22 23 24
28 29 30 31 25 26 27 28 29 30 31
```

In the above context, `mon1-fri` corresponds describes a span from 5th of August to the 9th of August, while `mon-fri1` covers 29th of July until 2nd of August.
6 changes: 3 additions & 3 deletions internals/cli/cmd_enter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,9 @@ func (s *PebbleSuite) TestEnterUnknownCommand(c *C) {

func (s *PebbleSuite) TestEnterServicesStatus(c *C) {
expectedOutput := dumbDedent(`
Service Startup Current Since
write-message-01 enabled inactive -
write-message-02 disabled inactive -
Service Startup Scheduled Current Since
write-message-01 enabled - inactive -
write-message-02 disabled - inactive -
`)

writeMessageServices(s)
Expand Down
8 changes: 6 additions & 2 deletions internals/cli/cmd_services.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,18 @@ func (cmd *cmdServices) writeText(services []*client.ServiceInfo) error {
w := tabWriter()
defer w.Flush()

fmt.Fprintln(w, "Service\tStartup\tCurrent\tSince")
fmt.Fprintln(w, "Service\tStartup\tScheduled\tCurrent\tSince")

for _, svc := range services {
scheduled := "-"
if !svc.Scheduled.IsZero() {
scheduled = cmd.fmtTime(svc.Scheduled)
}
since := "-"
if !svc.CurrentSince.IsZero() {
since = cmd.fmtTime(svc.CurrentSince)
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", svc.Name, svc.Startup, svc.Current, since)
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", svc.Name, svc.Startup, scheduled, svc.Current, since)
}
return nil
}
Expand Down
25 changes: 13 additions & 12 deletions internals/cli/cmd_services_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func (s *PebbleSuite) TestServices(c *check.C) {
"status-code": 200,
"result": [
{"name": "svc1", "current": "inactive", "startup": "enabled", "current-since": "2022-04-28T17:05:23+12:00"},
{"name": "svc2", "current": "inactive", "startup": "enabled"},
{"name": "svc2", "current": "inactive", "startup": "enabled", "scheduled": "2030-04-28T17:05:23+12:00"},
{"name": "svc3", "current": "backoff", "startup": "enabled"}
]
}`)
Expand All @@ -43,10 +43,10 @@ func (s *PebbleSuite) TestServices(c *check.C) {
c.Assert(err, check.IsNil)
c.Assert(rest, check.HasLen, 0)
c.Check(s.Stdout(), check.Equals, `
Service Startup Current Since
svc1 enabled inactive 2022-04-28
svc2 enabled inactive -
svc3 enabled backoff -
Service Startup Scheduled Current Since
svc1 enabled - inactive 2022-04-28
svc2 enabled 2030-04-28 inactive -
svc3 enabled - backoff -
`[1:])
c.Check(s.Stderr(), check.Equals, "")
}
Expand Down Expand Up @@ -133,17 +133,17 @@ func (s *PebbleSuite) TestServicesNames(c *check.C) {
"status-code": 200,
"result": [
{"name": "bar", "current": "active", "startup": "disabled", "current-since": "2022-04-28T17:05:23+12:00"},
{"name": "foo", "current": "inactive", "startup": "enabled"}
{"name": "foo", "current": "inactive", "startup": "enabled", "scheduled": "2030-04-28T17:05:23+12:00"}
]
}`)
})
rest, err := cli.ParserForTest().ParseArgs([]string{"services", "foo", "bar", "--abs-time"})
c.Assert(err, check.IsNil)
c.Assert(rest, check.HasLen, 0)
c.Check(s.Stdout(), check.Equals, `
Service Startup Current Since
bar disabled active 2022-04-28T17:05:23+12:00
foo enabled inactive -
Service Startup Scheduled Current Since
bar disabled - active 2022-04-28T17:05:23+12:00
foo enabled 2030-04-28T17:05:23+12:00 inactive -
`[1:])
c.Check(s.Stderr(), check.Equals, "")
}
Expand All @@ -158,15 +158,15 @@ func (s *PebbleSuite) TestServicesJSON(c *check.C) {
"status-code": 200,
"result": [
{"name": "svc1", "current": "inactive", "startup": "enabled", "current-since": "2022-04-28T17:05:23+12:00"},
{"name": "svc2", "current": "inactive", "startup": "enabled"},
{"name": "svc2", "current": "inactive", "startup": "enabled", "scheduled": "2030-04-28T17:05:23+12:00"},
{"name": "svc3", "current": "backoff", "startup": "enabled"}
]
}`)
})
rest, err := cli.ParserForTest().ParseArgs([]string{"services", "--format", "json"})
c.Assert(err, check.IsNil)
c.Assert(rest, check.HasLen, 0)
c.Check(s.Stdout(), check.Equals, `{"services":{"svc1":{"name":"svc1","startup":"enabled","current":"inactive","current-since":"2022-04-28T17:05:23+12:00"},"svc2":{"name":"svc2","startup":"enabled","current":"inactive"},"svc3":{"name":"svc3","startup":"enabled","current":"backoff"}}}`+"\n")
c.Check(s.Stdout(), check.Equals, `{"services":{"svc1":{"name":"svc1","startup":"enabled","current":"inactive","current-since":"2022-04-28T17:05:23+12:00"},"svc2":{"name":"svc2","startup":"enabled","current":"inactive","scheduled":"2030-04-28T17:05:23+12:00"},"svc3":{"name":"svc3","startup":"enabled","current":"backoff"}}}`+"\n")
c.Check(s.Stderr(), check.Equals, "")
}

Expand All @@ -180,7 +180,7 @@ func (s *PebbleSuite) TestServicesYAML(c *check.C) {
"status-code": 200,
"result": [
{"name": "svc1", "current": "inactive", "startup": "enabled", "current-since": "2022-04-28T17:05:23+12:00"},
{"name": "svc2", "current": "inactive", "startup": "enabled"},
{"name": "svc2", "current": "inactive", "startup": "enabled", "scheduled": "2030-04-28T17:05:23+12:00"},
{"name": "svc3", "current": "backoff", "startup": "enabled"}
]
}`)
Expand All @@ -199,6 +199,7 @@ services:
name: svc2
startup: enabled
current: inactive
scheduled: 2030-04-28T17:05:23+12:00
svc3:
name: svc3
startup: enabled
Expand Down
4 changes: 4 additions & 0 deletions internals/daemon/api_services.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type serviceInfo struct {
Startup string `json:"startup"`
Current string `json:"current"`
CurrentSince *time.Time `json:"current-since,omitempty"` // pointer as omitempty doesn't work with time.Time directly
Scheduled *time.Time `json:"scheduled,omitempty"` // pointer as omitempty doesn't work with time.Time directly
}

func v1GetServices(c *Command, r *http.Request, _ *UserState) Response {
Expand All @@ -54,6 +55,9 @@ func v1GetServices(c *Command, r *http.Request, _ *UserState) Response {
if !svc.CurrentSince.IsZero() {
info.CurrentSince = &svc.CurrentSince
}
if !svc.Scheduled.IsZero() {
info.Scheduled = &svc.Scheduled
}
infos = append(infos, info)
}
return SyncResponse(infos)
Expand Down
Loading
Loading