• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hknutzen / Netspoc-Approve / 16674559403

01 Aug 2025 12:03PM UTC coverage: 98.531% (-0.07%) from 98.6%
16674559403

push

github

hknutzen
CHANGELOG file

5230 of 5308 relevant lines covered (98.53%)

1.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.88
/go/pkg/ios/device.go
1
package ios
2

3
import (
4
        "fmt"
5
        "os"
6
        "regexp"
7
        "strings"
8
        "time"
9

10
        "github.com/hknutzen/Netspoc-Approve/go/pkg/cisco"
11
        "github.com/hknutzen/Netspoc-Approve/go/pkg/codefiles"
12
        "github.com/hknutzen/Netspoc-Approve/go/pkg/console"
13
        "github.com/hknutzen/Netspoc-Approve/go/pkg/errlog"
14
        "github.com/hknutzen/Netspoc-Approve/go/pkg/program"
15
)
16

17
type State struct {
18
        cisco.State
19
        reloadActive bool
20
}
21

22
func Setup() *State {
1✔
23
        s := &State{}
1✔
24
        s.SetupParser(cmdInfo)
1✔
25
        s.Model = "IOS"
1✔
26
        return s
1✔
27
}
1✔
28

29
func (s *State) LoadDevice(
30
        spocFile string, cfg *program.Config, logLogin, logConfig *os.File) error {
1✔
31

1✔
32
        user, pass, err := cfg.GetUserPass(codefiles.GetHostname(spocFile))
1✔
33
        if err != nil {
2✔
34
                return err
1✔
35
        }
1✔
36
        s.Conn, err = console.GetSSHConn(spocFile, user, cfg, logLogin)
1✔
37
        if err != nil {
2✔
38
                return err
1✔
39
        }
1✔
40
        hostName := codefiles.GetHostname(spocFile)
1✔
41
        s.LoginEnable(pass, cfg)
1✔
42
        s.setTerminal()
1✔
43
        s.logVersion()
1✔
44
        s.checkDeviceName(hostName)
1✔
45

1✔
46
        s.Conn.SetLogFH(logConfig)
1✔
47
        errlog.Info("Requesting device config")
1✔
48
        out := s.Conn.GetCmdOutput("sh run")
1✔
49
        errlog.Info("Got device config")
1✔
50
        s.DeviceCfg, err = s.ParseConfig([]byte(out), "<device>")
1✔
51
        errlog.Info("Parsed device config")
1✔
52
        if err != nil {
2✔
53
                err = fmt.Errorf("While reading device: %v", err)
1✔
54
        }
1✔
55
        return err
1✔
56
}
57

58
func (s *State) setTerminal() {
1✔
59
        s.Conn.SendCmd("term len 0")
1✔
60
        s.Conn.SendCmd("term width 512")
1✔
61
}
1✔
62

63
func (s *State) logVersion() {
1✔
64
        s.Conn.GetCmdOutput("sh ver")
1✔
65
}
1✔
66

67
func (s *State) checkDeviceName(name string) {
1✔
68
        // Force new prompt by issuing empty command.
1✔
69
        // Output is: \r\n\s*NAME#\s?
1✔
70
        out := strings.TrimSpace(s.Conn.IssueCmd("", `#[ ]?`))
1✔
71
        out = strings.TrimSuffix(out, "#")
1✔
72
        if name != out {
1✔
73
                errlog.Abort("Wrong device name: %q, expected: %q", out, name)
×
74
        }
×
75
}
76

77
func (s *State) prepareDevice() {
1✔
78
        s.Conn.SendCmd("configure terminal")
1✔
79
        // Don't slow down the system by logging to console.
1✔
80
        s.Conn.SendCmd("no logging console")
1✔
81
        // Enable logging synchronous to get a fresh prompt after
1✔
82
        // a reload banner is shown.
1✔
83
        s.Conn.SendCmd("line vty 0 15")
1✔
84
        s.Conn.SendCmd("logging synchronous level all")
1✔
85
        // Needed for default route to work as expected.
1✔
86
        s.Conn.SendCmd("ip subnet-zero")
1✔
87
        s.Conn.SendCmd("ip classless")
1✔
88
        s.Conn.SendCmd("end")
1✔
89
}
1✔
90

