openstack: implement api client and metadata list

Signed-off-by: Paul Meyer <49727155+katexochen@users.noreply.github.com>
This commit is contained in:
Paul Meyer 2023-03-07 11:37:25 +01:00
parent 418f08bf40
commit acbd70c741
9 changed files with 833 additions and 1 deletions

2
go.mod
View File

@ -219,6 +219,8 @@ require (
github.com/google/trillian v1.5.1 // indirect github.com/google/trillian v1.5.1 // indirect
github.com/google/uuid v1.3.0 // indirect github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
github.com/gophercloud/gophercloud v1.2.0
github.com/gophercloud/utils v0.0.0-20230301065655-769528992f29
github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/mux v1.8.0 // indirect
github.com/gosuri/uitable v0.0.4 // indirect github.com/gosuri/uitable v0.0.4 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect

6
go.sum
View File

@ -735,6 +735,11 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ= github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ=
github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gophercloud/gophercloud v1.1.1/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM=
github.com/gophercloud/gophercloud v1.2.0 h1:1oXyj4g54KBg/kFtCdMM6jtxSzeIyg8wv4z1HoGPp1E=
github.com/gophercloud/gophercloud v1.2.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM=
github.com/gophercloud/utils v0.0.0-20230301065655-769528992f29 h1:A2dT0wlliiBwnkWVPtSLXH1m36486l7sOwPRrjEm3hE=
github.com/gophercloud/utils v0.0.0-20230301065655-769528992f29/go.mod h1:z4Dey7xsTUXgcB1C8elMvGRKTjV1ez0eoYQlMrduG1g=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
github.com/goreleaser/goreleaser v0.134.0/go.mod h1:ZT6Y2rSYa6NxQzIsdfWWNWAlYGXGbreo66NmE+3X3WQ= github.com/goreleaser/goreleaser v0.134.0/go.mod h1:ZT6Y2rSYa6NxQzIsdfWWNWAlYGXGbreo66NmE+3X3WQ=
@ -1498,6 +1503,7 @@ golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=

View File

@ -10,6 +10,9 @@ import (
"context" "context"
"github.com/edgelesssys/constellation/v2/internal/role" "github.com/edgelesssys/constellation/v2/internal/role"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"github.com/gophercloud/gophercloud/openstack/networking/v2/subnets"
"github.com/gophercloud/gophercloud/pagination"
) )
type imdsAPI interface { type imdsAPI interface {
@ -21,3 +24,12 @@ type imdsAPI interface {
role(ctx context.Context) (role.Role, error) role(ctx context.Context) (role.Role, error)
vpcIP(ctx context.Context) (string, error) vpcIP(ctx context.Context) (string, error)
} }
type serversAPI interface {
ListServers(opts servers.ListOptsBuilder) pagerAPI
ListSubnets(opts subnets.ListOpts) pagerAPI
}
type pagerAPI interface {
AllPages() (pagination.Page, error)
}

View File

@ -10,6 +10,9 @@ import (
"context" "context"
"github.com/edgelesssys/constellation/v2/internal/role" "github.com/edgelesssys/constellation/v2/internal/role"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"github.com/gophercloud/gophercloud/openstack/networking/v2/subnets"
"github.com/gophercloud/gophercloud/pagination"
) )
type stubIMDSClient struct { type stubIMDSClient struct {
@ -56,3 +59,25 @@ func (c *stubIMDSClient) role(ctx context.Context) (role.Role, error) {
func (c *stubIMDSClient) vpcIP(ctx context.Context) (string, error) { func (c *stubIMDSClient) vpcIP(ctx context.Context) (string, error) {
return c.vpcIPResult, c.vpcIPErr return c.vpcIPResult, c.vpcIPErr
} }
type stubServersClient struct {
serversPager stubPager
subnetsPager stubPager
}
func (c *stubServersClient) ListServers(opts servers.ListOptsBuilder) pagerAPI {
return &c.serversPager
}
func (c *stubServersClient) ListSubnets(opts subnets.ListOpts) pagerAPI {
return &c.subnetsPager
}
type stubPager struct {
page pagination.Page
allPagesErr error
}
func (p *stubPager) AllPages() (pagination.Page, error) {
return p.page, p.allPagesErr
}

View File

@ -10,19 +10,77 @@ import (
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
"net/netip"
"strings"
"github.com/edgelesssys/constellation/v2/internal/cloud/metadata" "github.com/edgelesssys/constellation/v2/internal/cloud/metadata"
"github.com/edgelesssys/constellation/v2/internal/role"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"github.com/gophercloud/gophercloud/openstack/networking/v2/subnets"
"github.com/gophercloud/utils/openstack/clientconfig"
)
const (
roleTagFormat = "constellation-role-%s"
microversion = "2.42"
) )
// Cloud is the metadata client for OpenStack. // Cloud is the metadata client for OpenStack.
type Cloud struct { type Cloud struct {
api serversAPI
imds imdsAPI imds imdsAPI
} }
// New creates a new OpenStack metadata client. // New creates a new OpenStack metadata client.
func New(ctx context.Context) (*Cloud, error) { func New(ctx context.Context) (*Cloud, error) {
imds := &imdsClient{client: &http.Client{}} imds := &imdsClient{client: &http.Client{}}
return &Cloud{imds: imds}, nil
authURL, err := imds.authURL(ctx)
if err != nil {
return nil, fmt.Errorf("getting auth URL: %w", err)
}
username, err := imds.username(ctx)
if err != nil {
return nil, fmt.Errorf("getting token name: %w", err)
}
password, err := imds.password(ctx)
if err != nil {
return nil, fmt.Errorf("getting token password: %w", err)
}
userDomainName, err := imds.userDomainName(ctx)
if err != nil {
return nil, fmt.Errorf("getting user domain name: %w", err)
}
clientOpts := &clientconfig.ClientOpts{
AuthType: clientconfig.AuthV3Password,
AuthInfo: &clientconfig.AuthInfo{
AuthURL: authURL,
UserDomainName: userDomainName,
Username: username,
Password: password,
},
}
serversClient, err := clientconfig.NewServiceClient("compute", clientOpts)
if err != nil {
return nil, fmt.Errorf("creating compute client: %w", err)
}
serversClient.Microversion = microversion
subnetsClient, err := clientconfig.NewServiceClient("network", clientOpts)
if err != nil {
return nil, fmt.Errorf("creating network client: %w", err)
}
subnetsClient.Microversion = microversion
return &Cloud{
imds: imds,
api: &apiClient{
servers: serversClient,
subnets: subnetsClient,
},
}, nil
} }
// Self returns the metadata of the current instance. // Self returns the metadata of the current instance.
@ -51,3 +109,138 @@ func (c *Cloud) Self(ctx context.Context) (metadata.InstanceMetadata, error) {
VPCIP: vpcIP, VPCIP: vpcIP,
}, nil }, nil
} }
// List returns the metadata of all instances belonging to the same Constellation cluster.
func (c *Cloud) List(ctx context.Context) ([]metadata.InstanceMetadata, error) {
uid, err := c.imds.uid(ctx)
if err != nil {
return nil, fmt.Errorf("getting uid: %w", err)
}
uidTag := fmt.Sprintf("constellation-uid-%s", uid)
subnet, err := c.getSubnetCIDR(uidTag)
if err != nil {
return nil, err
}
srvs, err := c.getServers(uidTag)
if err != nil {
return nil, err
}
var result []metadata.InstanceMetadata
for _, s := range srvs {
if s.Name == "" {
continue
}
if s.ID == "" {
continue
}
if s.Tags == nil {
continue
}
var serverRole role.Role
for _, t := range *s.Tags {
if strings.HasPrefix(t, "constellation-role-") {
serverRole = role.FromString(strings.TrimPrefix(t, "constellation-role-"))
break
}
}
if serverRole == role.Unknown {
continue
}
subnetAddrs, err := parseSeverAddresses(s.Addresses)
if err != nil {
return nil, fmt.Errorf("parsing server %q addresses: %w", s.Name, err)
}
var vpcIP string
// In a best effort approach, we take the first fixed IPv4 address that is in the subnet
// belonging to our cluster.
for _, serverSubnet := range subnetAddrs {
for _, addr := range serverSubnet.Addresses {
if addr.Type != fixedIP {
continue
}
if addr.IPVersion != ipV4 {
continue
}
if addr.IP == "" {
continue
}
parsedAddr, err := netip.ParseAddr(addr.IP)
if err != nil {
continue
}
if !subnet.Contains(parsedAddr) {
continue
}
vpcIP = addr.IP
break
}
}
if vpcIP == "" {
continue
}
im := metadata.InstanceMetadata{
Name: s.Name,
ProviderID: s.ID,
Role: serverRole,
VPCIP: vpcIP,
}
result = append(result, im)
}
if len(result) == 0 {
return nil, fmt.Errorf("no instances belonging to this cluster found")
}
return result, nil
}
func (c *Cloud) getSubnetCIDR(uidTag string) (netip.Prefix, error) {
listSubnetsOpts := subnets.ListOpts{Tags: uidTag}
subnetsPage, err := c.api.ListSubnets(listSubnetsOpts).AllPages()
if err != nil {
return netip.Prefix{}, fmt.Errorf("listing subnets: %w", err)
}
nets, err := subnets.ExtractSubnets(subnetsPage)
if err != nil {
return netip.Prefix{}, fmt.Errorf("extracting subnets: %w", err)
}
if len(nets) != 1 {
return netip.Prefix{}, fmt.Errorf("expected exactly one subnet, got %d", len(nets))
}
cidr, err := netip.ParsePrefix(nets[0].CIDR)
if err != nil {
return netip.Prefix{}, fmt.Errorf("parsing subnet CIDR: %w", err)
}
return cidr, nil
}
func (c *Cloud) getServers(uidTag string) ([]servers.Server, error) {
listServersOpts := servers.ListOpts{Tags: uidTag}
serversPage, err := c.api.ListServers(listServersOpts).AllPages()
if err != nil {
return nil, fmt.Errorf("listing servers: %w", err)
}
servers, err := servers.ExtractServers(serversPage)
if err != nil {
return nil, fmt.Errorf("extracting servers: %w", err)
}
return servers, nil
}

