• 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

95.5
/go/pkg/panos/device.go
1
package panos
2

3
import (
4
        "encoding/xml"
5
        "errors"
6
        "fmt"
7
        "io"
8
        "net/http"
9
        "net/url"
10
        "os"
11
        "regexp"
12
        "strings"
13
        "time"
14

15
        "github.com/hknutzen/Netspoc-Approve/go/pkg/errlog"
16
        "github.com/hknutzen/Netspoc-Approve/go/pkg/httpdevice"
17
        "github.com/hknutzen/Netspoc-Approve/go/pkg/program"
18
)
19

20
type State struct {
21
        client       *http.Client
22
        devUser      string
23
        urlPrefix    string
24
        deviceCfg    *panConfig
25
        spocCfg      *panConfig
26
        changes      []change
27
        errUnmanaged []error
28
}
29

30
type change struct {
31
        Cmds []string
32
}
33

34
func (s *State) LoadDevice(
35
        path string, cfg *program.Config, logLogin, logConfig *os.File) error {
1✔
36

1✔
37
        devName := ""
1✔
38
        err := httpdevice.TryReachableHTTPLogin(path, cfg,
1✔
39
                func(name, ip, user, pass string) error {
2✔
40
                        client, addr := httpdevice.GetHTTPClient(cfg, ip)
1✔
41
                        s.client = client
1✔
42
                        s.devUser = user
1✔
43
                        key, err := s.getAPIKey(addr, user, pass, logLogin)
1✔
44
                        if err != nil {
2✔
45
                                return err
1✔
46
                        }
1✔
47
                        s.urlPrefix = fmt.Sprintf("%s/api/?key=%s&", addr, key)
1✔
48
                        if !s.checkHA(logLogin) {
2✔
49
                                return fmt.Errorf("not in active state: %s (%s)", ip, name)
1✔
50
                        }
1✔
51
                        devName = name
1✔
52
                        return nil
1✔
53
                })
54
        if err != nil {
2✔
55
                return err
1✔
56
        }
1✔
57

58
        // Use "get", not "show", to get candidate configuration.
59
        // Must not use active configuration, since candidate may have
60
        // been changed already by other user or by interrupted previous
61
        // run of this program.
62
        // Don't request full "config", but only "devices" part, since
63
        // config contains very large predefined application data.
64
        uri := "type=config&action=get&xpath=/config/devices"
1✔
65
        body, err := s.httpPrefixGetLog(uri, logConfig)
1✔
66
        if err != nil {
2✔
67
                return err
1✔
68
        }
1✔
69
        s.deviceCfg, err = parseResponseConfig(body)
1✔
70
        if err != nil {
2✔
71
                return fmt.Errorf("While reading device: %v", err)
1✔
72
        }
1✔
73
        return s.deviceCfg.checkDeviceName(devName)
1✔
74
}
75

76
func (s *State) getAPIKey(addr, user, pass string, logFH *os.File) (
77
        string, error) {
1✔
78

1✔
79
        base, err := url.Parse(addr)
1✔
80
        if err != nil {
1✔
81
                return "", err
×
82
        }
×
83
        base.Path += "api"
1✔
84
        params := url.Values{}
1✔
85
        params.Set("type", "keygen")
1✔
86
        params.Set("user", user)
1✔
87
        params.Set("password", pass)
1✔
88
        base.RawQuery = params.Encode()
1✔
89
        uri := base.String()
1✔
90
        passRE := regexp.MustCompile(`(password=).*?(&|$)`)
1✔
91
        loggedURI := passRE.ReplaceAllString(uri, "${1}xxx$2")
1✔
92
        errlog.DoLog(logFH, loggedURI)
1✔
93
        body, err := s.httpGet(uri)
1✔
94
        keyRE := regexp.MustCompile(`<key>.*</key>`)
1✔
95
        loggedBody := keyRE.ReplaceAllString(string(body), "<key>xxx</key>")
1✔
96
        errlog.DoLog(logFH, loggedBody)
1✔
97
        if err != nil {
2✔
98
                msg := err.Error()
1✔
99
                msg = passRE.ReplaceAllString(msg, "${1}xxx$2")
1✔
100
                return "", fmt.Errorf("API key %s", msg)
1✔
101
        }
1✔
102
        return parseAPIKey(body)
1✔
103
}
104

