Created custom exceptions for VM instance creation.
[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 VmInstanceCreationError(
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 VmInstanceCreationError(
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 VmInstanceCreationError(
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 VmInstanceCreationError(
182                     'Unable to add floating IP to port, cannot locate router '
183                     'with an external 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 VmInstanceCreationError(
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 VmInstanceCreationError(
322             'Timeout while attempting add floating IP to instance')
323
324     def get_os_creds(self):
325         """
326         Returns the OpenStack credentials used to create these objects
327         :return: the credentials
328         """
329         return self.__os_creds
330
331     def get_vm_inst(self):
332         """
333         Returns the latest version of this server object from OpenStack
334         :return: Server object
335         """
336         return self.__vm
337
338     def get_console_output(self):
339         """
340         Returns the vm console object for parsing logs
341         :return: the console output object
342         """
343         return nova_utils.get_server_console_output(self.__nova, self.__vm)
344
345     def get_port_ip(self, port_name, subnet_name=None):
346         """
347         Returns the first IP for the port corresponding with the port_name
348         parameter when subnet_name is None else returns the IP address that
349         corresponds to the subnet_name parameter
350         :param port_name: the name of the port from which to return the IP
351         :param subnet_name: the name of the subnet attached to this IP
352         :return: the IP or None if not found
353         """
354         port = self.get_port_by_name(port_name)
355         if port:
356             if subnet_name:
357                 subnet = neutron_utils.get_subnet_by_name(self.__neutron,
358                                                           subnet_name)
359                 if not subnet:
360                     logger.warning('Cannot retrieve port IP as subnet could '
361                                    'not be located with name - %s',
362                                    subnet_name)
363                     return None
364                 for fixed_ip in port.ips:
365                     if fixed_ip['subnet_id'] == subnet.id:
366                         return fixed_ip['ip_address']
367             else:
368                 if port.ips and len(port.ips) > 0:
369                     return port.ips[0]['ip_address']
370         return None
371
372     def get_port_mac(self, port_name):
373         """
374         Returns the first IP for the port corresponding with the port_name
375         parameter
376         TODO - Add in the subnet as an additional parameter as a port may have
377         multiple fixed_ips
378         :param port_name: the name of the port from which to return the IP
379         :return: the IP or None if not found
380         """
381         port = self.get_port_by_name(port_name)
382         if port:
383             return port.mac_address
384         return None
385
386     def get_port_by_name(self, port_name):
387         """
388         Retrieves the OpenStack port object by its given name
389         :param port_name: the name of the port
390         :return: the OpenStack port object or None if not exists
391         """
392         for key, port in self.__ports:
393             if key == port_name:
394                 return port
395         logger.warning('Cannot find port with name - ' + port_name)
396         return None
397
398     def get_vm_info(self):
399         """
400         Returns a dictionary of a VMs info as returned by OpenStack
401         :return: a dict()
402         """
403         return nova_utils.get_server_info(self.__nova, self.__vm)
404
405     def config_nics(self):
406         """
407         Responsible for configuring NICs on RPM systems where the instance has
408         more than one configured port
409         :return: the value returned by ansible_utils.apply_ansible_playbook()
410         """
411         if len(self.__ports) > 1 and len(self.__floating_ips) > 0:
412             if self.vm_active(block=True) and self.vm_ssh_active(block=True):
413                 for key, port in self.__ports:
414                     port_index = self.__ports.index((key, port))
415                     if port_index > 0:
416                         nic_name = 'eth' + repr(port_index)
417                         retval = self.__config_nic(
418                             nic_name, port,
419                             self.__get_first_provisioning_floating_ip().ip)
420                         logger.info('Configured NIC - %s on VM - %s',
421                                     nic_name, self.instance_settings.name)
422                         return retval
423
424     def __get_first_provisioning_floating_ip(self):
425         """
426         Returns the first floating IP tagged with the Floating IP name if
427         exists else the first one found
428         :return:
429         """
430         for floating_ip_setting in self.instance_settings.floating_ip_settings:
431             if floating_ip_setting.provisioning:
432                 fip = self.__floating_ip_dict.get(floating_ip_setting.name)
433                 if fip:
434                     return fip
435                 elif len(self.__floating_ips) > 0:
436                     return self.__floating_ips[0]
437
438     def __config_nic(self, nic_name, port, ip):
439         """
440         Although ports/NICs can contain multiple IPs, this code currently only
441         supports the first.
442
443         :param nic_name: Name of the interface
444         :param port: The port information containing the expected IP values.
445         :param ip: The IP on which to apply the playbook.
446         :return: the return value from ansible
447         """
448         port_ip = port.ips[0]['ip_address']
449         variables = {
450             'floating_ip': ip,
451             'nic_name': nic_name,
452             'nic_ip': port_ip
453         }
454
455         if self.image_settings.nic_config_pb_loc and self.keypair_settings:
456             return self.apply_ansible_playbook(
457                 self.image_settings.nic_config_pb_loc, variables)
458         else:
459             logger.warning(
460                 'VM %s cannot self configure NICs eth1++. No playbook or '
461                 'keypairs found.', self.instance_settings.name)
462
463     def apply_ansible_playbook(self, pb_file_loc, variables=None,
464                                fip_name=None):
465         """
466         Applies a playbook to a VM
467         :param pb_file_loc: the file location of the playbook to be applied
468         :param variables: a dict() of substitution values required by the
469                           playbook
470         :param fip_name: the name of the floating IP to use for applying the
471                          playbook (default - will take the first)
472         :return: the return value from ansible
473         """
474         return ansible_utils.apply_playbook(
475             pb_file_loc, [self.get_floating_ip(fip_name=fip_name).ip],
476             self.get_image_user(), self.keypair_settings.private_filepath,
477             variables, self.__os_creds.proxy_settings)
478
479     def get_image_user(self):
480         """
481         Returns the instance sudo_user if it has been configured in the
482         instance_settings else it returns the image_settings.image_user value
483         """
484         if self.instance_settings.sudo_user:
485             return self.instance_settings.sudo_user
486         else:
487             return self.image_settings.image_user
488
489     def vm_deleted(self, block=False, poll_interval=POLL_INTERVAL):
490         """
491         Returns true when the VM status returns the value of
492         expected_status_code or instance retrieval throws a NotFound exception.
493         :param block: When true, thread will block until active or timeout
494                       value in seconds has been exceeded (False)
495         :param poll_interval: The polling interval in seconds
496         :return: T/F
497         """
498         try:
499             return self.__vm_status_check(
500                 STATUS_DELETED, block,
501                 self.instance_settings.vm_delete_timeout, poll_interval)
502         except NotFound as e:
503             logger.debug(
504                 "Instance not found when querying status for %s with message "
505                 "%s", STATUS_DELETED, e)
506             return True
507
508     def vm_active(self, block=False, poll_interval=POLL_INTERVAL):
509         """
510         Returns true when the VM status returns the value of
511         expected_status_code
512         :param block: When true, thread will block until active or timeout
513                       value in seconds has been exceeded (False)
514         :param poll_interval: The polling interval in seconds
515         :return: T/F
516         """
517         return self.__vm_status_check(STATUS_ACTIVE, block,
518                                       self.instance_settings.vm_boot_timeout,
519                                       poll_interval)
520
521     def __vm_status_check(self, expected_status_code, block, timeout,
522                           poll_interval):
523         """
524         Returns true when the VM status returns the value of
525         expected_status_code
526         :param expected_status_code: instance status evaluated with this
527                                      string value
528         :param block: When true, thread will block until active or timeout
529                       value in seconds has been exceeded (False)
530         :param timeout: The timeout value
531         :param poll_interval: The polling interval in seconds
532         :return: T/F
533         """
534         # sleep and wait for VM status change
535         if block:
536             start = time.time()
537         else:
538             return self.__status(expected_status_code)
539
540         while timeout > time.time() - start:
541             status = self.__status(expected_status_code)
542             if status:
543                 logger.info('VM is - ' + expected_status_code)
544                 return True
545
546             logger.debug('Retry querying VM status in ' + str(
547                 poll_interval) + ' seconds')
548             time.sleep(poll_interval)
549             logger.debug('VM status query timeout in ' + str(
550                 timeout - (time.time() - start)))
551
552         logger.error(
553             'Timeout checking for VM status for ' + expected_status_code)
554         return False
555
556     def __status(self, expected_status_code):
557         """
558         Returns True when active else False
559         :param expected_status_code: instance status evaluated with this string
560                                      value
561         :return: T/F
562         """
563         if not self.__vm:
564             return False
565
566         status = nova_utils.get_server_status(self.__nova, self.__vm)
567         if not status:
568             logger.warning('Cannot find instance with id - ' + self.__vm.id)
569             return False
570
571         if status == 'ERROR':
572             raise VmInstanceCreationError(
573                 'Instance had an error during deployment')
574         logger.debug(
575             'Instance status [%s] is - %s', self.instance_settings.name,
576             status)
577         return status == expected_status_code
578
579     def vm_ssh_active(self, block=False, poll_interval=POLL_INTERVAL):
580         """
581         Returns true when the VM can be accessed via SSH
582         :param block: When true, thread will block until active or timeout
583                       value in seconds has been exceeded (False)
584         :param poll_interval: The polling interval
585         :return: T/F
586         """
587         # sleep and wait for VM status change
588         logger.info('Checking if VM is active')
589
590         timeout = self.instance_settings.ssh_connect_timeout
591
592         if self.vm_active(block=True):
593             if block:
594                 start = time.time()
595             else:
596                 start = time.time() - timeout
597
598             while timeout > time.time() - start:
599                 status = self.__ssh_active()
600                 if status:
601                     logger.info('SSH is active for VM instance')
602                     return True
603
604                 logger.debug('Retry SSH connection in ' + str(
605                     poll_interval) + ' seconds')
606                 time.sleep(poll_interval)
607                 logger.debug('SSH connection timeout in ' + str(
608                     timeout - (time.time() - start)))
609
610         logger.error('Timeout attempting to connect with VM via SSH')
611         return False
612
613     def __ssh_active(self):
614         """
615         Returns True when can create a SSH session else False
616         :return: T/F
617         """
618         if len(self.__floating_ips) > 0:
619             ssh = self.ssh_client()
620             if ssh:
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 and len(self.__floating_ips) > 0:
635             return self.__floating_ips[0]
636         return None
637
638     def ssh_client(self, fip_name=None):
639         """
640         Returns an SSH client using the name or the first known floating IP if
641         exists, else None
642         :param fip_name: the name of the floating IP to return
643         :return: the SSH client or None
644         """
645         fip = self.get_floating_ip(fip_name)
646         if fip:
647             return ansible_utils.ssh_client(
648                 self.__floating_ips[0].ip, 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     """