Add the function of getting endpoint
[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
19 from snaps.openstack.utils import neutron_utils
20 from snaps.openstack.utils import keystone_utils
21
22 __author__ = 'spisarski'
23
24 logger = logging.getLogger('OpenStackSecurityGroup')
25
26
27 class OpenStackSecurityGroup:
28     """
29     Class responsible for creating Security Groups
30     """
31
32     def __init__(self, os_creds, sec_grp_settings):
33         """
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
37         """
38         self.__os_creds = os_creds
39         self.sec_grp_settings = sec_grp_settings
40         self.__neutron = None
41         self.__keystone = None
42
43         # Attributes instantiated on create()
44         self.__security_group = None
45
46         # dict where the rule settings object is the key
47         self.__rules = dict()
48
49     def create(self, cleanup=False):
50         """
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
54         """
55         self.__neutron = neutron_utils.neutron_client(self.__os_creds)
56         self.__keystone = keystone_utils.keystone_client(self.__os_creds)
57
58         logger.info('Creating security group %s...' % self.sec_grp_settings.name)
59
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)
65
66             # Get the rules added for free
67             auto_rules = neutron_utils.get_rules_by_security_group(self.__neutron, self.__security_group)
68
69             ctr = 0
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
73                 ctr += 1
74
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
79
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)
82         else:
83             # Populate rules
84             existing_rules = neutron_utils.get_rules_by_security_group(self.__neutron, self.__security_group)
85
86             for existing_rule in existing_rules:
87                 # For Custom Rules
88                 rule_setting = self.__get_setting_from_rule(existing_rule)
89                 ctr = 0
90                 if not rule_setting:
91                     # For Free Rules
92                     rule_setting = self.__generate_rule_setting(existing_rule)
93                     ctr += 1
94
95                 self.__rules[rule_setting] = existing_rule
96
97         return self.__security_group
98
99     def __generate_rule_setting(self, rule):
100         """
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
104         """
105         rule_dict = rule['security_group_rule']
106         sec_grp_name = None
107         if rule_dict['security_group_id']:
108             sec_grp = neutron_utils.get_security_group_by_id(self.__neutron, rule_dict['security_group_id'])
109             if sec_grp:
110                 sec_grp_name = sec_grp['security_group']['name']
111
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)
118         return setting
119
120     def clean(self):
121         """
122         Removes and deletes the rules then the security group.
123         """
124         for setting, rule in self.__rules.items():
125             try:
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))
129                 pass
130         self.__rules = dict()
131
132         if self.__security_group:
133             try:
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))
137
138             self.__security_group = None
139
140     def get_security_group(self):
141         """
142         Returns the OpenStack security group object
143         :return:
144         """
145         return self.__security_group
146
147     def get_rules(self):
148         """
149         Returns the associated rules
150         :return:
151         """
152         return self.__rules
153
154     def add_rule(self, rule_setting):
155         """
156         Adds a rule to this security group
157         :param rule_setting: the rule configuration
158         """
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)
163
164     def remove_rule(self, rule_id=None, rule_setting=None):
165         """
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
169         """
170         rule_to_remove = None
171         if rule_id or rule_setting:
172             if rule_id:
173                 rule_to_remove = neutron_utils.get_rule_by_id(self.__neutron, self.__security_group, rule_id)
174             elif rule_setting:
175                 rule_to_remove = self.__rules.get(rule_setting)
176
177         if rule_to_remove:
178             neutron_utils.delete_security_group_rule(self.__neutron, rule_to_remove)
179             rule_setting = self.__get_setting_from_rule(rule_to_remove)
180             if rule_setting:
181                 self.__rules.pop(rule_setting)
182             else:
183                 logger.warning('Rule setting is None, cannot remove rule')
184
185     def __get_setting_from_rule(self, rule):
186         """
187         Returns the associated RuleSetting object for a given rule
188         :param rule: the Rule object
189         :return: the associated RuleSetting object or None
190         """
191         for rule_setting in self.sec_grp_settings.rule_settings:
192             if rule_setting.rule_eq(rule):
193                 return rule_setting
194         return None
195
196
197 class SecurityGroupSettings:
198     """
199     Class representing a keypair configuration
200     """
201
202     def __init__(self, config=None, name=None, description=None, project_name=None,
203                  rule_settings=list()):
204         """
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
211         :return:
212         """
213         if config:
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))
221         else:
222             self.name = name
223             self.description = description
224             self.project_name = project_name
225             self.rule_settings = rule_settings
226
227         if not self.name:
228             raise Exception('The attribute name is required')
229
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')
233
234     def dict_for_neutron(self, keystone):
235         """
236         Returns a dictionary object representing this object.
237         This is meant to be converted into JSON designed for use by the Neutron API
238
239         TODO - expand automated testing to exercise all parameters
240         :param keystone: the Keystone client
241         :return: the dictionary object
242         """
243         out = dict()
244
245         if self.name:
246             out['name'] = self.name
247         if self.description:
248             out['description'] = self.description
249         if self.project_name:
250             project = keystone_utils.get_project(keystone, self.project_name)
251             project_id = None
252             if project:
253                 project_id = project.id
254             if project_id:
255                 out['project_id'] = project_id
256             else:
257                 raise Exception('Could not find project ID for project named - ' + self.project_name)
258
259         return {'security_group': out}
260
261
262 class Direction(enum.Enum):
263     """
264     A rule's direction
265     """
266     ingress = 'ingress'
267     egress = 'egress'
268
269
270 class Protocol(enum.Enum):
271     """
272     A rule's protocol
273     """
274     icmp = 'icmp'
275     tcp = 'tcp'
276     udp = 'udp'
277     null = 'null'
278
279
280 class Ethertype(enum.Enum):
281     """
282     A rule's ethertype
283     """
284     IPv4 = 4
285     IPv6 = 6
286
287
288 class SecurityGroupRuleSettings:
289     """
290     Class representing a keypair configuration
291     """
292
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):
296         """
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
316                              parameter)
317         :param remote_ip_prefix: The remote IP prefix to associate with this metering rule packet (optional)
318
319         TODO - Need to support the tenant...
320         """
321
322         if config:
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'])
329
330             self.protocol = None
331             if config.get('protocol'):
332                 self.protocol = map_protocol(config['protocol'])
333             else:
334                 self.protocol = Protocol.null
335
336             self.ethertype = None
337             if config.get('ethertype'):
338                 self.ethertype = map_ethertype(config['ethertype'])
339
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')
343         else:
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
354
355         if not self.direction or not self.sec_grp_name:
356             raise Exception('direction and sec_grp_name are required')
357
358     def dict_for_neutron(self, neutron):
359         """
360         Returns a dictionary object representing this object.
361         This is meant to be converted into JSON designed for use by the Neutron API
362
363         :param neutron: the neutron client for performing lookups
364         :return: the dictionary object
365         """
366         out = dict()
367
368         if self.description:
369             out['description'] = self.description
370         if self.direction:
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
376         if self.ethertype:
377             out['ethertype'] = self.ethertype.name
378         if self.protocol:
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)
382             if sec_grp:
383                 out['security_group_id'] = sec_grp['security_group']['id']
384             else:
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
392
393         return {'security_group_rule': out}
394
395     def rule_eq(self, rule):
396         """
397         Returns True if this setting created the rule
398         :param rule: the rule to evaluate
399         :return: T/F
400         """
401         rule_dict = rule['security_group_rule']
402
403         if self.description is not None:
404             if rule_dict['description'] is not None and rule_dict['description'] != '':
405                 return False
406         elif self.description != rule_dict['description']:
407             if rule_dict['description'] != '':
408                 return False
409
410         if self.direction.name != rule_dict['direction']:
411             return False
412
413         if self.ethertype and rule_dict.get('ethertype'):
414             if self.ethertype.name != rule_dict['ethertype']:
415                 return False
416
417         if self.port_range_min and rule_dict.get('port_range_min'):
418             if self.port_range_min != rule_dict['port_range_min']:
419                 return False
420
421         if self.port_range_max and rule_dict.get('port_range_max'):
422             if self.port_range_max != rule_dict['port_range_max']:
423                 return False
424
425         if self.protocol and rule_dict.get('protocol'):
426             if self.protocol.name != rule_dict['protocol']:
427                 return False
428
429         if self.remote_group_id and rule_dict.get('remote_group_id'):
430             if self.remote_group_id != rule_dict['remote_group_id']:
431                 return False
432
433         if self.remote_ip_prefix and rule_dict.get('remote_ip_prefix'):
434             if self.remote_ip_prefix != rule_dict['remote_ip_prefix']:
435                 return False
436
437         return True
438
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
450
451     def __hash__(self):
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))
455
456
457 def map_direction(direction):
458     """
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
463     """
464     if not direction:
465         return None
466     if isinstance(direction, Direction):
467         return direction
468     else:
469         dir_str = str(direction)
470         if dir_str == 'egress':
471             return Direction.egress
472         elif dir_str == 'ingress':
473             return Direction.ingress
474         else:
475             raise Exception('Invalid Direction - ' + dir_str)
476
477
478 def map_protocol(protocol):
479     """
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
484     """
485     if not protocol:
486         return None
487     elif isinstance(protocol, Protocol):
488         return protocol
489     else:
490         proto_str = str(protocol)
491         if proto_str == 'icmp':
492             return Protocol.icmp
493         elif proto_str == 'tcp':
494             return Protocol.tcp
495         elif proto_str == 'udp':
496             return Protocol.udp
497         elif proto_str == 'null':
498             return Protocol.null
499         else:
500             raise Exception('Invalid Protocol - ' + proto_str)
501
502
503 def map_ethertype(ethertype):
504     """
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
509     """
510     if not ethertype:
511         return None
512     elif isinstance(ethertype, Ethertype):
513         return ethertype
514     else:
515         eth_str = str(ethertype)
516         if eth_str == 'IPv6':
517             return Ethertype.IPv6
518         elif eth_str == 'IPv4':
519             return Ethertype.IPv4
520         else:
521             raise Exception('Invalid Ethertype - ' + eth_str)