Merge lp:~gz/juju-core/secgroup_vpc into lp:~go-bot/juju-core/trunk

Proposed by Martin Packman
Status: Merged
Approved by: Martin Packman
Approved revision: no longer in the source branch.
Merged at revision: 1943
Proposed branch: lp:~gz/juju-core/secgroup_vpc
Merge into: lp:~go-bot/juju-core/trunk
Diff against target: 335 lines (+129/-57)
3 files modified
dependencies.tsv (+1/-1)
provider/ec2/ec2.go (+103/-47)
provider/ec2/live_test.go (+25/-9)
To merge this branch: bzr merge lp:~gz/juju-core/secgroup_vpc
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+188958@code.launchpad.net

Commit message

provider/ec2: Use security group ids in EC2 API

The additional options available with VPC security groups
comes with the limitation that they must be referred to by
id rather than by name. To support the otherwise mostly
transparent default VPC mode some new accounts use, switch
our provider infrastructure to lookup and use ids.

This adds some additional overhead in ec2 api roundtrips,
which would be avoidable in the ec2-classic case by
detecting if the account is default-vpc enabled using
DescribeAccountAttributes and continuing to just use the
names if not. For now, that is punted on that in favour
of verifying the new codepath works in all cases.

https://codereview.appspot.com/14309043/

R=rogpeppe

Description of the change

provider/ec2: Use security group ids in EC2 API

The additional options available with VPC security groups
comes with the limitation that they must be referred to by
id rather than by name. To support the otherwise mostly
transparent default VPC mode some new accounts use, switch
our provider infrastructure to lookup and use ids.

This adds some additional overhead in ec2 api roundtrips,
which would be avoidable in the ec2-classic case by
detecting if the account is default-vpc enabled using
DescribeAccountAttributes and continuing to just use the
names if not. For now, that is punted on that in favour
of verifying the new codepath works in all cases.

There are a couple of work items pending for this branch.

I have run and confirmed that a subset of the live tests,
including those most related to security group handling,
pass on both classic and default vpc regions. However, a
clean run of the whole live suite on trunk is eluding me.

Two small updates to goamz are required for the local live
suite to pass. These are both approved, but need to land,
and dependencies.csv updated.

https://codereview.appspot.com/14309043/

To post a comment you must log in.
Revision history for this message
Martin Packman (gz) wrote :

Reviewers: mp+188958_code.launchpad.net,

Message:
Please take a look.

Description:
provider/ec2: Use security group ids in EC2 API

The additional options available with VPC security groups
comes with the limitation that they must be referred to by
id rather than by name. To support the otherwise mostly
transparent default VPC mode some new accounts use, switch
our provider infrastructure to lookup and use ids.

This adds some additional overhead in ec2 api roundtrips,
which would be avoidable in the ec2-classic case by
detecting if the account is default-vpc enabled using
DescribeAccountAttributes and continuing to just use the
names if not. For now, that is punted on that in favour
of verifying the new codepath works in all cases.

There are a couple of work items pending for this branch.

I have run and confirmed that a subset of the live tests,
including those most related to security group handling,
pass on both classic and default vpc regions. However, a
clean run of the whole live suite on trunk is eluding me.

Two small updates to goamz are required for the local live
suite to pass. These are both approved, but need to land,
and dependencies.csv updated.

https://code.launchpad.net/~gz/juju-core/secgroup_vpc/+merge/188958

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/14309043/

Affected files (+124, -53 lines):
   A [revision details]
   M provider/ec2/ec2.go
   M provider/ec2/live_test.go

Revision history for this message
Roger Peppe (rogpeppe) wrote :

LGTM with some thoughts and suggestions below.

https://codereview.appspot.com/14309043/diff/1/provider/ec2/ec2.go
File provider/ec2/ec2.go (left):

https://codereview.appspot.com/14309043/diff/1/provider/ec2/ec2.go#oldcode796
provider/ec2/ec2.go:796: SourceGroups: sourceGroups,
This seems suspicious to me. How is the new code accomplishing the same
thing as this? (i.e. allowing inter-instance traffic between the same
group only)

