def test_down(self, monotonic_mock):
        checkerscript_path = os.path.join(os.path.dirname(__file__),
                                          'integration_down_checkerscript.py')

        monotonic_mock.return_value = 10
        master_loop = MasterLoop(self.connection, self.state_db_conn,
                                 'service1', checkerscript_path, None, 2, 1,
                                 10, '0.0.%s.1', b'secret', {}, DummyQueue())

        with transaction_cursor(self.connection) as cursor:
            cursor.execute('UPDATE scoring_gamecontrol SET start=NOW()')
            cursor.execute('UPDATE scoring_gamecontrol SET current_tick=0')
            cursor.execute(
                'INSERT INTO scoring_flag (service_id, protecting_team_id, tick)'
                '    VALUES (1, 2, 0)')
        monotonic_mock.return_value = 20

        master_loop.supervisor.queue_timeout = 0.01
        # Checker Script gets started, will return False because no messages yet
        self.assertFalse(master_loop.step())

        master_loop.supervisor.queue_timeout = 10
        while master_loop.step():
            pass
        with transaction_cursor(self.connection) as cursor:
            cursor.execute(
                'SELECT COUNT(*) FROM scoring_flag WHERE placement_end IS NOT NULL'
            )
            self.assertEqual(cursor.fetchone()[0], 1)
            cursor.execute('SELECT COUNT(*) FROM scoring_statuscheck')
            self.assertEqual(cursor.fetchone()[0], 1)
            cursor.execute('SELECT status FROM scoring_statuscheck'
                           '    WHERE service_id=1 AND team_id=2 AND tick=0')
            self.assertEqual(cursor.fetchone()[0], CheckResult.DOWN.value)
    def test_shutdown(self, monotonic_mock):
        checkerscript_path = '/dev/null'

        monotonic_mock.return_value = 10
        master_loop = MasterLoop(self.connection, self.state_db_conn,
                                 'service1', checkerscript_path, None, 2, 1,
                                 10, '0.0.%s.1', b'secret', {}, DummyQueue())

        with transaction_cursor(self.connection) as cursor:
            cursor.execute('UPDATE scoring_gamecontrol SET start=NOW()')
            cursor.execute('UPDATE scoring_gamecontrol SET current_tick=0')
            cursor.execute(
                'INSERT INTO scoring_flag (service_id, protecting_team_id, tick)'
                '    VALUES (1, 2, 0)')

        master_loop.shutting_down = True
        master_loop.supervisor.queue_timeout = 0.01
        monotonic_mock.return_value = 20
        # Will return False because no messages yet
        self.assertFalse(master_loop.step())
        with transaction_cursor(self.connection) as cursor:
            cursor.execute(
                'SELECT COUNT(*) FROM scoring_flag WHERE placement_start IS NOT NULL'
            )
            self.assertEqual(cursor.fetchone()[0], 0)
            cursor.execute('SELECT COUNT(*) FROM scoring_statuscheck')
            self.assertEqual(cursor.fetchone()[0], 0)
    def test_update_launch_params(self, check_duration_mock):
        # Very short duration, but should be ignored in tick 1
        check_duration_mock.return_value = 1

        self.master_loop.update_launch_params(-1)
        self.assertEqual(self.master_loop.tasks_per_launch, 0)

        with transaction_cursor(self.connection) as cursor:
            cursor.execute('UPDATE scoring_gamecontrol SET current_tick=1')
        self.master_loop.update_launch_params(1)
        self.assertEqual(self.master_loop.tasks_per_launch, 1)

        with transaction_cursor(self.connection) as cursor:
            for i in range(10, 400):
                username = '******'.format(i)
                email = '{}@example.org'.format(username)
                cursor.execute(
                    'INSERT INTO auth_user (id, username, first_name, last_name, email, password,'
                    '                       is_superuser, is_staff, is_active, date_joined)'
                    '    VALUES (%s, %s, %s, %s, %s, %s, false, false, true, NOW())',
                    (i, username, '', '', '', 'password'))
                cursor.execute(
                    'INSERT INTO registration_team (user_id, informal_email, image, affiliation,'
                    '                               country, nop_team)'
                    '    VALUES (%s, %s, %s, %s, %s, false)',
                    (i, email, '', '', 'World'))
                cursor.execute(
                    'INSERT INTO scoring_flag (service_id, protecting_team_id, tick)'
                    '    VALUES (1, %s, 1)', (i, ))
        self.master_loop.update_launch_params(1)
        self.assertEqual(self.master_loop.tasks_per_launch, 49)

        check_duration_mock.return_value = None
        self.master_loop.update_launch_params(10)
        self.assertEqual(self.master_loop.tasks_per_launch, 49)

        check_duration_mock.return_value = 3600
        self.master_loop.update_launch_params(10)
        self.assertEqual(self.master_loop.tasks_per_launch, 49)

        check_duration_mock.return_value = 90
        self.master_loop.update_launch_params(10)
        self.assertEqual(self.master_loop.tasks_per_launch, 7)

        self.master_loop.interval = 5
        self.master_loop.update_launch_params(10)
        self.assertEqual(self.master_loop.tasks_per_launch, 4)

        check_duration_mock.return_value = 10
        self.master_loop.interval = 10
        self.master_loop.tick_duration = datetime.timedelta(seconds=90)
        self.master_loop.update_launch_params(10)
        self.assertEqual(self.master_loop.tasks_per_launch, 7)
