1 # Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
2 # and others. All rights reserved.
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:
8 # http://www.apache.org/licenses/LICENSE-2.0
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.
18 from neutronclient.common.exceptions import NotFound, Conflict
20 from snaps.openstack.openstack_creator import OpenStackNetworkObject
21 from snaps.openstack.utils import keystone_utils
22 from snaps.openstack.utils import neutron_utils
24 __author__ = 'spisarski'
26 logger = logging.getLogger('OpenStackSecurityGroup')
29 class OpenStackSecurityGroup(OpenStackNetworkObject):
31 Class responsible for managing a Security Group in OpenStack
34 def __init__(self, os_creds, sec_grp_settings):
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
40 super(self.__class__, self).__init__(os_creds)
42 self.sec_grp_settings = sec_grp_settings
44 # Attributes instantiated on create()
45 self.__security_group = None
47 # dict where the rule settings object is the key
52 Loads existing security group.
53 :return: the security group domain object
55 super(self.__class__, self).initialize()
57 self.__security_group = neutron_utils.get_security_group(
58 self._neutron, sec_grp_settings=self.sec_grp_settings)
59 if self.__security_group:
61 existing_rules = neutron_utils.get_rules_by_security_group(
62 self._neutron, self.__security_group)
64 for existing_rule in existing_rules:
66 rule_setting = self.__get_setting_from_rule(existing_rule)
67 self.__rules[rule_setting] = existing_rule
69 return self.__security_group
73 Responsible for creating the security group.
74 :return: the security group domain object
78 if not self.__security_group:
80 'Creating security group %s...' % self.sec_grp_settings.name)
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)
87 # Get the rules added for free
88 auto_rules = neutron_utils.get_rules_by_security_group(
89 self._neutron, self.__security_group)
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
97 # Create the custom rules
98 for sec_grp_rule_setting in self.sec_grp_settings.rule_settings:
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',
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)
111 return self.__security_group
113 def __generate_rule_setting(self, rule):
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
120 sec_grp = neutron_utils.get_security_group_by_id(
121 self._neutron, rule.security_group_id)
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)
137 Removes and deletes the rules then the security group.
139 for setting, rule in self.__rules.items():
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))
145 self.__rules = dict()
147 if self.__security_group:
149 neutron_utils.delete_security_group(self._neutron,
150 self.__security_group)
151 except NotFound as e:
153 'Security Group not found, cannot delete - ' + str(e))
155 self.__security_group = None
157 def get_security_group(self):
159 Returns the OpenStack security group object
162 return self.__security_group
166 Returns the associated rules
171 def add_rule(self, rule_setting):
173 Adds a rule to this security group
174 :param rule_setting: the rule configuration
176 rule_setting.sec_grp_name = self.sec_grp_settings.name
177 new_rule = neutron_utils.create_security_group_rule(self._neutron,
179 self.__rules[rule_setting] = new_rule
180 self.sec_grp_settings.rule_settings.append(rule_setting)
182 def remove_rule(self, rule_id=None, rule_setting=None):
184 Removes a rule to this security group by id, name, or rule_setting
186 :param rule_id: the rule's id
187 :param rule_setting: the rule's setting object
189 rule_to_remove = None
190 if rule_id or rule_setting:
192 rule_to_remove = neutron_utils.get_rule_by_id(
193 self._neutron, self.__security_group, rule_id)
195 rule_to_remove = self.__rules.get(rule_setting)
198 neutron_utils.delete_security_group_rule(self._neutron,
200 rule_setting = self.__get_setting_from_rule(rule_to_remove)
202 self.__rules.pop(rule_setting)
204 logger.warning('Rule setting is None, cannot remove rule')
206 def __get_setting_from_rule(self, rule):
208 Returns the associated RuleSetting object for a given rule
209 :param rule: the Rule object
210 :return: the associated RuleSetting object or None
212 for rule_setting in self.sec_grp_settings.rule_settings:
213 if rule_setting.rule_eq(rule):
218 class SecurityGroupSettings:
220 Class representing a keypair configuration
223 def __init__(self, **kwargs):
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
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()
237 rule_settings = kwargs.get('rules')
238 if not rule_settings:
239 rule_settings = kwargs.get('rule_settings')
242 for rule_setting in rule_settings:
243 if isinstance(rule_setting, SecurityGroupRuleSettings):
244 self.rule_settings.append(rule_setting)
246 rule_setting['sec_grp_name'] = self.name
247 self.rule_settings.append(SecurityGroupRuleSettings(
251 raise SecurityGroupSettingsError('The attribute name is required')
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 '
259 def dict_for_neutron(self, keystone):
261 Returns a dictionary object representing this object.
262 This is meant to be converted into JSON designed for use by the Neutron
265 TODO - expand automated testing to exercise all parameters
266 :param keystone: the Keystone client
267 :return: the dictionary object
272 out['name'] = self.name
274 out['description'] = self.description
275 if self.project_name:
276 project = keystone_utils.get_project(
277 keystone=keystone, project_name=self.project_name)
280 project_id = project.id
282 out['tenant_id'] = project_id
284 raise SecurityGroupSettingsError(
285 'Could not find project ID for project named - ' +
288 return {'security_group': out}
291 class Direction(enum.Enum):
299 class Protocol(enum.Enum):
309 class Ethertype(enum.Enum):
317 class SecurityGroupSettingsError(Exception):
319 Exception to be thrown when security group settings attributes are
324 class SecurityGroupRuleSettings:
326 Class representing a keypair configuration
329 def __init__(self, **kwargs):
331 Constructor - all parameters are optional
332 :param sec_grp_name: The security group's name on which to add the
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
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)
362 TODO - Need to support the tenant...
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'])
373 if kwargs.get('protocol'):
374 self.protocol = map_protocol(kwargs['protocol'])
376 self.protocol = Protocol.null
378 self.ethertype = None
379 if kwargs.get('ethertype'):
380 self.ethertype = map_ethertype(kwargs['ethertype'])
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')
386 if not self.direction or not self.sec_grp_name:
387 raise SecurityGroupRuleSettingsError(
388 'direction and sec_grp_name are required')
390 def dict_for_neutron(self, neutron):
392 Returns a dictionary object representing this object.
393 This is meant to be converted into JSON designed for use by the Neutron
396 :param neutron: the neutron client for performing lookups
397 :return: the dictionary object
402 out['description'] = self.description
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
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)
417 out['security_group_id'] = sec_grp.id
419 raise SecurityGroupRuleSettingsError(
420 'Cannot locate security group with 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
427 return {'security_group_rule': out}
429 def rule_eq(self, rule):
431 Returns True if this setting created the rule
432 :param rule: the rule to evaluate
435 if self.description is not None:
436 if (rule.description is not None and
437 rule.description != ''):
439 elif self.description != rule.description:
440 if rule.description != '':
443 if self.direction.name != rule.direction:
446 if self.ethertype and rule.ethertype:
447 if self.ethertype.name != rule.ethertype:
450 if self.port_range_min and rule.port_range_min:
451 if self.port_range_min != rule.port_range_min:
454 if self.port_range_max and rule.port_range_max:
455 if self.port_range_max != rule.port_range_max:
458 if self.protocol and rule.protocol:
459 if self.protocol.name != rule.protocol:
462 if self.remote_group_id and rule.remote_group_id:
463 if self.remote_group_id != rule.remote_group_id:
466 if self.remote_ip_prefix and rule.remote_ip_prefix:
467 if self.remote_ip_prefix != rule.remote_ip_prefix:
472 def __eq__(self, other):
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)
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))
491 def map_direction(direction):
493 Takes a the direction value maps it to the Direction enum. When None return
495 :param direction: the direction value
496 :return: the Direction enum object
497 :raise: Exception if value is invalid
501 if isinstance(direction, Direction):
504 dir_str = str(direction)
505 if dir_str == 'egress':
506 return Direction.egress
507 elif dir_str == 'ingress':
508 return Direction.ingress
510 raise SecurityGroupRuleSettingsError(
511 'Invalid Direction - ' + dir_str)
514 def map_protocol(protocol):
516 Takes a the protocol value maps it to the Protocol enum. When None return
518 :param protocol: the protocol value
519 :return: the Protocol enum object
520 :raise: Exception if value is invalid
524 elif isinstance(protocol, Protocol):
527 proto_str = str(protocol)
528 if proto_str == 'icmp':
530 elif proto_str == 'tcp':
532 elif proto_str == 'udp':
534 elif proto_str == 'null':
537 raise SecurityGroupRuleSettingsError(
538 'Invalid Protocol - ' + proto_str)
541 def map_ethertype(ethertype):
543 Takes a the ethertype value maps it to the Ethertype enum. When None return
545 :param ethertype: the ethertype value
546 :return: the Ethertype enum object
547 :raise: Exception if value is invalid
551 elif isinstance(ethertype, Ethertype):
554 eth_str = str(ethertype)
555 if eth_str == 'IPv6':
556 return Ethertype.IPv6
557 elif eth_str == 'IPv4':
558 return Ethertype.IPv4
560 raise SecurityGroupRuleSettingsError(
561 'Invalid Ethertype - ' + eth_str)
564 class SecurityGroupRuleSettingsError(Exception):
566 Exception to be thrown when security group rule settings attributes are