Initial patch with all code from CableLabs repository.
[snaps.git] / snaps / deploy_venv.py
1 #!/usr/bin/python
2 #
3 # Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs")
4 #                    and others.  All rights reserved.
5 #
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:
9 #
10 #     http://www.apache.org/licenses/LICENSE-2.0
11 #
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.
17 #
18 # This script is responsible for deploying virtual environments
19 import argparse
20 import logging
21 import os
22 import re
23
24 import file_utils
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
33
34 __author__ = 'spisarski'
35
36 logger = logging.getLogger('deploy_venv')
37
38 ARG_NOT_SET = "argument not set"
39
40
41 def __get_os_credentials(os_conn_config):
42     """
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
46     """
47     proxy_settings = None
48     http_proxy = os_conn_config.get('http_proxy')
49     if 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)
53
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)
59
60
61 def __parse_ports_config(config):
62     """
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
67     """
68     out = list()
69     for port_config in config:
70         out.append(PortSettings(config=port_config.get('port')))
71     return out
72
73
74 def __create_images(os_conn_config, images_config, cleanup=False):
75     """
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
80     :return: dictionary
81     """
82     images = {}
83
84     if images_config:
85         try:
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():
93                 image_creator.clean()
94             raise e
95         logger.info('Created configured images')
96
97     return images
98
99
100 def __create_networks(os_conn_config, network_confs, cleanup=False):
101     """
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
106     :return: dictionary
107     """
108     network_dict = {}
109
110     if network_confs:
111         try:
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():
119                 net_creator.clean()
120             raise e
121
122         logger.info('Created configured networks')
123
124     return network_dict
125
126
127 def __create_routers(os_conn_config, router_confs, cleanup=False):
128     """
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
133     :return: dictionary
134     """
135     router_dict = {}
136     os_creds = __get_os_credentials(os_conn_config)
137
138     if router_confs:
139         try:
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()
147             raise e
148
149         logger.info('Created configured networks')
150
151     return router_dict
152
153
154 def __create_keypairs(os_conn_config, keypair_confs, cleanup=False):
155     """
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
160     :return: dictionary
161     """
162     keypairs_dict = {}
163     if keypair_confs:
164         try:
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()
173             raise e
174
175         logger.info('Created configured keypairs')
176
177     return keypairs_dict
178
179
180 def __create_instances(os_conn_config, instances_config, image_dict, keypairs_dict, cleanup=False):
181     """
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
188     :return: dictionary
189     """
190     os_creds = __get_os_credentials(os_conn_config)
191
192     vm_dict = {}
193
194     if instances_config:
195         try:
196             for instance_config in instances_config:
197                 conf = instance_config.get('instance')
198                 if conf:
199                     if image_dict:
200                         image_creator = image_dict.get(conf.get('imageName'))
201                         if image_creator:
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)
207                         else:
208                             raise Exception('Image creator instance not found. Cannot instantiate')
209                     else:
210                         raise Exception('Image dictionary is None. Cannot instantiate')
211                 else:
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():
216                 inst_creator.clean()
217             raise e
218
219         logger.info('Created configured instances')
220
221     return vm_dict
222
223
224 def __apply_ansible_playbooks(ansible_configs, vm_dict, env_file):
225     """
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
230                      file
231     :return: t/f - true if successful
232     """
233     logger.info("Applying Ansible Playbooks")
234     if ansible_configs:
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")
239                 return False
240
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)
244         os.chdir(env_dir)
245
246         # Apply playbooks
247         for ansible_config in ansible_configs:
248             __apply_ansible_playbook(ansible_config, vm_dict)
249
250         # Return to original directory
251         os.chdir(orig_cwd)
252
253     return True
254
255
256 def __apply_ansible_playbook(ansible_config, vm_dict):
257     """
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
261     :return:
262     """
263     if ansible_config:
264         remote_user, floating_ips, private_key_filepath, proxy_settings = __get_connection_info(ansible_config, vm_dict)
265         if floating_ips:
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)
270
271
272 def __get_connection_info(ansible_config, vm_dict):
273     """
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)
281     """
282     if ansible_config.get('hosts'):
283         hosts = ansible_config['hosts']
284         if len(hosts) > 0:
285             floating_ips = list()
286             remote_user = None
287             private_key_filepath = None
288             proxy_settings = None
289             for host in hosts:
290                 vm = vm_dict.get(host)
291                 fip = vm.get_floating_ip()
292                 if vm and fip:
293                     remote_user = vm.get_image_user()
294
295                     if fip:
296                         floating_ips.append(fip.ip)
297                     else:
298                         raise Exception('Could not find floating IP for VM - ' + vm.name)
299
300                     private_key_filepath = vm.keypair_settings.private_filepath
301                     proxy_settings = vm.get_os_creds().proxy_settings
302
303             return remote_user, floating_ips, private_key_filepath, proxy_settings
304     return None
305
306
307 def __get_variables(var_config, vm_dict):
308     """
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
313     """
314     if var_config and vm_dict and len(vm_dict) > 0:
315         variables = dict()
316         for key, value in var_config.iteritems():
317             value = __get_variable_value(value, vm_dict)
318             if key and value:
319                 variables[key] = value
320                 logger.info("Set Jinga2 variable with key [" + key + "] the value [" + value + ']')
321             else:
322                 logger.warn('Key [' + str(key) + '] or Value [' + str(value) + '] must not be None')
323         return variables
324     return None
325
326
327 def __get_variable_value(var_config_values, vm_dict):
328     """
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
332     :return:
333     """
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)
342     return None
343
344
345 def __get_string_variable_value(var_config_values):
346     """
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'
350     """
351     return var_config_values['value']
352
353
354 def __get_vm_attr_variable_value(var_config_values, vm_dict):
355     """
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
359     :return: the value
360     """
361     vm = vm_dict.get(var_config_values['vm_name'])
362     if vm:
363         if var_config_values['value'] == 'floating_ip':
364             return vm.get_floating_ip().ip
365
366
367 def __get_os_creds_variable_value(var_config_values, vm_dict):
368     """
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
372     :return: the value
373     """
374     logger.info("Retrieving OS Credentials")
375     vm = vm_dict.values()[0]
376
377     if vm:
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
390
391     logger.info("Returning none")
392     return None
393
394
395 def __get_vm_port_variable_value(var_config_values, vm_dict):
396     """
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
400     :return: the value
401     """
402     port_name = var_config_values.get('port_name')
403     vm_name = var_config_values.get('vm_name')
404
405     if port_name and vm_name:
406         vm = vm_dict.get(vm_name)
407         if vm:
408             port_value_id = var_config_values.get('port_value')
409             if port_value_id:
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)
414
415
416 def main(arguments):
417     """
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:
420
421     [defaults]
422     host_key_checking = False
423
424     CWD must be this directory where this script is located.
425
426     :return: To the OS
427     """
428     log_level = logging.INFO
429     if arguments.log_level != 'INFO':
430         log_level = logging.DEBUG
431     logging.basicConfig(level=log_level)
432
433     logger.info('Starting to Deploy')
434     config = file_utils.read_yaml(arguments.environment)
435     logger.info('Read configuration file - ' + arguments.environment)
436
437     if config:
438         os_config = config.get('openstack')
439
440         image_dict = {}
441         network_dict = {}
442         router_dict = {}
443         keypairs_dict = {}
444         vm_dict = {}
445
446         if os_config:
447             try:
448                 os_conn_config = os_config.get('connection')
449
450                 # Create images
451                 image_dict = __create_images(os_conn_config, os_config.get('images'),
452                                              arguments.clean is not ARG_NOT_SET)
453
454                 # Create network
455                 network_dict = __create_networks(os_conn_config, os_config.get('networks'),
456                                                  arguments.clean is not ARG_NOT_SET)
457
458                 # Create network
459                 router_dict = __create_routers(os_conn_config, os_config.get('routers'),
460                                                arguments.clean is not ARG_NOT_SET)
461
462                 # Create keypairs
463                 keypairs_dict = __create_keypairs(os_conn_config, os_config.get('keypairs'),
464                                                   arguments.clean is not ARG_NOT_SET)
465
466                 # Create instance
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)
473                 raise e
474
475
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():
484                 vm.config_nics()
485             logger.info('Completed NIC configuration')
486
487             # Provision VMs
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")
492     else:
493         logger.error('Unable to read configuration file - ' + arguments.environment)
494         exit(1)
495
496     exit(0)
497
498
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():
501         vm_inst.clean()
502     for key, kp_inst in keypairs_dict.iteritems():
503         kp_inst.clean()
504     for key, router_inst in router_dict.iteritems():
505         router_inst.clean()
506     for key, net_inst in network_dict.iteritems():
507         net_inst.clean()
508     if clean_image:
509         for key, image_inst in image_dict.iteritems():
510             image_inst.clean()
511
512
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__)))
516
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()
528
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'
531         exit(1)
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'
534         exit(1)
535     main(args)