// Package modules provides for parsing the modules on an ASA. package modules import ( "fmt" "regexp" "strings" "github.com/sirupsen/logrus" "github.rackspace.com/gorax/asaapi/pkg/logger" ) type ( // Lots of pointers here, strictly for use in outputting to JSON so an entry can be null CardType struct { Type string `json:"card_type"` Model string `json:"model"` SerialNo string `json:"serial_num"` } License struct { Name string `json:"name"` Status string `json:"status"` TimeRemaining string `json:"time_remaining"` } SSMPApplication struct { SSMAppName string `json:"ssm_app_name"` Status string `json:"status"` SSMAppVersion string `json:"ssm_app_version"` } MACRange struct { Lower string `json:"upper"` Upper string `json:"lower"` HWVersion string `json:"hw_version,omitempty"` FWVersion string `json:"fw_version,omitempty"` SWVersion string `json:"sw_version,omitempty"` } Status struct { Status string `json:"status"` DataPlaneStatus string `json:"data_plane_status"` Compatibility string `json:"compatibility"` } // Module details Module struct { CardType string `json:"card_type"` Compatibility string `json:"compatibility"` DataPlaneStatus string `json:"data_plane_status"` FWVersion string `json:"fw_version"` HWVersion string `json:"hw_version"` License *License `json:"license"` MacAddressRange *MACRange `json:"mac_address_range"` Model string `json:"model"` SerialNo string `json:"serial_num"` SSMApplication *SSMPApplication `json:"ssm_application,omitempty"` Status string `json:"status"` SWVersion string `json:"sw_version"` } DeviceModules struct { Modules map[string]Module `json:"modules"` } ) func parseMAC(line string, fields []int, sectionWidth int) (*string, *MACRange, error) { // fields is an array of ints indicating the length of each field in the table wantedFields := 5 if len(fields) != wantedFields { return nil, nil, fmt.Errorf("unexecpted number of fields for MAC address type. expected: %d, got: %d", wantedFields, len(fields)) } if len(line) < sectionWidth { pad(&line, sectionWidth) } r := buildRegex(fields) f := regexp.MustCompile(r).FindStringSubmatch(line) if len(f) != wantedFields+1 { return nil, nil, fmt.Errorf("error regex matching mac_address range to: '%s'", line) } innerR := regexp.MustCompile(`(\S+) to (\S+)`).FindStringSubmatch(f[2]) if len(innerR) != 3 { return nil, nil, fmt.Errorf("error regex matching innerR to %s", line) } modName := strings.TrimSpace(f[1]) l := strings.TrimSpace(innerR[1]) u := strings.TrimSpace(innerR[2]) h := strings.TrimSpace(f[3]) fw := strings.TrimSpace(f[4]) s := strings.TrimSpace(f[5]) m := new(MACRange) if l != "" { m.Lower = l } if u != "" { m.Upper = u } if h != "" { m.HWVersion = h } if fw != "" { m.FWVersion = fw } if s != "" { m.SWVersion = s } return &modName, m, nil } func buildRegex(fields []int) string { // build our regex string var r string for _, i := range fields { if len(r) == 0 { r = fmt.Sprintf("(.{%d})", i) } else { r = fmt.Sprintf("%s (.{%d})", r, i) } } return r } func parseAppName(line string, fields []int, sectionWidth int) (*string, *SSMPApplication, error) { // fields is an array of ints indicating the length of each field in the table wantedFields := 4 if len(fields) != wantedFields { return nil, nil, fmt.Errorf("unexecpted number of fields for license type. expected: %d, got: %d", wantedFields, len(fields)) } if len(line) < sectionWidth { pad(&line, sectionWidth) } // Mod SSM Application Name Status SSM Application Version // ---- ------------------------------ ---------------- -------------------------- r := buildRegex(fields) f := regexp.MustCompile(r).FindStringSubmatch(line) if len(f) != wantedFields+1 { return nil, nil, fmt.Errorf("error regex matching ssm application name against: %s", line) } modName := strings.TrimSpace(f[1]) an := strings.TrimSpace(f[2]) s := strings.TrimSpace(f[3]) av := strings.TrimSpace(f[4]) m := new(SSMPApplication) if an != "" { m.SSMAppName = an } if s != "" { m.Status = s } if av != "" { m.SSMAppVersion = av } return &modName, m, nil } func parseLicense(line string, fields []int, sectionWidth int) (*string, *License, error) { // fields is an array of ints indicating the length of each field in the table wantedFields := 4 if len(fields) != wantedFields { return nil, nil, fmt.Errorf("unexecpted number of fields for license type. expected: %d, got: %d", wantedFields, len(fields)) } if len(line) < sectionWidth { pad(&line, sectionWidth) } // Mod License Name License Status Time Remaining // ---- -------------- --------------- --------------- r := buildRegex(fields) f := regexp.MustCompile(r).FindStringSubmatch(line) if len(f) != wantedFields+1 { return nil, nil, fmt.Errorf("error regex matching license to: %s", line) } modName := strings.TrimSpace(f[1]) ln := strings.TrimSpace(f[2]) ls := strings.TrimSpace(f[3]) tr := strings.TrimSpace(f[4]) m := new(License) if ln != "" { m.Name = ln } if ls != "" { m.Status = ls } if tr != "" { m.TimeRemaining = tr } return &modName, m, nil } func parseCardType(line string, fields []int, sectionWidth int, connLog ...logrus.Ext1FieldLogger) (*string, *CardType, error) { log := selectLog(connLog...) wantedFields := 4 // fields is an array of ints indicating the length of each field in the table if len(fields) != wantedFields { return nil, nil, fmt.Errorf("unexecpted number of fields for card type. expected: %d, got: %d", wantedFields, len(fields)) } if len(line) < sectionWidth { pad(&line, sectionWidth) } // Mod Card Type Model Serial No. // ---- -------------------------------------------- ------------------ ----------- r := buildRegex(fields) f := regexp.MustCompile(r).FindStringSubmatch(line) if len(f) != wantedFields+1 { return nil, nil, fmt.Errorf("error regex matching card type against: %s", line) } modName := strings.TrimSpace(f[1]) cardType := strings.TrimSpace(f[2]) model := strings.TrimSpace(f[3]) sn := strings.TrimSpace(f[4]) log.WithFields(logrus.Fields{ "mod_name": modName, "card_type": cardType, "model": model, "serial_number": sn, }).Trace("Parsed Card Type line") m := new(CardType) if cardType != "" { m.Type = cardType } if model != "" { m.Model = model } if sn != "" { m.SerialNo = sn } return &modName, m, nil } func parseStatus(line string, fields []int, sectionWidth int) (*string, *Status, error) { wantedFields := 4 // fields is an array of ints indicating the length of each field in the table if len(fields) != wantedFields { return nil, nil, fmt.Errorf("unexecpted number of fields for status type. expected: %d, got: %d", wantedFields, len(fields)) } if len(line) < sectionWidth { pad(&line, sectionWidth) } // Mod Status Data Plane Status Compatibility // ---- ------------------ --------------------- ------------- r := buildRegex(fields) f := regexp.MustCompile(r).FindStringSubmatch(line) if len(f) != wantedFields+1 { // + 1 to account for [0] which is the complete match return nil, nil, fmt.Errorf("error regex matching status line against: %s", line) } modName := strings.TrimSpace(f[1]) s := strings.TrimSpace(f[2]) dps := strings.TrimSpace(f[3]) compat := strings.TrimSpace(f[4]) m := new(Status) if s != "" { m.Status = s } if dps != "" { m.DataPlaneStatus = dps } if compat != "" { m.Compatibility = compat } return &modName, m, nil } // Parse : Parses the raw output from a 'show module all' command to a map modules. The index is the name of the module. func Parse(raw string, connLog ...logrus.Ext1FieldLogger) (map[string]Module, error) { const ( mac = "MAC Address Range" ct = "Card Type" appName = "SSM Application Name" status = "Status" license = "License Name" ) log := selectLog(connLog...) dm := make(map[string]Module) split := strings.Split(raw, "\n") section := "none" first := false tarLen := 0 var fields []int for _, line := range split { if line == "" { // we're no longer in a section, skip log.WithField("section", section).Debug("Leaving section") section = "none" continue } if regexp.MustCompile(`(?i)fw-(\S+)-(\S+)#`).MatchString(line) { log.WithField("line", line).Debug("Think we have a firewall prompt from ssh, skipping") continue } switch section { case "none": log.Trace(line) // this is the first line of a section switch { case strings.Contains(line, mac): log.WithField("section", mac).Debug("Entering section") section = mac first = true case strings.Contains(line, ct): log.WithField("section", ct).Debug("Entering section") section = ct first = true case strings.Contains(line, appName): log.WithField("section", appName).Debug("Entering section") section = appName first = true case strings.Contains(line, status) && !strings.Contains(line, license) && !strings.Contains(line, appName): // Note: Multiple lines contain 'status', which is why we have to make sure it doesn't contain other matching words log.WithField("section", status).Debug("Entering section") section = status first = true case strings.Contains(line, license): log.WithField("section", license).Debug("Entering section") section = license first = true default: log.Debugf("Unknown Line: `%s`\n", line) continue } case ct: if first { first = false tarLen = len(line) split := strings.Split(line, " ") fields = []int{} // reset the fields var for _, i := range split { fields = append(fields, len(i)) } continue } modName, modCT, err := parseCardType(line, fields, tarLen) if err != nil { return nil, err } var m Module m, _ = dm[*modName] if m.CardType != "" { return nil, fmt.Errorf("card type entry already exists for module: %s", *modName) } m.CardType = modCT.Type m.Model = modCT.Model m.SerialNo = modCT.SerialNo dm[*modName] = m case appName: if first { first = false tarLen = len(line) split := strings.Split(line, " ") fields = []int{} // reset the fields var for _, i := range split { fields = append(fields, len(i)) } continue } modName, appMod, err := parseAppName(line, fields, tarLen) if err != nil { return nil, err } var m Module m, _ = dm[*modName] if m.SSMApplication != nil { return nil, fmt.Errorf("ssm app name already exists for module: %s", *modName) } m.SSMApplication = appMod dm[*modName] = m case status: if first { first = false tarLen = len(line) split := strings.Split(line, " ") fields = []int{} // reset the fields var for _, i := range split { fields = append(fields, len(i)) } continue } modName, modStat, err := parseStatus(line, fields, tarLen) if err != nil { return nil, err } var m Module m, _ = dm[*modName] m.Status = modStat.Status m.DataPlaneStatus = modStat.DataPlaneStatus m.Compatibility = modStat.Compatibility dm[*modName] = m case mac: if first { // skip the first entry of a section as it'll be ------'s first = false tarLen = len(line) split := strings.Split(line, " ") fields = []int{} // reset the fields var for _, i := range split { fields = append(fields, len(i)) } continue } modName, mac, err := parseMAC(line, fields, tarLen) if err != nil { return nil, err } var m Module m, _ = dm[*modName] if m.MacAddressRange != nil { return nil, fmt.Errorf("mac range already exists for module: %s", *modName) } m.SWVersion = mac.SWVersion m.HWVersion = mac.HWVersion m.FWVersion = mac.FWVersion mac.SWVersion = "" mac.HWVersion = "" mac.FWVersion = "" m.MacAddressRange = mac dm[*modName] = m case license: if first { first = false tarLen = len(line) split := strings.Split(line, " ") fields = []int{} // reset the fields var for _, i := range split { fields = append(fields, len(i)) } continue } modName, licMod, err := parseLicense(line, fields, tarLen) if err != nil { return nil, err } var m Module m, _ = dm[*modName] if m.License != nil { return nil, fmt.Errorf("license entry already exists for module: %s", *modName) } m.License = licMod dm[*modName] = m } } return dm, nil } // pad a string with spaces up to tLen func pad(line *string, tLen int, connLog ...logrus.Ext1FieldLogger) { log := selectLog(connLog...) oLen := len(*line) if oLen < tLen { for i := oLen; i < tLen; i++ { *line = *line + " " } } log.WithFields(logrus.Fields{ "original_length": oLen, "new_length": tLen, }).Trace("Padded line") } func selectLog(connLog ...logrus.Ext1FieldLogger) logrus.Ext1FieldLogger { if len(connLog) > 0 { return connLog[0] } return logger.Log }