3 # Copyright (c) 2016 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
25 from snaps.openstack.create_keypairs import KeypairSettings
26 from snaps.openstack.create_router import RouterSettings
27 from snaps.openstack.os_credentials import OSCreds, ProxySettings
28 from snaps.openstack.create_image import ImageSettings
29 from snaps.openstack.create_instance import VmInstanceSettings
30 from snaps.openstack.create_network import PortSettings, NetworkSettings
31 from snaps.provisioning import ansible_utils
32 from snaps.openstack.utils import deploy_utils
34 __author__ = 'spisarski'
36 logger = logging.getLogger('deploy_venv')
38 ARG_NOT_SET = "argument not set"
41 def __get_os_credentials(os_conn_config):
43 Returns an object containing all of the information required to access OpenStack APIs
44 :param os_conn_config: The configuration holding the credentials
45 :return: an OSCreds instance
48 http_proxy = os_conn_config.get('http_proxy')
50 tokens = re.split(':', http_proxy)
51 ssh_proxy_cmd = os_conn_config.get('ssh_proxy_cmd')
52 proxy_settings = ProxySettings(tokens[0], tokens[1], ssh_proxy_cmd)
54 return OSCreds(username=os_conn_config.get('username'),
55 password=os_conn_config.get('password'),
56 auth_url=os_conn_config.get('auth_url'),
57 project_name=os_conn_config.get('project_name'),
58 proxy_settings=proxy_settings)
61 def __parse_ports_config(config):
63 Parses the "ports" configuration
64 :param config: The dictionary to parse
65 :param os_creds: The OpenStack credentials object
66 :return: a list of PortConfig objects
69 for port_config in config:
70 out.append(PortSettings(config=port_config.get('port')))
74 def __create_images(os_conn_config, images_config, cleanup=False):
76 Returns a dictionary of images where the key is the image name and the value is the image object
77 :param os_conn_config: The OpenStack connection credentials
78 :param images_config: The list of image configurations
79 :param cleanup: Denotes whether or not this is being called for cleanup or not
86 for image_config_dict in images_config:
87 image_config = image_config_dict.get('image')
88 if image_config and image_config.get('name'):
89 images[image_config['name']] = deploy_utils.create_image(__get_os_credentials(os_conn_config),
90 ImageSettings(image_config), cleanup)
91 except Exception as e:
92 for key, image_creator in images.iteritems():
95 logger.info('Created configured images')
100 def __create_networks(os_conn_config, network_confs, cleanup=False):
102 Returns a dictionary of networks where the key is the network name and the value is the network object
103 :param os_conn_config: The OpenStack connection credentials
104 :param network_confs: The list of network configurations
105 :param cleanup: Denotes whether or not this is being called for cleanup or not
112 for network_conf in network_confs:
113 net_name = network_conf['network']['name']
114 os_creds = __get_os_credentials(os_conn_config)
115 network_dict[net_name] = deploy_utils.create_network(
116 os_creds, NetworkSettings(config=network_conf['network']), cleanup)
117 except Exception as e:
118 for key, net_creator in network_dict.iteritems():
122 logger.info('Created configured networks')
127 def __create_routers(os_conn_config, router_confs, cleanup=False):
129 Returns a dictionary of networks where the key is the network name and the value is the network object
130 :param os_conn_config: The OpenStack connection credentials
131 :param router_confs: The list of router configurations
132 :param cleanup: Denotes whether or not this is being called for cleanup or not
136 os_creds = __get_os_credentials(os_conn_config)
140 for router_conf in router_confs:
141 router_name = router_conf['router']['name']
142 router_dict[router_name] = deploy_utils.create_router(
143 os_creds, RouterSettings(config=router_conf['router']), cleanup)
144 except Exception as e:
145 for key, router_creator in router_dict.iteritems():
146 router_creator.clean()
149 logger.info('Created configured networks')
154 def __create_keypairs(os_conn_config, keypair_confs, cleanup=False):
156 Returns a dictionary of keypairs where the key is the keypair name and the value is the keypair object
157 :param os_conn_config: The OpenStack connection credentials
158 :param keypair_confs: The list of keypair configurations
159 :param cleanup: Denotes whether or not this is being called for cleanup or not
165 for keypair_dict in keypair_confs:
166 keypair_config = keypair_dict['keypair']
167 kp_settings = KeypairSettings(keypair_config)
168 keypairs_dict[keypair_config['name']] = deploy_utils.create_keypair(
169 __get_os_credentials(os_conn_config), kp_settings, cleanup)
170 except Exception as e:
171 for key, keypair_creator in keypairs_dict.iteritems():
172 keypair_creator.clean()
175 logger.info('Created configured keypairs')
180 def __create_instances(os_conn_config, instances_config, image_dict, keypairs_dict, cleanup=False):
182 Returns a dictionary of instances where the key is the instance name and the value is the VM object
183 :param os_conn_config: The OpenStack connection credentials
184 :param instances_config: The list of VM instance configurations
185 :param image_dict: A dictionary of images that will probably be used to instantiate the VM instance
186 :param keypairs_dict: A dictionary of keypairs that will probably be used to instantiate the VM instance
187 :param cleanup: Denotes whether or not this is being called for cleanup or not
190 os_creds = __get_os_credentials(os_conn_config)
196 for instance_config in instances_config:
197 conf = instance_config.get('instance')
200 image_creator = image_dict.get(conf.get('imageName'))
202 instance_settings = VmInstanceSettings(config=instance_config['instance'])
203 kp_name = conf.get('keypair_name')
204 vm_dict[conf['name']] = deploy_utils.create_vm_instance(
205 os_creds, instance_settings, image_creator.image_settings,
206 keypair_creator=keypairs_dict[kp_name], cleanup=cleanup)
208 raise Exception('Image creator instance not found. Cannot instantiate')
210 raise Exception('Image dictionary is None. Cannot instantiate')
212 raise Exception('Instance configuration is None. Cannot instantiate')
213 except Exception as e:
214 logger.error('Unexpected error creating instances. Attempting to cleanup environment - ' + e.message)
215 for key, inst_creator in vm_dict.iteritems():
219 logger.info('Created configured instances')
224 def __apply_ansible_playbooks(ansible_configs, vm_dict, env_file):
226 Applies ansible playbooks to running VMs with floating IPs
227 :param ansible_configs: a list of Ansible configurations
228 :param vm_dict: the dictionary of newly instantiated VMs where the VM name is the key
229 :param env_file: the path of the environment for setting the CWD so playbook location is relative to the deployment
231 :return: t/f - true if successful
233 logger.info("Applying Ansible Playbooks")
235 # Ensure all hosts are accepting SSH session requests
236 for vm_inst in vm_dict.values():
237 if not vm_inst.vm_ssh_active(block=True):
238 logger.warn("Timeout waiting for instance to respond to SSH requests")
241 # Set CWD so the deployment file's playbook location can leverage relative paths
242 orig_cwd = os.getcwd()
243 env_dir = os.path.dirname(env_file)
247 for ansible_config in ansible_configs:
248 __apply_ansible_playbook(ansible_config, vm_dict)
250 # Return to original directory
256 def __apply_ansible_playbook(ansible_config, vm_dict):
258 Applies an Ansible configuration setting
259 :param ansible_config: the configuration settings
260 :param vm_dict: the dictionary of newly instantiated VMs where the VM name is the key
264 remote_user, floating_ips, private_key_filepath, proxy_settings = __get_connection_info(ansible_config, vm_dict)
266 ansible_utils.apply_playbook(ansible_config['playbook_location'], floating_ips, remote_user,
267 private_key_filepath,
268 variables=__get_variables(ansible_config.get('variables'), vm_dict),
269 proxy_setting=proxy_settings)
272 def __get_connection_info(ansible_config, vm_dict):
274 Returns a tuple of data required for connecting to the running VMs
275 (remote_user, [floating_ips], private_key_filepath, proxy_settings)
276 :param ansible_config: the configuration settings
277 :param vm_dict: the dictionary of VMs where the VM name is the key
278 :return: tuple where the first element is the user and the second is a list of floating IPs and the third is the
279 private key file location and the fourth is an instance of the snaps.ProxySettings class
280 (note: in order to work, each of the hosts need to have the same sudo_user and private key file location values)
282 if ansible_config.get('hosts'):
283 hosts = ansible_config['hosts']
285 floating_ips = list()
287 private_key_filepath = None
288 proxy_settings = None
290 vm = vm_dict.get(host)
291 fip = vm.get_floating_ip()
293 remote_user = vm.get_image_user()
296 floating_ips.append(fip.ip)
298 raise Exception('Could not find floating IP for VM - ' + vm.name)
300 private_key_filepath = vm.keypair_settings.private_filepath
301 proxy_settings = vm.get_os_creds().proxy_settings
303 return remote_user, floating_ips, private_key_filepath, proxy_settings
307 def __get_variables(var_config, vm_dict):
309 Returns a dictionary of substitution variables to be used for Ansible templates
310 :param var_config: the variable configuration settings
311 :param vm_dict: the dictionary of VMs where the VM name is the key
312 :return: dictionary or None
314 if var_config and vm_dict and len(vm_dict) > 0:
316 for key, value in var_config.iteritems():
317 value = __get_variable_value(value, vm_dict)
319 variables[key] = value
320 logger.info("Set Jinga2 variable with key [" + key + "] the value [" + value + ']')
322 logger.warn('Key [' + str(key) + '] or Value [' + str(value) + '] must not be None')
327 def __get_variable_value(var_config_values, vm_dict):
329 Returns the associated variable value for use by Ansible for substitution purposes
330 :param var_config_values: the configuration dictionary
331 :param vm_dict: the dictionary containing all VMs where the key is the VM's name
334 if var_config_values['type'] == 'string':
335 return __get_string_variable_value(var_config_values)
336 if var_config_values['type'] == 'vm-attr':
337 return __get_vm_attr_variable_value(var_config_values, vm_dict)
338 if var_config_values['type'] == 'os_creds':
339 return __get_os_creds_variable_value(var_config_values, vm_dict)
340 if var_config_values['type'] == 'port':
341 return __get_vm_port_variable_value(var_config_values, vm_dict)
345 def __get_string_variable_value(var_config_values):
347 Returns the associated string value
348 :param var_config_values: the configuration dictionary
349 :return: the value contained in the dictionary with the key 'value'
351 return var_config_values['value']
354 def __get_vm_attr_variable_value(var_config_values, vm_dict):
356 Returns the associated value contained on a VM instance
357 :param var_config_values: the configuration dictionary
358 :param vm_dict: the dictionary containing all VMs where the key is the VM's name
361 vm = vm_dict.get(var_config_values['vm_name'])
363 if var_config_values['value'] == 'floating_ip':
364 return vm.get_floating_ip().ip
367 def __get_os_creds_variable_value(var_config_values, vm_dict):
369 Returns the associated OS credentials value
370 :param var_config_values: the configuration dictionary
371 :param vm_dict: the dictionary containing all VMs where the key is the VM's name
374 logger.info("Retrieving OS Credentials")
375 vm = vm_dict.values()[0]
378 if var_config_values['value'] == 'username':
379 logger.info("Returning OS username")
380 return vm.get_os_creds().username
381 elif var_config_values['value'] == 'password':
382 logger.info("Returning OS password")
383 return vm.get_os_creds().password
384 elif var_config_values['value'] == 'auth_url':
385 logger.info("Returning OS auth_url")
386 return vm.get_os_creds().auth_url
387 elif var_config_values['value'] == 'project_name':
388 logger.info("Returning OS project_name")
389 return vm.get_os_creds().project_name
391 logger.info("Returning none")
395 def __get_vm_port_variable_value(var_config_values, vm_dict):
397 Returns the associated OS credentials value
398 :param var_config_values: the configuration dictionary
399 :param vm_dict: the dictionary containing all VMs where the key is the VM's name
402 port_name = var_config_values.get('port_name')
403 vm_name = var_config_values.get('vm_name')
405 if port_name and vm_name:
406 vm = vm_dict.get(vm_name)
408 port_value_id = var_config_values.get('port_value')
410 if port_value_id == 'mac_address':
411 return vm.get_port_mac(port_name)
412 if port_value_id == 'ip_address':
413 return vm.get_port_ip(port_name)
418 Will need to set environment variable ANSIBLE_HOST_KEY_CHECKING=False or ...
419 Create a file located in /etc/ansible/ansible/cfg or ~/.ansible.cfg containing the following content:
422 host_key_checking = False
424 CWD must be this directory where this script is located.
428 log_level = logging.INFO
429 if arguments.log_level != 'INFO':
430 log_level = logging.DEBUG
431 logging.basicConfig(level=log_level)
433 logger.info('Starting to Deploy')
434 config = file_utils.read_yaml(arguments.environment)
435 logger.info('Read configuration file - ' + arguments.environment)
438 os_config = config.get('openstack')
448 os_conn_config = os_config.get('connection')
451 image_dict = __create_images(os_conn_config, os_config.get('images'),
452 arguments.clean is not ARG_NOT_SET)
455 network_dict = __create_networks(os_conn_config, os_config.get('networks'),
456 arguments.clean is not ARG_NOT_SET)
459 router_dict = __create_routers(os_conn_config, os_config.get('routers'),
460 arguments.clean is not ARG_NOT_SET)
463 keypairs_dict = __create_keypairs(os_conn_config, os_config.get('keypairs'),
464 arguments.clean is not ARG_NOT_SET)
467 vm_dict = __create_instances(os_conn_config, os_config.get('instances'), image_dict, keypairs_dict,
468 arguments.clean is not ARG_NOT_SET)
469 logger.info('Completed creating/retrieving all configured instances')
470 except Exception as e:
471 logger.error('Unexpected error deploying environment. Rolling back due to - ' + e.message)
472 __cleanup(vm_dict, keypairs_dict, router_dict, network_dict, image_dict, True)
476 # Must enter either block
477 if arguments.clean is not ARG_NOT_SET:
478 # Cleanup Environment
479 __cleanup(vm_dict, keypairs_dict, router_dict, network_dict, image_dict,
480 arguments.clean_image is not ARG_NOT_SET)
481 elif arguments.deploy is not ARG_NOT_SET:
482 logger.info('Configuring NICs where required')
483 for vm in vm_dict.itervalues():
485 logger.info('Completed NIC configuration')
488 ansible_config = config.get('ansible')
489 if ansible_config and vm_dict:
490 if not __apply_ansible_playbooks(ansible_config, vm_dict, arguments.environment):
491 logger.error("Problem applying ansible playbooks")
493 logger.error('Unable to read configuration file - ' + arguments.environment)
499 def __cleanup(vm_dict, keypairs_dict, router_dict, network_dict, image_dict, clean_image=False):
500 for key, vm_inst in vm_dict.iteritems():
502 for key, kp_inst in keypairs_dict.iteritems():
504 for key, router_inst in router_dict.iteritems():
506 for key, net_inst in network_dict.iteritems():
509 for key, image_inst in image_dict.iteritems():
513 if __name__ == '__main__':
514 # To ensure any files referenced via a relative path will begin from the diectory in which this file resides
515 os.chdir(os.path.dirname(os.path.realpath(__file__)))
517 parser = argparse.ArgumentParser()
518 parser.add_argument('-d', '--deploy', dest='deploy', nargs='?', default=ARG_NOT_SET,
519 help='When used, environment will be deployed and provisioned')
520 parser.add_argument('-c', '--clean', dest='clean', nargs='?', default=ARG_NOT_SET,
521 help='When used, the environment will be removed')
522 parser.add_argument('-i', '--clean-image', dest='clean_image', nargs='?', default=ARG_NOT_SET,
523 help='When cleaning, if this is set, the image will be cleaned too')
524 parser.add_argument('-e', '--env', dest='environment', required=True,
525 help='The environment configuration YAML file - REQUIRED')
526 parser.add_argument('-l', '--log-level', dest='log_level', default='INFO', help='Logging Level (INFO|DEBUG)')
527 args = parser.parse_args()
529 if args.deploy is ARG_NOT_SET and args.clean is ARG_NOT_SET:
530 print 'Must enter either -d for deploy or -c for cleaning up and environment'
532 if args.deploy is not ARG_NOT_SET and args.clean is not ARG_NOT_SET:
533 print 'Cannot enter both options -d/--deploy and -c/--clean'