105
// Check if high-availability is enabled and device is in <state>
106
// - "active" for <mode>Active-Passive</mode>
107
// - "active-primary" for <mode>Active-Active</mode>
108
/*
109
// <response status="success">
110
//         <result>
111
//          <enabled>yes</enabled>
112
//          <group>
113
//           <mode>Active-Passive</mode>
114
//           <local-info>
115
//            <state>active</state>
116
//           </local-info>
117
//          </group>
118
//         </result>
119
// </response>
120
*/
121
// Result is true if HA not enabled or if enabled and active.
122
func (s *State) checkHA(logFH *os.File) bool {
1✔
123
        uri :=
1✔
124
                "type=op&cmd=<show><high-availability><state/></high-availability></show>"
1✔
125
        body, err := s.httpPrefixGetLog(uri, logFH)
1✔
126
        if err != nil {
2✔
127
                return false
1✔
128
        }
1✔
129
        _, data, err := parseResponse(body)
1✔
130
        if err != nil {
2✔
131
                return false
1✔
132
        }
1✔
133
        type haState struct {
1✔
134
                Enabled string `xml:"enabled"`
1✔
135
                Mode    string `xml:"group>mode"`
1✔
136
                State   string `xml:"group>local-info>state"`
1✔
137
        }
1✔
138
        ha := new(haState)
1✔
139
        err = xml.Unmarshal(data, ha)
1✔
140
        if err != nil {
2✔
141
                return false
1✔
142
        }
1✔
143
        if ha.Enabled != "yes" {
2✔
144
                return true
1✔
145
        }
1✔
146
        switch ha.Mode {
1✔
147
        case "Active-Passive":
1✔
148
                return ha.State == "active"
1✔
149
        case "Active-Active":
1✔
150
                return ha.State == "active-primary"
1✔
151
        }
152
        return false
1✔
153
}
154

155
func (s *State) GetChanges() error {
1✔
156
        p1 := s.deviceCfg
1✔
157
        p2 := s.spocCfg
1✔
158
        err := processVsysPairs(p1, p2, func(v1, v2 *panVsys) error {
2✔
159
                if v1 == nil {
2✔
160
                        return fmt.Errorf(
1✔
161
                                "Unknown name '%s' in VSYS of device configuration", v2.Name)
1✔
162
                }
1✔
163
                if v2 == nil {
2✔
164
                        return nil
1✔
165
                }
1✔
166
                s.checkUnmanaged(v1)
1✔
167
                dev := p1.Devices.Entries[0].Name
1✔
168
                devPath := "/config/devices/entry" + nameAttr(dev)
1✔
169
                xPath := devPath + "/vsys/entry" + nameAttr(v2.Name)
1✔
170
                l := diffConfig(v1, v2, xPath)
1✔
171
                if len(l) != 0 {
2✔
172
                        s.changes = append(s.changes, change{l})
1✔
173
                }
1✔
174
                return nil
1✔
175
        })
176
        return err
1✔
177
}
178

179
func (s *State) checkUnmanaged(v *panVsys) {
1✔
180
        name := strings.ToLower(v.DisplayName)
1✔
181
        if !strings.Contains(name, "netspoc") {
2✔
182
                s.errUnmanaged = append(s.errUnmanaged,
1✔
183
                        fmt.Errorf("Missing NetSPoC in name of %s", v.Name))
1✔
184
        }
1✔
185
}
186

187
func (s *State) GetErrUnmanaged() []error {
1✔
188
        return s.errUnmanaged
1✔
189
}
1✔
190