Exemple #4
0
def get_new_tasks(db_conn, service_id, task_count, prohibit_changes=False):
    """
    Retrieves the given number of random open check tasks and marks them as in progress.
    """

    with transaction_cursor(db_conn, prohibit_changes) as cursor:
        cursor.execute('SELECT flag.id, flag.protecting_team_id, flag.tick, team.net_number'
                       '    FROM scoring_flag flag, scoring_gamecontrol control, registration_team team'
                       '    WHERE flag.placement_start is NULL'
                       '        AND flag.tick = control.current_tick'
                       '        AND flag.service_id = %s'
                       '        AND flag.protecting_team_id = team.user_id'
                       '    ORDER BY RANDOM()'
                       '    LIMIT %s'
                       '    FOR UPDATE OF flag', (service_id, task_count))
        tasks = cursor.fetchall()

        # Mark placement as in progress
        cursor.executemany('UPDATE scoring_flag'
                           '    SET placement_start = NOW()'
                           '    WHERE id = %s', [(task[0],) for task in tasks])

    return [{
        'team_id': task[1],
        'team_net_no': task[3],
        'tick': task[2]
    } for task in tasks]
Exemple #5
0
def commit_result(db_conn, service_id, team_net_no, tick, result, prohibit_changes=False, fake_team_id=None):
    """
    Saves the result from a Checker run to game database.
    """

    with transaction_cursor(db_conn, prohibit_changes) as cursor:
        cursor.execute('SELECT user_id FROM registration_team'
                       '    WHERE net_number = %s', (team_net_no,))
        data = cursor.fetchone()
        if data is None:
            if fake_team_id is None:
                logging.error('No team found with net number %d, cannot commit result', team_net_no)
                return
            data = (fake_team_id,)
        team_id = data[0]

        cursor.execute('INSERT INTO scoring_statuscheck'
                       '    (service_id, team_id, tick, status, timestamp)'
                       '    VALUES (%s, %s, %s, %s, NOW())', (service_id, team_id, tick, result))
        # (In case of `prohibit_changes`,) PostgreSQL checks the database grants even if nothing is matched
        # by `WHERE`
        cursor.execute('UPDATE scoring_flag'
                       '    SET placement_end = NOW()'
                       '    WHERE service_id = %s AND protecting_team_id = %s AND tick = %s', (service_id,
                                                                                               team_id,
                                                                                               tick))
Exemple #6
0
def increase_tick(db_conn, prohibit_changes=False):

    with transaction_cursor(db_conn, prohibit_changes) as cursor:
        cursor.execute('UPDATE scoring_gamecontrol SET current_tick = current_tick + 1')
        # Create flags for every service and team in the new tick
        cursor.execute('INSERT INTO scoring_flag (service_id, protecting_team_id, tick)'
                       '    SELECT service.id, user_id, current_tick'
                       '    FROM scoring_service service, registration_team, scoring_gamecontrol')
Exemple #7
0
    def test_before_game(self, sleep_mock, _):
        with transaction_cursor(self.connection) as cursor:
            cursor.execute('UPDATE scoring_gamecontrol SET start = datetime("now", "+1 hour"), '
                           '                               end = datetime("now", "+1 day")')

        controller.main_loop_step(self.connection, self.metrics, False)

        sleep_mock.assert_called_once_with(60)

        with transaction_cursor(self.connection) as cursor:
            cursor.execute('SELECT current_tick FROM scoring_gamecontrol')
            new_tick = cursor.fetchone()[0]
        self.assertEqual(new_tick, -1)

        with transaction_cursor(self.connection) as cursor:
            cursor.execute('SELECT COUNT(*) FROM scoring_flag')
            total_flag_count = cursor.fetchone()[0]
        self.assertEqual(total_flag_count, 0)
