Added proper assertion config NIC test.
[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.external_gateway_info:
194             network = neutron_utils.get_network_by_id(
195                 self.__neutron,
196                 router.external_gateway_info['network_id'])
197             if network:
198                 return 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             port = neutron_utils.get_port_by_name(self.__neutron,
267                                                   port_setting.name)
268             if port:
269                 ports.append((port_setting.name, {'port': port}))
270             elif not cleanup:
271                 # Exception will be raised when port with same name already
272                 # exists
273                 ports.append(
274                     (port_setting.name, neutron_utils.create_port(
275                         self.__neutron, self.__os_creds, port_setting)))
276
277         return ports
278
279     def __add_floating_ip(self, floating_ip, port, subnet, timeout=30,
280                           poll_interval=POLL_INTERVAL):
281         """
282         Returns True when active else False
283         TODO - Make timeout and poll_interval configurable...
284         """
285         ip = None
286
287         if subnet:
288             # Take IP of subnet if there is one configured on which to place
289             # the floating IP
290             for fixed_ip in port.fixed_ips:
291                 if fixed_ip['subnet_id'] == subnet['subnet']['id']:
292                     ip = fixed_ip['ip_address']
293                     break
294         else:
295             # Simply take the first
296             ip = port.ips[0]['ip_address']
297
298         if ip:
299             count = timeout / poll_interval
300             while count > 0:
301                 logger.debug('Attempting to add floating IP to instance')
302                 try:
303                     nova_utils.add_floating_ip_to_server(
304                         self.__nova, self.__vm, floating_ip, ip)
305                     logger.info(
306                         'Added floating IP %s to port IP %s on instance %s',
307                         floating_ip.ip, ip, self.instance_settings.name)
308                     return
309                 except Exception as e:
310                     logger.debug(
311                         'Retry adding floating IP to instance. Last attempt '
312                         'failed with - %s', e)
313                     time.sleep(poll_interval)
314                     count -= 1
315                     pass
316         else:
317             raise Exception(
318                 'Unable find IP address on which to place the floating IP')
319
320         logger.error('Timeout attempting to add the floating IP to instance.')
321         raise Exception('Timeout while attempting add floating IP to instance')
322
323     def get_os_creds(self):
324         """
325         Returns the OpenStack credentials used to create these objects
326         :return: the credentials
327         """
328         return self.__os_creds
329
330     def get_vm_inst(self):
331         """
332         Returns the latest version of this server object from OpenStack
333         :return: Server object
334         """
335         return self.__vm
336
337     def get_os_vm_server_obj(self):
338         """
339         Returns the OpenStack server object
340         :return: the server object
341         """
342         return nova_utils.get_latest_server_os_object(self.__nova, self.__vm)
343
344     def get_port_ip(self, port_name, subnet_name=None):
345         """
346         Returns the first IP for the port corresponding with the port_name
347         parameter when subnet_name is None else returns the IP address that
348         corresponds to the subnet_name parameter
349         :param port_name: the name of the port from which to return the IP
350         :param subnet_name: the name of the subnet attached to this IP
351         :return: the IP or None if not found
352         """
353         port = self.get_port_by_name(port_name)
354         if port:
355             if subnet_name:
356                 subnet = neutron_utils.get_subnet_by_name(self.__neutron,
357                                                           subnet_name)
358                 if not subnet:
359                     logger.warning('Cannot retrieve port IP as subnet could '
360                                    'not be located with name - %s',
361                                    subnet_name)
362                     return None
363                 for fixed_ip in port.ips:
364                     if fixed_ip['subnet_id'] == subnet.id:
365                         return fixed_ip['ip_address']
366             else:
367                 if port.ips and len(port.ips) > 0:
368                     return port.ips[0]['ip_address']
369         return None
370
371     def get_port_mac(self, port_name):
372         """
373         Returns the first IP for the port corresponding with the port_name
374         parameter
375         TODO - Add in the subnet as an additional parameter as a port may have
376         multiple fixed_ips
377         :param port_name: the name of the port from which to return the IP
378         :return: the IP or None if not found
379         """
380         port = self.get_port_by_name(port_name)
381         if port:
382             return port.mac_address
383         return None
384
385     def get_port_by_name(self, port_name):
386         """
387         Retrieves the OpenStack port object by its given name
388         :param port_name: the name of the port
389         :return: the OpenStack port object or None if not exists
390         """
391         for key, port in self.__ports:
392             if key == port_name:
393                 return port
394         logger.warning('Cannot find port with name - ' + port_name)
395         return None
396
397     def config_nics(self):
398         """
399         Responsible for configuring NICs on RPM systems where the instance has
400         more than one configured port
401         :return: the value returned by ansible_utils.apply_ansible_playbook()
402         """
403         if len(self.__ports) > 1 and len(self.__floating_ips) > 0:
404             if self.vm_active(block=True) and self.vm_ssh_active(block=True):
405                 for key, port in self.__ports:
406                     port_index = self.__ports.index((key, port))
407                     if port_index > 0:
408                         nic_name = 'eth' + repr(port_index)
409                         retval = self.__config_nic(
410                             nic_name, port,
411                             self.__get_first_provisioning_floating_ip().ip)
412                         logger.info('Configured NIC - %s on VM - %s',
413                                     nic_name, self.instance_settings.name)
414                         return retval
415
416     def __get_first_provisioning_floating_ip(self):
417         """
418         Returns the first floating IP tagged with the Floating IP name if
419         exists else the first one found
420         :return:
421         """
422         for floating_ip_setting in self.instance_settings.floating_ip_settings:
423             if floating_ip_setting.provisioning:
424                 fip = self.__floating_ip_dict.get(floating_ip_setting.name)
425                 if fip:
426                     return fip
427                 elif len(self.__floating_ips) > 0:
428                     return self.__floating_ips[0]
429
430     def __config_nic(self, nic_name, port, ip):
431         """
432         Although ports/NICs can contain multiple IPs, this code currently only
433         supports the first.
434
435         :param nic_name: Name of the interface
436         :param port: The port information containing the expected IP values.
437         :param ip: The IP on which to apply the playbook.
438         :return: the return value from ansible
439         """
440         port_ip = port.ips[0]['ip_address']
441         variables = {
442             'floating_ip': ip,
443             'nic_name': nic_name,
444             'nic_ip': port_ip
445         }
446
447         if self.image_settings.nic_config_pb_loc and self.keypair_settings:
448             return self.apply_ansible_playbook(
449                 self.image_settings.nic_config_pb_loc, variables)
450         else:
451             logger.warning(
452                 'VM %s cannot self configure NICs eth1++. No playbook or '
453                 'keypairs found.', self.instance_settings.name)
454
455     def apply_ansible_playbook(self, pb_file_loc, variables=None,
456                                fip_name=None):
457         """
458         Applies a playbook to a VM
459         :param pb_file_loc: the file location of the playbook to be applied
460         :param variables: a dict() of substitution values required by the
461                           playbook
462         :param fip_name: the name of the floating IP to use for applying the
463                          playbook (default - will take the first)
464         :return: the return value from ansible
465         """
466         return ansible_utils.apply_playbook(
467             pb_file_loc, [self.get_floating_ip(fip_name=fip_name).ip],
468             self.get_image_user(), self.keypair_settings.private_filepath,
469             variables, self.__os_creds.proxy_settings)
470
471     def get_image_user(self):
472         """
473         Returns the instance sudo_user if it has been configured in the
474         instance_settings else it returns the image_settings.image_user value
475         """
476         if self.instance_settings.sudo_user:
477             return self.instance_settings.sudo_user
478         else:
479             return self.image_settings.image_user
480
481     def vm_deleted(self, block=False, poll_interval=POLL_INTERVAL):
482         """
483         Returns true when the VM status returns the value of
484         expected_status_code or instance retrieval throws a NotFound exception.
485         :param block: When true, thread will block until active or timeout
486                       value in seconds has been exceeded (False)
487         :param poll_interval: The polling interval in seconds
488         :return: T/F
489         """
490         try:
491             return self.__vm_status_check(
492                 STATUS_DELETED, block,
493                 self.instance_settings.vm_delete_timeout, poll_interval)
494         except NotFound as e:
495             logger.debug(
496                 "Instance not found when querying status for %s with message "
497                 "%s", STATUS_DELETED, e)
498             return True
499
500     def vm_active(self, block=False, poll_interval=POLL_INTERVAL):
501         """
502         Returns true when the VM status returns the value of
503         expected_status_code
504         :param block: When true, thread will block until active or timeout
505                       value in seconds has been exceeded (False)
506         :param poll_interval: The polling interval in seconds
507         :return: T/F
508         """
509         return self.__vm_status_check(STATUS_ACTIVE, block,
510                                       self.instance_settings.vm_boot_timeout,
511                                       poll_interval)
512
513     def __vm_status_check(self, expected_status_code, block, timeout,
514                           poll_interval):
515         """
516         Returns true when the VM status returns the value of
517         expected_status_code
518         :param expected_status_code: instance status evaluated with this
519                                      string value
520         :param block: When true, thread will block until active or timeout
521                       value in seconds has been exceeded (False)
522         :param timeout: The timeout value
523         :param poll_interval: The polling interval in seconds
524         :return: T/F
525         """
526         # sleep and wait for VM status change
527         if block:
528             start = time.time()
529         else:
530             return self.__status(expected_status_code)
531
532         while timeout > time.time() - start:
533             status = self.__status(expected_status_code)
534             if status:
535                 logger.info('VM is - ' + expected_status_code)
536                 return True
537
538             logger.debug('Retry querying VM status in ' + str(
539                 poll_interval) + ' seconds')
540             time.sleep(poll_interval)
541             logger.debug('VM status query timeout in ' + str(
542                 timeout - (time.time() - start)))
543
544         logger.error(
545             'Timeout checking for VM status for ' + expected_status_code)
546         return False
547
548     def __status(self, expected_status_code):
549         """
550         Returns True when active else False
551         :param expected_status_code: instance status evaluated with this string
552                                      value
553         :return: T/F
554         """
555         if not self.__vm:
556             return False
557
558         instance = nova_utils.get_latest_server_os_object(
559             self.__nova, self.__vm)
560         if not instance:
561             logger.warning('Cannot find instance with id - ' + self.__vm.id)
562             return False
563
564         if instance.status == 'ERROR':
565             raise Exception('Instance had an error during deployment')
566         logger.debug(
567             'Instance status [%s] is - %s', self.instance_settings.name,
568             instance.status)
569         return instance.status == expected_status_code
570
571     def vm_ssh_active(self, block=False, poll_interval=POLL_INTERVAL):
572         """
573         Returns true when the VM can be accessed via SSH
574         :param block: When true, thread will block until active or timeout
575                       value in seconds has been exceeded (False)
576         :param poll_interval: The polling interval
577         :return: T/F
578         """
579         # sleep and wait for VM status change
580         logger.info('Checking if VM is active')
581
582         timeout = self.instance_settings.ssh_connect_timeout
583
584         if self.vm_active(block=True):
585             if block:
586                 start = time.time()
587             else:
588                 start = time.time() - timeout
589
590             while timeout > time.time() - start:
591                 status = self.__ssh_active()
592                 if status:
593                     logger.info('SSH is active for VM instance')
594                     return True
595
596                 logger.debug('Retry SSH connection in ' + str(
597                     poll_interval) + ' seconds')
598                 time.sleep(poll_interval)
599                 logger.debug('SSH connection timeout in ' + str(
600                     timeout - (time.time() - start)))
601
602         logger.error('Timeout attempting to connect with VM via SSH')
603         return False
604
605     def __ssh_active(self):
606         """
607         Returns True when can create a SSH session else False
608         :return: T/F
609         """
610         if len(self.__floating_ips) > 0:
611             ssh = self.ssh_client()
612             if ssh:
613                 return True
614         return False
615
616     def get_floating_ip(self, fip_name=None):
617         """
618         Returns the floating IP object byt name if found, else the first known,
619         else None
620         :param fip_name: the name of the floating IP to return
621         :return: the SSH client or None
622         """
623         fip = None
624         if fip_name and self.__floating_ip_dict.get(fip_name):
625             return self.__floating_ip_dict.get(fip_name)
626         if not fip and len(self.__floating_ips) > 0:
627             return self.__floating_ips[0]
628         return None
629
630     def ssh_client(self, fip_name=None):
631         """
632         Returns an SSH client using the name or the first known floating IP if
633         exists, else None
634         :param fip_name: the name of the floating IP to return
635         :return: the SSH client or None
636         """
637         fip = self.get_floating_ip(fip_name)
638         if fip:
639             return ansible_utils.ssh_client(
640                 self.__floating_ips[0].ip, self.get_image_user(),
641                 self.keypair_settings.private_filepath,
642                 proxy_settings=self.__os_creds.proxy_settings)
643         else:
644             logger.warning(
645                 'Cannot return an SSH client. No Floating IP configured')
646
647     def add_security_group(self, security_group):
648         """
649         Adds a security group to this VM. Call will block until VM is active.
650         :param security_group: the SNAPS SecurityGroup domain object
651         :return True if successful else False
652         """
653         self.vm_active(block=True)
654
655         if not security_group:
656             logger.warning('Security group object is None, cannot add')
657             return False
658
659         try:
660             nova_utils.add_security_group(self.__nova, self.get_vm_inst(),
661                                           security_group.name)
662             return True
663         except NotFound as e:
664             logger.warning('Security group not added - ' + str(e))
665             return False
666
667     def remove_security_group(self, security_group):
668         """
669         Removes a security group to this VM. Call will block until VM is active
670         :param security_group: the OpenStack security group object
671         :return True if successful else False
672         """
673         self.vm_active(block=True)
674
675         if not security_group:
676             logger.warning('Security group object is None, cannot remove')
677             return False
678
679         try:
680             nova_utils.remove_security_group(self.__nova, self.get_vm_inst(),
681                                              security_group)
682             return True
683         except NotFound as e:
684             logger.warning('Security group not removed - ' + str(e))
685             return False
686
687
688 class VmInstanceSettings:
689     """
690     Class responsible for holding configuration setting for a VM Instance
691     """
692
693     def __init__(self, **kwargs):
694         """
695         Constructor
696         :param name: the name of the VM
697         :param flavor: the VM's flavor
698         :param port_settings: the port configuration settings (required)
699         :param security_group_names: a set of names of the security groups to
700                                      add to the VM
701         :param floating_ip_settings: the floating IP configuration settings
702         :param sudo_user: the sudo user of the VM that will override the
703                           instance_settings.image_user when trying to
704                           connect to the VM
705         :param vm_boot_timeout: the amount of time a thread will sleep waiting
706                                 for an instance to boot
707         :param vm_delete_timeout: the amount of time a thread will sleep
708                                   waiting for an instance to be deleted
709         :param ssh_connect_timeout: the amount of time a thread will sleep
710                                     waiting obtaining an SSH connection to a VM
711         :param availability_zone: the name of the compute server on which to
712                                   deploy the VM (optional)
713         :param userdata: the cloud-init script to run after the VM has been
714                          started
715         """
716         self.name = kwargs.get('name')
717         self.flavor = kwargs.get('flavor')
718         self.sudo_user = kwargs.get('sudo_user')
719         self.userdata = kwargs.get('userdata')
720
721         self.port_settings = list()
722         port_settings = kwargs.get('ports')
723         if not port_settings:
724             port_settings = kwargs.get('port_settings')
725         if port_settings:
726             for port_setting in port_settings:
727                 if isinstance(port_setting, dict):
728                     self.port_settings.append(PortSettings(**port_setting))
729                 elif isinstance(port_setting, PortSettings):
730                     self.port_settings.append(port_setting)
731
732         if kwargs.get('security_group_names'):
733             if isinstance(kwargs['security_group_names'], list):
734                 self.security_group_names = kwargs['security_group_names']
735             elif isinstance(kwargs['security_group_names'], set):
736                 self.security_group_names = kwargs['security_group_names']
737             elif isinstance(kwargs['security_group_names'], str):
738                 self.security_group_names = [kwargs['security_group_names']]
739             else:
740                 raise Exception(
741                     'Invalid data type for security_group_names attribute')
742         else:
743             self.security_group_names = set()
744
745         self.floating_ip_settings = list()
746         floating_ip_settings = kwargs.get('floating_ips')
747         if not floating_ip_settings:
748             floating_ip_settings = kwargs.get('floating_ip_settings')
749         if floating_ip_settings:
750             for floating_ip_config in floating_ip_settings:
751                 if isinstance(floating_ip_config, FloatingIpSettings):
752                     self.floating_ip_settings.append(floating_ip_config)
753                 else:
754                     self.floating_ip_settings.append(FloatingIpSettings(
755                         **floating_ip_config['floating_ip']))
756
757         if kwargs.get('vm_boot_timeout'):
758             self.vm_boot_timeout = kwargs['vm_boot_timeout']
759         else:
760             self.vm_boot_timeout = 900
761
762         if kwargs.get('vm_delete_timeout'):
763             self.vm_delete_timeout = kwargs['vm_delete_timeout']
764         else:
765             self.vm_delete_timeout = 300
766
767         if kwargs.get('ssh_connect_timeout'):
768             self.ssh_connect_timeout = kwargs['ssh_connect_timeout']
769         else:
770             self.ssh_connect_timeout = 180
771
772         if kwargs.get('availability_zone'):
773             self.availability_zone = kwargs['availability_zone']
774         else:
775             self.availability_zone = None
776
777         if not self.name or not self.flavor:
778             raise Exception(
779                 'Instance configuration requires the attributes: name, flavor')
780
781         if len(self.port_settings) == 0:
782             raise Exception(
783                 'Instance configuration requires port settings (aka. NICS)')
784
785
786 class FloatingIpSettings:
787     """
788     Class responsible for holding configuration settings for a floating IP
789     """
790
791     def __init__(self, **kwargs):
792         """
793         Constructor
794         :param name: the name of the floating IP
795         :param port_name: the name of the router to the external network
796         :param router_name: the name of the router to the external network
797         :param subnet_name: the name of the subnet on which to attach the
798                             floating IP
799         :param provisioning: when true, this floating IP can be used for
800                              provisioning
801
802         TODO - provisioning flag is a hack as I have only observed a single
803         Floating IPs that actually works on an instance. Multiple floating IPs
804         placed on different subnets from the same port are especially
805         troublesome as you cannot predict which one will actually connect.
806         For now, it is recommended not to setup multiple floating IPs on an
807         instance unless absolutely necessary.
808         """
809         self.name = kwargs.get('name')
810         self.port_name = kwargs.get('port_name')
811         self.router_name = kwargs.get('router_name')
812         self.subnet_name = kwargs.get('subnet_name')
813         if kwargs.get('provisioning') is not None:
814             self.provisioning = kwargs['provisioning']
815         else:
816             self.provisioning = True
817
818         if not self.name or not self.port_name or not self.router_name:
819             raise Exception(
820                 'The attributes name, port_name and router_name are required '
821                 'for FloatingIPSettings')