Esempio n. 1
0
 def signal_handler(signal, frame):
     # prevent double-print on debug
     if not utils.debug:
         print('\nShutting down Argus on SigInt.')
     argus_debug('Shutting down Argus on SigInt.')
     if utils.argus_log is not None:
         utils.argus_log.close()
     sys.exit(0)
Esempio n. 2
0
 def _translate_field(self, jira_issue: 'JiraIssue') -> str:
     """
     For this issue, parse out the JiraProject it belongs to and translate our local field's readable text to
     whatever cf* is on the project side
     """
     jira_project = self._jira_connection.maybe_get_cached_jira_project(jira_issue.project_name)
     if jira_project is None:
         return 'None'
     argus_debug('JiraFilter: Attempting to translate {} for jira_issue: {}'.format(
         self.field, jira_issue.issue_key))
     return jira_project.translate_custom_field(self.field)
Esempio n. 3
0
    def _internal_matching_operation(self, jira_issue: 'JiraIssue', to_match: List[str]) -> bool:
        matches_one = False
        matches_all = True

        translated = self._translate_field(jira_issue)
        in_issue = translated in jira_issue
        value = 'Not found'
        if in_issue:
            value = jira_issue[translated]
        argus_debug('Checking for translated field {} in issue: {}. Found: {}. Value: {}. Filter: {}'.format(
            translated, jira_issue.issue_key, in_issue, value, self))

        if translated in jira_issue:
            argus_debug('Checking for {} in {}'.format(translated, jira_issue.issue_key))
            for match in to_match:
                argus_debug('Checking against match: {}'.format(match))
                if match in jira_issue[translated]:
                    argus_debug('   FOUND MATCH')
                    matches_one = True
                else:
                    matches_all = False

        if self.query_type() == 'OR':
            return matches_one

        return matches_one and matches_all
Esempio n. 4
0
def since(source, delta):
    # type: (datetime, str) -> datetime
    """
    Accepts string in format '-123d 3w 5m', negative optional on each, can take multiple space delim options
    :return: datetime object representing deltasd interval from datetime.now()
    """
    day_delta = _extract_time('d', delta)
    week_delta = _extract_time('w', delta)
    month_delta = _extract_time('m', delta)
    year_delta = _extract_time('y', delta)
    utils.argus_debug('since input source: {}. delta: [{}]. days:{} weeks:{} months:{} years:{}'.format(
        source, delta, day_delta, week_delta, month_delta, year_delta
    ))

    return source + relativedelta(days=day_delta, weeks=week_delta, months=month_delta, years=year_delta)
Esempio n. 5
0
 def populate_jira_issues(self, jira_connection: 'JiraConnection',
                          issues: List[List['JiraIssue']]) -> None:
     count_added = 0
     for list_of_issues in issues:
         for jira_issue in list_of_issues:
             for member in list(self._team_members.values()):
                 # Can't short-circuit here since one member may be assignee and another reviewer
                 if member.add_if_owns(jira_connection, jira_issue):
                     count_added += 1
     utils.argus_debug(
         'Team: {}. JiraConnection: {}. Count added: {}'.format(
             self.name, jira_connection.connection_name, count_added))
     for member in list(self._team_members.values()):
         utils.argus_debug(
             'At end of add_owned_issues for team: {}. Member: {}'.format(
                 self.name, member))