91
func (s *State) ApplyCommands(logFh *os.File) error {
1✔
92
        s.Conn.SetLogFH(logFh)
1✔
93
        s.prepareDevice()
1✔
94
        func() {
2✔
95
                s.scheduleReload()
1✔
96
                defer s.cancelReload()
1✔
97
                s.Conn.SendCmd("configure terminal")
1✔
98
                defer s.Conn.SendCmd("end")
1✔
99
                for _, chg := range s.Changes {
2✔
100
                        s.cmd(chg)
1✔
101
                }
1✔
102
        }()
103
        s.writeMem()
1✔
104
        return nil
1✔
105
}
106

107
// Output of "write mem":
108
// 1.
109
// Building configuration...
110
// Compressed configuration from 22772 bytes to 7054 bytes[OK]
111
// 2.
112
// Building configuration...
113
// [OK]
114
// 3.
115
// Warning: Attempting to overwrite an NVRAM configuration previously written
116
// by a different version of the system image.
117
// Overwrite the previous NVRAM configuration?[confirm]
118
// Building configuration...
119
// Compressed configuration from 10194 bytes to 5372 bytes[OK]
120
// 4.
121
// startup-config file open failed (Device or resource busy)
122
// In this case we retry the command up to three times.
123

124
func (s *State) writeMem() {
1✔
125
        retries := 2
1✔
126
        for {
2✔
127
                out := s.Conn.IssueCmd("write memory", `#[ ]?|\[confirm\]`)
1✔
128
                if strings.Contains(out, "Overwrite the previous NVRAM configuration") {
2✔
129
                        out = s.Conn.GetCmdOutput("")
1✔
130
                }
1✔
131
                if strings.Contains(out, "[OK]") {
2✔
132
                        return
1✔
133
                }
1✔
134
                if strings.Contains(out, "startup-config file open failed") {
2✔
135
                        if retries > 0 {
2✔
136
                                retries--
1✔
137
                                time.Sleep(3 * time.Second)
1✔
138
                                continue
1✔
139
                        }
140
                        errlog.Abort("write mem: startup-config open failed - giving up")
1✔
141
                }
142
                errlog.Abort("write mem: unexpected result: %s", out)
1✔
143
        }
144
}
145

146
// Send 1 or 2 commands in one data packet to device.
147
// No output expected from commands.
148
func (s *State) cmd(cmd string) {
1✔
149
        c1, c2, _ := strings.Cut(cmd, "\n")
1✔
150
        s.Conn.Send(cmd)
1✔
151
        needReload := false
1✔
152
        check := func(ci string) {
2✔
153
                out := s.Conn.GetOutput()
1✔
154
                out, needReload = s.stripReloadBanner(out)
1✔
155
                out = s.Conn.StripEcho(ci, out)
1✔
156
                if out != "" {
2✔
157
                        if !isValidOutput(ci, out) {
2✔
158
                                errlog.Abort("Got unexpected output from '%s':\n%s", ci, out)
1✔
159
                        }
1✔
160
                }
161
        }
162
        check(c1)
1✔
163
        if c2 != "" {
2✔
164
                check(c2)
1✔
165
        }
1✔
166
        if needReload {
2✔
167
                s.extendReload()
1✔
168
        }
1✔
169
}
170

171
func isValidOutput(cmd, out string) bool {
1✔
172
        for _, line := range strings.Split(out, "\n") {
2✔
173
                if line == "" {
2✔
174
                        continue
1✔
175
                }
176
                if strings.HasPrefix(line, "INFO:") {
2✔
177
                        continue
1✔
178
                }
179
                if strings.HasPrefix(line, "WARNING:") {
2✔
180
                        errlog.Warning("Got unexpected output from '%s':\n%s", cmd, line)
1✔
181
                        continue
1✔
182
                }
183
                return false
1✔
184
        }
185
        return true
1✔
186
}
187

188
func (s *State) CloseConnection() {
1✔
189
        if c := s.Conn; c != nil {
2✔
190
                s.Conn.Close()
1✔
191
        }
1✔
192
}
193

