Skip to content

Commit c7c62d5

Browse files
committed
feat(networking): add network, subnet, and secgroup data sources
Lookup by id or filters, acceptance-tested against the CE lab (name -> id resolution). Records the networking family CHANGELOG entry.
1 parent 7406dec commit c7c62d5

6 files changed

Lines changed: 463 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ All notable changes to this project are documented here. The format is based on
2828
- Images (Glance v2): `pcd_images_image` resource (local-file upload + web-download import,
2929
status waiter, checksum verify, unprotect-before-delete) and `pcd_images_image` /
3030
`pcd_images_image_ids` data sources.
31+
- Networking (Neutron v2): resources `pcd_networking_network`, `_subnet`, `_secgroup`,
32+
`_secgroup_rule`, `_router`, `_router_interface`; data sources `pcd_networking_network`,
33+
`_subnet`, `_secgroup`. (Floating IPs, ports, and remaining data sources to follow.)
3134

3235
### Known gaps
3336
- `cloud` (clouds.yaml) is declared but not yet implemented; it errors if set.

internal/provider/provider.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,8 @@ func (p *pcdProvider) DataSources(_ context.Context) []func() datasource.DataSou
6262
identity.NewRoleDataSource,
6363
images.NewImageDataSource,
6464
images.NewImageIDsDataSource,
65+
networking.NewNetworkDataSource,
66+
networking.NewSubnetDataSource,
67+
networking.NewSecgroupDataSource,
6568
}
6669
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) Platform9 Systems, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package networking_test
5+
6+
import (
7+
"testing"
8+
9+
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
10+
11+
"github.com/platform9/terraform-provider-pcd/internal/acctest"
12+
)
13+
14+
// TestAccNetworkingDataSources_byName creates a network, subnet, and security
15+
// group, then looks each up by name and asserts the resolved ID matches.
16+
func TestAccNetworkingDataSources_byName(t *testing.T) {
17+
resource.Test(t, resource.TestCase{
18+
PreCheck: func() { acctest.PreCheck(t) },
19+
ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories,
20+
CheckDestroy: resource.ComposeAggregateTestCheckFunc(
21+
testAccCheckSubnetDestroy(t),
22+
testAccCheckNetworkDestroy(t),
23+
testAccCheckSecgroupDestroy(t),
24+
),
25+
Steps: []resource.TestStep{
26+
{
27+
Config: testAccNetworkingDataSourcesConfig,
28+
Check: resource.ComposeAggregateTestCheckFunc(
29+
resource.TestCheckResourceAttrPair("data.pcd_networking_network.by_name", "id", "pcd_networking_network.test", "id"),
30+
resource.TestCheckResourceAttrPair("data.pcd_networking_subnet.by_name", "id", "pcd_networking_subnet.test", "id"),
31+
resource.TestCheckResourceAttrPair("data.pcd_networking_secgroup.by_name", "id", "pcd_networking_secgroup.test", "id"),
32+
resource.TestCheckResourceAttr("data.pcd_networking_subnet.by_name", "cidr", "10.102.0.0/24"),
33+
),
34+
},
35+
},
36+
})
37+
}
38+
39+
const testAccNetworkingDataSourcesConfig = `
40+
resource "pcd_networking_network" "test" {
41+
name = "tf-acc-ds-net"
42+
}
43+
44+
data "pcd_networking_network" "by_name" {
45+
name = pcd_networking_network.test.name
46+
}
47+
48+
resource "pcd_networking_subnet" "test" {
49+
name = "tf-acc-ds-subnet"
50+
network_id = pcd_networking_network.test.id
51+
cidr = "10.102.0.0/24"
52+
}
53+
54+
data "pcd_networking_subnet" "by_name" {
55+
name = pcd_networking_subnet.test.name
56+
}
57+
58+
resource "pcd_networking_secgroup" "test" {
59+
name = "tf-acc-ds-sg"
60+
}
61+
62+
data "pcd_networking_secgroup" "by_name" {
63+
name = pcd_networking_secgroup.test.name
64+
}
65+
`
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Copyright (c) Platform9 Systems, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
//
4+
// Ported from terraform-provider-openstack v3.4.0
5+
// (openstack/data_source_openstack_networking_network_v2.go), adapted for the
6+
// terraform-plugin-framework and PCD.
7+
8+
package networking
9+
10+
import (
11+
"context"
12+
"fmt"
13+
14+
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks"
15+
"github.com/hashicorp/terraform-plugin-framework/datasource"
16+
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
17+
"github.com/hashicorp/terraform-plugin-framework/types"
18+
19+
"github.com/platform9/terraform-provider-pcd/internal/clients"
20+
)
21+
22+
var (
23+
_ datasource.DataSource = (*networkDataSource)(nil)
24+
_ datasource.DataSourceWithConfigure = (*networkDataSource)(nil)
25+
)
26+
27+
// NewNetworkDataSource is the factory registered with the provider.
28+
func NewNetworkDataSource() datasource.DataSource {
29+
return &networkDataSource{}
30+
}
31+
32+
type networkDataSource struct {
33+
config *clients.Config
34+
}
35+
36+
type networkDataSourceModel struct {
37+
ID types.String `tfsdk:"id"`
38+
NetworkID types.String `tfsdk:"network_id"`
39+
Name types.String `tfsdk:"name"`
40+
Description types.String `tfsdk:"description"`
41+
AdminStateUp types.Bool `tfsdk:"admin_state_up"`
42+
Shared types.Bool `tfsdk:"shared"`
43+
External types.Bool `tfsdk:"external"`
44+
TenantID types.String `tfsdk:"tenant_id"`
45+
Region types.String `tfsdk:"region"`
46+
}
47+
48+
func (d *networkDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
49+
resp.TypeName = req.ProviderTypeName + "_networking_network"
50+
}
51+
52+
func (d *networkDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
53+
resp.Schema = schema.Schema{
54+
MarkdownDescription: "Look up a Neutron network by name or ID.",
55+
Attributes: map[string]schema.Attribute{
56+
"id": schema.StringAttribute{Computed: true, MarkdownDescription: "The network ID."},
57+
"network_id": schema.StringAttribute{Optional: true, MarkdownDescription: "Look up by network ID (takes precedence over name)."},
58+
"name": schema.StringAttribute{Optional: true, MarkdownDescription: "Look up by name."},
59+
"description": schema.StringAttribute{Computed: true, MarkdownDescription: "The network description."},
60+
"admin_state_up": schema.BoolAttribute{Computed: true, MarkdownDescription: "The administrative state."},
61+
"shared": schema.BoolAttribute{Computed: true, MarkdownDescription: "Whether the network is shared."},
62+
"external": schema.BoolAttribute{Computed: true, MarkdownDescription: "Whether the network is external."},
63+
"tenant_id": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Filter by (and report) the owning project."},
64+
"region": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "The region. Defaults to the provider's region."},
65+
},
66+
}
67+
}
68+
69+
func (d *networkDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
70+
d.config = configureClient(req.ProviderData, &resp.Diagnostics)
71+
}
72+
73+
func (d *networkDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
74+
var data networkDataSourceModel
75+
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
76+
if resp.Diagnostics.HasError() {
77+
return
78+
}
79+
80+
client, err := d.config.NetworkV2Client()
81+
if err != nil {
82+
resp.Diagnostics.AddError("networking: building v2 client", err.Error())
83+
return
84+
}
85+
86+
id := data.NetworkID.ValueString()
87+
if id == "" {
88+
pages, err := networks.List(client, networks.ListOpts{
89+
Name: data.Name.ValueString(),
90+
TenantID: data.TenantID.ValueString(),
91+
}).AllPages(ctx)
92+
if err != nil {
93+
resp.Diagnostics.AddError("networking: listing networks", err.Error())
94+
return
95+
}
96+
all, err := networks.ExtractNetworks(pages)
97+
if err != nil {
98+
resp.Diagnostics.AddError("networking: extracting networks", err.Error())
99+
return
100+
}
101+
switch len(all) {
102+
case 0:
103+
resp.Diagnostics.AddError("No network found", "No network matched the given criteria.")
104+
return
105+
case 1:
106+
id = all[0].ID
107+
default:
108+
resp.Diagnostics.AddError("Multiple networks found",
109+
fmt.Sprintf("%d networks matched; refine name/tenant_id.", len(all)))
110+
return
111+
}
112+
}
113+
114+
var n networkExtended
115+
if err := networks.Get(ctx, client, id).ExtractInto(&n); err != nil {
116+
resp.Diagnostics.AddError("networking: getting network", err.Error())
117+
return
118+
}
119+
120+
data.ID = types.StringValue(n.ID)
121+
data.NetworkID = types.StringValue(n.ID)
122+
data.Name = types.StringValue(n.Name)
123+
data.Description = types.StringValue(n.Description)
124+
data.AdminStateUp = types.BoolValue(n.AdminStateUp)
125+
data.Shared = types.BoolValue(n.Shared)
126+
data.External = types.BoolValue(n.NetworkExternalExt.External)
127+
data.TenantID = types.StringValue(n.TenantID)
128+
if data.Region.IsNull() || data.Region.IsUnknown() {
129+
data.Region = types.StringValue(d.config.Region)
130+
}
131+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
132+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// Copyright (c) Platform9 Systems, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
//
4+
// Ported from terraform-provider-openstack v3.4.0
5+
// (openstack/data_source_openstack_networking_secgroup_v2.go), adapted for the
6+
// terraform-plugin-framework and PCD.
7+
8+
package networking
9+
10+
import (
11+
"context"
12+
"fmt"
13+
14+
"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups"
15+
"github.com/hashicorp/terraform-plugin-framework/datasource"
16+
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
17+
"github.com/hashicorp/terraform-plugin-framework/types"
18+
19+
"github.com/platform9/terraform-provider-pcd/internal/clients"
20+
)
21+
22+
var (
23+
_ datasource.DataSource = (*secgroupDataSource)(nil)
24+
_ datasource.DataSourceWithConfigure = (*secgroupDataSource)(nil)
25+
)
26+
27+
// NewSecgroupDataSource is the factory registered with the provider.
28+
func NewSecgroupDataSource() datasource.DataSource {
29+
return &secgroupDataSource{}
30+
}
31+
32+
type secgroupDataSource struct {
33+
config *clients.Config
34+
}
35+
36+
type secgroupDataSourceModel struct {
37+
ID types.String `tfsdk:"id"`
38+
SecgroupID types.String `tfsdk:"secgroup_id"`
39+
Name types.String `tfsdk:"name"`
40+
Description types.String `tfsdk:"description"`
41+
TenantID types.String `tfsdk:"tenant_id"`
42+
Region types.String `tfsdk:"region"`
43+
}
44+
45+
func (d *secgroupDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
46+
resp.TypeName = req.ProviderTypeName + "_networking_secgroup"
47+
}
48+
49+
func (d *secgroupDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
50+
resp.Schema = schema.Schema{
51+
MarkdownDescription: "Look up a Neutron security group by name or ID.",
52+
Attributes: map[string]schema.Attribute{
53+
"id": schema.StringAttribute{Computed: true, MarkdownDescription: "The security group ID."},
54+
"secgroup_id": schema.StringAttribute{Optional: true, MarkdownDescription: "Look up by ID (takes precedence over name)."},
55+
"name": schema.StringAttribute{Optional: true, MarkdownDescription: "Look up by name."},
56+
"description": schema.StringAttribute{Computed: true, MarkdownDescription: "The description."},
57+
"tenant_id": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "Filter by (and report) the owning project."},
58+
"region": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "The region. Defaults to the provider's region."},
59+
},
60+
}
61+
}
62+
63+
func (d *secgroupDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
64+
d.config = configureClient(req.ProviderData, &resp.Diagnostics)
65+
}
66+
67+
func (d *secgroupDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
68+
var data secgroupDataSourceModel
69+
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
70+
if resp.Diagnostics.HasError() {
71+
return
72+
}
73+
74+
client, err := d.config.NetworkV2Client()
75+
if err != nil {
76+
resp.Diagnostics.AddError("networking: building v2 client", err.Error())
77+
return
78+
}
79+
80+
var sg *groups.SecGroup
81+
if v := data.SecgroupID.ValueString(); v != "" {
82+
sg, err = groups.Get(ctx, client, v).Extract()
83+
if err != nil {
84+
resp.Diagnostics.AddError("networking: getting security group", err.Error())
85+
return
86+
}
87+
} else {
88+
pages, err := groups.List(client, groups.ListOpts{
89+
Name: data.Name.ValueString(),
90+
TenantID: data.TenantID.ValueString(),
91+
}).AllPages(ctx)
92+
if err != nil {
93+
resp.Diagnostics.AddError("networking: listing security groups", err.Error())
94+
return
95+
}
96+
all, err := groups.ExtractGroups(pages)
97+
if err != nil {
98+
resp.Diagnostics.AddError("networking: extracting security groups", err.Error())
99+
return
100+
}
101+
switch len(all) {
102+
case 0:
103+
resp.Diagnostics.AddError("No security group found", "No security group matched the given criteria.")
104+
return
105+
case 1:
106+
sg = &all[0]
107+
default:
108+
resp.Diagnostics.AddError("Multiple security groups found",
109+
fmt.Sprintf("%d matched; refine name/tenant_id.", len(all)))
110+
return
111+
}
112+
}
113+
114+
data.ID = types.StringValue(sg.ID)
115+
data.SecgroupID = types.StringValue(sg.ID)
116+
data.Name = types.StringValue(sg.Name)
117+
data.Description = types.StringValue(sg.Description)
118+
data.TenantID = types.StringValue(sg.TenantID)
119+
if data.Region.IsNull() || data.Region.IsUnknown() {
120+
data.Region = types.StringValue(d.config.Region)
121+
}
122+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
123+
}

0 commit comments

Comments
 (0)