????
Current Path : /proc/self/root/lib/Acronis/PyShell/site-tools/ |
Current File : //proc/self/root/lib/Acronis/PyShell/site-tools/agents.py |
# -*- coding: utf-8 -*- import os import time import argparse import csv import datetime import json import logging import logging.handlers import requests now = datetime.datetime.now() TITLE = """Install or update the agents in bulk.""" TIMEOUT = 10 LOG_FILENAME = 'agents.{}.log'.format(now.strftime('%y%m%d%H%M%S')) LOG_FORMAT = "%(asctime)s - %(filename)s - %(levelname)s - %(message)s" CRED_FIELDNAMES = ['hostname', 'username', 'password', 'unit'] IN_ENCODING = 'utf-8-sig' OUT_ENCODING = 'utf-8' STDOUT_FALLBACK_ENCODING = 'utf-8' logger = logging.getLogger(__file__) parser = None params = None _URL = None _HEADERS = None def safe_print(msg): """Usual print, but detect and workaround stdout encoding errors.""" try: print(msg) except UnicodeEncodeError: print(str(msg).encode(STDOUT_FALLBACK_ENCODING)) def _init_logging(): logger.setLevel(logging.DEBUG) # create file handler which logs even debug messages fh = logging.FileHandler(LOG_FILENAME, encoding=OUT_ENCODING) fh.setLevel(logging.DEBUG) # create console handler with a higher log level ch = logging.StreamHandler() ch.setLevel(logging.ERROR) # formatter fmt = logging.Formatter(LOG_FORMAT) fh.setFormatter(fmt) ch.setFormatter(fmt) # add the handlers to the logger logger.addHandler(fh) logger.addHandler(ch) def _logging(method, msg, verbose=False, prefix=None): if verbose: if prefix: msg = prefix + msg safe_print(msg) method(msg) def do(method, uri, payload=None): """Common function to make HTTP request.""" url = _URL + uri if payload is None: log_payload = '""' elif 'password' in payload: log_payload = payload.copy() log_payload['password'] = '***' else: log_payload = payload logger.debug('URL: %s %s', method.__name__.upper(), url) logger.debug('PAYLOAD: %s', log_payload) try: response = method(url, data=json.dumps(payload), headers=_HEADERS) except requests.exceptions.ConnectionError as err: _logging(logger.error, err) exit(1) logger.debug('RESPONSE: %s %s', response, response.content) try: content = response.json() except json.decoder.JSONDecodeError: content = response.text return response.status_code, content class _TenantsHierarchy: # Information about all organizational units (aka tenants) accessible # by current user. # # Although all the tenants in the system form a tree, it may happen that # current user sees only several branches of this global tree. What he sees # is a set of several trees. class Error(Exception): # TenantsTree-related exceptions def __init__(self, tenant_locator, err_msg, error_level=0): self._tenant_locator = tenant_locator self._err_msg = err_msg self._error_level = error_level def __str__(self): return 'Error processing Organization Unit "{}": {}'.format( self._tenant_locator, self._err_msg) def __init__(self, root_tenants_guids, tenant_names, subtenants): # Fetch information about all the available organizational units # Arguments: # - subtenants: {tenant_guid: [subtenant_guid, ]} self._root_tenants_guids = root_tenants_guids self._tenants_names = tenant_names # {guid: name} self._tenants_parents = { # {tenant_guid: parent_tenant_guid} t_guid: None for t_guid in self._root_tenants_guids } for tenant, children in subtenants.items(): for subtenant in children: assert subtenant not in self._tenants_parents, ( "Tenant {} has more than one parent: {} and {}".format( subtenant, tenant, self._tenants_parents[subtenant]) ) self._tenants_parents[subtenant] = tenant self._subtenants_by_name = { # {tenant_guid: {subtenant_name: subtenant_guid}} tenant: { self._tenants_names[subtenant_guid]: subtenant_guid for subtenant_guid in children } for tenant, children in subtenants.items() } self._root_tenants_by_name = {} # {root_tenant_name: [guid, ]} # Because root tenant names can duplicate for root_guid in self._root_tenants_guids: name = self._tenants_names[root_guid] self._root_tenants_by_name.setdefault(name, []).append(root_guid) self._tenant_to_guid_cache = {} # cached results @classmethod def read_tenants_tree(cls, session): """_TenantsHierarchy constructor. (information is fatched from AMS).""" tenants_names = {} subtenants = {} # get current (default) tenant info status_code, response = do(session.get, "/api/gateway/session") assert status_code == 200, "Can't get organization units list" default_tenant_id = response['current_tenant'] # get list of all possible root tenants status_code, response = do( session.get, "/api/account_server/users/self/access_policies") assert status_code == 200, "Can't get organization units list" potential_root_tenants = set(item.get('tenant_id') for item in response['items']) potential_root_tenants.add(default_tenant_id) for root_tenant_guid in potential_root_tenants: # depth-first-search all available tenants tenants_stack = [[root_tenant_guid, ], ] positions = [0, ] while tenants_stack: if positions[-1] >= len(tenants_stack[-1]): tenants_stack.pop() positions.pop() continue cur_guid = tenants_stack[-1][positions[-1]] positions[-1] += 1 if cur_guid in tenants_names: # this tenant was processed previously. # the situation is possible if one of potential root tenants is # actually a child of another one. continue # get tenant properties status_code, response = do( session.get, "/api/account_server/tenants/{}".format(cur_guid)) assert status_code == 200, "Can't get tenant details" tenants_names[cur_guid] = response['name'] # get children status_code, response = do( session.get, "/api/account_server/tenants/{}/children".format(cur_guid)) assert status_code == 200, "Can't get tenant's children" cur_subtenants = response['items'] subtenants[cur_guid] = cur_subtenants if cur_subtenants: tenants_stack.append(cur_subtenants) positions.append(0) # leave only really root tenants all_subtenants = set( tenant_guid for _, subtenants_list in subtenants.items() for tenant_guid in subtenants_list) root_tenants_guids = [x for x in potential_root_tenants - all_subtenants] root_tenants_guids.sort() assert root_tenants_guids, "Can't get root organization unit(s)" return cls(root_tenants_guids, tenants_names, subtenants) def find_tenant_by_name(self, tenant_locator): """Get tenant guid by tenant name or path. tenant_locator may be: - full tenant path ("Organization/Unit Number 1/some subtenant") - just name of the tenant without path (ok, if there are no name duplicates.) - tenant's guid (in this case it is returned as is) """ if tenant_locator not in self._tenant_to_guid_cache: try: guid = self._find_tenant_by_name(tenant_locator) self._tenant_to_guid_cache[tenant_locator] = guid except self.Error as err: self._tenant_to_guid_cache[tenant_locator] = err result = self._tenant_to_guid_cache[tenant_locator] if isinstance(result, self.Error): raise result return result def _find_tenant_by_name(self, tenant_locator): # helper for find_tenant_by_name method. path_chunks = self._split_tenant_path(tenant_locator) if len(path_chunks) == 1: # it's not a path, but just a tenant name tenant_name = path_chunks[0] if tenant_name in self._tenants_names: return tenant_name # it's not a name but tenant's guid. guids = self._find_tenants_by_name(tenant_name) if not guids: raise self.Error( tenant_locator, 'Organization Unit "{}" not found'.format(tenant_name)) elif len(guids) == 1: # fine, single match! return guids[0] else: raise self.Error( tenant_locator, 'Ambiguous Organization Unit name "{}". ' 'It corresponds to units: {}. ' 'To avoid ambiguity specify full ' 'Organization Unit path.'.format(tenant_name, guids)) # need to find tenant by path # skip first and last empty chunks - corresponding to optional "/" in the # start or end of locator. if not path_chunks[0]: path_chunks.pop(0) if not path_chunks[-1]: path_chunks.pop() if not path_chunks: raise self.Error(tenant_locator, "Invalid unit locator") # looks like single slash # find the apropriate root tenant(s) root_chunk = path_chunks[0] if root_chunk in self._root_tenants_guids: # this is not a name of root chunk, but guid roots_to_check = [root_chunk, ] else: roots_to_check = self._root_tenants_by_name.get(root_chunk) if not roots_to_check: err_msg = '"{}" is not a root Organization Unit.'.format(root_chunk) raise self.Error(tenant_locator, err_msg) problems_by_root = [] # [error, ] for current_root_guid in roots_to_check: try: return self._find_tenant_by_path(current_root_guid, path_chunks) except self.Error as err: err._tenant_locator = tenant_locator problems_by_root.append(err) # We tried to look for the tenant starting from each appropriate tenant and # all the attempts failed. But only one error should be reported. Let's report # the 'deepest' error. error_to_report = problems_by_root[0] for err in problems_by_root: if err._error_level > error_to_report._error_level: error_to_report = err raise error_to_report def _find_tenant_by_path(self, root_guid, path_chunks): # ["path", "to", "tenant"] -> tenant_guid # # Search starting from the specified root tenant. # It is supposed (but not verified) that the first element of path # corresponds to the root_guid. cur_tenant_guid = root_guid current_depth = 0 for cur_tenant_name in path_chunks[1:]: current_depth += 1 next_tenant_guid = None if cur_tenant_name in self._tenants_names: # this is actually not a name, but guid # just need to check it is really a child of current tenant if self._tenants_parents[cur_tenant_name] == cur_tenant_guid: next_tenant_guid = cur_tenant_name else: next_tenant_guid = self._subtenants_by_name[cur_tenant_guid].get(cur_tenant_name) if not next_tenant_guid: # child tenant was not found! err_msg = ( '"{}" is not a child of "{}" Organization Unit. ' 'List of existing children: {}').format( cur_tenant_name, self._make_full_tenant_name(cur_tenant_guid), self._get_subtenants_names(cur_tenant_guid) ) raise self.Error("", err_msg, current_depth) cur_tenant_guid = next_tenant_guid if not cur_tenant_guid: raise self.Error("", "Invalid unit locator") return cur_tenant_guid def _find_tenants_by_name(self, tenant_name): # tenant_name (not full path!) -> list of tenants guids return [ guid for guid, name in self._tenants_names.items() if tenant_name == name] def _make_full_tenant_name(self, tenant_guid): # tenant_guid -> "Organization/Unit 1/SubUnit5" cur_tenant_guid = tenant_guid unit_names = [] while cur_tenant_guid: unit_names.append(self._tenants_names[cur_tenant_guid]) cur_tenant_guid = self._tenants_parents[cur_tenant_guid] unit_names = [ name if "/" not in name else '"name"' for name in reversed(unit_names)] return "/".join(name for name in unit_names) def _get_subtenants_names(self, tenant_guid): # returns list of all the parents of specified tenant return [ self._tenants_names[guid] for guid, parent_guid in self._tenants_parents.items() if parent_guid == tenant_guid] @staticmethod def _split_tenant_path(tenant_locator): # "path/to/unit" -> "path"; "to"; "unit" # fixme: support escape characters return tenant_locator.split("/") def create(session): """Establish the web session.""" payload = { 'machine': params.ams, 'username': params.username, 'password': params.password, 'remember': False, 'type': 'ams', 'NonRequiredParams': ['username', 'password'], } msg = 'Establish a web session for {}'.format(params.username) _logging(logger.info, msg, params.verbose) status_code, response = do(session.post, '/api/ams/session', payload) if status_code != 200: msg = 'Connection to AMS failed: {}.'.format(params.ams) _logging(logger.debug, msg, verbose=True) exit(2) return response def lola_register(session, hostname, username, password, action): """Returns entry_id as a string.""" uri = '/api/ams/user_profiles/current/credentials' payload = { 'address': hostname, 'userName': username, 'password': password, 'category': action, 'temporary': True } msg = 'Set the credentials for {} into LOLA'.format(username) _logging(logger.info, msg, params.verbose) return do(session.post, uri, payload) def make_subscription(session): """Creates a subscription.""" msg = 'Create a subscription' _logging(logger.info, msg, params.verbose) return do(session.post, '/api/subscriptions') def get_subscription(session, subscription_id): """Gets a content of the subsription.""" uri = '/api/subscriptions/{}'.format(subscription_id) payload = { 'timeout': TIMEOUT, 'action': 'pop', } msg = 'Check the subscription {}.'.format(subscription_id) _logging(logger.info, msg, params.verbose) return do(session.post, uri, payload) def bind_activity(session, activity_id, subscription_id): """Binds the activity with the subscription.""" uri = '/api/ams/activities/{}?isRootActivity=true&subscriptionId={}' uri = uri.format(activity_id, subscription_id) msg = 'Bind the activity {} on the subscription {}.'.format(activity_id, subscription_id) _logging(logger.info, msg, params.verbose) return do(session.get, uri) def install_agent_win(session, hostname, unit_id, subscription_id): """Returns activity_id as a string.""" payload = { 'subscriptionId': subscription_id, 'operationId': hostname, 'machines': [ { 'address': hostname, 'agents': [ 'agentForWindows', ], 'registrationAddress': params.ams, 'tenantId': unit_id, 'NonRequiredParams': [], } ], 'NonRequiredParams': [], } msg = 'Install agent on {}'.format(hostname) _logging(logger.info, msg, params.verbose) return do(session.post, '/api/ams/infrastructure/agents', payload) def host_list(): reader = csv.DictReader(params.agents, fieldnames=CRED_FIELDNAMES, delimiter=' ', restkey='_unexpected', skipinitialspace=True) for n, line in enumerate(reader): if n == 0: if all(line.get(col_name) in (col_name, None) for col_name in CRED_FIELDNAMES): # this is a header line continue hostname = line['hostname'] username = params.use_username if params.use_username else line['username'] password = params.use_password if params.use_password else line['password'] org_unit = params.use_unit if params.use_unit else line['unit'] if not hostname: # empty string - ignore continue if hostname and hostname.startswith('#'): # this is a comment continue if n == 0 and "," in hostname: raise Exception( "Hostname '{}' contains comma character and is invalid. " "Make sure the input file is space-delimited.".format(hostname)) if username is None: raise Exception("Username is not specified for host '{}'".format(hostname)) if password is None: raise Exception("Password is not specified for host '{}'".format(hostname)) if '_unexpected' in line: raise Exception( "Information for host '{0}' contains unexpected trailing characters:\n{1}\n\n" "Make sure values containig spaces are enclosed into double quotes.".format( hostname, line['_unexpected'])) yield hostname, username, password, org_unit def get_agent_ids(session): msg = 'Get information about agents' _logging(logger.info, msg, params.verbose) return do(session.get, '/api/ams/infrastructure/agents') def update_agent(session, username, password, agent_id): """Run update of agents. :param session: The web session object. :param username: Agent's username. :param password: Agent's password. :param agent_ids: The agents' ID. """ payload = { 'credentials': { 'userName': username, 'password': password, }, 'machinesIds': [agent_id], } msg = 'Update agent: {}.'.format(agent_id) _logging(logger.info, msg, params.verbose) return do(session.post, '/api/ams/resource_operations/run_auto_update', payload) class ReportedActivityState: # activity properties (as reported by wcs subscription) def __init__(self, activity_id, title, state, status, progress, problem_effect, problem_cause): self.activity_id = activity_id self.title = title self.state = state self.status = status self.progress = progress self.problem_effect = problem_effect # populated in case of failure self.problem_cause = problem_cause # def __str__(self): fmt = "[{} {}]: {}" if self.state == 'completed': if self.status == 'ok': msg = "Completed with status {}".format(self.status) else: msg = "Completed with status {}: {}: {}".format( self.status, self.problem_effect or "unknown effect", self.problem_cause or "unknown cause") else: msg = "State {}; Progress {}".format(self.state, self.progress) return fmt.format(self.activity_id, self.title, msg) @classmethod def from_subscr_item(cls, subscr_item): # item received through wcs subscription -> ActivitiState (or None) data = subscr_item.get('data') if not data: return None activity_id = subscr_item.get('key') or data.get('id') if not activity_id: return None # this item does not correspond to activity return ReportedActivityState( activity_id, data.get('title', '- unknown -'), data.get('state'), data.get('status'), data.get('progress'), cls._get_by_path(data, 'completionResult/effect'), cls._get_by_path(data, 'completionResult/cause'), ) @staticmethod def _get_by_path(o, path): # parser helper if not isinstance(path, list): path = path.split('/') node, tail = path[0], path[1:] value = o.get(node) return _get_by_path(value, tail) if isinstance(value, dict) else value class ActivitiesMonitor: # Monitors state of activities associated with install/update operations def __init__(self): self.activity2host = {} # all registered activities self.session = None self.subscription_id = None self.waiting_activities = set() self.activities_problems = {} def register_activity(self, host_id, activity_id): assert not self.is_started() self.activity2host[activity_id] = host_id def is_started(self): return self.session is not None def start(self, session): assert not self.is_started() self.session = session # New subscription should be created for activities monitoring. # The subscription created when starting install/upgrade operations # may be expired by now. _code, data = make_subscription(session) if not isinstance(data, dict): raise RuntimeError("Can't create subscription for activities monitor") self.subscription_id = data['id'] # bind all the registered activities to subscription to get updates when # activity state changes for activity_id in self.activity2host: status_code, sub_response = bind_activity(self.session, activity_id, self.subscription_id) if status_code != 200: logger.error( "Activity binding faild {} (for {})", activity_id, self.activity2host[activity_id]) self.activities_problems[activity_id] = "Failed to trace status" continue self.waiting_activities.add(activity_id) # response may contain activity status activity = ReportedActivityState.from_subscr_item(sub_response) if activity: self._process_activity_update(activity) def _get_activities_updates(self): # yield ReportedActivityState objects for updated activities assert self.is_started() status_code, response = get_subscription(self.session, self.subscription_id) if status_code != 200: return for subscr_item in response: activity = ReportedActivityState.from_subscr_item(subscr_item) if activity: yield activity def _process_activity_update(self, activity): # process ReportedActivityState if activity.activity_id not in self.activity2host: logger.debug("Unknown activity %s encountered", activity.activity_id) return if activity.state == 'completed': try: self.waiting_activities.remove(activity.activity_id) except KeyError: logger.debug("Unexpected completed activity %s", activity.activity_id) if activity.status == 'ok': _logging(logger.info, self._make_activity_descr(activity), verbose=True) else: msg = self._make_activity_descr(activity) self.activities_problems[activity.activity_id] = msg _logging(logger.error, msg, verbose=True) return # activity is not completed yet. Just log current status logger.debug(self._make_activity_descr(activity)) def check_updates(self): # returns if all the activities completed for activity in self._get_activities_updates(): self._process_activity_update(activity) def _make_activity_descr(self, activity): return "{} {}".format(self.activity2host.get(activity.activity_id, "- ?? -"), str(activity)) def all_completed(self): return not self.waiting_activities def problems_detected(self): return len(self.activities_problems) > 0 def install(session, subscription_id): activities_monitor = ActivitiesMonitor() tenants_set = None # will be initialized on demand failures = {} # {hostname: problem_descr} for hostname, username, password, org_unit in host_list(): if org_unit: if tenants_set is None: tenants_set = _TenantsHierarchy.read_tenants_tree(session) try: org_unit = tenants_set.find_tenant_by_name(org_unit) except _TenantsHierarchy.Error as err: failures[hostname] = "Invalid organization unit {} specified".format(org_unit) _logging(logger.error, str(err), params.verbose) continue status_code, response = lola_register(session, hostname, username, password, 'windows_remote_install') if not status_code == 200: msg = 'Lola register {} failed.'.format(hostname) failures[hostname] = msg _logging(logger.error, msg, params.verbose) continue status_code, response = install_agent_win(session, hostname, org_unit, subscription_id) if not status_code == 200: msg = 'Install on {} failed.'.format(hostname) failures[hostname] = msg _logging(logger.error, msg, params.verbose) continue activity_id = response.get('reminst_result', {}).get('activity_id', None) if activity_id: # some remote installation really happens _logging(logger.info, 'Hostname[{}] -> Activity[{}]'.format(hostname, activity_id), verbose=True) activities_monitor.register_activity(hostname, activity_id) if not params.wait: return True safe_print('Waiting...') activities_monitor.start(session) while not activities_monitor.all_completed(): time.sleep(TIMEOUT) activities_monitor.check_updates() num_waiting = len(activities_monitor.waiting_activities) _logging(logger.debug, "Waiting for {} {}...".format( num_waiting, "activity" if num_waiting == 1 else "activities"), params.verbose) problems_detected = failures or activities_monitor.problems_detected() return not problems_detected def update(session, subscription_id): activities_monitor = ActivitiesMonitor() status_code, response = get_agent_ids(session) if not status_code == 200: msg = 'Get agent ids for {} failed.'.format(params.ams) logger.error(msg) raise RuntimeError(msg) agents_map = {} creds = {hostname: (username, password) for hostname, username, password, _org_unit in host_list()} for item in response['data']: ip = item['Attributes']['ResidentialAddresses'][0] name = item['Attributes']['Name'].lower() try: installed_version = item['Attributes']['Agents'][0]['Version'] except: installed_version = None if item['Attributes']['Status'] and installed_version and installed_version >= "12": # Skip update of offline agents (Status: 0 - online, 1 - offline) # Agents of old version should be upgraded even if they are reported offline. msg = 'Agent on {} is offline. Skipping...'.format(ip) _logging(logger.info, msg, verbose=True) continue if not item['Attributes']['UpdateIsAvailable']: msg = 'No update will be applied for agent on {}. Skipping...'.format(ip) _logging(logger.info, msg, verbose=True) continue update_version = item['Attributes'].get('UpdateVersion', None) if update_version and update_version == installed_version: msg = 'Agent on {} is up to date. Skipping...'.format(ip) _logging(logger.info, msg, verbose=True) continue local_id = item['ID']['LocalID'] if ip not in creds and name.lower() not in creds: msg = 'No record for agent on {} in the credentials file. Skipping...'.format(ip) _logging(logger.info, msg, verbose=True, prefix='\t') continue username, password = creds[ip] if ip in creds else creds[name] agent_info = { 'machine_id': local_id, 'name': item['DisplayedName'], 'ip': ip, 'username': username, 'password': password, } agents_map[local_id] = agent_info msg = 'Agent on {} will be updated!'.format(ip) _logging(logger.info, msg, verbose=True) if len(agents_map) == 0: msg = 'Nothing to update!' _logging(logger.info, msg, verbose=True) return True for agent_id, agent_info in agents_map.items(): status_code, response = update_agent( session, agent_info['username'], agent_info['password'], agent_id) if not params.wait: continue for info in response['data']: agent_id = info['machine_id'] activity_id = info['activity_id'] activities_monitor.register_activity(agent_id, activity_id) if not params.wait: return True safe_print('Waiting...') activities_monitor.start(session) while not activities_monitor.all_completed(): time.sleep(TIMEOUT) activities_monitor.check_updates() _logging(logger.debug, "Waiting for {} {}...".format( num_waiting, "activity" if num_waiting == 1 else "activities"), params.verbose) return not activities_monitor.problems_detected() TPL_MACHINE = 'hostname="{}", username="{}", password="{}"' def main(): _init_logging() global _URL, _HEADERS _URL = 'http://{}:{}'.format(params.ams, params.port) _HEADERS = { 'content-type': 'application/json', 'Origin': _URL, } with requests.Session() as session: data = create(session) if params.list: status_code, response = get_agent_ids(session) if not status_code == 200: msg = 'Get agent ids for {} failed.'.format(params.ams) logger.error(msg) raise RuntimeError(msg) if params.dump: writer = csv.DictWriter(params.dump, fieldnames=CRED_FIELDNAMES, delimiter=' ') writer.writeheader() unique_hostnames = set() for item in response['data']: attributes = item['Attributes'] for agent in attributes['Agents']: if agent['Name']: ip_address = attributes['ResidentialAddresses'][0] data = { 'version': agent['Version'], 'ip': ip_address, 'name': attributes['Name'], 'title': agent['Name'], } safe_print('{name}\t{ip}\t{title}\t{version}'.format(**data)) if params.dump: if ip_address in unique_hostnames: continue unique_hostnames.add(ip_address) writer.writerow({ 'hostname': ip_address, 'username': params.dump_username, 'password': params.dump_password }) if params.dump: tpl = '-----\nThe agents list was dumped to "{}" file in CSV format.' safe_print(tpl.format(params.dump.name)) return status_code, data = make_subscription(session) if not isinstance(data, dict): raise RuntimeError('See log file.') subscription_id = data.get('id') if params.install: install(session, subscription_id) elif params.update: update(session, subscription_id) if __name__ == '__main__': parser = argparse.ArgumentParser(description=TITLE) parser.add_argument('-v', '--verbose', help='Make this script verbose.', action='store_true', default=False) ams_group = parser.add_argument_group('connection') ams_group.add_argument('-a', '--ams', required=True, help='AMS\' IP address or hostname.') ams_group.add_argument('-p', '--port', help='AMS\' port.', default=9877) ams_group.add_argument('--username', help='AMS\' username.', required=True) ams_group.add_argument('--password', help='AMS\' password.', required=True) main_group = parser.add_argument_group('install or update') group = main_group.add_mutually_exclusive_group() group.add_argument('-i', '--install', action='store_true', default=False, help='Do install.') group.add_argument('-u', '--update', action='store_true', default=False, help='Do update.') main_group.add_argument('--agents', type=argparse.FileType('r', encoding=IN_ENCODING), help='Space-delimited CSV file containing list of hostnames and credentials. ' 'Used for both install and update operations. ' 'Each line should contain hostname, username and password in this order.') main_group.add_argument('--use_username', help='Default username for agents, has a priority over file.') main_group.add_argument('--use_password', help='Default password for agents, has a priority over file.') main_group.add_argument('--use_unit', help='Default organization unit for installing agents, ' 'has a priority over file. Ignored by "update" command') main_group.add_argument('--wait', help='Waiting until complete.', action='store_true', default=False) listing_group = parser.add_argument_group('agent listing') listing_group.add_argument('-l', '--list', help='Listing the agents with versions.', action='store_true', default=False) listing_group.add_argument('--dump', nargs='?', type=argparse.FileType('w', encoding=OUT_ENCODING), help='Dump agents to file, dump_agent.csv by default.', const='dump_agent.csv') listing_group.add_argument('--dump_username', default='Administrator', help='Default username for agents') listing_group.add_argument('--dump_password', default='top_secret_password', help='Default password for agents') params = parser.parse_args() if not any([params.list, params.install, params.update]): safe_print('\nPlease specify one of the following commands: --install, --update, --list') parser.print_help() os.sys.exit(1) if (params.install or params.update) and not params.agents: safe_print('\nPlease, provide credentials information with --agent option.') safe_print('You may generate the appropriate file with -l option. See below...\n') parser.print_help() os.sys.exit(2) main()