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