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.config.keypair import KeypairConfig
32 from snaps.config.project import ProjectConfig
33 from snaps.config.router import RouterConfig
34 from snaps.config.user import UserConfig
35 from snaps.openstack.create_flavor import OpenStackFlavor
36 from snaps.openstack.create_image import OpenStackImage
37 from snaps.openstack.create_instance import VmInstanceSettings
38 from snaps.openstack.create_keypairs import OpenStackKeypair
39 from snaps.openstack.create_network import (
40 PortSettings, NetworkSettings, OpenStackNetwork)
41 from snaps.openstack.create_project import OpenStackProject
42 from snaps.openstack.create_qos import QoSSettings, OpenStackQoS
43 from snaps.openstack.create_router import OpenStackRouter
44 from snaps.openstack.create_security_group import (
45 OpenStackSecurityGroup, SecurityGroupSettings)
46 from snaps.openstack.create_user import OpenStackUser
47 from snaps.openstack.create_volume import OpenStackVolume, VolumeSettings
48 from snaps.openstack.create_volume_type import (
49 OpenStackVolumeType, VolumeTypeSettings)
50 from snaps.openstack.os_credentials import OSCreds, ProxySettings
51 from snaps.openstack.utils import deploy_utils
52 from snaps.provisioning import ansible_utils
54 __author__ = 'spisarski'
56 logger = logging.getLogger('snaps_launcher')
58 ARG_NOT_SET = "argument not set"
59 DEFAULT_CREDS_KEY = 'admin'
62 def __get_creds_dict(os_conn_config):
64 Returns a dict of OSCreds where the key is the creds name.
65 For backwards compatibility, credentials not contained in a list (only
66 one) will be returned with the key of None
67 :param os_conn_config: the credential configuration
68 :return: a dict of OSCreds objects
70 if 'connection' in os_conn_config:
71 return {DEFAULT_CREDS_KEY: __get_os_credentials(os_conn_config)}
72 elif 'connections' in os_conn_config:
74 for os_conn_dict in os_conn_config['connections']:
75 config = os_conn_dict.get('connection')
77 raise Exception('Invalid connection format')
79 name = config.get('name')
81 raise Exception('Connection config requires a name field')
83 out[name] = __get_os_credentials(os_conn_dict)
87 def __get_creds(os_creds_dict, os_user_dict, inst_config):
89 Returns the appropriate credentials
90 :param os_creds_dict: a dictionary of OSCreds objects where the name is the
92 :param os_user_dict: a dictionary of OpenStackUser objects where the name
95 :return: an OSCreds instance or None
97 os_creds = os_creds_dict.get(DEFAULT_CREDS_KEY)
98 if 'os_user' in inst_config:
99 os_user_conf = inst_config['os_user']
100 if 'name' in os_user_conf:
101 user_creator = os_user_dict.get(os_user_conf['name'])
103 return user_creator.get_os_creds(
104 project_name=os_user_conf.get('project_name'))
105 elif 'os_creds_name' in inst_config:
106 if 'os_creds_name' in inst_config:
107 os_creds = os_creds_dict[inst_config['os_creds_name']]
111 def __get_os_credentials(os_conn_config):
113 Returns an object containing all of the information required to access
115 :param os_conn_config: The configuration holding the credentials
116 :return: an OSCreds instance
118 config = os_conn_config.get('connection')
120 raise Exception('Invalid connection configuration')
122 proxy_settings = None
123 http_proxy = config.get('http_proxy')
125 tokens = re.split(':', http_proxy)
126 ssh_proxy_cmd = config.get('ssh_proxy_cmd')
127 proxy_settings = ProxySettings(host=tokens[0], port=tokens[1],
128 ssh_proxy_cmd=ssh_proxy_cmd)
130 if 'proxy_settings' in config:
131 host = config['proxy_settings'].get('host')
132 port = config['proxy_settings'].get('port')
133 if host and host != 'None' and port and port != 'None':
134 proxy_settings = ProxySettings(**config['proxy_settings'])
137 config['proxy_settings'] = proxy_settings
139 if config.get('proxy_settings'):
140 del config['proxy_settings']
142 return OSCreds(**config)
145 def __parse_ports_config(config):
147 Parses the "ports" configuration
148 :param config: The dictionary to parse
149 :return: a list of PortConfig objects
152 for port_config in config:
153 out.append(PortSettings(**port_config.get('port')))
157 def __create_instances(os_creds_dict, creator_class, config_class, config,
158 config_key, cleanup=False, os_users_dict=None):
160 Returns a dictionary of SNAPS creator objects where the key is the name
161 :param os_creds_dict: Dictionary of OSCreds objects where the key is the
163 :param config: The list of configurations for the same type
164 :param config_key: The list of configurations for the same type
165 :param cleanup: Denotes whether or not this is being called for cleanup
172 for config_dict in config:
173 inst_config = config_dict.get(config_key)
175 creator = creator_class(
176 __get_creds(os_creds_dict, os_users_dict, inst_config),
177 config_class(**inst_config))
183 out[inst_config['name']] = creator
184 logger.info('Created configured %s', config_key)
185 except Exception as e:
186 logger.error('Unexpected error instantiating creator [%s] '
187 'with exception %s', creator_class, e)
192 def __create_vm_instances(os_creds_dict, os_users_dict, instances_config,
193 image_dict, keypairs_dict, cleanup=False):
195 Returns a dictionary of OpenStackVmInstance objects where the key is the
197 :param os_creds_dict: Dictionary of OSCreds objects where the key is the
199 :param os_users_dict: Dictionary of OpenStackUser objects where the key is
201 :param instances_config: The list of VM instance configurations
202 :param image_dict: A dictionary of images that will probably be used to
203 instantiate the VM instance
204 :param keypairs_dict: A dictionary of keypairs that will probably be used
205 to instantiate the VM instance
206 :param cleanup: Denotes whether or not this is being called for cleanup
213 for instance_config in instances_config:
214 conf = instance_config.get('instance')
217 image_creator = image_dict.get(conf.get('imageName'))
219 instance_settings = VmInstanceSettings(
220 **instance_config['instance'])
221 kp_creator = keypairs_dict.get(
222 conf.get('keypair_name'))
224 'name']] = deploy_utils.create_vm_instance(
226 os_creds_dict, os_users_dict, conf),
228 image_creator.image_settings,
229 keypair_creator=kp_creator,
232 raise Exception('Image creator instance not found.'
233 ' Cannot instantiate')
235 raise Exception('Image dictionary is None. Cannot '
238 raise Exception('Instance configuration is None. Cannot '
240 logger.info('Created configured instances')
241 except Exception as e:
242 logger.error('Unexpected error creating VM instances - %s', e)
246 def __apply_ansible_playbooks(ansible_configs, os_creds_dict, vm_dict,
247 image_dict, flavor_dict, env_file):
249 Applies ansible playbooks to running VMs with floating IPs
250 :param ansible_configs: a list of Ansible configurations
251 :param os_creds_dict: Dictionary of OSCreds objects where the key is the
253 :param vm_dict: the dictionary of newly instantiated VMs where the name is
255 :param image_dict: the dictionary of newly instantiated images where the
257 :param flavor_dict: the dictionary of newly instantiated flavors where the
259 :param env_file: the path of the environment for setting the CWD so
260 playbook location is relative to the deployment file
261 :return: t/f - true if successful
263 logger.info("Applying Ansible Playbooks")
265 # Ensure all hosts are accepting SSH session requests
266 for vm_inst in list(vm_dict.values()):
267 if not vm_inst.vm_ssh_active(block=True):
269 "Timeout waiting for instance to respond to SSH requests")
272 # Set CWD so the deployment file's playbook location can leverage
274 orig_cwd = os.getcwd()
275 env_dir = os.path.dirname(env_file)
279 for ansible_config in ansible_configs:
280 if 'pre_sleep_time' in ansible_config:
282 sleep_time = int(ansible_config['pre_sleep_time'])
283 logger.info('Waiting %s seconds to apply playbooks',
285 time.sleep(sleep_time)
289 os_creds = os_creds_dict.get(None, 'admin')
290 __apply_ansible_playbook(ansible_config, os_creds, vm_dict,
291 image_dict, flavor_dict)
293 # Return to original directory
299 def __apply_ansible_playbook(ansible_config, os_creds, vm_dict, image_dict,
302 Applies an Ansible configuration setting
303 :param ansible_config: the configuration settings
304 :param os_creds: the OpenStack credentials object
305 :param vm_dict: the dictionary of newly instantiated VMs where the name is
307 :param image_dict: the dictionary of newly instantiated images where the
309 :param flavor_dict: the dictionary of newly instantiated flavors where the
313 (remote_user, floating_ips, private_key_filepath,
314 proxy_settings) = __get_connection_info(
315 ansible_config, vm_dict)
317 retval = ansible_utils.apply_playbook(
318 ansible_config['playbook_location'], floating_ips, remote_user,
319 private_key_filepath,
320 variables=__get_variables(ansible_config.get('variables'),
321 os_creds, vm_dict, image_dict,
323 proxy_setting=proxy_settings)
325 # Not a fatal type of event
327 'Unable to apply playbook found at location - %s',
328 ansible_config.get('playbook_location'))
331 def __get_connection_info(ansible_config, vm_dict):
333 Returns a tuple of data required for connecting to the running VMs
334 (remote_user, [floating_ips], private_key_filepath, proxy_settings)
335 :param ansible_config: the configuration settings
336 :param vm_dict: the dictionary of VMs where the VM name is the key
337 :return: tuple where the first element is the user and the second is a list
338 of floating IPs and the third is the
339 private key file location and the fourth is an instance of the
340 snaps.ProxySettings class
341 (note: in order to work, each of the hosts need to have the same sudo_user
342 and private key file location values)
344 if ansible_config.get('hosts'):
345 hosts = ansible_config['hosts']
347 floating_ips = list()
350 proxy_settings = None
352 vm = vm_dict.get(host)
354 fip = vm.get_floating_ip()
356 remote_user = vm.get_image_user()
359 floating_ips.append(fip.ip)
362 'Could not find floating IP for VM - ' +
365 pk_file = vm.keypair_settings.private_filepath
366 proxy_settings = vm.get_os_creds().proxy_settings
368 logger.error('Could not locate VM with name - ' + host)
370 return remote_user, floating_ips, pk_file, proxy_settings
374 def __get_variables(var_config, os_creds, vm_dict, image_dict, flavor_dict):
376 Returns a dictionary of substitution variables to be used for Ansible
378 :param var_config: the variable configuration settings
379 :param os_creds: the OpenStack credentials object
380 :param vm_dict: the dictionary of newly instantiated VMs where the name is
382 :param image_dict: the dictionary of newly instantiated images where the
384 :param flavor_dict: the dictionary of newly instantiated flavors where the
386 :return: dictionary or None
388 if var_config and vm_dict and len(vm_dict) > 0:
390 for key, value in var_config.items():
391 value = __get_variable_value(value, os_creds, vm_dict, image_dict,
394 variables[key] = value
396 "Set Jinga2 variable with key [%s] the value [%s]",
399 logger.warning('Key [%s] or Value [%s] must not be None',
400 str(key), str(value))
405 def __get_variable_value(var_config_values, os_creds, vm_dict, image_dict,
408 Returns the associated variable value for use by Ansible for substitution
410 :param var_config_values: the configuration dictionary
411 :param os_creds: the OpenStack credentials object
412 :param vm_dict: the dictionary of newly instantiated VMs where the name is
414 :param image_dict: the dictionary of newly instantiated images where the
416 :param flavor_dict: the dictionary of newly instantiated flavors where the
420 if var_config_values['type'] == 'string':
421 return __get_string_variable_value(var_config_values)
422 if var_config_values['type'] == 'vm-attr':
423 return __get_vm_attr_variable_value(var_config_values, vm_dict)
424 if var_config_values['type'] == 'os_creds':
425 return __get_os_creds_variable_value(var_config_values, os_creds)
426 if var_config_values['type'] == 'port':
427 return __get_vm_port_variable_value(var_config_values, vm_dict)
428 if var_config_values['type'] == 'floating_ip':
429 return __get_vm_fip_variable_value(var_config_values, vm_dict)
430 if var_config_values['type'] == 'image':
431 return __get_image_variable_value(var_config_values, image_dict)
432 if var_config_values['type'] == 'flavor':
433 return __get_flavor_variable_value(var_config_values, flavor_dict)
437 def __get_string_variable_value(var_config_values):
439 Returns the associated string value
440 :param var_config_values: the configuration dictionary
441 :return: the value contained in the dictionary with the key 'value'
443 return var_config_values['value']
446 def __get_vm_attr_variable_value(var_config_values, vm_dict):
448 Returns the associated value contained on a VM instance
449 :param var_config_values: the configuration dictionary
450 :param vm_dict: the dictionary containing all VMs where the key is the VM's
454 vm = vm_dict.get(var_config_values['vm_name'])
456 if var_config_values['value'] == 'floating_ip':
457 return vm.get_floating_ip().ip
458 if var_config_values['value'] == 'image_user':
459 return vm.get_image_user()
462 def __get_os_creds_variable_value(var_config_values, os_creds):
464 Returns the associated OS credentials value
465 :param var_config_values: the configuration dictionary
466 :param os_creds: the credentials
469 logger.info("Retrieving OS Credentials")
471 if var_config_values['value'] == 'username':
472 logger.info("Returning OS username")
473 return os_creds.username
474 elif var_config_values['value'] == 'password':
475 logger.info("Returning OS password")
476 return os_creds.password
477 elif var_config_values['value'] == 'auth_url':
478 logger.info("Returning OS auth_url")
479 return os_creds.auth_url
480 elif var_config_values['value'] == 'project_name':
481 logger.info("Returning OS project_name")
482 return os_creds.project_name
484 logger.info("Returning none")
488 def __get_vm_port_variable_value(var_config_values, vm_dict):
490 Returns the associated OS credentials value
491 :param var_config_values: the configuration dictionary
492 :param vm_dict: the dictionary containing all VMs where the key is the VM's
496 port_name = var_config_values.get('port_name')
497 vm_name = var_config_values.get('vm_name')
499 if port_name and vm_name:
500 vm = vm_dict.get(vm_name)
502 port_value_id = var_config_values.get('port_value')
504 if port_value_id == 'mac_address':
505 return vm.get_port_mac(port_name)
506 if port_value_id == 'ip_address':
507 return vm.get_port_ip(port_name)
510 def __get_vm_fip_variable_value(var_config_values, vm_dict):
512 Returns the floating IP value if found
513 :param var_config_values: the configuration dictionary
514 :param vm_dict: the dictionary containing all VMs where the key is the VM's
516 :return: the floating IP string value or None
518 fip_name = var_config_values.get('fip_name')
519 vm_name = var_config_values.get('vm_name')
522 vm = vm_dict.get(vm_name)
524 fip = vm.get_floating_ip(fip_name)
529 def __get_image_variable_value(var_config_values, image_dict):
531 Returns the associated image value
532 :param var_config_values: the configuration dictionary
533 :param image_dict: the dictionary containing all images where the key is
537 logger.info("Retrieving image values")
540 if var_config_values.get('image_name'):
541 image_creator = image_dict.get(var_config_values['image_name'])
543 if var_config_values.get('value') and \
544 var_config_values['value'] == 'id':
545 return image_creator.get_image().id
546 if var_config_values.get('value') and \
547 var_config_values['value'] == 'user':
548 return image_creator.image_settings.image_user
550 logger.info("Returning none")
554 def __get_flavor_variable_value(var_config_values, flavor_dict):
556 Returns the associated flavor value
557 :param var_config_values: the configuration dictionary
558 :param flavor_dict: the dictionary containing all flavor creators where the
560 :return: the value or None
562 logger.info("Retrieving flavor values")
565 if var_config_values.get('flavor_name'):
566 flavor_creator = flavor_dict.get(var_config_values['flavor_name'])
568 if var_config_values.get('value') and \
569 var_config_values['value'] == 'id':
570 return flavor_creator.get_flavor().id
575 Will need to set environment variable ANSIBLE_HOST_KEY_CHECKING=False or
576 Create a file located in /etc/ansible/ansible/cfg or ~/.ansible.cfg
577 containing the following content:
580 host_key_checking = False
582 CWD must be this directory where this script is located.
586 log_level = logging.INFO
587 if arguments.log_level != 'INFO':
588 log_level = logging.DEBUG
589 logging.basicConfig(level=log_level)
591 logger.info('Starting to Deploy')
593 # Apply env_file/substitution file to template
594 env = Environment(loader=FileSystemLoader(
595 searchpath=os.path.dirname(arguments.tmplt_file)))
596 template = env.get_template(os.path.basename(arguments.tmplt_file))
599 if arguments.env_file:
600 env_dict = file_utils.read_yaml(arguments.env_file)
601 output = template.render(**env_dict)
603 config = yaml.load(output)
606 os_config = config.get('openstack')
611 flavors_dict = dict()
612 os_creds_dict = dict()
613 clean = arguments.clean is not ARG_NOT_SET
616 os_creds_dict = __get_creds_dict(os_config)
620 projects_dict = __create_instances(
621 os_creds_dict, OpenStackProject, ProjectConfig,
622 os_config.get('projects'), 'project', clean)
623 creators.append(projects_dict)
626 users_dict = __create_instances(
627 os_creds_dict, OpenStackUser, UserConfig,
628 os_config.get('users'), 'user', clean)
629 creators.append(users_dict)
631 # Associate new users to projects
633 for project_creator in projects_dict.values():
634 users = project_creator.project_settings.users
635 for user_name in users:
636 user_creator = users_dict.get(user_name)
638 project_creator.assoc_user(
639 user_creator.get_user())
642 flavors_dict = __create_instances(
643 os_creds_dict, OpenStackFlavor, FlavorConfig,
644 os_config.get('flavors'), 'flavor', clean, users_dict)
645 creators.append(flavors_dict)
648 qos_dict = __create_instances(
649 os_creds_dict, OpenStackQoS, QoSSettings,
650 os_config.get('qos_specs'), 'qos_spec', clean, users_dict)
651 creators.append(qos_dict)
653 # Create volume types
654 vol_type_dict = __create_instances(
655 os_creds_dict, OpenStackVolumeType, VolumeTypeSettings,
656 os_config.get('volume_types'), 'volume_type', clean,
658 creators.append(vol_type_dict)
660 # Create volume types
661 vol_dict = __create_instances(
662 os_creds_dict, OpenStackVolume, VolumeSettings,
663 os_config.get('volumes'), 'volume', clean, users_dict)
664 creators.append(vol_dict)
667 images_dict = __create_instances(
668 os_creds_dict, OpenStackImage, ImageConfig,
669 os_config.get('images'), 'image', clean, users_dict)
670 creators.append(images_dict)
673 creators.append(__create_instances(
674 os_creds_dict, OpenStackNetwork, NetworkSettings,
675 os_config.get('networks'), 'network', clean, users_dict))
678 creators.append(__create_instances(
679 os_creds_dict, OpenStackRouter, RouterConfig,
680 os_config.get('routers'), 'router', clean, users_dict))
683 keypairs_dict = __create_instances(
684 os_creds_dict, OpenStackKeypair, KeypairConfig,
685 os_config.get('keypairs'), 'keypair', clean, users_dict)
686 creators.append(keypairs_dict)
688 # Create security groups
689 creators.append(__create_instances(
690 os_creds_dict, OpenStackSecurityGroup,
691 SecurityGroupSettings,
692 os_config.get('security_groups'), 'security_group', clean,
696 vm_dict = __create_vm_instances(
697 os_creds_dict, users_dict, os_config.get('instances'),
698 images_dict, keypairs_dict,
699 arguments.clean is not ARG_NOT_SET)
700 creators.append(vm_dict)
702 'Completed creating/retrieving all configured instances')
703 except Exception as e:
705 'Unexpected error deploying environment. Rolling back due'
709 # Must enter either block
710 if arguments.clean is not ARG_NOT_SET:
711 # Cleanup Environment
712 __cleanup(creators, arguments.clean_image is not ARG_NOT_SET)
713 elif arguments.deploy is not ARG_NOT_SET:
714 logger.info('Configuring NICs where required')
715 for vm in vm_dict.values():
717 logger.info('Completed NIC configuration')
720 ansible_config = config.get('ansible')
721 if ansible_config and vm_dict:
722 if not __apply_ansible_playbooks(ansible_config,
723 os_creds_dict, vm_dict,
724 images_dict, flavors_dict,
725 arguments.tmplt_file):
726 logger.error("Problem applying ansible playbooks")
729 'Unable to read configuration file - ' + arguments.tmplt_file)
735 def __cleanup(creators, clean_image=False):
736 for creator_dict in reversed(creators):
737 for key, creator in creator_dict.items():
738 if ((isinstance(creator, OpenStackImage) and clean_image)
739 or not isinstance(creator, OpenStackImage)):
742 except Exception as e:
743 logger.warning('Error cleaning component - %s', e)
746 if __name__ == '__main__':
747 # To ensure any files referenced via a relative path will begin from the
748 # directory in which this file resides
749 os.chdir(os.path.dirname(os.path.realpath(__file__)))
751 parser = argparse.ArgumentParser()
753 '-d', '--deploy', dest='deploy', nargs='?', default=ARG_NOT_SET,
754 help='When used, environment will be deployed and provisioned')
756 '-c', '--clean', dest='clean', nargs='?', default=ARG_NOT_SET,
757 help='When used, the environment will be removed')
759 '-i', '--clean-image', dest='clean_image', nargs='?',
761 help='When cleaning, if this is set, the image will be cleaned too')
763 '-t', '--tmplt', dest='tmplt_file', required=True,
764 help='The SNAPS deployment template YAML file - REQUIRED')
766 '-e', '--env-file', dest='env_file',
767 help='Yaml file containing substitution values to the env file')
769 '-l', '--log-level', dest='log_level', default='INFO',
770 help='Logging Level (INFO|DEBUG)')
771 args = parser.parse_args()
773 if args.deploy is ARG_NOT_SET and args.clean is ARG_NOT_SET:
775 'Must enter either -d for deploy or -c for cleaning up and '
778 if args.deploy is not ARG_NOT_SET and args.clean is not ARG_NOT_SET:
779 print('Cannot enter both options -d/--deploy and -c/--clean')