Supporting the protocol string value of 'any' for security group rules.
[snaps.git] / snaps / openstack / create_security_group.py
1 # Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
2 #                    and others.  All rights reserved.
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at:
7 #
8 #     http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 import logging
16
17 import enum
18 from neutronclient.common.exceptions import NotFound, Conflict
19
20 from snaps.openstack.openstack_creator import OpenStackNetworkObject
21 from snaps.openstack.utils import keystone_utils
22 from snaps.openstack.utils import neutron_utils
23
24 __author__ = 'spisarski'
25
26 logger = logging.getLogger('OpenStackSecurityGroup')
27
28
29 class OpenStackSecurityGroup(OpenStackNetworkObject):
30     """
31     Class responsible for managing a Security Group in OpenStack
32     """
33
34     def __init__(self, os_creds, sec_grp_settings):
35         """
36         Constructor - all parameters are required
37         :param os_creds: The credentials to connect with OpenStack
38         :param sec_grp_settings: The settings used to create a security group
39         """
40         super(self.__class__, self).__init__(os_creds)
41
42         self.sec_grp_settings = sec_grp_settings
43
44         # Attributes instantiated on create()
45         self.__security_group = None
46
47         # dict where the rule settings object is the key
48         self.__rules = dict()
49
50     def initialize(self):
51         """
52         Loads existing security group.
53         :return: the security group domain object
54         """
55         super(self.__class__, self).initialize()
56
57         self.__security_group = neutron_utils.get_security_group(
58             self._neutron, sec_grp_settings=self.sec_grp_settings)
59         if self.__security_group:
60             # Populate rules
61             existing_rules = neutron_utils.get_rules_by_security_group(
62                 self._neutron, self.__security_group)
63
64             for existing_rule in existing_rules:
65                 # For Custom Rules
66                 rule_setting = self.__get_setting_from_rule(existing_rule)
67                 self.__rules[rule_setting] = existing_rule
68
69             self.__security_group = neutron_utils.get_security_group_by_id(
70                 self._neutron, self.__security_group.id)
71
72         return self.__security_group
73
74     def create(self):
75         """
76         Responsible for creating the security group.
77         :return: the security group domain object
78         """
79         self.initialize()
80
81         if not self.__security_group:
82             logger.info(
83                 'Creating security group %s...' % self.sec_grp_settings.name)
84
85             keystone = keystone_utils.keystone_client(self._os_creds)
86             self.__security_group = neutron_utils.create_security_group(
87                 self._neutron, keystone,
88                 self.sec_grp_settings)
89
90             # Get the rules added for free
91             auto_rules = neutron_utils.get_rules_by_security_group(
92                 self._neutron, self.__security_group)
93
94             ctr = 0
95             for auto_rule in auto_rules:
96                 auto_rule_setting = self.__generate_rule_setting(auto_rule)
97                 self.__rules[auto_rule_setting] = auto_rule
98                 ctr += 1
99
100             # Create the custom rules
101             for sec_grp_rule_setting in self.sec_grp_settings.rule_settings:
102                 try:
103                     custom_rule = neutron_utils.create_security_group_rule(
104                         self._neutron, sec_grp_rule_setting)
105                     self.__rules[sec_grp_rule_setting] = custom_rule
106                 except Conflict as e:
107                     logger.warn('Unable to create rule due to conflict - %s',
108                                 e)
109
110             # Refresh security group object to reflect the new rules added
111             self.__security_group = neutron_utils.get_security_group(
112                 self._neutron, sec_grp_settings=self.sec_grp_settings)
113
114         return self.__security_group
115
116     def __generate_rule_setting(self, rule):
117         """
118         Creates a SecurityGroupRuleSettings object for a given rule
119         :param rule: the rule from which to create the
120                     SecurityGroupRuleSettings object
121         :return: the newly instantiated SecurityGroupRuleSettings object
122         """
123         sec_grp = neutron_utils.get_security_group_by_id(
124             self._neutron, rule.security_group_id)
125
126         setting = SecurityGroupRuleSettings(
127             description=rule.description,
128             direction=rule.direction,
129             ethertype=rule.ethertype,
130             port_range_min=rule.port_range_min,
131             port_range_max=rule.port_range_max,
132             protocol=rule.protocol,
133             remote_group_id=rule.remote_group_id,
134             remote_ip_prefix=rule.remote_ip_prefix,
135             sec_grp_name=sec_grp.name)
136         return setting
137
138     def clean(self):
139         """
140         Removes and deletes the rules then the security group.
141         """
142         for setting, rule in self.__rules.items():
143             try:
144                 neutron_utils.delete_security_group_rule(self._neutron, rule)
145             except NotFound as e:
146                 logger.warning('Rule not found, cannot delete - ' + str(e))
147                 pass
148         self.__rules = dict()
149
150         if self.__security_group:
151             try:
152                 neutron_utils.delete_security_group(self._neutron,
153                                                     self.__security_group)
154             except NotFound as e:
155                 logger.warning(
156                     'Security Group not found, cannot delete - ' + str(e))
157
158             self.__security_group = None
159
160     def get_security_group(self):
161         """
162         Returns the OpenStack security group object
163         :return:
164         """
165         return self.__security_group
166
167     def get_rules(self):
168         """
169         Returns the associated rules
170         :return:
171         """
172         return self.__rules
173
174     def add_rule(self, rule_setting):
175         """
176         Adds a rule to this security group
177         :param rule_setting: the rule configuration
178         """
179         rule_setting.sec_grp_name = self.sec_grp_settings.name
180         new_rule = neutron_utils.create_security_group_rule(self._neutron,
181                                                             rule_setting)
182         self.__rules[rule_setting] = new_rule
183         self.sec_grp_settings.rule_settings.append(rule_setting)
184
185     def remove_rule(self, rule_id=None, rule_setting=None):
186         """
187         Removes a rule to this security group by id, name, or rule_setting
188         object
189         :param rule_id: the rule's id
190         :param rule_setting: the rule's setting object
191         """
192         rule_to_remove = None
193         if rule_id or rule_setting:
194             if rule_id:
195                 rule_to_remove = neutron_utils.get_rule_by_id(
196                     self._neutron, self.__security_group, rule_id)
197             elif rule_setting:
198                 rule_to_remove = self.__rules.get(rule_setting)
199
200         if rule_to_remove:
201             neutron_utils.delete_security_group_rule(self._neutron,
202                                                      rule_to_remove)
203             rule_setting = self.__get_setting_from_rule(rule_to_remove)
204             if rule_setting:
205                 self.__rules.pop(rule_setting)
206             else:
207                 logger.warning('Rule setting is None, cannot remove rule')
208
209     def __get_setting_from_rule(self, rule):
210         """
211         Returns the associated RuleSetting object for a given rule
212         :param rule: the Rule object
213         :return: the associated RuleSetting object or None
214         """
215         for rule_setting in self.sec_grp_settings.rule_settings:
216             if rule_setting.rule_eq(rule):
217                 return rule_setting
218         return None
219
220
221 class SecurityGroupSettings:
222     """
223     Class representing a keypair configuration
224     """
225
226     def __init__(self, **kwargs):
227         """
228         Constructor
229         :param name: The security group's name (required)
230         :param description: The security group's description (optional)
231         :param project_name: The name of the project under which the security
232                              group will be created
233         :param rule_settings: a list of SecurityGroupRuleSettings objects
234         :return:
235         """
236         self.name = kwargs.get('name')
237         self.description = kwargs.get('description')
238         self.project_name = kwargs.get('project_name')
239         self.rule_settings = list()
240
241         rule_settings = kwargs.get('rules')
242         if not rule_settings:
243             rule_settings = kwargs.get('rule_settings')
244
245         if rule_settings:
246             for rule_setting in rule_settings:
247                 if isinstance(rule_setting, SecurityGroupRuleSettings):
248                     self.rule_settings.append(rule_setting)
249                 else:
250                     rule_setting['sec_grp_name'] = self.name
251                     self.rule_settings.append(SecurityGroupRuleSettings(
252                         **rule_setting))
253
254         if not self.name:
255             raise SecurityGroupSettingsError('The attribute name is required')
256
257         for rule_setting in self.rule_settings:
258             if rule_setting.sec_grp_name is not self.name:
259                 raise SecurityGroupSettingsError(
260                     'Rule settings must correspond with the name of this '
261                     'security group')
262
263     def dict_for_neutron(self, keystone):
264         """
265         Returns a dictionary object representing this object.
266         This is meant to be converted into JSON designed for use by the Neutron
267         API
268
269         TODO - expand automated testing to exercise all parameters
270         :param keystone: the Keystone client
271         :return: the dictionary object
272         """
273         out = dict()
274
275         if self.name:
276             out['name'] = self.name
277         if self.description:
278             out['description'] = self.description
279         if self.project_name:
280             project = keystone_utils.get_project(
281                 keystone=keystone, project_name=self.project_name)
282             project_id = None
283             if project:
284                 project_id = project.id
285             if project_id:
286                 out['tenant_id'] = project_id
287             else:
288                 raise SecurityGroupSettingsError(
289                     'Could not find project ID for project named - ' +
290                     self.project_name)
291
292         return {'security_group': out}
293
294
295 class Direction(enum.Enum):
296     """
297     A rule's direction
298     """
299     ingress = 'ingress'
300     egress = 'egress'
301
302
303 class Protocol(enum.Enum):
304     """
305     A rule's protocol
306     """
307     ah = 51
308     dccp = 33
309     egp = 8
310     esp = 50
311     gre = 47
312     icmp = 1
313     icmpv6 = 58
314     igmp = 2
315     ipv6_encap = 41
316     ipv6_frag = 44
317     ipv6_icmp = 58
318     ipv6_nonxt = 59
319     ipv6_opts = 60
320     ipv6_route = 43
321     ospf = 89
322     pgm = 113
323     rsvp = 46
324     sctp = 132
325     tcp = 6
326     udp = 17
327     udplite = 136
328     vrrp = 112
329     any = 'any'
330     null = 'null'
331
332
333 class Ethertype(enum.Enum):
334     """
335     A rule's ethertype
336     """
337     IPv4 = 4
338     IPv6 = 6
339
340
341 class SecurityGroupSettingsError(Exception):
342     """
343     Exception to be thrown when security group settings attributes are
344     invalid
345     """
346
347
348 class SecurityGroupRuleSettings:
349     """
350     Class representing a keypair configuration
351     """
352
353     def __init__(self, **kwargs):
354         """
355         Constructor - all parameters are optional
356         :param sec_grp_name: The security group's name on which to add the
357                              rule. (required)
358         :param description: The rule's description
359         :param direction: An enumeration of type
360                           create_security_group.RULE_DIRECTION (required)
361         :param remote_group_id: The group ID to associate with this rule
362                                 (this should be changed to group name once
363                                 snaps support Groups) (optional)
364         :param protocol: An enumeration of type
365                          create_security_group.RULE_PROTOCOL or a string value
366                          that will be mapped accordingly (optional)
367         :param ethertype: An enumeration of type
368                           create_security_group.RULE_ETHERTYPE (optional)
369         :param port_range_min: The minimum port number in the range that is
370                                matched by the security group rule. When the
371                                protocol is TCP or UDP, this value must be <=
372                                port_range_max.
373         :param port_range_max: The maximum port number in the range that is
374                                matched by the security group rule. When the
375                                protocol is TCP or UDP, this value must be <=
376                                port_range_max.
377         :param remote_ip_prefix: The remote IP prefix to associate with this
378                                  metering rule packet (optional)
379
380         TODO - Need to support the tenant...
381         """
382
383         self.description = kwargs.get('description')
384         self.sec_grp_name = kwargs.get('sec_grp_name')
385         self.remote_group_id = kwargs.get('remote_group_id')
386         self.direction = None
387         if kwargs.get('direction'):
388             self.direction = map_direction(kwargs['direction'])
389
390         self.protocol = None
391         if kwargs.get('protocol'):
392             self.protocol = map_protocol(kwargs['protocol'])
393         else:
394             self.protocol = Protocol.null
395
396         self.ethertype = None
397         if kwargs.get('ethertype'):
398             self.ethertype = map_ethertype(kwargs['ethertype'])
399
400         self.port_range_min = kwargs.get('port_range_min')
401         self.port_range_max = kwargs.get('port_range_max')
402         self.remote_ip_prefix = kwargs.get('remote_ip_prefix')
403
404         if not self.direction or not self.sec_grp_name:
405             raise SecurityGroupRuleSettingsError(
406                 'direction and sec_grp_name are required')
407
408     def dict_for_neutron(self, neutron):
409         """
410         Returns a dictionary object representing this object.
411         This is meant to be converted into JSON designed for use by the Neutron
412         API
413
414         :param neutron: the neutron client for performing lookups
415         :return: the dictionary object
416         """
417         out = dict()
418
419         if self.description:
420             out['description'] = self.description
421         if self.direction:
422             out['direction'] = self.direction.name
423         if self.port_range_min:
424             out['port_range_min'] = self.port_range_min
425         if self.port_range_max:
426             out['port_range_max'] = self.port_range_max
427         if self.ethertype:
428             out['ethertype'] = self.ethertype.name
429         if self.protocol and self.protocol.value != 'null':
430             out['protocol'] = self.protocol.value
431         if self.sec_grp_name:
432             sec_grp = neutron_utils.get_security_group(
433                 neutron, sec_grp_name=self.sec_grp_name)
434             if sec_grp:
435                 out['security_group_id'] = sec_grp.id
436             else:
437                 raise SecurityGroupRuleSettingsError(
438                     'Cannot locate security group with name - ' +
439                     self.sec_grp_name)
440         if self.remote_group_id:
441             out['remote_group_id'] = self.remote_group_id
442         if self.remote_ip_prefix:
443             out['remote_ip_prefix'] = self.remote_ip_prefix
444
445         return {'security_group_rule': out}
446
447     def rule_eq(self, rule):
448         """
449         Returns True if this setting created the rule
450         :param rule: the rule to evaluate
451         :return: T/F
452         """
453         if self.description is not None:
454             if (rule.description is not None and
455                     rule.description != ''):
456                 return False
457         elif self.description != rule.description:
458             if rule.description != '':
459                 return False
460
461         if self.direction.name != rule.direction:
462             return False
463
464         if self.ethertype and rule.ethertype:
465             if self.ethertype.name != rule.ethertype:
466                 return False
467
468         if self.port_range_min and rule.port_range_min:
469             if self.port_range_min != rule.port_range_min:
470                 return False
471
472         if self.port_range_max and rule.port_range_max:
473             if self.port_range_max != rule.port_range_max:
474                 return False
475
476         if self.protocol and rule.protocol:
477             if self.protocol.name != rule.protocol:
478                 return False
479
480         if self.remote_group_id and rule.remote_group_id:
481             if self.remote_group_id != rule.remote_group_id:
482                 return False
483
484         if self.remote_ip_prefix and rule.remote_ip_prefix:
485             if self.remote_ip_prefix != rule.remote_ip_prefix:
486                 return False
487
488         return True
489
490     def __eq__(self, other):
491         return (
492             self.description == other.description and
493             self.direction == other.direction and
494             self.port_range_min == other.port_range_min and
495             self.port_range_max == other.port_range_max and
496             self.ethertype == other.ethertype and
497             self.protocol == other.protocol and
498             self.sec_grp_name == other.sec_grp_name and
499             self.remote_group_id == other.remote_group_id and
500             self.remote_ip_prefix == other.remote_ip_prefix)
501
502     def __hash__(self):
503         return hash((self.sec_grp_name, self.description, self.direction,
504                      self.remote_group_id,
505                      self.protocol, self.ethertype, self.port_range_min,
506                      self.port_range_max, self.remote_ip_prefix))
507
508
509 def map_direction(direction):
510     """
511     Takes a the direction value maps it to the Direction enum. When None return
512     None
513     :param direction: the direction value
514     :return: the Direction enum object
515     :raise: Exception if value is invalid
516     """
517     if not direction:
518         return None
519     if isinstance(direction, Direction):
520         return direction
521     else:
522         dir_str = str(direction)
523         if dir_str == 'egress':
524             return Direction.egress
525         elif dir_str == 'ingress':
526             return Direction.ingress
527         else:
528             raise SecurityGroupRuleSettingsError(
529                 'Invalid Direction - ' + dir_str)
530
531
532 def map_protocol(protocol):
533     """
534     Takes a the protocol value maps it to the Protocol enum. When None return
535     None
536     :param protocol: the protocol value
537     :return: the Protocol enum object
538     :raise: Exception if value is invalid
539     """
540     if not protocol:
541         return None
542     elif isinstance(protocol, Protocol):
543         return protocol
544     else:
545         for proto_enum in Protocol:
546             if proto_enum.name == protocol or proto_enum.value == protocol:
547                 if proto_enum == Protocol.any:
548                     return Protocol.null
549                 return proto_enum
550         raise SecurityGroupRuleSettingsError(
551             'Invalid Protocol - ' + protocol)
552
553
554 def map_ethertype(ethertype):
555     """
556     Takes a the ethertype value maps it to the Ethertype enum. When None return
557     None
558     :param ethertype: the ethertype value
559     :return: the Ethertype enum object
560     :raise: Exception if value is invalid
561     """
562     if not ethertype:
563         return None
564     elif isinstance(ethertype, Ethertype):
565         return ethertype
566     else:
567         eth_str = str(ethertype)
568         if eth_str == 'IPv6':
569             return Ethertype.IPv6
570         elif eth_str == 'IPv4':
571             return Ethertype.IPv4
572         else:
573             raise SecurityGroupRuleSettingsError(
574                 'Invalid Ethertype - ' + eth_str)
575
576
577 class SecurityGroupRuleSettingsError(Exception):
578     """
579     Exception to be thrown when security group rule settings attributes are
580     invalid
581     """