3 # Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs")
4 # and others. All rights reserved.
6 # Licensed under the Apache License, Version 2.0 (the "License");
7 # you may not use this file except in compliance with the License.
8 # You may obtain a copy of the License at:
10 # http://www.apache.org/licenses/LICENSE-2.0
12 # Unless required by applicable law or agreed to in writing, software
13 # distributed under the License is distributed on an "AS IS" BASIS,
14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 # See the License for the specific language governing permissions and
16 # limitations under the License.
18 # This script is responsible for deploying virtual environments
24 from jinja2 import Environment, FileSystemLoader
28 from snaps import file_utils
29 from snaps.openstack.create_flavor import FlavorSettings, OpenStackFlavor
30 from snaps.openstack.create_image import OpenStackImage
31 from snaps.config.image import ImageConfig
32 from snaps.openstack.create_instance import VmInstanceSettings
33 from snaps.openstack.create_keypairs import KeypairSettings, OpenStackKeypair
34 from snaps.openstack.create_network import (
35 PortSettings, NetworkSettings, OpenStackNetwork)
36 from snaps.openstack.create_project import OpenStackProject, ProjectSettings
37 from snaps.openstack.create_qos import QoSSettings, OpenStackQoS
38 from snaps.openstack.create_router import RouterSettings, OpenStackRouter
39 from snaps.openstack.create_security_group import (
40 OpenStackSecurityGroup, SecurityGroupSettings)
41 from snaps.openstack.create_user import OpenStackUser, UserSettings
42 from snaps.openstack.create_volume import OpenStackVolume, VolumeSettings
43 from snaps.openstack.create_volume_type import (
44 OpenStackVolumeType, VolumeTypeSettings)
45 from snaps.openstack.os_credentials import OSCreds, ProxySettings
46 from snaps.openstack.utils import deploy_utils
47 from snaps.provisioning import ansible_utils
49 __author__ = 'spisarski'
51 logger = logging.getLogger('snaps_launcher')
53 ARG_NOT_SET = "argument not set"
54 DEFAULT_CREDS_KEY = 'admin'
57 def __get_creds_dict(os_conn_config):
59 Returns a dict of OSCreds where the key is the creds name.
60 For backwards compatibility, credentials not contained in a list (only
61 one) will be returned with the key of None
62 :param os_conn_config: the credential configuration
63 :return: a dict of OSCreds objects
65 if 'connection' in os_conn_config:
66 return {DEFAULT_CREDS_KEY: __get_os_credentials(os_conn_config)}
67 elif 'connections' in os_conn_config:
69 for os_conn_dict in os_conn_config['connections']:
70 config = os_conn_dict.get('connection')
72 raise Exception('Invalid connection format')
74 name = config.get('name')
76 raise Exception('Connection config requires a name field')
78 out[name] = __get_os_credentials(os_conn_dict)
82 def __get_creds(os_creds_dict, os_user_dict, inst_config):
84 Returns the appropriate credentials
85 :param os_creds_dict: a dictionary of OSCreds objects where the name is the
87 :param os_user_dict: a dictionary of OpenStackUser objects where the name
90 :return: an OSCreds instance or None
92 os_creds = os_creds_dict.get(DEFAULT_CREDS_KEY)
93 if 'os_user' in inst_config:
94 os_user_conf = inst_config['os_user']
95 if 'name' in os_user_conf:
96 user_creator = os_user_dict.get(os_user_conf['name'])
98 return user_creator.get_os_creds(
99 project_name=os_user_conf.get('project_name'))
100 elif 'os_creds_name' in inst_config:
101 if 'os_creds_name' in inst_config:
102 os_creds = os_creds_dict[inst_config['os_creds_name']]
106 def __get_os_credentials(os_conn_config):
108 Returns an object containing all of the information required to access
110 :param os_conn_config: The configuration holding the credentials
111 :return: an OSCreds instance
113 config = os_conn_config.get('connection')
115 raise Exception('Invalid connection configuration')
117 proxy_settings = None
118 http_proxy = config.get('http_proxy')
120 tokens = re.split(':', http_proxy)
121 ssh_proxy_cmd = config.get('ssh_proxy_cmd')
122 proxy_settings = ProxySettings(host=tokens[0], port=tokens[1],
123 ssh_proxy_cmd=ssh_proxy_cmd)
125 if 'proxy_settings' in config:
126 host = config['proxy_settings'].get('host')
127 port = config['proxy_settings'].get('port')
128 if host and host != 'None' and port and port != 'None':
129 proxy_settings = ProxySettings(**config['proxy_settings'])
132 config['proxy_settings'] = proxy_settings
134 if config.get('proxy_settings'):
135 del config['proxy_settings']
137 return OSCreds(**config)
140 def __parse_ports_config(config):
142 Parses the "ports" configuration
143 :param config: The dictionary to parse
144 :return: a list of PortConfig objects
147 for port_config in config:
148 out.append(PortSettings(**port_config.get('port')))
152 def __create_instances(os_creds_dict, creator_class, config_class, config,
153 config_key, cleanup=False, os_users_dict=None):
155 Returns a dictionary of SNAPS creator objects where the key is the name
156 :param os_creds_dict: Dictionary of OSCreds objects where the key is the
158 :param config: The list of configurations for the same type
159 :param config_key: The list of configurations for the same type
160 :param cleanup: Denotes whether or not this is being called for cleanup
167 for config_dict in config:
168 inst_config = config_dict.get(config_key)
170 creator = creator_class(
171 __get_creds(os_creds_dict, os_users_dict, inst_config),
172 config_class(**inst_config))
178 out[inst_config['name']] = creator
179 logger.info('Created configured %s', config_key)
180 except Exception as e:
181 logger.error('Unexpected error instantiating creator [%s] '
182 'with exception %s', creator_class, e)
187 def __create_vm_instances(os_creds_dict, os_users_dict, instances_config,
188 image_dict, keypairs_dict, cleanup=False):
190 Returns a dictionary of OpenStackVmInstance objects where the key is the
192 :param os_creds_dict: Dictionary of OSCreds objects where the key is the
194 :param os_users_dict: Dictionary of OpenStackUser objects where the key is
196 :param instances_config: The list of VM instance configurations
197 :param image_dict: A dictionary of images that will probably be used to
198 instantiate the VM instance
199 :param keypairs_dict: A dictionary of keypairs that will probably be used
200 to instantiate the VM instance
201 :param cleanup: Denotes whether or not this is being called for cleanup
208 for instance_config in instances_config:
209 conf = instance_config.get('instance')
212 image_creator = image_dict.get(conf.get('imageName'))
214 instance_settings = VmInstanceSettings(
215 **instance_config['instance'])
216 kp_creator = keypairs_dict.get(
217 conf.get('keypair_name'))
219 'name']] = deploy_utils.create_vm_instance(
221 os_creds_dict, os_users_dict, conf),
223 image_creator.image_settings,
224 keypair_creator=kp_creator,
227 raise Exception('Image creator instance not found.'
228 ' Cannot instantiate')
230 raise Exception('Image dictionary is None. Cannot '
233 raise Exception('Instance configuration is None. Cannot '
235 logger.info('Created configured instances')
236 except Exception as e:
237 logger.error('Unexpected error creating VM instances - %s', e)
241 def __apply_ansible_playbooks(ansible_configs, os_creds_dict, vm_dict,
242 image_dict, flavor_dict, env_file):
244 Applies ansible playbooks to running VMs with floating IPs
245 :param ansible_configs: a list of Ansible configurations
246 :param os_creds_dict: Dictionary of OSCreds objects where the key is the
248 :param vm_dict: the dictionary of newly instantiated VMs where the name is
250 :param image_dict: the dictionary of newly instantiated images where the
252 :param flavor_dict: the dictionary of newly instantiated flavors where the
254 :param env_file: the path of the environment for setting the CWD so
255 playbook location is relative to the deployment file
256 :return: t/f - true if successful
258 logger.info("Applying Ansible Playbooks")
260 # Ensure all hosts are accepting SSH session requests
261 for vm_inst in list(vm_dict.values()):
262 if not vm_inst.vm_ssh_active(block=True):
264 "Timeout waiting for instance to respond to SSH requests")
267 # Set CWD so the deployment file's playbook location can leverage
269 orig_cwd = os.getcwd()
270 env_dir = os.path.dirname(env_file)
274 for ansible_config in ansible_configs:
275 if 'pre_sleep_time' in ansible_config:
277 sleep_time = int(ansible_config['pre_sleep_time'])
278 logger.info('Waiting %s seconds to apply playbooks',
280 time.sleep(sleep_time)
284 os_creds = os_creds_dict.get(None, 'admin')
285 __apply_ansible_playbook(ansible_config, os_creds, vm_dict,
286 image_dict, flavor_dict)
288 # Return to original directory
294 def __apply_ansible_playbook(ansible_config, os_creds, vm_dict, image_dict,
297 Applies an Ansible configuration setting
298 :param ansible_config: the configuration settings
299 :param os_creds: the OpenStack credentials object
300 :param vm_dict: the dictionary of newly instantiated VMs where the name is
302 :param image_dict: the dictionary of newly instantiated images where the
304 :param flavor_dict: the dictionary of newly instantiated flavors where the
308 (remote_user, floating_ips, private_key_filepath,
309 proxy_settings) = __get_connection_info(
310 ansible_config, vm_dict)
312 retval = ansible_utils.apply_playbook(
313 ansible_config['playbook_location'], floating_ips, remote_user,
314 private_key_filepath,
315 variables=__get_variables(ansible_config.get('variables'),
316 os_creds, vm_dict, image_dict,
318 proxy_setting=proxy_settings)
320 # Not a fatal type of event
322 'Unable to apply playbook found at location - %s',
323 ansible_config.get('playbook_location'))
326 def __get_connection_info(ansible_config, vm_dict):
328 Returns a tuple of data required for connecting to the running VMs
329 (remote_user, [floating_ips], private_key_filepath, proxy_settings)
330 :param ansible_config: the configuration settings
331 :param vm_dict: the dictionary of VMs where the VM name is the key
332 :return: tuple where the first element is the user and the second is a list
333 of floating IPs and the third is the
334 private key file location and the fourth is an instance of the
335 snaps.ProxySettings class
336 (note: in order to work, each of the hosts need to have the same sudo_user
337 and private key file location values)
339 if ansible_config.get('hosts'):
340 hosts = ansible_config['hosts']
342 floating_ips = list()
345 proxy_settings = None
347 vm = vm_dict.get(host)
349 fip = vm.get_floating_ip()
351 remote_user = vm.get_image_user()
354 floating_ips.append(fip.ip)
357 'Could not find floating IP for VM - ' +
360 pk_file = vm.keypair_settings.private_filepath
361 proxy_settings = vm.get_os_creds().proxy_settings
363 logger.error('Could not locate VM with name - ' + host)
365 return remote_user, floating_ips, pk_file, proxy_settings
369 def __get_variables(var_config, os_creds, vm_dict, image_dict, flavor_dict):
371 Returns a dictionary of substitution variables to be used for Ansible
373 :param var_config: the variable configuration settings
374 :param os_creds: the OpenStack credentials object
375 :param vm_dict: the dictionary of newly instantiated VMs where the name is
377 :param image_dict: the dictionary of newly instantiated images where the
379 :param flavor_dict: the dictionary of newly instantiated flavors where the
381 :return: dictionary or None
383 if var_config and vm_dict and len(vm_dict) > 0:
385 for key, value in var_config.items():
386 value = __get_variable_value(value, os_creds, vm_dict, image_dict,
389 variables[key] = value
391 "Set Jinga2 variable with key [%s] the value [%s]",
394 logger.warning('Key [%s] or Value [%s] must not be None',
395 str(key), str(value))
400 def __get_variable_value(var_config_values, os_creds, vm_dict, image_dict,
403 Returns the associated variable value for use by Ansible for substitution
405 :param var_config_values: the configuration dictionary
406 :param os_creds: the OpenStack credentials object
407 :param vm_dict: the dictionary of newly instantiated VMs where the name is
409 :param image_dict: the dictionary of newly instantiated images where the
411 :param flavor_dict: the dictionary of newly instantiated flavors where the
415 if var_config_values['type'] == 'string':
416 return __get_string_variable_value(var_config_values)
417 if var_config_values['type'] == 'vm-attr':
418 return __get_vm_attr_variable_value(var_config_values, vm_dict)
419 if var_config_values['type'] == 'os_creds':
420 return __get_os_creds_variable_value(var_config_values, os_creds)
421 if var_config_values['type'] == 'port':
422 return __get_vm_port_variable_value(var_config_values, vm_dict)
423 if var_config_values['type'] == 'floating_ip':
424 return __get_vm_fip_variable_value(var_config_values, vm_dict)
425 if var_config_values['type'] == 'image':
426 return __get_image_variable_value(var_config_values, image_dict)
427 if var_config_values['type'] == 'flavor':
428 return __get_flavor_variable_value(var_config_values, flavor_dict)
432 def __get_string_variable_value(var_config_values):
434 Returns the associated string value
435 :param var_config_values: the configuration dictionary
436 :return: the value contained in the dictionary with the key 'value'
438 return var_config_values['value']
441 def __get_vm_attr_variable_value(var_config_values, vm_dict):
443 Returns the associated value contained on a VM instance
444 :param var_config_values: the configuration dictionary
445 :param vm_dict: the dictionary containing all VMs where the key is the VM's
449 vm = vm_dict.get(var_config_values['vm_name'])
451 if var_config_values['value'] == 'floating_ip':
452 return vm.get_floating_ip().ip
453 if var_config_values['value'] == 'image_user':
454 return vm.get_image_user()
457 def __get_os_creds_variable_value(var_config_values, os_creds):
459 Returns the associated OS credentials value
460 :param var_config_values: the configuration dictionary
461 :param os_creds: the credentials
464 logger.info("Retrieving OS Credentials")
466 if var_config_values['value'] == 'username':
467 logger.info("Returning OS username")
468 return os_creds.username
469 elif var_config_values['value'] == 'password':
470 logger.info("Returning OS password")
471 return os_creds.password
472 elif var_config_values['value'] == 'auth_url':
473 logger.info("Returning OS auth_url")
474 return os_creds.auth_url
475 elif var_config_values['value'] == 'project_name':
476 logger.info("Returning OS project_name")
477 return os_creds.project_name
479 logger.info("Returning none")
483 def __get_vm_port_variable_value(var_config_values, vm_dict):
485 Returns the associated OS credentials value
486 :param var_config_values: the configuration dictionary
487 :param vm_dict: the dictionary containing all VMs where the key is the VM's
491 port_name = var_config_values.get('port_name')
492 vm_name = var_config_values.get('vm_name')
494 if port_name and vm_name:
495 vm = vm_dict.get(vm_name)
497 port_value_id = var_config_values.get('port_value')
499 if port_value_id == 'mac_address':
500 return vm.get_port_mac(port_name)
501 if port_value_id == 'ip_address':
502 return vm.get_port_ip(port_name)
505 def __get_vm_fip_variable_value(var_config_values, vm_dict):
507 Returns the floating IP value if found
508 :param var_config_values: the configuration dictionary
509 :param vm_dict: the dictionary containing all VMs where the key is the VM's
511 :return: the floating IP string value or None
513 fip_name = var_config_values.get('fip_name')
514 vm_name = var_config_values.get('vm_name')
517 vm = vm_dict.get(vm_name)
519 fip = vm.get_floating_ip(fip_name)
524 def __get_image_variable_value(var_config_values, image_dict):
526 Returns the associated image value
527 :param var_config_values: the configuration dictionary
528 :param image_dict: the dictionary containing all images where the key is
532 logger.info("Retrieving image values")
535 if var_config_values.get('image_name'):
536 image_creator = image_dict.get(var_config_values['image_name'])
538 if var_config_values.get('value') and \
539 var_config_values['value'] == 'id':
540 return image_creator.get_image().id
541 if var_config_values.get('value') and \
542 var_config_values['value'] == 'user':
543 return image_creator.image_settings.image_user
545 logger.info("Returning none")
549 def __get_flavor_variable_value(var_config_values, flavor_dict):
551 Returns the associated flavor value
552 :param var_config_values: the configuration dictionary
553 :param flavor_dict: the dictionary containing all flavor creators where the
555 :return: the value or None
557 logger.info("Retrieving flavor values")
560 if var_config_values.get('flavor_name'):
561 flavor_creator = flavor_dict.get(var_config_values['flavor_name'])
563 if var_config_values.get('value') and \
564 var_config_values['value'] == 'id':
565 return flavor_creator.get_flavor().id
570 Will need to set environment variable ANSIBLE_HOST_KEY_CHECKING=False or
571 Create a file located in /etc/ansible/ansible/cfg or ~/.ansible.cfg
572 containing the following content:
575 host_key_checking = False
577 CWD must be this directory where this script is located.
581 log_level = logging.INFO
582 if arguments.log_level != 'INFO':
583 log_level = logging.DEBUG
584 logging.basicConfig(level=log_level)
586 logger.info('Starting to Deploy')
588 # Apply env_file/substitution file to template
589 env = Environment(loader=FileSystemLoader(
590 searchpath=os.path.dirname(arguments.tmplt_file)))
591 template = env.get_template(os.path.basename(arguments.tmplt_file))
594 if arguments.env_file:
595 env_dict = file_utils.read_yaml(arguments.env_file)
596 output = template.render(**env_dict)
598 config = yaml.load(output)
601 os_config = config.get('openstack')
606 flavors_dict = dict()
607 os_creds_dict = dict()
608 clean = arguments.clean is not ARG_NOT_SET
611 os_creds_dict = __get_creds_dict(os_config)
615 projects_dict = __create_instances(
616 os_creds_dict, OpenStackProject, ProjectSettings,
617 os_config.get('projects'), 'project', clean)
618 creators.append(projects_dict)
621 users_dict = __create_instances(
622 os_creds_dict, OpenStackUser, UserSettings,
623 os_config.get('users'), 'user', clean)
624 creators.append(users_dict)
626 # Associate new users to projects
628 for project_creator in projects_dict.values():
629 users = project_creator.project_settings.users
630 for user_name in users:
631 user_creator = users_dict.get(user_name)
633 project_creator.assoc_user(
634 user_creator.get_user())
637 flavors_dict = __create_instances(
638 os_creds_dict, OpenStackFlavor, FlavorSettings,
639 os_config.get('flavors'), 'flavor', clean, users_dict)
640 creators.append(flavors_dict)
643 qos_dict = __create_instances(
644 os_creds_dict, OpenStackQoS, QoSSettings,
645 os_config.get('qos_specs'), 'qos_spec', clean, users_dict)
646 creators.append(qos_dict)
648 # Create volume types
649 vol_type_dict = __create_instances(
650 os_creds_dict, OpenStackVolumeType, VolumeTypeSettings,
651 os_config.get('volume_types'), 'volume_type', clean,
653 creators.append(vol_type_dict)
655 # Create volume types
656 vol_dict = __create_instances(
657 os_creds_dict, OpenStackVolume, VolumeSettings,
658 os_config.get('volumes'), 'volume', clean, users_dict)
659 creators.append(vol_dict)
662 images_dict = __create_instances(
663 os_creds_dict, OpenStackImage, ImageConfig,
664 os_config.get('images'), 'image', clean, users_dict)
665 creators.append(images_dict)
668 creators.append(__create_instances(
669 os_creds_dict, OpenStackNetwork, NetworkSettings,
670 os_config.get('networks'), 'network', clean, users_dict))
673 creators.append(__create_instances(
674 os_creds_dict, OpenStackRouter, RouterSettings,
675 os_config.get('routers'), 'router', clean, users_dict))
678 keypairs_dict = __create_instances(
679 os_creds_dict, OpenStackKeypair, KeypairSettings,
680 os_config.get('keypairs'), 'keypair', clean, users_dict)
681 creators.append(keypairs_dict)
683 # Create security groups
684 creators.append(__create_instances(
685 os_creds_dict, OpenStackSecurityGroup,
686 SecurityGroupSettings,
687 os_config.get('security_groups'), 'security_group', clean,
691 vm_dict = __create_vm_instances(
692 os_creds_dict, users_dict, os_config.get('instances'),
693 images_dict, keypairs_dict,
694 arguments.clean is not ARG_NOT_SET)
695 creators.append(vm_dict)
697 'Completed creating/retrieving all configured instances')
698 except Exception as e:
700 'Unexpected error deploying environment. Rolling back due'
704 # Must enter either block
705 if arguments.clean is not ARG_NOT_SET:
706 # Cleanup Environment
707 __cleanup(creators, arguments.clean_image is not ARG_NOT_SET)
708 elif arguments.deploy is not ARG_NOT_SET:
709 logger.info('Configuring NICs where required')
710 for vm in vm_dict.values():
712 logger.info('Completed NIC configuration')
715 ansible_config = config.get('ansible')
716 if ansible_config and vm_dict:
717 if not __apply_ansible_playbooks(ansible_config,
718 os_creds_dict, vm_dict,
719 images_dict, flavors_dict,
720 arguments.tmplt_file):
721 logger.error("Problem applying ansible playbooks")
724 'Unable to read configuration file - ' + arguments.tmplt_file)
730 def __cleanup(creators, clean_image=False):
731 for creator_dict in reversed(creators):
732 for key, creator in creator_dict.items():
733 if ((isinstance(creator, OpenStackImage) and clean_image)
734 or not isinstance(creator, OpenStackImage)):
737 except Exception as e:
738 logger.warning('Error cleaning component - %s', e)
741 if __name__ == '__main__':
742 # To ensure any files referenced via a relative path will begin from the
743 # directory in which this file resides
744 os.chdir(os.path.dirname(os.path.realpath(__file__)))
746 parser = argparse.ArgumentParser()
748 '-d', '--deploy', dest='deploy', nargs='?', default=ARG_NOT_SET,
749 help='When used, environment will be deployed and provisioned')
751 '-c', '--clean', dest='clean', nargs='?', default=ARG_NOT_SET,
752 help='When used, the environment will be removed')
754 '-i', '--clean-image', dest='clean_image', nargs='?',
756 help='When cleaning, if this is set, the image will be cleaned too')
758 '-t', '--tmplt', dest='tmplt_file', required=True,
759 help='The SNAPS deployment template YAML file - REQUIRED')
761 '-e', '--env-file', dest='env_file',
762 help='Yaml file containing substitution values to the env file')
764 '-l', '--log-level', dest='log_level', default='INFO',
765 help='Logging Level (INFO|DEBUG)')
766 args = parser.parse_args()
768 if args.deploy is ARG_NOT_SET and args.clean is ARG_NOT_SET:
770 'Must enter either -d for deploy or -c for cleaning up and '
773 if args.deploy is not ARG_NOT_SET and args.clean is not ARG_NOT_SET:
774 print('Cannot enter both options -d/--deploy and -c/--clean')