Esempio n. 6
0
    def add_field_translations_from_file(self):
        """
        Pulls custom translations from conf/custom_params.cfg and initializes this JiraProject with them if they are
        not otherwise defined
        """
        if not os.path.exists('conf/custom_params.cfg'):
            return
        else:
            config_parser = configparser.RawConfigParser()
            config_parser.read('conf/custom_params.cfg')

        # Current limitation: one hard-coded set of config per project name. i.e. one URL
        if config_parser.has_section(self.project_name):
            url = config_parser.get(self.project_name, 'url').rstrip('/')
            if url != self._url:
                msg = 'WARNING! Found project {} but url mismatched (project: {} and file: {}). Not loading translations.'
                print(msg.format(self.project_name, self._url, url))
                return

            field_names = config_parser.get(self.project_name,
                                            'custom_fields').split(',')
            changed = False
            for field in field_names:
                if field not in self._custom_fields:
                    for cf in list(self._custom_fields.keys()):
                        print('   Known: {}'.format(cf))
                    changed = True
                    translated_value = config_parser.get(
                        self.project_name, field)
                    print(
                        'Adding missing custom field translation from conf/custom_params.cfg for '
                        'project: {} field: {} value: {}'.format(
                            self.project_name, field, translated_value))
                    self._custom_fields[field] = translated_value
            if changed:
                print(
                    'Migrated custom params from conf/custom_params.cfg into project: {}. Saving config.'
                    .format(self.project_name))
                self.save_config()
            else:
                argus_debug(
                    'No changes from custom_params necessary for {}'.format(
                        self.project_name))
Esempio n. 7
0
    def from_file(cls) -> 'TeamManager':
        print('Loading Team config from file')
        config_parser = configparser.RawConfigParser()
        try:
            result = TeamManager()

            if not os.path.exists(os.path.join(conf_dir, 'teams.cfg')):
                argus_debug('Did not find any existing conf/teams.cfg file. Empty TeamManager.')
                return result

            config_parser.read(os.path.join(conf_dir, 'teams.cfg'))

            # Add teams
            if config_parser.has_section('manager'):
                team_roots = config_parser.get('manager', 'team_names').split(',')
                for team_root in team_roots:
                    # Skip trailing ,
                    if team_root == '':
                        continue
                    name, jira_connection_name = team_root.split(':')
                    result._teams[name] = Team(name, jira_connection_name)
                    argus_debug('TeamManager.init: Adding team: {} from config'.format(name))

            # Add MemberIssuesByStatus
            for member_root_name in config_parser.sections():
                # TODO: Consider removing these two manualy bypasses. Kind of hacky to assume everything in config is member root.
                if member_root_name == 'manager' or member_root_name == 'organizations':
                    continue
                new_member = MemberIssuesByStatus.from_file(member_root_name, config_parser)
                team = result.get_team_by_name(new_member.primary_team)
                if team is None:
                    raise ValueError('Failed to find a constructed team with name: {}'.format(new_member.primary_team))
                team.add_existing_member(new_member)
                argus_debug('TeamManager init: Adding team member: {}'.format(new_member.full_name))

            # Init Orgs
            if config_parser.has_section('organizations'):
                for org_name in config_parser.get('organizations', 'org_names').split(','):
                    new_org = set()
                    for team_name in config_parser.get('organizations', org_name).split(','):
                        new_org.add(team_name)
                    result._organizations[org_name] = new_org

            return result
        except (AttributeError, ValueError, IOError) as e:
            print('Exception during creation of TeamManager. Config file name: {}. Exception stack follows:'.format(os.path.join(conf_dir, 'teams.cfg')))
            traceback.print_exc()
            raise e
