diff --git a/pango.go b/client.go similarity index 77% rename from pango.go rename to client.go index af24cb96..7c73012f 100644 --- a/pango.go +++ b/client.go @@ -1,134 +1,3 @@ -/* -Package pango is a golang cross version mechanism for interacting with Palo Alto -Networks devices (including physical and virtualized Next-generation Firewalls -and Panorama). Versioning support is in place for PANOS 6.1 to 8.0. - -To start, create a client connection with the desired parameters and then -initialize the connection: - - package main - - import ( - "log" - "github.com/PaloAltoNetworks/pango" - ) - - func main() { - var err error - c := pango.Firewall{Client: pango.Client{ - Hostname: "127.0.0.1", - Username: "admin", - Password: "admin", - Logging: pango.LogAction | pango.LogOp, - }} - if err = c.Initialize(); err != nil { - log.Printf("Failed to initialize client: %s", err) - return - } - log.Printf("Initialize ok") - } - -Initializing the connection creates the API key (if it was not already -specified), then performs "show system info" to get the PANOS version. Once -the firewall client is created, you can query and configure the Palo -Alto Networks device from the functions inside the various namespaces of the -client connection. Namespaces correspond to the various configuration areas -available in the GUI. For example: - - err = c.Network.EthernetInterface.Set(...) - myPolicies, err := c.Policies.Security.GetList(...) - -Generally speaking, there are the following functions inside each namespace: - - * GetList - * ShowList - * Get - * Show - * Set - * Edit - * Delete - -These functions correspond with PANOS Get, Show, Set, Edit, and -Delete API calls. Get(), Set(), and Edit() take and return normalized, -version independent objects. These version safe objects are typically named -Entry, which corresponds to how the object is placed in the PANOS XPATH. - -Some Entry objects have a special function, Defaults(). Invoking this -function will initialize the object with some default values. Each Entry -that implements Defaults() calls out in its documentation what parameters -are affected by this, and what the defaults are. - -For any version safe object, attempting to configure a parameter that your -PANOS doesn't support will be safely ignored in the resultant XML sent to the -firewall / Panorama. - -Using Edit Functions - -The PANOS XML API Edit command can be used to both create as well as update -existing config, however it can also truncate config for the given XPATH. Due -to this, if you want to use Edit(), you need to make sure that you perform -either a Get() or a Show() first, make your modification, then invoke -Edit() using that object. If you don't do this, you will truncate any sub -config. - -To learn more about PANOS XML API, please refer to the Palo Alto Netowrks -API documentation. - -Examples - -The following program will create ethernet1/7 as a DHCP interface and import -it into vsys1 if it isn't already present: - - package main - - import ( - "log" - "github.com/PaloAltoNetworks/pango" - "github.com/PaloAltoNetworks/pango/netw/eth" - ) - - func main() { - var err error - - c := &pango.Firewall{Client: pango.Client{ - Hostname: "127.0.0.1", - Username: "admin", - Password: "admin", - Logging: pango.LogAction | pango.LogOp, - }} - if err = c.Initialize(); err != nil { - log.Printf("Failed to initialize client: %s", err) - return - } - - e := eth.Entry{ - Name: "ethernet1/7", - Mode: "layer3", - EnableDhcp: true, - CreateDhcpDefaultRoute: true, - } - - interfaces, err := c.Network.EthernetInterface.GetList() - if err != nil { - log.Printf("Failed to get data interfaces: %s", err) - return - } - for i := range interfaces { - if e.Name == interfaces[i] { - log.Printf("%s already exists", e.Name) - return - } - } - - err = c.Network.EthernetInterface.Set("vsys1", e) - if err != nil { - log.Printf("Failed to create %s: %s", e.Name, err) - return - } - log.Printf("Created %s ok", e.Name) - } - -*/ package pango import ( @@ -143,20 +12,12 @@ import ( "github.com/PaloAltoNetworks/pango/version" "github.com/PaloAltoNetworks/pango/util" - - // Various namespace imports. - "github.com/PaloAltoNetworks/pango/netw" - "github.com/PaloAltoNetworks/pango/dev" - "github.com/PaloAltoNetworks/pango/poli" - "github.com/PaloAltoNetworks/pango/objs" - "github.com/PaloAltoNetworks/pango/licen" - "github.com/PaloAltoNetworks/pango/userid" ) // 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 PANOS XML API. The API key being used for +// 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. @@ -183,7 +44,7 @@ const ( ) // Client is a generic connector struct. It provides wrapper functions for -// invoking the various PANOS XPath API methods. After creating the client, +// invoking the various PAN-OS XPath API methods. After creating the client, // invoke Initialize() to prepare it for use. type Client struct { // Connection properties. @@ -213,44 +74,6 @@ type Client struct { ri int } -// Firewall is a firewall specific client, providing version safe functions -// for the PANOS Xpath API methods. After creating the object, invoke -// Initialize() to prepare it for use. -// -// It has the following namespaces: -// * Network -// * Device -// * Policies -// * Objects -// * Licensing -// * UserId -type Firewall struct { - Client - - // Namespaces - Network *netw.Netw - Device *dev.Dev - Policies *poli.Poli - Objects *objs.Objs - Licensing *licen.Licen - UserId *userid.UserId -} - -// Panorama is a panorama specific client, providing version safe functions -// for the PANOS Xpath API methods. After creating the object, invoke -// Initialize() to prepare it for use. -// -// It has the following namespaces: -// * Licensing -// * UserId -type Panorama struct { - Client - - // Namespaces - Licensing *licen.Licen - UserId *userid.UserId -} - // 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. @@ -282,7 +105,7 @@ func (c *Client) Versioning() version.Number { // 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 PANOS version. The full results are saved into the +// 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: @@ -309,66 +132,6 @@ func (c *Client) Initialize() error { return nil } -// Initialize does some initial setup of the Firewall connection, retrieves -// the API key if it was not already present, then performs "show system -// info" to get the PANOS 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 *Firewall) 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" - } - c.initNamespaces() - - return nil -} - -// Initialize does some initial setup of the Panorama connection, retrieves -// the API key if it was not already present, then performs "show system -// info" to get the PANOS 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 *Panorama) 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" - } - c.initNamespaces() - - return nil -} - // RetrieveApiKey retrieves the API key, which will require that both the // username and password are defined. // @@ -695,6 +458,8 @@ func (c *Client) WaitForJob(id uint, 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 { @@ -703,26 +468,56 @@ func (c *Client) WaitForJob(id uint, resp interface{}) error { } req := op_req{Id: id} - ans := util.BasicJob{} - for ans.Progress != 100 { + 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{} + // Get current percent complete. data, err = c.Op(req, "", nil, &ans) 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 + } + } } + // Check the results for a failed commit. if ans.Result == "FAIL" { if len(ans.Details) > 0 { return fmt.Errorf(ans.Details[0]) } 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") } if resp == nil { @@ -732,48 +527,6 @@ func (c *Client) WaitForJob(id uint, resp interface{}) error { return xml.Unmarshal(data, resp) } -// Commit performs a Firewall commit. -// -// Param desc is the optional commit description message you want associated -// with the commit. -// -// Params dan and pao are advanced options for doing partial commits. Setting -// param dan to false excludes the Device and Network configuration, while -// setting param pao to false excludes the Policy and Object configuration. -// -// Param force is if you want to force a commit even if no changes are -// required. -// -// Param sync should be true if you want this function to block until the -// commit job completes. -// -// Commits result in a job being submitted to the backend. The job ID and -// if an error was encountered or not are returned from this function. -func (c *Firewall) Commit(desc string, dan, pao, force, sync bool) (uint, error) { - c.LogAction("(commit) %q", desc) - - req := fwCommit{Description: desc} - if !dan || !pao { - req.Partial = &fwCommitPartial{} - if !dan { - req.Partial.Dan = "excluded" - } - if !pao { - req.Partial.Pao = "excluded" - } - } - if force { - req.Force = "" - } - - job, _, err := c.CommitConfig(req, "", nil) - if err != nil || !sync || job == 0 { - return job, err - } - - return job, c.WaitForJob(job, nil) -} - // LogAction writes a log message for SET/DELETE operations if LogAction is set. func (c *Client) LogAction(msg string, i ...interface{}) { if c.Logging & LogAction == LogAction { @@ -802,7 +555,7 @@ func (c *Client) LogUid(msg string, i ...interface{}) { } } -// Communicate sends the given data to PANOS. +// 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. @@ -1078,7 +831,7 @@ func (c *Client) Uid(cmd interface{}, vsys string, extras, ans interface{}) ([]b return c.Communicate(data, ans) } -// CommitConfig performs PANOS commits. This is the underlying function +// CommitConfig performs PAN-OS commits. This is the underlying function // invoked by Firewall.Commit() and Panorama.Commit(). // // The cmd param can be either a properly formatted XML string or a struct @@ -1221,34 +974,6 @@ func (c *Client) initSystemInfo() error { return nil } -func (c *Firewall) initNamespaces() { - c.Network = &netw.Netw{} - c.Network.Initialize(c) - - c.Device = &dev.Dev{} - c.Device.Initialize(c) - - c.Policies = &poli.Poli{} - c.Policies.Initialize(c) - - c.Objects = &objs.Objs{} - c.Objects.Initialize(c) - - c.Licensing = &licen.Licen{} - c.Licensing.Initialize(c) - - c.UserId = &userid.UserId{} - c.UserId.Initialize(c) -} - -func (c *Panorama) initNamespaces() { - c.Licensing = &licen.Licen{} - c.Licensing.Initialize(c) - - c.UserId = &userid.UserId{} - c.UserId.Initialize(c) -} - func (c *Client) typeConfig(action string, data url.Values, extras, ans interface{}) ([]byte, error) { var err error @@ -1459,7 +1184,7 @@ func (e panosStatus) codeError() string { } } -// panosErrorResponseWithLine is one of a few known error formats that PANOS +// panosErrorResponseWithLine is one of a few known error formats that PAN-OS // outputs. This has to be split from the other error struct because the // the XML unmarshaler doesn't like a single struct to have overlapping // definitions (the msg>line part). @@ -1479,7 +1204,7 @@ func (e panosErrorResponseWithLine) Error() string { } -// panosErrorResponseWithoutLine is one of a few known error formats that PANOS +// panosErrorResponseWithoutLine is one of a few known error formats that PAN-OS // outputs. It checks two locations that the error could be, and returns the // one that was discovered in its Error(). type panosErrorResponseWithoutLine struct { @@ -1511,15 +1236,3 @@ type configLocks struct { type commitLocks struct { Locks []util.Lock `xml:"result>commit-locks>entry"` } - -type fwCommit struct { - XMLName xml.Name `xml:"commit"` - Description string `xml:"description,omitempty"` - Partial *fwCommitPartial `xml:"partial"` - Force interface{} `xml:"force"` -} - -type fwCommitPartial struct { - Dan string `xml:"device-and-network,omitempty"` - Pao string `xml:"policy-and-objects,omitempty"` -} diff --git a/pango_test.go b/client_test.go similarity index 99% rename from pango_test.go rename to client_test.go index e5465e00..a15a1504 100644 --- a/pango_test.go +++ b/client_test.go @@ -161,7 +161,7 @@ func TestLogUidEnabled(t *testing.T) { } func TestRetrieveApiKey(t *testing.T) { - c := &Firewall{} + c := &Client{} c.rb = [][]byte{ []byte(testdata.ApiKeyXml), } diff --git a/connect.go b/connect.go new file mode 100644 index 00000000..d0ab5051 --- /dev/null +++ b/connect.go @@ -0,0 +1,36 @@ +package pango + +/* +Connect opens a connection to the PAN-OS client, then uses the "model" info +to return a pointer to either a Firewall or Panorama struct. + +The Initialize function is invoked as part of this discovery, so there is no +need to Initialize() the Client connection prior to invoking this. +*/ +func Connect(c Client) (interface{}, error) { + var err error + + logg := c.Logging + c.Logging = LogQuiet + + if err = c.Initialize(); err != nil { + return nil, err + } + + model := c.SystemInfo["model"] + if model == "Panorama" || model[:2] == "M-" { + pano := &Panorama{Client: c} + pano.Logging = logg + if err = pano.Initialize(); err != nil { + return nil, err + } + return pano, nil + } else { + fw := &Firewall{Client: c} + fw.Logging = logg + if err = fw.Initialize(); err != nil { + return nil, err + } + return fw, nil + } +} diff --git a/dev/general/general.go b/dev/general/general.go index ec9da3a6..dcfb6cc3 100644 --- a/dev/general/general.go +++ b/dev/general/general.go @@ -34,6 +34,9 @@ type Config struct { Domain string UpdateServer string VerifyUpdateServer bool + LoginBanner string + PanoramaPrimary string + PanoramaSecondary string DnsPrimary string DnsSecondary string NtpPrimaryAddress string @@ -85,6 +88,18 @@ func (o *Config) Merge(s Config) { o.VerifyUpdateServer = s.VerifyUpdateServer + if s.LoginBanner != "" { + o.LoginBanner = s.LoginBanner + } + + if s.PanoramaPrimary != "" { + o.PanoramaPrimary = s.PanoramaPrimary + } + + if s.PanoramaSecondary != "" { + o.PanoramaSecondary = s.PanoramaSecondary + } + if s.DnsPrimary != "" { o.DnsPrimary = s.DnsPrimary } @@ -228,6 +243,9 @@ func (o *container_v1) Normalize() Config { Domain: o.Answer.Domain, UpdateServer: o.Answer.UpdateServer, VerifyUpdateServer: util.AsBool(o.Answer.VerifyUpdateServer), + LoginBanner: o.Answer.LoginBanner, + PanoramaPrimary: o.Answer.PanoramaPrimary, + PanoramaSecondary: o.Answer.PanoramaSecondary, } if o.Answer.Dns != nil { ans.DnsPrimary = o.Answer.Dns.Primary @@ -319,21 +337,12 @@ func (o *container_v1) Normalize() Config { if o.Answer.LogLink != nil { ans.raw["ll"] = util.CleanRawXml(o.Answer.LogLink.Text) } - if o.Answer.LoginBanner != nil { - ans.raw["lb"] = util.CleanRawXml(o.Answer.LoginBanner.Text) - } if o.Answer.MotdAndBanner != nil { ans.raw["mab"] = util.CleanRawXml(o.Answer.MotdAndBanner.Text) } if o.Answer.Mtu != nil { ans.raw["mtu"] = util.CleanRawXml(o.Answer.Mtu.Text) } - if o.Answer.PanoramaServer != nil { - ans.raw["ps"] = util.CleanRawXml(o.Answer.PanoramaServer.Text) - } - if o.Answer.PanoramaServer2 != nil { - ans.raw["ps2"] = util.CleanRawXml(o.Answer.PanoramaServer2.Text) - } if o.Answer.PermittedIp != nil { ans.raw["pi"] = util.CleanRawXml(o.Answer.PermittedIp.Text) } @@ -383,13 +392,16 @@ func (o *container_v1) Normalize() Config { type config_v1 struct { XMLName xml.Name `xml:"system"` Hostname string `xml:"hostname"` - IpAddress string `xml:"ip-address"` - Netmask string `xml:"netmask"` - Gateway string `xml:"default-gateway"` + IpAddress string `xml:"ip-address,omitempty"` + Netmask string `xml:"netmask,omitempty"` + Gateway string `xml:"default-gateway,omitempty"` Timezone string `xml:"timezone"` Domain string `xml:"domain,omitempty"` UpdateServer string `xml:"update-server,omitempty"` VerifyUpdateServer string `xml:"server-verification"` + LoginBanner string `xml:"login-banner,omitempty"` + PanoramaPrimary string `xml:"panorama-server,omitempty"` + PanoramaSecondary string `xml:"panorama-server-2,omitempty"` Dns *deviceDns `xml:"dns-setting"` Ntp *deviceNtp `xml:"ntp-servers"` AckLoginBanner *util.RawXml `xml:"ack-login-banner"` @@ -406,11 +418,8 @@ type config_v1 struct { Locale *util.RawXml `xml:"locale"` LogExportSchedule *util.RawXml `xml:"log-export-schedule"` LogLink *util.RawXml `xml:"log-link"` - LoginBanner *util.RawXml `xml:"login-banner"` MotdAndBanner *util.RawXml `xml:"motd-and-banner"` Mtu *util.RawXml `xml:"mtu"` - PanoramaServer *util.RawXml `xml:"panorama-server"` - PanoramaServer2 *util.RawXml `xml:"panorama-server-2"` PermittedIp *util.RawXml `xml:"permitted-ip"` Route *util.RawXml `xml:"route"` SecureProxyPassword *util.RawXml `xml:"secure-proxy-password"` @@ -427,8 +436,8 @@ type config_v1 struct { } type deviceDns struct { - Primary string `xml:"servers>primary"` - Secondary string `xml:"servers>secondary"` + Primary string `xml:"servers>primary,omitempty"` + Secondary string `xml:"servers>secondary,omitempty"` } type deviceNtp struct { @@ -471,6 +480,9 @@ func specify_v1(c Config) interface{} { Domain: c.Domain, UpdateServer: c.UpdateServer, VerifyUpdateServer: util.YesNo(c.VerifyUpdateServer), + LoginBanner: c.LoginBanner, + PanoramaPrimary: c.PanoramaPrimary, + PanoramaSecondary: c.PanoramaSecondary, } if c.DnsPrimary != "" || c.DnsSecondary != "" { ans.Dns = &deviceDns{ @@ -570,21 +582,12 @@ func specify_v1(c Config) interface{} { if text, present := c.raw["ll"]; present { ans.LogLink = &util.RawXml{text} } - if text, present := c.raw["lb"]; present { - ans.LoginBanner = &util.RawXml{text} - } if text, present := c.raw["mab"]; present { ans.MotdAndBanner = &util.RawXml{text} } if text, present := c.raw["mtu"]; present { ans.Mtu = &util.RawXml{text} } - if text, present := c.raw["ps"]; present { - ans.PanoramaServer = &util.RawXml{text} - } - if text, present := c.raw["ps2"]; present { - ans.PanoramaServer2 = &util.RawXml{text} - } if text, present := c.raw["pi"]; present { ans.PermittedIp = &util.RawXml{text} } diff --git a/dev/general/general_test.go b/dev/general/general_test.go index 6b1cd82f..f664f2c7 100644 --- a/dev/general/general_test.go +++ b/dev/general/general_test.go @@ -20,6 +20,9 @@ func TestNormalization(t *testing.T) { Gateway: "10.1.1.1", Timezone: "US/Pacific", Domain: "example.com", + LoginBanner: "this is my banner", + PanoramaPrimary: "pano1", + PanoramaSecondary: "pano2", DnsPrimary: "10.1.1.1", DnsSecondary: "10.1.1.50", NtpPrimaryAddress: "10.1.1.1", @@ -44,11 +47,8 @@ func TestNormalization(t *testing.T) { "locale": "my locale", "les": "log export schedule", "ll": "log link", - "lb": "login banner", "mab": "motd and banner", "mtu": "mtu", - "ps": "panorama server", - "ps2": "panorama server 2", "pi": "permitted ip", "route": "route", "sppassword": "secure proxy password", @@ -70,6 +70,9 @@ func TestNormalization(t *testing.T) { Timezone: "UTC", UpdateServer: "updates.paloaltonetworks.com", VerifyUpdateServer: true, + LoginBanner: "This is a secure system", + PanoramaPrimary: "192.168.55.2", + PanoramaSecondary: "192.168.55.3", DnsPrimary: "10.2.1.5", NtpPrimaryAddress: "10.2.5.7", NtpPrimaryAuthType: SymmetricKeyAuth, diff --git a/doc.go b/doc.go new file mode 100644 index 00000000..872409cf --- /dev/null +++ b/doc.go @@ -0,0 +1,132 @@ +/* +Package pango is a golang cross version mechanism for interacting with Palo Alto +Networks devices (including physical and virtualized Next-generation Firewalls +and Panorama). Versioning support is in place for PAN-OS 6.1 to 8.1. + +To start, create a client connection with the desired parameters and then +initialize the connection: + + package main + + import ( + "log" + "github.com/PaloAltoNetworks/pango" + ) + + func main() { + var err error + c := pango.Firewall{Client: pango.Client{ + Hostname: "127.0.0.1", + Username: "admin", + Password: "admin", + Logging: pango.LogAction | pango.LogOp, + }} + if err = c.Initialize(); err != nil { + log.Printf("Failed to initialize client: %s", err) + return + } + log.Printf("Initialize ok") + } + +Initializing the connection creates the API key (if it was not already +specified), then performs "show system info" to get the PAN-OS version. Once +the firewall client is created, you can query and configure the Palo +Alto Networks device from the functions inside the various namespaces of the +client connection. Namespaces correspond to the various configuration areas +available in the GUI. For example: + + err = c.Network.EthernetInterface.Set(...) + myPolicies, err := c.Policies.Security.GetList(...) + +Generally speaking, there are the following functions inside each namespace: + + * GetList + * ShowList + * Get + * Show + * Set + * Edit + * Delete + +These functions correspond with PAN-OS Get, Show, Set, Edit, and +Delete API calls. Get(), Set(), and Edit() take and return normalized, +version independent objects. These version safe objects are typically named +Entry, which corresponds to how the object is placed in the PAN-OS XPATH. + +Some Entry objects have a special function, Defaults(). Invoking this +function will initialize the object with some default values. Each Entry +that implements Defaults() calls out in its documentation what parameters +are affected by this, and what the defaults are. + +For any version safe object, attempting to configure a parameter that your +PAN-OS doesn't support will be safely ignored in the resultant XML sent to the +firewall / Panorama. + +Using Edit Functions + +The PAN-OS XML API Edit command can be used to both create as well as update +existing config, however it can also truncate config for the given XPATH. Due +to this, if you want to use Edit(), you need to make sure that you perform +either a Get() or a Show() first, make your modification, then invoke +Edit() using that object. If you don't do this, you will truncate any sub +config. + +To learn more about PAN-OS XML API, please refer to the Palo Alto Netowrks +API documentation. + +Examples + +The following program will create ethernet1/7 as a DHCP interface and import +it into vsys1 if it isn't already present: + + package main + + import ( + "log" + "github.com/PaloAltoNetworks/pango" + "github.com/PaloAltoNetworks/pango/netw/eth" + ) + + func main() { + var err error + + c := &pango.Firewall{Client: pango.Client{ + Hostname: "127.0.0.1", + Username: "admin", + Password: "admin", + Logging: pango.LogAction | pango.LogOp, + }} + if err = c.Initialize(); err != nil { + log.Printf("Failed to initialize client: %s", err) + return + } + + e := eth.Entry{ + Name: "ethernet1/7", + Mode: "layer3", + EnableDhcp: true, + CreateDhcpDefaultRoute: true, + } + + interfaces, err := c.Network.EthernetInterface.GetList() + if err != nil { + log.Printf("Failed to get data interfaces: %s", err) + return + } + for i := range interfaces { + if e.Name == interfaces[i] { + log.Printf("%s already exists", e.Name) + return + } + } + + err = c.Network.EthernetInterface.Set("vsys1", e) + if err != nil { + log.Printf("Failed to create %s: %s", e.Name, err) + return + } + log.Printf("Created %s ok", e.Name) + } + +*/ +package pango diff --git a/fw.go b/fw.go new file mode 100644 index 00000000..13328b90 --- /dev/null +++ b/fw.go @@ -0,0 +1,145 @@ +package pango + +import ( + "encoding/xml" + + // Various namespace imports. + "github.com/PaloAltoNetworks/pango/netw" + "github.com/PaloAltoNetworks/pango/dev" + "github.com/PaloAltoNetworks/pango/poli" + "github.com/PaloAltoNetworks/pango/objs" + "github.com/PaloAltoNetworks/pango/licen" + "github.com/PaloAltoNetworks/pango/userid" +) + + +// Firewall is a firewall specific client, providing version safe functions +// for the PAN-OS Xpath API methods. After creating the object, invoke +// Initialize() to prepare it for use. +// +// It has the following namespaces: +// * Network +// * Device +// * Policies +// * Objects +// * Licensing +// * UserId +type Firewall struct { + Client + + // Namespaces + Network *netw.Netw + Device *dev.Dev + Policies *poli.Poli + Objects *objs.FwObjs + Licensing *licen.Licen + UserId *userid.UserId +} + +// Initialize does some initial setup of the Firewall 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 *Firewall) 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" + } + c.initNamespaces() + + return nil +} + +// Commit performs a Firewall commit. +// +// Param desc is the optional commit description message you want associated +// with the commit. +// +// Params dan and pao are advanced options for doing partial commits. Setting +// param dan to false excludes the Device and Network configuration, while +// setting param pao to false excludes the Policy and Object configuration. +// +// Param force is if you want to force a commit even if no changes are +// required. +// +// Param sync should be true if you want this function to block until the +// commit job completes. +// +// Commits result in a job being submitted to the backend. The job ID and +// if an error was encountered or not are returned from this function. +func (c *Firewall) Commit(desc string, dan, pao, force, sync bool) (uint, error) { + c.LogAction("(commit) %q", desc) + + req := fwCommit{Description: desc} + if !dan || !pao { + req.Partial = &fwCommitPartial{} + if !dan { + req.Partial.Dan = "excluded" + } + if !pao { + req.Partial.Pao = "excluded" + } + } + if force { + req.Force = "" + } + + job, _, err := c.CommitConfig(req, "", nil) + if err != nil || !sync || job == 0 { + return job, err + } + + return job, c.WaitForJob(job, nil) +} + +/** Private functions **/ + +func (c *Firewall) initNamespaces() { + c.Network = &netw.Netw{} + c.Network.Initialize(c) + + c.Device = &dev.Dev{} + c.Device.Initialize(c) + + c.Policies = &poli.Poli{} + c.Policies.Initialize(c) + + c.Objects = &objs.FwObjs{} + c.Objects.Initialize(c) + + c.Licensing = &licen.Licen{} + c.Licensing.Initialize(c) + + c.UserId = &userid.UserId{} + c.UserId.Initialize(c) +} + +/** Internal structs / functions **/ + +type fwCommit struct { + XMLName xml.Name `xml:"commit"` + Description string `xml:"description,omitempty"` + Partial *fwCommitPartial `xml:"partial"` + Force interface{} `xml:"force"` +} + +type fwCommitPartial struct { + Dan string `xml:"device-and-network,omitempty"` + Pao string `xml:"policy-and-objects,omitempty"` +} diff --git a/objs/addr/doc.go b/objs/addr/doc.go new file mode 100644 index 00000000..d6a2f76f --- /dev/null +++ b/objs/addr/doc.go @@ -0,0 +1,6 @@ +/* +Package addr is the ngfw.Objects.Address namespace. + +Normalized object: Entry +*/ +package addr diff --git a/objs/addr/entry.go b/objs/addr/entry.go new file mode 100644 index 00000000..01bbbae6 --- /dev/null +++ b/objs/addr/entry.go @@ -0,0 +1,97 @@ +package addr + +import ( + "encoding/xml" + + "github.com/PaloAltoNetworks/pango/util" +) + +// Constants for Entry.Type field. +const ( + IpNetmask string = "ip-netmask" + IpRange string = "ip-range" + Fqdn string = "fqdn" +) + +// Entry is a normalized, version independent representation of an address +// object. +type Entry struct { + Name string + Value string + Type string + Description string + Tags []string +} + +// Copy copies the information from source Entry `s` to this object. As the +// Name field relates to the XPATH of this object, this field is not copied. +func (o *Entry) Copy(s Entry) { + o.Value = s.Value + o.Type = s.Type + o.Description = s.Description + o.Tags = s.Tags +} + +/** Structs / functions for normalization. **/ + +type normalizer interface { + Normalize() Entry +} + +type container_v1 struct { + Answer entry_v1 `xml:"result>entry"` +} + +func (o *container_v1) Normalize() Entry { + ans := Entry{ + Name: o.Answer.Name, + Description: o.Answer.Description, + Tags: util.MemToStr(o.Answer.Tags), + } + switch { + case o.Answer.IpNetmask != nil: + ans.Type = IpNetmask + ans.Value = o.Answer.IpNetmask.Value + case o.Answer.IpRange != nil: + ans.Type = IpRange + ans.Value = o.Answer.IpRange.Value + case o.Answer.Fqdn != nil: + ans.Type = Fqdn + ans.Value = o.Answer.Fqdn.Value + } + + return ans +} + +type entry_v1 struct { + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + IpNetmask *valType `xml:"ip-netmask"` + IpRange *valType `xml:"ip-range"` + Fqdn *valType `xml:"fqdn"` + Description string `xml:"description"` + Tags *util.Member `xml:"tag"` +} + +type valType struct { + Value string `xml:",chardata"` +} + +func specify_v1(e Entry) interface{} { + ans := entry_v1{ + Name: e.Name, + Description: e.Description, + Tags: util.StrToMem(e.Tags), + } + vt := &valType{e.Value} + switch e.Type { + case IpNetmask: + ans.IpNetmask = vt + case IpRange: + ans.IpRange = vt + case Fqdn: + ans.Fqdn = vt + } + + return ans +} diff --git a/objs/addr/addr.go b/objs/addr/fw.go similarity index 52% rename from objs/addr/addr.go rename to objs/addr/fw.go index 1cde7a88..02a3a163 100644 --- a/objs/addr/addr.go +++ b/objs/addr/fw.go @@ -1,6 +1,3 @@ -// Package addr is the client.Objects.Address namespace. -// -// Normalized object: Entry package addr import ( @@ -10,70 +7,44 @@ import ( "github.com/PaloAltoNetworks/pango/util" ) -// Constants for Entry.Type field. -const ( - IpNetmask string = "ip-netmask" - IpRange string = "ip-range" - Fqdn string = "fqdn" -) - -// Entry is a normalized, version independent representation of an address -// object. -type Entry struct { - Name string - Value string - Type string - Description string - Tags []string -} - -// Copy copies the information from source Entry `s` to this object. As the -// Name field relates to the XPATH of this object, this field is not copied. -func (o *Entry) Copy(s Entry) { - o.Value = s.Value - o.Type = s.Type - o.Description = s.Description - o.Tags = s.Tags -} - -// Addr is a namespace struct, included as part of pango.Client. -type Addr struct { +// FwAddr is a namespace struct, included as part of pango.Firewall. +type FwAddr struct { con util.XapiClient } // Initialize is invoked when Initialize on the pango.Client is called. -func (c *Addr) Initialize(con util.XapiClient) { +func (c *FwAddr) Initialize(con util.XapiClient) { c.con = con } // GetList performs GET to retrieve a list of address objects. -func (c *Addr) GetList(vsys string) ([]string, error) { +func (c *FwAddr) GetList(vsys string) ([]string, error) { c.con.LogQuery("(get) list of address objects") path := c.xpath(vsys, nil) return c.con.EntryListUsing(c.con.Get, path[:len(path) - 1]) } // ShowList performs SHOW to retrieve a list of address objects. -func (c *Addr) ShowList(vsys string) ([]string, error) { +func (c *FwAddr) ShowList(vsys string) ([]string, error) { c.con.LogQuery("(show) list of address objects") path := c.xpath(vsys, nil) return c.con.EntryListUsing(c.con.Show, path[:len(path) - 1]) } // Get performs GET to retrieve information for the given address object. -func (c *Addr) Get(vsys, name string) (Entry, error) { +func (c *FwAddr) Get(vsys, name string) (Entry, error) { c.con.LogQuery("(get) address object %q", name) return c.details(c.con.Get, vsys, name) } // Get performs SHOW to retrieve information for the given address object. -func (c *Addr) Show(vsys, name string) (Entry, error) { +func (c *FwAddr) Show(vsys, name string) (Entry, error) { c.con.LogQuery("(show) address object %q", name) return c.details(c.con.Show, vsys, name) } // Set performs SET to create / update one or more address objects. -func (c *Addr) Set(vsys string, e ...Entry) error { +func (c *FwAddr) Set(vsys string, e ...Entry) error { var err error if len(e) == 0 { @@ -105,7 +76,7 @@ func (c *Addr) Set(vsys string, e ...Entry) error { } // Edit performs EDIT to create / update an address object. -func (c *Addr) Edit(vsys string, e Entry) error { +func (c *FwAddr) Edit(vsys string, e Entry) error { var err error _, fn := c.versioning() @@ -123,7 +94,7 @@ func (c *Addr) Edit(vsys string, e Entry) error { // Delete removes the given address objects from the firewall. // // Address objects can be either a string or an Entry object. -func (c *Addr) Delete(vsys string, e ...interface{}) error { +func (c *FwAddr) Delete(vsys string, e ...interface{}) error { var err error if len(e) == 0 { @@ -148,13 +119,13 @@ func (c *Addr) Delete(vsys string, e ...interface{}) error { return err } -/** Internal functions for the Addr struct **/ +/** Internal functions for the FwAddr struct **/ -func (c *Addr) versioning() (normalizer, func(Entry) (interface{})) { +func (c *FwAddr) versioning() (normalizer, func(Entry) (interface{})) { return &container_v1{}, specify_v1 } -func (c *Addr) details(fn util.Retriever, vsys, name string) (Entry, error) { +func (c *FwAddr) details(fn util.Retriever, vsys, name string) (Entry, error) { path := c.xpath(vsys, []string{name}) obj, _ := c.versioning() _, err := fn(path, nil, obj) @@ -166,11 +137,22 @@ func (c *Addr) details(fn util.Retriever, vsys, name string) (Entry, error) { return ans, nil } -func (c *Addr) xpath(vsys string, vals []string) []string { +func (c *FwAddr) xpath(vsys string, vals []string) []string { if vsys == "" { vsys = "vsys1" } + // Shared xpath. + if vsys == "shared" { + return []string { + "config", + "shared", + "address", + util.AsEntryXpath(vals), + } + } + + // Vsys xpath. return []string { "config", "devices", @@ -181,67 +163,3 @@ func (c *Addr) xpath(vsys string, vals []string) []string { util.AsEntryXpath(vals), } } - -/** Structs / functions for this namespace. **/ - -type normalizer interface { - Normalize() Entry -} - -type container_v1 struct { - Answer entry_v1 `xml:"result>entry"` -} - -func (o *container_v1) Normalize() Entry { - ans := Entry{ - Name: o.Answer.Name, - Description: o.Answer.Description, - Tags: util.MemToStr(o.Answer.Tags), - } - switch { - case o.Answer.IpNetmask != nil: - ans.Type = IpNetmask - ans.Value = o.Answer.IpNetmask.Value - case o.Answer.IpRange != nil: - ans.Type = IpRange - ans.Value = o.Answer.IpRange.Value - case o.Answer.Fqdn != nil: - ans.Type = Fqdn - ans.Value = o.Answer.Fqdn.Value - } - - return ans -} - -type entry_v1 struct { - XMLName xml.Name `xml:"entry"` - Name string `xml:"name,attr"` - IpNetmask *valType `xml:"ip-netmask"` - IpRange *valType `xml:"ip-range"` - Fqdn *valType `xml:"fqdn"` - Description string `xml:"description"` - Tags *util.Member `xml:"tag"` -} - -type valType struct { - Value string `xml:",chardata"` -} - -func specify_v1(e Entry) interface{} { - ans := entry_v1{ - Name: e.Name, - Description: e.Description, - Tags: util.StrToMem(e.Tags), - } - vt := &valType{e.Value} - switch e.Type { - case IpNetmask: - ans.IpNetmask = vt - case IpRange: - ans.IpRange = vt - case Fqdn: - ans.Fqdn = vt - } - - return ans -} diff --git a/objs/addr/addr_test.go b/objs/addr/fw_test.go similarity index 96% rename from objs/addr/addr_test.go rename to objs/addr/fw_test.go index 488052bc..0951f103 100644 --- a/objs/addr/addr_test.go +++ b/objs/addr/fw_test.go @@ -8,7 +8,7 @@ import ( ) -func TestNormalization(t *testing.T) { +func TestFwNormalization(t *testing.T) { testCases := []struct{ desc string vsys string @@ -37,7 +37,7 @@ func TestNormalization(t *testing.T) { } mc := &testdata.MockClient{} - ns := &Addr{} + ns := &FwAddr{} ns.Initialize(mc) for _, tc := range testCases { diff --git a/objs/addr/pano.go b/objs/addr/pano.go new file mode 100644 index 00000000..05213350 --- /dev/null +++ b/objs/addr/pano.go @@ -0,0 +1,165 @@ +package addr + +import ( + "fmt" + "encoding/xml" + + "github.com/PaloAltoNetworks/pango/util" +) + +// PanoAddr is a namespace struct, included as part of pango.Firewall. +type PanoAddr struct { + con util.XapiClient +} + +// Initialize is invoked when Initialize on the pango.Client is called. +func (c *PanoAddr) Initialize(con util.XapiClient) { + c.con = con +} + +// GetList performs GET to retrieve a list of address objects. +func (c *PanoAddr) GetList(dg string) ([]string, error) { + c.con.LogQuery("(get) list of address objects") + path := c.xpath(dg, nil) + return c.con.EntryListUsing(c.con.Get, path[:len(path) - 1]) +} + +// ShowList performs SHOW to retrieve a list of address objects. +func (c *PanoAddr) ShowList(dg string) ([]string, error) { + c.con.LogQuery("(show) list of address objects") + path := c.xpath(dg, nil) + return c.con.EntryListUsing(c.con.Show, path[:len(path) - 1]) +} + +// Get performs GET to retrieve information for the given address object. +func (c *PanoAddr) Get(dg, name string) (Entry, error) { + c.con.LogQuery("(get) address object %q", name) + return c.details(c.con.Get, dg, name) +} + +// Get performs SHOW to retrieve information for the given address object. +func (c *PanoAddr) Show(dg, name string) (Entry, error) { + c.con.LogQuery("(show) address object %q", name) + return c.details(c.con.Show, dg, name) +} + +// Set performs SET to create / update one or more address objects. +func (c *PanoAddr) Set(dg string, e ...Entry) error { + var err error + + if len(e) == 0 { + return nil + } + + _, fn := c.versioning() + names := make([]string, len(e)) + + // Build up the struct with the given configs. + d := util.BulkElement{XMLName: xml.Name{Local: "address"}} + for i := range e { + d.Data = append(d.Data, fn(e[i])) + names[i] = e[i].Name + } + c.con.LogAction("(set) address objects: %v", names) + + // Set xpath. + path := c.xpath(dg, names) + if len(e) == 1 { + path = path[:len(path) - 1] + } else { + path = path[:len(path) - 2] + } + + // Create the objects. + _, err = c.con.Set(path, d.Config(), nil, nil) + return err +} + +// Edit performs EDIT to create / update an address object. +func (c *PanoAddr) Edit(dg string, e Entry) error { + var err error + + _, fn := c.versioning() + + c.con.LogAction("(edit) address object %q", e.Name) + + // Set xpath. + path := c.xpath(dg, []string{e.Name}) + + // Create the objects. + _, err = c.con.Edit(path, fn(e), nil, nil) + return err +} + +// Delete removes the given address objects from the firewall. +// +// Address objects can be either a string or an Entry object. +func (c *PanoAddr) Delete(dg string, e ...interface{}) error { + var err error + + if len(e) == 0 { + return nil + } + + names := make([]string, len(e)) + for i := range e { + switch v := e[i].(type) { + case string: + names[i] = v + case Entry: + names[i] = v.Name + default: + return fmt.Errorf("Unsupported type to delete: %s", v) + } + } + c.con.LogAction("(delete) address objects: %v", names) + + path := c.xpath(dg, names) + _, err = c.con.Delete(path, nil, nil) + return err +} + +/** Internal functions for the PanoAddr struct **/ + +func (c *PanoAddr) versioning() (normalizer, func(Entry) (interface{})) { + return &container_v1{}, specify_v1 +} + +func (c *PanoAddr) details(fn util.Retriever, dg, name string) (Entry, error) { + path := c.xpath(dg, []string{name}) + obj, _ := c.versioning() + _, err := fn(path, nil, obj) + if err != nil { + return Entry{}, err + } + ans := obj.Normalize() + + return ans, nil +} + +func (c *PanoAddr) xpath(dg string, vals []string) []string { + if dg == "" { + dg = "shared" + } + + // Shared xpath. + if dg == "shared" { + return []string { + "config", + "shared", + "address", + util.AsEntryXpath(vals), + } + } + + // Vsys xpath. + return []string { + "config", + "devices", + util.AsEntryXpath([]string{"localhost.localdomain"}), + "device-group", + util.AsEntryXpath([]string{dg}), + "address", + util.AsEntryXpath(vals), + } +} diff --git a/objs/addr/pano_test.go b/objs/addr/pano_test.go new file mode 100644 index 00000000..e4d37514 --- /dev/null +++ b/objs/addr/pano_test.go @@ -0,0 +1,62 @@ +package addr + +import ( + "testing" + "reflect" + + "github.com/PaloAltoNetworks/pango/testdata" +) + + +func TestPanoNormalization(t *testing.T) { + testCases := []struct{ + desc string + dg string + conf Entry + }{ + {"test ip netmask", "", Entry{ + Name: "one", + Value: "10.1.1.0/24", + Type: IpNetmask, + Description: "my description", + Tags: []string{"tag1", "tag2"}, + }}, + {"test ip range", "dg1", Entry{ + Name: "two", + Value: "10.1.1.1-10.1.1.254", + Type: IpRange, + Description: "my description", + Tags: []string{"tag3", "tag4"}, + }}, + {"test fqdn", "dg2", Entry{ + Name: "three", + Value: "example.com", + Type: Fqdn, + Description: "my description", + }}, + } + + mc := &testdata.MockClient{} + ns := &PanoAddr{} + ns.Initialize(mc) + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + mc.Reset() + mc.AddResp("") + err := ns.Set(tc.dg, tc.conf) + if err != nil { + t.Errorf("Error in set: %s", err) + } else { + mc.AddResp(mc.Elm) + r, err := ns.Get(tc.dg, tc.conf.Name) + if err != nil { + t.Errorf("Error in get: %s", err) + } else if !reflect.DeepEqual(tc.conf, r) { + t.Errorf("%#v != %#v", tc.conf, r) + } + } + }) + } +} + diff --git a/objs/addrgrp/doc.go b/objs/addrgrp/doc.go new file mode 100644 index 00000000..17b78f3e --- /dev/null +++ b/objs/addrgrp/doc.go @@ -0,0 +1,6 @@ +/* +Package addrgrp is the client.Objects.AddressGroup namespace. + +Normalized object: Entry +*/ +package addrgrp diff --git a/objs/addrgrp/entry.go b/objs/addrgrp/entry.go new file mode 100644 index 00000000..c0eb24c4 --- /dev/null +++ b/objs/addrgrp/entry.go @@ -0,0 +1,80 @@ +package addrgrp + +import ( + "encoding/xml" + + "github.com/PaloAltoNetworks/pango/util" +) + + +// Entry is a normalized, version independent representation of an address +// group. The value set in DynamicMatch should be something like the following: +// +// * 'tag1' +// * 'tag1' or 'tag2' and 'tag3' +// +// The Tags param is for administrative tags for this address object +// group itself. +type Entry struct { + Name string + Description string + StaticAddresses []string + DynamicMatch string + Tags []string +} + +// Copy copies the information from source Entry `s` to this object. As the +// Name field relates to the XPATH of this object, this field is not copied. +func (o *Entry) Copy(s Entry) { + o.Description = s.Description + o.StaticAddresses = s.StaticAddresses + o.DynamicMatch = s.DynamicMatch + o.Tags = s.Tags +} + +/** Structs / functions for normalization. **/ + +type normalizer interface { + Normalize() Entry +} + +type container_v1 struct { + Answer entry_v1 `xml:"result>entry"` +} + +func (o *container_v1) Normalize() Entry { + ans := Entry{ + Name: o.Answer.Name, + Description: o.Answer.Description, + StaticAddresses: util.MemToStr(o.Answer.StaticAddresses), + Tags: util.MemToStr(o.Answer.Tags), + } + if o.Answer.DynamicMatch != nil { + ans.DynamicMatch = *o.Answer.DynamicMatch + } + + return ans +} + +type entry_v1 struct { + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + Description string `xml:"description"` + StaticAddresses *util.Member `xml:"static"` + DynamicMatch *string `xml:"dynamic>filter"` + Tags *util.Member `xml:"tag"` +} + +func specify_v1(e Entry) interface{} { + ans := entry_v1{ + Name: e.Name, + Description: e.Description, + StaticAddresses: util.StrToMem(e.StaticAddresses), + Tags: util.StrToMem(e.Tags), + } + if e.DynamicMatch != "" { + ans.DynamicMatch = &e.DynamicMatch + } + + return ans +} diff --git a/objs/addrgrp/addrgrp.go b/objs/addrgrp/fw.go similarity index 53% rename from objs/addrgrp/addrgrp.go rename to objs/addrgrp/fw.go index 0c0328f4..be736692 100644 --- a/objs/addrgrp/addrgrp.go +++ b/objs/addrgrp/fw.go @@ -1,6 +1,3 @@ -// Package addrgrp is the client.Objects.AddressGroup namespace. -// -// Normalized object: Entry package addrgrp import ( @@ -11,69 +8,44 @@ import ( ) -// Entry is a normalized, version independent representation of an address -// group. The value set in DynamicMatch should be something like the following: -// -// * 'tag1' -// * 'tag1' or 'tag2' and 'tag3' -// -// The Tags param is for administrative tags for this address object -// group itself. -type Entry struct { - Name string - Description string - StaticAddresses []string - DynamicMatch string - Tags []string -} - -// Copy copies the information from source Entry `s` to this object. As the -// Name field relates to the XPATH of this object, this field is not copied. -func (o *Entry) Copy(s Entry) { - o.Description = s.Description - o.StaticAddresses = s.StaticAddresses - o.DynamicMatch = s.DynamicMatch - o.Tags = s.Tags -} - -// AddrGrp is a namespace struct, included as part of pango.Client. -type AddrGrp struct { +// FwAddrGrp is a namespace struct, included as part of pango.Client. +type FwAddrGrp struct { con util.XapiClient } // Initialize is invoked when Initialize on the pango.Client is called. -func (c *AddrGrp) Initialize(con util.XapiClient) { +func (c *FwAddrGrp) Initialize(con util.XapiClient) { c.con = con } // GetList performs GET to retrieve a list of address groups. -func (c *AddrGrp) GetList(vsys string) ([]string, error) { +func (c *FwAddrGrp) GetList(vsys string) ([]string, error) { c.con.LogQuery("(get) list of address groups") path := c.xpath(vsys, nil) return c.con.EntryListUsing(c.con.Get, path[:len(path) - 1]) } // ShowList performs SHOW to retrieve a list of address groups. -func (c *AddrGrp) ShowList(vsys string) ([]string, error) { +func (c *FwAddrGrp) ShowList(vsys string) ([]string, error) { c.con.LogQuery("(show) list of address groups") path := c.xpath(vsys, nil) return c.con.EntryListUsing(c.con.Show, path[:len(path) - 1]) } // Get performs GET to retrieve information for the given address group. -func (c *AddrGrp) Get(vsys, name string) (Entry, error) { +func (c *FwAddrGrp) Get(vsys, name string) (Entry, error) { c.con.LogQuery("(get) address group %q", name) return c.details(c.con.Get, vsys, name) } // Get performs SHOW to retrieve information for the given address group. -func (c *AddrGrp) Show(vsys, name string) (Entry, error) { +func (c *FwAddrGrp) Show(vsys, name string) (Entry, error) { c.con.LogQuery("(show) address group %q", name) return c.details(c.con.Show, vsys, name) } // Set performs SET to create / update one or more address groups. -func (c *AddrGrp) Set(vsys string, e ...Entry) error { +func (c *FwAddrGrp) Set(vsys string, e ...Entry) error { var err error if len(e) == 0 { @@ -105,7 +77,7 @@ func (c *AddrGrp) Set(vsys string, e ...Entry) error { } // Edit performs EDIT to create / update an address group. -func (c *AddrGrp) Edit(vsys string, e Entry) error { +func (c *FwAddrGrp) Edit(vsys string, e Entry) error { var err error _, fn := c.versioning() @@ -123,7 +95,7 @@ func (c *AddrGrp) Edit(vsys string, e Entry) error { // Delete removes the given address groups from the firewall. // // Address groups can be either a string or an Entry object. -func (c *AddrGrp) Delete(vsys string, e ...interface{}) error { +func (c *FwAddrGrp) Delete(vsys string, e ...interface{}) error { var err error if len(e) == 0 { @@ -148,13 +120,13 @@ func (c *AddrGrp) Delete(vsys string, e ...interface{}) error { return err } -/** Internal functions for the AddrGrp struct **/ +/** Internal functions for the FwAddrGrp struct **/ -func (c *AddrGrp) versioning() (normalizer, func(Entry) (interface{})) { +func (c *FwAddrGrp) versioning() (normalizer, func(Entry) (interface{})) { return &container_v1{}, specify_v1 } -func (c *AddrGrp) details(fn util.Retriever, vsys, name string) (Entry, error) { +func (c *FwAddrGrp) details(fn util.Retriever, vsys, name string) (Entry, error) { path := c.xpath(vsys, []string{name}) obj, _ := c.versioning() _, err := fn(path, nil, obj) @@ -166,11 +138,20 @@ func (c *AddrGrp) details(fn util.Retriever, vsys, name string) (Entry, error) { return ans, nil } -func (c *AddrGrp) xpath(vsys string, vals []string) []string { +func (c *FwAddrGrp) xpath(vsys string, vals []string) []string { if vsys == "" { vsys = "vsys1" } + if vsys == "shared" { + return []string { + "config", + "shared", + "address-group", + util.AsEntryXpath(vals), + } + } + return []string { "config", "devices", @@ -181,50 +162,3 @@ func (c *AddrGrp) xpath(vsys string, vals []string) []string { util.AsEntryXpath(vals), } } - -/** Structs / functions for this namespace. **/ - -type normalizer interface { - Normalize() Entry -} - -type container_v1 struct { - Answer entry_v1 `xml:"result>entry"` -} - -func (o *container_v1) Normalize() Entry { - ans := Entry{ - Name: o.Answer.Name, - Description: o.Answer.Description, - StaticAddresses: util.MemToStr(o.Answer.StaticAddresses), - Tags: util.MemToStr(o.Answer.Tags), - } - if o.Answer.DynamicMatch != nil { - ans.DynamicMatch = *o.Answer.DynamicMatch - } - - return ans -} - -type entry_v1 struct { - XMLName xml.Name `xml:"entry"` - Name string `xml:"name,attr"` - Description string `xml:"description"` - StaticAddresses *util.Member `xml:"static"` - DynamicMatch *string `xml:"dynamic>filter"` - Tags *util.Member `xml:"tag"` -} - -func specify_v1(e Entry) interface{} { - ans := entry_v1{ - Name: e.Name, - Description: e.Description, - StaticAddresses: util.StrToMem(e.StaticAddresses), - Tags: util.StrToMem(e.Tags), - } - if e.DynamicMatch != "" { - ans.DynamicMatch = &e.DynamicMatch - } - - return ans -} diff --git a/objs/addrgrp/addrgrp_test.go b/objs/addrgrp/fw_test.go similarity index 96% rename from objs/addrgrp/addrgrp_test.go rename to objs/addrgrp/fw_test.go index b165562d..cb5fef8e 100644 --- a/objs/addrgrp/addrgrp_test.go +++ b/objs/addrgrp/fw_test.go @@ -8,7 +8,7 @@ import ( ) -func TestNormalization(t *testing.T) { +func TestFwNormalization(t *testing.T) { testCases := []struct{ desc string vsys string @@ -39,7 +39,7 @@ func TestNormalization(t *testing.T) { } mc := &testdata.MockClient{} - ns := &AddrGrp{} + ns := &FwAddrGrp{} ns.Initialize(mc) for _, tc := range testCases { diff --git a/objs/addrgrp/pano.go b/objs/addrgrp/pano.go new file mode 100644 index 00000000..cbfb2a74 --- /dev/null +++ b/objs/addrgrp/pano.go @@ -0,0 +1,164 @@ +package addrgrp + +import ( + "fmt" + "encoding/xml" + + "github.com/PaloAltoNetworks/pango/util" +) + + +// PanoAddrGrp is a namespace struct, included as part of pango.Client. +type PanoAddrGrp struct { + con util.XapiClient +} + +// Initialize is invoked when Initialize on the pango.Client is called. +func (c *PanoAddrGrp) Initialize(con util.XapiClient) { + c.con = con +} + +// GetList performs GET to retrieve a list of address groups. +func (c *PanoAddrGrp) GetList(dg string) ([]string, error) { + c.con.LogQuery("(get) list of address groups") + path := c.xpath(dg, nil) + return c.con.EntryListUsing(c.con.Get, path[:len(path) - 1]) +} + +// ShowList performs SHOW to retrieve a list of address groups. +func (c *PanoAddrGrp) ShowList(dg string) ([]string, error) { + c.con.LogQuery("(show) list of address groups") + path := c.xpath(dg, nil) + return c.con.EntryListUsing(c.con.Show, path[:len(path) - 1]) +} + +// Get performs GET to retrieve information for the given address group. +func (c *PanoAddrGrp) Get(dg, name string) (Entry, error) { + c.con.LogQuery("(get) address group %q", name) + return c.details(c.con.Get, dg, name) +} + +// Get performs SHOW to retrieve information for the given address group. +func (c *PanoAddrGrp) Show(dg, name string) (Entry, error) { + c.con.LogQuery("(show) address group %q", name) + return c.details(c.con.Show, dg, name) +} + +// Set performs SET to create / update one or more address groups. +func (c *PanoAddrGrp) Set(dg string, e ...Entry) error { + var err error + + if len(e) == 0 { + return nil + } + + _, fn := c.versioning() + names := make([]string, len(e)) + + // Build up the struct with the given configs. + d := util.BulkElement{XMLName: xml.Name{Local: "address-group"}} + for i := range e { + d.Data = append(d.Data, fn(e[i])) + names[i] = e[i].Name + } + c.con.LogAction("(set) address groups: %v", names) + + // Set xpath. + path := c.xpath(dg, names) + if len(e) == 1 { + path = path[:len(path) - 1] + } else { + path = path[:len(path) - 2] + } + + // Create the objects. + _, err = c.con.Set(path, d.Config(), nil, nil) + return err +} + +// Edit performs EDIT to create / update an address group. +func (c *PanoAddrGrp) Edit(dg string, e Entry) error { + var err error + + _, fn := c.versioning() + + c.con.LogAction("(edit) address group %q", e.Name) + + // Set xpath. + path := c.xpath(dg, []string{e.Name}) + + // Create the objects. + _, err = c.con.Edit(path, fn(e), nil, nil) + return err +} + +// Delete removes the given address groups from the firewall. +// +// Address groups can be either a string or an Entry object. +func (c *PanoAddrGrp) Delete(dg string, e ...interface{}) error { + var err error + + if len(e) == 0 { + return nil + } + + names := make([]string, len(e)) + for i := range e { + switch v := e[i].(type) { + case string: + names[i] = v + case Entry: + names[i] = v.Name + default: + return fmt.Errorf("Unsupported type to delete: %s", v) + } + } + c.con.LogAction("(delete) address groups: %v", names) + + path := c.xpath(dg, names) + _, err = c.con.Delete(path, nil, nil) + return err +} + +/** Internal functions for the PanoAddrGrp struct **/ + +func (c *PanoAddrGrp) versioning() (normalizer, func(Entry) (interface{})) { + return &container_v1{}, specify_v1 +} + +func (c *PanoAddrGrp) details(fn util.Retriever, dg, name string) (Entry, error) { + path := c.xpath(dg, []string{name}) + obj, _ := c.versioning() + _, err := fn(path, nil, obj) + if err != nil { + return Entry{}, err + } + ans := obj.Normalize() + + return ans, nil +} + +func (c *PanoAddrGrp) xpath(dg string, vals []string) []string { + if dg == "" { + dg = "shared" + } + + if dg == "shared" { + return []string { + "config", + "shared", + "address-group", + util.AsEntryXpath(vals), + } + } + + return []string { + "config", + "devices", + util.AsEntryXpath([]string{"localhost.localdomain"}), + "device-group", + util.AsEntryXpath([]string{dg}), + "address-group", + util.AsEntryXpath(vals), + } +} diff --git a/objs/addrgrp/pano_test.go b/objs/addrgrp/pano_test.go new file mode 100644 index 00000000..0118727d --- /dev/null +++ b/objs/addrgrp/pano_test.go @@ -0,0 +1,64 @@ +package addrgrp + +import ( + "testing" + "reflect" + + "github.com/PaloAltoNetworks/pango/testdata" +) + + +func TestPanoNormalization(t *testing.T) { + testCases := []struct{ + desc string + dg string + conf Entry + }{ + {"test static no tags", "", Entry{ + Name: "one", + Description: "my description", + StaticAddresses: []string{"adr1", "adr2"}, + }}, + {"test static with tags", "", Entry{ + Name: "one", + Description: "my description", + StaticAddresses: []string{"adr1", "adr2"}, + Tags: []string{"tag1", "tag2"}, + }}, + {"test dynamic no tags", "dg1", Entry{ + Name: "one", + Description: "my description", + DynamicMatch: "'tag1' or 'tag2' and 'tag3'", + }}, + {"test dynamic with tags", "dg2", Entry{ + Name: "one", + Description: "my description", + DynamicMatch: "'tag1' or 'tag2' and 'tag3'", + Tags: []string{"tag1", "tag2"}, + }}, + } + + mc := &testdata.MockClient{} + ns := &PanoAddrGrp{} + ns.Initialize(mc) + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + mc.Reset() + mc.AddResp("") + err := ns.Set(tc.dg, tc.conf) + if err != nil { + t.Errorf("Error in set: %s", err) + } else { + mc.AddResp(mc.Elm) + r, err := ns.Get(tc.dg, tc.conf.Name) + if err != nil { + t.Errorf("Error in get: %s", err) + } else if !reflect.DeepEqual(tc.conf, r) { + t.Errorf("%#v != %#v", tc.conf, r) + } + } + }) + } +} + diff --git a/objs/doc.go b/objs/doc.go new file mode 100644 index 00000000..3ac75d3c --- /dev/null +++ b/objs/doc.go @@ -0,0 +1,4 @@ +/* +Package objs is the client.Objects namespace. +*/ +package objs diff --git a/objs/objs.go b/objs/fw.go similarity index 54% rename from objs/objs.go rename to objs/fw.go index cd5470eb..507f4d0c 100644 --- a/objs/objs.go +++ b/objs/fw.go @@ -1,4 +1,3 @@ -// Package objs is the client.Objects namespace. package objs @@ -13,29 +12,29 @@ import ( ) -// Objs is the client.Objects namespace. -type Objs struct { - Address *addr.Addr - AddressGroup *addrgrp.AddrGrp - Services *srvc.Srvc - ServiceGroup *srvcgrp.SrvcGrp - Tags *tags.Tags +// FwObjs is the client.Objects namespace. +type FwObjs struct { + Address *addr.FwAddr + AddressGroup *addrgrp.FwAddrGrp + Services *srvc.FwSrvc + ServiceGroup *srvcgrp.FwSrvcGrp + Tags *tags.FwTags } // Initialize is invoked on client.Initialize(). -func (c *Objs) Initialize(i util.XapiClient) { - c.Address = &addr.Addr{} +func (c *FwObjs) Initialize(i util.XapiClient) { + c.Address = &addr.FwAddr{} c.Address.Initialize(i) - c.AddressGroup = &addrgrp.AddrGrp{} + c.AddressGroup = &addrgrp.FwAddrGrp{} c.AddressGroup.Initialize(i) - c.Services = &srvc.Srvc{} + c.Services = &srvc.FwSrvc{} c.Services.Initialize(i) - c.ServiceGroup = &srvcgrp.SrvcGrp{} + c.ServiceGroup = &srvcgrp.FwSrvcGrp{} c.ServiceGroup.Initialize(i) - c.Tags = &tags.Tags{} + c.Tags = &tags.FwTags{} c.Tags.Initialize(i) } diff --git a/objs/objs_test.go b/objs/fw_test.go similarity index 83% rename from objs/objs_test.go rename to objs/fw_test.go index 95b6fb0a..b3ad0c8b 100644 --- a/objs/objs_test.go +++ b/objs/fw_test.go @@ -7,9 +7,9 @@ import ( ) -func TestInitialize(t *testing.T) { +func TestFwInitialize(t *testing.T) { mc := &testdata.MockClient{} - o := &Objs{} + o := &FwObjs{} o.Initialize(mc) if o.Address == nil || o.AddressGroup == nil || o.Services == nil || o.ServiceGroup == nil || o.Tags == nil { diff --git a/objs/pano.go b/objs/pano.go new file mode 100644 index 00000000..ef623ff1 --- /dev/null +++ b/objs/pano.go @@ -0,0 +1,40 @@ +package objs + + +import ( + "github.com/PaloAltoNetworks/pango/util" + + "github.com/PaloAltoNetworks/pango/objs/addr" + "github.com/PaloAltoNetworks/pango/objs/addrgrp" + "github.com/PaloAltoNetworks/pango/objs/srvc" + "github.com/PaloAltoNetworks/pango/objs/srvcgrp" + "github.com/PaloAltoNetworks/pango/objs/tags" +) + + +// PanoObjs is the client.Objects namespace. +type PanoObjs struct { + Address *addr.PanoAddr + AddressGroup *addrgrp.PanoAddrGrp + Services *srvc.PanoSrvc + ServiceGroup *srvcgrp.PanoSrvcGrp + Tags *tags.PanoTags +} + +// Initialize is invoked on client.Initialize(). +func (c *PanoObjs) Initialize(i util.XapiClient) { + c.Address = &addr.PanoAddr{} + c.Address.Initialize(i) + + c.AddressGroup = &addrgrp.PanoAddrGrp{} + c.AddressGroup.Initialize(i) + + c.Services = &srvc.PanoSrvc{} + c.Services.Initialize(i) + + c.ServiceGroup = &srvcgrp.PanoSrvcGrp{} + c.ServiceGroup.Initialize(i) + + c.Tags = &tags.PanoTags{} + c.Tags.Initialize(i) +} diff --git a/objs/pano_test.go b/objs/pano_test.go new file mode 100644 index 00000000..54459ac1 --- /dev/null +++ b/objs/pano_test.go @@ -0,0 +1,19 @@ +package objs + +import ( + "testing" + + "github.com/PaloAltoNetworks/pango/testdata" +) + + +func TestPanoInitialize(t *testing.T) { + mc := &testdata.MockClient{} + o := &PanoObjs{} + o.Initialize(mc) + + if o.Address == nil || o.AddressGroup == nil || o.Services == nil || o.ServiceGroup == nil || o.Tags == nil { + t.Fail() + } +} + diff --git a/objs/srvc/doc.go b/objs/srvc/doc.go new file mode 100644 index 00000000..03b0837c --- /dev/null +++ b/objs/srvc/doc.go @@ -0,0 +1,4 @@ +// Package srvc is the client.Objects.Services namespace. +// +// Normalized object: Entry +package srvc diff --git a/objs/srvc/entry.go b/objs/srvc/entry.go new file mode 100644 index 00000000..448a42f5 --- /dev/null +++ b/objs/srvc/entry.go @@ -0,0 +1,97 @@ +package srvc + +import ( + "encoding/xml" + + "github.com/PaloAltoNetworks/pango/util" +) + + +// Entry is a normalized, version independent representation of a service +// object. +// +// Protocol should be either "tcp" or "udp". +type Entry struct { + Name string + Description string + Protocol string + SourcePort string + DestinationPort string + Tags []string +} + +// Copy copies the information from source Entry `s` to this object. As the +// Name field relates to the XPATH of this object, this field is not copied. +func (o *Entry) Copy(s Entry) { + o.Description = s.Description + o.Protocol = s.Protocol + o.SourcePort = s.SourcePort + o.DestinationPort = s.DestinationPort + o.Tags = s.Tags +} + +/** Structs / functions for normalization. **/ + +type normalizer interface { + Normalize() Entry +} + +type container_v1 struct { + Answer entry_v1 `xml:"result>entry"` +} + +func (o *container_v1) Normalize() Entry { + ans := Entry{ + Name: o.Answer.Name, + Description: o.Answer.Description, + Tags: util.MemToStr(o.Answer.Tags), + } + switch { + case o.Answer.TcpProto != nil: + ans.Protocol = "tcp" + ans.SourcePort = o.Answer.TcpProto.SourcePort + ans.DestinationPort = o.Answer.TcpProto.DestinationPort + case o.Answer.UdpProto != nil: + ans.Protocol = "udp" + ans.SourcePort = o.Answer.UdpProto.SourcePort + ans.DestinationPort = o.Answer.UdpProto.DestinationPort + } + + return ans +} + +type entry_v1 struct { + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + TcpProto *protoDef `xml:"protocol>tcp"` + UdpProto *protoDef `xml:"protocol>udp"` + Description string `xml:"description"` + Tags *util.Member `xml:"tag"` +} + +type protoDef struct { + SourcePort string `xml:"source-port,omitempty"` + DestinationPort string `xml:"port"` +} + +func specify_v1(e Entry) interface{} { + ans := entry_v1{ + Name: e.Name, + Description: e.Description, + Tags: util.StrToMem(e.Tags), + } + switch e.Protocol { + case "tcp": + ans.TcpProto = &protoDef{ + e.SourcePort, + e.DestinationPort, + } + case "udp": + ans.UdpProto = &protoDef{ + e.SourcePort, + e.DestinationPort, + } + } + + return ans +} diff --git a/objs/srvc/srvc.go b/objs/srvc/fw.go similarity index 51% rename from objs/srvc/srvc.go rename to objs/srvc/fw.go index 9d2692b7..037816ae 100644 --- a/objs/srvc/srvc.go +++ b/objs/srvc/fw.go @@ -1,6 +1,3 @@ -// Package srvc is the client.Objects.Services namespace. -// -// Normalized object: Entry package srvc import ( @@ -11,67 +8,44 @@ import ( ) -// Entry is a normalized, version independent representation of a service -// object. -// -// Protocol should be either "tcp" or "udp". -type Entry struct { - Name string - Description string - Protocol string - SourcePort string - DestinationPort string - Tags []string -} - -// Copy copies the information from source Entry `s` to this object. As the -// Name field relates to the XPATH of this object, this field is not copied. -func (o *Entry) Copy(s Entry) { - o.Description = s.Description - o.Protocol = s.Protocol - o.SourcePort = s.SourcePort - o.DestinationPort = s.DestinationPort - o.Tags = s.Tags -} - -// Srvc is a namespace struct, included as part of pango.Client. -type Srvc struct { +// FwSrvc is a namespace struct, included as part of pango.Client. +type FwSrvc struct { con util.XapiClient } // Initialize is invoked when Initialize on the pango.Client is called. -func (c *Srvc) Initialize(con util.XapiClient) { +func (c *FwSrvc) Initialize(con util.XapiClient) { c.con = con } // GetList performs GET to retrieve a list of service objects. -func (c *Srvc) GetList(vsys string) ([]string, error) { +func (c *FwSrvc) GetList(vsys string) ([]string, error) { c.con.LogQuery("(get) list of service objects") path := c.xpath(vsys, nil) return c.con.EntryListUsing(c.con.Get, path[:len(path) - 1]) } // ShowList performs SHOW to retrieve a list of service objects. -func (c *Srvc) ShowList(vsys string) ([]string, error) { +func (c *FwSrvc) ShowList(vsys string) ([]string, error) { c.con.LogQuery("(show) list of service objects") path := c.xpath(vsys, nil) return c.con.EntryListUsing(c.con.Show, path[:len(path) - 1]) } // Get performs GET to retrieve information for the given service object. -func (c *Srvc) Get(vsys, name string) (Entry, error) { +func (c *FwSrvc) Get(vsys, name string) (Entry, error) { c.con.LogQuery("(get) service object %q", name) return c.details(c.con.Get, vsys, name) } // Get performs SHOW to retrieve information for the given service object. -func (c *Srvc) Show(vsys, name string) (Entry, error) { +func (c *FwSrvc) Show(vsys, name string) (Entry, error) { c.con.LogQuery("(show) service object %q", name) return c.details(c.con.Show, vsys, name) } // Set performs SET to create / update one or more service objects. -func (c *Srvc) Set(vsys string, e ...Entry) error { +func (c *FwSrvc) Set(vsys string, e ...Entry) error { var err error if len(e) == 0 { @@ -103,7 +77,7 @@ func (c *Srvc) Set(vsys string, e ...Entry) error { } // Edit performs EDIT to create / update a service object. -func (c *Srvc) Edit(vsys string, e Entry) error { +func (c *FwSrvc) Edit(vsys string, e Entry) error { var err error _, fn := c.versioning() @@ -121,7 +95,7 @@ func (c *Srvc) Edit(vsys string, e Entry) error { // Delete removes the given service objects from the firewall. // // Service objects can be either a string or an Entry object. -func (c *Srvc) Delete(vsys string, e ...interface{}) error { +func (c *FwSrvc) Delete(vsys string, e ...interface{}) error { var err error if len(e) == 0 { @@ -146,13 +120,13 @@ func (c *Srvc) Delete(vsys string, e ...interface{}) error { return err } -/** Internal functions for the Srvc struct **/ +/** Internal functions for the FwSrvc struct **/ -func (c *Srvc) versioning() (normalizer, func(Entry) (interface{})) { +func (c *FwSrvc) versioning() (normalizer, func(Entry) (interface{})) { return &container_v1{}, specify_v1 } -func (c *Srvc) details(fn util.Retriever, vsys, name string) (Entry, error) { +func (c *FwSrvc) details(fn util.Retriever, vsys, name string) (Entry, error) { path := c.xpath(vsys, []string{name}) obj, _ := c.versioning() _, err := fn(path, nil, obj) @@ -164,11 +138,20 @@ func (c *Srvc) details(fn util.Retriever, vsys, name string) (Entry, error) { return ans, nil } -func (c *Srvc) xpath(vsys string, vals []string) []string { +func (c *FwSrvc) xpath(vsys string, vals []string) []string { if vsys == "" { vsys = "vsys1" } + if vsys == "shared" { + return []string { + "config", + "shared", + "service", + util.AsEntryXpath(vals), + } + } + return []string { "config", "devices", @@ -179,69 +162,3 @@ func (c *Srvc) xpath(vsys string, vals []string) []string { util.AsEntryXpath(vals), } } - -/** Structs / functions for this namespace. **/ - -type normalizer interface { - Normalize() Entry -} - -type container_v1 struct { - Answer entry_v1 `xml:"result>entry"` -} - -func (o *container_v1) Normalize() Entry { - ans := Entry{ - Name: o.Answer.Name, - Description: o.Answer.Description, - Tags: util.MemToStr(o.Answer.Tags), - } - switch { - case o.Answer.TcpProto != nil: - ans.Protocol = "tcp" - ans.SourcePort = o.Answer.TcpProto.SourcePort - ans.DestinationPort = o.Answer.TcpProto.DestinationPort - case o.Answer.UdpProto != nil: - ans.Protocol = "udp" - ans.SourcePort = o.Answer.UdpProto.SourcePort - ans.DestinationPort = o.Answer.UdpProto.DestinationPort - } - - return ans -} - -type entry_v1 struct { - XMLName xml.Name `xml:"entry"` - Name string `xml:"name,attr"` - TcpProto *protoDef `xml:"protocol>tcp"` - UdpProto *protoDef `xml:"protocol>udp"` - Description string `xml:"description"` - Tags *util.Member `xml:"tag"` -} - -type protoDef struct { - SourcePort string `xml:"source-port,omitempty"` - DestinationPort string `xml:"port"` -} - -func specify_v1(e Entry) interface{} { - ans := entry_v1{ - Name: e.Name, - Description: e.Description, - Tags: util.StrToMem(e.Tags), - } - switch e.Protocol { - case "tcp": - ans.TcpProto = &protoDef{ - e.SourcePort, - e.DestinationPort, - } - case "udp": - ans.UdpProto = &protoDef{ - e.SourcePort, - e.DestinationPort, - } - } - - return ans -} diff --git a/objs/srvc/srvc_test.go b/objs/srvc/fw_test.go similarity index 97% rename from objs/srvc/srvc_test.go rename to objs/srvc/fw_test.go index f656c2b0..a55d02aa 100644 --- a/objs/srvc/srvc_test.go +++ b/objs/srvc/fw_test.go @@ -8,7 +8,7 @@ import ( ) -func TestNormalization(t *testing.T) { +func TestFwNormalization(t *testing.T) { testCases := []struct{ desc string vsys string @@ -73,7 +73,7 @@ func TestNormalization(t *testing.T) { } mc := &testdata.MockClient{} - ns := &Srvc{} + ns := &FwSrvc{} ns.Initialize(mc) for _, tc := range testCases { diff --git a/objs/srvc/pano.go b/objs/srvc/pano.go new file mode 100644 index 00000000..fe8a35a9 --- /dev/null +++ b/objs/srvc/pano.go @@ -0,0 +1,164 @@ +package srvc + +import ( + "fmt" + "encoding/xml" + + "github.com/PaloAltoNetworks/pango/util" +) + + +// PanoSrvc is a namespace struct, included as part of pango.Client. +type PanoSrvc struct { + con util.XapiClient +} + +// Initialize is invoked when Initialize on the pango.Client is called. +func (c *PanoSrvc) Initialize(con util.XapiClient) { + c.con = con +} + +// GetList performs GET to retrieve a list of service objects. +func (c *PanoSrvc) GetList(dg string) ([]string, error) { + c.con.LogQuery("(get) list of service objects") + path := c.xpath(dg, nil) + return c.con.EntryListUsing(c.con.Get, path[:len(path) - 1]) +} + +// ShowList performs SHOW to retrieve a list of service objects. +func (c *PanoSrvc) ShowList(dg string) ([]string, error) { + c.con.LogQuery("(show) list of service objects") + path := c.xpath(dg, nil) + return c.con.EntryListUsing(c.con.Show, path[:len(path) - 1]) +} + +// Get performs GET to retrieve information for the given service object. +func (c *PanoSrvc) Get(dg, name string) (Entry, error) { + c.con.LogQuery("(get) service object %q", name) + return c.details(c.con.Get, dg, name) +} + +// Get performs SHOW to retrieve information for the given service object. +func (c *PanoSrvc) Show(dg, name string) (Entry, error) { + c.con.LogQuery("(show) service object %q", name) + return c.details(c.con.Show, dg, name) +} + +// Set performs SET to create / update one or more service objects. +func (c *PanoSrvc) Set(dg string, e ...Entry) error { + var err error + + if len(e) == 0 { + return nil + } + + _, fn := c.versioning() + names := make([]string, len(e)) + + // Build up the struct with the given configs. + d := util.BulkElement{XMLName: xml.Name{Local: "service"}} + for i := range e { + d.Data = append(d.Data, fn(e[i])) + names[i] = e[i].Name + } + c.con.LogAction("(set) service objects: %v", names) + + // Set xpath. + path := c.xpath(dg, names) + if len(e) == 1 { + path = path[:len(path) - 1] + } else { + path = path[:len(path) - 2] + } + + // Create the objects. + _, err = c.con.Set(path, d.Config(), nil, nil) + return err +} + +// Edit performs EDIT to create / update a service object. +func (c *PanoSrvc) Edit(dg string, e Entry) error { + var err error + + _, fn := c.versioning() + + c.con.LogAction("(edit) service object %q", e.Name) + + // Set xpath. + path := c.xpath(dg, []string{e.Name}) + + // Create the object. + _, err = c.con.Edit(path, fn(e), nil, nil) + return err +} + +// Delete removes the given service objects from the firewall. +// +// Service objects can be either a string or an Entry object. +func (c *PanoSrvc) Delete(dg string, e ...interface{}) error { + var err error + + if len(e) == 0 { + return nil + } + + names := make([]string, len(e)) + for i := range e { + switch v := e[i].(type) { + case string: + names[i] = v + case Entry: + names[i] = v.Name + default: + return fmt.Errorf("Unsupported type to delete: %s", v) + } + } + c.con.LogAction("(delete) service objects: %v", names) + + path := c.xpath(dg, names) + _, err = c.con.Delete(path, nil, nil) + return err +} + +/** Internal functions for the PanoSrvc struct **/ + +func (c *PanoSrvc) versioning() (normalizer, func(Entry) (interface{})) { + return &container_v1{}, specify_v1 +} + +func (c *PanoSrvc) details(fn util.Retriever, dg, name string) (Entry, error) { + path := c.xpath(dg, []string{name}) + obj, _ := c.versioning() + _, err := fn(path, nil, obj) + if err != nil { + return Entry{}, err + } + ans := obj.Normalize() + + return ans, nil +} + +func (c *PanoSrvc) xpath(dg string, vals []string) []string { + if dg == "" { + dg = "shared" + } + + if dg == "shared" { + return []string { + "config", + "shared", + "service", + util.AsEntryXpath(vals), + } + } + + return []string { + "config", + "devices", + util.AsEntryXpath([]string{"localhost.localdomain"}), + "vsys", + util.AsEntryXpath([]string{dg}), + "service", + util.AsEntryXpath(vals), + } +} diff --git a/objs/srvc/pano_test.go b/objs/srvc/pano_test.go new file mode 100644 index 00000000..7b9d00f1 --- /dev/null +++ b/objs/srvc/pano_test.go @@ -0,0 +1,98 @@ +package srvc + +import ( + "testing" + "reflect" + + "github.com/PaloAltoNetworks/pango/testdata" +) + + +func TestPanoNormalization(t *testing.T) { + testCases := []struct{ + desc string + dg string + conf Entry + }{ + {"tcp service no source port no tag", "", Entry{ + Name: "tcp1", + Description: "my service", + Protocol: "tcp", + DestinationPort: "1234", + }}, + {"tcp service no source port with tag", "", Entry{ + Name: "tcp2", + Description: "my service", + Protocol: "tcp", + DestinationPort: "1234", + Tags: []string{"tag1", "tag2"}, + }}, + {"tcp service with source port no tag", "", Entry{ + Name: "tcp3", + Description: "my service", + Protocol: "tcp", + SourcePort: "1025", + DestinationPort: "1234", + }}, + {"tcp service with source port with tag", "dg1", Entry{ + Name: "tcp4", + Description: "my service", + Protocol: "tcp", + SourcePort: "1025", + DestinationPort: "1234", + Tags: []string{"tag1", "tag2"}, + }}, + {"udp service no source port no tag", "dg2", Entry{ + Name: "udp1", + Description: "my service", + Protocol: "udp", + DestinationPort: "1234", + }}, + {"udp service no source port with tag", "dg3", Entry{ + Name: "udp2", + Description: "my service", + Protocol: "udp", + DestinationPort: "1234", + Tags: []string{"tag1", "tag2"}, + }}, + {"udp service with source port no tag", "dg4", Entry{ + Name: "udp3", + Description: "my service", + Protocol: "udp", + SourcePort: "1025", + DestinationPort: "1234", + }}, + {"udp service with source port with tag", "dg5", Entry{ + Name: "udp4", + Description: "my service", + Protocol: "udp", + SourcePort: "1025", + DestinationPort: "1234", + Tags: []string{"tag1", "tag2"}, + }}, + } + + mc := &testdata.MockClient{} + ns := &PanoSrvc{} + ns.Initialize(mc) + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + mc.Reset() + mc.AddResp("") + err := ns.Set(tc.dg, tc.conf) + if err != nil { + t.Errorf("Error in set: %s", err) + } else { + mc.AddResp(mc.Elm) + r, err := ns.Get(tc.dg, tc.conf.Name) + if err != nil { + t.Errorf("Error in get: %s", err) + } else if !reflect.DeepEqual(tc.conf, r) { + t.Errorf("%#v != %#v", tc.conf, r) + } + } + }) + } +} + diff --git a/objs/srvcgrp/doc.go b/objs/srvcgrp/doc.go new file mode 100644 index 00000000..9c14d21b --- /dev/null +++ b/objs/srvcgrp/doc.go @@ -0,0 +1,6 @@ +/* +Package srvcgrp is the client.Objects.ServiceGroup namespace. + +Normalized object: Entry +*/ +package srvcgrp diff --git a/objs/srvcgrp/entry.go b/objs/srvcgrp/entry.go new file mode 100644 index 00000000..a84cb8d7 --- /dev/null +++ b/objs/srvcgrp/entry.go @@ -0,0 +1,60 @@ +package srvcgrp + +import ( + "encoding/xml" + + "github.com/PaloAltoNetworks/pango/util" +) + + +// Entry is a normalized, version independent representation of a service +// group. +type Entry struct { + Name string + Services []string + Tags []string +} + +// Copy copies the information from source Entry `s` to this object. As the +// Name field relates to the XPATH of this object, this field is not copied. +func (o *Entry) Copy(s Entry) { + o.Services = s.Services + o.Tags = s.Tags +} + +/** Structs / functions for normalization. **/ + +type normalizer interface { + Normalize() Entry +} + +type container_v1 struct { + Answer entry_v1 `xml:"result>entry"` +} + +func (o *container_v1) Normalize() Entry { + ans := Entry{ + Name: o.Answer.Name, + Services: util.MemToStr(o.Answer.Services), + Tags: util.MemToStr(o.Answer.Tags), + } + + return ans +} + +type entry_v1 struct { + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + Services *util.Member `xml:"members"` + Tags *util.Member `xml:"tag"` +} + +func specify_v1(e Entry) interface{} { + ans := entry_v1{ + Name: e.Name, + Services: util.StrToMem(e.Services), + Tags: util.StrToMem(e.Tags), + } + + return ans +} diff --git a/objs/srvcgrp/srvcgrp.go b/objs/srvcgrp/fw.go similarity index 61% rename from objs/srvcgrp/srvcgrp.go rename to objs/srvcgrp/fw.go index 9e6bcb9f..64563555 100644 --- a/objs/srvcgrp/srvcgrp.go +++ b/objs/srvcgrp/fw.go @@ -1,6 +1,3 @@ -// Package srvcgrp is the client.Objects.ServiceGroup namespace. -// -// Normalized object: Entry package srvcgrp import ( @@ -11,59 +8,44 @@ import ( ) -// Entry is a normalized, version independent representation of a service -// group. -type Entry struct { - Name string - Services []string - Tags []string -} - -// Copy copies the information from source Entry `s` to this object. As the -// Name field relates to the XPATH of this object, this field is not copied. -func (o *Entry) Copy(s Entry) { - o.Services = s.Services - o.Tags = s.Tags -} - -// SrvcGrp is a namespace struct, included as part of pango.Client. -type SrvcGrp struct { +// FwSrvcGrp is a namespace struct, included as part of pango.Client. +type FwSrvcGrp struct { con util.XapiClient } // Initialize is invoked when Initialize on the pango.Client is called. -func (c *SrvcGrp) Initialize(con util.XapiClient) { +func (c *FwSrvcGrp) Initialize(con util.XapiClient) { c.con = con } // GetList performs GET to retrieve a list of service groups. -func (c *SrvcGrp) GetList(vsys string) ([]string, error) { +func (c *FwSrvcGrp) GetList(vsys string) ([]string, error) { c.con.LogQuery("(get) list of service groups") path := c.xpath(vsys, nil) return c.con.EntryListUsing(c.con.Get, path[:len(path) - 1]) } // ShowList performs SHOW to retrieve a list of service groups. -func (c *SrvcGrp) ShowList(vsys string) ([]string, error) { +func (c *FwSrvcGrp) ShowList(vsys string) ([]string, error) { c.con.LogQuery("(show) list of service groups") path := c.xpath(vsys, nil) return c.con.EntryListUsing(c.con.Show, path[:len(path) - 1]) } // Get performs GET to retrieve information for the given service group. -func (c *SrvcGrp) Get(vsys, name string) (Entry, error) { +func (c *FwSrvcGrp) Get(vsys, name string) (Entry, error) { c.con.LogQuery("(get) service group %q", name) return c.details(c.con.Get, vsys, name) } // Get performs SHOW to retrieve information for the given service group. -func (c *SrvcGrp) Show(vsys, name string) (Entry, error) { +func (c *FwSrvcGrp) Show(vsys, name string) (Entry, error) { c.con.LogQuery("(show) service group %q", name) return c.details(c.con.Show, vsys, name) } // Set performs SET to create / update one or more service groups. -func (c *SrvcGrp) Set(vsys string, e ...Entry) error { +func (c *FwSrvcGrp) Set(vsys string, e ...Entry) error { var err error if len(e) == 0 { @@ -95,7 +77,7 @@ func (c *SrvcGrp) Set(vsys string, e ...Entry) error { } // Edit performs EDIT to create / update a service group. -func (c *SrvcGrp) Edit(vsys string, e Entry) error { +func (c *FwSrvcGrp) Edit(vsys string, e Entry) error { var err error _, fn := c.versioning() @@ -113,7 +95,7 @@ func (c *SrvcGrp) Edit(vsys string, e Entry) error { // Delete removes the given service groups from the firewall. // // Service groups can be either a string or an Entry object. -func (c *SrvcGrp) Delete(vsys string, e ...interface{}) error { +func (c *FwSrvcGrp) Delete(vsys string, e ...interface{}) error { var err error if len(e) == 0 { @@ -138,13 +120,13 @@ func (c *SrvcGrp) Delete(vsys string, e ...interface{}) error { return err } -/** Internal functions for the SrvcGrp struct **/ +/** Internal functions for the FwSrvcGrp struct **/ -func (c *SrvcGrp) versioning() (normalizer, func(Entry) (interface{})) { +func (c *FwSrvcGrp) versioning() (normalizer, func(Entry) (interface{})) { return &container_v1{}, specify_v1 } -func (c *SrvcGrp) details(fn util.Retriever, vsys, name string) (Entry, error) { +func (c *FwSrvcGrp) details(fn util.Retriever, vsys, name string) (Entry, error) { path := c.xpath(vsys, []string{name}) obj, _ := c.versioning() _, err := fn(path, nil, obj) @@ -156,11 +138,20 @@ func (c *SrvcGrp) details(fn util.Retriever, vsys, name string) (Entry, error) { return ans, nil } -func (c *SrvcGrp) xpath(vsys string, vals []string) []string { +func (c *FwSrvcGrp) xpath(vsys string, vals []string) []string { if vsys == "" { vsys = "vsys1" } + if vsys == "shared" { + return []string{ + "config", + "shared", + "service-group", + util.AsEntryXpath(vals), + } + } + return []string { "config", "devices", @@ -171,40 +162,3 @@ func (c *SrvcGrp) xpath(vsys string, vals []string) []string { util.AsEntryXpath(vals), } } - -/** Structs / functions for this namespace. **/ - -type normalizer interface { - Normalize() Entry -} - -type container_v1 struct { - Answer entry_v1 `xml:"result>entry"` -} - -func (o *container_v1) Normalize() Entry { - ans := Entry{ - Name: o.Answer.Name, - Services: util.MemToStr(o.Answer.Services), - Tags: util.MemToStr(o.Answer.Tags), - } - - return ans -} - -type entry_v1 struct { - XMLName xml.Name `xml:"entry"` - Name string `xml:"name,attr"` - Services *util.Member `xml:"members"` - Tags *util.Member `xml:"tag"` -} - -func specify_v1(e Entry) interface{} { - ans := entry_v1{ - Name: e.Name, - Services: util.StrToMem(e.Services), - Tags: util.StrToMem(e.Tags), - } - - return ans -} diff --git a/objs/srvcgrp/srvcgrp_test.go b/objs/srvcgrp/fw_test.go similarity index 95% rename from objs/srvcgrp/srvcgrp_test.go rename to objs/srvcgrp/fw_test.go index bf8bfde0..9ca36cc4 100644 --- a/objs/srvcgrp/srvcgrp_test.go +++ b/objs/srvcgrp/fw_test.go @@ -8,7 +8,7 @@ import ( ) -func TestNormalization(t *testing.T) { +func TestFwNormalization(t *testing.T) { testCases := []struct{ desc string vsys string @@ -30,7 +30,7 @@ func TestNormalization(t *testing.T) { } mc := &testdata.MockClient{} - ns := &SrvcGrp{} + ns := &FwSrvcGrp{} ns.Initialize(mc) for _, tc := range testCases { diff --git a/objs/srvcgrp/pano.go b/objs/srvcgrp/pano.go new file mode 100644 index 00000000..d30eeae7 --- /dev/null +++ b/objs/srvcgrp/pano.go @@ -0,0 +1,164 @@ +package srvcgrp + +import ( + "fmt" + "encoding/xml" + + "github.com/PaloAltoNetworks/pango/util" +) + + +// PanoSrvcGrp is a namespace struct, included as part of pango.Client. +type PanoSrvcGrp struct { + con util.XapiClient +} + +// Initialize is invoked when Initialize on the pango.Client is called. +func (c *PanoSrvcGrp) Initialize(con util.XapiClient) { + c.con = con +} + +// GetList performs GET to retrieve a list of service groups. +func (c *PanoSrvcGrp) GetList(dg string) ([]string, error) { + c.con.LogQuery("(get) list of service groups") + path := c.xpath(dg, nil) + return c.con.EntryListUsing(c.con.Get, path[:len(path) - 1]) +} + +// ShowList performs SHOW to retrieve a list of service groups. +func (c *PanoSrvcGrp) ShowList(dg string) ([]string, error) { + c.con.LogQuery("(show) list of service groups") + path := c.xpath(dg, nil) + return c.con.EntryListUsing(c.con.Show, path[:len(path) - 1]) +} + +// Get performs GET to retrieve information for the given service group. +func (c *PanoSrvcGrp) Get(dg, name string) (Entry, error) { + c.con.LogQuery("(get) service group %q", name) + return c.details(c.con.Get, dg, name) +} + +// Get performs SHOW to retrieve information for the given service group. +func (c *PanoSrvcGrp) Show(dg, name string) (Entry, error) { + c.con.LogQuery("(show) service group %q", name) + return c.details(c.con.Show, dg, name) +} + +// Set performs SET to create / update one or more service groups. +func (c *PanoSrvcGrp) Set(dg string, e ...Entry) error { + var err error + + if len(e) == 0 { + return nil + } + + _, fn := c.versioning() + names := make([]string, len(e)) + + // Build up the struct with the given configs. + d := util.BulkElement{XMLName: xml.Name{Local: "service-group"}} + for i := range e { + d.Data = append(d.Data, fn(e[i])) + names[i] = e[i].Name + } + c.con.LogAction("(set) service groups: %v", names) + + // Set xpath. + path := c.xpath(dg, names) + if len(e) == 1 { + path = path[:len(path) - 1] + } else { + path = path[:len(path) - 2] + } + + // Create the objects. + _, err = c.con.Set(path, d.Config(), nil, nil) + return err +} + +// Edit performs EDIT to create / update a service group. +func (c *PanoSrvcGrp) Edit(dg string, e Entry) error { + var err error + + _, fn := c.versioning() + + c.con.LogAction("(edit) service group %q", e.Name) + + // Set xpath. + path := c.xpath(dg, []string{e.Name}) + + // Create the objects. + _, err = c.con.Edit(path, fn(e), nil, nil) + return err +} + +// Delete removes the given service groups from the firewall. +// +// Service groups can be either a string or an Entry object. +func (c *PanoSrvcGrp) Delete(dg string, e ...interface{}) error { + var err error + + if len(e) == 0 { + return nil + } + + names := make([]string, len(e)) + for i := range e { + switch v := e[i].(type) { + case string: + names[i] = v + case Entry: + names[i] = v.Name + default: + return fmt.Errorf("Unsupported type to delete: %s", v) + } + } + c.con.LogAction("(delete) service groups: %v", names) + + path := c.xpath(dg, names) + _, err = c.con.Delete(path, nil, nil) + return err +} + +/** Internal functions for the PanoSrvcGrp struct **/ + +func (c *PanoSrvcGrp) versioning() (normalizer, func(Entry) (interface{})) { + return &container_v1{}, specify_v1 +} + +func (c *PanoSrvcGrp) details(fn util.Retriever, dg, name string) (Entry, error) { + path := c.xpath(dg, []string{name}) + obj, _ := c.versioning() + _, err := fn(path, nil, obj) + if err != nil { + return Entry{}, err + } + ans := obj.Normalize() + + return ans, nil +} + +func (c *PanoSrvcGrp) xpath(dg string, vals []string) []string { + if dg == "" { + dg = "shared" + } + + if dg == "shared" { + return []string{ + "config", + "shared", + "service-group", + util.AsEntryXpath(vals), + } + } + + return []string { + "config", + "devices", + util.AsEntryXpath([]string{"localhost.localdomain"}), + "device-group", + util.AsEntryXpath([]string{dg}), + "service-group", + util.AsEntryXpath(vals), + } +} diff --git a/objs/srvcgrp/pano_test.go b/objs/srvcgrp/pano_test.go new file mode 100644 index 00000000..0d11fbdd --- /dev/null +++ b/objs/srvcgrp/pano_test.go @@ -0,0 +1,55 @@ +package srvcgrp + +import ( + "testing" + "reflect" + + "github.com/PaloAltoNetworks/pango/testdata" +) + + +func TestPanoNormalization(t *testing.T) { + testCases := []struct{ + desc string + dg string + conf Entry + }{ + {"test no services", "", Entry{ + Name: "one", + Tags: []string{"one", "two"}, + }}, + {"test one service", "dg1", Entry{ + Name: "two", + Services: []string{"svc1"}, + Tags: []string{"single"}, + }}, + {"test two services", "dg2", Entry{ + Name: "three", + Services: []string{"svc1", "svc2"}, + }}, + } + + mc := &testdata.MockClient{} + ns := &PanoSrvcGrp{} + ns.Initialize(mc) + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + mc.Reset() + mc.AddResp("") + err := ns.Set(tc.dg, tc.conf) + if err != nil { + t.Errorf("Error in set: %s", err) + } else { + mc.AddResp(mc.Elm) + r, err := ns.Get(tc.dg, tc.conf.Name) + if err != nil { + t.Errorf("Error in get: %s", err) + } else if !reflect.DeepEqual(tc.conf, r) { + t.Errorf("%#v != %#v", tc.conf, r) + } + } + }) + } +} + diff --git a/objs/tags/doc.go b/objs/tags/doc.go new file mode 100644 index 00000000..2a028a05 --- /dev/null +++ b/objs/tags/doc.go @@ -0,0 +1,4 @@ +// Package tags is the client.Objects.Tags namespace. +// +// Normalized object: Entry +package tags diff --git a/objs/tags/entry.go b/objs/tags/entry.go new file mode 100644 index 00000000..28cd6824 --- /dev/null +++ b/objs/tags/entry.go @@ -0,0 +1,124 @@ +package tags + +import ( + "fmt" + "encoding/xml" +) + +// These are the color constants you can use in Entry.SetColor(). Note that +// each version of PANOS has added colors, so if you are looking for maximum +// compatibility, only use the first 16 colors (17 including None). +const ( + None = iota + Red + Green + Blue + Yellow + Copper + Orange + Purple + Gray + LightGreen + Cyan + LightGray + BlueGray + Lime + Black + Gold + Brown + Olive + _ + Maroon + RedOrange + YellowOrange + ForestGreen + TurquoiseBlue + AzureBlue + CeruleanBlue + MidnightBlue + MediumBlue + CobaltBlue + BlueViolet + MediumViolet + MediumRose + Lavender + Orchid + Thistle + Peach + Salmon + Magenta + RedViolet + Mahogany + BurntSienna + Chestnut +) + +// Entry is a normalized, version independent representation of an +// administrative tag. Note that colors should be set to a string +// such as `color5` or `color13`. If you want to set a color using the +// color name (e.g. - "red"), use the SetColor function. +type Entry struct { + Name string + Color string + Comment string +} + +// Copy copies the information from source Entry `s` to this object. As the +// Name field relates to the XPATH of this object, this field is not copied. +func (o *Entry) Copy(s Entry) { + o.Color = s.Color + o.Comment = s.Comment +} + +// SetColor takes a color constant (e.g. - Olive) and converts it to a color +// enum (e.g. - "color17"). +// +// Note that color availability varies according to version: +// +// * 6.1 - 7.0: None - Brown +// * 7.1 - 8.0: None - Olive +// * 8.1: None - Chestnut +func (o *Entry) SetColor(v int) { + if v == 0 { + o.Color = "" + } else { + o.Color = fmt.Sprintf("color%d", v) + } +} + +/** Structs / functions for normalization. **/ + +type normalizer interface { + Normalize() Entry +} + +type container_v1 struct { + Answer entry_v1 `xml:"result>entry"` +} + +func (o *container_v1) Normalize() Entry { + ans := Entry{ + Name: o.Answer.Name, + Color: o.Answer.Color, + Comment: o.Answer.Comment, + } + + return ans +} + +type entry_v1 struct { + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + Color string `xml:"color,omitempty"` + Comment string `xml:"comments,omitempty"` +} + +func specify_v1(e Entry) interface{} { + ans := entry_v1{ + Name: e.Name, + Color: e.Color, + Comment: e.Comment, + } + + return ans +} diff --git a/objs/tags/tags.go b/objs/tags/fw.go similarity index 50% rename from objs/tags/tags.go rename to objs/tags/fw.go index e36342ca..3ae45ca2 100644 --- a/objs/tags/tags.go +++ b/objs/tags/fw.go @@ -1,6 +1,3 @@ -// Package tags is the client.Objects.Tags namespace. -// -// Normalized object: Entry package tags import ( @@ -10,125 +7,44 @@ import ( "github.com/PaloAltoNetworks/pango/util" ) -// These are the color constants you can use in Entry.SetColor(). Note that -// each version of PANOS has added colors, so if you are looking for maximum -// compatibility, only use the first 16 colors (17 including None). -const ( - None = iota - Red - Green - Blue - Yellow - Copper - Orange - Purple - Gray - LightGreen - Cyan - LightGray - BlueGray - Lime - Black - Gold - Brown - Olive - _ - Maroon - RedOrange - YellowOrange - ForestGreen - TurquoiseBlue - AzureBlue - CeruleanBlue - MidnightBlue - MediumBlue - CobaltBlue - BlueViolet - MediumViolet - MediumRose - Lavender - Orchid - Thistle - Peach - Salmon - Magenta - RedViolet - Mahogany - BurntSienna - Chestnut -) - -// Entry is a normalized, version independent representation of an -// administrative tag. Note that colors should be set to a string -// such as `color5` or `color13`. If you want to set a color using the -// color name (e.g. - "red"), use the SetColor function. -type Entry struct { - Name string - Color string - Comment string -} - -// Copy copies the information from source Entry `s` to this object. As the -// Name field relates to the XPATH of this object, this field is not copied. -func (o *Entry) Copy(s Entry) { - o.Color = s.Color - o.Comment = s.Comment -} - -// SetColor takes a color constant (e.g. - Olive) and converts it to a color -// enum (e.g. - "color17"). -// -// Note that color availability varies according to version: -// -// * 6.1 - 7.0: None - Brown -// * 7.1 - 8.0: None - Olive -// * 8.1: None - Chestnut -func (o *Entry) SetColor(v int) { - if v == 0 { - o.Color = "" - } else { - o.Color = fmt.Sprintf("color%d", v) - } -} - -// Tags is a namespace struct, included as part of pango.Client. -type Tags struct { +// FwTags is a namespace struct, included as part of pango.Client. +type FwTags struct { con util.XapiClient } // Initialize is invoked when Initialize on the pango.Client is called. -func (c *Tags) Initialize(con util.XapiClient) { +func (c *FwTags) Initialize(con util.XapiClient) { c.con = con } // GetList performs GET to retrieve a list of administrative tags. -func (c *Tags) GetList(vsys string) ([]string, error) { +func (c *FwTags) GetList(vsys string) ([]string, error) { c.con.LogQuery("(get) list of administrative tags") path := c.xpath(vsys, nil) return c.con.EntryListUsing(c.con.Get, path[:len(path) - 1]) } // ShowList performs SHOW to retrieve a list of administrative tags. -func (c *Tags) ShowList(vsys string) ([]string, error) { +func (c *FwTags) ShowList(vsys string) ([]string, error) { c.con.LogQuery("(show) list of administrative tags") path := c.xpath(vsys, nil) return c.con.EntryListUsing(c.con.Show, path[:len(path) - 1]) } // Get performs GET to retrieve information for the given administrative tag. -func (c *Tags) Get(vsys, name string) (Entry, error) { +func (c *FwTags) Get(vsys, name string) (Entry, error) { c.con.LogQuery("(get) administrative tag %q", name) return c.details(c.con.Get, vsys, name) } // Get performs SHOW to retrieve information for the given administrative tag. -func (c *Tags) Show(vsys, name string) (Entry, error) { +func (c *FwTags) Show(vsys, name string) (Entry, error) { c.con.LogQuery("(show) administrative tag %q", name) return c.details(c.con.Show, vsys, name) } // Set performs SET to create / update one or more administrative tags. -func (c *Tags) Set(vsys string, e ...Entry) error { +func (c *FwTags) Set(vsys string, e ...Entry) error { var err error if len(e) == 0 { @@ -160,7 +76,7 @@ func (c *Tags) Set(vsys string, e ...Entry) error { } // Edit performs EDIT to create / update an administrative tag. -func (c *Tags) Edit(vsys string, e Entry) error { +func (c *FwTags) Edit(vsys string, e Entry) error { var err error _, fn := c.versioning() @@ -178,7 +94,7 @@ func (c *Tags) Edit(vsys string, e Entry) error { // Delete removes the given administrative tags from the firewall. // // Administrative tags can be either a string or an Entry object. -func (c *Tags) Delete(vsys string, e ...interface{}) error { +func (c *FwTags) Delete(vsys string, e ...interface{}) error { var err error if len(e) == 0 { @@ -203,13 +119,13 @@ func (c *Tags) Delete(vsys string, e ...interface{}) error { return err } -/** Internal functions for the Tags struct **/ +/** Internal functions for the FwTags struct **/ -func (c *Tags) versioning() (normalizer, func(Entry) (interface{})) { +func (c *FwTags) versioning() (normalizer, func(Entry) (interface{})) { return &container_v1{}, specify_v1 } -func (c *Tags) details(fn util.Retriever, vsys, name string) (Entry, error) { +func (c *FwTags) details(fn util.Retriever, vsys, name string) (Entry, error) { path := c.xpath(vsys, []string{name}) obj, _ := c.versioning() _, err := fn(path, nil, obj) @@ -221,11 +137,20 @@ func (c *Tags) details(fn util.Retriever, vsys, name string) (Entry, error) { return ans, nil } -func (c *Tags) xpath(vsys string, vals []string) []string { +func (c *FwTags) xpath(vsys string, vals []string) []string { if vsys == "" { vsys = "vsys1" } + if vsys == "shared" { + return []string { + "config", + "shared", + "tag", + util.AsEntryXpath(vals), + } + } + return []string { "config", "devices", @@ -236,40 +161,3 @@ func (c *Tags) xpath(vsys string, vals []string) []string { util.AsEntryXpath(vals), } } - -/** Structs / functions for this namespace. **/ - -type normalizer interface { - Normalize() Entry -} - -type container_v1 struct { - Answer entry_v1 `xml:"result>entry"` -} - -func (o *container_v1) Normalize() Entry { - ans := Entry{ - Name: o.Answer.Name, - Color: o.Answer.Color, - Comment: o.Answer.Comment, - } - - return ans -} - -type entry_v1 struct { - XMLName xml.Name `xml:"entry"` - Name string `xml:"name,attr"` - Color string `xml:"color,omitempty"` - Comment string `xml:"comments,omitempty"` -} - -func specify_v1(e Entry) interface{} { - ans := entry_v1{ - Name: e.Name, - Color: e.Color, - Comment: e.Comment, - } - - return ans -} diff --git a/objs/tags/tags_test.go b/objs/tags/fw_test.go similarity index 95% rename from objs/tags/tags_test.go rename to objs/tags/fw_test.go index 75667c3a..c661ac67 100644 --- a/objs/tags/tags_test.go +++ b/objs/tags/fw_test.go @@ -8,7 +8,7 @@ import ( ) -func TestNormalization(t *testing.T) { +func TestFwNormalization(t *testing.T) { testCases := []struct{ desc string vsys string @@ -33,7 +33,7 @@ func TestNormalization(t *testing.T) { } mc := &testdata.MockClient{} - ns := &Tags{} + ns := &FwTags{} ns.Initialize(mc) for _, tc := range testCases { diff --git a/objs/tags/pano.go b/objs/tags/pano.go new file mode 100644 index 00000000..5a68a96a --- /dev/null +++ b/objs/tags/pano.go @@ -0,0 +1,163 @@ +package tags + +import ( + "fmt" + "encoding/xml" + + "github.com/PaloAltoNetworks/pango/util" +) + +// PanoTags is a namespace struct, included as part of pango.Client. +type PanoTags struct { + con util.XapiClient +} + +// Initialize is invoked when Initialize on the pango.Client is called. +func (c *PanoTags) Initialize(con util.XapiClient) { + c.con = con +} + +// GetList performs GET to retrieve a list of administrative tags. +func (c *PanoTags) GetList(dg string) ([]string, error) { + c.con.LogQuery("(get) list of administrative tags") + path := c.xpath(dg, nil) + return c.con.EntryListUsing(c.con.Get, path[:len(path) - 1]) +} + +// ShowList performs SHOW to retrieve a list of administrative tags. +func (c *PanoTags) ShowList(dg string) ([]string, error) { + c.con.LogQuery("(show) list of administrative tags") + path := c.xpath(dg, nil) + return c.con.EntryListUsing(c.con.Show, path[:len(path) - 1]) +} + +// Get performs GET to retrieve information for the given administrative tag. +func (c *PanoTags) Get(dg, name string) (Entry, error) { + c.con.LogQuery("(get) administrative tag %q", name) + return c.details(c.con.Get, dg, name) +} + +// Get performs SHOW to retrieve information for the given administrative tag. +func (c *PanoTags) Show(dg, name string) (Entry, error) { + c.con.LogQuery("(show) administrative tag %q", name) + return c.details(c.con.Show, dg, name) +} + +// Set performs SET to create / update one or more administrative tags. +func (c *PanoTags) Set(dg string, e ...Entry) error { + var err error + + if len(e) == 0 { + return nil + } + + _, fn := c.versioning() + names := make([]string, len(e)) + + // Build up the struct with the given configs. + d := util.BulkElement{XMLName: xml.Name{Local: "tag"}} + for i := range e { + d.Data = append(d.Data, fn(e[i])) + names[i] = e[i].Name + } + c.con.LogAction("(set) administrative tags: %v", names) + + // Set xpath. + path := c.xpath(dg, names) + if len(e) == 1 { + path = path[:len(path) - 1] + } else { + path = path[:len(path) - 2] + } + + // Create the objects. + _, err = c.con.Set(path, d.Config(), nil, nil) + return err +} + +// Edit performs EDIT to create / update an administrative tag. +func (c *PanoTags) Edit(dg string, e Entry) error { + var err error + + _, fn := c.versioning() + + c.con.LogAction("(edit) administrative tag %q", e.Name) + + // Set xpath. + path := c.xpath(dg, []string{e.Name}) + + // Create the objects. + _, err = c.con.Edit(path, fn(e), nil, nil) + return err +} + +// Delete removes the given administrative tags from the firewall. +// +// Administrative tags can be either a string or an Entry object. +func (c *PanoTags) Delete(dg string, e ...interface{}) error { + var err error + + if len(e) == 0 { + return nil + } + + names := make([]string, len(e)) + for i := range e { + switch v := e[i].(type) { + case string: + names[i] = v + case Entry: + names[i] = v.Name + default: + return fmt.Errorf("Unsupported type to delete: %s", v) + } + } + c.con.LogAction("(delete) administrative tags: %v", names) + + path := c.xpath(dg, names) + _, err = c.con.Delete(path, nil, nil) + return err +} + +/** Internal functions for the PanoTags struct **/ + +func (c *PanoTags) versioning() (normalizer, func(Entry) (interface{})) { + return &container_v1{}, specify_v1 +} + +func (c *PanoTags) details(fn util.Retriever, dg, name string) (Entry, error) { + path := c.xpath(dg, []string{name}) + obj, _ := c.versioning() + _, err := fn(path, nil, obj) + if err != nil { + return Entry{}, err + } + ans := obj.Normalize() + + return ans, nil +} + +func (c *PanoTags) xpath(dg string, vals []string) []string { + if dg == "" { + dg = "shared" + } + + if dg == "shared" { + return []string { + "config", + "shared", + "tag", + util.AsEntryXpath(vals), + } + } + + return []string { + "config", + "devices", + util.AsEntryXpath([]string{"localhost.localdomain"}), + "device-group", + util.AsEntryXpath([]string{dg}), + "tag", + util.AsEntryXpath(vals), + } +} diff --git a/objs/tags/pano_test.go b/objs/tags/pano_test.go new file mode 100644 index 00000000..e16be3d9 --- /dev/null +++ b/objs/tags/pano_test.go @@ -0,0 +1,58 @@ +package tags + +import ( + "testing" + "reflect" + + "github.com/PaloAltoNetworks/pango/testdata" +) + + +func TestPanoNormalization(t *testing.T) { + testCases := []struct{ + desc string + dg string + conf Entry + }{ + {"test with all fields", "", Entry{ + Name: "one", + Color: "color1", + Comment: "first test", + }}, + {"test no color", "", Entry{ + Name: "two", + Comment: "second test", + }}, + {"test no comment", "dg1", Entry{ + Name: "three", + Color: "color3", + }}, + {"test no color or comment", "dg2", Entry{ + Name: "four", + }}, + } + + mc := &testdata.MockClient{} + ns := &PanoTags{} + ns.Initialize(mc) + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + mc.Reset() + mc.AddResp("") + err := ns.Set(tc.dg, tc.conf) + if err != nil { + t.Errorf("Error in set: %s", err) + } else { + mc.AddResp(mc.Elm) + r, err := ns.Get(tc.dg, tc.conf.Name) + if err != nil { + t.Errorf("Error in get: %s", err) + } else if !reflect.DeepEqual(tc.conf, r) { + t.Errorf("%#v != %#v", tc.conf, r) + } + } + }) + } +} + diff --git a/pano.go b/pano.go new file mode 100644 index 00000000..263d86d5 --- /dev/null +++ b/pano.go @@ -0,0 +1,143 @@ +package pango + +import ( + "encoding/xml" + + "github.com/PaloAltoNetworks/pango/util" + + // Various namespace imports. + "github.com/PaloAltoNetworks/pango/objs" + "github.com/PaloAltoNetworks/pango/pnrm" + "github.com/PaloAltoNetworks/pango/licen" + "github.com/PaloAltoNetworks/pango/userid" +) + + +// Panorama is a panorama specific client, providing version safe functions +// for the PAN-OS Xpath API methods. After creating the object, invoke +// Initialize() to prepare it for use. +// +// It has the following namespaces: +// * Licensing +// * UserId +type Panorama struct { + Client + + // Namespaces + Licensing *licen.Licen + UserId *userid.UserId + Panorama *pnrm.Pnrm + Objects *objs.PanoObjs +} + +// Initialize does some initial setup of the Panorama 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 *Panorama) 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" + } + c.initNamespaces() + + return nil +} + +// CommitAll performs a Panorama commit-all. +// +// Param dg is the device group you want to commit-all on. Note that all other +// params are ignored / unused if the device group is left empty. +// +// Param desc is the optional commit description message you want associated +// with the commit. +// +// Param serials is the list of serial numbers you want to limit the commit-all +// to that are also in the device group dg. +// +// Param tmpl should be true if you want to push template config as well. +// +// Param sync should be true if you want this function to block until the +// commit job completes. +// +// Commits result in a job being submitted to the backend. The job ID and +// if an error was encountered or not are returned from this function. +func (c *Panorama) CommitAll(dg, desc string, serials []string, tmpl, sync bool) (uint, error) { + c.LogAction("(commit-all) %q", desc) + + req := panoDgCommit{} + if dg != "" { + sp := sharedPolicy{ + Description: desc, + WithTemplate: util.YesNo(tmpl), + Dg: deviceGroup{ + Entry: deviceGroupEntry{ + Name: dg, + Devices: util.StrToEnt(serials), + }, + }, + } + req.Policy = &sp + } + + job, _, err := c.CommitConfig(req, "all", nil) + if err != nil || !sync || job == 0 { + return job, err + } + + return job, c.WaitForJob(job, nil) +} + +/** Private functions **/ + +func (c *Panorama) initNamespaces() { + c.Licensing = &licen.Licen{} + c.Licensing.Initialize(c) + + c.UserId = &userid.UserId{} + c.UserId.Initialize(c) + + c.Panorama = &pnrm.Pnrm{} + c.Panorama.Initialize(c) + + c.Objects = &objs.PanoObjs{} + c.Objects.Initialize(c) +} + +/** Internal structs / functions **/ + +type panoDgCommit struct { + XMLName xml.Name `xml:"commit-all"` + Policy *sharedPolicy `xml:"shared-policy"` +} + +type sharedPolicy struct { + Dg deviceGroup `xml:"device-group"` + Description string `xml:"description,omitempty"` + WithTemplate string `xml:"include-template"` +} + +type deviceGroup struct { + Entry deviceGroupEntry `xml:"entry"` +} + +type deviceGroupEntry struct { + Name string `xml:"name,attr"` + Devices *util.Entry `xml:"devices"` +} diff --git a/pnrm/dg/dg.go b/pnrm/dg/dg.go new file mode 100644 index 00000000..b84ad9a5 --- /dev/null +++ b/pnrm/dg/dg.go @@ -0,0 +1,205 @@ +/* +Package dg is the client.Panorama.DeviceGroup namespace. + +Normalized object: Entry +*/ +package dg + +import ( + "fmt" + "encoding/xml" + + "github.com/PaloAltoNetworks/pango/util" +) + +// Entry is a normalized, version independent representation of a virtual +// router. +type Entry struct { + Name string + Description string + Devices []string +} + +// Copy copies the information from source's Entry `s` to this object. As the +// Name field relates to the XPATH of this object, this field is not copied. +func (o *Entry) Copy(s Entry) { + o.Description = s.Description + o.Devices = s.Devices +} + +// Dg is the client.Panorama.DeviceGroup namespace. +type Dg struct { + con util.XapiClient +} + +// Initialize is invoked by client.Initialize(). +func (c *Dg) Initialize(con util.XapiClient) { + c.con = con +} + +// ShowList performs SHOW to retrieve a list of device groups. +func (c *Dg) ShowList() ([]string, error) { + c.con.LogQuery("(show) list of device groups") + path := c.xpath(nil) + return c.con.EntryListUsing(c.con.Show, path[:len(path) - 1]) +} + +// GetList performs GET to retrieve a list of device groups. +func (c *Dg) GetList() ([]string, error) { + c.con.LogQuery("(get) list of device groups") + path := c.xpath(nil) + return c.con.EntryListUsing(c.con.Get, path[:len(path) - 1]) +} + +// Get performs GET to retrieve information for the given device group. +func (c *Dg) Get(name string) (Entry, error) { + c.con.LogQuery("(get) device group %q", name) + return c.details(c.con.Get, name) +} + +// Show performs SHOW to retrieve information for the given device group. +func (c *Dg) Show(name string) (Entry, error) { + c.con.LogQuery("(show) device group %q", name) + return c.details(c.con.Show, name) +} + +// Set performs SET to create / update one or more device groups. +func (c *Dg) Set(e ...Entry) error { + var err error + + if len(e) == 0 { + return nil + } + + _, fn := c.versioning() + names := make([]string, len(e)) + + // Build up the struct with the given configs. + d := util.BulkElement{XMLName: xml.Name{Local: "device-group"}} + for i := range e { + d.Data = append(d.Data, fn(e[i])) + names[i] = e[i].Name + } + c.con.LogAction("(set) device groups: %v", names) + + // Set xpath. + path := c.xpath(names) + if len(e) == 1 { + path = path[:len(path) - 1] + } else { + path = path[:len(path) - 2] + } + + // Create the device groups. + _, err = c.con.Set(path, d.Config(), nil, nil) + return err +} + +// Edit performs EDIT to create / update a device group. +func (c *Dg) Edit(e Entry) error { + var err error + + _, fn := c.versioning() + + c.con.LogAction("(edit) device group %q", e.Name) + + // Set xpath. + path := c.xpath([]string{e.Name}) + + // Edit the device group. + _, err = c.con.Edit(path, fn(e), nil, nil) + return err +} + +// Delete removes the given device groups from the firewall. +// +// Device groups can be a string or an Entry object. +func (c *Dg) Delete(e ...interface{}) error { + var err error + + if len(e) == 0 { + return nil + } + + names := make([]string, len(e)) + for i := range e { + switch v := e[i].(type) { + case string: + names[i] = v + case Entry: + names[i] = v.Name + default: + return fmt.Errorf("Unknown type sent to delete: %s", v) + } + } + c.con.LogAction("(delete) device groups: %v", names) + + // Remove the device groups. + path := c.xpath(names) + _, err = c.con.Delete(path, nil, nil) + return err +} + +/** Internal functions for the Dg struct **/ + +func (c *Dg) versioning() (normalizer, func(Entry) (interface{})) { + return &container_v1{}, specify_v1 +} + +func (c *Dg) details(fn util.Retriever, name string) (Entry, error) { + path := c.xpath([]string{name}) + obj, _ := c.versioning() + if _, err := fn(path, nil, obj); err != nil { + return Entry{}, err + } + ans := obj.Normalize() + + return ans, nil +} + +func (c *Dg) xpath(vals []string) []string { + return []string{ + "config", + "devices", + util.AsEntryXpath([]string{"localhost.localdomain"}), + "device-group", + util.AsEntryXpath(vals), + } +} + +/** Structs / functions for normalization. **/ + +type normalizer interface { + Normalize() Entry +} + +type container_v1 struct { + Answer entry_v1 `xml:"result>entry"` +} + +func (o *container_v1) Normalize() Entry { + ans := Entry{ + Name: o.Answer.Name, + Description: o.Answer.Description, + Devices: util.EntToStr(o.Answer.Devices), + } + + return ans +} + +type entry_v1 struct { + XMLName xml.Name `xml:"entry"` + Name string `xml:"name,attr"` + Description string `xml:"description"` + Devices *util.Entry `xml:"devices"` +} + +func specify_v1(e Entry) interface{} { + ans := entry_v1{ + Name: e.Name, + Description: e.Description, + Devices: util.StrToEnt(e.Devices), + } + + return ans +} diff --git a/pnrm/dg/dg_test.go b/pnrm/dg/dg_test.go new file mode 100644 index 00000000..efed8540 --- /dev/null +++ b/pnrm/dg/dg_test.go @@ -0,0 +1,55 @@ +package dg + +import ( + "testing" + "reflect" + + "github.com/PaloAltoNetworks/pango/testdata" +) + + +func TestNormalization(t *testing.T) { + testCases := []struct{ + desc string + conf Entry + }{ + {"test one", Entry{ + Name: "one", + Description: "my description", + }}, + {"test two", Entry{ + Name: "two", + Description: "with devices", + Devices: []string{"001234", "998765"}, + }}, + {"test three", Entry{ + Name: "three", + Description: "with one device", + Devices: []string{"001234"}, + }}, + } + + mc := &testdata.MockClient{} + ns := &Dg{} + ns.Initialize(mc) + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + mc.Reset() + mc.AddResp("") + err := ns.Set(tc.conf) + if err != nil { + t.Errorf("Error in set: %s", err) + } else { + mc.AddResp(mc.Elm) + r, err := ns.Get(tc.conf.Name) + if err != nil { + t.Errorf("Error in get: %s", err) + } else if !reflect.DeepEqual(tc.conf, r) { + t.Errorf("%#v != %#v", tc.conf, r) + } + } + }) + } +} + diff --git a/pnrm/pnrm.go b/pnrm/pnrm.go new file mode 100644 index 00000000..d0d446bd --- /dev/null +++ b/pnrm/pnrm.go @@ -0,0 +1,23 @@ +/* +Package pnrm is the client.Panorama namespace. +*/ +package pnrm + + +import ( + "github.com/PaloAltoNetworks/pango/util" + + "github.com/PaloAltoNetworks/pango/pnrm/dg" +) + + +// Pnrm is the panorama.DeviceGroup namespace. +type Pnrm struct { + DeviceGroup *dg.Dg +} + +// Initialize is invoked on panorama.Initialize(). +func (c *Pnrm) Initialize(i util.XapiClient) { + c.DeviceGroup = &dg.Dg{} + c.DeviceGroup.Initialize(i) +} diff --git a/pnrm/pnrm_test.go b/pnrm/pnrm_test.go new file mode 100644 index 00000000..c57a2d8b --- /dev/null +++ b/pnrm/pnrm_test.go @@ -0,0 +1,19 @@ +package pnrm + +import ( + "testing" + + "github.com/PaloAltoNetworks/pango/testdata" +) + + +func TestPanoInitialize(t *testing.T) { + mc := &testdata.MockClient{} + o := &Pnrm{} + o.Initialize(mc) + + if o.DeviceGroup == nil { + t.Fail() + } +} + diff --git a/util/util.go b/util/util.go index eef20afa..a8b23d89 100644 --- a/util/util.go +++ b/util/util.go @@ -246,4 +246,11 @@ type BasicJob struct { Result string `xml:"result>job>result"` Progress uint `xml:"result>job>progress"` Details []string `xml:"result>job>details>line"` + Devices []devJob `xml:"result>job>devices>entry"` +} + +// Internally used by BasicJob for panorama commit-all. +type devJob struct { + Serial string `xml:"serial-no"` + Result string `xml:"result"` }