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