Esempio n. 8
0
    def __init__(self, team_manager):
        """
        Recreates any JiraConnections and JiraViews based on saved data in conf/jira.cfg
        """
        # Holds connected Jira objects to be queried by JiraViews
        self._jira_connections = {}  # type: Dict[str, JiraConnection]

        # JiraViews, caching filters for different ways to view Jira Data. Implicit 1:1 JiraConnection to JiraView
        self.jira_views = {}  # type: Dict[str, JiraView]

        self.jira_dashboards = {}  # type: Dict[str, JiraDashboard]

        # Used during JiraDependency resolution to notify user of missing JiraProjects
        self.missing_project_counts = {}  # type: Dict[str, int]

        self._display_filter = DisplayFilter.default()

        if os.path.exists(jira_conf_file):
            config_parser = configparser.RawConfigParser()
            config_parser.read(jira_conf_file)

            connection_names = []
            if config_parser.has_section(
                    'JiraManager') and config_parser.has_option(
                        'JiraManager', 'Connections'):
                connection_names = config_parser.get('JiraManager',
                                                     'Connections').split(',')

            # JiraConnections are the root of our container hierarchy
            for connection_name in connection_names:
                if connection_name == '':
                    pass
                try:
                    jira_connection = JiraConnection.from_file(connection_name)
                    # If we had an error on init we obviously cannot add this
                    if jira_connection is None:
                        continue
                    self._jira_connections[
                        jira_connection.connection_name] = jira_connection
                except ConfigError as ce:
                    print('ConfigError with project {}: {}'.format(
                        connection_name, ce))

            # Construct JiraViews so they can be used during JiraDashboard creation.
            view_names = []
            if config_parser.has_option('JiraManager', 'Views'):
                view_names = config_parser.get('JiraManager',
                                               'Views').split(',')

            for name in view_names:
                try:
                    jv = JiraView.from_file(self, name, team_manager)
                    self.jira_views[jv.name] = jv
                except ConfigError as ce:
                    print('ConfigError with jira view {}: {}'.format(name, ce))

            if config_parser.has_section('Dashboards'):
                for dash in config_parser.options('Dashboards'):
                    dash_views = {}
                    for view in view_names:
                        if view not in self.jira_views:
                            print(
                                'Found dashboard {} with invalid view: {}. Skipping init: manually remove from config.'
                                .format(dash, view))
                            break
                        dash_views[view] = self.jira_views[view]
                    self.jira_dashboards[dash] = JiraDashboard(
                        dash, dash_views)

        if len(self._jira_connections) == 0:
            print_separator(30)
            print(
                'No JIRA Connections found. Prompting to add first connection.'
            )
            self.add_connection()

        # Initialize JiraProjects from locally cached files
        for file_name in os.listdir(jira_project_dir):
            full_path = os.path.join(jira_project_dir, file_name)
            print(
                'Processing locally cached JiraProject: {}'.format(full_path))
            # Init based on matching the name of this connection and .cfg
            print_separator(30)
            try:
                new_jira_project = JiraProject.from_file(full_path, self)
                if new_jira_project is None:
                    print('Error initializing from {}. Skipping'.format(
                        full_path))
                    break
                if new_jira_project.jira_connection is None:
                    add = get_input(
                        'Did not find JiraConnection for JiraProject: {}. Would you like to add one now? (y/n)'
                    )
                    if add == 'y':
                        new_jira_connection = self.add_connection(
                            'Name the connection (reference url: {}):'.format(
                                new_jira_project.url))
                        new_jira_connection.save_config()
                        new_jira_project.jira_connection = new_jira_connection
                    else:
                        print(
                            'Did not add JiraConnection, so cannot link and use JiraProject.'
                        )
                        continue
                print('Updating with new data from JIRA instance')
                new_jira_project.refresh()
                new_jira_project.jira_connection.add_and_link_jira_project(
                    new_jira_project)
            except (configparser.NoSectionError, ConfigError) as e:
                print(
                    'WARNING! Encountered error initializing JiraProject from file {}: {}'
                    .format(full_path, e))
                print(
                    'This JiraProject will not be initialized. Remove it manually from disk in conf/jira/projects and data/jira/'
                )

        if os.path.exists('conf/custom_params.cfg'):
            config_parser = configparser.RawConfigParser()
            config_parser.read('conf/custom_params.cfg')
            custom_projects = config_parser.get('CUSTOM_PROJECTS',
                                                'project_names').split(',')

            for project_name in custom_projects:
                argus_debug(
                    'Processing immutable config for custom project: {}'.
                    format(project_name))
                # Find the JiraConnection w/matching URL, if any
                url = config_parser.get(project_name, 'url').rstrip('/')

                jira_project = self.maybe_get_cached_jira_project(
                    url, project_name)
                if jira_project is not None:
                    # Don't need to cache since already done on ctor for JiraProject
                    argus_debug('Project already initialized. Skipping.')
                    continue

                # Didn't find the JiraProject, so we need to build one, cache, and link.
                custom_fields = {}
                field_names = config_parser.get(project_name,
                                                'custom_fields').split(',')
                for field in field_names:
                    custom_fields[field] = config_parser.get(
                        project_name, field)

                parent_jira_connection = None
                for jira_connection in list(self._jira_connections.values()):
                    if jira_connection.url == url:
                        parent_jira_connection = jira_connection
                        break

                # Create a JiraConnection for this JiraProject if we do not yet have one
                if parent_jira_connection is None:
                    print(
                        'WARNING! Did not find JiraConnection for project: {}, attempting to match url: {}'
                        .format(project_name, url))
                    print('Known JiraConnections and their urls:')
                    for jira_connection in list(
                            self._jira_connections.values()):
                        print('   {}: {}'.format(
                            jira_connection.connection_name,
                            jira_connection.url))
                    if is_yes('Would you like to add one now?'):
                        parent_jira_connection = self.add_connection(
                            'Name the connection (reference url: {}):'.format(
                                url))
                    else:
                        print(
                            'JiraProject data and config will not be added nor cached. Either add it manually or restart Argus and reply y'
                        )
                        break

                new_jira_project = JiraProject(parent_jira_connection,
                                               project_name, url,
                                               custom_fields)
                new_jira_project.refresh()
                parent_jira_connection.add_and_link_jira_project(
                    new_jira_project)
        print('Resolving dependencies between JiraIssues')
        self._resolve_issue_dependencies()
        print('JiraManager initialization complete.')
