8000 Merge pull request #3270 from gophercloud/bp-v2-c925eea-6e6dbe5 · gophercloud/gophercloud@e3bceef · GitHub
[go: up one dir, main page]

Skip to content

Commit e3bceef

Browse files
Merge pull request #3270 from gophercloud/bp-v2-c925eea-6e6dbe5
[v2] SG rules: implement bulk create
2 parents 335a41f + 89e66ee commit e3bceef

File tree

6 files changed

+220
-7
lines changed

6 files changed

+220
-7
lines changed
8000

internal/acceptance/openstack/networking/v2/extensions/extensions.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,44 @@ func CreateSecurityGroupRule(t *testing.T, client *gophercloud.ServiceClient, se
140140
return rule, nil
141141
}
142142

143+
// CreateSecurityGroupRulesBulk will create security group rules with a random name
144+
// and random port between 80 and 99.
145+
// An error will be returned if one was failed to be created.
146+
func CreateSecurityGroupRulesBulk(t *testing.T, client *gophercloud.ServiceClient, secGroupID string) ([]rules.SecGroupRule, error) {
147+
t.Logf("Attempting to bulk create security group rules in group: %s", secGroupID)
148+
149+
sgRulesCreateOpts := make([]rules.CreateOpts, 3)
150+
for i := range 3 {
151+
description := "Rule description"
152+
fromPort := tools.RandomInt(80, 89)
153+
toPort := tools.RandomInt(90, 99)
154+
155+
sgRulesCreateOpts[i] = rules.CreateOpts{
156+
Description: description,
157+
Direction: "ingress",
158+
EtherType: "IPv4",
159+
SecGroupID: secGroupID,
160+
PortRangeMin: fromPort,
161+
PortRangeMax: toPort,
162+
Protocol: rules.ProtocolTCP,
163+
}
164+
}
165+
166+
rules, err := rules.CreateBulk(context.TODO(), client, sgRulesCreateOpts).Extract()
167+
if err != nil {
168+
return rules, err
169+
}
170+
171+
for i, rule := range rules {
172+
t.Logf("Created security group rule: %s", rule.ID)
173+
174+
th.AssertEquals(t, sgRulesCreateOpts[i].SecGroupID, rule.SecGroupID)
175+
th.AssertEquals(t, sgRulesCreateOpts[i].Description, rule.Description)
176+
}
177+
178+
return rules, nil
179+
}
180+
143181
// DeleteSecurityGroup will delete a security group of a specified ID.
144182
// A fatal error will occur if the deletion failed. This works best as a
145183
// deferred function

