1# Copyright (c) 2006-2011 Mitch Garnaat http://garnaat.org/
2# Copyright (c) 2011, Eucalyptus Systems, Inc.
3#
4# Permission is hereby granted, free of charge, to any person obtaining a
5# copy of this software and associated documentation files (the
6# "Software"), to deal in the Software without restriction, including
7# without limitation the rights to use, copy, modify, merge, publish, dis-
8# tribute, sublicense, and/or sell copies of the Software, and to permit
9# persons to whom the Software is furnished to do so, subject to the fol-
10# lowing conditions:
11#
12# The above copyright notice and this permission notice shall be included
13# in all copies or substantial portions of the Software.
14#
15# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
17# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
18# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
19# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21# IN THE SOFTWARE.
22
23"""
24Represents an EC2 Security Group
25"""
26from boto.ec2.ec2object import TaggedEC2Object
27from boto.exception import BotoClientError
28
29
30class SecurityGroup(TaggedEC2Object):
31
32    def __init__(self, connection=None, owner_id=None,
33                 name=None, description=None, id=None):
34        super(SecurityGroup, self).__init__(connection)
35        self.id = id
36        self.owner_id = owner_id
37        self.name = name
38        self.description = description
39        self.vpc_id = None
40        self.rules = IPPermissionsList()
41        self.rules_egress = IPPermissionsList()
42
43    def __repr__(self):
44        return 'SecurityGroup:%s' % self.name
45
46    def startElement(self, name, attrs, connection):
47        retval = super(SecurityGroup, self).startElement(name, attrs, connection)
48        if retval is not None:
49            return retval
50        if name == 'ipPermissions':
51            return self.rules
52        elif name == 'ipPermissionsEgress':
53            return self.rules_egress
54        else:
55            return None
56
57    def endElement(self, name, value, connection):
58        if name == 'ownerId':
59            self.owner_id = value
60        elif name == 'groupId':
61            self.id = value
62        elif name == 'groupName':
63            self.name = value
64        elif name == 'vpcId':
65            self.vpc_id = value
66        elif name == 'groupDescription':
67            self.description = value
68        elif name == 'ipRanges':
69            pass
70        elif name == 'return':
71            if value == 'false':
72                self.status = False
73            elif value == 'true':
74                self.status = True
75            else:
76                raise Exception(
77                    'Unexpected value of status %s for group %s' % (
78                        value,
79                        self.name
80                    )
81                )
82        else:
83            setattr(self, name, value)
84
85    def delete(self, dry_run=False):
86        if self.vpc_id:
87            return self.connection.delete_security_group(
88                group_id=self.id,
89                dry_run=dry_run
90            )
91        else:
92            return self.connection.delete_security_group(
93                self.name,
94                dry_run=dry_run
95            )
96
97    def add_rule(self, ip_protocol, from_port, to_port,
98                 src_group_name, src_group_owner_id, cidr_ip,
99                 src_group_group_id, dry_run=False):
100        """
101        Add a rule to the SecurityGroup object.  Note that this method
102        only changes the local version of the object.  No information
103        is sent to EC2.
104        """
105        rule = IPPermissions(self)
106        rule.ip_protocol = ip_protocol
107        rule.from_port = from_port
108        rule.to_port = to_port
109        self.rules.append(rule)
110        rule.add_grant(
111            src_group_name,
112            src_group_owner_id,
113            cidr_ip,
114            src_group_group_id,
115            dry_run=dry_run
116        )
117
118    def remove_rule(self, ip_protocol, from_port, to_port,
119                    src_group_name, src_group_owner_id, cidr_ip,
120                    src_group_group_id, dry_run=False):
121        """
122        Remove a rule to the SecurityGroup object.  Note that this method
123        only changes the local version of the object.  No information
124        is sent to EC2.
125        """
126        if not self.rules:
127            raise ValueError("The security group has no rules")
128
129        target_rule = None
130        for rule in self.rules:
131            if rule.ip_protocol == ip_protocol:
132                if rule.from_port == from_port:
133                    if rule.to_port == to_port:
134                        target_rule = rule
135                        target_grant = None
136                        for grant in rule.grants:
137                            if grant.name == src_group_name or grant.group_id == src_group_group_id:
138                                if grant.owner_id == src_group_owner_id:
139                                    if grant.cidr_ip == cidr_ip:
140                                        target_grant = grant
141                        if target_grant:
142                            rule.grants.remove(target_grant)
143            if len(rule.grants) == 0:
144                self.rules.remove(target_rule)
145
146    def authorize(self, ip_protocol=None, from_port=None, to_port=None,
147                  cidr_ip=None, src_group=None, dry_run=False):
148        """
149        Add a new rule to this security group.
150        You need to pass in either src_group_name
151        OR ip_protocol, from_port, to_port,
152        and cidr_ip.  In other words, either you are authorizing another
153        group or you are authorizing some ip-based rule.
154
155        :type ip_protocol: string
156        :param ip_protocol: Either tcp | udp | icmp
157
158        :type from_port: int
159        :param from_port: The beginning port number you are enabling
160
161        :type to_port: int
162        :param to_port: The ending port number you are enabling
163
164        :type cidr_ip: string or list of strings
165        :param cidr_ip: The CIDR block you are providing access to.
166                        See http://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing
167
168        :type src_group: :class:`boto.ec2.securitygroup.SecurityGroup` or
169                         :class:`boto.ec2.securitygroup.GroupOrCIDR`
170        :param src_group: The Security Group you are granting access to.
171
172        :rtype: bool
173        :return: True if successful.
174        """
175        group_name = None
176        if not self.vpc_id:
177            group_name = self.name
178        group_id = None
179        if self.vpc_id:
180            group_id = self.id
181        src_group_name = None
182        src_group_owner_id = None
183        src_group_group_id = None
184        if src_group:
185            cidr_ip = None
186            src_group_owner_id = src_group.owner_id
187            if not self.vpc_id:
188                src_group_name = src_group.name
189            else:
190                if hasattr(src_group, 'group_id'):
191                    src_group_group_id = src_group.group_id
192                else:
193                    src_group_group_id = src_group.id
194        status = self.connection.authorize_security_group(group_name,
195                                                          src_group_name,
196                                                          src_group_owner_id,
197                                                          ip_protocol,
198                                                          from_port,
199                                                          to_port,
200                                                          cidr_ip,
201                                                          group_id,
202                                                          src_group_group_id,
203                                                          dry_run=dry_run)
204        if status:
205            if not isinstance(cidr_ip, list):
206                cidr_ip = [cidr_ip]
207            for single_cidr_ip in cidr_ip:
208                self.add_rule(ip_protocol, from_port, to_port, src_group_name,
209                              src_group_owner_id, single_cidr_ip,
210                              src_group_group_id, dry_run=dry_run)
211        return status
212
213    def revoke(self, ip_protocol=None, from_port=None, to_port=None,
214               cidr_ip=None, src_group=None, dry_run=False):
215        group_name = None
216        if not self.vpc_id:
217            group_name = self.name
218        group_id = None
219        if self.vpc_id:
220            group_id = self.id
221        src_group_name = None
222        src_group_owner_id = None
223        src_group_group_id = None
224        if src_group:
225            cidr_ip = None
226            src_group_owner_id = src_group.owner_id
227            if not self.vpc_id:
228                src_group_name = src_group.name
229            else:
230                if hasattr(src_group, 'group_id'):
231                    src_group_group_id = src_group.group_id
232                else:
233                    src_group_group_id = src_group.id
234        status = self.connection.revoke_security_group(group_name,
235                                                       src_group_name,
236                                                       src_group_owner_id,
237                                                       ip_protocol,
238                                                       from_port,
239                                                       to_port,
240                                                       cidr_ip,
241                                                       group_id,
242                                                       src_group_group_id,
243                                                       dry_run=dry_run)
244        if status:
245            self.remove_rule(ip_protocol, from_port, to_port, src_group_name,
246                             src_group_owner_id, cidr_ip, src_group_group_id,
247                             dry_run=dry_run)
248        return status
249
250    def copy_to_region(self, region, name=None, dry_run=False):
251        """
252        Create a copy of this security group in another region.
253        Note that the new security group will be a separate entity
254        and will not stay in sync automatically after the copy
255        operation.
256
257        :type region: :class:`boto.ec2.regioninfo.RegionInfo`
258        :param region: The region to which this security group will be copied.
259
260        :type name: string
261        :param name: The name of the copy.  If not supplied, the copy
262                     will have the same name as this security group.
263
264        :rtype: :class:`boto.ec2.securitygroup.SecurityGroup`
265        :return: The new security group.
266        """
267        if region.name == self.region:
268            raise BotoClientError('Unable to copy to the same Region')
269        conn_params = self.connection.get_params()
270        rconn = region.connect(**conn_params)
271        sg = rconn.create_security_group(
272            name or self.name,
273            self.description,
274            dry_run=dry_run
275        )
276        source_groups = []
277        for rule in self.rules:
278            for grant in rule.grants:
279                grant_nom = grant.name or grant.group_id
280                if grant_nom:
281                    if grant_nom not in source_groups:
282                        source_groups.append(grant_nom)
283                        sg.authorize(None, None, None, None, grant,
284                                     dry_run=dry_run)
285                else:
286                    sg.authorize(rule.ip_protocol, rule.from_port, rule.to_port,
287                                 grant.cidr_ip, dry_run=dry_run)
288        return sg
289
290    def instances(self, dry_run=False):
291        """
292        Find all of the current instances that are running within this
293        security group.
294
295        :rtype: list of :class:`boto.ec2.instance.Instance`
296        :return: A list of Instance objects
297        """
298        rs = []
299        if self.vpc_id:
300            rs.extend(self.connection.get_all_reservations(
301                filters={'instance.group-id': self.id},
302                dry_run=dry_run
303            ))
304        else:
305            rs.extend(self.connection.get_all_reservations(
306                filters={'group-id': self.id},
307                dry_run=dry_run
308            ))
309        instances = [i for r in rs for i in r.instances]
310        return instances
311
312
313class IPPermissionsList(list):
314
315    def startElement(self, name, attrs, connection):
316        if name == 'item':
317            self.append(IPPermissions(self))
318            return self[-1]
319        return None
320
321    def endElement(self, name, value, connection):
322        pass
323
324
325class IPPermissions(object):
326
327    def __init__(self, parent=None):
328        self.parent = parent
329        self.ip_protocol = None
330        self.from_port = None
331        self.to_port = None
332        self.grants = []
333
334    def __repr__(self):
335        return 'IPPermissions:%s(%s-%s)' % (self.ip_protocol,
336                                            self.from_port, self.to_port)
337
338    def startElement(self, name, attrs, connection):
339        if name == 'item':
340            self.grants.append(GroupOrCIDR(self))
341            return self.grants[-1]
342        return None
343
344    def endElement(self, name, value, connection):
345        if name == 'ipProtocol':
346            self.ip_protocol = value
347        elif name == 'fromPort':
348            self.from_port = value
349        elif name == 'toPort':
350            self.to_port = value
351        else:
352            setattr(self, name, value)
353
354    def add_grant(self, name=None, owner_id=None, cidr_ip=None, group_id=None,
355                  dry_run=False):
356        grant = GroupOrCIDR(self)
357        grant.owner_id = owner_id
358        grant.group_id = group_id
359        grant.name = name
360        grant.cidr_ip = cidr_ip
361        self.grants.append(grant)
362        return grant
363
364
365class GroupOrCIDR(object):
366
367    def __init__(self, parent=None):
368        self.owner_id = None
369        self.group_id = None
370        self.name = None
371        self.cidr_ip = None
372
373    def __repr__(self):
374        if self.cidr_ip:
375            return '%s' % self.cidr_ip
376        else:
377            return '%s-%s' % (self.name or self.group_id, self.owner_id)
378
379    def startElement(self, name, attrs, connection):
380        return None
381
382    def endElement(self, name, value, connection):
383        if name == 'userId':
384            self.owner_id = value
385        elif name == 'groupId':
386            self.group_id = value
387        elif name == 'groupName':
388            self.name = value
389        if name == 'cidrIp':
390            self.cidr_ip = value
391        else:
392            setattr(self, name, value)
393