Chromium Code Reviews| Index: infra/tools/master_manager_launcher/desired_state_parser.py |
| diff --git a/infra/tools/master_manager_launcher/desired_state_parser.py b/infra/tools/master_manager_launcher/desired_state_parser.py |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..2230be9225224834808d275398496a8a3cf0299f |
| --- /dev/null |
| +++ b/infra/tools/master_manager_launcher/desired_state_parser.py |
| @@ -0,0 +1,122 @@ |
| +#!/usr/bin/python |
| +# Copyright 2015 Google Inc. All Rights Reserved. |
| +# pylint: disable=F0401 |
| + |
| +"""Parse, validate and query the desired master state json.""" |
| + |
| +import bisect |
| +import json |
| +import logging |
| +import operator |
| +import os |
| + |
| +from infra.libs.buildbot import master |
| +from infra.libs.time_functions import timestamp |
| +from infra.services.master_lifecycle import buildbot_state |
| + |
| + |
| +LOGGER = logging.getLogger(__name__) |
| + |
| + |
| +def load_desired_state_file(filename): # pragma: no cover |
|
agable
2015/05/06 23:29:30
Feels odd. Not finding the file is exception-worth
ghost stip (do not use)
2015/05/07 06:50:57
Done.
|
| + with open(filename) as f: |
| + desired_state = json.load(f) |
| + if not desired_master_state_is_valid(desired_state): |
| + return None |
| + return desired_state |
| + |
| + |
| +def desired_master_state_is_valid(desired_state): |
| + """Verify that the desired_master_state file is valid.""" |
| + now = timestamp.utcnow_ts() |
| + |
| + for mastername, states in desired_state.iteritems(): |
| + # Verify desired_state and transition_time_utc are present. |
| + for k in ('desired_state', 'transition_time_utc'): |
| + if not all(k in state for state in states): |
| + LOGGER.error( |
| + 'one or more states for master %s do not contain %s', mastername, k) |
| + return False |
| + |
| + # Verify the list is properly sorted. |
| + sorted_states = sorted( |
|
agable
2015/05/06 23:29:30
Why does it matter if they're sorted in the file w
ghost stip (do not use)
2015/05/07 06:50:57
Because a human editing / reading the file would b
agable
2015/05/07 17:59:31
Fair enough, that makes sense.
|
| + states, key=operator.itemgetter('transition_time_utc')) |
| + if sorted_states != states: |
| + LOGGER.error('master %s does not have states sorted by timestamp', |
| + mastername) |
| + LOGGER.error('should be:\n%s', json.dumps(sorted_states, indent=2)) |
| + return False |
| + |
| + # Verify desired_state and timestamp are valid. |
| + for state in states: |
| + if (state['desired_state'] not in |
| + buildbot_state.STATES['desired_buildbot_state']): |
| + LOGGER.error( |
| + 'desired_state \'%s\' is not one of %s', |
| + state['desired_state'], |
| + buildbot_state.STATES['desired_buildbot_state']) |
| + return False |
| + |
| + if not isinstance(state['transition_time_utc'], (int, float)): |
| + LOGGER.error( |
| + 'transition_time_utc \'%s\' is not an int or float', |
| + state['transition_time_utc']) |
| + return False |
| + |
| + # Verify there is at least one state in the past. |
| + if not get_master_state(states, now=now): |
|
agable
2015/05/06 23:29:30
Why? Why can't I create the file containing just a
iannucci
2015/05/06 23:48:05
Because then the script doesn't know what state it
ghost stip (do not use)
2015/05/07 06:50:57
Right, this is a discussion Robbie and I had in mo
agable
2015/05/07 17:59:31
I don't see how my proposal goes against your disc
ghost stip (do not use)
2015/05/07 19:49:39
There is only one case where a person who wants to
agable
2015/05/08 00:14:53
Case 1.5: The file used to contain a bunch of entr
ghost stip (do not use)
2015/05/08 00:22:13
You can't clean it out for readability fully, beca
|
| + LOGGER.error( |
| + 'master %s does not have a state older than %s', mastername, now) |
| + return False |
| + |
| + return True |
| + |
| + |
| +def get_master_state(states, now=None): |
| + """Returns the latest state earlier than the current (or specified) time. |
| + |
| + If there are three items, each with transition times of 100, 200 and 300: |
| + * calling when 'now' is 50 will return None |
| + * calling when 'now' is 150 will return the first item |
| + * calling when 'now' is 400 will return the third item |
| + """ |
| + now = now or timestamp.utcnow_ts() |
| + |
| + times = [x['transition_time_utc'] for x in states] |
| + index = bisect.bisect_left(times, now) |
| + if index: |
| + return states[index - 1] |
| + return None |
| + |
| + |
| +def get_masters_for_host(desired_state, build_dir, hostname): |
| + """Identify which masters on this host should be managed. |
| + |
| + Returns two lists: triggered_masters and ignored_masters. |
|
agable
2015/05/06 23:29:30
Any reason they're sets instead of lists? Ordering
ghost stip (do not use)
2015/05/07 06:50:57
TypeError: unhashable type: 'dict'
ignored_master
|
| + |
| + triggered_masters is a list of dicts. Each dict is the full dict from |
| + mastermap.py with two extra keys: 'fulldir' (the absolute path to the master |
| + directory), and 'states' (a list of desired states sorted by transition time, |
| + pulled from the desired states file). |
| + |
| + ignored_masters is merely a list of 'dirname' strings (ex: master.chromium). |
|
agable
2015/05/06 23:29:30
Docstring should explain why they're ignored
ghost stip (do not use)
2015/05/07 06:50:57
Done.
|
| + """ |
| + triggered_masters = [] |
| + ignored_masters = [] |
| + for master_dict in master.get_mastermap_for_host( |
| + build_dir, hostname): |
| + if master_dict['dirname'] in desired_state: |
| + if master_dict['internal']: |
| + master_dir = os.path.abspath(os.path.join( |
| + build_dir, os.pardir, 'build_internal', 'masters', |
| + master_dict['dirname'])) |
| + else: |
| + master_dir = os.path.abspath(os.path.join( |
| + build_dir, 'masters', master_dict['dirname'])) |
| + master_dict['fulldir'] = master_dir |
| + master_dict['states'] = desired_state[master_dict['dirname']] |
| + |
| + triggered_masters.append(master_dict) |
| + else: |
| + ignored_masters.append(master_dict['dirname']) |
| + return triggered_masters, ignored_masters |