Exemple #8
0
    def test_after_game_nonstop(self, sleep_mock, _):
        with transaction_cursor(self.connection) as cursor:
            cursor.execute('UPDATE scoring_gamecontrol SET start = datetime("now", "-1 day"), '
                           '                               end = datetime("now"), '
                           '                               current_tick=479')

        controller.main_loop_step(self.connection, self.metrics, True)
        sleep_mock.assert_called_once_with(0)

        with transaction_cursor(self.connection) as cursor:
            cursor.execute('SELECT current_tick FROM scoring_gamecontrol')
            new_tick = cursor.fetchone()[0]
        self.assertEqual(new_tick, 480)

        with transaction_cursor(self.connection) as cursor:
            cursor.execute('SELECT COUNT(*) FROM scoring_flag WHERE tick=480')
            tick_flag_count = cursor.fetchone()[0]
        self.assertEqual(tick_flag_count, 6)
    def test_prohibit_changes(self, _):
        with transaction_cursor(self.connection) as cursor:
            cursor.execute('SELECT * FROM scoring_gamecontrol ORDER BY id')
            old_gamecontrol = cursor.fetchall()
            cursor.execute('SELECT * FROM scoring_flag ORDER BY id')
            old_flag = cursor.fetchall()

        database.get_control_info(self.connection, prohibit_changes=True)
        database.increase_tick(self.connection, prohibit_changes=True)

        with transaction_cursor(self.connection) as cursor:
            cursor.execute('SELECT * FROM scoring_gamecontrol ORDER BY id')
            new_gamecontrol = cursor.fetchall()
            cursor.execute('SELECT * FROM scoring_flag ORDER BY id')
            new_flag = cursor.fetchall()

        self.assertEqual(old_gamecontrol, new_gamecontrol)
        self.assertEqual(old_flag, new_flag)
Exemple #10
0
    def test_next_tick_overdue(self, sleep_mock, _):
        with transaction_cursor(self.connection) as cursor:
            cursor.execute('UPDATE scoring_gamecontrol SET start = datetime("now", "-19 minutes"), '
                           '                               end = datetime("now", "+1421 minutes"), '
                           '                               current_tick=5')

        controller.main_loop_step(self.connection, self.metrics, False)

        sleep_mock.assert_called_once_with(0)

        with transaction_cursor(self.connection) as cursor:
            cursor.execute('SELECT current_tick FROM scoring_gamecontrol')
            new_tick = cursor.fetchone()[0]
        self.assertEqual(new_tick, 6)

        with transaction_cursor(self.connection) as cursor:
            cursor.execute('SELECT COUNT(*) FROM scoring_flag WHERE tick=6')
            tick_flag_count = cursor.fetchone()[0]
        self.assertEqual(tick_flag_count, 6)
    def test_handle_flag_request(self):
        with transaction_cursor(self.connection) as cursor:
            cursor.execute('UPDATE scoring_gamecontrol SET start=NOW()')

        task_info = {
            'service': 'service1',
            '_team_id': 2,
            'team': 92,
            'tick': 1
        }

        params1 = {'tick': 1}
        resp1 = self.master_loop.handle_flag_request(task_info, params1)
        params2 = {'tick': 1}
        resp2 = self.master_loop.handle_flag_request(task_info, params2)
        params3 = {'tick': 1, 'payload': 'TmV2ZXIgZ28='}
        resp3 = self.master_loop.handle_flag_request(task_info, params3)

        self.assertEqual(resp1, resp2)
        self.assertNotEqual(resp1, resp3)

        params4 = {'tick': 2}
        resp4 = self.master_loop.handle_flag_request(task_info, params4)
        params5 = {'tick': 2}
        resp5 = self.master_loop.handle_flag_request(task_info, params5)

        self.assertEqual(resp4, resp5)
        self.assertNotEqual(resp1, resp4)

        params6 = {}
        self.assertIsNone(
            self.master_loop.handle_flag_request(task_info, params6))

        # Changing the start time changes all flags
        with transaction_cursor(self.connection) as cursor:
            # SQLite syntax for tests
            cursor.execute(
                'UPDATE scoring_gamecontrol SET start=DATETIME("now", "+1 hour")'
            )
        resp1_again = self.master_loop.handle_flag_request(task_info, params1)
        resp4_again = self.master_loop.handle_flag_request(task_info, params4)
        self.assertNotEqual(resp1, resp1_again)
        self.assertNotEqual(resp4, resp4_again)
