1 # Copyright (c) 2016 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
40 self.__neutron = neutron_utils.neutron_client(os_creds)
41 self.__keystone = keystone_utils.keystone_client(os_creds)
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 logger.info('Creating security group %s...' % self.sec_grp_settings.name)
57 self.__security_group = neutron_utils.get_security_group(self.__neutron, self.sec_grp_settings.name)
58 if not self.__security_group and not cleanup:
59 # Create the security group
60 self.__security_group = neutron_utils.create_security_group(self.__neutron, self.__keystone,
61 self.sec_grp_settings)
63 # Get the rules added for free
64 auto_rules = neutron_utils.get_rules_by_security_group(self.__neutron, self.__security_group)
67 for auto_rule in auto_rules:
68 auto_rule_setting = self.__generate_rule_setting(auto_rule)
69 self.__rules[auto_rule_setting] = auto_rule
72 # Create the custom rules
73 for sec_grp_rule_setting in self.sec_grp_settings.rule_settings:
74 custom_rule = neutron_utils.create_security_group_rule(self.__neutron, sec_grp_rule_setting)
75 self.__rules[sec_grp_rule_setting] = custom_rule
77 # Refresh security group object to reflect the new rules added to it
78 self.__security_group = neutron_utils.get_security_group(self.__neutron, self.sec_grp_settings.name)
81 existing_rules = neutron_utils.get_rules_by_security_group(self.__neutron, self.__security_group)
83 for existing_rule in existing_rules:
85 rule_setting = self.__get_setting_from_rule(existing_rule)
89 rule_setting = self.__generate_rule_setting(existing_rule)
92 self.__rules[rule_setting] = existing_rule
94 return self.__security_group
96 def __generate_rule_setting(self, rule):
98 Creates a SecurityGroupRuleSettings object for a given rule
99 :param rule: the rule from which to create the SecurityGroupRuleSettings object
100 :return: the newly instantiated SecurityGroupRuleSettings object
102 rule_dict = rule['security_group_rule']
104 if rule_dict['security_group_id']:
105 sec_grp = neutron_utils.get_security_group_by_id(self.__neutron, rule_dict['security_group_id'])
107 sec_grp_name = sec_grp['security_group']['name']
109 setting = SecurityGroupRuleSettings(description=rule_dict['description'],
110 direction=rule_dict['direction'], ethertype=rule_dict['ethertype'],
111 port_range_min=rule_dict['port_range_min'],
112 port_range_max=rule_dict['port_range_max'], protocol=rule_dict['protocol'],
113 remote_group_id=rule_dict['remote_group_id'],
114 remote_ip_prefix=rule_dict['remote_ip_prefix'], sec_grp_name=sec_grp_name)
119 Removes and deletes the rules then the security group.
121 for setting, rule in self.__rules.iteritems():
123 neutron_utils.delete_security_group_rule(self.__neutron, rule)
124 except NotFound as e:
125 logger.warn('Rule not found, cannot delete - ' + e.message)
127 self.__rules = dict()
129 if self.__security_group:
131 neutron_utils.delete_security_group(self.__neutron, self.__security_group)
132 except NotFound as e:
133 logger.warn('Security Group not found, cannot delete - ' + e.message)
135 self.__security_group = None
137 def get_security_group(self):
139 Returns the OpenStack security group object
142 return self.__security_group
146 Returns the associated rules
151 def add_rule(self, rule_setting):
153 Adds a rule to this security group
154 :param rule_setting: the rule configuration
156 rule_setting.sec_grp_name = self.sec_grp_settings.name
157 new_rule = neutron_utils.create_security_group_rule(self.__neutron, rule_setting)
158 self.__rules[rule_setting] = new_rule
159 self.sec_grp_settings.rule_settings.append(rule_setting)
161 def remove_rule(self, rule_id=None, rule_setting=None):
163 Removes a rule to this security group by id, name, or rule_setting object
164 :param rule_id: the rule's id
165 :param rule_setting: the rule's setting object
167 rule_to_remove = None
168 if rule_id or rule_setting:
170 rule_to_remove = neutron_utils.get_rule_by_id(self.__neutron, self.__security_group, rule_id)
172 rule_to_remove = self.__rules.get(rule_setting)
175 neutron_utils.delete_security_group_rule(self.__neutron, rule_to_remove)
176 rule_setting = self.__get_setting_from_rule(rule_to_remove)
178 self.__rules.pop(rule_setting)
180 logger.warn('Rule setting is None, cannot remove rule')
182 def __get_setting_from_rule(self, rule):
184 Returns the associated RuleSetting object for a given rule
185 :param rule: the Rule object
186 :return: the associated RuleSetting object or None
188 for rule_setting in self.sec_grp_settings.rule_settings:
189 if rule_setting.rule_eq(rule):
194 class SecurityGroupSettings:
196 Class representing a keypair configuration
199 def __init__(self, config=None, name=None, description=None, project_name=None,
200 rule_settings=list()):
202 Constructor - all parameters are optional
203 :param config: Should be a dict object containing the configuration settings using the attribute names below
204 as each member's the key and overrides any of the other parameters.
205 :param name: The keypair name.
206 :param description: The security group's description
207 :param project_name: The name of the project under which the security group will be created
211 self.name = config.get('name')
212 self.description = config.get('description')
213 self.project_name = config.get('project_name')
214 self.rule_settings = list()
215 if config.get('rules') and type(config['rules']) is list:
216 for config_rule in config['rules']:
217 self.rule_settings.append(SecurityGroupRuleSettings(config=config_rule))
220 self.description = description
221 self.project_name = project_name
222 self.rule_settings = rule_settings
225 raise Exception('The attribute name is required')
227 for rule_setting in self.rule_settings:
228 if rule_setting.sec_grp_name is not self.name:
229 raise Exception('Rule settings must correspond with the name of this security group')
231 def dict_for_neutron(self, keystone):
233 Returns a dictionary object representing this object.
234 This is meant to be converted into JSON designed for use by the Neutron API
236 TODO - expand automated testing to exercise all parameters
237 :param keystone: the Keystone client
238 :return: the dictionary object
243 out['name'] = self.name
245 out['description'] = self.description
246 if self.project_name:
247 project = keystone_utils.get_project(keystone, self.project_name)
250 project_id = project.id
252 out['project_id'] = project_id
254 raise Exception('Could not find project ID for project named - ' + self.project_name)
256 return {'security_group': out}
259 class Direction(enum.Enum):
267 class Protocol(enum.Enum):
277 class Ethertype(enum.Enum):
285 class SecurityGroupRuleSettings:
287 Class representing a keypair configuration
290 def __init__(self, config=None, sec_grp_name=None, description=None, direction=None,
291 remote_group_id=None, protocol=None, ethertype=None, port_range_min=None, port_range_max=None,
292 sec_grp_rule=None, remote_ip_prefix=None):
294 Constructor - all parameters are optional
295 :param config: Should be a dict object containing the configuration settings using the attribute names below
296 as each member's the key and overrides any of the other parameters.
297 :param sec_grp_name: The security group's name on which to add the rule. (required)
298 :param description: The rule's description
299 :param direction: An enumeration of type create_security_group.RULE_DIRECTION (required)
300 :param remote_group_id: The group ID to associate with this rule (this should be changed to group name
301 once snaps support Groups) (optional)
302 :param protocol: An enumeration of type create_security_group.RULE_PROTOCOL or a string value that will be
303 mapped accordingly (optional)
304 :param ethertype: An enumeration of type create_security_group.RULE_ETHERTYPE (optional)
305 :param port_range_min: The minimum port number in the range that is matched by the security group rule. When
306 the protocol is TCP or UDP, this value must be <= port_range_max. When the protocol is
307 ICMP, this value must be an ICMP type.
308 :param port_range_max: The maximum 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 sec_grp_rule: The OpenStack rule object to a security group rule object to associate
312 (note: Cannot be set using the config object nor can I see any real uses for this
314 :param remote_ip_prefix: The remote IP prefix to associate with this metering rule packet (optional)
316 TODO - Need to support the tenant...
320 self.description = config.get('description')
321 self.sec_grp_name = config.get('sec_grp_name')
322 self.remote_group_id = config.get('remote_group_id')
323 self.direction = None
324 if config.get('direction'):
325 self.direction = map_direction(config['direction'])
328 if config.get('protocol'):
329 self.protocol = map_protocol(config['protocol'])
331 self.protocol = Protocol.null
333 self.ethertype = None
334 if config.get('ethertype'):
335 self.ethertype = map_ethertype(config['ethertype'])
337 self.port_range_min = config.get('port_range_min')
338 self.port_range_max = config.get('port_range_max')
339 self.remote_ip_prefix = config.get('remote_ip_prefix')
341 self.description = description
342 self.sec_grp_name = sec_grp_name
343 self.remote_group_id = remote_group_id
344 self.direction = map_direction(direction)
345 self.protocol = map_protocol(protocol)
346 self.ethertype = map_ethertype(ethertype)
347 self.port_range_min = port_range_min
348 self.port_range_max = port_range_max
349 self.sec_grp_rule = sec_grp_rule
350 self.remote_ip_prefix = remote_ip_prefix
352 if not self.direction or not self.sec_grp_name:
353 raise Exception('direction and sec_grp_name are required')
355 def dict_for_neutron(self, neutron):
357 Returns a dictionary object representing this object.
358 This is meant to be converted into JSON designed for use by the Neutron API
360 :param neutron: the neutron client for performing lookups
361 :return: the dictionary object
366 out['description'] = self.description
368 out['direction'] = self.direction.name
369 if self.port_range_min:
370 out['port_range_min'] = self.port_range_min
371 if self.port_range_max:
372 out['port_range_max'] = self.port_range_max
374 out['ethertype'] = self.ethertype.name
376 out['protocol'] = self.protocol.name
377 if self.sec_grp_name:
378 sec_grp = neutron_utils.get_security_group(neutron, self.sec_grp_name)
380 out['security_group_id'] = sec_grp['security_group']['id']
382 raise Exception('Cannot locate security group with name - ' + self.sec_grp_name)
383 if self.remote_group_id:
384 out['remote_group_id'] = self.remote_group_id
385 if self.sec_grp_rule:
386 out['security_group_rule'] = self.sec_grp_rule
387 if self.remote_ip_prefix:
388 out['remote_ip_prefix'] = self.remote_ip_prefix
390 return {'security_group_rule': out}
392 def rule_eq(self, rule):
394 Returns True if this setting created the rule
395 :param rule: the rule to evaluate
398 rule_dict = rule['security_group_rule']
400 if self.description is not None:
401 if rule_dict['description'] is not None and rule_dict['description'] != '':
403 elif self.description != rule_dict['description']:
404 if rule_dict['description'] != '':
407 if self.direction.name != rule_dict['direction']:
410 if self.ethertype and rule_dict.get('ethertype'):
411 if self.ethertype.name != rule_dict['ethertype']:
414 if self.port_range_min and rule_dict.get('port_range_min'):
415 if self.port_range_min != rule_dict['port_range_min']:
418 if self.port_range_max and rule_dict.get('port_range_max'):
419 if self.port_range_max != rule_dict['port_range_max']:
422 if self.protocol and rule_dict.get('protocol'):
423 if self.protocol.name != rule_dict['protocol']:
426 if self.remote_group_id and rule_dict.get('remote_group_id'):
427 if self.remote_group_id != rule_dict['remote_group_id']:
430 if self.remote_ip_prefix and rule_dict.get('remote_ip_prefix'):
431 if self.remote_ip_prefix != rule_dict['remote_ip_prefix']:
436 def __eq__(self, other):
437 return self.description == other.description and \
438 self.direction == other.direction and \
439 self.port_range_min == other.port_range_min and \
440 self.port_range_max == other.port_range_max and \
441 self.ethertype == other.ethertype and \
442 self.protocol == other.protocol and \
443 self.sec_grp_name == other.sec_grp_name and \
444 self.remote_group_id == other.remote_group_id and \
445 self.sec_grp_rule == other.sec_grp_rule and \
446 self.remote_ip_prefix == other.remote_ip_prefix
449 return hash((self.sec_grp_name, self.description, self.direction, self.remote_group_id,
450 self.protocol, self.ethertype, self.port_range_min, self.port_range_max, self.sec_grp_rule,
451 self.remote_ip_prefix))
454 def map_direction(direction):
456 Takes a the direction value maps it to the Direction enum. When None return None
457 :param direction: the direction value
458 :return: the Direction enum object
459 :raise: Exception if value is invalid
463 if type(direction) is Direction:
465 elif isinstance(direction, basestring):
466 if direction == 'egress':
467 return Direction.egress
468 elif direction == 'ingress':
469 return Direction.ingress
471 raise Exception('Invalid Direction - ' + direction)
473 raise Exception('Invalid Direction object - ' + str(direction))
476 def map_protocol(protocol):
478 Takes a the protocol value maps it to the Protocol enum. When None return None
479 :param protocol: the protocol value
480 :return: the Protocol enum object
481 :raise: Exception if value is invalid
485 elif type(protocol) is Protocol:
487 elif isinstance(protocol, basestring):
488 if protocol == 'icmp':
490 elif protocol == 'tcp':
492 elif protocol == 'udp':
494 elif protocol == 'null':
497 raise Exception('Invalid Protocol - ' + protocol)
499 raise Exception('Invalid Protocol object - ' + str(protocol))
502 def map_ethertype(ethertype):
504 Takes a the ethertype value maps it to the Ethertype enum. When None return None
505 :param ethertype: the ethertype value
506 :return: the Ethertype enum object
507 :raise: Exception if value is invalid
511 elif type(ethertype) is Ethertype:
513 elif isinstance(ethertype, basestring):
514 if ethertype == 'IPv6':
515 return Ethertype.IPv6
516 elif ethertype == 'IPv4':
517 return Ethertype.IPv4
519 raise Exception('Invalid Ethertype - ' + ethertype)
521 raise Exception('Invalid Ethertype object - ' + str(ethertype))