github.com/coreos/mantle@v0.13.0/network/journal/record.go (about) 1 // Copyright 2017 CoreOS, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package journal 16 17 import ( 18 "context" 19 "io" 20 "os" 21 22 "github.com/kballard/go-shellquote" 23 "golang.org/x/crypto/ssh" 24 25 "github.com/coreos/mantle/system/exec" 26 ) 27 28 type Recorder struct { 29 formatter Formatter 30 cursor string 31 status chan error 32 rawFile io.WriteCloser 33 } 34 35 func NewRecorder(f Formatter, rawFile io.WriteCloser) *Recorder { 36 return &Recorder{ 37 formatter: f, 38 rawFile: rawFile, 39 status: make(chan error, 1), 40 } 41 } 42 43 func (r *Recorder) journalctl() []string { 44 cmd := []string{"journalctl", 45 "--output=export", "--follow", "--lines=all"} 46 if r.cursor == "" { 47 cmd = append(cmd, "--boot") 48 } else { 49 cmd = append(cmd, "--after-cursor", r.cursor) 50 } 51 return cmd 52 } 53 54 func (r *Recorder) record(export io.Reader) error { 55 exportTee := io.TeeReader(export, r.rawFile) 56 src := NewExportReader(exportTee) 57 for { 58 entry, err := src.ReadEntry() 59 if err != nil { 60 if err == io.EOF { 61 err = nil 62 } 63 return err 64 } 65 66 r.cursor = string(entry[FIELD_CURSOR]) 67 68 if err := r.formatter.WriteEntry(entry); err != nil { 69 return err 70 } 71 } 72 } 73 74 func (r *Recorder) StartSSH(ctx context.Context, client *ssh.Client) error { 75 ctx, cancel := context.WithCancel(ctx) 76 go func() { 77 <-ctx.Done() 78 client.Close() 79 }() 80 81 journal, err := client.NewSession() 82 if err != nil { 83 cancel() 84 return err 85 } 86 journal.Stderr = os.Stderr 87 88 export, err := journal.StdoutPipe() 89 if err != nil { 90 cancel() 91 return err 92 } 93 94 cmd := shellquote.Join(r.journalctl()...) 95 if err := journal.Start(cmd); err != nil { 96 cancel() 97 return err 98 } 99 100 go func() { 101 err := r.record(export) 102 cancel() 103 err2 := journal.Wait() 104 // Tolerate closed/canceled SSH connections. 105 if _, ok := err2.(*ssh.ExitMissingError); ok { 106 err2 = nil 107 } 108 if err == nil && err2 != nil { 109 err = err2 110 } 111 r.status <- err 112 }() 113 114 return nil 115 } 116 117 func (r *Recorder) StartLocal(ctx context.Context) error { 118 cmd := r.journalctl() 119 journal := exec.CommandContext(ctx, cmd[0], cmd[1:]...) 120 journal.Stderr = os.Stderr 121 122 export, err := journal.StdoutPipe() 123 if err != nil { 124 return err 125 } 126 127 if err := journal.Start(); err != nil { 128 return err 129 } 130 131 go func() { 132 err := r.record(export) 133 err2 := journal.Wait() 134 if err == nil && err2 != nil { 135 err = err2 136 } 137 r.status <- err 138 }() 139 140 return nil 141 } 142 143 func (r *Recorder) Wait() error { 144 return <-r.status 145 } 146 147 func (r *Recorder) RunSSH(ctx context.Context, client *ssh.Client) error { 148 if err := r.StartSSH(ctx, client); err != nil { 149 return err 150 } 151 return r.Wait() 152 } 153 154 func (r *Recorder) RunLocal(ctx context.Context) error { 155 if err := r.StartLocal(ctx); err != nil { 156 return err 157 } 158 return r.Wait() 159 }