Exemple #12
0
    def test_long_after_game(self, sleep_mock, _):
        with transaction_cursor(self.connection) as cursor:
            cursor.execute('UPDATE scoring_gamecontrol SET start = datetime("now", "-1465 minutes"), '
                           '                               end = datetime("now", "-25 minutes"), '
                           '                               current_tick=479')

        controller.main_loop_step(self.connection, self.metrics, False)
        self.assertEqual(sleep_mock.call_count, 2)
        self.assertEqual(sleep_mock.call_args_list[0][0][0], 0)
        self.assertEqual(sleep_mock.call_args_list[1][0][0], 60)

        with transaction_cursor(self.connection) as cursor:
            cursor.execute('SELECT current_tick FROM scoring_gamecontrol')
            new_tick = cursor.fetchone()[0]
        self.assertEqual(new_tick, 479)

        with transaction_cursor(self.connection) as cursor:
            cursor.execute('SELECT COUNT(*) FROM scoring_flag')
            total_flag_count = cursor.fetchone()[0]
        self.assertEqual(total_flag_count, 0)
Exemple #13
0
def store_state(db_conn, service_id, team_net_no, identifier, data, prohibit_changes=False):
    """
    Stores Checker data in state database.
    """

    with transaction_cursor(db_conn, prohibit_changes) as cursor:
        # (In case of `prohibit_changes`,) PostgreSQL checks the database grants even if no CONFLICT occurs
        cursor.execute('INSERT INTO checkerstate (service_id, team_net_no, identifier, data)'
                       '    VALUES (%s, %s, %s, %s)'
                       '    ON CONFLICT (service_id, team_net_no, identifier)'
                       '        DO UPDATE SET data = EXCLUDED.data', (service_id, team_net_no, identifier,
                                                                      data))
Exemple #14
0
def _get_flags_counts(db_conn, flag_where_clause, prohibit_changes):

    with transaction_cursor(db_conn, prohibit_changes) as cursor:
        cursor.execute(
            'SELECT service.slug, COUNT(flag.id)'  # nosec
            '    FROM scoring_service service'
            '    LEFT JOIN (SELECT * FROM scoring_flag WHERE {}) AS flag'
            '        ON flag.service_id=service.id'
            '    GROUP BY service.slug'.format(flag_where_clause))
        counts = cursor.fetchall()

    return dict(counts)
Exemple #15
0
    def test_first_tick(self, sleep_mock, _):
        with transaction_cursor(self.connection) as cursor:
            cursor.execute('UPDATE scoring_gamecontrol SET start = datetime("now"), '
                           '                               end = datetime("now", "+1 day")')

        controller.main_loop_step(self.connection, self.metrics, False)
        sleep_mock.assert_called_once_with(0)

        with transaction_cursor(self.connection) as cursor:
            cursor.execute('SELECT current_tick FROM scoring_gamecontrol')
            new_tick = cursor.fetchone()[0]
        self.assertEqual(new_tick, 0)

        with transaction_cursor(self.connection) as cursor:
            cursor.execute('SELECT COUNT(*) FROM scoring_flag')
            total_flag_count = cursor.fetchone()[0]
        self.assertEqual(total_flag_count, 6)

        with transaction_cursor(self.connection) as cursor:
            cursor.execute('SELECT COUNT(*) FROM scoring_flag WHERE service_id=1')
            service_flag_count = cursor.fetchone()[0]
        self.assertEqual(service_flag_count, 3)

        with transaction_cursor(self.connection) as cursor:
            cursor.execute('SELECT COUNT(*) FROM scoring_flag WHERE protecting_team_id=4')
            team_flag_count = cursor.fetchone()[0]
        self.assertEqual(team_flag_count, 2)

        with transaction_cursor(self.connection) as cursor:
            cursor.execute('SELECT COUNT(*) FROM scoring_flag WHERE tick=0')
            tick_flag_count = cursor.fetchone()[0]
        self.assertEqual(tick_flag_count, 6)
Exemple #16
0
def update_scoring(db_conn):

    with transaction_cursor(db_conn) as cursor:
        cursor.execute('UPDATE scoring_flag as outerflag'
                       '    SET bonus = 1 / ('
                       '        SELECT greatest(1, count(*))'
                       '        FROM scoring_flag'
                       '        LEFT OUTER JOIN scoring_capture ON scoring_capture.flag_id = scoring_flag.id'
                       '        WHERE scoring_capture.flag_id = outerflag.id)'
                       '    FROM scoring_gamecontrol'
                       '    WHERE outerflag.tick + scoring_gamecontrol.valid_ticks < '
                       '        scoring_gamecontrol.current_tick AND outerflag.bonus IS NULL')
        cursor.execute('REFRESH MATERIALIZED VIEW "scoring_scoreboard"')
