Skip to content

Commit 4303ab8

Browse files
authored
Merge pull request #6 from platform9/feat/compute
feat(compute): Nova keypair/flavor/servergroup/instance and data sources
2 parents 51977b7 + cc8208a commit 4303ab8

17 files changed

Lines changed: 1866 additions & 0 deletions

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ All notable changes to this project are documented here. The format is based on
3131
- Networking (Neutron v2): resources `pcd_networking_network`, `_subnet`, `_secgroup`,
3232
`_secgroup_rule`, `_router`, `_router_interface`; data sources `pcd_networking_network`,
3333
`_subnet`, `_secgroup`. (Floating IPs, ports, and remaining data sources to follow.)
34+
- Compute (Nova v2): resources `pcd_compute_keypair`, `pcd_compute_flavor`,
35+
`pcd_compute_servergroup` (acceptance-tested); `pcd_compute_instance` (code-complete —
36+
boot verification is blocked on a lab image-library issue, see DECISIONS.md); data
37+
sources `pcd_compute_flavor`, `pcd_compute_keypair`, `pcd_compute_availability_zones`.
3438

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

DECISIONS.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,22 @@
33
Records deviations from the PCD-1070 implementation plan and material findings that
44
change scope. The plan's Section 3 decisions remain locked unless noted here.
55

6+
## 2026-07-11 — BLOCKER: hypervisor pcd-iso-test cannot spawn instances
7+
8+
Nova boots fail: an instance goes BUILD → ERROR with *"Exceeded maximum number of
9+
retries. Exhausted all hosts available for retrying build failures"*
10+
(`nova.exception.MaxRetriesExceeded`). Ruled out via the API and libvirt on
11+
cannon-ubuntu: nested virtualization (CPU mode is `host-passthrough`), capacity
12+
(4 vCPU / 7 GB / 131 GB free), image (reaches `active` via web-download), and
13+
network (created fine). The real `nova-compute` spawn error lives on the host, but
14+
SSH to `pcd-iso-test` (172.16.122.232) is rejected — the `pcd_automation` key is not
15+
authorized there (it is the repurposed "iso-test" VM). Most likely an OVN
16+
port-binding failure or a `pf9-hostagent`/nova-compute issue on the freshly-onboarded
17+
host. **Needs the owner** to check `nova-compute.log` on the host (or grant SSH
18+
access). `pcd_compute_instance` code is complete and correct up to the boot; its boot
19+
acceptance test will pass once the host can spawn a VM. keypair/flavor/servergroup and
20+
the compute data sources are testable without a boot and continue.
21+
622
## 2026-07-11 — networking: floating IPs and extras deferred
723

