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.qos import QoSConfig
34 from snaps.config.router import RouterConfig
35 from snaps.config.user import UserConfig
36 from snaps.openstack.create_flavor import OpenStackFlavor
37 from snaps.openstack.create_image import OpenStackImage
38 from snaps.openstack.create_instance import VmInstanceSettings
39 from snaps.openstack.create_keypairs import OpenStackKeypair
40 from snaps.openstack.create_network import (
41 PortSettings, NetworkSettings, OpenStackNetwork)
42 from snaps.openstack.create_project import OpenStackProject
43 from snaps.openstack.create_qos import OpenStackQoS
44 from snaps.openstack.create_router import OpenStackRouter
45 from snaps.openstack.create_security_group import (
46 OpenStackSecurityGroup, SecurityGroupSettings)
47 from snaps.openstack.create_user import OpenStackUser
48 from snaps.openstack.create_volume import OpenStackVolume, VolumeSettings
49 from snaps.openstack.create_volume_type import (
50 OpenStackVolumeType, VolumeTypeSettings)
51 from snaps.openstack.os_credentials import OSCreds, ProxySettings
52 from snaps.openstack.utils import deploy_utils
53 from snaps.provisioning import ansible_utils
55 __author__ = 'spisarski'
57 logger = logging.getLogger('snaps_launcher')
59 ARG_NOT_SET = "argument not set"
60 DEFAULT_CREDS_KEY = 'admin'
63 def __get_creds_dict(os_conn_config):
65 Returns a dict of OSCreds where the key is the creds name.
66 For backwards compatibility, credentials not contained in a list (only
67 one) will be returned with the key of None
68 :param os_conn_config: the credential configuration
69 :return: a dict of OSCreds objects
71 if 'connection' in os_conn_config:
72 return {DEFAULT_CREDS_KEY: __get_os_credentials(os_conn_config)}
73 elif 'connections' in os_conn_config:
75 for os_conn_dict in os_conn_config['connections']:
76 config = os_conn_dict.get('connection')
78 raise Exception('Invalid connection format')
80 name = config.get('name')
82 raise Exception('Connection config requires a name field')
84 out[name] = __get_os_credentials(os_conn_dict)
88 def __get_creds(os_creds_dict, os_user_dict, inst_config):
90 Returns the appropriate credentials
91 :param os_creds_dict: a dictionary of OSCreds objects where the name is the
93 :param os_user_dict: a dictionary of OpenStackUser objects where the name
96 :return: an OSCreds instance or None
98 os_creds = os_creds_dict.get(DEFAULT_CREDS_KEY)
99 if 'os_user' in inst_config:
100 os_user_conf = inst_config['os_user']
101 if 'name' in os_user_conf:
102 user_creator = os_user_dict.get(os_user_conf['name'])
104 return user_creator.get_os_creds(
105 project_name=os_user_conf.get('project_name'))
106 elif 'os_creds_name' in inst_config:
107 if 'os_creds_name' in inst_config:
108 os_creds = os_creds_dict[inst_config['os_creds_name']]
112 def __get_os_credentials(os_conn_config):
114 Returns an object containing all of the information required to access
116 :param os_conn_config: The configuration holding the credentials
117 :return: an OSCreds instance
119 config = os_conn_config.get('connection')
121 raise Exception('Invalid connection configuration')
123 proxy_settings = None
124 http_proxy = config.get('http_proxy')
126 tokens = re.split(':', http_proxy)
127 ssh_proxy_cmd = config.get('ssh_proxy_cmd')
128 proxy_settings = ProxySettings(host=tokens[0], port=tokens[1],
129 ssh_proxy_cmd=ssh_proxy_cmd)
131 if 'proxy_settings' in config:
132 host = config['proxy_settings'].get('host')
133 port = config['proxy_settings'].get('port')
134 if host and host != 'None' and port and port != 'None':
135 proxy_settings = ProxySettings(**config['proxy_settings'])
138 config['proxy_settings'] = proxy_settings
140 if config.get('proxy_settings'):
141 del config['proxy_settings']
143 return OSCreds(**config)
146 def __parse_ports_config(config):
148 Parses the "ports" configuration
149 :param config: The dictionary to parse
150 :return: a list of PortConfig objects
153 for port_config in config:
154 out.append(PortSettings(**port_config.get('port')))
158 def __create_instances(os_creds_dict, creator_class, config_class, config,
159 config_key, cleanup=False, os_users_dict=None):
161 Returns a dictionary of SNAPS creator objects where the key is the name
162 :param os_creds_dict: Dictionary of OSCreds objects where the key is the
164 :param config: The list of configurations for the same type
165 :param config_key: The list of configurations for the same type
166 :param cleanup: Denotes whether or not this is being called for cleanup
173 for config_dict in config:
174 inst_config = config_dict.get(config_key)
176 creator = creator_class(
177 __get_creds(os_creds_dict, os_users_dict, inst_config),
178 config_class(**inst_config))
184 out[inst_config['name']] = creator
185 logger.info('Created configured %s', config_key)
186 except Exception as e:
187 logger.error('Unexpected error instantiating creator [%s] '
188 'with exception %s', creator_class, e)
193 def __create_vm_instances(os_creds_dict, os_users_dict, instances_config,
194 image_dict, keypairs_dict, cleanup=False):
196 Returns a dictionary of OpenStackVmInstance objects where the key is the
198 :param os_creds_dict: Dictionary of OSCreds objects where the key is the
200 :param os_users_dict: Dictionary of OpenStackUser objects where the key is
202 :param instances_config: The list of VM instance configurations
203 :param image_dict: A dictionary of images that will probably be used to
204 instantiate the VM instance
205 :param keypairs_dict: A dictionary of keypairs that will probably be used
206 to instantiate the VM instance
207 :param cleanup: Denotes whether or not this is being called for cleanup
214 for instance_config in instances_config:
215 conf = instance_config.get('instance')
218 image_creator = image_dict.get(conf.get('imageName'))
220 instance_settings = VmInstanceSettings(
221 **instance_config['instance'])
222 kp_creator = keypairs_dict.get(
223 conf.get('keypair_name'))
225 'name']] = deploy_utils.create_vm_instance(
227 os_creds_dict, os_users_dict, conf),
229 image_creator.image_settings,
230 keypair_creator=kp_creator,
233 raise Exception('Image creator instance not found.'
234 ' Cannot instantiate')
236 raise Exception('Image dictionary is None. Cannot '
239 raise Exception('Instance configuration is None. Cannot '
241 logger.info('Created configured instances')
242 except Exception as e:
243 logger.error('Unexpected error creating VM instances - %s', e)
247 def __apply_ansible_playbooks(ansible_configs, os_creds_dict, vm_dict,
248 image_dict, flavor_dict, env_file):
250 Applies ansible playbooks to running VMs with floating IPs
251 :param ansible_configs: a list of Ansible configurations
252 :param os_creds_dict: Dictionary of OSCreds objects where the key is the
254 :param vm_dict: the dictionary of newly instantiated VMs where the name is
256 :param image_dict: the dictionary of newly instantiated images where the
258 :param flavor_dict: the dictionary of newly instantiated flavors where the
260 :param env_file: the path of the environment for setting the CWD so
261 playbook location is relative to the deployment file
262 :return: t/f - true if successful
264 logger.info("Applying Ansible Playbooks")
266 # Ensure all hosts are accepting SSH session requests
267 for vm_inst in list(vm_dict.values()):
268 if not vm_inst.vm_ssh_active(block=True):
270 "Timeout waiting for instance to respond to SSH requests")
273 # Set CWD so the deployment file's playbook location can leverage
275 orig_cwd = os.getcwd()
276 env_dir = os.path.dirname(env_file)
280 for ansible_config in ansible_configs:
281 if 'pre_sleep_time' in ansible_config:
283 sleep_time = int(ansible_config['pre_sleep_time'])
284 logger.info('Waiting %s seconds to apply playbooks',
286 time.sleep(sleep_time)
290 os_creds = os_creds_dict.get(None, 'admin')
291 __apply_ansible_playbook(ansible_config, os_creds, vm_dict,
292 image_dict, flavor_dict)
294 # Return to original directory
300 def __apply_ansible_playbook(ansible_config, os_creds, vm_dict, image_dict,
303 Applies an Ansible configuration setting
304 :param ansible_config: the configuration settings
305 :param os_creds: the OpenStack credentials object
306 :param vm_dict: the dictionary of newly instantiated VMs where the name is
308 :param image_dict: the dictionary of newly instantiated images where the
310 :param flavor_dict: the dictionary of newly instantiated flavors where the
314 (remote_user, floating_ips, private_key_filepath,
315 proxy_settings) = __get_connection_info(
316 ansible_config, vm_dict)
318 retval = ansible_utils.apply_playbook(
319 ansible_config['playbook_location'], floating_ips, remote_user,
320 private_key_filepath,
321 variables=__get_variables(ansible_config.get('variables'),
322 os_creds, vm_dict, image_dict,
324 proxy_setting=proxy_settings)
326 # Not a fatal type of event
328 'Unable to apply playbook found at location - %s',
329 ansible_config.get('playbook_location'))
332 def __get_connection_info(ansible_config, vm_dict):
334 Returns a tuple of data required for connecting to the running VMs
335 (remote_user, [floating_ips], private_key_filepath, proxy_settings)
336 :param ansible_config: the configuration settings
337 :param vm_dict: the dictionary of VMs where the VM name is the key
338 :return: tuple where the first element is the user and the second is a list
339 of floating IPs and the third is the
340 private key file location and the fourth is an instance of the
341 snaps.ProxySettings class
342 (note: in order to work, each of the hosts need to have the same sudo_user
343 and private key file location values)
345 if ansible_config.get('hosts'):
346 hosts = ansible_config['hosts']
348 floating_ips = list()
351 proxy_settings = None
353 vm = vm_dict.get(host)
355 fip = vm.get_floating_ip()
357 remote_user = vm.get_image_user()
360 floating_ips.append(fip.ip)
363 'Could not find floating IP for VM - ' +
366 pk_file = vm.keypair_settings.private_filepath
367 proxy_settings = vm.get_os_creds().proxy_settings
369 logger.error('Could not locate VM with name - ' + host)
371 return remote_user, floating_ips, pk_file, proxy_settings
375 def __get_variables(var_config, os_creds, vm_dict, image_dict, flavor_dict):
377 Returns a dictionary of substitution variables to be used for Ansible
379 :param var_config: the variable configuration settings
380 :param os_creds: the OpenStack credentials object
381 :param vm_dict: the dictionary of newly instantiated VMs where the name is
383 :param image_dict: the dictionary of newly instantiated images where the
385 :param flavor_dict: the dictionary of newly instantiated flavors where the
387 :return: dictionary or None
389 if var_config and vm_dict and len(vm_dict) > 0:
391 for key, value in var_config.items():
392 value = __get_variable_value(value, os_creds, vm_dict, image_dict,
395 variables[key] = value
397 "Set Jinga2 variable with key [%s] the value [%s]",
400 logger.warning('Key [%s] or Value [%s] must not be None',
401 str(key), str(value))
406 def __get_variable_value(var_config_values, os_creds, vm_dict, image_dict,
409 Returns the associated variable value for use by Ansible for substitution
411 :param var_config_values: the configuration dictionary
412 :param os_creds: the OpenStack credentials object
413 :param vm_dict: the dictionary of newly instantiated VMs where the name is
415 :param image_dict: the dictionary of newly instantiated images where the
417 :param flavor_dict: the dictionary of newly instantiated flavors where the
421 if var_config_values['type'] == 'string':
422 return __get_string_variable_value(var_config_values)
423 if var_config_values['type'] == 'vm-attr':
424 return __get_vm_attr_variable_value(var_config_values, vm_dict)
425 if var_config_values['type'] == 'os_creds':
426 return __get_os_creds_variable_value(var_config_values, os_creds)
427 if var_config_values['type'] == 'port':
428 return __get_vm_port_variable_value(var_config_values, vm_dict)
429 if var_config_values['type'] == 'floating_ip':
430 return __get_vm_fip_variable_value(var_config_values, vm_dict)
431 if var_config_values['type'] == 'image':
432 return __get_image_variable_value(var_config_values, image_dict)
433 if var_config_values['type'] == 'flavor':
434 return __get_flavor_variable_value(var_config_values, flavor_dict)
438 def __get_string_variable_value(var_config_values):
440 Returns the associated string value
441 :param var_config_values: the configuration dictionary
442 :return: the value contained in the dictionary with the key 'value'
444 return var_config_values['value']
447 def __get_vm_attr_variable_value(var_config_values, vm_dict):
449 Returns the associated value contained on a VM instance
450 :param var_config_values: the configuration dictionary
451 :param vm_dict: the dictionary containing all VMs where the key is the VM's
455 vm = vm_dict.get(var_config_values['vm_name'])
457 if var_config_values['value'] == 'floating_ip':
458 return vm.get_floating_ip().ip
459 if var_config_values['value'] == 'image_user':
460 return vm.get_image_user()
463 def __get_os_creds_variable_value(var_config_values, os_creds):
465 Returns the associated OS credentials value
466 :param var_config_values: the configuration dictionary
467 :param os_creds: the credentials
470 logger.info("Retrieving OS Credentials")
472 if var_config_values['value'] == 'username':
473 logger.info("Returning OS username")
474 return os_creds.username
475 elif var_config_values['value'] == 'password':
476 logger.info("Returning OS password")
477 return os_creds.password
478 elif var_config_values['value'] == 'auth_url':
479 logger.info("Returning OS auth_url")
480 return os_creds.auth_url
481 elif var_config_values['value'] == 'project_name':
482 logger.info("Returning OS project_name")
483 return os_creds.project_name
485 logger.info("Returning none")
489 def __get_vm_port_variable_value(var_config_values, vm_dict):
491 Returns the associated OS credentials value
492 :param var_config_values: the configuration dictionary
493 :param vm_dict: the dictionary containing all VMs where the key is the VM's
497 port_name = var_config_values.get('port_name')
498 vm_name = var_config_values.get('vm_name')
500 if port_name and vm_name:
501 vm = vm_dict.get(vm_name)
503 port_value_id = var_config_values.get('port_value')
505 if port_value_id == 'mac_address':
506 return vm.get_port_mac(port_name)
507 if port_value_id == 'ip_address':
508 return vm.get_port_ip(port_name)
511 def __get_vm_fip_variable_value(var_config_values, vm_dict):
513 Returns the floating IP value if found
514 :param var_config_values: the configuration dictionary
515 :param vm_dict: the dictionary containing all VMs where the key is the VM's
517 :return: the floating IP string value or None
519 fip_name = var_config_values.get('fip_name')
520 vm_name = var_config_values.get('vm_name')
523 vm = vm_dict.get(vm_name)
525 fip = vm.get_floating_ip(fip_name)
530 def __get_image_variable_value(var_config_values, image_dict):
532 Returns the associated image value
533 :param var_config_values: the configuration dictionary
534 :param image_dict: the dictionary containing all images where the key is
538 logger.info("Retrieving image values")
541 if var_config_values.get('image_name'):
542 image_creator = image_dict.get(var_config_values['image_name'])
544 if var_config_values.get('value') and \
545 var_config_values['value'] == 'id':
546 return image_creator.get_image().id
547 if var_config_values.get('value') and \
548 var_config_values['value'] == 'user':
549 return image_creator.image_settings.image_user
551 logger.info("Returning none")
555 def __get_flavor_variable_value(var_config_values, flavor_dict):
557 Returns the associated flavor value
558 :param var_config_values: the configuration dictionary
559 :param flavor_dict: the dictionary containing all flavor creators where the
561 :return: the value or None
563 logger.info("Retrieving flavor values")
566 if var_config_values.get('flavor_name'):
567 flavor_creator = flavor_dict.get(var_config_values['flavor_name'])
569 if var_config_values.get('value') and \
570 var_config_values['value'] == 'id':
571 return flavor_creator.get_flavor().id
576 Will need to set environment variable ANSIBLE_HOST_KEY_CHECKING=False or
577 Create a file located in /etc/ansible/ansible/cfg or ~/.ansible.cfg
578 containing the following content:
581 host_key_checking = False
583 CWD must be this directory where this script is located.
587 log_level = logging.INFO
588 if arguments.log_level != 'INFO':
589 log_level = logging.DEBUG
590 logging.basicConfig(level=log_level)
592 logger.info('Starting to Deploy')
594 # Apply env_file/substitution file to template
595 env = Environment(loader=FileSystemLoader(
596 searchpath=os.path.dirname(arguments.tmplt_file)))
597 template = env.get_template(os.path.basename(arguments.tmplt_file))
600 if arguments.env_file:
601 env_dict = file_utils.read_yaml(arguments.env_file)
602 output = template.render(**env_dict)
604 config = yaml.load(output)
607 os_config = config.get('openstack')
612 flavors_dict = dict()
613 os_creds_dict = dict()
614 clean = arguments.clean is not ARG_NOT_SET
617 os_creds_dict = __get_creds_dict(os_config)
621 projects_dict = __create_instances(
622 os_creds_dict, OpenStackProject, ProjectConfig,
623 os_config.get('projects'), 'project', clean)
624 creators.append(projects_dict)
627 users_dict = __create_instances(
628 os_creds_dict, OpenStackUser, UserConfig,
629 os_config.get('users'), 'user', clean)
630 creators.append(users_dict)
632 # Associate new users to projects
634 for project_creator in projects_dict.values():
635 users = project_creator.project_settings.users
636 for user_name in users:
637 user_creator = users_dict.get(user_name)
639 project_creator.assoc_user(
640 user_creator.get_user())
643 flavors_dict = __create_instances(
644 os_creds_dict, OpenStackFlavor, FlavorConfig,
645 os_config.get('flavors'), 'flavor', clean, users_dict)
646 creators.append(flavors_dict)
649 qos_dict = __create_instances(
650 os_creds_dict, OpenStackQoS, QoSConfig,
651 os_config.get('qos_specs'), 'qos_spec', clean, users_dict)
652 creators.append(qos_dict)
654 # Create volume types
655 vol_type_dict = __create_instances(
656 os_creds_dict, OpenStackVolumeType, VolumeTypeSettings,
657 os_config.get('volume_types'), 'volume_type', clean,
659 creators.append(vol_type_dict)
661 # Create volume types
662 vol_dict = __create_instances(
663 os_creds_dict, OpenStackVolume, VolumeSettings,
664 os_config.get('volumes'), 'volume', clean, users_dict)
665 creators.append(vol_dict)
668 images_dict = __create_instances(
669 os_creds_dict, OpenStackImage, ImageConfig,
670 os_config.get('images'), 'image', clean, users_dict)
671 creators.append(images_dict)
674 creators.append(__create_instances(
675 os_creds_dict, OpenStackNetwork, NetworkSettings,
676 os_config.get('networks'), 'network', clean, users_dict))
679 creators.append(__create_instances(
680 os_creds_dict, OpenStackRouter, RouterConfig,
681 os_config.get('routers'), 'router', clean, users_dict))
684 keypairs_dict = __create_instances(
685 os_creds_dict, OpenStackKeypair, KeypairConfig,
686 os_config.get('keypairs'), 'keypair', clean, users_dict)
687 creators.append(keypairs_dict)
689 # Create security groups
690 creators.append(__create_instances(
691 os_creds_dict, OpenStackSecurityGroup,
692 SecurityGroupSettings,
693 os_config.get('security_groups'), 'security_group', clean,
697 vm_dict = __create_vm_instances(
698 os_creds_dict, users_dict, os_config.get('instances'),
699 images_dict, keypairs_dict,
700 arguments.clean is not ARG_NOT_SET)
701 creators.append(vm_dict)
703 'Completed creating/retrieving all configured instances')
704 except Exception as e:
706 'Unexpected error deploying environment. Rolling back due'
710 # Must enter either block
711 if arguments.clean is not ARG_NOT_SET:
712 # Cleanup Environment
713 __cleanup(creators, arguments.clean_image is not ARG_NOT_SET)
714 elif arguments.deploy is not ARG_NOT_SET:
715 logger.info('Configuring NICs where required')
716 for vm in vm_dict.values():
718 logger.info('Completed NIC configuration')
721 ansible_config = config.get('ansible')
722 if ansible_config and vm_dict:
723 if not __apply_ansible_playbooks(ansible_config,
724 os_creds_dict, vm_dict,
725 images_dict, flavors_dict,
726 arguments.tmplt_file):
727 logger.error("Problem applying ansible playbooks")
730 'Unable to read configuration file - ' + arguments.tmplt_file)
736 def __cleanup(creators, clean_image=False):
737 for creator_dict in reversed(creators):
738 for key, creator in creator_dict.items():
739 if ((isinstance(creator, OpenStackImage) and clean_image)
740 or not isinstance(creator, OpenStackImage)):
743 except Exception as e:
744 logger.warning('Error cleaning component - %s', e)
747 if __name__ == '__main__':
748 # To ensure any files referenced via a relative path will begin from the
749 # directory in which this file resides
750 os.chdir(os.path.dirname(os.path.realpath(__file__)))
752 parser = argparse.ArgumentParser()
754 '-d', '--deploy', dest='deploy', nargs='?', default=ARG_NOT_SET,
755 help='When used, environment will be deployed and provisioned')
757 '-c', '--clean', dest='clean', nargs='?', default=ARG_NOT_SET,
758 help='When used, the environment will be removed')
760 '-i', '--clean-image', dest='clean_image', nargs='?',
762 help='When cleaning, if this is set, the image will be cleaned too')
764 '-t', '--tmplt', dest='tmplt_file', required=True,
765 help='The SNAPS deployment template YAML file - REQUIRED')
767 '-e', '--env-file', dest='env_file',
768 help='Yaml file containing substitution values to the env file')
770 '-l', '--log-level', dest='log_level', default='INFO',
771 help='Logging Level (INFO|DEBUG)')
772 args = parser.parse_args()
774 if args.deploy is ARG_NOT_SET and args.clean is ARG_NOT_SET:
776 'Must enter either -d for deploy or -c for cleaning up and '
779 if args.deploy is not ARG_NOT_SET and args.clean is not ARG_NOT_SET:
780 print('Cannot enter both options -d/--deploy and -c/--clean')