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