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
19 from snaps.openstack.utils import neutron_utils
20 from snaps.openstack.utils import keystone_utils
22 __author__ = 'spisarski'
24 logger = logging.getLogger('OpenStackSecurityGroup')
27 class OpenStackSecurityGroup:
29 Class responsible for creating Security Groups
32 def __init__(self, os_creds, sec_grp_settings):
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
38 self.__os_creds = os_creds
39 self.sec_grp_settings = sec_grp_settings
41 self.__keystone = None
43 # Attributes instantiated on create()
44 self.__security_group = None
46 # dict where the rule settings object is the key
49 def create(self, cleanup=False):
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
55 self.__neutron = neutron_utils.neutron_client(self.__os_creds)
56 self.__keystone = keystone_utils.keystone_client(self.__os_creds)
58 logger.info('Creating security group %s...' % self.sec_grp_settings.name)
60 self.__security_group = neutron_utils.get_security_group(self.__neutron, self.sec_grp_settings.name)
61 if not self.__security_group and not cleanup:
62 # Create the security group
63 self.__security_group = neutron_utils.create_security_group(self.__neutron, self.__keystone,
64 self.sec_grp_settings)
66 # Get the rules added for free
67 auto_rules = neutron_utils.get_rules_by_security_group(self.__neutron, self.__security_group)
70 for auto_rule in auto_rules:
71 auto_rule_setting = self.__generate_rule_setting(auto_rule)
72 self.__rules[auto_rule_setting] = auto_rule
75 # Create the custom rules
76 for sec_grp_rule_setting in self.sec_grp_settings.rule_settings:
77 custom_rule = neutron_utils.create_security_group_rule(self.__neutron, sec_grp_rule_setting)
78 self.__rules[sec_grp_rule_setting] = custom_rule
80 # Refresh security group object to reflect the new rules added to it
81 self.__security_group = neutron_utils.get_security_group(self.__neutron, self.sec_grp_settings.name)
84 existing_rules = neutron_utils.get_rules_by_security_group(self.__neutron, self.__security_group)
86 for existing_rule in existing_rules:
88 rule_setting = self.__get_setting_from_rule(existing_rule)
92 rule_setting = self.__generate_rule_setting(existing_rule)
95 self.__rules[rule_setting] = existing_rule
97 return self.__security_group
99 def __generate_rule_setting(self, rule):
101 Creates a SecurityGroupRuleSettings object for a given rule
102 :param rule: the rule from which to create the SecurityGroupRuleSettings object
103 :return: the newly instantiated SecurityGroupRuleSettings object
105 rule_dict = rule['security_group_rule']
107 if rule_dict['security_group_id']:
108 sec_grp = neutron_utils.get_security_group_by_id(self.__neutron, rule_dict['security_group_id'])
110 sec_grp_name = sec_grp['security_group']['name']
112 setting = SecurityGroupRuleSettings(description=rule_dict['description'],
113 direction=rule_dict['direction'], ethertype=rule_dict['ethertype'],
114 port_range_min=rule_dict['port_range_min'],
115 port_range_max=rule_dict['port_range_max'], protocol=rule_dict['protocol'],
116 remote_group_id=rule_dict['remote_group_id'],
117 remote_ip_prefix=rule_dict['remote_ip_prefix'], sec_grp_name=sec_grp_name)
122 Removes and deletes the rules then the security group.
124 for setting, rule in self.__rules.items():
126 neutron_utils.delete_security_group_rule(self.__neutron, rule)
127 except NotFound as e:
128 logger.warning('Rule not found, cannot delete - ' + str(e))
130 self.__rules = dict()
132 if self.__security_group:
134 neutron_utils.delete_security_group(self.__neutron, self.__security_group)
135 except NotFound as e:
136 logger.warning('Security Group not found, cannot delete - ' + str(e))
138 self.__security_group = None
140 def get_security_group(self):
142 Returns the OpenStack security group object
145 return self.__security_group
149 Returns the associated rules
154 def add_rule(self, rule_setting):
156 Adds a rule to this security group
157 :param rule_setting: the rule configuration
159 rule_setting.sec_grp_name = self.sec_grp_settings.name
160 new_rule = neutron_utils.create_security_group_rule(self.__neutron, rule_setting)
161 self.__rules[rule_setting] = new_rule
162 self.sec_grp_settings.rule_settings.append(rule_setting)
164 def remove_rule(self, rule_id=None, rule_setting=None):
166 Removes a rule to this security group by id, name, or rule_setting object
167 :param rule_id: the rule's id
168 :param rule_setting: the rule's setting object
170 rule_to_remove = None
171 if rule_id or rule_setting:
173 rule_to_remove = neutron_utils.get_rule_by_id(self.__neutron, self.__security_group, rule_id)
175 rule_to_remove = self.__rules.get(rule_setting)
178 neutron_utils.delete_security_group_rule(self.__neutron, rule_to_remove)
179 rule_setting = self.__get_setting_from_rule(rule_to_remove)
181 self.__rules.pop(rule_setting)
183 logger.warning('Rule setting is None, cannot remove rule')
185 def __get_setting_from_rule(self, rule):
187 Returns the associated RuleSetting object for a given rule
188 :param rule: the Rule object
189 :return: the associated RuleSetting object or None
191 for rule_setting in self.sec_grp_settings.rule_settings:
192 if rule_setting.rule_eq(rule):
197 class SecurityGroupSettings:
199 Class representing a keypair configuration
202 def __init__(self, config=None, name=None, description=None, project_name=None,
203 rule_settings=list()):
205 Constructor - all parameters are optional
206 :param config: Should be a dict object containing the configuration settings using the attribute names below
207 as each member's the key and overrides any of the other parameters.
208 :param name: The keypair name.
209 :param description: The security group's description
210 :param project_name: The name of the project under which the security group will be created
214 self.name = config.get('name')
215 self.description = config.get('description')
216 self.project_name = config.get('project_name')
217 self.rule_settings = list()
218 if config.get('rules') and type(config['rules']) is list:
219 for config_rule in config['rules']:
220 self.rule_settings.append(SecurityGroupRuleSettings(config=config_rule))
223 self.description = description
224 self.project_name = project_name
225 self.rule_settings = rule_settings
228 raise Exception('The attribute name is required')
230 for rule_setting in self.rule_settings:
231 if rule_setting.sec_grp_name is not self.name:
232 raise Exception('Rule settings must correspond with the name of this security group')
234 def dict_for_neutron(self, keystone):
236 Returns a dictionary object representing this object.
237 This is meant to be converted into JSON designed for use by the Neutron API
239 TODO - expand automated testing to exercise all parameters
240 :param keystone: the Keystone client
241 :return: the dictionary object
246 out['name'] = self.name
248 out['description'] = self.description
249 if self.project_name:
250 project = keystone_utils.get_project(keystone, self.project_name)
253 project_id = project.id
255 out['project_id'] = project_id
257 raise Exception('Could not find project ID for project named - ' + self.project_name)
259 return {'security_group': out}
262 class Direction(enum.Enum):
270 class Protocol(enum.Enum):
280 class Ethertype(enum.Enum):
288 class SecurityGroupRuleSettings:
290 Class representing a keypair configuration
293 def __init__(self, config=None, sec_grp_name=None, description=None, direction=None,
294 remote_group_id=None, protocol=None, ethertype=None, port_range_min=None, port_range_max=None,
295 sec_grp_rule=None, remote_ip_prefix=None):
297 Constructor - all parameters are optional
298 :param config: Should be a dict object containing the configuration settings using the attribute names below
299 as each member's the key and overrides any of the other parameters.
300 :param sec_grp_name: The security group's name on which to add the rule. (required)
301 :param description: The rule's description
302 :param direction: An enumeration of type create_security_group.RULE_DIRECTION (required)
303 :param remote_group_id: The group ID to associate with this rule (this should be changed to group name
304 once snaps support Groups) (optional)
305 :param protocol: An enumeration of type create_security_group.RULE_PROTOCOL or a string value that will be
306 mapped accordingly (optional)
307 :param ethertype: An enumeration of type create_security_group.RULE_ETHERTYPE (optional)
308 :param port_range_min: The minimum 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 port_range_max: The maximum port number in the range that is matched by the security group rule. When
312 the protocol is TCP or UDP, this value must be <= port_range_max. When the protocol is
313 ICMP, this value must be an ICMP type.
314 :param sec_grp_rule: The OpenStack rule object to a security group rule object to associate
315 (note: Cannot be set using the config object nor can I see any real uses for this
317 :param remote_ip_prefix: The remote IP prefix to associate with this metering rule packet (optional)
319 TODO - Need to support the tenant...
323 self.description = config.get('description')
324 self.sec_grp_name = config.get('sec_grp_name')
325 self.remote_group_id = config.get('remote_group_id')
326 self.direction = None
327 if config.get('direction'):
328 self.direction = map_direction(config['direction'])
331 if config.get('protocol'):
332 self.protocol = map_protocol(config['protocol'])
334 self.protocol = Protocol.null
336 self.ethertype = None
337 if config.get('ethertype'):
338 self.ethertype = map_ethertype(config['ethertype'])
340 self.port_range_min = config.get('port_range_min')
341 self.port_range_max = config.get('port_range_max')
342 self.remote_ip_prefix = config.get('remote_ip_prefix')
344 self.description = description
345 self.sec_grp_name = sec_grp_name
346 self.remote_group_id = remote_group_id
347 self.direction = map_direction(direction)
348 self.protocol = map_protocol(protocol)
349 self.ethertype = map_ethertype(ethertype)
350 self.port_range_min = port_range_min
351 self.port_range_max = port_range_max
352 self.sec_grp_rule = sec_grp_rule
353 self.remote_ip_prefix = remote_ip_prefix
355 if not self.direction or not self.sec_grp_name:
356 raise Exception('direction and sec_grp_name are required')
358 def dict_for_neutron(self, neutron):
360 Returns a dictionary object representing this object.
361 This is meant to be converted into JSON designed for use by the Neutron API
363 :param neutron: the neutron client for performing lookups
364 :return: the dictionary object
369 out['description'] = self.description
371 out['direction'] = self.direction.name
372 if self.port_range_min:
373 out['port_range_min'] = self.port_range_min
374 if self.port_range_max:
375 out['port_range_max'] = self.port_range_max
377 out['ethertype'] = self.ethertype.name
379 out['protocol'] = self.protocol.name
380 if self.sec_grp_name:
381 sec_grp = neutron_utils.get_security_group(neutron, self.sec_grp_name)
383 out['security_group_id'] = sec_grp['security_group']['id']
385 raise Exception('Cannot locate security group with name - ' + self.sec_grp_name)
386 if self.remote_group_id:
387 out['remote_group_id'] = self.remote_group_id
388 if self.sec_grp_rule:
389 out['security_group_rule'] = self.sec_grp_rule
390 if self.remote_ip_prefix:
391 out['remote_ip_prefix'] = self.remote_ip_prefix
393 return {'security_group_rule': out}
395 def rule_eq(self, rule):
397 Returns True if this setting created the rule
398 :param rule: the rule to evaluate
401 rule_dict = rule['security_group_rule']
403 if self.description is not None:
404 if rule_dict['description'] is not None and rule_dict['description'] != '':
406 elif self.description != rule_dict['description']:
407 if rule_dict['description'] != '':
410 if self.direction.name != rule_dict['direction']:
413 if self.ethertype and rule_dict.get('ethertype'):
414 if self.ethertype.name != rule_dict['ethertype']:
417 if self.port_range_min and rule_dict.get('port_range_min'):
418 if self.port_range_min != rule_dict['port_range_min']:
421 if self.port_range_max and rule_dict.get('port_range_max'):
422 if self.port_range_max != rule_dict['port_range_max']:
425 if self.protocol and rule_dict.get('protocol'):
426 if self.protocol.name != rule_dict['protocol']:
429 if self.remote_group_id and rule_dict.get('remote_group_id'):
430 if self.remote_group_id != rule_dict['remote_group_id']:
433 if self.remote_ip_prefix and rule_dict.get('remote_ip_prefix'):
434 if self.remote_ip_prefix != rule_dict['remote_ip_prefix']:
439 def __eq__(self, other):
440 return self.description == other.description and \
441 self.direction == other.direction and \
442 self.port_range_min == other.port_range_min and \
443 self.port_range_max == other.port_range_max and \
444 self.ethertype == other.ethertype and \
445 self.protocol == other.protocol and \
446 self.sec_grp_name == other.sec_grp_name and \
447 self.remote_group_id == other.remote_group_id and \
448 self.sec_grp_rule == other.sec_grp_rule and \
449 self.remote_ip_prefix == other.remote_ip_prefix
452 return hash((self.sec_grp_name, self.description, self.direction, self.remote_group_id,
453 self.protocol, self.ethertype, self.port_range_min, self.port_range_max, self.sec_grp_rule,
454 self.remote_ip_prefix))
457 def map_direction(direction):
459 Takes a the direction value maps it to the Direction enum. When None return None
460 :param direction: the direction value
461 :return: the Direction enum object
462 :raise: Exception if value is invalid
466 if isinstance(direction, Direction):
469 dir_str = str(direction)
470 if dir_str == 'egress':
471 return Direction.egress
472 elif dir_str == 'ingress':
473 return Direction.ingress
475 raise Exception('Invalid Direction - ' + dir_str)
478 def map_protocol(protocol):
480 Takes a the protocol value maps it to the Protocol enum. When None return None
481 :param protocol: the protocol value
482 :return: the Protocol enum object
483 :raise: Exception if value is invalid
487 elif isinstance(protocol, Protocol):
490 proto_str = str(protocol)
491 if proto_str == 'icmp':
493 elif proto_str == 'tcp':
495 elif proto_str == 'udp':
497 elif proto_str == 'null':
500 raise Exception('Invalid Protocol - ' + proto_str)
503 def map_ethertype(ethertype):
505 Takes a the ethertype value maps it to the Ethertype enum. When None return None
506 :param ethertype: the ethertype value
507 :return: the Ethertype enum object
508 :raise: Exception if value is invalid
512 elif isinstance(ethertype, Ethertype):
515 eth_str = str(ethertype)
516 if eth_str == 'IPv6':
517 return Ethertype.IPv6
518 elif eth_str == 'IPv4':
519 return Ethertype.IPv4
521 raise Exception('Invalid Ethertype - ' + eth_str)