90677300eae325982480a57b7fbefd1a714f834f
[snaps.git] / examples / launch.py
1 #!/usr/bin/python
2 #
3 # Copyright (c) 2017 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 re
22
23 import os
24 from snaps import file_utils
25 from snaps.openstack.create_flavor import FlavorSettings, OpenStackFlavor
26 from snaps.openstack.create_image import ImageSettings, OpenStackImage
27 from snaps.openstack.create_instance import VmInstanceSettings
28 from snaps.openstack.create_keypairs import KeypairSettings
29 from snaps.openstack.create_network import PortSettings, NetworkSettings
30 from snaps.openstack.create_router import RouterSettings
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
45     OpenStack APIs
46     :param os_conn_config: The configuration holding the credentials
47     :return: an OSCreds instance
48     """
49     proxy_settings = None
50     http_proxy = os_conn_config.get('http_proxy')
51     if http_proxy:
52         tokens = re.split(':', http_proxy)
53         ssh_proxy_cmd = os_conn_config.get('ssh_proxy_cmd')
54         proxy_settings = ProxySettings(host=tokens[0], port=tokens[1],
55                                        ssh_proxy_cmd=ssh_proxy_cmd)
56
57     os_conn_config['proxy_settings'] = proxy_settings
58
59     return OSCreds(**os_conn_config)
60
61
62 def __parse_ports_config(config):
63     """
64     Parses the "ports" configuration
65     :param config: The dictionary to parse
66     :return: a list of PortConfig objects
67     """
68     out = list()
69     for port_config in config:
70         out.append(PortSettings(**port_config.get('port')))
71     return out
72
73
74 def __create_flavors(os_conn_config, flavors_config, cleanup=False):
75     """
76     Returns a dictionary of flavors where the key is the image name and the
77     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
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(
91                         __get_os_credentials(os_conn_config),
92                         FlavorSettings(**flavor_config))
93                     flavor_creator.create(cleanup=cleanup)
94                     flavors[flavor_config['name']] = flavor_creator
95         except Exception as e:
96             for key, flavor_creator in flavors.items():
97                 flavor_creator.clean()
98             raise e
99         logger.info('Created configured flavors')
100
101     return flavors
102
103
104 def __create_images(os_conn_config, images_config, cleanup=False):
105     """
106     Returns a dictionary of images where the key is the image name and the
107     value is the image object
108     :param os_conn_config: The OpenStack connection credentials
109     :param images_config: The list of image configurations
110     :param cleanup: Denotes whether or not this is being called for cleanup
111     :return: dictionary
112     """
113     images = {}
114
115     if images_config:
116         try:
117             for image_config_dict in images_config:
118                 image_config = image_config_dict.get('image')
119                 if image_config and image_config.get('name'):
120                     images[image_config['name']] = deploy_utils.create_image(
121                         __get_os_credentials(os_conn_config),
122                         ImageSettings(**image_config), cleanup)
123         except Exception as e:
124             for key, image_creator in images.items():
125                 image_creator.clean()
126             raise e
127         logger.info('Created configured images')
128
129     return images
130
131
132 def __create_networks(os_conn_config, network_confs, cleanup=False):
133     """
134     Returns a dictionary of networks where the key is the network name and the
135     value is the network object
136     :param os_conn_config: The OpenStack connection credentials
137     :param network_confs: The list of network configurations
138     :param cleanup: Denotes whether or not this is being called for cleanup
139     :return: dictionary
140     """
141     network_dict = {}
142
143     if network_confs:
144         try:
145             for network_conf in network_confs:
146                 net_name = network_conf['network']['name']
147                 os_creds = __get_os_credentials(os_conn_config)
148                 network_dict[net_name] = deploy_utils.create_network(
149                     os_creds, NetworkSettings(**network_conf['network']),
150                     cleanup)
151         except Exception as e:
152             for key, net_creator in network_dict.items():
153                 net_creator.clean()
154             raise e
155
156         logger.info('Created configured networks')
157
158     return network_dict
159
160
161 def __create_routers(os_conn_config, router_confs, cleanup=False):
162     """
163     Returns a dictionary of networks where the key is the network name and the
164     value is the network object
165     :param os_conn_config: The OpenStack connection credentials
166     :param router_confs: The list of router configurations
167     :param cleanup: Denotes whether or not this is being called for cleanup
168     :return: dictionary
169     """
170     router_dict = {}
171     os_creds = __get_os_credentials(os_conn_config)
172
173     if router_confs:
174         try:
175             for router_conf in router_confs:
176                 router_name = router_conf['router']['name']
177                 router_dict[router_name] = deploy_utils.create_router(
178                     os_creds, RouterSettings(**router_conf['router']), cleanup)
179         except Exception as e:
180             for key, router_creator in router_dict.items():
181                 router_creator.clean()
182             raise e
183
184         logger.info('Created configured networks')
185
186     return router_dict
187
188
189 def __create_keypairs(os_conn_config, keypair_confs, cleanup=False):
190     """
191     Returns a dictionary of keypairs where the key is the keypair name and the
192     value is the keypair object
193     :param os_conn_config: The OpenStack connection credentials
194     :param keypair_confs: The list of keypair configurations
195     :param cleanup: Denotes whether or not this is being called for cleanup
196     :return: dictionary
197     """
198     keypairs_dict = {}
199     if keypair_confs:
200         try:
201             for keypair_dict in keypair_confs:
202                 keypair_config = keypair_dict['keypair']
203                 kp_settings = KeypairSettings(**keypair_config)
204                 keypairs_dict[
205                     keypair_config['name']] = deploy_utils.create_keypair(
206                     __get_os_credentials(os_conn_config), kp_settings, cleanup)
207         except Exception as e:
208             for key, keypair_creator in keypairs_dict.items():
209                 keypair_creator.clean()
210             raise e
211
212         logger.info('Created configured keypairs')
213
214     return keypairs_dict
215
216
217 def __create_instances(os_conn_config, instances_config, image_dict,
218                        keypairs_dict, cleanup=False):
219     """
220     Returns a dictionary of instances where the key is the instance name and
221     the value is the VM object
222     :param os_conn_config: The OpenStack connection credentials
223     :param instances_config: The list of VM instance configurations
224     :param image_dict: A dictionary of images that will probably be used to
225                        instantiate the VM instance
226     :param keypairs_dict: A dictionary of keypairs that will probably be used
227                           to instantiate the VM instance
228     :param cleanup: Denotes whether or not this is being called for cleanup
229     :return: dictionary
230     """
231     os_creds = __get_os_credentials(os_conn_config)
232
233     vm_dict = {}
234
235     if instances_config:
236         try:
237             for instance_config in instances_config:
238                 conf = instance_config.get('instance')
239                 if conf:
240                     if image_dict:
241                         image_creator = image_dict.get(conf.get('imageName'))
242                         if image_creator:
243                             instance_settings = VmInstanceSettings(
244                                 **instance_config['instance'])
245                             kp_name = conf.get('keypair_name')
246                             vm_dict[conf[
247                                 'name']] = deploy_utils.create_vm_instance(
248                                 os_creds, instance_settings,
249                                 image_creator.image_settings,
250                                 keypair_creator=keypairs_dict[kp_name],
251                                 cleanup=cleanup)
252                         else:
253                             raise Exception('Image creator instance not found.'
254                                             ' Cannot instantiate')
255                     else:
256                         raise Exception('Image dictionary is None. Cannot '
257                                         'instantiate')
258                 else:
259                     raise Exception('Instance configuration is None. Cannot '
260                                     'instantiate')
261         except Exception as e:
262             logger.error('Unexpected error creating instances. Attempting to '
263                          'cleanup environment - %s', e)
264             for key, inst_creator in vm_dict.items():
265                 inst_creator.clean()
266             raise e
267
268         logger.info('Created configured instances')
269     # TODO Should there be an error if there isn't an instances config
270     return vm_dict
271
272
273 def __apply_ansible_playbooks(ansible_configs, os_conn_config, vm_dict,
274                               image_dict, flavor_dict, env_file):
275     """
276     Applies ansible playbooks to running VMs with floating IPs
277     :param ansible_configs: a list of Ansible configurations
278     :param os_conn_config: the OpenStack connection configuration used to
279                            create an OSCreds instance
280     :param vm_dict: the dictionary of newly instantiated VMs where the name is
281                     the key
282     :param image_dict: the dictionary of newly instantiated images where the
283                        name is the key
284     :param flavor_dict: the dictionary of newly instantiated flavors where the
285                         name is the key
286     :param env_file: the path of the environment for setting the CWD so
287                      playbook location is relative to the deployment file
288     :return: t/f - true if successful
289     """
290     logger.info("Applying Ansible Playbooks")
291     if ansible_configs:
292         # Ensure all hosts are accepting SSH session requests
293         for vm_inst in list(vm_dict.values()):
294             if not vm_inst.vm_ssh_active(block=True):
295                 logger.warning(
296                     "Timeout waiting for instance to respond to SSH requests")
297                 return False
298
299         # Set CWD so the deployment file's playbook location can leverage
300         # relative paths
301         orig_cwd = os.getcwd()
302         env_dir = os.path.dirname(env_file)
303         os.chdir(env_dir)
304
305         # Apply playbooks
306         for ansible_config in ansible_configs:
307             os_creds = __get_os_credentials(os_conn_config)
308             __apply_ansible_playbook(ansible_config, os_creds, vm_dict,
309                                      image_dict, flavor_dict)
310
311         # Return to original directory
312         os.chdir(orig_cwd)
313
314     return True
315
316
317 def __apply_ansible_playbook(ansible_config, os_creds, vm_dict, image_dict,
318                              flavor_dict):
319     """
320     Applies an Ansible configuration setting
321     :param ansible_config: the configuration settings
322     :param os_creds: the OpenStack credentials object
323     :param vm_dict: the dictionary of newly instantiated VMs where the name is
324                     the key
325     :param image_dict: the dictionary of newly instantiated images where the
326                        name is the key
327     :param flavor_dict: the dictionary of newly instantiated flavors where the
328                         name is the key
329     """
330     if ansible_config:
331         (remote_user, floating_ips, private_key_filepath,
332          proxy_settings) = __get_connection_info(
333             ansible_config, vm_dict)
334         if floating_ips:
335             retval = ansible_utils.apply_playbook(
336                 ansible_config['playbook_location'], floating_ips, remote_user,
337                 private_key_filepath,
338                 variables=__get_variables(ansible_config.get('variables'),
339                                           os_creds, vm_dict, image_dict,
340                                           flavor_dict),
341                 proxy_setting=proxy_settings)
342             if retval != 0:
343                 # Not a fatal type of event
344                 logger.warning(
345                     'Unable to apply playbook found at location - ' +
346                     ansible_config('playbook_location'))
347
348
349 def __get_connection_info(ansible_config, vm_dict):
350     """
351     Returns a tuple of data required for connecting to the running VMs
352     (remote_user, [floating_ips], private_key_filepath, proxy_settings)
353     :param ansible_config: the configuration settings
354     :param vm_dict: the dictionary of VMs where the VM name is the key
355     :return: tuple where the first element is the user and the second is a list
356              of floating IPs and the third is the
357     private key file location and the fourth is an instance of the
358     snaps.ProxySettings class
359     (note: in order to work, each of the hosts need to have the same sudo_user
360     and private key file location values)
361     """
362     if ansible_config.get('hosts'):
363         hosts = ansible_config['hosts']
364         if len(hosts) > 0:
365             floating_ips = list()
366             remote_user = None
367             pk_file = None
368             proxy_settings = None
369             for host in hosts:
370                 vm = vm_dict.get(host)
371                 if vm:
372                     fip = vm.get_floating_ip()
373                     if fip:
374                         remote_user = vm.get_image_user()
375
376                         if fip:
377                             floating_ips.append(fip.ip)
378                         else:
379                             raise Exception(
380                                 'Could not find floating IP for VM - ' +
381                                 vm.name)
382
383                         pk_file = vm.keypair_settings.private_filepath
384                         proxy_settings = vm.get_os_creds().proxy_settings
385                 else:
386                     logger.error('Could not locate VM with name - ' + host)
387
388             return remote_user, floating_ips, pk_file, proxy_settings
389     return None
390
391
392 def __get_variables(var_config, os_creds, vm_dict, image_dict, flavor_dict):
393     """
394     Returns a dictionary of substitution variables to be used for Ansible
395     templates
396     :param var_config: the variable configuration settings
397     :param os_creds: the OpenStack credentials object
398     :param vm_dict: the dictionary of newly instantiated VMs where the name is
399                     the key
400     :param image_dict: the dictionary of newly instantiated images where the
401                        name is the key
402     :param flavor_dict: the dictionary of newly instantiated flavors where the
403                         name is the key
404     :return: dictionary or None
405     """
406     if var_config and vm_dict and len(vm_dict) > 0:
407         variables = dict()
408         for key, value in var_config.items():
409             value = __get_variable_value(value, os_creds, vm_dict, image_dict,
410                                          flavor_dict)
411             if key and value:
412                 variables[key] = value
413                 logger.info(
414                     "Set Jinga2 variable with key [%s] the value [%s]",
415                     key, value)
416             else:
417                 logger.warning('Key [%s] or Value [%s] must not be None',
418                                str(key), str(value))
419         return variables
420     return None
421
422
423 def __get_variable_value(var_config_values, os_creds, vm_dict, image_dict,
424                          flavor_dict):
425     """
426     Returns the associated variable value for use by Ansible for substitution
427     purposes
428     :param var_config_values: the configuration dictionary
429     :param os_creds: the OpenStack credentials object
430     :param vm_dict: the dictionary of newly instantiated VMs where the name is
431                     the key
432     :param image_dict: the dictionary of newly instantiated images where the
433                        name is the key
434     :param flavor_dict: the dictionary of newly instantiated flavors where the
435                         name is the key
436     :return:
437     """
438     if var_config_values['type'] == 'string':
439         return __get_string_variable_value(var_config_values)
440     if var_config_values['type'] == 'vm-attr':
441         return __get_vm_attr_variable_value(var_config_values, vm_dict)
442     if var_config_values['type'] == 'os_creds':
443         return __get_os_creds_variable_value(var_config_values, os_creds)
444     if var_config_values['type'] == 'port':
445         return __get_vm_port_variable_value(var_config_values, vm_dict)
446     if var_config_values['type'] == 'image':
447         return __get_image_variable_value(var_config_values, image_dict)
448     if var_config_values['type'] == 'flavor':
449         return __get_flavor_variable_value(var_config_values, flavor_dict)
450     return None
451
452
453 def __get_string_variable_value(var_config_values):
454     """
455     Returns the associated string value
456     :param var_config_values: the configuration dictionary
457     :return: the value contained in the dictionary with the key 'value'
458     """
459     return var_config_values['value']
460
461
462 def __get_vm_attr_variable_value(var_config_values, vm_dict):
463     """
464     Returns the associated value contained on a VM instance
465     :param var_config_values: the configuration dictionary
466     :param vm_dict: the dictionary containing all VMs where the key is the VM's
467                     name
468     :return: the value
469     """
470     vm = vm_dict.get(var_config_values['vm_name'])
471     if vm:
472         if var_config_values['value'] == 'floating_ip':
473             return vm.get_floating_ip().ip
474         if var_config_values['value'] == 'image_user':
475             return vm.get_image_user()
476
477
478 def __get_os_creds_variable_value(var_config_values, os_creds):
479     """
480     Returns the associated OS credentials value
481     :param var_config_values: the configuration dictionary
482     :param os_creds: the credentials
483     :return: the value
484     """
485     logger.info("Retrieving OS Credentials")
486     if os_creds:
487         if var_config_values['value'] == 'username':
488             logger.info("Returning OS username")
489             return os_creds.username
490         elif var_config_values['value'] == 'password':
491             logger.info("Returning OS password")
492             return os_creds.password
493         elif var_config_values['value'] == 'auth_url':
494             logger.info("Returning OS auth_url")
495             return os_creds.auth_url
496         elif var_config_values['value'] == 'project_name':
497             logger.info("Returning OS project_name")
498             return os_creds.project_name
499
500     logger.info("Returning none")
501     return None
502
503
504 def __get_vm_port_variable_value(var_config_values, vm_dict):
505     """
506     Returns the associated OS credentials value
507     :param var_config_values: the configuration dictionary
508     :param vm_dict: the dictionary containing all VMs where the key is the VM's
509                     name
510     :return: the value
511     """
512     port_name = var_config_values.get('port_name')
513     vm_name = var_config_values.get('vm_name')
514
515     if port_name and vm_name:
516         vm = vm_dict.get(vm_name)
517         if vm:
518             port_value_id = var_config_values.get('port_value')
519             if port_value_id:
520                 if port_value_id == 'mac_address':
521                     return vm.get_port_mac(port_name)
522                 if port_value_id == 'ip_address':
523                     return vm.get_port_ip(port_name)
524
525
526 def __get_image_variable_value(var_config_values, image_dict):
527     """
528     Returns the associated image value
529     :param var_config_values: the configuration dictionary
530     :param image_dict: the dictionary containing all images where the key is
531                        the name
532     :return: the value
533     """
534     logger.info("Retrieving image values")
535
536     if image_dict:
537         if var_config_values.get('image_name'):
538             image_creator = image_dict.get(var_config_values['image_name'])
539             if image_creator:
540                 if var_config_values.get('value') and \
541                                 var_config_values['value'] == 'id':
542                     return image_creator.get_image().id
543                 if var_config_values.get('value') and \
544                         var_config_values['value'] == 'user':
545                     return image_creator.image_settings.image_user
546
547     logger.info("Returning none")
548     return None
549
550
551 def __get_flavor_variable_value(var_config_values, flavor_dict):
552     """
553     Returns the associated flavor value
554     :param var_config_values: the configuration dictionary
555     :param flavor_dict: the dictionary containing all flavor creators where the
556                         key is the name
557     :return: the value or None
558     """
559     logger.info("Retrieving flavor values")
560
561     if flavor_dict:
562         if var_config_values.get('flavor_name'):
563             flavor_creator = flavor_dict.get(var_config_values['flavor_name'])
564             if flavor_creator:
565                 if var_config_values.get('value') and \
566                                 var_config_values['value'] == 'id':
567                     return flavor_creator.get_flavor().id
568
569
570 def main(arguments):
571     """
572     Will need to set environment variable ANSIBLE_HOST_KEY_CHECKING=False or
573     Create a file located in /etc/ansible/ansible/cfg or ~/.ansible.cfg
574     containing the following content:
575
576     [defaults]
577     host_key_checking = False
578
579     CWD must be this directory where this script is located.
580
581     :return: To the OS
582     """
583     log_level = logging.INFO
584     if arguments.log_level != 'INFO':
585         log_level = logging.DEBUG
586     logging.basicConfig(level=log_level)
587
588     logger.info('Starting to Deploy')
589     config = file_utils.read_yaml(arguments.environment)
590     logger.debug('Read configuration file - ' + arguments.environment)
591
592     if config:
593         os_config = config.get('openstack')
594
595         os_conn_config = None
596         creators = list()
597         vm_dict = dict()
598         images_dict = dict()
599         flavors_dict = dict()
600
601         if os_config:
602             try:
603                 os_conn_config = os_config.get('connection')
604
605                 # Create flavors
606                 flavors_dict = __create_flavors(
607                     os_conn_config, os_config.get('flavors'),
608                     arguments.clean is not ARG_NOT_SET)
609                 creators.append(flavors_dict)
610
611                 # Create images
612                 images_dict = __create_images(
613                     os_conn_config, os_config.get('images'),
614                     arguments.clean is not ARG_NOT_SET)
615                 creators.append(images_dict)
616
617                 # Create network
618                 creators.append(__create_networks(
619                     os_conn_config, os_config.get('networks'),
620                     arguments.clean is not ARG_NOT_SET))
621
622                 # Create routers
623                 creators.append(__create_routers(
624                     os_conn_config, os_config.get('routers'),
625                     arguments.clean is not ARG_NOT_SET))
626
627                 # Create keypairs
628                 keypairs_dict = __create_keypairs(
629                     os_conn_config, os_config.get('keypairs'),
630                     arguments.clean is not ARG_NOT_SET)
631                 creators.append(keypairs_dict)
632
633                 # Create instance
634                 vm_dict = __create_instances(
635                     os_conn_config, os_config.get('instances'),
636                     images_dict, keypairs_dict,
637                     arguments.clean is not ARG_NOT_SET)
638                 creators.append(vm_dict)
639                 logger.info(
640                     'Completed creating/retrieving all configured instances')
641             except Exception as e:
642                 logger.error(
643                     'Unexpected error deploying environment. Rolling back due'
644                     ' to - ' + str(e))
645                 __cleanup(creators)
646                 raise
647
648         # Must enter either block
649         if arguments.clean is not ARG_NOT_SET:
650             # Cleanup Environment
651             __cleanup(creators, arguments.clean_image is not ARG_NOT_SET)
652         elif arguments.deploy is not ARG_NOT_SET:
653             logger.info('Configuring NICs where required')
654             for vm in vm_dict.values():
655                 vm.config_nics()
656             logger.info('Completed NIC configuration')
657
658             # Provision VMs
659             ansible_config = config.get('ansible')
660             if ansible_config and vm_dict:
661                 if not __apply_ansible_playbooks(ansible_config,
662                                                  os_conn_config, vm_dict,
663                                                  images_dict, flavors_dict,
664                                                  arguments.environment):
665                     logger.error("Problem applying ansible playbooks")
666     else:
667         logger.error(
668             'Unable to read configuration file - ' + arguments.environment)
669         exit(1)
670
671     exit(0)
672
673
674 def __cleanup(creators, clean_image=False):
675     for creator_dict in reversed(creators):
676         for key, creator in creator_dict.items():
677             if (isinstance(creator, OpenStackImage) and clean_image) or \
678                     not isinstance(creator, OpenStackImage):
679                 try:
680                     creator.clean()
681                 except Exception as e:
682                     logger.warning('Error cleaning component - %s', e)
683
684
685 if __name__ == '__main__':
686     # To ensure any files referenced via a relative path will begin from the
687     # directory in which this file resides
688     os.chdir(os.path.dirname(os.path.realpath(__file__)))
689
690     parser = argparse.ArgumentParser()
691     parser.add_argument(
692         '-d', '--deploy', dest='deploy', nargs='?', default=ARG_NOT_SET,
693         help='When used, environment will be deployed and provisioned')
694     parser.add_argument(
695         '-c', '--clean', dest='clean', nargs='?', default=ARG_NOT_SET,
696         help='When used, the environment will be removed')
697     parser.add_argument(
698         '-i', '--clean-image', dest='clean_image', nargs='?',
699         default=ARG_NOT_SET,
700         help='When cleaning, if this is set, the image will be cleaned too')
701     parser.add_argument(
702         '-e', '--env', dest='environment', required=True,
703         help='The environment configuration YAML file - REQUIRED')
704     parser.add_argument(
705         '-l', '--log-level', dest='log_level', default='INFO',
706         help='Logging Level (INFO|DEBUG)')
707     args = parser.parse_args()
708
709     if args.deploy is ARG_NOT_SET and args.clean is ARG_NOT_SET:
710         print(
711             'Must enter either -d for deploy or -c for cleaning up and '
712             'environment')
713         exit(1)
714     if args.deploy is not ARG_NOT_SET and args.clean is not ARG_NOT_SET:
715         print('Cannot enter both options -d/--deploy and -c/--clean')
716         exit(1)
717     main(args)