View File

@ -13,6 +13,10 @@ import (
"github.com/edgelesssys/constellation/v2/internal/cloud/metadata" "github.com/edgelesssys/constellation/v2/internal/cloud/metadata"
"github.com/edgelesssys/constellation/v2/internal/role" "github.com/edgelesssys/constellation/v2/internal/role"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"github.com/gophercloud/gophercloud/openstack/networking/v2/subnets"
"github.com/gophercloud/gophercloud/pagination"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -93,3 +97,313 @@ func TestSelf(t *testing.T) {
}) })
} }
} }
func TestList(t *testing.T) {
someErr := fmt.Errorf("failed")
// newTestAddrs returns a set of raw server addresses as we would get from
// a ListServers call and as expected by the parseSeverAddresses function.
// The hardcoded addresses don't match what we are looking for. A valid
// address can be injected. You can pass a second valid address to test
// that the first valid one is chosen.
newTestAddrs := func(vpcIP1, vpcIP2 string) map[string]any {
return map[string]any{
"network1": []any{
map[string]any{
"addr": "198.51.100.0",
"version": 4,
"OS-EXT-IPS:type": "fixed",
"OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:0c:0c:0c",
},
},
"network2": []any{
map[string]any{
"addr": "192.0.2.1",
"version": 4,
"OS-EXT-IPS:type": "floating",
"OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:0c:0c:0c",
},
map[string]any{
"addr": "2001:db8:3333:4444:5555:6666:7777:8888",
"version": 6,
"OS-EXT-IPS:type": "fixed",
"OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:0c:0c:0c",
},
map[string]any{
"addr": vpcIP1,
"version": 4,
"OS-EXT-IPS:type": "fixed",
"OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:0c:0c:0c",
},
map[string]any{
"addr": vpcIP2,
"version": 4,
"OS-EXT-IPS:type": "fixed",
"OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:0c:0c:0c",
},
},
}
}
// newSubnetPager returns a subnet pager as we would get from a ListSubnets
newSubnetPager := func(nets []subnets.Subnet, err error) stubPager {
return stubPager{
page: subnets.SubnetPage{
LinkedPageBase: pagination.LinkedPageBase{
PageResult: pagination.PageResult{
Result: gophercloud.Result{
Body: struct {
Subnets []subnets.Subnet `json:"subnets"`
}{nets},
Err: err,
},
},
},
},
}
}
// newSeverPager returns a server pager as we would get from a ListServers
newSeverPager := func(srvs []servers.Server, err error) stubPager {
return stubPager{
page: servers.ServerPage{
LinkedPageBase: pagination.LinkedPageBase{
PageResult: pagination.PageResult{
Result: gophercloud.Result{
Body: struct {
Servers []servers.Server `json:"servers"`
}{srvs},
Err: err,
},
},
},
},
}
}
testCases := map[string]struct {
imds imdsAPI
api serversAPI
want []metadata.InstanceMetadata
wantErr bool
}{
"success": {
imds: &stubIMDSClient{uidResult: "uid"},
api: &stubServersClient{
serversPager: newSeverPager([]servers.Server{
{
Name: "name1",
ID: "id1",
Tags: &[]string{"constellation-role-control-plane", "constellation-uid-7777"},
Addresses: newTestAddrs("192.0.2.5", ""),
},
{
Name: "name2",
ID: "id2",
Tags: &[]string{"constellation-role-worker", "constellation-uid-7777"},
Addresses: newTestAddrs("192.0.2.6", "192.0.2.99"),
},
{
Name: "name3",
ID: "id3",
Tags: &[]string{"constellation-role-worker", "constellation-uid-8888"},
Addresses: newTestAddrs("198.51.100.1", ""),
},
}, nil),
subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil),
},
want: []metadata.InstanceMetadata{
{
Name: "name1",
ProviderID: "id1",
Role: role.ControlPlane,
VPCIP: "192.0.2.5",
},
{
Name: "name2",
ProviderID: "id2",
Role: role.Worker,
VPCIP: "192.0.2.6",
},
},
},
"no servers found": {
imds: &stubIMDSClient{uidResult: "uid"},
api: &stubServersClient{
serversPager: newSeverPager([]servers.Server{}, nil),
subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil),
},
wantErr: true,
},
"imds uid error": {
imds: &stubIMDSClient{uidErr: someErr},
wantErr: true,
},
"list subnets error": {
imds: &stubIMDSClient{uidResult: "uid"},
api: &stubServersClient{
subnetsPager: stubPager{allPagesErr: someErr},
},
wantErr: true,
},
"extract subnets error": {
imds: &stubIMDSClient{uidResult: "uid"},
api: &stubServersClient{
subnetsPager: newSubnetPager([]subnets.Subnet{}, someErr),
},
wantErr: true,
},
"multiple subnets error": {
imds: &stubIMDSClient{uidResult: "uid"},
api: &stubServersClient{
subnetsPager: newSubnetPager([]subnets.Subnet{
{CIDR: "192.0.2.0/24"},
{CIDR: "198.51.100.0/24"},
}, nil),
},
wantErr: true,
},
"parse subnet error": {
imds: &stubIMDSClient{uidResult: "uid"},
api: &stubServersClient{
subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "notAnIP"}}, nil),
},
wantErr: true,
},
"list servers error": {
imds: &stubIMDSClient{uidResult: "uid"},
api: &stubServersClient{
serversPager: stubPager{allPagesErr: someErr},
subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil),
},
wantErr: true,
},
"extract servers error": {
imds: &stubIMDSClient{uidResult: "uid"},
api: &stubServersClient{
serversPager: newSeverPager([]servers.Server{}, someErr),
subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil),
},
wantErr: true,
},
"sever with empty name skipped": {
imds: &stubIMDSClient{uidResult: "uid"},
api: &stubServersClient{
serversPager: newSeverPager([]servers.Server{
{
ID: "id1",
Tags: &[]string{"constellation-role-control-plane", "constellation-uid-7777"},
Addresses: newTestAddrs("192.0.2.5", ""),
},
}, nil),
subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil),
},
wantErr: true,
},
"sever with nil tags skipped": {
imds: &stubIMDSClient{uidResult: "uid"},
api: &stubServersClient{
serversPager: newSeverPager([]servers.Server{
{
Name: "name1",
ID: "id1",
Addresses: newTestAddrs("192.0.2.5", ""),
},
}, nil),
subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil),
},
wantErr: true,
},
"server with empty ID skipped": {
imds: &stubIMDSClient{uidResult: "uid"},
api: &stubServersClient{
serversPager: newSeverPager([]servers.Server{
{
Name: "name1",
Tags: &[]string{"constellation-role-control-plane", "constellation-uid-7777"},
Addresses: newTestAddrs("192.0.2.5", ""),
},
}, nil),
subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil),
},
wantErr: true,
},
"server with unknown role skipped": {
imds: &stubIMDSClient{uidResult: "uid"},
api: &stubServersClient{
serversPager: newSeverPager([]servers.Server{
{
Name: "name1",
ID: "id1",
Tags: &[]string{"constellation-role-unknown", "constellation-uid-7777"},
Addresses: newTestAddrs("192.0.2.5", ""),
},
}, nil),
subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil),
},
wantErr: true,
},
"server without role skipped": {
imds: &stubIMDSClient{uidResult: "uid"},
api: &stubServersClient{
serversPager: newSeverPager([]servers.Server{
{
Name: "name1",
ID: "id1",
Tags: &[]string{"constellation-uid-7777"},
Addresses: newTestAddrs("192.0.2.5", ""),
},
}, nil),
subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil),
},
wantErr: true,
},
"server without parseable addresses skipped": {
imds: &stubIMDSClient{uidResult: "uid"},
api: &stubServersClient{
serversPager: newSeverPager([]servers.Server{
{
Name: "name1",
ID: "id1",
Tags: &[]string{"constellation-role-control-plane", "constellation-uid-7777"},
Addresses: map[string]any{"foo": "bar"},
},
}, nil),
subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil),
},
wantErr: true,
},
"server addresses contains in": {
imds: &stubIMDSClient{uidResult: "uid"},
api: &stubServersClient{
serversPager: newSeverPager([]servers.Server{
{
Name: "name1",
ID: "id1",
Tags: &[]string{"constellation-role-control-plane", "constellation-uid-7777"},
Addresses: newTestAddrs("invalidIP", ""),
},
}, nil),
subnetsPager: newSubnetPager([]subnets.Subnet{{CIDR: "192.0.2.0/24"}}, nil),
},
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
c := &Cloud{imds: tc.imds, api: tc.api}
got, err := c.List(context.Background())
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
assert.Equal(tc.want, got)
}
})
}
}

