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