Merge lp:~mvo/snappy/15.04-services into lp:~snappy-dev/snappy/15.04-deprecated

Proposed by Michael Vogt on 2015-09-14
Status: Rejected
Rejected by: Michael Vogt on 2015-09-15
Proposed branch: lp:~mvo/snappy/15.04-services
Merge into: lp:~snappy-dev/snappy/15.04-deprecated
Diff against target: 1348 lines (+890/-63)
14 files modified
cmd/snappy/cmd_service.go (+178/-0)
helpers/winsize.go (+48/-0)
i18n/dummy.go (+25/-0)
snappy/build.go (+1/-1)
snappy/click.go (+13/-13)
snappy/click_test.go (+18/-18)
snappy/errors.go (+3/-0)
snappy/parts.go (+2/-2)
snappy/service.go (+183/-0)
snappy/service_test.go (+179/-0)
snappy/snapp.go (+15/-15)
snappy/snapp_test.go (+13/-13)
systemd/systemd.go (+118/-0)
systemd/systemd_test.go (+94/-1)
To merge this branch: bzr merge lp:~mvo/snappy/15.04-services
Reviewer Review Type Date Requested Status
Snappy Developers 2015-09-14 Pending
Review via email: mp+270965@code.launchpad.net

Description of the Change

Backport snappy service command and helpers for the REST API.

To post a comment you must log in.

Unmerged revisions

482. By Michael Vogt on 2015-09-14

cherry pick -r649..650 from lp:snappy

481. By Michael Vogt on 2015-09-14

cherry pick -r 608..609 of lp:snappy

480. By Michael Vogt on 2015-09-14

