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