Added support for offline testing
[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         if not self.__vm:
500             return False
501
502         instance = self.__nova.servers.get(self.__vm.id)
503         if not instance:
504             logger.warning('Cannot find instance with id - ' + self.__vm.id)
505             return False
506
507         if instance.status == 'ERROR':
508             raise Exception('Instance had an error during deployment')
509         logger.debug('Instance status [' + self.instance_settings.name + '] is - ' + instance.status)
510         return instance.status == expected_status_code
511
512     def vm_ssh_active(self, block=False, poll_interval=POLL_INTERVAL):
513         """
514         Returns true when the VM can be accessed via SSH
515         :param block: When true, thread will block until active or timeout value in seconds has been exceeded (False)
516         :param poll_interval: The polling interval
517         :return: T/F
518         """
519         # sleep and wait for VM status change
520         logger.info('Checking if VM is active')
521
522         timeout = self.instance_settings.ssh_connect_timeout
523
524         if self.vm_active(block=True):
525             if block:
526                 start = time.time()
527             else:
528                 start = time.time() - timeout
529
530             while timeout > time.time() - start:
531                 status = self.__ssh_active()
532                 if status:
533                     logger.info('SSH is active for VM instance')
534                     return True
535
536                 logger.debug('Retry SSH connection in ' + str(poll_interval) + ' seconds')
537                 time.sleep(poll_interval)
538                 logger.debug('SSH connection timeout in ' + str(timeout - (time.time() - start)))
539
540         logger.error('Timeout attempting to connect with VM via SSH')
541         return False
542
543     def __ssh_active(self):
544         """
545         Returns True when can create a SSH session else False
546         :return: T/F
547         """
548         if len(self.__floating_ips) > 0:
549             ssh = self.ssh_client()
550             if ssh:
551                 return True
552         return False
553
554     def get_floating_ip(self, fip_name=None):
555         """
556         Returns the floating IP object byt name if found, else the first known, else None
557         :param fip_name: the name of the floating IP to return
558         :return: the SSH client or None
559         """
560         fip = None
561         if fip_name and self.__floating_ip_dict.get(fip_name):
562             return self.__floating_ip_dict.get(fip_name)
563         if not fip and len(self.__floating_ips) > 0:
564             return self.__floating_ips[0]
565         return None
566
567     def ssh_client(self, fip_name=None):
568         """
569         Returns an SSH client using the name or the first known floating IP if exists, else None
570         :param fip_name: the name of the floating IP to return
571         :return: the SSH client or None
572         """
573         fip = self.get_floating_ip(fip_name)
574         if fip:
575             return ansible_utils.ssh_client(self.__floating_ips[0].ip, self.get_image_user(),
576                                             self.keypair_settings.private_filepath,
577                                             proxy_settings=self.__os_creds.proxy_settings)
578         else:
579             logger.warning('Cannot return an SSH client. No Floating IP configured')
580
581     def add_security_group(self, security_group):
582         """
583         Adds a security group to this VM. Call will block until VM is active.
584         :param security_group: the OpenStack security group object
585         :return True if successful else False
586         """
587         self.vm_active(block=True)
588
589         if not security_group:
590             logger.warning('Security group object is None, cannot add')
591             return False
592
593         try:
594             nova_utils.add_security_group(self.__nova, self.get_vm_inst(), security_group['security_group']['name'])
595             return True
596         except NotFound as e:
597             logger.warning('Security group not added - ' + str(e))
598             return False
599
600     def remove_security_group(self, security_group):
601         """
602         Removes a security group to this VM. Call will block until VM is active.
603         :param security_group: the OpenStack security group object
604         :return True if successful else False
605         """
606         self.vm_active(block=True)
607
608         if not security_group:
609             logger.warning('Security group object is None, cannot remove')
610             return False
611
612         try:
613             nova_utils.remove_security_group(self.__nova, self.get_vm_inst(), security_group)
614             return True
615         except NotFound as e:
616             logger.warning('Security group not removed - ' + str(e))
617             return False
618
619
620 class VmInstanceSettings:
621     """
622     Class responsible for holding configuration setting for a VM Instance
623     """
624     def __init__(self, config=None, name=None, flavor=None, port_settings=list(), security_group_names=set(),
625                  floating_ip_settings=list(), sudo_user=None, vm_boot_timeout=900,
626                  vm_delete_timeout=300, ssh_connect_timeout=180, availability_zone=None, userdata=None):
627         """
628         Constructor
629         :param config: dict() object containing the configuration settings using the attribute names below as each
630                        member's the key and overrides any of the other parameters.
631         :param name: the name of the VM
632         :param flavor: the VM's flavor
633         :param port_settings: the port configuration settings (required)
634         :param security_group_names: a set of names of the security groups to add to the VM
635         :param floating_ip_settings: the floating IP configuration settings
636         :param sudo_user: the sudo user of the VM that will override the instance_settings.image_user when trying to
637                           connect to the VM
638         :param vm_boot_timeout: the amount of time a thread will sleep waiting for an instance to boot
639         :param vm_delete_timeout: the amount of time a thread will sleep waiting for an instance to be deleted
640         :param ssh_connect_timeout: the amount of time a thread will sleep waiting obtaining an SSH connection to a VM
641         :param availability_zone: the name of the compute server on which to deploy the VM (optional)
642         :param userdata: the cloud-init script to run after the VM has been started
643         """
644         if config:
645             self.name = config.get('name')
646             self.flavor = config.get('flavor')
647             self.sudo_user = config.get('sudo_user')
648             self.userdata = config.get('userdata')
649
650             self.port_settings = list()
651             if config.get('ports'):
652                 for port_config in config['ports']:
653                     if isinstance(port_config, PortSettings):
654                         self.port_settings.append(port_config)
655                     else:
656                         self.port_settings.append(PortSettings(config=port_config['port']))
657
658             if config.get('security_group_names'):
659                 if isinstance(config['security_group_names'], list):
660                     self.security_group_names = set(config['security_group_names'])
661                 elif isinstance(config['security_group_names'], set):
662                     self.security_group_names = config['security_group_names']
663                 elif isinstance(config['security_group_names'], str):
664                     self.security_group_names = [config['security_group_names']]
665                 else:
666                     raise Exception('Invalid data type for security_group_names attribute')
667             else:
668                 self.security_group_names = set()
669
670             self.floating_ip_settings = list()
671             if config.get('floating_ips'):
672                 for floating_ip_config in config['floating_ips']:
673                     if isinstance(floating_ip_config, FloatingIpSettings):
674                         self.floating_ip_settings.append(floating_ip_config)
675                     else:
676                         self.floating_ip_settings.append(FloatingIpSettings(config=floating_ip_config['floating_ip']))
677
678             if config.get('vm_boot_timeout'):
679                 self.vm_boot_timeout = config['vm_boot_timeout']
680             else:
681                 self.vm_boot_timeout = vm_boot_timeout
682
683             if config.get('vm_delete_timeout'):
684                 self.vm_delete_timeout = config['vm_delete_timeout']
685             else:
686                 self.vm_delete_timeout = vm_delete_timeout
687
688             if config.get('ssh_connect_timeout'):
689                 self.ssh_connect_timeout = config['ssh_connect_timeout']
690             else:
691                 self.ssh_connect_timeout = ssh_connect_timeout
692
693             if config.get('availability_zone'):
694                 self.availability_zone = config['availability_zone']
695             else:
696                 self.availability_zone = None
697         else:
698             self.name = name
699             self.flavor = flavor
700             self.port_settings = port_settings
701             self.security_group_names = security_group_names
702             self.floating_ip_settings = floating_ip_settings
703             self.sudo_user = sudo_user
704             self.vm_boot_timeout = vm_boot_timeout
705             self.vm_delete_timeout = vm_delete_timeout
706             self.ssh_connect_timeout = ssh_connect_timeout
707             self.availability_zone = availability_zone
708             self.userdata = userdata
709
710         if not self.name or not self.flavor:
711             raise Exception('Instance configuration requires the attributes: name, flavor')
712
713         if len(self.port_settings) == 0:
714             raise Exception('Instance configuration requires port settings (aka. NICS)')
715
716
717 class FloatingIpSettings:
718     """
719     Class responsible for holding configuration settings for a floating IP
720     """
721     def __init__(self, config=None, name=None, port_name=None, router_name=None, subnet_name=None, provisioning=True):
722         """
723         Constructor
724         :param config: dict() object containing the configuration settings using the attribute names below as each
725                        member's the key and overrides any of the other parameters.
726         :param name: the name of the floating IP
727         :param port_name: the name of the router to the external network
728         :param router_name: the name of the router to the external network
729         :param subnet_name: the name of the subnet on which to attach the floating IP
730         :param provisioning: when true, this floating IP can be used for provisioning
731
732         TODO - provisioning flag is a hack as I have only observed a single Floating IPs that actually works on
733         an instance. Multiple floating IPs placed on different subnets from the same port are especially troublesome
734         as you cannot predict which one will actually connect. For now, it is recommended not to setup multiple
735         floating IPs on an instance unless absolutely necessary.
736         """
737         if config:
738             self.name = config.get('name')
739             self.port_name = config.get('port_name')
740             self.router_name = config.get('router_name')
741             self.subnet_name = config.get('subnet_name')
742             if config.get('provisioning') is not None:
743                 self.provisioning = config['provisioning']
744             else:
745                 self.provisioning = provisioning
746         else:
747             self.name = name
748             self.port_name = port_name
749             self.router_name = router_name
750             self.subnet_name = subnet_name
751             self.provisioning = provisioning
752
753         if not self.name or not self.port_name or not self.router_name:
754             raise Exception('The attributes name, port_name and router_name are required for FloatingIPSettings')