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