blob: 58757e4da2683f6abd2b5d278272c02a50323e0c [file] [log] [blame] [edit]
"""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)