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.user import UserConfig
34 from snaps.openstack.create_flavor import OpenStackFlavor
35 from snaps.openstack.create_image import OpenStackImage
36 from snaps.openstack.create_instance import VmInstanceSettings
37 from snaps.openstack.create_keypairs import OpenStackKeypair
38 from snaps.openstack.create_network import (
39 PortSettings, NetworkSettings, OpenStackNetwork)
40 from snaps.openstack.create_project import OpenStackProject
41 from snaps.openstack.create_qos import QoSSettings, OpenStackQoS
42 from snaps.openstack.create_router import RouterSettings, OpenStackRouter
43 from snaps.openstack.create_security_group import (
44 OpenStackSecurityGroup, SecurityGroupSettings)
45 from snaps.openstack.create_user import OpenStackUser
46 from snaps.openstack.create_volume import OpenStackVolume, VolumeSettings
47 from snaps.openstack.create_volume_type import (
48 OpenStackVolumeType, VolumeTypeSettings)
49 from snaps.openstack.os_credentials import OSCreds, ProxySettings
50 from snaps.openstack.utils import deploy_utils
51 from snaps.provisioning import ansible_utils
53 __author__ = 'spisarski'
55 logger = logging.getLogger('snaps_launcher')
57 ARG_NOT_SET = "argument not set"
58 DEFAULT_CREDS_KEY = 'admin'
61 def __get_creds_dict(os_conn_config):
63 Returns a dict of OSCreds where the key is the creds name.
64 For backwards compatibility, credentials not contained in a list (only
65 one) will be returned with the key of None
66 :param os_conn_config: the credential configuration
67 :return: a dict of OSCreds objects
69 if 'connection' in os_conn_config:
70 return {DEFAULT_CREDS_KEY: __get_os_credentials(os_conn_config)}
71 elif 'connections' in os_conn_config:
73 for os_conn_dict in os_conn_config['connections']:
74 config = os_conn_dict.get('connection')
76 raise Exception('Invalid connection format')
78 name = config.get('name')
80 raise Exception('Connection config requires a name field')
82 out[name] = __get_os_credentials(os_conn_dict)
86 def __get_creds(os_creds_dict, os_user_dict, inst_config):
88 Returns the appropriate credentials
89 :param os_creds_dict: a dictionary of OSCreds objects where the name is the
91 :param os_user_dict: a dictionary of OpenStackUser objects where the name
94 :return: an OSCreds instance or None
96 os_creds = os_creds_dict.get(DEFAULT_CREDS_KEY)
97 if 'os_user' in inst_config:
98 os_user_conf = inst_config['os_user']
99 if 'name' in os_user_conf:
100 user_creator = os_user_dict.get(os_user_conf['name'])
102 return user_creator.get_os_creds(
103 project_name=os_user_conf.get('project_name'))
104 elif 'os_creds_name' in inst_config:
105 if 'os_creds_name' in inst_config:
106 os_creds = os_creds_dict[inst_config['os_creds_name']]
110 def __get_os_credentials(os_conn_config):
112 Returns an object containing all of the information required to access
114 :param os_conn_config: The configuration holding the credentials
115 :return: an OSCreds instance
117 config = os_conn_config.get('connection')
119 raise Exception('Invalid connection configuration')
121 proxy_settings = None
122 http_proxy = config.get('http_proxy')
124 tokens = re.split(':', http_proxy)
125 ssh_proxy_cmd = config.get('ssh_proxy_cmd')
126 proxy_settings = ProxySettings(host=tokens[0], port=tokens[1],
127 ssh_proxy_cmd=ssh_proxy_cmd)
129 if 'proxy_settings' in config:
130 host = config['proxy_settings'].get('host')
131 port = config['proxy_settings'].get('port')
132 if host and host != 'None' and port and port != 'None':
133 proxy_settings = ProxySettings(**config['proxy_settings'])
136 config['proxy_settings'] = proxy_settings
138 if config.get('proxy_settings'):
139 del config['proxy_settings']
141 return OSCreds(**config)
144 def __parse_ports_config(config):
146 Parses the "ports" configuration
147 :param config: The dictionary to parse
148 :return: a list of PortConfig objects
151 for port_config in config:
152 out.append(PortSettings(**port_config.get('port')))
156 def __create_instances(os_creds_dict, creator_class, config_class, config,
157 config_key, cleanup=False, os_users_dict=None):
159 Returns a dictionary of SNAPS creator objects where the key is the name
160 :param os_creds_dict: Dictionary of OSCreds objects where the key is the
162 :param config: The list of configurations for the same type
163 :param config_key: The list of configurations for the same type
164 :param cleanup: Denotes whether or not this is being called for cleanup
171 for config_dict in config:
172 inst_config = config_dict.get(config_key)
174 creator = creator_class(
175 __get_creds(os_creds_dict, os_users_dict, inst_config),
176 config_class(**inst_config))
182 out[inst_config['name']] = creator
183 logger.info('Created configured %s', config_key)
184 except Exception as e:
185 logger.error('Unexpected error instantiating creator [%s] '
186 'with exception %s', creator_class, e)
191 def __create_vm_instances(os_creds_dict, os_users_dict, instances_config,
192 image_dict, keypairs_dict, cleanup=False):
194 Returns a dictionary of OpenStackVmInstance objects where the key is the
196 :param os_creds_dict: Dictionary of OSCreds objects where the key is the
198 :param os_users_dict: Dictionary of OpenStackUser objects where the key is
200 :param instances_config: The list of VM instance configurations
201 :param image_dict: A dictionary of images that will probably be used to
202 instantiate the VM instance
203 :param keypairs_dict: A dictionary of keypairs that will probably be used
204 to instantiate the VM instance
205 :param cleanup: Denotes whether or not this is being called for cleanup
212 for instance_config in instances_config:
213 conf = instance_config.get('instance')
216 image_creator = image_dict.get(conf.get('imageName'))
218 instance_settings = VmInstanceSettings(
219 **instance_config['instance'])
220 kp_creator = keypairs_dict.get(
221 conf.get('keypair_name'))
223 'name']] = deploy_utils.create_vm_instance(
225 os_creds_dict, os_users_dict, conf),
227 image_creator.image_settings,
228 keypair_creator=kp_creator,
231 raise Exception('Image creator instance not found.'
232 ' Cannot instantiate')
234 raise Exception('Image dictionary is None. Cannot '
237 raise Exception('Instance configuration is None. Cannot '
239 logger.info('Created configured instances')
240 except Exception as e:
241 logger.error('Unexpected error creating VM instances - %s', e)
245 def __apply_ansible_playbooks(ansible_configs, os_creds_dict, vm_dict,
246 image_dict, flavor_dict, env_file):
248 Applies ansible playbooks to running VMs with floating IPs
249 :param ansible_configs: a list of Ansible configurations
250 :param os_creds_dict: Dictionary of OSCreds objects where the key is the
252 :param vm_dict: the dictionary of newly instantiated VMs where the name is
254 :param image_dict: the dictionary of newly instantiated images where the
256 :param flavor_dict: the dictionary of newly instantiated flavors where the
258 :param env_file: the path of the environment for setting the CWD so
259 playbook location is relative to the deployment file
260 :return: t/f - true if successful
262 logger.info("Applying Ansible Playbooks")
264 # Ensure all hosts are accepting SSH session requests
265 for vm_inst in list(vm_dict.values()):
266 if not vm_inst.vm_ssh_active(block=True):
268 "Timeout waiting for instance to respond to SSH requests")
271 # Set CWD so the deployment file's playbook location can leverage
273 orig_cwd = os.getcwd()
274 env_dir = os.path.dirname(env_file)
278 for ansible_config in ansible_configs:
279 if 'pre_sleep_time' in ansible_config:
281 sleep_time = int(ansible_config['pre_sleep_time'])
282 logger.info('Waiting %s seconds to apply playbooks',
284 time.sleep(sleep_time)
288 os_creds = os_creds_dict.get(None, 'admin')
289 __apply_ansible_playbook(ansible_config, os_creds, vm_dict,
290 image_dict, flavor_dict)
292 # Return to original directory
298 def __apply_ansible_playbook(ansible_config, os_creds, vm_dict, image_dict,
301 Applies an Ansible configuration setting
302 :param ansible_config: the configuration settings
303 :param os_creds: the OpenStack credentials object
304 :param vm_dict: the dictionary of newly instantiated VMs where the name is
306 :param image_dict: the dictionary of newly instantiated images where the
308 :param flavor_dict: the dictionary of newly instantiated flavors where the
312 (remote_user, floating_ips, private_key_filepath,
313 proxy_settings) = __get_connection_info(
314 ansible_config, vm_dict)
316 retval = ansible_utils.apply_playbook(
317 ansible_config['playbook_location'], floating_ips, remote_user,
318 private_key_filepath,
319 variables=__get_variables(ansible_config.get('variables'),
320 os_creds, vm_dict, image_dict,
322 proxy_setting=proxy_settings)
324 # Not a fatal type of event
326 'Unable to apply playbook found at location - %s',
327 ansible_config.get('playbook_location'))
330 def __get_connection_info(ansible_config, vm_dict):
332 Returns a tuple of data required for connecting to the running VMs
333 (remote_user, [floating_ips], private_key_filepath, proxy_settings)
334 :param ansible_config: the configuration settings
335 :param vm_dict: the dictionary of VMs where the VM name is the key
336 :return: tuple where the first element is the user and the second is a list
337 of floating IPs and the third is the
338 private key file location and the fourth is an instance of the
339 snaps.ProxySettings class
340 (note: in order to work, each of the hosts need to have the same sudo_user
341 and private key file location values)
343 if ansible_config.get('hosts'):
344 hosts = ansible_config['hosts']
346 floating_ips = list()
349 proxy_settings = None
351 vm = vm_dict.get(host)
353 fip = vm.get_floating_ip()
355 remote_user = vm.get_image_user()
358 floating_ips.append(fip.ip)
361 'Could not find floating IP for VM - ' +
364 pk_file = vm.keypair_settings.private_filepath
365 proxy_settings = vm.get_os_creds().proxy_settings
367 logger.error('Could not locate VM with name - ' + host)
369 return remote_user, floating_ips, pk_file, proxy_settings
373 def __get_variables(var_config, os_creds, vm_dict, image_dict, flavor_dict):
375 Returns a dictionary of substitution variables to be used for Ansible
377 :param var_config: the variable configuration settings
378 :param os_creds: the OpenStack credentials object
379 :param vm_dict: the dictionary of newly instantiated VMs where the name is
381 :param image_dict: the dictionary of newly instantiated images where the
383 :param flavor_dict: the dictionary of newly instantiated flavors where the
385 :return: dictionary or None
387 if var_config and vm_dict and len(vm_dict) > 0:
389 for key, value in var_config.items():
390 value = __get_variable_value(value, os_creds, vm_dict, image_dict,
393 variables[key] = value
395 "Set Jinga2 variable with key [%s] the value [%s]",
398 logger.warning('Key [%s] or Value [%s] must not be None',
399 str(key), str(value))
404 def __get_variable_value(var_config_values, os_creds, vm_dict, image_dict,
407 Returns the associated variable value for use by Ansible for substitution
409 :param var_config_values: the configuration dictionary
410 :param os_creds: the OpenStack credentials object
411 :param vm_dict: the dictionary of newly instantiated VMs where the name is
413 :param image_dict: the dictionary of newly instantiated images where the
415 :param flavor_dict: the dictionary of newly instantiated flavors where the
419 if var_config_values['type'] == 'string':
420 return __get_string_variable_value(var_config_values)
421 if var_config_values['type'] == 'vm-attr':
422 return __get_vm_attr_variable_value(var_config_values, vm_dict)
423 if var_config_values['type'] == 'os_creds':
424 return __get_os_creds_variable_value(var_config_values, os_creds)
425 if var_config_values['type'] == 'port':
426 return __get_vm_port_variable_value(var_config_values, vm_dict)
427 if var_config_values['type'] == 'floating_ip':
428 return __get_vm_fip_variable_value(var_config_values, vm_dict)
429 if var_config_values['type'] == 'image':
430 return __get_image_variable_value(var_config_values, image_dict)
431 if var_config_values['type'] == 'flavor':
432 return __get_flavor_variable_value(var_config_values, flavor_dict)
436 def __get_string_variable_value(var_config_values):
438 Returns the associated string value
439 :param var_config_values: the configuration dictionary
440 :return: the value contained in the dictionary with the key 'value'
442 return var_config_values['value']
445 def __get_vm_attr_variable_value(var_config_values, vm_dict):
447 Returns the associated value contained on a VM instance
448 :param var_config_values: the configuration dictionary
449 :param vm_dict: the dictionary containing all VMs where the key is the VM's
453 vm = vm_dict.get(var_config_values['vm_name'])
455 if var_config_values['value'] == 'floating_ip':
456 return vm.get_floating_ip().ip
457 if var_config_values['value'] == 'image_user':
458 return vm.get_image_user()
461 def __get_os_creds_variable_value(var_config_values, os_creds):
463 Returns the associated OS credentials value
464 :param var_config_values: the configuration dictionary
465 :param os_creds: the credentials
468 logger.info("Retrieving OS Credentials")
470 if var_config_values['value'] == 'username':
471 logger.info("Returning OS username")
472 return os_creds.username
473 elif var_config_values['value'] == 'password':
474 logger.info("Returning OS password")
475 return os_creds.password
476 elif var_config_values['value'] == 'auth_url':
477 logger.info("Returning OS auth_url")
478 return os_creds.auth_url
479 elif var_config_values['value'] == 'project_name':
480 logger.info("Returning OS project_name")
481 return os_creds.project_name
483 logger.info("Returning none")
487 def __get_vm_port_variable_value(var_config_values, vm_dict):
489 Returns the associated OS credentials value
490 :param var_config_values: the configuration dictionary
491 :param vm_dict: the dictionary containing all VMs where the key is the VM's
495 port_name = var_config_values.get('port_name')
496 vm_name = var_config_values.get('vm_name')
498 if port_name and vm_name:
499 vm = vm_dict.get(vm_name)
501 port_value_id = var_config_values.get('port_value')
503 if port_value_id == 'mac_address':
504 return vm.get_port_mac(port_name)
505 if port_value_id == 'ip_address':
506 return vm.get_port_ip(port_name)
509 def __get_vm_fip_variable_value(var_config_values, vm_dict):
511 Returns the floating IP value if found
512 :param var_config_values: the configuration dictionary
513 :param vm_dict: the dictionary containing all VMs where the key is the VM's
515 :return: the floating IP string value or None
517 fip_name = var_config_values.get('fip_name')
518 vm_name = var_config_values.get('vm_name')
521 vm = vm_dict.get(vm_name)
523 fip = vm.get_floating_ip(fip_name)
528 def __get_image_variable_value(var_config_values, image_dict):
530 Returns the associated image value
531 :param var_config_values: the configuration dictionary
532 :param image_dict: the dictionary containing all images where the key is
536 logger.info("Retrieving image values")
539 if var_config_values.get('image_name'):
540 image_creator = image_dict.get(var_config_values['image_name'])
542 if var_config_values.get('value') and \
543 var_config_values['value'] == 'id':
544 return image_creator.get_image().id
545 if var_config_values.get('value') and \
546 var_config_values['value'] == 'user':
547 return image_creator.image_settings.image_user
549 logger.info("Returning none")
553 def __get_flavor_variable_value(var_config_values, flavor_dict):
555 Returns the associated flavor value
556 :param var_config_values: the configuration dictionary
557 :param flavor_dict: the dictionary containing all flavor creators where the
559 :return: the value or None
561 logger.info("Retrieving flavor values")
564 if var_config_values.get('flavor_name'):
565 flavor_creator = flavor_dict.get(var_config_values['flavor_name'])
567 if var_config_values.get('value') and \
568 var_config_values['value'] == 'id':
569 return flavor_creator.get_flavor().id
574 Will need to set environment variable ANSIBLE_HOST_KEY_CHECKING=False or
575 Create a file located in /etc/ansible/ansible/cfg or ~/.ansible.cfg
576 containing the following content:
579 host_key_checking = False
581 CWD must be this directory where this script is located.
585 log_level = logging.INFO
586 if arguments.log_level != 'INFO':
587 log_level = logging.DEBUG
588 logging.basicConfig(level=log_level)
590 logger.info('Starting to Deploy')
592 # Apply env_file/substitution file to template
593 env = Environment(loader=FileSystemLoader(
594 searchpath=os.path.dirname(arguments.tmplt_file)))
595 template = env.get_template(os.path.basename(arguments.tmplt_file))
598 if arguments.env_file:
599 env_dict = file_utils.read_yaml(arguments.env_file)
600 output = template.render(**env_dict)
602 config = yaml.load(output)
605 os_config = config.get('openstack')
610 flavors_dict = dict()
611 os_creds_dict = dict()
612 clean = arguments.clean is not ARG_NOT_SET
615 os_creds_dict = __get_creds_dict(os_config)
619 projects_dict = __create_instances(
620 os_creds_dict, OpenStackProject, ProjectConfig,
621 os_config.get('projects'), 'project', clean)
622 creators.append(projects_dict)
625 users_dict = __create_instances(
626 os_creds_dict, OpenStackUser, UserConfig,
627 os_config.get('users'), 'user', clean)
628 creators.append(users_dict)
630 # Associate new users to projects
632 for project_creator in projects_dict.values():
633 users = project_creator.project_settings.users
634 for user_name in users:
635 user_creator = users_dict.get(user_name)
637 project_creator.assoc_user(
638 user_creator.get_user())
641 flavors_dict = __create_instances(
642 os_creds_dict, OpenStackFlavor, FlavorConfig,
643 os_config.get('flavors'), 'flavor', clean, users_dict)
644 creators.append(flavors_dict)
647 qos_dict = __create_instances(
648 os_creds_dict, OpenStackQoS, QoSSettings,
649 os_config.get('qos_specs'), 'qos_spec', clean, users_dict)
650 creators.append(qos_dict)
652 # Create volume types
653 vol_type_dict = __create_instances(
654 os_creds_dict, OpenStackVolumeType, VolumeTypeSettings,
655 os_config.get('volume_types'), 'volume_type', clean,
657 creators.append(vol_type_dict)
659 # Create volume types
660 vol_dict = __create_instances(
661 os_creds_dict, OpenStackVolume, VolumeSettings,
662 os_config.get('volumes'), 'volume', clean, users_dict)
663 creators.append(vol_dict)
666 images_dict = __create_instances(
667 os_creds_dict, OpenStackImage, ImageConfig,
668 os_config.get('images'), 'image', clean, users_dict)
669 creators.append(images_dict)
672 creators.append(__create_instances(
673 os_creds_dict, OpenStackNetwork, NetworkSettings,
674 os_config.get('networks'), 'network', clean, users_dict))
677 creators.append(__create_instances(
678 os_creds_dict, OpenStackRouter, RouterSettings,
679 os_config.get('routers'), 'router', clean, users_dict))
682 keypairs_dict = __create_instances(
683 os_creds_dict, OpenStackKeypair, KeypairConfig,
684 os_config.get('keypairs'), 'keypair', clean, users_dict)
685 creators.append(keypairs_dict)
687 # Create security groups
688 creators.append(__create_instances(
689 os_creds_dict, OpenStackSecurityGroup,
690 SecurityGroupSettings,
691 os_config.get('security_groups'), 'security_group', clean,
695 vm_dict = __create_vm_instances(
696 os_creds_dict, users_dict, os_config.get('instances'),
697 images_dict, keypairs_dict,
698 arguments.clean is not ARG_NOT_SET)
699 creators.append(vm_dict)
701 'Completed creating/retrieving all configured instances')
702 except Exception as e:
704 'Unexpected error deploying environment. Rolling back due'
708 # Must enter either block
709 if arguments.clean is not ARG_NOT_SET:
710 # Cleanup Environment
711 __cleanup(creators, arguments.clean_image is not ARG_NOT_SET)
712 elif arguments.deploy is not ARG_NOT_SET:
713 logger.info('Configuring NICs where required')
714 for vm in vm_dict.values():
716 logger.info('Completed NIC configuration')
719 ansible_config = config.get('ansible')
720 if ansible_config and vm_dict:
721 if not __apply_ansible_playbooks(ansible_config,
722 os_creds_dict, vm_dict,
723 images_dict, flavors_dict,
724 arguments.tmplt_file):
725 logger.error("Problem applying ansible playbooks")
728 'Unable to read configuration file - ' + arguments.tmplt_file)
734 def __cleanup(creators, clean_image=False):
735 for creator_dict in reversed(creators):
736 for key, creator in creator_dict.items():
737 if ((isinstance(creator, OpenStackImage) and clean_image)
738 or not isinstance(creator, OpenStackImage)):
741 except Exception as e:
742 logger.warning('Error cleaning component - %s', e)
745 if __name__ == '__main__':
746 # To ensure any files referenced via a relative path will begin from the
747 # directory in which this file resides
748 os.chdir(os.path.dirname(os.path.realpath(__file__)))
750 parser = argparse.ArgumentParser()
752 '-d', '--deploy', dest='deploy', nargs='?', default=ARG_NOT_SET,
753 help='When used, environment will be deployed and provisioned')
755 '-c', '--clean', dest='clean', nargs='?', default=ARG_NOT_SET,
756 help='When used, the environment will be removed')
758 '-i', '--clean-image', dest='clean_image', nargs='?',
760 help='When cleaning, if this is set, the image will be cleaned too')
762 '-t', '--tmplt', dest='tmplt_file', required=True,
763 help='The SNAPS deployment template YAML file - REQUIRED')
765 '-e', '--env-file', dest='env_file',
766 help='Yaml file containing substitution values to the env file')
768 '-l', '--log-level', dest='log_level', default='INFO',
769 help='Logging Level (INFO|DEBUG)')
770 args = parser.parse_args()
772 if args.deploy is ARG_NOT_SET and args.clean is ARG_NOT_SET:
774 'Must enter either -d for deploy or -c for cleaning up and '
777 if args.deploy is not ARG_NOT_SET and args.clean is not ARG_NOT_SET:
778 print('Cannot enter both options -d/--deploy and -c/--clean')