Exemple #17
0
    def test_next_tick_undue(self, sleep_mock, _):
        with transaction_cursor(self.connection) as cursor:
            cursor.execute('UPDATE scoring_gamecontrol SET start = datetime("now", "-1030 seconds"), '
                           '                               end = datetime("now", "+85370 seconds"), '
                           '                               current_tick=5')

        controller.main_loop_step(self.connection, self.metrics, False)

        sleep_mock.assert_called_once()
        sleep_arg = sleep_mock.call_args[0][0]
        self.assertGreater(sleep_arg, 40)
        self.assertLessEqual(sleep_arg, 50)

        with transaction_cursor(self.connection) as cursor:
            cursor.execute('SELECT current_tick FROM scoring_gamecontrol')
            new_tick = cursor.fetchone()[0]
        self.assertEqual(new_tick, 5)

        with transaction_cursor(self.connection) as cursor:
            cursor.execute('SELECT COUNT(*) FROM scoring_flag')
            tick_flag_count = cursor.fetchone()[0]
        self.assertEqual(tick_flag_count, 0)
Exemple #18
0
def get_exploiting_teams_counts(db_conn, prohibit_changes=False):

    with transaction_cursor(db_conn, prohibit_changes) as cursor:
        cursor.execute(
            'SELECT service.slug, COUNT(DISTINCT capture.capturing_team_id)'
            '    FROM scoring_service service'
            '    JOIN scoring_flag flag ON flag.service_id = service.id'
            '    LEFT JOIN (SELECT * FROM scoring_capture) AS capture'
            '        ON capture.flag_id = flag.id'
            '    GROUP BY service.slug')
        counts = cursor.fetchall()

    return dict(counts)
Exemple #19
0
def get_current_tick(db_conn, prohibit_changes=False):
    """
    Reads the current tick from the game database.
    """

    with transaction_cursor(db_conn, prohibit_changes) as cursor:
        cursor.execute('SELECT current_tick FROM scoring_gamecontrol')
        result = cursor.fetchone()

    if result is None:
        raise DBDataError('Game control information has not been configured')

    return result[0]
Exemple #20
0
def get_task_count(db_conn, service_id, prohibit_changes=False):
    """
    Returns the total number of tasks for the given service in the current tick.
    With our current Controller implementation, this should always be equal to the number of teams.
    """

    with transaction_cursor(db_conn, prohibit_changes) as cursor:
        cursor.execute('SELECT COUNT(*)'
                       '    FROM scoring_flag flag, scoring_gamecontrol control'
                       '    WHERE flag.tick = control.current_tick'
                       '        AND flag.service_id = %s', (service_id,))
        result = cursor.fetchone()

    return result[0]
Exemple #21
0
def main():

    arg_parser = get_arg_parser_with_db('CTF Gameserver Controller')
    arg_parser.add_argument(
        '--nonstop',
        action='store_true',
        help='Use current time as start time and '
        'ignore CTF end time from the database. Useful for testing checkers.')

    args = arg_parser.parse_args()

    logging.basicConfig(format='[%(levelname)s] %(message)s')
    numeric_loglevel = getattr(logging, args.loglevel.upper())
    logging.getLogger().setLevel(numeric_loglevel)

    try:
        db_conn = psycopg2.connect(host=args.dbhost,
                                   database=args.dbname,
                                   user=args.dbuser,
                                   password=args.dbpassword)
    except psycopg2.OperationalError as e:
        logging.error('Could not establish database connection: %s', e)
        return os.EX_UNAVAILABLE
    logging.info('Established database connection')

    # Keep our mental model easy by always using (timezone-aware) UTC for dates and times
    with transaction_cursor(db_conn) as cursor:
        cursor.execute('SET TIME ZONE "UTC"')

    # Check database grants
    try:
        try:
            database.get_control_info(db_conn, prohibit_changes=True)
        except DBDataError as e:
            logging.warning('Invalid database state: %s', e)

        database.increase_tick(db_conn, prohibit_changes=True)
    except psycopg2.ProgrammingError as e:
        if e.pgcode == postgres_errors.INSUFFICIENT_PRIVILEGE:
            # Log full exception because only the backtrace will tell which kind of permission is missing
            logging.exception('Missing database permissions:')
            return os.EX_NOPERM
        else:
            raise

    daemon.notify('READY=1')

    while True:
        main_loop_step(db_conn, args.nonstop)