https://codereview.appspot.com/14309043/diff/1/provider/ec2/ec2.go
File provider/ec2/ec2.go (right):

https://codereview.appspot.com/14309043/diff/1/provider/ec2/ec2.go#newcode465
provider/ec2/ec2.go:465: func (e *environ)
lookupGroupInfoByName(groupName string) (ec2.SecurityGroupInfo, error) {
The "lookup" part of this method name seems a bit superfluous to me.

I think "groupInfoByName" and "groupByName" would read quite nicely.

https://codereview.appspot.com/14309043/diff/1/provider/ec2/ec2.go#newcode502
provider/ec2/ec2.go:502: filter.Add("instance.group-id", group.Id)
I think this (and probably other stuff too) will need the
dependencies.tsv file to be updated.

https://codereview.appspot.com/14309043/diff/1/provider/ec2/ec2.go#newcode885
provider/ec2/ec2.go:885: // If it exists, its permissions are set to
perms.
Ah, I see now, I think.

This could perhaps do with a comment:
// Any entries in perms without a SourceIPs entry
// will be given permissions for the named group
// only.

https://codereview.appspot.com/14309043/diff/1/provider/ec2/ec2.go#newcode953
provider/ec2/ec2.go:953: // id in source groups, using group ids only.
This comment no longer makes much sense.

How about something like:

// newPermSetForGroup returns a set of all the permissions
// in the given slice of IPPerms. It ignores the name
// and owner id in source groups; any entry with
// no source IPs will be assigned to the given group.

?

https://codereview.appspot.com/14309043/diff/1/provider/ec2/live_test.go
File provider/ec2/live_test.go (right):

https://codereview.appspot.com/14309043/diff/1/provider/ec2/live_test.go#newcode255
provider/ec2/live_test.go:255: inst_ids := []instance.Id{inst0.Id(),
inst1.Id()}
s/inst_ids/instIds/

https://codereview.appspot.com/14309043/diff/1/provider/ec2/live_test.go#newcode256
provider/ec2/live_test.go:256: ids_from_insts := func(insts
[]instance.Instance) (ids []instance.Id) {
s/ids_from_insts/idsFromInsts/

https://codereview.appspot.com/14309043/diff/1/provider/ec2/live_test.go#newcode265
provider/ec2/live_test.go:265: all_insts, err := t.Env.AllInstances()
s/all_insts/allInsts/

https://codereview.appspot.com/14309043/

Revision history for this message
Martin Packman (gz) wrote :

Please take a look.

https://codereview.appspot.com/14309043/diff/1/provider/ec2/ec2.go
File provider/ec2/ec2.go (right):

https://codereview.appspot.com/14309043/diff/1/provider/ec2/ec2.go#newcode465
provider/ec2/ec2.go:465: func (e *environ)
lookupGroupInfoByName(groupName string) (ec2.SecurityGroupInfo, error) {
On 2013/10/03 00:08:53, rog wrote:
> The "lookup" part of this method name seems a bit superfluous to me.

> I think "groupInfoByName" and "groupByName" would read quite nicely.

Done.

https://codereview.appspot.com/14309043/diff/1/provider/ec2/ec2.go#newcode502
provider/ec2/ec2.go:502: filter.Add("instance.group-id", group.Id)
On 2013/10/03 00:08:53, rog wrote:
> I think this (and probably other stuff too) will need the
dependencies.tsv file
> to be updated.

Now it's landed, have updated the dependencies.tsv file.

https://codereview.appspot.com/14309043/diff/1/provider/ec2/ec2.go#newcode885
provider/ec2/ec2.go:885: // If it exists, its permissions are set to
perms.
On 2013/10/03 00:08:53, rog wrote:
> Ah, I see now, I think.

> This could perhaps do with a comment:
> // Any entries in perms without a SourceIPs entry
> // will be given permissions for the named group
> // only.

That's reasonable, have added something along those lines.

https://codereview.appspot.com/14309043/diff/1/provider/ec2/ec2.go#newcode953
provider/ec2/ec2.go:953: // id in source groups, using group ids only.
On 2013/10/03 00:08:53, rog wrote:
> This comment no longer makes much sense.

> How about something like:

> // newPermSetForGroup returns a set of all the permissions
> // in the given slice of IPPerms. It ignores the name
> // and owner id in source groups; any entry with
> // no source IPs will be assigned to the given group.

> ?

Have added something along these lines.

https://codereview.appspot.com/14309043/diff/1/provider/ec2/live_test.go
File provider/ec2/live_test.go (right):

https://codereview.appspot.com/14309043/diff/1/provider/ec2/live_test.go#newcode255
provider/ec2/live_test.go:255: inst_ids := []instance.Id{inst0.Id(),
inst1.Id()}
On 2013/10/03 00:08:53, rog wrote:
> s/inst_ids/instIds/

Done.

https://codereview.appspot.com/14309043/diff/1/provider/ec2/live_test.go#newcode256
provider/ec2/live_test.go:256: ids_from_insts := func(insts
[]instance.Instance) (ids []instance.Id) {
On 2013/10/03 00:08:53, rog wrote:
> s/ids_from_insts/idsFromInsts/

Done.

https://codereview.appspot.com/14309043/diff/1/provider/ec2/live_test.go#newcode265
provider/ec2/live_test.go:265: all_insts, err := t.Env.AllInstances()
On 2013/10/03 00:08:53, rog wrote:
> s/all_insts/allInsts/

Done.

https://codereview.appspot.com/14309043/diff/1/provider/ec2/live_test.go#newcode435
provider/ec2/live_test.go:435: func hasSecurityGroup(r amzec2.Instance,
g amzec2.SecurityGroup) bool {
Also renamed this from r.

https://codereview.appspot.com/14309043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'dependencies.tsv'
2--- dependencies.tsv 2013-09-15 07:36:52 +0000
3+++ dependencies.tsv 2013-10-03 14:20:06 +0000
4@@ -2,7 +2,7 @@
5 code.google.com/p/go.net hg 3591c18acabc99439c783463ef00e6dc277eee39 77
6 labix.org/v2/mgo bzr gustavo@niemeyer.net-20130830194015-zk51jnp704tqmp7n 240
7 launchpad.net/gnuflag bzr roger.peppe@canonical.com-20121003093437-zcyyw0lpvj2nifpk 12
8-launchpad.net/goamz bzr martin.packman@canonical.com-20130820120225-azobjlnr281zn72q 40
9+launchpad.net/goamz bzr martin.packman@canonical.com-20131003121033-vsyrs6xtmr1le102 42
10 launchpad.net/gocheck bzr gustavo@niemeyer.net-20130302024745-6ikofwq2c03h7giu 85
11 launchpad.net/golxc bzr frank.mueller@canonical.com-20130617094614-jelomk87defuetol 5
12 launchpad.net/gomaasapi bzr tarmac-20130730082905-neqonnq9tk03ll8f 43
13
14=== modified file 'provider/ec2/ec2.go'
15--- provider/ec2/ec2.go 2013-10-02 22:22:01 +0000
16+++ provider/ec2/ec2.go 2013-10-03 14:20:06 +0000
17@@ -459,6 +459,49 @@
18 return e.terminateInstances(ids)
19 }
20
21+// groupInfoByName returns information on the security group
22+// with the given name including rules and other details.
23+func (e *environ) groupInfoByName(groupName string) (ec2.SecurityGroupInfo, error) {
24+ // Non-default VPC does not support name-based group lookups, can
25+ // use a filter by group name instead when support is needed.
26+ limitToGroups := []ec2.SecurityGroup{{Name: groupName}}
27+ resp, err := e.ec2().SecurityGroups(limitToGroups, nil)
28+ if err != nil {
29+ return ec2.SecurityGroupInfo{}, err
30+ }
31+ if len(resp.Groups) != 1 {
32+ return ec2.SecurityGroupInfo{}, fmt.Errorf("expected one security group named %q, got %v", groupName, resp.Groups)
33+ }
34+ return resp.Groups[0], nil
35+}
36+
37+// groupByName returns the security group with the given name.
38+func (e *environ) groupByName(groupName string) (ec2.SecurityGroup, error) {
39+ groupInfo, err := e.groupInfoByName(groupName)
40+ return groupInfo.SecurityGroup, err
41+}
42+
43+// addGroupFilter sets a limit an instance filter so only those machines
44+// with the juju environment wide security group associated will be listed.
45+//
46+// An EC2 API call is required to resolve the group name to an id, as VPC
47+// enabled accounts do not support name based filtering.
48+// TODO: Detect classic accounts and just filter by name for those.
49+//
50+// Callers must handle InvalidGroup.NotFound errors to mean the same as no
51+// matching instances.
52+func (e *environ) addGroupFilter(filter *ec2.Filter) error {
53+ groupName := e.jujuGroupName()
54+ group, err := e.groupByName(groupName)
55+ if err != nil {
56+ return err
57+ }
58+ // EC2 should support filtering with and without the 'instance.'
59+ // prefix, but only the form with seems to work with default VPC.
60+ filter.Add("instance.group-id", group.Id)
61+ return nil
62+}
63+
64 // gatherInstances tries to get information on each instance
65 // id whose corresponding insts slot is nil.
66 // It returns environs.ErrPartialInstances if the insts
67@@ -475,7 +518,13 @@
68 }
69 filter := ec2.NewFilter()
70 filter.Add("instance-state-name", "pending", "running")
71- filter.Add("group-name", e.jujuGroupName())
72+ err := e.addGroupFilter(filter)
73+ if err != nil {
74+ if ec2ErrCode(err) == "InvalidGroup.NotFound" {
75+ return environs.ErrPartialInstances
76+ }
77+ return err
78+ }
79 filter.Add("instance-id", need...)
80 resp, err := e.ec2().Instances(nil, filter)
81 if err != nil {
82@@ -538,7 +587,13 @@
83 func (e *environ) AllInstances() ([]instance.Instance, error) {
84 filter := ec2.NewFilter()
85 filter.Add("instance-state-name", "pending", "running")
86- filter.Add("group-name", e.jujuGroupName())
87+ err := e.addGroupFilter(filter)
88+ if err != nil {
89+ if ec2ErrCode(err) == "InvalidGroup.NotFound" {
90+ return nil, nil
91+ }
92+ return nil, err
93+ }
94 resp, err := e.ec2().Instances(nil, filter)
95 if err != nil {
96 return nil, err
97@@ -576,9 +631,12 @@
98 return nil
99 }
100 // Give permissions for anyone to access the given ports.
101+ g, err := e.groupByName(name)
102+ if err != nil {
103+ return err
104+ }
105 ipPerms := portsToIPPerms(ports)
106- g := ec2.SecurityGroup{Name: name}
107- _, err := e.ec2().AuthorizeSecurityGroup(g, ipPerms)
108+ _, err = e.ec2().AuthorizeSecurityGroup(g, ipPerms)
109 if err != nil && ec2ErrCode(err) == "InvalidPermission.Duplicate" {
110 if len(ports) == 1 {
111 return nil
112@@ -608,8 +666,11 @@
113 // Revoke permissions for anyone to access the given ports.
114 // Note that ec2 allows the revocation of permissions that aren't
115 // granted, so this is naturally idempotent.
116- g := ec2.SecurityGroup{Name: name}
117- _, err := e.ec2().RevokeSecurityGroup(g, portsToIPPerms(ports))
118+ g, err := e.groupByName(name)
119+ if err != nil {
120+ return err
121+ }
122+ _, err = e.ec2().RevokeSecurityGroup(g, portsToIPPerms(ports))
123 if err != nil {
124 return fmt.Errorf("cannot close ports: %v", err)
125 }
126@@ -617,15 +678,11 @@
127 }
128
129 func (e *environ) portsInGroup(name string) (ports []instance.Port, err error) {
130- g := ec2.SecurityGroup{Name: name}
131- resp, err := e.ec2().SecurityGroups([]ec2.SecurityGroup{g}, nil)
132+ group, err := e.groupInfoByName(name)
133 if err != nil {
134 return nil, err
135 }
136- if len(resp.Groups) != 1 {
137- return nil, fmt.Errorf("expected one security group, got %d", len(resp.Groups))
138- }
139- for _, p := range resp.Groups[0].IPPerms {
140+ for _, p := range group.IPPerms {
141 if len(p.SourceIPs) != 1 {
142 logger.Warningf("unexpected IP permission found: %v", p)
143 continue
144@@ -767,7 +824,6 @@
145 // addition, a specific machine security group is created for each
146 // machine, so that its firewall rules can be configured per machine.
147 func (e *environ) setUpGroups(machineId string, statePort, apiPort int) ([]ec2.SecurityGroup, error) {
148- sourceGroups := []ec2.UserSecurityGroup{{Name: e.jujuGroupName()}}
149 jujuGroup, err := e.ensureGroup(e.jujuGroupName(),
150 []ec2.IPPerm{
151 {
152@@ -789,22 +845,19 @@
153 SourceIPs: []string{"0.0.0.0/0"},
154 },
155 {
156- Protocol: "tcp",
157- FromPort: 0,
158- ToPort: 65535,
159- SourceGroups: sourceGroups,
160- },
161- {
162- Protocol: "udp",
163- FromPort: 0,
164- ToPort: 65535,
165- SourceGroups: sourceGroups,
166- },
167- {
168- Protocol: "icmp",
169- FromPort: -1,
170- ToPort: -1,
171- SourceGroups: sourceGroups,
172+ Protocol: "tcp",
173+ FromPort: 0,
174+ ToPort: 65535,
175+ },
176+ {
177+ Protocol: "udp",
178+ FromPort: 0,
179+ ToPort: 65535,
180+ },
181+ {
182+ Protocol: "icmp",
183+ FromPort: -1,
184+ ToPort: -1,
185 },
186 })
187 if err != nil {
188@@ -829,6 +882,8 @@
189 // ensureGroup returns the security group with name and perms.
190 // If a group with name does not exist, one will be created.
191 // If it exists, its permissions are set to perms.
192+// Any entries in perms without SourceIPs will be granted for
193+// the named group only.
194 func (e *environ) ensureGroup(name string, perms []ec2.IPPerm) (g ec2.SecurityGroup, err error) {
195 ec2inst := e.ec2()
196 resp, err := ec2inst.CreateSecurityGroup(name, "juju group")
197@@ -849,10 +904,10 @@
198 // description here, but if it does it's probably due
199 // to something deliberately playing games with juju,
200 // so we ignore it.
201- have = newPermSet(info.IPPerms)
202 g = info.SecurityGroup
203+ have = newPermSetForGroup(info.IPPerms, g)
204 }
205- want := newPermSet(perms)
206+ want := newPermSetForGroup(perms, g)
207 revoke := make(permSet)
208 for p := range have {
209 if !want[p] {
210@@ -885,19 +940,20 @@
211 // to access the given range of ports. Only one of groupName or ipAddr
212 // should be non-empty.
213 type permKey struct {
214- protocol string
215- fromPort int
216- toPort int
217- groupName string
218- ipAddr string
219+ protocol string
220+ fromPort int
221+ toPort int
222+ groupId string
223+ ipAddr string
224 }
225
226 type permSet map[permKey]bool
227
228-// newPermSet returns a set of all the permissions in the
229+// newPermSetForGroup returns a set of all the permissions in the
230 // given slice of IPPerms. It ignores the name and owner
231-// id in source groups, using group ids only.
232-func newPermSet(ps []ec2.IPPerm) permSet {
233+// id in source groups, and any entry with no source ips will
234+// be granted for the given group only.
235+func newPermSetForGroup(ps []ec2.IPPerm, group ec2.SecurityGroup) permSet {
236 m := make(permSet)
237 for _, p := range ps {
238 k := permKey{
239@@ -905,13 +961,13 @@
240 fromPort: p.FromPort,
241 toPort: p.ToPort,
242 }
243- for _, g := range p.SourceGroups {
244- k.groupName = g.Name
245- m[k] = true
246- }
247- k.groupName = ""
248- for _, ip := range p.SourceIPs {
249- k.ipAddr = ip
250+ if len(p.SourceIPs) > 0 {
251+ for _, ip := range p.SourceIPs {
252+ k.ipAddr = ip
253+ m[k] = true
254+ }
255+ } else {
256+ k.groupId = group.Id
257 m[k] = true
258 }
259 }
260@@ -932,7 +988,7 @@
261 if p.ipAddr != "" {
262 ipp.SourceIPs = []string{p.ipAddr}
263 } else {
264- ipp.SourceGroups = []ec2.UserSecurityGroup{{Name: p.groupName}}
265+ ipp.SourceGroups = []ec2.UserSecurityGroup{{Id: p.groupId}}
266 }
267 ps = append(ps, ipp)
268 }
269
270=== modified file 'provider/ec2/live_test.go'
271--- provider/ec2/live_test.go 2013-09-30 19:40:06 +0000
272+++ provider/ec2/live_test.go 2013-10-03 14:20:06 +0000
273@@ -147,6 +147,7 @@
274 }
275
276 func (t *LiveTests) TestInstanceGroups(c *gc.C) {
277+ t.PrepareOnce(c)
278 ec2conn := ec2.EnvironEC2(t.Env)
279
280 groups := amzec2.SecurityGroupNames(
281@@ -235,20 +236,35 @@
282 for _, r := range resp.Reservations {
283 c.Assert(r.Instances, gc.HasLen, 1)
284 // each instance must be part of the general juju group.
285- msg := gc.Commentf("reservation %#v", r)
286- c.Assert(hasSecurityGroup(r, groups[0]), gc.Equals, true, msg)
287 inst := r.Instances[0]
288+ msg := gc.Commentf("instance %#v", inst)
289+ c.Assert(hasSecurityGroup(inst, groups[0]), gc.Equals, true, msg)
290 switch instance.Id(inst.InstanceId) {
291 case inst0.Id():
292- c.Assert(hasSecurityGroup(r, groups[1]), gc.Equals, true, msg)
293- c.Assert(hasSecurityGroup(r, groups[2]), gc.Equals, false, msg)
294+ c.Assert(hasSecurityGroup(inst, groups[1]), gc.Equals, true, msg)
295+ c.Assert(hasSecurityGroup(inst, groups[2]), gc.Equals, false, msg)
296 case inst1.Id():
297- c.Assert(hasSecurityGroup(r, groups[2]), gc.Equals, true, msg)
298- c.Assert(hasSecurityGroup(r, groups[1]), gc.Equals, false, msg)
299+ c.Assert(hasSecurityGroup(inst, groups[2]), gc.Equals, true, msg)
300+ c.Assert(hasSecurityGroup(inst, groups[1]), gc.Equals, false, msg)
301 default:
302 c.Errorf("unknown instance found: %v", inst)
303 }
304 }
305+
306+ // Check that listing those instances finds them using the groups
307+ instIds := []instance.Id{inst0.Id(), inst1.Id()}
308+ idsFromInsts := func(insts []instance.Instance) (ids []instance.Id) {
309+ for _, inst := range insts {
310+ ids = append(ids, inst.Id())
311+ }
312+ return ids
313+ }
314+ insts, err := t.Env.Instances(instIds)
315+ c.Assert(err, gc.IsNil)
316+ c.Assert(instIds, jc.SameContents, idsFromInsts(insts))
317+ allInsts, err := t.Env.AllInstances()
318+ c.Assert(err, gc.IsNil)
319+ c.Assert(instIds, jc.SameContents, idsFromInsts(allInsts))
320 }
321
322 func (t *LiveTests) TestDestroy(c *gc.C) {
323@@ -416,9 +432,9 @@
324 return gi.SecurityGroup
325 }
326
327-func hasSecurityGroup(r amzec2.Reservation, g amzec2.SecurityGroup) bool {
328- for _, rg := range r.SecurityGroups {
329- if rg.Id == g.Id {
330+func hasSecurityGroup(inst amzec2.Instance, group amzec2.SecurityGroup) bool {
331+ for _, instGroup := range inst.SecurityGroups {
332+ if instGroup.Id == group.Id {
333 return true
334 }
335 }

Subscribers

People subscribed via source and target branches

to status/vote changes: