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