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