194
const reloadMinutes = 2
195

196
func (s *State) scheduleReload() {
1✔
197
        s.sendReloadCmd(false)
1✔
198
}
1✔
199

200
func (s *State) extendReload() {
1✔
201
        s.sendReloadCmd(true)
1✔
202
}
1✔
203

204
func (s *State) sendReloadCmd(withDo bool) {
1✔
205
        cmd := fmt.Sprintf("reload in %d", reloadMinutes)
1✔
206
        if withDo {
2✔
207
                cmd = "do " + cmd
1✔
208
        }
1✔
209
        out := s.Conn.IssueCmd(cmd, `\[yes\/no\]:\ |\[confirm\]`)
1✔
210
        // System configuration has been modified. Save? [yes/no]:
1✔
211
        if strings.Contains(out, "[yes/no]") {
2✔
212
                // Leave our changes unsaved, to be sure that a reload
1✔
213
                // gets last good configuration.
1✔
214
                s.Conn.IssueCmd("n", `\[confirm\]`)
1✔
215
        }
1✔
216
        // Confirm the reload with empty command, wait for the standard prompt.
217
        s.reloadActive = true
1✔
218
        s.Conn.SendCmd("")
1✔
219
}
220

221
func (s *State) cancelReload() {
1✔
222
        // Don't wait for standard prompt, but for banner message, which is
1✔
223
        // sent asynchronously.
1✔
224
        s.Conn.IssueCmd("reload cancel", `--- SHUTDOWN ABORTED ---`)
1✔
225
        // Because of 'logging synchronous' we are sure to get another prompt.
1✔
226
        s.Conn.WaitShort(`[#] ?$`)
1✔
227
        // Synchronize expect buffers with empty command.
1✔
228
        s.Conn.SendCmd("")
1✔
229
        s.reloadActive = false
1✔
230
}
1✔
231

232
/*
233
Remove banner message from command output and
234
check if a renewal of running reload process is needed.
235

236
If a reload is scheduled or aborted, a banner message will be inserted into
237
the expected command output:
238
<three empty lines>
239
<BELL>
240
***
241
*** --- <message> ---
242
***
243
This message is schown some time before the actual reload takes place:
244
  - SHUTDOWN in 0:05:00
245
  - SHUTDOWN in 0:01:00
246
*/
247

248
// End of line has already been converted from \r\n to \n.
249
var bannerRe = regexp.MustCompile(`\n\n\n\x07[*]{3}\n[*]{3}([^\n]+)\n[*]{3}\n`)
250

251
func (s *State) stripReloadBanner(out string) (string, bool) {
1✔
252
        if s.reloadActive {
2✔
253
                // Find message inside banner.
1✔
254
                if l := bannerRe.FindStringSubmatchIndex(out); l != nil {
2✔
255
                        msg := out[l[2]:l[3]]
1✔
256
                        prefix := out[:l[0]]
1✔
257
                        postfix := out[l[1]:]
1✔
258
                        out = prefix + postfix
1✔
259
                        if strings.TrimSpace(prefix+postfix) == "" {
2✔
260
                                // Because of 'logging synchronous' we are sure to get another prompt
1✔
261
                                // if the banner is the only output before current prompt.
1✔
262
                                // Read next prompt.
1✔
263
                                errlog.Info("Found banner before output, expecting another prompt")
1✔
264
                                out = s.Conn.WaitShort(`[#] ?$`)
1✔
265
                                out = s.Conn.StripStdPrompt(out)
1✔
266
                        } else if prefix != "" && strings.TrimSpace(postfix) == "" {
3✔
267
                                // Try to read another prompt if banner is shown directly
1✔
268
                                // behind current output.
1✔
269
                                errlog.Info("Found banner after output, checking another prompt")
1✔
270
                                if s.Conn.TryPrompt() {
2✔
271
                                        errlog.Info("- Found prompt")
1✔
272
                                }
1✔
273
                        }
274
                        matched, _ := regexp.MatchString(`SHUTDOWN in 0?0:01:00`, msg)
1✔
275
                        return out, matched
1✔
276
                }
277
        }
278
        return out, false
1✔
279
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc