diff --git a/client.go b/client.go index 6a200d3..bb61a23 100644 --- a/client.go +++ b/client.go @@ -2,13 +2,13 @@ package pango import ( "bytes" + "context" "crypto/tls" "encoding/json" "encoding/xml" "fmt" "io" - "io/ioutil" - "log" + "log/slog" "mime" "mime/multipart" "net/http" @@ -19,2098 +19,1242 @@ import ( "time" "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/generic" "github.com/PaloAltoNetworks/pango/plugin" "github.com/PaloAltoNetworks/pango/util" "github.com/PaloAltoNetworks/pango/version" + "github.com/PaloAltoNetworks/pango/xmlapi" ) -// These bit flags control what is logged by client connections. Of the flags -// available for use, LogSend and LogReceive will log ALL communication between -// the connection object and the PAN-OS XML API. The API key being used for -// communication will be blanked out, but no other sensitive data will be. As -// such, those two flags should be considered for debugging only. To disable -// all logging, set the logging level as LogQuiet. -// -// As of right now, pango is not officially supported by Palo Alto Networks TAC, -// however using the API itself via cURL is. If you run into an issue and you believe -// it to be a PAN-OS problem, you can enable a cURL output logging style to have pango -// output an equivalent cURL command to use when interfacing with TAC. -// -// If you want to get the cURL command so that you can run it yourself, then set -// the LogCurlWithPersonalData flag, which will output your real API key, hostname, -// and any custom headers you have configured the client to send to PAN-OS. -// -// The bit-wise flags are as follows: -// -// * LogQuiet: disables all logging -// * LogAction: action being performed (Set / Edit / Delete functions) -// * LogQuery: queries being run (Get / Show functions) -// * LogOp: operation commands (Op functions) -// * LogUid: User-Id commands (Uid functions) -// * LogLog: log retrieval commands -// * LogExport: log export commands -// * LogXpath: the resultant xpath -// * LogSend: xml docuemnt being sent -// * LogReceive: xml responses being received -// * LogOsxCurl: output an OSX cURL command for the data being sent in -// * LogCurlWithPersonalData: If doing a curl style logging, then include -// personal data in the curl command instead of tokens. -const ( - LogQuiet = 1 << (iota + 1) - LogAction - LogQuery - LogOp - LogUid - LogLog - LogExport - LogImport - LogXpath - LogSend - LogReceive - LogOsxCurl - LogCurlWithPersonalData -) +type LoggingInfo struct { + SLogHandler slog.Handler `json:"-"` + LogCategories LogCategory `json:"-"` + LogSymbols []string `json:"log_symbols"` + LogLevel slog.Level `json:"log_level"` +} -// Client is a generic connector struct. It provides wrapper functions for -// invoking the various PAN-OS XPath API methods. After creating the client, -// invoke Initialize() to prepare it for use. -// -// Many of the functions attached to this struct will take a param named -// `extras`. Under normal circumstances this will just be nil, but if you have -// some extra values you need to send in with your request you can specify them -// here. -// -// Likewise, a lot of these functions will return a slice of bytes. Under normal -// circumstances, you don't need to do anything with this, but sometimes you do, -// so you can find the raw XML returned from PAN-OS there. +// Client is an XML API client connection. If provides wrapper functions +// for invoking the various PAN-OS API methods. After creating the client, +// invoke Setup() followed by Initialize() to prepare it for use. type Client struct { - // Connection properties. - Hostname string `json:"hostname"` - Username string `json:"username"` - Password string `json:"password"` - ApiKey string `json:"api_key"` - Protocol string `json:"protocol"` - Port uint `json:"port"` - Timeout int `json:"timeout"` - Target string `json:"target"` - Headers map[string]string `json:"headers"` + Hostname string `json:"hostname"` + Username string `json:"username"` + Password string `json:"password"` + ApiKey string `json:"api_key"` + Protocol string `json:"protocol"` + Port int `json:"port"` + Target string `json:"target"` + ApiKeyInRequest bool `json:"api_key_in_request"` + Headers map[string]string `json:"headers"` + Logging LoggingInfo `json:"logging"` // Set to true if you want to check environment variables // for auth and connection properties. - CheckEnvironment bool `json:"-"` + CheckEnvironment bool `json:"-"` + AuthFile string `json:"-"` - // HTTP transport options. Note that the VerifyCertificate setting is - // only used if you do not specify a HTTP transport yourself. - VerifyCertificate bool `json:"verify_certificate"` - Transport *http.Transport `json:"-"` + // HTTP transport options. Note that the SkipVerifyCertificate setting + // is only used if you do not specify a HTTP transport yourself. + SkipVerifyCertificate bool `json:"skip_verify_certificate"` + Transport *http.Transport `json:"-"` // Variables determined at runtime. - Version version.Number `json:"-"` - SystemInfo map[string]string `json:"-"` - Plugin []plugin.Info `json:"-"` - MultiConfigure *MultiConfigure `json:"-"` - - // Logging level. - Logging uint32 `json:"-"` - LoggingFromInitialize []string `json:"logging"` + Version version.Number `json:"-"` + SystemInfo map[string]string `json:"-"` + Plugin []plugin.Info `json:"-"` // Internal variables. - credsFile string con *http.Client api_url string - configTree *util.XmlNode + configTree *generic.Xml + logger *categoryLogger // Variables for testing, response bytes, headers, and response index. - rp []url.Values - rb [][]byte - rh []http.Header - ri int + testInput []*http.Request + testOutput []*http.Response + testIndex int authFileContent []byte } -// String is the string representation of a client connection. Both the -// password and API key are replaced with stars, if set, making it safe -// to print the client connection in log messages. -func (c *Client) String() string { - var passwd string - var api_key string - - if c.Password == "" { - passwd = "" - } else { - passwd = "********" - } - - if c.ApiKey == "" { - api_key = "" - } else { - api_key = "********" - } - - return fmt.Sprintf( - "{Hostname:%s Username:%s Password:%s ApiKey:%s Protocol:%s Port:%d Timeout:%d Logging:%d}", - c.Hostname, c.Username, passwd, api_key, c.Protocol, c.Port, c.Timeout, c.Logging) -} - -// Versioning returns the client version number. +// Versioning returns the version number of PAN-OS. func (c *Client) Versioning() version.Number { return c.Version } -// Plugins returns the plugin information. +// Plugins returns the list of plugins. func (c *Client) Plugins() []plugin.Info { return c.Plugin } -// Initialize does some initial setup of the Client connection, retrieves -// the API key if it was not already present, then performs "show system -// info" to get the PAN-OS version. The full results are saved into the -// client's SystemInfo map. -// -// If not specified, the following is assumed: -// * Protocol: https -// * Port: (unspecified) -// * Timeout: 10 -// * Logging: LogAction | LogUid -func (c *Client) Initialize() error { - if len(c.rb) == 0 { - var e error - - if e = c.initCon(); e != nil { - return e - } else if e = c.initApiKey(); e != nil { - return e - } else if e = c.initSystemInfo(); e != nil { - return e - } - } else { - c.Hostname = "localhost" - c.ApiKey = "password" - } - - return nil -} - -// InitializeUsing does Initialize(), but takes in a filename that contains -// fallback authentication credentials if they aren't specified. -// -// The order of preference for auth / connection settings is: -// -// * explicitly set -// * environment variable (set chkenv to true to enable this) -// * json file -func (c *Client) InitializeUsing(filename string, chkenv bool) error { - c.CheckEnvironment = chkenv - c.credsFile = filename - - return c.Initialize() +// GetTarget returns the Target param, used in certain API calls. +func (c *Client) GetTarget() string { + return c.Target } -// RetrieveApiKey retrieves the API key, which will require that both the -// username and password are defined. -// -// The currently set ApiKey is forgotten when invoking this function. -func (c *Client) RetrieveApiKey() error { - c.LogAction("%s: Retrieving API key", c.Hostname) +// IsPanorama returns true if this is Panorama. +func (c *Client) IsPanorama() (bool, error) { + if len(c.SystemInfo) == 0 { + return false, fmt.Errorf("SystemInfo is nil") + } - type key_gen_ans struct { - Key string `xml:"result>key"` + model, ok := c.SystemInfo["model"] + if !ok { + return false, fmt.Errorf("model not present in SystemInfo") } - c.ApiKey = "" - ans := key_gen_ans{} - data := url.Values{} - data.Add("user", c.Username) - data.Add("password", c.Password) - data.Add("type", "keygen") + return model == "Panorama" || strings.HasPrefix(model, "M-"), nil +} - _, _, err := c.Communicate(data, &ans) +// IsFirewall returns true if this PAN-OS seems to be a NGFW instance. +func (c *Client) IsFirewall() (bool, error) { + ans, err := c.IsPanorama() if err != nil { - return err + return ans, err } - c.ApiKey = ans.Key - - return nil + return !ans, nil } -// EntryListUsing retrieves an list of entries using the given function, either -// Get or Show. -func (c *Client) EntryListUsing(fn util.Retriever, path []string) ([]string, error) { +// Setup does validation and initialization in preparation to start executing API +// commands against PAN-OS. +func (c *Client) Setup() error { var err error - type Entry struct { - Name string `xml:"name,attr"` + + // Load up the JSON config file. + var json_client Client + if c.AuthFile != "" { + var b []byte + if len(c.testOutput) == 0 { + b, err = os.ReadFile(c.AuthFile) + } else { + b = c.authFileContent + } + + if err != nil { + return err + } + + if err = json.Unmarshal(b, &json_client); err != nil { + return err + } } - type resp_struct struct { - Entries []Entry `xml:"result>entry"` + // Hostname. + if c.Hostname == "" { + if val := os.Getenv("PANOS_HOSTNAME"); c.CheckEnvironment && val != "" { + c.Hostname = val + } else if json_client.Hostname != "" { + c.Hostname = json_client.Hostname + } } - if path == nil { - return nil, fmt.Errorf("xpath is empty") + // Username. + if c.Username == "" { + if val := os.Getenv("PANOS_USERNAME"); c.CheckEnvironment && val != "" { + c.Username = val + } else { + c.Username = json_client.Username + } } - path = append(path, "entry", "@name") - resp := resp_struct{} - _, err = fn(path, nil, &resp) - if err != nil { - e2, ok := err.(errors.Panos) - if ok && e2.ObjectNotFound() { - return nil, nil + // Password. + if c.Password == "" { + if val := os.Getenv("PANOS_PASSWORD"); c.CheckEnvironment && val != "" { + c.Password = val + } else { + c.Password = json_client.Password } - return nil, err } - ans := make([]string, len(resp.Entries)) - for i := range resp.Entries { - ans[i] = resp.Entries[i].Name + // API key. + if c.ApiKey == "" { + if val := os.Getenv("PANOS_API_KEY"); c.CheckEnvironment && val != "" { + c.ApiKey = val + } else { + c.ApiKey = json_client.ApiKey + } } - return ans, nil -} + // Protocol. + if c.Protocol == "" { + if val := os.Getenv("PANOS_PROTOCOL"); c.CheckEnvironment && val != "" { + c.Protocol = val + } else if json_client.Protocol != "" { + c.Protocol = json_client.Protocol + } else { + c.Protocol = "https" + } + } + if c.Protocol != "http" && c.Protocol != "https" { + return fmt.Errorf("Invalid protocol %q. Must be \"http\" or \"https\"", c.Protocol) + } -// MemberListUsing retrieves an list of members using the given function, either -// Get or Show. -func (c *Client) MemberListUsing(fn util.Retriever, path []string) ([]string, error) { - type resp_struct struct { - Members []string `xml:"result>member"` + // Port. + if c.Port == 0 { + if val := os.Getenv("PANOS_PORT"); c.CheckEnvironment && val != "" { + if cp, err := strconv.Atoi(val); err != nil { + return fmt.Errorf("Failed to parse the env port number: %s", err) + } else { + c.Port = cp + } + } else if json_client.Port != 0 { + c.Port = json_client.Port + } + } + if c.Port > 65535 { + return fmt.Errorf("Port %d is out of bounds", c.Port) } - if path == nil { - return nil, fmt.Errorf("xpath is empty") + // Target. + if c.Target == "" { + if val := os.Getenv("PANOS_TARGET"); c.CheckEnvironment && val != "" { + c.Target = val + } else { + c.Target = json_client.Target + } } - path = append(path, "member") - resp := resp_struct{} - _, err := fn(path, nil, &resp) - if err != nil { - e2, ok := err.(errors.Panos) - if ok && e2.ObjectNotFound() { - return nil, nil + // Headers. + if len(c.Headers) == 0 { + if val := os.Getenv("PANOS_HEADERS"); c.CheckEnvironment && val != "" { + if err := json.Unmarshal([]byte(val), &c.Headers); err != nil { + return err + } + } + if len(c.Headers) == 0 && len(json_client.Headers) > 0 { + c.Headers = make(map[string]string) + for k, v := range json_client.Headers { + c.Headers[k] = v + } } - return nil, err } - return resp.Members, nil -} + // Skip verify cert. + if !c.SkipVerifyCertificate { + if val := os.Getenv("PANOS_SKIP_VERIFY_CERTIFICATE"); c.CheckEnvironment && val != "" { + if vcb, err := strconv.ParseBool(val); err != nil { + return err + } else if vcb { + c.SkipVerifyCertificate = vcb + } + } + if !c.SkipVerifyCertificate && json_client.SkipVerifyCertificate { + c.SkipVerifyCertificate = true + } + } -// RequestPasswordHash requests a password hash of the given string. -func (c *Client) RequestPasswordHash(val string) (string, error) { - c.LogOp("(op) creating password hash") - type phash_req struct { - XMLName xml.Name `xml:"request"` - Val string `xml:"password-hash>password"` + err = c.setupLogging(c.Logging) + if err != nil { + return err } - type phash_ans struct { - Hash string `xml:"result>phash"` + // Setup the client. + if c.Transport == nil { + c.Transport = &http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: c.SkipVerifyCertificate, + }, + } + } + c.con = &http.Client{ + Transport: c.Transport, } - req := phash_req{Val: val} - ans := phash_ans{} + // Sanity check. + if c.Hostname == "" { + return fmt.Errorf("hostname must be specified") + } + if c.ApiKey == "" && (c.Username == "" && c.Password == "") { + return fmt.Errorf("either API key or both username and password must be specified") + } - if _, err := c.Op(req, "", nil, &ans); err != nil { - return "", err + // Configure the api url. + if c.Port == 0 { + c.api_url = fmt.Sprintf("%s://%s/api", c.Protocol, c.Hostname) + } else { + c.api_url = fmt.Sprintf("%s://%s:%d/api", c.Protocol, c.Hostname, c.Port) } - return ans.Hash, nil + return nil } -// ValidateConfig performs a commit config validation check. -// -// Setting sync to true means that this function will block until the job -// finishes. -// -// -// The sleep param is an optional sleep duration to wait between polling for -// job completion. This param is only used if sync is set to true. +// SetupLocalInspection configures the client for local inspection. // -// This function returns the job ID and if any errors were encountered. -func (c *Client) ValidateConfig(sync bool, sleep time.Duration) (uint, error) { +// The version number is taken from the schema if it's present. If it is not +// present, then the version number falls back to what's specified in panosVersion. +func (c *Client) SetupLocalInspection(schema, panosVersion string) error { var err error - c.LogOp("(op) validating config") - type op_req struct { - XMLName xml.Name `xml:"validate"` - Cmd string `xml:"full"` - } - job_ans := util.JobResponse{} - _, err = c.Op(op_req{}, "", nil, &job_ans) - if err != nil { - return 0, err + if err = c.LoadPanosConfig([]byte(schema)); err != nil { + return err } - id := job_ans.Id - if !sync { - return id, nil + if c.Version.Major == 0 && c.Version.Minor == 0 && c.Version.Patch == 0 { + if panosVersion == "" { + return fmt.Errorf("no version found in the schema; the version must be specified") + } + + c.Version, err = version.New(panosVersion) + if err != nil { + return err + } } - return id, c.WaitForJob(id, sleep, nil, nil) + return nil } -// RevertToRunningConfig discards any changes made and reverts to the last -// config committed. -func (c *Client) RevertToRunningConfig() error { - c.LogOp("(op) reverting to running config") - _, err := c.Op("running-config.xml", "", nil, nil) - return err +// Initialize retrieves the API key if needed then retrieves the system info. +func (c *Client) Initialize(ctx context.Context) error { + var err error + + if c.ApiKey == "" { + if err = c.RetrieveApiKey(ctx); err != nil { + return err + } + } + + if err = c.RetrieveSystemInfo(ctx); err != nil { + return err + } + + if err = c.RetrievePlugins(ctx); err != nil { + return err + } + + return nil } -// ConfigLocks returns any config locks that are currently in place. -// -// If vsys is an empty string, then the vsys will default to "shared". -func (c *Client) ConfigLocks(vsys string) ([]util.Lock, error) { +// RetrieveSystemInfo performs "show system info" and saves it SystemInfo. +func (c *Client) RetrieveSystemInfo(ctx context.Context) error { var err error - var cmd string - ans := configLocks{} - if vsys == "" { - vsys = "shared" + type req struct { + XMLName xml.Name `xml:"show"` + Cmd string `xml:"system>info"` } - if c.Version.Gte(version.Number{9, 1, 0, ""}) { - var tgt string - if vsys == "shared" { - tgt = "all" - } else { - tgt = vsys - } - cmd = fmt.Sprintf("%s", tgt) - } else { - cmd = "" + type system struct { + Tags []generic.Xml `xml:",any"` } - c.LogOp("(op) getting config locks for scope %q", vsys) - _, err = c.Op(cmd, vsys, nil, &ans) - if err != nil { - return nil, err + type resp struct { + System system `xml:"result>system"` } - return ans.Locks, nil -} -// LockConfig locks the config for the given scope with the given comment. -// -// If vsys is an empty string, the scope defaults to "shared". -func (c *Client) LockConfig(vsys, comment string) error { - if vsys == "" { - vsys = "shared" + cmd := &xmlapi.Op{ + Command: req{}, + Target: c.Target, } - c.LogOp("(op) locking config for scope %q", vsys) - var inner string - if comment == "" { - inner = "" - } else { - inner = fmt.Sprintf("%s", comment) + var ans resp + if _, _, err = c.Communicate(ctx, cmd, false, &ans); err != nil { + return err } - cmd := fmt.Sprintf("%s", inner) - _, err := c.Op(cmd, vsys, nil, nil) - return err + c.SystemInfo = make(map[string]string, len(ans.System.Tags)) + for _, t := range ans.System.Tags { + if t.TrimmedText == nil { + continue + } + c.SystemInfo[t.XMLName.Local] = *t.TrimmedText + if t.XMLName.Local == "sw-version" { + c.Version, err = version.New(*t.TrimmedText) + if err != nil { + return err + } + } + } + + return nil } -// UnlockConfig removes the config lock on the given scope. +// RetrievePanosConfig retrieves the running config, candidate config, +// or the specified saved config file. // -// If vsys is an empty string, the scope defaults to "shared". -func (c *Client) UnlockConfig(vsys string) error { - if vsys == "" { - vsys = "shared" +// If the name is candidate, then the candidate config is retrieved. If the +// name is running, then the running config is retrieved. Otherwise, then +// the name is assumed to be the name of the saved config. +func (c *Client) RetrievePanosConfig(ctx context.Context, name string) ([]byte, error) { + type req struct { + XMLName xml.Name `xml:"show"` + Running *string `xml:"config>running"` + Candidate *string `xml:"config>candidate"` + Saved *string `xml:"config>saved"` } - type cmd struct { - XMLName xml.Name `xml:"request"` - Cmd string `xml:"config-lock>remove"` + type rdata struct { + Data []byte `xml:",innerxml"` } - c.LogOp("(op) unlocking config for scope %q", vsys) - _, err := c.Op(cmd{}, vsys, nil, nil) - return err -} + type resp struct { + XMLName xml.Name `xml:"response"` + Result rdata `xml:"result"` + } -// CommitLocks returns any commit locks that are currently in place. -// -// If vsys is an empty string, then the vsys will default to "shared". -func (c *Client) CommitLocks(vsys string) ([]util.Lock, error) { - if vsys == "" { - vsys = "shared" + var s string + cs := req{} + switch name { + case "candidate": + cs.Candidate = &s + case "running": + cs.Running = &s + default: + cs.Saved = &name } - c.LogOp("(op) getting commit locks for scope %q", vsys) - ans := commitLocks{} - _, err := c.Op("", vsys, nil, &ans) - if err != nil { + cmd := &xmlapi.Op{ + Command: cs, + Target: c.Target, + } + + var ans resp + if _, _, err := c.Communicate(ctx, cmd, false, &ans); err != nil { return nil, err } - return ans.Locks, nil + + return ans.Result.Data, nil } -// LockCommits locks commits for the given scope with the given comment. +// RetrieveApiKey refreshes the API key. // -// If vsys is an empty string, the scope defaults to "shared". -func (c *Client) LockCommits(vsys, comment string) error { - if vsys == "" { - vsys = "shared" +// This function unsets the ApiKey value and thus requires that the Username and Password +// be specified. +func (c *Client) RetrieveApiKey(ctx context.Context) error { + type key_gen_ans struct { + Key string `xml:"result>key"` } - c.LogOp("(op) locking commits for scope %q", vsys) - var inner string - if comment == "" { - inner = "" - } else { - inner = fmt.Sprintf("%s", comment) + cmd := &xmlapi.KeyGen{ + Username: c.Username, + Password: c.Password, } - cmd := fmt.Sprintf("%s", inner) - _, err := c.Op(cmd, vsys, nil, nil) - return err + var ans key_gen_ans + _, _, err := c.Communicate(ctx, cmd, false, &ans) + if err != nil { + return err + } + + c.ApiKey = ans.Key + + return nil } -// UnlockCommits removes the commit lock on the given scope owned by the given -// admin, if this admin is someone other than the current acting admin. -// -// If vsys is an empty string, the scope defaults to "shared". -func (c *Client) UnlockCommits(vsys, admin string) error { - if vsys == "" { - vsys = "shared" +// RetrievePlugins refreshes the Plugins info from PAN-OS. +func (c *Client) RetrievePlugins(ctx context.Context) error { + cmd := &xmlapi.Op{ + Command: plugin.GetPlugins{}, } - type cmd struct { - XMLName xml.Name `xml:"request"` - Admin string `xml:"commit-lock>remove>admin,omitempty"` + var ans plugin.PackageListing + _, _, err := c.Communicate(ctx, cmd, false, &ans) + if err != nil { + return err } - c.LogOp("(op) unlocking commits for scope %q", vsys) - _, err := c.Op(cmd{Admin: admin}, vsys, nil, nil) - return err + c.Plugin = ans.Listing() + + return nil } -// WaitForJob polls the device, waiting for the specified job to finish. +// LoadPanosConfig stores the given XML document into this client, allowing +// the user to use various namespace functions to query the config. This +// is referred to as local inspection mode. // -// The sleep param is the length of time to wait between polling for job -// completion. +// The config given must be in the form of `...`. // -// The extras param should be either nil or a url.Values{} to be mixed in with -// the constructed request. -// -// If you want to unmarshal the response into a struct, then pass in a -// pointer to the struct for the "resp" param. If you just want to know if -// the job completed with a status other than "FAIL", you only need to check -// the returned error message. -// -// In the case that there are multiple errors returned from the job, the first -// error is returned as the error string, and no unmarshaling is attempted. -func (c *Client) WaitForJob(id uint, sleep time.Duration, extras, resp interface{}) error { - var err error - var prev uint - var data []byte - dp := false - all_ok := true - - c.LogOp("(op) waiting for job %d", id) - type op_req struct { - XMLName xml.Name `xml:"show"` - Id uint `xml:"jobs>id"` +// If the 'detail-version' attribute is present in the XML, then it's saved to this +// instance's Version attribute. +func (c *Client) LoadPanosConfig(config []byte) error { + var ans generic.Xml + if err := xml.Unmarshal(config, &ans); err != nil { + return err } - req := op_req{Id: id} - var ans util.BasicJob - for { - // We need to zero out the response each iteration because the slices - // of strings append to each other instead of zeroing out. - ans = util.BasicJob{} + if ans.XMLName.Local != "config" { + return fmt.Errorf("Expected \"config\" at the root, found %q", ans.XMLName.Local) + } - // Get current percent complete. - data, err = c.Op(req, "", extras, &ans) + if ans.DetailedVersion != nil { + vn, err := version.New(*ans.DetailedVersion) if err != nil { return err } - - // Output percent complete if it's new. - if ans.Progress != prev { - prev = ans.Progress - c.LogOp("(op) job %d: %d percent complete", id, prev) - } - - // Check for device commits. - all_done := true - for _, d := range ans.Devices { - c.LogOp("%q result: %s", d.Serial, d.Result) - if d.Result == "PEND" { - all_done = false - break - } else if d.Result != "OK" && all_ok { - all_ok = false - } - } - - // Check for end condition. - if ans.Progress == 100 { - if all_done { - break - } else if !dp { - c.LogOp("(op) Waiting for %d device commits ...", len(ans.Devices)) - dp = true - } - } - - if sleep > 0 { - time.Sleep(sleep) - } - } - - // Check the results for a failed commit. - if ans.Result == "FAIL" { - if len(ans.Details.Lines) > 0 { - return fmt.Errorf(ans.Details.String()) - } else { - return fmt.Errorf("Job %d has failed to complete successfully", id) - } - } else if !all_ok { - return fmt.Errorf("Commit failed on one or more devices") + c.Version = vn } - if resp == nil { - return nil + c.configTree = &generic.Xml{ + XMLName: xml.Name{ + Local: "a", + }, + Nodes: []generic.Xml{ans}, } - return xml.Unmarshal(data, resp) + return nil } -// LogAction writes a log message for SET/EDIT/DELETE operations if LogAction is set. -func (c *Client) LogAction(msg string, i ...interface{}) { - if c.Logging&LogAction == LogAction { - log.Printf(msg, i...) +// ReadFromConfig returns the XML at the given XPATH location. This is +// referred to as local inspection mode. +// +// If the XPATH is a listing, then set withPackaging to false. +func (c *Client) ReadFromConfig(ctx context.Context, path []string, withPackaging bool, ans any) ([]byte, error) { + if c.configTree == nil { + return nil, fmt.Errorf("no config loaded") } -} -// LogQuery writes a log message for GET/SHOW operations if LogQuery is set. -func (c *Client) LogQuery(msg string, i ...interface{}) { - if c.Logging&LogQuery == LogQuery { - log.Printf(msg, i...) + if len(path) == 0 { + return nil, fmt.Errorf("path is empty") } -} -// LogOp writes a log message for OP operations if LogOp is set. -func (c *Client) LogOp(msg string, i ...interface{}) { - if c.Logging&LogOp == LogOp { - log.Printf(msg, i...) - } -} + entryPrefix := "entry[@name='" + entrySuffix := "']" -// LogUid writes a log message for User-Id operations if LogUid is set. -func (c *Client) LogUid(msg string, i ...interface{}) { - if c.Logging&LogUid == LogUid { - log.Printf(msg, i...) - } -} - -// LogLog writes a log message for LOG operations if LogLog is set. -func (c *Client) LogLog(msg string, i ...interface{}) { - if c.Logging&LogLog == LogLog { - log.Printf(msg, i...) - } -} + config := c.configTree + for _, pp := range path { + var tag, name string + if strings.HasPrefix(pp, entryPrefix) && strings.HasSuffix(pp, entrySuffix) { + tag = "entry" + name = strings.TrimSuffix(strings.TrimPrefix(pp, entryPrefix), entrySuffix) + } else { + tag = pp + } -// LogExport writes a log message for EXPORT operations if LogExport is set. -func (c *Client) LogExport(msg string, i ...interface{}) { - if c.Logging&LogExport == LogExport { - log.Printf(msg, i...) - } -} + found := false + for _, node := range config.Nodes { + if node.XMLName.Local == tag && (node.Name == nil || *node.Name == name) { + found = true + config = &node + break + } + } -// LogImport writes a log message for IMPORT operations if LogImport is set. -func (c *Client) LogImport(msg string, i ...interface{}) { - if c.Logging&LogImport == LogImport { - log.Printf(msg, i...) + if !found { + config = nil + break + } } -} -// Communicate sends the given data to PAN-OS. -// -// The ans param should be a pointer to a struct to unmarshal the response -// into or nil. -// -// Any response received from the server is returned, along with any errors -// encountered. -// -// Even if an answer struct is given, we first check for known error formats. If -// a known error format is detected, unmarshalling into the answer struct is not -// performed. -// -// If the API key is set, but not present in the given data, then it is added in. -func (c *Client) Communicate(data url.Values, ans interface{}) ([]byte, http.Header, error) { - if c.ApiKey != "" && data.Get("key") == "" { - data.Set("key", c.ApiKey) + if config == nil { + return nil, errors.ObjectNotFound() } - c.logSend(data) - - body, hdrs, err := c.post(data) + b, err := xml.Marshal(config) if err != nil { - return body, hdrs, err + return b, err } - return body, hdrs, c.endCommunication(body, ans) -} - -// CommunicateFile does a file upload to PAN-OS. -// -// The content param is the content of the file you want to upload. -// -// The filename param is the basename of the file you want to specify in the -// multipart form upload. -// -// The fp param is the name of the param for the file upload. -// -// The ans param should be a pointer to a struct to unmarshal the response -// into or nil. -// -// Any response received from the server is returned, along with any errors -// encountered. -// -// Even if an answer struct is given, we first check for known error formats. If -// a known error format is detected, unmarshalling into the answer struct is not -// performed. -// -// If the API key is set, but not present in the given data, then it is added in. -func (c *Client) CommunicateFile(content, filename, fp string, data url.Values, ans interface{}) ([]byte, http.Header, error) { - var err error - - if c.ApiKey != "" && data.Get("key") == "" { - data.Set("key", c.ApiKey) + var newb []byte + if !withPackaging { + newb = append([]byte(nil), b...) + } else { + newb = make([]byte, 0, len(b)+7) + newb = append(newb, []byte("")...) + newb = append(newb, b...) + newb = append(newb, []byte("")...) } - c.logSend(data) - - buf := bytes.Buffer{} - w := multipart.NewWriter(&buf) - - for k := range data { - w.WriteField(k, data.Get(k)) + if ans == nil { + return newb, nil } - w2, err := w.CreateFormFile(fp, filename) - if err != nil { - return nil, nil, err - } + err = xml.Unmarshal(newb, ans) + return newb, err +} - if _, err = io.Copy(w2, strings.NewReader(content)); err != nil { - return nil, nil, err +func (c *Client) Clock(ctx context.Context) (time.Time, error) { + type creq struct { + XMLName xml.Name `xml:"show"` + Cmd string `xml:"clock"` } - w.Close() - - req, err := http.NewRequest("POST", c.api_url, &buf) - if err != nil { - return nil, nil, err - } - req.Header.Set("Content-Type", w.FormDataContentType()) - for k, v := range c.Headers { - req.Header.Set(k, v) + type cresp struct { + Result string `xml:"result"` } - res, err := c.con.Do(req) - if err != nil { - return nil, nil, err + cmd := &xmlapi.Op{ + Command: creq{}, + Target: c.Target, } + var ans cresp - defer res.Body.Close() - body, err := ioutil.ReadAll(res.Body) - if err != nil { - return body, res.Header, err + if _, _, err := c.Communicate(ctx, cmd, false, &ans); err != nil { + return time.Time{}, err } - return body, res.Header, c.endCommunication(body, ans) + return time.Parse(time.UnixDate+"\n", ans.Result) } -// Op runs an operational or "op" type command. -// -// The req param can be either a properly formatted XML string or a struct -// that can be marshalled into XML. -// -// The vsys param is the vsys the op command should be executed in, if any. -// -// The extras param should be either nil or a url.Values{} to be mixed in with -// the constructed request. +// MultiConfig does a "multi-config" type command. // -// The ans param should be a pointer to a struct to unmarshal the response -// into or nil. +// Param strict should be true if you want strict transactional support. // -// Any response received from the server is returned, along with any errors -// encountered. -func (c *Client) Op(req interface{}, vsys string, extras, ans interface{}) ([]byte, error) { - var err error - data := url.Values{} - data.Set("type", "op") - - if err = addToData("cmd", req, true, &data); err != nil { - return nil, err +// Note that the error returned from this function is only if there was an error +// unmarshaling the response into the the multi config response struct. If the +// multi config itself failed, then the reason can be found in its results. +func (c *Client) MultiConfig(ctx context.Context, mc *xmlapi.MultiConfig, strict bool, extras url.Values) ([]byte, *http.Response, *xmlapi.MultiConfigResponse, error) { + cmd := &xmlapi.Config{ + Action: "multi-config", + Element: mc, + StrictTransactional: strict, + Target: c.Target, } - if vsys != "" { - data.Set("vsys", vsys) + text, httpResp, err := c.Communicate(ctx, cmd, false, nil) + + // If the text is empty, then the err will have a real error we should report to the + // invoker. However, if the text is not empty, then ignore the error and parse the + // multi config results using the specialized struct custom designed to handle it. + if len(text) == 0 { + return text, httpResp, nil, err } - if c.Target != "" { - data.Set("target", c.Target) + mcResp, err := xmlapi.NewMultiConfigResponse(text) + if err != nil { + return text, httpResp, nil, err } - if err = mergeUrlValues(&data, extras); err != nil { - return nil, err + if !mcResp.Ok() { + return text, httpResp, mcResp, mcResp } - b, _, err := c.Communicate(data, ans) - return b, err + return text, httpResp, mcResp, nil } -// Log submits a "log" command. -// -// Use `WaitForLogs` to get the results of the log command. -// -// The extras param should be either nil or a url.Values{} to be mixed in with -// the constructed request. -// -// Any response received from the server is returned, along with any errors -// encountered. -func (c *Client) Log(logType, action, query, dir string, nlogs, skip int, extras, ans interface{}) ([]byte, error) { - data := url.Values{} - data.Set("type", "log") - - if logType != "" { - data.Set("log-type", logType) +// RequestPasswordHash requests a password hash of the given string. +func (c *Client) RequestPasswordHash(ctx context.Context, v string) (string, error) { + type phash_req struct { + XMLName xml.Name `xml:"request"` + Val string `xml:"password-hash>password"` } - if action != "" { - data.Set("action", action) + cmd := &xmlapi.Op{ + Command: phash_req{ + Val: v, + }, + Target: c.Target, } - if query != "" { - data.Set("query", query) + type phash_ans struct { + Hash string `xml:"result>phash"` } - if dir != "" { - data.Set("dir", dir) + var ans phash_ans + if _, _, err := c.Communicate(ctx, cmd, false, &ans); err != nil { + return "", err } - if nlogs != 0 { - data.Set("nlogs", strconv.Itoa(nlogs)) - } + return ans.Hash, nil +} - if skip != 0 { - data.Set("skip", strconv.Itoa(skip)) +// ValidateConfig performs a commit config validation check. +// +// Use WaitForJob and the uint returned from this function to get the +// results of the job. +func (c *Client) ValidateConfig(ctx context.Context, sleep time.Duration) (uint, error) { + type req struct { + XMLName xml.Name `xml:"validate"` + Cmd string `xml:"full"` } - if err := mergeUrlValues(&data, extras); err != nil { - return nil, err + cmd := &xmlapi.Op{ + Command: req{}, + Target: c.Target, } - b, _, err := c.Communicate(data, ans) - return b, err + id, _, _, err := c.StartJob(ctx, cmd) + return id, err } -// WaitForLogs performs repeated log retrieval operations until the log job is complete -// or the timeout is reached. +// WaitForLogs polls PAN-OS until the given log retrieval job is complete. // -// Specify a timeout of zero to wait indefinitely. -// -// The ans param should be a pointer to a struct to unmarshal the response -// into or nil. -// -// Any response received from the server is returned, along with any errors -// encountered. -func (c *Client) WaitForLogs(id uint, sleep, timeout time.Duration, ans interface{}) ([]byte, error) { +// The sleep param is the time to wait between polling. Note that a sleep +// time less than 2 seconds may cause PAN-OS to take longer to finish the +// job. +func (c *Client) WaitForLogs(ctx context.Context, id uint, sleep time.Duration, resp any) ([]byte, error) { + if id == 0 { + return nil, fmt.Errorf("job ID must be specified") + } + var err error var data []byte var prev string - start := time.Now() - end := start.Add(timeout) - extras := url.Values{} - extras.Set("job-id", fmt.Sprintf("%d", id)) - c.LogLog("(log) waiting for logs: %d", id) + cmd := &xmlapi.Log{ + Action: "get", + JobId: id, + } - var resp util.BasicJob + var ans util.BasicJob for { - resp = util.BasicJob{} + ans = util.BasicJob{} - data, err = c.Log("", "get", "", "", 0, 0, extras, &resp) + data, _, err = c.Communicate(ctx, cmd, false, &ans) if err != nil { return data, err } - if resp.Status != prev { - prev = resp.Status - c.LogLog("(log) job %d status: %s", id, prev) + if ans.Status != prev { + prev = ans.Status + // log %d id and %s prev } - if resp.Status == "FIN" { + if ans.Status == "FIN" { break } - if timeout > 0 && end.After(time.Now()) { - return data, fmt.Errorf("timeout") - } - if sleep > 0 { time.Sleep(sleep) } } - if resp.Result == "FAIL" { - if len(resp.Details.Lines) > 0 { - return data, fmt.Errorf(resp.Details.String()) + if ans.Result == "FAIL" { + if len(ans.Details.Lines) > 0 { + return data, fmt.Errorf(ans.Details.String()) } else { - return data, fmt.Errorf("Job %d has failed to complete successfully", id) + return data, fmt.Errorf("Job %d has failed", id) } } - if ans == nil { + if resp == nil { return data, nil } - err = xml.Unmarshal(data, ans) + err = xml.Unmarshal(data, resp) return data, err } -// Show runs a "show" type command. -// -// The path param should be either a string or a slice of strings. -// -// The extras param should be either nil or a url.Values{} to be mixed in with -// the constructed request. -// -// The ans param should be a pointer to a struct to unmarshal the response -// into or nil. -// -// Any response received from the server is returned, along with any errors -// encountered. -func (c *Client) Show(path, extras, ans interface{}) ([]byte, error) { - data := url.Values{} - xp := util.AsXpath(path) - c.logXpath(xp) - data.Set("xpath", xp) - - return c.typeConfig("show", data, nil, extras, ans) -} - -// Get runs a "get" type command. -// -// The path param should be either a string or a slice of strings. -// -// The extras param should be either nil or a url.Values{} to be mixed in with -// the constructed request. -// -// The ans param should be a pointer to a struct to unmarshal the response -// into or nil. -// -// Any response received from the server is returned, along with any errors -// encountered. -func (c *Client) Get(path, extras, ans interface{}) ([]byte, error) { - data := url.Values{} - xp := util.AsXpath(path) - c.logXpath(xp) - data.Set("xpath", xp) - - return c.typeConfig("get", data, nil, extras, ans) -} - -// Delete runs a "delete" type command, removing the supplied xpath and -// everything underneath it. -// -// The path param should be either a string or a slice of strings. -// -// The extras param should be either nil or a url.Values{} to be mixed in with -// the constructed request. -// -// The ans param should be a pointer to a struct to unmarshal the response -// into or nil. -// -// Any response received from the server is returned, along with any errors -// encountered. -func (c *Client) Delete(path, extras, ans interface{}) ([]byte, error) { - data := url.Values{} - xp := util.AsXpath(path) - c.logXpath(xp) - data.Set("xpath", xp) - - return c.typeConfig("delete", data, nil, extras, ans) -} - -// Set runs a "set" type command, creating the element at the given xpath. -// -// The path param should be either a string or a slice of strings. -// -// The element param can be either a string of properly formatted XML to send -// or a struct which can be marshaled into a string. -// -// The extras param should be either nil or a url.Values{} to be mixed in with -// the constructed request. -// -// The ans param should be a pointer to a struct to unmarshal the response -// into or nil. -// -// Any response received from the server is returned, along with any errors -// encountered. -func (c *Client) Set(path, element, extras, ans interface{}) ([]byte, error) { - data := url.Values{} - xp := util.AsXpath(path) - c.logXpath(xp) - data.Set("xpath", xp) - - return c.typeConfig("set", data, element, extras, ans) -} - -// Edit runs a "edit" type command, modifying what is at the given xpath -// with the supplied element. -// -// The path param should be either a string or a slice of strings. -// -// The element param can be either a string of properly formatted XML to send -// or a struct which can be marshaled into a string. -// -// The extras param should be either nil or a url.Values{} to be mixed in with -// the constructed request. -// -// The ans param should be a pointer to a struct to unmarshal the response -// into or nil. -// -// Any response received from the server is returned, along with any errors -// encountered. -func (c *Client) Edit(path, element, extras, ans interface{}) ([]byte, error) { - data := url.Values{} - xp := util.AsXpath(path) - c.logXpath(xp) - data.Set("xpath", xp) - - return c.typeConfig("edit", data, element, extras, ans) -} - -// Move does a "move" type command. -func (c *Client) Move(path interface{}, where, dst string, extras, ans interface{}) ([]byte, error) { - data := url.Values{} - xp := util.AsXpath(path) - c.logXpath(xp) - data.Set("xpath", xp) - - if where != "" { - data.Set("where", where) - } - - if dst != "" { - data.Set("dst", dst) - } - - return c.typeConfig("move", data, nil, extras, ans) -} - -// Rename does a "rename" type command. -func (c *Client) Rename(path interface{}, newname string, extras, ans interface{}) ([]byte, error) { - data := url.Values{} - xp := util.AsXpath(path) - c.logXpath(xp) - data.Set("xpath", xp) - data.Set("newname", newname) - - return c.typeConfig("rename", data, nil, extras, ans) -} - -// MultiConfig does a "multi-config" type command. +// WaitForJob polls PAN-OS until the given job finishes. // -// Param strict should be true if you want strict transactional support. -// -// Note that the error returned from this function is only if there was an error -// unmarshaling the response into the the multi config response struct. If the -// multi config itself failed, then the reason can be found in its results. -func (c *Client) MultiConfig(element MultiConfigure, strict bool, extras interface{}) ([]byte, MultiConfigureResponse, error) { - data := url.Values{} - if strict { - data.Set("strict-transactional", "yes") - } - - text, _ := c.typeConfig("multi-config", data, element, extras, nil) - - resp := MultiConfigureResponse{} - err := xml.Unmarshal(text, &resp) - return text, resp, err -} - -// Uid performs User-ID API calls. -func (c *Client) Uid(cmd interface{}, vsys string, extras, ans interface{}) ([]byte, error) { +// The sleep param is the time to wait between polling. Note that a sleep +// time less than 2 seconds may cause PAN-OS to take longer to finish the +// job. +func (c *Client) WaitForJob(ctx context.Context, id uint, sleep time.Duration, resp any) error { var err error - data := url.Values{} - data.Set("type", "user-id") - - if err = addToData("cmd", cmd, true, &data); err != nil { - return nil, err - } - - if vsys != "" { - data.Set("vsys", vsys) - } - - if c.Target != "" { - data.Set("target", c.Target) - } - - if err = mergeUrlValues(&data, extras); err != nil { - return nil, err - } - - b, _, err := c.Communicate(data, ans) - return b, err -} - -// Import performs an import type command. -// -// The cat param is the category. -// -// The content param is the content of the file you want to upload. -// -// The filename param is the basename of the file you want to specify in the -// multipart form upload. -// -// The fp param is the name of the param for the file upload. -// -// The extras param should be either nil or a url.Values{} to be mixed in with -// the constructed request. -// -// The ans param should be a pointer to a struct to unmarshal the response -// into or nil. -// -// Any response received from the server is returned, along with any errors -// encountered. -func (c *Client) Import(cat, content, filename, fp string, timeout time.Duration, extras, ans interface{}) ([]byte, error) { - if timeout < 0 { - return nil, fmt.Errorf("timeout cannot be negative") - } else if timeout > 0 { - defer func(c *Client, v time.Duration) { - c.con.Timeout = v - }(c, c.con.Timeout) - c.con.Timeout = timeout - } - - data := url.Values{} - data.Set("type", "import") - data.Set("category", cat) - - if err := mergeUrlValues(&data, extras); err != nil { - return nil, err - } - - b, _, err := c.CommunicateFile(content, filename, fp, data, ans) - return b, err -} - -// Commit performs PAN-OS commits. -// -// The cmd param can be a properly formatted XML string, a struct that can -// be marshalled into XML, or one of the commit types that can be found in the -// commit package. -// -// The action param is the commit action to be taken. If you are using one of the -// commit structs as the `cmd` param and the action param is an empty string, then -// the action is taken from the commit struct passed in. -// -// The extras param should be either nil or a url.Values{} to be mixed in with -// the constructed request. -// -// Commits result in a job being submitted to the backend. The job ID, assuming -// the commit action was successfully submitted, the response from the server, -// and if an error was encountered or not are all returned from this function. -func (c *Client) Commit(cmd interface{}, action string, extras interface{}) (uint, []byte, error) { - var err error - data := url.Values{} - data.Set("type", "commit") - - if err = addToData("cmd", cmd, true, &data); err != nil { - return 0, nil, err - } - - if action != "" { - data.Set("action", action) - } else if ca, ok := cmd.(util.Actioner); ok && ca.Action() != "" { - data.Set("action", ca.Action()) - } - - if c.Target != "" { - data.Set("target", c.Target) - } - - if err = mergeUrlValues(&data, extras); err != nil { - return 0, nil, err - } - - ans := util.JobResponse{} - b, _, err := c.Communicate(data, &ans) - return ans.Id, b, err -} - -// Export runs an "export" type command. -// -// The category param specifies the desired file type to export. -// -// The extras param should be either nil or a url.Values{} to be mixed in with -// the constructed request. -// -// The ans param should be a pointer to a struct to unmarshal the response -// into or nil. -// -// Any response received from the server is returned, along with any errors -// encountered. -// -// If the export invoked results in a file being downloaded from PAN-OS, then -// the string returned is the name of the remote file that is retrieved, -// otherwise it's just an empty string. -func (c *Client) Export(category string, timeout time.Duration, extras, ans interface{}) (string, []byte, error) { - if timeout < 0 { - return "", nil, fmt.Errorf("timeout cannot be negative") - } else if timeout > 0 { - defer func(c *Client, v time.Duration) { - c.con.Timeout = v - }(c, c.con.Timeout) - c.con.Timeout = timeout - } - - data := url.Values{} - data.Set("type", "export") - - if category != "" { - data.Set("category", category) - } + var prev uint + var data []byte + dp := false + all_ok := true - if err := mergeUrlValues(&data, extras); err != nil { - return "", nil, err + type req struct { + XMLName xml.Name `xml:"show"` + Id uint `xml:"jobs>id"` } - var filename string - b, hdrs, err := c.Communicate(data, ans) - if err == nil && hdrs != nil { - // Check and see if there's a filename in the content disposition. - mediatype, params, err := mime.ParseMediaType(hdrs.Get("Content-Disposition")) - if err == nil && mediatype == "attachment" { - filename = params["filename"] - } + cmd := &xmlapi.Op{ + Command: req{ + Id: id, + }, + Target: c.Target, } - return filename, b, err -} - -/*** Internal functions ***/ - -func (c *Client) initCon() error { - var tout time.Duration - - // Load up the JSON config file. - json_client := &Client{} - if c.credsFile != "" { - var ( - b []byte - err error - ) - if len(c.rb) == 0 { - b, err = ioutil.ReadFile(c.credsFile) - } else { - b, err = c.authFileContent, nil - } + var ans util.BasicJob + for { + ans = util.BasicJob{} + data, _, err = c.Communicate(ctx, cmd, false, &ans) if err != nil { return err } - if err = json.Unmarshal(b, &json_client); err != nil { - return err - } - } - - // Hostname. - if c.Hostname == "" { - if val := os.Getenv("PANOS_HOSTNAME"); c.CheckEnvironment && val != "" { - c.Hostname = val - } else { - c.Hostname = json_client.Hostname - } - } - - // Username. - if c.Username == "" { - if val := os.Getenv("PANOS_USERNAME"); c.CheckEnvironment && val != "" { - c.Username = val - } else { - c.Username = json_client.Username - } - } - - // Password. - if c.Password == "" { - if val := os.Getenv("PANOS_PASSWORD"); c.CheckEnvironment && val != "" { - c.Password = val - } else { - c.Password = json_client.Password - } - } - - // API key. - if c.ApiKey == "" { - if val := os.Getenv("PANOS_API_KEY"); c.CheckEnvironment && val != "" { - c.ApiKey = val - } else { - c.ApiKey = json_client.ApiKey - } - } - - // Protocol. - if c.Protocol == "" { - if val := os.Getenv("PANOS_PROTOCOL"); c.CheckEnvironment && val != "" { - c.Protocol = val - } else if json_client.Protocol != "" { - c.Protocol = json_client.Protocol - } else { - c.Protocol = "https" - } - } - if c.Protocol != "http" && c.Protocol != "https" { - return fmt.Errorf("Invalid protocol %q. Must be \"http\" or \"https\"", c.Protocol) - } - - // Port. - if c.Port == 0 { - if val := os.Getenv("PANOS_PORT"); c.CheckEnvironment && val != "" { - if cp, err := strconv.Atoi(val); err != nil { - return fmt.Errorf("Failed to parse the env port number: %s", err) - } else { - c.Port = uint(cp) - } - } else if json_client.Port != 0 { - c.Port = json_client.Port + if ans.Progress != prev { + prev = ans.Progress + // log the change. } - } - if c.Port > 65535 { - return fmt.Errorf("Port %d is out of bounds", c.Port) - } - // Timeout. - if c.Timeout == 0 { - if val := os.Getenv("PANOS_TIMEOUT"); c.CheckEnvironment && val != "" { - if ival, err := strconv.Atoi(val); err != nil { - return fmt.Errorf("Failed to parse timeout env var as int: %s", err) - } else { - c.Timeout = ival + // Check for device commits. + all_done := true + for _, d := range ans.Devices { + // log %q d.Serial %s d.Result + if d.Result == "PEND" { + all_done = false + break + } else if d.Result != "OK" && all_ok { + all_ok = false } - } else if json_client.Timeout != 0 { - c.Timeout = json_client.Timeout - } else { - c.Timeout = 10 } - } - if c.Timeout <= 0 { - return fmt.Errorf("Timeout for %q must be a positive int", c.Hostname) - } - tout = time.Duration(time.Duration(c.Timeout) * time.Second) - // Target. - if c.Target == "" { - if val := os.Getenv("PANOS_TARGET"); c.CheckEnvironment && val != "" { - c.Target = val - } else { - c.Target = json_client.Target - } - } - - // Headers. - if len(c.Headers) == 0 { - if val := os.Getenv("PANOS_HEADERS"); c.CheckEnvironment && val != "" { - if err := json.Unmarshal([]byte(val), &c.Headers); err != nil { - return err - } - } - if len(c.Headers) == 0 && len(json_client.Headers) > 0 { - c.Headers = make(map[string]string) - for k, v := range json_client.Headers { - c.Headers[k] = v + // Check if the job's done. + if ans.Progress == 100 { + if all_done { + break + } else if !dp { + // log waiting for %d devices len(ans.Devices) + dp = true } } - } - // Verify cert. - if !c.VerifyCertificate { - if val := os.Getenv("PANOS_VERIFY_CERTIFICATE"); c.CheckEnvironment && val != "" { - if vcb, err := strconv.ParseBool(val); err != nil { - return err - } else if vcb { - c.VerifyCertificate = vcb - } - } - if !c.VerifyCertificate && json_client.VerifyCertificate { - c.VerifyCertificate = json_client.VerifyCertificate + if sleep > 0 { + time.Sleep(sleep) } } - // Logging. - if c.Logging == 0 { - var ll []string - if val := os.Getenv("PANOS_LOGGING"); c.CheckEnvironment && val != "" { - ll = strings.Split(val, ",") - } else { - ll = json_client.LoggingFromInitialize - } - if len(ll) > 0 { - var lv uint32 - for _, x := range ll { - switch x { - case "quiet": - lv |= LogQuiet - case "action": - lv |= LogAction - case "query": - lv |= LogQuery - case "op": - lv |= LogOp - case "uid": - lv |= LogUid - case "xpath": - lv |= LogXpath - case "send": - lv |= LogSend - case "receive": - lv |= LogReceive - default: - return fmt.Errorf("Unknown logging requested: %s", x) - } - } - c.Logging = lv + if ans.Result == "FAIL" { + if len(ans.Details.Lines) > 0 { + return fmt.Errorf(ans.Details.String()) } else { - c.Logging = LogAction | LogUid - } - } - - // Setup the https client. - if c.Transport == nil { - c.Transport = &http.Transport{ - Proxy: http.ProxyFromEnvironment, - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: !c.VerifyCertificate, - }, - } - } - c.con = &http.Client{ - Transport: c.Transport, - Timeout: tout, - } - - // Sanity check. - if c.Hostname == "" { - return fmt.Errorf("No hostname specified") - } else if c.ApiKey == "" && (c.Username == "" && c.Password == "") { - return fmt.Errorf("No username/password or API key given") - } - - // Configure the api url - if c.Port == 0 { - c.api_url = fmt.Sprintf("%s://%s/api", c.Protocol, c.Hostname) - } else { - c.api_url = fmt.Sprintf("%s://%s:%d/api", c.Protocol, c.Hostname, c.Port) - } - - return nil -} - -func (c *Client) initApiKey() error { - if c.ApiKey != "" { - return nil - } - - return c.RetrieveApiKey() -} - -func (c *Client) initSystemInfo() error { - var err error - c.LogOp("(op) show system info") - - // Run "show system info" - type system_info_req struct { - XMLName xml.Name `xml:"show"` - Cmd string `xml:"system>info"` - } - - type tagVal struct { - XMLName xml.Name - Value string `xml:",chardata"` - } - - type sysTag struct { - XMLName xml.Name `xml:"system"` - Tag []tagVal `xml:",any"` - } - - type system_info_ans struct { - System sysTag `xml:"result>system"` - } - - req := system_info_req{} - ans := system_info_ans{} - - _, err = c.Op(req, "", nil, &ans) - if err != nil { - return err - } - - c.SystemInfo = make(map[string]string, len(ans.System.Tag)) - for i := range ans.System.Tag { - c.SystemInfo[ans.System.Tag[i].XMLName.Local] = ans.System.Tag[i].Value - if ans.System.Tag[i].XMLName.Local == "sw-version" { - c.Version, err = version.New(ans.System.Tag[i].Value) - if err != nil { - return fmt.Errorf("Error parsing version %s: %s", ans.System.Tag[i].Value, err) - } + return fmt.Errorf("Job %d has failed", id) } + } else if !all_ok { + return fmt.Errorf("Commit failed on one or more devices") } - return nil -} - -func (c *Client) initPlugins() { - c.LogOp("(op) getting plugin info") - - var req plugin.GetPlugins - var ans plugin.PackageListing - - if _, err := c.Op(req, "", nil, &ans); err != nil { - c.LogAction("WARNING: Failed to get plugin info: %s", err) - return + if resp == nil { + return nil } - c.Plugin = ans.Listing() + return xml.Unmarshal(data, resp) } -func (c *Client) typeConfig(action string, data url.Values, element, extras, ans interface{}) ([]byte, error) { - var err error - - if c.MultiConfigure != nil && (action == "set" || - action == "edit" || - action == "delete") { - r := MultiConfigureRequest{ - XMLName: xml.Name{Local: action}, - Xpath: data.Get("xpath"), - } - if element != nil { - r.Data = element - } - c.MultiConfigure.Reqs = append(c.MultiConfigure.Reqs, r) - return nil, nil +// RevertToRunningConfig discards any changes made and reverts to the last +// committed config. +func (c *Client) RevertToRunningConfig(ctx context.Context, vsys string) error { + type req struct { + XMLName xml.Name `xml:"load"` + Cmd string `xml:"config>from"` } - data.Set("type", "config") - data.Set("action", action) - - if element != nil { - if err = addToData("element", element, true, &data); err != nil { - return nil, err - } + cmd := &xmlapi.Op{ + Command: req{ + Cmd: "running-config.xml", + }, + Vsys: vsys, + Target: c.Target, } - if c.Target != "" { - data.Set("target", c.Target) - } + _, _, err := c.Communicate(ctx, cmd, false, nil) + return err +} - if err = mergeUrlValues(&data, extras); err != nil { - return nil, err +// StartJob sends the given command, which starts a job on PAN-OS. +// +// The uint returned is the job ID. +func (c *Client) StartJob(ctx context.Context, cmd util.PangoCommand) (uint, []byte, *http.Response, error) { + var ans util.JobResponse + b, resp, err := c.Communicate(ctx, cmd, false, &ans) + if err != nil { + return 0, b, resp, err } - b, _, err := c.Communicate(data, ans) - return b, err + return ans.Id, b, resp, nil } -func (c *Client) logXpath(p string) { - if c.Logging&LogXpath == LogXpath { - log.Printf("(xpath) %s", p) +// Communicate sends the given content to PAN-OS. +// +// The API key is sent either in the request body or as a header. +// +// The timeout for the operation is taken from the context. +func (c *Client) Communicate(ctx context.Context, cmd util.PangoCommand, strip bool, ans any) ([]byte, *http.Response, error) { + if cmd == nil { + return nil, nil, fmt.Errorf("cmd is nil") } -} -// VsysImport imports the given names into the specified template / vsys. -func (c *Client) VsysImport(loc, tmpl, ts, vsys string, names []string) error { - path := c.xpathImport(tmpl, ts, vsys) - if len(names) == 0 || vsys == "" { - return nil - } else if len(names) == 1 { - path = append(path, loc) + data, err := cmd.AsUrlValues() + if err != nil { + return nil, nil, err } - obj := util.BulkElement{XMLName: xml.Name{Local: loc}} - for i := range names { - obj.Data = append(obj.Data, vis{xml.Name{Local: "member"}, names[i]}) + if c.ApiKeyInRequest && c.ApiKey != "" && data.Get("key") == "" { + data.Set("key", c.ApiKey) } - _, err := c.Set(path, obj.Config(), nil, nil) - return err -} + c.logSend(data) -// VsysUnimport removes the given names from all (template, optional) vsys. -func (c *Client) VsysUnimport(loc, tmpl, ts string, names []string) error { - if len(names) == 0 { - return nil + req, err := http.NewRequestWithContext(ctx, "POST", c.api_url, strings.NewReader(data.Encode())) + if err != nil { + return nil, nil, err } - path := make([]string, 0, 14) - path = append(path, c.xpathImport(tmpl, ts, "")...) - path = append(path, loc, util.AsMemberXpath(names)) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - _, err := c.Delete(path, nil, nil) - if err != nil { - e2, ok := err.(errors.Panos) - if ok && e2.ObjectNotFound() { - return nil - } - } - return err + return c.sendRequest(ctx, req, strip, ans) } -// IsImported checks if the importable object is actually imported in the -// specified location. -func (c *Client) IsImported(loc, tmpl, ts, vsys, name string) (bool, error) { - path := make([]string, 0, 14) - path = append(path, c.xpathImport(tmpl, ts, vsys)...) - path = append(path, loc, util.AsMemberXpath([]string{name})) - - _, err := c.Get(path, nil, nil) - if err == nil { - if vsys != "" { - return true, nil - } else { - return false, nil - } +// ImportFile imports the given file into PAN-OS. +func (c *Client) ImportFile(ctx context.Context, cmd *xmlapi.Import, content, filename, fp string, strip bool, ans any) ([]byte, *http.Response, error) { + if cmd == nil { + return nil, nil, fmt.Errorf("cmd is nil") } - e2, ok := err.(errors.Panos) - if ok && e2.ObjectNotFound() { - if vsys != "" { - return false, nil - } else { - return true, nil - } + data, err := cmd.AsUrlValues() + if err != nil { + return nil, nil, err } - return false, err -} + if c.ApiKeyInRequest && c.ApiKey != "" && data.Get("key") == "" { + data.Set("key", c.ApiKey) + } -func (c *Client) xpathImport(tmpl, ts, vsys string) []string { - ans := make([]string, 0, 12) - if tmpl != "" || ts != "" { - ans = append(ans, util.TemplateXpathPrefix(tmpl, ts)...) - } - ans = append(ans, - "config", - "devices", - util.AsEntryXpath([]string{"localhost.localdomain"}), - "vsys", - util.AsEntryXpath([]string{vsys}), - "import", - "network", - ) - - return ans -} + c.logSend(data) -func (c *Client) post(data url.Values) ([]byte, http.Header, error) { - if len(c.rb) == 0 { - req, err := http.NewRequest("POST", c.api_url, strings.NewReader(data.Encode())) - if err != nil { - return nil, nil, err - } - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - for k, v := range c.Headers { - req.Header.Set(k, v) - } + buf := bytes.Buffer{} + w := multipart.NewWriter(&buf) - r, err := c.con.Do(req) + for k := range data { + err = w.WriteField(k, data.Get(k)) if err != nil { return nil, nil, err } - - defer r.Body.Close() - ans, err := ioutil.ReadAll(r.Body) - return ans, r.Header, err - } else { - if c.ri < len(c.rb) { - c.rp = append(c.rp, data) - } - body := c.rb[c.ri%len(c.rb)] - var hdr http.Header - if len(c.rh) > 0 { - hdr = c.rh[c.ri%len(c.rh)] - } - c.ri++ - return body, hdr, nil } -} - -func (c *Client) endCommunication(body []byte, ans interface{}) error { - var err error - if c.Logging&LogReceive == LogReceive { - log.Printf("Response = %s", body) + w2, err := w.CreateFormFile(fp, filename) + if err != nil { + return nil, nil, err } - // Check for errors first - if err = errors.Parse(body); err != nil { - return err + if _, err = io.Copy(w2, strings.NewReader(content)); err != nil { + return nil, nil, err } - // Return the body string if we weren't given something to unmarshal into - if ans == nil { - return nil - } + w.Close() - // Unmarshal using the struct passed in - err = xml.Unmarshal(body, ans) + req, err := http.NewRequestWithContext(ctx, "POST", c.api_url, &buf) if err != nil { - return fmt.Errorf("Error unmarshaling into provided interface: %s", err) - } - - return nil -} - -/* -PositionFirstEntity moves an element before another one using the Move API command. - -Param `mvt` is a util.Move* constant. - -Param `rel` is the relative entity that `mvt` is in relation to. - -Param `ent` is the entity that is to be positioned. - -Param `path` is the XPATH of `ent`. - -Param `elms` is the ordered list of entities that should include both -`rel` and `ent`. -be found. -*/ -func (c *Client) PositionFirstEntity(mvt int, rel, ent string, path, elms []string) error { - // Sanity checks. - if rel == ent { - return fmt.Errorf("Can't position %q in relation to itself", rel) - } else if mvt < util.MoveSkip && mvt > util.MoveBottom { - return fmt.Errorf("Invalid position int given: %d", mvt) - } else if (mvt == util.MoveBefore || mvt == util.MoveDirectlyBefore || mvt == util.MoveAfter || mvt == util.MoveDirectlyAfter) && rel == "" { - return fmt.Errorf("Specify 'ref' in order to perform relative group positioning") + return nil, nil, err } - var err error - fIdx := -1 - oIdx := -1 - - switch mvt { - case util.MoveSkip: - return nil - case util.MoveTop: - _, em := c.Move(path, "top", "", nil, nil) - if em != nil && em.Error() != "already at the top" { - err = em - } - case util.MoveBottom: - _, em := c.Move(path, "bottom", "", nil, nil) - if em != nil && em.Error() != "already at the bottom" { - err = em - } - default: - // Find the indexes of the first rule and the ref rule. - for i, v := range elms { - if v == ent { - fIdx = i - } else if v == rel { - oIdx = i - } - if fIdx != -1 && oIdx != -1 { - break - } - } - - // Sanity check: both rules should be present. - if fIdx == -1 { - return fmt.Errorf("Entity to be moved %q does not exist", ent) - } else if oIdx == -1 { - return fmt.Errorf("Reference entity %q does not exist", rel) - } - - // Move the first element, if needed. - if (mvt == util.MoveBefore && fIdx > oIdx) || (mvt == util.MoveDirectlyBefore && fIdx+1 != oIdx) { - _, err = c.Move(path, "before", rel, nil, nil) - } else if (mvt == util.MoveAfter && fIdx < oIdx) || (mvt == util.MoveDirectlyAfter && fIdx != oIdx+1) { - _, err = c.Move(path, "after", rel, nil, nil) - } - } + req.Header.Set("Content-Type", w.FormDataContentType()) - return err + return c.sendRequest(ctx, req, strip, ans) } -// Clock gets the time on the PAN-OS appliance. -func (c *Client) Clock() (time.Time, error) { - type t_req struct { - XMLName xml.Name `xml:"show"` - Cmd string `xml:"clock"` +// ExportFile retrieves a file from PAN-OS. +func (c *Client) ExportFile(ctx context.Context, cmd *xmlapi.Export, ans any) (string, []byte, *http.Response, error) { + if cmd == nil { + return "", nil, nil, fmt.Errorf("cmd is nil") } - type t_resp struct { - Result string `xml:"result"` + data, err := cmd.AsUrlValues() + if err != nil { + return "", nil, nil, err } - req := t_req{} - ans := t_resp{} - - c.LogOp("(op) getting system time") - if _, err := c.Op(req, "", nil, &ans); err != nil { - return time.Time{}, err + if c.ApiKeyInRequest && c.ApiKey != "" && data.Get("key") == "" { + data.Set("key", c.ApiKey) } - return time.Parse(time.UnixDate+"\n", ans.Result) -} - -// PrepareMultiConfigure will start a multi config command. -// -// Capacity is the initial capacity of the requests to be sent. -func (c *Client) PrepareMultiConfigure(capacity int) { - c.MultiConfigure = &MultiConfigure{ - Reqs: make([]MultiConfigureRequest, 0, capacity), + req, err := http.NewRequestWithContext(ctx, "POST", c.api_url, strings.NewReader(data.Encode())) + if err != nil { + return "", nil, nil, err } -} -// SendMultiConfigure will send the accumulated multi configure request. -// -// Param strict should be true if you want strict transactional support. -// -// Note that the error returned from this function is only if there was an error -// unmarshaling the response into the the multi config response struct. If the -// multi config itself failed, then the reason can be found in its results. -func (c *Client) SendMultiConfigure(strict bool) (MultiConfigureResponse, error) { - if c.MultiConfigure == nil { - return MultiConfigureResponse{}, nil + b, resp, err := c.sendRequest(ctx, req, false, ans) + if err != nil { + return "", b, resp, err } - mc := c.MultiConfigure - c.MultiConfigure = nil + var filename string + mediatype, params, err := mime.ParseMediaType(resp.Header.Get("Content-Disposition")) + if err == nil && mediatype == "attachment" { + filename = params["filename"] + } - _, ans, err := c.MultiConfig(*mc, strict, nil) - return ans, err + return filename, b, resp, nil } -// GetTechSupportFile returns the tech support .tgz file. -// -// This function returns the name of the tech support file, the file -// contents, and an error if one occurred. -// -// The timeout param is the new timeout (in seconds) to temporarily assign to -// client connections to allow for the successful download of the tech support -// file. If the timeout is zero, then pango.Client.Timeout is the timeout for -// tech support file retrieval. -func (c *Client) GetTechSupportFile(timeout time.Duration) (string, []byte, error) { - if timeout < 0 { - return "", nil, fmt.Errorf("timeout cannot be negative") +// GetTechSupportFile returns the tech support file .tgz file. +func (c *Client) GetTechSupportFile(ctx context.Context) (string, []byte, error) { + cmd := &xmlapi.Export{ + Category: "tech-support", } - var err error - var resp util.JobResponse - cmd := "tech-support" - - c.LogExport("(export) tech support file") - - // Request the tech support file creation. - _, _, err = c.Export(cmd, 0, nil, &resp) + id, _, _, err := c.StartJob(ctx, cmd) if err != nil { return "", nil, err } - if resp.Id == 0 { - return "", nil, fmt.Errorf("Job ID was not found") - } - extras := url.Values{} - extras.Set("action", "status") - extras.Set("job-id", fmt.Sprintf("%d", resp.Id)) + cmd.Action = "status" + cmd.JobId = id - // Poll the job until it's done. - var pr util.BasicJob + var resp util.BasicJob var prev uint for { - _, _, err = c.Export(cmd, 0, extras, &pr) - if err != nil { + resp = util.BasicJob{} + + if _, _, _, err = c.ExportFile(ctx, cmd, &resp); err != nil { return "", nil, err } // The progress is not an uint when the job completes, so don't print // the progress as 0 when the job is actually complete. - if pr.Progress != prev && pr.Progress != 0 { - prev = pr.Progress - c.LogExport("(export) tech support job %d: %d percent complete", resp.Id, prev) + if resp.Progress != prev && resp.Progress != 0 { + prev = resp.Progress + // log %d resp.Id at %d prev percentage complete. } - if pr.Status == "FIN" { + if resp.Status == "FIN" { break } time.Sleep(2 * time.Second) } - if pr.Result == "FAIL" { - return "", nil, fmt.Errorf(pr.Details.String()) + if resp.Result == "FAIL" { + return "", nil, fmt.Errorf(resp.Details.String()) } - extras.Set("action", "get") - return c.Export(cmd, timeout, extras, nil) + cmd.Action = "get" + + filename, b, _, err := c.ExportFile(ctx, cmd, nil) + return filename, b, err } -// RetrievePanosConfig retrieves either the running config, candidate config, -// or the specified saved config file, then does `LoadPanosConfig()` to save it. // -// After the config is loaded, config can be queried and retrieved using -// any `FromPanosConfig()` methods. +// Internal functions. // -// Param `value` can be the word "candidate" to load candidate config or -// `running` to load running config. If the value is neither of those, it -// is assumed to be the name of a saved config and that is loaded. -func (c *Client) RetrievePanosConfig(value string) error { - type getConfig struct { - XMLName xml.Name `xml:"show"` - Running *string `xml:"config>running"` - Candidate *string `xml:"config>candidate"` - Saved *string `xml:"config>saved"` + +func (c *Client) setupLogging(logging LoggingInfo) error { + var logger *slog.Logger + + if logging.SLogHandler == nil { + logger = slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: logging.LogLevel})) + logger.Info("No slog handler provided, creating default os.Stderr handler.", "LogLevel", logging.LogLevel.Level()) + } else { + logger = slog.New(logging.SLogHandler) + if logging.LogLevel != 0 { + logger.Warn("LogLevel is ignored when using custom SLog handler.") + } } - type data struct { - Data []byte `xml:",innerxml"` + // 1. logging.LogCategories has the highest priority + // 2. If logging.LogCategories is not set, we check logging.LogSymbols + // 3. If logging.LogSymbols is empty and c.CheckEnvironment is true we consult + // PANOS_LOGGING environment variable. + // 4. If no logging categories have been selected, default to basic library logging + // (i.e. "pango" category) + logMask := logging.LogCategories + var err error + if logMask == 0 { + logMask, err = LogCategoryFromStrings(logging.LogSymbols) + if err != nil { + return err + } + + if logMask == 0 { + if val := os.Getenv("PANOS_LOGGING"); c.CheckEnvironment && val != "" { + symbols := strings.Split(val, ",") + logMask, err = LogCategoryFromStrings(symbols) + if err != nil { + return err + } + } + } } - type resp struct { - XMLName xml.Name `xml:"response"` - Result data `xml:"result"` + // To disable logging completely, use custom SLog handler that discards all logs, + // e.g. https://github.com/golang/go/issues/62005 for the example of such handler. + if logMask == 0 { + logMask = LogCategoryPango } - s := "" - req := getConfig{} - switch value { - case "candidate": - req.Candidate = &s - case "running": - req.Running = &s - default: - req.Saved = &value + enabledLogging, _ := LogCategoryAsStrings(logMask) + logger.Info("Pango logging configured", "symbols", enabledLogging) + + c.logger = newCategoryLogger(logger, logMask) + + return nil +} + +func (c *Client) sendRequest(ctx context.Context, req *http.Request, strip bool, ans any) ([]byte, *http.Response, error) { + // Optional: set the API key in the header. + if !c.ApiKeyInRequest && c.ApiKey != "" { + req.Header.Set("X-PAN-KEY", c.ApiKey) } - ans := resp{} - if _, err := c.Op(req, "", nil, &ans); err != nil { - return err + // Configure all user given headers. + for k, v := range c.Headers { + req.Header.Set(k, v) } - return c.LoadPanosConfig(ans.Result.Data) -} + // Perform the operation. + var err error + var resp *http.Response + if len(c.testOutput) != 0 { + c.testInput = append(c.testInput, req) + resp = c.testOutput[c.testIndex%len(c.testOutput)] + c.testIndex++ + } else { + resp, err = c.con.Do(req) + } -// LoadPanosConfig stores the given XML document into the local client instance. -// -// The `config` can either be `...` or something that contians -// only the config document (such as `...`). -// -// After the config is loaded, config can be queried and retrieved using -// any `FromPanosConfig()` methods. -func (c *Client) LoadPanosConfig(config []byte) error { - log.Printf("load panos config") - if err := xml.Unmarshal(config, &c.configTree); err != nil { - return err + if err != nil { + return nil, nil, err } - if c.configTree.XMLName.Local == "config" { - // Add a place holder parent util.XmlNode. - c.configTree = &util.XmlNode{ - XMLName: xml.Name{ - Local: "a", - }, - Nodes: []util.XmlNode{ - *c.configTree, - }, + // Read in the response. + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return body, resp, err + } + + // Check the response for errors. + if err = errors.Parse(body); err != nil { + return body, resp, err + } + + // Optional: strip the "response" tag from the XML returned. + if strip { + var index int + gt := []byte(">") + lt := []byte("<") + + index = bytes.Index(body, gt) + if index > 0 { + body = body[index+1:] + index = bytes.LastIndex(body, lt) + if index > 0 { + body = body[:index] + } } - return nil } - if len(c.configTree.Nodes) == 1 && c.configTree.Nodes[0].XMLName.Local == "config" { - // Already has a place holder parent. - return nil + if ans == nil { + return body, resp, nil } - c.configTree = nil - return fmt.Errorf("doesn't seem to be a config tree") -} + // Optional: unmarshal using the struct passed in. + err = xml.Unmarshal(body, ans) + if err != nil { + return body, resp, fmt.Errorf("err unmarshaling into provided interface: %s", err) + } + + c.logger.WithLogCategory(LogCategoryReceive).Debug("Received data from server", "body", c.prepareReceiveDataForLogging(body)) -// ConfigTree returns the configuration tree that was loaded either via -// `RetrievePanosConfig()` or `LoadPanosConfig()`. -func (c *Client) ConfigTree() *util.XmlNode { - return c.configTree + return body, resp, nil } func (c *Client) logSend(data url.Values) { - var b strings.Builder - - // Traditional send logging. - if c.Logging&LogSend == LogSend { - if b.Len() > 0 { - fmt.Fprintf(&b, "\n") - } - realKey := data.Get("key") - if realKey != "" { - data.Set("key", "########") - } - fmt.Fprintf(&b, "Sending data: %#v", data) - if realKey != "" { - data.Set("key", realKey) - } + c.logger.WithLogCategory(LogCategoryPango).Debug("Hello World!") + sendData := slog.Group("data", c.prepareSendDataForLogging(data)...) + if c.logger.enabledFor(LogCategoryCurl) { + curlEquivalent := slog.Group("curl", c.prepareSendDataAsCurl(data)...) + c.logger.WithLogCategory(LogCategorySend).Debug("data sent to the server", sendData, curlEquivalent) + } else { + c.logger.WithLogCategory(LogCategorySend).Debug("data sent to the server", sendData) } +} - // Log the send data as an OSX curl command. - if c.Logging&LogOsxCurl == LogOsxCurl { - if b.Len() > 0 { - fmt.Fprintf(&b, "\n") - } - special := map[string]string{ - "key": "", - "element": "", - } - ev := url.Values{} - for k := range data { - var isSpecial bool - for sk := range special { - if sk == k { - isSpecial = true - special[k] = data.Get(k) - break - } - } - if !isSpecial { - ev[k] = make([]string, 0, len(data[k])) - for i := range data[k] { - ev[k] = append(ev[k], data[k][i]) - } - } - } +func (c *Client) prepareSendDataForLogging(data url.Values) []any { + filterSensitive := !c.logger.enabledFor(LogCategorySensitive) - // Build up the curl command. - fmt.Fprintf(&b, "curl") - // Verify cert. - if !c.VerifyCertificate { - fmt.Fprintf(&b, " -k") - } - // Headers. - if len(c.Headers) > 0 && c.Logging&LogCurlWithPersonalData == LogCurlWithPersonalData { - for k, v := range c.Headers { - if v != "" { - fmt.Fprintf(&b, " --header '%s: %s'", k, v) - } else { - fmt.Fprintf(&b, " --header '%s;'", k) + preparedValues := make([]any, 0) + + for key, values := range data { + for _, value := range values { + preparedValues = append(preparedValues, key) + if filterSensitive { + switch key { + case "key", "password": + preparedValues = append(preparedValues, "***") + default: + preparedValues = append(preparedValues, value) } - } - } - // Add URL encoded values. - if special["key"] != "" { - if c.Logging&LogCurlWithPersonalData == LogCurlWithPersonalData { - ev.Set("key", special["key"]) } else { - ev.Set("key", "APIKEY") + preparedValues = append(preparedValues, value) } } - // Add in the element, if present. - if special["element"] != "" { - fmt.Fprintf(&b, " --data-urlencode element@element.xml") - } - // URL. - fmt.Fprintf(&b, " '%s://", c.Protocol) - if c.Logging&LogCurlWithPersonalData == LogCurlWithPersonalData { - fmt.Fprintf(&b, "%s", c.Hostname) - } else { - fmt.Fprintf(&b, "HOST") - } - if c.Port != 0 { - fmt.Fprintf(&b, ":%d", c.Port) - } - fmt.Fprintf(&b, "/api") - if len(ev) > 0 { - fmt.Fprintf(&b, "?%s", ev.Encode()) - } - fmt.Fprintf(&b, "'") - // Data. - if special["element"] != "" { - fmt.Fprintf(&b, "\nelement.xml:\n%s", special["element"]) - } } - if b.Len() > 0 { - log.Printf("%s", b.String()) - } + return preparedValues } -/** Non-struct private functions **/ +func (c *Client) prepareSendDataAsCurl(data url.Values) []any { + filterSensitive := !c.logger.enabledFor(LogCategorySensitive) -func mergeUrlValues(data *url.Values, extras interface{}) error { - if extras == nil { - return nil + // A map of items and their values for items that require additional + // processing, and not be sent as is. + special := map[string]string{ + "element": "", } - ev, ok := extras.(url.Values) - if !ok { - return fmt.Errorf("extras needs to be of type url.Values or nil") - } + sensitive := []string{"user", "password", "key", "host"} - for key := range ev { - data.Set(key, ev.Get(key)) - } + values := url.Values{} + for key, value := range data { + isSpecial := false + for needle := range special { + if key == needle { + isSpecial = true + special[key] = value[0] + break + } + } - return nil -} + isSensitive := false + for _, needle := range sensitive { + if key == needle { + isSensitive = true + break + } + } -func addToData(key string, i interface{}, attemptMarshal bool, data *url.Values) error { - if i == nil { - return nil + // Only non-special values should be part of the + // encoded url, and sensitive values should only be + // visible when LogCategorySensitiveis set. + if !isSpecial { + if isSensitive && filterSensitive { + values[key] = []string{strings.ToUpper(key)} + } else { + values[key] = value + } + } } - val, err := asString(i, attemptMarshal) - if err != nil { - return err + curlCmd := []string{"curl"} + + if c.SkipVerifyCertificate { + curlCmd = append(curlCmd, "-k") } - data.Set(key, val) - return nil -} + if len(c.Headers) > 0 && !filterSensitive { + for k, v := range c.Headers { + curlCmd = append(curlCmd, "--header") + var header string + if v == "" { + header = fmt.Sprintf("'%s;'", k) + } else { + header = fmt.Sprintf("'%s: %s'", k, v) + } + curlCmd = append(curlCmd, header) + } + } -func asString(i interface{}, attemptMarshal bool) (string, error) { - if a, ok := i.(fmt.Stringer); ok { - return a.String(), nil + hostname := c.Hostname + if filterSensitive { + hostname = "HOST" + } + port := "" + if c.Port != 0 { + port = fmt.Sprintf(":%d", c.Port) } - if b, ok := i.(util.Elementer); ok { - i = b.Element() + encodedValues := "" + if len(values) > 0 { + encodedValues = fmt.Sprintf("?%s", values.Encode()) } - switch val := i.(type) { - case nil: - return "", fmt.Errorf("nil encountered") - case string: - return val, nil - default: - if !attemptMarshal { - return "", fmt.Errorf("value must be string or fmt.Stringer") - } + urlTemplate := "'%s://%s%s/api%s'" + apiUrl := fmt.Sprintf(urlTemplate, c.Protocol, hostname, port, encodedValues) - rb, err := xml.Marshal(val) - if err != nil { - return "", err - } - return string(rb), nil - } -} + curlCmd = append(curlCmd, apiUrl) -// vis is a vsys import struct. -type vis struct { - XMLName xml.Name - Text string `xml:",chardata"` -} + curlData := []any{"command", strings.Join(curlCmd, " ")} + + if special["element"] != "" { + curlData = append(curlData, "element.xml") + curlData = append(curlData, special["element"]) + } -type configLocks struct { - Locks []util.Lock `xml:"result>config-locks>entry"` + return curlData } -type commitLocks struct { - Locks []util.Lock `xml:"result>commit-locks>entry"` +func (c *Client) prepareReceiveDataForLogging(body []byte) string { + replacedBody := string(body) + keyStartIdx := strings.Index(replacedBody, "") + keyEndIdx := strings.Index(replacedBody, "") + if keyStartIdx != -1 && keyEndIdx != -1 { + replacedBody = replacedBody[:keyStartIdx] + "***" + replacedBody[keyEndIdx+len(""):] + } + return replacedBody } diff --git a/device/services/dns/config.go b/device/services/dns/config.go new file mode 100644 index 0000000..4e04f76 --- /dev/null +++ b/device/services/dns/config.go @@ -0,0 +1,158 @@ +package dns + +import ( + "encoding/xml" + + "github.com/PaloAltoNetworks/pango/generic" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +type Config struct { + DnsSetting *DnsSetting + FqdnRefreshTime *int64 + + Misc map[string][]generic.Xml +} +type DnsSetting struct { + Servers *DnsSettingServers +} +type DnsSettingServers struct { + Primary *string + Secondary *string +} +type configXmlContainer struct { + XMLName xml.Name `xml:"result"` + Answer []configXml `xml:"system"` +} +type configXml struct { + XMLName xml.Name `xml:"system"` + DnsSetting *DnsSettingXml `xml:"dns-setting,omitempty"` + FqdnRefreshTime *int64 `xml:"fqdn-refresh-time,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type DnsSettingXml struct { + Servers *DnsSettingServersXml `xml:"servers,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type DnsSettingServersXml struct { + Primary *string `xml:"primary,omitempty"` + Secondary *string `xml:"secondary,omitempty"` + + Misc []generic.Xml `xml:",any"` +} + +func Versioning(vn version.Number) (Specifier, Normalizer, error) { + return specifyConfig, &configXmlContainer{}, nil +} +func specifyConfig(o *Config) (any, error) { + config := configXml{} + var nestedDnsSetting *DnsSettingXml + if o.DnsSetting != nil { + nestedDnsSetting = &DnsSettingXml{} + if _, ok := o.Misc["DnsSetting"]; ok { + nestedDnsSetting.Misc = o.Misc["DnsSetting"] + } + if o.DnsSetting.Servers != nil { + nestedDnsSetting.Servers = &DnsSettingServersXml{} + if _, ok := o.Misc["DnsSettingServers"]; ok { + nestedDnsSetting.Servers.Misc = o.Misc["DnsSettingServers"] + } + if o.DnsSetting.Servers.Primary != nil { + nestedDnsSetting.Servers.Primary = o.DnsSetting.Servers.Primary + } + if o.DnsSetting.Servers.Secondary != nil { + nestedDnsSetting.Servers.Secondary = o.DnsSetting.Servers.Secondary + } + } + } + config.DnsSetting = nestedDnsSetting + + config.FqdnRefreshTime = o.FqdnRefreshTime + + config.Misc = o.Misc["Config"] + + return config, nil +} +func (c *configXmlContainer) Normalize() ([]*Config, error) { + configList := make([]*Config, 0, len(c.Answer)) + for _, o := range c.Answer { + config := &Config{ + Misc: make(map[string][]generic.Xml), + } + var nestedDnsSetting *DnsSetting + if o.DnsSetting != nil { + nestedDnsSetting = &DnsSetting{} + if o.DnsSetting.Misc != nil { + config.Misc["DnsSetting"] = o.DnsSetting.Misc + } + if o.DnsSetting.Servers != nil { + nestedDnsSetting.Servers = &DnsSettingServers{} + if o.DnsSetting.Servers.Misc != nil { + config.Misc["DnsSettingServers"] = o.DnsSetting.Servers.Misc + } + if o.DnsSetting.Servers.Primary != nil { + nestedDnsSetting.Servers.Primary = o.DnsSetting.Servers.Primary + } + if o.DnsSetting.Servers.Secondary != nil { + nestedDnsSetting.Servers.Secondary = o.DnsSetting.Servers.Secondary + } + } + } + config.DnsSetting = nestedDnsSetting + + config.FqdnRefreshTime = o.FqdnRefreshTime + + config.Misc["Config"] = o.Misc + + configList = append(configList, config) + } + + return configList, nil +} + +func SpecMatches(a, b *Config) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + + // Don't compare Name. + if !matchDnsSetting(a.DnsSetting, b.DnsSetting) { + return false + } + if !util.Ints64Match(a.FqdnRefreshTime, b.FqdnRefreshTime) { + return false + } + + return true +} + +func matchDnsSettingServers(a *DnsSettingServers, b *DnsSettingServers) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.StringsMatch(a.Primary, b.Primary) { + return false + } + if !util.StringsMatch(a.Secondary, b.Secondary) { + return false + } + return true +} +func matchDnsSetting(a *DnsSetting, b *DnsSetting) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !matchDnsSettingServers(a.Servers, b.Servers) { + return false + } + return true +} diff --git a/device/services/dns/interfaces.go b/device/services/dns/interfaces.go new file mode 100644 index 0000000..188f454 --- /dev/null +++ b/device/services/dns/interfaces.go @@ -0,0 +1,7 @@ +package dns + +type Specifier func(*Config) (any, error) + +type Normalizer interface { + Normalize() ([]*Config, error) +} diff --git a/device/services/dns/location.go b/device/services/dns/location.go new file mode 100644 index 0000000..4d42ef9 --- /dev/null +++ b/device/services/dns/location.go @@ -0,0 +1,180 @@ +package dns + +import ( + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +type ImportLocation interface { + XpathForLocation(version.Number, util.ILocation) ([]string, error) + MarshalPangoXML([]string) (string, error) + UnmarshalPangoXML([]byte) ([]string, error) +} + +type Location struct { + System *SystemLocation `json:"system,omitempty"` + Template *TemplateLocation `json:"template,omitempty"` + TemplateStack *TemplateStackLocation `json:"template_stack,omitempty"` +} + +type SystemLocation struct { + NgfwDevice string `json:"ngfw_device"` +} + +type TemplateLocation struct { + NgfwDevice string `json:"ngfw_device"` + PanoramaDevice string `json:"panorama_device"` + Template string `json:"template"` +} + +type TemplateStackLocation struct { + NgfwDevice string `json:"ngfw_device"` + PanoramaDevice string `json:"panorama_device"` + TemplateStack string `json:"template_stack"` +} + +func NewSystemLocation() *Location { + return &Location{System: &SystemLocation{ + NgfwDevice: "localhost.localdomain", + }, + } +} +func NewTemplateLocation() *Location { + return &Location{Template: &TemplateLocation{ + NgfwDevice: "localhost.localdomain", + PanoramaDevice: "localhost.localdomain", + Template: "", + }, + } +} +func NewTemplateStackLocation() *Location { + return &Location{TemplateStack: &TemplateStackLocation{ + NgfwDevice: "localhost.localdomain", + PanoramaDevice: "localhost.localdomain", + TemplateStack: "", + }, + } +} + +func (o Location) IsValid() error { + count := 0 + + switch { + case o.System != nil: + if o.System.NgfwDevice == "" { + return fmt.Errorf("NgfwDevice is unspecified") + } + count++ + case o.Template != nil: + if o.Template.NgfwDevice == "" { + return fmt.Errorf("NgfwDevice is unspecified") + } + if o.Template.PanoramaDevice == "" { + return fmt.Errorf("PanoramaDevice is unspecified") + } + if o.Template.Template == "" { + return fmt.Errorf("Template is unspecified") + } + count++ + case o.TemplateStack != nil: + if o.TemplateStack.NgfwDevice == "" { + return fmt.Errorf("NgfwDevice is unspecified") + } + if o.TemplateStack.PanoramaDevice == "" { + return fmt.Errorf("PanoramaDevice is unspecified") + } + if o.TemplateStack.TemplateStack == "" { + return fmt.Errorf("TemplateStack is unspecified") + } + count++ + } + + if count == 0 { + return fmt.Errorf("no path specified") + } + + if count > 1 { + return fmt.Errorf("multiple paths specified: only one should be specified") + } + + return nil +} + +func (o Location) XpathPrefix(vn version.Number) ([]string, error) { + + var ans []string + + switch { + case o.System != nil: + if o.System.NgfwDevice == "" { + return nil, fmt.Errorf("NgfwDevice is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.System.NgfwDevice}), + "deviceconfig", + "system", + } + case o.Template != nil: + if o.Template.NgfwDevice == "" { + return nil, fmt.Errorf("NgfwDevice is unspecified") + } + if o.Template.PanoramaDevice == "" { + return nil, fmt.Errorf("PanoramaDevice is unspecified") + } + if o.Template.Template == "" { + return nil, fmt.Errorf("Template is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.Template.PanoramaDevice}), + "template", + util.AsEntryXpath([]string{o.Template.Template}), + "config", + "devices", + util.AsEntryXpath([]string{o.Template.NgfwDevice}), + "deviceconfig", + "system", + } + case o.TemplateStack != nil: + if o.TemplateStack.NgfwDevice == "" { + return nil, fmt.Errorf("NgfwDevice is unspecified") + } + if o.TemplateStack.PanoramaDevice == "" { + return nil, fmt.Errorf("PanoramaDevice is unspecified") + } + if o.TemplateStack.TemplateStack == "" { + return nil, fmt.Errorf("TemplateStack is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.TemplateStack.PanoramaDevice}), + "template-stack", + util.AsEntryXpath([]string{o.TemplateStack.TemplateStack}), + "config", + "devices", + util.AsEntryXpath([]string{o.TemplateStack.NgfwDevice}), + "deviceconfig", + "system", + } + default: + return nil, errors.NoLocationSpecifiedError + } + + return ans, nil +} +func (o Location) Xpath(vn version.Number) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + + return ans, nil +} diff --git a/device/services/dns/service.go b/device/services/dns/service.go new file mode 100644 index 0000000..83ca11b --- /dev/null +++ b/device/services/dns/service.go @@ -0,0 +1,175 @@ +package dns + +import ( + "context" + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/xmlapi" +) + +type Service struct { + client util.PangoClient +} + +func NewService(client util.PangoClient) *Service { + return &Service{ + client: client, + } +} + +// Create adds new item, then returns the result. +func (s *Service) Create(ctx context.Context, loc Location, config *Config) (*Config, error) { + + vn := s.client.Versioning() + + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + path, err := loc.Xpath(vn) + if err != nil { + return nil, err + } + createSpec, err := specifier(config) + if err != nil { + return nil, err + } + + cmd := &xmlapi.Config{ + Action: "set", + Xpath: util.AsXpath(path[:len(path)-1]), + Element: createSpec, + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, false, nil); err != nil { + return nil, err + } + return s.Read(ctx, loc, "get") +} + +// Read returns the given config object, using the specified action ("get" or "show"). +func (s *Service) Read(ctx context.Context, loc Location, action string) (*Config, error) { + return s.read(ctx, loc, action, false) +} + +// ReadFromConfig returns the given config object from the loaded XML config. +// Requires that client.LoadPanosConfig() has been invoked. +func (s *Service) ReadFromConfig(ctx context.Context, loc Location) (*Config, error) { + return s.read(ctx, loc, "", true) +} + +func (s *Service) read(ctx context.Context, loc Location, action string, usePanosConfig bool) (*Config, error) { + vn := s.client.Versioning() + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + path, err := loc.Xpath(vn) + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, true, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, errors.ObjectNotFound() + } + return nil, err + } + } + + list, err := normalizer.Normalize() + if err != nil { + return nil, err + } else if len(list) != 1 { + return nil, fmt.Errorf("expected to %q 1 entry, got %d", action, len(list)) + } + + return list[0], nil +} + +func (s *Service) Update(ctx context.Context, loc Location, entry *Config) (*Config, error) { + + vn := s.client.Versioning() + updates := xmlapi.NewMultiConfig(2) + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + var old *Config + old, err = s.Read(ctx, loc, "get") + if err != nil { + return nil, err + } else if old == nil { + return nil, fmt.Errorf("previous object doesn't exist for update") + } + if !SpecMatches(entry, old) { + path, err := loc.Xpath(vn) + if err != nil { + return nil, err + } + + updateSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + updates.Add(&xmlapi.Config{ + Action: "edit", + Xpath: util.AsXpath(path), + Element: updateSpec, + Target: s.client.GetTarget(), + }) + } + + if len(updates.Operations) != 0 { + if _, _, _, err = s.client.MultiConfig(ctx, updates, false, nil); err != nil { + return nil, err + } + } + return s.Read(ctx, loc, "get") +} + +// Delete deletes the given item. +func (s *Service) Delete(ctx context.Context, loc Location, config *Config) error { + return s.delete(ctx, loc, config) +} +func (s *Service) delete(ctx context.Context, loc Location, config *Config) error { + + vn := s.client.Versioning() + path, err := loc.Xpath(vn) + if err != nil { + return err + } + deleteSuffixes := []string{} + deleteSuffixes = append(deleteSuffixes, "dns-setting") + deleteSuffixes = append(deleteSuffixes, "fqdn-refresh-time") + + for _, suffix := range deleteSuffixes { + cmd := &xmlapi.Config{ + Action: "delete", + Xpath: util.AsXpath(append(path, suffix)), + Target: s.client.GetTarget(), + } + + _, _, err = s.client.Communicate(ctx, cmd, false, nil) + + if err != nil { + return err + } + } + return nil +} diff --git a/device/services/ntp/config.go b/device/services/ntp/config.go new file mode 100644 index 0000000..59ee99c --- /dev/null +++ b/device/services/ntp/config.go @@ -0,0 +1,538 @@ +package ntp + +import ( + "encoding/xml" + + "github.com/PaloAltoNetworks/pango/generic" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +type Config struct { + NtpServers *NtpServers + + Misc map[string][]generic.Xml +} +type NtpServers struct { + PrimaryNtpServer *NtpServersPrimaryNtpServer + SecondaryNtpServer *NtpServersSecondaryNtpServer +} +type NtpServersPrimaryNtpServer struct { + AuthenticationType *NtpServersPrimaryNtpServerAuthenticationType + NtpServerAddress *string +} +type NtpServersPrimaryNtpServerAuthenticationType struct { + Autokey *string + None *string + SymmetricKey *NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKey +} +type NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKey struct { + KeyId *int64 + Md5 *NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKeyMd5 + Sha1 *NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKeySha1 +} +type NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKeyMd5 struct { + AuthenticationKey *string +} +type NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKeySha1 struct { + AuthenticationKey *string +} +type NtpServersSecondaryNtpServer struct { + AuthenticationType *NtpServersSecondaryNtpServerAuthenticationType + NtpServerAddress *string +} +type NtpServersSecondaryNtpServerAuthenticationType struct { + Autokey *string + None *string + SymmetricKey *NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKey +} +type NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKey struct { + KeyId *int64 + Md5 *NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKeyMd5 + Sha1 *NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKeySha1 +} +type NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKeyMd5 struct { + AuthenticationKey *string +} +type NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKeySha1 struct { + AuthenticationKey *string +} +type configXmlContainer struct { + XMLName xml.Name `xml:"result"` + Answer []configXml `xml:"system"` +} +type configXml struct { + XMLName xml.Name `xml:"system"` + NtpServers *NtpServersXml `xml:"ntp-servers,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type NtpServersXml struct { + PrimaryNtpServer *NtpServersPrimaryNtpServerXml `xml:"primary-ntp-server,omitempty"` + SecondaryNtpServer *NtpServersSecondaryNtpServerXml `xml:"secondary-ntp-server,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type NtpServersPrimaryNtpServerXml struct { + AuthenticationType *NtpServersPrimaryNtpServerAuthenticationTypeXml `xml:"authentication-type,omitempty"` + NtpServerAddress *string `xml:"ntp-server-address,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type NtpServersPrimaryNtpServerAuthenticationTypeXml struct { + Autokey *string `xml:"autokey,omitempty"` + None *string `xml:"none,omitempty"` + SymmetricKey *NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKeyXml `xml:"symmetric-key,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKeyXml struct { + KeyId *int64 `xml:"key-id,omitempty"` + Md5 *NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKeyMd5Xml `xml:"algorithm>md5,omitempty"` + Sha1 *NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKeySha1Xml `xml:"algorithm>sha1,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKeyMd5Xml struct { + AuthenticationKey *string `xml:"authentication-key,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKeySha1Xml struct { + AuthenticationKey *string `xml:"authentication-key,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type NtpServersSecondaryNtpServerXml struct { + AuthenticationType *NtpServersSecondaryNtpServerAuthenticationTypeXml `xml:"authentication-type,omitempty"` + NtpServerAddress *string `xml:"ntp-server-address,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type NtpServersSecondaryNtpServerAuthenticationTypeXml struct { + Autokey *string `xml:"autokey,omitempty"` + None *string `xml:"none,omitempty"` + SymmetricKey *NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKeyXml `xml:"symmetric-key,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKeyXml struct { + KeyId *int64 `xml:"key-id,omitempty"` + Md5 *NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKeyMd5Xml `xml:"algorithm>md5,omitempty"` + Sha1 *NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKeySha1Xml `xml:"algorithm>sha1,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKeyMd5Xml struct { + AuthenticationKey *string `xml:"authentication-key,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKeySha1Xml struct { + AuthenticationKey *string `xml:"authentication-key,omitempty"` + + Misc []generic.Xml `xml:",any"` +} + +func Versioning(vn version.Number) (Specifier, Normalizer, error) { + return specifyConfig, &configXmlContainer{}, nil +} +func specifyConfig(o *Config) (any, error) { + config := configXml{} + var nestedNtpServers *NtpServersXml + if o.NtpServers != nil { + nestedNtpServers = &NtpServersXml{} + if _, ok := o.Misc["NtpServers"]; ok { + nestedNtpServers.Misc = o.Misc["NtpServers"] + } + if o.NtpServers.PrimaryNtpServer != nil { + nestedNtpServers.PrimaryNtpServer = &NtpServersPrimaryNtpServerXml{} + if _, ok := o.Misc["NtpServersPrimaryNtpServer"]; ok { + nestedNtpServers.PrimaryNtpServer.Misc = o.Misc["NtpServersPrimaryNtpServer"] + } + if o.NtpServers.PrimaryNtpServer.NtpServerAddress != nil { + nestedNtpServers.PrimaryNtpServer.NtpServerAddress = o.NtpServers.PrimaryNtpServer.NtpServerAddress + } + if o.NtpServers.PrimaryNtpServer.AuthenticationType != nil { + nestedNtpServers.PrimaryNtpServer.AuthenticationType = &NtpServersPrimaryNtpServerAuthenticationTypeXml{} + if _, ok := o.Misc["NtpServersPrimaryNtpServerAuthenticationType"]; ok { + nestedNtpServers.PrimaryNtpServer.AuthenticationType.Misc = o.Misc["NtpServersPrimaryNtpServerAuthenticationType"] + } + if o.NtpServers.PrimaryNtpServer.AuthenticationType.None != nil { + nestedNtpServers.PrimaryNtpServer.AuthenticationType.None = o.NtpServers.PrimaryNtpServer.AuthenticationType.None + } + if o.NtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey != nil { + nestedNtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey = &NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKeyXml{} + if _, ok := o.Misc["NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKey"]; ok { + nestedNtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.Misc = o.Misc["NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKey"] + } + if o.NtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.KeyId != nil { + nestedNtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.KeyId = o.NtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.KeyId + } + if o.NtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.Md5 != nil { + nestedNtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.Md5 = &NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKeyMd5Xml{} + if _, ok := o.Misc["NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKeyMd5"]; ok { + nestedNtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.Md5.Misc = o.Misc["NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKeyMd5"] + } + if o.NtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.Md5.AuthenticationKey != nil { + nestedNtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.Md5.AuthenticationKey = o.NtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.Md5.AuthenticationKey + } + } + if o.NtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.Sha1 != nil { + nestedNtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.Sha1 = &NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKeySha1Xml{} + if _, ok := o.Misc["NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKeySha1"]; ok { + nestedNtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.Sha1.Misc = o.Misc["NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKeySha1"] + } + if o.NtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.Sha1.AuthenticationKey != nil { + nestedNtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.Sha1.AuthenticationKey = o.NtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.Sha1.AuthenticationKey + } + } + } + if o.NtpServers.PrimaryNtpServer.AuthenticationType.Autokey != nil { + nestedNtpServers.PrimaryNtpServer.AuthenticationType.Autokey = o.NtpServers.PrimaryNtpServer.AuthenticationType.Autokey + } + } + } + if o.NtpServers.SecondaryNtpServer != nil { + nestedNtpServers.SecondaryNtpServer = &NtpServersSecondaryNtpServerXml{} + if _, ok := o.Misc["NtpServersSecondaryNtpServer"]; ok { + nestedNtpServers.SecondaryNtpServer.Misc = o.Misc["NtpServersSecondaryNtpServer"] + } + if o.NtpServers.SecondaryNtpServer.NtpServerAddress != nil { + nestedNtpServers.SecondaryNtpServer.NtpServerAddress = o.NtpServers.SecondaryNtpServer.NtpServerAddress + } + if o.NtpServers.SecondaryNtpServer.AuthenticationType != nil { + nestedNtpServers.SecondaryNtpServer.AuthenticationType = &NtpServersSecondaryNtpServerAuthenticationTypeXml{} + if _, ok := o.Misc["NtpServersSecondaryNtpServerAuthenticationType"]; ok { + nestedNtpServers.SecondaryNtpServer.AuthenticationType.Misc = o.Misc["NtpServersSecondaryNtpServerAuthenticationType"] + } + if o.NtpServers.SecondaryNtpServer.AuthenticationType.None != nil { + nestedNtpServers.SecondaryNtpServer.AuthenticationType.None = o.NtpServers.SecondaryNtpServer.AuthenticationType.None + } + if o.NtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey != nil { + nestedNtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey = &NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKeyXml{} + if _, ok := o.Misc["NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKey"]; ok { + nestedNtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.Misc = o.Misc["NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKey"] + } + if o.NtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.KeyId != nil { + nestedNtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.KeyId = o.NtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.KeyId + } + if o.NtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.Sha1 != nil { + nestedNtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.Sha1 = &NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKeySha1Xml{} + if _, ok := o.Misc["NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKeySha1"]; ok { + nestedNtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.Sha1.Misc = o.Misc["NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKeySha1"] + } + if o.NtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.Sha1.AuthenticationKey != nil { + nestedNtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.Sha1.AuthenticationKey = o.NtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.Sha1.AuthenticationKey + } + } + if o.NtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.Md5 != nil { + nestedNtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.Md5 = &NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKeyMd5Xml{} + if _, ok := o.Misc["NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKeyMd5"]; ok { + nestedNtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.Md5.Misc = o.Misc["NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKeyMd5"] + } + if o.NtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.Md5.AuthenticationKey != nil { + nestedNtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.Md5.AuthenticationKey = o.NtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.Md5.AuthenticationKey + } + } + } + if o.NtpServers.SecondaryNtpServer.AuthenticationType.Autokey != nil { + nestedNtpServers.SecondaryNtpServer.AuthenticationType.Autokey = o.NtpServers.SecondaryNtpServer.AuthenticationType.Autokey + } + } + } + } + config.NtpServers = nestedNtpServers + + config.Misc = o.Misc["Config"] + + return config, nil +} +func (c *configXmlContainer) Normalize() ([]*Config, error) { + configList := make([]*Config, 0, len(c.Answer)) + for _, o := range c.Answer { + config := &Config{ + Misc: make(map[string][]generic.Xml), + } + var nestedNtpServers *NtpServers + if o.NtpServers != nil { + nestedNtpServers = &NtpServers{} + if o.NtpServers.Misc != nil { + config.Misc["NtpServers"] = o.NtpServers.Misc + } + if o.NtpServers.PrimaryNtpServer != nil { + nestedNtpServers.PrimaryNtpServer = &NtpServersPrimaryNtpServer{} + if o.NtpServers.PrimaryNtpServer.Misc != nil { + config.Misc["NtpServersPrimaryNtpServer"] = o.NtpServers.PrimaryNtpServer.Misc + } + if o.NtpServers.PrimaryNtpServer.NtpServerAddress != nil { + nestedNtpServers.PrimaryNtpServer.NtpServerAddress = o.NtpServers.PrimaryNtpServer.NtpServerAddress + } + if o.NtpServers.PrimaryNtpServer.AuthenticationType != nil { + nestedNtpServers.PrimaryNtpServer.AuthenticationType = &NtpServersPrimaryNtpServerAuthenticationType{} + if o.NtpServers.PrimaryNtpServer.AuthenticationType.Misc != nil { + config.Misc["NtpServersPrimaryNtpServerAuthenticationType"] = o.NtpServers.PrimaryNtpServer.AuthenticationType.Misc + } + if o.NtpServers.PrimaryNtpServer.AuthenticationType.None != nil { + nestedNtpServers.PrimaryNtpServer.AuthenticationType.None = o.NtpServers.PrimaryNtpServer.AuthenticationType.None + } + if o.NtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey != nil { + nestedNtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey = &NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKey{} + if o.NtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.Misc != nil { + config.Misc["NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKey"] = o.NtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.Misc + } + if o.NtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.KeyId != nil { + nestedNtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.KeyId = o.NtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.KeyId + } + if o.NtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.Sha1 != nil { + nestedNtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.Sha1 = &NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKeySha1{} + if o.NtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.Sha1.Misc != nil { + config.Misc["NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKeySha1"] = o.NtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.Sha1.Misc + } + if o.NtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.Sha1.AuthenticationKey != nil { + nestedNtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.Sha1.AuthenticationKey = o.NtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.Sha1.AuthenticationKey + } + } + if o.NtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.Md5 != nil { + nestedNtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.Md5 = &NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKeyMd5{} + if o.NtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.Md5.Misc != nil { + config.Misc["NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKeyMd5"] = o.NtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.Md5.Misc + } + if o.NtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.Md5.AuthenticationKey != nil { + nestedNtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.Md5.AuthenticationKey = o.NtpServers.PrimaryNtpServer.AuthenticationType.SymmetricKey.Md5.AuthenticationKey + } + } + } + if o.NtpServers.PrimaryNtpServer.AuthenticationType.Autokey != nil { + nestedNtpServers.PrimaryNtpServer.AuthenticationType.Autokey = o.NtpServers.PrimaryNtpServer.AuthenticationType.Autokey + } + } + } + if o.NtpServers.SecondaryNtpServer != nil { + nestedNtpServers.SecondaryNtpServer = &NtpServersSecondaryNtpServer{} + if o.NtpServers.SecondaryNtpServer.Misc != nil { + config.Misc["NtpServersSecondaryNtpServer"] = o.NtpServers.SecondaryNtpServer.Misc + } + if o.NtpServers.SecondaryNtpServer.NtpServerAddress != nil { + nestedNtpServers.SecondaryNtpServer.NtpServerAddress = o.NtpServers.SecondaryNtpServer.NtpServerAddress + } + if o.NtpServers.SecondaryNtpServer.AuthenticationType != nil { + nestedNtpServers.SecondaryNtpServer.AuthenticationType = &NtpServersSecondaryNtpServerAuthenticationType{} + if o.NtpServers.SecondaryNtpServer.AuthenticationType.Misc != nil { + config.Misc["NtpServersSecondaryNtpServerAuthenticationType"] = o.NtpServers.SecondaryNtpServer.AuthenticationType.Misc + } + if o.NtpServers.SecondaryNtpServer.AuthenticationType.None != nil { + nestedNtpServers.SecondaryNtpServer.AuthenticationType.None = o.NtpServers.SecondaryNtpServer.AuthenticationType.None + } + if o.NtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey != nil { + nestedNtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey = &NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKey{} + if o.NtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.Misc != nil { + config.Misc["NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKey"] = o.NtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.Misc + } + if o.NtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.KeyId != nil { + nestedNtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.KeyId = o.NtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.KeyId + } + if o.NtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.Md5 != nil { + nestedNtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.Md5 = &NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKeyMd5{} + if o.NtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.Md5.Misc != nil { + config.Misc["NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKeyMd5"] = o.NtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.Md5.Misc + } + if o.NtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.Md5.AuthenticationKey != nil { + nestedNtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.Md5.AuthenticationKey = o.NtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.Md5.AuthenticationKey + } + } + if o.NtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.Sha1 != nil { + nestedNtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.Sha1 = &NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKeySha1{} + if o.NtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.Sha1.Misc != nil { + config.Misc["NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKeySha1"] = o.NtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.Sha1.Misc + } + if o.NtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.Sha1.AuthenticationKey != nil { + nestedNtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.Sha1.AuthenticationKey = o.NtpServers.SecondaryNtpServer.AuthenticationType.SymmetricKey.Sha1.AuthenticationKey + } + } + } + if o.NtpServers.SecondaryNtpServer.AuthenticationType.Autokey != nil { + nestedNtpServers.SecondaryNtpServer.AuthenticationType.Autokey = o.NtpServers.SecondaryNtpServer.AuthenticationType.Autokey + } + } + } + } + config.NtpServers = nestedNtpServers + + config.Misc["Config"] = o.Misc + + configList = append(configList, config) + } + + return configList, nil +} + +func SpecMatches(a, b *Config) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + + // Don't compare Name. + if !matchNtpServers(a.NtpServers, b.NtpServers) { + return false + } + + return true +} + +func matchNtpServersPrimaryNtpServerAuthenticationTypeSymmetricKeySha1(a *NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKeySha1, b *NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKeySha1) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.StringsMatch(a.AuthenticationKey, b.AuthenticationKey) { + return false + } + return true +} +func matchNtpServersPrimaryNtpServerAuthenticationTypeSymmetricKeyMd5(a *NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKeyMd5, b *NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKeyMd5) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.StringsMatch(a.AuthenticationKey, b.AuthenticationKey) { + return false + } + return true +} +func matchNtpServersPrimaryNtpServerAuthenticationTypeSymmetricKey(a *NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKey, b *NtpServersPrimaryNtpServerAuthenticationTypeSymmetricKey) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.Ints64Match(a.KeyId, b.KeyId) { + return false + } + if !matchNtpServersPrimaryNtpServerAuthenticationTypeSymmetricKeySha1(a.Sha1, b.Sha1) { + return false + } + if !matchNtpServersPrimaryNtpServerAuthenticationTypeSymmetricKeyMd5(a.Md5, b.Md5) { + return false + } + return true +} +func matchNtpServersPrimaryNtpServerAuthenticationType(a *NtpServersPrimaryNtpServerAuthenticationType, b *NtpServersPrimaryNtpServerAuthenticationType) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.StringsMatch(a.None, b.None) { + return false + } + if !matchNtpServersPrimaryNtpServerAuthenticationTypeSymmetricKey(a.SymmetricKey, b.SymmetricKey) { + return false + } + if !util.StringsMatch(a.Autokey, b.Autokey) { + return false + } + return true +} +func matchNtpServersPrimaryNtpServer(a *NtpServersPrimaryNtpServer, b *NtpServersPrimaryNtpServer) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.StringsMatch(a.NtpServerAddress, b.NtpServerAddress) { + return false + } + if !matchNtpServersPrimaryNtpServerAuthenticationType(a.AuthenticationType, b.AuthenticationType) { + return false + } + return true +} +func matchNtpServersSecondaryNtpServerAuthenticationTypeSymmetricKeyMd5(a *NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKeyMd5, b *NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKeyMd5) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.StringsMatch(a.AuthenticationKey, b.AuthenticationKey) { + return false + } + return true +} +func matchNtpServersSecondaryNtpServerAuthenticationTypeSymmetricKeySha1(a *NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKeySha1, b *NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKeySha1) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.StringsMatch(a.AuthenticationKey, b.AuthenticationKey) { + return false + } + return true +} +func matchNtpServersSecondaryNtpServerAuthenticationTypeSymmetricKey(a *NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKey, b *NtpServersSecondaryNtpServerAuthenticationTypeSymmetricKey) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.Ints64Match(a.KeyId, b.KeyId) { + return false + } + if !matchNtpServersSecondaryNtpServerAuthenticationTypeSymmetricKeyMd5(a.Md5, b.Md5) { + return false + } + if !matchNtpServersSecondaryNtpServerAuthenticationTypeSymmetricKeySha1(a.Sha1, b.Sha1) { + return false + } + return true +} +func matchNtpServersSecondaryNtpServerAuthenticationType(a *NtpServersSecondaryNtpServerAuthenticationType, b *NtpServersSecondaryNtpServerAuthenticationType) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.StringsMatch(a.None, b.None) { + return false + } + if !matchNtpServersSecondaryNtpServerAuthenticationTypeSymmetricKey(a.SymmetricKey, b.SymmetricKey) { + return false + } + if !util.StringsMatch(a.Autokey, b.Autokey) { + return false + } + return true +} +func matchNtpServersSecondaryNtpServer(a *NtpServersSecondaryNtpServer, b *NtpServersSecondaryNtpServer) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.StringsMatch(a.NtpServerAddress, b.NtpServerAddress) { + return false + } + if !matchNtpServersSecondaryNtpServerAuthenticationType(a.AuthenticationType, b.AuthenticationType) { + return false + } + return true +} +func matchNtpServers(a *NtpServers, b *NtpServers) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !matchNtpServersPrimaryNtpServer(a.PrimaryNtpServer, b.PrimaryNtpServer) { + return false + } + if !matchNtpServersSecondaryNtpServer(a.SecondaryNtpServer, b.SecondaryNtpServer) { + return false + } + return true +} diff --git a/device/services/ntp/interfaces.go b/device/services/ntp/interfaces.go new file mode 100644 index 0000000..a5e3956 --- /dev/null +++ b/device/services/ntp/interfaces.go @@ -0,0 +1,7 @@ +package ntp + +type Specifier func(*Config) (any, error) + +type Normalizer interface { + Normalize() ([]*Config, error) +} diff --git a/device/services/ntp/location.go b/device/services/ntp/location.go new file mode 100644 index 0000000..9dec490 --- /dev/null +++ b/device/services/ntp/location.go @@ -0,0 +1,180 @@ +package ntp + +import ( + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +type ImportLocation interface { + XpathForLocation(version.Number, util.ILocation) ([]string, error) + MarshalPangoXML([]string) (string, error) + UnmarshalPangoXML([]byte) ([]string, error) +} + +type Location struct { + System *SystemLocation `json:"system,omitempty"` + Template *TemplateLocation `json:"template,omitempty"` + TemplateStack *TemplateStackLocation `json:"template_stack,omitempty"` +} + +type SystemLocation struct { + NgfwDevice string `json:"ngfw_device"` +} + +type TemplateLocation struct { + NgfwDevice string `json:"ngfw_device"` + PanoramaDevice string `json:"panorama_device"` + Template string `json:"template"` +} + +type TemplateStackLocation struct { + NgfwDevice string `json:"ngfw_device"` + PanoramaDevice string `json:"panorama_device"` + TemplateStack string `json:"template_stack"` +} + +func NewSystemLocation() *Location { + return &Location{System: &SystemLocation{ + NgfwDevice: "localhost.localdomain", + }, + } +} +func NewTemplateLocation() *Location { + return &Location{Template: &TemplateLocation{ + NgfwDevice: "localhost.localdomain", + PanoramaDevice: "localhost.localdomain", + Template: "", + }, + } +} +func NewTemplateStackLocation() *Location { + return &Location{TemplateStack: &TemplateStackLocation{ + NgfwDevice: "localhost.localdomain", + PanoramaDevice: "localhost.localdomain", + TemplateStack: "", + }, + } +} + +func (o Location) IsValid() error { + count := 0 + + switch { + case o.System != nil: + if o.System.NgfwDevice == "" { + return fmt.Errorf("NgfwDevice is unspecified") + } + count++ + case o.Template != nil: + if o.Template.NgfwDevice == "" { + return fmt.Errorf("NgfwDevice is unspecified") + } + if o.Template.PanoramaDevice == "" { + return fmt.Errorf("PanoramaDevice is unspecified") + } + if o.Template.Template == "" { + return fmt.Errorf("Template is unspecified") + } + count++ + case o.TemplateStack != nil: + if o.TemplateStack.NgfwDevice == "" { + return fmt.Errorf("NgfwDevice is unspecified") + } + if o.TemplateStack.PanoramaDevice == "" { + return fmt.Errorf("PanoramaDevice is unspecified") + } + if o.TemplateStack.TemplateStack == "" { + return fmt.Errorf("TemplateStack is unspecified") + } + count++ + } + + if count == 0 { + return fmt.Errorf("no path specified") + } + + if count > 1 { + return fmt.Errorf("multiple paths specified: only one should be specified") + } + + return nil +} + +func (o Location) XpathPrefix(vn version.Number) ([]string, error) { + + var ans []string + + switch { + case o.System != nil: + if o.System.NgfwDevice == "" { + return nil, fmt.Errorf("NgfwDevice is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.System.NgfwDevice}), + "deviceconfig", + "system", + } + case o.Template != nil: + if o.Template.NgfwDevice == "" { + return nil, fmt.Errorf("NgfwDevice is unspecified") + } + if o.Template.PanoramaDevice == "" { + return nil, fmt.Errorf("PanoramaDevice is unspecified") + } + if o.Template.Template == "" { + return nil, fmt.Errorf("Template is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.Template.PanoramaDevice}), + "template", + util.AsEntryXpath([]string{o.Template.Template}), + "config", + "devices", + util.AsEntryXpath([]string{o.Template.NgfwDevice}), + "deviceconfig", + "system", + } + case o.TemplateStack != nil: + if o.TemplateStack.NgfwDevice == "" { + return nil, fmt.Errorf("NgfwDevice is unspecified") + } + if o.TemplateStack.PanoramaDevice == "" { + return nil, fmt.Errorf("PanoramaDevice is unspecified") + } + if o.TemplateStack.TemplateStack == "" { + return nil, fmt.Errorf("TemplateStack is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.TemplateStack.PanoramaDevice}), + "template-stack", + util.AsEntryXpath([]string{o.TemplateStack.TemplateStack}), + "config", + "devices", + util.AsEntryXpath([]string{o.TemplateStack.NgfwDevice}), + "deviceconfig", + "system", + } + default: + return nil, errors.NoLocationSpecifiedError + } + + return ans, nil +} +func (o Location) Xpath(vn version.Number) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + + return ans, nil +} diff --git a/device/services/ntp/service.go b/device/services/ntp/service.go new file mode 100644 index 0000000..da4ab75 --- /dev/null +++ b/device/services/ntp/service.go @@ -0,0 +1,174 @@ +package ntp + +import ( + "context" + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/xmlapi" +) + +type Service struct { + client util.PangoClient +} + +func NewService(client util.PangoClient) *Service { + return &Service{ + client: client, + } +} + +// Create adds new item, then returns the result. +func (s *Service) Create(ctx context.Context, loc Location, config *Config) (*Config, error) { + + vn := s.client.Versioning() + + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + path, err := loc.Xpath(vn) + if err != nil { + return nil, err + } + createSpec, err := specifier(config) + if err != nil { + return nil, err + } + + cmd := &xmlapi.Config{ + Action: "set", + Xpath: util.AsXpath(path[:len(path)-1]), + Element: createSpec, + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, false, nil); err != nil { + return nil, err + } + return s.Read(ctx, loc, "get") +} + +// Read returns the given config object, using the specified action ("get" or "show"). +func (s *Service) Read(ctx context.Context, loc Location, action string) (*Config, error) { + return s.read(ctx, loc, action, false) +} + +// ReadFromConfig returns the given config object from the loaded XML config. +// Requires that client.LoadPanosConfig() has been invoked. +func (s *Service) ReadFromConfig(ctx context.Context, loc Location) (*Config, error) { + return s.read(ctx, loc, "", true) +} + +func (s *Service) read(ctx context.Context, loc Location, action string, usePanosConfig bool) (*Config, error) { + vn := s.client.Versioning() + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + path, err := loc.Xpath(vn) + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, true, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, errors.ObjectNotFound() + } + return nil, err + } + } + + list, err := normalizer.Normalize() + if err != nil { + return nil, err + } else if len(list) != 1 { + return nil, fmt.Errorf("expected to %q 1 entry, got %d", action, len(list)) + } + + return list[0], nil +} + +func (s *Service) Update(ctx context.Context, loc Location, entry *Config) (*Config, error) { + + vn := s.client.Versioning() + updates := xmlapi.NewMultiConfig(2) + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + var old *Config + old, err = s.Read(ctx, loc, "get") + if err != nil { + return nil, err + } else if old == nil { + return nil, fmt.Errorf("previous object doesn't exist for update") + } + if !SpecMatches(entry, old) { + path, err := loc.Xpath(vn) + if err != nil { + return nil, err + } + + updateSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + updates.Add(&xmlapi.Config{ + Action: "edit", + Xpath: util.AsXpath(path), + Element: updateSpec, + Target: s.client.GetTarget(), + }) + } + + if len(updates.Operations) != 0 { + if _, _, _, err = s.client.MultiConfig(ctx, updates, false, nil); err != nil { + return nil, err + } + } + return s.Read(ctx, loc, "get") +} + +// Delete deletes the given item. +func (s *Service) Delete(ctx context.Context, loc Location, config *Config) error { + return s.delete(ctx, loc, config) +} +func (s *Service) delete(ctx context.Context, loc Location, config *Config) error { + + vn := s.client.Versioning() + path, err := loc.Xpath(vn) + if err != nil { + return err + } + deleteSuffixes := []string{} + deleteSuffixes = append(deleteSuffixes, "ntp-servers") + + for _, suffix := range deleteSuffixes { + cmd := &xmlapi.Config{ + Action: "delete", + Xpath: util.AsXpath(append(path, suffix)), + Target: s.client.GetTarget(), + } + + _, _, err = s.client.Communicate(ctx, cmd, false, nil) + + if err != nil { + return err + } + } + return nil +} diff --git a/discard.go b/discard.go new file mode 100644 index 0000000..8bad67d --- /dev/null +++ b/discard.go @@ -0,0 +1,18 @@ +package pango + +import ( + "context" + "log/slog" +) + +// discardHandler is an slog handler which is always disabled. +// +// This slog handler implementation is always disabled, and therefore +// logs nothing. It is used to filter out log categories not +// explicitly enabled. +type discardHandler struct{} + +func (d discardHandler) Enabled(context.Context, slog.Level) bool { return false } +func (d discardHandler) Handle(context.Context, slog.Record) error { return nil } +func (d discardHandler) WithAttrs(attrs []slog.Attr) slog.Handler { return d } +func (d discardHandler) WithGroup(name string) slog.Handler { return d } diff --git a/errors/panos.go b/errors/panos.go index b2e5b3c..16a604b 100644 --- a/errors/panos.go +++ b/errors/panos.go @@ -5,8 +5,6 @@ import ( stderr "errors" "fmt" "strings" - - "github.com/PaloAltoNetworks/pango/util" ) var InvalidFilterError = stderr.New("filter is improperly formatted") @@ -35,6 +33,15 @@ func (e Panos) ObjectNotFound() bool { return e.Code == 7 } +func IsObjectNotFound(e error) bool { + e2, ok := e.(Panos) + if ok && e2.ObjectNotFound() { + return true + } + + return false +} + // ObjectNotFound returns an object not found error. func ObjectNotFound() Panos { return Panos{ @@ -67,8 +74,12 @@ type errorCheck struct { } type errorCheckMsg struct { - Line []util.CdataText `xml:"line"` - Message string `xml:",chardata"` + Line []errLine `xml:"line"` + Message string `xml:",chardata"` +} + +type errLine struct { + Text string `xml:",cdata"` } func (e *errorCheck) Failed() bool { diff --git a/example/main.go b/example/main.go new file mode 100644 index 0000000..7c04e4d --- /dev/null +++ b/example/main.go @@ -0,0 +1,1099 @@ +package main + +import ( + "context" + "encoding/xml" + "fmt" + "log" + + "github.com/PaloAltoNetworks/pango" + "github.com/PaloAltoNetworks/pango/device/services/dns" + "github.com/PaloAltoNetworks/pango/device/services/ntp" + "github.com/PaloAltoNetworks/pango/network/interface/ethernet" + "github.com/PaloAltoNetworks/pango/network/interface/loopback" + "github.com/PaloAltoNetworks/pango/network/profiles/interface_management" + "github.com/PaloAltoNetworks/pango/network/virtual_router" + "github.com/PaloAltoNetworks/pango/network/zone" + "github.com/PaloAltoNetworks/pango/objects/address" + address_group "github.com/PaloAltoNetworks/pango/objects/address/group" + "github.com/PaloAltoNetworks/pango/objects/service" + service_group "github.com/PaloAltoNetworks/pango/objects/service/group" + "github.com/PaloAltoNetworks/pango/objects/tag" + "github.com/PaloAltoNetworks/pango/panorama/device_group" + "github.com/PaloAltoNetworks/pango/panorama/template" + "github.com/PaloAltoNetworks/pango/panorama/template_stack" + "github.com/PaloAltoNetworks/pango/policies/rules/security" + "github.com/PaloAltoNetworks/pango/rule" + "github.com/PaloAltoNetworks/pango/util" +) + +func main() { + var err error + ctx := context.Background() + + // FW + c := &pango.Client{ + CheckEnvironment: true, + SkipVerifyCertificate: true, + ApiKeyInRequest: true, + } + if err = c.Setup(); err != nil { + log.Printf("Failed to setup client: %s", err) + return + } + log.Printf("Setup client %s (%s)", c.Hostname, c.Username) + + if err = c.Initialize(ctx); err != nil { + log.Printf("Failed to initialize client: %s", err) + return + } + + if ok, _ := c.IsPanorama(); ok { + log.Printf("Connected to Panorama, so templates and device groups are going to be created") + checkTemplate(c, ctx) + checkTemplateStack(c, ctx) + checkDeviceGroup(c, ctx) + } else { + log.Printf("Connected to firewall, so templates and device groups are not going to be created") + } + + // CHECKS + checkVr(c, ctx) + checkZone(c, ctx) + checkInterfaceMgmtProfile(c, ctx) + checkEthernetLayer3Static(c, ctx) + checkEthernetLayer3Dhcp(c, ctx) + checkEthernetHa(c, ctx) + checkLoopback(c, ctx) + checkVrZoneWithEthernet(c, ctx) + checkSecurityPolicyRules(c, ctx) + checkSecurityPolicyRulesMove(c, ctx) + checkSharedObjects(c, ctx) + checkTag(c, ctx) + checkAddress(c, ctx) + checkService(c, ctx) + checkNtp(c, ctx) + checkDns(c, ctx) +} + +func checkTemplate(c *pango.Client, ctx context.Context) { + entry := template.Entry{ + Name: "codegen_template", + Description: util.String("This is a template created by codegen."), + DefaultVsys: util.String("vsys1"), + Config: &template.Config{ + Devices: []template.ConfigDevices{ + { + Name: "localhost.localdomain", + Vsys: []template.ConfigDevicesVsys{ + { + Name: "vsys1", + }, + }, + }, + }, + }, + } + + location := template.NewPanoramaLocation() + api := template.NewService(c) + + reply, err := api.Create(ctx, *location, entry) + if err != nil { + log.Printf("Failed to create template: %s", err) + return + } + log.Printf("Template %s created\n", reply.Name) +} + +func checkTemplateStack(c *pango.Client, ctx context.Context) { + entry := template_stack.Entry{ + Name: "codegen_template_stack", + Description: util.String("This is a template stack created by codegen."), + Templates: []string{"codegen_template"}, + DefaultVsys: util.String("vsys1"), + } + + location := template_stack.NewPanoramaLocation() + api := template_stack.NewService(c) + + reply, err := api.Create(ctx, *location, entry) + if err != nil { + log.Printf("Failed to create template stack: %s", err) + return + } + log.Printf("Template stack %s created\n", reply.Name) +} + +func checkDeviceGroup(c *pango.Client, ctx context.Context) { + entry := device_group.Entry{ + Name: "codegen_device_group", + Description: util.String("This is a device group created by codegen."), + Templates: []string{"codegen_template"}, + } + + location := device_group.NewPanoramaLocation() + + api := device_group.NewService(c) + + reply, err := api.Create(ctx, *location, entry) + if err != nil { + log.Printf("Failed to create device group: %s", err) + return + } + log.Printf("Device group %s created\n", reply.Name) +} + +func checkSharedObjects(c *pango.Client, ctx context.Context) { + if ok, _ := c.IsPanorama(); ok { + addressObject := address.Entry{ + Name: "codegen_address_shared1", + Description: util.String("This is a shared address created by codegen."), + IpNetmask: util.String("1.2.3.4"), + } + + addressLocation := address.Location{ + Shared: true, + } + addressApi := address.NewService(c) + addressReply, err := addressApi.Create(ctx, addressLocation, addressObject) + if err != nil { + log.Printf("Failed to create object: %s", err) + return + } + log.Printf("Address '%s=%s' created", addressReply.Name, *addressReply.IpNetmask) + + err = addressApi.Delete(ctx, addressLocation, addressReply.Name) + if err != nil { + log.Printf("Failed to delete object: %s", err) + return + } + log.Printf("Address '%s' deleted", addressReply.Name) + } +} + +func checkVr(c *pango.Client, ctx context.Context) { + entry := virtual_router.Entry{ + Name: "codegen_vr", + Protocol: &virtual_router.Protocol{ + Bgp: &virtual_router.ProtocolBgp{ + Enable: util.Bool(false), + }, + Ospf: &virtual_router.ProtocolOspf{ + Enable: util.Bool(false), + }, + Ospfv3: &virtual_router.ProtocolOspfv3{ + Enable: util.Bool(false), + }, + Rip: &virtual_router.ProtocolRip{ + Enable: util.Bool(false), + }, + }, + RoutingTable: &virtual_router.RoutingTable{ + // Ip: &virtual_router.RoutingTableIp{ + // StaticRoutes: []virtual_router.RoutingTableIpStaticRoutes{ + // { + // Name: "default", + // Destination: util.String("0.0.0.0/0"), + // Interface: util.String("ethernet1/2"), + // NextHop: &virtual_router.RoutingTableIpStaticRoutesNextHop{ + // IpAddress: util.String("1.1.1.1"), + // }, + // Metric: util.Int(64), + // AdminDist: util.Int(120), + // }, + // }, + // }, + // Ipv6: &virtual_router.RoutingTableIpv6{ + // StaticRoutes: []virtual_router.RoutingTableIpv6StaticRoutes{ + // { + // Name: "default", + // Destination: util.String("0.0.0.0/0"), + // NextHop: &virtual_router.RoutingTableIpv6StaticRoutesNextHop{ + // Ipv6Address: util.String("2001:0000:130F:0000:0000:09C0:876A:230D"), + // }, + // Metric: util.Int(24), + // AdminDist: util.Int(20), + // }, + // }, + // }, + }, + Ecmp: &virtual_router.Ecmp{ + Enable: util.Bool(true), + SymmetricReturn: util.Bool(true), + MaxPaths: util.Int(3), + Algorithm: &virtual_router.EcmpAlgorithm{ + // IpHash: &virtual_router.EcmpAlgorithmIpHash{ + // HashSeed: util.Int(1234), + // UsePort: util.Bool(true), + // SrcOnly: util.Bool(true), + // }, + // WeightedRoundRobin: &virtual_router.EcmpAlgorithmWeightedRoundRobin{ + // Interfaces: []virtual_router.EcmpAlgorithmWeightedRoundRobinInterfaces{ + // { + // Name: "ethernet1/2", + // Weight: util.Int(1), + // }, + // { + // Name: "ethernet1/3", + // Weight: util.Int(2), + // }, + // }, + // }, + }, + }, + AdministrativeDistances: &virtual_router.AdministrativeDistances{ + OspfInt: util.Int(77), + OspfExt: util.Int(88), + }, + } + var location *virtual_router.Location + if ok, _ := c.IsPanorama(); ok { + location = virtual_router.NewTemplateLocation() + location.Template.Template = "codegen_template" + } else { + location = virtual_router.NewNgfwLocation() + } + api := virtual_router.NewService(c) + + reply, err := api.Create(ctx, *location, entry) + if err != nil { + log.Printf("Failed to create VR: %s", err) + return + } + log.Printf("VR %s created\n", reply.Name) +} + +func checkEthernetLayer3Static(c *pango.Client, ctx context.Context) { + entry := ethernet.Entry{ + Name: "ethernet1/2", + Comment: util.String("This is a ethernet1/2"), + Layer3: ðernet.Layer3{ + NdpProxy: util.Bool(true), + Lldp: util.Bool(true), + AdjustTcpMss: ðernet.Layer3AdjustTcpMss{ + Enable: util.Bool(true), + Ipv4MssAdjustment: util.Int(250), + Ipv6MssAdjustment: util.Int(250), + }, + Mtu: util.Int(1280), + Ips: []string{"11.11.11.11", "22.22.22.22"}, + Ipv6: ðernet.Layer3Ipv6{ + Addresses: []ethernet.Layer3Ipv6Addresses{ + { + EnableOnInterface: util.Bool(false), + Name: "2001:0000:130F:0000:0000:09C0:876A:230B", + }, + { + EnableOnInterface: util.Bool(true), + Name: "2001:0000:130F:0000:0000:09C0:876A:230C", + }, + }, + }, + InterfaceManagementProfile: util.String("codegen_mgmt_profile"), + }, + } + var location *ethernet.Location + if ok, _ := c.IsPanorama(); ok { + location = ethernet.NewTemplateLocation() + location.Template.Template = "codegen_template" + } else { + location = ethernet.NewNgfwLocation() + } + api := ethernet.NewService(c) + + reply, err := api.Create(ctx, *location, entry) + if err != nil { + log.Printf("Failed to create ethernet: %s", err) + return + } + log.Printf("Ethernet layer3 %s created\n", reply.Name) +} + +func checkEthernetLayer3Dhcp(c *pango.Client, ctx context.Context) { + entry := ethernet.Entry{ + Name: "ethernet1/3", + Comment: util.String("This is a ethernet1/3"), + Layer3: ðernet.Layer3{ + InterfaceManagementProfile: util.String("codegen_mgmt_profile"), + DhcpClient: ðernet.Layer3DhcpClient{ + CreateDefaultRoute: util.Bool(false), + DefaultRouteMetric: util.Int(64), + Enable: util.Bool(true), + SendHostname: ðernet.Layer3DhcpClientSendHostname{ + Enable: util.Bool(true), + Hostname: util.String("codegen_dhcp"), + }, + }, + }, + } + var location *ethernet.Location + if ok, _ := c.IsPanorama(); ok { + location = ethernet.NewTemplateLocation() + location.Template.Template = "codegen_template" + } else { + location = ethernet.NewNgfwLocation() + } + api := ethernet.NewService(c) + + reply, err := api.Create(ctx, *location, entry) + if err != nil { + log.Printf("Failed to create ethernet: %s", err) + return + } + log.Printf("Ethernet layer3 %s created\n", reply.Name) +} + +func checkEthernetHa(c *pango.Client, ctx context.Context) { + entry := ethernet.Entry{ + Name: "ethernet1/10", + Comment: util.String("This is a ethernet1/10"), + Ha: ðernet.Ha{}, + } + var location *ethernet.Location + if ok, _ := c.IsPanorama(); ok { + location = ethernet.NewTemplateLocation() + location.Template.Template = "codegen_template" + } else { + location = ethernet.NewNgfwLocation() + } + api := ethernet.NewService(c) + + reply, err := api.Create(ctx, *location, entry) + if err != nil { + log.Printf("Failed to create ethernet: %s", err) + return + } + log.Printf("Ethernet HA %s created\n", reply.Name) +} + +func checkLoopback(c *pango.Client, ctx context.Context) { + entry := loopback.Entry{ + Name: "loopback.123", + AdjustTcpMss: &loopback.AdjustTcpMss{ + Enable: util.Bool(true), + Ipv4MssAdjustment: util.Int(250), + Ipv6MssAdjustment: util.Int(250), + }, + Comment: util.String("This is a loopback entry"), + Mtu: util.Int(1280), + Ips: []string{"1.1.1.1", "2.2.2.2"}, + Ipv6: &loopback.Ipv6{ + Addresses: []loopback.Ipv6Addresses{ + { + EnableOnInterface: util.Bool(false), + Name: "2001:0000:130F:0000:0000:09C0:876A:130B", + }, + { + EnableOnInterface: util.Bool(true), + Name: "2001:0000:130F:0000:0000:09C0:876A:130C", + }, + }, + }, + InterfaceManagementProfile: util.String("codegen_mgmt_profile"), + } + var location *loopback.Location + if ok, _ := c.IsPanorama(); ok { + location = loopback.NewTemplateLocation() + location.Template.Template = "codegen_template" + } else { + location = loopback.NewNgfwLocation() + } + api := loopback.NewService(c) + + reply, err := api.Create(ctx, *location, entry) + if err != nil { + log.Printf("Failed to create loopback: %s", err) + return + } + log.Printf("Loopback %s created\n", reply.Name) +} + +func checkZone(c *pango.Client, ctx context.Context) { + entry := zone.Entry{ + Name: "codegen_zone", + EnableUserIdentification: util.Bool(true), + Network: &zone.Network{ + EnablePacketBufferProtection: util.Bool(false), + Layer3: []string{}, + }, + DeviceAcl: &zone.DeviceAcl{ + IncludeList: []string{"1.2.3.4"}, + }, + UserAcl: &zone.UserAcl{ + ExcludeList: []string{"1.2.3.4"}, + }, + } + var location *zone.Location + if ok, _ := c.IsPanorama(); ok { + location = zone.NewTemplateLocation() + location.Template.Template = "codegen_template" + } else { + location = zone.NewVsysLocation() + } + api := zone.NewService(c) + + reply, err := api.Create(ctx, *location, entry) + if err != nil { + log.Printf("Failed to create zone: %s", err) + return + } + log.Printf("Zone %s created\n", reply.Name) +} + +func checkInterfaceMgmtProfile(c *pango.Client, ctx context.Context) { + entry := interface_management.Entry{ + Name: "codegen_mgmt_profile", + Http: util.Bool(true), + Ping: util.Bool(true), + PermittedIps: []string{"1.1.1.1", "2.2.2.2"}, + } + var location *interface_management.Location + if ok, _ := c.IsPanorama(); ok { + location = interface_management.NewTemplateLocation() + location.Template.Template = "codegen_template" + } else { + location = interface_management.NewNgfwLocation() + } + api := interface_management.NewService(c) + + reply, err := api.Create(ctx, *location, entry) + if err != nil { + log.Printf("Failed to create interface management profile: %s", err) + return + } + log.Printf("Interface management profile %s created\n", reply.Name) +} + +func checkVrZoneWithEthernet(c *pango.Client, ctx context.Context) { + // UPDATE VR ABOUT INTERFACES + var locationVr *virtual_router.Location + if ok, _ := c.IsPanorama(); ok { + locationVr = virtual_router.NewTemplateLocation() + locationVr.Template.Template = "codegen_template" + } else { + locationVr = virtual_router.NewNgfwLocation() + } + apiVr := virtual_router.NewService(c) + + replyVr, err := apiVr.Read(ctx, *locationVr, "codegen_vr", "get") + if err != nil { + log.Printf("Failed to read VR: %s", err) + return + } + log.Printf("VR %s read\n", replyVr.Name) + + replyVr.Interfaces = []string{"ethernet1/2", "ethernet1/3"} + + replyVr, err = apiVr.Update(ctx, *locationVr, *replyVr, "codegen_vr") + if err != nil { + log.Printf("Failed to update VR: %s", err) + return + } + log.Printf("VR %s updated with %s\n", replyVr.Name, replyVr.Interfaces) + + // UPDATE ZONE ABOUT INTERFACES + var locationZone *zone.Location + if ok, _ := c.IsPanorama(); ok { + locationZone = zone.NewTemplateLocation() + locationZone.Template.Template = "codegen_template" + } else { + locationZone = zone.NewVsysLocation() + } + apiZone := zone.NewService(c) + + replyZone, err := apiZone.Read(ctx, *locationZone, "codegen_zone", "get") + if err != nil { + log.Printf("Failed to read zone: %s", err) + return + } + log.Printf("Zone %s read\n", replyZone.Name) + + replyZone.Network = &zone.Network{ + EnablePacketBufferProtection: util.Bool(false), + Layer3: []string{"ethernet1/2", "ethernet1/3"}, + } + + replyZone, err = apiZone.Update(ctx, *locationZone, *replyZone, "codegen_zone") + if err != nil { + log.Printf("Failed to update zone: %s", err) + return + } + log.Printf("Zone %s updated with %s\n", replyZone.Name, replyZone.Network.Layer3) + + // DELETE INTERFACES FROM VR + replyVr.Interfaces = []string{} + + replyVr, err = apiVr.Update(ctx, *locationVr, *replyVr, "codegen_vr") + if err != nil { + log.Printf("Failed to update VR: %s", err) + return + } + log.Printf("VR %s updated with %s\n", replyVr.Name, replyVr.Interfaces) + + // DELETE INTERFACES FROM ZONE + replyZone.Network = &zone.Network{ + EnablePacketBufferProtection: util.Bool(false), + Layer3: []string{}, + } + + replyZone, err = apiZone.Update(ctx, *locationZone, *replyZone, "codegen_zone") + if err != nil { + log.Printf("Failed to update zone: %s", err) + return + } + log.Printf("Zone %s updated with %s\n", replyZone.Name, replyZone.Network.Layer3) + + // DELETE INTERFACES + var ethernetLocation *ethernet.Location + if ok, _ := c.IsPanorama(); ok { + ethernetLocation = ethernet.NewTemplateLocation() + ethernetLocation.Template.Template = "codegen_template" + } else { + ethernetLocation = ethernet.NewNgfwLocation() + } + api := ethernet.NewService(c) + + interfacesToDelete := []string{"ethernet1/2", "ethernet1/3"} + for _, iface := range interfacesToDelete { + err = api.Delete(ctx, *ethernetLocation, iface) + if err != nil { + log.Printf("Failed to delete ethernet: %s", err) + return + } + log.Printf("Ethernet %s deleted\n", iface) + } +} + +func checkSecurityPolicyRules(c *pango.Client, ctx context.Context) { + // SECURITY POLICY RULE - ADD + securityPolicyRuleEntry := security.Entry{ + Name: "codegen_rule", + Description: util.String("initial description"), + Action: util.String("allow"), + SourceZones: []string{"any"}, + SourceAddresses: []string{"any"}, + DestinationZones: []string{"any"}, + DestinationAddresses: []string{"any"}, + Applications: []string{"any"}, + Services: []string{"application-default"}, + } + + var securityPolicyRuleLocation *security.Location + if ok, _ := c.IsPanorama(); ok { + securityPolicyRuleLocation = security.NewDeviceGroupLocation() + securityPolicyRuleLocation.DeviceGroup.DeviceGroup = "codegen_device_group" + } else { + securityPolicyRuleLocation = security.NewVsysLocation() + } + + securityPolicyRuleApi := security.NewService(c) + securityPolicyRuleReply, err := securityPolicyRuleApi.Create(ctx, *securityPolicyRuleLocation, securityPolicyRuleEntry) + if err != nil { + log.Printf("Failed to create security policy rule: %s", err) + return + } + log.Printf("Security policy rule '%s:%s' with description '%s' created", *securityPolicyRuleReply.Uuid, securityPolicyRuleReply.Name, *securityPolicyRuleReply.Description) + + // SECURITY POLICY RULE - READ + securityPolicyRuleReply, err = securityPolicyRuleApi.Read(ctx, *securityPolicyRuleLocation, securityPolicyRuleReply.Name, "get") + if err != nil { + log.Printf("Failed to update security policy rule: %s", err) + return + } + log.Printf("Security policy rule '%s:%s' with description '%s' read", *securityPolicyRuleReply.Uuid, securityPolicyRuleReply.Name, *securityPolicyRuleReply.Description) + + // SECURITY POLICY RULE - UPDATE + securityPolicyRuleEntry.Description = util.String("changed description") + securityPolicyRuleReply, err = securityPolicyRuleApi.Update(ctx, *securityPolicyRuleLocation, securityPolicyRuleEntry, securityPolicyRuleReply.Name) + if err != nil { + log.Printf("Failed to update security policy rule: %s", err) + return + } + log.Printf("Security policy rule '%s:%s' with description '%s' updated", *securityPolicyRuleReply.Uuid, securityPolicyRuleReply.Name, *securityPolicyRuleReply.Description) + + // SECURITY POLICY RULE - READ BY ID + securityPolicyRuleReply, err = securityPolicyRuleApi.ReadById(ctx, *securityPolicyRuleLocation, *securityPolicyRuleReply.Uuid, "get") + if err != nil { + log.Printf("Failed to update security policy rule: %s", err) + return + } + log.Printf("Security policy rule '%s:%s' with description '%s' read by id", *securityPolicyRuleReply.Uuid, securityPolicyRuleReply.Name, *securityPolicyRuleReply.Description) + + // SECURITY POLICY RULE - UPDATE 2 + securityPolicyRuleEntry.Description = util.String("changed by id description") + securityPolicyRuleReply, err = securityPolicyRuleApi.UpdateById(ctx, *securityPolicyRuleLocation, securityPolicyRuleEntry, *securityPolicyRuleReply.Uuid) + if err != nil { + log.Printf("Failed to update security policy rule: %s", err) + return + } + log.Printf("Security policy rule '%s:%s' with description '%s' updated", *securityPolicyRuleReply.Uuid, securityPolicyRuleReply.Name, *securityPolicyRuleReply.Description) + + // SECURITY POLICY RULE - HIT COUNT + if ok, _ := c.IsFirewall(); ok { + hitCount, err := securityPolicyRuleApi.HitCount(ctx, *securityPolicyRuleLocation, "test-policy") + if err != nil { + log.Printf("Failed to get hit count for security policy rule: %s", err) + return + } + if len(hitCount) > 0 { + log.Printf("Security policy rule '%d' hit count", hitCount[0].HitCount) + } else { + log.Printf("Security policy rule not found") + } + } + + // SECURITY POLICY RULE - AUDIT COMMENT + err = securityPolicyRuleApi.SetAuditComment(ctx, *securityPolicyRuleLocation, securityPolicyRuleReply.Name, "another audit comment") + if err != nil { + log.Printf("Failed to set audit comment for security policy rule: %s", err) + return + } + + comment, err := securityPolicyRuleApi.CurrentAuditComment(ctx, *securityPolicyRuleLocation, securityPolicyRuleEntry.Name) + if err != nil { + log.Printf("Failed to get audit comment for security policy rule: %s", err) + return + } + log.Printf("Security policy rule '%s:%s' current comment: '%s'", *securityPolicyRuleReply.Uuid, securityPolicyRuleReply.Name, comment) + + comments, err := securityPolicyRuleApi.AuditCommentHistory(ctx, *securityPolicyRuleLocation, securityPolicyRuleEntry.Name, "forward", 10, 0) + if err != nil { + log.Printf("Failed to get audit comments for security policy rule: %s", err) + return + } + for _, comment := range comments { + log.Printf("Security policy rule '%s:%s' comment history: '%s:%s'", *securityPolicyRuleReply.Uuid, securityPolicyRuleReply.Name, comment.Time, comment.Comment) + } + + // SECURITY POLICY RULE - DELETE + err = securityPolicyRuleApi.DeleteById(ctx, *securityPolicyRuleLocation, *securityPolicyRuleReply.Uuid) + if err != nil { + log.Printf("Failed to delete security policy rule: %s", err) + return + } + log.Printf("Security policy rule '%s' deleted", securityPolicyRuleReply.Name) + + // SECURITY POLICY RULE - FORCE ERROR WHILE DELETE + err = securityPolicyRuleApi.Delete(ctx, *securityPolicyRuleLocation, securityPolicyRuleReply.Name) + if err != nil { + log.Printf("Failed to delete security policy rule: %s", err) + } else { + log.Printf("Security policy rule '%s' deleted", securityPolicyRuleReply.Name) + } +} + +func checkSecurityPolicyRulesMove(c *pango.Client, ctx context.Context) { + // SECURITY POLICY RULE - MOVE GROUP + var securityPolicyRuleLocation *security.Location + if ok, _ := c.IsPanorama(); ok { + securityPolicyRuleLocation = security.NewDeviceGroupLocation() + securityPolicyRuleLocation.DeviceGroup.DeviceGroup = "codegen_device_group" + } else { + securityPolicyRuleLocation = security.NewVsysLocation() + } + + securityPolicyRuleApi := security.NewService(c) + + securityPolicyRulesNames := make([]string, 10) + var securityPolicyRulesEntries []security.Entry + for i := 0; i < 10; i++ { + securityPolicyRulesNames[i] = fmt.Sprintf("codegen_rule%d", i) + securityPolicyRuleItem := security.Entry{ + Name: securityPolicyRulesNames[i], + Description: util.String("initial description"), + Action: util.String("allow"), + SourceZones: []string{"any"}, + SourceAddresses: []string{"any"}, + DestinationZones: []string{"any"}, + DestinationAddresses: []string{"any"}, + Applications: []string{"any"}, + Services: []string{"application-default"}, + } + securityPolicyRulesEntries = append(securityPolicyRulesEntries, securityPolicyRuleItem) + securityPolicyRuleItemReply, err := securityPolicyRuleApi.Create(ctx, *securityPolicyRuleLocation, securityPolicyRuleItem) + if err != nil { + log.Printf("Failed to create security policy rule: %s", err) + return + } + log.Printf("Security policy rule '%s:%s' with description '%s' created", *securityPolicyRuleItemReply.Uuid, securityPolicyRuleItemReply.Name, *securityPolicyRuleItemReply.Description) + } + rulePositionBefore7 := rule.Position{ + First: nil, + Last: nil, + SomewhereBefore: nil, + DirectlyBefore: util.String("codegen_rule7"), + SomewhereAfter: nil, + DirectlyAfter: nil, + } + rulePositionBottom := rule.Position{ + First: nil, + Last: util.Bool(true), + SomewhereBefore: nil, + DirectlyBefore: nil, + SomewhereAfter: nil, + DirectlyAfter: nil, + } + var securityPolicyRulesEntriesToMove []security.Entry + securityPolicyRulesEntriesToMove = append(securityPolicyRulesEntriesToMove, securityPolicyRulesEntries[3]) + securityPolicyRulesEntriesToMove = append(securityPolicyRulesEntriesToMove, securityPolicyRulesEntries[5]) + for _, securityPolicyRuleItemToMove := range securityPolicyRulesEntriesToMove { + log.Printf("Security policy rule '%s' is going to be moved", securityPolicyRuleItemToMove.Name) + } + err := securityPolicyRuleApi.MoveGroup(ctx, *securityPolicyRuleLocation, rulePositionBefore7, securityPolicyRulesEntriesToMove) + if err != nil { + log.Printf("Failed to move security policy rules %v: %s", securityPolicyRulesEntriesToMove, err) + return + } + securityPolicyRulesEntriesToMove = []security.Entry{securityPolicyRulesEntries[1]} + for _, securityPolicyRuleItemToMove := range securityPolicyRulesEntriesToMove { + log.Printf("Security policy rule '%s' is going to be moved", securityPolicyRuleItemToMove.Name) + } + err = securityPolicyRuleApi.MoveGroup(ctx, *securityPolicyRuleLocation, rulePositionBottom, securityPolicyRulesEntriesToMove) + if err != nil { + log.Printf("Failed to move security policy rules %v: %s", securityPolicyRulesEntriesToMove, err) + return + } + err = securityPolicyRuleApi.Delete(ctx, *securityPolicyRuleLocation, securityPolicyRulesNames...) + if err != nil { + log.Printf("Failed to delete security policy rules %s: %s", securityPolicyRulesNames, err) + return + } +} + +func checkTag(c *pango.Client, ctx context.Context) { + // TAG - CREATE + tagColor := tag.ColorAzureBlue + tagObject := tag.Entry{ + Name: "codegen_color", + Color: &tagColor, + } + + var tagLocation *tag.Location + if ok, _ := c.IsPanorama(); ok { + tagLocation = tag.NewDeviceGroupLocation() + tagLocation.DeviceGroup.DeviceGroup = "codegen_device_group" + } else { + tagLocation = tag.NewSharedLocation() + } + + tagApi := tag.NewService(c) + tagReply, err := tagApi.Create(ctx, *tagLocation, tagObject) + if err != nil { + log.Printf("Failed to create object: %s", err) + return + } + log.Printf("Tag '%s' created", tagReply.Name) + + // TAG - DELETE + err = tagApi.Delete(ctx, *tagLocation, tagReply.Name) + if err != nil { + log.Printf("Failed to delete object: %s", err) + return + } + log.Printf("Tag '%s' deleted", tagReply.Name) +} + +func checkAddress(c *pango.Client, ctx context.Context) { + // ADDRESS - CREATE + addressObject := address.Entry{ + Name: "codegen_address_test1", + IpNetmask: util.String("12.13.14.25"), + } + + var addressLocation *address.Location + if ok, _ := c.IsPanorama(); ok { + addressLocation = address.NewDeviceGroupLocation() + addressLocation.DeviceGroup.DeviceGroup = "codegen_device_group" + } else { + addressLocation = address.NewSharedLocation() + } + + addressApi := address.NewService(c) + addressReply, err := addressApi.Create(ctx, *addressLocation, addressObject) + if err != nil { + log.Printf("Failed to create object: %s", err) + return + } + log.Printf("Address '%s=%s' created", addressReply.Name, *addressReply.IpNetmask) + + // ADDRESS - LIST + addresses, err := addressApi.List(ctx, *addressLocation, "get", "name starts-with 'codegen'", "'") + if err != nil { + log.Printf("Failed to list object: %s", err) + } else { + for index, item := range addresses { + log.Printf("Address %d: '%s'", index, item.Name) + } + } + + // ADDRESS - GROUP + addressGroupObject := address_group.Entry{ + Name: "codegen_address_group_test1", + Static: []string{addressReply.Name}, + } + + var addressGroupLocation *address_group.Location + if ok, _ := c.IsPanorama(); ok { + addressGroupLocation = address_group.NewDeviceGroupLocation() + addressGroupLocation.DeviceGroup.DeviceGroup = "codegen_device_group" + } else { + addressGroupLocation = address_group.NewSharedLocation() + } + + addressGroupApi := address_group.NewService(c) + addressGroupReply, err := addressGroupApi.Create(ctx, *addressGroupLocation, addressGroupObject) + if err != nil { + log.Printf("Failed to create object: %s", err) + return + } + log.Printf("Address group '%s' created", addressGroupReply.Name) + + // ADDRESS - GROUP - DELETE + err = addressGroupApi.Delete(ctx, *addressGroupLocation, addressGroupReply.Name) + if err != nil { + log.Printf("Failed to delete object: %s", err) + return + } + log.Printf("Address group '%s' deleted", addressGroupReply.Name) + + // ADDRESS - DELETE + err = addressApi.Delete(ctx, *addressLocation, addressReply.Name) + if err != nil { + log.Printf("Failed to delete object: %s", err) + return + } + log.Printf("Address '%s' deleted", addressReply.Name) +} + +func checkService(c *pango.Client, ctx context.Context) { + // SERVICE - ADD + serviceObject := service.Entry{ + Name: "codegen_service_test1", + Description: util.String("test description"), + Protocol: &service.Protocol{ + Tcp: &service.ProtocolTcp{ + DestinationPort: util.Int(8642), + Override: &service.ProtocolTcpOverride{ + HalfcloseTimeout: util.Int(124), + Timeout: util.Int(125), + TimewaitTimeout: util.Int(127), + }, + }, + }, + } + + var serviceLocation *service.Location + if ok, _ := c.IsPanorama(); ok { + serviceLocation = service.NewDeviceGroupLocation() + serviceLocation.DeviceGroup.DeviceGroup = "codegen_device_group" + } else { + serviceLocation = service.NewVsysLocation() + } + + serviceApi := service.NewService(c) + serviceReply, err := serviceApi.Create(ctx, *serviceLocation, serviceObject) + if err != nil { + log.Printf("Failed to create object: %s", err) + return + } + log.Printf("Service '%s=%d' created", serviceReply.Name, *serviceReply.Protocol.Tcp.DestinationPort) + + // SERVICE - UPDATE 1 + serviceObject.Description = util.String("changed description") + + serviceReply, err = serviceApi.Update(ctx, *serviceLocation, serviceObject, serviceReply.Name) + if err != nil { + log.Printf("Failed to update object: %s", err) + return + } + log.Printf("Service '%s=%d' updated", serviceReply.Name, *serviceReply.Protocol.Tcp.DestinationPort) + + // SERVICE - UPDATE 2 + serviceObject.Protocol.Tcp.DestinationPort = util.Int(1234) + + serviceReply, err = serviceApi.Update(ctx, *serviceLocation, serviceObject, serviceReply.Name) + if err != nil { + log.Printf("Failed to update object: %s", err) + return + } + log.Printf("Service '%s=%d' updated", serviceReply.Name, *serviceReply.Protocol.Tcp.DestinationPort) + + // SERVICE - RENAME + newServiceName := "codegen_service_test2" + serviceObject.Name = newServiceName + + serviceReply, err = serviceApi.Update(ctx, *serviceLocation, serviceObject, serviceReply.Name) + if err != nil { + log.Printf("Failed to update object: %s", err) + return + } + log.Printf("Service '%s=%d' renamed", serviceReply.Name, *serviceReply.Protocol.Tcp.DestinationPort) + + // SERVICE GROUP ADD + serviceGroupEntry := service_group.Entry{ + Name: "codegen_service_group_test1", + Members: []string{serviceReply.Name}, + } + + var serviceGroupLocation *service_group.Location + if ok, _ := c.IsPanorama(); ok { + serviceGroupLocation = service_group.NewDeviceGroupLocation() + serviceGroupLocation.DeviceGroup.DeviceGroup = "codegen_device_group" + } else { + serviceGroupLocation = service_group.NewVsysLocation() + } + + serviceGroupApi := service_group.NewService(c) + serviceGroupReply, err := serviceGroupApi.Create(ctx, *serviceGroupLocation, serviceGroupEntry) + if err != nil { + log.Printf("Failed to create object: %s", err) + return + } + log.Printf("Service group '%s' created", serviceGroupReply.Name) + + // SERVICE GROUP DELETE + err = serviceGroupApi.Delete(ctx, *serviceGroupLocation, serviceGroupReply.Name) + if err != nil { + log.Printf("Failed to delete object: %s", err) + return + } + log.Printf("Service group '%s' deleted", serviceGroupReply.Name) + + // SERVICE - LIST + //services, err := serviceApi.List(ctx, serviceLocation, "get", "name starts-with 'test'", "'") + services, err := serviceApi.List(ctx, *serviceLocation, "get", "", "") + if err != nil { + log.Printf("Failed to list object: %s", err) + } else { + for index, item := range services { + log.Printf("Service %d: '%s'", index, item.Name) + } + } + + // SERVICE - DELETE + err = serviceApi.Delete(ctx, *serviceLocation, newServiceName) + if err != nil { + log.Printf("Failed to delete object: %s", err) + return + } + log.Printf("Service '%s' deleted", newServiceName) + + // SERVICE - READ + serviceLocation = service.NewVsysLocation() + + serviceApi = service.NewService(c) + serviceReply, err = serviceApi.Read(ctx, *serviceLocation, "test", "get") + if err != nil { + log.Printf("Failed to read object: %s", err) + return + } + readDescription := "" + if serviceReply.Description != nil { + readDescription = *serviceReply.Description + } + keys := make([]string, 0, len(serviceReply.Misc)) + xmls := make([]string, 0, len(serviceReply.Misc)) + for key := range serviceReply.Misc { + keys = append(keys, key) + data, _ := xml.Marshal(serviceReply.Misc[key]) + xmls = append(xmls, string(data)) + } + log.Printf("Service '%s=%d, description: %s misc XML: %s, misc keys: %s' read", + serviceReply.Name, *serviceReply.Protocol.Tcp.DestinationPort, readDescription, xmls, keys) + + // SERVICE - UPDATE 3 + serviceReply.Description = util.String("some text changed now") + + serviceReply, err = serviceApi.Update(ctx, *serviceLocation, *serviceReply, "test") + if err != nil { + log.Printf("Failed to update object: %s", err) + return + } + readDescription = "" + if serviceReply.Description != nil { + readDescription = *serviceReply.Description + } + keys = make([]string, 0, len(serviceReply.Misc)) + xmls = make([]string, 0, len(serviceReply.Misc)) + for key := range serviceReply.Misc { + keys = append(keys, key) + data, _ := xml.Marshal(serviceReply.Misc[key]) + xmls = append(xmls, string(data)) + } + log.Printf("Service '%s=%d, description: %s misc XML: %s, misc keys: %s' update", + serviceReply.Name, *serviceReply.Protocol.Tcp.DestinationPort, readDescription, xmls, keys) +} + +func checkNtp(c *pango.Client, ctx context.Context) { + // NTP - ADD + ntpConfig := ntp.Config{ + NtpServers: &ntp.NtpServers{ + PrimaryNtpServer: &ntp.NtpServersPrimaryNtpServer{ + NtpServerAddress: util.String("11.12.13.14"), + }, + }, + } + + var ntpLocation *ntp.Location + if ok, _ := c.IsPanorama(); ok { + ntpLocation = ntp.NewTemplateLocation() + ntpLocation.Template.Template = "codegen_template" + } else { + ntpLocation = ntp.NewSystemLocation() + } + + ntpApi := ntp.NewService(c) + ntpReply, err := ntpApi.Create(ctx, *ntpLocation, ntpConfig) + if err != nil { + log.Printf("Failed to create NTP: %s", err) + return + } + log.Printf("NTP '%s' created", *ntpReply.NtpServers.PrimaryNtpServer.NtpServerAddress) + + // NTP - DELETE + err = ntpApi.Delete(ctx, *ntpLocation, ntpConfig) + if err != nil { + log.Printf("Failed to delete object: %s", err) + return + } + log.Print("NTP deleted") + return +} + +func checkDns(c *pango.Client, ctx context.Context) { + // DNS - ADD + dnsConfig := dns.Config{ + DnsSetting: &dns.DnsSetting{ + Servers: &dns.DnsSettingServers{ + Primary: util.String("8.8.8.8"), + Secondary: util.String("4.4.4.4"), + }, + }, + FqdnRefreshTime: util.Int(27), + } + + var dnsLocation *dns.Location + if ok, _ := c.IsPanorama(); ok { + dnsLocation = dns.NewTemplateLocation() + dnsLocation.Template.Template = "codegen_template" + } else { + dnsLocation = dns.NewSystemLocation() + } + + dnsApi := dns.NewService(c) + dnsReply, err := dnsApi.Create(ctx, *dnsLocation, dnsConfig) + if err != nil { + log.Printf("Failed to create DNS: %s", err) + return + } + log.Printf("DNS '%s, %s' created", *dnsReply.DnsSetting.Servers.Primary, *dnsReply.DnsSetting.Servers.Secondary) + + // DNS - DELETE + err = dnsApi.Delete(ctx, *dnsLocation, dnsConfig) + if err != nil { + log.Printf("Failed to delete object: %s", err) + return + } + log.Print("DNS deleted") +} diff --git a/filtering/parse.go b/filtering/parse.go index 45b5ce3..514ccbc 100644 --- a/filtering/parse.go +++ b/filtering/parse.go @@ -9,12 +9,12 @@ import ( ) func Parse(s string, quote string) (*Group, error) { - if len(quote) != 1 { + if s == "" { + return nil, nil + } else if len(quote) != 1 { return nil, fmt.Errorf("quote character should be one character") } else if quote == "&" || quote == "|" || quote == "(" || quote == ")" || quote == " " || quote == `\` || quote == "!" || quote == "." || quote == "<" || quote == ">" || quote == "=" || quote == "-" || quote == "_" { return nil, fmt.Errorf("quote character cannot be a reserved character") - } else if s == "" { - return nil, nil } var ch rune diff --git a/filtering/parse_test.go b/filtering/parse_test.go index 411b5db..0e38864 100644 --- a/filtering/parse_test.go +++ b/filtering/parse_test.go @@ -932,6 +932,7 @@ func TestParseReturnsError(t *testing.T) { "foobar", "(", ")", + "", } for _, s := range checks { @@ -965,12 +966,3 @@ func TestParseWithInvalidQuoteReturnsError(t *testing.T) { } } } - -func TestParseEmptyString(t *testing.T) { - resp, err := Parse("", `"`) - if err != nil { - t.Fatalf("parse empty string error: %s", err) - } else if resp != nil { - t.Fatalf("parse empty string has non-nil group") - } -} diff --git a/go.mod b/go.mod index 3866f35..8d42ce0 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,20 @@ module github.com/PaloAltoNetworks/pango -go 1.21 +go 1.22.5 + +require ( + github.com/onsi/ginkgo/v2 v2.19.0 + github.com/onsi/gomega v1.33.1 +) + +require ( + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + golang.org/x/tools v0.21.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1564c9b --- /dev/null +++ b/go.sum @@ -0,0 +1,23 @@ +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg= +github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= +github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= +github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= +github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= +golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/logging.go b/logging.go new file mode 100644 index 0000000..c3e71ee --- /dev/null +++ b/logging.go @@ -0,0 +1,196 @@ +package pango + +import ( + "fmt" + "log/slog" + "math/bits" +) + +// LogCategory is a bitmask describing what categories of logging to enable. +// +// Available bit-wise flags are as follows: +// +// - LogCategoryPango: Basic library-level logging +// - LogCategoryOp: Logging of operation commands (Op functions) +// - LogCategorySend: Logging of the data being sent to the server. All sensitive +// data will be scrubbed from the response unless LogCategorySensitive +// is explicitly added to the mask +// - LogCategoryReceive: Logging of the data being received from the server. All +// sensitive data will be scrubbed from the response unless LogCategorySensitive +// is explicitly added to the mask +// - LogCategoryCurl: When used along with LogCategorySend, an equivalent curl +// command will be logged +// - LogCategoryAll: A meta-category, enabling all above categories at once +// - LogCategorySensitive: Logging of sensitive data like hostnames, logins, +// passwords or API keys of any sort +type LogCategory uint + +const ( + LogCategoryPango LogCategory = 1 << iota + LogCategoryOp + LogCategorySend + LogCategoryReceive + LogCategoryCurl + LogCategoryAll = LogCategoryPango | LogCategoryOp | LogCategorySend | + LogCategoryReceive | LogCategoryCurl + // Make sure that LogCategorySensitive is always last, explicitly set to 1 << 32 + LogCategorySensitive LogCategory = 1 << 32 +) + +var logCategoryToString = map[LogCategory]string{ + LogCategoryPango: "pango", + LogCategoryOp: "op", + LogCategorySend: "send", + LogCategoryReceive: "receive", + LogCategoryCurl: "curl", + LogCategoryAll: "all", + LogCategorySensitive: "sensitive", +} + +func createStringToCategoryMap(categories map[LogCategory]string) map[string]LogCategory { + // Instead of keeping two maps for two way association, we + // just generate reversed map on the fly. This function is not + // going to be used outside of the initial library setup, so + // the slight performance penalty is not an issue. + stringsMap := make(map[string]LogCategory, len(logCategoryToString)) + for category, sym := range logCategoryToString { + stringsMap[sym] = category + } + + return stringsMap +} + +// LogCategoryFromStrings transforms list with categories into its bitmask equivalent. +// +// This function takes a list of strings, representing log categories +// that can be used to filter what gets logged by pango library. This list +// can change over time as more categories are added to the library. +// +// It returns LogCategory bitmask which can be then used to configure +// logger. If unknown log category string is given as part of the +// list, error is returned instead. +func LogCategoryFromStrings(symbols []string) (LogCategory, error) { + stringsMap := createStringToCategoryMap(logCategoryToString) + + var logCategoriesMask LogCategory + for _, elt := range symbols { + category, ok := stringsMap[elt] + if !ok { + return 0, fmt.Errorf("unknown log category: %s", elt) + } + + logCategoriesMask |= category + slog.Info("logCategoriesMask", "equal", logCategoriesMask) + } + return logCategoriesMask, nil +} + +// LogCategoryAsStrings interprets given LogCategory bitmask into its string representation. +// +// This function takes LogCategory bitmask as argument, and converts +// it into a list of strings, where each element represents a single +// category. LogCategoryAll is converted into a list of enabled +// categories, without "all". +// +// It returns a list of categories as strings, or error if invalid +// LogCategory mask has been provided. +func LogCategoryAsStrings(categories LogCategory) ([]string, error) { + symbols := make([]string, 0) + + // Calculate a number of high bits in the categories mask, to make + // sure all categories other than LogCategoryAll have been matched. + highBits := bits.OnesCount(uint(categories)) + + // Iterate over all available log categories, skipping + // LogCategoryAll as we can't distinguish between explicitly + // ORing all LogCategories and using LogCategoryAll. + for key, value := range logCategoryToString { + if key == LogCategoryAll { + continue + } + if categories&key == key { + symbols = append(symbols, value) + } + } + + // Return an error if number of high bits in the categories + // mask is lower than length of the symbols list + if len(symbols) < highBits && (categories&LogCategoryAll != LogCategoryAll) { + return nil, fmt.Errorf("invalid LogCategory bitmask") + } + + return symbols, nil +} + +// LogCategoryToSymbol returns string representation of the given LogCategory +// +// The given LogCategory can only have single bit set high, and cannot +// match LogCategoryAll. To convert LogCategory bitmask into a list of categories, +// use LogCategoryToStrings instead. +// +// It returns string representation of the log category, or error if +// unknown category has been provided. +func LogCategoryToString(category LogCategory) (string, error) { + if category&LogCategoryAll == LogCategoryAll { + return "", fmt.Errorf("cannot convert LogCategoryAll into a category string.") + } + symbol, ok := logCategoryToString[category] + if ok { + return symbol, nil + } + + return "", fmt.Errorf("unknown LogCategory: %d", category) +} + +// StringToLogCategory returns LogCategory mask matching given string category. +// +// The given string should be a single category, and not "all". To convert "all" +// into a list of enabled log categories, use LogCategoryFromStrings. +// +// It returns LogCategory representation of the given category string, or en +// error if either "all" or unknown string has been given. +func StringToLogCategory(sym string) (LogCategory, error) { + if sym == logCategoryToString[LogCategoryAll] { + return 0, fmt.Errorf("cannot convert \"all\" category string into LogCategory") + } + for key, value := range logCategoryToString { + if value == sym { + return key, nil + } + } + + return 0, fmt.Errorf("Unknown logging symbol: %s", sym) +} + +type categoryLogger struct { + logger *slog.Logger + discardLogger *slog.Logger + categories LogCategory +} + +func newCategoryLogger(logger *slog.Logger, categories LogCategory) *categoryLogger { + return &categoryLogger{ + logger: logger, + discardLogger: slog.New(discardHandler{}), + categories: categories, + } +} + +func (l *categoryLogger) WithLogCategory(category LogCategory) *slog.Logger { + matched, ok := logCategoryToString[category] + + // If the category cannot be matched, instead of returning + // error we use "unknown" instead. + if !ok { + matched = "unknown" + } + + if l.categories&category == category { + return l.logger.WithGroup(matched) + } + return l.discardLogger.WithGroup(matched) +} + +func (l *categoryLogger) enabledFor(category LogCategory) bool { + return l.categories&category == category +} diff --git a/logging_test.go b/logging_test.go new file mode 100644 index 0000000..b48a73e --- /dev/null +++ b/logging_test.go @@ -0,0 +1,90 @@ +package pango + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("While configuring logging", func() { + When("converting category strings into LogCategory bitmask", func() { + Context("with unknown string category", func() { + It("should fail with error message", func() { + categories := []string{"invalid"} + _, err := LogCategoryFromStrings(categories) + Expect(err).Should(HaveOccurred()) + }) + }) + + Context("with \"all\" string present", func() { + It("should return LogCategoryMask without LogCategorySensitive bit set", func() { + categories := []string{"all"} + categoriesMask, err := LogCategoryFromStrings(categories) + Expect(err).Should(Succeed()) + Expect(categoriesMask & LogCategorySensitive).ShouldNot(Equal(LogCategorySensitive)) + }) + }) + + Context("with \"sensitive\" string present", func() { + It("should return LogCategoryMask with LogCategorySensitive bit set", func() { + categories := []string{"receive", "sensitive"} + categoriesMask, err := LogCategoryFromStrings(categories) + Expect(err).Should(Succeed()) + expectedMask := LogCategoryReceive | LogCategorySensitive + Expect(categoriesMask).To(Equal(expectedMask)) + }) + }) + }) + + When("converting LogCategory bitmask into category strings", func() { + Context("with invalid bitmask set", func() { + It("should return error about LogCategoty bitmask being invalid", func() { + categories := LogCategory(1 << 31) + _, err := LogCategoryAsStrings(categories) + Expect(err).Should(HaveOccurred()) + }) + }) + + Context("with valid bitmask without LogCategorySensitive", func() { + It("the list should not \"sensitive\" category", func() { + categories := LogCategoryReceive | LogCategorySend + result, err := LogCategoryAsStrings(categories) + Expect(err).ShouldNot(HaveOccurred()) + Expect(result).Should(ContainElements([]string{"send", "receive"})) + Expect(result).ShouldNot(ContainElement("sensitive")) + }) + }) + + Context("with bitmask set to LogCategoryAll", func() { + var categories []string + BeforeEach(func() { + for _, v := range logCategoryToString { + if v != "all" && v != "sensitive" { + categories = append(categories, v) + } + + } + }) + + It("the list should not contain \"all\" category", func() { + result, err := LogCategoryAsStrings(LogCategoryAll) + Expect(err).ShouldNot(HaveOccurred()) + Expect(result).ShouldNot(ContainElement("all")) + }) + + It("the list should not contain \"sensitive\" category", func() { + result, err := LogCategoryAsStrings(LogCategoryAll) + Expect(err).ShouldNot(HaveOccurred()) + Expect(result).ShouldNot(ContainElement("sensitive")) + }) + }) + + Context("with explicitly added LogCategorySensitive", func() { + It("should have \"sensitive\" element", func() { + result, err := LogCategoryAsStrings(LogCategoryCurl | LogCategorySensitive) + Expect(err).ShouldNot(HaveOccurred()) + Expect(result).To(HaveLen(2)) + Expect(result).Should(ContainElements([]string{"curl", "sensitive"})) + }) + }) + }) +}) diff --git a/network/interface/ethernet/entry.go b/network/interface/ethernet/entry.go new file mode 100644 index 0000000..483db0c --- /dev/null +++ b/network/interface/ethernet/entry.go @@ -0,0 +1,1823 @@ +package ethernet + +import ( + "encoding/xml" + "fmt" + + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/generic" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +var ( + _ filtering.Fielder = &Entry{} +) + +var ( + Suffix = []string{"network", "interface", "ethernet"} +) + +type Entry struct { + Name string + Comment *string + LinkDuplex *string + LinkSpeed *string + LinkState *string + Poe *Poe + Ha *Ha + Layer3 *Layer3 + Tap *Tap + + Misc map[string][]generic.Xml +} + +type Ha struct { +} +type Layer3 struct { + AdjustTcpMss *Layer3AdjustTcpMss + Arp []Layer3Arp + Bonjour *Layer3Bonjour + DhcpClient *Layer3DhcpClient + InterfaceManagementProfile *string + Ips []Layer3Ips + Ipv6 *Layer3Ipv6 + Lldp *Layer3Lldp + Mtu *int64 + NdpProxy *bool + NetflowProfile *string + SdwanLinkSettings *Layer3SdwanLinkSettings + UntaggedSubInterface *bool +} +type Layer3AdjustTcpMss struct { + Enable *bool + Ipv4MssAdjustment *int64 + Ipv6MssAdjustment *int64 +} +type Layer3Arp struct { + HwAddress *string + Name string +} +type Layer3Bonjour struct { + Enable *bool +} +type Layer3DhcpClient struct { + CreateDefaultRoute *bool + DefaultRouteMetric *int64 + Enable *bool + SendHostname *Layer3DhcpClientSendHostname +} +type Layer3DhcpClientSendHostname struct { + Enable *bool + Hostname *string +} +type Layer3Ips struct { + Name string + SdwanGateway *string +} +type Layer3Ipv6 struct { + Addresses []Layer3Ipv6Addresses + DnsServer *Layer3Ipv6DnsServer + Enabled *bool + InterfaceId *string + NeighborDiscovery *Layer3Ipv6NeighborDiscovery +} +type Layer3Ipv6Addresses struct { + Advertise *Layer3Ipv6AddressesAdvertise + Anycast *string + EnableOnInterface *bool + Name string + Prefix *string +} +type Layer3Ipv6AddressesAdvertise struct { + AutoConfigFlag *bool + Enable *bool + OnlinkFlag *bool + PreferredLifetime *string + ValidLifetime *string +} +type Layer3Ipv6DnsServer struct { + DnsSupport *Layer3Ipv6DnsServerDnsSupport + Enable *bool + Source *Layer3Ipv6DnsServerSource +} +type Layer3Ipv6DnsServerDnsSupport struct { + Enable *bool + Server []Layer3Ipv6DnsServerDnsSupportServer + Suffix []Layer3Ipv6DnsServerDnsSupportSuffix +} +type Layer3Ipv6DnsServerDnsSupportServer struct { + Lifetime *int64 + Name string +} +type Layer3Ipv6DnsServerDnsSupportSuffix struct { + Lifetime *int64 + Name string +} +type Layer3Ipv6DnsServerSource struct { + Dhcpv6 *Layer3Ipv6DnsServerSourceDhcpv6 + Manual *Layer3Ipv6DnsServerSourceManual +} +type Layer3Ipv6DnsServerSourceDhcpv6 struct { + PrefixPool *string +} +type Layer3Ipv6DnsServerSourceManual struct { + Suffix []Layer3Ipv6DnsServerSourceManualSuffix +} +type Layer3Ipv6DnsServerSourceManualSuffix struct { + Lifetime *int64 + Name string +} +type Layer3Ipv6NeighborDiscovery struct { + DadAttempts *int64 + EnableDad *bool + EnableNdpMonitor *bool + Neighbor []Layer3Ipv6NeighborDiscoveryNeighbor + NsInterval *int64 + ReachableTime *int64 + RouterAdvertisement *Layer3Ipv6NeighborDiscoveryRouterAdvertisement +} +type Layer3Ipv6NeighborDiscoveryNeighbor struct { + HwAddress *string + Name string +} +type Layer3Ipv6NeighborDiscoveryRouterAdvertisement struct { + Enable *bool + EnableConsistencyCheck *bool + HopLimit *string + Lifetime *int64 + LinkMtu *string + ManagedFlag *bool + MaxInterval *int64 + MinInterval *int64 + OtherFlag *bool + ReachableTime *string + RetransmissionTimer *string + RouterPreference *string +} +type Layer3Lldp struct { + Enable *bool + Profile *string +} +type Layer3SdwanLinkSettings struct { + Enable *bool + SdwanInterfaceProfile *string + UpstreamNat *Layer3SdwanLinkSettingsUpstreamNat +} +type Layer3SdwanLinkSettingsUpstreamNat struct { + Enable *bool + StaticIp *string +} +type Poe struct { + Enabled *bool + ReservedPower *int64 +} +type Tap struct { + NetflowProfile *string +} + +type entryXmlContainer struct { + Answer []entryXml `xml:"entry"` +} + +type entryXml struct { + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + Comment *string `xml:"comment,omitempty"` + LinkDuplex *string `xml:"link-duplex,omitempty"` + LinkSpeed *string `xml:"link-speed,omitempty"` + LinkState *string `xml:"link-state,omitempty"` + Poe *PoeXml + Ha *HaXml `xml:"ha,omitempty"` + Layer3 *Layer3Xml `xml:"layer3,omitempty"` + Tap *TapXml `xml:"tap,omitempty"` + + Misc []generic.Xml `xml:",any"` +} + +type HaXml struct { + Misc []generic.Xml `xml:",any"` +} +type Layer3Xml struct { + AdjustTcpMss *Layer3AdjustTcpMssXml `xml:"adjust-tcp-mss,omitempty"` + Arp []Layer3ArpXml `xml:"arp>entry,omitempty"` + Bonjour *Layer3BonjourXml `xml:"bonjour,omitempty"` + DhcpClient *Layer3DhcpClientXml `xml:"dhcp-client,omitempty"` + InterfaceManagementProfile *string `xml:"interface-management-profile,omitempty"` + Ips []Layer3IpsXml `xml:"ip>entry,omitempty"` + Ipv6 *Layer3Ipv6Xml `xml:"ipv6,omitempty"` + Lldp *Layer3LldpXml `xml:"lldp,omitempty"` + Mtu *int64 `xml:"mtu,omitempty"` + NdpProxy *string `xml:"ndp-proxy>enabled,omitempty"` + NetflowProfile *string `xml:"netflow-profile,omitempty"` + SdwanLinkSettings *Layer3SdwanLinkSettingsXml `xml:"sdwan-link-settings,omitempty"` + UntaggedSubInterface *string `xml:"untagged-sub-interface,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type Layer3AdjustTcpMssXml struct { + Enable *string `xml:"enable,omitempty"` + Ipv4MssAdjustment *int64 `xml:"ipv4-mss-adjustment,omitempty"` + Ipv6MssAdjustment *int64 `xml:"ipv6-mss-adjustment,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type Layer3ArpXml struct { + HwAddress *string `xml:"hw-address,omitempty"` + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + + Misc []generic.Xml `xml:",any"` +} +type Layer3BonjourXml struct { + Enable *string + + Misc []generic.Xml `xml:",any"` +} +type Layer3DhcpClientXml struct { + CreateDefaultRoute *string `xml:"create-default-route,omitempty"` + DefaultRouteMetric *int64 `xml:"default-route-metric,omitempty"` + Enable *string `xml:"enable,omitempty"` + SendHostname *Layer3DhcpClientSendHostnameXml `xml:"send-hostname,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type Layer3DhcpClientSendHostnameXml struct { + Enable *string `xml:"enable,omitempty"` + Hostname *string `xml:"hostname,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type Layer3IpsXml struct { + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + SdwanGateway *string `xml:"sdwan-gateway,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type Layer3Ipv6Xml struct { + Addresses []Layer3Ipv6AddressesXml `xml:"address>entry,omitempty"` + DnsServer *Layer3Ipv6DnsServerXml `xml:"dns-server,omitempty"` + Enabled *string `xml:"enabled,omitempty"` + InterfaceId *string `xml:"interface-id,omitempty"` + NeighborDiscovery *Layer3Ipv6NeighborDiscoveryXml `xml:"neighbor-discovery,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type Layer3Ipv6AddressesXml struct { + Advertise *Layer3Ipv6AddressesAdvertiseXml `xml:"advertise,omitempty"` + Anycast *string `xml:"anycast,omitempty"` + EnableOnInterface *string `xml:"enable-on-interface,omitempty"` + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + Prefix *string `xml:"prefix,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type Layer3Ipv6AddressesAdvertiseXml struct { + AutoConfigFlag *string `xml:"auto-config-flag,omitempty"` + Enable *string `xml:"enable,omitempty"` + OnlinkFlag *string `xml:"onlink-flag,omitempty"` + PreferredLifetime *string `xml:"preferred-lifetime,omitempty"` + ValidLifetime *string `xml:"valid-lifetime,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type Layer3Ipv6DnsServerXml struct { + DnsSupport *Layer3Ipv6DnsServerDnsSupportXml `xml:"dns-support,omitempty"` + Enable *string `xml:"enable,omitempty"` + Source *Layer3Ipv6DnsServerSourceXml `xml:"source,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type Layer3Ipv6DnsServerDnsSupportXml struct { + Enable *string + Server []Layer3Ipv6DnsServerDnsSupportServerXml `xml:"server>entry,omitempty"` + Suffix []Layer3Ipv6DnsServerDnsSupportSuffixXml `xml:"suffix>entry,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type Layer3Ipv6DnsServerDnsSupportServerXml struct { + Lifetime *int64 + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + + Misc []generic.Xml `xml:",any"` +} +type Layer3Ipv6DnsServerDnsSupportSuffixXml struct { + Lifetime *int64 + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + + Misc []generic.Xml `xml:",any"` +} +type Layer3Ipv6DnsServerSourceXml struct { + Dhcpv6 *Layer3Ipv6DnsServerSourceDhcpv6Xml `xml:"dhcpv6,omitempty"` + Manual *Layer3Ipv6DnsServerSourceManualXml `xml:"manual,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type Layer3Ipv6DnsServerSourceDhcpv6Xml struct { + PrefixPool *string `xml:"prefix-pool,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type Layer3Ipv6DnsServerSourceManualXml struct { + Suffix []Layer3Ipv6DnsServerSourceManualSuffixXml `xml:"suffix>entry,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type Layer3Ipv6DnsServerSourceManualSuffixXml struct { + Lifetime *int64 `xml:"lifetime,omitempty"` + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + + Misc []generic.Xml `xml:",any"` +} +type Layer3Ipv6NeighborDiscoveryXml struct { + DadAttempts *int64 `xml:"dad-attempts,omitempty"` + EnableDad *string `xml:"enable-dad,omitempty"` + EnableNdpMonitor *string `xml:"enable-ndp-monitor,omitempty"` + Neighbor []Layer3Ipv6NeighborDiscoveryNeighborXml `xml:"neighbor>entry,omitempty"` + NsInterval *int64 `xml:"ns-interval,omitempty"` + ReachableTime *int64 `xml:"reachable-time,omitempty"` + RouterAdvertisement *Layer3Ipv6NeighborDiscoveryRouterAdvertisementXml `xml:"router-advertisement,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type Layer3Ipv6NeighborDiscoveryNeighborXml struct { + HwAddress *string `xml:"hw-address,omitempty"` + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + + Misc []generic.Xml `xml:",any"` +} +type Layer3Ipv6NeighborDiscoveryRouterAdvertisementXml struct { + Enable *string `xml:"enable,omitempty"` + EnableConsistencyCheck *string `xml:"enable-consistency-check,omitempty"` + HopLimit *string `xml:"hop-limit,omitempty"` + Lifetime *int64 `xml:"lifetime,omitempty"` + LinkMtu *string `xml:"link-mtu,omitempty"` + ManagedFlag *string `xml:"managed-flag,omitempty"` + MaxInterval *int64 `xml:"max-interval,omitempty"` + MinInterval *int64 `xml:"min-interval,omitempty"` + OtherFlag *string `xml:"other-flag,omitempty"` + ReachableTime *string `xml:"reachable-time,omitempty"` + RetransmissionTimer *string `xml:"retransmission-timer,omitempty"` + RouterPreference *string `xml:"router-preference,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type Layer3LldpXml struct { + Enable *string `xml:"enable,omitempty"` + Profile *string `xml:"profile,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type Layer3SdwanLinkSettingsXml struct { + Enable *string `xml:"enable,omitempty"` + SdwanInterfaceProfile *string `xml:"sdwan-interface-profile,omitempty"` + UpstreamNat *Layer3SdwanLinkSettingsUpstreamNatXml `xml:"upstream-nat,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type Layer3SdwanLinkSettingsUpstreamNatXml struct { + Enable *string `xml:"enable,omitempty"` + StaticIp *string `xml:"static-ip>ip-address,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type PoeXml struct { + Enabled *string `xml:"poe-enabled,omitempty"` + ReservedPower *int64 `xml:"poe-rsvd-pwr,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type TapXml struct { + NetflowProfile *string `xml:"netflow-profile,omitempty"` + + Misc []generic.Xml `xml:",any"` +} + +func (e *Entry) Field(v string) (any, error) { + if v == "name" || v == "Name" { + return e.Name, nil + } + if v == "comment" || v == "Comment" { + return e.Comment, nil + } + if v == "link_duplex" || v == "LinkDuplex" { + return e.LinkDuplex, nil + } + if v == "link_speed" || v == "LinkSpeed" { + return e.LinkSpeed, nil + } + if v == "link_state" || v == "LinkState" { + return e.LinkState, nil + } + if v == "poe" || v == "Poe" { + return e.Poe, nil + } + if v == "ha" || v == "Ha" { + return e.Ha, nil + } + if v == "layer3" || v == "Layer3" { + return e.Layer3, nil + } + if v == "tap" || v == "Tap" { + return e.Tap, nil + } + + return nil, fmt.Errorf("unknown field") +} + +func Versioning(vn version.Number) (Specifier, Normalizer, error) { + return specifyEntry, &entryXmlContainer{}, nil +} + +func specifyEntry(o *Entry) (any, error) { + entry := entryXml{} + + entry.Name = o.Name + entry.Comment = o.Comment + entry.LinkDuplex = o.LinkDuplex + entry.LinkSpeed = o.LinkSpeed + entry.LinkState = o.LinkState + var nestedPoe *PoeXml + if o.Poe != nil { + nestedPoe = &PoeXml{} + if _, ok := o.Misc["Poe"]; ok { + nestedPoe.Misc = o.Misc["Poe"] + } + if o.Poe.ReservedPower != nil { + nestedPoe.ReservedPower = o.Poe.ReservedPower + } + if o.Poe.Enabled != nil { + nestedPoe.Enabled = util.YesNo(o.Poe.Enabled, nil) + } + } + entry.Poe = nestedPoe + + var nestedHa *HaXml + if o.Ha != nil { + nestedHa = &HaXml{} + if _, ok := o.Misc["Ha"]; ok { + nestedHa.Misc = o.Misc["Ha"] + } + } + entry.Ha = nestedHa + + var nestedLayer3 *Layer3Xml + if o.Layer3 != nil { + nestedLayer3 = &Layer3Xml{} + if _, ok := o.Misc["Layer3"]; ok { + nestedLayer3.Misc = o.Misc["Layer3"] + } + if o.Layer3.Ips != nil { + nestedLayer3.Ips = []Layer3IpsXml{} + for _, oLayer3Ips := range o.Layer3.Ips { + nestedLayer3Ips := Layer3IpsXml{} + if _, ok := o.Misc["Layer3Ips"]; ok { + nestedLayer3Ips.Misc = o.Misc["Layer3Ips"] + } + if oLayer3Ips.SdwanGateway != nil { + nestedLayer3Ips.SdwanGateway = oLayer3Ips.SdwanGateway + } + if oLayer3Ips.Name != "" { + nestedLayer3Ips.Name = oLayer3Ips.Name + } + nestedLayer3.Ips = append(nestedLayer3.Ips, nestedLayer3Ips) + } + } + if o.Layer3.Ipv6 != nil { + nestedLayer3.Ipv6 = &Layer3Ipv6Xml{} + if _, ok := o.Misc["Layer3Ipv6"]; ok { + nestedLayer3.Ipv6.Misc = o.Misc["Layer3Ipv6"] + } + if o.Layer3.Ipv6.Addresses != nil { + nestedLayer3.Ipv6.Addresses = []Layer3Ipv6AddressesXml{} + for _, oLayer3Ipv6Addresses := range o.Layer3.Ipv6.Addresses { + nestedLayer3Ipv6Addresses := Layer3Ipv6AddressesXml{} + if _, ok := o.Misc["Layer3Ipv6Addresses"]; ok { + nestedLayer3Ipv6Addresses.Misc = o.Misc["Layer3Ipv6Addresses"] + } + if oLayer3Ipv6Addresses.Anycast != nil { + nestedLayer3Ipv6Addresses.Anycast = oLayer3Ipv6Addresses.Anycast + } + if oLayer3Ipv6Addresses.Advertise != nil { + nestedLayer3Ipv6Addresses.Advertise = &Layer3Ipv6AddressesAdvertiseXml{} + if _, ok := o.Misc["Layer3Ipv6AddressesAdvertise"]; ok { + nestedLayer3Ipv6Addresses.Advertise.Misc = o.Misc["Layer3Ipv6AddressesAdvertise"] + } + if oLayer3Ipv6Addresses.Advertise.Enable != nil { + nestedLayer3Ipv6Addresses.Advertise.Enable = util.YesNo(oLayer3Ipv6Addresses.Advertise.Enable, nil) + } + if oLayer3Ipv6Addresses.Advertise.ValidLifetime != nil { + nestedLayer3Ipv6Addresses.Advertise.ValidLifetime = oLayer3Ipv6Addresses.Advertise.ValidLifetime + } + if oLayer3Ipv6Addresses.Advertise.PreferredLifetime != nil { + nestedLayer3Ipv6Addresses.Advertise.PreferredLifetime = oLayer3Ipv6Addresses.Advertise.PreferredLifetime + } + if oLayer3Ipv6Addresses.Advertise.OnlinkFlag != nil { + nestedLayer3Ipv6Addresses.Advertise.OnlinkFlag = util.YesNo(oLayer3Ipv6Addresses.Advertise.OnlinkFlag, nil) + } + if oLayer3Ipv6Addresses.Advertise.AutoConfigFlag != nil { + nestedLayer3Ipv6Addresses.Advertise.AutoConfigFlag = util.YesNo(oLayer3Ipv6Addresses.Advertise.AutoConfigFlag, nil) + } + } + if oLayer3Ipv6Addresses.Name != "" { + nestedLayer3Ipv6Addresses.Name = oLayer3Ipv6Addresses.Name + } + if oLayer3Ipv6Addresses.EnableOnInterface != nil { + nestedLayer3Ipv6Addresses.EnableOnInterface = util.YesNo(oLayer3Ipv6Addresses.EnableOnInterface, nil) + } + if oLayer3Ipv6Addresses.Prefix != nil { + nestedLayer3Ipv6Addresses.Prefix = oLayer3Ipv6Addresses.Prefix + } + nestedLayer3.Ipv6.Addresses = append(nestedLayer3.Ipv6.Addresses, nestedLayer3Ipv6Addresses) + } + } + if o.Layer3.Ipv6.NeighborDiscovery != nil { + nestedLayer3.Ipv6.NeighborDiscovery = &Layer3Ipv6NeighborDiscoveryXml{} + if _, ok := o.Misc["Layer3Ipv6NeighborDiscovery"]; ok { + nestedLayer3.Ipv6.NeighborDiscovery.Misc = o.Misc["Layer3Ipv6NeighborDiscovery"] + } + if o.Layer3.Ipv6.NeighborDiscovery.Neighbor != nil { + nestedLayer3.Ipv6.NeighborDiscovery.Neighbor = []Layer3Ipv6NeighborDiscoveryNeighborXml{} + for _, oLayer3Ipv6NeighborDiscoveryNeighbor := range o.Layer3.Ipv6.NeighborDiscovery.Neighbor { + nestedLayer3Ipv6NeighborDiscoveryNeighbor := Layer3Ipv6NeighborDiscoveryNeighborXml{} + if _, ok := o.Misc["Layer3Ipv6NeighborDiscoveryNeighbor"]; ok { + nestedLayer3Ipv6NeighborDiscoveryNeighbor.Misc = o.Misc["Layer3Ipv6NeighborDiscoveryNeighbor"] + } + if oLayer3Ipv6NeighborDiscoveryNeighbor.HwAddress != nil { + nestedLayer3Ipv6NeighborDiscoveryNeighbor.HwAddress = oLayer3Ipv6NeighborDiscoveryNeighbor.HwAddress + } + if oLayer3Ipv6NeighborDiscoveryNeighbor.Name != "" { + nestedLayer3Ipv6NeighborDiscoveryNeighbor.Name = oLayer3Ipv6NeighborDiscoveryNeighbor.Name + } + nestedLayer3.Ipv6.NeighborDiscovery.Neighbor = append(nestedLayer3.Ipv6.NeighborDiscovery.Neighbor, nestedLayer3Ipv6NeighborDiscoveryNeighbor) + } + } + if o.Layer3.Ipv6.NeighborDiscovery.EnableNdpMonitor != nil { + nestedLayer3.Ipv6.NeighborDiscovery.EnableNdpMonitor = util.YesNo(o.Layer3.Ipv6.NeighborDiscovery.EnableNdpMonitor, nil) + } + if o.Layer3.Ipv6.NeighborDiscovery.EnableDad != nil { + nestedLayer3.Ipv6.NeighborDiscovery.EnableDad = util.YesNo(o.Layer3.Ipv6.NeighborDiscovery.EnableDad, nil) + } + if o.Layer3.Ipv6.NeighborDiscovery.DadAttempts != nil { + nestedLayer3.Ipv6.NeighborDiscovery.DadAttempts = o.Layer3.Ipv6.NeighborDiscovery.DadAttempts + } + if o.Layer3.Ipv6.NeighborDiscovery.NsInterval != nil { + nestedLayer3.Ipv6.NeighborDiscovery.NsInterval = o.Layer3.Ipv6.NeighborDiscovery.NsInterval + } + if o.Layer3.Ipv6.NeighborDiscovery.ReachableTime != nil { + nestedLayer3.Ipv6.NeighborDiscovery.ReachableTime = o.Layer3.Ipv6.NeighborDiscovery.ReachableTime + } + if o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement != nil { + nestedLayer3.Ipv6.NeighborDiscovery.RouterAdvertisement = &Layer3Ipv6NeighborDiscoveryRouterAdvertisementXml{} + if _, ok := o.Misc["Layer3Ipv6NeighborDiscoveryRouterAdvertisement"]; ok { + nestedLayer3.Ipv6.NeighborDiscovery.RouterAdvertisement.Misc = o.Misc["Layer3Ipv6NeighborDiscoveryRouterAdvertisement"] + } + if o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.RetransmissionTimer != nil { + nestedLayer3.Ipv6.NeighborDiscovery.RouterAdvertisement.RetransmissionTimer = o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.RetransmissionTimer + } + if o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.OtherFlag != nil { + nestedLayer3.Ipv6.NeighborDiscovery.RouterAdvertisement.OtherFlag = util.YesNo(o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.OtherFlag, nil) + } + if o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.EnableConsistencyCheck != nil { + nestedLayer3.Ipv6.NeighborDiscovery.RouterAdvertisement.EnableConsistencyCheck = util.YesNo(o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.EnableConsistencyCheck, nil) + } + if o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.LinkMtu != nil { + nestedLayer3.Ipv6.NeighborDiscovery.RouterAdvertisement.LinkMtu = o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.LinkMtu + } + if o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.MaxInterval != nil { + nestedLayer3.Ipv6.NeighborDiscovery.RouterAdvertisement.MaxInterval = o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.MaxInterval + } + if o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.MinInterval != nil { + nestedLayer3.Ipv6.NeighborDiscovery.RouterAdvertisement.MinInterval = o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.MinInterval + } + if o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.ReachableTime != nil { + nestedLayer3.Ipv6.NeighborDiscovery.RouterAdvertisement.ReachableTime = o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.ReachableTime + } + if o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.HopLimit != nil { + nestedLayer3.Ipv6.NeighborDiscovery.RouterAdvertisement.HopLimit = o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.HopLimit + } + if o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.Lifetime != nil { + nestedLayer3.Ipv6.NeighborDiscovery.RouterAdvertisement.Lifetime = o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.Lifetime + } + if o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.RouterPreference != nil { + nestedLayer3.Ipv6.NeighborDiscovery.RouterAdvertisement.RouterPreference = o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.RouterPreference + } + if o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.ManagedFlag != nil { + nestedLayer3.Ipv6.NeighborDiscovery.RouterAdvertisement.ManagedFlag = util.YesNo(o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.ManagedFlag, nil) + } + if o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.Enable != nil { + nestedLayer3.Ipv6.NeighborDiscovery.RouterAdvertisement.Enable = util.YesNo(o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.Enable, nil) + } + } + } + if o.Layer3.Ipv6.DnsServer != nil { + nestedLayer3.Ipv6.DnsServer = &Layer3Ipv6DnsServerXml{} + if _, ok := o.Misc["Layer3Ipv6DnsServer"]; ok { + nestedLayer3.Ipv6.DnsServer.Misc = o.Misc["Layer3Ipv6DnsServer"] + } + if o.Layer3.Ipv6.DnsServer.Enable != nil { + nestedLayer3.Ipv6.DnsServer.Enable = util.YesNo(o.Layer3.Ipv6.DnsServer.Enable, nil) + } + if o.Layer3.Ipv6.DnsServer.Source != nil { + nestedLayer3.Ipv6.DnsServer.Source = &Layer3Ipv6DnsServerSourceXml{} + if _, ok := o.Misc["Layer3Ipv6DnsServerSource"]; ok { + nestedLayer3.Ipv6.DnsServer.Source.Misc = o.Misc["Layer3Ipv6DnsServerSource"] + } + if o.Layer3.Ipv6.DnsServer.Source.Dhcpv6 != nil { + nestedLayer3.Ipv6.DnsServer.Source.Dhcpv6 = &Layer3Ipv6DnsServerSourceDhcpv6Xml{} + if _, ok := o.Misc["Layer3Ipv6DnsServerSourceDhcpv6"]; ok { + nestedLayer3.Ipv6.DnsServer.Source.Dhcpv6.Misc = o.Misc["Layer3Ipv6DnsServerSourceDhcpv6"] + } + if o.Layer3.Ipv6.DnsServer.Source.Dhcpv6.PrefixPool != nil { + nestedLayer3.Ipv6.DnsServer.Source.Dhcpv6.PrefixPool = o.Layer3.Ipv6.DnsServer.Source.Dhcpv6.PrefixPool + } + } + if o.Layer3.Ipv6.DnsServer.Source.Manual != nil { + nestedLayer3.Ipv6.DnsServer.Source.Manual = &Layer3Ipv6DnsServerSourceManualXml{} + if _, ok := o.Misc["Layer3Ipv6DnsServerSourceManual"]; ok { + nestedLayer3.Ipv6.DnsServer.Source.Manual.Misc = o.Misc["Layer3Ipv6DnsServerSourceManual"] + } + if o.Layer3.Ipv6.DnsServer.Source.Manual.Suffix != nil { + nestedLayer3.Ipv6.DnsServer.Source.Manual.Suffix = []Layer3Ipv6DnsServerSourceManualSuffixXml{} + for _, oLayer3Ipv6DnsServerSourceManualSuffix := range o.Layer3.Ipv6.DnsServer.Source.Manual.Suffix { + nestedLayer3Ipv6DnsServerSourceManualSuffix := Layer3Ipv6DnsServerSourceManualSuffixXml{} + if _, ok := o.Misc["Layer3Ipv6DnsServerSourceManualSuffix"]; ok { + nestedLayer3Ipv6DnsServerSourceManualSuffix.Misc = o.Misc["Layer3Ipv6DnsServerSourceManualSuffix"] + } + if oLayer3Ipv6DnsServerSourceManualSuffix.Lifetime != nil { + nestedLayer3Ipv6DnsServerSourceManualSuffix.Lifetime = oLayer3Ipv6DnsServerSourceManualSuffix.Lifetime + } + if oLayer3Ipv6DnsServerSourceManualSuffix.Name != "" { + nestedLayer3Ipv6DnsServerSourceManualSuffix.Name = oLayer3Ipv6DnsServerSourceManualSuffix.Name + } + nestedLayer3.Ipv6.DnsServer.Source.Manual.Suffix = append(nestedLayer3.Ipv6.DnsServer.Source.Manual.Suffix, nestedLayer3Ipv6DnsServerSourceManualSuffix) + } + } + } + } + if o.Layer3.Ipv6.DnsServer.DnsSupport != nil { + nestedLayer3.Ipv6.DnsServer.DnsSupport = &Layer3Ipv6DnsServerDnsSupportXml{} + if _, ok := o.Misc["Layer3Ipv6DnsServerDnsSupport"]; ok { + nestedLayer3.Ipv6.DnsServer.DnsSupport.Misc = o.Misc["Layer3Ipv6DnsServerDnsSupport"] + } + if o.Layer3.Ipv6.DnsServer.DnsSupport.Enable != nil { + nestedLayer3.Ipv6.DnsServer.DnsSupport.Enable = util.YesNo(o.Layer3.Ipv6.DnsServer.DnsSupport.Enable, nil) + } + if o.Layer3.Ipv6.DnsServer.DnsSupport.Server != nil { + nestedLayer3.Ipv6.DnsServer.DnsSupport.Server = []Layer3Ipv6DnsServerDnsSupportServerXml{} + for _, oLayer3Ipv6DnsServerDnsSupportServer := range o.Layer3.Ipv6.DnsServer.DnsSupport.Server { + nestedLayer3Ipv6DnsServerDnsSupportServer := Layer3Ipv6DnsServerDnsSupportServerXml{} + if _, ok := o.Misc["Layer3Ipv6DnsServerDnsSupportServer"]; ok { + nestedLayer3Ipv6DnsServerDnsSupportServer.Misc = o.Misc["Layer3Ipv6DnsServerDnsSupportServer"] + } + if oLayer3Ipv6DnsServerDnsSupportServer.Lifetime != nil { + nestedLayer3Ipv6DnsServerDnsSupportServer.Lifetime = oLayer3Ipv6DnsServerDnsSupportServer.Lifetime + } + if oLayer3Ipv6DnsServerDnsSupportServer.Name != "" { + nestedLayer3Ipv6DnsServerDnsSupportServer.Name = oLayer3Ipv6DnsServerDnsSupportServer.Name + } + nestedLayer3.Ipv6.DnsServer.DnsSupport.Server = append(nestedLayer3.Ipv6.DnsServer.DnsSupport.Server, nestedLayer3Ipv6DnsServerDnsSupportServer) + } + } + if o.Layer3.Ipv6.DnsServer.DnsSupport.Suffix != nil { + nestedLayer3.Ipv6.DnsServer.DnsSupport.Suffix = []Layer3Ipv6DnsServerDnsSupportSuffixXml{} + for _, oLayer3Ipv6DnsServerDnsSupportSuffix := range o.Layer3.Ipv6.DnsServer.DnsSupport.Suffix { + nestedLayer3Ipv6DnsServerDnsSupportSuffix := Layer3Ipv6DnsServerDnsSupportSuffixXml{} + if _, ok := o.Misc["Layer3Ipv6DnsServerDnsSupportSuffix"]; ok { + nestedLayer3Ipv6DnsServerDnsSupportSuffix.Misc = o.Misc["Layer3Ipv6DnsServerDnsSupportSuffix"] + } + if oLayer3Ipv6DnsServerDnsSupportSuffix.Lifetime != nil { + nestedLayer3Ipv6DnsServerDnsSupportSuffix.Lifetime = oLayer3Ipv6DnsServerDnsSupportSuffix.Lifetime + } + if oLayer3Ipv6DnsServerDnsSupportSuffix.Name != "" { + nestedLayer3Ipv6DnsServerDnsSupportSuffix.Name = oLayer3Ipv6DnsServerDnsSupportSuffix.Name + } + nestedLayer3.Ipv6.DnsServer.DnsSupport.Suffix = append(nestedLayer3.Ipv6.DnsServer.DnsSupport.Suffix, nestedLayer3Ipv6DnsServerDnsSupportSuffix) + } + } + } + } + if o.Layer3.Ipv6.Enabled != nil { + nestedLayer3.Ipv6.Enabled = util.YesNo(o.Layer3.Ipv6.Enabled, nil) + } + if o.Layer3.Ipv6.InterfaceId != nil { + nestedLayer3.Ipv6.InterfaceId = o.Layer3.Ipv6.InterfaceId + } + } + if o.Layer3.AdjustTcpMss != nil { + nestedLayer3.AdjustTcpMss = &Layer3AdjustTcpMssXml{} + if _, ok := o.Misc["Layer3AdjustTcpMss"]; ok { + nestedLayer3.AdjustTcpMss.Misc = o.Misc["Layer3AdjustTcpMss"] + } + if o.Layer3.AdjustTcpMss.Ipv4MssAdjustment != nil { + nestedLayer3.AdjustTcpMss.Ipv4MssAdjustment = o.Layer3.AdjustTcpMss.Ipv4MssAdjustment + } + if o.Layer3.AdjustTcpMss.Ipv6MssAdjustment != nil { + nestedLayer3.AdjustTcpMss.Ipv6MssAdjustment = o.Layer3.AdjustTcpMss.Ipv6MssAdjustment + } + if o.Layer3.AdjustTcpMss.Enable != nil { + nestedLayer3.AdjustTcpMss.Enable = util.YesNo(o.Layer3.AdjustTcpMss.Enable, nil) + } + } + if o.Layer3.Arp != nil { + nestedLayer3.Arp = []Layer3ArpXml{} + for _, oLayer3Arp := range o.Layer3.Arp { + nestedLayer3Arp := Layer3ArpXml{} + if _, ok := o.Misc["Layer3Arp"]; ok { + nestedLayer3Arp.Misc = o.Misc["Layer3Arp"] + } + if oLayer3Arp.HwAddress != nil { + nestedLayer3Arp.HwAddress = oLayer3Arp.HwAddress + } + if oLayer3Arp.Name != "" { + nestedLayer3Arp.Name = oLayer3Arp.Name + } + nestedLayer3.Arp = append(nestedLayer3.Arp, nestedLayer3Arp) + } + } + if o.Layer3.NdpProxy != nil { + nestedLayer3.NdpProxy = util.YesNo(o.Layer3.NdpProxy, nil) + } + if o.Layer3.Lldp != nil { + nestedLayer3.Lldp = &Layer3LldpXml{} + if _, ok := o.Misc["Layer3Lldp"]; ok { + nestedLayer3.Lldp.Misc = o.Misc["Layer3Lldp"] + } + if o.Layer3.Lldp.Enable != nil { + nestedLayer3.Lldp.Enable = util.YesNo(o.Layer3.Lldp.Enable, nil) + } + if o.Layer3.Lldp.Profile != nil { + nestedLayer3.Lldp.Profile = o.Layer3.Lldp.Profile + } + } + if o.Layer3.Mtu != nil { + nestedLayer3.Mtu = o.Layer3.Mtu + } + if o.Layer3.SdwanLinkSettings != nil { + nestedLayer3.SdwanLinkSettings = &Layer3SdwanLinkSettingsXml{} + if _, ok := o.Misc["Layer3SdwanLinkSettings"]; ok { + nestedLayer3.SdwanLinkSettings.Misc = o.Misc["Layer3SdwanLinkSettings"] + } + if o.Layer3.SdwanLinkSettings.SdwanInterfaceProfile != nil { + nestedLayer3.SdwanLinkSettings.SdwanInterfaceProfile = o.Layer3.SdwanLinkSettings.SdwanInterfaceProfile + } + if o.Layer3.SdwanLinkSettings.UpstreamNat != nil { + nestedLayer3.SdwanLinkSettings.UpstreamNat = &Layer3SdwanLinkSettingsUpstreamNatXml{} + if _, ok := o.Misc["Layer3SdwanLinkSettingsUpstreamNat"]; ok { + nestedLayer3.SdwanLinkSettings.UpstreamNat.Misc = o.Misc["Layer3SdwanLinkSettingsUpstreamNat"] + } + if o.Layer3.SdwanLinkSettings.UpstreamNat.StaticIp != nil { + nestedLayer3.SdwanLinkSettings.UpstreamNat.StaticIp = o.Layer3.SdwanLinkSettings.UpstreamNat.StaticIp + } + if o.Layer3.SdwanLinkSettings.UpstreamNat.Enable != nil { + nestedLayer3.SdwanLinkSettings.UpstreamNat.Enable = util.YesNo(o.Layer3.SdwanLinkSettings.UpstreamNat.Enable, nil) + } + } + if o.Layer3.SdwanLinkSettings.Enable != nil { + nestedLayer3.SdwanLinkSettings.Enable = util.YesNo(o.Layer3.SdwanLinkSettings.Enable, nil) + } + } + if o.Layer3.UntaggedSubInterface != nil { + nestedLayer3.UntaggedSubInterface = util.YesNo(o.Layer3.UntaggedSubInterface, nil) + } + if o.Layer3.DhcpClient != nil { + nestedLayer3.DhcpClient = &Layer3DhcpClientXml{} + if _, ok := o.Misc["Layer3DhcpClient"]; ok { + nestedLayer3.DhcpClient.Misc = o.Misc["Layer3DhcpClient"] + } + if o.Layer3.DhcpClient.Enable != nil { + nestedLayer3.DhcpClient.Enable = util.YesNo(o.Layer3.DhcpClient.Enable, nil) + } + if o.Layer3.DhcpClient.CreateDefaultRoute != nil { + nestedLayer3.DhcpClient.CreateDefaultRoute = util.YesNo(o.Layer3.DhcpClient.CreateDefaultRoute, nil) + } + if o.Layer3.DhcpClient.DefaultRouteMetric != nil { + nestedLayer3.DhcpClient.DefaultRouteMetric = o.Layer3.DhcpClient.DefaultRouteMetric + } + if o.Layer3.DhcpClient.SendHostname != nil { + nestedLayer3.DhcpClient.SendHostname = &Layer3DhcpClientSendHostnameXml{} + if _, ok := o.Misc["Layer3DhcpClientSendHostname"]; ok { + nestedLayer3.DhcpClient.SendHostname.Misc = o.Misc["Layer3DhcpClientSendHostname"] + } + if o.Layer3.DhcpClient.SendHostname.Hostname != nil { + nestedLayer3.DhcpClient.SendHostname.Hostname = o.Layer3.DhcpClient.SendHostname.Hostname + } + if o.Layer3.DhcpClient.SendHostname.Enable != nil { + nestedLayer3.DhcpClient.SendHostname.Enable = util.YesNo(o.Layer3.DhcpClient.SendHostname.Enable, nil) + } + } + } + if o.Layer3.InterfaceManagementProfile != nil { + nestedLayer3.InterfaceManagementProfile = o.Layer3.InterfaceManagementProfile + } + if o.Layer3.NetflowProfile != nil { + nestedLayer3.NetflowProfile = o.Layer3.NetflowProfile + } + if o.Layer3.Bonjour != nil { + nestedLayer3.Bonjour = &Layer3BonjourXml{} + if _, ok := o.Misc["Layer3Bonjour"]; ok { + nestedLayer3.Bonjour.Misc = o.Misc["Layer3Bonjour"] + } + if o.Layer3.Bonjour.Enable != nil { + nestedLayer3.Bonjour.Enable = util.YesNo(o.Layer3.Bonjour.Enable, nil) + } + } + } + entry.Layer3 = nestedLayer3 + + var nestedTap *TapXml + if o.Tap != nil { + nestedTap = &TapXml{} + if _, ok := o.Misc["Tap"]; ok { + nestedTap.Misc = o.Misc["Tap"] + } + if o.Tap.NetflowProfile != nil { + nestedTap.NetflowProfile = o.Tap.NetflowProfile + } + } + entry.Tap = nestedTap + + entry.Misc = o.Misc["Entry"] + + return entry, nil +} +func (c *entryXmlContainer) Normalize() ([]*Entry, error) { + entryList := make([]*Entry, 0, len(c.Answer)) + for _, o := range c.Answer { + entry := &Entry{ + Misc: make(map[string][]generic.Xml), + } + entry.Name = o.Name + entry.Comment = o.Comment + entry.LinkDuplex = o.LinkDuplex + entry.LinkSpeed = o.LinkSpeed + entry.LinkState = o.LinkState + var nestedPoe *Poe + if o.Poe != nil { + nestedPoe = &Poe{} + if o.Poe.Misc != nil { + entry.Misc["Poe"] = o.Poe.Misc + } + if o.Poe.ReservedPower != nil { + nestedPoe.ReservedPower = o.Poe.ReservedPower + } + if o.Poe.Enabled != nil { + nestedPoe.Enabled = util.AsBool(o.Poe.Enabled, nil) + } + } + entry.Poe = nestedPoe + + var nestedHa *Ha + if o.Ha != nil { + nestedHa = &Ha{} + if o.Ha.Misc != nil { + entry.Misc["Ha"] = o.Ha.Misc + } + } + entry.Ha = nestedHa + + var nestedLayer3 *Layer3 + if o.Layer3 != nil { + nestedLayer3 = &Layer3{} + if o.Layer3.Misc != nil { + entry.Misc["Layer3"] = o.Layer3.Misc + } + if o.Layer3.Bonjour != nil { + nestedLayer3.Bonjour = &Layer3Bonjour{} + if o.Layer3.Bonjour.Misc != nil { + entry.Misc["Layer3Bonjour"] = o.Layer3.Bonjour.Misc + } + if o.Layer3.Bonjour.Enable != nil { + nestedLayer3.Bonjour.Enable = util.AsBool(o.Layer3.Bonjour.Enable, nil) + } + } + if o.Layer3.SdwanLinkSettings != nil { + nestedLayer3.SdwanLinkSettings = &Layer3SdwanLinkSettings{} + if o.Layer3.SdwanLinkSettings.Misc != nil { + entry.Misc["Layer3SdwanLinkSettings"] = o.Layer3.SdwanLinkSettings.Misc + } + if o.Layer3.SdwanLinkSettings.Enable != nil { + nestedLayer3.SdwanLinkSettings.Enable = util.AsBool(o.Layer3.SdwanLinkSettings.Enable, nil) + } + if o.Layer3.SdwanLinkSettings.SdwanInterfaceProfile != nil { + nestedLayer3.SdwanLinkSettings.SdwanInterfaceProfile = o.Layer3.SdwanLinkSettings.SdwanInterfaceProfile + } + if o.Layer3.SdwanLinkSettings.UpstreamNat != nil { + nestedLayer3.SdwanLinkSettings.UpstreamNat = &Layer3SdwanLinkSettingsUpstreamNat{} + if o.Layer3.SdwanLinkSettings.UpstreamNat.Misc != nil { + entry.Misc["Layer3SdwanLinkSettingsUpstreamNat"] = o.Layer3.SdwanLinkSettings.UpstreamNat.Misc + } + if o.Layer3.SdwanLinkSettings.UpstreamNat.Enable != nil { + nestedLayer3.SdwanLinkSettings.UpstreamNat.Enable = util.AsBool(o.Layer3.SdwanLinkSettings.UpstreamNat.Enable, nil) + } + if o.Layer3.SdwanLinkSettings.UpstreamNat.StaticIp != nil { + nestedLayer3.SdwanLinkSettings.UpstreamNat.StaticIp = o.Layer3.SdwanLinkSettings.UpstreamNat.StaticIp + } + } + } + if o.Layer3.UntaggedSubInterface != nil { + nestedLayer3.UntaggedSubInterface = util.AsBool(o.Layer3.UntaggedSubInterface, nil) + } + if o.Layer3.DhcpClient != nil { + nestedLayer3.DhcpClient = &Layer3DhcpClient{} + if o.Layer3.DhcpClient.Misc != nil { + entry.Misc["Layer3DhcpClient"] = o.Layer3.DhcpClient.Misc + } + if o.Layer3.DhcpClient.Enable != nil { + nestedLayer3.DhcpClient.Enable = util.AsBool(o.Layer3.DhcpClient.Enable, nil) + } + if o.Layer3.DhcpClient.CreateDefaultRoute != nil { + nestedLayer3.DhcpClient.CreateDefaultRoute = util.AsBool(o.Layer3.DhcpClient.CreateDefaultRoute, nil) + } + if o.Layer3.DhcpClient.DefaultRouteMetric != nil { + nestedLayer3.DhcpClient.DefaultRouteMetric = o.Layer3.DhcpClient.DefaultRouteMetric + } + if o.Layer3.DhcpClient.SendHostname != nil { + nestedLayer3.DhcpClient.SendHostname = &Layer3DhcpClientSendHostname{} + if o.Layer3.DhcpClient.SendHostname.Misc != nil { + entry.Misc["Layer3DhcpClientSendHostname"] = o.Layer3.DhcpClient.SendHostname.Misc + } + if o.Layer3.DhcpClient.SendHostname.Enable != nil { + nestedLayer3.DhcpClient.SendHostname.Enable = util.AsBool(o.Layer3.DhcpClient.SendHostname.Enable, nil) + } + if o.Layer3.DhcpClient.SendHostname.Hostname != nil { + nestedLayer3.DhcpClient.SendHostname.Hostname = o.Layer3.DhcpClient.SendHostname.Hostname + } + } + } + if o.Layer3.InterfaceManagementProfile != nil { + nestedLayer3.InterfaceManagementProfile = o.Layer3.InterfaceManagementProfile + } + if o.Layer3.NetflowProfile != nil { + nestedLayer3.NetflowProfile = o.Layer3.NetflowProfile + } + if o.Layer3.Mtu != nil { + nestedLayer3.Mtu = o.Layer3.Mtu + } + if o.Layer3.Ips != nil { + nestedLayer3.Ips = []Layer3Ips{} + for _, oLayer3Ips := range o.Layer3.Ips { + nestedLayer3Ips := Layer3Ips{} + if oLayer3Ips.Misc != nil { + entry.Misc["Layer3Ips"] = oLayer3Ips.Misc + } + if oLayer3Ips.SdwanGateway != nil { + nestedLayer3Ips.SdwanGateway = oLayer3Ips.SdwanGateway + } + if oLayer3Ips.Name != "" { + nestedLayer3Ips.Name = oLayer3Ips.Name + } + nestedLayer3.Ips = append(nestedLayer3.Ips, nestedLayer3Ips) + } + } + if o.Layer3.Ipv6 != nil { + nestedLayer3.Ipv6 = &Layer3Ipv6{} + if o.Layer3.Ipv6.Misc != nil { + entry.Misc["Layer3Ipv6"] = o.Layer3.Ipv6.Misc + } + if o.Layer3.Ipv6.DnsServer != nil { + nestedLayer3.Ipv6.DnsServer = &Layer3Ipv6DnsServer{} + if o.Layer3.Ipv6.DnsServer.Misc != nil { + entry.Misc["Layer3Ipv6DnsServer"] = o.Layer3.Ipv6.DnsServer.Misc + } + if o.Layer3.Ipv6.DnsServer.DnsSupport != nil { + nestedLayer3.Ipv6.DnsServer.DnsSupport = &Layer3Ipv6DnsServerDnsSupport{} + if o.Layer3.Ipv6.DnsServer.DnsSupport.Misc != nil { + entry.Misc["Layer3Ipv6DnsServerDnsSupport"] = o.Layer3.Ipv6.DnsServer.DnsSupport.Misc + } + if o.Layer3.Ipv6.DnsServer.DnsSupport.Enable != nil { + nestedLayer3.Ipv6.DnsServer.DnsSupport.Enable = util.AsBool(o.Layer3.Ipv6.DnsServer.DnsSupport.Enable, nil) + } + if o.Layer3.Ipv6.DnsServer.DnsSupport.Server != nil { + nestedLayer3.Ipv6.DnsServer.DnsSupport.Server = []Layer3Ipv6DnsServerDnsSupportServer{} + for _, oLayer3Ipv6DnsServerDnsSupportServer := range o.Layer3.Ipv6.DnsServer.DnsSupport.Server { + nestedLayer3Ipv6DnsServerDnsSupportServer := Layer3Ipv6DnsServerDnsSupportServer{} + if oLayer3Ipv6DnsServerDnsSupportServer.Misc != nil { + entry.Misc["Layer3Ipv6DnsServerDnsSupportServer"] = oLayer3Ipv6DnsServerDnsSupportServer.Misc + } + if oLayer3Ipv6DnsServerDnsSupportServer.Lifetime != nil { + nestedLayer3Ipv6DnsServerDnsSupportServer.Lifetime = oLayer3Ipv6DnsServerDnsSupportServer.Lifetime + } + if oLayer3Ipv6DnsServerDnsSupportServer.Name != "" { + nestedLayer3Ipv6DnsServerDnsSupportServer.Name = oLayer3Ipv6DnsServerDnsSupportServer.Name + } + nestedLayer3.Ipv6.DnsServer.DnsSupport.Server = append(nestedLayer3.Ipv6.DnsServer.DnsSupport.Server, nestedLayer3Ipv6DnsServerDnsSupportServer) + } + } + if o.Layer3.Ipv6.DnsServer.DnsSupport.Suffix != nil { + nestedLayer3.Ipv6.DnsServer.DnsSupport.Suffix = []Layer3Ipv6DnsServerDnsSupportSuffix{} + for _, oLayer3Ipv6DnsServerDnsSupportSuffix := range o.Layer3.Ipv6.DnsServer.DnsSupport.Suffix { + nestedLayer3Ipv6DnsServerDnsSupportSuffix := Layer3Ipv6DnsServerDnsSupportSuffix{} + if oLayer3Ipv6DnsServerDnsSupportSuffix.Misc != nil { + entry.Misc["Layer3Ipv6DnsServerDnsSupportSuffix"] = oLayer3Ipv6DnsServerDnsSupportSuffix.Misc + } + if oLayer3Ipv6DnsServerDnsSupportSuffix.Name != "" { + nestedLayer3Ipv6DnsServerDnsSupportSuffix.Name = oLayer3Ipv6DnsServerDnsSupportSuffix.Name + } + if oLayer3Ipv6DnsServerDnsSupportSuffix.Lifetime != nil { + nestedLayer3Ipv6DnsServerDnsSupportSuffix.Lifetime = oLayer3Ipv6DnsServerDnsSupportSuffix.Lifetime + } + nestedLayer3.Ipv6.DnsServer.DnsSupport.Suffix = append(nestedLayer3.Ipv6.DnsServer.DnsSupport.Suffix, nestedLayer3Ipv6DnsServerDnsSupportSuffix) + } + } + } + if o.Layer3.Ipv6.DnsServer.Enable != nil { + nestedLayer3.Ipv6.DnsServer.Enable = util.AsBool(o.Layer3.Ipv6.DnsServer.Enable, nil) + } + if o.Layer3.Ipv6.DnsServer.Source != nil { + nestedLayer3.Ipv6.DnsServer.Source = &Layer3Ipv6DnsServerSource{} + if o.Layer3.Ipv6.DnsServer.Source.Misc != nil { + entry.Misc["Layer3Ipv6DnsServerSource"] = o.Layer3.Ipv6.DnsServer.Source.Misc + } + if o.Layer3.Ipv6.DnsServer.Source.Dhcpv6 != nil { + nestedLayer3.Ipv6.DnsServer.Source.Dhcpv6 = &Layer3Ipv6DnsServerSourceDhcpv6{} + if o.Layer3.Ipv6.DnsServer.Source.Dhcpv6.Misc != nil { + entry.Misc["Layer3Ipv6DnsServerSourceDhcpv6"] = o.Layer3.Ipv6.DnsServer.Source.Dhcpv6.Misc + } + if o.Layer3.Ipv6.DnsServer.Source.Dhcpv6.PrefixPool != nil { + nestedLayer3.Ipv6.DnsServer.Source.Dhcpv6.PrefixPool = o.Layer3.Ipv6.DnsServer.Source.Dhcpv6.PrefixPool + } + } + if o.Layer3.Ipv6.DnsServer.Source.Manual != nil { + nestedLayer3.Ipv6.DnsServer.Source.Manual = &Layer3Ipv6DnsServerSourceManual{} + if o.Layer3.Ipv6.DnsServer.Source.Manual.Misc != nil { + entry.Misc["Layer3Ipv6DnsServerSourceManual"] = o.Layer3.Ipv6.DnsServer.Source.Manual.Misc + } + if o.Layer3.Ipv6.DnsServer.Source.Manual.Suffix != nil { + nestedLayer3.Ipv6.DnsServer.Source.Manual.Suffix = []Layer3Ipv6DnsServerSourceManualSuffix{} + for _, oLayer3Ipv6DnsServerSourceManualSuffix := range o.Layer3.Ipv6.DnsServer.Source.Manual.Suffix { + nestedLayer3Ipv6DnsServerSourceManualSuffix := Layer3Ipv6DnsServerSourceManualSuffix{} + if oLayer3Ipv6DnsServerSourceManualSuffix.Misc != nil { + entry.Misc["Layer3Ipv6DnsServerSourceManualSuffix"] = oLayer3Ipv6DnsServerSourceManualSuffix.Misc + } + if oLayer3Ipv6DnsServerSourceManualSuffix.Lifetime != nil { + nestedLayer3Ipv6DnsServerSourceManualSuffix.Lifetime = oLayer3Ipv6DnsServerSourceManualSuffix.Lifetime + } + if oLayer3Ipv6DnsServerSourceManualSuffix.Name != "" { + nestedLayer3Ipv6DnsServerSourceManualSuffix.Name = oLayer3Ipv6DnsServerSourceManualSuffix.Name + } + nestedLayer3.Ipv6.DnsServer.Source.Manual.Suffix = append(nestedLayer3.Ipv6.DnsServer.Source.Manual.Suffix, nestedLayer3Ipv6DnsServerSourceManualSuffix) + } + } + } + } + } + if o.Layer3.Ipv6.Enabled != nil { + nestedLayer3.Ipv6.Enabled = util.AsBool(o.Layer3.Ipv6.Enabled, nil) + } + if o.Layer3.Ipv6.InterfaceId != nil { + nestedLayer3.Ipv6.InterfaceId = o.Layer3.Ipv6.InterfaceId + } + if o.Layer3.Ipv6.Addresses != nil { + nestedLayer3.Ipv6.Addresses = []Layer3Ipv6Addresses{} + for _, oLayer3Ipv6Addresses := range o.Layer3.Ipv6.Addresses { + nestedLayer3Ipv6Addresses := Layer3Ipv6Addresses{} + if oLayer3Ipv6Addresses.Misc != nil { + entry.Misc["Layer3Ipv6Addresses"] = oLayer3Ipv6Addresses.Misc + } + if oLayer3Ipv6Addresses.EnableOnInterface != nil { + nestedLayer3Ipv6Addresses.EnableOnInterface = util.AsBool(oLayer3Ipv6Addresses.EnableOnInterface, nil) + } + if oLayer3Ipv6Addresses.Prefix != nil { + nestedLayer3Ipv6Addresses.Prefix = oLayer3Ipv6Addresses.Prefix + } + if oLayer3Ipv6Addresses.Anycast != nil { + nestedLayer3Ipv6Addresses.Anycast = oLayer3Ipv6Addresses.Anycast + } + if oLayer3Ipv6Addresses.Advertise != nil { + nestedLayer3Ipv6Addresses.Advertise = &Layer3Ipv6AddressesAdvertise{} + if oLayer3Ipv6Addresses.Advertise.Misc != nil { + entry.Misc["Layer3Ipv6AddressesAdvertise"] = oLayer3Ipv6Addresses.Advertise.Misc + } + if oLayer3Ipv6Addresses.Advertise.Enable != nil { + nestedLayer3Ipv6Addresses.Advertise.Enable = util.AsBool(oLayer3Ipv6Addresses.Advertise.Enable, nil) + } + if oLayer3Ipv6Addresses.Advertise.ValidLifetime != nil { + nestedLayer3Ipv6Addresses.Advertise.ValidLifetime = oLayer3Ipv6Addresses.Advertise.ValidLifetime + } + if oLayer3Ipv6Addresses.Advertise.PreferredLifetime != nil { + nestedLayer3Ipv6Addresses.Advertise.PreferredLifetime = oLayer3Ipv6Addresses.Advertise.PreferredLifetime + } + if oLayer3Ipv6Addresses.Advertise.OnlinkFlag != nil { + nestedLayer3Ipv6Addresses.Advertise.OnlinkFlag = util.AsBool(oLayer3Ipv6Addresses.Advertise.OnlinkFlag, nil) + } + if oLayer3Ipv6Addresses.Advertise.AutoConfigFlag != nil { + nestedLayer3Ipv6Addresses.Advertise.AutoConfigFlag = util.AsBool(oLayer3Ipv6Addresses.Advertise.AutoConfigFlag, nil) + } + } + if oLayer3Ipv6Addresses.Name != "" { + nestedLayer3Ipv6Addresses.Name = oLayer3Ipv6Addresses.Name + } + nestedLayer3.Ipv6.Addresses = append(nestedLayer3.Ipv6.Addresses, nestedLayer3Ipv6Addresses) + } + } + if o.Layer3.Ipv6.NeighborDiscovery != nil { + nestedLayer3.Ipv6.NeighborDiscovery = &Layer3Ipv6NeighborDiscovery{} + if o.Layer3.Ipv6.NeighborDiscovery.Misc != nil { + entry.Misc["Layer3Ipv6NeighborDiscovery"] = o.Layer3.Ipv6.NeighborDiscovery.Misc + } + if o.Layer3.Ipv6.NeighborDiscovery.EnableDad != nil { + nestedLayer3.Ipv6.NeighborDiscovery.EnableDad = util.AsBool(o.Layer3.Ipv6.NeighborDiscovery.EnableDad, nil) + } + if o.Layer3.Ipv6.NeighborDiscovery.DadAttempts != nil { + nestedLayer3.Ipv6.NeighborDiscovery.DadAttempts = o.Layer3.Ipv6.NeighborDiscovery.DadAttempts + } + if o.Layer3.Ipv6.NeighborDiscovery.NsInterval != nil { + nestedLayer3.Ipv6.NeighborDiscovery.NsInterval = o.Layer3.Ipv6.NeighborDiscovery.NsInterval + } + if o.Layer3.Ipv6.NeighborDiscovery.ReachableTime != nil { + nestedLayer3.Ipv6.NeighborDiscovery.ReachableTime = o.Layer3.Ipv6.NeighborDiscovery.ReachableTime + } + if o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement != nil { + nestedLayer3.Ipv6.NeighborDiscovery.RouterAdvertisement = &Layer3Ipv6NeighborDiscoveryRouterAdvertisement{} + if o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.Misc != nil { + entry.Misc["Layer3Ipv6NeighborDiscoveryRouterAdvertisement"] = o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.Misc + } + if o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.OtherFlag != nil { + nestedLayer3.Ipv6.NeighborDiscovery.RouterAdvertisement.OtherFlag = util.AsBool(o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.OtherFlag, nil) + } + if o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.EnableConsistencyCheck != nil { + nestedLayer3.Ipv6.NeighborDiscovery.RouterAdvertisement.EnableConsistencyCheck = util.AsBool(o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.EnableConsistencyCheck, nil) + } + if o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.LinkMtu != nil { + nestedLayer3.Ipv6.NeighborDiscovery.RouterAdvertisement.LinkMtu = o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.LinkMtu + } + if o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.RetransmissionTimer != nil { + nestedLayer3.Ipv6.NeighborDiscovery.RouterAdvertisement.RetransmissionTimer = o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.RetransmissionTimer + } + if o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.MinInterval != nil { + nestedLayer3.Ipv6.NeighborDiscovery.RouterAdvertisement.MinInterval = o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.MinInterval + } + if o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.ReachableTime != nil { + nestedLayer3.Ipv6.NeighborDiscovery.RouterAdvertisement.ReachableTime = o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.ReachableTime + } + if o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.HopLimit != nil { + nestedLayer3.Ipv6.NeighborDiscovery.RouterAdvertisement.HopLimit = o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.HopLimit + } + if o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.Lifetime != nil { + nestedLayer3.Ipv6.NeighborDiscovery.RouterAdvertisement.Lifetime = o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.Lifetime + } + if o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.RouterPreference != nil { + nestedLayer3.Ipv6.NeighborDiscovery.RouterAdvertisement.RouterPreference = o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.RouterPreference + } + if o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.ManagedFlag != nil { + nestedLayer3.Ipv6.NeighborDiscovery.RouterAdvertisement.ManagedFlag = util.AsBool(o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.ManagedFlag, nil) + } + if o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.Enable != nil { + nestedLayer3.Ipv6.NeighborDiscovery.RouterAdvertisement.Enable = util.AsBool(o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.Enable, nil) + } + if o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.MaxInterval != nil { + nestedLayer3.Ipv6.NeighborDiscovery.RouterAdvertisement.MaxInterval = o.Layer3.Ipv6.NeighborDiscovery.RouterAdvertisement.MaxInterval + } + } + if o.Layer3.Ipv6.NeighborDiscovery.Neighbor != nil { + nestedLayer3.Ipv6.NeighborDiscovery.Neighbor = []Layer3Ipv6NeighborDiscoveryNeighbor{} + for _, oLayer3Ipv6NeighborDiscoveryNeighbor := range o.Layer3.Ipv6.NeighborDiscovery.Neighbor { + nestedLayer3Ipv6NeighborDiscoveryNeighbor := Layer3Ipv6NeighborDiscoveryNeighbor{} + if oLayer3Ipv6NeighborDiscoveryNeighbor.Misc != nil { + entry.Misc["Layer3Ipv6NeighborDiscoveryNeighbor"] = oLayer3Ipv6NeighborDiscoveryNeighbor.Misc + } + if oLayer3Ipv6NeighborDiscoveryNeighbor.HwAddress != nil { + nestedLayer3Ipv6NeighborDiscoveryNeighbor.HwAddress = oLayer3Ipv6NeighborDiscoveryNeighbor.HwAddress + } + if oLayer3Ipv6NeighborDiscoveryNeighbor.Name != "" { + nestedLayer3Ipv6NeighborDiscoveryNeighbor.Name = oLayer3Ipv6NeighborDiscoveryNeighbor.Name + } + nestedLayer3.Ipv6.NeighborDiscovery.Neighbor = append(nestedLayer3.Ipv6.NeighborDiscovery.Neighbor, nestedLayer3Ipv6NeighborDiscoveryNeighbor) + } + } + if o.Layer3.Ipv6.NeighborDiscovery.EnableNdpMonitor != nil { + nestedLayer3.Ipv6.NeighborDiscovery.EnableNdpMonitor = util.AsBool(o.Layer3.Ipv6.NeighborDiscovery.EnableNdpMonitor, nil) + } + } + } + if o.Layer3.AdjustTcpMss != nil { + nestedLayer3.AdjustTcpMss = &Layer3AdjustTcpMss{} + if o.Layer3.AdjustTcpMss.Misc != nil { + entry.Misc["Layer3AdjustTcpMss"] = o.Layer3.AdjustTcpMss.Misc + } + if o.Layer3.AdjustTcpMss.Ipv4MssAdjustment != nil { + nestedLayer3.AdjustTcpMss.Ipv4MssAdjustment = o.Layer3.AdjustTcpMss.Ipv4MssAdjustment + } + if o.Layer3.AdjustTcpMss.Ipv6MssAdjustment != nil { + nestedLayer3.AdjustTcpMss.Ipv6MssAdjustment = o.Layer3.AdjustTcpMss.Ipv6MssAdjustment + } + if o.Layer3.AdjustTcpMss.Enable != nil { + nestedLayer3.AdjustTcpMss.Enable = util.AsBool(o.Layer3.AdjustTcpMss.Enable, nil) + } + } + if o.Layer3.Arp != nil { + nestedLayer3.Arp = []Layer3Arp{} + for _, oLayer3Arp := range o.Layer3.Arp { + nestedLayer3Arp := Layer3Arp{} + if oLayer3Arp.Misc != nil { + entry.Misc["Layer3Arp"] = oLayer3Arp.Misc + } + if oLayer3Arp.HwAddress != nil { + nestedLayer3Arp.HwAddress = oLayer3Arp.HwAddress + } + if oLayer3Arp.Name != "" { + nestedLayer3Arp.Name = oLayer3Arp.Name + } + nestedLayer3.Arp = append(nestedLayer3.Arp, nestedLayer3Arp) + } + } + if o.Layer3.NdpProxy != nil { + nestedLayer3.NdpProxy = util.AsBool(o.Layer3.NdpProxy, nil) + } + if o.Layer3.Lldp != nil { + nestedLayer3.Lldp = &Layer3Lldp{} + if o.Layer3.Lldp.Misc != nil { + entry.Misc["Layer3Lldp"] = o.Layer3.Lldp.Misc + } + if o.Layer3.Lldp.Enable != nil { + nestedLayer3.Lldp.Enable = util.AsBool(o.Layer3.Lldp.Enable, nil) + } + if o.Layer3.Lldp.Profile != nil { + nestedLayer3.Lldp.Profile = o.Layer3.Lldp.Profile + } + } + } + entry.Layer3 = nestedLayer3 + + var nestedTap *Tap + if o.Tap != nil { + nestedTap = &Tap{} + if o.Tap.Misc != nil { + entry.Misc["Tap"] = o.Tap.Misc + } + if o.Tap.NetflowProfile != nil { + nestedTap.NetflowProfile = o.Tap.NetflowProfile + } + } + entry.Tap = nestedTap + + entry.Misc["Entry"] = o.Misc + + entryList = append(entryList, entry) + } + + return entryList, nil +} + +func SpecMatches(a, b *Entry) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + + // Don't compare Name. + if !util.StringsMatch(a.Comment, b.Comment) { + return false + } + if !util.StringsMatch(a.LinkDuplex, b.LinkDuplex) { + return false + } + if !util.StringsMatch(a.LinkSpeed, b.LinkSpeed) { + return false + } + if !util.StringsMatch(a.LinkState, b.LinkState) { + return false + } + if !matchPoe(a.Poe, b.Poe) { + return false + } + if !matchHa(a.Ha, b.Ha) { + return false + } + if !matchLayer3(a.Layer3, b.Layer3) { + return false + } + if !matchTap(a.Tap, b.Tap) { + return false + } + + return true +} + +func matchPoe(a *Poe, b *Poe) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.Ints64Match(a.ReservedPower, b.ReservedPower) { + return false + } + if !util.BoolsMatch(a.Enabled, b.Enabled) { + return false + } + return true +} +func matchHa(a *Ha, b *Ha) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + return true +} +func matchLayer3DhcpClientSendHostname(a *Layer3DhcpClientSendHostname, b *Layer3DhcpClientSendHostname) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.BoolsMatch(a.Enable, b.Enable) { + return false + } + if !util.StringsMatch(a.Hostname, b.Hostname) { + return false + } + return true +} +func matchLayer3DhcpClient(a *Layer3DhcpClient, b *Layer3DhcpClient) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !matchLayer3DhcpClientSendHostname(a.SendHostname, b.SendHostname) { + return false + } + if !util.BoolsMatch(a.Enable, b.Enable) { + return false + } + if !util.BoolsMatch(a.CreateDefaultRoute, b.CreateDefaultRoute) { + return false + } + if !util.Ints64Match(a.DefaultRouteMetric, b.DefaultRouteMetric) { + return false + } + return true +} +func matchLayer3Bonjour(a *Layer3Bonjour, b *Layer3Bonjour) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.BoolsMatch(a.Enable, b.Enable) { + return false + } + return true +} +func matchLayer3SdwanLinkSettingsUpstreamNat(a *Layer3SdwanLinkSettingsUpstreamNat, b *Layer3SdwanLinkSettingsUpstreamNat) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.BoolsMatch(a.Enable, b.Enable) { + return false + } + if !util.StringsMatch(a.StaticIp, b.StaticIp) { + return false + } + return true +} +func matchLayer3SdwanLinkSettings(a *Layer3SdwanLinkSettings, b *Layer3SdwanLinkSettings) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.BoolsMatch(a.Enable, b.Enable) { + return false + } + if !util.StringsMatch(a.SdwanInterfaceProfile, b.SdwanInterfaceProfile) { + return false + } + if !matchLayer3SdwanLinkSettingsUpstreamNat(a.UpstreamNat, b.UpstreamNat) { + return false + } + return true +} +func matchLayer3AdjustTcpMss(a *Layer3AdjustTcpMss, b *Layer3AdjustTcpMss) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.Ints64Match(a.Ipv4MssAdjustment, b.Ipv4MssAdjustment) { + return false + } + if !util.Ints64Match(a.Ipv6MssAdjustment, b.Ipv6MssAdjustment) { + return false + } + if !util.BoolsMatch(a.Enable, b.Enable) { + return false + } + return true +} +func matchLayer3Arp(a []Layer3Arp, b []Layer3Arp) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + for _, a := range a { + for _, b := range b { + if !util.StringsMatch(a.HwAddress, b.HwAddress) { + return false + } + if !util.StringsEqual(a.Name, b.Name) { + return false + } + } + } + return true +} +func matchLayer3Lldp(a *Layer3Lldp, b *Layer3Lldp) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.BoolsMatch(a.Enable, b.Enable) { + return false + } + if !util.StringsMatch(a.Profile, b.Profile) { + return false + } + return true +} +func matchLayer3Ips(a []Layer3Ips, b []Layer3Ips) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + for _, a := range a { + for _, b := range b { + if !util.StringsMatch(a.SdwanGateway, b.SdwanGateway) { + return false + } + if !util.StringsEqual(a.Name, b.Name) { + return false + } + } + } + return true +} +func matchLayer3Ipv6AddressesAdvertise(a *Layer3Ipv6AddressesAdvertise, b *Layer3Ipv6AddressesAdvertise) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.BoolsMatch(a.AutoConfigFlag, b.AutoConfigFlag) { + return false + } + if !util.BoolsMatch(a.Enable, b.Enable) { + return false + } + if !util.StringsMatch(a.ValidLifetime, b.ValidLifetime) { + return false + } + if !util.StringsMatch(a.PreferredLifetime, b.PreferredLifetime) { + return false + } + if !util.BoolsMatch(a.OnlinkFlag, b.OnlinkFlag) { + return false + } + return true +} +func matchLayer3Ipv6Addresses(a []Layer3Ipv6Addresses, b []Layer3Ipv6Addresses) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + for _, a := range a { + for _, b := range b { + if !util.BoolsMatch(a.EnableOnInterface, b.EnableOnInterface) { + return false + } + if !util.StringsMatch(a.Prefix, b.Prefix) { + return false + } + if !util.StringsMatch(a.Anycast, b.Anycast) { + return false + } + if !matchLayer3Ipv6AddressesAdvertise(a.Advertise, b.Advertise) { + return false + } + if !util.StringsEqual(a.Name, b.Name) { + return false + } + } + } + return true +} +func matchLayer3Ipv6NeighborDiscoveryRouterAdvertisement(a *Layer3Ipv6NeighborDiscoveryRouterAdvertisement, b *Layer3Ipv6NeighborDiscoveryRouterAdvertisement) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.StringsMatch(a.HopLimit, b.HopLimit) { + return false + } + if !util.Ints64Match(a.Lifetime, b.Lifetime) { + return false + } + if !util.StringsMatch(a.RouterPreference, b.RouterPreference) { + return false + } + if !util.BoolsMatch(a.ManagedFlag, b.ManagedFlag) { + return false + } + if !util.BoolsMatch(a.Enable, b.Enable) { + return false + } + if !util.Ints64Match(a.MaxInterval, b.MaxInterval) { + return false + } + if !util.Ints64Match(a.MinInterval, b.MinInterval) { + return false + } + if !util.StringsMatch(a.ReachableTime, b.ReachableTime) { + return false + } + if !util.StringsMatch(a.LinkMtu, b.LinkMtu) { + return false + } + if !util.StringsMatch(a.RetransmissionTimer, b.RetransmissionTimer) { + return false + } + if !util.BoolsMatch(a.OtherFlag, b.OtherFlag) { + return false + } + if !util.BoolsMatch(a.EnableConsistencyCheck, b.EnableConsistencyCheck) { + return false + } + return true +} +func matchLayer3Ipv6NeighborDiscoveryNeighbor(a []Layer3Ipv6NeighborDiscoveryNeighbor, b []Layer3Ipv6NeighborDiscoveryNeighbor) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + for _, a := range a { + for _, b := range b { + if !util.StringsMatch(a.HwAddress, b.HwAddress) { + return false + } + if !util.StringsEqual(a.Name, b.Name) { + return false + } + } + } + return true +} +func matchLayer3Ipv6NeighborDiscovery(a *Layer3Ipv6NeighborDiscovery, b *Layer3Ipv6NeighborDiscovery) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !matchLayer3Ipv6NeighborDiscoveryNeighbor(a.Neighbor, b.Neighbor) { + return false + } + if !util.BoolsMatch(a.EnableNdpMonitor, b.EnableNdpMonitor) { + return false + } + if !util.BoolsMatch(a.EnableDad, b.EnableDad) { + return false + } + if !util.Ints64Match(a.DadAttempts, b.DadAttempts) { + return false + } + if !util.Ints64Match(a.NsInterval, b.NsInterval) { + return false + } + if !util.Ints64Match(a.ReachableTime, b.ReachableTime) { + return false + } + if !matchLayer3Ipv6NeighborDiscoveryRouterAdvertisement(a.RouterAdvertisement, b.RouterAdvertisement) { + return false + } + return true +} +func matchLayer3Ipv6DnsServerSourceManualSuffix(a []Layer3Ipv6DnsServerSourceManualSuffix, b []Layer3Ipv6DnsServerSourceManualSuffix) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + for _, a := range a { + for _, b := range b { + if !util.Ints64Match(a.Lifetime, b.Lifetime) { + return false + } + if !util.StringsEqual(a.Name, b.Name) { + return false + } + } + } + return true +} +func matchLayer3Ipv6DnsServerSourceManual(a *Layer3Ipv6DnsServerSourceManual, b *Layer3Ipv6DnsServerSourceManual) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !matchLayer3Ipv6DnsServerSourceManualSuffix(a.Suffix, b.Suffix) { + return false + } + return true +} +func matchLayer3Ipv6DnsServerSourceDhcpv6(a *Layer3Ipv6DnsServerSourceDhcpv6, b *Layer3Ipv6DnsServerSourceDhcpv6) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.StringsMatch(a.PrefixPool, b.PrefixPool) { + return false + } + return true +} +func matchLayer3Ipv6DnsServerSource(a *Layer3Ipv6DnsServerSource, b *Layer3Ipv6DnsServerSource) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !matchLayer3Ipv6DnsServerSourceDhcpv6(a.Dhcpv6, b.Dhcpv6) { + return false + } + if !matchLayer3Ipv6DnsServerSourceManual(a.Manual, b.Manual) { + return false + } + return true +} +func matchLayer3Ipv6DnsServerDnsSupportServer(a []Layer3Ipv6DnsServerDnsSupportServer, b []Layer3Ipv6DnsServerDnsSupportServer) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + for _, a := range a { + for _, b := range b { + if !util.Ints64Match(a.Lifetime, b.Lifetime) { + return false + } + if !util.StringsEqual(a.Name, b.Name) { + return false + } + } + } + return true +} +func matchLayer3Ipv6DnsServerDnsSupportSuffix(a []Layer3Ipv6DnsServerDnsSupportSuffix, b []Layer3Ipv6DnsServerDnsSupportSuffix) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + for _, a := range a { + for _, b := range b { + if !util.StringsEqual(a.Name, b.Name) { + return false + } + if !util.Ints64Match(a.Lifetime, b.Lifetime) { + return false + } + } + } + return true +} +func matchLayer3Ipv6DnsServerDnsSupport(a *Layer3Ipv6DnsServerDnsSupport, b *Layer3Ipv6DnsServerDnsSupport) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.BoolsMatch(a.Enable, b.Enable) { + return false + } + if !matchLayer3Ipv6DnsServerDnsSupportServer(a.Server, b.Server) { + return false + } + if !matchLayer3Ipv6DnsServerDnsSupportSuffix(a.Suffix, b.Suffix) { + return false + } + return true +} +func matchLayer3Ipv6DnsServer(a *Layer3Ipv6DnsServer, b *Layer3Ipv6DnsServer) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.BoolsMatch(a.Enable, b.Enable) { + return false + } + if !matchLayer3Ipv6DnsServerSource(a.Source, b.Source) { + return false + } + if !matchLayer3Ipv6DnsServerDnsSupport(a.DnsSupport, b.DnsSupport) { + return false + } + return true +} +func matchLayer3Ipv6(a *Layer3Ipv6, b *Layer3Ipv6) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.BoolsMatch(a.Enabled, b.Enabled) { + return false + } + if !util.StringsMatch(a.InterfaceId, b.InterfaceId) { + return false + } + if !matchLayer3Ipv6Addresses(a.Addresses, b.Addresses) { + return false + } + if !matchLayer3Ipv6NeighborDiscovery(a.NeighborDiscovery, b.NeighborDiscovery) { + return false + } + if !matchLayer3Ipv6DnsServer(a.DnsServer, b.DnsServer) { + return false + } + return true +} +func matchLayer3(a *Layer3, b *Layer3) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !matchLayer3DhcpClient(a.DhcpClient, b.DhcpClient) { + return false + } + if !util.StringsMatch(a.InterfaceManagementProfile, b.InterfaceManagementProfile) { + return false + } + if !util.StringsMatch(a.NetflowProfile, b.NetflowProfile) { + return false + } + if !matchLayer3Bonjour(a.Bonjour, b.Bonjour) { + return false + } + if !matchLayer3SdwanLinkSettings(a.SdwanLinkSettings, b.SdwanLinkSettings) { + return false + } + if !util.BoolsMatch(a.UntaggedSubInterface, b.UntaggedSubInterface) { + return false + } + if !matchLayer3AdjustTcpMss(a.AdjustTcpMss, b.AdjustTcpMss) { + return false + } + if !matchLayer3Arp(a.Arp, b.Arp) { + return false + } + if !util.BoolsMatch(a.NdpProxy, b.NdpProxy) { + return false + } + if !matchLayer3Lldp(a.Lldp, b.Lldp) { + return false + } + if !util.Ints64Match(a.Mtu, b.Mtu) { + return false + } + if !matchLayer3Ips(a.Ips, b.Ips) { + return false + } + if !matchLayer3Ipv6(a.Ipv6, b.Ipv6) { + return false + } + return true +} +func matchTap(a *Tap, b *Tap) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.StringsMatch(a.NetflowProfile, b.NetflowProfile) { + return false + } + return true +} + +func (o *Entry) EntryName() string { + return o.Name +} + +func (o *Entry) SetEntryName(name string) { + o.Name = name +} diff --git a/network/interface/ethernet/interfaces.go b/network/interface/ethernet/interfaces.go new file mode 100644 index 0000000..45b962d --- /dev/null +++ b/network/interface/ethernet/interfaces.go @@ -0,0 +1,7 @@ +package ethernet + +type Specifier func(*Entry) (any, error) + +type Normalizer interface { + Normalize() ([]*Entry, error) +} diff --git a/network/interface/ethernet/location.go b/network/interface/ethernet/location.go new file mode 100644 index 0000000..e64d895 --- /dev/null +++ b/network/interface/ethernet/location.go @@ -0,0 +1,606 @@ +package ethernet + +import ( + "encoding/xml" + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +type ImportLocation interface { + XpathForLocation(version.Number, util.ILocation) ([]string, error) + MarshalPangoXML([]string) (string, error) + UnmarshalPangoXML([]byte) ([]string, error) +} +type Layer3TemplateType int + +const ( + layer3TemplateVsys Layer3TemplateType = iota + layer3TemplateZone Layer3TemplateType = iota + layer3TemplateVirtualRouter Layer3TemplateType = iota + layer3TemplateLogicalRouter Layer3TemplateType = iota +) + +type Layer3TemplateImportLocation struct { + typ Layer3TemplateType + vsys *Layer3TemplateVsysImportLocation + zone *Layer3TemplateZoneImportLocation + virtualRouter *Layer3TemplateVirtualRouterImportLocation + logicalRouter *Layer3TemplateLogicalRouterImportLocation +} + +type Layer3TemplateVsysImportLocation struct { + xpath []string + vsys string +} + +type Layer3TemplateVsysImportLocationSpec struct { + Vsys string +} + +func NewLayer3TemplateVsysImportLocation(spec Layer3TemplateVsysImportLocationSpec) *Layer3TemplateImportLocation { + location := &Layer3TemplateVsysImportLocation{ + vsys: spec.Vsys, + } + + return &Layer3TemplateImportLocation{ + typ: layer3TemplateVsys, + vsys: location, + } +} + +func (o *Layer3TemplateVsysImportLocation) XpathForLocation(vn version.Number, loc util.ILocation) ([]string, error) { + ans, err := loc.XpathPrefix(vn) + if err != nil { + return nil, err + } + + importAns := []string{ + "vsys", + util.AsEntryXpath([]string{o.vsys}), + "import", + "network", + "interface", + } + + return append(ans, importAns...), nil +} + +func (o *Layer3TemplateVsysImportLocation) MarshalPangoXML(interfaces []string) (string, error) { + type member struct { + Name string `xml:",chardata"` + } + + type request struct { + XMLName xml.Name `xml:"interface"` + Members []member `xml:"member"` + } + + var members []member + for _, elt := range interfaces { + members = append(members, member{Name: elt}) + } + + expected := request{ + Members: members, + } + bytes, err := xml.Marshal(expected) + if err != nil { + return "", err + } + + return string(bytes), nil +} + +func (o *Layer3TemplateVsysImportLocation) UnmarshalPangoXML(bytes []byte) ([]string, error) { + type member struct { + Name string `xml:",chardata"` + } + + type response struct { + Members []member `xml:"result>interface>member"` + } + + var existing response + err := xml.Unmarshal(bytes, &existing) + if err != nil { + return nil, err + } + + var interfaces []string + for _, elt := range existing.Members { + interfaces = append(interfaces, elt.Name) + } + + return interfaces, nil +} + +type Layer3TemplateZoneImportLocation struct { + xpath []string + vsys string + zone string +} + +type Layer3TemplateZoneImportLocationSpec struct { + Vsys string + Zone string +} + +func NewLayer3TemplateZoneImportLocation(spec Layer3TemplateZoneImportLocationSpec) *Layer3TemplateImportLocation { + location := &Layer3TemplateZoneImportLocation{ + vsys: spec.Vsys, + zone: spec.Zone, + } + + return &Layer3TemplateImportLocation{ + typ: layer3TemplateZone, + zone: location, + } +} + +func (o *Layer3TemplateZoneImportLocation) XpathForLocation(vn version.Number, loc util.ILocation) ([]string, error) { + ans, err := loc.XpathPrefix(vn) + if err != nil { + return nil, err + } + + importAns := []string{ + "vsys", + util.AsEntryXpath([]string{o.vsys}), + "zone", + util.AsEntryXpath([]string{o.zone}), + "network", + "layer3", + } + + return append(ans, importAns...), nil +} + +func (o *Layer3TemplateZoneImportLocation) MarshalPangoXML(interfaces []string) (string, error) { + type member struct { + Name string `xml:",chardata"` + } + + type request struct { + XMLName xml.Name `xml:"layer3"` + Members []member `xml:"member"` + } + + var members []member + for _, elt := range interfaces { + members = append(members, member{Name: elt}) + } + + expected := request{ + Members: members, + } + bytes, err := xml.Marshal(expected) + if err != nil { + return "", err + } + + return string(bytes), nil +} + +func (o *Layer3TemplateZoneImportLocation) UnmarshalPangoXML(bytes []byte) ([]string, error) { + type member struct { + Name string `xml:",chardata"` + } + + type response struct { + Members []member `xml:"result>layer3>member"` + } + + var existing response + err := xml.Unmarshal(bytes, &existing) + if err != nil { + return nil, err + } + + var interfaces []string + for _, elt := range existing.Members { + interfaces = append(interfaces, elt.Name) + } + + return interfaces, nil +} + +type Layer3TemplateVirtualRouterImportLocation struct { + xpath []string + router string + vsys string +} + +type Layer3TemplateVirtualRouterImportLocationSpec struct { + Router string + Vsys string +} + +func NewLayer3TemplateVirtualRouterImportLocation(spec Layer3TemplateVirtualRouterImportLocationSpec) *Layer3TemplateImportLocation { + location := &Layer3TemplateVirtualRouterImportLocation{ + router: spec.Router, + vsys: spec.Vsys, + } + + return &Layer3TemplateImportLocation{ + typ: layer3TemplateVirtualRouter, + virtualRouter: location, + } +} + +func (o *Layer3TemplateVirtualRouterImportLocation) XpathForLocation(vn version.Number, loc util.ILocation) ([]string, error) { + ans, err := loc.XpathPrefix(vn) + if err != nil { + return nil, err + } + + importAns := []string{ + "network", + "virtual-router", + util.AsEntryXpath([]string{o.router}), + "interface", + } + + return append(ans, importAns...), nil +} + +func (o *Layer3TemplateVirtualRouterImportLocation) MarshalPangoXML(interfaces []string) (string, error) { + type member struct { + Name string `xml:",chardata"` + } + + type request struct { + XMLName xml.Name `xml:"interface"` + Members []member `xml:"member"` + } + + var members []member + for _, elt := range interfaces { + members = append(members, member{Name: elt}) + } + + expected := request{ + Members: members, + } + bytes, err := xml.Marshal(expected) + if err != nil { + return "", err + } + + return string(bytes), nil +} + +func (o *Layer3TemplateVirtualRouterImportLocation) UnmarshalPangoXML(bytes []byte) ([]string, error) { + type member struct { + Name string `xml:",chardata"` + } + + type response struct { + Members []member `xml:"result>interface>member"` + } + + var existing response + err := xml.Unmarshal(bytes, &existing) + if err != nil { + return nil, err + } + + var interfaces []string + for _, elt := range existing.Members { + interfaces = append(interfaces, elt.Name) + } + + return interfaces, nil +} + +type Layer3TemplateLogicalRouterImportLocation struct { + xpath []string + router string + vrf string + vsys string +} + +type Layer3TemplateLogicalRouterImportLocationSpec struct { + Router string + Vrf string + Vsys string +} + +func NewLayer3TemplateLogicalRouterImportLocation(spec Layer3TemplateLogicalRouterImportLocationSpec) *Layer3TemplateImportLocation { + location := &Layer3TemplateLogicalRouterImportLocation{ + router: spec.Router, + vrf: spec.Vrf, + vsys: spec.Vsys, + } + + return &Layer3TemplateImportLocation{ + typ: layer3TemplateLogicalRouter, + logicalRouter: location, + } +} + +func (o *Layer3TemplateLogicalRouterImportLocation) XpathForLocation(vn version.Number, loc util.ILocation) ([]string, error) { + ans, err := loc.XpathPrefix(vn) + if err != nil { + return nil, err + } + + importAns := []string{ + "network", + "logical-router", + util.AsEntryXpath([]string{o.router}), + "vrf", + util.AsEntryXpath([]string{o.vrf}), + "interface", + } + + return append(ans, importAns...), nil +} + +func (o *Layer3TemplateLogicalRouterImportLocation) MarshalPangoXML(interfaces []string) (string, error) { + type member struct { + Name string `xml:",chardata"` + } + + type request struct { + XMLName xml.Name `xml:"interface"` + Members []member `xml:"member"` + } + + var members []member + for _, elt := range interfaces { + members = append(members, member{Name: elt}) + } + + expected := request{ + Members: members, + } + bytes, err := xml.Marshal(expected) + if err != nil { + return "", err + } + + return string(bytes), nil +} + +func (o *Layer3TemplateLogicalRouterImportLocation) UnmarshalPangoXML(bytes []byte) ([]string, error) { + type member struct { + Name string `xml:",chardata"` + } + + type response struct { + Members []member `xml:"result>interface>member"` + } + + var existing response + err := xml.Unmarshal(bytes, &existing) + if err != nil { + return nil, err + } + + var interfaces []string + for _, elt := range existing.Members { + interfaces = append(interfaces, elt.Name) + } + + return interfaces, nil +} + +func (o *Layer3TemplateImportLocation) MarshalPangoXML(interfaces []string) (string, error) { + switch o.typ { + case layer3TemplateVsys: + return o.vsys.MarshalPangoXML(interfaces) + case layer3TemplateZone: + return o.zone.MarshalPangoXML(interfaces) + case layer3TemplateVirtualRouter: + return o.virtualRouter.MarshalPangoXML(interfaces) + case layer3TemplateLogicalRouter: + return o.logicalRouter.MarshalPangoXML(interfaces) + default: + return "", fmt.Errorf("invalid import location") + } +} + +func (o *Layer3TemplateImportLocation) UnmarshalPangoXML(bytes []byte) ([]string, error) { + switch o.typ { + case layer3TemplateVsys: + return o.vsys.UnmarshalPangoXML(bytes) + case layer3TemplateZone: + return o.zone.UnmarshalPangoXML(bytes) + case layer3TemplateVirtualRouter: + return o.virtualRouter.UnmarshalPangoXML(bytes) + case layer3TemplateLogicalRouter: + return o.logicalRouter.UnmarshalPangoXML(bytes) + default: + return nil, fmt.Errorf("invalid import location") + } +} + +func (o *Layer3TemplateImportLocation) XpathForLocation(vn version.Number, loc util.ILocation) ([]string, error) { + switch o.typ { + case layer3TemplateVsys: + return o.vsys.XpathForLocation(vn, loc) + case layer3TemplateZone: + return o.zone.XpathForLocation(vn, loc) + case layer3TemplateVirtualRouter: + return o.virtualRouter.XpathForLocation(vn, loc) + case layer3TemplateLogicalRouter: + return o.logicalRouter.XpathForLocation(vn, loc) + default: + return nil, fmt.Errorf("invalid import location") + } +} + +type Location struct { + Ngfw *NgfwLocation `json:"ngfw,omitempty"` + Template *TemplateLocation `json:"template,omitempty"` + TemplateStack *TemplateStackLocation `json:"template_stack,omitempty"` +} + +type NgfwLocation struct { + NgfwDevice string `json:"ngfw_device"` +} + +type TemplateLocation struct { + NgfwDevice string `json:"ngfw_device"` + PanoramaDevice string `json:"panorama_device"` + Template string `json:"template"` +} + +type TemplateStackLocation struct { + NgfwDevice string `json:"ngfw_device"` + PanoramaDevice string `json:"panorama_device"` + TemplateStack string `json:"template_stack"` +} + +func NewNgfwLocation() *Location { + return &Location{Ngfw: &NgfwLocation{ + NgfwDevice: "localhost.localdomain", + }, + } +} +func NewTemplateLocation() *Location { + return &Location{Template: &TemplateLocation{ + NgfwDevice: "localhost.localdomain", + PanoramaDevice: "localhost.localdomain", + Template: "", + }, + } +} +func NewTemplateStackLocation() *Location { + return &Location{TemplateStack: &TemplateStackLocation{ + NgfwDevice: "localhost.localdomain", + PanoramaDevice: "localhost.localdomain", + TemplateStack: "", + }, + } +} + +func (o Location) IsValid() error { + count := 0 + + switch { + case o.Ngfw != nil: + if o.Ngfw.NgfwDevice == "" { + return fmt.Errorf("NgfwDevice is unspecified") + } + count++ + case o.Template != nil: + if o.Template.NgfwDevice == "" { + return fmt.Errorf("NgfwDevice is unspecified") + } + if o.Template.PanoramaDevice == "" { + return fmt.Errorf("PanoramaDevice is unspecified") + } + if o.Template.Template == "" { + return fmt.Errorf("Template is unspecified") + } + count++ + case o.TemplateStack != nil: + if o.TemplateStack.NgfwDevice == "" { + return fmt.Errorf("NgfwDevice is unspecified") + } + if o.TemplateStack.PanoramaDevice == "" { + return fmt.Errorf("PanoramaDevice is unspecified") + } + if o.TemplateStack.TemplateStack == "" { + return fmt.Errorf("TemplateStack is unspecified") + } + count++ + } + + if count == 0 { + return fmt.Errorf("no path specified") + } + + if count > 1 { + return fmt.Errorf("multiple paths specified: only one should be specified") + } + + return nil +} + +func (o Location) XpathPrefix(vn version.Number) ([]string, error) { + + var ans []string + + switch { + case o.Ngfw != nil: + if o.Ngfw.NgfwDevice == "" { + return nil, fmt.Errorf("NgfwDevice is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.Ngfw.NgfwDevice}), + } + case o.Template != nil: + if o.Template.NgfwDevice == "" { + return nil, fmt.Errorf("NgfwDevice is unspecified") + } + if o.Template.PanoramaDevice == "" { + return nil, fmt.Errorf("PanoramaDevice is unspecified") + } + if o.Template.Template == "" { + return nil, fmt.Errorf("Template is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.Template.PanoramaDevice}), + "template", + util.AsEntryXpath([]string{o.Template.Template}), + "config", + "devices", + util.AsEntryXpath([]string{o.Template.NgfwDevice}), + } + case o.TemplateStack != nil: + if o.TemplateStack.NgfwDevice == "" { + return nil, fmt.Errorf("NgfwDevice is unspecified") + } + if o.TemplateStack.PanoramaDevice == "" { + return nil, fmt.Errorf("PanoramaDevice is unspecified") + } + if o.TemplateStack.TemplateStack == "" { + return nil, fmt.Errorf("TemplateStack is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.TemplateStack.PanoramaDevice}), + "template-stack", + util.AsEntryXpath([]string{o.TemplateStack.TemplateStack}), + "config", + "devices", + util.AsEntryXpath([]string{o.TemplateStack.NgfwDevice}), + } + default: + return nil, errors.NoLocationSpecifiedError + } + + return ans, nil +} +func (o Location) XpathWithEntryName(vn version.Number, name string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsEntryXpath([]string{name})) + + return ans, nil +} +func (o Location) XpathWithUuid(vn version.Number, uuid string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsUuidXpath(uuid)) + + return ans, nil +} diff --git a/network/interface/ethernet/service.go b/network/interface/ethernet/service.go new file mode 100644 index 0000000..7ad26bb --- /dev/null +++ b/network/interface/ethernet/service.go @@ -0,0 +1,388 @@ +package ethernet + +import ( + "context" + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/xmlapi" +) + +type Service struct { + client util.PangoClient +} + +func NewService(client util.PangoClient) *Service { + return &Service{ + client: client, + } +} + +// Create adds new item, then returns the result. +func (s *Service) Create(ctx context.Context, loc Location, importLocations []ImportLocation, entry *Entry) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + path, err := loc.XpathWithEntryName(vn, entry.Name) + if err != nil { + return nil, err + } + createSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + cmd := &xmlapi.Config{ + Action: "set", + Xpath: util.AsXpath(path[:len(path)-1]), + Element: createSpec, + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, false, nil); err != nil { + return nil, err + } + err = s.importToLocations(ctx, loc, importLocations, entry.Name) + if err != nil { + return nil, err + } + return s.Read(ctx, loc, entry.Name, "get") +} + +func (s *Service) importToLocations(ctx context.Context, loc Location, importLocations []ImportLocation, entryName string) error { + vn := s.client.Versioning() + for _, elt := range importLocations { + xpath, err := elt.XpathForLocation(vn, loc) + + cmd := &xmlapi.Config{ + Action: "get", + Xpath: util.AsXpath(xpath), + } + + bytes, _, err := s.client.Communicate(ctx, cmd, false, nil) + if err != nil && !errors.IsObjectNotFound(err) { + return err + } + + existing, err := elt.UnmarshalPangoXML(bytes) + if err != nil { + return err + } + + for _, elt := range existing { + if elt == entryName { + return nil + } + } + + existing = append(existing, entryName) + + element, err := elt.MarshalPangoXML(existing) + if err != nil { + return err + } + + cmd = &xmlapi.Config{ + Action: "set", + Xpath: util.AsXpath(xpath[:len(xpath)-1]), + Element: element, + } + + _, _, err = s.client.Communicate(ctx, cmd, false, nil) + if err != nil { + return err + } + } + + return nil +} + +func (s *Service) unimportFromLocations(ctx context.Context, updates *xmlapi.MultiConfig, loc Location, importLocations []ImportLocation, values []string) error { + vn := s.client.Versioning() + valuesByName := make(map[string]bool) + for _, elt := range values { + valuesByName[elt] = true + } + for _, elt := range importLocations { + xpath, err := elt.XpathForLocation(vn, loc) + + cmd := &xmlapi.Config{ + Action: "get", + Xpath: util.AsXpath(xpath), + } + + bytes, _, err := s.client.Communicate(ctx, cmd, false, nil) + if err != nil && !errors.IsObjectNotFound(err) { + return err + } + + existing, err := elt.UnmarshalPangoXML(bytes) + if err != nil { + return err + } + + var filtered []string + for _, elt := range existing { + if _, found := valuesByName[elt]; !found { + filtered = append(filtered, elt) + } + } + + element, err := elt.MarshalPangoXML(filtered) + if err != nil { + return err + } + + cmd = &xmlapi.Config{ + Action: "edit", + Xpath: util.AsXpath(xpath), + Element: element, + } + + _, _, err = s.client.Communicate(ctx, cmd, false, nil) + if err != nil { + return err + } + } + + return nil +} + +// Read returns the given config object, using the specified action ("get" or "show"). +func (s *Service) Read(ctx context.Context, loc Location, name, action string) (*Entry, error) { + return s.read(ctx, loc, name, action, false) +} + +// ReadFromConfig returns the given config object from the loaded XML config. +// Requires that client.LoadPanosConfig() has been invoked. +func (s *Service) ReadFromConfig(ctx context.Context, loc Location, name string) (*Entry, error) { + return s.read(ctx, loc, name, "", true) +} + +func (s *Service) read(ctx context.Context, loc Location, value, action string, usePanosConfig bool) (*Entry, error) { + if value == "" { + return nil, errors.NameNotSpecifiedError + } + vn := s.client.Versioning() + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + var path []string + path, err = loc.XpathWithEntryName(vn, value) + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, true, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, errors.ObjectNotFound() + } + return nil, err + } + } + + list, err := normalizer.Normalize() + if err != nil { + return nil, err + } else if len(list) != 1 { + return nil, fmt.Errorf("expected to %q 1 entry, got %d", action, len(list)) + } + + return list[0], nil +} + +// Update updates the given config object, then returns the result. +func (s *Service) Update(ctx context.Context, loc Location, entry *Entry, name string) (*Entry, error) { + return s.update(ctx, loc, entry, name) +} +func (s *Service) update(ctx context.Context, loc Location, entry *Entry, value string) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + updates := xmlapi.NewMultiConfig(2) + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + var old *Entry + if value != "" && value != entry.Name { + path, err := loc.XpathWithEntryName(vn, value) + if err != nil { + return nil, err + } + + old, err = s.Read(ctx, loc, value, "get") + + updates.Add(&xmlapi.Config{ + Action: "rename", + Xpath: util.AsXpath(path), + NewName: entry.Name, + Target: s.client.GetTarget(), + }) + } else { + old, err = s.Read(ctx, loc, entry.Name, "get") + } + if err != nil { + return nil, err + } else if old == nil { + return nil, fmt.Errorf("previous object doesn't exist for update") + } + if !SpecMatches(entry, old) { + path, err := loc.XpathWithEntryName(vn, entry.Name) + if err != nil { + return nil, err + } + + updateSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + updates.Add(&xmlapi.Config{ + Action: "edit", + Xpath: util.AsXpath(path), + Element: updateSpec, + Target: s.client.GetTarget(), + }) + } + + if len(updates.Operations) != 0 { + if _, _, _, err = s.client.MultiConfig(ctx, updates, false, nil); err != nil { + return nil, err + } + } + return s.Read(ctx, loc, entry.Name, "get") +} + +// Delete deletes the given item. +func (s *Service) Delete(ctx context.Context, loc Location, importLocations []ImportLocation, name ...string) error { + return s.delete(ctx, loc, importLocations, name) +} +func (s *Service) delete(ctx context.Context, loc Location, importLocations []ImportLocation, values []string) error { + for _, value := range values { + if value == "" { + return errors.NameNotSpecifiedError + } + } + + vn := s.client.Versioning() + var err error + deletes := xmlapi.NewMultiConfig(len(values)) + err = s.unimportFromLocations(ctx, deletes, loc, importLocations, values) + if err != nil { + return err + } + for _, value := range values { + var path []string + path, err = loc.XpathWithEntryName(vn, value) + if err != nil { + return err + } + deletes.Add(&xmlapi.Config{ + Action: "delete", + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + }) + } + + _, _, _, err = s.client.MultiConfig(ctx, deletes, false, nil) + + return err +} + +// List returns a list of objects using the given action ("get" or "show"). +// Params filter and quote are for client side filtering. +func (s *Service) List(ctx context.Context, loc Location, action, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, action, filter, quote, false) +} + +// ListFromConfig returns a list of objects at the given location. +// Requires that client.LoadPanosConfig() has been invoked. +// Params filter and quote are for client side filtering. +func (s *Service) ListFromConfig(ctx context.Context, loc Location, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, "", filter, quote, true) +} + +func (s *Service) list(ctx context.Context, loc Location, action, filter, quote string, usePanosConfig bool) ([]*Entry, error) { + var err error + + var logic *filtering.Group + if filter != "" { + logic, err = filtering.Parse(filter, quote) + if err != nil { + return nil, err + } + } + + vn := s.client.Versioning() + + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + + path, err := loc.XpathWithEntryName(vn, "") + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, false, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, nil + } + return nil, err + } + } + + listing, err := normalizer.Normalize() + if err != nil || logic == nil { + return listing, err + } + + filtered := make([]*Entry, 0, len(listing)) + for _, x := range listing { + ok, err := logic.Matches(x) + if err != nil { + return nil, err + } + if ok { + filtered = append(filtered, x) + } + } + + return filtered, nil +} diff --git a/network/interface/loopback/entry.go b/network/interface/loopback/entry.go new file mode 100644 index 0000000..2e24574 --- /dev/null +++ b/network/interface/loopback/entry.go @@ -0,0 +1,339 @@ +package loopback + +import ( + "encoding/xml" + "fmt" + + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/generic" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +var ( + _ filtering.Fielder = &Entry{} +) + +var ( + Suffix = []string{"network", "interface", "loopback", "units"} +) + +type Entry struct { + Name string + AdjustTcpMss *AdjustTcpMss + Comment *string + InterfaceManagementProfile *string + Ips []string + Ipv6 *Ipv6 + Mtu *int64 + NetflowProfile *string + + Misc map[string][]generic.Xml +} + +type AdjustTcpMss struct { + Enable *bool + Ipv4MssAdjustment *int64 + Ipv6MssAdjustment *int64 +} +type Ipv6 struct { + Addresses []Ipv6Addresses + Enabled *bool +} +type Ipv6Addresses struct { + EnableOnInterface *bool + Name string +} + +type entryXmlContainer struct { + Answer []entryXml `xml:"entry"` +} + +type entryXml struct { + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + AdjustTcpMss *AdjustTcpMssXml `xml:"adjust-tcp-mss,omitempty"` + Comment *string `xml:"comment,omitempty"` + InterfaceManagementProfile *string `xml:"interface-management-profile,omitempty"` + Ips *util.EntryType `xml:"ip,omitempty"` + Ipv6 *Ipv6Xml `xml:"ipv6,omitempty"` + Mtu *int64 `xml:"mtu,omitempty"` + NetflowProfile *string `xml:"netflow-profile,omitempty"` + + Misc []generic.Xml `xml:",any"` +} + +type AdjustTcpMssXml struct { + Enable *string `xml:"enable,omitempty"` + Ipv4MssAdjustment *int64 `xml:"ipv4-mss-adjustment,omitempty"` + Ipv6MssAdjustment *int64 `xml:"ipv6-mss-adjustment,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type Ipv6Xml struct { + Addresses []Ipv6AddressesXml `xml:"address>entry,omitempty"` + Enabled *string `xml:"enabled,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type Ipv6AddressesXml struct { + EnableOnInterface *string `xml:"enable-on-interface,omitempty"` + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + + Misc []generic.Xml `xml:",any"` +} + +func (e *Entry) Field(v string) (any, error) { + if v == "name" || v == "Name" { + return e.Name, nil + } + if v == "adjust_tcp_mss" || v == "AdjustTcpMss" { + return e.AdjustTcpMss, nil + } + if v == "comment" || v == "Comment" { + return e.Comment, nil + } + if v == "interface_management_profile" || v == "InterfaceManagementProfile" { + return e.InterfaceManagementProfile, nil + } + if v == "ips" || v == "Ips" { + return e.Ips, nil + } + if v == "ips|LENGTH" || v == "Ips|LENGTH" { + return int64(len(e.Ips)), nil + } + if v == "ipv6" || v == "Ipv6" { + return e.Ipv6, nil + } + if v == "mtu" || v == "Mtu" { + return e.Mtu, nil + } + if v == "netflow_profile" || v == "NetflowProfile" { + return e.NetflowProfile, nil + } + + return nil, fmt.Errorf("unknown field") +} + +func Versioning(vn version.Number) (Specifier, Normalizer, error) { + return specifyEntry, &entryXmlContainer{}, nil +} + +func specifyEntry(o *Entry) (any, error) { + entry := entryXml{} + + entry.Name = o.Name + var nestedAdjustTcpMss *AdjustTcpMssXml + if o.AdjustTcpMss != nil { + nestedAdjustTcpMss = &AdjustTcpMssXml{} + if _, ok := o.Misc["AdjustTcpMss"]; ok { + nestedAdjustTcpMss.Misc = o.Misc["AdjustTcpMss"] + } + if o.AdjustTcpMss.Enable != nil { + nestedAdjustTcpMss.Enable = util.YesNo(o.AdjustTcpMss.Enable, nil) + } + if o.AdjustTcpMss.Ipv4MssAdjustment != nil { + nestedAdjustTcpMss.Ipv4MssAdjustment = o.AdjustTcpMss.Ipv4MssAdjustment + } + if o.AdjustTcpMss.Ipv6MssAdjustment != nil { + nestedAdjustTcpMss.Ipv6MssAdjustment = o.AdjustTcpMss.Ipv6MssAdjustment + } + } + entry.AdjustTcpMss = nestedAdjustTcpMss + + entry.Comment = o.Comment + entry.InterfaceManagementProfile = o.InterfaceManagementProfile + entry.Ips = util.StrToEnt(o.Ips) + var nestedIpv6 *Ipv6Xml + if o.Ipv6 != nil { + nestedIpv6 = &Ipv6Xml{} + if _, ok := o.Misc["Ipv6"]; ok { + nestedIpv6.Misc = o.Misc["Ipv6"] + } + if o.Ipv6.Enabled != nil { + nestedIpv6.Enabled = util.YesNo(o.Ipv6.Enabled, nil) + } + if o.Ipv6.Addresses != nil { + nestedIpv6.Addresses = []Ipv6AddressesXml{} + for _, oIpv6Addresses := range o.Ipv6.Addresses { + nestedIpv6Addresses := Ipv6AddressesXml{} + if _, ok := o.Misc["Ipv6Addresses"]; ok { + nestedIpv6Addresses.Misc = o.Misc["Ipv6Addresses"] + } + if oIpv6Addresses.EnableOnInterface != nil { + nestedIpv6Addresses.EnableOnInterface = util.YesNo(oIpv6Addresses.EnableOnInterface, nil) + } + if oIpv6Addresses.Name != "" { + nestedIpv6Addresses.Name = oIpv6Addresses.Name + } + nestedIpv6.Addresses = append(nestedIpv6.Addresses, nestedIpv6Addresses) + } + } + } + entry.Ipv6 = nestedIpv6 + + entry.Mtu = o.Mtu + entry.NetflowProfile = o.NetflowProfile + + entry.Misc = o.Misc["Entry"] + + return entry, nil +} +func (c *entryXmlContainer) Normalize() ([]*Entry, error) { + entryList := make([]*Entry, 0, len(c.Answer)) + for _, o := range c.Answer { + entry := &Entry{ + Misc: make(map[string][]generic.Xml), + } + entry.Name = o.Name + var nestedAdjustTcpMss *AdjustTcpMss + if o.AdjustTcpMss != nil { + nestedAdjustTcpMss = &AdjustTcpMss{} + if o.AdjustTcpMss.Misc != nil { + entry.Misc["AdjustTcpMss"] = o.AdjustTcpMss.Misc + } + if o.AdjustTcpMss.Enable != nil { + nestedAdjustTcpMss.Enable = util.AsBool(o.AdjustTcpMss.Enable, nil) + } + if o.AdjustTcpMss.Ipv4MssAdjustment != nil { + nestedAdjustTcpMss.Ipv4MssAdjustment = o.AdjustTcpMss.Ipv4MssAdjustment + } + if o.AdjustTcpMss.Ipv6MssAdjustment != nil { + nestedAdjustTcpMss.Ipv6MssAdjustment = o.AdjustTcpMss.Ipv6MssAdjustment + } + } + entry.AdjustTcpMss = nestedAdjustTcpMss + + entry.Comment = o.Comment + entry.InterfaceManagementProfile = o.InterfaceManagementProfile + entry.Ips = util.EntToStr(o.Ips) + var nestedIpv6 *Ipv6 + if o.Ipv6 != nil { + nestedIpv6 = &Ipv6{} + if o.Ipv6.Misc != nil { + entry.Misc["Ipv6"] = o.Ipv6.Misc + } + if o.Ipv6.Enabled != nil { + nestedIpv6.Enabled = util.AsBool(o.Ipv6.Enabled, nil) + } + if o.Ipv6.Addresses != nil { + nestedIpv6.Addresses = []Ipv6Addresses{} + for _, oIpv6Addresses := range o.Ipv6.Addresses { + nestedIpv6Addresses := Ipv6Addresses{} + if oIpv6Addresses.Misc != nil { + entry.Misc["Ipv6Addresses"] = oIpv6Addresses.Misc + } + if oIpv6Addresses.EnableOnInterface != nil { + nestedIpv6Addresses.EnableOnInterface = util.AsBool(oIpv6Addresses.EnableOnInterface, nil) + } + if oIpv6Addresses.Name != "" { + nestedIpv6Addresses.Name = oIpv6Addresses.Name + } + nestedIpv6.Addresses = append(nestedIpv6.Addresses, nestedIpv6Addresses) + } + } + } + entry.Ipv6 = nestedIpv6 + + entry.Mtu = o.Mtu + entry.NetflowProfile = o.NetflowProfile + + entry.Misc["Entry"] = o.Misc + + entryList = append(entryList, entry) + } + + return entryList, nil +} + +func SpecMatches(a, b *Entry) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + + // Don't compare Name. + if !matchAdjustTcpMss(a.AdjustTcpMss, b.AdjustTcpMss) { + return false + } + if !util.StringsMatch(a.Comment, b.Comment) { + return false + } + if !util.StringsMatch(a.InterfaceManagementProfile, b.InterfaceManagementProfile) { + return false + } + if !util.OrderedListsMatch(a.Ips, b.Ips) { + return false + } + if !matchIpv6(a.Ipv6, b.Ipv6) { + return false + } + if !util.Ints64Match(a.Mtu, b.Mtu) { + return false + } + if !util.StringsMatch(a.NetflowProfile, b.NetflowProfile) { + return false + } + + return true +} + +func matchAdjustTcpMss(a *AdjustTcpMss, b *AdjustTcpMss) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.BoolsMatch(a.Enable, b.Enable) { + return false + } + if !util.Ints64Match(a.Ipv4MssAdjustment, b.Ipv4MssAdjustment) { + return false + } + if !util.Ints64Match(a.Ipv6MssAdjustment, b.Ipv6MssAdjustment) { + return false + } + return true +} +func matchIpv6Addresses(a []Ipv6Addresses, b []Ipv6Addresses) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + for _, a := range a { + for _, b := range b { + if !util.BoolsMatch(a.EnableOnInterface, b.EnableOnInterface) { + return false + } + if !util.StringsEqual(a.Name, b.Name) { + return false + } + } + } + return true +} +func matchIpv6(a *Ipv6, b *Ipv6) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.BoolsMatch(a.Enabled, b.Enabled) { + return false + } + if !matchIpv6Addresses(a.Addresses, b.Addresses) { + return false + } + return true +} + +func (o *Entry) EntryName() string { + return o.Name +} + +func (o *Entry) SetEntryName(name string) { + o.Name = name +} diff --git a/network/interface/loopback/interfaces.go b/network/interface/loopback/interfaces.go new file mode 100644 index 0000000..96207a8 --- /dev/null +++ b/network/interface/loopback/interfaces.go @@ -0,0 +1,7 @@ +package loopback + +type Specifier func(*Entry) (any, error) + +type Normalizer interface { + Normalize() ([]*Entry, error) +} diff --git a/network/interface/loopback/location.go b/network/interface/loopback/location.go new file mode 100644 index 0000000..f0db0be --- /dev/null +++ b/network/interface/loopback/location.go @@ -0,0 +1,187 @@ +package loopback + +import ( + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +type ImportLocation interface { + XpathForLocation(version.Number, util.ILocation) ([]string, error) + MarshalPangoXML([]string) (string, error) + UnmarshalPangoXML([]byte) ([]string, error) +} + +type Location struct { + Ngfw *NgfwLocation `json:"ngfw,omitempty"` + Template *TemplateLocation `json:"template,omitempty"` + TemplateStack *TemplateStackLocation `json:"template_stack,omitempty"` +} + +type NgfwLocation struct { + NgfwDevice string `json:"ngfw_device"` +} + +type TemplateLocation struct { + NgfwDevice string `json:"ngfw_device"` + PanoramaDevice string `json:"panorama_device"` + Template string `json:"template"` +} + +type TemplateStackLocation struct { + NgfwDevice string `json:"ngfw_device"` + PanoramaDevice string `json:"panorama_device"` + TemplateStack string `json:"template_stack"` +} + +func NewNgfwLocation() *Location { + return &Location{Ngfw: &NgfwLocation{ + NgfwDevice: "localhost.localdomain", + }, + } +} +func NewTemplateLocation() *Location { + return &Location{Template: &TemplateLocation{ + NgfwDevice: "localhost.localdomain", + PanoramaDevice: "localhost.localdomain", + Template: "", + }, + } +} +func NewTemplateStackLocation() *Location { + return &Location{TemplateStack: &TemplateStackLocation{ + NgfwDevice: "localhost.localdomain", + PanoramaDevice: "localhost.localdomain", + TemplateStack: "", + }, + } +} + +func (o Location) IsValid() error { + count := 0 + + switch { + case o.Ngfw != nil: + if o.Ngfw.NgfwDevice == "" { + return fmt.Errorf("NgfwDevice is unspecified") + } + count++ + case o.Template != nil: + if o.Template.NgfwDevice == "" { + return fmt.Errorf("NgfwDevice is unspecified") + } + if o.Template.PanoramaDevice == "" { + return fmt.Errorf("PanoramaDevice is unspecified") + } + if o.Template.Template == "" { + return fmt.Errorf("Template is unspecified") + } + count++ + case o.TemplateStack != nil: + if o.TemplateStack.NgfwDevice == "" { + return fmt.Errorf("NgfwDevice is unspecified") + } + if o.TemplateStack.PanoramaDevice == "" { + return fmt.Errorf("PanoramaDevice is unspecified") + } + if o.TemplateStack.TemplateStack == "" { + return fmt.Errorf("TemplateStack is unspecified") + } + count++ + } + + if count == 0 { + return fmt.Errorf("no path specified") + } + + if count > 1 { + return fmt.Errorf("multiple paths specified: only one should be specified") + } + + return nil +} + +func (o Location) XpathPrefix(vn version.Number) ([]string, error) { + + var ans []string + + switch { + case o.Ngfw != nil: + if o.Ngfw.NgfwDevice == "" { + return nil, fmt.Errorf("NgfwDevice is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.Ngfw.NgfwDevice}), + } + case o.Template != nil: + if o.Template.NgfwDevice == "" { + return nil, fmt.Errorf("NgfwDevice is unspecified") + } + if o.Template.PanoramaDevice == "" { + return nil, fmt.Errorf("PanoramaDevice is unspecified") + } + if o.Template.Template == "" { + return nil, fmt.Errorf("Template is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.Template.PanoramaDevice}), + "template", + util.AsEntryXpath([]string{o.Template.Template}), + "config", + "devices", + util.AsEntryXpath([]string{o.Template.NgfwDevice}), + } + case o.TemplateStack != nil: + if o.TemplateStack.NgfwDevice == "" { + return nil, fmt.Errorf("NgfwDevice is unspecified") + } + if o.TemplateStack.PanoramaDevice == "" { + return nil, fmt.Errorf("PanoramaDevice is unspecified") + } + if o.TemplateStack.TemplateStack == "" { + return nil, fmt.Errorf("TemplateStack is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.TemplateStack.PanoramaDevice}), + "template-stack", + util.AsEntryXpath([]string{o.TemplateStack.TemplateStack}), + "config", + "devices", + util.AsEntryXpath([]string{o.TemplateStack.NgfwDevice}), + } + default: + return nil, errors.NoLocationSpecifiedError + } + + return ans, nil +} +func (o Location) XpathWithEntryName(vn version.Number, name string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsEntryXpath([]string{name})) + + return ans, nil +} +func (o Location) XpathWithUuid(vn version.Number, uuid string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsUuidXpath(uuid)) + + return ans, nil +} diff --git a/network/interface/loopback/service.go b/network/interface/loopback/service.go new file mode 100644 index 0000000..b088ca0 --- /dev/null +++ b/network/interface/loopback/service.go @@ -0,0 +1,281 @@ +package loopback + +import ( + "context" + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/xmlapi" +) + +type Service struct { + client util.PangoClient +} + +func NewService(client util.PangoClient) *Service { + return &Service{ + client: client, + } +} + +// Create adds new item, then returns the result. +func (s *Service) Create(ctx context.Context, loc Location, entry *Entry) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + path, err := loc.XpathWithEntryName(vn, entry.Name) + if err != nil { + return nil, err + } + createSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + cmd := &xmlapi.Config{ + Action: "set", + Xpath: util.AsXpath(path[:len(path)-1]), + Element: createSpec, + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, false, nil); err != nil { + return nil, err + } + return s.Read(ctx, loc, entry.Name, "get") +} + +// Read returns the given config object, using the specified action ("get" or "show"). +func (s *Service) Read(ctx context.Context, loc Location, name, action string) (*Entry, error) { + return s.read(ctx, loc, name, action, false) +} + +// ReadFromConfig returns the given config object from the loaded XML config. +// Requires that client.LoadPanosConfig() has been invoked. +func (s *Service) ReadFromConfig(ctx context.Context, loc Location, name string) (*Entry, error) { + return s.read(ctx, loc, name, "", true) +} + +func (s *Service) read(ctx context.Context, loc Location, value, action string, usePanosConfig bool) (*Entry, error) { + if value == "" { + return nil, errors.NameNotSpecifiedError + } + vn := s.client.Versioning() + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + var path []string + path, err = loc.XpathWithEntryName(vn, value) + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, true, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, errors.ObjectNotFound() + } + return nil, err + } + } + + list, err := normalizer.Normalize() + if err != nil { + return nil, err + } else if len(list) != 1 { + return nil, fmt.Errorf("expected to %q 1 entry, got %d", action, len(list)) + } + + return list[0], nil +} + +// Update updates the given config object, then returns the result. +func (s *Service) Update(ctx context.Context, loc Location, entry *Entry, name string) (*Entry, error) { + return s.update(ctx, loc, entry, name) +} +func (s *Service) update(ctx context.Context, loc Location, entry *Entry, value string) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + updates := xmlapi.NewMultiConfig(2) + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + var old *Entry + if value != "" && value != entry.Name { + path, err := loc.XpathWithEntryName(vn, value) + if err != nil { + return nil, err + } + + old, err = s.Read(ctx, loc, value, "get") + + updates.Add(&xmlapi.Config{ + Action: "rename", + Xpath: util.AsXpath(path), + NewName: entry.Name, + Target: s.client.GetTarget(), + }) + } else { + old, err = s.Read(ctx, loc, entry.Name, "get") + } + if err != nil { + return nil, err + } else if old == nil { + return nil, fmt.Errorf("previous object doesn't exist for update") + } + if !SpecMatches(entry, old) { + path, err := loc.XpathWithEntryName(vn, entry.Name) + if err != nil { + return nil, err + } + + updateSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + updates.Add(&xmlapi.Config{ + Action: "edit", + Xpath: util.AsXpath(path), + Element: updateSpec, + Target: s.client.GetTarget(), + }) + } + + if len(updates.Operations) != 0 { + if _, _, _, err = s.client.MultiConfig(ctx, updates, false, nil); err != nil { + return nil, err + } + } + return s.Read(ctx, loc, entry.Name, "get") +} + +// Delete deletes the given item. +func (s *Service) Delete(ctx context.Context, loc Location, name ...string) error { + return s.delete(ctx, loc, name) +} +func (s *Service) delete(ctx context.Context, loc Location, values []string) error { + for _, value := range values { + if value == "" { + return errors.NameNotSpecifiedError + } + } + + vn := s.client.Versioning() + var err error + deletes := xmlapi.NewMultiConfig(len(values)) + for _, value := range values { + var path []string + path, err = loc.XpathWithEntryName(vn, value) + if err != nil { + return err + } + deletes.Add(&xmlapi.Config{ + Action: "delete", + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + }) + } + + _, _, _, err = s.client.MultiConfig(ctx, deletes, false, nil) + + return err +} + +// List returns a list of objects using the given action ("get" or "show"). +// Params filter and quote are for client side filtering. +func (s *Service) List(ctx context.Context, loc Location, action, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, action, filter, quote, false) +} + +// ListFromConfig returns a list of objects at the given location. +// Requires that client.LoadPanosConfig() has been invoked. +// Params filter and quote are for client side filtering. +func (s *Service) ListFromConfig(ctx context.Context, loc Location, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, "", filter, quote, true) +} + +func (s *Service) list(ctx context.Context, loc Location, action, filter, quote string, usePanosConfig bool) ([]*Entry, error) { + var err error + + var logic *filtering.Group + if filter != "" { + logic, err = filtering.Parse(filter, quote) + if err != nil { + return nil, err + } + } + + vn := s.client.Versioning() + + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + + path, err := loc.XpathWithEntryName(vn, "") + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, false, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, nil + } + return nil, err + } + } + + listing, err := normalizer.Normalize() + if err != nil || logic == nil { + return listing, err + } + + filtered := make([]*Entry, 0, len(listing)) + for _, x := range listing { + ok, err := logic.Matches(x) + if err != nil { + return nil, err + } + if ok { + filtered = append(filtered, x) + } + } + + return filtered, nil +} diff --git a/network/profiles/interface_management/entry.go b/network/profiles/interface_management/entry.go new file mode 100644 index 0000000..e69baef --- /dev/null +++ b/network/profiles/interface_management/entry.go @@ -0,0 +1,216 @@ +package interface_management + +import ( + "encoding/xml" + "fmt" + + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/generic" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +var ( + _ filtering.Fielder = &Entry{} +) + +var ( + Suffix = []string{"network", "profiles", "interface-management-profile"} +) + +type Entry struct { + Name string + Http *bool + HttpOcsp *bool + Https *bool + PermittedIps []string + Ping *bool + ResponsePages *bool + Snmp *bool + Ssh *bool + Telnet *bool + UseridService *bool + UseridSyslogListenerSsl *bool + UseridSyslogListenerUdp *bool + + Misc map[string][]generic.Xml +} + +type entryXmlContainer struct { + Answer []entryXml `xml:"entry"` +} + +type entryXml struct { + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + Http *string `xml:"http,omitempty"` + HttpOcsp *string `xml:"http-ocsp,omitempty"` + Https *string `xml:"https,omitempty"` + PermittedIps *util.EntryType `xml:"permitted-ip,omitempty"` + Ping *string `xml:"ping,omitempty"` + ResponsePages *string `xml:"response-pages,omitempty"` + Snmp *string `xml:"snmp,omitempty"` + Ssh *string `xml:"ssh,omitempty"` + Telnet *string `xml:"telnet,omitempty"` + UseridService *string `xml:"userid-service,omitempty"` + UseridSyslogListenerSsl *string `xml:"userid-syslog-listener-ssl,omitempty"` + UseridSyslogListenerUdp *string `xml:"userid-syslog-listener-udp,omitempty"` + + Misc []generic.Xml `xml:",any"` +} + +func (e *Entry) Field(v string) (any, error) { + if v == "name" || v == "Name" { + return e.Name, nil + } + if v == "http" || v == "Http" { + return e.Http, nil + } + if v == "http_ocsp" || v == "HttpOcsp" { + return e.HttpOcsp, nil + } + if v == "https" || v == "Https" { + return e.Https, nil + } + if v == "permitted_ips" || v == "PermittedIps" { + return e.PermittedIps, nil + } + if v == "permitted_ips|LENGTH" || v == "PermittedIps|LENGTH" { + return int64(len(e.PermittedIps)), nil + } + if v == "ping" || v == "Ping" { + return e.Ping, nil + } + if v == "response_pages" || v == "ResponsePages" { + return e.ResponsePages, nil + } + if v == "snmp" || v == "Snmp" { + return e.Snmp, nil + } + if v == "ssh" || v == "Ssh" { + return e.Ssh, nil + } + if v == "telnet" || v == "Telnet" { + return e.Telnet, nil + } + if v == "userid_service" || v == "UseridService" { + return e.UseridService, nil + } + if v == "userid_syslog_listener_ssl" || v == "UseridSyslogListenerSsl" { + return e.UseridSyslogListenerSsl, nil + } + if v == "userid_syslog_listener_udp" || v == "UseridSyslogListenerUdp" { + return e.UseridSyslogListenerUdp, nil + } + + return nil, fmt.Errorf("unknown field") +} + +func Versioning(vn version.Number) (Specifier, Normalizer, error) { + return specifyEntry, &entryXmlContainer{}, nil +} + +func specifyEntry(o *Entry) (any, error) { + entry := entryXml{} + + entry.Name = o.Name + entry.Http = util.YesNo(o.Http, nil) + entry.HttpOcsp = util.YesNo(o.HttpOcsp, nil) + entry.Https = util.YesNo(o.Https, nil) + entry.PermittedIps = util.StrToEnt(o.PermittedIps) + entry.Ping = util.YesNo(o.Ping, nil) + entry.ResponsePages = util.YesNo(o.ResponsePages, nil) + entry.Snmp = util.YesNo(o.Snmp, nil) + entry.Ssh = util.YesNo(o.Ssh, nil) + entry.Telnet = util.YesNo(o.Telnet, nil) + entry.UseridService = util.YesNo(o.UseridService, nil) + entry.UseridSyslogListenerSsl = util.YesNo(o.UseridSyslogListenerSsl, nil) + entry.UseridSyslogListenerUdp = util.YesNo(o.UseridSyslogListenerUdp, nil) + + entry.Misc = o.Misc["Entry"] + + return entry, nil +} +func (c *entryXmlContainer) Normalize() ([]*Entry, error) { + entryList := make([]*Entry, 0, len(c.Answer)) + for _, o := range c.Answer { + entry := &Entry{ + Misc: make(map[string][]generic.Xml), + } + entry.Name = o.Name + entry.Http = util.AsBool(o.Http, nil) + entry.HttpOcsp = util.AsBool(o.HttpOcsp, nil) + entry.Https = util.AsBool(o.Https, nil) + entry.PermittedIps = util.EntToStr(o.PermittedIps) + entry.Ping = util.AsBool(o.Ping, nil) + entry.ResponsePages = util.AsBool(o.ResponsePages, nil) + entry.Snmp = util.AsBool(o.Snmp, nil) + entry.Ssh = util.AsBool(o.Ssh, nil) + entry.Telnet = util.AsBool(o.Telnet, nil) + entry.UseridService = util.AsBool(o.UseridService, nil) + entry.UseridSyslogListenerSsl = util.AsBool(o.UseridSyslogListenerSsl, nil) + entry.UseridSyslogListenerUdp = util.AsBool(o.UseridSyslogListenerUdp, nil) + + entry.Misc["Entry"] = o.Misc + + entryList = append(entryList, entry) + } + + return entryList, nil +} + +func SpecMatches(a, b *Entry) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + + // Don't compare Name. + if !util.BoolsMatch(a.Http, b.Http) { + return false + } + if !util.BoolsMatch(a.HttpOcsp, b.HttpOcsp) { + return false + } + if !util.BoolsMatch(a.Https, b.Https) { + return false + } + if !util.OrderedListsMatch(a.PermittedIps, b.PermittedIps) { + return false + } + if !util.BoolsMatch(a.Ping, b.Ping) { + return false + } + if !util.BoolsMatch(a.ResponsePages, b.ResponsePages) { + return false + } + if !util.BoolsMatch(a.Snmp, b.Snmp) { + return false + } + if !util.BoolsMatch(a.Ssh, b.Ssh) { + return false + } + if !util.BoolsMatch(a.Telnet, b.Telnet) { + return false + } + if !util.BoolsMatch(a.UseridService, b.UseridService) { + return false + } + if !util.BoolsMatch(a.UseridSyslogListenerSsl, b.UseridSyslogListenerSsl) { + return false + } + if !util.BoolsMatch(a.UseridSyslogListenerUdp, b.UseridSyslogListenerUdp) { + return false + } + + return true +} + +func (o *Entry) EntryName() string { + return o.Name +} + +func (o *Entry) SetEntryName(name string) { + o.Name = name +} diff --git a/network/profiles/interface_management/interfaces.go b/network/profiles/interface_management/interfaces.go new file mode 100644 index 0000000..31a952f --- /dev/null +++ b/network/profiles/interface_management/interfaces.go @@ -0,0 +1,7 @@ +package interface_management + +type Specifier func(*Entry) (any, error) + +type Normalizer interface { + Normalize() ([]*Entry, error) +} diff --git a/network/profiles/interface_management/location.go b/network/profiles/interface_management/location.go new file mode 100644 index 0000000..64ea8d9 --- /dev/null +++ b/network/profiles/interface_management/location.go @@ -0,0 +1,187 @@ +package interface_management + +import ( + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +type ImportLocation interface { + XpathForLocation(version.Number, util.ILocation) ([]string, error) + MarshalPangoXML([]string) (string, error) + UnmarshalPangoXML([]byte) ([]string, error) +} + +type Location struct { + Ngfw *NgfwLocation `json:"ngfw,omitempty"` + Template *TemplateLocation `json:"template,omitempty"` + TemplateStack *TemplateStackLocation `json:"template_stack,omitempty"` +} + +type NgfwLocation struct { + NgfwDevice string `json:"ngfw_device"` +} + +type TemplateLocation struct { + NgfwDevice string `json:"ngfw_device"` + PanoramaDevice string `json:"panorama_device"` + Template string `json:"template"` +} + +type TemplateStackLocation struct { + NgfwDevice string `json:"ngfw_device"` + PanoramaDevice string `json:"panorama_device"` + TemplateStack string `json:"template_stack"` +} + +func NewNgfwLocation() *Location { + return &Location{Ngfw: &NgfwLocation{ + NgfwDevice: "localhost.localdomain", + }, + } +} +func NewTemplateLocation() *Location { + return &Location{Template: &TemplateLocation{ + NgfwDevice: "localhost.localdomain", + PanoramaDevice: "localhost.localdomain", + Template: "", + }, + } +} +func NewTemplateStackLocation() *Location { + return &Location{TemplateStack: &TemplateStackLocation{ + NgfwDevice: "localhost.localdomain", + PanoramaDevice: "localhost.localdomain", + TemplateStack: "", + }, + } +} + +func (o Location) IsValid() error { + count := 0 + + switch { + case o.Ngfw != nil: + if o.Ngfw.NgfwDevice == "" { + return fmt.Errorf("NgfwDevice is unspecified") + } + count++ + case o.Template != nil: + if o.Template.NgfwDevice == "" { + return fmt.Errorf("NgfwDevice is unspecified") + } + if o.Template.PanoramaDevice == "" { + return fmt.Errorf("PanoramaDevice is unspecified") + } + if o.Template.Template == "" { + return fmt.Errorf("Template is unspecified") + } + count++ + case o.TemplateStack != nil: + if o.TemplateStack.NgfwDevice == "" { + return fmt.Errorf("NgfwDevice is unspecified") + } + if o.TemplateStack.PanoramaDevice == "" { + return fmt.Errorf("PanoramaDevice is unspecified") + } + if o.TemplateStack.TemplateStack == "" { + return fmt.Errorf("TemplateStack is unspecified") + } + count++ + } + + if count == 0 { + return fmt.Errorf("no path specified") + } + + if count > 1 { + return fmt.Errorf("multiple paths specified: only one should be specified") + } + + return nil +} + +func (o Location) XpathPrefix(vn version.Number) ([]string, error) { + + var ans []string + + switch { + case o.Ngfw != nil: + if o.Ngfw.NgfwDevice == "" { + return nil, fmt.Errorf("NgfwDevice is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.Ngfw.NgfwDevice}), + } + case o.Template != nil: + if o.Template.NgfwDevice == "" { + return nil, fmt.Errorf("NgfwDevice is unspecified") + } + if o.Template.PanoramaDevice == "" { + return nil, fmt.Errorf("PanoramaDevice is unspecified") + } + if o.Template.Template == "" { + return nil, fmt.Errorf("Template is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.Template.PanoramaDevice}), + "template", + util.AsEntryXpath([]string{o.Template.Template}), + "config", + "devices", + util.AsEntryXpath([]string{o.Template.NgfwDevice}), + } + case o.TemplateStack != nil: + if o.TemplateStack.NgfwDevice == "" { + return nil, fmt.Errorf("NgfwDevice is unspecified") + } + if o.TemplateStack.PanoramaDevice == "" { + return nil, fmt.Errorf("PanoramaDevice is unspecified") + } + if o.TemplateStack.TemplateStack == "" { + return nil, fmt.Errorf("TemplateStack is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.TemplateStack.PanoramaDevice}), + "template-stack", + util.AsEntryXpath([]string{o.TemplateStack.TemplateStack}), + "config", + "devices", + util.AsEntryXpath([]string{o.TemplateStack.NgfwDevice}), + } + default: + return nil, errors.NoLocationSpecifiedError + } + + return ans, nil +} +func (o Location) XpathWithEntryName(vn version.Number, name string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsEntryXpath([]string{name})) + + return ans, nil +} +func (o Location) XpathWithUuid(vn version.Number, uuid string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsUuidXpath(uuid)) + + return ans, nil +} diff --git a/network/profiles/interface_management/service.go b/network/profiles/interface_management/service.go new file mode 100644 index 0000000..248eed1 --- /dev/null +++ b/network/profiles/interface_management/service.go @@ -0,0 +1,281 @@ +package interface_management + +import ( + "context" + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/xmlapi" +) + +type Service struct { + client util.PangoClient +} + +func NewService(client util.PangoClient) *Service { + return &Service{ + client: client, + } +} + +// Create adds new item, then returns the result. +func (s *Service) Create(ctx context.Context, loc Location, entry *Entry) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + path, err := loc.XpathWithEntryName(vn, entry.Name) + if err != nil { + return nil, err + } + createSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + cmd := &xmlapi.Config{ + Action: "set", + Xpath: util.AsXpath(path[:len(path)-1]), + Element: createSpec, + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, false, nil); err != nil { + return nil, err + } + return s.Read(ctx, loc, entry.Name, "get") +} + +// Read returns the given config object, using the specified action ("get" or "show"). +func (s *Service) Read(ctx context.Context, loc Location, name, action string) (*Entry, error) { + return s.read(ctx, loc, name, action, false) +} + +// ReadFromConfig returns the given config object from the loaded XML config. +// Requires that client.LoadPanosConfig() has been invoked. +func (s *Service) ReadFromConfig(ctx context.Context, loc Location, name string) (*Entry, error) { + return s.read(ctx, loc, name, "", true) +} + +func (s *Service) read(ctx context.Context, loc Location, value, action string, usePanosConfig bool) (*Entry, error) { + if value == "" { + return nil, errors.NameNotSpecifiedError + } + vn := s.client.Versioning() + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + var path []string + path, err = loc.XpathWithEntryName(vn, value) + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, true, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, errors.ObjectNotFound() + } + return nil, err + } + } + + list, err := normalizer.Normalize() + if err != nil { + return nil, err + } else if len(list) != 1 { + return nil, fmt.Errorf("expected to %q 1 entry, got %d", action, len(list)) + } + + return list[0], nil +} + +// Update updates the given config object, then returns the result. +func (s *Service) Update(ctx context.Context, loc Location, entry *Entry, name string) (*Entry, error) { + return s.update(ctx, loc, entry, name) +} +func (s *Service) update(ctx context.Context, loc Location, entry *Entry, value string) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + updates := xmlapi.NewMultiConfig(2) + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + var old *Entry + if value != "" && value != entry.Name { + path, err := loc.XpathWithEntryName(vn, value) + if err != nil { + return nil, err + } + + old, err = s.Read(ctx, loc, value, "get") + + updates.Add(&xmlapi.Config{ + Action: "rename", + Xpath: util.AsXpath(path), + NewName: entry.Name, + Target: s.client.GetTarget(), + }) + } else { + old, err = s.Read(ctx, loc, entry.Name, "get") + } + if err != nil { + return nil, err + } else if old == nil { + return nil, fmt.Errorf("previous object doesn't exist for update") + } + if !SpecMatches(entry, old) { + path, err := loc.XpathWithEntryName(vn, entry.Name) + if err != nil { + return nil, err + } + + updateSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + updates.Add(&xmlapi.Config{ + Action: "edit", + Xpath: util.AsXpath(path), + Element: updateSpec, + Target: s.client.GetTarget(), + }) + } + + if len(updates.Operations) != 0 { + if _, _, _, err = s.client.MultiConfig(ctx, updates, false, nil); err != nil { + return nil, err + } + } + return s.Read(ctx, loc, entry.Name, "get") +} + +// Delete deletes the given item. +func (s *Service) Delete(ctx context.Context, loc Location, name ...string) error { + return s.delete(ctx, loc, name) +} +func (s *Service) delete(ctx context.Context, loc Location, values []string) error { + for _, value := range values { + if value == "" { + return errors.NameNotSpecifiedError + } + } + + vn := s.client.Versioning() + var err error + deletes := xmlapi.NewMultiConfig(len(values)) + for _, value := range values { + var path []string + path, err = loc.XpathWithEntryName(vn, value) + if err != nil { + return err + } + deletes.Add(&xmlapi.Config{ + Action: "delete", + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + }) + } + + _, _, _, err = s.client.MultiConfig(ctx, deletes, false, nil) + + return err +} + +// List returns a list of objects using the given action ("get" or "show"). +// Params filter and quote are for client side filtering. +func (s *Service) List(ctx context.Context, loc Location, action, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, action, filter, quote, false) +} + +// ListFromConfig returns a list of objects at the given location. +// Requires that client.LoadPanosConfig() has been invoked. +// Params filter and quote are for client side filtering. +func (s *Service) ListFromConfig(ctx context.Context, loc Location, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, "", filter, quote, true) +} + +func (s *Service) list(ctx context.Context, loc Location, action, filter, quote string, usePanosConfig bool) ([]*Entry, error) { + var err error + + var logic *filtering.Group + if filter != "" { + logic, err = filtering.Parse(filter, quote) + if err != nil { + return nil, err + } + } + + vn := s.client.Versioning() + + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + + path, err := loc.XpathWithEntryName(vn, "") + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, false, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, nil + } + return nil, err + } + } + + listing, err := normalizer.Normalize() + if err != nil || logic == nil { + return listing, err + } + + filtered := make([]*Entry, 0, len(listing)) + for _, x := range listing { + ok, err := logic.Matches(x) + if err != nil { + return nil, err + } + if ok { + filtered = append(filtered, x) + } + } + + return filtered, nil +} diff --git a/network/virtual_router/entry.go b/network/virtual_router/entry.go new file mode 100644 index 0000000..664a03b --- /dev/null +++ b/network/virtual_router/entry.go @@ -0,0 +1,1262 @@ +package virtual_router + +import ( + "encoding/xml" + "fmt" + + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/generic" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +var ( + _ filtering.Fielder = &Entry{} +) + +var ( + Suffix = []string{"network", "virtual-router"} +) + +type Entry struct { + Name string + AdministrativeDistances *AdministrativeDistances + Ecmp *Ecmp + Interfaces []string + Protocol *Protocol + RoutingTable *RoutingTable + + Misc map[string][]generic.Xml +} + +type AdministrativeDistances struct { + Ebgp *int64 + Ibgp *int64 + OspfExt *int64 + OspfInt *int64 + Ospfv3Ext *int64 + Ospfv3Int *int64 + Rip *int64 + Static *int64 + StaticIpv6 *int64 +} +type Ecmp struct { + Algorithm *EcmpAlgorithm + Enable *bool + MaxPaths *int64 + StrictSourcePath *bool + SymmetricReturn *bool +} +type EcmpAlgorithm struct { + BalancedRoundRobin *EcmpAlgorithmBalancedRoundRobin + IpHash *EcmpAlgorithmIpHash + IpModulo *EcmpAlgorithmIpModulo + WeightedRoundRobin *EcmpAlgorithmWeightedRoundRobin +} +type EcmpAlgorithmBalancedRoundRobin struct { +} +type EcmpAlgorithmIpHash struct { + HashSeed *int64 + SrcOnly *bool + UsePort *bool +} +type EcmpAlgorithmIpModulo struct { +} +type EcmpAlgorithmWeightedRoundRobin struct { + Interfaces []EcmpAlgorithmWeightedRoundRobinInterfaces +} +type EcmpAlgorithmWeightedRoundRobinInterfaces struct { + Name string + Weight *int64 +} +type Protocol struct { + Bgp *ProtocolBgp + Ospf *ProtocolOspf + Ospfv3 *ProtocolOspfv3 + Rip *ProtocolRip +} +type ProtocolBgp struct { + Enable *bool +} +type ProtocolOspf struct { + Enable *bool +} +type ProtocolOspfv3 struct { + Enable *bool +} +type ProtocolRip struct { + Enable *bool +} +type RoutingTable struct { + Ip *RoutingTableIp + Ipv6 *RoutingTableIpv6 +} +type RoutingTableIp struct { + StaticRoutes []RoutingTableIpStaticRoutes +} +type RoutingTableIpStaticRoutes struct { + AdminDist *int64 + Destination *string + Interface *string + Metric *int64 + Name string + NextHop *RoutingTableIpStaticRoutesNextHop + RouteTable *string +} +type RoutingTableIpStaticRoutesNextHop struct { + Fqdn *string + IpAddress *string + NextVr *string + Tunnel *string +} +type RoutingTableIpv6 struct { + StaticRoutes []RoutingTableIpv6StaticRoutes +} +type RoutingTableIpv6StaticRoutes struct { + AdminDist *int64 + Destination *string + Interface *string + Metric *int64 + Name string + NextHop *RoutingTableIpv6StaticRoutesNextHop + RouteTable *string +} +type RoutingTableIpv6StaticRoutesNextHop struct { + Fqdn *string + Ipv6Address *string + NextVr *string + Tunnel *string +} + +type entryXmlContainer struct { + Answer []entryXml `xml:"entry"` +} + +type entryXml struct { + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + AdministrativeDistances *AdministrativeDistancesXml `xml:"admin-dists,omitempty"` + Ecmp *EcmpXml `xml:"ecmp,omitempty"` + Interfaces *util.MemberType `xml:"interface,omitempty"` + Protocol *ProtocolXml `xml:"protocol,omitempty"` + RoutingTable *RoutingTableXml `xml:"routing-table,omitempty"` + + Misc []generic.Xml `xml:",any"` +} + +type AdministrativeDistancesXml struct { + Ebgp *int64 `xml:"ebgp,omitempty"` + Ibgp *int64 `xml:"ibgp,omitempty"` + OspfExt *int64 `xml:"ospf-ext,omitempty"` + OspfInt *int64 `xml:"ospf-int,omitempty"` + Ospfv3Ext *int64 `xml:"ospfv3-ext,omitempty"` + Ospfv3Int *int64 `xml:"ospfv3-int,omitempty"` + Rip *int64 `xml:"rip,omitempty"` + Static *int64 `xml:"static,omitempty"` + StaticIpv6 *int64 `xml:"static-ipv6,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type EcmpXml struct { + Algorithm *EcmpAlgorithmXml `xml:"algorithm,omitempty"` + Enable *string `xml:"enable,omitempty"` + MaxPaths *int64 `xml:"max-path,omitempty"` + StrictSourcePath *string `xml:"strict-source-path,omitempty"` + SymmetricReturn *string `xml:"symmetric-return,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type EcmpAlgorithmXml struct { + BalancedRoundRobin *EcmpAlgorithmBalancedRoundRobinXml `xml:"balanced-round-robin,omitempty"` + IpHash *EcmpAlgorithmIpHashXml `xml:"ip-hash,omitempty"` + IpModulo *EcmpAlgorithmIpModuloXml `xml:"ip-modulo,omitempty"` + WeightedRoundRobin *EcmpAlgorithmWeightedRoundRobinXml `xml:"weighted-round-robin,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type EcmpAlgorithmBalancedRoundRobinXml struct { + Misc []generic.Xml `xml:",any"` +} +type EcmpAlgorithmIpHashXml struct { + HashSeed *int64 `xml:"hash-seed,omitempty"` + SrcOnly *string `xml:"src-only,omitempty"` + UsePort *string `xml:"use-port,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type EcmpAlgorithmIpModuloXml struct { + Misc []generic.Xml `xml:",any"` +} +type EcmpAlgorithmWeightedRoundRobinXml struct { + Interfaces []EcmpAlgorithmWeightedRoundRobinInterfacesXml `xml:"interface>entry,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type EcmpAlgorithmWeightedRoundRobinInterfacesXml struct { + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + Weight *int64 + + Misc []generic.Xml `xml:",any"` +} +type ProtocolXml struct { + Bgp *ProtocolBgpXml `xml:"bgp,omitempty"` + Ospf *ProtocolOspfXml `xml:"ospf,omitempty"` + Ospfv3 *ProtocolOspfv3Xml `xml:"ospfv3,omitempty"` + Rip *ProtocolRipXml `xml:"rip,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type ProtocolBgpXml struct { + Enable *string `xml:"enable,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type ProtocolOspfXml struct { + Enable *string `xml:"enable,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type ProtocolOspfv3Xml struct { + Enable *string `xml:"enable,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type ProtocolRipXml struct { + Enable *string `xml:"enable,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type RoutingTableXml struct { + Ip *RoutingTableIpXml `xml:"ip,omitempty"` + Ipv6 *RoutingTableIpv6Xml `xml:"ipv6,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type RoutingTableIpXml struct { + StaticRoutes []RoutingTableIpStaticRoutesXml `xml:"static-route>entry,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type RoutingTableIpStaticRoutesXml struct { + AdminDist *int64 `xml:"admin-dist,omitempty"` + Destination *string `xml:"destination,omitempty"` + Interface *string `xml:"interface,omitempty"` + Metric *int64 `xml:"metric,omitempty"` + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + NextHop *RoutingTableIpStaticRoutesNextHopXml `xml:"nexthop,omitempty"` + RouteTable *string `xml:"route-table,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type RoutingTableIpStaticRoutesNextHopXml struct { + Fqdn *string `xml:"fqdn,omitempty"` + IpAddress *string `xml:"ip-address,omitempty"` + NextVr *string `xml:"next-vr,omitempty"` + Tunnel *string `xml:"tunnel,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type RoutingTableIpv6Xml struct { + StaticRoutes []RoutingTableIpv6StaticRoutesXml `xml:"static-route>entry,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type RoutingTableIpv6StaticRoutesXml struct { + AdminDist *int64 `xml:"admin-dist,omitempty"` + Destination *string `xml:"destination,omitempty"` + Interface *string `xml:"interface,omitempty"` + Metric *int64 `xml:"metric,omitempty"` + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + NextHop *RoutingTableIpv6StaticRoutesNextHopXml `xml:"nexthop,omitempty"` + RouteTable *string `xml:"route-table,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type RoutingTableIpv6StaticRoutesNextHopXml struct { + Fqdn *string `xml:"fqdn,omitempty"` + Ipv6Address *string `xml:"ipv6-address,omitempty"` + NextVr *string `xml:"next-vr,omitempty"` + Tunnel *string `xml:"tunnel,omitempty"` + + Misc []generic.Xml `xml:",any"` +} + +func (e *Entry) Field(v string) (any, error) { + if v == "name" || v == "Name" { + return e.Name, nil + } + if v == "administrative_distances" || v == "AdministrativeDistances" { + return e.AdministrativeDistances, nil + } + if v == "ecmp" || v == "Ecmp" { + return e.Ecmp, nil + } + if v == "interfaces" || v == "Interfaces" { + return e.Interfaces, nil + } + if v == "interfaces|LENGTH" || v == "Interfaces|LENGTH" { + return int64(len(e.Interfaces)), nil + } + if v == "protocol" || v == "Protocol" { + return e.Protocol, nil + } + if v == "routing_table" || v == "RoutingTable" { + return e.RoutingTable, nil + } + + return nil, fmt.Errorf("unknown field") +} + +func Versioning(vn version.Number) (Specifier, Normalizer, error) { + return specifyEntry, &entryXmlContainer{}, nil +} + +func specifyEntry(o *Entry) (any, error) { + entry := entryXml{} + + entry.Name = o.Name + var nestedAdministrativeDistances *AdministrativeDistancesXml + if o.AdministrativeDistances != nil { + nestedAdministrativeDistances = &AdministrativeDistancesXml{} + if _, ok := o.Misc["AdministrativeDistances"]; ok { + nestedAdministrativeDistances.Misc = o.Misc["AdministrativeDistances"] + } + if o.AdministrativeDistances.Ospfv3Int != nil { + nestedAdministrativeDistances.Ospfv3Int = o.AdministrativeDistances.Ospfv3Int + } + if o.AdministrativeDistances.Ebgp != nil { + nestedAdministrativeDistances.Ebgp = o.AdministrativeDistances.Ebgp + } + if o.AdministrativeDistances.Static != nil { + nestedAdministrativeDistances.Static = o.AdministrativeDistances.Static + } + if o.AdministrativeDistances.OspfInt != nil { + nestedAdministrativeDistances.OspfInt = o.AdministrativeDistances.OspfInt + } + if o.AdministrativeDistances.OspfExt != nil { + nestedAdministrativeDistances.OspfExt = o.AdministrativeDistances.OspfExt + } + if o.AdministrativeDistances.Rip != nil { + nestedAdministrativeDistances.Rip = o.AdministrativeDistances.Rip + } + if o.AdministrativeDistances.StaticIpv6 != nil { + nestedAdministrativeDistances.StaticIpv6 = o.AdministrativeDistances.StaticIpv6 + } + if o.AdministrativeDistances.Ospfv3Ext != nil { + nestedAdministrativeDistances.Ospfv3Ext = o.AdministrativeDistances.Ospfv3Ext + } + if o.AdministrativeDistances.Ibgp != nil { + nestedAdministrativeDistances.Ibgp = o.AdministrativeDistances.Ibgp + } + } + entry.AdministrativeDistances = nestedAdministrativeDistances + + var nestedEcmp *EcmpXml + if o.Ecmp != nil { + nestedEcmp = &EcmpXml{} + if _, ok := o.Misc["Ecmp"]; ok { + nestedEcmp.Misc = o.Misc["Ecmp"] + } + if o.Ecmp.SymmetricReturn != nil { + nestedEcmp.SymmetricReturn = util.YesNo(o.Ecmp.SymmetricReturn, nil) + } + if o.Ecmp.StrictSourcePath != nil { + nestedEcmp.StrictSourcePath = util.YesNo(o.Ecmp.StrictSourcePath, nil) + } + if o.Ecmp.MaxPaths != nil { + nestedEcmp.MaxPaths = o.Ecmp.MaxPaths + } + if o.Ecmp.Algorithm != nil { + nestedEcmp.Algorithm = &EcmpAlgorithmXml{} + if _, ok := o.Misc["EcmpAlgorithm"]; ok { + nestedEcmp.Algorithm.Misc = o.Misc["EcmpAlgorithm"] + } + if o.Ecmp.Algorithm.BalancedRoundRobin != nil { + nestedEcmp.Algorithm.BalancedRoundRobin = &EcmpAlgorithmBalancedRoundRobinXml{} + if _, ok := o.Misc["EcmpAlgorithmBalancedRoundRobin"]; ok { + nestedEcmp.Algorithm.BalancedRoundRobin.Misc = o.Misc["EcmpAlgorithmBalancedRoundRobin"] + } + } + if o.Ecmp.Algorithm.IpModulo != nil { + nestedEcmp.Algorithm.IpModulo = &EcmpAlgorithmIpModuloXml{} + if _, ok := o.Misc["EcmpAlgorithmIpModulo"]; ok { + nestedEcmp.Algorithm.IpModulo.Misc = o.Misc["EcmpAlgorithmIpModulo"] + } + } + if o.Ecmp.Algorithm.IpHash != nil { + nestedEcmp.Algorithm.IpHash = &EcmpAlgorithmIpHashXml{} + if _, ok := o.Misc["EcmpAlgorithmIpHash"]; ok { + nestedEcmp.Algorithm.IpHash.Misc = o.Misc["EcmpAlgorithmIpHash"] + } + if o.Ecmp.Algorithm.IpHash.SrcOnly != nil { + nestedEcmp.Algorithm.IpHash.SrcOnly = util.YesNo(o.Ecmp.Algorithm.IpHash.SrcOnly, nil) + } + if o.Ecmp.Algorithm.IpHash.UsePort != nil { + nestedEcmp.Algorithm.IpHash.UsePort = util.YesNo(o.Ecmp.Algorithm.IpHash.UsePort, nil) + } + if o.Ecmp.Algorithm.IpHash.HashSeed != nil { + nestedEcmp.Algorithm.IpHash.HashSeed = o.Ecmp.Algorithm.IpHash.HashSeed + } + } + if o.Ecmp.Algorithm.WeightedRoundRobin != nil { + nestedEcmp.Algorithm.WeightedRoundRobin = &EcmpAlgorithmWeightedRoundRobinXml{} + if _, ok := o.Misc["EcmpAlgorithmWeightedRoundRobin"]; ok { + nestedEcmp.Algorithm.WeightedRoundRobin.Misc = o.Misc["EcmpAlgorithmWeightedRoundRobin"] + } + if o.Ecmp.Algorithm.WeightedRoundRobin.Interfaces != nil { + nestedEcmp.Algorithm.WeightedRoundRobin.Interfaces = []EcmpAlgorithmWeightedRoundRobinInterfacesXml{} + for _, oEcmpAlgorithmWeightedRoundRobinInterfaces := range o.Ecmp.Algorithm.WeightedRoundRobin.Interfaces { + nestedEcmpAlgorithmWeightedRoundRobinInterfaces := EcmpAlgorithmWeightedRoundRobinInterfacesXml{} + if _, ok := o.Misc["EcmpAlgorithmWeightedRoundRobinInterfaces"]; ok { + nestedEcmpAlgorithmWeightedRoundRobinInterfaces.Misc = o.Misc["EcmpAlgorithmWeightedRoundRobinInterfaces"] + } + if oEcmpAlgorithmWeightedRoundRobinInterfaces.Weight != nil { + nestedEcmpAlgorithmWeightedRoundRobinInterfaces.Weight = oEcmpAlgorithmWeightedRoundRobinInterfaces.Weight + } + if oEcmpAlgorithmWeightedRoundRobinInterfaces.Name != "" { + nestedEcmpAlgorithmWeightedRoundRobinInterfaces.Name = oEcmpAlgorithmWeightedRoundRobinInterfaces.Name + } + nestedEcmp.Algorithm.WeightedRoundRobin.Interfaces = append(nestedEcmp.Algorithm.WeightedRoundRobin.Interfaces, nestedEcmpAlgorithmWeightedRoundRobinInterfaces) + } + } + } + } + if o.Ecmp.Enable != nil { + nestedEcmp.Enable = util.YesNo(o.Ecmp.Enable, nil) + } + } + entry.Ecmp = nestedEcmp + + entry.Interfaces = util.StrToMem(o.Interfaces) + var nestedProtocol *ProtocolXml + if o.Protocol != nil { + nestedProtocol = &ProtocolXml{} + if _, ok := o.Misc["Protocol"]; ok { + nestedProtocol.Misc = o.Misc["Protocol"] + } + if o.Protocol.Ospfv3 != nil { + nestedProtocol.Ospfv3 = &ProtocolOspfv3Xml{} + if _, ok := o.Misc["ProtocolOspfv3"]; ok { + nestedProtocol.Ospfv3.Misc = o.Misc["ProtocolOspfv3"] + } + if o.Protocol.Ospfv3.Enable != nil { + nestedProtocol.Ospfv3.Enable = util.YesNo(o.Protocol.Ospfv3.Enable, nil) + } + } + if o.Protocol.Bgp != nil { + nestedProtocol.Bgp = &ProtocolBgpXml{} + if _, ok := o.Misc["ProtocolBgp"]; ok { + nestedProtocol.Bgp.Misc = o.Misc["ProtocolBgp"] + } + if o.Protocol.Bgp.Enable != nil { + nestedProtocol.Bgp.Enable = util.YesNo(o.Protocol.Bgp.Enable, nil) + } + } + if o.Protocol.Rip != nil { + nestedProtocol.Rip = &ProtocolRipXml{} + if _, ok := o.Misc["ProtocolRip"]; ok { + nestedProtocol.Rip.Misc = o.Misc["ProtocolRip"] + } + if o.Protocol.Rip.Enable != nil { + nestedProtocol.Rip.Enable = util.YesNo(o.Protocol.Rip.Enable, nil) + } + } + if o.Protocol.Ospf != nil { + nestedProtocol.Ospf = &ProtocolOspfXml{} + if _, ok := o.Misc["ProtocolOspf"]; ok { + nestedProtocol.Ospf.Misc = o.Misc["ProtocolOspf"] + } + if o.Protocol.Ospf.Enable != nil { + nestedProtocol.Ospf.Enable = util.YesNo(o.Protocol.Ospf.Enable, nil) + } + } + } + entry.Protocol = nestedProtocol + + var nestedRoutingTable *RoutingTableXml + if o.RoutingTable != nil { + nestedRoutingTable = &RoutingTableXml{} + if _, ok := o.Misc["RoutingTable"]; ok { + nestedRoutingTable.Misc = o.Misc["RoutingTable"] + } + if o.RoutingTable.Ip != nil { + nestedRoutingTable.Ip = &RoutingTableIpXml{} + if _, ok := o.Misc["RoutingTableIp"]; ok { + nestedRoutingTable.Ip.Misc = o.Misc["RoutingTableIp"] + } + if o.RoutingTable.Ip.StaticRoutes != nil { + nestedRoutingTable.Ip.StaticRoutes = []RoutingTableIpStaticRoutesXml{} + for _, oRoutingTableIpStaticRoutes := range o.RoutingTable.Ip.StaticRoutes { + nestedRoutingTableIpStaticRoutes := RoutingTableIpStaticRoutesXml{} + if _, ok := o.Misc["RoutingTableIpStaticRoutes"]; ok { + nestedRoutingTableIpStaticRoutes.Misc = o.Misc["RoutingTableIpStaticRoutes"] + } + if oRoutingTableIpStaticRoutes.NextHop != nil { + nestedRoutingTableIpStaticRoutes.NextHop = &RoutingTableIpStaticRoutesNextHopXml{} + if _, ok := o.Misc["RoutingTableIpStaticRoutesNextHop"]; ok { + nestedRoutingTableIpStaticRoutes.NextHop.Misc = o.Misc["RoutingTableIpStaticRoutesNextHop"] + } + if oRoutingTableIpStaticRoutes.NextHop.IpAddress != nil { + nestedRoutingTableIpStaticRoutes.NextHop.IpAddress = oRoutingTableIpStaticRoutes.NextHop.IpAddress + } + if oRoutingTableIpStaticRoutes.NextHop.Fqdn != nil { + nestedRoutingTableIpStaticRoutes.NextHop.Fqdn = oRoutingTableIpStaticRoutes.NextHop.Fqdn + } + if oRoutingTableIpStaticRoutes.NextHop.NextVr != nil { + nestedRoutingTableIpStaticRoutes.NextHop.NextVr = oRoutingTableIpStaticRoutes.NextHop.NextVr + } + if oRoutingTableIpStaticRoutes.NextHop.Tunnel != nil { + nestedRoutingTableIpStaticRoutes.NextHop.Tunnel = oRoutingTableIpStaticRoutes.NextHop.Tunnel + } + } + if oRoutingTableIpStaticRoutes.AdminDist != nil { + nestedRoutingTableIpStaticRoutes.AdminDist = oRoutingTableIpStaticRoutes.AdminDist + } + if oRoutingTableIpStaticRoutes.Metric != nil { + nestedRoutingTableIpStaticRoutes.Metric = oRoutingTableIpStaticRoutes.Metric + } + if oRoutingTableIpStaticRoutes.RouteTable != nil { + nestedRoutingTableIpStaticRoutes.RouteTable = oRoutingTableIpStaticRoutes.RouteTable + } + if oRoutingTableIpStaticRoutes.Name != "" { + nestedRoutingTableIpStaticRoutes.Name = oRoutingTableIpStaticRoutes.Name + } + if oRoutingTableIpStaticRoutes.Destination != nil { + nestedRoutingTableIpStaticRoutes.Destination = oRoutingTableIpStaticRoutes.Destination + } + if oRoutingTableIpStaticRoutes.Interface != nil { + nestedRoutingTableIpStaticRoutes.Interface = oRoutingTableIpStaticRoutes.Interface + } + nestedRoutingTable.Ip.StaticRoutes = append(nestedRoutingTable.Ip.StaticRoutes, nestedRoutingTableIpStaticRoutes) + } + } + } + if o.RoutingTable.Ipv6 != nil { + nestedRoutingTable.Ipv6 = &RoutingTableIpv6Xml{} + if _, ok := o.Misc["RoutingTableIpv6"]; ok { + nestedRoutingTable.Ipv6.Misc = o.Misc["RoutingTableIpv6"] + } + if o.RoutingTable.Ipv6.StaticRoutes != nil { + nestedRoutingTable.Ipv6.StaticRoutes = []RoutingTableIpv6StaticRoutesXml{} + for _, oRoutingTableIpv6StaticRoutes := range o.RoutingTable.Ipv6.StaticRoutes { + nestedRoutingTableIpv6StaticRoutes := RoutingTableIpv6StaticRoutesXml{} + if _, ok := o.Misc["RoutingTableIpv6StaticRoutes"]; ok { + nestedRoutingTableIpv6StaticRoutes.Misc = o.Misc["RoutingTableIpv6StaticRoutes"] + } + if oRoutingTableIpv6StaticRoutes.NextHop != nil { + nestedRoutingTableIpv6StaticRoutes.NextHop = &RoutingTableIpv6StaticRoutesNextHopXml{} + if _, ok := o.Misc["RoutingTableIpv6StaticRoutesNextHop"]; ok { + nestedRoutingTableIpv6StaticRoutes.NextHop.Misc = o.Misc["RoutingTableIpv6StaticRoutesNextHop"] + } + if oRoutingTableIpv6StaticRoutes.NextHop.Ipv6Address != nil { + nestedRoutingTableIpv6StaticRoutes.NextHop.Ipv6Address = oRoutingTableIpv6StaticRoutes.NextHop.Ipv6Address + } + if oRoutingTableIpv6StaticRoutes.NextHop.Fqdn != nil { + nestedRoutingTableIpv6StaticRoutes.NextHop.Fqdn = oRoutingTableIpv6StaticRoutes.NextHop.Fqdn + } + if oRoutingTableIpv6StaticRoutes.NextHop.NextVr != nil { + nestedRoutingTableIpv6StaticRoutes.NextHop.NextVr = oRoutingTableIpv6StaticRoutes.NextHop.NextVr + } + if oRoutingTableIpv6StaticRoutes.NextHop.Tunnel != nil { + nestedRoutingTableIpv6StaticRoutes.NextHop.Tunnel = oRoutingTableIpv6StaticRoutes.NextHop.Tunnel + } + } + if oRoutingTableIpv6StaticRoutes.AdminDist != nil { + nestedRoutingTableIpv6StaticRoutes.AdminDist = oRoutingTableIpv6StaticRoutes.AdminDist + } + if oRoutingTableIpv6StaticRoutes.Metric != nil { + nestedRoutingTableIpv6StaticRoutes.Metric = oRoutingTableIpv6StaticRoutes.Metric + } + if oRoutingTableIpv6StaticRoutes.RouteTable != nil { + nestedRoutingTableIpv6StaticRoutes.RouteTable = oRoutingTableIpv6StaticRoutes.RouteTable + } + if oRoutingTableIpv6StaticRoutes.Name != "" { + nestedRoutingTableIpv6StaticRoutes.Name = oRoutingTableIpv6StaticRoutes.Name + } + if oRoutingTableIpv6StaticRoutes.Destination != nil { + nestedRoutingTableIpv6StaticRoutes.Destination = oRoutingTableIpv6StaticRoutes.Destination + } + if oRoutingTableIpv6StaticRoutes.Interface != nil { + nestedRoutingTableIpv6StaticRoutes.Interface = oRoutingTableIpv6StaticRoutes.Interface + } + nestedRoutingTable.Ipv6.StaticRoutes = append(nestedRoutingTable.Ipv6.StaticRoutes, nestedRoutingTableIpv6StaticRoutes) + } + } + } + } + entry.RoutingTable = nestedRoutingTable + + entry.Misc = o.Misc["Entry"] + + return entry, nil +} +func (c *entryXmlContainer) Normalize() ([]*Entry, error) { + entryList := make([]*Entry, 0, len(c.Answer)) + for _, o := range c.Answer { + entry := &Entry{ + Misc: make(map[string][]generic.Xml), + } + entry.Name = o.Name + var nestedAdministrativeDistances *AdministrativeDistances + if o.AdministrativeDistances != nil { + nestedAdministrativeDistances = &AdministrativeDistances{} + if o.AdministrativeDistances.Misc != nil { + entry.Misc["AdministrativeDistances"] = o.AdministrativeDistances.Misc + } + if o.AdministrativeDistances.StaticIpv6 != nil { + nestedAdministrativeDistances.StaticIpv6 = o.AdministrativeDistances.StaticIpv6 + } + if o.AdministrativeDistances.Ospfv3Ext != nil { + nestedAdministrativeDistances.Ospfv3Ext = o.AdministrativeDistances.Ospfv3Ext + } + if o.AdministrativeDistances.Ibgp != nil { + nestedAdministrativeDistances.Ibgp = o.AdministrativeDistances.Ibgp + } + if o.AdministrativeDistances.Rip != nil { + nestedAdministrativeDistances.Rip = o.AdministrativeDistances.Rip + } + if o.AdministrativeDistances.Ebgp != nil { + nestedAdministrativeDistances.Ebgp = o.AdministrativeDistances.Ebgp + } + if o.AdministrativeDistances.Static != nil { + nestedAdministrativeDistances.Static = o.AdministrativeDistances.Static + } + if o.AdministrativeDistances.OspfInt != nil { + nestedAdministrativeDistances.OspfInt = o.AdministrativeDistances.OspfInt + } + if o.AdministrativeDistances.OspfExt != nil { + nestedAdministrativeDistances.OspfExt = o.AdministrativeDistances.OspfExt + } + if o.AdministrativeDistances.Ospfv3Int != nil { + nestedAdministrativeDistances.Ospfv3Int = o.AdministrativeDistances.Ospfv3Int + } + } + entry.AdministrativeDistances = nestedAdministrativeDistances + + var nestedEcmp *Ecmp + if o.Ecmp != nil { + nestedEcmp = &Ecmp{} + if o.Ecmp.Misc != nil { + entry.Misc["Ecmp"] = o.Ecmp.Misc + } + if o.Ecmp.Enable != nil { + nestedEcmp.Enable = util.AsBool(o.Ecmp.Enable, nil) + } + if o.Ecmp.SymmetricReturn != nil { + nestedEcmp.SymmetricReturn = util.AsBool(o.Ecmp.SymmetricReturn, nil) + } + if o.Ecmp.StrictSourcePath != nil { + nestedEcmp.StrictSourcePath = util.AsBool(o.Ecmp.StrictSourcePath, nil) + } + if o.Ecmp.MaxPaths != nil { + nestedEcmp.MaxPaths = o.Ecmp.MaxPaths + } + if o.Ecmp.Algorithm != nil { + nestedEcmp.Algorithm = &EcmpAlgorithm{} + if o.Ecmp.Algorithm.Misc != nil { + entry.Misc["EcmpAlgorithm"] = o.Ecmp.Algorithm.Misc + } + if o.Ecmp.Algorithm.IpModulo != nil { + nestedEcmp.Algorithm.IpModulo = &EcmpAlgorithmIpModulo{} + if o.Ecmp.Algorithm.IpModulo.Misc != nil { + entry.Misc["EcmpAlgorithmIpModulo"] = o.Ecmp.Algorithm.IpModulo.Misc + } + } + if o.Ecmp.Algorithm.IpHash != nil { + nestedEcmp.Algorithm.IpHash = &EcmpAlgorithmIpHash{} + if o.Ecmp.Algorithm.IpHash.Misc != nil { + entry.Misc["EcmpAlgorithmIpHash"] = o.Ecmp.Algorithm.IpHash.Misc + } + if o.Ecmp.Algorithm.IpHash.SrcOnly != nil { + nestedEcmp.Algorithm.IpHash.SrcOnly = util.AsBool(o.Ecmp.Algorithm.IpHash.SrcOnly, nil) + } + if o.Ecmp.Algorithm.IpHash.UsePort != nil { + nestedEcmp.Algorithm.IpHash.UsePort = util.AsBool(o.Ecmp.Algorithm.IpHash.UsePort, nil) + } + if o.Ecmp.Algorithm.IpHash.HashSeed != nil { + nestedEcmp.Algorithm.IpHash.HashSeed = o.Ecmp.Algorithm.IpHash.HashSeed + } + } + if o.Ecmp.Algorithm.WeightedRoundRobin != nil { + nestedEcmp.Algorithm.WeightedRoundRobin = &EcmpAlgorithmWeightedRoundRobin{} + if o.Ecmp.Algorithm.WeightedRoundRobin.Misc != nil { + entry.Misc["EcmpAlgorithmWeightedRoundRobin"] = o.Ecmp.Algorithm.WeightedRoundRobin.Misc + } + if o.Ecmp.Algorithm.WeightedRoundRobin.Interfaces != nil { + nestedEcmp.Algorithm.WeightedRoundRobin.Interfaces = []EcmpAlgorithmWeightedRoundRobinInterfaces{} + for _, oEcmpAlgorithmWeightedRoundRobinInterfaces := range o.Ecmp.Algorithm.WeightedRoundRobin.Interfaces { + nestedEcmpAlgorithmWeightedRoundRobinInterfaces := EcmpAlgorithmWeightedRoundRobinInterfaces{} + if oEcmpAlgorithmWeightedRoundRobinInterfaces.Misc != nil { + entry.Misc["EcmpAlgorithmWeightedRoundRobinInterfaces"] = oEcmpAlgorithmWeightedRoundRobinInterfaces.Misc + } + if oEcmpAlgorithmWeightedRoundRobinInterfaces.Weight != nil { + nestedEcmpAlgorithmWeightedRoundRobinInterfaces.Weight = oEcmpAlgorithmWeightedRoundRobinInterfaces.Weight + } + if oEcmpAlgorithmWeightedRoundRobinInterfaces.Name != "" { + nestedEcmpAlgorithmWeightedRoundRobinInterfaces.Name = oEcmpAlgorithmWeightedRoundRobinInterfaces.Name + } + nestedEcmp.Algorithm.WeightedRoundRobin.Interfaces = append(nestedEcmp.Algorithm.WeightedRoundRobin.Interfaces, nestedEcmpAlgorithmWeightedRoundRobinInterfaces) + } + } + } + if o.Ecmp.Algorithm.BalancedRoundRobin != nil { + nestedEcmp.Algorithm.BalancedRoundRobin = &EcmpAlgorithmBalancedRoundRobin{} + if o.Ecmp.Algorithm.BalancedRoundRobin.Misc != nil { + entry.Misc["EcmpAlgorithmBalancedRoundRobin"] = o.Ecmp.Algorithm.BalancedRoundRobin.Misc + } + } + } + } + entry.Ecmp = nestedEcmp + + entry.Interfaces = util.MemToStr(o.Interfaces) + var nestedProtocol *Protocol + if o.Protocol != nil { + nestedProtocol = &Protocol{} + if o.Protocol.Misc != nil { + entry.Misc["Protocol"] = o.Protocol.Misc + } + if o.Protocol.Rip != nil { + nestedProtocol.Rip = &ProtocolRip{} + if o.Protocol.Rip.Misc != nil { + entry.Misc["ProtocolRip"] = o.Protocol.Rip.Misc + } + if o.Protocol.Rip.Enable != nil { + nestedProtocol.Rip.Enable = util.AsBool(o.Protocol.Rip.Enable, nil) + } + } + if o.Protocol.Ospf != nil { + nestedProtocol.Ospf = &ProtocolOspf{} + if o.Protocol.Ospf.Misc != nil { + entry.Misc["ProtocolOspf"] = o.Protocol.Ospf.Misc + } + if o.Protocol.Ospf.Enable != nil { + nestedProtocol.Ospf.Enable = util.AsBool(o.Protocol.Ospf.Enable, nil) + } + } + if o.Protocol.Ospfv3 != nil { + nestedProtocol.Ospfv3 = &ProtocolOspfv3{} + if o.Protocol.Ospfv3.Misc != nil { + entry.Misc["ProtocolOspfv3"] = o.Protocol.Ospfv3.Misc + } + if o.Protocol.Ospfv3.Enable != nil { + nestedProtocol.Ospfv3.Enable = util.AsBool(o.Protocol.Ospfv3.Enable, nil) + } + } + if o.Protocol.Bgp != nil { + nestedProtocol.Bgp = &ProtocolBgp{} + if o.Protocol.Bgp.Misc != nil { + entry.Misc["ProtocolBgp"] = o.Protocol.Bgp.Misc + } + if o.Protocol.Bgp.Enable != nil { + nestedProtocol.Bgp.Enable = util.AsBool(o.Protocol.Bgp.Enable, nil) + } + } + } + entry.Protocol = nestedProtocol + + var nestedRoutingTable *RoutingTable + if o.RoutingTable != nil { + nestedRoutingTable = &RoutingTable{} + if o.RoutingTable.Misc != nil { + entry.Misc["RoutingTable"] = o.RoutingTable.Misc + } + if o.RoutingTable.Ip != nil { + nestedRoutingTable.Ip = &RoutingTableIp{} + if o.RoutingTable.Ip.Misc != nil { + entry.Misc["RoutingTableIp"] = o.RoutingTable.Ip.Misc + } + if o.RoutingTable.Ip.StaticRoutes != nil { + nestedRoutingTable.Ip.StaticRoutes = []RoutingTableIpStaticRoutes{} + for _, oRoutingTableIpStaticRoutes := range o.RoutingTable.Ip.StaticRoutes { + nestedRoutingTableIpStaticRoutes := RoutingTableIpStaticRoutes{} + if oRoutingTableIpStaticRoutes.Misc != nil { + entry.Misc["RoutingTableIpStaticRoutes"] = oRoutingTableIpStaticRoutes.Misc + } + if oRoutingTableIpStaticRoutes.Name != "" { + nestedRoutingTableIpStaticRoutes.Name = oRoutingTableIpStaticRoutes.Name + } + if oRoutingTableIpStaticRoutes.Destination != nil { + nestedRoutingTableIpStaticRoutes.Destination = oRoutingTableIpStaticRoutes.Destination + } + if oRoutingTableIpStaticRoutes.Interface != nil { + nestedRoutingTableIpStaticRoutes.Interface = oRoutingTableIpStaticRoutes.Interface + } + if oRoutingTableIpStaticRoutes.NextHop != nil { + nestedRoutingTableIpStaticRoutes.NextHop = &RoutingTableIpStaticRoutesNextHop{} + if oRoutingTableIpStaticRoutes.NextHop.Misc != nil { + entry.Misc["RoutingTableIpStaticRoutesNextHop"] = oRoutingTableIpStaticRoutes.NextHop.Misc + } + if oRoutingTableIpStaticRoutes.NextHop.Tunnel != nil { + nestedRoutingTableIpStaticRoutes.NextHop.Tunnel = oRoutingTableIpStaticRoutes.NextHop.Tunnel + } + if oRoutingTableIpStaticRoutes.NextHop.IpAddress != nil { + nestedRoutingTableIpStaticRoutes.NextHop.IpAddress = oRoutingTableIpStaticRoutes.NextHop.IpAddress + } + if oRoutingTableIpStaticRoutes.NextHop.Fqdn != nil { + nestedRoutingTableIpStaticRoutes.NextHop.Fqdn = oRoutingTableIpStaticRoutes.NextHop.Fqdn + } + if oRoutingTableIpStaticRoutes.NextHop.NextVr != nil { + nestedRoutingTableIpStaticRoutes.NextHop.NextVr = oRoutingTableIpStaticRoutes.NextHop.NextVr + } + } + if oRoutingTableIpStaticRoutes.AdminDist != nil { + nestedRoutingTableIpStaticRoutes.AdminDist = oRoutingTableIpStaticRoutes.AdminDist + } + if oRoutingTableIpStaticRoutes.Metric != nil { + nestedRoutingTableIpStaticRoutes.Metric = oRoutingTableIpStaticRoutes.Metric + } + if oRoutingTableIpStaticRoutes.RouteTable != nil { + nestedRoutingTableIpStaticRoutes.RouteTable = oRoutingTableIpStaticRoutes.RouteTable + } + nestedRoutingTable.Ip.StaticRoutes = append(nestedRoutingTable.Ip.StaticRoutes, nestedRoutingTableIpStaticRoutes) + } + } + } + if o.RoutingTable.Ipv6 != nil { + nestedRoutingTable.Ipv6 = &RoutingTableIpv6{} + if o.RoutingTable.Ipv6.Misc != nil { + entry.Misc["RoutingTableIpv6"] = o.RoutingTable.Ipv6.Misc + } + if o.RoutingTable.Ipv6.StaticRoutes != nil { + nestedRoutingTable.Ipv6.StaticRoutes = []RoutingTableIpv6StaticRoutes{} + for _, oRoutingTableIpv6StaticRoutes := range o.RoutingTable.Ipv6.StaticRoutes { + nestedRoutingTableIpv6StaticRoutes := RoutingTableIpv6StaticRoutes{} + if oRoutingTableIpv6StaticRoutes.Misc != nil { + entry.Misc["RoutingTableIpv6StaticRoutes"] = oRoutingTableIpv6StaticRoutes.Misc + } + if oRoutingTableIpv6StaticRoutes.RouteTable != nil { + nestedRoutingTableIpv6StaticRoutes.RouteTable = oRoutingTableIpv6StaticRoutes.RouteTable + } + if oRoutingTableIpv6StaticRoutes.Name != "" { + nestedRoutingTableIpv6StaticRoutes.Name = oRoutingTableIpv6StaticRoutes.Name + } + if oRoutingTableIpv6StaticRoutes.Destination != nil { + nestedRoutingTableIpv6StaticRoutes.Destination = oRoutingTableIpv6StaticRoutes.Destination + } + if oRoutingTableIpv6StaticRoutes.Interface != nil { + nestedRoutingTableIpv6StaticRoutes.Interface = oRoutingTableIpv6StaticRoutes.Interface + } + if oRoutingTableIpv6StaticRoutes.NextHop != nil { + nestedRoutingTableIpv6StaticRoutes.NextHop = &RoutingTableIpv6StaticRoutesNextHop{} + if oRoutingTableIpv6StaticRoutes.NextHop.Misc != nil { + entry.Misc["RoutingTableIpv6StaticRoutesNextHop"] = oRoutingTableIpv6StaticRoutes.NextHop.Misc + } + if oRoutingTableIpv6StaticRoutes.NextHop.Ipv6Address != nil { + nestedRoutingTableIpv6StaticRoutes.NextHop.Ipv6Address = oRoutingTableIpv6StaticRoutes.NextHop.Ipv6Address + } + if oRoutingTableIpv6StaticRoutes.NextHop.Fqdn != nil { + nestedRoutingTableIpv6StaticRoutes.NextHop.Fqdn = oRoutingTableIpv6StaticRoutes.NextHop.Fqdn + } + if oRoutingTableIpv6StaticRoutes.NextHop.NextVr != nil { + nestedRoutingTableIpv6StaticRoutes.NextHop.NextVr = oRoutingTableIpv6StaticRoutes.NextHop.NextVr + } + if oRoutingTableIpv6StaticRoutes.NextHop.Tunnel != nil { + nestedRoutingTableIpv6StaticRoutes.NextHop.Tunnel = oRoutingTableIpv6StaticRoutes.NextHop.Tunnel + } + } + if oRoutingTableIpv6StaticRoutes.AdminDist != nil { + nestedRoutingTableIpv6StaticRoutes.AdminDist = oRoutingTableIpv6StaticRoutes.AdminDist + } + if oRoutingTableIpv6StaticRoutes.Metric != nil { + nestedRoutingTableIpv6StaticRoutes.Metric = oRoutingTableIpv6StaticRoutes.Metric + } + nestedRoutingTable.Ipv6.StaticRoutes = append(nestedRoutingTable.Ipv6.StaticRoutes, nestedRoutingTableIpv6StaticRoutes) + } + } + } + } + entry.RoutingTable = nestedRoutingTable + + entry.Misc["Entry"] = o.Misc + + entryList = append(entryList, entry) + } + + return entryList, nil +} + +func SpecMatches(a, b *Entry) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + + // Don't compare Name. + if !matchAdministrativeDistances(a.AdministrativeDistances, b.AdministrativeDistances) { + return false + } + if !matchEcmp(a.Ecmp, b.Ecmp) { + return false + } + if !util.OrderedListsMatch(a.Interfaces, b.Interfaces) { + return false + } + if !matchProtocol(a.Protocol, b.Protocol) { + return false + } + if !matchRoutingTable(a.RoutingTable, b.RoutingTable) { + return false + } + + return true +} + +func matchRoutingTableIpStaticRoutesNextHop(a *RoutingTableIpStaticRoutesNextHop, b *RoutingTableIpStaticRoutesNextHop) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.StringsMatch(a.Fqdn, b.Fqdn) { + return false + } + if !util.StringsMatch(a.NextVr, b.NextVr) { + return false + } + if !util.StringsMatch(a.Tunnel, b.Tunnel) { + return false + } + if !util.StringsMatch(a.IpAddress, b.IpAddress) { + return false + } + return true +} +func matchRoutingTableIpStaticRoutes(a []RoutingTableIpStaticRoutes, b []RoutingTableIpStaticRoutes) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + for _, a := range a { + for _, b := range b { + if !util.Ints64Match(a.Metric, b.Metric) { + return false + } + if !util.StringsMatch(a.RouteTable, b.RouteTable) { + return false + } + if !util.StringsEqual(a.Name, b.Name) { + return false + } + if !util.StringsMatch(a.Destination, b.Destination) { + return false + } + if !util.StringsMatch(a.Interface, b.Interface) { + return false + } + if !matchRoutingTableIpStaticRoutesNextHop(a.NextHop, b.NextHop) { + return false + } + if !util.Ints64Match(a.AdminDist, b.AdminDist) { + return false + } + } + } + return true +} +func matchRoutingTableIp(a *RoutingTableIp, b *RoutingTableIp) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !matchRoutingTableIpStaticRoutes(a.StaticRoutes, b.StaticRoutes) { + return false + } + return true +} +func matchRoutingTableIpv6StaticRoutesNextHop(a *RoutingTableIpv6StaticRoutesNextHop, b *RoutingTableIpv6StaticRoutesNextHop) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.StringsMatch(a.Fqdn, b.Fqdn) { + return false + } + if !util.StringsMatch(a.NextVr, b.NextVr) { + return false + } + if !util.StringsMatch(a.Tunnel, b.Tunnel) { + return false + } + if !util.StringsMatch(a.Ipv6Address, b.Ipv6Address) { + return false + } + return true +} +func matchRoutingTableIpv6StaticRoutes(a []RoutingTableIpv6StaticRoutes, b []RoutingTableIpv6StaticRoutes) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + for _, a := range a { + for _, b := range b { + if !util.StringsMatch(a.Destination, b.Destination) { + return false + } + if !util.StringsMatch(a.Interface, b.Interface) { + return false + } + if !matchRoutingTableIpv6StaticRoutesNextHop(a.NextHop, b.NextHop) { + return false + } + if !util.Ints64Match(a.AdminDist, b.AdminDist) { + return false + } + if !util.Ints64Match(a.Metric, b.Metric) { + return false + } + if !util.StringsMatch(a.RouteTable, b.RouteTable) { + return false + } + if !util.StringsEqual(a.Name, b.Name) { + return false + } + } + } + return true +} +func matchRoutingTableIpv6(a *RoutingTableIpv6, b *RoutingTableIpv6) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !matchRoutingTableIpv6StaticRoutes(a.StaticRoutes, b.StaticRoutes) { + return false + } + return true +} +func matchRoutingTable(a *RoutingTable, b *RoutingTable) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !matchRoutingTableIp(a.Ip, b.Ip) { + return false + } + if !matchRoutingTableIpv6(a.Ipv6, b.Ipv6) { + return false + } + return true +} +func matchProtocolBgp(a *ProtocolBgp, b *ProtocolBgp) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.BoolsMatch(a.Enable, b.Enable) { + return false + } + return true +} +func matchProtocolRip(a *ProtocolRip, b *ProtocolRip) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.BoolsMatch(a.Enable, b.Enable) { + return false + } + return true +} +func matchProtocolOspf(a *ProtocolOspf, b *ProtocolOspf) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.BoolsMatch(a.Enable, b.Enable) { + return false + } + return true +} +func matchProtocolOspfv3(a *ProtocolOspfv3, b *ProtocolOspfv3) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.BoolsMatch(a.Enable, b.Enable) { + return false + } + return true +} +func matchProtocol(a *Protocol, b *Protocol) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !matchProtocolBgp(a.Bgp, b.Bgp) { + return false + } + if !matchProtocolRip(a.Rip, b.Rip) { + return false + } + if !matchProtocolOspf(a.Ospf, b.Ospf) { + return false + } + if !matchProtocolOspfv3(a.Ospfv3, b.Ospfv3) { + return false + } + return true +} +func matchEcmpAlgorithmIpModulo(a *EcmpAlgorithmIpModulo, b *EcmpAlgorithmIpModulo) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + return true +} +func matchEcmpAlgorithmIpHash(a *EcmpAlgorithmIpHash, b *EcmpAlgorithmIpHash) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.BoolsMatch(a.SrcOnly, b.SrcOnly) { + return false + } + if !util.BoolsMatch(a.UsePort, b.UsePort) { + return false + } + if !util.Ints64Match(a.HashSeed, b.HashSeed) { + return false + } + return true +} +func matchEcmpAlgorithmWeightedRoundRobinInterfaces(a []EcmpAlgorithmWeightedRoundRobinInterfaces, b []EcmpAlgorithmWeightedRoundRobinInterfaces) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + for _, a := range a { + for _, b := range b { + if !util.Ints64Match(a.Weight, b.Weight) { + return false + } + if !util.StringsEqual(a.Name, b.Name) { + return false + } + } + } + return true +} +func matchEcmpAlgorithmWeightedRoundRobin(a *EcmpAlgorithmWeightedRoundRobin, b *EcmpAlgorithmWeightedRoundRobin) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !matchEcmpAlgorithmWeightedRoundRobinInterfaces(a.Interfaces, b.Interfaces) { + return false + } + return true +} +func matchEcmpAlgorithmBalancedRoundRobin(a *EcmpAlgorithmBalancedRoundRobin, b *EcmpAlgorithmBalancedRoundRobin) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + return true +} +func matchEcmpAlgorithm(a *EcmpAlgorithm, b *EcmpAlgorithm) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !matchEcmpAlgorithmWeightedRoundRobin(a.WeightedRoundRobin, b.WeightedRoundRobin) { + return false + } + if !matchEcmpAlgorithmBalancedRoundRobin(a.BalancedRoundRobin, b.BalancedRoundRobin) { + return false + } + if !matchEcmpAlgorithmIpModulo(a.IpModulo, b.IpModulo) { + return false + } + if !matchEcmpAlgorithmIpHash(a.IpHash, b.IpHash) { + return false + } + return true +} +func matchEcmp(a *Ecmp, b *Ecmp) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.BoolsMatch(a.Enable, b.Enable) { + return false + } + if !util.BoolsMatch(a.SymmetricReturn, b.SymmetricReturn) { + return false + } + if !util.BoolsMatch(a.StrictSourcePath, b.StrictSourcePath) { + return false + } + if !util.Ints64Match(a.MaxPaths, b.MaxPaths) { + return false + } + if !matchEcmpAlgorithm(a.Algorithm, b.Algorithm) { + return false + } + return true +} +func matchAdministrativeDistances(a *AdministrativeDistances, b *AdministrativeDistances) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.Ints64Match(a.Static, b.Static) { + return false + } + if !util.Ints64Match(a.OspfInt, b.OspfInt) { + return false + } + if !util.Ints64Match(a.OspfExt, b.OspfExt) { + return false + } + if !util.Ints64Match(a.Ospfv3Int, b.Ospfv3Int) { + return false + } + if !util.Ints64Match(a.Ebgp, b.Ebgp) { + return false + } + if !util.Ints64Match(a.StaticIpv6, b.StaticIpv6) { + return false + } + if !util.Ints64Match(a.Ospfv3Ext, b.Ospfv3Ext) { + return false + } + if !util.Ints64Match(a.Ibgp, b.Ibgp) { + return false + } + if !util.Ints64Match(a.Rip, b.Rip) { + return false + } + return true +} + +func (o *Entry) EntryName() string { + return o.Name +} + +func (o *Entry) SetEntryName(name string) { + o.Name = name +} diff --git a/network/virtual_router/interfaces.go b/network/virtual_router/interfaces.go new file mode 100644 index 0000000..c133d65 --- /dev/null +++ b/network/virtual_router/interfaces.go @@ -0,0 +1,7 @@ +package virtual_router + +type Specifier func(*Entry) (any, error) + +type Normalizer interface { + Normalize() ([]*Entry, error) +} diff --git a/network/virtual_router/location.go b/network/virtual_router/location.go new file mode 100644 index 0000000..613ea73 --- /dev/null +++ b/network/virtual_router/location.go @@ -0,0 +1,187 @@ +package virtual_router + +import ( + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +type ImportLocation interface { + XpathForLocation(version.Number, util.ILocation) ([]string, error) + MarshalPangoXML([]string) (string, error) + UnmarshalPangoXML([]byte) ([]string, error) +} + +type Location struct { + Ngfw *NgfwLocation `json:"ngfw,omitempty"` + Template *TemplateLocation `json:"template,omitempty"` + TemplateStack *TemplateStackLocation `json:"template_stack,omitempty"` +} + +type NgfwLocation struct { + NgfwDevice string `json:"ngfw_device"` +} + +type TemplateLocation struct { + NgfwDevice string `json:"ngfw_device"` + PanoramaDevice string `json:"panorama_device"` + Template string `json:"template"` +} + +type TemplateStackLocation struct { + NgfwDevice string `json:"ngfw_device"` + PanoramaDevice string `json:"panorama_device"` + TemplateStack string `json:"template_stack"` +} + +func NewNgfwLocation() *Location { + return &Location{Ngfw: &NgfwLocation{ + NgfwDevice: "localhost.localdomain", + }, + } +} +func NewTemplateLocation() *Location { + return &Location{Template: &TemplateLocation{ + NgfwDevice: "localhost.localdomain", + PanoramaDevice: "localhost.localdomain", + Template: "", + }, + } +} +func NewTemplateStackLocation() *Location { + return &Location{TemplateStack: &TemplateStackLocation{ + NgfwDevice: "localhost.localdomain", + PanoramaDevice: "localhost.localdomain", + TemplateStack: "", + }, + } +} + +func (o Location) IsValid() error { + count := 0 + + switch { + case o.Ngfw != nil: + if o.Ngfw.NgfwDevice == "" { + return fmt.Errorf("NgfwDevice is unspecified") + } + count++ + case o.Template != nil: + if o.Template.NgfwDevice == "" { + return fmt.Errorf("NgfwDevice is unspecified") + } + if o.Template.PanoramaDevice == "" { + return fmt.Errorf("PanoramaDevice is unspecified") + } + if o.Template.Template == "" { + return fmt.Errorf("Template is unspecified") + } + count++ + case o.TemplateStack != nil: + if o.TemplateStack.NgfwDevice == "" { + return fmt.Errorf("NgfwDevice is unspecified") + } + if o.TemplateStack.PanoramaDevice == "" { + return fmt.Errorf("PanoramaDevice is unspecified") + } + if o.TemplateStack.TemplateStack == "" { + return fmt.Errorf("TemplateStack is unspecified") + } + count++ + } + + if count == 0 { + return fmt.Errorf("no path specified") + } + + if count > 1 { + return fmt.Errorf("multiple paths specified: only one should be specified") + } + + return nil +} + +func (o Location) XpathPrefix(vn version.Number) ([]string, error) { + + var ans []string + + switch { + case o.Ngfw != nil: + if o.Ngfw.NgfwDevice == "" { + return nil, fmt.Errorf("NgfwDevice is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.Ngfw.NgfwDevice}), + } + case o.Template != nil: + if o.Template.NgfwDevice == "" { + return nil, fmt.Errorf("NgfwDevice is unspecified") + } + if o.Template.PanoramaDevice == "" { + return nil, fmt.Errorf("PanoramaDevice is unspecified") + } + if o.Template.Template == "" { + return nil, fmt.Errorf("Template is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.Template.PanoramaDevice}), + "template", + util.AsEntryXpath([]string{o.Template.Template}), + "config", + "devices", + util.AsEntryXpath([]string{o.Template.NgfwDevice}), + } + case o.TemplateStack != nil: + if o.TemplateStack.NgfwDevice == "" { + return nil, fmt.Errorf("NgfwDevice is unspecified") + } + if o.TemplateStack.PanoramaDevice == "" { + return nil, fmt.Errorf("PanoramaDevice is unspecified") + } + if o.TemplateStack.TemplateStack == "" { + return nil, fmt.Errorf("TemplateStack is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.TemplateStack.PanoramaDevice}), + "template-stack", + util.AsEntryXpath([]string{o.TemplateStack.TemplateStack}), + "config", + "devices", + util.AsEntryXpath([]string{o.TemplateStack.NgfwDevice}), + } + default: + return nil, errors.NoLocationSpecifiedError + } + + return ans, nil +} +func (o Location) XpathWithEntryName(vn version.Number, name string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsEntryXpath([]string{name})) + + return ans, nil +} +func (o Location) XpathWithUuid(vn version.Number, uuid string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsUuidXpath(uuid)) + + return ans, nil +} diff --git a/network/virtual_router/service.go b/network/virtual_router/service.go new file mode 100644 index 0000000..ef35a2f --- /dev/null +++ b/network/virtual_router/service.go @@ -0,0 +1,281 @@ +package virtual_router + +import ( + "context" + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/xmlapi" +) + +type Service struct { + client util.PangoClient +} + +func NewService(client util.PangoClient) *Service { + return &Service{ + client: client, + } +} + +// Create adds new item, then returns the result. +func (s *Service) Create(ctx context.Context, loc Location, entry *Entry) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + path, err := loc.XpathWithEntryName(vn, entry.Name) + if err != nil { + return nil, err + } + createSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + cmd := &xmlapi.Config{ + Action: "set", + Xpath: util.AsXpath(path[:len(path)-1]), + Element: createSpec, + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, false, nil); err != nil { + return nil, err + } + return s.Read(ctx, loc, entry.Name, "get") +} + +// Read returns the given config object, using the specified action ("get" or "show"). +func (s *Service) Read(ctx context.Context, loc Location, name, action string) (*Entry, error) { + return s.read(ctx, loc, name, action, false) +} + +// ReadFromConfig returns the given config object from the loaded XML config. +// Requires that client.LoadPanosConfig() has been invoked. +func (s *Service) ReadFromConfig(ctx context.Context, loc Location, name string) (*Entry, error) { + return s.read(ctx, loc, name, "", true) +} + +func (s *Service) read(ctx context.Context, loc Location, value, action string, usePanosConfig bool) (*Entry, error) { + if value == "" { + return nil, errors.NameNotSpecifiedError + } + vn := s.client.Versioning() + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + var path []string + path, err = loc.XpathWithEntryName(vn, value) + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, true, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, errors.ObjectNotFound() + } + return nil, err + } + } + + list, err := normalizer.Normalize() + if err != nil { + return nil, err + } else if len(list) != 1 { + return nil, fmt.Errorf("expected to %q 1 entry, got %d", action, len(list)) + } + + return list[0], nil +} + +// Update updates the given config object, then returns the result. +func (s *Service) Update(ctx context.Context, loc Location, entry *Entry, name string) (*Entry, error) { + return s.update(ctx, loc, entry, name) +} +func (s *Service) update(ctx context.Context, loc Location, entry *Entry, value string) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + updates := xmlapi.NewMultiConfig(2) + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + var old *Entry + if value != "" && value != entry.Name { + path, err := loc.XpathWithEntryName(vn, value) + if err != nil { + return nil, err + } + + old, err = s.Read(ctx, loc, value, "get") + + updates.Add(&xmlapi.Config{ + Action: "rename", + Xpath: util.AsXpath(path), + NewName: entry.Name, + Target: s.client.GetTarget(), + }) + } else { + old, err = s.Read(ctx, loc, entry.Name, "get") + } + if err != nil { + return nil, err + } else if old == nil { + return nil, fmt.Errorf("previous object doesn't exist for update") + } + if !SpecMatches(entry, old) { + path, err := loc.XpathWithEntryName(vn, entry.Name) + if err != nil { + return nil, err + } + + updateSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + updates.Add(&xmlapi.Config{ + Action: "edit", + Xpath: util.AsXpath(path), + Element: updateSpec, + Target: s.client.GetTarget(), + }) + } + + if len(updates.Operations) != 0 { + if _, _, _, err = s.client.MultiConfig(ctx, updates, false, nil); err != nil { + return nil, err + } + } + return s.Read(ctx, loc, entry.Name, "get") +} + +// Delete deletes the given item. +func (s *Service) Delete(ctx context.Context, loc Location, name ...string) error { + return s.delete(ctx, loc, name) +} +func (s *Service) delete(ctx context.Context, loc Location, values []string) error { + for _, value := range values { + if value == "" { + return errors.NameNotSpecifiedError + } + } + + vn := s.client.Versioning() + var err error + deletes := xmlapi.NewMultiConfig(len(values)) + for _, value := range values { + var path []string + path, err = loc.XpathWithEntryName(vn, value) + if err != nil { + return err + } + deletes.Add(&xmlapi.Config{ + Action: "delete", + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + }) + } + + _, _, _, err = s.client.MultiConfig(ctx, deletes, false, nil) + + return err +} + +// List returns a list of objects using the given action ("get" or "show"). +// Params filter and quote are for client side filtering. +func (s *Service) List(ctx context.Context, loc Location, action, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, action, filter, quote, false) +} + +// ListFromConfig returns a list of objects at the given location. +// Requires that client.LoadPanosConfig() has been invoked. +// Params filter and quote are for client side filtering. +func (s *Service) ListFromConfig(ctx context.Context, loc Location, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, "", filter, quote, true) +} + +func (s *Service) list(ctx context.Context, loc Location, action, filter, quote string, usePanosConfig bool) ([]*Entry, error) { + var err error + + var logic *filtering.Group + if filter != "" { + logic, err = filtering.Parse(filter, quote) + if err != nil { + return nil, err + } + } + + vn := s.client.Versioning() + + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + + path, err := loc.XpathWithEntryName(vn, "") + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, false, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, nil + } + return nil, err + } + } + + listing, err := normalizer.Normalize() + if err != nil || logic == nil { + return listing, err + } + + filtered := make([]*Entry, 0, len(listing)) + for _, x := range listing { + ok, err := logic.Matches(x) + if err != nil { + return nil, err + } + if ok { + filtered = append(filtered, x) + } + } + + return filtered, nil +} diff --git a/network/zone/entry.go b/network/zone/entry.go new file mode 100644 index 0000000..b344d08 --- /dev/null +++ b/network/zone/entry.go @@ -0,0 +1,355 @@ +package zone + +import ( + "encoding/xml" + "fmt" + + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/generic" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +var ( + _ filtering.Fielder = &Entry{} +) + +var ( + Suffix = []string{"zone"} +) + +type Entry struct { + Name string + DeviceAcl *DeviceAcl + EnableDeviceIdentification *bool + EnableUserIdentification *bool + Network *Network + UserAcl *UserAcl + + Misc map[string][]generic.Xml +} + +type DeviceAcl struct { + ExcludeList []string + IncludeList []string +} +type Network struct { + EnablePacketBufferProtection *bool + LogSetting []string + ZoneProtectionProfile []string + Layer2 []string + Layer3 []string + Tap []string + VirtualWire []string +} +type UserAcl struct { + ExcludeList []string + IncludeList []string +} + +type entryXmlContainer struct { + Answer []entryXml `xml:"entry"` +} + +type entryXml struct { + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + DeviceAcl *DeviceAclXml `xml:"device-acl,omitempty"` + EnableDeviceIdentification *string `xml:"enable-device-identification,omitempty"` + EnableUserIdentification *string `xml:"enable-user-identification,omitempty"` + Network *NetworkXml `xml:"network,omitempty"` + UserAcl *UserAclXml `xml:"user-acl,omitempty"` + + Misc []generic.Xml `xml:",any"` +} + +type DeviceAclXml struct { + ExcludeList *util.MemberType `xml:"exclude-list,omitempty"` + IncludeList *util.MemberType `xml:"include-list,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type NetworkXml struct { + EnablePacketBufferProtection *string `xml:"enable-packet-buffer-protection,omitempty"` + LogSetting *util.MemberType `xml:"log-setting,omitempty"` + ZoneProtectionProfile *util.MemberType `xml:"zone-protection-profile,omitempty"` + Layer2 *util.MemberType `xml:"layer2,omitempty"` + Layer3 *util.MemberType `xml:"layer3,omitempty"` + Tap *util.MemberType `xml:"tap,omitempty"` + VirtualWire *util.MemberType `xml:"virtual-wire,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type UserAclXml struct { + ExcludeList *util.MemberType `xml:"exclude-list,omitempty"` + IncludeList *util.MemberType `xml:"include-list,omitempty"` + + Misc []generic.Xml `xml:",any"` +} + +func (e *Entry) Field(v string) (any, error) { + if v == "name" || v == "Name" { + return e.Name, nil + } + if v == "device_acl" || v == "DeviceAcl" { + return e.DeviceAcl, nil + } + if v == "enable_device_identification" || v == "EnableDeviceIdentification" { + return e.EnableDeviceIdentification, nil + } + if v == "enable_user_identification" || v == "EnableUserIdentification" { + return e.EnableUserIdentification, nil + } + if v == "network" || v == "Network" { + return e.Network, nil + } + if v == "user_acl" || v == "UserAcl" { + return e.UserAcl, nil + } + + return nil, fmt.Errorf("unknown field") +} + +func Versioning(vn version.Number) (Specifier, Normalizer, error) { + return specifyEntry, &entryXmlContainer{}, nil +} + +func specifyEntry(o *Entry) (any, error) { + entry := entryXml{} + + entry.Name = o.Name + var nestedDeviceAcl *DeviceAclXml + if o.DeviceAcl != nil { + nestedDeviceAcl = &DeviceAclXml{} + if _, ok := o.Misc["DeviceAcl"]; ok { + nestedDeviceAcl.Misc = o.Misc["DeviceAcl"] + } + if o.DeviceAcl.IncludeList != nil { + nestedDeviceAcl.IncludeList = util.StrToMem(o.DeviceAcl.IncludeList) + } + if o.DeviceAcl.ExcludeList != nil { + nestedDeviceAcl.ExcludeList = util.StrToMem(o.DeviceAcl.ExcludeList) + } + } + entry.DeviceAcl = nestedDeviceAcl + + entry.EnableDeviceIdentification = util.YesNo(o.EnableDeviceIdentification, nil) + entry.EnableUserIdentification = util.YesNo(o.EnableUserIdentification, nil) + var nestedNetwork *NetworkXml + if o.Network != nil { + nestedNetwork = &NetworkXml{} + if _, ok := o.Misc["Network"]; ok { + nestedNetwork.Misc = o.Misc["Network"] + } + if o.Network.EnablePacketBufferProtection != nil { + nestedNetwork.EnablePacketBufferProtection = util.YesNo(o.Network.EnablePacketBufferProtection, nil) + } + if o.Network.ZoneProtectionProfile != nil { + nestedNetwork.ZoneProtectionProfile = util.StrToMem(o.Network.ZoneProtectionProfile) + } + if o.Network.LogSetting != nil { + nestedNetwork.LogSetting = util.StrToMem(o.Network.LogSetting) + } + if o.Network.Layer3 != nil { + nestedNetwork.Layer3 = util.StrToMem(o.Network.Layer3) + } + if o.Network.Layer2 != nil { + nestedNetwork.Layer2 = util.StrToMem(o.Network.Layer2) + } + if o.Network.VirtualWire != nil { + nestedNetwork.VirtualWire = util.StrToMem(o.Network.VirtualWire) + } + if o.Network.Tap != nil { + nestedNetwork.Tap = util.StrToMem(o.Network.Tap) + } + } + entry.Network = nestedNetwork + + var nestedUserAcl *UserAclXml + if o.UserAcl != nil { + nestedUserAcl = &UserAclXml{} + if _, ok := o.Misc["UserAcl"]; ok { + nestedUserAcl.Misc = o.Misc["UserAcl"] + } + if o.UserAcl.IncludeList != nil { + nestedUserAcl.IncludeList = util.StrToMem(o.UserAcl.IncludeList) + } + if o.UserAcl.ExcludeList != nil { + nestedUserAcl.ExcludeList = util.StrToMem(o.UserAcl.ExcludeList) + } + } + entry.UserAcl = nestedUserAcl + + entry.Misc = o.Misc["Entry"] + + return entry, nil +} +func (c *entryXmlContainer) Normalize() ([]*Entry, error) { + entryList := make([]*Entry, 0, len(c.Answer)) + for _, o := range c.Answer { + entry := &Entry{ + Misc: make(map[string][]generic.Xml), + } + entry.Name = o.Name + var nestedDeviceAcl *DeviceAcl + if o.DeviceAcl != nil { + nestedDeviceAcl = &DeviceAcl{} + if o.DeviceAcl.Misc != nil { + entry.Misc["DeviceAcl"] = o.DeviceAcl.Misc + } + if o.DeviceAcl.IncludeList != nil { + nestedDeviceAcl.IncludeList = util.MemToStr(o.DeviceAcl.IncludeList) + } + if o.DeviceAcl.ExcludeList != nil { + nestedDeviceAcl.ExcludeList = util.MemToStr(o.DeviceAcl.ExcludeList) + } + } + entry.DeviceAcl = nestedDeviceAcl + + entry.EnableDeviceIdentification = util.AsBool(o.EnableDeviceIdentification, nil) + entry.EnableUserIdentification = util.AsBool(o.EnableUserIdentification, nil) + var nestedNetwork *Network + if o.Network != nil { + nestedNetwork = &Network{} + if o.Network.Misc != nil { + entry.Misc["Network"] = o.Network.Misc + } + if o.Network.EnablePacketBufferProtection != nil { + nestedNetwork.EnablePacketBufferProtection = util.AsBool(o.Network.EnablePacketBufferProtection, nil) + } + if o.Network.ZoneProtectionProfile != nil { + nestedNetwork.ZoneProtectionProfile = util.MemToStr(o.Network.ZoneProtectionProfile) + } + if o.Network.LogSetting != nil { + nestedNetwork.LogSetting = util.MemToStr(o.Network.LogSetting) + } + if o.Network.Layer3 != nil { + nestedNetwork.Layer3 = util.MemToStr(o.Network.Layer3) + } + if o.Network.Layer2 != nil { + nestedNetwork.Layer2 = util.MemToStr(o.Network.Layer2) + } + if o.Network.VirtualWire != nil { + nestedNetwork.VirtualWire = util.MemToStr(o.Network.VirtualWire) + } + if o.Network.Tap != nil { + nestedNetwork.Tap = util.MemToStr(o.Network.Tap) + } + } + entry.Network = nestedNetwork + + var nestedUserAcl *UserAcl + if o.UserAcl != nil { + nestedUserAcl = &UserAcl{} + if o.UserAcl.Misc != nil { + entry.Misc["UserAcl"] = o.UserAcl.Misc + } + if o.UserAcl.IncludeList != nil { + nestedUserAcl.IncludeList = util.MemToStr(o.UserAcl.IncludeList) + } + if o.UserAcl.ExcludeList != nil { + nestedUserAcl.ExcludeList = util.MemToStr(o.UserAcl.ExcludeList) + } + } + entry.UserAcl = nestedUserAcl + + entry.Misc["Entry"] = o.Misc + + entryList = append(entryList, entry) + } + + return entryList, nil +} + +func SpecMatches(a, b *Entry) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + + // Don't compare Name. + if !matchDeviceAcl(a.DeviceAcl, b.DeviceAcl) { + return false + } + if !util.BoolsMatch(a.EnableDeviceIdentification, b.EnableDeviceIdentification) { + return false + } + if !util.BoolsMatch(a.EnableUserIdentification, b.EnableUserIdentification) { + return false + } + if !matchNetwork(a.Network, b.Network) { + return false + } + if !matchUserAcl(a.UserAcl, b.UserAcl) { + return false + } + + return true +} + +func matchUserAcl(a *UserAcl, b *UserAcl) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.OrderedListsMatch(a.IncludeList, b.IncludeList) { + return false + } + if !util.OrderedListsMatch(a.ExcludeList, b.ExcludeList) { + return false + } + return true +} +func matchDeviceAcl(a *DeviceAcl, b *DeviceAcl) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.OrderedListsMatch(a.IncludeList, b.IncludeList) { + return false + } + if !util.OrderedListsMatch(a.ExcludeList, b.ExcludeList) { + return false + } + return true +} +func matchNetwork(a *Network, b *Network) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.BoolsMatch(a.EnablePacketBufferProtection, b.EnablePacketBufferProtection) { + return false + } + if !util.OrderedListsMatch(a.ZoneProtectionProfile, b.ZoneProtectionProfile) { + return false + } + if !util.OrderedListsMatch(a.LogSetting, b.LogSetting) { + return false + } + if !util.OrderedListsMatch(a.Tap, b.Tap) { + return false + } + if !util.OrderedListsMatch(a.Layer3, b.Layer3) { + return false + } + if !util.OrderedListsMatch(a.Layer2, b.Layer2) { + return false + } + if !util.OrderedListsMatch(a.VirtualWire, b.VirtualWire) { + return false + } + return true +} + +func (o *Entry) EntryName() string { + return o.Name +} + +func (o *Entry) SetEntryName(name string) { + o.Name = name +} diff --git a/network/zone/interfaces.go b/network/zone/interfaces.go new file mode 100644 index 0000000..8228a03 --- /dev/null +++ b/network/zone/interfaces.go @@ -0,0 +1,7 @@ +package zone + +type Specifier func(*Entry) (any, error) + +type Normalizer interface { + Normalize() ([]*Entry, error) +} diff --git a/network/zone/location.go b/network/zone/location.go new file mode 100644 index 0000000..3d1ca60 --- /dev/null +++ b/network/zone/location.go @@ -0,0 +1,243 @@ +package zone + +import ( + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +type ImportLocation interface { + XpathForLocation(version.Number, util.ILocation) ([]string, error) + MarshalPangoXML([]string) (string, error) + UnmarshalPangoXML([]byte) ([]string, error) +} + +type Location struct { + FromPanoramaVsys *FromPanoramaVsysLocation `json:"from_panorama_vsys,omitempty"` + Template *TemplateLocation `json:"template,omitempty"` + TemplateStack *TemplateStackLocation `json:"template_stack,omitempty"` + Vsys *VsysLocation `json:"vsys,omitempty"` +} + +type FromPanoramaVsysLocation struct { + Vsys string `json:"vsys"` +} + +type TemplateLocation struct { + NgfwDevice string `json:"ngfw_device"` + PanoramaDevice string `json:"panorama_device"` + Template string `json:"template"` + Vsys string `json:"vsys"` +} + +type TemplateStackLocation struct { + NgfwDevice string `json:"ngfw_device"` + PanoramaDevice string `json:"panorama_device"` + TemplateStack string `json:"template_stack"` + Vsys string `json:"vsys"` +} + +type VsysLocation struct { + NgfwDevice string `json:"ngfw_device"` + Vsys string `json:"vsys"` +} + +func NewFromPanoramaVsysLocation() *Location { + return &Location{FromPanoramaVsys: &FromPanoramaVsysLocation{ + Vsys: "vsys1", + }, + } +} +func NewTemplateLocation() *Location { + return &Location{Template: &TemplateLocation{ + NgfwDevice: "localhost.localdomain", + PanoramaDevice: "localhost.localdomain", + Template: "", + Vsys: "vsys1", + }, + } +} +func NewTemplateStackLocation() *Location { + return &Location{TemplateStack: &TemplateStackLocation{ + NgfwDevice: "localhost.localdomain", + PanoramaDevice: "localhost.localdomain", + TemplateStack: "", + Vsys: "vsys1", + }, + } +} +func NewVsysLocation() *Location { + return &Location{Vsys: &VsysLocation{ + NgfwDevice: "localhost.localdomain", + Vsys: "vsys1", + }, + } +} + +func (o Location) IsValid() error { + count := 0 + + switch { + case o.FromPanoramaVsys != nil: + if o.FromPanoramaVsys.Vsys == "" { + return fmt.Errorf("Vsys is unspecified") + } + count++ + case o.Template != nil: + if o.Template.NgfwDevice == "" { + return fmt.Errorf("NgfwDevice is unspecified") + } + if o.Template.PanoramaDevice == "" { + return fmt.Errorf("PanoramaDevice is unspecified") + } + if o.Template.Template == "" { + return fmt.Errorf("Template is unspecified") + } + if o.Template.Vsys == "" { + return fmt.Errorf("Vsys is unspecified") + } + count++ + case o.TemplateStack != nil: + if o.TemplateStack.NgfwDevice == "" { + return fmt.Errorf("NgfwDevice is unspecified") + } + if o.TemplateStack.PanoramaDevice == "" { + return fmt.Errorf("PanoramaDevice is unspecified") + } + if o.TemplateStack.TemplateStack == "" { + return fmt.Errorf("TemplateStack is unspecified") + } + if o.TemplateStack.Vsys == "" { + return fmt.Errorf("Vsys is unspecified") + } + count++ + case o.Vsys != nil: + if o.Vsys.NgfwDevice == "" { + return fmt.Errorf("NgfwDevice is unspecified") + } + if o.Vsys.Vsys == "" { + return fmt.Errorf("Vsys is unspecified") + } + count++ + } + + if count == 0 { + return fmt.Errorf("no path specified") + } + + if count > 1 { + return fmt.Errorf("multiple paths specified: only one should be specified") + } + + return nil +} + +func (o Location) XpathPrefix(vn version.Number) ([]string, error) { + + var ans []string + + switch { + case o.FromPanoramaVsys != nil: + if o.FromPanoramaVsys.Vsys == "" { + return nil, fmt.Errorf("Vsys is unspecified") + } + ans = []string{ + "config", + "panorama", + "vsys", + util.AsEntryXpath([]string{o.FromPanoramaVsys.Vsys}), + } + case o.Template != nil: + if o.Template.NgfwDevice == "" { + return nil, fmt.Errorf("NgfwDevice is unspecified") + } + if o.Template.PanoramaDevice == "" { + return nil, fmt.Errorf("PanoramaDevice is unspecified") + } + if o.Template.Template == "" { + return nil, fmt.Errorf("Template is unspecified") + } + if o.Template.Vsys == "" { + return nil, fmt.Errorf("Vsys is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.Template.PanoramaDevice}), + "template", + util.AsEntryXpath([]string{o.Template.Template}), + "config", + "devices", + util.AsEntryXpath([]string{o.Template.NgfwDevice}), + "vsys", + util.AsEntryXpath([]string{o.Template.Vsys}), + } + case o.TemplateStack != nil: + if o.TemplateStack.NgfwDevice == "" { + return nil, fmt.Errorf("NgfwDevice is unspecified") + } + if o.TemplateStack.PanoramaDevice == "" { + return nil, fmt.Errorf("PanoramaDevice is unspecified") + } + if o.TemplateStack.TemplateStack == "" { + return nil, fmt.Errorf("TemplateStack is unspecified") + } + if o.TemplateStack.Vsys == "" { + return nil, fmt.Errorf("Vsys is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.TemplateStack.PanoramaDevice}), + "template-stack", + util.AsEntryXpath([]string{o.TemplateStack.TemplateStack}), + "config", + "devices", + util.AsEntryXpath([]string{o.TemplateStack.NgfwDevice}), + "vsys", + util.AsEntryXpath([]string{o.TemplateStack.Vsys}), + } + case o.Vsys != nil: + if o.Vsys.NgfwDevice == "" { + return nil, fmt.Errorf("NgfwDevice is unspecified") + } + if o.Vsys.Vsys == "" { + return nil, fmt.Errorf("Vsys is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.Vsys.NgfwDevice}), + "vsys", + util.AsEntryXpath([]string{o.Vsys.Vsys}), + } + default: + return nil, errors.NoLocationSpecifiedError + } + + return ans, nil +} +func (o Location) XpathWithEntryName(vn version.Number, name string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsEntryXpath([]string{name})) + + return ans, nil +} +func (o Location) XpathWithUuid(vn version.Number, uuid string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsUuidXpath(uuid)) + + return ans, nil +} diff --git a/network/zone/service.go b/network/zone/service.go new file mode 100644 index 0000000..2af5928 --- /dev/null +++ b/network/zone/service.go @@ -0,0 +1,281 @@ +package zone + +import ( + "context" + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/xmlapi" +) + +type Service struct { + client util.PangoClient +} + +func NewService(client util.PangoClient) *Service { + return &Service{ + client: client, + } +} + +// Create adds new item, then returns the result. +func (s *Service) Create(ctx context.Context, loc Location, entry *Entry) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + path, err := loc.XpathWithEntryName(vn, entry.Name) + if err != nil { + return nil, err + } + createSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + cmd := &xmlapi.Config{ + Action: "set", + Xpath: util.AsXpath(path[:len(path)-1]), + Element: createSpec, + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, false, nil); err != nil { + return nil, err + } + return s.Read(ctx, loc, entry.Name, "get") +} + +// Read returns the given config object, using the specified action ("get" or "show"). +func (s *Service) Read(ctx context.Context, loc Location, name, action string) (*Entry, error) { + return s.read(ctx, loc, name, action, false) +} + +// ReadFromConfig returns the given config object from the loaded XML config. +// Requires that client.LoadPanosConfig() has been invoked. +func (s *Service) ReadFromConfig(ctx context.Context, loc Location, name string) (*Entry, error) { + return s.read(ctx, loc, name, "", true) +} + +func (s *Service) read(ctx context.Context, loc Location, value, action string, usePanosConfig bool) (*Entry, error) { + if value == "" { + return nil, errors.NameNotSpecifiedError + } + vn := s.client.Versioning() + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + var path []string + path, err = loc.XpathWithEntryName(vn, value) + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, true, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, errors.ObjectNotFound() + } + return nil, err + } + } + + list, err := normalizer.Normalize() + if err != nil { + return nil, err + } else if len(list) != 1 { + return nil, fmt.Errorf("expected to %q 1 entry, got %d", action, len(list)) + } + + return list[0], nil +} + +// Update updates the given config object, then returns the result. +func (s *Service) Update(ctx context.Context, loc Location, entry *Entry, name string) (*Entry, error) { + return s.update(ctx, loc, entry, name) +} +func (s *Service) update(ctx context.Context, loc Location, entry *Entry, value string) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + updates := xmlapi.NewMultiConfig(2) + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + var old *Entry + if value != "" && value != entry.Name { + path, err := loc.XpathWithEntryName(vn, value) + if err != nil { + return nil, err + } + + old, err = s.Read(ctx, loc, value, "get") + + updates.Add(&xmlapi.Config{ + Action: "rename", + Xpath: util.AsXpath(path), + NewName: entry.Name, + Target: s.client.GetTarget(), + }) + } else { + old, err = s.Read(ctx, loc, entry.Name, "get") + } + if err != nil { + return nil, err + } else if old == nil { + return nil, fmt.Errorf("previous object doesn't exist for update") + } + if !SpecMatches(entry, old) { + path, err := loc.XpathWithEntryName(vn, entry.Name) + if err != nil { + return nil, err + } + + updateSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + updates.Add(&xmlapi.Config{ + Action: "edit", + Xpath: util.AsXpath(path), + Element: updateSpec, + Target: s.client.GetTarget(), + }) + } + + if len(updates.Operations) != 0 { + if _, _, _, err = s.client.MultiConfig(ctx, updates, false, nil); err != nil { + return nil, err + } + } + return s.Read(ctx, loc, entry.Name, "get") +} + +// Delete deletes the given item. +func (s *Service) Delete(ctx context.Context, loc Location, name ...string) error { + return s.delete(ctx, loc, name) +} +func (s *Service) delete(ctx context.Context, loc Location, values []string) error { + for _, value := range values { + if value == "" { + return errors.NameNotSpecifiedError + } + } + + vn := s.client.Versioning() + var err error + deletes := xmlapi.NewMultiConfig(len(values)) + for _, value := range values { + var path []string + path, err = loc.XpathWithEntryName(vn, value) + if err != nil { + return err + } + deletes.Add(&xmlapi.Config{ + Action: "delete", + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + }) + } + + _, _, _, err = s.client.MultiConfig(ctx, deletes, false, nil) + + return err +} + +// List returns a list of objects using the given action ("get" or "show"). +// Params filter and quote are for client side filtering. +func (s *Service) List(ctx context.Context, loc Location, action, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, action, filter, quote, false) +} + +// ListFromConfig returns a list of objects at the given location. +// Requires that client.LoadPanosConfig() has been invoked. +// Params filter and quote are for client side filtering. +func (s *Service) ListFromConfig(ctx context.Context, loc Location, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, "", filter, quote, true) +} + +func (s *Service) list(ctx context.Context, loc Location, action, filter, quote string, usePanosConfig bool) ([]*Entry, error) { + var err error + + var logic *filtering.Group + if filter != "" { + logic, err = filtering.Parse(filter, quote) + if err != nil { + return nil, err + } + } + + vn := s.client.Versioning() + + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + + path, err := loc.XpathWithEntryName(vn, "") + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, false, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, nil + } + return nil, err + } + } + + listing, err := normalizer.Normalize() + if err != nil || logic == nil { + return listing, err + } + + filtered := make([]*Entry, 0, len(listing)) + for _, x := range listing { + ok, err := logic.Matches(x) + if err != nil { + return nil, err + } + if ok { + filtered = append(filtered, x) + } + } + + return filtered, nil +} diff --git a/objects/address/entry.go b/objects/address/entry.go index d74ede2..9ac30da 100644 --- a/objects/address/entry.go +++ b/objects/address/entry.go @@ -21,24 +21,30 @@ var ( type Entry struct { Name string Description *string - Tags []string // ordered + Tags []string + Fqdn *string IpNetmask *string IpRange *string - Fqdn *string - IpWildcard *string // PAN-OS 9.0 + IpWildcard *string Misc map[string][]generic.Xml } -func (e *Entry) CopyMiscFrom(v *Entry) { - if v == nil || len(v.Misc) == 0 { - return - } +type entryXmlContainer struct { + Answer []entryXml `xml:"entry"` +} - e.Misc = make(map[string][]generic.Xml) - for key := range v.Misc { - e.Misc[key] = append([]generic.Xml(nil), v.Misc[key]...) - } +type entryXml struct { + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + Description *string `xml:"description,omitempty"` + Tags *util.MemberType `xml:"tag,omitempty"` + Fqdn *string `xml:"fqdn,omitempty"` + IpNetmask *string `xml:"ip-netmask,omitempty"` + IpRange *string `xml:"ip-range,omitempty"` + IpWildcard *string `xml:"ip-wildcard,omitempty"` + + Misc []generic.Xml `xml:",any"` } func (e *Entry) Field(v string) (any, error) { @@ -54,15 +60,15 @@ func (e *Entry) Field(v string) (any, error) { if v == "tags|LENGTH" || v == "Tags|LENGTH" { return int64(len(e.Tags)), nil } + if v == "fqdn" || v == "Fqdn" { + return e.Fqdn, nil + } if v == "ip_netmask" || v == "IpNetmask" { return e.IpNetmask, nil } if v == "ip_range" || v == "IpRange" { return e.IpRange, nil } - if v == "fqdn" || v == "Fqdn" { - return e.Fqdn, nil - } if v == "ip_wildcard" || v == "IpWildcard" { return e.IpWildcard, nil } @@ -71,60 +77,44 @@ func (e *Entry) Field(v string) (any, error) { } func Versioning(vn version.Number) (Specifier, Normalizer, error) { - return Entry1Specify, &Entry1Container{}, nil + return specifyEntry, &entryXmlContainer{}, nil } -func Entry1Specify(o Entry) (any, error) { - ans := Entry1{} - ans.Name = o.Name - ans.Description = o.Description - ans.Tags = util.StrToMem(o.Tags) - ans.IpNetmask = o.IpNetmask - ans.IpRange = o.IpRange - ans.Fqdn = o.Fqdn - ans.IpWildcard = o.IpWildcard +func specifyEntry(o *Entry) (any, error) { + entry := entryXml{} - ans.Misc = o.Misc[fmt.Sprintf("%s\n%s", "Entry", o.Name)] + entry.Name = o.Name + entry.Description = o.Description + entry.Tags = util.StrToMem(o.Tags) + entry.Fqdn = o.Fqdn + entry.IpNetmask = o.IpNetmask + entry.IpRange = o.IpRange + entry.IpWildcard = o.IpWildcard - return ans, nil -} + entry.Misc = o.Misc["Entry"] -type Entry1Container struct { - Answer []Entry1 `xml:"entry"` + return entry, nil } - -func (c *Entry1Container) Normalize() ([]Entry, error) { - ans := make([]Entry, 0, len(c.Answer)) - for _, var0 := range c.Answer { - var1 := Entry{ +func (c *entryXmlContainer) Normalize() ([]*Entry, error) { + entryList := make([]*Entry, 0, len(c.Answer)) + for _, o := range c.Answer { + entry := &Entry{ Misc: make(map[string][]generic.Xml), } - var1.Name = var0.Name - var1.Description = var0.Description - var1.IpNetmask = var0.IpNetmask - var1.IpRange = var0.IpRange - var1.Fqdn = var0.Fqdn - var1.IpWildcard = var0.IpWildcard + entry.Name = o.Name + entry.Description = o.Description + entry.Tags = util.MemToStr(o.Tags) + entry.Fqdn = o.Fqdn + entry.IpNetmask = o.IpNetmask + entry.IpRange = o.IpRange + entry.IpWildcard = o.IpWildcard - var1.Misc[fmt.Sprintf("%s\n%s", "Entry", var0.Name)] = var0.Misc + entry.Misc["Entry"] = o.Misc - ans = append(ans, var1) + entryList = append(entryList, entry) } - return ans, nil -} - -type Entry1 struct { - XMLName xml.Name `xml:"entry"` - Name string `xml:"name,attr"` - IpNetmask *string `xml:"ip-netmask"` - IpRange *string `xml:"ip-range"` - Fqdn *string `xml:"fqdn"` - IpWildcard *string `xml:"ip-wildcard"` - Description *string `xml:"description,omitempty"` - Tags *util.MemberType `xml:"tag"` - - Misc []generic.Xml `xml:",any"` + return entryList, nil } func SpecMatches(a, b *Entry) bool { @@ -135,30 +125,32 @@ func SpecMatches(a, b *Entry) bool { } // Don't compare Name. - - if !util.OptionalStringsMatch(a.Description, b.Description) { + if !util.StringsMatch(a.Description, b.Description) { return false } - if !util.OrderedListsMatch(a.Tags, b.Tags) { return false } - - if !util.OptionalStringsMatch(a.IpNetmask, b.IpNetmask) { + if !util.StringsMatch(a.Fqdn, b.Fqdn) { return false } - - if !util.OptionalStringsMatch(a.IpRange, b.IpRange) { + if !util.StringsMatch(a.IpNetmask, b.IpNetmask) { return false } - - if !util.OptionalStringsMatch(a.Fqdn, b.Fqdn) { + if !util.StringsMatch(a.IpRange, b.IpRange) { return false } - - if !util.OptionalStringsMatch(a.IpWildcard, b.IpWildcard) { + if !util.StringsMatch(a.IpWildcard, b.IpWildcard) { return false } return true } + +func (o *Entry) EntryName() string { + return o.Name +} + +func (o *Entry) SetEntryName(name string) { + o.Name = name +} diff --git a/objects/address/group/entry.go b/objects/address/group/entry.go new file mode 100644 index 0000000..813f1df --- /dev/null +++ b/objects/address/group/entry.go @@ -0,0 +1,136 @@ +package group + +import ( + "encoding/xml" + "fmt" + + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/generic" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +var ( + _ filtering.Fielder = &Entry{} +) + +var ( + Suffix = []string{"address-group"} +) + +type Entry struct { + Name string + Description *string + Tags []string + Dynamic *string + Static []string + + Misc map[string][]generic.Xml +} + +type entryXmlContainer struct { + Answer []entryXml `xml:"entry"` +} + +type entryXml struct { + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + Description *string `xml:"description,omitempty"` + Tags *util.MemberType `xml:"tag,omitempty"` + Dynamic *string `xml:"dynamic>filter,omitempty"` + Static *util.MemberType `xml:"static,omitempty"` + + Misc []generic.Xml `xml:",any"` +} + +func (e *Entry) Field(v string) (any, error) { + if v == "name" || v == "Name" { + return e.Name, nil + } + if v == "description" || v == "Description" { + return e.Description, nil + } + if v == "tags" || v == "Tags" { + return e.Tags, nil + } + if v == "tags|LENGTH" || v == "Tags|LENGTH" { + return int64(len(e.Tags)), nil + } + if v == "dynamic" || v == "Dynamic" { + return e.Dynamic, nil + } + if v == "static" || v == "Static" { + return e.Static, nil + } + + return nil, fmt.Errorf("unknown field") +} + +func Versioning(vn version.Number) (Specifier, Normalizer, error) { + return specifyEntry, &entryXmlContainer{}, nil +} + +func specifyEntry(o *Entry) (any, error) { + entry := entryXml{} + + entry.Name = o.Name + entry.Description = o.Description + entry.Tags = util.StrToMem(o.Tags) + entry.Dynamic = o.Dynamic + entry.Static = util.StrToMem(o.Static) + + entry.Misc = o.Misc["Entry"] + + return entry, nil +} +func (c *entryXmlContainer) Normalize() ([]*Entry, error) { + entryList := make([]*Entry, 0, len(c.Answer)) + for _, o := range c.Answer { + entry := &Entry{ + Misc: make(map[string][]generic.Xml), + } + entry.Name = o.Name + entry.Description = o.Description + entry.Tags = util.MemToStr(o.Tags) + entry.Dynamic = o.Dynamic + entry.Static = util.MemToStr(o.Static) + + entry.Misc["Entry"] = o.Misc + + entryList = append(entryList, entry) + } + + return entryList, nil +} + +func SpecMatches(a, b *Entry) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + + // Don't compare Name. + if !util.StringsMatch(a.Description, b.Description) { + return false + } + if !util.OrderedListsMatch(a.Tags, b.Tags) { + return false + } + if !util.StringsMatch(a.Dynamic, b.Dynamic) { + return false + } + if !util.OrderedListsMatch(a.Static, b.Static) { + return false + } + + return true +} + +func (o *Entry) EntryName() string { + return o.Name +} + +func (o *Entry) SetEntryName(name string) { + o.Name = name +} diff --git a/objects/address/group/interfaces.go b/objects/address/group/interfaces.go new file mode 100644 index 0000000..463f1be --- /dev/null +++ b/objects/address/group/interfaces.go @@ -0,0 +1,7 @@ +package group + +type Specifier func(*Entry) (any, error) + +type Normalizer interface { + Normalize() ([]*Entry, error) +} diff --git a/objects/address/group/location.go b/objects/address/group/location.go new file mode 100644 index 0000000..204884e --- /dev/null +++ b/objects/address/group/location.go @@ -0,0 +1,188 @@ +package group + +import ( + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +type ImportLocation interface { + XpathForLocation(version.Number, util.ILocation) ([]string, error) + MarshalPangoXML([]string) (string, error) + UnmarshalPangoXML([]byte) ([]string, error) +} + +type Location struct { + DeviceGroup *DeviceGroupLocation `json:"device_group,omitempty"` + FromPanoramaShared bool `json:"from_panorama_shared"` + FromPanoramaVsys *FromPanoramaVsysLocation `json:"from_panorama_vsys,omitempty"` + Shared bool `json:"shared"` + Vsys *VsysLocation `json:"vsys,omitempty"` +} + +type DeviceGroupLocation struct { + DeviceGroup string `json:"device_group"` + PanoramaDevice string `json:"panorama_device"` +} + +type FromPanoramaVsysLocation struct { + Vsys string `json:"vsys"` +} + +type VsysLocation struct { + NgfwDevice string `json:"ngfw_device"` + Vsys string `json:"vsys"` +} + +func NewDeviceGroupLocation() *Location { + return &Location{DeviceGroup: &DeviceGroupLocation{ + DeviceGroup: "", + PanoramaDevice: "localhost.localdomain", + }, + } +} +func NewFromPanoramaVsysLocation() *Location { + return &Location{FromPanoramaVsys: &FromPanoramaVsysLocation{ + Vsys: "vsys1", + }, + } +} +func NewSharedLocation() *Location { + return &Location{ + Shared: true, + } +} +func NewVsysLocation() *Location { + return &Location{Vsys: &VsysLocation{ + NgfwDevice: "localhost.localdomain", + Vsys: "vsys1", + }, + } +} + +func (o Location) IsValid() error { + count := 0 + + switch { + case o.DeviceGroup != nil: + if o.DeviceGroup.DeviceGroup == "" { + return fmt.Errorf("DeviceGroup is unspecified") + } + if o.DeviceGroup.PanoramaDevice == "" { + return fmt.Errorf("PanoramaDevice is unspecified") + } + count++ + case o.FromPanoramaShared: + count++ + case o.FromPanoramaVsys != nil: + if o.FromPanoramaVsys.Vsys == "" { + return fmt.Errorf("Vsys is unspecified") + } + count++ + case o.Shared: + count++ + case o.Vsys != nil: + if o.Vsys.NgfwDevice == "" { + return fmt.Errorf("NgfwDevice is unspecified") + } + if o.Vsys.Vsys == "" { + return fmt.Errorf("Vsys is unspecified") + } + count++ + } + + if count == 0 { + return fmt.Errorf("no path specified") + } + + if count > 1 { + return fmt.Errorf("multiple paths specified: only one should be specified") + } + + return nil +} + +func (o Location) XpathPrefix(vn version.Number) ([]string, error) { + + var ans []string + + switch { + case o.DeviceGroup != nil: + if o.DeviceGroup.DeviceGroup == "" { + return nil, fmt.Errorf("DeviceGroup is unspecified") + } + if o.DeviceGroup.PanoramaDevice == "" { + return nil, fmt.Errorf("PanoramaDevice is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.DeviceGroup.PanoramaDevice}), + "device-group", + util.AsEntryXpath([]string{o.DeviceGroup.DeviceGroup}), + } + case o.FromPanoramaShared: + ans = []string{ + "config", + "panorama", + "shared", + } + case o.FromPanoramaVsys != nil: + if o.FromPanoramaVsys.Vsys == "" { + return nil, fmt.Errorf("Vsys is unspecified") + } + ans = []string{ + "config", + "panorama", + "vsys", + util.AsEntryXpath([]string{o.FromPanoramaVsys.Vsys}), + } + case o.Shared: + ans = []string{ + "config", + "shared", + } + case o.Vsys != nil: + if o.Vsys.NgfwDevice == "" { + return nil, fmt.Errorf("NgfwDevice is unspecified") + } + if o.Vsys.Vsys == "" { + return nil, fmt.Errorf("Vsys is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.Vsys.NgfwDevice}), + "vsys", + util.AsEntryXpath([]string{o.Vsys.Vsys}), + } + default: + return nil, errors.NoLocationSpecifiedError + } + + return ans, nil +} +func (o Location) XpathWithEntryName(vn version.Number, name string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsEntryXpath([]string{name})) + + return ans, nil +} +func (o Location) XpathWithUuid(vn version.Number, uuid string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsUuidXpath(uuid)) + + return ans, nil +} diff --git a/objects/address/group/service.go b/objects/address/group/service.go new file mode 100644 index 0000000..ed586c7 --- /dev/null +++ b/objects/address/group/service.go @@ -0,0 +1,281 @@ +package group + +import ( + "context" + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/xmlapi" +) + +type Service struct { + client util.PangoClient +} + +func NewService(client util.PangoClient) *Service { + return &Service{ + client: client, + } +} + +// Create adds new item, then returns the result. +func (s *Service) Create(ctx context.Context, loc Location, entry *Entry) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + path, err := loc.XpathWithEntryName(vn, entry.Name) + if err != nil { + return nil, err + } + createSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + cmd := &xmlapi.Config{ + Action: "set", + Xpath: util.AsXpath(path[:len(path)-1]), + Element: createSpec, + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, false, nil); err != nil { + return nil, err + } + return s.Read(ctx, loc, entry.Name, "get") +} + +// Read returns the given config object, using the specified action ("get" or "show"). +func (s *Service) Read(ctx context.Context, loc Location, name, action string) (*Entry, error) { + return s.read(ctx, loc, name, action, false) +} + +// ReadFromConfig returns the given config object from the loaded XML config. +// Requires that client.LoadPanosConfig() has been invoked. +func (s *Service) ReadFromConfig(ctx context.Context, loc Location, name string) (*Entry, error) { + return s.read(ctx, loc, name, "", true) +} + +func (s *Service) read(ctx context.Context, loc Location, value, action string, usePanosConfig bool) (*Entry, error) { + if value == "" { + return nil, errors.NameNotSpecifiedError + } + vn := s.client.Versioning() + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + var path []string + path, err = loc.XpathWithEntryName(vn, value) + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, true, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, errors.ObjectNotFound() + } + return nil, err + } + } + + list, err := normalizer.Normalize() + if err != nil { + return nil, err + } else if len(list) != 1 { + return nil, fmt.Errorf("expected to %q 1 entry, got %d", action, len(list)) + } + + return list[0], nil +} + +// Update updates the given config object, then returns the result. +func (s *Service) Update(ctx context.Context, loc Location, entry *Entry, name string) (*Entry, error) { + return s.update(ctx, loc, entry, name) +} +func (s *Service) update(ctx context.Context, loc Location, entry *Entry, value string) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + updates := xmlapi.NewMultiConfig(2) + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + var old *Entry + if value != "" && value != entry.Name { + path, err := loc.XpathWithEntryName(vn, value) + if err != nil { + return nil, err + } + + old, err = s.Read(ctx, loc, value, "get") + + updates.Add(&xmlapi.Config{ + Action: "rename", + Xpath: util.AsXpath(path), + NewName: entry.Name, + Target: s.client.GetTarget(), + }) + } else { + old, err = s.Read(ctx, loc, entry.Name, "get") + } + if err != nil { + return nil, err + } else if old == nil { + return nil, fmt.Errorf("previous object doesn't exist for update") + } + if !SpecMatches(entry, old) { + path, err := loc.XpathWithEntryName(vn, entry.Name) + if err != nil { + return nil, err + } + + updateSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + updates.Add(&xmlapi.Config{ + Action: "edit", + Xpath: util.AsXpath(path), + Element: updateSpec, + Target: s.client.GetTarget(), + }) + } + + if len(updates.Operations) != 0 { + if _, _, _, err = s.client.MultiConfig(ctx, updates, false, nil); err != nil { + return nil, err + } + } + return s.Read(ctx, loc, entry.Name, "get") +} + +// Delete deletes the given item. +func (s *Service) Delete(ctx context.Context, loc Location, name ...string) error { + return s.delete(ctx, loc, name) +} +func (s *Service) delete(ctx context.Context, loc Location, values []string) error { + for _, value := range values { + if value == "" { + return errors.NameNotSpecifiedError + } + } + + vn := s.client.Versioning() + var err error + deletes := xmlapi.NewMultiConfig(len(values)) + for _, value := range values { + var path []string + path, err = loc.XpathWithEntryName(vn, value) + if err != nil { + return err + } + deletes.Add(&xmlapi.Config{ + Action: "delete", + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + }) + } + + _, _, _, err = s.client.MultiConfig(ctx, deletes, false, nil) + + return err +} + +// List returns a list of objects using the given action ("get" or "show"). +// Params filter and quote are for client side filtering. +func (s *Service) List(ctx context.Context, loc Location, action, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, action, filter, quote, false) +} + +// ListFromConfig returns a list of objects at the given location. +// Requires that client.LoadPanosConfig() has been invoked. +// Params filter and quote are for client side filtering. +func (s *Service) ListFromConfig(ctx context.Context, loc Location, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, "", filter, quote, true) +} + +func (s *Service) list(ctx context.Context, loc Location, action, filter, quote string, usePanosConfig bool) ([]*Entry, error) { + var err error + + var logic *filtering.Group + if filter != "" { + logic, err = filtering.Parse(filter, quote) + if err != nil { + return nil, err + } + } + + vn := s.client.Versioning() + + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + + path, err := loc.XpathWithEntryName(vn, "") + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, false, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, nil + } + return nil, err + } + } + + listing, err := normalizer.Normalize() + if err != nil || logic == nil { + return listing, err + } + + filtered := make([]*Entry, 0, len(listing)) + for _, x := range listing { + ok, err := logic.Matches(x) + if err != nil { + return nil, err + } + if ok { + filtered = append(filtered, x) + } + } + + return filtered, nil +} diff --git a/objects/address/interfaces.go b/objects/address/interfaces.go index 6cd929b..7abde91 100644 --- a/objects/address/interfaces.go +++ b/objects/address/interfaces.go @@ -1,7 +1,7 @@ package address -type Specifier func(Entry) (any, error) +type Specifier func(*Entry) (any, error) type Normalizer interface { - Normalize() ([]Entry, error) + Normalize() ([]*Entry, error) } diff --git a/objects/address/location.go b/objects/address/location.go index b250abd..4c95d35 100644 --- a/objects/address/location.go +++ b/objects/address/location.go @@ -8,41 +8,88 @@ import ( "github.com/PaloAltoNetworks/pango/version" ) +type ImportLocation interface { + XpathForLocation(version.Number, util.ILocation) ([]string, error) + MarshalPangoXML([]string) (string, error) + UnmarshalPangoXML([]byte) ([]string, error) +} + type Location struct { - Shared bool `json:"shared"` - Vsys *VsysLocation `json:"vsys,omitempty"` - DeviceGroup *DeviceGroupLocation `json:"device_group,omitempty"` - FromPanorama bool `json:"from_panorama"` + DeviceGroup *DeviceGroupLocation `json:"device_group,omitempty"` + FromPanoramaShared bool `json:"from_panorama_shared"` + FromPanoramaVsys *FromPanoramaVsysLocation `json:"from_panorama_vsys,omitempty"` + Shared bool `json:"shared"` + Vsys *VsysLocation `json:"vsys,omitempty"` } -func (o Location) IsValid() error { - count := 0 +type DeviceGroupLocation struct { + DeviceGroup string `json:"device_group"` + PanoramaDevice string `json:"panorama_device"` +} - if o.Shared { - count++ - } +type FromPanoramaVsysLocation struct { + Vsys string `json:"vsys"` +} - if o.Vsys != nil { - if o.Vsys.Name == "" { - return fmt.Errorf("vsys.name is unspecified") - } - if o.Vsys.NgfwDevice == "" { - return fmt.Errorf("vsys.ngfw_device is unspecified") - } - count++ +type VsysLocation struct { + NgfwDevice string `json:"ngfw_device"` + Vsys string `json:"vsys"` +} + +func NewDeviceGroupLocation() *Location { + return &Location{DeviceGroup: &DeviceGroupLocation{ + DeviceGroup: "", + PanoramaDevice: "localhost.localdomain", + }, + } +} +func NewFromPanoramaVsysLocation() *Location { + return &Location{FromPanoramaVsys: &FromPanoramaVsysLocation{ + Vsys: "vsys1", + }, + } +} +func NewSharedLocation() *Location { + return &Location{ + Shared: true, } +} +func NewVsysLocation() *Location { + return &Location{Vsys: &VsysLocation{ + NgfwDevice: "localhost.localdomain", + Vsys: "vsys1", + }, + } +} + +func (o Location) IsValid() error { + count := 0 - if o.DeviceGroup != nil { - if o.DeviceGroup.Name == "" { - return fmt.Errorf("device_group.name is unspecified") + switch { + case o.DeviceGroup != nil: + if o.DeviceGroup.DeviceGroup == "" { + return fmt.Errorf("DeviceGroup is unspecified") } if o.DeviceGroup.PanoramaDevice == "" { - return fmt.Errorf("device_group.panorama_device is unspecified") + return fmt.Errorf("PanoramaDevice is unspecified") } count++ - } - - if o.FromPanorama { + case o.FromPanoramaShared: + count++ + case o.FromPanoramaVsys != nil: + if o.FromPanoramaVsys.Vsys == "" { + return fmt.Errorf("Vsys is unspecified") + } + count++ + case o.Shared: + count++ + case o.Vsys != nil: + if o.Vsys.NgfwDevice == "" { + return fmt.Errorf("NgfwDevice is unspecified") + } + if o.Vsys.Vsys == "" { + return fmt.Errorf("Vsys is unspecified") + } count++ } @@ -57,10 +104,41 @@ func (o Location) IsValid() error { return nil } -func (o Location) Xpath(vn version.Number, name string) ([]string, error) { +func (o Location) XpathPrefix(vn version.Number) ([]string, error) { + var ans []string switch { + case o.DeviceGroup != nil: + if o.DeviceGroup.DeviceGroup == "" { + return nil, fmt.Errorf("DeviceGroup is unspecified") + } + if o.DeviceGroup.PanoramaDevice == "" { + return nil, fmt.Errorf("PanoramaDevice is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.DeviceGroup.PanoramaDevice}), + "device-group", + util.AsEntryXpath([]string{o.DeviceGroup.DeviceGroup}), + } + case o.FromPanoramaShared: + ans = []string{ + "config", + "panorama", + "shared", + } + case o.FromPanoramaVsys != nil: + if o.FromPanoramaVsys.Vsys == "" { + return nil, fmt.Errorf("Vsys is unspecified") + } + ans = []string{ + "config", + "panorama", + "vsys", + util.AsEntryXpath([]string{o.FromPanoramaVsys.Vsys}), + } case o.Shared: ans = []string{ "config", @@ -70,48 +148,41 @@ func (o Location) Xpath(vn version.Number, name string) ([]string, error) { if o.Vsys.NgfwDevice == "" { return nil, fmt.Errorf("NgfwDevice is unspecified") } - if o.Vsys.Name == "" { - return nil, fmt.Errorf("Name is unspecified") + if o.Vsys.Vsys == "" { + return nil, fmt.Errorf("Vsys is unspecified") } ans = []string{ "config", "devices", util.AsEntryXpath([]string{o.Vsys.NgfwDevice}), "vsys", - util.AsEntryXpath([]string{o.Vsys.Name}), - } - case o.DeviceGroup != nil: - if o.DeviceGroup.PanoramaDevice == "" { - return nil, fmt.Errorf("PanoramaDevice is unspecified") - } - if o.DeviceGroup.Name == "" { - return nil, fmt.Errorf("Name is unspecified") + util.AsEntryXpath([]string{o.Vsys.Vsys}), } - ans = []string{ - "config", - "devices", - util.AsEntryXpath([]string{o.DeviceGroup.PanoramaDevice}), - "device-group", - util.AsEntryXpath([]string{o.DeviceGroup.Name}), - } - case o.FromPanorama: - ans = []string{"config", "panorama"} default: return nil, errors.NoLocationSpecifiedError } + return ans, nil +} +func (o Location) XpathWithEntryName(vn version.Number, name string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } ans = append(ans, Suffix...) ans = append(ans, util.AsEntryXpath([]string{name})) return ans, nil } +func (o Location) XpathWithUuid(vn version.Number, uuid string) ([]string, error) { -type VsysLocation struct { - NgfwDevice string `json:"ngfw_device"` - Name string `json:"name"` -} + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsUuidXpath(uuid)) -type DeviceGroupLocation struct { - PanoramaDevice string `json:"panorama_device"` - Name string `json:"name"` + return ans, nil } diff --git a/objects/address/service.go b/objects/address/service.go index 12a5115..23b0ea6 100644 --- a/objects/address/service.go +++ b/objects/address/service.go @@ -20,26 +20,22 @@ func NewService(client util.PangoClient) *Service { } } -// Create creates the given config object. -func (s *Service) Create(ctx context.Context, loc Location, entry Entry) (*Entry, error) { +// Create adds new item, then returns the result. +func (s *Service) Create(ctx context.Context, loc Location, entry *Entry) (*Entry, error) { if entry.Name == "" { return nil, errors.NameNotSpecifiedError } vn := s.client.Versioning() - // Get versioning stuff. specifier, _, err := Versioning(vn) if err != nil { return nil, err } - - // Get the xpath. - path, err := loc.Xpath(vn, entry.Name) + path, err := loc.XpathWithEntryName(vn, entry.Name) if err != nil { return nil, err } - createSpec, err := specifier(entry) if err != nil { return nil, err @@ -52,93 +48,72 @@ func (s *Service) Create(ctx context.Context, loc Location, entry Entry) (*Entry Target: s.client.GetTarget(), } - // Perform the set. if _, _, err = s.client.Communicate(ctx, cmd, false, nil); err != nil { return nil, err } - - // Return the Read results. return s.Read(ctx, loc, entry.Name, "get") } -// Read returns the given config object, using the specified action. -// -// Param action should be either "get" or "show". +// Read returns the given config object, using the specified action ("get" or "show"). func (s *Service) Read(ctx context.Context, loc Location, name, action string) (*Entry, error) { - if name == "" { - return nil, errors.NameNotSpecifiedError - } - - vn := s.client.Versioning() - _, normalizer, err := Versioning(vn) - if err != nil { - return nil, err - } - - path, err := loc.Xpath(vn, name) - if err != nil { - return nil, err - } - - cmd := &xmlapi.Config{ - Action: action, - Xpath: util.AsXpath(path), - Target: s.client.GetTarget(), - } - - if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { - // action=show returns empty config like this - if err.Error() == "No such node" && action == "show" { - return nil, errors.ObjectNotFound() - } - return nil, err - } - - list, err := normalizer.Normalize() - if err != nil { - return nil, err - } else if len(list) != 1 { - return nil, fmt.Errorf("expected to %q 1 entry, got %d", action, len(list)) - } - - return &list[0], nil + return s.read(ctx, loc, name, action, false) } // ReadFromConfig returns the given config object from the loaded XML config. -// // Requires that client.LoadPanosConfig() has been invoked. func (s *Service) ReadFromConfig(ctx context.Context, loc Location, name string) (*Entry, error) { - if name == "" { + return s.read(ctx, loc, name, "", true) +} + +func (s *Service) read(ctx context.Context, loc Location, value, action string, usePanosConfig bool) (*Entry, error) { + if value == "" { return nil, errors.NameNotSpecifiedError } - vn := s.client.Versioning() _, normalizer, err := Versioning(vn) if err != nil { return nil, err } - - path, err := loc.Xpath(vn, name) + var path []string + path, err = loc.XpathWithEntryName(vn, value) if err != nil { return nil, err } - if _, err = s.client.ReadFromConfig(ctx, path, true, normalizer); err != nil { - return nil, err + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, true, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, errors.ObjectNotFound() + } + return nil, err + } } list, err := normalizer.Normalize() if err != nil { return nil, err } else if len(list) != 1 { - return nil, fmt.Errorf("expected to find 1 entry, got %d", len(list)) + return nil, fmt.Errorf("expected to %q 1 entry, got %d", action, len(list)) } - return &list[0], nil + return list[0], nil } // Update updates the given config object, then returns the result. -func (s *Service) Update(ctx context.Context, loc Location, entry Entry, oldName string) (*Entry, error) { +func (s *Service) Update(ctx context.Context, loc Location, entry *Entry, name string) (*Entry, error) { + return s.update(ctx, loc, entry, name) +} +func (s *Service) update(ctx context.Context, loc Location, entry *Entry, value string) (*Entry, error) { if entry.Name == "" { return nil, errors.NameNotSpecifiedError } @@ -149,40 +124,35 @@ func (s *Service) Update(ctx context.Context, loc Location, entry Entry, oldName if err != nil { return nil, err } - - // Get the old config. var old *Entry - if oldName != "" && oldName != entry.Name { - // Action needed: rename. - path, err := loc.Xpath(vn, oldName) + if value != "" && value != entry.Name { + path, err := loc.XpathWithEntryName(vn, value) if err != nil { return nil, err } - old, err = s.Read(ctx, loc, oldName, "get") + old, err = s.Read(ctx, loc, value, "get") updates.Add(&xmlapi.Config{ Action: "rename", Xpath: util.AsXpath(path), NewName: entry.Name, + Target: s.client.GetTarget(), }) } else { old, err = s.Read(ctx, loc, entry.Name, "get") } if err != nil { return nil, err + } else if old == nil { + return nil, fmt.Errorf("previous object doesn't exist for update") } - - if !SpecMatches(&entry, old) { - // Action needed: edit. - path, err := loc.Xpath(vn, entry.Name) + if !SpecMatches(entry, old) { + path, err := loc.XpathWithEntryName(vn, entry.Name) if err != nil { return nil, err } - // Copy over the misc stuff. - entry.CopyMiscFrom(old) - updateSpec, err := specifier(entry) if err != nil { return nil, err @@ -192,50 +162,64 @@ func (s *Service) Update(ctx context.Context, loc Location, entry Entry, oldName Action: "edit", Xpath: util.AsXpath(path), Element: updateSpec, + Target: s.client.GetTarget(), }) } - // Do the updates we've built up. if len(updates.Operations) != 0 { if _, _, _, err = s.client.MultiConfig(ctx, updates, false, nil); err != nil { return nil, err } } - - // Return the read results. return s.Read(ctx, loc, entry.Name, "get") } // Delete deletes the given item. -func (s *Service) Delete(ctx context.Context, loc Location, name string) error { - if name == "" { - return errors.NameNotSpecifiedError +func (s *Service) Delete(ctx context.Context, loc Location, name ...string) error { + return s.delete(ctx, loc, name) +} +func (s *Service) delete(ctx context.Context, loc Location, values []string) error { + for _, value := range values { + if value == "" { + return errors.NameNotSpecifiedError + } } vn := s.client.Versioning() - - path, err := loc.Xpath(vn, name) - if err != nil { - return err - } - - cmd := &xmlapi.Config{ - Action: "delete", - Xpath: util.AsXpath(path), - Target: s.client.GetTarget(), + var err error + deletes := xmlapi.NewMultiConfig(len(values)) + for _, value := range values { + var path []string + path, err = loc.XpathWithEntryName(vn, value) + if err != nil { + return err + } + deletes.Add(&xmlapi.Config{ + Action: "delete", + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + }) } - _, _, err = s.client.Communicate(ctx, cmd, false, nil) + _, _, _, err = s.client.MultiConfig(ctx, deletes, false, nil) return err } -// List returns a list of service objects using the given action. -// -// Param action should be either "get" or "show". -// +// List returns a list of objects using the given action ("get" or "show"). // Params filter and quote are for client side filtering. -func (s *Service) List(ctx context.Context, loc Location, action, filter, quote string) ([]Entry, error) { +func (s *Service) List(ctx context.Context, loc Location, action, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, action, filter, quote, false) +} + +// ListFromConfig returns a list of objects at the given location. +// Requires that client.LoadPanosConfig() has been invoked. +// Params filter and quote are for client side filtering. +func (s *Service) ListFromConfig(ctx context.Context, loc Location, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, "", filter, quote, true) +} + +func (s *Service) list(ctx context.Context, loc Location, action, filter, quote string, usePanosConfig bool) ([]*Entry, error) { var err error var logic *filtering.Group @@ -253,85 +237,38 @@ func (s *Service) List(ctx context.Context, loc Location, action, filter, quote return nil, err } - path, err := loc.Xpath(vn, "") + path, err := loc.XpathWithEntryName(vn, "") if err != nil { return nil, err } - cmd := &xmlapi.Config{ - Action: action, - Xpath: util.AsXpath(path), - Target: s.client.GetTarget(), - } - - if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { - // action=show returns empty config like this, it is not an error. - if err.Error() == "No such node" && action == "show" { - return nil, nil - } - return nil, err - } - - listing, err := normalizer.Normalize() - if err != nil || logic == nil { - return listing, err - } - - filtered := make([]Entry, 0, len(listing)) - for _, x := range listing { - ok, err := logic.Matches(&x) - if err != nil { + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, false, normalizer); err != nil { return nil, err } - if ok { - filtered = append(filtered, x) + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), } - } - - return filtered, nil -} - -// ListFromConfig returns a list of objects at the given location. -// -// Requires that client.LoadPanosConfig() has been invoked. -// -// Params filter and quote are for client side filtering. -func (s *Service) ListFromConfig(ctx context.Context, loc Location, filter, quote string) ([]Entry, error) { - var err error - var logic *filtering.Group - if filter != "" { - logic, err = filtering.Parse(filter, quote) - if err != nil { + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, nil + } return nil, err } } - vn := s.client.Versioning() - - _, normalizer, err := Versioning(vn) - if err != nil { - return nil, err - } - - path, err := loc.Xpath(vn, "") - if err != nil { - return nil, err - } - path = path[:len(path)-1] - - if _, err = s.client.ReadFromConfig(ctx, path, false, normalizer); err != nil { - return nil, err - } - listing, err := normalizer.Normalize() if err != nil || logic == nil { return listing, err } - filtered := make([]Entry, 0, len(listing)) + filtered := make([]*Entry, 0, len(listing)) for _, x := range listing { - ok, err := logic.Matches(&x) + ok, err := logic.Matches(x) if err != nil { return nil, err } @@ -342,123 +279,3 @@ func (s *Service) ListFromConfig(ctx context.Context, loc Location, filter, quot return filtered, nil } - -// ConfigureGroup performs all necessary set / edit / delete commands to ensure that the -// objects are configured as specified. -func (s *Service) ConfigureGroup(ctx context.Context, loc Location, entries []Entry, prevNames []string) ([]Entry, error) { - var err error - - vn := s.client.Versioning() - updates := xmlapi.NewMultiConfig(len(prevNames) + len(entries)) - specifier, _, err := Versioning(vn) - if err != nil { - return nil, err - } - - curObjs, err := s.List(ctx, loc, "get", "", "") - if err != nil { - return nil, err - } - - //unfound := make([]Entry, 0, len(entries)) - - // Determine set vs edit for desired objects. - for _, entry := range entries { - var found bool - for _, live := range curObjs { - if entry.Name == live.Name { - found = true - if !SpecMatches(&entry, &live) { - path, err := loc.Xpath(vn, entry.Name) - if err != nil { - return nil, err - } - - // Copy over the misc stuff. - entry.CopyMiscFrom(&live) - - elm, err := specifier(entry) - if err != nil { - return nil, err - } - - updates.Add(&xmlapi.Config{ - Action: "edit", - Xpath: util.AsXpath(path), - Element: elm, - }) - } - break - } - } - - if !found { - path, err := loc.Xpath(vn, entry.Name) - if err != nil { - return nil, err - } - - elm, err := specifier(entry) - if err != nil { - return nil, err - } - - updates.Add(&xmlapi.Config{ - Action: "set", - Xpath: util.AsXpath(path), - Element: elm, - }) - } - } - - // Determine which old objects need to be removed. - if len(prevNames) != 0 { - for _, name := range prevNames { - var found bool - for _, entry := range entries { - if entry.Name == name { - found = true - break - } - } - - if !found { - path, err := loc.Xpath(vn, name) - if err != nil { - return nil, err - } - - updates.Add(&xmlapi.Config{ - Action: "delete", - Xpath: util.AsXpath(path), - }) - } - } - } - - // Perform the multi-config if there was stuff to do. - if len(updates.Operations) != 0 { - if _, _, _, err = s.client.MultiConfig(ctx, updates, false, nil); err != nil { - return nil, err - } - } - - // Get the live version of the entries passed in. - curObjs, err = s.List(ctx, loc, "get", "", "") - if err != nil { - return nil, err - } - - ans := make([]Entry, 0, len(entries)) - for _, entry := range entries { - for _, live := range curObjs { - if entry.Name == live.Name { - ans = append(ans, live) - break - } - } - } - - // Done. - return ans, nil -} diff --git a/objects/profiles/entry.go b/objects/profiles/entry.go new file mode 100644 index 0000000..fff04b5 --- /dev/null +++ b/objects/profiles/entry.go @@ -0,0 +1,136 @@ +package profiles + +import ( + "encoding/xml" + "fmt" + + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/generic" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +var ( + _ filtering.Fielder = &Entry{} +) + +var ( + Suffix = []string{"custom-url-category"} +) + +type Entry struct { + Name string + Description *string + DisableOverride *bool + List []string + Type *string + + Misc map[string][]generic.Xml +} + +type entryXmlContainer struct { + Answer []entryXml `xml:"entry"` +} + +type entryXml struct { + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + Description *string `xml:"description,omitempty"` + DisableOverride *string `xml:"disable-override,omitempty"` + List *util.MemberType `xml:"list,omitempty"` + Type *string `xml:"type,omitempty"` + + Misc []generic.Xml `xml:",any"` +} + +func (e *Entry) Field(v string) (any, error) { + if v == "name" || v == "Name" { + return e.Name, nil + } + if v == "description" || v == "Description" { + return e.Description, nil + } + if v == "disable_override" || v == "DisableOverride" { + return e.DisableOverride, nil + } + if v == "list" || v == "List" { + return e.List, nil + } + if v == "list|LENGTH" || v == "List|LENGTH" { + return int64(len(e.List)), nil + } + if v == "type" || v == "Type" { + return e.Type, nil + } + + return nil, fmt.Errorf("unknown field") +} + +func Versioning(vn version.Number) (Specifier, Normalizer, error) { + return specifyEntry, &entryXmlContainer{}, nil +} + +func specifyEntry(o *Entry) (any, error) { + entry := entryXml{} + + entry.Name = o.Name + entry.Description = o.Description + entry.DisableOverride = util.YesNo(o.DisableOverride, nil) + entry.List = util.StrToMem(o.List) + entry.Type = o.Type + + entry.Misc = o.Misc["Entry"] + + return entry, nil +} +func (c *entryXmlContainer) Normalize() ([]*Entry, error) { + entryList := make([]*Entry, 0, len(c.Answer)) + for _, o := range c.Answer { + entry := &Entry{ + Misc: make(map[string][]generic.Xml), + } + entry.Name = o.Name + entry.Description = o.Description + entry.DisableOverride = util.AsBool(o.DisableOverride, nil) + entry.List = util.MemToStr(o.List) + entry.Type = o.Type + + entry.Misc["Entry"] = o.Misc + + entryList = append(entryList, entry) + } + + return entryList, nil +} + +func SpecMatches(a, b *Entry) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + + // Don't compare Name. + if !util.StringsMatch(a.Description, b.Description) { + return false + } + if !util.BoolsMatch(a.DisableOverride, b.DisableOverride) { + return false + } + if !util.OrderedListsMatch(a.List, b.List) { + return false + } + if !util.StringsMatch(a.Type, b.Type) { + return false + } + + return true +} + +func (o *Entry) EntryName() string { + return o.Name +} + +func (o *Entry) SetEntryName(name string) { + o.Name = name +} diff --git a/objects/profiles/interfaces.go b/objects/profiles/interfaces.go new file mode 100644 index 0000000..f896f9b --- /dev/null +++ b/objects/profiles/interfaces.go @@ -0,0 +1,7 @@ +package profiles + +type Specifier func(*Entry) (any, error) + +type Normalizer interface { + Normalize() ([]*Entry, error) +} diff --git a/objects/profiles/location.go b/objects/profiles/location.go new file mode 100644 index 0000000..0942a23 --- /dev/null +++ b/objects/profiles/location.go @@ -0,0 +1,193 @@ +package profiles + +import ( + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +type ImportLocation interface { + XpathForLocation(version.Number, util.ILocation) ([]string, error) + MarshalPangoXML([]string) (string, error) + UnmarshalPangoXML([]byte) ([]string, error) +} + +type Location struct { + DeviceGroup *DeviceGroupLocation `json:"device_group,omitempty"` + FromPanoramaShared bool `json:"from_panorama_shared"` + FromPanoramaVsys *FromPanoramaVsysLocation `json:"from_panorama_vsys,omitempty"` + Shared bool `json:"shared"` + Vsys *VsysLocation `json:"vsys,omitempty"` +} + +type DeviceGroupLocation struct { + DeviceGroup string `json:"device_group"` + PanoramaDevice string `json:"panorama_device"` +} + +type FromPanoramaVsysLocation struct { + Vsys string `json:"vsys"` +} + +type VsysLocation struct { + NgfwDevice string `json:"ngfw_device"` + Vsys string `json:"vsys"` +} + +func NewDeviceGroupLocation() *Location { + return &Location{DeviceGroup: &DeviceGroupLocation{ + DeviceGroup: "", + PanoramaDevice: "localhost.localdomain", + }, + } +} +func NewFromPanoramaVsysLocation() *Location { + return &Location{FromPanoramaVsys: &FromPanoramaVsysLocation{ + Vsys: "vsys1", + }, + } +} +func NewSharedLocation() *Location { + return &Location{ + Shared: true, + } +} +func NewVsysLocation() *Location { + return &Location{Vsys: &VsysLocation{ + NgfwDevice: "localhost.localdomain", + Vsys: "vsys1", + }, + } +} + +func (o Location) IsValid() error { + count := 0 + + switch { + case o.DeviceGroup != nil: + if o.DeviceGroup.DeviceGroup == "" { + return fmt.Errorf("DeviceGroup is unspecified") + } + if o.DeviceGroup.PanoramaDevice == "" { + return fmt.Errorf("PanoramaDevice is unspecified") + } + count++ + case o.FromPanoramaShared: + count++ + case o.FromPanoramaVsys != nil: + if o.FromPanoramaVsys.Vsys == "" { + return fmt.Errorf("Vsys is unspecified") + } + count++ + case o.Shared: + count++ + case o.Vsys != nil: + if o.Vsys.NgfwDevice == "" { + return fmt.Errorf("NgfwDevice is unspecified") + } + if o.Vsys.Vsys == "" { + return fmt.Errorf("Vsys is unspecified") + } + count++ + } + + if count == 0 { + return fmt.Errorf("no path specified") + } + + if count > 1 { + return fmt.Errorf("multiple paths specified: only one should be specified") + } + + return nil +} + +func (o Location) XpathPrefix(vn version.Number) ([]string, error) { + + var ans []string + + switch { + case o.DeviceGroup != nil: + if o.DeviceGroup.DeviceGroup == "" { + return nil, fmt.Errorf("DeviceGroup is unspecified") + } + if o.DeviceGroup.PanoramaDevice == "" { + return nil, fmt.Errorf("PanoramaDevice is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.DeviceGroup.PanoramaDevice}), + "device-group", + util.AsEntryXpath([]string{o.DeviceGroup.DeviceGroup}), + "profiles", + } + case o.FromPanoramaShared: + ans = []string{ + "config", + "panorama", + "shared", + "profiles", + } + case o.FromPanoramaVsys != nil: + if o.FromPanoramaVsys.Vsys == "" { + return nil, fmt.Errorf("Vsys is unspecified") + } + ans = []string{ + "config", + "panorama", + "vsys", + util.AsEntryXpath([]string{o.FromPanoramaVsys.Vsys}), + "profiles", + } + case o.Shared: + ans = []string{ + "config", + "shared", + "profiles", + } + case o.Vsys != nil: + if o.Vsys.NgfwDevice == "" { + return nil, fmt.Errorf("NgfwDevice is unspecified") + } + if o.Vsys.Vsys == "" { + return nil, fmt.Errorf("Vsys is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.Vsys.NgfwDevice}), + "vsys", + util.AsEntryXpath([]string{o.Vsys.Vsys}), + "profiles", + } + default: + return nil, errors.NoLocationSpecifiedError + } + + return ans, nil +} +func (o Location) XpathWithEntryName(vn version.Number, name string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsEntryXpath([]string{name})) + + return ans, nil +} +func (o Location) XpathWithUuid(vn version.Number, uuid string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsUuidXpath(uuid)) + + return ans, nil +} diff --git a/objects/profiles/service.go b/objects/profiles/service.go new file mode 100644 index 0000000..90e5581 --- /dev/null +++ b/objects/profiles/service.go @@ -0,0 +1,281 @@ +package profiles + +import ( + "context" + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/xmlapi" +) + +type Service struct { + client util.PangoClient +} + +func NewService(client util.PangoClient) *Service { + return &Service{ + client: client, + } +} + +// Create adds new item, then returns the result. +func (s *Service) Create(ctx context.Context, loc Location, entry *Entry) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + path, err := loc.XpathWithEntryName(vn, entry.Name) + if err != nil { + return nil, err + } + createSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + cmd := &xmlapi.Config{ + Action: "set", + Xpath: util.AsXpath(path[:len(path)-1]), + Element: createSpec, + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, false, nil); err != nil { + return nil, err + } + return s.Read(ctx, loc, entry.Name, "get") +} + +// Read returns the given config object, using the specified action ("get" or "show"). +func (s *Service) Read(ctx context.Context, loc Location, name, action string) (*Entry, error) { + return s.read(ctx, loc, name, action, false) +} + +// ReadFromConfig returns the given config object from the loaded XML config. +// Requires that client.LoadPanosConfig() has been invoked. +func (s *Service) ReadFromConfig(ctx context.Context, loc Location, name string) (*Entry, error) { + return s.read(ctx, loc, name, "", true) +} + +func (s *Service) read(ctx context.Context, loc Location, value, action string, usePanosConfig bool) (*Entry, error) { + if value == "" { + return nil, errors.NameNotSpecifiedError + } + vn := s.client.Versioning() + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + var path []string + path, err = loc.XpathWithEntryName(vn, value) + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, true, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, errors.ObjectNotFound() + } + return nil, err + } + } + + list, err := normalizer.Normalize() + if err != nil { + return nil, err + } else if len(list) != 1 { + return nil, fmt.Errorf("expected to %q 1 entry, got %d", action, len(list)) + } + + return list[0], nil +} + +// Update updates the given config object, then returns the result. +func (s *Service) Update(ctx context.Context, loc Location, entry *Entry, name string) (*Entry, error) { + return s.update(ctx, loc, entry, name) +} +func (s *Service) update(ctx context.Context, loc Location, entry *Entry, value string) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + updates := xmlapi.NewMultiConfig(2) + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + var old *Entry + if value != "" && value != entry.Name { + path, err := loc.XpathWithEntryName(vn, value) + if err != nil { + return nil, err + } + + old, err = s.Read(ctx, loc, value, "get") + + updates.Add(&xmlapi.Config{ + Action: "rename", + Xpath: util.AsXpath(path), + NewName: entry.Name, + Target: s.client.GetTarget(), + }) + } else { + old, err = s.Read(ctx, loc, entry.Name, "get") + } + if err != nil { + return nil, err + } else if old == nil { + return nil, fmt.Errorf("previous object doesn't exist for update") + } + if !SpecMatches(entry, old) { + path, err := loc.XpathWithEntryName(vn, entry.Name) + if err != nil { + return nil, err + } + + updateSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + updates.Add(&xmlapi.Config{ + Action: "edit", + Xpath: util.AsXpath(path), + Element: updateSpec, + Target: s.client.GetTarget(), + }) + } + + if len(updates.Operations) != 0 { + if _, _, _, err = s.client.MultiConfig(ctx, updates, false, nil); err != nil { + return nil, err + } + } + return s.Read(ctx, loc, entry.Name, "get") +} + +// Delete deletes the given item. +func (s *Service) Delete(ctx context.Context, loc Location, name ...string) error { + return s.delete(ctx, loc, name) +} +func (s *Service) delete(ctx context.Context, loc Location, values []string) error { + for _, value := range values { + if value == "" { + return errors.NameNotSpecifiedError + } + } + + vn := s.client.Versioning() + var err error + deletes := xmlapi.NewMultiConfig(len(values)) + for _, value := range values { + var path []string + path, err = loc.XpathWithEntryName(vn, value) + if err != nil { + return err + } + deletes.Add(&xmlapi.Config{ + Action: "delete", + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + }) + } + + _, _, _, err = s.client.MultiConfig(ctx, deletes, false, nil) + + return err +} + +// List returns a list of objects using the given action ("get" or "show"). +// Params filter and quote are for client side filtering. +func (s *Service) List(ctx context.Context, loc Location, action, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, action, filter, quote, false) +} + +// ListFromConfig returns a list of objects at the given location. +// Requires that client.LoadPanosConfig() has been invoked. +// Params filter and quote are for client side filtering. +func (s *Service) ListFromConfig(ctx context.Context, loc Location, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, "", filter, quote, true) +} + +func (s *Service) list(ctx context.Context, loc Location, action, filter, quote string, usePanosConfig bool) ([]*Entry, error) { + var err error + + var logic *filtering.Group + if filter != "" { + logic, err = filtering.Parse(filter, quote) + if err != nil { + return nil, err + } + } + + vn := s.client.Versioning() + + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + + path, err := loc.XpathWithEntryName(vn, "") + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, false, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, nil + } + return nil, err + } + } + + listing, err := normalizer.Normalize() + if err != nil || logic == nil { + return listing, err + } + + filtered := make([]*Entry, 0, len(listing)) + for _, x := range listing { + ok, err := logic.Matches(x) + if err != nil { + return nil, err + } + if ok { + filtered = append(filtered, x) + } + } + + return filtered, nil +} diff --git a/objects/service/entry.go b/objects/service/entry.go new file mode 100644 index 0000000..ecdae2b --- /dev/null +++ b/objects/service/entry.go @@ -0,0 +1,413 @@ +package service + +import ( + "encoding/xml" + "fmt" + + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/generic" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +var ( + _ filtering.Fielder = &Entry{} +) + +var ( + Suffix = []string{"service"} +) + +type Entry struct { + Name string + Description *string + Protocol *Protocol + Tags []string + + Misc map[string][]generic.Xml +} + +type Protocol struct { + Tcp *ProtocolTcp + Udp *ProtocolUdp +} +type ProtocolTcp struct { + DestinationPort *int64 + Override *ProtocolTcpOverride + SourcePort *int64 +} +type ProtocolTcpOverride struct { + HalfcloseTimeout *int64 + Timeout *int64 + TimewaitTimeout *int64 +} +type ProtocolUdp struct { + DestinationPort *int64 + Override *ProtocolUdpOverride + SourcePort *int64 +} +type ProtocolUdpOverride struct { + No *string + Yes *ProtocolUdpOverrideYes +} +type ProtocolUdpOverrideYes struct { + Timeout *int64 +} + +type entryXmlContainer struct { + Answer []entryXml `xml:"entry"` +} + +type entryXml struct { + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + Description *string `xml:"description,omitempty"` + Protocol *ProtocolXml `xml:"protocol,omitempty"` + Tags *util.MemberType `xml:"tag,omitempty"` + + Misc []generic.Xml `xml:",any"` +} + +type ProtocolXml struct { + Tcp *ProtocolTcpXml `xml:"tcp,omitempty"` + Udp *ProtocolUdpXml `xml:"udp,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type ProtocolTcpXml struct { + DestinationPort *int64 `xml:"port,omitempty"` + Override *ProtocolTcpOverrideXml `xml:"override>yes,omitempty"` + SourcePort *int64 `xml:"source-port,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type ProtocolTcpOverrideXml struct { + HalfcloseTimeout *int64 `xml:"halfclose-timeout,omitempty"` + Timeout *int64 `xml:"timeout,omitempty"` + TimewaitTimeout *int64 `xml:"timewait-timeout,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type ProtocolUdpXml struct { + DestinationPort *int64 `xml:"port,omitempty"` + Override *ProtocolUdpOverrideXml `xml:"override,omitempty"` + SourcePort *int64 `xml:"source-port,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type ProtocolUdpOverrideXml struct { + No *string `xml:"no,omitempty"` + Yes *ProtocolUdpOverrideYesXml `xml:"yes,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type ProtocolUdpOverrideYesXml struct { + Timeout *int64 `xml:"timeout,omitempty"` + + Misc []generic.Xml `xml:",any"` +} + +func (e *Entry) Field(v string) (any, error) { + if v == "name" || v == "Name" { + return e.Name, nil + } + if v == "description" || v == "Description" { + return e.Description, nil + } + if v == "protocol" || v == "Protocol" { + return e.Protocol, nil + } + if v == "tags" || v == "Tags" { + return e.Tags, nil + } + if v == "tags|LENGTH" || v == "Tags|LENGTH" { + return int64(len(e.Tags)), nil + } + + return nil, fmt.Errorf("unknown field") +} + +func Versioning(vn version.Number) (Specifier, Normalizer, error) { + return specifyEntry, &entryXmlContainer{}, nil +} + +func specifyEntry(o *Entry) (any, error) { + entry := entryXml{} + + entry.Name = o.Name + entry.Description = o.Description + var nestedProtocol *ProtocolXml + if o.Protocol != nil { + nestedProtocol = &ProtocolXml{} + if _, ok := o.Misc["Protocol"]; ok { + nestedProtocol.Misc = o.Misc["Protocol"] + } + if o.Protocol.Tcp != nil { + nestedProtocol.Tcp = &ProtocolTcpXml{} + if _, ok := o.Misc["ProtocolTcp"]; ok { + nestedProtocol.Tcp.Misc = o.Misc["ProtocolTcp"] + } + if o.Protocol.Tcp.DestinationPort != nil { + nestedProtocol.Tcp.DestinationPort = o.Protocol.Tcp.DestinationPort + } + if o.Protocol.Tcp.SourcePort != nil { + nestedProtocol.Tcp.SourcePort = o.Protocol.Tcp.SourcePort + } + if o.Protocol.Tcp.Override != nil { + nestedProtocol.Tcp.Override = &ProtocolTcpOverrideXml{} + if _, ok := o.Misc["ProtocolTcpOverride"]; ok { + nestedProtocol.Tcp.Override.Misc = o.Misc["ProtocolTcpOverride"] + } + if o.Protocol.Tcp.Override.TimewaitTimeout != nil { + nestedProtocol.Tcp.Override.TimewaitTimeout = o.Protocol.Tcp.Override.TimewaitTimeout + } + if o.Protocol.Tcp.Override.Timeout != nil { + nestedProtocol.Tcp.Override.Timeout = o.Protocol.Tcp.Override.Timeout + } + if o.Protocol.Tcp.Override.HalfcloseTimeout != nil { + nestedProtocol.Tcp.Override.HalfcloseTimeout = o.Protocol.Tcp.Override.HalfcloseTimeout + } + } + } + if o.Protocol.Udp != nil { + nestedProtocol.Udp = &ProtocolUdpXml{} + if _, ok := o.Misc["ProtocolUdp"]; ok { + nestedProtocol.Udp.Misc = o.Misc["ProtocolUdp"] + } + if o.Protocol.Udp.DestinationPort != nil { + nestedProtocol.Udp.DestinationPort = o.Protocol.Udp.DestinationPort + } + if o.Protocol.Udp.SourcePort != nil { + nestedProtocol.Udp.SourcePort = o.Protocol.Udp.SourcePort + } + if o.Protocol.Udp.Override != nil { + nestedProtocol.Udp.Override = &ProtocolUdpOverrideXml{} + if _, ok := o.Misc["ProtocolUdpOverride"]; ok { + nestedProtocol.Udp.Override.Misc = o.Misc["ProtocolUdpOverride"] + } + if o.Protocol.Udp.Override.Yes != nil { + nestedProtocol.Udp.Override.Yes = &ProtocolUdpOverrideYesXml{} + if _, ok := o.Misc["ProtocolUdpOverrideYes"]; ok { + nestedProtocol.Udp.Override.Yes.Misc = o.Misc["ProtocolUdpOverrideYes"] + } + if o.Protocol.Udp.Override.Yes.Timeout != nil { + nestedProtocol.Udp.Override.Yes.Timeout = o.Protocol.Udp.Override.Yes.Timeout + } + } + if o.Protocol.Udp.Override.No != nil { + nestedProtocol.Udp.Override.No = o.Protocol.Udp.Override.No + } + } + } + } + entry.Protocol = nestedProtocol + + entry.Tags = util.StrToMem(o.Tags) + + entry.Misc = o.Misc["Entry"] + + return entry, nil +} +func (c *entryXmlContainer) Normalize() ([]*Entry, error) { + entryList := make([]*Entry, 0, len(c.Answer)) + for _, o := range c.Answer { + entry := &Entry{ + Misc: make(map[string][]generic.Xml), + } + entry.Name = o.Name + entry.Description = o.Description + var nestedProtocol *Protocol + if o.Protocol != nil { + nestedProtocol = &Protocol{} + if o.Protocol.Misc != nil { + entry.Misc["Protocol"] = o.Protocol.Misc + } + if o.Protocol.Udp != nil { + nestedProtocol.Udp = &ProtocolUdp{} + if o.Protocol.Udp.Misc != nil { + entry.Misc["ProtocolUdp"] = o.Protocol.Udp.Misc + } + if o.Protocol.Udp.DestinationPort != nil { + nestedProtocol.Udp.DestinationPort = o.Protocol.Udp.DestinationPort + } + if o.Protocol.Udp.SourcePort != nil { + nestedProtocol.Udp.SourcePort = o.Protocol.Udp.SourcePort + } + if o.Protocol.Udp.Override != nil { + nestedProtocol.Udp.Override = &ProtocolUdpOverride{} + if o.Protocol.Udp.Override.Misc != nil { + entry.Misc["ProtocolUdpOverride"] = o.Protocol.Udp.Override.Misc + } + if o.Protocol.Udp.Override.Yes != nil { + nestedProtocol.Udp.Override.Yes = &ProtocolUdpOverrideYes{} + if o.Protocol.Udp.Override.Yes.Misc != nil { + entry.Misc["ProtocolUdpOverrideYes"] = o.Protocol.Udp.Override.Yes.Misc + } + if o.Protocol.Udp.Override.Yes.Timeout != nil { + nestedProtocol.Udp.Override.Yes.Timeout = o.Protocol.Udp.Override.Yes.Timeout + } + } + if o.Protocol.Udp.Override.No != nil { + nestedProtocol.Udp.Override.No = o.Protocol.Udp.Override.No + } + } + } + if o.Protocol.Tcp != nil { + nestedProtocol.Tcp = &ProtocolTcp{} + if o.Protocol.Tcp.Misc != nil { + entry.Misc["ProtocolTcp"] = o.Protocol.Tcp.Misc + } + if o.Protocol.Tcp.DestinationPort != nil { + nestedProtocol.Tcp.DestinationPort = o.Protocol.Tcp.DestinationPort + } + if o.Protocol.Tcp.SourcePort != nil { + nestedProtocol.Tcp.SourcePort = o.Protocol.Tcp.SourcePort + } + if o.Protocol.Tcp.Override != nil { + nestedProtocol.Tcp.Override = &ProtocolTcpOverride{} + if o.Protocol.Tcp.Override.Misc != nil { + entry.Misc["ProtocolTcpOverride"] = o.Protocol.Tcp.Override.Misc + } + if o.Protocol.Tcp.Override.Timeout != nil { + nestedProtocol.Tcp.Override.Timeout = o.Protocol.Tcp.Override.Timeout + } + if o.Protocol.Tcp.Override.HalfcloseTimeout != nil { + nestedProtocol.Tcp.Override.HalfcloseTimeout = o.Protocol.Tcp.Override.HalfcloseTimeout + } + if o.Protocol.Tcp.Override.TimewaitTimeout != nil { + nestedProtocol.Tcp.Override.TimewaitTimeout = o.Protocol.Tcp.Override.TimewaitTimeout + } + } + } + } + entry.Protocol = nestedProtocol + + entry.Tags = util.MemToStr(o.Tags) + + entry.Misc["Entry"] = o.Misc + + entryList = append(entryList, entry) + } + + return entryList, nil +} + +func SpecMatches(a, b *Entry) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + + // Don't compare Name. + if !util.StringsMatch(a.Description, b.Description) { + return false + } + if !matchProtocol(a.Protocol, b.Protocol) { + return false + } + if !util.OrderedListsMatch(a.Tags, b.Tags) { + return false + } + + return true +} + +func matchProtocolTcpOverride(a *ProtocolTcpOverride, b *ProtocolTcpOverride) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.Ints64Match(a.HalfcloseTimeout, b.HalfcloseTimeout) { + return false + } + if !util.Ints64Match(a.TimewaitTimeout, b.TimewaitTimeout) { + return false + } + if !util.Ints64Match(a.Timeout, b.Timeout) { + return false + } + return true +} +func matchProtocolTcp(a *ProtocolTcp, b *ProtocolTcp) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.Ints64Match(a.DestinationPort, b.DestinationPort) { + return false + } + if !util.Ints64Match(a.SourcePort, b.SourcePort) { + return false + } + if !matchProtocolTcpOverride(a.Override, b.Override) { + return false + } + return true +} +func matchProtocolUdpOverrideYes(a *ProtocolUdpOverrideYes, b *ProtocolUdpOverrideYes) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.Ints64Match(a.Timeout, b.Timeout) { + return false + } + return true +} +func matchProtocolUdpOverride(a *ProtocolUdpOverride, b *ProtocolUdpOverride) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.StringsMatch(a.No, b.No) { + return false + } + if !matchProtocolUdpOverrideYes(a.Yes, b.Yes) { + return false + } + return true +} +func matchProtocolUdp(a *ProtocolUdp, b *ProtocolUdp) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.Ints64Match(a.SourcePort, b.SourcePort) { + return false + } + if !matchProtocolUdpOverride(a.Override, b.Override) { + return false + } + if !util.Ints64Match(a.DestinationPort, b.DestinationPort) { + return false + } + return true +} +func matchProtocol(a *Protocol, b *Protocol) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !matchProtocolTcp(a.Tcp, b.Tcp) { + return false + } + if !matchProtocolUdp(a.Udp, b.Udp) { + return false + } + return true +} + +func (o *Entry) EntryName() string { + return o.Name +} + +func (o *Entry) SetEntryName(name string) { + o.Name = name +} diff --git a/objects/service/group/entry.go b/objects/service/group/entry.go new file mode 100644 index 0000000..58d2faa --- /dev/null +++ b/objects/service/group/entry.go @@ -0,0 +1,129 @@ +package group + +import ( + "encoding/xml" + "fmt" + + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/generic" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +var ( + _ filtering.Fielder = &Entry{} +) + +var ( + Suffix = []string{"service-group"} +) + +type Entry struct { + Name string + Description *string + Members []string + Tags []string + + Misc map[string][]generic.Xml +} + +type entryXmlContainer struct { + Answer []entryXml `xml:"entry"` +} + +type entryXml struct { + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + Description *string `xml:"description,omitempty"` + Members *util.MemberType `xml:"members,omitempty"` + Tags *util.MemberType `xml:"tag,omitempty"` + + Misc []generic.Xml `xml:",any"` +} + +func (e *Entry) Field(v string) (any, error) { + if v == "name" || v == "Name" { + return e.Name, nil + } + if v == "description" || v == "Description" { + return e.Description, nil + } + if v == "members" || v == "Members" { + return e.Members, nil + } + if v == "members|LENGTH" || v == "Members|LENGTH" { + return int64(len(e.Members)), nil + } + if v == "tags" || v == "Tags" { + return e.Tags, nil + } + if v == "tags|LENGTH" || v == "Tags|LENGTH" { + return int64(len(e.Tags)), nil + } + + return nil, fmt.Errorf("unknown field") +} + +func Versioning(vn version.Number) (Specifier, Normalizer, error) { + return specifyEntry, &entryXmlContainer{}, nil +} + +func specifyEntry(o *Entry) (any, error) { + entry := entryXml{} + + entry.Name = o.Name + entry.Description = o.Description + entry.Members = util.StrToMem(o.Members) + entry.Tags = util.StrToMem(o.Tags) + + entry.Misc = o.Misc["Entry"] + + return entry, nil +} +func (c *entryXmlContainer) Normalize() ([]*Entry, error) { + entryList := make([]*Entry, 0, len(c.Answer)) + for _, o := range c.Answer { + entry := &Entry{ + Misc: make(map[string][]generic.Xml), + } + entry.Name = o.Name + entry.Description = o.Description + entry.Members = util.MemToStr(o.Members) + entry.Tags = util.MemToStr(o.Tags) + + entry.Misc["Entry"] = o.Misc + + entryList = append(entryList, entry) + } + + return entryList, nil +} + +func SpecMatches(a, b *Entry) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + + // Don't compare Name. + if !util.StringsMatch(a.Description, b.Description) { + return false + } + if !util.OrderedListsMatch(a.Members, b.Members) { + return false + } + if !util.OrderedListsMatch(a.Tags, b.Tags) { + return false + } + + return true +} + +func (o *Entry) EntryName() string { + return o.Name +} + +func (o *Entry) SetEntryName(name string) { + o.Name = name +} diff --git a/objects/service/group/interfaces.go b/objects/service/group/interfaces.go new file mode 100644 index 0000000..463f1be --- /dev/null +++ b/objects/service/group/interfaces.go @@ -0,0 +1,7 @@ +package group + +type Specifier func(*Entry) (any, error) + +type Normalizer interface { + Normalize() ([]*Entry, error) +} diff --git a/objects/service/group/location.go b/objects/service/group/location.go new file mode 100644 index 0000000..204884e --- /dev/null +++ b/objects/service/group/location.go @@ -0,0 +1,188 @@ +package group + +import ( + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +type ImportLocation interface { + XpathForLocation(version.Number, util.ILocation) ([]string, error) + MarshalPangoXML([]string) (string, error) + UnmarshalPangoXML([]byte) ([]string, error) +} + +type Location struct { + DeviceGroup *DeviceGroupLocation `json:"device_group,omitempty"` + FromPanoramaShared bool `json:"from_panorama_shared"` + FromPanoramaVsys *FromPanoramaVsysLocation `json:"from_panorama_vsys,omitempty"` + Shared bool `json:"shared"` + Vsys *VsysLocation `json:"vsys,omitempty"` +} + +type DeviceGroupLocation struct { + DeviceGroup string `json:"device_group"` + PanoramaDevice string `json:"panorama_device"` +} + +type FromPanoramaVsysLocation struct { + Vsys string `json:"vsys"` +} + +type VsysLocation struct { + NgfwDevice string `json:"ngfw_device"` + Vsys string `json:"vsys"` +} + +func NewDeviceGroupLocation() *Location { + return &Location{DeviceGroup: &DeviceGroupLocation{ + DeviceGroup: "", + PanoramaDevice: "localhost.localdomain", + }, + } +} +func NewFromPanoramaVsysLocation() *Location { + return &Location{FromPanoramaVsys: &FromPanoramaVsysLocation{ + Vsys: "vsys1", + }, + } +} +func NewSharedLocation() *Location { + return &Location{ + Shared: true, + } +} +func NewVsysLocation() *Location { + return &Location{Vsys: &VsysLocation{ + NgfwDevice: "localhost.localdomain", + Vsys: "vsys1", + }, + } +} + +func (o Location) IsValid() error { + count := 0 + + switch { + case o.DeviceGroup != nil: + if o.DeviceGroup.DeviceGroup == "" { + return fmt.Errorf("DeviceGroup is unspecified") + } + if o.DeviceGroup.PanoramaDevice == "" { + return fmt.Errorf("PanoramaDevice is unspecified") + } + count++ + case o.FromPanoramaShared: + count++ + case o.FromPanoramaVsys != nil: + if o.FromPanoramaVsys.Vsys == "" { + return fmt.Errorf("Vsys is unspecified") + } + count++ + case o.Shared: + count++ + case o.Vsys != nil: + if o.Vsys.NgfwDevice == "" { + return fmt.Errorf("NgfwDevice is unspecified") + } + if o.Vsys.Vsys == "" { + return fmt.Errorf("Vsys is unspecified") + } + count++ + } + + if count == 0 { + return fmt.Errorf("no path specified") + } + + if count > 1 { + return fmt.Errorf("multiple paths specified: only one should be specified") + } + + return nil +} + +func (o Location) XpathPrefix(vn version.Number) ([]string, error) { + + var ans []string + + switch { + case o.DeviceGroup != nil: + if o.DeviceGroup.DeviceGroup == "" { + return nil, fmt.Errorf("DeviceGroup is unspecified") + } + if o.DeviceGroup.PanoramaDevice == "" { + return nil, fmt.Errorf("PanoramaDevice is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.DeviceGroup.PanoramaDevice}), + "device-group", + util.AsEntryXpath([]string{o.DeviceGroup.DeviceGroup}), + } + case o.FromPanoramaShared: + ans = []string{ + "config", + "panorama", + "shared", + } + case o.FromPanoramaVsys != nil: + if o.FromPanoramaVsys.Vsys == "" { + return nil, fmt.Errorf("Vsys is unspecified") + } + ans = []string{ + "config", + "panorama", + "vsys", + util.AsEntryXpath([]string{o.FromPanoramaVsys.Vsys}), + } + case o.Shared: + ans = []string{ + "config", + "shared", + } + case o.Vsys != nil: + if o.Vsys.NgfwDevice == "" { + return nil, fmt.Errorf("NgfwDevice is unspecified") + } + if o.Vsys.Vsys == "" { + return nil, fmt.Errorf("Vsys is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.Vsys.NgfwDevice}), + "vsys", + util.AsEntryXpath([]string{o.Vsys.Vsys}), + } + default: + return nil, errors.NoLocationSpecifiedError + } + + return ans, nil +} +func (o Location) XpathWithEntryName(vn version.Number, name string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsEntryXpath([]string{name})) + + return ans, nil +} +func (o Location) XpathWithUuid(vn version.Number, uuid string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsUuidXpath(uuid)) + + return ans, nil +} diff --git a/objects/service/group/service.go b/objects/service/group/service.go new file mode 100644 index 0000000..ed586c7 --- /dev/null +++ b/objects/service/group/service.go @@ -0,0 +1,281 @@ +package group + +import ( + "context" + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/xmlapi" +) + +type Service struct { + client util.PangoClient +} + +func NewService(client util.PangoClient) *Service { + return &Service{ + client: client, + } +} + +// Create adds new item, then returns the result. +func (s *Service) Create(ctx context.Context, loc Location, entry *Entry) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + path, err := loc.XpathWithEntryName(vn, entry.Name) + if err != nil { + return nil, err + } + createSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + cmd := &xmlapi.Config{ + Action: "set", + Xpath: util.AsXpath(path[:len(path)-1]), + Element: createSpec, + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, false, nil); err != nil { + return nil, err + } + return s.Read(ctx, loc, entry.Name, "get") +} + +// Read returns the given config object, using the specified action ("get" or "show"). +func (s *Service) Read(ctx context.Context, loc Location, name, action string) (*Entry, error) { + return s.read(ctx, loc, name, action, false) +} + +// ReadFromConfig returns the given config object from the loaded XML config. +// Requires that client.LoadPanosConfig() has been invoked. +func (s *Service) ReadFromConfig(ctx context.Context, loc Location, name string) (*Entry, error) { + return s.read(ctx, loc, name, "", true) +} + +func (s *Service) read(ctx context.Context, loc Location, value, action string, usePanosConfig bool) (*Entry, error) { + if value == "" { + return nil, errors.NameNotSpecifiedError + } + vn := s.client.Versioning() + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + var path []string + path, err = loc.XpathWithEntryName(vn, value) + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, true, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, errors.ObjectNotFound() + } + return nil, err + } + } + + list, err := normalizer.Normalize() + if err != nil { + return nil, err + } else if len(list) != 1 { + return nil, fmt.Errorf("expected to %q 1 entry, got %d", action, len(list)) + } + + return list[0], nil +} + +// Update updates the given config object, then returns the result. +func (s *Service) Update(ctx context.Context, loc Location, entry *Entry, name string) (*Entry, error) { + return s.update(ctx, loc, entry, name) +} +func (s *Service) update(ctx context.Context, loc Location, entry *Entry, value string) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + updates := xmlapi.NewMultiConfig(2) + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + var old *Entry + if value != "" && value != entry.Name { + path, err := loc.XpathWithEntryName(vn, value) + if err != nil { + return nil, err + } + + old, err = s.Read(ctx, loc, value, "get") + + updates.Add(&xmlapi.Config{ + Action: "rename", + Xpath: util.AsXpath(path), + NewName: entry.Name, + Target: s.client.GetTarget(), + }) + } else { + old, err = s.Read(ctx, loc, entry.Name, "get") + } + if err != nil { + return nil, err + } else if old == nil { + return nil, fmt.Errorf("previous object doesn't exist for update") + } + if !SpecMatches(entry, old) { + path, err := loc.XpathWithEntryName(vn, entry.Name) + if err != nil { + return nil, err + } + + updateSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + updates.Add(&xmlapi.Config{ + Action: "edit", + Xpath: util.AsXpath(path), + Element: updateSpec, + Target: s.client.GetTarget(), + }) + } + + if len(updates.Operations) != 0 { + if _, _, _, err = s.client.MultiConfig(ctx, updates, false, nil); err != nil { + return nil, err + } + } + return s.Read(ctx, loc, entry.Name, "get") +} + +// Delete deletes the given item. +func (s *Service) Delete(ctx context.Context, loc Location, name ...string) error { + return s.delete(ctx, loc, name) +} +func (s *Service) delete(ctx context.Context, loc Location, values []string) error { + for _, value := range values { + if value == "" { + return errors.NameNotSpecifiedError + } + } + + vn := s.client.Versioning() + var err error + deletes := xmlapi.NewMultiConfig(len(values)) + for _, value := range values { + var path []string + path, err = loc.XpathWithEntryName(vn, value) + if err != nil { + return err + } + deletes.Add(&xmlapi.Config{ + Action: "delete", + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + }) + } + + _, _, _, err = s.client.MultiConfig(ctx, deletes, false, nil) + + return err +} + +// List returns a list of objects using the given action ("get" or "show"). +// Params filter and quote are for client side filtering. +func (s *Service) List(ctx context.Context, loc Location, action, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, action, filter, quote, false) +} + +// ListFromConfig returns a list of objects at the given location. +// Requires that client.LoadPanosConfig() has been invoked. +// Params filter and quote are for client side filtering. +func (s *Service) ListFromConfig(ctx context.Context, loc Location, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, "", filter, quote, true) +} + +func (s *Service) list(ctx context.Context, loc Location, action, filter, quote string, usePanosConfig bool) ([]*Entry, error) { + var err error + + var logic *filtering.Group + if filter != "" { + logic, err = filtering.Parse(filter, quote) + if err != nil { + return nil, err + } + } + + vn := s.client.Versioning() + + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + + path, err := loc.XpathWithEntryName(vn, "") + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, false, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, nil + } + return nil, err + } + } + + listing, err := normalizer.Normalize() + if err != nil || logic == nil { + return listing, err + } + + filtered := make([]*Entry, 0, len(listing)) + for _, x := range listing { + ok, err := logic.Matches(x) + if err != nil { + return nil, err + } + if ok { + filtered = append(filtered, x) + } + } + + return filtered, nil +} diff --git a/objects/service/interfaces.go b/objects/service/interfaces.go new file mode 100644 index 0000000..7ae6bdc --- /dev/null +++ b/objects/service/interfaces.go @@ -0,0 +1,7 @@ +package service + +type Specifier func(*Entry) (any, error) + +type Normalizer interface { + Normalize() ([]*Entry, error) +} diff --git a/objects/service/location.go b/objects/service/location.go new file mode 100644 index 0000000..6e29a18 --- /dev/null +++ b/objects/service/location.go @@ -0,0 +1,188 @@ +package service + +import ( + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +type ImportLocation interface { + XpathForLocation(version.Number, util.ILocation) ([]string, error) + MarshalPangoXML([]string) (string, error) + UnmarshalPangoXML([]byte) ([]string, error) +} + +type Location struct { + DeviceGroup *DeviceGroupLocation `json:"device_group,omitempty"` + FromPanoramaShared bool `json:"from_panorama_shared"` + FromPanoramaVsys *FromPanoramaVsysLocation `json:"from_panorama_vsys,omitempty"` + Shared bool `json:"shared"` + Vsys *VsysLocation `json:"vsys,omitempty"` +} + +type DeviceGroupLocation struct { + DeviceGroup string `json:"device_group"` + PanoramaDevice string `json:"panorama_device"` +} + +type FromPanoramaVsysLocation struct { + Vsys string `json:"vsys"` +} + +type VsysLocation struct { + NgfwDevice string `json:"ngfw_device"` + Vsys string `json:"vsys"` +} + +func NewDeviceGroupLocation() *Location { + return &Location{DeviceGroup: &DeviceGroupLocation{ + DeviceGroup: "", + PanoramaDevice: "localhost.localdomain", + }, + } +} +func NewFromPanoramaVsysLocation() *Location { + return &Location{FromPanoramaVsys: &FromPanoramaVsysLocation{ + Vsys: "vsys1", + }, + } +} +func NewSharedLocation() *Location { + return &Location{ + Shared: true, + } +} +func NewVsysLocation() *Location { + return &Location{Vsys: &VsysLocation{ + NgfwDevice: "localhost.localdomain", + Vsys: "vsys1", + }, + } +} + +func (o Location) IsValid() error { + count := 0 + + switch { + case o.DeviceGroup != nil: + if o.DeviceGroup.DeviceGroup == "" { + return fmt.Errorf("DeviceGroup is unspecified") + } + if o.DeviceGroup.PanoramaDevice == "" { + return fmt.Errorf("PanoramaDevice is unspecified") + } + count++ + case o.FromPanoramaShared: + count++ + case o.FromPanoramaVsys != nil: + if o.FromPanoramaVsys.Vsys == "" { + return fmt.Errorf("Vsys is unspecified") + } + count++ + case o.Shared: + count++ + case o.Vsys != nil: + if o.Vsys.NgfwDevice == "" { + return fmt.Errorf("NgfwDevice is unspecified") + } + if o.Vsys.Vsys == "" { + return fmt.Errorf("Vsys is unspecified") + } + count++ + } + + if count == 0 { + return fmt.Errorf("no path specified") + } + + if count > 1 { + return fmt.Errorf("multiple paths specified: only one should be specified") + } + + return nil +} + +func (o Location) XpathPrefix(vn version.Number) ([]string, error) { + + var ans []string + + switch { + case o.DeviceGroup != nil: + if o.DeviceGroup.DeviceGroup == "" { + return nil, fmt.Errorf("DeviceGroup is unspecified") + } + if o.DeviceGroup.PanoramaDevice == "" { + return nil, fmt.Errorf("PanoramaDevice is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.DeviceGroup.PanoramaDevice}), + "device-group", + util.AsEntryXpath([]string{o.DeviceGroup.DeviceGroup}), + } + case o.FromPanoramaShared: + ans = []string{ + "config", + "panorama", + "shared", + } + case o.FromPanoramaVsys != nil: + if o.FromPanoramaVsys.Vsys == "" { + return nil, fmt.Errorf("Vsys is unspecified") + } + ans = []string{ + "config", + "panorama", + "vsys", + util.AsEntryXpath([]string{o.FromPanoramaVsys.Vsys}), + } + case o.Shared: + ans = []string{ + "config", + "shared", + } + case o.Vsys != nil: + if o.Vsys.NgfwDevice == "" { + return nil, fmt.Errorf("NgfwDevice is unspecified") + } + if o.Vsys.Vsys == "" { + return nil, fmt.Errorf("Vsys is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.Vsys.NgfwDevice}), + "vsys", + util.AsEntryXpath([]string{o.Vsys.Vsys}), + } + default: + return nil, errors.NoLocationSpecifiedError + } + + return ans, nil +} +func (o Location) XpathWithEntryName(vn version.Number, name string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsEntryXpath([]string{name})) + + return ans, nil +} +func (o Location) XpathWithUuid(vn version.Number, uuid string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsUuidXpath(uuid)) + + return ans, nil +} diff --git a/objects/service/service.go b/objects/service/service.go new file mode 100644 index 0000000..42a6234 --- /dev/null +++ b/objects/service/service.go @@ -0,0 +1,281 @@ +package service + +import ( + "context" + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/xmlapi" +) + +type Service struct { + client util.PangoClient +} + +func NewService(client util.PangoClient) *Service { + return &Service{ + client: client, + } +} + +// Create adds new item, then returns the result. +func (s *Service) Create(ctx context.Context, loc Location, entry *Entry) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + path, err := loc.XpathWithEntryName(vn, entry.Name) + if err != nil { + return nil, err + } + createSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + cmd := &xmlapi.Config{ + Action: "set", + Xpath: util.AsXpath(path[:len(path)-1]), + Element: createSpec, + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, false, nil); err != nil { + return nil, err + } + return s.Read(ctx, loc, entry.Name, "get") +} + +// Read returns the given config object, using the specified action ("get" or "show"). +func (s *Service) Read(ctx context.Context, loc Location, name, action string) (*Entry, error) { + return s.read(ctx, loc, name, action, false) +} + +// ReadFromConfig returns the given config object from the loaded XML config. +// Requires that client.LoadPanosConfig() has been invoked. +func (s *Service) ReadFromConfig(ctx context.Context, loc Location, name string) (*Entry, error) { + return s.read(ctx, loc, name, "", true) +} + +func (s *Service) read(ctx context.Context, loc Location, value, action string, usePanosConfig bool) (*Entry, error) { + if value == "" { + return nil, errors.NameNotSpecifiedError + } + vn := s.client.Versioning() + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + var path []string + path, err = loc.XpathWithEntryName(vn, value) + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, true, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, errors.ObjectNotFound() + } + return nil, err + } + } + + list, err := normalizer.Normalize() + if err != nil { + return nil, err + } else if len(list) != 1 { + return nil, fmt.Errorf("expected to %q 1 entry, got %d", action, len(list)) + } + + return list[0], nil +} + +// Update updates the given config object, then returns the result. +func (s *Service) Update(ctx context.Context, loc Location, entry *Entry, name string) (*Entry, error) { + return s.update(ctx, loc, entry, name) +} +func (s *Service) update(ctx context.Context, loc Location, entry *Entry, value string) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + updates := xmlapi.NewMultiConfig(2) + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + var old *Entry + if value != "" && value != entry.Name { + path, err := loc.XpathWithEntryName(vn, value) + if err != nil { + return nil, err + } + + old, err = s.Read(ctx, loc, value, "get") + + updates.Add(&xmlapi.Config{ + Action: "rename", + Xpath: util.AsXpath(path), + NewName: entry.Name, + Target: s.client.GetTarget(), + }) + } else { + old, err = s.Read(ctx, loc, entry.Name, "get") + } + if err != nil { + return nil, err + } else if old == nil { + return nil, fmt.Errorf("previous object doesn't exist for update") + } + if !SpecMatches(entry, old) { + path, err := loc.XpathWithEntryName(vn, entry.Name) + if err != nil { + return nil, err + } + + updateSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + updates.Add(&xmlapi.Config{ + Action: "edit", + Xpath: util.AsXpath(path), + Element: updateSpec, + Target: s.client.GetTarget(), + }) + } + + if len(updates.Operations) != 0 { + if _, _, _, err = s.client.MultiConfig(ctx, updates, false, nil); err != nil { + return nil, err + } + } + return s.Read(ctx, loc, entry.Name, "get") +} + +// Delete deletes the given item. +func (s *Service) Delete(ctx context.Context, loc Location, name ...string) error { + return s.delete(ctx, loc, name) +} +func (s *Service) delete(ctx context.Context, loc Location, values []string) error { + for _, value := range values { + if value == "" { + return errors.NameNotSpecifiedError + } + } + + vn := s.client.Versioning() + var err error + deletes := xmlapi.NewMultiConfig(len(values)) + for _, value := range values { + var path []string + path, err = loc.XpathWithEntryName(vn, value) + if err != nil { + return err + } + deletes.Add(&xmlapi.Config{ + Action: "delete", + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + }) + } + + _, _, _, err = s.client.MultiConfig(ctx, deletes, false, nil) + + return err +} + +// List returns a list of objects using the given action ("get" or "show"). +// Params filter and quote are for client side filtering. +func (s *Service) List(ctx context.Context, loc Location, action, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, action, filter, quote, false) +} + +// ListFromConfig returns a list of objects at the given location. +// Requires that client.LoadPanosConfig() has been invoked. +// Params filter and quote are for client side filtering. +func (s *Service) ListFromConfig(ctx context.Context, loc Location, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, "", filter, quote, true) +} + +func (s *Service) list(ctx context.Context, loc Location, action, filter, quote string, usePanosConfig bool) ([]*Entry, error) { + var err error + + var logic *filtering.Group + if filter != "" { + logic, err = filtering.Parse(filter, quote) + if err != nil { + return nil, err + } + } + + vn := s.client.Versioning() + + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + + path, err := loc.XpathWithEntryName(vn, "") + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, false, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, nil + } + return nil, err + } + } + + listing, err := normalizer.Normalize() + if err != nil || logic == nil { + return listing, err + } + + filtered := make([]*Entry, 0, len(listing)) + for _, x := range listing { + ok, err := logic.Matches(x) + if err != nil { + return nil, err + } + if ok { + filtered = append(filtered, x) + } + } + + return filtered, nil +} diff --git a/objects/tag/entry.go b/objects/tag/entry.go new file mode 100644 index 0000000..51d888c --- /dev/null +++ b/objects/tag/entry.go @@ -0,0 +1,156 @@ +package tag + +import ( + "encoding/xml" + "fmt" + + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/generic" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +var ( + _ filtering.Fielder = &Entry{} +) + +var ( + Suffix = []string{"tag"} +) + +type Entry struct { + Name string + Color *string + Comments *string + + Misc map[string][]generic.Xml +} + +type entryXmlContainer struct { + Answer []entryXml `xml:"entry"` +} + +type entryXml struct { + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + Color *string `xml:"color,omitempty"` + Comments *string `xml:"comments,omitempty"` + + Misc []generic.Xml `xml:",any"` +} + +const ( + ColorAzureBlue = "color24" + ColorBlack = "color14" + ColorBlueGray = "color12" + ColorBlueViolet = "color30" + ColorBrown = "color16" + ColorBurntSienna = "color41" + ColorCeruleanBlue = "color25" + ColorChestnut = "color42" + ColorCobaltBlue = "color28" + ColorCopper = "color4" + ColorCyan = "color9" + ColorForestGreen = "color22" + ColorGold = "color15" + ColorGray = "color7" + ColorGreen = "color2" + ColorLavender = "color33" + ColorLightGray = "color10" + ColorLightGreen = "color8" + ColorLime = "color13" + ColorMagenta = "color38" + ColorMahogany = "color40" + ColorMaroon = "color19" + ColorMediumBlue = "color27" + ColorMediumRose = "color32" + ColorMediumViolet = "color31" + ColorMidnightBlue = "color26" + ColorOlive = "color17" + ColorOrange = "color5" + ColorOrchid = "color34" + ColorPeach = "color36" + ColorPurple = "color6" + ColorRed = "color1" + ColorRedViolet = "color39" + ColorRedOrange = "color20" + ColorSalmon = "color37" + ColorThistle = "color35" + ColorTurquoiseBlue = "color23" + ColorVioletBlue = "color29" + ColorYellow = "color3" + ColorYellowOrange = "color21" +) + +func (e *Entry) Field(v string) (any, error) { + if v == "name" || v == "Name" { + return e.Name, nil + } + if v == "color" || v == "Color" { + return e.Color, nil + } + if v == "comments" || v == "Comments" { + return e.Comments, nil + } + + return nil, fmt.Errorf("unknown field") +} + +func Versioning(vn version.Number) (Specifier, Normalizer, error) { + return specifyEntry, &entryXmlContainer{}, nil +} + +func specifyEntry(o *Entry) (any, error) { + entry := entryXml{} + + entry.Name = o.Name + entry.Color = o.Color + entry.Comments = o.Comments + + entry.Misc = o.Misc["Entry"] + + return entry, nil +} +func (c *entryXmlContainer) Normalize() ([]*Entry, error) { + entryList := make([]*Entry, 0, len(c.Answer)) + for _, o := range c.Answer { + entry := &Entry{ + Misc: make(map[string][]generic.Xml), + } + entry.Name = o.Name + entry.Color = o.Color + entry.Comments = o.Comments + + entry.Misc["Entry"] = o.Misc + + entryList = append(entryList, entry) + } + + return entryList, nil +} + +func SpecMatches(a, b *Entry) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + + // Don't compare Name. + if !util.StringsMatch(a.Color, b.Color) { + return false + } + if !util.StringsMatch(a.Comments, b.Comments) { + return false + } + + return true +} + +func (o *Entry) EntryName() string { + return o.Name +} + +func (o *Entry) SetEntryName(name string) { + o.Name = name +} diff --git a/objects/tag/interfaces.go b/objects/tag/interfaces.go new file mode 100644 index 0000000..a4a515b --- /dev/null +++ b/objects/tag/interfaces.go @@ -0,0 +1,7 @@ +package tag + +type Specifier func(*Entry) (any, error) + +type Normalizer interface { + Normalize() ([]*Entry, error) +} diff --git a/objects/tag/location.go b/objects/tag/location.go new file mode 100644 index 0000000..ac2ff82 --- /dev/null +++ b/objects/tag/location.go @@ -0,0 +1,188 @@ +package tag + +import ( + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +type ImportLocation interface { + XpathForLocation(version.Number, util.ILocation) ([]string, error) + MarshalPangoXML([]string) (string, error) + UnmarshalPangoXML([]byte) ([]string, error) +} + +type Location struct { + DeviceGroup *DeviceGroupLocation `json:"device_group,omitempty"` + FromPanoramaShared bool `json:"from_panorama_shared"` + FromPanoramaVsys *FromPanoramaVsysLocation `json:"from_panorama_vsys,omitempty"` + Shared bool `json:"shared"` + Vsys *VsysLocation `json:"vsys,omitempty"` +} + +type DeviceGroupLocation struct { + DeviceGroup string `json:"device_group"` + PanoramaDevice string `json:"panorama_device"` +} + +type FromPanoramaVsysLocation struct { + Vsys string `json:"vsys"` +} + +type VsysLocation struct { + NgfwDevice string `json:"ngfw_device"` + Vsys string `json:"vsys"` +} + +func NewDeviceGroupLocation() *Location { + return &Location{DeviceGroup: &DeviceGroupLocation{ + DeviceGroup: "", + PanoramaDevice: "localhost.localdomain", + }, + } +} +func NewFromPanoramaVsysLocation() *Location { + return &Location{FromPanoramaVsys: &FromPanoramaVsysLocation{ + Vsys: "vsys1", + }, + } +} +func NewSharedLocation() *Location { + return &Location{ + Shared: true, + } +} +func NewVsysLocation() *Location { + return &Location{Vsys: &VsysLocation{ + NgfwDevice: "localhost.localdomain", + Vsys: "vsys1", + }, + } +} + +func (o Location) IsValid() error { + count := 0 + + switch { + case o.DeviceGroup != nil: + if o.DeviceGroup.DeviceGroup == "" { + return fmt.Errorf("DeviceGroup is unspecified") + } + if o.DeviceGroup.PanoramaDevice == "" { + return fmt.Errorf("PanoramaDevice is unspecified") + } + count++ + case o.FromPanoramaShared: + count++ + case o.FromPanoramaVsys != nil: + if o.FromPanoramaVsys.Vsys == "" { + return fmt.Errorf("Vsys is unspecified") + } + count++ + case o.Shared: + count++ + case o.Vsys != nil: + if o.Vsys.NgfwDevice == "" { + return fmt.Errorf("NgfwDevice is unspecified") + } + if o.Vsys.Vsys == "" { + return fmt.Errorf("Vsys is unspecified") + } + count++ + } + + if count == 0 { + return fmt.Errorf("no path specified") + } + + if count > 1 { + return fmt.Errorf("multiple paths specified: only one should be specified") + } + + return nil +} + +func (o Location) XpathPrefix(vn version.Number) ([]string, error) { + + var ans []string + + switch { + case o.DeviceGroup != nil: + if o.DeviceGroup.DeviceGroup == "" { + return nil, fmt.Errorf("DeviceGroup is unspecified") + } + if o.DeviceGroup.PanoramaDevice == "" { + return nil, fmt.Errorf("PanoramaDevice is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.DeviceGroup.PanoramaDevice}), + "device-group", + util.AsEntryXpath([]string{o.DeviceGroup.DeviceGroup}), + } + case o.FromPanoramaShared: + ans = []string{ + "config", + "panorama", + "shared", + } + case o.FromPanoramaVsys != nil: + if o.FromPanoramaVsys.Vsys == "" { + return nil, fmt.Errorf("Vsys is unspecified") + } + ans = []string{ + "config", + "panorama", + "vsys", + util.AsEntryXpath([]string{o.FromPanoramaVsys.Vsys}), + } + case o.Shared: + ans = []string{ + "config", + "shared", + } + case o.Vsys != nil: + if o.Vsys.NgfwDevice == "" { + return nil, fmt.Errorf("NgfwDevice is unspecified") + } + if o.Vsys.Vsys == "" { + return nil, fmt.Errorf("Vsys is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.Vsys.NgfwDevice}), + "vsys", + util.AsEntryXpath([]string{o.Vsys.Vsys}), + } + default: + return nil, errors.NoLocationSpecifiedError + } + + return ans, nil +} +func (o Location) XpathWithEntryName(vn version.Number, name string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsEntryXpath([]string{name})) + + return ans, nil +} +func (o Location) XpathWithUuid(vn version.Number, uuid string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsUuidXpath(uuid)) + + return ans, nil +} diff --git a/objects/tag/service.go b/objects/tag/service.go new file mode 100644 index 0000000..1398eed --- /dev/null +++ b/objects/tag/service.go @@ -0,0 +1,281 @@ +package tag + +import ( + "context" + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/xmlapi" +) + +type Service struct { + client util.PangoClient +} + +func NewService(client util.PangoClient) *Service { + return &Service{ + client: client, + } +} + +// Create adds new item, then returns the result. +func (s *Service) Create(ctx context.Context, loc Location, entry *Entry) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + path, err := loc.XpathWithEntryName(vn, entry.Name) + if err != nil { + return nil, err + } + createSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + cmd := &xmlapi.Config{ + Action: "set", + Xpath: util.AsXpath(path[:len(path)-1]), + Element: createSpec, + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, false, nil); err != nil { + return nil, err + } + return s.Read(ctx, loc, entry.Name, "get") +} + +// Read returns the given config object, using the specified action ("get" or "show"). +func (s *Service) Read(ctx context.Context, loc Location, name, action string) (*Entry, error) { + return s.read(ctx, loc, name, action, false) +} + +// ReadFromConfig returns the given config object from the loaded XML config. +// Requires that client.LoadPanosConfig() has been invoked. +func (s *Service) ReadFromConfig(ctx context.Context, loc Location, name string) (*Entry, error) { + return s.read(ctx, loc, name, "", true) +} + +func (s *Service) read(ctx context.Context, loc Location, value, action string, usePanosConfig bool) (*Entry, error) { + if value == "" { + return nil, errors.NameNotSpecifiedError + } + vn := s.client.Versioning() + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + var path []string + path, err = loc.XpathWithEntryName(vn, value) + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, true, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, errors.ObjectNotFound() + } + return nil, err + } + } + + list, err := normalizer.Normalize() + if err != nil { + return nil, err + } else if len(list) != 1 { + return nil, fmt.Errorf("expected to %q 1 entry, got %d", action, len(list)) + } + + return list[0], nil +} + +// Update updates the given config object, then returns the result. +func (s *Service) Update(ctx context.Context, loc Location, entry *Entry, name string) (*Entry, error) { + return s.update(ctx, loc, entry, name) +} +func (s *Service) update(ctx context.Context, loc Location, entry *Entry, value string) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + updates := xmlapi.NewMultiConfig(2) + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + var old *Entry + if value != "" && value != entry.Name { + path, err := loc.XpathWithEntryName(vn, value) + if err != nil { + return nil, err + } + + old, err = s.Read(ctx, loc, value, "get") + + updates.Add(&xmlapi.Config{ + Action: "rename", + Xpath: util.AsXpath(path), + NewName: entry.Name, + Target: s.client.GetTarget(), + }) + } else { + old, err = s.Read(ctx, loc, entry.Name, "get") + } + if err != nil { + return nil, err + } else if old == nil { + return nil, fmt.Errorf("previous object doesn't exist for update") + } + if !SpecMatches(entry, old) { + path, err := loc.XpathWithEntryName(vn, entry.Name) + if err != nil { + return nil, err + } + + updateSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + updates.Add(&xmlapi.Config{ + Action: "edit", + Xpath: util.AsXpath(path), + Element: updateSpec, + Target: s.client.GetTarget(), + }) + } + + if len(updates.Operations) != 0 { + if _, _, _, err = s.client.MultiConfig(ctx, updates, false, nil); err != nil { + return nil, err + } + } + return s.Read(ctx, loc, entry.Name, "get") +} + +// Delete deletes the given item. +func (s *Service) Delete(ctx context.Context, loc Location, name ...string) error { + return s.delete(ctx, loc, name) +} +func (s *Service) delete(ctx context.Context, loc Location, values []string) error { + for _, value := range values { + if value == "" { + return errors.NameNotSpecifiedError + } + } + + vn := s.client.Versioning() + var err error + deletes := xmlapi.NewMultiConfig(len(values)) + for _, value := range values { + var path []string + path, err = loc.XpathWithEntryName(vn, value) + if err != nil { + return err + } + deletes.Add(&xmlapi.Config{ + Action: "delete", + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + }) + } + + _, _, _, err = s.client.MultiConfig(ctx, deletes, false, nil) + + return err +} + +// List returns a list of objects using the given action ("get" or "show"). +// Params filter and quote are for client side filtering. +func (s *Service) List(ctx context.Context, loc Location, action, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, action, filter, quote, false) +} + +// ListFromConfig returns a list of objects at the given location. +// Requires that client.LoadPanosConfig() has been invoked. +// Params filter and quote are for client side filtering. +func (s *Service) ListFromConfig(ctx context.Context, loc Location, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, "", filter, quote, true) +} + +func (s *Service) list(ctx context.Context, loc Location, action, filter, quote string, usePanosConfig bool) ([]*Entry, error) { + var err error + + var logic *filtering.Group + if filter != "" { + logic, err = filtering.Parse(filter, quote) + if err != nil { + return nil, err + } + } + + vn := s.client.Versioning() + + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + + path, err := loc.XpathWithEntryName(vn, "") + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, false, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, nil + } + return nil, err + } + } + + listing, err := normalizer.Normalize() + if err != nil || logic == nil { + return listing, err + } + + filtered := make([]*Entry, 0, len(listing)) + for _, x := range listing { + ok, err := logic.Matches(x) + if err != nil { + return nil, err + } + if ok { + filtered = append(filtered, x) + } + } + + return filtered, nil +} diff --git a/pango_suite_test.go b/pango_suite_test.go new file mode 100644 index 0000000..2d9f1b0 --- /dev/null +++ b/pango_suite_test.go @@ -0,0 +1,15 @@ +package pango_test + +import ( + "log/slog" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestPango(t *testing.T) { + slog.SetDefault(slog.New(slog.NewTextHandler(GinkgoWriter, &slog.HandlerOptions{Level: slog.LevelDebug}))) + RegisterFailHandler(Fail) + RunSpecs(t, "Pango Suite") +} diff --git a/panorama/device_group/entry.go b/panorama/device_group/entry.go new file mode 100644 index 0000000..5bf9765 --- /dev/null +++ b/panorama/device_group/entry.go @@ -0,0 +1,207 @@ +package device_group + +import ( + "encoding/xml" + "fmt" + + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/generic" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +var ( + _ filtering.Fielder = &Entry{} +) + +var ( + Suffix = []string{"device-group"} +) + +type Entry struct { + Name string + AuthorizationCode *string + Description *string + Devices []Devices + Templates []string + + Misc map[string][]generic.Xml +} + +type Devices struct { + Name string + Vsys []string +} + +type entryXmlContainer struct { + Answer []entryXml `xml:"entry"` +} + +type entryXml struct { + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + AuthorizationCode *string `xml:"authorization-code,omitempty"` + Description *string `xml:"description,omitempty"` + Devices []DevicesXml `xml:"devices>entry,omitempty"` + Templates *util.MemberType `xml:"reference-templates,omitempty"` + + Misc []generic.Xml `xml:",any"` +} + +type DevicesXml struct { + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + Vsys *util.MemberType `xml:"vsys,omitempty"` + + Misc []generic.Xml `xml:",any"` +} + +func (e *Entry) Field(v string) (any, error) { + if v == "name" || v == "Name" { + return e.Name, nil + } + if v == "authorization_code" || v == "AuthorizationCode" { + return e.AuthorizationCode, nil + } + if v == "description" || v == "Description" { + return e.Description, nil + } + if v == "devices" || v == "Devices" { + return e.Devices, nil + } + if v == "devices|LENGTH" || v == "Devices|LENGTH" { + return int64(len(e.Devices)), nil + } + if v == "templates" || v == "Templates" { + return e.Templates, nil + } + if v == "templates|LENGTH" || v == "Templates|LENGTH" { + return int64(len(e.Templates)), nil + } + + return nil, fmt.Errorf("unknown field") +} + +func Versioning(vn version.Number) (Specifier, Normalizer, error) { + return specifyEntry, &entryXmlContainer{}, nil +} + +func specifyEntry(o *Entry) (any, error) { + entry := entryXml{} + + entry.Name = o.Name + entry.AuthorizationCode = o.AuthorizationCode + entry.Description = o.Description + var nestedDevicesCol []DevicesXml + if o.Devices != nil { + nestedDevicesCol = []DevicesXml{} + for _, oDevices := range o.Devices { + nestedDevices := DevicesXml{} + if _, ok := o.Misc["Devices"]; ok { + nestedDevices.Misc = o.Misc["Devices"] + } + if oDevices.Vsys != nil { + nestedDevices.Vsys = util.StrToMem(oDevices.Vsys) + } + if oDevices.Name != "" { + nestedDevices.Name = oDevices.Name + } + nestedDevicesCol = append(nestedDevicesCol, nestedDevices) + } + entry.Devices = nestedDevicesCol + } + + entry.Templates = util.StrToMem(o.Templates) + + entry.Misc = o.Misc["Entry"] + + return entry, nil +} +func (c *entryXmlContainer) Normalize() ([]*Entry, error) { + entryList := make([]*Entry, 0, len(c.Answer)) + for _, o := range c.Answer { + entry := &Entry{ + Misc: make(map[string][]generic.Xml), + } + entry.Name = o.Name + entry.AuthorizationCode = o.AuthorizationCode + entry.Description = o.Description + var nestedDevicesCol []Devices + if o.Devices != nil { + nestedDevicesCol = []Devices{} + for _, oDevices := range o.Devices { + nestedDevices := Devices{} + if oDevices.Misc != nil { + entry.Misc["Devices"] = oDevices.Misc + } + if oDevices.Vsys != nil { + nestedDevices.Vsys = util.MemToStr(oDevices.Vsys) + } + if oDevices.Name != "" { + nestedDevices.Name = oDevices.Name + } + nestedDevicesCol = append(nestedDevicesCol, nestedDevices) + } + entry.Devices = nestedDevicesCol + } + + entry.Templates = util.MemToStr(o.Templates) + + entry.Misc["Entry"] = o.Misc + + entryList = append(entryList, entry) + } + + return entryList, nil +} + +func SpecMatches(a, b *Entry) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + + // Don't compare Name. + if !util.StringsMatch(a.AuthorizationCode, b.AuthorizationCode) { + return false + } + if !util.StringsMatch(a.Description, b.Description) { + return false + } + if !matchDevices(a.Devices, b.Devices) { + return false + } + if !util.OrderedListsMatch(a.Templates, b.Templates) { + return false + } + + return true +} + +func matchDevices(a []Devices, b []Devices) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + for _, a := range a { + for _, b := range b { + if !util.OrderedListsMatch(a.Vsys, b.Vsys) { + return false + } + if !util.StringsEqual(a.Name, b.Name) { + return false + } + } + } + return true +} + +func (o *Entry) EntryName() string { + return o.Name +} + +func (o *Entry) SetEntryName(name string) { + o.Name = name +} diff --git a/panorama/device_group/interfaces.go b/panorama/device_group/interfaces.go new file mode 100644 index 0000000..c7693d4 --- /dev/null +++ b/panorama/device_group/interfaces.go @@ -0,0 +1,7 @@ +package device_group + +type Specifier func(*Entry) (any, error) + +type Normalizer interface { + Normalize() ([]*Entry, error) +} diff --git a/panorama/device_group/location.go b/panorama/device_group/location.go new file mode 100644 index 0000000..c17ea3b --- /dev/null +++ b/panorama/device_group/location.go @@ -0,0 +1,95 @@ +package device_group + +import ( + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +type ImportLocation interface { + XpathForLocation(version.Number, util.ILocation) ([]string, error) + MarshalPangoXML([]string) (string, error) + UnmarshalPangoXML([]byte) ([]string, error) +} + +type Location struct { + Panorama *PanoramaLocation `json:"panorama,omitempty"` +} + +type PanoramaLocation struct { + PanoramaDevice string `json:"panorama_device"` +} + +func NewPanoramaLocation() *Location { + return &Location{Panorama: &PanoramaLocation{ + PanoramaDevice: "localhost.localdomain", + }, + } +} + +func (o Location) IsValid() error { + count := 0 + + switch { + case o.Panorama != nil: + if o.Panorama.PanoramaDevice == "" { + return fmt.Errorf("PanoramaDevice is unspecified") + } + count++ + } + + if count == 0 { + return fmt.Errorf("no path specified") + } + + if count > 1 { + return fmt.Errorf("multiple paths specified: only one should be specified") + } + + return nil +} + +func (o Location) XpathPrefix(vn version.Number) ([]string, error) { + + var ans []string + + switch { + case o.Panorama != nil: + if o.Panorama.PanoramaDevice == "" { + return nil, fmt.Errorf("PanoramaDevice is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.Panorama.PanoramaDevice}), + } + default: + return nil, errors.NoLocationSpecifiedError + } + + return ans, nil +} +func (o Location) XpathWithEntryName(vn version.Number, name string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsEntryXpath([]string{name})) + + return ans, nil +} +func (o Location) XpathWithUuid(vn version.Number, uuid string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsUuidXpath(uuid)) + + return ans, nil +} diff --git a/panorama/device_group/service.go b/panorama/device_group/service.go new file mode 100644 index 0000000..aafca6a --- /dev/null +++ b/panorama/device_group/service.go @@ -0,0 +1,281 @@ +package device_group + +import ( + "context" + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/xmlapi" +) + +type Service struct { + client util.PangoClient +} + +func NewService(client util.PangoClient) *Service { + return &Service{ + client: client, + } +} + +// Create adds new item, then returns the result. +func (s *Service) Create(ctx context.Context, loc Location, entry *Entry) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + path, err := loc.XpathWithEntryName(vn, entry.Name) + if err != nil { + return nil, err + } + createSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + cmd := &xmlapi.Config{ + Action: "set", + Xpath: util.AsXpath(path[:len(path)-1]), + Element: createSpec, + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, false, nil); err != nil { + return nil, err + } + return s.Read(ctx, loc, entry.Name, "get") +} + +// Read returns the given config object, using the specified action ("get" or "show"). +func (s *Service) Read(ctx context.Context, loc Location, name, action string) (*Entry, error) { + return s.read(ctx, loc, name, action, false) +} + +// ReadFromConfig returns the given config object from the loaded XML config. +// Requires that client.LoadPanosConfig() has been invoked. +func (s *Service) ReadFromConfig(ctx context.Context, loc Location, name string) (*Entry, error) { + return s.read(ctx, loc, name, "", true) +} + +func (s *Service) read(ctx context.Context, loc Location, value, action string, usePanosConfig bool) (*Entry, error) { + if value == "" { + return nil, errors.NameNotSpecifiedError + } + vn := s.client.Versioning() + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + var path []string + path, err = loc.XpathWithEntryName(vn, value) + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, true, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, errors.ObjectNotFound() + } + return nil, err + } + } + + list, err := normalizer.Normalize() + if err != nil { + return nil, err + } else if len(list) != 1 { + return nil, fmt.Errorf("expected to %q 1 entry, got %d", action, len(list)) + } + + return list[0], nil +} + +// Update updates the given config object, then returns the result. +func (s *Service) Update(ctx context.Context, loc Location, entry *Entry, name string) (*Entry, error) { + return s.update(ctx, loc, entry, name) +} +func (s *Service) update(ctx context.Context, loc Location, entry *Entry, value string) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + updates := xmlapi.NewMultiConfig(2) + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + var old *Entry + if value != "" && value != entry.Name { + path, err := loc.XpathWithEntryName(vn, value) + if err != nil { + return nil, err + } + + old, err = s.Read(ctx, loc, value, "get") + + updates.Add(&xmlapi.Config{ + Action: "rename", + Xpath: util.AsXpath(path), + NewName: entry.Name, + Target: s.client.GetTarget(), + }) + } else { + old, err = s.Read(ctx, loc, entry.Name, "get") + } + if err != nil { + return nil, err + } else if old == nil { + return nil, fmt.Errorf("previous object doesn't exist for update") + } + if !SpecMatches(entry, old) { + path, err := loc.XpathWithEntryName(vn, entry.Name) + if err != nil { + return nil, err + } + + updateSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + updates.Add(&xmlapi.Config{ + Action: "edit", + Xpath: util.AsXpath(path), + Element: updateSpec, + Target: s.client.GetTarget(), + }) + } + + if len(updates.Operations) != 0 { + if _, _, _, err = s.client.MultiConfig(ctx, updates, false, nil); err != nil { + return nil, err + } + } + return s.Read(ctx, loc, entry.Name, "get") +} + +// Delete deletes the given item. +func (s *Service) Delete(ctx context.Context, loc Location, name ...string) error { + return s.delete(ctx, loc, name) +} +func (s *Service) delete(ctx context.Context, loc Location, values []string) error { + for _, value := range values { + if value == "" { + return errors.NameNotSpecifiedError + } + } + + vn := s.client.Versioning() + var err error + deletes := xmlapi.NewMultiConfig(len(values)) + for _, value := range values { + var path []string + path, err = loc.XpathWithEntryName(vn, value) + if err != nil { + return err + } + deletes.Add(&xmlapi.Config{ + Action: "delete", + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + }) + } + + _, _, _, err = s.client.MultiConfig(ctx, deletes, false, nil) + + return err +} + +// List returns a list of objects using the given action ("get" or "show"). +// Params filter and quote are for client side filtering. +func (s *Service) List(ctx context.Context, loc Location, action, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, action, filter, quote, false) +} + +// ListFromConfig returns a list of objects at the given location. +// Requires that client.LoadPanosConfig() has been invoked. +// Params filter and quote are for client side filtering. +func (s *Service) ListFromConfig(ctx context.Context, loc Location, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, "", filter, quote, true) +} + +func (s *Service) list(ctx context.Context, loc Location, action, filter, quote string, usePanosConfig bool) ([]*Entry, error) { + var err error + + var logic *filtering.Group + if filter != "" { + logic, err = filtering.Parse(filter, quote) + if err != nil { + return nil, err + } + } + + vn := s.client.Versioning() + + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + + path, err := loc.XpathWithEntryName(vn, "") + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, false, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, nil + } + return nil, err + } + } + + listing, err := normalizer.Normalize() + if err != nil || logic == nil { + return listing, err + } + + filtered := make([]*Entry, 0, len(listing)) + for _, x := range listing { + ok, err := logic.Matches(x) + if err != nil { + return nil, err + } + if ok { + filtered = append(filtered, x) + } + } + + return filtered, nil +} diff --git a/panorama/template/entry.go b/panorama/template/entry.go new file mode 100644 index 0000000..836a4f5 --- /dev/null +++ b/panorama/template/entry.go @@ -0,0 +1,339 @@ +package template + +import ( + "encoding/xml" + "fmt" + + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/generic" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +var ( + _ filtering.Fielder = &Entry{} +) + +var ( + Suffix = []string{"template"} +) + +type Entry struct { + Name string + Config *Config + DefaultVsys *string + Description *string + + Misc map[string][]generic.Xml +} + +type Config struct { + Devices []ConfigDevices +} +type ConfigDevices struct { + Name string + Vsys []ConfigDevicesVsys +} +type ConfigDevicesVsys struct { + Import *ConfigDevicesVsysImport + Name string +} +type ConfigDevicesVsysImport struct { + Network *ConfigDevicesVsysImportNetwork +} +type ConfigDevicesVsysImportNetwork struct { + Interfaces []string +} + +type entryXmlContainer struct { + Answer []entryXml `xml:"entry"` +} + +type entryXml struct { + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + Config *ConfigXml `xml:"config,omitempty"` + DefaultVsys *string `xml:"settings>default-vsys,omitempty"` + Description *string `xml:"description,omitempty"` + + Misc []generic.Xml `xml:",any"` +} + +type ConfigXml struct { + Devices []ConfigDevicesXml `xml:"devices>entry,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type ConfigDevicesXml struct { + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + Vsys []ConfigDevicesVsysXml `xml:"vsys>entry,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type ConfigDevicesVsysXml struct { + Import *ConfigDevicesVsysImportXml `xml:"import,omitempty"` + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + + Misc []generic.Xml `xml:",any"` +} +type ConfigDevicesVsysImportXml struct { + Network *ConfigDevicesVsysImportNetworkXml `xml:"network,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type ConfigDevicesVsysImportNetworkXml struct { + Interfaces *util.MemberType `xml:"interface,omitempty"` + + Misc []generic.Xml `xml:",any"` +} + +func (e *Entry) Field(v string) (any, error) { + if v == "name" || v == "Name" { + return e.Name, nil + } + if v == "config" || v == "Config" { + return e.Config, nil + } + if v == "default_vsys" || v == "DefaultVsys" { + return e.DefaultVsys, nil + } + if v == "description" || v == "Description" { + return e.Description, nil + } + + return nil, fmt.Errorf("unknown field") +} + +func Versioning(vn version.Number) (Specifier, Normalizer, error) { + return specifyEntry, &entryXmlContainer{}, nil +} + +func specifyEntry(o *Entry) (any, error) { + entry := entryXml{} + + entry.Name = o.Name + var nestedConfig *ConfigXml + if o.Config != nil { + nestedConfig = &ConfigXml{} + if _, ok := o.Misc["Config"]; ok { + nestedConfig.Misc = o.Misc["Config"] + } + if o.Config.Devices != nil { + nestedConfig.Devices = []ConfigDevicesXml{} + for _, oConfigDevices := range o.Config.Devices { + nestedConfigDevices := ConfigDevicesXml{} + if _, ok := o.Misc["ConfigDevices"]; ok { + nestedConfigDevices.Misc = o.Misc["ConfigDevices"] + } + if oConfigDevices.Vsys != nil { + nestedConfigDevices.Vsys = []ConfigDevicesVsysXml{} + for _, oConfigDevicesVsys := range oConfigDevices.Vsys { + nestedConfigDevicesVsys := ConfigDevicesVsysXml{} + if _, ok := o.Misc["ConfigDevicesVsys"]; ok { + nestedConfigDevicesVsys.Misc = o.Misc["ConfigDevicesVsys"] + } + if oConfigDevicesVsys.Import != nil { + nestedConfigDevicesVsys.Import = &ConfigDevicesVsysImportXml{} + if _, ok := o.Misc["ConfigDevicesVsysImport"]; ok { + nestedConfigDevicesVsys.Import.Misc = o.Misc["ConfigDevicesVsysImport"] + } + if oConfigDevicesVsys.Import.Network != nil { + nestedConfigDevicesVsys.Import.Network = &ConfigDevicesVsysImportNetworkXml{} + if _, ok := o.Misc["ConfigDevicesVsysImportNetwork"]; ok { + nestedConfigDevicesVsys.Import.Network.Misc = o.Misc["ConfigDevicesVsysImportNetwork"] + } + if oConfigDevicesVsys.Import.Network.Interfaces != nil { + nestedConfigDevicesVsys.Import.Network.Interfaces = util.StrToMem(oConfigDevicesVsys.Import.Network.Interfaces) + } + } + } + if oConfigDevicesVsys.Name != "" { + nestedConfigDevicesVsys.Name = oConfigDevicesVsys.Name + } + nestedConfigDevices.Vsys = append(nestedConfigDevices.Vsys, nestedConfigDevicesVsys) + } + } + if oConfigDevices.Name != "" { + nestedConfigDevices.Name = oConfigDevices.Name + } + nestedConfig.Devices = append(nestedConfig.Devices, nestedConfigDevices) + } + } + } + entry.Config = nestedConfig + + entry.DefaultVsys = o.DefaultVsys + entry.Description = o.Description + + entry.Misc = o.Misc["Entry"] + + return entry, nil +} +func (c *entryXmlContainer) Normalize() ([]*Entry, error) { + entryList := make([]*Entry, 0, len(c.Answer)) + for _, o := range c.Answer { + entry := &Entry{ + Misc: make(map[string][]generic.Xml), + } + entry.Name = o.Name + var nestedConfig *Config + if o.Config != nil { + nestedConfig = &Config{} + if o.Config.Misc != nil { + entry.Misc["Config"] = o.Config.Misc + } + if o.Config.Devices != nil { + nestedConfig.Devices = []ConfigDevices{} + for _, oConfigDevices := range o.Config.Devices { + nestedConfigDevices := ConfigDevices{} + if oConfigDevices.Misc != nil { + entry.Misc["ConfigDevices"] = oConfigDevices.Misc + } + if oConfigDevices.Name != "" { + nestedConfigDevices.Name = oConfigDevices.Name + } + if oConfigDevices.Vsys != nil { + nestedConfigDevices.Vsys = []ConfigDevicesVsys{} + for _, oConfigDevicesVsys := range oConfigDevices.Vsys { + nestedConfigDevicesVsys := ConfigDevicesVsys{} + if oConfigDevicesVsys.Misc != nil { + entry.Misc["ConfigDevicesVsys"] = oConfigDevicesVsys.Misc + } + if oConfigDevicesVsys.Import != nil { + nestedConfigDevicesVsys.Import = &ConfigDevicesVsysImport{} + if oConfigDevicesVsys.Import.Misc != nil { + entry.Misc["ConfigDevicesVsysImport"] = oConfigDevicesVsys.Import.Misc + } + if oConfigDevicesVsys.Import.Network != nil { + nestedConfigDevicesVsys.Import.Network = &ConfigDevicesVsysImportNetwork{} + if oConfigDevicesVsys.Import.Network.Misc != nil { + entry.Misc["ConfigDevicesVsysImportNetwork"] = oConfigDevicesVsys.Import.Network.Misc + } + if oConfigDevicesVsys.Import.Network.Interfaces != nil { + nestedConfigDevicesVsys.Import.Network.Interfaces = util.MemToStr(oConfigDevicesVsys.Import.Network.Interfaces) + } + } + } + if oConfigDevicesVsys.Name != "" { + nestedConfigDevicesVsys.Name = oConfigDevicesVsys.Name + } + nestedConfigDevices.Vsys = append(nestedConfigDevices.Vsys, nestedConfigDevicesVsys) + } + } + nestedConfig.Devices = append(nestedConfig.Devices, nestedConfigDevices) + } + } + } + entry.Config = nestedConfig + + entry.DefaultVsys = o.DefaultVsys + entry.Description = o.Description + + entry.Misc["Entry"] = o.Misc + + entryList = append(entryList, entry) + } + + return entryList, nil +} + +func SpecMatches(a, b *Entry) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + + // Don't compare Name. + if !matchConfig(a.Config, b.Config) { + return false + } + if !util.StringsMatch(a.DefaultVsys, b.DefaultVsys) { + return false + } + if !util.StringsMatch(a.Description, b.Description) { + return false + } + + return true +} + +func matchConfigDevicesVsysImportNetwork(a *ConfigDevicesVsysImportNetwork, b *ConfigDevicesVsysImportNetwork) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.OrderedListsMatch(a.Interfaces, b.Interfaces) { + return false + } + return true +} +func matchConfigDevicesVsysImport(a *ConfigDevicesVsysImport, b *ConfigDevicesVsysImport) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !matchConfigDevicesVsysImportNetwork(a.Network, b.Network) { + return false + } + return true +} +func matchConfigDevicesVsys(a []ConfigDevicesVsys, b []ConfigDevicesVsys) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + for _, a := range a { + for _, b := range b { + if !matchConfigDevicesVsysImport(a.Import, b.Import) { + return false + } + if !util.StringsEqual(a.Name, b.Name) { + return false + } + } + } + return true +} +func matchConfigDevices(a []ConfigDevices, b []ConfigDevices) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + for _, a := range a { + for _, b := range b { + if !matchConfigDevicesVsys(a.Vsys, b.Vsys) { + return false + } + if !util.StringsEqual(a.Name, b.Name) { + return false + } + } + } + return true +} +func matchConfig(a *Config, b *Config) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !matchConfigDevices(a.Devices, b.Devices) { + return false + } + return true +} + +func (o *Entry) EntryName() string { + return o.Name +} + +func (o *Entry) SetEntryName(name string) { + o.Name = name +} diff --git a/panorama/template/interfaces.go b/panorama/template/interfaces.go new file mode 100644 index 0000000..df09955 --- /dev/null +++ b/panorama/template/interfaces.go @@ -0,0 +1,7 @@ +package template + +type Specifier func(*Entry) (any, error) + +type Normalizer interface { + Normalize() ([]*Entry, error) +} diff --git a/panorama/template/location.go b/panorama/template/location.go new file mode 100644 index 0000000..27cdd62 --- /dev/null +++ b/panorama/template/location.go @@ -0,0 +1,95 @@ +package template + +import ( + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +type ImportLocation interface { + XpathForLocation(version.Number, util.ILocation) ([]string, error) + MarshalPangoXML([]string) (string, error) + UnmarshalPangoXML([]byte) ([]string, error) +} + +type Location struct { + Panorama *PanoramaLocation `json:"panorama,omitempty"` +} + +type PanoramaLocation struct { + PanoramaDevice string `json:"panorama_device"` +} + +func NewPanoramaLocation() *Location { + return &Location{Panorama: &PanoramaLocation{ + PanoramaDevice: "localhost.localdomain", + }, + } +} + +func (o Location) IsValid() error { + count := 0 + + switch { + case o.Panorama != nil: + if o.Panorama.PanoramaDevice == "" { + return fmt.Errorf("PanoramaDevice is unspecified") + } + count++ + } + + if count == 0 { + return fmt.Errorf("no path specified") + } + + if count > 1 { + return fmt.Errorf("multiple paths specified: only one should be specified") + } + + return nil +} + +func (o Location) XpathPrefix(vn version.Number) ([]string, error) { + + var ans []string + + switch { + case o.Panorama != nil: + if o.Panorama.PanoramaDevice == "" { + return nil, fmt.Errorf("PanoramaDevice is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.Panorama.PanoramaDevice}), + } + default: + return nil, errors.NoLocationSpecifiedError + } + + return ans, nil +} +func (o Location) XpathWithEntryName(vn version.Number, name string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsEntryXpath([]string{name})) + + return ans, nil +} +func (o Location) XpathWithUuid(vn version.Number, uuid string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsUuidXpath(uuid)) + + return ans, nil +} diff --git a/panorama/template/service.go b/panorama/template/service.go new file mode 100644 index 0000000..646ebc5 --- /dev/null +++ b/panorama/template/service.go @@ -0,0 +1,281 @@ +package template + +import ( + "context" + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/xmlapi" +) + +type Service struct { + client util.PangoClient +} + +func NewService(client util.PangoClient) *Service { + return &Service{ + client: client, + } +} + +// Create adds new item, then returns the result. +func (s *Service) Create(ctx context.Context, loc Location, entry *Entry) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + path, err := loc.XpathWithEntryName(vn, entry.Name) + if err != nil { + return nil, err + } + createSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + cmd := &xmlapi.Config{ + Action: "set", + Xpath: util.AsXpath(path[:len(path)-1]), + Element: createSpec, + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, false, nil); err != nil { + return nil, err + } + return s.Read(ctx, loc, entry.Name, "get") +} + +// Read returns the given config object, using the specified action ("get" or "show"). +func (s *Service) Read(ctx context.Context, loc Location, name, action string) (*Entry, error) { + return s.read(ctx, loc, name, action, false) +} + +// ReadFromConfig returns the given config object from the loaded XML config. +// Requires that client.LoadPanosConfig() has been invoked. +func (s *Service) ReadFromConfig(ctx context.Context, loc Location, name string) (*Entry, error) { + return s.read(ctx, loc, name, "", true) +} + +func (s *Service) read(ctx context.Context, loc Location, value, action string, usePanosConfig bool) (*Entry, error) { + if value == "" { + return nil, errors.NameNotSpecifiedError + } + vn := s.client.Versioning() + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + var path []string + path, err = loc.XpathWithEntryName(vn, value) + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, true, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, errors.ObjectNotFound() + } + return nil, err + } + } + + list, err := normalizer.Normalize() + if err != nil { + return nil, err + } else if len(list) != 1 { + return nil, fmt.Errorf("expected to %q 1 entry, got %d", action, len(list)) + } + + return list[0], nil +} + +// Update updates the given config object, then returns the result. +func (s *Service) Update(ctx context.Context, loc Location, entry *Entry, name string) (*Entry, error) { + return s.update(ctx, loc, entry, name) +} +func (s *Service) update(ctx context.Context, loc Location, entry *Entry, value string) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + updates := xmlapi.NewMultiConfig(2) + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + var old *Entry + if value != "" && value != entry.Name { + path, err := loc.XpathWithEntryName(vn, value) + if err != nil { + return nil, err + } + + old, err = s.Read(ctx, loc, value, "get") + + updates.Add(&xmlapi.Config{ + Action: "rename", + Xpath: util.AsXpath(path), + NewName: entry.Name, + Target: s.client.GetTarget(), + }) + } else { + old, err = s.Read(ctx, loc, entry.Name, "get") + } + if err != nil { + return nil, err + } else if old == nil { + return nil, fmt.Errorf("previous object doesn't exist for update") + } + if !SpecMatches(entry, old) { + path, err := loc.XpathWithEntryName(vn, entry.Name) + if err != nil { + return nil, err + } + + updateSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + updates.Add(&xmlapi.Config{ + Action: "edit", + Xpath: util.AsXpath(path), + Element: updateSpec, + Target: s.client.GetTarget(), + }) + } + + if len(updates.Operations) != 0 { + if _, _, _, err = s.client.MultiConfig(ctx, updates, false, nil); err != nil { + return nil, err + } + } + return s.Read(ctx, loc, entry.Name, "get") +} + +// Delete deletes the given item. +func (s *Service) Delete(ctx context.Context, loc Location, name ...string) error { + return s.delete(ctx, loc, name) +} +func (s *Service) delete(ctx context.Context, loc Location, values []string) error { + for _, value := range values { + if value == "" { + return errors.NameNotSpecifiedError + } + } + + vn := s.client.Versioning() + var err error + deletes := xmlapi.NewMultiConfig(len(values)) + for _, value := range values { + var path []string + path, err = loc.XpathWithEntryName(vn, value) + if err != nil { + return err + } + deletes.Add(&xmlapi.Config{ + Action: "delete", + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + }) + } + + _, _, _, err = s.client.MultiConfig(ctx, deletes, false, nil) + + return err +} + +// List returns a list of objects using the given action ("get" or "show"). +// Params filter and quote are for client side filtering. +func (s *Service) List(ctx context.Context, loc Location, action, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, action, filter, quote, false) +} + +// ListFromConfig returns a list of objects at the given location. +// Requires that client.LoadPanosConfig() has been invoked. +// Params filter and quote are for client side filtering. +func (s *Service) ListFromConfig(ctx context.Context, loc Location, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, "", filter, quote, true) +} + +func (s *Service) list(ctx context.Context, loc Location, action, filter, quote string, usePanosConfig bool) ([]*Entry, error) { + var err error + + var logic *filtering.Group + if filter != "" { + logic, err = filtering.Parse(filter, quote) + if err != nil { + return nil, err + } + } + + vn := s.client.Versioning() + + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + + path, err := loc.XpathWithEntryName(vn, "") + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, false, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, nil + } + return nil, err + } + } + + listing, err := normalizer.Normalize() + if err != nil || logic == nil { + return listing, err + } + + filtered := make([]*Entry, 0, len(listing)) + for _, x := range listing { + ok, err := logic.Matches(x) + if err != nil { + return nil, err + } + if ok { + filtered = append(filtered, x) + } + } + + return filtered, nil +} diff --git a/panorama/template_stack/entry.go b/panorama/template_stack/entry.go new file mode 100644 index 0000000..e5a38e4 --- /dev/null +++ b/panorama/template_stack/entry.go @@ -0,0 +1,191 @@ +package template_stack + +import ( + "encoding/xml" + "fmt" + + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/generic" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +var ( + _ filtering.Fielder = &Entry{} +) + +var ( + Suffix = []string{"template-stack"} +) + +type Entry struct { + Name string + DefaultVsys *string + Description *string + Devices []string + Templates []string + UserGroupSource *UserGroupSource + + Misc map[string][]generic.Xml +} + +type UserGroupSource struct { + MasterDevice *string +} + +type entryXmlContainer struct { + Answer []entryXml `xml:"entry"` +} + +type entryXml struct { + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + DefaultVsys *string `xml:"settings>default-vsys,omitempty"` + Description *string `xml:"description,omitempty"` + Devices *util.EntryType `xml:"devices,omitempty"` + Templates *util.MemberType `xml:"templates,omitempty"` + UserGroupSource *UserGroupSourceXml `xml:"user-group-source,omitempty"` + + Misc []generic.Xml `xml:",any"` +} + +type UserGroupSourceXml struct { + MasterDevice *string `xml:"master-device,omitempty"` + + Misc []generic.Xml `xml:",any"` +} + +func (e *Entry) Field(v string) (any, error) { + if v == "name" || v == "Name" { + return e.Name, nil + } + if v == "default_vsys" || v == "DefaultVsys" { + return e.DefaultVsys, nil + } + if v == "description" || v == "Description" { + return e.Description, nil + } + if v == "devices" || v == "Devices" { + return e.Devices, nil + } + if v == "devices|LENGTH" || v == "Devices|LENGTH" { + return int64(len(e.Devices)), nil + } + if v == "templates" || v == "Templates" { + return e.Templates, nil + } + if v == "templates|LENGTH" || v == "Templates|LENGTH" { + return int64(len(e.Templates)), nil + } + if v == "user_group_source" || v == "UserGroupSource" { + return e.UserGroupSource, nil + } + + return nil, fmt.Errorf("unknown field") +} + +func Versioning(vn version.Number) (Specifier, Normalizer, error) { + return specifyEntry, &entryXmlContainer{}, nil +} + +func specifyEntry(o *Entry) (any, error) { + entry := entryXml{} + + entry.Name = o.Name + entry.DefaultVsys = o.DefaultVsys + entry.Description = o.Description + entry.Devices = util.StrToEnt(o.Devices) + entry.Templates = util.StrToMem(o.Templates) + var nestedUserGroupSource *UserGroupSourceXml + if o.UserGroupSource != nil { + nestedUserGroupSource = &UserGroupSourceXml{} + if _, ok := o.Misc["UserGroupSource"]; ok { + nestedUserGroupSource.Misc = o.Misc["UserGroupSource"] + } + if o.UserGroupSource.MasterDevice != nil { + nestedUserGroupSource.MasterDevice = o.UserGroupSource.MasterDevice + } + } + entry.UserGroupSource = nestedUserGroupSource + + entry.Misc = o.Misc["Entry"] + + return entry, nil +} +func (c *entryXmlContainer) Normalize() ([]*Entry, error) { + entryList := make([]*Entry, 0, len(c.Answer)) + for _, o := range c.Answer { + entry := &Entry{ + Misc: make(map[string][]generic.Xml), + } + entry.Name = o.Name + entry.DefaultVsys = o.DefaultVsys + entry.Description = o.Description + entry.Devices = util.EntToStr(o.Devices) + entry.Templates = util.MemToStr(o.Templates) + var nestedUserGroupSource *UserGroupSource + if o.UserGroupSource != nil { + nestedUserGroupSource = &UserGroupSource{} + if o.UserGroupSource.Misc != nil { + entry.Misc["UserGroupSource"] = o.UserGroupSource.Misc + } + if o.UserGroupSource.MasterDevice != nil { + nestedUserGroupSource.MasterDevice = o.UserGroupSource.MasterDevice + } + } + entry.UserGroupSource = nestedUserGroupSource + + entry.Misc["Entry"] = o.Misc + + entryList = append(entryList, entry) + } + + return entryList, nil +} + +func SpecMatches(a, b *Entry) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + + // Don't compare Name. + if !util.StringsMatch(a.DefaultVsys, b.DefaultVsys) { + return false + } + if !util.StringsMatch(a.Description, b.Description) { + return false + } + if !util.OrderedListsMatch(a.Devices, b.Devices) { + return false + } + if !util.OrderedListsMatch(a.Templates, b.Templates) { + return false + } + if !matchUserGroupSource(a.UserGroupSource, b.UserGroupSource) { + return false + } + + return true +} + +func matchUserGroupSource(a *UserGroupSource, b *UserGroupSource) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.StringsMatch(a.MasterDevice, b.MasterDevice) { + return false + } + return true +} + +func (o *Entry) EntryName() string { + return o.Name +} + +func (o *Entry) SetEntryName(name string) { + o.Name = name +} diff --git a/panorama/template_stack/interfaces.go b/panorama/template_stack/interfaces.go new file mode 100644 index 0000000..1e96891 --- /dev/null +++ b/panorama/template_stack/interfaces.go @@ -0,0 +1,7 @@ +package template_stack + +type Specifier func(*Entry) (any, error) + +type Normalizer interface { + Normalize() ([]*Entry, error) +} diff --git a/panorama/template_stack/location.go b/panorama/template_stack/location.go new file mode 100644 index 0000000..8880f36 --- /dev/null +++ b/panorama/template_stack/location.go @@ -0,0 +1,95 @@ +package template_stack + +import ( + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +type ImportLocation interface { + XpathForLocation(version.Number, util.ILocation) ([]string, error) + MarshalPangoXML([]string) (string, error) + UnmarshalPangoXML([]byte) ([]string, error) +} + +type Location struct { + Panorama *PanoramaLocation `json:"panorama,omitempty"` +} + +type PanoramaLocation struct { + PanoramaDevice string `json:"panorama_device"` +} + +func NewPanoramaLocation() *Location { + return &Location{Panorama: &PanoramaLocation{ + PanoramaDevice: "localhost.localdomain", + }, + } +} + +func (o Location) IsValid() error { + count := 0 + + switch { + case o.Panorama != nil: + if o.Panorama.PanoramaDevice == "" { + return fmt.Errorf("PanoramaDevice is unspecified") + } + count++ + } + + if count == 0 { + return fmt.Errorf("no path specified") + } + + if count > 1 { + return fmt.Errorf("multiple paths specified: only one should be specified") + } + + return nil +} + +func (o Location) XpathPrefix(vn version.Number) ([]string, error) { + + var ans []string + + switch { + case o.Panorama != nil: + if o.Panorama.PanoramaDevice == "" { + return nil, fmt.Errorf("PanoramaDevice is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.Panorama.PanoramaDevice}), + } + default: + return nil, errors.NoLocationSpecifiedError + } + + return ans, nil +} +func (o Location) XpathWithEntryName(vn version.Number, name string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsEntryXpath([]string{name})) + + return ans, nil +} +func (o Location) XpathWithUuid(vn version.Number, uuid string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsUuidXpath(uuid)) + + return ans, nil +} diff --git a/panorama/template_stack/service.go b/panorama/template_stack/service.go new file mode 100644 index 0000000..0d72d66 --- /dev/null +++ b/panorama/template_stack/service.go @@ -0,0 +1,281 @@ +package template_stack + +import ( + "context" + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/xmlapi" +) + +type Service struct { + client util.PangoClient +} + +func NewService(client util.PangoClient) *Service { + return &Service{ + client: client, + } +} + +// Create adds new item, then returns the result. +func (s *Service) Create(ctx context.Context, loc Location, entry *Entry) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + path, err := loc.XpathWithEntryName(vn, entry.Name) + if err != nil { + return nil, err + } + createSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + cmd := &xmlapi.Config{ + Action: "set", + Xpath: util.AsXpath(path[:len(path)-1]), + Element: createSpec, + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, false, nil); err != nil { + return nil, err + } + return s.Read(ctx, loc, entry.Name, "get") +} + +// Read returns the given config object, using the specified action ("get" or "show"). +func (s *Service) Read(ctx context.Context, loc Location, name, action string) (*Entry, error) { + return s.read(ctx, loc, name, action, false) +} + +// ReadFromConfig returns the given config object from the loaded XML config. +// Requires that client.LoadPanosConfig() has been invoked. +func (s *Service) ReadFromConfig(ctx context.Context, loc Location, name string) (*Entry, error) { + return s.read(ctx, loc, name, "", true) +} + +func (s *Service) read(ctx context.Context, loc Location, value, action string, usePanosConfig bool) (*Entry, error) { + if value == "" { + return nil, errors.NameNotSpecifiedError + } + vn := s.client.Versioning() + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + var path []string + path, err = loc.XpathWithEntryName(vn, value) + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, true, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, errors.ObjectNotFound() + } + return nil, err + } + } + + list, err := normalizer.Normalize() + if err != nil { + return nil, err + } else if len(list) != 1 { + return nil, fmt.Errorf("expected to %q 1 entry, got %d", action, len(list)) + } + + return list[0], nil +} + +// Update updates the given config object, then returns the result. +func (s *Service) Update(ctx context.Context, loc Location, entry *Entry, name string) (*Entry, error) { + return s.update(ctx, loc, entry, name) +} +func (s *Service) update(ctx context.Context, loc Location, entry *Entry, value string) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + updates := xmlapi.NewMultiConfig(2) + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + var old *Entry + if value != "" && value != entry.Name { + path, err := loc.XpathWithEntryName(vn, value) + if err != nil { + return nil, err + } + + old, err = s.Read(ctx, loc, value, "get") + + updates.Add(&xmlapi.Config{ + Action: "rename", + Xpath: util.AsXpath(path), + NewName: entry.Name, + Target: s.client.GetTarget(), + }) + } else { + old, err = s.Read(ctx, loc, entry.Name, "get") + } + if err != nil { + return nil, err + } else if old == nil { + return nil, fmt.Errorf("previous object doesn't exist for update") + } + if !SpecMatches(entry, old) { + path, err := loc.XpathWithEntryName(vn, entry.Name) + if err != nil { + return nil, err + } + + updateSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + updates.Add(&xmlapi.Config{ + Action: "edit", + Xpath: util.AsXpath(path), + Element: updateSpec, + Target: s.client.GetTarget(), + }) + } + + if len(updates.Operations) != 0 { + if _, _, _, err = s.client.MultiConfig(ctx, updates, false, nil); err != nil { + return nil, err + } + } + return s.Read(ctx, loc, entry.Name, "get") +} + +// Delete deletes the given item. +func (s *Service) Delete(ctx context.Context, loc Location, name ...string) error { + return s.delete(ctx, loc, name) +} +func (s *Service) delete(ctx context.Context, loc Location, values []string) error { + for _, value := range values { + if value == "" { + return errors.NameNotSpecifiedError + } + } + + vn := s.client.Versioning() + var err error + deletes := xmlapi.NewMultiConfig(len(values)) + for _, value := range values { + var path []string + path, err = loc.XpathWithEntryName(vn, value) + if err != nil { + return err + } + deletes.Add(&xmlapi.Config{ + Action: "delete", + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + }) + } + + _, _, _, err = s.client.MultiConfig(ctx, deletes, false, nil) + + return err +} + +// List returns a list of objects using the given action ("get" or "show"). +// Params filter and quote are for client side filtering. +func (s *Service) List(ctx context.Context, loc Location, action, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, action, filter, quote, false) +} + +// ListFromConfig returns a list of objects at the given location. +// Requires that client.LoadPanosConfig() has been invoked. +// Params filter and quote are for client side filtering. +func (s *Service) ListFromConfig(ctx context.Context, loc Location, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, "", filter, quote, true) +} + +func (s *Service) list(ctx context.Context, loc Location, action, filter, quote string, usePanosConfig bool) ([]*Entry, error) { + var err error + + var logic *filtering.Group + if filter != "" { + logic, err = filtering.Parse(filter, quote) + if err != nil { + return nil, err + } + } + + vn := s.client.Versioning() + + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + + path, err := loc.XpathWithEntryName(vn, "") + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, false, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, nil + } + return nil, err + } + } + + listing, err := normalizer.Normalize() + if err != nil || logic == nil { + return listing, err + } + + filtered := make([]*Entry, 0, len(listing)) + for _, x := range listing { + ok, err := logic.Matches(x) + if err != nil { + return nil, err + } + if ok { + filtered = append(filtered, x) + } + } + + return filtered, nil +} diff --git a/panorama/template_variable/entry.go b/panorama/template_variable/entry.go new file mode 100644 index 0000000..e1f73c5 --- /dev/null +++ b/panorama/template_variable/entry.go @@ -0,0 +1,265 @@ +package template_variable + +import ( + "encoding/xml" + "fmt" + + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/generic" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +var ( + _ filtering.Fielder = &Entry{} +) + +var ( + Suffix = []string{"variable"} +) + +type Entry struct { + Name string + Description *string + Type *Type + + Misc map[string][]generic.Xml +} + +type Type struct { + AsNumber *string + DeviceId *string + DevicePriority *string + EgressMax *string + Fqdn *string + GroupId *string + Interface *string + IpNetmask *string + IpRange *string + LinkTag *string + QosProfile *string +} + +type entryXmlContainer struct { + Answer []entryXml `xml:"entry"` +} + +type entryXml struct { + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + Description *string `xml:"description,omitempty"` + Type *TypeXml `xml:"type,omitempty"` + + Misc []generic.Xml `xml:",any"` +} + +type TypeXml struct { + AsNumber *string `xml:"as-number,omitempty"` + DeviceId *string `xml:"device-id,omitempty"` + DevicePriority *string `xml:"device-priority,omitempty"` + EgressMax *string `xml:"egress-max,omitempty"` + Fqdn *string `xml:"fqdn,omitempty"` + GroupId *string `xml:"group-id,omitempty"` + Interface *string `xml:"interface,omitempty"` + IpNetmask *string `xml:"ip-netmask,omitempty"` + IpRange *string `xml:"ip-range,omitempty"` + LinkTag *string `xml:"link-tag,omitempty"` + QosProfile *string `xml:"qos-profile,omitempty"` + + Misc []generic.Xml `xml:",any"` +} + +func (e *Entry) Field(v string) (any, error) { + if v == "name" || v == "Name" { + return e.Name, nil + } + if v == "description" || v == "Description" { + return e.Description, nil + } + if v == "type" || v == "Type" { + return e.Type, nil + } + + return nil, fmt.Errorf("unknown field") +} + +func Versioning(vn version.Number) (Specifier, Normalizer, error) { + return specifyEntry, &entryXmlContainer{}, nil +} + +func specifyEntry(o *Entry) (any, error) { + entry := entryXml{} + + entry.Name = o.Name + entry.Description = o.Description + var nestedType *TypeXml + if o.Type != nil { + nestedType = &TypeXml{} + if _, ok := o.Misc["Type"]; ok { + nestedType.Misc = o.Misc["Type"] + } + if o.Type.DeviceId != nil { + nestedType.DeviceId = o.Type.DeviceId + } + if o.Type.Interface != nil { + nestedType.Interface = o.Type.Interface + } + if o.Type.QosProfile != nil { + nestedType.QosProfile = o.Type.QosProfile + } + if o.Type.EgressMax != nil { + nestedType.EgressMax = o.Type.EgressMax + } + if o.Type.LinkTag != nil { + nestedType.LinkTag = o.Type.LinkTag + } + if o.Type.IpNetmask != nil { + nestedType.IpNetmask = o.Type.IpNetmask + } + if o.Type.GroupId != nil { + nestedType.GroupId = o.Type.GroupId + } + if o.Type.DevicePriority != nil { + nestedType.DevicePriority = o.Type.DevicePriority + } + if o.Type.AsNumber != nil { + nestedType.AsNumber = o.Type.AsNumber + } + if o.Type.IpRange != nil { + nestedType.IpRange = o.Type.IpRange + } + if o.Type.Fqdn != nil { + nestedType.Fqdn = o.Type.Fqdn + } + } + entry.Type = nestedType + + entry.Misc = o.Misc["Entry"] + + return entry, nil +} +func (c *entryXmlContainer) Normalize() ([]*Entry, error) { + entryList := make([]*Entry, 0, len(c.Answer)) + for _, o := range c.Answer { + entry := &Entry{ + Misc: make(map[string][]generic.Xml), + } + entry.Name = o.Name + entry.Description = o.Description + var nestedType *Type + if o.Type != nil { + nestedType = &Type{} + if o.Type.Misc != nil { + entry.Misc["Type"] = o.Type.Misc + } + if o.Type.IpRange != nil { + nestedType.IpRange = o.Type.IpRange + } + if o.Type.Fqdn != nil { + nestedType.Fqdn = o.Type.Fqdn + } + if o.Type.DevicePriority != nil { + nestedType.DevicePriority = o.Type.DevicePriority + } + if o.Type.AsNumber != nil { + nestedType.AsNumber = o.Type.AsNumber + } + if o.Type.IpNetmask != nil { + nestedType.IpNetmask = o.Type.IpNetmask + } + if o.Type.GroupId != nil { + nestedType.GroupId = o.Type.GroupId + } + if o.Type.DeviceId != nil { + nestedType.DeviceId = o.Type.DeviceId + } + if o.Type.Interface != nil { + nestedType.Interface = o.Type.Interface + } + if o.Type.QosProfile != nil { + nestedType.QosProfile = o.Type.QosProfile + } + if o.Type.EgressMax != nil { + nestedType.EgressMax = o.Type.EgressMax + } + if o.Type.LinkTag != nil { + nestedType.LinkTag = o.Type.LinkTag + } + } + entry.Type = nestedType + + entry.Misc["Entry"] = o.Misc + + entryList = append(entryList, entry) + } + + return entryList, nil +} + +func SpecMatches(a, b *Entry) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + + // Don't compare Name. + if !util.StringsMatch(a.Description, b.Description) { + return false + } + if !matchType(a.Type, b.Type) { + return false + } + + return true +} + +func matchType(a *Type, b *Type) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.StringsMatch(a.AsNumber, b.AsNumber) { + return false + } + if !util.StringsMatch(a.IpRange, b.IpRange) { + return false + } + if !util.StringsMatch(a.Fqdn, b.Fqdn) { + return false + } + if !util.StringsMatch(a.DevicePriority, b.DevicePriority) { + return false + } + if !util.StringsMatch(a.Interface, b.Interface) { + return false + } + if !util.StringsMatch(a.QosProfile, b.QosProfile) { + return false + } + if !util.StringsMatch(a.EgressMax, b.EgressMax) { + return false + } + if !util.StringsMatch(a.LinkTag, b.LinkTag) { + return false + } + if !util.StringsMatch(a.IpNetmask, b.IpNetmask) { + return false + } + if !util.StringsMatch(a.GroupId, b.GroupId) { + return false + } + if !util.StringsMatch(a.DeviceId, b.DeviceId) { + return false + } + return true +} + +func (o *Entry) EntryName() string { + return o.Name +} + +func (o *Entry) SetEntryName(name string) { + o.Name = name +} diff --git a/panorama/template_variable/interfaces.go b/panorama/template_variable/interfaces.go new file mode 100644 index 0000000..94d2433 --- /dev/null +++ b/panorama/template_variable/interfaces.go @@ -0,0 +1,7 @@ +package template_variable + +type Specifier func(*Entry) (any, error) + +type Normalizer interface { + Normalize() ([]*Entry, error) +} diff --git a/panorama/template_variable/location.go b/panorama/template_variable/location.go new file mode 100644 index 0000000..b34ec41 --- /dev/null +++ b/panorama/template_variable/location.go @@ -0,0 +1,105 @@ +package template_variable + +import ( + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +type ImportLocation interface { + XpathForLocation(version.Number, util.ILocation) ([]string, error) + MarshalPangoXML([]string) (string, error) + UnmarshalPangoXML([]byte) ([]string, error) +} + +type Location struct { + Template *TemplateLocation `json:"template,omitempty"` +} + +type TemplateLocation struct { + PanoramaDevice string `json:"panorama_device"` + Template string `json:"template"` +} + +func NewTemplateLocation() *Location { + return &Location{Template: &TemplateLocation{ + PanoramaDevice: "localhost.localdomain", + Template: "", + }, + } +} + +func (o Location) IsValid() error { + count := 0 + + switch { + case o.Template != nil: + if o.Template.PanoramaDevice == "" { + return fmt.Errorf("PanoramaDevice is unspecified") + } + if o.Template.Template == "" { + return fmt.Errorf("Template is unspecified") + } + count++ + } + + if count == 0 { + return fmt.Errorf("no path specified") + } + + if count > 1 { + return fmt.Errorf("multiple paths specified: only one should be specified") + } + + return nil +} + +func (o Location) XpathPrefix(vn version.Number) ([]string, error) { + + var ans []string + + switch { + case o.Template != nil: + if o.Template.PanoramaDevice == "" { + return nil, fmt.Errorf("PanoramaDevice is unspecified") + } + if o.Template.Template == "" { + return nil, fmt.Errorf("Template is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.Template.PanoramaDevice}), + "template", + util.AsEntryXpath([]string{o.Template.Template}), + } + default: + return nil, errors.NoLocationSpecifiedError + } + + return ans, nil +} +func (o Location) XpathWithEntryName(vn version.Number, name string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsEntryXpath([]string{name})) + + return ans, nil +} +func (o Location) XpathWithUuid(vn version.Number, uuid string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsUuidXpath(uuid)) + + return ans, nil +} diff --git a/panorama/template_variable/service.go b/panorama/template_variable/service.go new file mode 100644 index 0000000..0d6a236 --- /dev/null +++ b/panorama/template_variable/service.go @@ -0,0 +1,281 @@ +package template_variable + +import ( + "context" + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/xmlapi" +) + +type Service struct { + client util.PangoClient +} + +func NewService(client util.PangoClient) *Service { + return &Service{ + client: client, + } +} + +// Create adds new item, then returns the result. +func (s *Service) Create(ctx context.Context, loc Location, entry *Entry) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + path, err := loc.XpathWithEntryName(vn, entry.Name) + if err != nil { + return nil, err + } + createSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + cmd := &xmlapi.Config{ + Action: "set", + Xpath: util.AsXpath(path[:len(path)-1]), + Element: createSpec, + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, false, nil); err != nil { + return nil, err + } + return s.Read(ctx, loc, entry.Name, "get") +} + +// Read returns the given config object, using the specified action ("get" or "show"). +func (s *Service) Read(ctx context.Context, loc Location, name, action string) (*Entry, error) { + return s.read(ctx, loc, name, action, false) +} + +// ReadFromConfig returns the given config object from the loaded XML config. +// Requires that client.LoadPanosConfig() has been invoked. +func (s *Service) ReadFromConfig(ctx context.Context, loc Location, name string) (*Entry, error) { + return s.read(ctx, loc, name, "", true) +} + +func (s *Service) read(ctx context.Context, loc Location, value, action string, usePanosConfig bool) (*Entry, error) { + if value == "" { + return nil, errors.NameNotSpecifiedError + } + vn := s.client.Versioning() + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + var path []string + path, err = loc.XpathWithEntryName(vn, value) + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, true, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, errors.ObjectNotFound() + } + return nil, err + } + } + + list, err := normalizer.Normalize() + if err != nil { + return nil, err + } else if len(list) != 1 { + return nil, fmt.Errorf("expected to %q 1 entry, got %d", action, len(list)) + } + + return list[0], nil +} + +// Update updates the given config object, then returns the result. +func (s *Service) Update(ctx context.Context, loc Location, entry *Entry, name string) (*Entry, error) { + return s.update(ctx, loc, entry, name) +} +func (s *Service) update(ctx context.Context, loc Location, entry *Entry, value string) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + updates := xmlapi.NewMultiConfig(2) + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + var old *Entry + if value != "" && value != entry.Name { + path, err := loc.XpathWithEntryName(vn, value) + if err != nil { + return nil, err + } + + old, err = s.Read(ctx, loc, value, "get") + + updates.Add(&xmlapi.Config{ + Action: "rename", + Xpath: util.AsXpath(path), + NewName: entry.Name, + Target: s.client.GetTarget(), + }) + } else { + old, err = s.Read(ctx, loc, entry.Name, "get") + } + if err != nil { + return nil, err + } else if old == nil { + return nil, fmt.Errorf("previous object doesn't exist for update") + } + if !SpecMatches(entry, old) { + path, err := loc.XpathWithEntryName(vn, entry.Name) + if err != nil { + return nil, err + } + + updateSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + updates.Add(&xmlapi.Config{ + Action: "edit", + Xpath: util.AsXpath(path), + Element: updateSpec, + Target: s.client.GetTarget(), + }) + } + + if len(updates.Operations) != 0 { + if _, _, _, err = s.client.MultiConfig(ctx, updates, false, nil); err != nil { + return nil, err + } + } + return s.Read(ctx, loc, entry.Name, "get") +} + +// Delete deletes the given item. +func (s *Service) Delete(ctx context.Context, loc Location, name ...string) error { + return s.delete(ctx, loc, name) +} +func (s *Service) delete(ctx context.Context, loc Location, values []string) error { + for _, value := range values { + if value == "" { + return errors.NameNotSpecifiedError + } + } + + vn := s.client.Versioning() + var err error + deletes := xmlapi.NewMultiConfig(len(values)) + for _, value := range values { + var path []string + path, err = loc.XpathWithEntryName(vn, value) + if err != nil { + return err + } + deletes.Add(&xmlapi.Config{ + Action: "delete", + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + }) + } + + _, _, _, err = s.client.MultiConfig(ctx, deletes, false, nil) + + return err +} + +// List returns a list of objects using the given action ("get" or "show"). +// Params filter and quote are for client side filtering. +func (s *Service) List(ctx context.Context, loc Location, action, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, action, filter, quote, false) +} + +// ListFromConfig returns a list of objects at the given location. +// Requires that client.LoadPanosConfig() has been invoked. +// Params filter and quote are for client side filtering. +func (s *Service) ListFromConfig(ctx context.Context, loc Location, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, "", filter, quote, true) +} + +func (s *Service) list(ctx context.Context, loc Location, action, filter, quote string, usePanosConfig bool) ([]*Entry, error) { + var err error + + var logic *filtering.Group + if filter != "" { + logic, err = filtering.Parse(filter, quote) + if err != nil { + return nil, err + } + } + + vn := s.client.Versioning() + + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + + path, err := loc.XpathWithEntryName(vn, "") + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, false, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, nil + } + return nil, err + } + } + + listing, err := normalizer.Normalize() + if err != nil || logic == nil { + return listing, err + } + + filtered := make([]*Entry, 0, len(listing)) + for _, x := range listing { + ok, err := logic.Matches(x) + if err != nil { + return nil, err + } + if ok { + filtered = append(filtered, x) + } + } + + return filtered, nil +} diff --git a/policies/rules/security/const.go b/policies/rules/security/const.go new file mode 100644 index 0000000..802e05e --- /dev/null +++ b/policies/rules/security/const.go @@ -0,0 +1,3 @@ +package security + +const RuleType = "security" diff --git a/policies/rules/security/entry.go b/policies/rules/security/entry.go new file mode 100644 index 0000000..987d4d1 --- /dev/null +++ b/policies/rules/security/entry.go @@ -0,0 +1,525 @@ +package security + +import ( + "encoding/xml" + "fmt" + + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/generic" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +var ( + _ filtering.Fielder = &Entry{} +) + +var ( + Suffix = []string{"security", "rules"} +) + +type Entry struct { + Name string + Action *string + Applications []string + Categories []string + Description *string + DestinationAddresses []string + DestinationHips []string + DestinationZones []string + DisableServerResponseInspection *bool + Disabled *bool + IcmpUnreachable *bool + LogEnd *bool + LogSetting *string + LogStart *bool + NegateDestination *bool + NegateSource *bool + ProfileSetting *ProfileSetting + RuleType *string + Services []string + SourceAddresses []string + SourceHips []string + SourceUsers []string + SourceZones []string + Tags []string + Uuid *string + + Misc map[string][]generic.Xml +} + +type ProfileSetting struct { + Group *string + Profiles *ProfileSettingProfiles +} +type ProfileSettingProfiles struct { + DataFiltering []string + FileBlocking []string + Spyware []string + UrlFiltering []string + Virus []string + Vulnerability []string + WildfireAnalysis []string +} + +type entryXmlContainer struct { + Answer []entryXml `xml:"entry"` +} + +type entryXml struct { + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + Action *string `xml:"action,omitempty"` + Applications *util.MemberType `xml:"application,omitempty"` + Categories *util.MemberType `xml:"category,omitempty"` + Description *string `xml:"description,omitempty"` + DestinationAddresses *util.MemberType `xml:"destination,omitempty"` + DestinationHips *util.MemberType `xml:"destination-hip,omitempty"` + DestinationZones *util.MemberType `xml:"to,omitempty"` + DisableServerResponseInspection *string `xml:"option>disable-server-response-inspection,omitempty"` + Disabled *string `xml:"disabled,omitempty"` + IcmpUnreachable *string `xml:"icmp-unreachable,omitempty"` + LogEnd *string `xml:"log-end,omitempty"` + LogSetting *string `xml:"log-setting,omitempty"` + LogStart *string `xml:"log-start,omitempty"` + NegateDestination *string `xml:"negate-destination,omitempty"` + NegateSource *string `xml:"negate-source,omitempty"` + ProfileSetting *ProfileSettingXml `xml:"profile-setting,omitempty"` + RuleType *string `xml:"rule-type,omitempty"` + Services *util.MemberType `xml:"service,omitempty"` + SourceAddresses *util.MemberType `xml:"source,omitempty"` + SourceHips *util.MemberType `xml:"source-hip,omitempty"` + SourceUsers *util.MemberType `xml:"source-user,omitempty"` + SourceZones *util.MemberType `xml:"from,omitempty"` + Tags *util.MemberType `xml:"tag,omitempty"` + Uuid *string `xml:"uuid,attr,omitempty"` + + Misc []generic.Xml `xml:",any"` +} + +type ProfileSettingXml struct { + Group *string `xml:"group,omitempty"` + Profiles *ProfileSettingProfilesXml `xml:"profiles,omitempty"` + + Misc []generic.Xml `xml:",any"` +} +type ProfileSettingProfilesXml struct { + DataFiltering *util.MemberType `xml:"data-filtering,omitempty"` + FileBlocking *util.MemberType `xml:"file-blocking,omitempty"` + Spyware *util.MemberType `xml:"spyware,omitempty"` + UrlFiltering *util.MemberType `xml:"url-filtering,omitempty"` + Virus *util.MemberType `xml:"virus,omitempty"` + Vulnerability *util.MemberType `xml:"vulnerability,omitempty"` + WildfireAnalysis *util.MemberType `xml:"wildfire-analysis,omitempty"` + + Misc []generic.Xml `xml:",any"` +} + +func (e *Entry) Field(v string) (any, error) { + if v == "name" || v == "Name" { + return e.Name, nil + } + if v == "action" || v == "Action" { + return e.Action, nil + } + if v == "applications" || v == "Applications" { + return e.Applications, nil + } + if v == "applications|LENGTH" || v == "Applications|LENGTH" { + return int64(len(e.Applications)), nil + } + if v == "categories" || v == "Categories" { + return e.Categories, nil + } + if v == "categories|LENGTH" || v == "Categories|LENGTH" { + return int64(len(e.Categories)), nil + } + if v == "description" || v == "Description" { + return e.Description, nil + } + if v == "destination_addresses" || v == "DestinationAddresses" { + return e.DestinationAddresses, nil + } + if v == "destination_addresses|LENGTH" || v == "DestinationAddresses|LENGTH" { + return int64(len(e.DestinationAddresses)), nil + } + if v == "destination_hips" || v == "DestinationHips" { + return e.DestinationHips, nil + } + if v == "destination_hips|LENGTH" || v == "DestinationHips|LENGTH" { + return int64(len(e.DestinationHips)), nil + } + if v == "destination_zones" || v == "DestinationZones" { + return e.DestinationZones, nil + } + if v == "destination_zones|LENGTH" || v == "DestinationZones|LENGTH" { + return int64(len(e.DestinationZones)), nil + } + if v == "disable_server_response_inspection" || v == "DisableServerResponseInspection" { + return e.DisableServerResponseInspection, nil + } + if v == "disabled" || v == "Disabled" { + return e.Disabled, nil + } + if v == "icmp_unreachable" || v == "IcmpUnreachable" { + return e.IcmpUnreachable, nil + } + if v == "log_end" || v == "LogEnd" { + return e.LogEnd, nil + } + if v == "log_setting" || v == "LogSetting" { + return e.LogSetting, nil + } + if v == "log_start" || v == "LogStart" { + return e.LogStart, nil + } + if v == "negate_destination" || v == "NegateDestination" { + return e.NegateDestination, nil + } + if v == "negate_source" || v == "NegateSource" { + return e.NegateSource, nil + } + if v == "profile_setting" || v == "ProfileSetting" { + return e.ProfileSetting, nil + } + if v == "rule_type" || v == "RuleType" { + return e.RuleType, nil + } + if v == "services" || v == "Services" { + return e.Services, nil + } + if v == "services|LENGTH" || v == "Services|LENGTH" { + return int64(len(e.Services)), nil + } + if v == "source_addresses" || v == "SourceAddresses" { + return e.SourceAddresses, nil + } + if v == "source_addresses|LENGTH" || v == "SourceAddresses|LENGTH" { + return int64(len(e.SourceAddresses)), nil + } + if v == "source_hips" || v == "SourceHips" { + return e.SourceHips, nil + } + if v == "source_hips|LENGTH" || v == "SourceHips|LENGTH" { + return int64(len(e.SourceHips)), nil + } + if v == "source_users" || v == "SourceUsers" { + return e.SourceUsers, nil + } + if v == "source_users|LENGTH" || v == "SourceUsers|LENGTH" { + return int64(len(e.SourceUsers)), nil + } + if v == "source_zones" || v == "SourceZones" { + return e.SourceZones, nil + } + if v == "source_zones|LENGTH" || v == "SourceZones|LENGTH" { + return int64(len(e.SourceZones)), nil + } + if v == "tags" || v == "Tags" { + return e.Tags, nil + } + if v == "tags|LENGTH" || v == "Tags|LENGTH" { + return int64(len(e.Tags)), nil + } + if v == "uuid" || v == "Uuid" { + return e.Uuid, nil + } + + return nil, fmt.Errorf("unknown field") +} + +func Versioning(vn version.Number) (Specifier, Normalizer, error) { + return specifyEntry, &entryXmlContainer{}, nil +} + +func specifyEntry(o *Entry) (any, error) { + entry := entryXml{} + + entry.Name = o.Name + entry.Action = o.Action + entry.Applications = util.StrToMem(o.Applications) + entry.Categories = util.StrToMem(o.Categories) + entry.Description = o.Description + entry.DestinationAddresses = util.StrToMem(o.DestinationAddresses) + entry.DestinationHips = util.StrToMem(o.DestinationHips) + entry.DestinationZones = util.StrToMem(o.DestinationZones) + entry.DisableServerResponseInspection = util.YesNo(o.DisableServerResponseInspection, nil) + entry.Disabled = util.YesNo(o.Disabled, nil) + entry.IcmpUnreachable = util.YesNo(o.IcmpUnreachable, nil) + entry.LogEnd = util.YesNo(o.LogEnd, nil) + entry.LogSetting = o.LogSetting + entry.LogStart = util.YesNo(o.LogStart, nil) + entry.NegateDestination = util.YesNo(o.NegateDestination, nil) + entry.NegateSource = util.YesNo(o.NegateSource, nil) + var nestedProfileSetting *ProfileSettingXml + if o.ProfileSetting != nil { + nestedProfileSetting = &ProfileSettingXml{} + if _, ok := o.Misc["ProfileSetting"]; ok { + nestedProfileSetting.Misc = o.Misc["ProfileSetting"] + } + if o.ProfileSetting.Group != nil { + nestedProfileSetting.Group = o.ProfileSetting.Group + } + if o.ProfileSetting.Profiles != nil { + nestedProfileSetting.Profiles = &ProfileSettingProfilesXml{} + if _, ok := o.Misc["ProfileSettingProfiles"]; ok { + nestedProfileSetting.Profiles.Misc = o.Misc["ProfileSettingProfiles"] + } + if o.ProfileSetting.Profiles.UrlFiltering != nil { + nestedProfileSetting.Profiles.UrlFiltering = util.StrToMem(o.ProfileSetting.Profiles.UrlFiltering) + } + if o.ProfileSetting.Profiles.FileBlocking != nil { + nestedProfileSetting.Profiles.FileBlocking = util.StrToMem(o.ProfileSetting.Profiles.FileBlocking) + } + if o.ProfileSetting.Profiles.WildfireAnalysis != nil { + nestedProfileSetting.Profiles.WildfireAnalysis = util.StrToMem(o.ProfileSetting.Profiles.WildfireAnalysis) + } + if o.ProfileSetting.Profiles.DataFiltering != nil { + nestedProfileSetting.Profiles.DataFiltering = util.StrToMem(o.ProfileSetting.Profiles.DataFiltering) + } + if o.ProfileSetting.Profiles.Virus != nil { + nestedProfileSetting.Profiles.Virus = util.StrToMem(o.ProfileSetting.Profiles.Virus) + } + if o.ProfileSetting.Profiles.Spyware != nil { + nestedProfileSetting.Profiles.Spyware = util.StrToMem(o.ProfileSetting.Profiles.Spyware) + } + if o.ProfileSetting.Profiles.Vulnerability != nil { + nestedProfileSetting.Profiles.Vulnerability = util.StrToMem(o.ProfileSetting.Profiles.Vulnerability) + } + } + } + entry.ProfileSetting = nestedProfileSetting + + entry.RuleType = o.RuleType + entry.Services = util.StrToMem(o.Services) + entry.SourceAddresses = util.StrToMem(o.SourceAddresses) + entry.SourceHips = util.StrToMem(o.SourceHips) + entry.SourceUsers = util.StrToMem(o.SourceUsers) + entry.SourceZones = util.StrToMem(o.SourceZones) + entry.Tags = util.StrToMem(o.Tags) + entry.Uuid = o.Uuid + + entry.Misc = o.Misc["Entry"] + + return entry, nil +} +func (c *entryXmlContainer) Normalize() ([]*Entry, error) { + entryList := make([]*Entry, 0, len(c.Answer)) + for _, o := range c.Answer { + entry := &Entry{ + Misc: make(map[string][]generic.Xml), + } + entry.Name = o.Name + entry.Action = o.Action + entry.Applications = util.MemToStr(o.Applications) + entry.Categories = util.MemToStr(o.Categories) + entry.Description = o.Description + entry.DestinationAddresses = util.MemToStr(o.DestinationAddresses) + entry.DestinationHips = util.MemToStr(o.DestinationHips) + entry.DestinationZones = util.MemToStr(o.DestinationZones) + entry.DisableServerResponseInspection = util.AsBool(o.DisableServerResponseInspection, nil) + entry.Disabled = util.AsBool(o.Disabled, nil) + entry.IcmpUnreachable = util.AsBool(o.IcmpUnreachable, nil) + entry.LogEnd = util.AsBool(o.LogEnd, nil) + entry.LogSetting = o.LogSetting + entry.LogStart = util.AsBool(o.LogStart, nil) + entry.NegateDestination = util.AsBool(o.NegateDestination, nil) + entry.NegateSource = util.AsBool(o.NegateSource, nil) + var nestedProfileSetting *ProfileSetting + if o.ProfileSetting != nil { + nestedProfileSetting = &ProfileSetting{} + if o.ProfileSetting.Misc != nil { + entry.Misc["ProfileSetting"] = o.ProfileSetting.Misc + } + if o.ProfileSetting.Group != nil { + nestedProfileSetting.Group = o.ProfileSetting.Group + } + if o.ProfileSetting.Profiles != nil { + nestedProfileSetting.Profiles = &ProfileSettingProfiles{} + if o.ProfileSetting.Profiles.Misc != nil { + entry.Misc["ProfileSettingProfiles"] = o.ProfileSetting.Profiles.Misc + } + if o.ProfileSetting.Profiles.DataFiltering != nil { + nestedProfileSetting.Profiles.DataFiltering = util.MemToStr(o.ProfileSetting.Profiles.DataFiltering) + } + if o.ProfileSetting.Profiles.Virus != nil { + nestedProfileSetting.Profiles.Virus = util.MemToStr(o.ProfileSetting.Profiles.Virus) + } + if o.ProfileSetting.Profiles.Spyware != nil { + nestedProfileSetting.Profiles.Spyware = util.MemToStr(o.ProfileSetting.Profiles.Spyware) + } + if o.ProfileSetting.Profiles.Vulnerability != nil { + nestedProfileSetting.Profiles.Vulnerability = util.MemToStr(o.ProfileSetting.Profiles.Vulnerability) + } + if o.ProfileSetting.Profiles.UrlFiltering != nil { + nestedProfileSetting.Profiles.UrlFiltering = util.MemToStr(o.ProfileSetting.Profiles.UrlFiltering) + } + if o.ProfileSetting.Profiles.FileBlocking != nil { + nestedProfileSetting.Profiles.FileBlocking = util.MemToStr(o.ProfileSetting.Profiles.FileBlocking) + } + if o.ProfileSetting.Profiles.WildfireAnalysis != nil { + nestedProfileSetting.Profiles.WildfireAnalysis = util.MemToStr(o.ProfileSetting.Profiles.WildfireAnalysis) + } + } + } + entry.ProfileSetting = nestedProfileSetting + + entry.RuleType = o.RuleType + entry.Services = util.MemToStr(o.Services) + entry.SourceAddresses = util.MemToStr(o.SourceAddresses) + entry.SourceHips = util.MemToStr(o.SourceHips) + entry.SourceUsers = util.MemToStr(o.SourceUsers) + entry.SourceZones = util.MemToStr(o.SourceZones) + entry.Tags = util.MemToStr(o.Tags) + entry.Uuid = o.Uuid + + entry.Misc["Entry"] = o.Misc + + entryList = append(entryList, entry) + } + + return entryList, nil +} + +func SpecMatches(a, b *Entry) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + + // Don't compare Name. + if !util.StringsMatch(a.Action, b.Action) { + return false + } + if !util.OrderedListsMatch(a.Applications, b.Applications) { + return false + } + if !util.OrderedListsMatch(a.Categories, b.Categories) { + return false + } + if !util.StringsMatch(a.Description, b.Description) { + return false + } + if !util.OrderedListsMatch(a.DestinationAddresses, b.DestinationAddresses) { + return false + } + if !util.OrderedListsMatch(a.DestinationHips, b.DestinationHips) { + return false + } + if !util.OrderedListsMatch(a.DestinationZones, b.DestinationZones) { + return false + } + if !util.BoolsMatch(a.DisableServerResponseInspection, b.DisableServerResponseInspection) { + return false + } + if !util.BoolsMatch(a.Disabled, b.Disabled) { + return false + } + if !util.BoolsMatch(a.IcmpUnreachable, b.IcmpUnreachable) { + return false + } + if !util.BoolsMatch(a.LogEnd, b.LogEnd) { + return false + } + if !util.StringsMatch(a.LogSetting, b.LogSetting) { + return false + } + if !util.BoolsMatch(a.LogStart, b.LogStart) { + return false + } + if !util.BoolsMatch(a.NegateDestination, b.NegateDestination) { + return false + } + if !util.BoolsMatch(a.NegateSource, b.NegateSource) { + return false + } + if !matchProfileSetting(a.ProfileSetting, b.ProfileSetting) { + return false + } + if !util.StringsMatch(a.RuleType, b.RuleType) { + return false + } + if !util.OrderedListsMatch(a.Services, b.Services) { + return false + } + if !util.OrderedListsMatch(a.SourceAddresses, b.SourceAddresses) { + return false + } + if !util.OrderedListsMatch(a.SourceHips, b.SourceHips) { + return false + } + if !util.OrderedListsMatch(a.SourceUsers, b.SourceUsers) { + return false + } + if !util.OrderedListsMatch(a.SourceZones, b.SourceZones) { + return false + } + if !util.OrderedListsMatch(a.Tags, b.Tags) { + return false + } + if !util.StringsMatch(a.Uuid, b.Uuid) { + return false + } + + return true +} + +func matchProfileSettingProfiles(a *ProfileSettingProfiles, b *ProfileSettingProfiles) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.OrderedListsMatch(a.FileBlocking, b.FileBlocking) { + return false + } + if !util.OrderedListsMatch(a.WildfireAnalysis, b.WildfireAnalysis) { + return false + } + if !util.OrderedListsMatch(a.DataFiltering, b.DataFiltering) { + return false + } + if !util.OrderedListsMatch(a.Virus, b.Virus) { + return false + } + if !util.OrderedListsMatch(a.Spyware, b.Spyware) { + return false + } + if !util.OrderedListsMatch(a.Vulnerability, b.Vulnerability) { + return false + } + if !util.OrderedListsMatch(a.UrlFiltering, b.UrlFiltering) { + return false + } + return true +} +func matchProfileSetting(a *ProfileSetting, b *ProfileSetting) bool { + if a == nil && b != nil || a != nil && b == nil { + return false + } else if a == nil && b == nil { + return true + } + if !util.StringsMatch(a.Group, b.Group) { + return false + } + if !matchProfileSettingProfiles(a.Profiles, b.Profiles) { + return false + } + return true +} + +func (o *Entry) EntryName() string { + return o.Name +} + +func (o *Entry) SetEntryName(name string) { + o.Name = name +} +func (o *Entry) EntryUuid() *string { + return o.Uuid +} + +func (o *Entry) SetEntryUuid(uuid *string) { + o.Uuid = uuid +} diff --git a/policies/rules/security/interfaces.go b/policies/rules/security/interfaces.go new file mode 100644 index 0000000..d15711c --- /dev/null +++ b/policies/rules/security/interfaces.go @@ -0,0 +1,7 @@ +package security + +type Specifier func(*Entry) (any, error) + +type Normalizer interface { + Normalize() ([]*Entry, error) +} diff --git a/policies/rules/security/location.go b/policies/rules/security/location.go new file mode 100644 index 0000000..d24cdb8 --- /dev/null +++ b/policies/rules/security/location.go @@ -0,0 +1,210 @@ +package security + +import ( + "fmt" + + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" +) + +type ImportLocation interface { + XpathForLocation(version.Number, util.ILocation) ([]string, error) + MarshalPangoXML([]string) (string, error) + UnmarshalPangoXML([]byte) ([]string, error) +} + +type Location struct { + DeviceGroup *DeviceGroupLocation `json:"device_group,omitempty"` + FromPanoramaVsys *FromPanoramaVsysLocation `json:"from_panorama_vsys,omitempty"` + Shared *SharedLocation `json:"shared,omitempty"` + Vsys *VsysLocation `json:"vsys,omitempty"` +} + +type DeviceGroupLocation struct { + DeviceGroup string `json:"device_group"` + PanoramaDevice string `json:"panorama_device"` + Rulebase string `json:"rulebase"` +} + +type FromPanoramaVsysLocation struct { + Vsys string `json:"vsys"` +} + +type SharedLocation struct { + Rulebase string `json:"rulebase"` +} + +type VsysLocation struct { + NgfwDevice string `json:"ngfw_device"` + Rulebase string `json:"rulebase"` + Vsys string `json:"vsys"` +} + +func NewDeviceGroupLocation() *Location { + return &Location{DeviceGroup: &DeviceGroupLocation{ + DeviceGroup: "", + PanoramaDevice: "localhost.localdomain", + Rulebase: "pre-rulebase", + }, + } +} +func NewFromPanoramaVsysLocation() *Location { + return &Location{FromPanoramaVsys: &FromPanoramaVsysLocation{ + Vsys: "vsys1", + }, + } +} +func NewSharedLocation() *Location { + return &Location{Shared: &SharedLocation{ + Rulebase: "pre-rulebase", + }, + } +} +func NewVsysLocation() *Location { + return &Location{Vsys: &VsysLocation{ + NgfwDevice: "localhost.localdomain", + Rulebase: "pre-rulebase", + Vsys: "vsys1", + }, + } +} + +func (o Location) IsValid() error { + count := 0 + + switch { + case o.DeviceGroup != nil: + if o.DeviceGroup.DeviceGroup == "" { + return fmt.Errorf("DeviceGroup is unspecified") + } + if o.DeviceGroup.PanoramaDevice == "" { + return fmt.Errorf("PanoramaDevice is unspecified") + } + if o.DeviceGroup.Rulebase == "" { + return fmt.Errorf("Rulebase is unspecified") + } + count++ + case o.FromPanoramaVsys != nil: + if o.FromPanoramaVsys.Vsys == "" { + return fmt.Errorf("Vsys is unspecified") + } + count++ + case o.Shared != nil: + if o.Shared.Rulebase == "" { + return fmt.Errorf("Rulebase is unspecified") + } + count++ + case o.Vsys != nil: + if o.Vsys.NgfwDevice == "" { + return fmt.Errorf("NgfwDevice is unspecified") + } + if o.Vsys.Rulebase == "" { + return fmt.Errorf("Rulebase is unspecified") + } + if o.Vsys.Vsys == "" { + return fmt.Errorf("Vsys is unspecified") + } + count++ + } + + if count == 0 { + return fmt.Errorf("no path specified") + } + + if count > 1 { + return fmt.Errorf("multiple paths specified: only one should be specified") + } + + return nil +} + +func (o Location) XpathPrefix(vn version.Number) ([]string, error) { + + var ans []string + + switch { + case o.DeviceGroup != nil: + if o.DeviceGroup.DeviceGroup == "" { + return nil, fmt.Errorf("DeviceGroup is unspecified") + } + if o.DeviceGroup.PanoramaDevice == "" { + return nil, fmt.Errorf("PanoramaDevice is unspecified") + } + if o.DeviceGroup.Rulebase == "" { + return nil, fmt.Errorf("Rulebase is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.DeviceGroup.PanoramaDevice}), + "device-group", + util.AsEntryXpath([]string{o.DeviceGroup.DeviceGroup}), + o.DeviceGroup.Rulebase, + } + case o.FromPanoramaVsys != nil: + if o.FromPanoramaVsys.Vsys == "" { + return nil, fmt.Errorf("Vsys is unspecified") + } + ans = []string{ + "config", + "panorama", + "vsys", + util.AsEntryXpath([]string{o.FromPanoramaVsys.Vsys}), + "rulebase", + } + case o.Shared != nil: + if o.Shared.Rulebase == "" { + return nil, fmt.Errorf("Rulebase is unspecified") + } + ans = []string{ + "config", + "shared", + o.Shared.Rulebase, + } + case o.Vsys != nil: + if o.Vsys.NgfwDevice == "" { + return nil, fmt.Errorf("NgfwDevice is unspecified") + } + if o.Vsys.Rulebase == "" { + return nil, fmt.Errorf("Rulebase is unspecified") + } + if o.Vsys.Vsys == "" { + return nil, fmt.Errorf("Vsys is unspecified") + } + ans = []string{ + "config", + "devices", + util.AsEntryXpath([]string{o.Vsys.NgfwDevice}), + "vsys", + util.AsEntryXpath([]string{o.Vsys.Vsys}), + "rulebase", + } + default: + return nil, errors.NoLocationSpecifiedError + } + + return ans, nil +} +func (o Location) XpathWithEntryName(vn version.Number, name string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsEntryXpath([]string{name})) + + return ans, nil +} +func (o Location) XpathWithUuid(vn version.Number, uuid string) ([]string, error) { + + ans, err := o.XpathPrefix(vn) + if err != nil { + return nil, err + } + ans = append(ans, Suffix...) + ans = append(ans, util.AsUuidXpath(uuid)) + + return ans, nil +} diff --git a/policies/rules/security/service.go b/policies/rules/security/service.go new file mode 100644 index 0000000..a61fa7f --- /dev/null +++ b/policies/rules/security/service.go @@ -0,0 +1,862 @@ +package security + +import ( + "context" + "fmt" + "net/url" + "strings" + "time" + + "github.com/PaloAltoNetworks/pango/audit" + "github.com/PaloAltoNetworks/pango/errors" + "github.com/PaloAltoNetworks/pango/filtering" + "github.com/PaloAltoNetworks/pango/rule" + "github.com/PaloAltoNetworks/pango/util" + "github.com/PaloAltoNetworks/pango/version" + "github.com/PaloAltoNetworks/pango/xmlapi" +) + +type Service struct { + client util.PangoClient +} + +func NewService(client util.PangoClient) *Service { + return &Service{ + client: client, + } +} + +// Create adds new item, then returns the result. +func (s *Service) Create(ctx context.Context, loc Location, entry *Entry) (*Entry, error) { + if entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + path, err := loc.XpathWithEntryName(vn, entry.Name) + if err != nil { + return nil, err + } + createSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + cmd := &xmlapi.Config{ + Action: "set", + Xpath: util.AsXpath(path[:len(path)-1]), + Element: createSpec, + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, false, nil); err != nil { + return nil, err + } + return s.Read(ctx, loc, entry.Name, "get") +} + +// Read returns the given config object, using the specified action ("get" or "show"). +func (s *Service) Read(ctx context.Context, loc Location, name, action string) (*Entry, error) { + return s.read(ctx, loc, name, action, true, false) +} + +// ReadById returns the given config object with specified ID, using the specified action ("get" or "show"). +func (s *Service) ReadById(ctx context.Context, loc Location, uuid, action string) (*Entry, error) { + return s.read(ctx, loc, uuid, action, false, false) +} + +// ReadFromConfig returns the given config object from the loaded XML config. +// Requires that client.LoadPanosConfig() has been invoked. +func (s *Service) ReadFromConfig(ctx context.Context, loc Location, name string) (*Entry, error) { + return s.read(ctx, loc, name, "", true, true) +} + +// ReadFromConfigById returns the given config object with specified ID from the loaded XML config. +// Requires that client.LoadPanosConfig() has been invoked. +func (s *Service) ReadFromConfigById(ctx context.Context, loc Location, uuid string) (*Entry, error) { + return s.read(ctx, loc, uuid, "", false, true) +} + +func (s *Service) read(ctx context.Context, loc Location, value, action string, byName, usePanosConfig bool) (*Entry, error) { + if byName && value == "" { + return nil, errors.NameNotSpecifiedError + } + if !byName && value == "" { + return nil, errors.UuidNotSpecifiedError + } + vn := s.client.Versioning() + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + var path []string + if byName { + path, err = loc.XpathWithEntryName(vn, value) + } else { + path, err = loc.XpathWithUuid(vn, value) + } + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, true, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, errors.ObjectNotFound() + } + return nil, err + } + } + + list, err := normalizer.Normalize() + if err != nil { + return nil, err + } else if len(list) != 1 { + return nil, fmt.Errorf("expected to %q 1 entry, got %d", action, len(list)) + } + + return list[0], nil +} + +// Update updates the given config object, then returns the result. +func (s *Service) Update(ctx context.Context, loc Location, entry *Entry, name string) (*Entry, error) { + return s.update(ctx, loc, entry, name, true) +} + +// UpdateById updates the given config object, then returns the result. +func (s *Service) UpdateById(ctx context.Context, loc Location, entry *Entry, uuid string) (*Entry, error) { + return s.update(ctx, loc, entry, uuid, false) +} +func (s *Service) update(ctx context.Context, loc Location, entry *Entry, value string, byName bool) (*Entry, error) { + if byName && entry.Name == "" { + return nil, errors.NameNotSpecifiedError + } + if !byName && value == "" { + return nil, errors.UuidNotSpecifiedError + } + + vn := s.client.Versioning() + updates := xmlapi.NewMultiConfig(2) + specifier, _, err := Versioning(vn) + if err != nil { + return nil, err + } + var old *Entry + if byName { + if value != "" && value != entry.Name { + path, err := loc.XpathWithEntryName(vn, value) + if err != nil { + return nil, err + } + + old, err = s.Read(ctx, loc, value, "get") + + updates.Add(&xmlapi.Config{ + Action: "rename", + Xpath: util.AsXpath(path), + NewName: entry.Name, + Target: s.client.GetTarget(), + }) + } else { + old, err = s.Read(ctx, loc, entry.Name, "get") + } + } else { + old, err = s.ReadById(ctx, loc, value, "get") + } + if err != nil { + return nil, err + } else if old == nil { + return nil, fmt.Errorf("previous object doesn't exist for update") + } + if !SpecMatches(entry, old) { + path, err := loc.XpathWithEntryName(vn, entry.Name) + if err != nil { + return nil, err + } + + updateSpec, err := specifier(entry) + if err != nil { + return nil, err + } + + updates.Add(&xmlapi.Config{ + Action: "edit", + Xpath: util.AsXpath(path), + Element: updateSpec, + Target: s.client.GetTarget(), + }) + } + + if len(updates.Operations) != 0 { + if _, _, _, err = s.client.MultiConfig(ctx, updates, false, nil); err != nil { + return nil, err + } + } + if byName { + return s.Read(ctx, loc, entry.Name, "get") + } else { + return s.ReadById(ctx, loc, value, "get") + } +} + +// Delete deletes the given item. +func (s *Service) Delete(ctx context.Context, loc Location, name ...string) error { + return s.delete(ctx, loc, name, true) +} + +// DeleteById deletes the given item with specified ID. +func (s *Service) DeleteById(ctx context.Context, loc Location, uuid ...string) error { + return s.delete(ctx, loc, uuid, false) +} +func (s *Service) delete(ctx context.Context, loc Location, values []string, byName bool) error { + for _, value := range values { + if byName && value == "" { + return errors.NameNotSpecifiedError + } + if !byName && value == "" { + return errors.UuidNotSpecifiedError + } + } + + vn := s.client.Versioning() + var err error + deletes := xmlapi.NewMultiConfig(len(values)) + for _, value := range values { + var path []string + if byName { + path, err = loc.XpathWithEntryName(vn, value) + } else { + path, err = loc.XpathWithUuid(vn, value) + } + if err != nil { + return err + } + deletes.Add(&xmlapi.Config{ + Action: "delete", + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + }) + } + + _, _, _, err = s.client.MultiConfig(ctx, deletes, false, nil) + + return err +} + +// List returns a list of objects using the given action ("get" or "show"). +// Params filter and quote are for client side filtering. +func (s *Service) List(ctx context.Context, loc Location, action, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, action, filter, quote, false) +} + +// ListFromConfig returns a list of objects at the given location. +// Requires that client.LoadPanosConfig() has been invoked. +// Params filter and quote are for client side filtering. +func (s *Service) ListFromConfig(ctx context.Context, loc Location, filter, quote string) ([]*Entry, error) { + return s.list(ctx, loc, "", filter, quote, true) +} + +func (s *Service) list(ctx context.Context, loc Location, action, filter, quote string, usePanosConfig bool) ([]*Entry, error) { + var err error + + var logic *filtering.Group + if filter != "" { + logic, err = filtering.Parse(filter, quote) + if err != nil { + return nil, err + } + } + + vn := s.client.Versioning() + + _, normalizer, err := Versioning(vn) + if err != nil { + return nil, err + } + + path, err := loc.XpathWithEntryName(vn, "") + if err != nil { + return nil, err + } + + if usePanosConfig { + if _, err = s.client.ReadFromConfig(ctx, path, false, normalizer); err != nil { + return nil, err + } + } else { + cmd := &xmlapi.Config{ + Action: action, + Xpath: util.AsXpath(path), + Target: s.client.GetTarget(), + } + + if _, _, err = s.client.Communicate(ctx, cmd, true, normalizer); err != nil { + if err.Error() == "No such node" && action == "show" { + return nil, nil + } + return nil, err + } + } + + listing, err := normalizer.Normalize() + if err != nil || logic == nil { + return listing, err + } + + filtered := make([]*Entry, 0, len(listing)) + for _, x := range listing { + ok, err := logic.Matches(x) + if err != nil { + return nil, err + } + if ok { + filtered = append(filtered, x) + } + } + + return filtered, nil +} + +// MoveGroup arranges the given rules in the order specified. +// Any rule with a UUID specified is ignored. +// Only the rule names are considered for the purposes of the rule placement. +func (s *Service) MoveGroup(ctx context.Context, loc Location, position rule.Position, entries []*Entry) error { + if len(entries) == 0 { + return nil + } + + listing, err := s.List(ctx, loc, "get", "", "") + if err != nil { + return err + } else if len(listing) == 0 { + return fmt.Errorf("no rules present") + } + + rp := make(map[string]int) + for idx, live := range listing { + rp[live.Name] = idx + } + + vn := s.client.Versioning() + updates := xmlapi.NewMultiConfig(len(entries)) + + var ok, topDown bool + var otherIndex int + baseIndex := -1 + switch { + case position.First != nil && *position.First: + topDown, baseIndex, ok, err = s.moveTop(topDown, entries, baseIndex, ok, rp, loc, vn, updates) + if err != nil { + return err + } + case position.Last != nil && *position.Last: + baseIndex, ok, err = s.moveBottom(entries, baseIndex, ok, rp, listing, loc, vn, updates) + if err != nil { + return err + } + case position.SomewhereAfter != nil && *position.SomewhereAfter != "": + topDown, baseIndex, ok, otherIndex, err = s.moveSomewhereAfter(topDown, entries, baseIndex, ok, rp, otherIndex, position, loc, vn, updates) + if err != nil { + return err + } + case position.SomewhereBefore != nil && *position.SomewhereBefore != "": + baseIndex, ok, otherIndex, err = s.moveSomewhereBefore(entries, baseIndex, ok, rp, otherIndex, position, loc, vn, updates) + if err != nil { + return err + } + case position.DirectlyAfter != nil && *position.DirectlyAfter != "": + topDown, baseIndex, ok, otherIndex, err = s.moveDirectlyAfter(topDown, entries, baseIndex, ok, rp, otherIndex, position, loc, vn, updates) + if err != nil { + return err + } + case position.DirectlyBefore != nil && *position.DirectlyBefore != "": + baseIndex, ok, err = s.moveDirectlyBefore(entries, baseIndex, ok, rp, otherIndex, position, loc, vn, updates) + if err != nil { + return err + } + default: + topDown = true + target := entries[0] + + baseIndex, ok = rp[target.Name] + if !ok { + return fmt.Errorf("could not find rule %q for first positioning", target.Name) + } + } + + var prevName, where string + if topDown { + prevName = entries[0].Name + where = "after" + } else { + prevName = entries[len(entries)-1].Name + where = "before" + } + + for i := 1; i < len(entries); i++ { + err := s.moveRestOfRules(topDown, entries, i, baseIndex, rp, loc, vn, updates, where, prevName) + if err != nil { + return err + } + } + + if len(updates.Operations) > 0 { + _, _, _, err = s.client.MultiConfig(ctx, updates, false, nil) + return err + } + + return nil +} + +func (s *Service) moveRestOfRules(topDown bool, entries []*Entry, i int, baseIndex int, rp map[string]int, loc Location, vn version.Number, updates *xmlapi.MultiConfig, where string, prevName string) error { + var target Entry + var desiredIndex int + if topDown { + target = *entries[i] + desiredIndex = baseIndex + i + } else { + target = *entries[len(entries)-1-i] + desiredIndex = baseIndex - i + } + + idx, ok := rp[target.Name] + if !ok { + return fmt.Errorf("rule %q not present", target.Name) + } + + if idx != desiredIndex { + path, err := loc.XpathWithEntryName(vn, target.Name) + if err != nil { + return err + } + + if idx < desiredIndex { + for name, val := range rp { + if val > idx && val <= desiredIndex { + rp[name] = val - 1 + } + } + } else { + for name, val := range rp { + if val < idx && val >= desiredIndex { + rp[name] = val + 1 + } + } + } + rp[target.Name] = desiredIndex + + updates.Add(&xmlapi.Config{ + Action: "move", + Xpath: util.AsXpath(path), + Where: where, + Destination: prevName, + Target: s.client.GetTarget(), + }) + } + + prevName = target.Name + return nil +} + +func (s *Service) moveDirectlyBefore(entries []*Entry, baseIndex int, ok bool, rp map[string]int, otherIndex int, position rule.Position, loc Location, vn version.Number, updates *xmlapi.MultiConfig) (int, bool, error) { + target := entries[len(entries)-1] + + baseIndex, ok = rp[target.Name] + if !ok { + return 0, false, fmt.Errorf("could not find rule %q for initial positioning", target.Name) + } + + otherIndex, ok = rp[*position.DirectlyBefore] + if !ok { + return 0, false, fmt.Errorf("could not find referenced rule %q", *position.DirectlyBefore) + } + + if baseIndex+1 != otherIndex { + path, err := loc.XpathWithEntryName(vn, target.Name) + if err != nil { + return 0, false, err + } + + for name, val := range rp { + switch { + case name == target.Name: + rp[name] = otherIndex + case val < baseIndex && val >= otherIndex: + rp[name] = val + 1 + } + } + + updates.Add(&xmlapi.Config{ + Action: "move", + Xpath: util.AsXpath(path), + Where: "before", + Destination: *position.DirectlyBefore, + Target: s.client.GetTarget(), + }) + + baseIndex = otherIndex + } + return baseIndex, ok, nil +} + +func (s *Service) moveDirectlyAfter(topDown bool, entries []*Entry, baseIndex int, ok bool, rp map[string]int, otherIndex int, position rule.Position, loc Location, vn version.Number, updates *xmlapi.MultiConfig) (bool, int, bool, int, error) { + topDown = true + target := entries[0] + + baseIndex, ok = rp[target.Name] + if !ok { + return false, 0, false, 0, fmt.Errorf("could not find rule %q for initial positioning", target.Name) + } + + otherIndex, ok = rp[*position.DirectlyAfter] + if !ok { + return false, 0, false, 0, fmt.Errorf("could not find referenced rule %q for initial positioning", *position.DirectlyAfter) + } + + if baseIndex != otherIndex+1 { + path, err := loc.XpathWithEntryName(vn, target.Name) + if err != nil { + return false, 0, false, 0, err + } + + for name, val := range rp { + switch { + case name == target.Name: + rp[name] = otherIndex + case val > baseIndex && val <= otherIndex: + rp[name] = otherIndex - 1 + } + } + + updates.Add(&xmlapi.Config{ + Action: "move", + Xpath: util.AsXpath(path), + Where: "after", + Destination: *position.DirectlyAfter, + Target: s.client.GetTarget(), + }) + + baseIndex = otherIndex + } + return topDown, baseIndex, ok, otherIndex, nil +} + +func (s *Service) moveSomewhereBefore(entries []*Entry, baseIndex int, ok bool, rp map[string]int, otherIndex int, position rule.Position, loc Location, vn version.Number, updates *xmlapi.MultiConfig) (int, bool, int, error) { + target := entries[len(entries)-1] + + baseIndex, ok = rp[target.Name] + if !ok { + return 0, false, 0, fmt.Errorf("could not find rule %q for initial positioning", target.Name) + } + + otherIndex, ok = rp[*position.SomewhereBefore] + if !ok { + return 0, false, 0, fmt.Errorf("could not find referenced rule %q", *position.SomewhereBefore) + } + + if baseIndex > otherIndex { + path, err := loc.XpathWithEntryName(vn, target.Name) + if err != nil { + return 0, false, 0, err + } + + for name, val := range rp { + switch { + case name == target.Name: + rp[name] = otherIndex + case val < baseIndex && val >= otherIndex: + rp[name] = val + 1 + } + } + + updates.Add(&xmlapi.Config{ + Action: "move", + Xpath: util.AsXpath(path), + Where: "before", + Destination: *position.SomewhereBefore, + Target: s.client.GetTarget(), + }) + + baseIndex = otherIndex + } + return baseIndex, ok, otherIndex, nil +} + +func (s *Service) moveSomewhereAfter(topDown bool, entries []*Entry, baseIndex int, ok bool, rp map[string]int, otherIndex int, position rule.Position, loc Location, vn version.Number, updates *xmlapi.MultiConfig) (bool, int, bool, int, error) { + topDown = true + target := entries[0] + + baseIndex, ok = rp[target.Name] + if !ok { + return false, 0, false, 0, fmt.Errorf("could not find rule %q for initial positioning", target.Name) + } + + otherIndex, ok = rp[*position.SomewhereAfter] + if !ok { + return false, 0, false, 0, fmt.Errorf("could not find referenced rule %q for initial positioning", *position.SomewhereAfter) + } + + if baseIndex < otherIndex { + path, err := loc.XpathWithEntryName(vn, target.Name) + if err != nil { + return false, 0, false, 0, err + } + + for name, val := range rp { + switch { + case name == target.Name: + rp[name] = otherIndex + case val > baseIndex && val <= otherIndex: + rp[name] = otherIndex - 1 + } + } + + updates.Add(&xmlapi.Config{ + Action: "move", + Xpath: util.AsXpath(path), + Where: "after", + Destination: *position.SomewhereAfter, + Target: s.client.GetTarget(), + }) + + baseIndex = otherIndex + } + return topDown, baseIndex, ok, otherIndex, nil +} + +func (s *Service) moveBottom(entries []*Entry, baseIndex int, ok bool, rp map[string]int, listing []*Entry, loc Location, vn version.Number, updates *xmlapi.MultiConfig) (int, bool, error) { + target := entries[len(entries)-1] + + baseIndex, ok = rp[target.Name] + if !ok { + return 0, false, fmt.Errorf("could not find rule %q for last positioning", target.Name) + } + + if baseIndex != len(listing)-1 { + path, err := loc.XpathWithEntryName(vn, target.Name) + if err != nil { + return 0, false, err + } + + for name, val := range rp { + switch { + case name == target.Name: + rp[name] = len(listing) - 1 + case val > baseIndex: + rp[name] = val - 1 + } + } + + // some versions of PAN-OS require that the destination always be set + var dst string + if !vn.Gte(util.FixedPanosVersionForMultiConfigMove) { + dst = "bottom" + } + + updates.Add(&xmlapi.Config{ + Action: "move", + Xpath: util.AsXpath(path), + Where: "bottom", + Destination: dst, + Target: s.client.GetTarget(), + }) + + baseIndex = len(listing) - 1 + } + return baseIndex, ok, nil +} + +func (s *Service) moveTop(topDown bool, entries []*Entry, baseIndex int, ok bool, rp map[string]int, loc Location, vn version.Number, updates *xmlapi.MultiConfig) (bool, int, bool, error) { + topDown = true + target := entries[0] + + baseIndex, ok = rp[target.Name] + if !ok { + return false, 0, false, fmt.Errorf("could not find rule %q for first positioning", target.Name) + } + + if baseIndex != 0 { + path, err := loc.XpathWithEntryName(vn, target.Name) + if err != nil { + return false, 0, false, err + } + + for name, val := range rp { + switch { + case name == entries[0].Name: + rp[name] = 0 + case val < baseIndex: + rp[name] = val + 1 + } + } + + // some versions of PAN-OS require that the destination always be set + var dst string + if !vn.Gte(util.FixedPanosVersionForMultiConfigMove) { + dst = "top" + } + + updates.Add(&xmlapi.Config{ + Action: "move", + Xpath: util.AsXpath(path), + Where: "top", + Destination: dst, + Target: s.client.GetTarget(), + }) + + baseIndex = 0 + } + return topDown, baseIndex, ok, nil +} + +// HitCount returns the hit count for the given rule. +func (s *Service) HitCount(ctx context.Context, loc Location, rules ...string) ([]util.HitCount, error) { + switch { + case loc.Vsys != nil: + cmd := &xmlapi.Op{ + Command: util.NewHitCountRequest(RuleType, loc.Vsys.Vsys, rules), + Target: s.client.GetTarget(), + } + var resp util.HitCountResponse + + if _, _, err := s.client.Communicate(ctx, cmd, false, &resp); err != nil { + return nil, err + } + + return resp.Results, nil + } + + return nil, fmt.Errorf("unsupported location") +} + +// SetAuditComment sets the given audit comment for the given rule. +func (s *Service) SetAuditComment(ctx context.Context, loc Location, name, comment string) error { + if name == "" { + return errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + + path, err := loc.XpathWithEntryName(vn, name) + if err != nil { + return err + } + + cmd := &xmlapi.Op{ + Command: audit.SetComment{ + Xpath: util.AsXpath(path), + Comment: comment, + }, + Target: s.client.GetTarget(), + } + + _, _, err = s.client.Communicate(ctx, cmd, false, nil) + return err +} + +// CurrentAuditComment gets any current uncommitted audit comment for the given rule. +func (s *Service) CurrentAuditComment(ctx context.Context, loc Location, name string) (string, error) { + if name == "" { + return "", errors.NameNotSpecifiedError + } + + vn := s.client.Versioning() + + path, err := loc.XpathWithEntryName(vn, name) + if err != nil { + return "", err + } + + cmd := &xmlapi.Op{ + Command: audit.GetComment{ + Xpath: util.AsXpath(path), + }, + Target: s.client.GetTarget(), + } + + var resp audit.UncommittedComment + if _, _, err = s.client.Communicate(ctx, cmd, false, &resp); err != nil { + return "", err + } + + return resp.Comment, nil +} + +// AuditCommentHistory returns a chunk of historical audit comment logs. +func (s *Service) AuditCommentHistory(ctx context.Context, loc Location, name, direction string, nlogs, skip int) ([]audit.Comment, error) { + if name == "" { + return nil, errors.NameNotSpecifiedError + } + + var err error + var base, vsysDg string + switch { + case loc.Vsys != nil: + vsysDg = loc.Vsys.Vsys + base = "rulebase" + case loc.Shared != nil: + vsysDg = "shared" + base = loc.Shared.Rulebase + case loc.DeviceGroup != nil: + vsysDg = loc.DeviceGroup.DeviceGroup + base = loc.DeviceGroup.Rulebase + } + + if vsysDg == "" || base == "" { + return nil, fmt.Errorf("unsupported location") + } + + query := strings.Join([]string{ + "(subtype eq audit-comment)", + fmt.Sprintf("(path contains '\\'%s\\'')", name), + fmt.Sprintf("(path contains '%s')", RuleType), + fmt.Sprintf("(path contains %s)", base), + fmt.Sprintf("(path contains '\\'%s\\'')", vsysDg), + }, " and ") + extras := url.Values{} + extras.Set("uniq", "yes") + + cmd := &xmlapi.Log{ + LogType: "config", + Query: query, + Direction: direction, + Nlogs: nlogs, + Skip: skip, + Extras: extras, + } + + var job util.JobResponse + if _, _, err = s.client.Communicate(ctx, cmd, false, &job); err != nil { + return nil, err + } + + var resp audit.CommentHistory + if _, err = s.client.WaitForLogs(ctx, job.Id, 1*time.Second, &resp); err != nil { + return nil, err + } + + if len(resp.Comments) != 0 { + if clock, err := s.client.Clock(ctx); err == nil { + for i := range resp.Comments { + resp.Comments[i].SetTime(clock) + } + } + } + + return resp.Comments, nil +} diff --git a/util/comparison.go b/util/comparison.go index fb350d1..fd1ccd5 100644 --- a/util/comparison.go +++ b/util/comparison.go @@ -71,6 +71,10 @@ func StringsMatch(a, b *string) bool { return *a == *b } +func StringsEqual(a, b string) bool { + return a == b +} + func BoolsMatch(a, b *bool) bool { if a == nil && b == nil { return true @@ -78,7 +82,7 @@ func BoolsMatch(a, b *bool) bool { return false } - return *a == *b + return *a == *b } func FloatsMatch(a, b *float64) bool { @@ -88,27 +92,37 @@ func FloatsMatch(a, b *float64) bool { return false } - return *a == *b + return *a == *b +} + +func IntsMatch(a, b *int) bool { + if a == nil && b == nil { + return true + } else if a == nil || b == nil { + return false + } + + return *a == *b } -func IntsMatch(a, b *int64) bool { +func Ints64Match(a, b *int64) bool { if a == nil && b == nil { return true } else if a == nil || b == nil { return false } - return *a == *b + return *a == *b } func AnysMatch(a, b any) bool { - if a == nil && b == nil { - return true - } + if a == nil && b == nil { + return true + } - if a == nil || b == nil { - return false - } + if a == nil || b == nil { + return false + } - return true + return true } diff --git a/util/location.go b/util/location.go new file mode 100644 index 0000000..af6278d --- /dev/null +++ b/util/location.go @@ -0,0 +1,7 @@ +package util + +import "github.com/PaloAltoNetworks/pango/version" + +type ILocation interface { + XpathPrefix(version.Number) ([]string, error) +} diff --git a/util/pangoclient.go b/util/pangoclient.go index 5934d5f..eeff7c3 100644 --- a/util/pangoclient.go +++ b/util/pangoclient.go @@ -19,6 +19,7 @@ type PangoClient interface { GetTarget() string IsPanorama() (bool, error) IsFirewall() (bool, error) + Clock(context.Context) (time.Time, error) // Local inspection mode functions. ReadFromConfig(context.Context, []string, bool, any) ([]byte, error) diff --git a/util/util.go b/util/util.go index 64c8384..b7e0b73 100644 --- a/util/util.go +++ b/util/util.go @@ -8,8 +8,12 @@ import ( "fmt" "regexp" "strings" + + "github.com/PaloAltoNetworks/pango/version" ) +var FixedPanosVersionForMultiConfigMove = version.Number{99, 99, 99, ""} + // VsysEntryType defines an entry config node with vsys entries underneath. type VsysEntryType struct { Entries []VsysEntry `xml:"entry"` @@ -59,19 +63,37 @@ func MapToVsysEnt(e map[string][]string) *VsysEntryType { } // YesNo returns "yes" on true, "no" on false. -func YesNo(v bool) string { - if v { - return "yes" +func YesNo(val *bool, defaultVal *bool) *string { + if val == nil && defaultVal == nil { + return nil + } + + result := "no" + if val != nil { + if *val { + result = "yes" + } + } else if *defaultVal { + result = "yes" } - return "no" + return &result } // AsBool returns true on yes, else false. -func AsBool(val string) bool { - if val == "yes" { - return true +func AsBool(val *string, defaultVal *string) *bool { + if val == nil && defaultVal == nil { + return nil } - return false + + result := false + if val != nil { + if *val == "yes" { + result = true + } + } else if *defaultVal == "yes" { + result = true + } + return &result } // AsXpath makes an xpath out of the given interface. @@ -110,7 +132,7 @@ func AsEntryXpath(vals []string) string { // AsUuidXpath returns an xpath segment as a UUID location. func AsUuidXpath(v string) string { - return fmt.Sprintf("entry[@uuid='%s']", v) + return fmt.Sprintf("entry[@uuid='%s']", v) } // AsMemberXpath returns the given values as a member xpath segment. diff --git a/xmlapi/multiconfig.go b/xmlapi/multiconfig.go index 24a4684..bac943e 100644 --- a/xmlapi/multiconfig.go +++ b/xmlapi/multiconfig.go @@ -4,6 +4,8 @@ import ( "encoding/xml" "fmt" "strings" + + "github.com/PaloAltoNetworks/pango/errors" ) // Returns a new struct for performing multi-config operations with. @@ -72,7 +74,7 @@ func NewMultiConfigResponse(text []byte) (*MultiConfigResponse, error) { return nil, fmt.Errorf("no data received in the multi-config response") } - var ans MultiConfigResponse + ans := MultiConfigResponse{raw: text} err := xml.Unmarshal(text, &ans) return &ans, err @@ -85,6 +87,8 @@ type MultiConfigResponse struct { Status string `xml:"status,attr"` Code int `xml:"code,attr"` Results []MultiConfigResponseElement `xml:"response"` + + raw []byte `xml:"-"` } // Ok returns if there was an error or not. @@ -95,7 +99,10 @@ func (m *MultiConfigResponse) Ok() bool { // Error returns the error if there was one. func (m *MultiConfigResponse) Error() string { if len(m.Results) == 0 { - return "" + if err := errors.Parse(m.raw); err != nil { + return err.Error() + } + return "unknown multi-config error format" } r := m.Results[len(m.Results)-1]