cherry pick -r600..601 of lp:snappy to merge snappy services command

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'cmd/snappy/cmd_service.go'
2--- cmd/snappy/cmd_service.go 1970-01-01 00:00:00 +0000
3+++ cmd/snappy/cmd_service.go 2015-09-14 14:13:47 +0000
4@@ -0,0 +1,178 @@
5+// -*- Mode: Go; indent-tabs-mode: t -*-
6+
7+/*
8+ * Copyright (C) 2014-2015 Canonical Ltd
9+ *
10+ * This program is free software: you can redistribute it and/or modify
11+ * it under the terms of the GNU General Public License version 3 as
12+ * published by the Free Software Foundation.
13+ *
14+ * This program is distributed in the hope that it will be useful,
15+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
16+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+ * GNU General Public License for more details.
18+ *
19+ * You should have received a copy of the GNU General Public License
20+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
21+ *
22+ */
23+
24+package main
25+
26+import (
27+ "fmt"
28+ "os"
29+ "text/tabwriter"
30+
31+ "launchpad.net/snappy/helpers"
32+ "launchpad.net/snappy/i18n"
33+ "launchpad.net/snappy/logger"
34+ "launchpad.net/snappy/priv"
35+ "launchpad.net/snappy/progress"
36+ "launchpad.net/snappy/snappy"
37+)
38+
39+type cmdService struct {
40+ Status svcStatus `command:"status"`
41+ Start svcStart `command:"start"`
42+ Stop svcStop `command:"stop"`
43+ Restart svcRestart `command:"restart"`
44+ Logs svcLogs `command:"logs"`
45+}
46+
47+type svcBase struct {
48+ Args struct {
49+ Snap string `positional-arg-name:"snap"`
50+ Service string `positional-arg-name:"service"`
51+ } `positional-args:"yes"`
52+}
53+
54+type svcStatus struct{ svcBase }
55+type svcStart struct{ svcBase }
56+type svcStop struct{ svcBase }
57+type svcRestart struct{ svcBase }
58+type svcLogs struct{ svcBase }
59+
60+func init() {
61+ _, err := parser.AddCommand("service",
62+ i18n.G("Query and modify snappy services"),
63+ i18n.G("Query and modify snappy services of locally-installed packages"),
64+ &cmdService{})
65+
66+ if err != nil {
67+ logger.LogAndPanic(err)
68+ }
69+
70+}
71+
72+const (
73+ doStatus = iota
74+ doStart
75+ doStop
76+ doRestart
77+ doLogs
78+)
79+
80+func (s *svcBase) doExecute(cmd int) ([]string, error) {
81+ actor, err := snappy.FindServices(s.Args.Snap, s.Args.Service, progress.MakeProgressBar(""))
82+ if err != nil {
83+ return nil, err
84+ }
85+
86+ switch cmd {
87+ case doStatus:
88+ return actor.Status()
89+ case doLogs:
90+ return actor.Loglines()
91+ case doStart:
92+ return nil, actor.Start()
93+ case doStop:
94+ return nil, actor.Stop()
95+ case doRestart:
96+ return nil, actor.Restart()
97+ default:
98+ panic("can't happen")
99+ }
100+}
101+
102+func (s *svcStatus) Execute(args []string) error {
103+ stati, err := s.doExecute(doStatus)
104+ if err != nil {
105+ return err
106+ }
107+
108+ w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0)
109+
110+ ws, _ := helpers.GetTermWinsize()
111+ rows := int(ws.Row) - 2
112+
113+ header := i18n.G("Snap\tService\tState")
114+
115+ for i, status := range stati {
116+ // print a header every $rows rows if rows is bigger
117+ // than 10; otherwise, just the once. 10 is arbitrary,
118+ // but the thinking is that on the one hand you don't
119+ // want to be using up too much space with headers on
120+ // really small terminals and on the other rows might
121+ // actually be negative if you're not on a tty.
122+ if i%rows == 0 && (i == 0 || rows > 10) {
123+ fmt.Fprintln(w, header)
124+ }
125+ fmt.Fprintln(w, status)
126+ }
127+ w.Flush()
128+
129+ return err
130+}
131+
132+func (s *svcLogs) Execute([]string) error {
133+ privMutex := priv.New()
134+ if err := privMutex.TryLock(); err != nil {
135+ return err
136+ }
137+ defer privMutex.Unlock()
138+
139+ logs, err := s.doExecute(doLogs)
140+ if err != nil {
141+ return err
142+ }
143+
144+ for i := range logs {
145+ fmt.Println(logs[i])
146+ }
147+
148+ return nil
149+}
150+
151+func (s *svcStart) Execute(args []string) error {
152+ privMutex := priv.New()
153+ if err := privMutex.TryLock(); err != nil {
154+ return err
155+ }
156+ defer privMutex.Unlock()
157+
158+ _, err := s.doExecute(doStart)
159+ return err
160+}
161+
162+func (s *svcStop) Execute(args []string) error {
163+ privMutex := priv.New()
164+ if err := privMutex.TryLock(); err != nil {
165+ return err
166+ }
167+ defer privMutex.Unlock()
168+
169+ _, err := s.doExecute(doStop)
170+ return err
171+}
172+
173+func (s *svcRestart) Execute(args []string) error {
174+ privMutex := priv.New()
175+ if err := privMutex.TryLock(); err != nil {
176+ return err
177+ }
178+ defer privMutex.Unlock()
179+
180+ _, err := s.doExecute(doRestart)
181+ return err
182+}
183
184=== added file 'helpers/winsize.go'
185--- helpers/winsize.go 1970-01-01 00:00:00 +0000
186+++ helpers/winsize.go 2015-09-14 14:13:47 +0000
187@@ -0,0 +1,48 @@
188+// -*- Mode: Go; indent-tabs-mode: t -*-
189+
190+/*
191+ * Copyright (C) 2014-2015 Canonical Ltd
192+ *
193+ * This program is free software: you can redistribute it and/or modify
194+ * it under the terms of the GNU General Public License version 3 as
195+ * published by the Free Software Foundation.
196+ *
197+ * This program is distributed in the hope that it will be useful,
198+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
199+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
200+ * GNU General Public License for more details.
201+ *
202+ * You should have received a copy of the GNU General Public License
203+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
204+ *
205+ */
206+
207+package helpers
208+
209+import (
210+ "syscall"
211+ "unsafe"
212+)
213+
214+// Winsize is from tty_ioctl(4)
215+type Winsize struct {
216+ Row uint16
217+ Col uint16
218+ xpixel uint16 // unused
219+ Ypixel uint16 // unused
220+}
221+
222+// GetTermWinsize performs the TIOCGWINSZ ioctl on stdout
223+func GetTermWinsize() (*Winsize, error) {
224+ ws := &Winsize{}
225+ x, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(syscall.Stdout), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(ws)))
226+
227+ if int(x) == -1 {
228+ // returning ws on error lets people that don't care
229+ // about the error get on with querying the struct
230+ // (which will be empty on error).
231+ return ws, errno
232+ }
233+
234+ return ws, nil
235+}
236
237=== added directory 'i18n'
238=== added file 'i18n/dummy.go'
239--- i18n/dummy.go 1970-01-01 00:00:00 +0000
240+++ i18n/dummy.go 2015-09-14 14:13:47 +0000
241@@ -0,0 +1,25 @@
242+// -*- Mode: Go; indent-tabs-mode: t -*-
243+
244+/*
245+ * Copyright (C) 2014-2015 Canonical Ltd
246+ *
247+ * This program is free software: you can redistribute it and/or modify
248+ * it under the terms of the GNU General Public License version 3 as
249+ * published by the Free Software Foundation.
250+ *
251+ * This program is distributed in the hope that it will be useful,
252+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
253+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
254+ * GNU General Public License for more details.
255+ *
256+ * You should have received a copy of the GNU General Public License
257+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
258+ *
259+ */
260+
261+package i18n
262+
263+// dummy implementation
264+func G(s string) string {
265+ return s
266+}
267
268=== added directory 'po'
269=== modified file 'snappy/build.go'
270--- snappy/build.go 2015-05-07 08:28:03 +0000
271+++ snappy/build.go 2015-09-14 14:13:47 +0000
272@@ -144,7 +144,7 @@
273 return err
274 }
275
276- for _, v := range m.Services {
277+ for _, v := range m.ServiceYamls {
278 hookName := filepath.Base(v.Name)
279
280 if _, ok := m.Integration[hookName]; !ok {
281
282=== modified file 'snappy/click.go'
283--- snappy/click.go 2015-09-11 19:56:16 +0000
284+++ snappy/click.go 2015-09-14 14:13:47 +0000
285@@ -496,11 +496,11 @@
286 return nil
287 }
288
289-func verifyServiceYaml(service Service) error {
290+func verifyServiceYaml(service ServiceYaml) error {
291 return verifyStructStringsAgainstWhitelist(service, servicesBinariesStringsWhitelist)
292 }
293
294-func generateSnapServicesFile(service Service, baseDir string, aaProfile string, m *packageYaml) (string, error) {
295+func generateSnapServicesFile(service ServiceYaml, baseDir string, aaProfile string, m *packageYaml) (string, error) {
296 if err := verifyServiceYaml(service); err != nil {
297 return "", err
298 }
299@@ -537,7 +537,7 @@
300 }), nil
301 }
302
303-func generateSnapSocketFile(service Service, baseDir string, aaProfile string, m *packageYaml) (string, error) {
304+func generateSnapSocketFile(service ServiceYaml, baseDir string, aaProfile string, m *packageYaml) (string, error) {
305 if err := verifyServiceYaml(service); err != nil {
306 return "", err
307 }
308@@ -554,21 +554,21 @@
309 }), nil
310 }
311
312-func generateServiceFileName(m *packageYaml, service Service) string {
313+func generateServiceFileName(m *packageYaml, service ServiceYaml) string {
314 return filepath.Join(snapServicesDir, fmt.Sprintf("%s_%s_%s.service", m.Name, service.Name, m.Version))
315 }
316
317-func generateBusPolicyFileName(m *packageYaml, service Service) string {
318+func generateBusPolicyFileName(m *packageYaml, service ServiceYaml) string {
319 return filepath.Join(snapBusPolicyDir, fmt.Sprintf("%s_%s_%s.conf", m.Name, service.Name, m.Version))
320 }
321
322-func generateSocketFileName(m *packageYaml, service Service) string {
323+func generateSocketFileName(m *packageYaml, service ServiceYaml) string {
324 return filepath.Join(snapServicesDir, fmt.Sprintf("%s_%s_%s.socket", m.Name, service.Name, m.Version))
325 }
326
327 // takes a directory and removes the global root, this is needed
328 // when the SetRoot option is used and we need to generate
329-// content for the "Services" and "Binaries" section
330+// content for the "ServiceYamls" and "Binaries" section
331 var stripGlobalRootDir = stripGlobalRootDirImpl
332
333 func stripGlobalRootDirImpl(dir string) string {
334@@ -594,7 +594,7 @@
335 return err
336 }
337
338- for _, service := range m.Services {
339+ for _, service := range m.ServiceYamls {
340 aaProfile, err := getSecurityProfile(m, service.Name, baseDir)
341 if err != nil {
342 return err
343@@ -687,7 +687,7 @@
344 return err
345 }
346 sysd := systemd.New(globalRootDir, inter)
347- for _, service := range m.Services {
348+ for _, service := range m.ServiceYamls {
349 serviceName := filepath.Base(generateServiceFileName(m, service))
350 if err := sysd.Disable(serviceName); err != nil {
351 return err
352@@ -718,7 +718,7 @@
353 }
354
355 // only reload if we actually had services
356- if len(m.Services) > 0 {
357+ if len(m.ServiceYamls) > 0 {
358 if err := sysd.DaemonReload(); err != nil {
359 return err
360 }
361@@ -795,7 +795,7 @@
362 // done via the click hooks but we really want to generate
363 // it all here
364
365- for _, svc := range m.Services {
366+ for _, svc := range m.ServiceYamls {
367 if err := addOneSecurityPolicy(m, svc.Name, svc.SecurityDefinitions, baseDir); err != nil {
368 return err
369 }
370@@ -825,7 +825,7 @@
371
372 func (m *packageYaml) removeSecurityPolicy(baseDir string) error {
373 // TODO: move apparmor policy removal here
374- for _, service := range m.Services {
375+ for _, service := range m.ServiceYamls {
376 if err := removeOneSecurityPolicy(m, service.Name, baseDir); err != nil {
377 return err
378 }
379@@ -1135,7 +1135,7 @@
380 if !dep.IsActive() {
381 continue
382 }
383- for _, svc := range dep.Services() {
384+ for _, svc := range dep.ServiceYamls() {
385 serviceName := filepath.Base(generateServiceFileName(dep.m, svc))
386 timeout := time.Duration(svc.StopTimeout)
387 if err = sysd.Stop(serviceName, timeout); err != nil {
388
389=== modified file 'snappy/click_test.go'
390--- snappy/click_test.go 2015-09-11 19:56:16 +0000
391+++ snappy/click_test.go 2015-09-14 14:13:47 +0000
392@@ -1208,7 +1208,7 @@
393 )
394
395 func (s *SnapTestSuite) TestSnappyGenerateSnapServiceTypeForking(c *C) {
396- service := Service{
397+ service := ServiceYaml{
398 Name: "xkcd-webserver",
399 Start: "bin/foo start",
400 Stop: "bin/foo stop",
401@@ -1228,7 +1228,7 @@
402 }
403
404 func (s *SnapTestSuite) TestSnappyGenerateSnapServiceAppWrapper(c *C) {
405- service := Service{
406+ service := ServiceYaml{
407 Name: "xkcd-webserver",
408 Start: "bin/foo start",
409 Stop: "bin/foo stop",
410@@ -1247,7 +1247,7 @@
411 }
412
413 func (s *SnapTestSuite) TestSnappyGenerateSnapServiceAppWrapperWithExternalPort(c *C) {
414- service := Service{
415+ service := ServiceYaml{
416 Name: "xkcd-webserver",
417 Start: "bin/foo start",
418 Stop: "bin/foo stop",
419@@ -1267,7 +1267,7 @@
420 }
421
422 func (s *SnapTestSuite) TestSnappyGenerateSnapServiceFmkWrapper(c *C) {
423- service := Service{
424+ service := ServiceYaml{
425 Name: "xkcd-webserver",
426 Start: "bin/foo start",
427 Stop: "bin/foo stop",
428@@ -1290,7 +1290,7 @@
429 }
430
431 func (s *SnapTestSuite) TestSnappyGenerateSnapServiceWrapperWhitelist(c *C) {
432- service := Service{Name: "xkcd-webserver",
433+ service := ServiceYaml{Name: "xkcd-webserver",
434 Start: "bin/foo start",
435 Stop: "bin/foo stop",
436 PostStop: "bin/foo post-stop",
437@@ -1307,23 +1307,23 @@
438 }
439
440 func (s *SnapTestSuite) TestServiceWhitelistSimple(c *C) {
441- c.Assert(verifyServiceYaml(Service{Name: "foo"}), IsNil)
442- c.Assert(verifyServiceYaml(Service{Description: "foo"}), IsNil)
443- c.Assert(verifyServiceYaml(Service{Start: "foo"}), IsNil)
444- c.Assert(verifyServiceYaml(Service{Stop: "foo"}), IsNil)
445- c.Assert(verifyServiceYaml(Service{PostStop: "foo"}), IsNil)
446+ c.Assert(verifyServiceYaml(ServiceYaml{Name: "foo"}), IsNil)
447+ c.Assert(verifyServiceYaml(ServiceYaml{Description: "foo"}), IsNil)
448+ c.Assert(verifyServiceYaml(ServiceYaml{Start: "foo"}), IsNil)
449+ c.Assert(verifyServiceYaml(ServiceYaml{Stop: "foo"}), IsNil)
450+ c.Assert(verifyServiceYaml(ServiceYaml{PostStop: "foo"}), IsNil)
451 }
452
453 func (s *SnapTestSuite) TestServiceWhitelistIllegal(c *C) {
454- c.Assert(verifyServiceYaml(Service{Name: "x\n"}), NotNil)
455- c.Assert(verifyServiceYaml(Service{Description: "foo\n"}), NotNil)
456- c.Assert(verifyServiceYaml(Service{Start: "foo\n"}), NotNil)
457- c.Assert(verifyServiceYaml(Service{Stop: "foo\n"}), NotNil)
458- c.Assert(verifyServiceYaml(Service{PostStop: "foo\n"}), NotNil)
459+ c.Assert(verifyServiceYaml(ServiceYaml{Name: "x\n"}), NotNil)
460+ c.Assert(verifyServiceYaml(ServiceYaml{Description: "foo\n"}), NotNil)
461+ c.Assert(verifyServiceYaml(ServiceYaml{Start: "foo\n"}), NotNil)
462+ c.Assert(verifyServiceYaml(ServiceYaml{Stop: "foo\n"}), NotNil)
463+ c.Assert(verifyServiceYaml(ServiceYaml{PostStop: "foo\n"}), NotNil)
464 }
465
466 func (s *SnapTestSuite) TestServiceWhitelistError(c *C) {
467- err := verifyServiceYaml(Service{Name: "x\n"})
468+ err := verifyServiceYaml(ServiceYaml{Name: "x\n"})
469 c.Assert(err.Error(), Equals, `services description field 'Name' contains illegal 'x
470 ' (legal: '^[A-Za-z0-9/. _#:-]*$')`)
471 }
472@@ -1545,7 +1545,7 @@
473 }
474
475 func (s *SnapTestSuite) TestSnappyGenerateSnapSocket(c *C) {
476- service := Service{Name: "xkcd-webserver",
477+ service := ServiceYaml{Name: "xkcd-webserver",
478 Start: "bin/foo start",
479 Description: "meep",
480 Socket: true,
481@@ -1579,7 +1579,7 @@
482 }
483
484 func (s *SnapTestSuite) TestSnappyGenerateSnapServiceWithSockte(c *C) {
485- service := Service{
486+ service := ServiceYaml{
487 Name: "xkcd-webserver",
488 Start: "bin/foo start",
489 Stop: "bin/foo stop",
490
491=== modified file 'snappy/errors.go'
492--- snappy/errors.go 2015-07-06 21:12:34 +0000
493+++ snappy/errors.go 2015-09-14 14:13:47 +0000
494@@ -29,6 +29,9 @@
495 // ErrPackageNotFound is returned when a snap can not be found
496 ErrPackageNotFound = errors.New("snappy package not found")
497
498+ // ErrServiceNotFound is returned when a service can not be found
499+ ErrServiceNotFound = errors.New("snappy service not found")
500+
501 // ErrNeedRoot is returned when a command needs root privs but
502 // the caller is not root
503 ErrNeedRoot = errors.New("this command requires root access. Please re-run using 'sudo'")
504
505=== modified file 'snappy/parts.go'
506--- snappy/parts.go 2015-07-07 02:35:22 +0000
507+++ snappy/parts.go 2015-09-14 14:13:47 +0000
508@@ -67,8 +67,8 @@
509 )
510
511 // Services implements snappy packages that offer services
512-type Services interface {
513- Services() []Service
514+type ServicesYamls interface {
515+ Services() []ServiceYaml
516 }
517
518 // Configuration allows requesting an oem snappy package type's config
519
520=== added file 'snappy/service.go'
521--- snappy/service.go 1970-01-01 00:00:00 +0000
522+++ snappy/service.go 2015-09-14 14:13:47 +0000
523@@ -0,0 +1,183 @@
524+// -*- Mode: Go; indent-tabs-mode: t -*-
525+
526+/*
527+ * Copyright (C) 2014-2015 Canonical Ltd
528+ *
529+ * This program is free software: you can redistribute it and/or modify
530+ * it under the terms of the GNU General Public License version 3 as
531+ * published by the Free Software Foundation.
532+ *
533+ * This program is distributed in the hope that it will be useful,
534+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
535+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
536+ * GNU General Public License for more details.
537+ *
538+ * You should have received a copy of the GNU General Public License
539+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
540+ *
541+ */
542+
543+package snappy
544+
545+import (
546+ "fmt"
547+ "path/filepath"
548+ "time"
549+
550+ "launchpad.net/snappy/i18n"
551+ "launchpad.net/snappy/progress"
552+ "launchpad.net/snappy/systemd"
553+)
554+
555+type svcT struct {
556+ m *packageYaml
557+ svc *ServiceYaml
558+}
559+
560+// A ServiceActor collects the services found by FindServices and lets
561+// you perform differnt actions (start, stop, etc) on them.
562+type ServiceActor struct {
563+ svcs []*svcT
564+ pb progress.Meter
565+ sysd systemd.Systemd
566+}
567+
568+// FindServices finds all matching services (empty string matches all)
569+// and lets you perform different actions (start, stop, etc) on them.
570+//
571+// If a snap is specified and no matching snaps are found,
572+// ErrPackageNotFound is returned. If a snap is specified and the
573+// matching snaps has no matching services, ErrServiceNotFound is
574+// returned.
575+//
576+// If no snap is specified, an empty result is not an error.
577+func FindServices(snapName string, serviceName string, pb progress.Meter) (*ServiceActor, error) {
578+ var svcs []*svcT
579+
580+ repo := NewMetaLocalRepository()
581+ installed, _ := repo.Installed()
582+
583+ foundSnap := false
584+ for _, part := range installed {
585+ snap, ok := part.(*SnapPart)
586+ if !ok {
587+ // can't happen
588+ continue
589+ }
590+ if snapName != "" && snapName != snap.Name() {
591+ continue
592+ }
593+ foundSnap = true
594+
595+ yamls := snap.ServiceYamls()
596+ for i := range yamls {
597+ if serviceName != "" && serviceName != yamls[i].Name {
598+ continue
599+ }
600+ s := &svcT{
601+ m: snap.m,
602+ svc: &yamls[i],
603+ }
604+ svcs = append(svcs, s)
605+ }
606+ }
607+ if snapName != "" {
608+ if !foundSnap {
609+ return nil, ErrPackageNotFound
610+ }
611+ if len(svcs) == 0 {
612+ return nil, ErrServiceNotFound
613+ }
614+ }
615+
616+ return &ServiceActor{
617+ svcs: svcs,
618+ pb: pb,
619+ sysd: systemd.New(globalRootDir, pb),
620+ }, nil
621+}
622+
623+// Status of all the found services.
624+func (actor *ServiceActor) Status() ([]string, error) {
625+ var stati []string
626+ for _, svc := range actor.svcs {
627+ svcname := filepath.Base(generateServiceFileName(svc.m, *svc.svc))
628+ status, err := actor.sysd.Status(svcname)
629+ if err != nil {
630+ return nil, err
631+ }
632+ status = fmt.Sprintf("%s\t%s\t%s", svc.m.Name, svc.svc.Name, status)
633+ stati = append(stati, status)
634+ }
635+
636+ return stati, nil
637+}
638+
639+// Start all the found services.
640+func (actor *ServiceActor) Start() error {
641+ for _, svc := range actor.svcs {
642+ svcname := filepath.Base(generateServiceFileName(svc.m, *svc.svc))
643+ if err := actor.sysd.Start(svcname); err != nil {
644+ // TRANSLATORS: the first %s is the package name, the second is the service name; the %v is the error
645+ return fmt.Errorf(i18n.G("unable to start %s's service %s: %v"), svc.m.Name, svc.svc.Name, err)
646+ }
647+ }
648+
649+ return nil
650+}
651+
652+// Stop all the found services.
653+func (actor *ServiceActor) Stop() error {
654+ for _, svc := range actor.svcs {
655+ svcname := filepath.Base(generateServiceFileName(svc.m, *svc.svc))
656+ if err := actor.sysd.Stop(svcname, time.Duration(svc.svc.StopTimeout)); err != nil {
657+ // TRANSLATORS: the first %s is the package name, the second is the service name; the %v is the error
658+ return fmt.Errorf(i18n.G("unable to stop %s's service %s: %v"), svc.m.Name, svc.svc.Name, err)
659+ }
660+ }
661+
662+ return nil
663+}
664+
665+// Restart all the found services.
666+func (actor *ServiceActor) Restart() error {
667+ err := actor.Stop()
668+ if err != nil {
669+ return err
670+ }
671+
672+ return actor.Start()
673+}
674+
675+// Logs for all found services.
676+func (actor *ServiceActor) Logs() ([]systemd.Log, error) {
677+ var svcnames []string
678+
679+ for _, svc := range actor.svcs {
680+ svcname := filepath.Base(generateServiceFileName(svc.m, *svc.svc))
681+ svcnames = append(svcnames, svcname)
682+ }
683+
684+ logs, err := actor.sysd.Logs(svcnames)
685+ if err != nil {
686+ return nil, fmt.Errorf(i18n.G("unable to get logs: %v"), err)
687+ }
688+
689+ return logs, nil
690+}
691+
692+// Loglines serializes the logs for all found services
693+func (actor *ServiceActor) Loglines() ([]string, error) {
694+ var lines []string
695+
696+ logs, err := actor.Logs()
697+ if err != nil {
698+ return nil, err
699+ }
700+
701+ for i := range logs {
702+ lines = append(lines, logs[i].String())
703+ }
704+
705+ return lines, nil
706+}
707
708=== added file 'snappy/service_test.go'
709--- snappy/service_test.go 1970-01-01 00:00:00 +0000
710+++ snappy/service_test.go 2015-09-14 14:13:47 +0000
711@@ -0,0 +1,179 @@
712+// -*- Mode: Go; indent-tabs-mode: t -*-
713+
714+/*
715+ * Copyright (C) 2014-2015 Canonical Ltd
716+ *
717+ * This program is free software: you can redistribute it and/or modify
718+ * it under the terms of the GNU General Public License version 3 as
719+ * published by the Free Software Foundation.
720+ *
721+ * This program is distributed in the hope that it will be useful,
722+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
723+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
724+ * GNU General Public License for more details.
725+ *
726+ * You should have received a copy of the GNU General Public License
727+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
728+ *
729+ */
730+
731+package snappy
732+
733+import (
734+ "errors"
735+ "os"
736+
737+ . "gopkg.in/check.v1"
738+
739+ "launchpad.net/snappy/progress"
740+ "launchpad.net/snappy/systemd"
741+)
742+
743+type ServiceActorSuite struct {
744+ i int
745+ argses [][]string
746+ outs [][]byte
747+ errors []error
748+ j int
749+ jsvcs [][]string
750+ jouts [][]byte
751+ jerrs []error
752+ pb progress.Meter
753+}
754+
755+var _ = Suite(&ServiceActorSuite{})
756+
757+// borrowed from systemd_test
758+func (s *ServiceActorSuite) myRun(args ...string) (out []byte, err error) {
759+ s.argses = append(s.argses, args)
760+ if s.i < len(s.outs) {
761+ out = s.outs[s.i]
762+ }
763+ if s.i < len(s.errors) {
764+ err = s.errors[s.i]
765+ }
766+ s.i++
767+ return out, err
768+}
769+func (s *ServiceActorSuite) myJctl(svcs []string) (out []byte, err error) {
770+ s.jsvcs = append(s.jsvcs, svcs)
771+
772+ if s.j < len(s.jouts) {
773+ out = s.jouts[s.j]
774+ }
775+ if s.j < len(s.jerrs) {
776+ err = s.jerrs[s.j]
777+ }
778+ s.j++
779+
780+ return out, err
781+}
782+
783+func (s *ServiceActorSuite) SetUpTest(c *C) {
784+ // force UTC timezone, for reproducible timestamps
785+ os.Setenv("TZ", "")
786+
787+ SetRootDir(c.MkDir())
788+ systemd.SystemctlCmd = s.myRun
789+ systemd.JournalctlCmd = s.myJctl
790+ makeInstalledMockSnap(globalRootDir, "")
791+ s.i = 0
792+ s.argses = nil
793+ s.errors = nil
794+ s.outs = nil
795+ s.j = 0
796+ s.jsvcs = nil
797+ s.jouts = nil
798+ s.jerrs = nil
799+ s.pb = &MockProgressMeter{}
800+}
801+
802+func (s *ServiceActorSuite) TestFindServicesNoPackages(c *C) {
803+ _, err := FindServices("notfound", "", s.pb)
804+ c.Check(err, Equals, ErrPackageNotFound)
805+}
806+
807+func (s *ServiceActorSuite) TestFindServicesNoPackagesNoPattern(c *C) {
808+ // tricky way of hiding the installed package ;)
809+ SetRootDir(c.MkDir())
810+ actor, err := FindServices("", "", s.pb)
811+ c.Check(err, IsNil)
812+ c.Assert(actor, NotNil)
813+ c.Check(actor.svcs, HasLen, 0)
814+}
815+
816+func (s *ServiceActorSuite) TestFindServicesNoServices(c *C) {
817+ _, err := FindServices("hello-app", "notfound", s.pb)
818+ c.Check(err, Equals, ErrServiceNotFound)
819+}
820+
821+func (s *ServiceActorSuite) TestFindServicesFindsServices(c *C) {
822+ actor, err := FindServices("", "", s.pb)
823+ c.Assert(err, IsNil)
824+ c.Assert(actor, NotNil)
825+ c.Check(actor.svcs, HasLen, 1)
826+
827+ s.outs = [][]byte{
828+ nil, // for the "stop"
829+ []byte("ActiveState=inactive\n"),
830+ nil, // for the "start"
831+ nil, // for the "stop"
832+ []byte("ActiveState=inactive\n"),
833+ nil, // for the "start"
834+ []byte("Id=x\nLoadState=loaded\nActiveState=active\nSubState=running\n"), // status
835+ }
836+ s.errors = []error{
837+ nil, nil, // stop & check
838+ nil, // start
839+ nil, nil, nil, // restart (== stop & start)
840+ nil, // status
841+ &systemd.Timeout{}, // flag
842+ }
843+ s.jerrs = nil
844+ s.jouts = [][]byte{
845+ []byte(`{"foo": "bar", "baz": 42}`), // for the Logs call
846+ []byte(`{"__REALTIME_TIMESTAMP":"42","MESSAGE":"hi"}`), // for the Loglines call
847+ }
848+
849+ c.Check(actor.Stop(), IsNil)
850+ c.Check(actor.Start(), IsNil)
851+ c.Check(actor.Restart(), IsNil)
852+ status, err := actor.Status()
853+ c.Check(err, IsNil)
854+ c.Assert(status, HasLen, 1)
855+ c.Check(status[0], Equals, "hello-app\tsvc1\tloaded; active (running)")
856+
857+ logs, err := actor.Logs()
858+ c.Check(err, IsNil)
859+ c.Check(logs, DeepEquals, []systemd.Log{{"foo": "bar", "baz": 42.}})
860+ lines, err := actor.Loglines()
861+ c.Check(err, IsNil)
862+ c.Check(lines, DeepEquals, []string{"1970-01-01T00:00:00.000042Z - hi"})
863+}
864+
865+func (s *ServiceActorSuite) TestFindServicesReportsErrors(c *C) {
866+ actor, err := FindServices("", "", s.pb)
867+ c.Assert(err, IsNil)
868+ c.Assert(actor, NotNil)
869+ c.Check(actor.svcs, HasLen, 1)
870+
871+ anError := errors.New("error")
872+
873+ s.errors = []error{
874+ anError, // stop
875+ anError, // start
876+ anError, // restart
877+ anError, // status
878+ }
879+ s.jerrs = []error{anError, anError}
880+
881+ c.Check(actor.Stop(), NotNil)
882+ c.Check(actor.Start(), NotNil)
883+ c.Check(actor.Restart(), NotNil)
884+ _, err = actor.Status()
885+ c.Check(err, NotNil)
886+ _, err = actor.Logs()
887+ c.Check(err, NotNil)
888+ _, err = actor.Loglines()
889+ c.Check(err, NotNil)
890+}
891
892=== modified file 'snappy/snapp.go'
893--- snappy/snapp.go 2015-09-11 19:56:16 +0000
894+++ snappy/snapp.go 2015-09-14 14:13:47 +0000
895@@ -72,7 +72,7 @@
896 }
897
898 // Port is used to declare the Port and Negotiable status of such port
899-// that is bound to a Service.
900+// that is bound to a ServiceYaml.
901 type Port struct {
902 Port string `yaml:"port,omitempty"`
903 Negotiable bool `yaml:"negotiable,omitempty"`
904@@ -135,8 +135,8 @@
905 return false
906 }
907
908-// Service represents a service inside a SnapPart
909-type Service struct {
910+// ServiceYaml represents a service inside a SnapPart
911+type ServiceYaml struct {
912 Name string `yaml:"name" json:"name,omitempty"`
913 Description string `yaml:"description,omitempty" json:"description,omitempty"`
914
915@@ -199,8 +199,8 @@
916 DeprecatedFramework string `yaml:"framework,omitempty"`
917 Frameworks []string `yaml:"frameworks,omitempty"`
918
919- Services []Service `yaml:"services,omitempty"`
920- Binaries []Binary `yaml:"binaries,omitempty"`
921+ ServiceYamls []ServiceYaml `yaml:"services,omitempty"`
922+ Binaries []Binary `yaml:"binaries,omitempty"`
923
924 // oem snap only
925 OEM OEM `yaml:"oem,omitempty"`
926@@ -305,7 +305,7 @@
927 return nil, err
928 }
929 }
930- for _, service := range m.Services {
931+ for _, service := range m.ServiceYamls {
932 if err := verifyServiceYaml(service); err != nil {
933 return nil, err
934 }
935@@ -322,9 +322,9 @@
936 }
937 }
938
939- for i := range m.Services {
940- if m.Services[i].StopTimeout == 0 {
941- m.Services[i].StopTimeout = DefaultTimeout
942+ for i := range m.ServiceYamls {
943+ if m.ServiceYamls[i].StopTimeout == 0 {
944+ m.ServiceYamls[i].StopTimeout = DefaultTimeout
945 }
946 }
947
948@@ -336,7 +336,7 @@
949 for _, bin := range m.Binaries {
950 d[bin.Name] = struct{}{}
951 }
952- for _, svc := range m.Services {
953+ for _, svc := range m.ServiceYamls {
954 if _, ok := d[svc.Name]; ok {
955 return ErrNameClash(svc.Name)
956 }
957@@ -605,12 +605,12 @@
958 return st.ModTime()
959 }
960
961-// Services return a list of Service the package declares
962-func (s *SnapPart) Services() []Service {
963- return s.m.Services
964+// ServiceYamls return a list of ServiceYamls the package declares
965+func (s *SnapPart) ServiceYamls() []ServiceYaml {
966+ return s.m.ServiceYamls
967 }
968
969-// Binaries return a list of Service the package declares
970+// Binaries return a list of BinaryDescription the package declares
971 func (s *SnapPart) Binaries() []Binary {
972 return s.m.Binaries
973 }
974@@ -740,7 +740,7 @@
975 func (s *SnapPart) RequestAppArmorUpdate(policies, templates map[string]bool) error {
976
977 fullName := Dirname(s)
978- for _, svc := range s.Services() {
979+ for _, svc := range s.ServiceYamls() {
980 if svc.NeedsAppArmorUpdate(policies, templates) {
981 if err := updateAppArmorJSONTimestamp(fullName, svc.Name, s.Version()); err != nil {
982 return err
983
984=== modified file 'snappy/snapp_test.go'
985--- snappy/snapp_test.go 2015-08-02 12:21:07 +0000
986+++ snappy/snapp_test.go 2015-09-14 14:13:47 +0000
987@@ -146,7 +146,7 @@
988 c.Assert(snap.Version(), Equals, "1.10")
989 c.Assert(snap.IsActive(), Equals, false)
990
991- services := snap.Services()
992+ services := snap.ServiceYamls()
993 c.Assert(services, HasLen, 1)
994 c.Assert(services[0].Name, Equals, "svc1")
995
996@@ -831,7 +831,7 @@
997 c.Assert(snap.Version(), Equals, "1.10")
998 c.Assert(snap.IsActive(), Equals, false)
999
1000- services := snap.Services()
1001+ services := snap.ServiceYamls()
1002 c.Assert(services, HasLen, 2)
1003
1004 c.Assert(services[0].Name, Equals, "svc1")
1005@@ -1025,13 +1025,13 @@
1006 m, err := parsePackageYamlData(securityServicePackageYaml)
1007 c.Assert(err, IsNil)
1008
1009- c.Assert(m.Services[0].Name, Equals, "testme-service")
1010- c.Assert(m.Services[0].Start, Equals, "bin/testme-service.start")
1011- c.Assert(m.Services[0].Stop, Equals, "bin/testme-service.stop")
1012- c.Assert(m.Services[0].SecurityCaps, HasLen, 2)
1013- c.Assert(m.Services[0].SecurityCaps[0], Equals, "networking")
1014- c.Assert(m.Services[0].SecurityCaps[1], Equals, "foo_group")
1015- c.Assert(m.Services[0].SecurityTemplate, Equals, "foo_template")
1016+ c.Assert(m.ServiceYamls[0].Name, Equals, "testme-service")
1017+ c.Assert(m.ServiceYamls[0].Start, Equals, "bin/testme-service.start")
1018+ c.Assert(m.ServiceYamls[0].Stop, Equals, "bin/testme-service.stop")
1019+ c.Assert(m.ServiceYamls[0].SecurityCaps, HasLen, 2)
1020+ c.Assert(m.ServiceYamls[0].SecurityCaps[0], Equals, "networking")
1021+ c.Assert(m.ServiceYamls[0].SecurityCaps[1], Equals, "foo_group")
1022+ c.Assert(m.ServiceYamls[0].SecurityTemplate, Equals, "foo_template")
1023 }
1024
1025 func (s *SnapTestSuite) TestPackageYamlFrameworkParsing(c *C) {
1026@@ -1287,8 +1287,8 @@
1027 }
1028 defer func() { timestampUpdater = helpers.UpdateTimestamp }()
1029 // if one of the services needs updating, it's updated and returned
1030- svc := Service{Name: "svc", SecurityDefinitions: SecurityDefinitions{SecurityTemplate: "foo"}}
1031- part := &SnapPart{m: &packageYaml{Name: "part", Services: []Service{svc}, Version: "42"}, namespace: testNamespace}
1032+ svc := ServiceYaml{Name: "svc", SecurityDefinitions: SecurityDefinitions{SecurityTemplate: "foo"}}
1033+ part := &SnapPart{m: &packageYaml{Name: "part", ServiceYamls: []ServiceYaml{svc}, Version: "42"}, namespace: testNamespace}
1034 err := part.RequestAppArmorUpdate(nil, map[string]bool{"foo": true})
1035 c.Assert(err, IsNil)
1036 c.Assert(updated, HasLen, 1)
1037@@ -1318,9 +1318,9 @@
1038 return nil
1039 }
1040 defer func() { timestampUpdater = helpers.UpdateTimestamp }()
1041- svc := Service{Name: "svc", SecurityDefinitions: SecurityDefinitions{SecurityTemplate: "foo"}}
1042+ svc := ServiceYaml{Name: "svc", SecurityDefinitions: SecurityDefinitions{SecurityTemplate: "foo"}}
1043 bin := Binary{Name: "echo", SecurityDefinitions: SecurityDefinitions{SecurityTemplate: "foo"}}
1044- part := &SnapPart{m: &packageYaml{Services: []Service{svc}, Binaries: []Binary{bin}, Version: "42"}, namespace: testNamespace}
1045+ part := &SnapPart{m: &packageYaml{ServiceYamls: []ServiceYaml{svc}, Binaries: []Binary{bin}, Version: "42"}, namespace: testNamespace}
1046 err := part.RequestAppArmorUpdate(nil, nil)
1047 c.Check(err, IsNil)
1048 c.Check(updated, HasLen, 0)
1049
1050=== modified file 'systemd/systemd.go'
1051--- systemd/systemd.go 2015-09-11 19:56:16 +0000
1052+++ systemd/systemd.go 2015-09-14 14:13:47 +0000
1053@@ -19,11 +19,14 @@
1054
1055 import (
1056 "bytes"
1057+ "encoding/json"
1058 "fmt"
1059+ "io"
1060 "os"
1061 "os/exec"
1062 "path/filepath"
1063 "regexp"
1064+ "strconv"
1065 "strings"
1066 "text/template"
1067 "time"
1068@@ -56,6 +59,26 @@
1069 // systemctl. It's exported so it can be overridden by testing.
1070 var SystemctlCmd = run
1071
1072+// jctl calls journalctl to get the JSON logs of the given services, wrapping the error if any.
1073+func jctl(svcs []string) ([]byte, error) {
1074+ cmd := []string{"journalctl", "-o", "json"}
1075+
1076+ for i := range svcs {
1077+ cmd = append(cmd, "-u", svcs[i])
1078+ }
1079+
1080+ bs, err := exec.Command(cmd[0], cmd[1:]...).Output() // journalctl can be messy with its stderr
1081+ if err != nil {
1082+ exitCode, _ := helpers.ExitCode(err)
1083+ return nil, &Error{cmd: cmd, exitCode: exitCode, msg: bs}
1084+ }
1085+
1086+ return bs, nil
1087+}
1088+
1089+// JournalctlCmd is called from Logs to run journalctl; exported for testing.
1090+var JournalctlCmd = jctl
1091+
1092 // Systemd exposes a minimal interface to manage systemd via the systemctl command.
1093 type Systemd interface {
1094 DaemonReload() error
1095@@ -67,8 +90,13 @@
1096 Restart(service string, timeout time.Duration) error
1097 GenServiceFile(desc *ServiceDescription) string
1098 GenSocketFile(desc *ServiceDescription) string
1099+ Status(service string) (string, error)
1100+ Logs(services []string) ([]Log, error)
1101 }
1102
1103+// A Log is a single entry in the systemd journal
1104+type Log map[string]interface{}
1105+
1106 // ServiceDescription describes a snappy systemd service
1107 type ServiceDescription struct {
1108 AppName string
1109@@ -151,6 +179,66 @@
1110 return err
1111 }
1112
1113+// Logs for the given service
1114+func (*systemd) Logs(serviceNames []string) ([]Log, error) {
1115+ bs, err := JournalctlCmd(serviceNames)
1116+ if err != nil {
1117+ return nil, err
1118+ }
1119+
1120+ const noEntries = "-- No entries --\n"
1121+ if len(bs) == len(noEntries) && string(bs) == noEntries {
1122+ return nil, nil
1123+ }
1124+
1125+ var logs []Log
1126+ dec := json.NewDecoder(bytes.NewReader(bs))
1127+ for {
1128+ var log Log
1129+
1130+ err = dec.Decode(&log)
1131+ if err != nil {
1132+ break
1133+ }
1134+
1135+ logs = append(logs, log)
1136+ }
1137+
1138+ if err != io.EOF {
1139+ return nil, err
1140+ }
1141+
1142+ return logs, nil
1143+}
1144+
1145+var statusregex = regexp.MustCompile(`(?m)^(?:(.*?)=(.*))?$`)
1146+
1147+func (s *systemd) Status(serviceName string) (string, error) {
1148+ bs, err := SystemctlCmd("show", "--property=Id,LoadState,ActiveState,SubState", serviceName)
1149+ if err != nil {
1150+ return "", err
1151+ }
1152+
1153+ load, active, sub := "", "", ""
1154+
1155+ for _, bs := range statusregex.FindAllSubmatch(bs, -1) {
1156+ if len(bs[0]) > 0 {
1157+ k := string(bs[1])
1158+ v := string(bs[2])
1159+ switch k {
1160+ case "LoadState":
1161+ load = v
1162+ case "ActiveState":
1163+ active = v
1164+ case "SubState":
1165+ sub = v
1166+ }
1167+ }
1168+ }
1169+
1170+ return fmt.Sprintf("%s; %s (%s)", load, active, sub), nil
1171+}
1172+
1173 // Stop the given service, and wait until it has stopped.
1174 func (s *systemd) Stop(serviceName string, timeout time.Duration) error {
1175 if _, err := SystemctlCmd("stop", serviceName); err != nil {
1176@@ -341,3 +429,33 @@
1177 _, isTimeout := err.(*Timeout)
1178 return isTimeout
1179 }
1180+
1181+const myFmt = "2006-01-02T15:04:05.000000Z07:00"
1182+
1183+func (l Log) String() string {
1184+ t := "-(no timestamp!)-"
1185+ if ius, ok := l["__REALTIME_TIMESTAMP"]; ok {
1186+ // according to systemd.journal-fields(7) it's microseconds as a decimal string
1187+ sus, ok := ius.(string)
1188+ if ok {
1189+ if us, err := strconv.ParseInt(sus, 10, 64); err == nil {
1190+ t = time.Unix(us/1000000, 1000*(us%1000000)).Format(myFmt)
1191+ } else {
1192+ t = fmt.Sprintf("-(timestamp not a decimal number: %#v)-", sus)
1193+ }
1194+ } else {
1195+ t = fmt.Sprintf("-(timestamp not a string: %#v)-", ius)
1196+ }
1197+ }
1198+
1199+ sid, ok := l["SYSLOG_IDENTIFIER"].(string)
1200+ if !ok {
1201+ sid = "-"
1202+ }
1203+ msg, ok := l["MESSAGE"].(string)
1204+ if !ok {
1205+ msg = "-"
1206+ }
1207+
1208+ return fmt.Sprintf("%s %s %s", t, sid, msg)
1209+}
1210
1211=== modified file 'systemd/systemd_test.go'
1212--- systemd/systemd_test.go 2015-09-01 17:46:30 +0000
1213+++ systemd/systemd_test.go 2015-09-14 14:13:47 +0000
1214@@ -46,22 +46,39 @@
1215 argses [][]string
1216 errors []error
1217 outs [][]byte
1218- rep *testreporter
1219+
1220+ j int
1221+ jsvcs [][]string
1222+ jouts [][]byte
1223+ jerrs []error
1224+
1225+ rep *testreporter
1226 }
1227
1228 var _ = Suite(&SystemdTestSuite{})
1229
1230 func (s *SystemdTestSuite) SetUpTest(c *C) {
1231+ // force UTC timezone, for reproducible timestamps
1232+ os.Setenv("TZ", "")
1233+
1234 SystemctlCmd = s.myRun
1235 s.i = 0
1236 s.argses = nil
1237 s.errors = nil
1238 s.outs = nil
1239+
1240+ JournalctlCmd = s.myJctl
1241+ s.j = 0
1242+ s.jsvcs = nil
1243+ s.jouts = nil
1244+ s.jerrs = nil
1245+
1246 s.rep = new(testreporter)
1247 }
1248
1249 func (s *SystemdTestSuite) TearDownTest(c *C) {
1250 SystemctlCmd = run
1251+ JournalctlCmd = jctl
1252 }
1253
1254 func (s *SystemdTestSuite) myRun(args ...string) (out []byte, err error) {
1255@@ -76,6 +93,20 @@
1256 return out, err
1257 }
1258
1259+func (s *SystemdTestSuite) myJctl(svcs []string) (out []byte, err error) {
1260+ s.jsvcs = append(s.jsvcs, svcs)
1261+
1262+ if s.j < len(s.jouts) {
1263+ out = s.jouts[s.j]
1264+ }
1265+ if s.j < len(s.jerrs) {
1266+ err = s.jerrs[s.j]
1267+ }
1268+ s.j++
1269+
1270+ return out, err
1271+}
1272+
1273 func (s *SystemdTestSuite) errorRun(args ...string) (out []byte, err error) {
1274 return nil, &Error{cmd: args, exitCode: 1, msg: []byte("error on error")}
1275 }
1276@@ -109,6 +140,16 @@
1277 c.Check(s.argses[1], DeepEquals, s.argses[3])
1278 }
1279
1280+func (s *SystemdTestSuite) TestStatus(c *C) {
1281+ s.outs = [][]byte{
1282+ []byte("Id=Thing\nLoadState=LoadState\nActiveState=ActiveState\nSubState=SubState\n"),
1283+ }
1284+ s.errors = []error{nil}
1285+ out, err := New("", s.rep).Status("foo")
1286+ c.Assert(err, IsNil)
1287+ c.Check(out, Equals, "LoadState; ActiveState (SubState)")
1288+}
1289+
1290 func (s *SystemdTestSuite) TestStopTimeout(c *C) {
1291 oldSteps := stopSteps
1292 oldDelay := stopDelay
1293@@ -300,3 +341,55 @@
1294 c.Check(IsTimeout(os.ErrInvalid), Equals, false)
1295 c.Check(IsTimeout(&Timeout{}), Equals, true)
1296 }
1297+
1298+func (s *SystemdTestSuite) TestLogErrJctl(c *C) {
1299+ s.jerrs = []error{&Timeout{}}
1300+
1301+ logs, err := New("", s.rep).Logs([]string{"foo"})
1302+ c.Check(err, NotNil)
1303+ c.Check(logs, IsNil)
1304+ c.Check(s.jsvcs, DeepEquals, [][]string{{"foo"}})
1305+ c.Check(s.j, Equals, 1)
1306+}
1307+
1308+func (s *SystemdTestSuite) TestLogErrJSON(c *C) {
1309+ s.jouts = [][]byte{[]byte("this is not valid json.")}
1310+
1311+ logs, err := New("", s.rep).Logs([]string{"foo"})
1312+ c.Check(err, NotNil)
1313+ c.Check(logs, IsNil)
1314+ c.Check(s.jsvcs, DeepEquals, [][]string{{"foo"}})
1315+ c.Check(s.j, Equals, 1)
1316+}
1317+
1318+func (s *SystemdTestSuite) TestLogs(c *C) {
1319+ s.jouts = [][]byte{[]byte(`{"a": 1}
1320+{"a": 2}
1321+`)}
1322+
1323+ logs, err := New("", s.rep).Logs([]string{"foo"})
1324+ c.Check(err, IsNil)
1325+ c.Check(logs, DeepEquals, []Log{{"a": 1.}, {"a": 2.}})
1326+ c.Check(s.jsvcs, DeepEquals, [][]string{{"foo"}})
1327+ c.Check(s.j, Equals, 1)
1328+}
1329+
1330+func (s *SystemdTestSuite) TestLogString(c *C) {
1331+ c.Check(Log{}.String(), Equals, "-(no timestamp!)- - -")
1332+ c.Check(Log{
1333+ "__REALTIME_TIMESTAMP": 42,
1334+ }.String(), Equals, "-(timestamp not a string: 42)- - -")
1335+ c.Check(Log{
1336+ "__REALTIME_TIMESTAMP": "what",
1337+ }.String(), Equals, "-(timestamp not a decimal number: \"what\")- - -")
1338+ c.Check(Log{
1339+ "__REALTIME_TIMESTAMP": "0",
1340+ "MESSAGE": "hi",
1341+ }.String(), Equals, "1970-01-01T00:00:00.000000Z - hi")
1342+ c.Check(Log{
1343+ "__REALTIME_TIMESTAMP": "42",
1344+ "MESSAGE": "hi",
1345+ "SYSLOG_IDENTIFIER": "me",
1346+ }.String(), Equals, "1970-01-01T00:00:00.000042Z me hi")
1347+
1348+}

Subscribers

People subscribed via source and target branches