Exemple #22
0
def load_state(db_conn, service_id, team_net_no, identifier, prohibit_changes=False):
    """
    Loads Checker data from state database.
    """

    with transaction_cursor(db_conn, prohibit_changes) as cursor:
        cursor.execute('SELECT data FROM checkerstate'
                       '    WHERE service_id = %s'
                       '        AND team_net_no = %s'
                       '        AND identifier = %s', (service_id, team_net_no, identifier))
        data = cursor.fetchone()

    if data is None:
        return None
    return data[0]
    def setUp(self):
        self.state_db_conn = sqlite3.connect(':memory:')
        with transaction_cursor(self.state_db_conn) as cursor:
            cursor.execute(
                'CREATE TABLE checkerstate ('
                '    team_net_no INTEGER,'
                '    service_id INTEGER,'
                '    identifier CHARACTER VARYING (128),'
                '    data TEXT, '
                '    PRIMARY KEY (team_net_no, service_id, identifier)'
                ')')

        self.check_duration_patch = patch(
            'ctf_gameserver.checker.database.get_check_duration')
        check_duration_mock = self.check_duration_patch.start()
        check_duration_mock.return_value = None
Exemple #24
0
def get_service_attributes(db_conn, service_slug, prohibit_changes=False):
    """
    Returns ID and name of a service for a given slug.
    """

    with transaction_cursor(db_conn, prohibit_changes) as cursor:
        cursor.execute('SELECT id, name FROM scoring_service WHERE slug = %s', (service_slug,))
        result = cursor.fetchone()

    if result is None:
        raise DBDataError('Service has not been configured')

    return {
        'id': result[0],
        'name': result[1]
    }
Exemple #25
0
def get_control_info(db_conn, prohibit_changes=False):
    """
    Returns a dictionary containing relevant information about the competion, as stored in the game database.
    """

    with transaction_cursor(db_conn, prohibit_changes) as cursor:
        cursor.execute('SELECT start, valid_ticks, tick_duration FROM scoring_gamecontrol')
        result = cursor.fetchone()

    if result is None:
        raise DBDataError('Game control information has not been configured')

    return {
        'contest_start': result[0],
        'valid_ticks': result[1],
        'tick_duration': result[2]
    }
Exemple #26
0
def get_check_duration(db_conn, service_id, std_dev_count, prohibit_changes=False):
    """
    Estimates the duration of checks for the given service from the average runtime of previous runs and its
    standard deviation. We include all previous runs to accomodate to Checker Scripts with varying runtimes.
    `std_dev_count` is the number of standard deviations to add to the average, i.e. increasing it will lead
    to a greater result. Assuming a normal distribution, 2 standard deviations will include ~ 95 % of
    previous results.
    """

    with transaction_cursor(db_conn, prohibit_changes) as cursor:
        cursor.execute('SELECT avg(extract(epoch from (placement_end - placement_start))) + %s *'
                       '       stddev_pop(extract(epoch from (placement_end - placement_start)))'
                       '    FROM scoring_flag, scoring_gamecontrol'
                       '    WHERE service_id = %s AND tick < current_tick', (std_dev_count, service_id))
        result = cursor.fetchone()

    return result[0]
