Implemented the ability to attach volumes to VM instances.
[snaps.git] / snaps / openstack / create_instance.py
1 # Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
2 #                    and others.  All rights reserved.
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at:
7 #
8 #     http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 import logging
16 import time
17
18 from neutronclient.common.exceptions import PortNotFoundClient
19 from novaclient.exceptions import NotFound
20
21 from snaps.openstack.create_network import PortSettings
22 from snaps.openstack.openstack_creator import OpenStackComputeObject
23 from snaps.openstack.utils import glance_utils, cinder_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             port = port_dict.get(floating_ip_setting.port_name)
192
193             if not port:
194                 raise VmInstanceCreationError(
195                     'Cannot find port object with name - ' +
196                     floating_ip_setting.port_name)
197
198             # Setup Floating IP only if there is a router with an external
199             # gateway
200             ext_gateway = self.__ext_gateway_by_router(
201                 floating_ip_setting.router_name)
202             if ext_gateway:
203                 subnet = neutron_utils.get_subnet(
204                     self.__neutron,
205                     subnet_name=floating_ip_setting.subnet_name)
206                 floating_ip = neutron_utils.create_floating_ip(
207                     self.__neutron, ext_gateway)
208                 self.__floating_ip_dict[floating_ip_setting.name] = floating_ip
209
210                 logger.info(
211                     'Created floating IP %s via router - %s', floating_ip.ip,
212                     floating_ip_setting.router_name)
213                 self.__add_floating_ip(floating_ip, port, subnet)
214             else:
215                 raise VmInstanceCreationError(
216                     'Unable to add floating IP to port, cannot locate router '
217                     'with an external gateway ')
218
219     def __ext_gateway_by_router(self, router_name):
220         """
221         Returns network name for the external network attached to a router or
222         None if not found
223         :param router_name: The name of the router to lookup
224         :return: the external network name or None
225         """
226         router = neutron_utils.get_router(
227             self.__neutron, router_name=router_name)
228         if router and router.external_gateway_info:
229             network = neutron_utils.get_network_by_id(
230                 self.__neutron,
231                 router.external_gateway_info['network_id'])
232             if network:
233                 return network.name
234         return None
235
236     def clean(self):
237         """
238         Destroys the VM instance
239         """
240
241         # Cleanup floating IPs
242         for name, floating_ip in self.__floating_ip_dict.items():
243             try:
244                 logger.info('Deleting Floating IP - ' + floating_ip.ip)
245                 neutron_utils.delete_floating_ip(self.__neutron, floating_ip)
246             except Exception as e:
247                 logger.error('Error deleting Floating IP - ' + str(e))
248         self.__floating_ip_dict = dict()
249
250         # Detach Volume
251         for volume_rec in self.__vm.volume_ids:
252             cinder = cinder_utils.cinder_client(self._os_creds)
253             volume = cinder_utils.get_volume_by_id(cinder, volume_rec['id'])
254             if volume:
255                 try:
256                     vm = nova_utils.detach_volume(
257                         self._nova, self.__vm, volume, 30)
258                     if vm:
259                         self.__vm = vm
260                     else:
261                         logger.warn(
262                             'Timeout waiting to detach volume %s', volume.name)
263                 except Exception as e:
264                     logger.error('Unexpected error detaching volume %s '
265                                  'with error %s', volume.name, e)
266             else:
267                 logger.warn('Unable to detach volume with ID - [%s]',
268                             volume_rec['id'])
269
270         # Cleanup ports
271         for name, port in self.__ports:
272             logger.info('Deleting Port with ID - %s ', port.id)
273             try:
274                 neutron_utils.delete_port(self.__neutron, port)
275             except PortNotFoundClient as e:
276                 logger.warning('Unexpected error deleting port - %s', e)
277                 pass
278         self.__ports = list()
279
280         # Cleanup VM
281         if self.__vm:
282             try:
283                 logger.info(
284                     'Deleting VM instance - ' + self.instance_settings.name)
285                 nova_utils.delete_vm_instance(self._nova, self.__vm)
286             except Exception as e:
287                 logger.error('Error deleting VM - %s', e)
288
289             # Block until instance cannot be found or returns the status of
290             # DELETED
291             logger.info('Checking deletion status')
292
293             try:
294                 if self.vm_deleted(block=True):
295                     logger.info(
296                         'VM has been properly deleted VM with name - %s',
297                         self.instance_settings.name)
298                     self.__vm = None
299                 else:
300                     logger.error(
301                         'VM not deleted within the timeout period of %s '
302                         'seconds', self.instance_settings.vm_delete_timeout)
303             except Exception as e:
304                 logger.error(
305                     'Unexpected error while checking VM instance status - %s',
306                     e)
307
308     def __query_ports(self, port_settings):
309         """
310         Returns the previously configured ports or an empty list if none
311         exist
312         :param port_settings: A list of PortSetting objects
313         :return: a list of OpenStack port tuples where the first member is the
314                  port name and the second is the port object
315         """
316         ports = list()
317
318         for port_setting in port_settings:
319             port = neutron_utils.get_port(
320                 self.__neutron, port_settings=port_setting)
321             if port:
322                 ports.append((port_setting.name, port))
323
324         return ports
325
326     def __create_ports(self, port_settings):
327         """
328         Returns the previously configured ports or creates them if they do not
329         exist
330         :param port_settings: A list of PortSetting objects
331         :return: a list of OpenStack port tuples where the first member is the
332                  port name and the second is the port object
333         """
334         ports = list()
335
336         for port_setting in port_settings:
337             port = neutron_utils.get_port(
338                 self.__neutron, port_settings=port_setting)
339             if not port:
340                 port = neutron_utils.create_port(
341                     self.__neutron, self._os_creds, port_setting)
342                 if port:
343                     ports.append((port_setting.name, port))
344
345         return ports
346
347     def __add_floating_ip(self, floating_ip, port, subnet, timeout=30,
348                           poll_interval=POLL_INTERVAL):
349         """
350         Returns True when active else False
351         TODO - Make timeout and poll_interval configurable...
352         """
353         ip = None
354
355         if subnet:
356             # Take IP of subnet if there is one configured on which to place
357             # the floating IP
358             for fixed_ip in port.ips:
359                 if fixed_ip['subnet_id'] == subnet.id:
360                     ip = fixed_ip['ip_address']
361                     break
362         else:
363             # Simply take the first
364             ip = port.ips[0]['ip_address']
365
366         if ip:
367             count = timeout / poll_interval
368             while count > 0:
369                 logger.debug('Attempting to add floating IP to instance')
370                 try:
371                     nova_utils.add_floating_ip_to_server(
372                         self._nova, self.__vm, floating_ip, ip)
373                     logger.info(
374                         'Added floating IP %s to port IP %s on instance %s',
375                         floating_ip.ip, ip, self.instance_settings.name)
376                     return
377                 except Exception as e:
378                     logger.debug(
379                         'Retry adding floating IP to instance. Last attempt '
380                         'failed with - %s', e)
381                     time.sleep(poll_interval)
382                     count -= 1
383                     pass
384         else:
385             raise VmInstanceCreationError(
386                 'Unable find IP address on which to place the floating IP')
387
388         logger.error('Timeout attempting to add the floating IP to instance.')
389         raise VmInstanceCreationError(
390             'Timeout while attempting add floating IP to instance')
391
392     def get_os_creds(self):
393         """
394         Returns the OpenStack credentials used to create these objects
395         :return: the credentials
396         """
397         return self._os_creds
398
399     def get_vm_inst(self):
400         """
401         Returns the latest version of this server object from OpenStack
402         :return: Server object
403         """
404         return nova_utils.get_server_object_by_id(self._nova, self.__vm.id)
405
406     def get_console_output(self):
407         """
408         Returns the vm console object for parsing logs
409         :return: the console output object
410         """
411         return nova_utils.get_server_console_output(self._nova, self.__vm)
412
413     def get_port_ip(self, port_name, subnet_name=None):
414         """
415         Returns the first IP for the port corresponding with the port_name
416         parameter when subnet_name is None else returns the IP address that
417         corresponds to the subnet_name parameter
418         :param port_name: the name of the port from which to return the IP
419         :param subnet_name: the name of the subnet attached to this IP
420         :return: the IP or None if not found
421         """
422         port = self.get_port_by_name(port_name)
423         if port:
424             if subnet_name:
425                 subnet = neutron_utils.get_subnet(
426                     self.__neutron, subnet_name=subnet_name)
427                 if not subnet:
428                     logger.warning('Cannot retrieve port IP as subnet could '
429                                    'not be located with name - %s',
430                                    subnet_name)
431                     return None
432                 for fixed_ip in port.ips:
433                     if fixed_ip['subnet_id'] == subnet.id:
434                         return fixed_ip['ip_address']
435             else:
436                 if port.ips and len(port.ips) > 0:
437                     return port.ips[0]['ip_address']
438         return None
439
440     def get_port_mac(self, port_name):
441         """
442         Returns the first IP for the port corresponding with the port_name
443         parameter
444         TODO - Add in the subnet as an additional parameter as a port may have
445         multiple fixed_ips
446         :param port_name: the name of the port from which to return the IP
447         :return: the IP or None if not found
448         """
449         port = self.get_port_by_name(port_name)
450         if port:
451             return port.mac_address
452         return None
453
454     def get_port_by_name(self, port_name):
455         """
456         Retrieves the OpenStack port object by its given name
457         :param port_name: the name of the port
458         :return: the OpenStack port object or None if not exists
459         """
460         for key, port in self.__ports:
461             if key == port_name:
462                 return port
463         logger.warning('Cannot find port with name - ' + port_name)
464         return None
465
466     def get_vm_info(self):
467         """
468         Returns a dictionary of a VMs info as returned by OpenStack
469         :return: a dict()
470         """
471         return nova_utils.get_server_info(self._nova, self.__vm)
472
473     def config_nics(self):
474         """
475         Responsible for configuring NICs on RPM systems where the instance has
476         more than one configured port
477         :return: the value returned by ansible_utils.apply_ansible_playbook()
478         """
479         if len(self.__ports) > 1 and len(self.__floating_ip_dict) > 0:
480             if self.vm_active(block=True) and self.vm_ssh_active(block=True):
481                 for key, port in self.__ports:
482                     port_index = self.__ports.index((key, port))
483                     if port_index > 0:
484                         nic_name = 'eth' + repr(port_index)
485                         retval = self.__config_nic(
486                             nic_name, port,
487                             self.__get_first_provisioning_floating_ip().ip)
488                         logger.info('Configured NIC - %s on VM - %s',
489                                     nic_name, self.instance_settings.name)
490                         return retval
491
492     def __get_first_provisioning_floating_ip(self):
493         """
494         Returns the first floating IP tagged with the Floating IP name if
495         exists else the first one found
496         :return:
497         """
498         for floating_ip_setting in self.instance_settings.floating_ip_settings:
499             if floating_ip_setting.provisioning:
500                 fip = self.__floating_ip_dict.get(floating_ip_setting.name)
501                 if fip:
502                     return fip
503                 elif len(self.__floating_ip_dict) > 0:
504                     for key, fip in self.__floating_ip_dict.items():
505                         return fip
506
507     def __config_nic(self, nic_name, port, ip):
508         """
509         Although ports/NICs can contain multiple IPs, this code currently only
510         supports the first.
511
512         :param nic_name: Name of the interface
513         :param port: The port information containing the expected IP values.
514         :param ip: The IP on which to apply the playbook.
515         :return: the return value from ansible
516         """
517         port_ip = port.ips[0]['ip_address']
518         variables = {
519             'floating_ip': ip,
520             'nic_name': nic_name,
521             'nic_ip': port_ip
522         }
523
524         if self.image_settings.nic_config_pb_loc and self.keypair_settings:
525             return self.apply_ansible_playbook(
526                 self.image_settings.nic_config_pb_loc, variables)
527         else:
528             logger.warning(
529                 'VM %s cannot self configure NICs eth1++. No playbook or '
530                 'keypairs found.', self.instance_settings.name)
531
532     def apply_ansible_playbook(self, pb_file_loc, variables=None,
533                                fip_name=None):
534         """
535         Applies a playbook to a VM
536         :param pb_file_loc: the file location of the playbook to be applied
537         :param variables: a dict() of substitution values required by the
538                           playbook
539         :param fip_name: the name of the floating IP to use for applying the
540                          playbook (default - will take the first)
541         :return: the return value from ansible
542         """
543         return ansible_utils.apply_playbook(
544             pb_file_loc, [self.get_floating_ip(fip_name=fip_name).ip],
545             self.get_image_user(), self.keypair_settings.private_filepath,
546             variables, self._os_creds.proxy_settings)
547
548     def get_image_user(self):
549         """
550         Returns the instance sudo_user if it has been configured in the
551         instance_settings else it returns the image_settings.image_user value
552         """
553         if self.instance_settings.sudo_user:
554             return self.instance_settings.sudo_user
555         else:
556             return self.image_settings.image_user
557
558     def vm_deleted(self, block=False, poll_interval=POLL_INTERVAL):
559         """
560         Returns true when the VM status returns the value of
561         expected_status_code or instance retrieval throws a NotFound exception.
562         :param block: When true, thread will block until active or timeout
563                       value in seconds has been exceeded (False)
564         :param poll_interval: The polling interval in seconds
565         :return: T/F
566         """
567         try:
568             return self.__vm_status_check(
569                 STATUS_DELETED, block,
570                 self.instance_settings.vm_delete_timeout, poll_interval)
571         except NotFound as e:
572             logger.debug(
573                 "Instance not found when querying status for %s with message "
574                 "%s", STATUS_DELETED, e)
575             return True
576
577     def vm_active(self, block=False, poll_interval=POLL_INTERVAL):
578         """
579         Returns true when the VM status returns the value of the constant
580         STATUS_ACTIVE
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         return self.__vm_status_check(STATUS_ACTIVE, block,
587                                       self.instance_settings.vm_boot_timeout,
588                                       poll_interval)
589
590     def __vm_status_check(self, expected_status_code, block, timeout,
591                           poll_interval):
592         """
593         Returns true when the VM status returns the value of
594         expected_status_code
595         :param expected_status_code: instance status evaluated with this
596                                      string value
597         :param block: When true, thread will block until active or timeout
598                       value in seconds has been exceeded (False)
599         :param timeout: The timeout value
600         :param poll_interval: The polling interval in seconds
601         :return: T/F
602         """
603         # sleep and wait for VM status change
604         if block:
605             start = time.time()
606         else:
607             return self.__status(expected_status_code)
608
609         while timeout > time.time() - start:
610             status = self.__status(expected_status_code)
611             if status:
612                 logger.info('VM is - ' + expected_status_code)
613                 return True
614
615             logger.debug('Retry querying VM status in ' + str(
616                 poll_interval) + ' seconds')
617             time.sleep(poll_interval)
618             logger.debug('VM status query timeout in ' + str(
619                 timeout - (time.time() - start)))
620
621         logger.error(
622             'Timeout checking for VM status for ' + expected_status_code)
623         return False
624
625     def __status(self, expected_status_code):
626         """
627         Returns True when active else False
628         :param expected_status_code: instance status evaluated with this string
629                                      value
630         :return: T/F
631         """
632         if not self.__vm:
633             if expected_status_code == STATUS_DELETED:
634                 return True
635             else:
636                 return False
637
638         status = nova_utils.get_server_status(self._nova, self.__vm)
639         if not status:
640             logger.warning('Cannot find instance with id - ' + self.__vm.id)
641             return False
642
643         if status == 'ERROR':
644             raise VmInstanceCreationError(
645                 'Instance had an error during deployment')
646         logger.debug(
647             'Instance status [%s] is - %s', self.instance_settings.name,
648             status)
649         return status == expected_status_code
650
651     def vm_ssh_active(self, block=False, poll_interval=POLL_INTERVAL):
652         """
653         Returns true when the VM can be accessed via SSH
654         :param block: When true, thread will block until active or timeout
655                       value in seconds has been exceeded (False)
656         :param poll_interval: The polling interval
657         :return: T/F
658         """
659         # sleep and wait for VM status change
660         logger.info('Checking if VM is active')
661
662         timeout = self.instance_settings.ssh_connect_timeout
663
664         if self.vm_active(block=True):
665             if block:
666                 start = time.time()
667             else:
668                 start = time.time() - timeout
669
670             while timeout > time.time() - start:
671                 status = self.__ssh_active()
672                 if status:
673                     logger.info('SSH is active for VM instance')
674                     return True
675
676                 logger.debug('Retry SSH connection in ' + str(
677                     poll_interval) + ' seconds')
678                 time.sleep(poll_interval)
679                 logger.debug('SSH connection timeout in ' + str(
680                     timeout - (time.time() - start)))
681
682         logger.error('Timeout attempting to connect with VM via SSH')
683         return False
684
685     def __ssh_active(self):
686         """
687         Returns True when can create a SSH session else False
688         :return: T/F
689         """
690         if len(self.__floating_ip_dict) > 0:
691             ssh = self.ssh_client()
692             if ssh:
693                 ssh.close()
694                 return True
695         return False
696
697     def get_floating_ip(self, fip_name=None):
698         """
699         Returns the floating IP object byt name if found, else the first known,
700         else None
701         :param fip_name: the name of the floating IP to return
702         :return: the SSH client or None
703         """
704         fip = None
705         if fip_name and self.__floating_ip_dict.get(fip_name):
706             return self.__floating_ip_dict.get(fip_name)
707         if not fip:
708             return self.__get_first_provisioning_floating_ip()
709
710     def ssh_client(self, fip_name=None):
711         """
712         Returns an SSH client using the name or the first known floating IP if
713         exists, else None
714         :param fip_name: the name of the floating IP to return
715         :return: the SSH client or None
716         """
717         fip = self.get_floating_ip(fip_name)
718         if fip:
719             return ansible_utils.ssh_client(
720                 self.__get_first_provisioning_floating_ip().ip,
721                 self.get_image_user(),
722                 self.keypair_settings.private_filepath,
723                 proxy_settings=self._os_creds.proxy_settings)
724         else:
725             logger.warning(
726                 'Cannot return an SSH client. No Floating IP configured')
727
728     def add_security_group(self, security_group):
729         """
730         Adds a security group to this VM. Call will block until VM is active.
731         :param security_group: the SNAPS SecurityGroup domain object
732         :return True if successful else False
733         """
734         self.vm_active(block=True)
735
736         if not security_group:
737             logger.warning('Security group object is None, cannot add')
738             return False
739
740         try:
741             nova_utils.add_security_group(self._nova, self.get_vm_inst(),
742                                           security_group.name)
743             return True
744         except NotFound as e:
745             logger.warning('Security group not added - ' + str(e))
746             return False
747
748     def remove_security_group(self, security_group):
749         """
750         Removes a security group to this VM. Call will block until VM is active
751         :param security_group: the OpenStack security group object
752         :return True if successful else False
753         """
754         self.vm_active(block=True)
755
756         if not security_group:
757             logger.warning('Security group object is None, cannot remove')
758             return False
759
760         try:
761             nova_utils.remove_security_group(self._nova, self.get_vm_inst(),
762                                              security_group)
763             return True
764         except NotFound as e:
765             logger.warning('Security group not removed - ' + str(e))
766             return False
767
768
769 class VmInstanceSettings:
770     """
771     Class responsible for holding configuration setting for a VM Instance
772     """
773
774     def __init__(self, **kwargs):
775         """
776         Constructor
777         :param name: the name of the VM
778         :param flavor: the VM's flavor name
779         :param port_settings: the port configuration settings (required)
780         :param security_group_names: a set of names of the security groups to
781                                      add to the VM
782         :param floating_ip_settings: the floating IP configuration settings
783         :param sudo_user: the sudo user of the VM that will override the
784                           instance_settings.image_user when trying to
785                           connect to the VM
786         :param vm_boot_timeout: the amount of time a thread will sleep waiting
787                                 for an instance to boot
788         :param vm_delete_timeout: the amount of time a thread will sleep
789                                   waiting for an instance to be deleted
790         :param ssh_connect_timeout: the amount of time a thread will sleep
791                                     waiting obtaining an SSH connection to a VM
792         :param availability_zone: the name of the compute server on which to
793                                   deploy the VM (optional)
794         :param volume_names: a list of the names of the volume to attach
795                              (optional)
796         :param userdata: the string contents of any optional cloud-init script
797                          to execute after the VM has been activated.
798                          This value may also contain a dict who's key value
799                          must contain the key 'cloud-init_file' which denotes
800                          the location of some file containing the cloud-init
801                          script
802         """
803         self.name = kwargs.get('name')
804         self.flavor = kwargs.get('flavor')
805         self.sudo_user = kwargs.get('sudo_user')
806         self.userdata = kwargs.get('userdata')
807
808         self.port_settings = list()
809         port_settings = kwargs.get('ports')
810         if not port_settings:
811             port_settings = kwargs.get('port_settings')
812         if port_settings:
813             for port_setting in port_settings:
814                 if isinstance(port_setting, dict):
815                     self.port_settings.append(PortSettings(**port_setting))
816                 elif isinstance(port_setting, PortSettings):
817                     self.port_settings.append(port_setting)
818
819         if kwargs.get('security_group_names'):
820             if isinstance(kwargs['security_group_names'], list):
821                 self.security_group_names = kwargs['security_group_names']
822             elif isinstance(kwargs['security_group_names'], set):
823                 self.security_group_names = kwargs['security_group_names']
824             elif isinstance(kwargs['security_group_names'], str):
825                 self.security_group_names = [kwargs['security_group_names']]
826             else:
827                 raise VmInstanceSettingsError(
828                     'Invalid data type for security_group_names attribute')
829         else:
830             self.security_group_names = set()
831
832         self.floating_ip_settings = list()
833         floating_ip_settings = kwargs.get('floating_ips')
834         if not floating_ip_settings:
835             floating_ip_settings = kwargs.get('floating_ip_settings')
836         if floating_ip_settings:
837             for floating_ip_config in floating_ip_settings:
838                 if isinstance(floating_ip_config, FloatingIpSettings):
839                     self.floating_ip_settings.append(floating_ip_config)
840                 else:
841                     self.floating_ip_settings.append(FloatingIpSettings(
842                         **floating_ip_config['floating_ip']))
843
844         self.vm_boot_timeout = kwargs.get('vm_boot_timeout', 900)
845         self.vm_delete_timeout = kwargs.get('vm_delete_timeout', 300)
846         self.ssh_connect_timeout = kwargs.get('ssh_connect_timeout', 180)
847         self.availability_zone = kwargs.get('availability_zone')
848         self.volume_names = kwargs.get('volume_names')
849
850         if self.volume_names and not isinstance(self.volume_names, list):
851             raise VmInstanceSettingsError('volume_names must be a list')
852
853         if not self.name or not self.flavor:
854             raise VmInstanceSettingsError(
855                 'Instance configuration requires the attributes: name, flavor')
856
857         if len(self.port_settings) == 0:
858             raise VmInstanceSettingsError(
859                 'Instance configuration requires port settings (aka. NICS)')
860
861
862 class FloatingIpSettings:
863     """
864     Class responsible for holding configuration settings for a floating IP
865     """
866
867     def __init__(self, **kwargs):
868         """
869         Constructor
870         :param name: the name of the floating IP
871         :param port_name: the name of the router to the external network
872         :param router_name: the name of the router to the external network
873         :param subnet_name: the name of the subnet on which to attach the
874                             floating IP
875         :param provisioning: when true, this floating IP can be used for
876                              provisioning
877
878         TODO - provisioning flag is a hack as I have only observed a single
879         Floating IPs that actually works on an instance. Multiple floating IPs
880         placed on different subnets from the same port are especially
881         troublesome as you cannot predict which one will actually connect.
882         For now, it is recommended not to setup multiple floating IPs on an
883         instance unless absolutely necessary.
884         """
885         self.name = kwargs.get('name')
886         self.port_name = kwargs.get('port_name')
887         self.port_id = kwargs.get('port_id')
888         self.router_name = kwargs.get('router_name')
889         self.subnet_name = kwargs.get('subnet_name')
890         if kwargs.get('provisioning') is not None:
891             self.provisioning = kwargs['provisioning']
892         else:
893             self.provisioning = True
894
895         # if not self.name or not self.port_name or not self.router_name:
896         if not self.name or not self.router_name:
897             raise FloatingIpSettingsError(
898                 'The attributes name, port_name and router_name are required')
899
900         if not self.port_name and not self.port_id:
901             raise FloatingIpSettingsError(
902                 'The attributes port_name or port_id are required')
903
904
905 class VmInstanceSettingsError(Exception):
906     """
907     Exception to be thrown when an VM instance settings are incorrect
908     """
909
910
911 class FloatingIpSettingsError(Exception):
912     """
913     Exception to be thrown when an VM instance settings are incorrect
914     """
915
916
917 class VmInstanceCreationError(Exception):
918     """
919     Exception to be thrown when an VM instance cannot be created
920     """