Esempio n. 9
0
    def get_issues(self, string_matches=None):
        # type: (List[str]) -> Dict[str, JiraIssue]
        """
        Applies nested JiraFilters to all associated cached JiraProjects for the contained JiraConnection
        :param string_matches: substring(s) to match against JiraIssue fields for further refining of a search
        :return: {} of key -> JiraIssue that match JiraFilters and input regexes
        """

        if string_matches is None:
            string_matches = []

        source_issues = self.jira_connection.cached_jira_issues

        matching_issues = {}
        excluded_count = 0

        for issue_list in source_issues:
            for jira_issue in issue_list:
                matched = False

                has_or = False
                matched_or = False

                if utils.debug:
                    print_separator(30)
                    argus_debug(
                        'Matching against JiraIssue with key: {key}, assignee: {assignee}, rev: {rev}, rev2: {rev2}, res: {res}'
                        .format(
                            key=jira_issue.issue_key,
                            assignee=jira_issue['assignee'],
                            rev=jira_issue.get_value(self.jira_connection,
                                                     'reviewer'),
                            rev2=jira_issue.get_value(self.jira_connection,
                                                      'reviewer2'),
                            res=jira_issue.get_value(self.jira_connection,
                                                     'resolution')))
                    for jira_filter in list(self._jira_filters.values()):
                        argus_debug(
                            'Processing filter: {}'.format(jira_filter))

                excluded = False
                argus_debug('Checking jira_filter match for issue: {}'.format(
                    jira_issue.issue_key))
                for jira_filter in list(self._jira_filters.values()):
                    argus_debug('Processing filter: {}'.format(jira_filter))
                    # if we have an OR filter in the JiraFilter, we need to match at least one to be valid
                    if jira_filter.query_type() == 'OR':
                        has_or = True

                    if not jira_issue.matches_any(self.jira_connection,
                                                  string_matches):
                        argus_debug(
                            '   Skipping {}. Didn\'t match regexes: {}'.format(
                                jira_filter.extract_value(jira_issue),
                                ','.join(string_matches)))
                        excluded_count += 1
                        break

                    if jira_filter.includes_jira_issue(jira_issue):
                        argus_debug('   Matched: {} with value: {}'.format(
                            jira_filter,
                            jira_filter.extract_value(jira_issue)))
                        matched = True
                        if jira_filter.query_type() == 'OR':
                            matched_or = True
                    elif jira_filter.excludes_jira_issue(jira_issue):
                        argus_debug('   Excluded by: {} with value: {}'.format(
                            jira_filter,
                            jira_filter.extract_value(jira_issue)))
                        matched = True
                        excluded = True
                        break
                    # Didn't match and is required, we exclude this JiraIssue
                    elif jira_filter.query_type() == 'AND':
                        argus_debug(
                            '   Didn\'t match: {} with value: {} and was AND. Excluding.'
                            .format(jira_filter,
                                    jira_filter.extract_value(jira_issue)))
                        excluded = True
                    # Didn't match and was OR, don't flag anything
                    else:
                        argus_debug(
                            '   Didn\'t match: {} with value and was OR. Doing nothing: {}'
                            .format(jira_filter,
                                    jira_filter.extract_value(jira_issue)))

                    # Cannot short-circuit on match since exclusion beats inclusion and we have to keep checking, but can
                    # on exclusion bool
                    if excluded:
                        excluded_count += 1
                        break

                argus_debug('      key: {} matched: {}. excluded: {}'.format(
                    jira_issue.issue_key, matched, excluded))

                if not excluded:
                    if has_or and not matched_or:
                        argus_debug(
                            '   has_or on filter, did not match on or field. Excluding.'
                        )
                    elif matched:
                        matching_issues[jira_issue.issue_key] = jira_issue

        print(
            'Returning total of {} JiraIssues matching JiraView {}. Excluded count: {}'
            .format(len(list(matching_issues.keys())), self.name,
                    excluded_count))

        return matching_issues
