|
1 | 1 | package provider_test |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "context" |
| 5 | + "encoding/xml" |
4 | 6 | "fmt" |
5 | 7 | "testing" |
6 | 8 |
|
| 9 | + "github.com/PaloAltoNetworks/pango/generic" |
| 10 | + "github.com/PaloAltoNetworks/pango/panorama/template_stack" |
| 11 | + "github.com/PaloAltoNetworks/pango/util" |
| 12 | + "github.com/PaloAltoNetworks/pango/xmlapi" |
7 | 13 | "github.com/hashicorp/terraform-plugin-testing/config" |
8 | 14 | "github.com/hashicorp/terraform-plugin-testing/helper/acctest" |
9 | 15 | "github.com/hashicorp/terraform-plugin-testing/helper/resource" |
10 | 16 | "github.com/hashicorp/terraform-plugin-testing/knownvalue" |
11 | 17 | "github.com/hashicorp/terraform-plugin-testing/statecheck" |
| 18 | + "github.com/hashicorp/terraform-plugin-testing/terraform" |
12 | 19 | "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" |
13 | 20 | ) |
14 | 21 |
|
@@ -399,3 +406,230 @@ resource "panos_template_stack" "example" { |
399 | 406 | default_vsys = "vsys1" |
400 | 407 | } |
401 | 408 | ` |
| 409 | + |
| 410 | +// TestAccTemplateStack_DeviceVariablePreservation verifies that updating a |
| 411 | +// template stack (e.g. changing description) does not delete per-device |
| 412 | +// variable overrides stored as sub-elements of device entries. |
| 413 | +// |
| 414 | +// This reproduces a reported bug where the PUT request sends device entries |
| 415 | +// without their child elements, causing PAN-OS to delete per-device template |
| 416 | +// variable overrides. |
| 417 | +func TestAccTemplateStack_DeviceVariablePreservation(t *testing.T) { |
| 418 | + t.Parallel() |
| 419 | + |
| 420 | + nameSuffix := acctest.RandStringFromCharSet(6, acctest.CharSetAlphaNum) |
| 421 | + prefix := fmt.Sprintf("test-acc-%s", nameSuffix) |
| 422 | + |
| 423 | + suffix := acctest.RandStringFromCharSet(13, "0123456789") |
| 424 | + serialNumber := fmt.Sprintf("00%s", suffix) |
| 425 | + |
| 426 | + stackName := fmt.Sprintf("%s-stack", prefix) |
| 427 | + |
| 428 | + location := config.ObjectVariable(map[string]config.Variable{ |
| 429 | + "panorama": config.ObjectVariable(map[string]config.Variable{}), |
| 430 | + }) |
| 431 | + |
| 432 | + configVars := map[string]config.Variable{ |
| 433 | + "prefix": config.StringVariable(prefix), |
| 434 | + "location": location, |
| 435 | + "serial_number": config.StringVariable(serialNumber), |
| 436 | + } |
| 437 | + |
| 438 | + // Build the xpath for direct SDK access since Read/Update have a bug |
| 439 | + // with name formatting. Use ReadWithXpath/UpdateWithXpath instead. |
| 440 | + loc := template_stack.Location{ |
| 441 | + Panorama: &template_stack.PanoramaLocation{ |
| 442 | + PanoramaDevice: "localhost.localdomain", |
| 443 | + }, |
| 444 | + } |
| 445 | + stackXpathParts, err := loc.XpathWithComponents(sdkClient.Versioning(), util.AsEntryXpath(stackName)) |
| 446 | + if err != nil { |
| 447 | + t.Fatalf("Failed to build template stack xpath: %v", err) |
| 448 | + } |
| 449 | + stackXpath := util.AsXpath(stackXpathParts) |
| 450 | + |
| 451 | + // injectDeviceVariable uses a direct API call to add a per-device variable |
| 452 | + // override to the device entry, simulating a user setting local values |
| 453 | + // via the Panorama UI. |
| 454 | + // |
| 455 | + // We use sdkClient.Communicate directly because the SDK's UpdateWithXpath |
| 456 | + // uses SpecMatches which doesn't compare Misc fields, so it would skip |
| 457 | + // the update. |
| 458 | + injectDeviceVariable := func() { |
| 459 | + // Build the xpath to the specific device entry within the template stack. |
| 460 | + deviceXpath := fmt.Sprintf("%s/devices/entry[@name='%s']", stackXpath, serialNumber) |
| 461 | + |
| 462 | + // Override the stack-level template variable with a per-device value. |
| 463 | + // The variable name must match the existing template variable. |
| 464 | + varName := fmt.Sprintf("$%s-var", prefix) |
| 465 | + variableXml := generic.Xml{ |
| 466 | + XMLName: xml.Name{Local: "variable"}, |
| 467 | + Nodes: []generic.Xml{ |
| 468 | + { |
| 469 | + XMLName: xml.Name{Local: "entry"}, |
| 470 | + Name: &varName, |
| 471 | + Nodes: []generic.Xml{ |
| 472 | + { |
| 473 | + XMLName: xml.Name{Local: "type"}, |
| 474 | + Nodes: []generic.Xml{ |
| 475 | + { |
| 476 | + XMLName: xml.Name{Local: "ip-netmask"}, |
| 477 | + Text: []byte("10.0.0.1/24"), |
| 478 | + }, |
| 479 | + }, |
| 480 | + }, |
| 481 | + }, |
| 482 | + }, |
| 483 | + }, |
| 484 | + } |
| 485 | + |
| 486 | + cmd := &xmlapi.Config{ |
| 487 | + Action: "set", |
| 488 | + Xpath: deviceXpath, |
| 489 | + Element: variableXml, |
| 490 | + Target: sdkClient.GetTarget(), |
| 491 | + } |
| 492 | + |
| 493 | + if _, _, err := sdkClient.Communicate(context.TODO(), cmd, false, nil); err != nil { |
| 494 | + t.Fatalf("Failed to inject per-device variable: %v", err) |
| 495 | + } |
| 496 | + } |
| 497 | + |
| 498 | + // checkDeviceVariableExists verifies that the per-device variable data |
| 499 | + // survived the Terraform update by reading the template stack via the |
| 500 | + // pango SDK and checking the device entry's Misc field. |
| 501 | + checkDeviceVariableExists := func(s *terraform.State) error { |
| 502 | + svc := template_stack.NewService(sdkClient) |
| 503 | + |
| 504 | + entry, err := svc.ReadWithXpath(context.TODO(), stackXpath, "get") |
| 505 | + if err != nil { |
| 506 | + return fmt.Errorf("failed to read template stack: %v", err) |
| 507 | + } |
| 508 | + |
| 509 | + for _, device := range entry.Devices { |
| 510 | + if device.Name == serialNumber { |
| 511 | + if len(device.Misc) == 0 { |
| 512 | + return fmt.Errorf( |
| 513 | + "device %s lost its Misc data: per-device variable overrides were deleted during template stack update", |
| 514 | + serialNumber, |
| 515 | + ) |
| 516 | + } |
| 517 | + return nil |
| 518 | + } |
| 519 | + } |
| 520 | + return fmt.Errorf("device %s not found in template stack after update", serialNumber) |
| 521 | + } |
| 522 | + |
| 523 | + resource.Test(t, resource.TestCase{ |
| 524 | + PreCheck: func() { testAccPreCheck(t) }, |
| 525 | + ProtoV6ProviderFactories: testAccProviders, |
| 526 | + Steps: []resource.TestStep{ |
| 527 | + { |
| 528 | + Config: templateStack_DeviceVarPreservation_Step1_Tmpl, |
| 529 | + ConfigVariables: configVars, |
| 530 | + ConfigStateChecks: []statecheck.StateCheck{ |
| 531 | + statecheck.ExpectKnownValue( |
| 532 | + "panos_template_stack.test", |
| 533 | + tfjsonpath.New("description"), |
| 534 | + knownvalue.StringExact("Original description"), |
| 535 | + ), |
| 536 | + statecheck.ExpectKnownValue( |
| 537 | + "panos_template_stack.test", |
| 538 | + tfjsonpath.New("devices"), |
| 539 | + knownvalue.ListExact([]knownvalue.Check{ |
| 540 | + knownvalue.ObjectExact(map[string]knownvalue.Check{ |
| 541 | + "name": knownvalue.StringExact(serialNumber), |
| 542 | + }), |
| 543 | + }), |
| 544 | + ), |
| 545 | + }, |
| 546 | + }, |
| 547 | + { |
| 548 | + PreConfig: injectDeviceVariable, |
| 549 | + Config: templateStack_DeviceVarPreservation_Step2_Tmpl, |
| 550 | + ConfigVariables: configVars, |
| 551 | + Check: checkDeviceVariableExists, |
| 552 | + ConfigStateChecks: []statecheck.StateCheck{ |
| 553 | + statecheck.ExpectKnownValue( |
| 554 | + "panos_template_stack.test", |
| 555 | + tfjsonpath.New("description"), |
| 556 | + knownvalue.StringExact("Updated description"), |
| 557 | + ), |
| 558 | + statecheck.ExpectKnownValue( |
| 559 | + "panos_template_stack.test", |
| 560 | + tfjsonpath.New("devices"), |
| 561 | + knownvalue.ListExact([]knownvalue.Check{ |
| 562 | + knownvalue.ObjectExact(map[string]knownvalue.Check{ |
| 563 | + "name": knownvalue.StringExact(serialNumber), |
| 564 | + }), |
| 565 | + }), |
| 566 | + ), |
| 567 | + }, |
| 568 | + }, |
| 569 | + }, |
| 570 | + }) |
| 571 | +} |
| 572 | + |
| 573 | +const templateStack_DeviceVarPreservation_Step1_Tmpl = ` |
| 574 | +variable "prefix" { type = string } |
| 575 | +variable "location" { type = any } |
| 576 | +variable "serial_number" { type = string } |
| 577 | +
|
| 578 | +resource "panos_template" "test" { |
| 579 | + location = var.location |
| 580 | + name = "${var.prefix}-template" |
| 581 | +} |
| 582 | +
|
| 583 | +resource "panos_firewall_device" "test" { |
| 584 | + location = var.location |
| 585 | + name = var.serial_number |
| 586 | + hostname = "fw-devvar.example.com" |
| 587 | + ip = "192.0.2.10" |
| 588 | +} |
| 589 | +
|
| 590 | +resource "panos_template_stack" "test" { |
| 591 | + location = var.location |
| 592 | + name = "${var.prefix}-stack" |
| 593 | + description = "Original description" |
| 594 | + templates = [panos_template.test.name] |
| 595 | + devices = [{ name = panos_firewall_device.test.name }] |
| 596 | +} |
| 597 | +
|
| 598 | +resource "panos_template_variable" "test" { |
| 599 | + location = { template_stack = { name = panos_template_stack.test.name } } |
| 600 | + name = format("$%s-var", var.prefix) |
| 601 | + type = { ip_netmask = "10.0.0.0/24" } |
| 602 | +} |
| 603 | +` |
| 604 | + |
| 605 | +const templateStack_DeviceVarPreservation_Step2_Tmpl = ` |
| 606 | +variable "prefix" { type = string } |
| 607 | +variable "location" { type = any } |
| 608 | +variable "serial_number" { type = string } |
| 609 | +
|
| 610 | +resource "panos_template" "test" { |
| 611 | + location = var.location |
| 612 | + name = "${var.prefix}-template" |
| 613 | +} |
| 614 | +
|
| 615 | +resource "panos_firewall_device" "test" { |
| 616 | + location = var.location |
| 617 | + name = var.serial_number |
| 618 | + hostname = "fw-devvar.example.com" |
| 619 | + ip = "192.0.2.10" |
| 620 | +} |
| 621 | +
|
| 622 | +resource "panos_template_stack" "test" { |
| 623 | + location = var.location |
| 624 | + name = "${var.prefix}-stack" |
| 625 | + description = "Updated description" |
| 626 | + templates = [panos_template.test.name] |
| 627 | + devices = [{ name = panos_firewall_device.test.name }] |
| 628 | +} |
| 629 | +
|
| 630 | +resource "panos_template_variable" "test" { |
| 631 | + location = { template_stack = { name = panos_template_stack.test.name } } |
| 632 | + name = format("$%s-var", var.prefix) |
| 633 | + type = { ip_netmask = "10.0.0.0/24" } |
| 634 | +} |
| 635 | +` |
0 commit comments