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