Exemple #27
0
def get_control_info(db_conn, prohibit_changes=False):
    """
    Returns a dictionary contatining relevant information about the competion, as stored in the database.
    """

    with transaction_cursor(db_conn, prohibit_changes) as cursor:
        cursor.execute('SELECT start, "end", tick_duration, current_tick FROM scoring_gamecontrol')
        result = cursor.fetchone()

    if result is None:
        raise DBDataError('Game control information has not been configured')
    start, end, duration, tick = result

    return {
        'start': ensure_utc_aware(start),
        'end': ensure_utc_aware(end),
        'tick_duration': duration,
        'current_tick': tick
    }
    def test_handle_result_request(self):
        task_info = {
            'service': 'service1',
            '_team_id': 2,
            'team': 92,
            'tick': 1
        }
        param = CheckResult.OK.value
        start_time = datetime.datetime.utcnow().replace(microsecond=0)
        self.assertIsNone(
            self.master_loop.handle_result_request(task_info, param))
        with transaction_cursor(self.connection) as cursor:
            cursor.execute('SELECT COUNT(*) FROM scoring_statuscheck')
            self.assertEqual(cursor.fetchone()[0], 1)
            cursor.execute(
                'SELECT status FROM scoring_statuscheck'
                '    WHERE service_id = 1 AND team_id = 2 AND tick = 1')
            self.assertEqual(cursor.fetchone()[0], CheckResult.OK.value)
            cursor.execute(
                'SELECT placement_end FROM scoring_flag'
                '    WHERE service_id = 1 AND protecting_team_id = 2 AND tick = 1'
            )
            self.assertGreaterEqual(cursor.fetchone()[0], start_time)

        task_info['tick'] = 2
        param = CheckResult.FAULTY.value
        start_time = datetime.datetime.utcnow().replace(microsecond=0)
        self.assertIsNone(
            self.master_loop.handle_result_request(task_info, param))
        with transaction_cursor(self.connection) as cursor:
            cursor.execute(
                'SELECT status FROM scoring_statuscheck'
                '    WHERE service_id = 1 AND team_id = 2 AND tick = 2')
            self.assertEqual(cursor.fetchone()[0], CheckResult.FAULTY.value)
            cursor.execute(
                'SELECT placement_end FROM scoring_flag'
                '    WHERE service_id = 1 AND protecting_team_id = 2 AND tick = 2'
            )
            self.assertGreaterEqual(cursor.fetchone()[0], start_time)

        task_info['tick'] = 3
        param = 'Not an int'
        self.assertIsNone(
            self.master_loop.handle_result_request(task_info, param))
        with transaction_cursor(self.connection) as cursor:
            cursor.execute(
                'SELECT status FROM scoring_statuscheck'
                '    WHERE service_id = 1 AND team_id = 2 AND tick = 3')
            self.assertIsNone(cursor.fetchone())
            cursor.execute(
                'SELECT placement_end FROM scoring_flag'
                '    WHERE service_id = 1 AND protecting_team_id = 2 AND tick = 3'
            )
            self.assertIsNone(cursor.fetchone()[0])

        param = 1337
        self.assertIsNone(
            self.master_loop.handle_result_request(task_info, param))
        with transaction_cursor(self.connection) as cursor:
            cursor.execute(
                'SELECT status FROM scoring_statuscheck'
                '    WHERE service_id = 1 AND team_id = 2 AND tick = 3')
            self.assertIsNone(cursor.fetchone())
            cursor.execute(
                'SELECT placement_end FROM scoring_flag'
                '    WHERE service_id = 1 AND protecting_team_id = 2 AND tick = 3'
            )
            self.assertIsNone(cursor.fetchone()[0])
    def test_state(self, monotonic_mock):
        checkerscript_path = os.path.join(
            os.path.dirname(__file__), 'integration_state_checkerscript.py')

        monotonic_mock.return_value = 10
        master_loop = MasterLoop(self.connection, self.state_db_conn,
                                 'service1', checkerscript_path, None, 2, 1,
                                 10, '0.0.%s.1', b'secret', {}, DummyQueue())

        with transaction_cursor(self.state_db_conn) as cursor:
            # Prepopulate state for the non-checked service to ensure we'll never get this data returned
            data = 'gAN9cQBYAwAAAGZvb3EBWAMAAABiYXJxAnMu'
            cursor.execute(
                'INSERT INTO checkerstate (team_net_no, service_id, identifier, data)'
                '    VALUES (92, 2, %s, %s), (93, 2, %s, %s)',
                ('key1', data, 'key2', data))

        # Tick 0
        with transaction_cursor(self.connection) as cursor:
            cursor.execute('UPDATE scoring_gamecontrol SET start=NOW()')
            cursor.execute('UPDATE scoring_gamecontrol SET current_tick=0')
            cursor.execute(
                'INSERT INTO scoring_flag (service_id, protecting_team_id, tick)'
                '    VALUES (1, 2, 0), (1, 3, 0)')
        monotonic_mock.return_value = 20
        master_loop.supervisor.queue_timeout = 0.01
        self.assertFalse(master_loop.step())
        monotonic_mock.return_value = 100
        master_loop.supervisor.queue_timeout = 10
        while master_loop.step() or master_loop.get_running_script_count() > 0:
            pass
        with transaction_cursor(self.connection) as cursor:
            cursor.execute('SELECT COUNT(*) FROM scoring_flag'
                           '    WHERE placement_end IS NOT NULL')
            self.assertEqual(cursor.fetchone()[0], 2)
            cursor.execute(
                'SELECT COUNT(*) FROM scoring_statuscheck WHERE status=%s',
                (CheckResult.OK.value, ))
            self.assertEqual(cursor.fetchone()[0], 2)

        # Tick 1
        with transaction_cursor(self.connection) as cursor:
            cursor.execute('UPDATE scoring_gamecontrol SET current_tick=1')
            cursor.execute(
                'INSERT INTO scoring_flag (service_id, protecting_team_id, tick)'
                '    VALUES (1, 2, 1), (1, 3, 1)')
        monotonic_mock.return_value = 200
        master_loop.supervisor.queue_timeout = 0.01
        self.assertFalse(master_loop.step())
        monotonic_mock.return_value = 280
        master_loop.supervisor.queue_timeout = 10
        while master_loop.step() or master_loop.get_running_script_count() > 0:
            pass
        with transaction_cursor(self.connection) as cursor:
            cursor.execute('SELECT COUNT(*) FROM scoring_flag'
                           '    WHERE placement_end IS NOT NULL')
            self.assertEqual(cursor.fetchone()[0], 4)
            cursor.execute(
                'SELECT COUNT(*) FROM scoring_statuscheck WHERE status=%s',
                (CheckResult.OK.value, ))
            self.assertEqual(cursor.fetchone()[0], 4)

        # Tick 2
        with transaction_cursor(self.connection) as cursor:
            cursor.execute('UPDATE scoring_gamecontrol SET current_tick=2')
            cursor.execute(
                'INSERT INTO scoring_flag (service_id, protecting_team_id, tick)'
                '    VALUES (1, 2, 2), (1, 3, 2)')
        monotonic_mock.return_value = 380
        master_loop.supervisor.queue_timeout = 0.01
        self.assertFalse(master_loop.step())
        monotonic_mock.return_value = 460
        master_loop.supervisor.queue_timeout = 10
        while master_loop.step() or master_loop.get_running_script_count() > 0:
            pass
        with transaction_cursor(self.connection) as cursor:
            cursor.execute('SELECT COUNT(*) FROM scoring_flag'
                           '    WHERE placement_end IS NOT NULL')
            self.assertEqual(cursor.fetchone()[0], 6)
            cursor.execute(
                'SELECT COUNT(*) FROM scoring_statuscheck WHERE status=%s',
                (CheckResult.OK.value, ))
            self.assertEqual(cursor.fetchone()[0], 6)
    def test_sudo_unfinished(self, monotonic_mock, warning_mock):
        if shutil.which('sudo') is None or not os.path.exists(
                '/etc/sudoers.d/ctf-checker'):
            raise SkipTest('sudo or sudo config not available')

        checkerscript_path = os.path.join(
            os.path.dirname(__file__),
            'integration_unfinished_checkerscript.py')

        checkerscript_pidfile = tempfile.NamedTemporaryFile()
        os.chmod(checkerscript_pidfile.name, 0o666)
        os.environ['CHECKERSCRIPT_PIDFILE'] = checkerscript_pidfile.name

        monotonic_mock.return_value = 10
        master_loop = MasterLoop(self.connection, self.state_db_conn,
                                 'service1', checkerscript_path,
                                 'ctf-checkerrunner', 2, 1, 10, '0.0.%s.1',
                                 b'secret', {}, DummyQueue())

        with transaction_cursor(self.connection) as cursor:
            cursor.execute('UPDATE scoring_gamecontrol SET start=NOW()')
            cursor.execute('UPDATE scoring_gamecontrol SET current_tick=0')
            cursor.execute(
                'INSERT INTO scoring_flag (service_id, protecting_team_id, tick)'
                '    VALUES (1, 2, 0)')
        monotonic_mock.return_value = 20

        master_loop.supervisor.queue_timeout = 0.01
        # Checker Script gets started, will return False because no messages yet
        self.assertFalse(master_loop.step())
        master_loop.supervisor.queue_timeout = 10
        self.assertTrue(master_loop.step())

        checkerscript_pidfile.seek(0)
        checkerscript_pid = int(checkerscript_pidfile.read())

        def signal_script():
            subprocess.check_call([
                'sudo', '--user=ctf-checkerrunner', '--', 'kill', '-0',
                str(checkerscript_pid)
            ])

        # Ensure process is running by sending signal 0
        signal_script()

        master_loop.supervisor.queue_timeout = 0.01
        monotonic_mock.return_value = 50
        self.assertFalse(master_loop.step())
        # Process should still be running
        signal_script()

        with transaction_cursor(self.connection) as cursor:
            cursor.execute('UPDATE scoring_gamecontrol SET current_tick=1')
        monotonic_mock.return_value = 190
        self.assertFalse(master_loop.step())
        # Poll whether the process has been killed
        for _ in range(100):
            try:
                signal_script()
            except subprocess.CalledProcessError:
                break
            time.sleep(0.1)
        with self.assertRaises(subprocess.CalledProcessError):
            signal_script()

        with transaction_cursor(self.connection) as cursor:
            cursor.execute(
                'SELECT COUNT(*) FROM scoring_flag'
                '    WHERE placement_start IS NOT NULL AND placement_end IS NULL'
            )
            self.assertEqual(cursor.fetchone()[0], 1)
            cursor.execute('SELECT COUNT(*) FROM scoring_statuscheck')
            self.assertEqual(cursor.fetchone()[0], 0)

        warning_mock.assert_called_with('Terminating all %d Runner processes',
                                        1)

        del os.environ['CHECKERSCRIPT_PIDFILE']
        checkerscript_pidfile.close()