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.config.flavor import FlavorConfig
30 from snaps.config.image import ImageConfig
31 from snaps.openstack.create_flavor import OpenStackFlavor
32 from snaps.openstack.create_image import OpenStackImage
33 from snaps.openstack.create_instance import VmInstanceSettings
34 from snaps.openstack.create_keypairs import KeypairSettings, OpenStackKeypair
35 from snaps.openstack.create_network import (
36 PortSettings, NetworkSettings, OpenStackNetwork)
37 from snaps.openstack.create_project import OpenStackProject, ProjectSettings
38 from snaps.openstack.create_qos import QoSSettings, OpenStackQoS
39 from snaps.openstack.create_router import RouterSettings, OpenStackRouter
40 from snaps.openstack.create_security_group import (
41 OpenStackSecurityGroup, SecurityGroupSettings)
42 from snaps.openstack.create_user import OpenStackUser, UserSettings
43 from snaps.openstack.create_volume import OpenStackVolume, VolumeSettings
44 from snaps.openstack.create_volume_type import (
45 OpenStackVolumeType, VolumeTypeSettings)
46 from snaps.openstack.os_credentials import OSCreds, ProxySettings
47 from snaps.openstack.utils import deploy_utils
48 from snaps.provisioning import ansible_utils
50 __author__ = 'spisarski'
52 logger = logging.getLogger('snaps_launcher')
54 ARG_NOT_SET = "argument not set"
55 DEFAULT_CREDS_KEY = 'admin'
58 def __get_creds_dict(os_conn_config):
60 Returns a dict of OSCreds where the key is the creds name.
61 For backwards compatibility, credentials not contained in a list (only
62 one) will be returned with the key of None
63 :param os_conn_config: the credential configuration
64 :return: a dict of OSCreds objects
66 if 'connection' in os_conn_config:
67 return {DEFAULT_CREDS_KEY: __get_os_credentials(os_conn_config)}
68 elif 'connections' in os_conn_config:
70 for os_conn_dict in os_conn_config['connections']:
71 config = os_conn_dict.get('connection')
73 raise Exception('Invalid connection format')
75 name = config.get('name')
77 raise Exception('Connection config requires a name field')
79 out[name] = __get_os_credentials(os_conn_dict)
83 def __get_creds(os_creds_dict, os_user_dict, inst_config):
85 Returns the appropriate credentials
86 :param os_creds_dict: a dictionary of OSCreds objects where the name is the
88 :param os_user_dict: a dictionary of OpenStackUser objects where the name
91 :return: an OSCreds instance or None
93 os_creds = os_creds_dict.get(DEFAULT_CREDS_KEY)
94 if 'os_user' in inst_config:
95 os_user_conf = inst_config['os_user']
96 if 'name' in os_user_conf:
97 user_creator = os_user_dict.get(os_user_conf['name'])
99 return user_creator.get_os_creds(
100 project_name=os_user_conf.get('project_name'))
101 elif 'os_creds_name' in inst_config:
102 if 'os_creds_name' in inst_config:
103 os_creds = os_creds_dict[inst_config['os_creds_name']]
107 def __get_os_credentials(os_conn_config):
109 Returns an object containing all of the information required to access
111 :param os_conn_config: The configuration holding the credentials
112 :return: an OSCreds instance
114 config = os_conn_config.get('connection')
116 raise Exception('Invalid connection configuration')
118 proxy_settings = None
119 http_proxy = config.get('http_proxy')
121 tokens = re.split(':', http_proxy)
122 ssh_proxy_cmd = config.get('ssh_proxy_cmd')
123 proxy_settings = ProxySettings(host=tokens[0], port=tokens[1],
124 ssh_proxy_cmd=ssh_proxy_cmd)
126 if 'proxy_settings' in config:
127 host = config['proxy_settings'].get('host')
128 port = config['proxy_settings'].get('port')
129 if host and host != 'None' and port and port != 'None':
130 proxy_settings = ProxySettings(**config['proxy_settings'])
133 config['proxy_settings'] = proxy_settings
135 if config.get('proxy_settings'):
136 del config['proxy_settings']
138 return OSCreds(**config)
141 def __parse_ports_config(config):
143 Parses the "ports" configuration
144 :param config: The dictionary to parse
145 :return: a list of PortConfig objects
148 for port_config in config:
149 out.append(PortSettings(**port_config.get('port')))
153 def __create_instances(os_creds_dict, creator_class, config_class, config,
154 config_key, cleanup=False, os_users_dict=None):
156 Returns a dictionary of SNAPS creator objects where the key is the name
157 :param os_creds_dict: Dictionary of OSCreds objects where the key is the
159 :param config: The list of configurations for the same type
160 :param config_key: The list of configurations for the same type
161 :param cleanup: Denotes whether or not this is being called for cleanup
168 for config_dict in config:
169 inst_config = config_dict.get(config_key)
171 creator = creator_class(
172 __get_creds(os_creds_dict, os_users_dict, inst_config),
173 config_class(**inst_config))
179 out[inst_config['name']] = creator
180 logger.info('Created configured %s', config_key)
181 except Exception as e:
182 logger.error('Unexpected error instantiating creator [%s] '
183 'with exception %s', creator_class, e)
188 def __create_vm_instances(os_creds_dict, os_users_dict, instances_config,
189 image_dict, keypairs_dict, cleanup=False):
191 Returns a dictionary of OpenStackVmInstance objects where the key is the
193 :param os_creds_dict: Dictionary of OSCreds objects where the key is the
195 :param os_users_dict: Dictionary of OpenStackUser objects where the key is
197 :param instances_config: The list of VM instance configurations
198 :param image_dict: A dictionary of images that will probably be used to
199 instantiate the VM instance
200 :param keypairs_dict: A dictionary of keypairs that will probably be used
201 to instantiate the VM instance
202 :param cleanup: Denotes whether or not this is being called for cleanup
209 for instance_config in instances_config:
210 conf = instance_config.get('instance')
213 image_creator = image_dict.get(conf.get('imageName'))
215 instance_settings = VmInstanceSettings(
216 **instance_config['instance'])
217 kp_creator = keypairs_dict.get(
218 conf.get('keypair_name'))
220 'name']] = deploy_utils.create_vm_instance(
222 os_creds_dict, os_users_dict, conf),
224 image_creator.image_settings,
225 keypair_creator=kp_creator,
228 raise Exception('Image creator instance not found.'
229 ' Cannot instantiate')
231 raise Exception('Image dictionary is None. Cannot '
234 raise Exception('Instance configuration is None. Cannot '
236 logger.info('Created configured instances')
237 except Exception as e:
238 logger.error('Unexpected error creating VM instances - %s', e)
242 def __apply_ansible_playbooks(ansible_configs, os_creds_dict, vm_dict,
243 image_dict, flavor_dict, env_file):
245 Applies ansible playbooks to running VMs with floating IPs
246 :param ansible_configs: a list of Ansible configurations
247 :param os_creds_dict: Dictionary of OSCreds objects where the key is the
249 :param vm_dict: the dictionary of newly instantiated VMs where the name is
251 :param image_dict: the dictionary of newly instantiated images where the
253 :param flavor_dict: the dictionary of newly instantiated flavors where the
255 :param env_file: the path of the environment for setting the CWD so
256 playbook location is relative to the deployment file
257 :return: t/f - true if successful
259 logger.info("Applying Ansible Playbooks")
261 # Ensure all hosts are accepting SSH session requests
262 for vm_inst in list(vm_dict.values()):
263 if not vm_inst.vm_ssh_active(block=True):
265 "Timeout waiting for instance to respond to SSH requests")
268 # Set CWD so the deployment file's playbook location can leverage
270 orig_cwd = os.getcwd()
271 env_dir = os.path.dirname(env_file)
275 for ansible_config in ansible_configs:
276 if 'pre_sleep_time' in ansible_config:
278 sleep_time = int(ansible_config['pre_sleep_time'])
279 logger.info('Waiting %s seconds to apply playbooks',
281 time.sleep(sleep_time)
285 os_creds = os_creds_dict.get(None, 'admin')
286 __apply_ansible_playbook(ansible_config, os_creds, vm_dict,
287 image_dict, flavor_dict)
289 # Return to original directory
295 def __apply_ansible_playbook(ansible_config, os_creds, vm_dict, image_dict,
298 Applies an Ansible configuration setting
299 :param ansible_config: the configuration settings
300 :param os_creds: the OpenStack credentials object
301 :param vm_dict: the dictionary of newly instantiated VMs where the name is
303 :param image_dict: the dictionary of newly instantiated images where the
305 :param flavor_dict: the dictionary of newly instantiated flavors where the
309 (remote_user, floating_ips, private_key_filepath,
310 proxy_settings) = __get_connection_info(
311 ansible_config, vm_dict)
313 retval = ansible_utils.apply_playbook(
314 ansible_config['playbook_location'], floating_ips, remote_user,
315 private_key_filepath,
316 variables=__get_variables(ansible_config.get('variables'),
317 os_creds, vm_dict, image_dict,
319 proxy_setting=proxy_settings)
321 # Not a fatal type of event
323 'Unable to apply playbook found at location - %s',
324 ansible_config.get('playbook_location'))
327 def __get_connection_info(ansible_config, vm_dict):
329 Returns a tuple of data required for connecting to the running VMs
330 (remote_user, [floating_ips], private_key_filepath, proxy_settings)
331 :param ansible_config: the configuration settings
332 :param vm_dict: the dictionary of VMs where the VM name is the key
333 :return: tuple where the first element is the user and the second is a list
334 of floating IPs and the third is the
335 private key file location and the fourth is an instance of the
336 snaps.ProxySettings class
337 (note: in order to work, each of the hosts need to have the same sudo_user
338 and private key file location values)
340 if ansible_config.get('hosts'):
341 hosts = ansible_config['hosts']
343 floating_ips = list()
346 proxy_settings = None
348 vm = vm_dict.get(host)
350 fip = vm.get_floating_ip()
352 remote_user = vm.get_image_user()
355 floating_ips.append(fip.ip)
358 'Could not find floating IP for VM - ' +
361 pk_file = vm.keypair_settings.private_filepath
362 proxy_settings = vm.get_os_creds().proxy_settings
364 logger.error('Could not locate VM with name - ' + host)
366 return remote_user, floating_ips, pk_file, proxy_settings
370 def __get_variables(var_config, os_creds, vm_dict, image_dict, flavor_dict):
372 Returns a dictionary of substitution variables to be used for Ansible
374 :param var_config: the variable configuration settings
375 :param os_creds: the OpenStack credentials object
376 :param vm_dict: the dictionary of newly instantiated VMs where the name is
378 :param image_dict: the dictionary of newly instantiated images where the
380 :param flavor_dict: the dictionary of newly instantiated flavors where the
382 :return: dictionary or None
384 if var_config and vm_dict and len(vm_dict) > 0:
386 for key, value in var_config.items():
387 value = __get_variable_value(value, os_creds, vm_dict, image_dict,
390 variables[key] = value
392 "Set Jinga2 variable with key [%s] the value [%s]",
395 logger.warning('Key [%s] or Value [%s] must not be None',
396 str(key), str(value))
401 def __get_variable_value(var_config_values, os_creds, vm_dict, image_dict,
404 Returns the associated variable value for use by Ansible for substitution
406 :param var_config_values: the configuration dictionary
407 :param os_creds: the OpenStack credentials object
408 :param vm_dict: the dictionary of newly instantiated VMs where the name is
410 :param image_dict: the dictionary of newly instantiated images where the
412 :param flavor_dict: the dictionary of newly instantiated flavors where the
416 if var_config_values['type'] == 'string':
417 return __get_string_variable_value(var_config_values)
418 if var_config_values['type'] == 'vm-attr':
419 return __get_vm_attr_variable_value(var_config_values, vm_dict)
420 if var_config_values['type'] == 'os_creds':
421 return __get_os_creds_variable_value(var_config_values, os_creds)
422 if var_config_values['type'] == 'port':
423 return __get_vm_port_variable_value(var_config_values, vm_dict)
424 if var_config_values['type'] == 'floating_ip':
425 return __get_vm_fip_variable_value(var_config_values, vm_dict)
426 if var_config_values['type'] == 'image':
427 return __get_image_variable_value(var_config_values, image_dict)
428 if var_config_values['type'] == 'flavor':
429 return __get_flavor_variable_value(var_config_values, flavor_dict)
433 def __get_string_variable_value(var_config_values):
435 Returns the associated string value
436 :param var_config_values: the configuration dictionary
437 :return: the value contained in the dictionary with the key 'value'
439 return var_config_values['value']
442 def __get_vm_attr_variable_value(var_config_values, vm_dict):
444 Returns the associated value contained on a VM instance
445 :param var_config_values: the configuration dictionary
446 :param vm_dict: the dictionary containing all VMs where the key is the VM's
450 vm = vm_dict.get(var_config_values['vm_name'])
452 if var_config_values['value'] == 'floating_ip':
453 return vm.get_floating_ip().ip
454 if var_config_values['value'] == 'image_user':
455 return vm.get_image_user()
458 def __get_os_creds_variable_value(var_config_values, os_creds):
460 Returns the associated OS credentials value
461 :param var_config_values: the configuration dictionary
462 :param os_creds: the credentials
465 logger.info("Retrieving OS Credentials")
467 if var_config_values['value'] == 'username':
468 logger.info("Returning OS username")
469 return os_creds.username
470 elif var_config_values['value'] == 'password':
471 logger.info("Returning OS password")
472 return os_creds.password
473 elif var_config_values['value'] == 'auth_url':
474 logger.info("Returning OS auth_url")
475 return os_creds.auth_url
476 elif var_config_values['value'] == 'project_name':
477 logger.info("Returning OS project_name")
478 return os_creds.project_name
480 logger.info("Returning none")
484 def __get_vm_port_variable_value(var_config_values, vm_dict):
486 Returns the associated OS credentials value
487 :param var_config_values: the configuration dictionary
488 :param vm_dict: the dictionary containing all VMs where the key is the VM's
492 port_name = var_config_values.get('port_name')
493 vm_name = var_config_values.get('vm_name')
495 if port_name and vm_name:
496 vm = vm_dict.get(vm_name)
498 port_value_id = var_config_values.get('port_value')
500 if port_value_id == 'mac_address':
501 return vm.get_port_mac(port_name)
502 if port_value_id == 'ip_address':
503 return vm.get_port_ip(port_name)
506 def __get_vm_fip_variable_value(var_config_values, vm_dict):
508 Returns the floating IP value if found
509 :param var_config_values: the configuration dictionary
510 :param vm_dict: the dictionary containing all VMs where the key is the VM's
512 :return: the floating IP string value or None
514 fip_name = var_config_values.get('fip_name')
515 vm_name = var_config_values.get('vm_name')
518 vm = vm_dict.get(vm_name)
520 fip = vm.get_floating_ip(fip_name)
525 def __get_image_variable_value(var_config_values, image_dict):
527 Returns the associated image value
528 :param var_config_values: the configuration dictionary
529 :param image_dict: the dictionary containing all images where the key is
533 logger.info("Retrieving image values")
536 if var_config_values.get('image_name'):
537 image_creator = image_dict.get(var_config_values['image_name'])
539 if var_config_values.get('value') and \
540 var_config_values['value'] == 'id':
541 return image_creator.get_image().id
542 if var_config_values.get('value') and \
543 var_config_values['value'] == 'user':
544 return image_creator.image_settings.image_user
546 logger.info("Returning none")
550 def __get_flavor_variable_value(var_config_values, flavor_dict):
552 Returns the associated flavor value
553 :param var_config_values: the configuration dictionary
554 :param flavor_dict: the dictionary containing all flavor creators where the
556 :return: the value or None
558 logger.info("Retrieving flavor values")
561 if var_config_values.get('flavor_name'):
562 flavor_creator = flavor_dict.get(var_config_values['flavor_name'])
564 if var_config_values.get('value') and \
565 var_config_values['value'] == 'id':
566 return flavor_creator.get_flavor().id
571 Will need to set environment variable ANSIBLE_HOST_KEY_CHECKING=False or
572 Create a file located in /etc/ansible/ansible/cfg or ~/.ansible.cfg
573 containing the following content:
576 host_key_checking = False
578 CWD must be this directory where this script is located.
582 log_level = logging.INFO
583 if arguments.log_level != 'INFO':
584 log_level = logging.DEBUG
585 logging.basicConfig(level=log_level)
587 logger.info('Starting to Deploy')
589 # Apply env_file/substitution file to template
590 env = Environment(loader=FileSystemLoader(
591 searchpath=os.path.dirname(arguments.tmplt_file)))
592 template = env.get_template(os.path.basename(arguments.tmplt_file))
595 if arguments.env_file:
596 env_dict = file_utils.read_yaml(arguments.env_file)
597 output = template.render(**env_dict)
599 config = yaml.load(output)
602 os_config = config.get('openstack')
607 flavors_dict = dict()
608 os_creds_dict = dict()
609 clean = arguments.clean is not ARG_NOT_SET
612 os_creds_dict = __get_creds_dict(os_config)
616 projects_dict = __create_instances(
617 os_creds_dict, OpenStackProject, ProjectSettings,
618 os_config.get('projects'), 'project', clean)
619 creators.append(projects_dict)
622 users_dict = __create_instances(
623 os_creds_dict, OpenStackUser, UserSettings,
624 os_config.get('users'), 'user', clean)
625 creators.append(users_dict)
627 # Associate new users to projects
629 for project_creator in projects_dict.values():
630 users = project_creator.project_settings.users
631 for user_name in users:
632 user_creator = users_dict.get(user_name)
634 project_creator.assoc_user(
635 user_creator.get_user())
638 flavors_dict = __create_instances(
639 os_creds_dict, OpenStackFlavor, FlavorConfig,
640 os_config.get('flavors'), 'flavor', clean, users_dict)
641 creators.append(flavors_dict)
644 qos_dict = __create_instances(
645 os_creds_dict, OpenStackQoS, QoSSettings,
646 os_config.get('qos_specs'), 'qos_spec', clean, users_dict)
647 creators.append(qos_dict)
649 # Create volume types
650 vol_type_dict = __create_instances(
651 os_creds_dict, OpenStackVolumeType, VolumeTypeSettings,
652 os_config.get('volume_types'), 'volume_type', clean,
654 creators.append(vol_type_dict)
656 # Create volume types
657 vol_dict = __create_instances(
658 os_creds_dict, OpenStackVolume, VolumeSettings,
659 os_config.get('volumes'), 'volume', clean, users_dict)
660 creators.append(vol_dict)
663 images_dict = __create_instances(
664 os_creds_dict, OpenStackImage, ImageConfig,
665 os_config.get('images'), 'image', clean, users_dict)
666 creators.append(images_dict)
669 creators.append(__create_instances(
670 os_creds_dict, OpenStackNetwork, NetworkSettings,
671 os_config.get('networks'), 'network', clean, users_dict))
674 creators.append(__create_instances(
675 os_creds_dict, OpenStackRouter, RouterSettings,
676 os_config.get('routers'), 'router', clean, users_dict))
679 keypairs_dict = __create_instances(
680 os_creds_dict, OpenStackKeypair, KeypairSettings,
681 os_config.get('keypairs'), 'keypair', clean, users_dict)
682 creators.append(keypairs_dict)
684 # Create security groups
685 creators.append(__create_instances(
686 os_creds_dict, OpenStackSecurityGroup,
687 SecurityGroupSettings,
688 os_config.get('security_groups'), 'security_group', clean,
692 vm_dict = __create_vm_instances(
693 os_creds_dict, users_dict, os_config.get('instances'),
694 images_dict, keypairs_dict,
695 arguments.clean is not ARG_NOT_SET)
696 creators.append(vm_dict)
698 'Completed creating/retrieving all configured instances')
699 except Exception as e:
701 'Unexpected error deploying environment. Rolling back due'
705 # Must enter either block
706 if arguments.clean is not ARG_NOT_SET:
707 # Cleanup Environment
708 __cleanup(creators, arguments.clean_image is not ARG_NOT_SET)
709 elif arguments.deploy is not ARG_NOT_SET:
710 logger.info('Configuring NICs where required')
711 for vm in vm_dict.values():
713 logger.info('Completed NIC configuration')
716 ansible_config = config.get('ansible')
717 if ansible_config and vm_dict:
718 if not __apply_ansible_playbooks(ansible_config,
719 os_creds_dict, vm_dict,
720 images_dict, flavors_dict,
721 arguments.tmplt_file):
722 logger.error("Problem applying ansible playbooks")
725 'Unable to read configuration file - ' + arguments.tmplt_file)
731 def __cleanup(creators, clean_image=False):
732 for creator_dict in reversed(creators):
733 for key, creator in creator_dict.items():
734 if ((isinstance(creator, OpenStackImage) and clean_image)
735 or not isinstance(creator, OpenStackImage)):
738 except Exception as e:
739 logger.warning('Error cleaning component - %s', e)
742 if __name__ == '__main__':
743 # To ensure any files referenced via a relative path will begin from the
744 # directory in which this file resides
745 os.chdir(os.path.dirname(os.path.realpath(__file__)))
747 parser = argparse.ArgumentParser()
749 '-d', '--deploy', dest='deploy', nargs='?', default=ARG_NOT_SET,
750 help='When used, environment will be deployed and provisioned')
752 '-c', '--clean', dest='clean', nargs='?', default=ARG_NOT_SET,
753 help='When used, the environment will be removed')
755 '-i', '--clean-image', dest='clean_image', nargs='?',
757 help='When cleaning, if this is set, the image will be cleaned too')
759 '-t', '--tmplt', dest='tmplt_file', required=True,
760 help='The SNAPS deployment template YAML file - REQUIRED')
762 '-e', '--env-file', dest='env_file',
763 help='Yaml file containing substitution values to the env file')
765 '-l', '--log-level', dest='log_level', default='INFO',
766 help='Logging Level (INFO|DEBUG)')
767 args = parser.parse_args()
769 if args.deploy is ARG_NOT_SET and args.clean is ARG_NOT_SET:
771 'Must enter either -d for deploy or -c for cleaning up and '
774 if args.deploy is not ARG_NOT_SET and args.clean is not ARG_NOT_SET:
775 print('Cannot enter both options -d/--deploy and -c/--clean')