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