| """Utility methods to retrieve devicemap.yaml data.""" |
| |
| import json |
| import logging |
| import os |
| import yaml |
| |
| |
| class Error(Exception): |
| pass |
| |
| |
| class DeviceMapError(Error): |
| pass |
| |
| |
| class Configs(object): |
| """Represents all of the configs in the devicemap.""" |
| _DEVICE_NODE_NAME = 'hosted_devices' |
| _HOST_IP = 'host_ip' |
| |
| def __init__(self, configs_dict=None): |
| self._configs_dict = configs_dict or _ReadDeviceMap() |
| self.allowed_dimensions = self._configs_dict.get('allowed_dimensions') |
| self.hosts = self._configs_dict.get('hosts') |
| |
| def GetAllowedDimensions(self, config_type): |
| """Gets the set() of allowed dimensions for the given type of config.""" |
| if config_type not in self.allowed_dimensions: |
| raise DeviceMapError('config type not defined: %s' % config_type) |
| return set(self.allowed_dimensions[config_type]) |
| |
| def GetBotConfig(self, hostname, bot_num): |
| """Gets the bot config for the given hostname & bot_num (1-indexed).""" |
| host = self._GetHostConfig(hostname) |
| hosted_devices = host[self._DEVICE_NODE_NAME] |
| device_index = bot_num - 1 |
| if device_index >= len(hosted_devices): |
| raise DeviceMapError('Bot #%d is not defined under host %s' % ( |
| bot_num, hostname)) |
| config_dict = hosted_devices[device_index] |
| host_dimensions = self._GetHostDimensions(host) |
| return BotConfig(config_dict, host_dimensions) |
| |
| def GetNumBotConfigs(self, hostname): |
| """Gets the # of bot configs assigned to the given hostname.""" |
| return len(self._GetHostedDevices(hostname)) |
| |
| def _GetHostConfig(self, hostname): |
| """Returns the complete host config dict for a given hostname.""" |
| if hostname not in self.hosts: |
| raise DeviceMapError('host %s is not defined in devicemap' % hostname) |
| return self.hosts[hostname] |
| |
| def _GetHostDimensions(self, host_config): |
| """Returns a dict of bot dimensions, populated from the host. |
| |
| These dimensions can be overridden at the individual device level. |
| """ |
| return { |
| 'location': host_config.get('location', 'UNSPECIFIED') |
| } |
| |
| def _GetHostedDevices(self, hostname): |
| return self._GetHostConfig(hostname)[self._DEVICE_NODE_NAME] |
| |
| def GetHosts(self): |
| return self.hosts |
| |
| def GetHostIps(self): |
| # Implicitly making host_ip a mandatory field here |
| return [self.hosts[h][self._HOST_IP] for h in self.hosts.keys()] |
| |
| |
| class BotConfig(object): |
| """Represents a single bot config for a Testbed or Device.""" |
| |
| def __init__(self, bot_config_dict, host_dimensions=None): |
| """Initialize bot config object. |
| |
| Args: |
| bot_config_dict: Dict of bot info for a single device from devicemap. |
| host_dimensions: Bot dimensions populated from the host of the device. |
| """ |
| self.type = self._ParseConfigType(bot_config_dict) |
| self._raw_type = list(bot_config_dict.keys())[0] |
| self._data = bot_config_dict[self._raw_type] |
| self._host_dimensions = host_dimensions or {} |
| if self.type == 'Testbed': |
| self.dimensions, self.state = self._ParseTestbedConfig() |
| elif self.type == 'Device': |
| self.dimensions, self.state = self._ParseDeviceConfig() |
| else: |
| raise NotImplementedError('devicemap type "%s" not defined' % self.type) |
| |
| def __repr__(self): |
| return str(self.as_dict) |
| |
| @property |
| def as_dict(self): |
| # Transform dimensions & state to conform to swarming's schema for bots |
| for dimension, value in self.dimensions.items(): |
| if not value: |
| raise DeviceMapError('Dimensions must not be empty: %s' % dimension) |
| # Dimension values must be a list/tuple |
| if not hasattr(value, '__iter__'): |
| self.dimensions[dimension] = [value] |
| return { |
| 'dimensions': self.dimensions, |
| 'state': self.state, |
| } |
| |
| def _ParseConfigType(self, config_dict): |
| if not len(list(config_dict.keys())) == 1: |
| raise DeviceMapError( |
| ('Configuration invalid -- expected single entry for a "Testbed" ' |
| 'or a single device. Got: %s') % list(config_dict.keys())) |
| config_type = list(config_dict.keys())[0] |
| # Any configs that are named all in lowercase are assumed to be a device. |
| # (e.g. lexx, luther, biggie, joplin, etc). Any configs with a leading |
| # uppercase are a specified config type (e.g. Testbed). |
| if config_type.islower(): |
| config_type = 'Device' |
| return config_type |
| |
| def _ParseTestbedConfig(self): |
| """Get the bot config dict() for the given hostname & bot_num.""" |
| dimensions = self._host_dimensions |
| dimensions.update(self._data.get('dimensions')) |
| state = self._data.get('extra_state') |
| return dimensions, state |
| |
| def _ParseDeviceConfig(self): |
| """Get the bot config dict() for the given hostname & bot_num.""" |
| state = {} |
| dimensions = self._host_dimensions |
| dimensions.update(self._data) |
| dimensions['device_type'] = self._raw_type |
| dimensions['group'] = dimensions.get('group') or 'COMMON' |
| dimensions['nic'] = dimensions.get('nic') or 'ethernet' |
| dimensions['use_tunnel'] = dimensions.get('use_tunnel') or 'false' |
| # FDR device between tests unless in 'signed_in' group. |
| dimensions['factory_reset'] = ( |
| dimensions.get('factory_reset') or |
| str('signed_in' not in dimensions['group']).lower()) |
| # Rename 'ip' to 'device_ip' for convenience (keeps config file concise) |
| if 'ip' in dimensions: |
| dimensions['device_ip'] = dimensions['ip'] |
| del dimensions['ip'] |
| if 'extra_state' in dimensions: |
| state = dimensions.get('extra_state', {}) |
| del dimensions['extra_state'] |
| return dimensions, state |
| |
| |
| def _ReadDeviceMap(): |
| """Reads all configs from devicemap.yaml config file as a dict().""" |
| pwd = os.path.dirname(os.path.realpath(__file__)) |
| with open(os.path.join(pwd, 'devicemap.yaml'), 'r') as f: |
| return yaml.safe_load(f) |