3dbf559576c7a0c750c57a0558e0114d90badac1
[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 Exception('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 Exception(
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 Exception(
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 SecurityGroupRuleSettings:
309     """
310     Class representing a keypair configuration
311     """
312
313     def __init__(self, **kwargs):
314         """
315         Constructor - all parameters are optional
316         :param sec_grp_name: The security group's name on which to add the
317                              rule. (required)
318         :param description: The rule's description
319         :param direction: An enumeration of type
320                           create_security_group.RULE_DIRECTION (required)
321         :param remote_group_id: The group ID to associate with this rule
322                                 (this should be changed to group name once
323                                 snaps support Groups) (optional)
324         :param protocol: An enumeration of type
325                          create_security_group.RULE_PROTOCOL or a string value
326                          that will be mapped accordingly (optional)
327         :param ethertype: An enumeration of type
328                           create_security_group.RULE_ETHERTYPE (optional)
329         :param port_range_min: The minimum port number in the range that is
330                                matched by the security group rule. When the
331                                protocol is TCP or UDP, this value must be <=
332                                port_range_max. When the protocol is ICMP, this
333                                value must be an ICMP type.
334         :param port_range_max: The maximum port number in the range that is
335                                matched by the security group rule. When the
336                                protocol is TCP or UDP, this value must be <=
337                                port_range_max. When the protocol is ICMP, this
338                                value must be an ICMP type.
339         :param sec_grp_rule: The OpenStack rule object to a security group rule
340                              object to associate
341                              (note: Cannot be set using the config object nor
342                              can I see any real uses for this parameter)
343         :param remote_ip_prefix: The remote IP prefix to associate with this
344                                  metering rule packet (optional)
345
346         TODO - Need to support the tenant...
347         """
348
349         self.description = kwargs.get('description')
350         self.sec_grp_name = kwargs.get('sec_grp_name')
351         self.remote_group_id = kwargs.get('remote_group_id')
352         self.direction = None
353         if kwargs.get('direction'):
354             self.direction = map_direction(kwargs['direction'])
355
356         self.protocol = None
357         if kwargs.get('protocol'):
358             self.protocol = map_protocol(kwargs['protocol'])
359         else:
360             self.protocol = Protocol.null
361
362         self.ethertype = None
363         if kwargs.get('ethertype'):
364             self.ethertype = map_ethertype(kwargs['ethertype'])
365
366         self.port_range_min = kwargs.get('port_range_min')
367         self.port_range_max = kwargs.get('port_range_max')
368         self.remote_ip_prefix = kwargs.get('remote_ip_prefix')
369
370         if not self.direction or not self.sec_grp_name:
371             raise Exception('direction and sec_grp_name are required')
372
373     def dict_for_neutron(self, neutron):
374         """
375         Returns a dictionary object representing this object.
376         This is meant to be converted into JSON designed for use by the Neutron
377         API
378
379         :param neutron: the neutron client for performing lookups
380         :return: the dictionary object
381         """
382         out = dict()
383
384         if self.description:
385             out['description'] = self.description
386         if self.direction:
387             out['direction'] = self.direction.name
388         if self.port_range_min:
389             out['port_range_min'] = self.port_range_min
390         if self.port_range_max:
391             out['port_range_max'] = self.port_range_max
392         if self.ethertype:
393             out['ethertype'] = self.ethertype.name
394         if self.protocol and self.protocol.name != 'null':
395             out['protocol'] = self.protocol.name
396         if self.sec_grp_name:
397             sec_grp = neutron_utils.get_security_group(
398                 neutron, self.sec_grp_name)
399             if sec_grp:
400                 out['security_group_id'] = sec_grp.id
401             else:
402                 raise Exception(
403                     'Cannot locate security group with name - ' +
404                     self.sec_grp_name)
405         if self.remote_group_id:
406             out['remote_group_id'] = self.remote_group_id
407         if self.remote_ip_prefix:
408             out['remote_ip_prefix'] = self.remote_ip_prefix
409
410         return {'security_group_rule': out}
411
412     def rule_eq(self, rule):
413         """
414         Returns True if this setting created the rule
415         :param rule: the rule to evaluate
416         :return: T/F
417         """
418         if self.description is not None:
419             if (rule.description is not None and
420                     rule.description != ''):
421                 return False
422         elif self.description != rule.description:
423             if rule.description != '':
424                 return False
425
426         if self.direction.name != rule.direction:
427             return False
428
429         if self.ethertype and rule.ethertype:
430             if self.ethertype.name != rule.ethertype:
431                 return False
432
433         if self.port_range_min and rule.port_range_min:
434             if self.port_range_min != rule.port_range_min:
435                 return False
436
437         if self.port_range_max and rule.port_range_max:
438             if self.port_range_max != rule.port_range_max:
439                 return False
440
441         if self.protocol and rule.protocol:
442             if self.protocol.name != rule.protocol:
443                 return False
444
445         if self.remote_group_id and rule.remote_group_id:
446             if self.remote_group_id != rule.remote_group_id:
447                 return False
448
449         if self.remote_ip_prefix and rule.remote_ip_prefix:
450             if self.remote_ip_prefix != rule.remote_ip_prefix:
451                 return False
452
453         return True
454
455     def __eq__(self, other):
456         return (
457             self.description == other.description and
458             self.direction == other.direction and
459             self.port_range_min == other.port_range_min and
460             self.port_range_max == other.port_range_max and
461             self.ethertype == other.ethertype and
462             self.protocol == other.protocol and
463             self.sec_grp_name == other.sec_grp_name and
464             self.remote_group_id == other.remote_group_id and
465             self.remote_ip_prefix == other.remote_ip_prefix)
466
467     def __hash__(self):
468         return hash((self.sec_grp_name, self.description, self.direction,
469                      self.remote_group_id,
470                      self.protocol, self.ethertype, self.port_range_min,
471                      self.port_range_max, self.remote_ip_prefix))
472
473
474 def map_direction(direction):
475     """
476     Takes a the direction value maps it to the Direction enum. When None return
477     None
478     :param direction: the direction value
479     :return: the Direction enum object
480     :raise: Exception if value is invalid
481     """
482     if not direction:
483         return None
484     if isinstance(direction, Direction):
485         return direction
486     else:
487         dir_str = str(direction)
488         if dir_str == 'egress':
489             return Direction.egress
490         elif dir_str == 'ingress':
491             return Direction.ingress
492         else:
493             raise Exception('Invalid Direction - ' + dir_str)
494
495
496 def map_protocol(protocol):
497     """
498     Takes a the protocol value maps it to the Protocol enum. When None return
499     None
500     :param protocol: the protocol value
501     :return: the Protocol enum object
502     :raise: Exception if value is invalid
503     """
504     if not protocol:
505         return None
506     elif isinstance(protocol, Protocol):
507         return protocol
508     else:
509         proto_str = str(protocol)
510         if proto_str == 'icmp':
511             return Protocol.icmp
512         elif proto_str == 'tcp':
513             return Protocol.tcp
514         elif proto_str == 'udp':
515             return Protocol.udp
516         elif proto_str == 'null':
517             return Protocol.null
518         else:
519             raise Exception('Invalid Protocol - ' + proto_str)
520
521
522 def map_ethertype(ethertype):
523     """
524     Takes a the ethertype value maps it to the Ethertype enum. When None return
525     None
526     :param ethertype: the ethertype value
527     :return: the Ethertype enum object
528     :raise: Exception if value is invalid
529     """
530     if not ethertype:
531         return None
532     elif isinstance(ethertype, Ethertype):
533         return ethertype
534     else:
535         eth_str = str(ethertype)
536         if eth_str == 'IPv6':
537             return Ethertype.IPv6
538         elif eth_str == 'IPv4':
539             return Ethertype.IPv4
540         else:
541             raise Exception('Invalid Ethertype - ' + eth_str)