Added the ability to create custom flavors with the deploy_venv.py application.
[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 from snaps import file_utils
25 from snaps.openstack.create_flavor import FlavorSettings, OpenStackFlavor
26 from snaps.openstack.create_image import ImageSettings
27 from snaps.openstack.create_instance import VmInstanceSettings
28 from snaps.openstack.create_network import PortSettings, NetworkSettings
29 from snaps.openstack.create_router import RouterSettings
30 from snaps.openstack.create_keypairs import KeypairSettings
31 from snaps.openstack.os_credentials import OSCreds, ProxySettings
32 from snaps.openstack.utils import deploy_utils
33 from snaps.provisioning import ansible_utils
34
35 __author__ = 'spisarski'
36
37 logger = logging.getLogger('deploy_venv')
38
39 ARG_NOT_SET = "argument not set"
40
41
42 def __get_os_credentials(os_conn_config):
43     """
44     Returns an object containing all of the information required to access OpenStack APIs
45     :param os_conn_config: The configuration holding the credentials
46     :return: an OSCreds instance
47     """
48     proxy_settings = None
49     http_proxy = os_conn_config.get('http_proxy')
50     if http_proxy:
51         tokens = re.split(':', http_proxy)
52         ssh_proxy_cmd = os_conn_config.get('ssh_proxy_cmd')
53         proxy_settings = ProxySettings(tokens[0], tokens[1], ssh_proxy_cmd)
54
55     return OSCreds(username=os_conn_config.get('username'),
56                    password=os_conn_config.get('password'),
57                    auth_url=os_conn_config.get('auth_url'),
58                    project_name=os_conn_config.get('project_name'),
59                    proxy_settings=proxy_settings)
60
61
62 def __parse_ports_config(config):
63     """
64     Parses the "ports" configuration
65     :param config: The dictionary to parse
66     :param os_creds: The OpenStack credentials object
67     :return: a list of PortConfig objects
68     """
69     out = list()
70     for port_config in config:
71         out.append(PortSettings(config=port_config.get('port')))
72     return out
73
74
75 def __create_flavors(os_conn_config, flavors_config, cleanup=False):
76     """
77     Returns a dictionary of flavors where the key is the image name and the value is the image object
78     :param os_conn_config: The OpenStack connection credentials
79     :param flavors_config: The list of image configurations
80     :param cleanup: Denotes whether or not this is being called for cleanup or not
81     :return: dictionary
82     """
83     flavors = {}
84
85     if flavors_config:
86         try:
87             for flavor_config_dict in flavors_config:
88                 flavor_config = flavor_config_dict.get('flavor')
89                 if flavor_config and flavor_config.get('name'):
90                     flavor_creator = OpenStackFlavor(__get_os_credentials(os_conn_config),
91                                                      FlavorSettings(flavor_config))
92                     flavor_creator.create(cleanup=cleanup)
93                     flavors[flavor_config['name']] = flavor_creator
94         except Exception as e:
95             for key, flavor_creator in flavors.iteritems():
96                 flavor_creator.clean()
97             raise e
98         logger.info('Created configured flavors')
99
100     return flavors
101
102
103 def __create_images(os_conn_config, images_config, cleanup=False):
104     """
105     Returns a dictionary of images where the key is the image name and the value is the image object
106     :param os_conn_config: The OpenStack connection credentials
107     :param images_config: The list of image configurations
108     :param cleanup: Denotes whether or not this is being called for cleanup or not
109     :return: dictionary
110     """
111     images = {}
112
113     if images_config:
114         try:
115             for image_config_dict in images_config:
116                 image_config = image_config_dict.get('image')
117                 if image_config and image_config.get('name'):
118                     images[image_config['name']] = deploy_utils.create_image(__get_os_credentials(os_conn_config),
119                                                                              ImageSettings(image_config), cleanup)
120         except Exception as e:
121             for key, image_creator in images.iteritems():
122                 image_creator.clean()
123             raise e
124         logger.info('Created configured images')
125
126     return images
127
128
129 def __create_networks(os_conn_config, network_confs, cleanup=False):
130     """
131     Returns a dictionary of networks where the key is the network name and the value is the network object
132     :param os_conn_config: The OpenStack connection credentials
133     :param network_confs: The list of network configurations
134     :param cleanup: Denotes whether or not this is being called for cleanup or not
135     :return: dictionary
136     """
137     network_dict = {}
138
139     if network_confs:
140         try:
141             for network_conf in network_confs:
142                 net_name = network_conf['network']['name']
143                 os_creds = __get_os_credentials(os_conn_config)
144                 network_dict[net_name] = deploy_utils.create_network(
145                     os_creds, NetworkSettings(config=network_conf['network']), cleanup)
146         except Exception as e:
147             for key, net_creator in network_dict.iteritems():
148                 net_creator.clean()
149             raise e
150
151         logger.info('Created configured networks')
152
153     return network_dict
154
155
156 def __create_routers(os_conn_config, router_confs, cleanup=False):
157     """
158     Returns a dictionary of networks where the key is the network name and the value is the network object
159     :param os_conn_config: The OpenStack connection credentials
160     :param router_confs: The list of router configurations
161     :param cleanup: Denotes whether or not this is being called for cleanup or not
162     :return: dictionary
163     """
164     router_dict = {}
165     os_creds = __get_os_credentials(os_conn_config)
166
167     if router_confs:
168         try:
169             for router_conf in router_confs:
170                 router_name = router_conf['router']['name']
171                 router_dict[router_name] = deploy_utils.create_router(
172                     os_creds, RouterSettings(config=router_conf['router']), cleanup)
173         except Exception as e:
174             for key, router_creator in router_dict.iteritems():
175                 router_creator.clean()
176             raise e
177
178         logger.info('Created configured networks')
179
180     return router_dict
181
182
183 def __create_keypairs(os_conn_config, keypair_confs, cleanup=False):
184     """
185     Returns a dictionary of keypairs where the key is the keypair name and the value is the keypair object
186     :param os_conn_config: The OpenStack connection credentials
187     :param keypair_confs: The list of keypair configurations
188     :param cleanup: Denotes whether or not this is being called for cleanup or not
189     :return: dictionary
190     """
191     keypairs_dict = {}
192     if keypair_confs:
193         try:
194             for keypair_dict in keypair_confs:
195                 keypair_config = keypair_dict['keypair']
196                 kp_settings = KeypairSettings(keypair_config)
197                 keypairs_dict[keypair_config['name']] = deploy_utils.create_keypair(
198                     __get_os_credentials(os_conn_config), kp_settings, cleanup)
199         except Exception as e:
200             for key, keypair_creator in keypairs_dict.iteritems():
201                 keypair_creator.clean()
202             raise e
203
204         logger.info('Created configured keypairs')
205
206     return keypairs_dict
207
208
209 def __create_instances(os_conn_config, instances_config, image_dict, keypairs_dict, cleanup=False):
210     """
211     Returns a dictionary of instances where the key is the instance name and the value is the VM object
212     :param os_conn_config: The OpenStack connection credentials
213     :param instances_config: The list of VM instance configurations
214     :param image_dict: A dictionary of images that will probably be used to instantiate the VM instance
215     :param keypairs_dict: A dictionary of keypairs that will probably be used to instantiate the VM instance
216     :param cleanup: Denotes whether or not this is being called for cleanup or not
217     :return: dictionary
218     """
219     os_creds = __get_os_credentials(os_conn_config)
220
221     vm_dict = {}
222
223     if instances_config:
224         try:
225             for instance_config in instances_config:
226                 conf = instance_config.get('instance')
227                 if conf:
228                     if image_dict:
229                         image_creator = image_dict.get(conf.get('imageName'))
230                         if image_creator:
231                             instance_settings = VmInstanceSettings(config=instance_config['instance'])
232                             kp_name = conf.get('keypair_name')
233                             vm_dict[conf['name']] = deploy_utils.create_vm_instance(
234                                 os_creds, instance_settings, image_creator.image_settings,
235                                 keypair_creator=keypairs_dict[kp_name], cleanup=cleanup)
236                         else:
237                             raise Exception('Image creator instance not found. Cannot instantiate')
238                     else:
239                         raise Exception('Image dictionary is None. Cannot instantiate')
240                 else:
241                     raise Exception('Instance configuration is None. Cannot instantiate')
242         except Exception as e:
243             logger.error('Unexpected error creating instances. Attempting to cleanup environment - ' + e.message)
244             for key, inst_creator in vm_dict.iteritems():
245                 inst_creator.clean()
246             raise e
247
248         logger.info('Created configured instances')
249
250     return vm_dict
251
252
253 def __apply_ansible_playbooks(ansible_configs, vm_dict, env_file):
254     """
255     Applies ansible playbooks to running VMs with floating IPs
256     :param ansible_configs: a list of Ansible configurations
257     :param vm_dict: the dictionary of newly instantiated VMs where the VM name is the key
258     :param env_file: the path of the environment for setting the CWD so playbook location is relative to the deployment
259                      file
260     :return: t/f - true if successful
261     """
262     logger.info("Applying Ansible Playbooks")
263     if ansible_configs:
264         # Ensure all hosts are accepting SSH session requests
265         for vm_inst in vm_dict.values():
266             if not vm_inst.vm_ssh_active(block=True):
267                 logger.warn("Timeout waiting for instance to respond to SSH requests")
268                 return False
269
270         # Set CWD so the deployment file's playbook location can leverage relative paths
271         orig_cwd = os.getcwd()
272         env_dir = os.path.dirname(env_file)
273         os.chdir(env_dir)
274
275         # Apply playbooks
276         for ansible_config in ansible_configs:
277             __apply_ansible_playbook(ansible_config, vm_dict)
278
279         # Return to original directory
280         os.chdir(orig_cwd)
281
282     return True
283
284
285 def __apply_ansible_playbook(ansible_config, vm_dict):
286     """
287     Applies an Ansible configuration setting
288     :param ansible_config: the configuration settings
289     :param vm_dict: the dictionary of newly instantiated VMs where the VM name is the key
290     :return:
291     """
292     if ansible_config:
293         remote_user, floating_ips, private_key_filepath, proxy_settings = __get_connection_info(ansible_config, vm_dict)
294         if floating_ips:
295             ansible_utils.apply_playbook(ansible_config['playbook_location'], floating_ips, remote_user,
296                                          private_key_filepath,
297                                          variables=__get_variables(ansible_config.get('variables'), vm_dict),
298                                          proxy_setting=proxy_settings)
299
300
301 def __get_connection_info(ansible_config, vm_dict):
302     """
303     Returns a tuple of data required for connecting to the running VMs
304     (remote_user, [floating_ips], private_key_filepath, proxy_settings)
305     :param ansible_config: the configuration settings
306     :param vm_dict: the dictionary of VMs where the VM name is the key
307     :return: tuple where the first element is the user and the second is a list of floating IPs and the third is the
308     private key file location and the fourth is an instance of the snaps.ProxySettings class
309     (note: in order to work, each of the hosts need to have the same sudo_user and private key file location values)
310     """
311     if ansible_config.get('hosts'):
312         hosts = ansible_config['hosts']
313         if len(hosts) > 0:
314             floating_ips = list()
315             remote_user = None
316             private_key_filepath = None
317             proxy_settings = None
318             for host in hosts:
319                 vm = vm_dict.get(host)
320                 if vm:
321                     fip = vm.get_floating_ip()
322                     if fip:
323                         remote_user = vm.get_image_user()
324
325                         if fip:
326                             floating_ips.append(fip.ip)
327                         else:
328                             raise Exception('Could not find floating IP for VM - ' + vm.name)
329
330                         private_key_filepath = vm.keypair_settings.private_filepath
331                         proxy_settings = vm.get_os_creds().proxy_settings
332                 else:
333                     logger.error('Could not locate VM with name - ' + host)
334
335             return remote_user, floating_ips, private_key_filepath, proxy_settings
336     return None
337
338
339 def __get_variables(var_config, vm_dict):
340     """
341     Returns a dictionary of substitution variables to be used for Ansible templates
342     :param var_config: the variable configuration settings
343     :param vm_dict: the dictionary of VMs where the VM name is the key
344     :return: dictionary or None
345     """
346     if var_config and vm_dict and len(vm_dict) > 0:
347         variables = dict()
348         for key, value in var_config.iteritems():
349             value = __get_variable_value(value, vm_dict)
350             if key and value:
351                 variables[key] = value
352                 logger.info("Set Jinga2 variable with key [" + key + "] the value [" + value + ']')
353             else:
354                 logger.warn('Key [' + str(key) + '] or Value [' + str(value) + '] must not be None')
355         return variables
356     return None
357
358
359 def __get_variable_value(var_config_values, vm_dict):
360     """
361     Returns the associated variable value for use by Ansible for substitution purposes
362     :param var_config_values: the configuration dictionary
363     :param vm_dict: the dictionary containing all VMs where the key is the VM's name
364     :return:
365     """
366     if var_config_values['type'] == 'string':
367         return __get_string_variable_value(var_config_values)
368     if var_config_values['type'] == 'vm-attr':
369         return __get_vm_attr_variable_value(var_config_values, vm_dict)
370     if var_config_values['type'] == 'os_creds':
371         return __get_os_creds_variable_value(var_config_values, vm_dict)
372     if var_config_values['type'] == 'port':
373         return __get_vm_port_variable_value(var_config_values, vm_dict)
374     return None
375
376
377 def __get_string_variable_value(var_config_values):
378     """
379     Returns the associated string value
380     :param var_config_values: the configuration dictionary
381     :return: the value contained in the dictionary with the key 'value'
382     """
383     return var_config_values['value']
384
385
386 def __get_vm_attr_variable_value(var_config_values, vm_dict):
387     """
388     Returns the associated value contained on a VM instance
389     :param var_config_values: the configuration dictionary
390     :param vm_dict: the dictionary containing all VMs where the key is the VM's name
391     :return: the value
392     """
393     vm = vm_dict.get(var_config_values['vm_name'])
394     if vm:
395         if var_config_values['value'] == 'floating_ip':
396             return vm.get_floating_ip().ip
397
398
399 def __get_os_creds_variable_value(var_config_values, vm_dict):
400     """
401     Returns the associated OS credentials value
402     :param var_config_values: the configuration dictionary
403     :param vm_dict: the dictionary containing all VMs where the key is the VM's name
404     :return: the value
405     """
406     logger.info("Retrieving OS Credentials")
407     vm = vm_dict.values()[0]
408
409     if vm:
410         if var_config_values['value'] == 'username':
411             logger.info("Returning OS username")
412             return vm.get_os_creds().username
413         elif var_config_values['value'] == 'password':
414             logger.info("Returning OS password")
415             return vm.get_os_creds().password
416         elif var_config_values['value'] == 'auth_url':
417             logger.info("Returning OS auth_url")
418             return vm.get_os_creds().auth_url
419         elif var_config_values['value'] == 'project_name':
420             logger.info("Returning OS project_name")
421             return vm.get_os_creds().project_name
422
423     logger.info("Returning none")
424     return None
425
426
427 def __get_vm_port_variable_value(var_config_values, vm_dict):
428     """
429     Returns the associated OS credentials value
430     :param var_config_values: the configuration dictionary
431     :param vm_dict: the dictionary containing all VMs where the key is the VM's name
432     :return: the value
433     """
434     port_name = var_config_values.get('port_name')
435     vm_name = var_config_values.get('vm_name')
436
437     if port_name and vm_name:
438         vm = vm_dict.get(vm_name)
439         if vm:
440             port_value_id = var_config_values.get('port_value')
441             if port_value_id:
442                 if port_value_id == 'mac_address':
443                     return vm.get_port_mac(port_name)
444                 if port_value_id == 'ip_address':
445                     return vm.get_port_ip(port_name)
446
447
448 def main(arguments):
449     """
450     Will need to set environment variable ANSIBLE_HOST_KEY_CHECKING=False or ...
451     Create a file located in /etc/ansible/ansible/cfg or ~/.ansible.cfg containing the following content:
452
453     [defaults]
454     host_key_checking = False
455
456     CWD must be this directory where this script is located.
457
458     :return: To the OS
459     """
460     log_level = logging.INFO
461     if arguments.log_level != 'INFO':
462         log_level = logging.DEBUG
463     logging.basicConfig(level=log_level)
464
465     logger.info('Starting to Deploy')
466     config = file_utils.read_yaml(arguments.environment)
467     logger.info('Read configuration file - ' + arguments.environment)
468
469     if config:
470         os_config = config.get('openstack')
471
472         image_dict = {}
473         network_dict = {}
474         router_dict = {}
475         keypairs_dict = {}
476         vm_dict = {}
477
478         if os_config:
479             try:
480                 os_conn_config = os_config.get('connection')
481
482                 # Create flavors
483                 flavor_dict = __create_flavors(os_conn_config, os_config.get('flavors'),
484                                               arguments.clean is not ARG_NOT_SET)
485
486                 # Create images
487                 image_dict = __create_images(os_conn_config, os_config.get('images'),
488                                              arguments.clean is not ARG_NOT_SET)
489
490                 # Create network
491                 network_dict = __create_networks(os_conn_config, os_config.get('networks'),
492                                                  arguments.clean is not ARG_NOT_SET)
493
494                 # Create network
495                 router_dict = __create_routers(os_conn_config, os_config.get('routers'),
496                                                arguments.clean is not ARG_NOT_SET)
497
498                 # Create keypairs
499                 keypairs_dict = __create_keypairs(os_conn_config, os_config.get('keypairs'),
500                                                   arguments.clean is not ARG_NOT_SET)
501
502                 # Create instance
503                 vm_dict = __create_instances(os_conn_config, os_config.get('instances'), image_dict, keypairs_dict,
504                                              arguments.clean is not ARG_NOT_SET)
505                 logger.info('Completed creating/retrieving all configured instances')
506             except Exception as e:
507                 logger.error('Unexpected error deploying environment. Rolling back due to - ' + e.message)
508                 __cleanup(vm_dict, keypairs_dict, router_dict, network_dict, image_dict, flavor_dict, True)
509                 raise e
510
511
512         # Must enter either block
513         if arguments.clean is not ARG_NOT_SET:
514             # Cleanup Environment
515             __cleanup(vm_dict, keypairs_dict, router_dict, network_dict, image_dict, flavor_dict,
516                       arguments.clean_image is not ARG_NOT_SET)
517         elif arguments.deploy is not ARG_NOT_SET:
518             logger.info('Configuring NICs where required')
519             for vm in vm_dict.itervalues():
520                 vm.config_nics()
521             logger.info('Completed NIC configuration')
522
523             # Provision VMs
524             ansible_config = config.get('ansible')
525             if ansible_config and vm_dict:
526                 if not __apply_ansible_playbooks(ansible_config, vm_dict, arguments.environment):
527                     logger.error("Problem applying ansible playbooks")
528     else:
529         logger.error('Unable to read configuration file - ' + arguments.environment)
530         exit(1)
531
532     exit(0)
533
534
535 def __cleanup(vm_dict, keypairs_dict, router_dict, network_dict, image_dict, flavor_dict, clean_image=False):
536     for key, vm_inst in vm_dict.iteritems():
537         vm_inst.clean()
538     for key, kp_inst in keypairs_dict.iteritems():
539         kp_inst.clean()
540     for key, router_inst in router_dict.iteritems():
541         router_inst.clean()
542     for key, net_inst in network_dict.iteritems():
543         net_inst.clean()
544     if clean_image:
545         for key, image_inst in image_dict.iteritems():
546             image_inst.clean()
547     for key, flavor_inst in flavor_dict.iteritems():
548         flavor_inst.clean()
549
550
551 if __name__ == '__main__':
552     # To ensure any files referenced via a relative path will begin from the diectory in which this file resides
553     os.chdir(os.path.dirname(os.path.realpath(__file__)))
554
555     parser = argparse.ArgumentParser()
556     parser.add_argument('-d', '--deploy', dest='deploy', nargs='?', default=ARG_NOT_SET,
557                         help='When used, environment will be deployed and provisioned')
558     parser.add_argument('-c', '--clean', dest='clean', nargs='?', default=ARG_NOT_SET,
559                         help='When used, the environment will be removed')
560     parser.add_argument('-i', '--clean-image', dest='clean_image', nargs='?', default=ARG_NOT_SET,
561                         help='When cleaning, if this is set, the image will be cleaned too')
562     parser.add_argument('-e', '--env', dest='environment', required=True,
563                         help='The environment configuration YAML file - REQUIRED')
564     parser.add_argument('-l', '--log-level', dest='log_level', default='INFO', help='Logging Level (INFO|DEBUG)')
565     args = parser.parse_args()
566
567     if args.deploy is ARG_NOT_SET and args.clean is ARG_NOT_SET:
568         print 'Must enter either -d for deploy or -c for cleaning up and environment'
569         exit(1)
570     if args.deploy is not ARG_NOT_SET and args.clean is not ARG_NOT_SET:
571         print 'Cannot enter both options -d/--deploy and -c/--clean'
572         exit(1)
573     main(args)