View File

@ -0,0 +1,111 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package openstack
import "fmt"
// parseServerAddresses parses the untyped Addresses field of a sever struct.
func parseSeverAddresses(addrsMap map[string]any) ([]serverSubnetAddresses, error) {
result := []serverSubnetAddresses{}
for name, v := range addrsMap {
subnet := serverSubnetAddresses{NetworkName: name}
v, ok := v.([]any)
if !ok {
return nil, fmt.Errorf("server address %q is not of form []any", name)
}
for _, v := range v {
nic := serverAddress{}
v, ok := v.(map[string]any)
if !ok {
return nil, fmt.Errorf("element in server address %q is not of form []map[string]any", name)
}
if mac, ok := v["OS-EXT-IPS-MAC:mac_addr"].(string); ok {
nic.MAC = mac
}
if ip, ok := v["addr"].(string); ok {
nic.IP = ip
}
if ipVersion, ok := v["version"].(float64); ok {
version, err := ipVersionFromFloat(ipVersion)
if err != nil {
return nil, err
}
nic.IPVersion = version
}
if typ, ok := v["OS-EXT-IPS:type"].(string); ok {
ipType, err := ipTypeFromString(typ)
if err != nil {
return nil, err
}
nic.Type = ipType
}
subnet.Addresses = append(subnet.Addresses, nic)
}
result = append(result, subnet)
}
return result, nil
}
type serverSubnetAddresses struct {
NetworkName string
Addresses []serverAddress
}
type serverAddress struct {
Type ipType
IPVersion ipVersion
IP string
MAC string
}
type ipVersion int
const (
ipVUnknown ipVersion = 0
ipV4 ipVersion = 4
ipV6 ipVersion = 6
)
func ipVersionFromFloat(f float64) (ipVersion, error) {
switch f {
case 4:
return ipV4, nil
case 6:
return ipV6, nil
default:
return 0, fmt.Errorf("unknown IP version %f", f)
}
}
type ipType string
const (
unknownIP ipType = "unknown"
fixedIP ipType = "fixed"
floatingIP ipType = "floating"
)
func ipTypeFromString(s string) (ipType, error) {
switch s {
case "fixed":
return fixedIP, nil
case "floating":
return floatingIP, nil
default:
return unknownIP, fmt.Errorf("unknown IP type %s", s)
}
}

