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