Esempio n. 10
0
    def __init__(self, jira_manager: JiraManager, team_manager: TeamManager,
                 options: Dict[str, str]) -> None:
        self.jira_manager = jira_manager
        self.team_manager = team_manager

        # TODO: Clean up the coupling with main_menu.
        self.jenkins_manager = JenkinsManager(self)

        if 'triage_csv' in options:
            jira_connections = {}
            for jira_connection in self.jira_manager.jira_connections():
                argus_debug('Init connection: {}'.format(
                    jira_connection.connection_name))
                jira_connections[
                    jira_connection.connection_name] = jira_connection
            triage_update = TriageUpdate(
                jira_connections,
                self.jira_manager.get_all_cached_jira_projects())
            triage_out = options[
                'triage_out'] if 'triage_out' in options else None
            triage_update.process(options['triage_csv'], triage_out)

        if 'dashboard' in options:
            user_key = options['dashboard']
            dash_keys = list(self.jira_manager.jira_dashboards.keys())

            if user_key in dash_keys:
                self.jira_manager.jira_dashboards[user_key].display_dashboard(
                    self.jira_manager, self.jira_manager.jira_views)
            else:
                print('Oops... Error with dashboard name {}'.format(user_key))
                print('Possible dashboard names : {}'.format(
                    ','.join(dash_keys)))
                print('Starting Argus normally...')

        if 'verbose' in options:
            utils.debug = True
            utils.argus_log = open('argus.log', 'w')

        self.main_menu = [
            MenuOption('d',
                       'Dashboards',
                       self.go_to_dashboards_menu,
                       pause=False),
            MenuOption('v',
                       'Jira Views',
                       self.go_to_jira_views_menu,
                       pause=False),
            MenuOption('p',
                       'JiraProject Queries',
                       self.go_to_projects_menu,
                       pause=False),
            MenuOption.print_blank_line(),
            MenuOption('t',
                       'Run a Team-Based Report',
                       self._run_team_report,
                       pause=False),
            MenuOption('u',
                       'Run an org-based Report',
                       self._run_org_report,
                       pause=False),
            MenuOption('e',
                       'View Escalations',
                       self.jira_manager.display_escalations,
                       pause=False),
            MenuOption.print_blank_line(),
            MenuOption('r',
                       'Generate a Pre-Determined Report',
                       self.go_to_reports_menu,
                       pause=False),
            MenuOption('m',
                       'Team Management',
                       self.go_to_teams_menu,
                       pause=False),
            MenuOption('c',
                       'Jira Connections',
                       self.go_to_jira_connections_menu,
                       pause=False),
            MenuOption.print_blank_line(),
            MenuOption('j',
                       'Jenkins Menu',
                       self.go_to_jenkins_menu,
                       pause=False),
            MenuOption.print_blank_line(),
            MenuOption('o',
                       'Change Options',
                       self.go_to_options_menu,
                       pause=False),
            MenuOption.print_blank_line(),
            MenuOption('h', 'Help', self._display_readme, pause=False),
            MenuOption.quit_program()
        ]
        if Config.Experiment is True:
            self.main_menu.append(MenuOption.print_blank_line())
            self.main_menu.append(
                MenuOption('x',
                           'Debug',
                           self.jira_manager.run_debug,
                           pause=False))

        self.dashboards_menu = [
            MenuOption('l', 'List all available dashboards',
                       self.jira_manager.list_dashboards),
            MenuOption('d', 'Display a dashboard\'s results',
                       self.jira_manager.display_dashboard),
            MenuOption('c', 'Create a dashboard',
                       self.jira_manager.add_dashboard),
            MenuOption('e', 'Edit a dashboard',
                       self.jira_manager.edit_dashboard),
            MenuOption('r', 'Remove a dashboard',
                       self.jira_manager.remove_dashboard),
            MenuOption.print_blank_line(),
            MenuOption.return_to_previous_menu(self.go_to_main_menu)
        ]

        self.jira_views_menu = [
            MenuOption('l', 'List all defined JiraViews',
                       self.jira_manager.list_all_jira_views),
            MenuOption('d', 'Display a JiraView\'s results',
                       self.jira_manager.display_view),
            MenuOption('a', 'Add a JiraView', self._add_view),
            MenuOption('e', 'Edit a JiraView', self._edit_view),
            MenuOption('r', 'Remove a JiraView',
                       self.jira_manager.remove_view),
            MenuOption.print_blank_line(),
            MenuOption.return_to_previous_menu(self.go_to_main_menu)
        ]

        self.reports_menu = [
            MenuOption(
                'f',
                'FixVersion report (release). Query all tickets with a specified FixVersion',
                self.jira_manager.report_fix_version),
            MenuOption('s',
                       'Add a single-user multi-JIRA open ticket dashboard',
                       self._add_multi_jira_dashboard),
            MenuOption('l', 'Add a label-based cross-cutting view',
                       self.jira_manager.add_label_view),
            MenuOption.print_blank_line(),
            MenuOption.return_to_previous_menu(self.go_to_main_menu)
        ]

        self.team_menu = [
            MenuOption('l', 'List all defined Teams',
                       self.team_manager.list_teams),
            MenuOption('a', 'Add a new team', self._add_team),
            MenuOption('e', 'Edit an existing team', self._edit_team),
            MenuOption('r', 'Remove a team', self.team_manager.remove_team),
            MenuOption(
                'x',
                'Link a team member to two accounts across JiraConnections',
                self.add_linked_member),
            MenuOption('d', 'Delete a cross-Jira link',
                       self.team_manager.remove_linked_member),
            MenuOption('o', 'Add an organization',
                       self.team_manager.add_organization),
            MenuOption('p', 'Remove an organization',
                       self.team_manager.remove_organization),
            MenuOption.print_blank_line(),
            MenuOption.return_to_previous_menu(self.go_to_main_menu)
        ]

        self.jira_connections_menu = [
            MenuOption('a', 'Add a JIRA connection',
                       self.jira_manager.add_connection),
            MenuOption('r', 'Remove a JIRA connection and all related views',
                       self.jira_manager.remove_connection),
            MenuOption(
                'c',
                'Cache offline ticket data for a JiraProject on a connection',
                self.jira_manager.cache_new_jira_project_data),
            MenuOption(
                'd',
                'Delete offline cached ticket data for a JiraProject on a connection',
                self.jira_manager.delete_cached_jira_project),
            MenuOption('l', 'List all configured Jiraconnections',
                       self.jira_manager.list_jira_connections),
            MenuOption.print_blank_line(),
            MenuOption.return_to_previous_menu(self.go_to_main_menu)
        ]

        self.jenkins_menu = [
            MenuOption('r',
                       'Reports Manager',
                       self.go_to_jenkins_reports_manager_menu,
                       pause=False),
            MenuOption('c',
                       'Connections Manager',
                       self.go_to_jenkins_connections_manager_menu,
                       pause=False),
            MenuOption.print_blank_line(),
            MenuOption.return_to_previous_menu(self.go_to_main_menu)
        ]

        self.jenkins_reports_manager_menu = [
            MenuOption('o',
                       'Open custom report',
                       self.jenkins_manager.select_active_report,
                       pause=False),
            MenuOption('a',
                       'Add a custom report',
                       self.jenkins_manager.add_custom_report,
                       pause=False),
            MenuOption('r',
                       'Remove a custom report',
                       self.jenkins_manager.remove_custom_report,
                       pause=False),
            MenuOption('l', 'List custom reports',
                       self.jenkins_manager.list_custom_reports),
            MenuOption.print_blank_line(),
            MenuOption.return_to_previous_menu(self.go_to_jenkins_menu)
        ]

        self.jenkins_report_menu = [
            MenuOption('v', 'View report',
                       self.jenkins_manager.view_custom_report),
            MenuOption('a',
                       'Add a job',
                       self.jenkins_manager.add_custom_report_job,
                       pause=False),
            MenuOption('r',
                       'Remove a job',
                       self.jenkins_manager.remove_custom_report_job,
                       pause=False),
            MenuOption.print_blank_line(),
            MenuOption.return_to_previous_menu(
                self.go_to_jenkins_reports_manager_menu)
        ]

        self.jenkins_connections_manager_menu = [
            MenuOption('o',
                       'Open connection',
                       self.jenkins_manager.select_active_connection,
                       pause=False),
            MenuOption('a',
                       'Add a connection',
                       self.jenkins_manager.add_connection,
                       pause=False),
            MenuOption('r',
                       'Remove a connection',
                       self.jenkins_manager.remove_connection,
                       pause=False),
            MenuOption('l', 'List connections',
                       self.jenkins_manager.list_connections),
            MenuOption.print_blank_line(),
            MenuOption.return_to_previous_menu(self.go_to_jenkins_menu)
        ]

        self.jenkins_connection_menu = [
            MenuOption('v',
                       'View cached jobs',
                       self.jenkins_manager.view_cached_jobs,
                       pause=False),
            MenuOption('d',
                       'Download jobs to cache',
                       self.jenkins_manager.download_jobs,
                       pause=False),
            MenuOption.print_blank_line(),
            MenuOption('l', 'List saved views',
                       self.jenkins_manager.list_views),
            MenuOption('a',
                       'Add a view',
                       self.jenkins_manager.add_view,
                       pause=False),
            MenuOption('r',
                       'Remove a view',
                       self.jenkins_manager.remove_view,
                       pause=False),
            MenuOption.print_blank_line(),
            MenuOption.return_to_previous_menu(
                self.go_to_jenkins_connections_manager_menu)
        ]

        self.options_menu = [
            MenuOption('p', 'Change Argus password', self._change_password),
            MenuOption('b', 'Change browser', self._change_browser),
            MenuOption('v', 'Toggle Verbose/Debug', self._change_debug),
            MenuOption('d', 'Toggle Display dependencies',
                       self._change_show_dependencies),
            MenuOption('o', 'Toggle show open dependencies only',
                       self._change_dependency_type),
            MenuOption.print_blank_line(),
            MenuOption.return_to_previous_menu(self.go_to_main_menu)
        ]

        self.projects_menu = [
            MenuOption('l',
                       'List locally cached projects',
                       self.jira_manager.list_projects,
                       pause=True),
            MenuOption('s',
                       'Search locally cached JiraIssues for a string',
                       self.jira_manager.search_projects,
                       pause=False),
            MenuOption('a',
                       'Add new JiraProject offline cache',
                       self.jira_manager.cache_new_jira_project_data,
                       pause=True),
            MenuOption(
                'd',
                'Delete offline cached ticket data for a JiraProject on a connection',
                self.jira_manager.delete_cached_jira_project),
            MenuOption('u',
                       'Update all locally cached project JIRA data',
                       self.jira_manager.update_cached_jira_project_data,
                       pause=False),
            MenuOption.print_blank_line(),
            MenuOption.return_to_previous_menu(self.go_to_main_menu)
        ]

        self.active_menu = self.main_menu  # type: List[MenuOption]
        self.menu_header = 'uninit'  # type: str
        self.go_to_main_menu()

        self._load_config()

        # let user read startup info
        pause()