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