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