1 # Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
2 # and others. All rights reserved.
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:
8 # http://www.apache.org/licenses/LICENSE-2.0
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.
18 from neutronclient.common.exceptions import PortNotFoundClient
19 from novaclient.exceptions import NotFound, BadRequest
21 from snaps.config.vm_inst import VmInstanceConfig, FloatingIpConfig
22 from snaps.openstack.openstack_creator import OpenStackComputeObject
23 from snaps.openstack.utils import glance_utils, cinder_utils, settings_utils
24 from snaps.openstack.utils import neutron_utils
25 from snaps.openstack.utils import nova_utils
26 from snaps.openstack.utils.nova_utils import RebootType
27 from snaps.provisioning import ansible_utils
29 __author__ = 'spisarski'
31 logger = logging.getLogger('create_instance')
34 STATUS_ACTIVE = 'ACTIVE'
35 STATUS_DELETED = 'DELETED'
38 class OpenStackVmInstance(OpenStackComputeObject):
40 Class responsible for managing a VM instance in OpenStack
43 def __init__(self, os_creds, instance_settings, image_settings,
44 keypair_settings=None):
47 :param os_creds: The connection credentials to the OpenStack API
48 :param instance_settings: Contains the settings for this VM
49 :param image_settings: The OpenStack image object settings
50 :param keypair_settings: The keypair metadata (Optional)
53 super(self.__class__, self).__init__(os_creds)
57 self.instance_settings = instance_settings
58 self.image_settings = image_settings
59 self.keypair_settings = keypair_settings
61 self.__floating_ip_dict = dict()
63 # Instantiated in self.create()
66 # Note: this object does not change after the VM becomes active
71 Loads the existing VMInst, Port, FloatingIps
72 :return: VMInst domain object
74 super(self.__class__, self).initialize()
76 self.__neutron = neutron_utils.neutron_client(self._os_creds)
78 self.__ports = self.__query_ports(self.instance_settings.port_settings)
79 self.__lookup_existing_vm_by_name()
81 def create(self, block=False):
83 Creates a VM instance and associated objects unless they already exist
84 :param block: Thread will block until instance has either become
85 active, error, or timeout waiting.
86 Additionally, when True, floating IPs will not be applied
88 :return: VMInst domain object
92 if len(self.__ports) == 0:
93 self.__ports = self.__create_ports(
94 self.instance_settings.port_settings)
96 self.__create_vm(block)
100 def __lookup_existing_vm_by_name(self):
102 Populates the member variables 'self.vm' and 'self.floating_ips' if a
103 VM with the same name already exists
106 server = nova_utils.get_server(
107 self._nova, vm_inst_settings=self.instance_settings)
109 if server.name == self.instance_settings.name:
112 'Found existing machine with name - %s',
113 self.instance_settings.name)
115 fips = neutron_utils.get_floating_ips(self.__neutron,
117 for port_id, fip in fips:
118 settings = self.instance_settings.floating_ip_settings
119 for fip_setting in settings:
120 if port_id == fip_setting.port_id:
121 self.__floating_ip_dict[fip_setting.name] = fip
123 port = neutron_utils.get_port_by_id(
124 self.__neutron, port_id)
125 if port and port.name == fip_setting.port_name:
126 self.__floating_ip_dict[fip_setting.name] = fip
128 def __create_vm(self, block=False):
130 Responsible for creating the VM instance
131 :param block: Thread will block until instance has either become
132 active, error, or timeout waiting. Floating IPs will be
133 assigned after active when block=True
135 glance = glance_utils.glance_client(self._os_creds)
136 self.__vm = nova_utils.create_server(
137 self._nova, self.__neutron, glance, self.instance_settings,
138 self.image_settings, self.keypair_settings)
139 logger.info('Created instance with name - %s',
140 self.instance_settings.name)
143 if not self.vm_active(block=True):
144 raise VmInstanceCreationError(
145 'Fatal error, VM did not become ACTIVE within the alloted '
148 # Create server should do this but found it needed to occur here
149 for sec_grp_name in self.instance_settings.security_group_names:
150 if self.vm_active(block=True):
151 nova_utils.add_security_group(self._nova, self.__vm,
154 raise VmInstanceCreationError(
155 'Cannot applying security group with name ' +
157 ' to VM that did not activate with name - ' +
158 self.instance_settings.name)
160 if self.instance_settings.volume_names:
161 for volume_name in self.instance_settings.volume_names:
162 cinder = cinder_utils.cinder_client(self._os_creds)
163 volume = cinder_utils.get_volume(
164 cinder, volume_name=volume_name)
166 if volume and self.vm_active(block=True):
168 vm = nova_utils.attach_volume(
169 self._nova, self.__vm, volume, timeout)
174 logger.warn('Volume [%s] not attached within timeout '
175 'of [%s]', volume.name, timeout)
177 logger.warn('Unable to attach volume named [%s]',
180 self.__apply_floating_ips()
182 def __apply_floating_ips(self):
184 Applies the configured floating IPs to the necessary ports
187 for key, port in self.__ports:
188 port_dict[key] = port
191 for floating_ip_setting in self.instance_settings.floating_ip_settings:
192 self.add_floating_ip(floating_ip_setting)
194 def add_floating_ip(self, floating_ip_setting):
196 Adds a floating IP to a running instance
197 :param floating_ip_setting - the floating IP configuration
200 for key, port in self.__ports:
201 port_dict[key] = port
204 port = port_dict.get(floating_ip_setting.port_name)
207 raise VmInstanceCreationError(
208 'Cannot find port object with name - ' +
209 floating_ip_setting.port_name)
211 # Setup Floating IP only if there is a router with an external
213 ext_gateway = self.__ext_gateway_by_router(
214 floating_ip_setting.router_name)
216 subnet = neutron_utils.get_subnet(
218 subnet_name=floating_ip_setting.subnet_name)
219 floating_ip = neutron_utils.create_floating_ip(
220 self.__neutron, ext_gateway)
221 self.__floating_ip_dict[floating_ip_setting.name] = floating_ip
224 'Created floating IP %s via router - %s', floating_ip.ip,
225 floating_ip_setting.router_name)
226 self.__add_floating_ip(floating_ip, port, subnet)
228 raise VmInstanceCreationError(
229 'Unable to add floating IP to port, cannot locate router '
230 'with an external gateway ')
232 def __ext_gateway_by_router(self, router_name):
234 Returns network name for the external network attached to a router or
236 :param router_name: The name of the router to lookup
237 :return: the external network name or None
239 router = neutron_utils.get_router(
240 self.__neutron, router_name=router_name)
241 if router and router.external_network_id:
242 network = neutron_utils.get_network_by_id(
243 self.__neutron, router.external_network_id)
250 Destroys the VM instance
253 # Cleanup floating IPs
254 for name, floating_ip in self.__floating_ip_dict.items():
256 logger.info('Deleting Floating IP - ' + floating_ip.ip)
257 neutron_utils.delete_floating_ip(self.__neutron, floating_ip)
258 except Exception as e:
259 logger.error('Error deleting Floating IP - ' + str(e))
260 self.__floating_ip_dict = dict()
263 for volume_rec in self.__vm.volume_ids:
264 cinder = cinder_utils.cinder_client(self._os_creds)
265 volume = cinder_utils.get_volume_by_id(cinder, volume_rec['id'])
268 vm = nova_utils.detach_volume(
269 self._nova, self.__vm, volume, 30)
274 'Timeout waiting to detach volume %s', volume.name)
275 except Exception as e:
276 logger.error('Unexpected error detaching volume %s '
277 'with error %s', volume.name, e)
279 logger.warn('Unable to detach volume with ID - [%s]',
283 for name, port in self.__ports:
284 logger.info('Deleting Port with ID - %s ', port.id)
286 neutron_utils.delete_port(self.__neutron, port)
287 except PortNotFoundClient as e:
288 logger.warning('Unexpected error deleting port - %s', e)
290 self.__ports = list()
296 'Deleting VM instance - ' + self.instance_settings.name)
297 nova_utils.delete_vm_instance(self._nova, self.__vm)
298 except Exception as e:
299 logger.error('Error deleting VM - %s', e)
301 # Block until instance cannot be found or returns the status of
303 logger.info('Checking deletion status')
306 if self.vm_deleted(block=True):
308 'VM has been properly deleted VM with name - %s',
309 self.instance_settings.name)
313 'VM not deleted within the timeout period of %s '
314 'seconds', self.instance_settings.vm_delete_timeout)
315 except Exception as e:
317 'Unexpected error while checking VM instance status - %s',
320 def __query_ports(self, port_settings):
322 Returns the previously configured ports or an empty list if none
324 :param port_settings: A list of PortSetting objects
325 :return: a list of OpenStack port tuples where the first member is the
326 port name and the second is the port object
330 for port_setting in port_settings:
331 port = neutron_utils.get_port(
332 self.__neutron, port_settings=port_setting)
334 ports.append((port_setting.name, port))
338 def __create_ports(self, port_settings):
340 Returns the previously configured ports or creates them if they do not
342 :param port_settings: A list of PortSetting objects
343 :return: a list of OpenStack port tuples where the first member is the
344 port name and the second is the port object
348 for port_setting in port_settings:
349 port = neutron_utils.get_port(
350 self.__neutron, port_settings=port_setting)
352 port = neutron_utils.create_port(
353 self.__neutron, self._os_creds, port_setting)
355 ports.append((port_setting.name, port))
359 def __add_floating_ip(self, floating_ip, port, subnet, timeout=30,
360 poll_interval=POLL_INTERVAL):
362 Returns True when active else False
363 TODO - Make timeout and poll_interval configurable...
368 # Take IP of subnet if there is one configured on which to place
370 for fixed_ip in port.ips:
371 if fixed_ip['subnet_id'] == subnet.id:
372 ip = fixed_ip['ip_address']
375 # Simply take the first
376 ip = port.ips[0]['ip_address']
379 count = timeout / poll_interval
381 logger.debug('Attempting to add floating IP to instance')
383 nova_utils.add_floating_ip_to_server(
384 self._nova, self.__vm, floating_ip, ip)
386 'Added floating IP %s to port IP %s on instance %s',
387 floating_ip.ip, ip, self.instance_settings.name)
389 except BadRequest as bre:
390 logger.error('Cannot add floating IP [%s]', bre)
392 except Exception as e:
394 'Retry adding floating IP to instance. Last attempt '
395 'failed with - %s', e)
396 time.sleep(poll_interval)
400 raise VmInstanceCreationError(
401 'Unable find IP address on which to place the floating IP')
403 logger.error('Timeout attempting to add the floating IP to instance.')
404 raise VmInstanceCreationError(
405 'Timeout while attempting add floating IP to instance')
407 def get_os_creds(self):
409 Returns the OpenStack credentials used to create these objects
410 :return: the credentials
412 return self._os_creds
414 def get_vm_inst(self):
416 Returns the latest version of this server object from OpenStack
417 :return: Server object
419 return nova_utils.get_server_object_by_id(self._nova, self.__vm.id)
421 def get_console_output(self):
423 Returns the vm console object for parsing logs
424 :return: the console output object
426 return nova_utils.get_server_console_output(self._nova, self.__vm)
428 def get_port_ip(self, port_name, subnet_name=None):
430 Returns the first IP for the port corresponding with the port_name
431 parameter when subnet_name is None else returns the IP address that
432 corresponds to the subnet_name parameter
433 :param port_name: the name of the port from which to return the IP
434 :param subnet_name: the name of the subnet attached to this IP
435 :return: the IP or None if not found
437 port = self.get_port_by_name(port_name)
440 subnet = neutron_utils.get_subnet(
441 self.__neutron, subnet_name=subnet_name)
443 logger.warning('Cannot retrieve port IP as subnet could '
444 'not be located with name - %s',
447 for fixed_ip in port.ips:
448 if fixed_ip['subnet_id'] == subnet.id:
449 return fixed_ip['ip_address']
451 if port.ips and len(port.ips) > 0:
452 return port.ips[0]['ip_address']
455 def get_port_mac(self, port_name):
457 Returns the first IP for the port corresponding with the port_name
459 TODO - Add in the subnet as an additional parameter as a port may have
461 :param port_name: the name of the port from which to return the IP
462 :return: the IP or None if not found
464 port = self.get_port_by_name(port_name)
466 return port.mac_address
469 def get_port_by_name(self, port_name):
471 Retrieves the OpenStack port object by its given name
472 :param port_name: the name of the port
473 :return: the OpenStack port object or None if not exists
475 for key, port in self.__ports:
478 logger.warning('Cannot find port with name - ' + port_name)
481 def get_vm_info(self):
483 Returns a dictionary of a VMs info as returned by OpenStack
486 return nova_utils.get_server_info(self._nova, self.__vm)
488 def config_nics(self):
490 Responsible for configuring NICs on RPM systems where the instance has
491 more than one configured port
492 :return: the value returned by ansible_utils.apply_ansible_playbook()
494 if len(self.__ports) > 1 and len(self.__floating_ip_dict) > 0:
495 if self.vm_active(block=True) and self.vm_ssh_active(block=True):
496 for key, port in self.__ports:
497 port_index = self.__ports.index((key, port))
499 nic_name = 'eth' + repr(port_index)
500 retval = self.__config_nic(
502 self.__get_first_provisioning_floating_ip().ip)
503 logger.info('Configured NIC - %s on VM - %s',
504 nic_name, self.instance_settings.name)
507 def __get_first_provisioning_floating_ip(self):
509 Returns the first floating IP tagged with the Floating IP name if
510 exists else the first one found
513 for floating_ip_setting in self.instance_settings.floating_ip_settings:
514 if floating_ip_setting.provisioning:
515 fip = self.__floating_ip_dict.get(floating_ip_setting.name)
518 elif len(self.__floating_ip_dict) > 0:
519 for key, fip in self.__floating_ip_dict.items():
522 # When cannot be found above
523 if len(self.__floating_ip_dict) > 0:
524 for key, fip in self.__floating_ip_dict.items():
527 def __config_nic(self, nic_name, port, ip):
529 Although ports/NICs can contain multiple IPs, this code currently only
532 :param nic_name: Name of the interface
533 :param port: The port information containing the expected IP values.
534 :param ip: The IP on which to apply the playbook.
535 :return: the return value from ansible
537 port_ip = port.ips[0]['ip_address']
540 'nic_name': nic_name,
544 if self.image_settings.nic_config_pb_loc and self.keypair_settings:
545 return self.apply_ansible_playbook(
546 self.image_settings.nic_config_pb_loc, variables)
549 'VM %s cannot self configure NICs eth1++. No playbook or '
550 'keypairs found.', self.instance_settings.name)
552 def apply_ansible_playbook(self, pb_file_loc, variables=None,
555 Applies a playbook to a VM
556 :param pb_file_loc: the file location of the playbook to be applied
557 :param variables: a dict() of substitution values required by the
559 :param fip_name: the name of the floating IP to use for applying the
560 playbook (default - will take the first)
561 :return: the return value from ansible
563 return ansible_utils.apply_playbook(
564 pb_file_loc, [self.get_floating_ip(fip_name=fip_name).ip],
565 self.get_image_user(), self.keypair_settings.private_filepath,
566 variables, self._os_creds.proxy_settings)
568 def get_image_user(self):
570 Returns the instance sudo_user if it has been configured in the
571 instance_settings else it returns the image_settings.image_user value
573 if self.instance_settings.sudo_user:
574 return self.instance_settings.sudo_user
576 return self.image_settings.image_user
578 def vm_deleted(self, block=False, poll_interval=POLL_INTERVAL):
580 Returns true when the VM status returns the value of
581 expected_status_code or instance retrieval throws a NotFound exception.
582 :param block: When true, thread will block until active or timeout
583 value in seconds has been exceeded (False)
584 :param poll_interval: The polling interval in seconds
588 return self.__vm_status_check(
589 STATUS_DELETED, block,
590 self.instance_settings.vm_delete_timeout, poll_interval)
591 except NotFound as e:
593 "Instance not found when querying status for %s with message "
594 "%s", STATUS_DELETED, e)
597 def vm_active(self, block=False, poll_interval=POLL_INTERVAL):
599 Returns true when the VM status returns the value of the constant
601 :param block: When true, thread will block until active or timeout
602 value in seconds has been exceeded (False)
603 :param poll_interval: The polling interval in seconds
606 if self.__vm_status_check(
607 STATUS_ACTIVE, block, self.instance_settings.vm_boot_timeout,
609 self.__vm = nova_utils.get_server_object_by_id(
610 self._nova, self.__vm.id)
614 def __vm_status_check(self, expected_status_code, block, timeout,
617 Returns true when the VM status returns the value of
619 :param expected_status_code: instance status evaluated with this
621 :param block: When true, thread will block until active or timeout
622 value in seconds has been exceeded (False)
623 :param timeout: The timeout value
624 :param poll_interval: The polling interval in seconds
627 # sleep and wait for VM status change
631 return self.__status(expected_status_code)
633 while timeout > time.time() - start:
634 status = self.__status(expected_status_code)
636 logger.info('VM is - ' + expected_status_code)
639 logger.debug('Retry querying VM status in ' + str(
640 poll_interval) + ' seconds')
641 time.sleep(poll_interval)
642 logger.debug('VM status query timeout in ' + str(
643 timeout - (time.time() - start)))
646 'Timeout checking for VM status for ' + expected_status_code)
649 def __status(self, expected_status_code):
651 Returns True when active else False
652 :param expected_status_code: instance status evaluated with this string
657 if expected_status_code == STATUS_DELETED:
662 status = nova_utils.get_server_status(self._nova, self.__vm)
664 logger.warning('Cannot find instance with id - ' + self.__vm.id)
667 if status == 'ERROR':
668 raise VmInstanceCreationError(
669 'Instance had an error during deployment')
671 'Instance status [%s] is - %s', self.instance_settings.name,
673 return status == expected_status_code
675 def vm_ssh_active(self, block=False, poll_interval=POLL_INTERVAL):
677 Returns true when the VM can be accessed via SSH
678 :param block: When true, thread will block until active or timeout
679 value in seconds has been exceeded (False)
680 :param poll_interval: The polling interval
683 # sleep and wait for VM status change
684 logger.info('Checking if VM is active')
686 timeout = self.instance_settings.ssh_connect_timeout
688 if self.vm_active(block=True):
692 start = time.time() - timeout
694 while timeout > time.time() - start:
695 status = self.__ssh_active()
697 logger.info('SSH is active for VM instance')
700 logger.debug('Retry SSH connection in ' + str(
701 poll_interval) + ' seconds')
702 time.sleep(poll_interval)
703 logger.debug('SSH connection timeout in ' + str(
704 timeout - (time.time() - start)))
706 logger.error('Timeout attempting to connect with VM via SSH')
709 def __ssh_active(self):
711 Returns True when can create a SSH session else False
714 if len(self.__floating_ip_dict) > 0:
715 ssh = self.ssh_client()
721 def get_floating_ip(self, fip_name=None):
723 Returns the floating IP object byt name if found, else the first known,
725 :param fip_name: the name of the floating IP to return
726 :return: the SSH client or None
728 if fip_name and self.__floating_ip_dict.get(fip_name):
729 return self.__floating_ip_dict.get(fip_name)
731 return self.__get_first_provisioning_floating_ip()
733 def ssh_client(self, fip_name=None):
735 Returns an SSH client using the name or the first known floating IP if
737 :param fip_name: the name of the floating IP to return
738 :return: the SSH client or None
740 fip = self.get_floating_ip(fip_name)
742 return ansible_utils.ssh_client(
743 self.__get_first_provisioning_floating_ip().ip,
744 self.get_image_user(),
745 self.keypair_settings.private_filepath,
746 proxy_settings=self._os_creds.proxy_settings)
748 FloatingIPAllocationError(
749 'Cannot return an SSH client. No Floating IP configured')
751 def add_security_group(self, security_group):
753 Adds a security group to this VM. Call will block until VM is active.
754 :param security_group: the SNAPS SecurityGroup domain object
755 :return True if successful else False
757 self.vm_active(block=True)
759 if not security_group:
760 logger.warning('Security group object is None, cannot add')
764 nova_utils.add_security_group(self._nova, self.get_vm_inst(),
767 except NotFound as e:
768 logger.warning('Security group not added - ' + str(e))
771 def remove_security_group(self, security_group):
773 Removes a security group to this VM. Call will block until VM is active
774 :param security_group: the OpenStack security group object
775 :return True if successful else False
777 self.vm_active(block=True)
779 if not security_group:
780 logger.warning('Security group object is None, cannot remove')
784 nova_utils.remove_security_group(self._nova, self.get_vm_inst(),
787 except NotFound as e:
788 logger.warning('Security group not removed - ' + str(e))
791 def reboot(self, reboot_type=RebootType.soft):
794 :param reboot_type: instance of
795 snaps.openstack.utils.nova_utils.RebootType
799 nova_utils.reboot_server(
800 self._nova, self.__vm, reboot_type=reboot_type)
803 def generate_creator(os_creds, vm_inst, image_config, keypair_config=None):
805 Initializes an OpenStackVmInstance object
806 :param os_creds: the OpenStack credentials
807 :param vm_inst: the SNAPS-OO VmInst domain object
808 :param image_config: the associated ImageConfig object
809 :param keypair_config: the associated KeypairConfig object (optional)
810 :return: an initialized OpenStackVmInstance object
812 nova = nova_utils.nova_client(os_creds)
813 neutron = neutron_utils.neutron_client(os_creds)
814 derived_inst_config = settings_utils.create_vm_inst_config(
815 nova, neutron, vm_inst)
817 derived_inst_creator = OpenStackVmInstance(
818 os_creds, derived_inst_config, image_config, keypair_config)
819 derived_inst_creator.initialize()
820 return derived_inst_creator
823 class VmInstanceSettings(VmInstanceConfig):
825 Deprecated, use snaps.config.vm_inst.VmInstanceConfig instead
827 def __init__(self, **kwargs):
828 from warnings import warn
829 warn('Use snaps.config.vm_inst.VmInstanceConfig instead',
831 super(self.__class__, self).__init__(**kwargs)
834 class FloatingIpSettings(FloatingIpConfig):
836 Deprecated, use snaps.config.vm_inst.FloatingIpConfig instead
838 def __init__(self, **kwargs):
839 from warnings import warn
840 warn('Use snaps.config.vm_inst.FloatingIpConfig instead',
842 super(self.__class__, self).__init__(**kwargs)
845 class VmInstanceCreationError(Exception):
847 Exception to be thrown when an VM instance cannot be created
851 class FloatingIPAllocationError(Exception):
853 Exception to be thrown when an VM instance cannot allocate a floating IP