internal/acceptance/openstack/networking/v2/extensions/security_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ func TestSecurityGroupsCreateUpdateDelete(t *testing.T) {
2626
th.AssertNoErr(t, err)
2727
defer DeleteSecurityGroupRule(t, client, rule.ID)
2828

29+
rules, err := CreateSecurityGroupRulesBulk(t, client, group.ID)
30+
th.AssertNoErr(t, err)
31+
for _, r := range rules {
32+
defer DeleteSecurityGroupRule(t, client, r.ID)
33+
}
34+
2935
tools.PrintResource(t, group)
3036

3137
var name = "Update group"

openstack/networking/v2/extensions/security/rules/requests.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,23 @@ func Create(ctx context.Context, c *gophercloud.ServiceClient, opts CreateOptsBu
150150
return
151151
}
152152

153+
// CreateBulk is an operation which adds new security group rules and associates them
154+
// with an existing security group (whose ID is specified in CreateOpts).
155+
// As of Dalmatian (2024.2) neutron only allows bulk creation of rules when
156+
// they all belong to the same tenant and security group.
157+
// https://github.com/openstack/neutron/blob/6183792/neutron/db/securitygroups_db.py#L814-L828
158+
func CreateBulk(ctx context.Context, c *gophercloud.ServiceClient, opts []CreateOpts) (r CreateBulkResult) {
159+
body, err := gophercloud.BuildRequestBody(opts, "security_group_rules")
160+
if err != nil {
161+
r.Err = err
162+
return
163+
}
164+
165+
resp, err := c.Post(ctx, rootURL(c), body, &r.Body, nil)
166+
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
167+
return
168+
}
169+
153170
// Get retrieves a particular security group rule based on its unique ID.
154171
func Get(ctx context.Context, c *gophercloud.ServiceClient, id string) (r GetResult) {
155172
resp, err := c.Get(ctx, resourceURL(c, id), &r.Body, nil)

openstack/networking/v2/extensions/security/rules/results.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ type commonResult struct {
103103
gophercloud.Result
104104
}
105105

106+
type bulkResult struct {
107+
gophercloud.Result
108+
}
109+
106110
// Extract is a function that accepts a result and extracts a security rule.
107111
func (r commonResult) Extract() (*SecGroupRule, error) {
108112
var s struct {
@@ -112,12 +116,27 @@ func (r commonResult) Extract() (*SecGroupRule, error) {
112116
return s.SecGroupRule, err
113117
}
114118

119+
// Extract is a function that accepts a result and extracts security rules.
120+
func (r bulkResult) Extract() ([]SecGroupRule, error) {
121+
var s struct {
122+
SecGroupRules []SecGroupRule `json:"security_group_rules"`
123+
}
124+
err := r.ExtractInto(&s)
125+
return s.SecGroupRules, err
126+
}
127+
115128
// CreateResult represents the result of a create operation. Call its Extract
116129
// method to interpret it as a SecGroupRule.
117130
type CreateResult struct {
118131
commonResult
119132
}
120133

134+
// CreateBulkResult represents the result of a bulk create operation. Call its
135+
// Extract method to interpret it as a slice of SecGroupRules.
136+
type CreateBulkResult struct {
137+
bulkResult
138+
}
139+
121140
// GetResult represents the result of a get operation. Call its Extract
122141
// method to interpret it as a SecGroupRule.
123142
type GetResult struct {

openstack/networking/v2/extensions/security/rules/testing/requests_test.go

Lines changed: 95 additions & 0 deletions
< 65CE td data-grid-cell-id="diff-bfb8a7681276b275caa147f2c125dd084759d464ed2032d9dbef6e8e1518e77c-224-299-0" data-selected="false" role="gridcell" style="background-color:var(--diffBlob-additionNum-bgColor, var(--diffBlob-addition-bgColor-num));text-align:center" tabindex="-1" valign="top" class="focusable-grid-cell diff-line-number position-relative left-side">
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,101 @@ func TestCreateAnyProtocol(t *testing.T) {
222222
th.AssertNoErr(t, err)
223223
}
224224

225+
func TestCreateBulk(t *testing.T) {
226+
th.SetupHTTP()
227+
defer th.TeardownHTTP()
228+
229+
th.Mux.HandleFunc("/v2.0/security-group-rules", func(w http.ResponseWriter, r *http.Request) {
230+
th.TestMethod(t, r, "POST")
231+
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
232+
th.TestHeader(t, r, "Content-Type", "application/json")
233+
th.TestHeader(t, r, "Accept", "application/json")
234+
th.TestJSONRequest(t, r, `
235+
{
236+
"security_group_rules": [
237+
{
238+
"description": "test description of rule",
239+
"direction": "ingress",
240+
"port_range_min": 80,
241+
"ethertype": "IPv4",
242+
"port_range_max": 80,
243+
"protocol": "tcp",
244+
"remote_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
245+
"security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a"
246+
},
247+
{
248+
"description": "test description of rule",
249+
"direction": "ingress",
250+
"port_range_min": 443,
251+
"ethertype": "IPv4",
252+
"port_range_max": 443,
253+
"protocol": "tcp",
254+
"security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a"
255+
}
256+
]
257+
}
258+
`)
259+
260+
w.Header().Add("Content-Type", "application/json")
261+
w.WriteHeader(http.StatusCreated)
262+
263+
fmt.Fprint(w, `
264+
{
265+
"security_group_rules": [
266+
{
267+
"description": "test description of rule",
268+
"direction": "ingress",
269+
"ethertype": "IPv4",
270+
"port_range_max": 80,
271+
"port_range_min": 80,
272+
"protocol": "tcp",
273+
"remote_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
274+
"remote_ip_prefix": null,
275+
"security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a",
276+
"tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
277+
},
278+
{
279+
"description": "test description of rule",
280+
"direction": "ingress",
281+
"ethertype": "IPv4",
282+
"port_range_max": 443,
283+
"port_range_min": 443,
284+
"protocol": "tcp",
285+
"remote_group_id": null,
286+
"remote_ip_prefix": null,
287+
"security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a",
288+
"tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
289+
}
290+
]
291+
}
292+
`)
293+
})
294+
295+
opts := []rules.CreateOpts{
296+
{
297+
Description: "test description of rule",
298+
Direction: "ingress",
299+
PortRangeMin: 80,
300+
EtherType: rules.EtherType4,
301+
PortRangeMax: 80,
302+
Protocol: "tcp",
303+
RemoteGroupID: "85cc3048-abc3-43cc-89b3-377341426ac5",
304+
SecGroupID: "a7734e61-b545-452d-a3cd-0189cbd9747a",
305+
},
306+
{
307+
Description: "test description of rule",
308+
Direction: "ingress",
309+
PortRangeMin: 443,
310+
EtherType: rules.EtherType4,
311+
PortRangeMax: 443,
312+
Protocol: "tcp",
313+
SecGroupID: "a7734e61-b545-452d-a3cd-0189cbd9747a",
314+
},
315+
}
316+
_, err := rules.CreateBulk(context.TODO(), fake.ServiceClient(), opts).Extract()
317+
th.AssertNoErr(t, err)
318+
}
319+
225320
func TestRequiredCreateOpts(t *testing.T) {
226321
res := rules.Create(context.TODO(), fake.ServiceClient(), rules.CreateOpts{Direction: rules.DirIngress})
227322
if res.Err == nil {

params.go

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ import (
1111
)
1212

1313
/*
14-
BuildRequestBody builds a map[string]interface from the given `struct`. If
15-
parent is not an empty string, the final map[string]interface returned will
16-
encapsulate the built one. For example:
14+
BuildRequestBody builds a map[string]interface from the given `struct`, or
15+
collection of `structs`. If parent is not an empty string, the final
16+
map[string]interface returned will encapsulate the built one. Parent is
17+
required when passing a list of `structs`.
18+
For example:
1719
1820
disk := 1
1921
createOpts := flavors.CreateOpts{
@@ -27,7 +29,29 @@ encapsulate the built one. For example:
2729
2830
body, err := gophercloud.BuildRequestBody(createOpts, "flavor")
2931
30-
The above example can be run as-is, however it is recommended to look at how
32+
33+
opts := []rules.CreateOpts{
34+
{
35+
Direction: "ingress",
36+
PortRangeMin: 80,
37+
EtherType: rules.EtherType4,
38+
PortRangeMax: 80,
39+
Protocol: "tcp",
40+
SecGroupID: "a7734e61-b545-452d-a3cd-0189cbd9747a",
41+
},
42+
{
43+
Direction: "ingress",
44+
PortRangeMin: 443,
45+
EtherType: rules.EtherType4,
46+
PortRangeMax: 443,
47+
Protocol: "tcp",
48+
SecGroupID: "a7734e61-b545-452d-a3cd-0189cbd9747a",
49+
},
50+
}
51+
52+
body, err := gophercloud.BuildRequestBody(opts, "security_group_rules")
53+
54+
The above examples can be run as-is, however it is recommended to look at how
3155
BuildRequestBody is used within Gophercloud to more fully understand how it
3256
fits within the request process as a whole rather than use it directly as shown
3357
above.
@@ -44,7 +68,8 @@ func BuildRequestBody(opts any, parent string) (map[string]any, error) {
4468
}
4569

4670
optsMap := make(map[string]any)
47-
if optsValue.Kind() == reflect.Struct {
71+
switch optsValue.Kind() {
72+
case reflect.Struct:
4873
//fmt.Printf("optsValue.Kind() is a reflect.Struct: %+v\n", optsValue.Kind())
4974
for i := 0; i < optsValue.NumField(); i++ {
5075
v := optsValue.Field(i)
@@ -184,9 +209,22 @@ func BuildRequestBody(opts any, parent string) (map[string]any, error) {
184209
}
185210
//fmt.Printf("optsMap after parent added: %+v\n", optsMap)
186211
return optsMap, nil
212+
case reflect.Slice, reflect.Array:
213+
optsMaps := make([]map[string]any, optsValue.Len())
214+
for i := 0; i < optsValue.Len(); i++ {
215+
b, err := BuildRequestBody(optsValue.Index(i).Interface(), "")
216+
if err != nil {
217+
return nil, err
218+
}
219+
optsMaps[i] = b
220+
}
221+
if parent == "" {
222+
return nil, fmt.Errorf("Parent is required when passing an array or a slice.")
223+
}
224+
return map[string]any{parent: optsMaps}, nil
187225
}
188-
// Return an error if the underlying type of 'opts' isn't a struct.
189-
return nil, fmt.Errorf("Options type is not a struct.")
226+
// Return an error if we can't work with the underlying type of 'opts'
227+
return nil, fmt.Errorf("Options type is not a struct, a slice, or an array.")
190228
}
191229

192230
// EnabledState is a convenience type, mostly used in Create and Update

0 commit comments

Comments
 (0)
0