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