def setUp(self): ''' If the cloud info file is not present assume running in a UNITTEST environment. This will allow for exercising some of the code without having to be running in a cloud VM. ''' # Create the client Object self.cs_client = CSClient(**DUMMY_CS_CONFIG) self.cs_client.http = HttpUnitTest()
def __init__(self, conf): ''' conf: argparse dict ''' tool_dir = {} if 'pwd' in conf and conf['pwd']: tool_dir = {'tool_dir': PWD_TOOLING} # Create the Client Object self.client = CSClient(**conf) self.client.test_connection() # Get any optional tooling from the Config Server tooling_status, tarball = self.client.get_tooling() if tooling_status != 200: LOGGER.error('Get Tooling returned: %s' % tooling_status) raise AAErrorGetTooling('Get Tooling returned: %s' % tooling_status) self.tooling = Tooling(tarball, **tool_dir)
def main(): ''' Description: This script will be used on EC2 for configuring the running instance based on Cloud Engine configuration supplied at launch time in the user data. Config Server Status: 200 HTTP OK - Success and no more data of this type 202 HTTP Accepted - Success and more data of this type 404 HTTP Not Found - This may be temporary so try again ''' # parse the args and setup logging conf = parse_args() log_file = {} if 'pwd' in conf and conf.pwd: log_file = {'logfile_name': 'audrey.log'} logger = setup_logging(level=conf.log_level, **log_file) if not conf.endpoint: # discover the cloud I'm on # update the conf with the user data #conf = dict(vars(conf).items() + user_data.discover().read().items()) vars(conf).update(user_data.discover().read().items()) # ensure the conf is a dictionary, not a namespace if hasattr(conf, '__dict__'): conf = vars(conf) logger.info('Invoked audrey main') # Create the Client Object and test connectivity # to CS by negotiating the api version client = CSClient(**conf) client.test_connection() # Get the agent object agent = AudreyFactory(client.api_version).agent # run the agent agent(conf).run()
def parse_cs_str(self, src, tooling): """ Description: Parse the provides parameters text message sent from the Config Server. Input: The provides parameters string obtained from the Config Server. The delimiters will be an | and an & To ensure all the data was received the entire string will be terminated with an "|". This will be a continuous text string (no CR or New Line). Format: |name1&name2...&nameN| e.g.: |ipaddress&virtual| Returns: - Provides populated with param names as the keys and None as the value """ CSClient.validate_message(src) # split and prune the payload src = src[1:-1].split("|") if len(src) >= 1: for provides in src[0].split("&"): if provides: self[provides] = None return self
def parse_cs_str(self, src, tooling): ''' Description: Parse the provides parameters text message sent from the Config Server. Input: The provides parameters string obtained from the Config Server. The delimiters will be an | and an & To ensure all the data was received the entire string will be terminated with an "|". This will be a continuous text string (no CR or New Line). Format: |name1&name2...&nameN| e.g.: |ipaddress&virtual| Returns: - Provides populated with param names as the keys and None as the value ''' CSClient.validate_message(src) # split and prune the payload src = src[1:-1].split('|') if len(src) >= 1: for provides in src[0].split('&'): if provides: self[provides] = None return self
class TestAudreyCSClient(unittest.TestCase): ''' Class for exercising the gets and put to and from the CS ''' def setUp(self): ''' If the cloud info file is not present assume running in a UNITTEST environment. This will allow for exercising some of the code without having to be running in a cloud VM. ''' # Create the client Object self.cs_client = CSClient(**DUMMY_CS_CONFIG) self.cs_client.http = HttpUnitTest() def test_success_get_configs(self): ''' Success case: - Exercise get_configs() ''' self.cs_client.get_configs() def test_success_get_tooling(self): ''' Success case: - Exercise get_tooling() ''' self.cs_client.get_tooling() def test_success_get_provides(self): ''' Success case: - Exercise get_provides() ''' self.cs_client.get_provides() def test_success_get_confs_n_provides(self): ''' Success case: - Exercise get_configs() and get_provides() ''' self.cs_client.get_configs() self.cs_client.get_provides() def test_success_put_provides(self): ''' Success case: - Exercise put_provides_values() ''' self.cs_client.put_provides('') def test_error_http_status(self): ''' Success case: - Get a 401 ''' self.assertRaises(AAError, self.cs_client._validate_http_status, HttpUnitTest.HttpUnitTestResponse(401)) def test_catch_get_exception(self): ''' Success case: - get fails but audrey recovers ''' self.cs_client._get('http://hostname/raiseException') def test_catch_put_exception(self): ''' Success case: - put fails but audrey recovers ''' self.cs_client._put('http://hostname/raiseException') def test_failed_version(self): audrey.csclient.VERSION_URL = '/badversion' self.assertRaises(AAErrorApiNegotiation, self.cs_client.test_connection)
def parse_require_config(src, tooling): ''' Description: Parse the required config text message sent from the Config Server. Input: The required config string obtained from the Config Server, delimited by an | and an & Two tags will mark the sections of the data, '|service|' and '|parameters|' To ensure all the data was received the entire string will be terminated with an "|". The string "|service|" will precede a service names. The string "|parameters|" will precede the parameters for the preceeding service, in the form: names&<b64 encoded values>. This will be a continuous text string (no CR or New Line). Format (repeating for each service): |service|<s1>|parameters|name1&<b64val>|name2&<b64val>|nameN&<b64v>| e.g.: |service|ssh::server|parameters|ssh_port&<b64('22')> |service|apache2::common|apache_port&<b64('8081')>| Returns: - A list of ServiceParams objects. ''' services = [] new = None CSClient.validate_message(src) # Message specific validation if src == '||': # special case indicating no required config needed. return [] # split on pipe and chop of first and last, they will always be empty src = src.split('|')[1:-1] # get the indexes of the service identifiers srvs = deque([i for i, x in enumerate(src) if x == 'service']) srvs.append(len(src)) if srvs[0] != 0: raise AAError(('|service| is not the first tag found. %s') % (src)) while len(srvs) > 1: # rebuild a single service's cs string svc_str = "|%s|" % "|".join(src[srvs[0]:srvs[1]]) name = src[srvs[0] + 1] if name in ['service', 'parameters'] or '&' in name: raise AAError('invalid service name: %s' % name) # instanciate the service with it's name svc = Service(name, tooling) svc.parse_configs(svc_str) services.append(svc) srvs.popleft() return services
class AgentV1(object): ''' contains the main logic for processing This object is compatible with API Version 1 ''' def __init__(self, conf): ''' conf: argparse dict ''' tool_dir = {} if 'pwd' in conf and conf['pwd']: tool_dir = {'tool_dir': PWD_TOOLING} # Create the Client Object self.client = CSClient(**conf) self.client.test_connection() # Get any optional tooling from the Config Server tooling_status, tarball = self.client.get_tooling() if tooling_status != 200: LOGGER.error('Get Tooling returned: %s' % tooling_status) raise AAErrorGetTooling('Get Tooling returned: %s' % tooling_status) self.tooling = Tooling(tarball, **tool_dir) def run(self): ''' Main agent loop, called by main() in /usr/bin/audrey ''' # 0 means don't run again # -1 is non zero so initial runs will happen config_status = -1 provides_status = -1 max_retry = MAX_RETRY loop_count = 60 services = [] # Process the Requires and Provides parameters until the HTTP status # from the get_configs and the get_params both return 200 while config_status or provides_status: LOGGER.debug('Config Parameter status: ' + str(config_status)) LOGGER.debug('Return Parameter status: ' + str(provides_status)) # Get the Required Configs from the Config Server if config_status: config_status, configs = self.client.get_configs() # Configure the system with the provided Required Configs if config_status == 200: services = Service.parse_require_config(configs, self.tooling) self.tooling.invoke_tooling(services) # don't do any more config status work # now that the tooling has run config_status = 0 else: LOGGER.info( 'No configuration parameters provided. status: ' + \ str(config_status)) # Get the requested provides from the Config Server if provides_status: get_status = self.client.get_provides()[0] # Gather the values from the system for the requested provides if get_status == 200: params_values = Provides().generate_cs_str() else: params_values = '|&|' # Put the requested provides with values to the Config Server provides_status = self.client.put_provides(params_values)[0] if provides_status == 200: # don't operate on params anymore, all have been provided. provides_status = 0 # Retry a number of times if 404 HTTP Not Found is returned. if config_status == 404 or provides_status == 404: LOGGER.error('404 from Config Server.') LOGGER.error('Required Config status: %s' % config_status) LOGGER.info('Return Parameter status: %s' % provides_status) max_retry -= 1 if max_retry < 0: raise AAError('Too many 404 Config Server responses.') if loop_count: loop_count-=1 sleep(SLEEP_SECS) else: break
def parse_require_config(src, tooling): ''' Description: Parse the required config text message sent from the Config Server. Input: The required config string obtained from the Config Server, delimited by an | and an & Two tags will mark the sections of the data, '|service|' and '|parameters|' To ensure all the data was received the entire string will be terminated with an "|". The string "|service|" will precede a service names. The string "|parameters|" will precede the parameters for the preceeding service, in the form: names&<b64 encoded values>. This will be a continuous text string (no CR or New Line). Format (repeating for each service): |service|<s1>|parameters|name1&<b64val>|name2&<b64val>|nameN&<b64v>| e.g.: |service|ssh::server|parameters|ssh_port&<b64('22')> |service|apache2::common|apache_port&<b64('8081')>| Returns: - A list of ServiceParams objects. ''' services = [] new = None CSClient.validate_message(src) # Message specific validation if src == '||': # special case indicating no required config needed. return [] # split on pipe and chop of first and last, they will always be empty src = src.split('|')[1:-1] # get the indexes of the service identifiers srvs = deque([i for i,x in enumerate(src) if x == 'service']) srvs.append(len(src)) if srvs[0] != 0: raise AAError(('|service| is not the first tag found. %s') % (src)) while len(srvs) > 1: # rebuild a single service's cs string svc_str = "|%s|" % "|".join(src[srvs[0]:srvs[1]]) name = src[srvs[0]+1] if name in ['service', 'parameters'] or '&' in name: raise AAError('invalid service name: %s' % name) # instanciate the service with it's name svc = Service(name, tooling) svc.parse_configs(svc_str) services.append(svc) srvs.popleft() return services
class AgentV1(object): ''' contains the main logic for processing This object is compatible with API Version 1 ''' def __init__(self, conf): ''' conf: argparse dict ''' tool_dir = {} if 'pwd' in conf and conf['pwd']: tool_dir = {'tool_dir': PWD_TOOLING} # Create the Client Object self.client = CSClient(**conf) self.client.test_connection() # Get any optional tooling from the Config Server tooling_status, tarball = self.client.get_tooling() if tooling_status != 200: LOGGER.error('Get Tooling returned: %s' % tooling_status) raise AAErrorGetTooling('Get Tooling returned: %s' % tooling_status) self.tooling = Tooling(tarball, **tool_dir) def run(self): ''' Main agent loop, called by main() in /usr/bin/audrey ''' # 0 means don't run again # -1 is non zero so initial runs will happen config_status = -1 provides_status = -1 max_retry = MAX_RETRY loop_count = 60 services = [] # Process the Requires and Provides parameters until the HTTP status # from the get_configs and the get_params both return 200 while config_status or provides_status: LOGGER.debug('Config Parameter status: ' + str(config_status)) LOGGER.debug('Return Parameter status: ' + str(provides_status)) # Get the Required Configs from the Config Server if config_status: config_status, configs = self.client.get_configs() # Configure the system with the provided Required Configs if config_status == 200: services = Service.parse_require_config( configs, self.tooling) self.tooling.invoke_tooling(services) # don't do any more config status work # now that the tooling has run config_status = 0 else: LOGGER.info( 'No configuration parameters provided. status: ' + \ str(config_status)) # Get the requested provides from the Config Server if provides_status: get_status = self.client.get_provides()[0] # Gather the values from the system for the requested provides if get_status == 200: params_values = Provides().generate_cs_str() else: params_values = '|&|' # Put the requested provides with values to the Config Server provides_status = self.client.put_provides(params_values)[0] if provides_status == 200: # don't operate on params anymore, all have been provided. provides_status = 0 # Retry a number of times if 404 HTTP Not Found is returned. if config_status == 404 or provides_status == 404: LOGGER.error('404 from Config Server.') LOGGER.error('Required Config status: %s' % config_status) LOGGER.info('Return Parameter status: %s' % provides_status) max_retry -= 1 if max_retry < 0: raise AAError('Too many 404 Config Server responses.') if loop_count: loop_count -= 1 sleep(SLEEP_SECS) else: break