Created domain class for ports.
[snaps.git] / snaps / openstack / create_instance.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 import time
17
18 from neutronclient.common.exceptions import PortNotFoundClient
19 from novaclient.exceptions import NotFound
20
21 from snaps.openstack.create_network import PortSettings
22 from snaps.openstack.utils import glance_utils
23 from snaps.openstack.utils import neutron_utils
24 from snaps.openstack.utils import nova_utils
25 from snaps.provisioning import ansible_utils
26
27 __author__ = 'spisarski'
28
29 logger = logging.getLogger('create_instance')
30
31 POLL_INTERVAL = 3
32 STATUS_ACTIVE = 'ACTIVE'
33 STATUS_DELETED = 'DELETED'
34
35
36 class OpenStackVmInstance:
37     """
38     Class responsible for creating a VM instance in OpenStack
39     """
40
41     def __init__(self, os_creds, instance_settings, image_settings,
42                  keypair_settings=None):
43         """
44         Constructor
45         :param os_creds: The connection credentials to the OpenStack API
46         :param instance_settings: Contains the settings for this VM
47         :param image_settings: The OpenStack image object settings
48         :param keypair_settings: The keypair metadata (Optional)
49         :raises Exception
50         """
51         self.__os_creds = os_creds
52
53         self.__nova = None
54         self.__neutron = None
55
56         self.instance_settings = instance_settings
57         self.image_settings = image_settings
58         self.keypair_settings = keypair_settings
59
60         # TODO - get rid of FIP list and only use the dict(). Need to fix
61         # populating this object when already exists
62         self.__floating_ips = list()
63         self.__floating_ip_dict = dict()
64
65         # Instantiated in self.create()
66         self.__ports = list()
67
68         # Note: this object does not change after the VM becomes active
69         self.__vm = None
70
71     def create(self, cleanup=False, block=False):
72         """
73         Creates a VM instance
74         :param cleanup: When true, only perform lookups for OpenStack objects.
75         :param block: Thread will block until instance has either become
76                       active, error, or timeout waiting.
77                       Additionally, when True, floating IPs will not be applied
78                       until VM is active.
79         :return: The VM reference object
80         """
81         self.__nova = nova_utils.nova_client(self.__os_creds)
82         self.__neutron = neutron_utils.neutron_client(self.__os_creds)
83
84         self.__ports = self.__setup_ports(self.instance_settings.port_settings,
85                                           cleanup)
86         self.__lookup_existing_vm_by_name()
87         if not self.__vm and not cleanup:
88             self.__create_vm(block)
89         return self.__vm
90
91     def __lookup_existing_vm_by_name(self):
92         """
93         Populates the member variables 'self.vm' and 'self.floating_ips' if a
94         VM with the same name already exists
95         within the project
96         """
97         servers = nova_utils.get_servers_by_name(self.__nova,
98                                                  self.instance_settings.name)
99         for server in servers:
100             if server.name == self.instance_settings.name:
101                 self.__vm = server
102                 logger.info(
103                     'Found existing machine with name - %s',
104                     self.instance_settings.name)
105                 fips = neutron_utils.get_floating_ips(self.__neutron)
106                 for fip in fips:
107                     for subnet_name, ips in server.networks.items():
108                         if fip.ip in ips:
109                             self.__floating_ips.append(fip)
110                             # TODO - Determine a means to associate to the FIP
111                             # configuration and add to FIP map
112
113     def __create_vm(self, block=False):
114         """
115         Responsible for creating the VM instance
116         :param block: Thread will block until instance has either become
117                       active, error, or timeout waiting. Floating IPs will be
118                       assigned after active when block=True
119         """
120         glance = glance_utils.glance_client(self.__os_creds)
121         self.__vm = nova_utils.create_server(
122             self.__nova, self.__neutron, glance, self.instance_settings,
123             self.image_settings, self.keypair_settings)
124         logger.info('Created instance with name - %s',
125                     self.instance_settings.name)
126
127         if block:
128             if not self.vm_active(block=True):
129                 raise Exception(
130                     'Fatal error, VM did not become ACTIVE within the alloted '
131                     'time')
132
133         # Create server should do this but found it needed to occur here
134         for sec_grp_name in self.instance_settings.security_group_names:
135             if self.vm_active(block=True):
136                 nova_utils.add_security_group(self.__nova, self.__vm,
137                                               sec_grp_name)
138             else:
139                 raise Exception(
140                     'Cannot applying security group with name ' +
141                     sec_grp_name +
142                     ' to VM that did not activate with name - ' +
143                     self.instance_settings.name)
144
145         self.__apply_floating_ips()
146
147     def __apply_floating_ips(self):
148         """
149         Applies the configured floating IPs to the necessary ports
150         """
151         port_dict = dict()
152         for key, port in self.__ports:
153             port_dict[key] = port
154
155         # Apply floating IPs
156         for floating_ip_setting in self.instance_settings.floating_ip_settings:
157             port = port_dict.get(floating_ip_setting.port_name)
158
159             if not port:
160                 raise Exception(
161                     'Cannot find port object with name - ' +
162                     floating_ip_setting.port_name)
163
164             # Setup Floating IP only if there is a router with an external
165             # gateway
166             ext_gateway = self.__ext_gateway_by_router(
167                 floating_ip_setting.router_name)
168             if ext_gateway:
169                 subnet = neutron_utils.get_subnet_by_name(
170                     self.__neutron, floating_ip_setting.subnet_name)
171                 floating_ip = neutron_utils.create_floating_ip(
172                     self.__neutron, ext_gateway)
173                 self.__floating_ips.append(floating_ip)
174                 self.__floating_ip_dict[floating_ip_setting.name] = floating_ip
175
176                 logger.info(
177                     'Created floating IP %s via router - %s', floating_ip.ip,
178                     floating_ip_setting.router_name)
179                 self.__add_floating_ip(floating_ip, port, subnet)
180             else:
181                 raise Exception('Unable to add floating IP to port,'
182                                 ' cannot locate router with an external '
183                                 'gateway ')
184
185     def __ext_gateway_by_router(self, router_name):
186         """
187         Returns network name for the external network attached to a router or
188         None if not found
189         :param router_name: The name of the router to lookup
190         :return: the external network name or None
191         """
192         router = neutron_utils.get_router_by_name(self.__neutron, router_name)
193         if router and router['router'].get('external_gateway_info'):
194             network = neutron_utils.get_network_by_id(
195                 self.__neutron,
196                 router['router']['external_gateway_info']['network_id'])
197             if network:
198                 return network['network']['name']
199         return None
200
201     def clean(self):
202         """
203         Destroys the VM instance
204         """
205
206         # Cleanup floating IPs
207         for floating_ip in self.__floating_ips:
208             try:
209                 logger.info('Deleting Floating IP - ' + floating_ip.ip)
210                 neutron_utils.delete_floating_ip(self.__neutron, floating_ip)
211             except Exception as e:
212                 logger.error('Error deleting Floating IP - ' + str(e))
213         self.__floating_ips = list()
214         self.__floating_ip_dict = dict()
215
216         # Cleanup ports
217         for name, port in self.__ports:
218             logger.info('Deleting Port - ' + name)
219             try:
220                 neutron_utils.delete_port(self.__neutron, port)
221             except PortNotFoundClient as e:
222                 logger.warning('Unexpected error deleting port - %s', e)
223                 pass
224         self.__ports = list()
225
226         # Cleanup VM
227         if self.__vm:
228             try:
229                 logger.info(
230                     'Deleting VM instance - ' + self.instance_settings.name)
231                 nova_utils.delete_vm_instance(self.__nova, self.__vm)
232             except Exception as e:
233                 logger.error('Error deleting VM - %s', e)
234
235             # Block until instance cannot be found or returns the status of
236             # DELETED
237             logger.info('Checking deletion status')
238
239             try:
240                 if self.vm_deleted(block=True):
241                     logger.info(
242                         'VM has been properly deleted VM with name - %s',
243                         self.instance_settings.name)
244                     self.__vm = None
245                 else:
246                     logger.error(
247                         'VM not deleted within the timeout period of %s '
248                         'seconds', self.instance_settings.vm_delete_timeout)
249             except Exception as e:
250                 logger.error(
251                     'Unexpected error while checking VM instance status - %s',
252                     e)
253
254     def __setup_ports(self, port_settings, cleanup):
255         """
256         Returns the previously configured ports or creates them if they do not
257         exist
258         :param port_settings: A list of PortSetting objects
259         :param cleanup: When true, only perform lookups for OpenStack objects.
260         :return: a list of OpenStack port tuples where the first member is the
261                  port name and the second is the port object
262         """
263         ports = list()
264
265         for port_setting in port_settings:
266             # First check to see if network already has this port
267             # TODO/FIXME - this could potentially cause problems if another
268             # port with the same name exists
269             # VM has the same network/port name pair
270             found = False
271
272             # TODO/FIXME - should we not be iterating on ports for the specific
273             # network in question as unique port names
274             # seem to only be important by network
275             existing_ports = self.__neutron.list_ports()['ports']
276             for existing_port in existing_ports:
277                 if existing_port['name'] == port_setting.name:
278                     ports.append((port_setting.name, {'port': existing_port}))
279                     found = True
280                     break
281
282             if not found and not cleanup:
283                 ports.append((port_setting.name,
284                               neutron_utils.create_port(self.__neutron,
285                                                         self.__os_creds,
286                                                         port_setting)))
287
288         return ports
289
290     def __add_floating_ip(self, floating_ip, port, subnet, timeout=30,
291                           poll_interval=POLL_INTERVAL):
292         """
293         Returns True when active else False
294         TODO - Make timeout and poll_interval configurable...
295         """
296         ip = None
297
298         if subnet:
299             # Take IP of subnet if there is one configured on which to place
300             # the floating IP
301             for fixed_ip in port.fixed_ips:
302                 if fixed_ip['subnet_id'] == subnet['subnet']['id']:
303                     ip = fixed_ip['ip_address']
304                     break
305         else:
306             # Simply take the first
307             ip = port.ips[0]['ip_address']
308
309         if ip:
310             count = timeout / poll_interval
311             while count > 0:
312                 logger.debug('Attempting to add floating IP to instance')
313                 try:
314                     nova_utils.add_floating_ip_to_server(
315                         self.__nova, self.__vm, floating_ip, ip)
316                     logger.info(
317                         'Added floating IP %s to port IP %s on instance %s',
318                         floating_ip.ip, ip, self.instance_settings.name)
319                     return
320                 except Exception as e:
321                     logger.debug(
322                         'Retry adding floating IP to instance. Last attempt '
323                         'failed with - %s', e)
324                     time.sleep(poll_interval)
325                     count -= 1
326                     pass
327         else:
328             raise Exception(
329                 'Unable find IP address on which to place the floating IP')
330
331         logger.error('Timeout attempting to add the floating IP to instance.')
332         raise Exception('Timeout while attempting add floating IP to instance')
333
334     def get_os_creds(self):
335         """
336         Returns the OpenStack credentials used to create these objects
337         :return: the credentials
338         """
339         return self.__os_creds
340
341     def get_vm_inst(self):
342         """
343         Returns the latest version of this server object from OpenStack
344         :return: Server object
345         """
346         return self.__vm
347
348     def get_os_vm_server_obj(self):
349         """
350         Returns the OpenStack server object
351         :return: the server object
352         """
353         return nova_utils.get_latest_server_os_object(self.__nova, self.__vm)
354
355     def get_port_ip(self, port_name, subnet_name=None):
356         """
357         Returns the first IP for the port corresponding with the port_name
358         parameter when subnet_name is None else returns the IP address that
359         corresponds to the subnet_name parameter
360         :param port_name: the name of the port from which to return the IP
361         :param subnet_name: the name of the subnet attached to this IP
362         :return: the IP or None if not found
363         """
364         port = self.get_port_by_name(port_name)
365         if port:
366             if subnet_name:
367                 subnet = neutron_utils.get_subnet_by_name(self.__neutron,
368                                                           subnet_name)
369                 if not subnet:
370                     logger.warning('Cannot retrieve port IP as subnet could '
371                                    'not be located with name - %s',
372                                    subnet_name)
373                     return None
374                 for fixed_ip in port.ips:
375                     if fixed_ip['subnet_id'] == subnet['subnet']['id']:
376                         return fixed_ip['ip_address']
377             else:
378                 if port.ips and len(port.ips) > 0:
379                     return port.ips[0]['ip_address']
380         return None
381
382     def get_port_mac(self, port_name):
383         """
384         Returns the first IP for the port corresponding with the port_name
385         parameter
386         TODO - Add in the subnet as an additional parameter as a port may have
387         multiple fixed_ips
388         :param port_name: the name of the port from which to return the IP
389         :return: the IP or None if not found
390         """
391         port = self.get_port_by_name(port_name)
392         if port:
393             return port.mac_address
394         return None
395
396     def get_port_by_name(self, port_name):
397         """
398         Retrieves the OpenStack port object by its given name
399         :param port_name: the name of the port
400         :return: the OpenStack port object or None if not exists
401         """
402         for key, port in self.__ports:
403             if key == port_name:
404                 return port
405         logger.warning('Cannot find port with name - ' + port_name)
406         return None
407
408     def config_nics(self):
409         """
410         Responsible for configuring NICs on RPM systems where the instance has
411         more than one configured port
412         :return: None
413         """
414         if len(self.__ports) > 1 and len(self.__floating_ips) > 0:
415             if self.vm_active(block=True) and self.vm_ssh_active(block=True):
416                 for key, port in self.__ports:
417                     port_index = self.__ports.index((key, port))
418                     if port_index > 0:
419                         nic_name = 'eth' + repr(port_index)
420                         self.__config_nic(
421                             nic_name, port,
422                             self.__get_first_provisioning_floating_ip().ip)
423                         logger.info('Configured NIC - %s on VM - %s',
424                                     nic_name, self.instance_settings.name)
425
426     def __get_first_provisioning_floating_ip(self):
427         """
428         Returns the first floating IP tagged with the Floating IP name if
429         exists else the first one found
430         :return:
431         """
432         for floating_ip_setting in self.instance_settings.floating_ip_settings:
433             if floating_ip_setting.provisioning:
434                 fip = self.__floating_ip_dict.get(floating_ip_setting.name)
435                 if fip:
436                     return fip
437                 elif len(self.__floating_ips) > 0:
438                     return self.__floating_ips[0]
439
440     def __config_nic(self, nic_name, port, ip):
441         """
442         Although ports/NICs can contain multiple IPs, this code currently only
443         supports the first.
444
445         :param nic_name: Name of the interface
446         :param port: The port information containing the expected IP values.
447         :param ip: The IP on which to apply the playbook.
448         :return: the return value from ansible
449         """
450         port_ip = port.ips[0]['ip_address']
451         variables = {
452             'floating_ip': ip,
453             'nic_name': nic_name,
454             'nic_ip': port_ip
455         }
456
457         if self.image_settings.nic_config_pb_loc and self.keypair_settings:
458             return self.apply_ansible_playbook(
459                 self.image_settings.nic_config_pb_loc, variables)
460         else:
461             logger.warning(
462                 'VM %s cannot self configure NICs eth1++. No playbook or '
463                 'keypairs found.', self.instance_settings.name)
464
465     def apply_ansible_playbook(self, pb_file_loc, variables=None,
466                                fip_name=None):
467         """
468         Applies a playbook to a VM
469         :param pb_file_loc: the file location of the playbook to be applied
470         :param variables: a dict() of substitution values required by the
471                           playbook
472         :param fip_name: the name of the floating IP to use for applying the
473                          playbook (default - will take the first)
474         :return: the return value from ansible
475         """
476         return ansible_utils.apply_playbook(
477             pb_file_loc, [self.get_floating_ip(fip_name=fip_name).ip],
478             self.get_image_user(), self.keypair_settings.private_filepath,
479             variables, self.__os_creds.proxy_settings)
480
481     def get_image_user(self):
482         """
483         Returns the instance sudo_user if it has been configured in the
484         instance_settings else it returns the image_settings.image_user value
485         """
486         if self.instance_settings.sudo_user:
487             return self.instance_settings.sudo_user
488         else:
489             return self.image_settings.image_user
490
491     def vm_deleted(self, block=False, poll_interval=POLL_INTERVAL):
492         """
493         Returns true when the VM status returns the value of
494         expected_status_code or instance retrieval throws a NotFound exception.
495         :param block: When true, thread will block until active or timeout
496                       value in seconds has been exceeded (False)
497         :param poll_interval: The polling interval in seconds
498         :return: T/F
499         """
500         try:
501             return self.__vm_status_check(
502                 STATUS_DELETED, block,
503                 self.instance_settings.vm_delete_timeout, poll_interval)
504         except NotFound as e:
505             logger.debug(
506                 "Instance not found when querying status for %s with message "
507                 "%s", STATUS_DELETED, e)
508             return True
509
510     def vm_active(self, block=False, poll_interval=POLL_INTERVAL):
511         """
512         Returns true when the VM status returns the value of
513         expected_status_code
514         :param block: When true, thread will block until active or timeout
515                       value in seconds has been exceeded (False)
516         :param poll_interval: The polling interval in seconds
517         :return: T/F
518         """
519         return self.__vm_status_check(STATUS_ACTIVE, block,
520                                       self.instance_settings.vm_boot_timeout,
521                                       poll_interval)
522
523     def __vm_status_check(self, expected_status_code, block, timeout,
524                           poll_interval):
525         """
526         Returns true when the VM status returns the value of
527         expected_status_code
528         :param expected_status_code: instance status evaluated with this
529                                      string value
530         :param block: When true, thread will block until active or timeout
531                       value in seconds has been exceeded (False)
532         :param timeout: The timeout value
533         :param poll_interval: The polling interval in seconds
534         :return: T/F
535         """
536         # sleep and wait for VM status change
537         if block:
538             start = time.time()
539         else:
540             return self.__status(expected_status_code)
541
542         while timeout > time.time() - start:
543             status = self.__status(expected_status_code)
544             if status:
545                 logger.info('VM is - ' + expected_status_code)
546                 return True
547
548             logger.debug('Retry querying VM status in ' + str(
549                 poll_interval) + ' seconds')
550             time.sleep(poll_interval)
551             logger.debug('VM status query timeout in ' + str(
552                 timeout - (time.time() - start)))
553
554         logger.error(
555             'Timeout checking for VM status for ' + expected_status_code)
556         return False
557
558     def __status(self, expected_status_code):
559         """
560         Returns True when active else False
561         :param expected_status_code: instance status evaluated with this string
562                                      value
563         :return: T/F
564         """
565         if not self.__vm:
566             return False
567
568         instance = nova_utils.get_latest_server_os_object(
569             self.__nova, self.__vm)
570         if not instance:
571             logger.warning('Cannot find instance with id - ' + self.__vm.id)
572             return False
573
574         if instance.status == 'ERROR':
575             raise Exception('Instance had an error during deployment')
576         logger.debug(
577             'Instance status [%s] is - %s', self.instance_settings.name,
578             instance.status)
579         return instance.status == expected_status_code
580
581     def vm_ssh_active(self, block=False, poll_interval=POLL_INTERVAL):
582         """
583         Returns true when the VM can be accessed via SSH
584         :param block: When true, thread will block until active or timeout
585                       value in seconds has been exceeded (False)
586         :param poll_interval: The polling interval
587         :return: T/F
588         """
589         # sleep and wait for VM status change
590         logger.info('Checking if VM is active')
591
592         timeout = self.instance_settings.ssh_connect_timeout
593
594         if self.vm_active(block=True):
595             if block:
596                 start = time.time()
597             else:
598                 start = time.time() - timeout
599
600             while timeout > time.time() - start:
601                 status = self.__ssh_active()
602                 if status:
603                     logger.info('SSH is active for VM instance')
604                     return True
605
606                 logger.debug('Retry SSH connection in ' + str(
607                     poll_interval) + ' seconds')
608                 time.sleep(poll_interval)
609                 logger.debug('SSH connection timeout in ' + str(
610                     timeout - (time.time() - start)))
611
612         logger.error('Timeout attempting to connect with VM via SSH')
613         return False
614
615     def __ssh_active(self):
616         """
617         Returns True when can create a SSH session else False
618         :return: T/F
619         """
620         if len(self.__floating_ips) > 0:
621             ssh = self.ssh_client()
622             if ssh:
623                 return True
624         return False
625
626     def get_floating_ip(self, fip_name=None):
627         """
628         Returns the floating IP object byt name if found, else the first known,
629         else None
630         :param fip_name: the name of the floating IP to return
631         :return: the SSH client or None
632         """
633         fip = None
634         if fip_name and self.__floating_ip_dict.get(fip_name):
635             return self.__floating_ip_dict.get(fip_name)
636         if not fip and len(self.__floating_ips) > 0:
637             return self.__floating_ips[0]
638         return None
639
640     def ssh_client(self, fip_name=None):
641         """
642         Returns an SSH client using the name or the first known floating IP if
643         exists, else None
644         :param fip_name: the name of the floating IP to return
645         :return: the SSH client or None
646         """
647         fip = self.get_floating_ip(fip_name)
648         if fip:
649             return ansible_utils.ssh_client(
650                 self.__floating_ips[0].ip, self.get_image_user(),
651                 self.keypair_settings.private_filepath,
652                 proxy_settings=self.__os_creds.proxy_settings)
653         else:
654             logger.warning(
655                 'Cannot return an SSH client. No Floating IP configured')
656
657     def add_security_group(self, security_group):
658         """
659         Adds a security group to this VM. Call will block until VM is active.
660         :param security_group: the SNAPS SecurityGroup domain object
661         :return True if successful else False
662         """
663         self.vm_active(block=True)
664
665         if not security_group:
666             logger.warning('Security group object is None, cannot add')
667             return False
668
669         try:
670             nova_utils.add_security_group(self.__nova, self.get_vm_inst(),
671                                           security_group.name)
672             return True
673         except NotFound as e:
674             logger.warning('Security group not added - ' + str(e))
675             return False
676
677     def remove_security_group(self, security_group):
678         """
679         Removes a security group to this VM. Call will block until VM is active
680         :param security_group: the OpenStack security group object
681         :return True if successful else False
682         """
683         self.vm_active(block=True)
684
685         if not security_group:
686             logger.warning('Security group object is None, cannot remove')
687             return False
688
689         try:
690             nova_utils.remove_security_group(self.__nova, self.get_vm_inst(),
691                                              security_group)
692             return True
693         except NotFound as e:
694             logger.warning('Security group not removed - ' + str(e))
695             return False
696
697
698 class VmInstanceSettings:
699     """
700     Class responsible for holding configuration setting for a VM Instance
701     """
702
703     def __init__(self, **kwargs):
704         """
705         Constructor
706         :param name: the name of the VM
707         :param flavor: the VM's flavor
708         :param port_settings: the port configuration settings (required)
709         :param security_group_names: a set of names of the security groups to
710                                      add to the VM
711         :param floating_ip_settings: the floating IP configuration settings
712         :param sudo_user: the sudo user of the VM that will override the
713                           instance_settings.image_user when trying to
714                           connect to the VM
715         :param vm_boot_timeout: the amount of time a thread will sleep waiting
716                                 for an instance to boot
717         :param vm_delete_timeout: the amount of time a thread will sleep
718                                   waiting for an instance to be deleted
719         :param ssh_connect_timeout: the amount of time a thread will sleep
720                                     waiting obtaining an SSH connection to a VM
721         :param availability_zone: the name of the compute server on which to
722                                   deploy the VM (optional)
723         :param userdata: the cloud-init script to run after the VM has been
724                          started
725         """
726         self.name = kwargs.get('name')
727         self.flavor = kwargs.get('flavor')
728         self.sudo_user = kwargs.get('sudo_user')
729         self.userdata = kwargs.get('userdata')
730
731         self.port_settings = list()
732         port_settings = kwargs.get('ports')
733         if not port_settings:
734             port_settings = kwargs.get('port_settings')
735         if port_settings:
736             for port_setting in port_settings:
737                 if isinstance(port_setting, dict):
738                     self.port_settings.append(PortSettings(**port_setting))
739                 elif isinstance(port_setting, PortSettings):
740                     self.port_settings.append(port_setting)
741
742         if kwargs.get('security_group_names'):
743             if isinstance(kwargs['security_group_names'], list):
744                 self.security_group_names = kwargs['security_group_names']
745             elif isinstance(kwargs['security_group_names'], set):
746                 self.security_group_names = kwargs['security_group_names']
747             elif isinstance(kwargs['security_group_names'], str):
748                 self.security_group_names = [kwargs['security_group_names']]
749             else:
750                 raise Exception(
751                     'Invalid data type for security_group_names attribute')
752         else:
753             self.security_group_names = set()
754
755         self.floating_ip_settings = list()
756         floating_ip_settings = kwargs.get('floating_ips')
757         if not floating_ip_settings:
758             floating_ip_settings = kwargs.get('floating_ip_settings')
759         if floating_ip_settings:
760             for floating_ip_config in floating_ip_settings:
761                 if isinstance(floating_ip_config, FloatingIpSettings):
762                     self.floating_ip_settings.append(floating_ip_config)
763                 else:
764                     self.floating_ip_settings.append(FloatingIpSettings(
765                         **floating_ip_config['floating_ip']))
766
767         if kwargs.get('vm_boot_timeout'):
768             self.vm_boot_timeout = kwargs['vm_boot_timeout']
769         else:
770             self.vm_boot_timeout = 900
771
772         if kwargs.get('vm_delete_timeout'):
773             self.vm_delete_timeout = kwargs['vm_delete_timeout']
774         else:
775             self.vm_delete_timeout = 300
776
777         if kwargs.get('ssh_connect_timeout'):
778             self.ssh_connect_timeout = kwargs['ssh_connect_timeout']
779         else:
780             self.ssh_connect_timeout = 180
781
782         if kwargs.get('availability_zone'):
783             self.availability_zone = kwargs['availability_zone']
784         else:
785             self.availability_zone = None
786
787         if not self.name or not self.flavor:
788             raise Exception(
789                 'Instance configuration requires the attributes: name, flavor')
790
791         if len(self.port_settings) == 0:
792             raise Exception(
793                 'Instance configuration requires port settings (aka. NICS)')
794
795
796 class FloatingIpSettings:
797     """
798     Class responsible for holding configuration settings for a floating IP
799     """
800
801     def __init__(self, **kwargs):
802         """
803         Constructor
804         :param name: the name of the floating IP
805         :param port_name: the name of the router to the external network
806         :param router_name: the name of the router to the external network
807         :param subnet_name: the name of the subnet on which to attach the
808                             floating IP
809         :param provisioning: when true, this floating IP can be used for
810                              provisioning
811
812         TODO - provisioning flag is a hack as I have only observed a single
813         Floating IPs that actually works on an instance. Multiple floating IPs
814         placed on different subnets from the same port are especially
815         troublesome as you cannot predict which one will actually connect.
816         For now, it is recommended not to setup multiple floating IPs on an
817         instance unless absolutely necessary.
818         """
819         self.name = kwargs.get('name')
820         self.port_name = kwargs.get('port_name')
821         self.router_name = kwargs.get('router_name')
822         self.subnet_name = kwargs.get('subnet_name')
823         if kwargs.get('provisioning') is not None:
824             self.provisioning = kwargs['provisioning']
825         else:
826             self.provisioning = True
827
828         if not self.name or not self.port_name or not self.router_name:
829             raise Exception(
830                 'The attributes name, port_name and router_name are required '
831                 'for FloatingIPSettings')