View File

@ -0,0 +1,143 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package openstack
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseServerAddresses(t *testing.T) {
testCases := map[string]struct {
input string
expected []serverSubnetAddresses
wantErr bool
}{
"valid": {
input: `
{
"network1": [
{
"OS-EXT-IPS-MAC:mac_addr": "00:00:00:00:00:00",
"version": 4,
"addr": "192.0.2.1",
"OS-EXT-IPS:type": "fixed"
},
{
"OS-EXT-IPS-MAC:mac_addr": "00:00:00:00:00:01",
"version": 6,
"addr": "2001:db8:3333:4444:5555:6666:7777:8888",
"OS-EXT-IPS:type": "floating"
}
],
"network2": [
{
"OS-EXT-IPS-MAC:mac_addr": "00:00:00:00:00:02",
"version": 4,
"addr": "192.0.2.3",
"OS-EXT-IPS:type": "floating"
}
]
}`,
expected: []serverSubnetAddresses{
{
NetworkName: "network1",
Addresses: []serverAddress{
{
Type: fixedIP,
IPVersion: ipV4,
IP: "192.0.2.1",
MAC: "00:00:00:00:00:00",
},
{
Type: floatingIP,
IPVersion: ipV6,
IP: "2001:db8:3333:4444:5555:6666:7777:8888",
MAC: "00:00:00:00:00:01",
},
},
},
{
NetworkName: "network2",
Addresses: []serverAddress{
{
Type: floatingIP,
IPVersion: ipV4,
IP: "192.0.2.3",
MAC: "00:00:00:00:00:02",
},
},
},
},
},
"invalid ip version": {
input: `
{
"network1": [
{
"OS-EXT-IPS-MAC:mac_addr": "00:00:00:00:00:00",
"version": 5,
"addr": "192.0.2.1",
"OS-EXT-IPS:type": "fixed"
}
]
}`,
wantErr: true,
},
"invalid ip type": {
input: `
{
"network1": [
{
"OS-EXT-IPS-MAC:mac_addr": "00:00:00:00:00:00",
"version": 4,
"addr": "192.0.2.1",
"OS-EXT-IPS:type": "invalid"
}
]
}`,
wantErr: true,
},
"invalid second lvl structure": {
input: `
{
"network1": { "invalid": "structure" }
}`,
wantErr: true,
},
"invalid third lvl structure": {
input: `
{
"network1": [ "invalid" ]
}`,
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
var addrsMap map[string]any
err := json.Unmarshal([]byte(tc.input), &addrsMap)
require.NoError(err)
addrs, err := parseSeverAddresses(addrsMap)
if tc.wantErr {
assert.Error(err)
return
}
assert.NoError(err)
assert.ElementsMatch(tc.expected, addrs)
})
}
}

View File

@ -0,0 +1,26 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package openstack
import (
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"github.com/gophercloud/gophercloud/openstack/networking/v2/subnets"
)
type apiClient struct {
servers *gophercloud.ServiceClient
subnets *gophercloud.ServiceClient
}
func (c *apiClient) ListServers(opts servers.ListOptsBuilder) pagerAPI {
return servers.List(c.servers, opts)
}
func (c *apiClient) ListSubnets(opts subnets.ListOpts) pagerAPI {
return subnets.List(c.subnets, opts)
}