824
The CE lab has no external Neutron network, so `pcd_networking_floatingip` (which

internal/clients/config.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,17 @@ func (c *Config) NetworkV2Client() (*gophercloud.ServiceClient, error) {
213213
return client, nil
214214
}
215215

216+
// ComputeV2Client returns a Nova v2 service client, honoring an endpoint_overrides
217+
// entry for the "compute" service type if present.
218+
func (c *Config) ComputeV2Client() (*gophercloud.ServiceClient, error) {
219+
client, err := openstack.NewComputeV2(c.Provider, c.endpointOpts())
220+
if err != nil {
221+
return nil, fmt.Errorf("pcd: creating compute v2 client: %w", err)
222+
}
223+
c.applyOverride(client, "compute")
224+
return client, nil
225+
}
226+
216227
// applyOverride points a service client at an operator-supplied endpoint when
217228
// endpoint_overrides names its service type.
218229
func (c *Config) applyOverride(client *gophercloud.ServiceClient, serviceType string) {

internal/provider/provider.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/hashicorp/terraform-plugin-framework/provider"
1111
"github.com/hashicorp/terraform-plugin-framework/resource"
1212

13+
"github.com/platform9/terraform-provider-pcd/internal/services/compute"
1314
"github.com/platform9/terraform-provider-pcd/internal/services/identity"
1415
"github.com/platform9/terraform-provider-pcd/internal/services/images"
1516
"github.com/platform9/terraform-provider-pcd/internal/services/networking"
@@ -51,6 +52,10 @@ func (p *pcdProvider) Resources(_ context.Context) []func() resource.Resource {
5152
networking.NewSecgroupRuleResource,
5253
networking.NewRouterResource,
5354
networking.NewRouterInterfaceResource,
55+
compute.NewKeypairResource,
56+
compute.NewInstanceResource,
57+
compute.NewFlavorResource,
58+
compute.NewServergroupResource,
5459
}
5560
}
5661

@@ -65,5 +70,8 @@ func (p *pcdProvider) DataSources(_ context.Context) []func() datasource.DataSou
6570
networking.NewNetworkDataSource,
6671
networking.NewSubnetDataSource,
6772
networking.NewSecgroupDataSource,
73+
compute.NewFlavorDataSource,
74+
compute.NewKeypairDataSource,
75+
compute.NewAvailabilityZonesDataSource,
6876
}
6977
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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_compute_availability_zones_v2.go), adapted
6+
// for the terraform-plugin-framework and PCD.
7+
8+
package compute
9+
10+
import (
11+
"context"
12+
13+
"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/availabilityzones"
14+
"github.com/hashicorp/terraform-plugin-framework/datasource"
15+
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
16+
"github.com/hashicorp/terraform-plugin-framework/types"
17+
18+
"github.com/platform9/terraform-provider-pcd/internal/clients"
19+
)
20+
21+
var (
22+
_ datasource.DataSource = (*azDataSource)(nil)
23+
_ datasource.DataSourceWithConfigure = (*azDataSource)(nil)
24+
)
25+
26+
// NewAvailabilityZonesDataSource is the factory registered with the provider.
27+
func NewAvailabilityZonesDataSource() datasource.DataSource {
28+
return &azDataSource{}
29+
}
30+
31+
type azDataSource struct {
32+
config *clients.Config
33+
}
34+
35+
type azDataSourceModel struct {
36+
ID types.String `tfsdk:"id"`
37+
State types.String `tfsdk:"state"`
38+
Names types.List `tfsdk:"names"`
39+
Region types.String `tfsdk:"region"`
40+
}
41+
42+
func (d *azDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
43+
resp.TypeName = req.ProviderTypeName + "_compute_availability_zones"
44+
}
45+
46+
func (d *azDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
47+
resp.Schema = schema.Schema{
48+
MarkdownDescription: "Return the compute availability zones.",
49+
Attributes: map[string]schema.Attribute{
50+
"id": schema.StringAttribute{Computed: true, MarkdownDescription: "Data source identifier."},
51+
"state": schema.StringAttribute{Optional: true, MarkdownDescription: "Filter by state: available (default) or unavailable."},
52+
"names": schema.ListAttribute{Computed: true, ElementType: types.StringType, MarkdownDescription: "The matching availability zone names."},
53+
"region": schema.StringAttribute{Optional: true, Computed: true, MarkdownDescription: "The region. Defaults to the provider's region."},
54+
},
55+
}
56+
}
57+
58+
func (d *azDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
59+
d.config = configureClient(req.ProviderData, &resp.Diagnostics)
60+
}
61+
62+
func (d *azDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
63+
var data azDataSourceModel
64+
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
65+
if resp.Diagnostics.HasError() {
66+
return
67+
}
68+
69+
client, err := d.config.ComputeV2Client()
70+
if err != nil {
71+
resp.Diagnostics.AddError("compute: building v2 client", err.Error())
72+
return
73+
}
74+
75+
pages, err := availabilityzones.List(client).AllPages(ctx)
76+
if err != nil {
77+
resp.Diagnostics.AddError("compute: listing availability zones", err.Error())
78+
return
79+
}
80+
zones, err := availabilityzones.ExtractAvailabilityZones(pages)
81+
if err != nil {
82+
resp.Diagnostics.AddError("compute: extracting availability zones", err.Error())
83+
return
84+
}
85+
86+
wantAvailable := data.State.ValueString() != "unavailable"
87+
names := make([]string, 0, len(zones))
88+
for _, z := range zones {
89+
if z.ZoneState.Available == wantAvailable {
90+
names = append(names, z.ZoneName)
91+
}
92+
}
93+
94+
nameList, diags := types.ListValueFrom(ctx, types.StringType, names)
95+
resp.Diagnostics.Append(diags...)
96+
data.Names = nameList
97+
if data.Region.IsNull() || data.Region.IsUnknown() {
98+
data.Region = types.StringValue(d.config.Region)
99+
}
100+
data.ID = types.StringValue(d.config.Region + ":azs")
101+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
102+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) Platform9 Systems, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
// Package compute implements the pcd_compute_* resources and data sources
5+
// (Nova v2), ported from terraform-provider-openstack v3.4.0.
6+
package compute
7+
8+
import (
9+
"fmt"
10+
11+
"github.com/hashicorp/terraform-plugin-framework/diag"
12+
13+
"github.com/platform9/terraform-provider-pcd/internal/clients"
14+
)
15+
16+
// configureClient extracts the shared *clients.Config from ProviderData.
17+
func configureClient(providerData any, diags *diag.Diagnostics) *clients.Config {
18+
if providerData == nil {
19+
return nil
20+
}
21+
config, ok := providerData.(*clients.Config)
22+
if !ok {
23+
diags.AddError(
24+
"Unexpected provider data type",
25+
fmt.Sprintf("Expected *clients.Config, got %T. This is a bug in the provider.", providerData),
26+
)
27+
return nil
28+
}
29+
return config
30+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright (c) Platform9 Systems, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package compute_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+
func TestAccComputeAvailabilityZones_basic(t *testing.T) {
15+
resource.Test(t, resource.TestCase{
16+
PreCheck: func() { acctest.PreCheck(t) },
17+
ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories,
18+
Steps: []resource.TestStep{
19+
{
20+
Config: `data "pcd_compute_availability_zones" "zones" {}`,
21+
Check: resource.ComposeAggregateTestCheckFunc(
22+
resource.TestCheckResourceAttrSet("data.pcd_compute_availability_zones.zones", "names.0"),
23+
),
24+
},
25+
},
26+
})
27+
}
28+
29+
func TestAccComputeKeypairDataSource_basic(t *testing.T) {
30+
resource.Test(t, resource.TestCase{
31+
PreCheck: func() { acctest.PreCheck(t) },
32+
ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories,
33+
CheckDestroy: testAccCheckKeypairDestroy(t),
34+
Steps: []resource.TestStep{
35+
{
36+
Config: `
37+
resource "pcd_compute_keypair" "test" {
38+
name = "tf-acc-kp-ds"
39+
}
40+
41+
data "pcd_compute_keypair" "by_name" {
42+
name = pcd_compute_keypair.test.name
43+
}
44+
`,
45+
Check: resource.ComposeAggregateTestCheckFunc(
46+
resource.TestCheckResourceAttrPair("data.pcd_compute_keypair.by_name", "id", "pcd_compute_keypair.test", "name"),
47+
resource.TestCheckResourceAttrSet("data.pcd_compute_keypair.by_name", "public_key"),
48+
resource.TestCheckResourceAttrSet("data.pcd_compute_keypair.by_name", "fingerprint"),
49+
),
50+
},
51+
},
52+
})
53+
}

0 commit comments

Comments
 (0)