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