191
func (s *State) ApplyCommands(logFH *os.File) error {
1✔
192
        doCmd := func(cmd string) (string, []byte, error) {
2✔
193
                body, err := s.httpPrefixGetLog(cmd, logFH)
1✔
194
                if err != nil {
2✔
195
                        return "", nil, err
1✔
196
                }
1✔
197
                return parseResponse(body)
1✔
198
        }
199
        commit := func() error {
2✔
200
                cmd := "type=commit&action=partial&cmd=<commit><partial><admin><member>" +
1✔
201
                        s.devUser + "</member></admin></partial></commit>"
1✔
202
                msg, data, err := doCmd(cmd)
1✔
203
                if err != nil {
2✔
204
                        return err
1✔
205
                }
1✔
206
                if strings.Contains(msg, "There are no changes to commit") ||
1✔
207
                        strings.Contains(msg, "The result of this commit would be the same") {
2✔
208
                        return nil
1✔
209
                }
1✔
210
                if msg != "" {
2✔
211
                        return fmt.Errorf("Unexpected message: %s", msg)
1✔
212
                }
1✔
213
                type enqueued struct {
1✔
214
                        Job string `xml:"job"`
1✔
215
                }
1✔
216
                j := new(enqueued)
1✔
217
                err = xml.Unmarshal(data, j)
1✔
218
                if err != nil {
1✔
219
                        return err
×
220
                }
×
221
                id := j.Job
1✔
222
                simulated := os.Getenv("SIMULATE_ROUTER") != ""
1✔
223
                for {
2✔
224
                        if !simulated {
1✔
225
                                time.Sleep(10 * time.Second)
×
226
                        }
×
227
                        cmd := "type=op&cmd=<show><jobs><id>" + id + "</id></jobs></show>"
1✔
228
                        _, data, err := doCmd(cmd)
1✔
229
                        if err != nil {
2✔
230
                                return err
1✔
231
                        }
1✔
232
                        type status struct {
1✔
233
                                Result string `xml:"job>result"`
1✔
234
                        }
1✔
235
                        s := new(status)
1✔
236
                        err = xml.Unmarshal(data, s)
1✔
237
                        if err != nil {
1✔
238
                                return err
×
239
                        }
×
240
                        switch s.Result {
1✔
241
                        case "PEND":
×
242
                                continue
×
243
                        case "OK":
1✔
244
                                return nil
1✔
245
                        default:
1✔
246
                                return fmt.Errorf("Unexpected job result: %q", s.Result)
1✔
247
                        }
248
                }
249
        }
250
        for _, chg := range s.changes {
2✔
251
                for _, cmd := range chg.Cmds {
2✔
252
                        _, _, err := doCmd(cmd)
1✔
253
                        if err != nil {
2✔
254
                                return fmt.Errorf("Command failed with %v", err)
1✔
255
                        }
1✔
256
                }
257
        }
258
        if err := commit(); err != nil {
2✔
259
                return fmt.Errorf("Commit failed: %v", err)
1✔
260
        }
1✔
261
        return nil
1✔
262
}
263

264
var apiRE = regexp.MustCompile(`[?]key=.*?&`)
265

266
func (s *State) httpPrefixGetLog(uri string, logFH *os.File) ([]byte, error) {
1✔
267
        uri = s.urlPrefix + uri
1✔
268
        loggedURI := apiRE.ReplaceAllString(uri, "?key=xxx&")
1✔
269
        errlog.DoLog(logFH, loggedURI)
1✔
270
        body, err := s.httpGet(uri)
1✔
271
        errlog.DoLog(logFH, string(body))
1✔
272
        return body, err
1✔
273
}
1✔
274

275
func (s *State) httpGet(uri string) ([]byte, error) {
1✔
276
        resp, err := s.client.Get(uri)
1✔
277
        if err != nil {
2✔
278
                return nil, err
1✔
279
        }
1✔
280
        body, err := io.ReadAll(resp.Body)
1✔
281
        resp.Body.Close()
1✔
282
        if resp.StatusCode != http.StatusOK {
2✔
283
                msg := fmt.Sprintf("status code: %d", resp.StatusCode)
1✔
284
                if len(body) != 0 {
2✔
285
                        msg += "\n" + string(body)
1✔
286
                }
1✔
287
                return body, errors.New(msg)
1✔
288
        }
289
        return body, err
1✔
290
}
291

292
func nameAttr(n string) string {
1✔
293
        return "[@name='" + url.QueryEscape(n) + "']"
1✔
294
}
1✔
295

296
func textAttr(n string) string {
1✔
297
        return "[text()='" + url.QueryEscape(n) + "']"
1✔
298
}
1✔
299

300
func (s *State) HasChanges() bool {
1✔
301
        return len(s.changes) != 0
1✔
302
}
1✔
303

304
func (s *State) ShowChanges() string {
1✔
305
        var collect strings.Builder
1✔
306
        for _, chg := range s.changes {
2✔
307
                for _, c := range chg.Cmds {
2✔
308
                        c, _ = url.QueryUnescape(c)
1✔
309
                        fmt.Fprintln(&collect, c)
1✔
310
                }
1✔
311
        }
312
        return collect.String()
1✔
313
}
314

315
func (s *State) CloseConnection() {}
1✔
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