Beispiel #1
0
 def _parse_user_data(self, data):
     '''
     Take a string in form version|[value|][value|][value|]...
     parses according to version and populate the respective self var.
     Conductor puts the UUID into the oauth_key field.
     At minimum this function expects to find a | in the string
     this is in effort not to log oauth secrets.
     '''
     LOGGER.debug('Parsing User Data')
     user_data = data.split('|')
     if len(user_data) > 1:
         if user_data[0] == '1':
             # version 1
             # format version|endpoint|oauth_key|oauth_secret
             ud_version, endpoint, \
                 oauth_key, oauth_secret = user_data
             self.ud_version = ud_version
             self.endpoint = endpoint
             self.oauth_key = oauth_key
             self.oauth_secret = oauth_secret
             return {'endpoint': self.endpoint,
                     'oauth_key': self.oauth_key,
                     'oauth_secret': self.oauth_secret, }
         else:
             raise AAError('Invalid User Data Version: %s' % user_data[0])
     else:
         raise AAError('Could not get user data version, parse failed')
Beispiel #2
0
    def unpack_tooling(self):
        '''
        Description:
            Methods used to untar the user provided tarball

            Perform validation of the text message sent from the
            Config Server. Validate, open and write out the contents
            of the user provided tarball.
        '''
        LOGGER.info('Invoked unpack_tooling()')

        # Validate the specified tarfile.
        if not os.path.exists(self.tarball):
            raise AAError('File does not exist: %s ' % self.tarball)
        if not tarfile.is_tarfile(self.tarball):
            raise AAErrorInvalidTar('Not a valid tar file: %s' % self.tarball)

        # Attempt to extract the contents from the specified tarfile.
        # If tarfile access or content is bad report to the user to aid
        # problem resolution.
        try:
            tarf = tarfile.open(self.tarball)
            tarf.extractall(path=self.user_dir)
            tarf.close()
        # Capture and report errors with the tarfile
        except (tarfile.TarError, tarfile.ReadError, \
                tarfile.CompressionError, tarfile.StreamError, \
                tarfile.ExtractError, IOError), (strerror):
            raise AAError(('Failed to access tar file %s. Error: %s') %  \
                (self.tarball, strerror))
Beispiel #3
0
    def read(self):
        #
        # If on RHEV-M the user data will be contained on the
        # floppy device in file deltacloud-user-data.txt.
        # To access it:
        #    modprobe floppy
        #    mount /dev/fd0 /media
        #    read /media/deltacloud-user-data.txt
        #
        # Note:
        # On RHEVm the deltacloud drive had been delivering the user
        # data base64 decoded at one point that changed such that the
        # deltacloud drive leaves the date base64 encoded. This
        # Code segment will handle both base64 encoded and decoded
        # user data.
        #
        # Since ':' is used as a field delimiter in the user data
        # and is not a valid base64 char, if ':' is found assume
        # the data is already base64 decoded.
        #
        #    modprobe floppy
        cmd = ['/sbin/modprobe', 'floppy']
        ret = run_cmd(cmd)
        if ret['subproc'].returncode != 0:
            raise AAError(('Failed command: \n%s \nError: \n%s') % \
                (' '.join(cmd), str(ret['err'])))

        cmd = ['/bin/mkdir', '/media']
        ret = run_cmd(cmd)
        # If /media is already there (1) or any other error (0)
        if (ret['subproc'].returncode != 1) and  \
           (ret['subproc'].returncode != 0):
            raise AAError(('Failed command: \n%s \nError: \n%s') % \
                (' '.join(cmd), str(ret['err'])))

        cmd = ['/bin/mount', '/dev/fd0', '/media']
        ret = run_cmd(cmd)
        # If /media is already mounted (32) or any other error (0)
        if (ret['subproc'].returncode != 32) and  \
           (ret['subproc'].returncode != 0):
            raise AAError(('Failed command: \n%s \nError: \n%s') % \
                (' '.join(cmd), str(ret['err'])))

        # Condfig Server (CS) address:port.
        # This could be done using "with open()" but that's not available
        # in Python 2.4 as used on RHEL5
        try:
            user_data_file = open(DELTA_CLOUD_USER_DATA, 'r')
            user_data = user_data_file.read().strip()
            user_data_file.close()
        except Exception, e:
            raise AAError('Failed accessing RHEVm user data: %s' % e)
Beispiel #4
0
    def get_tooling(self):
        '''
        Description:
            get any optional user supplied tooling which is
            provided as a tarball
        '''
        LOGGER.info('Invoked CSClient.get_tooling()')
        url = self._cs_url(TOOLING_URL)
        headers = {'Accept': 'content-disposition'}

        tarball = ''
        response, body = self._get(url, headers=headers)
        self._validate_http_status(response)

        # Parse the file name burried in the response header
        # at: response['content-disposition']
        # as: 'attachment; tarball="tarball.tgz"'
        if (response.status == 200) or (response.status == 202):
            tarball = response['content-disposition']. \
                lstrip('attachment; filename=').replace('"', '')

            # Create the temporary tarfile
            try:
                self.tmpdir = tempfile.mkdtemp()
                self.tarball = self.tmpdir + '/' + tarball
                temptar = open(self.tarball, 'w')
                temptar.write(body)
                temptar.close()
            except IOError, err:
                raise AAError(('File not found or not a tar file: %s ' + \
                        'Error: %s') % (self.tarball, err))
Beispiel #5
0
    def find_tooling(self, service_name):
        '''
        Description:
            Given a service name return the path to the configuration
            tooling.

            Search for the service start executable in the user
            tooling directory.
                self.tool_dir + '/user/<service name>/start'

            If not found there search for the it in the documented directory
            here built in tooling should be placed.
                self.tool_dir + '/AUDREY_TOOLING/<service name>/start'

            If not found there search for the it in the Red Hat tooling
            directory.
                self.tool_dir + '/REDHAT/<service name>/start'

           If not found there raise an error.

        Returns:
            return 1 - True if top level tooling found, False otherwise.
            return 2 - path to tooling
        '''
        if not service_name:
            raise AAError('Empty service name passed')
        # common join
        service_start = os.path.join(service_name, 'start')
        # returns, check the paths and return the tuple
        tooling_paths = [
            (True, os.path.join(self.user_dir, 'start')),
            (False, os.path.join(self.user_dir, service_start)),
            (False, os.path.join(self.audrey_dir, service_start)),
            (False, os.path.join(self.redhat_dir, service_start)),
        ]

        for path in tooling_paths:
            if os.access(path[1], os.X_OK):
                return path

        # No tooling found. Raise an error.
        raise AAError('No configuration tooling found for service: %s' % \
                                                                  service_name)
Beispiel #6
0
    def read(self):
        try:
            max_attempts = 5
            headers = {'Accept': 'text/plain'}
            while max_attempts:
                response, body = httplib2.Http().request(EC2_USER_DATA_URL,
                                                         headers=headers)
                if response.status == 200:
                    break
                max_attempts -= 1

            if response.status != 200:
                raise AAError('Max attempts to get EC2 user data \
                        exceeded.')

            if '|' not in body:
                body = base64.b64decode(body)
            return self._parse_user_data(body)

        except Exception, err:
            raise AAError('Failed accessing EC2 user data: %s' % err)
Beispiel #7
0
 def _validate_http_status(response):
     '''
     Description:
         Confirm the http status is one of:
         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
     '''
     if isinstance(response, Exception):
         raise response
     if response.status not in [200, 202, 404]:
         raise AAError('Invalid HTTP status code: %s' % response.status)
    def read(self):
        #
        # If on vSphere the user data will be contained on the
        # floppy device in file deltacloud-user-data.txt.
        # To access it:
        #    mount /dev/fd0 /media
        #    read /media/deltacloud-user-data.txt
        #
        # Note:
        # On vSphere the deltacloud drive had been delivering the user
        # data base64 decoded at one point that changed such that the
        # deltacloud drive leaves the date base64 encoded. This
        # Code segment will handle both base64 encoded and decoded
        # user data.
        cmd = ['/bin/mkdir', '/media']
        ret = run_cmd(cmd)
        # If /media is already there (1) or any other error (0)
        if (ret['subproc'].returncode != 1) and  \
           (ret['subproc'].returncode != 0):
            raise AAError(('Failed command: \n%s \nError: \n%s') % \
                (' '.join(cmd), str(ret['err'])))

        cmd = ['/bin/mount', '/dev/cdrom', '/media']
        ret = run_cmd(cmd)
        # If /media is already mounted (32) or any other error (0)
        if (ret['subproc'].returncode != 32) and  \
           (ret['subproc'].returncode != 0):
            raise AAError(('Failed command: \n%s \nError: \n%s') % \
                (' '.join(cmd), str(ret['err'])))

        try:
            user_data_file = open(DELTA_CLOUD_USER_DATA, 'r')
            user_data = user_data_file.read().strip()
            user_data_file.close()
        except Exception, e:
            raise AAError('Failed accessing vSphere user data file. %s' % e)
Beispiel #9
0
    def __init__(self, tarball, tool_dir=TOOLING_DIR):
        '''
        Description:
            Set initial state so it can be tracked.
        '''
        self.tool_dir = tool_dir
        self.user_dir = os.path.join(tool_dir, 'user')
        self.audrey_dir = os.path.join(tool_dir, 'AUDREY_TOOLING')
        self.redhat_dir = os.path.join(tool_dir, 'REDHAT')
        self.tarball = tarball

        # Create the extraction destination
        if not os.path.exists(self.user_dir):
            try:
                os.makedirs(self.user_dir)
            except OSError, err:
                raise AAError(('Failed to create directory %s. ' + \
                               'Error: %s') % (self.user_dir, err))
Beispiel #10
0
def discover():
    '''
    Description:
        User Data is passed to the launching instance which
        provides the Config Server contact information.

        Cloud providers expose the user data differently.
        It is necessary to determine which cloud provider
        the current instance is running on to determine
        how to access the user data. Images built with
        image factory will contain a CLOUD_INFO_FILE which
        contains a string identifying the cloud provider.

        Images not built with Imagefactory will try to
        determine what the cloud provider is based on system
        information.
    '''

    LOGGER.debug('Invoked discover')

    if os.path.exists(CLOUD_INFO_FILE):
        cloud_info = open(CLOUD_INFO_FILE)
        cloud_type = cloud_info.read().strip().upper()
        cloud_info.close()

    else:
        cloud_type = _get_cloud_type()

    LOGGER.debug('cloud_type: ' + str(cloud_type))

    if 'EC2' in cloud_type:
        import audrey.user_data_ec2
        return audrey.user_data_ec2.UserData()
    elif 'RHEV' in cloud_type:
        import audrey.user_data_rhev
        return audrey.user_data_rhev.UserData()
    elif 'VSPHERE' in cloud_type:
        import audrey.user_data_vsphere
        return audrey.user_data_vsphere.UserData()
    else:
        raise AAError('Cloud type "%s" is invalid.' % cloud_type)
Beispiel #11
0
 def test_connection(self, max_retry=5):
     '''
     call configserver's version endpoint and pass my compat api versions
     then parse the response to retireve the api version
     we should operate on
     '''
     # try and wait for connectivity if it's not there
     url = self._cs_url(VERSION_URL)
     response, body = self._get(url, {'Accept': 'text/xml'})
     while isinstance(response, Exception):
         if max_retry:
             max_retry -= 1
             LOGGER.info('Failed attempt to contact config server')
             sleep(SLEEP_SECS)
         else:
             raise AAError("Cannot establish connection to %s" % url)
         response, body = self._get(url)
     try:
         api_v = ElementTree.fromstring(body)
         api_v = api_v.find('api-version')
         self.api_version = int(api_v.text)
         LOGGER.info('Negotiated API V%s' % self.api_version)
     except Exception, err:
         raise AAErrorApiNegotiation(err)
Beispiel #12
0
def get_system_info(facts=None):
    '''
    Description:
        Get the system info to be used for generating this instances
        provides back to the Config Server.

        Currently utilizes Puppet's facter via a Python subprocess call.

    Input:
        optional list of fact names

    Returns:
        A dictionary of system info name/value pairs.

    '''
    cmd = ['/usr/bin/facter']
    if facts:
        fact = facts[0]
        cmd.extend(facts)
    ret = run_cmd(cmd)
    if ret['subproc'].returncode != 0:
        raise AAError(('Failed command: \n%s \nError: \n%s') % \
            (' '.join(cmd), str(ret['err'])))

    facts = {}
    facts = ret['out'].split('\n')[:-1]
    if not facts:
        return {}
    if len(facts) == 1:
        return {fact: facts[0]}
    facts = [x.split(' => ') for x in facts]
    facts = dict(facts)
    for key in facts.keys():
        if not facts[key]:
            del facts[key]
    return facts
Beispiel #13
0
 def read(self):
     '''
     Dummy function, indended to be overridden
     should return (endpoint, oauth_jey, oauth_secret)
     '''
     raise AAError("%s read() not overridden. Execution Aborted" % self)
Beispiel #14
0
 def validate_message(src):
     '''
     Perform validation of the text message sent from the Config Server.
     '''
     if not src.startswith('|') or not src.endswith('|'):
         raise AAError(('Invalid start and end characters: %s') % (src))
Beispiel #15
0
    def run(self):
        '''
        Main loop called by main() in /usr/bin/audrey
        '''
        provides_len = 0
        services_len = 0
        retry_ct = 0

        status, provides_str = self.client.get_provides()
        if status == 200:
            services, provides = Provides().parse_cs_str(
                provides_str, self.tooling)
        else:
            raise AAError('HTTP %s from provides & services list' % status)

        # process services and provides, removing them from the ques
        # as they have been processed.
        while services or provides:

            # Put the requested provides with values to the Config Server
            provides_str = provides.generate_cs_str()
            LOGGER.debug('Put Provides: %s' % provides_str)
            status = self.client.put_provides(provides_str)[0]
            # report non 200 status
            if status != 200:
                raise AAErrorPutProvides('Put provides returned %s' % status)
            # clean regardless of status, otherwise we'll get in
            # an infinite loop.
            provides.clean()

            # check for required configs per service
            for service in services.keys():
                svc = services[service]
                # Get the Required Configs from Config Server for the service
                status, configs = self.client.get_configs(svc.name)
                svc.parse_configs(configs)

                # Configure the system with the provided Required Configs
                if status == 202:
                    # couldn't be given all the configs yet.
                    continue
                else:
                    if status == 200:
                        # got all the configs, so invoke and report status
                        status = svc.invoke_tooling()
                    LOGGER.info('Service %s returns %s' % (service, status))
                    # report service status
                    status = self.client.put_provides(
                        svc.generate_cs_str(status))[0]
                    # report non 200 status on service status put
                    if status != 200:
                        raise AAErrorPutProvides('Put service status %s' %
                                                 status)
                    # the service has been processed
                    del services[service]

            if services_len == len(services) and provides_len == len(provides):
                if retry_ct == MAX_RETRY:
                    LOGGER.error("Max retry count exceeded. Exiting.")
                    exit(1)
                retry_ct += 1
            else:
                services_len = len(services)
                provides_len = len(provides)
                retry_ct = 0

            sleep(SLEEP_SECS)
Beispiel #16
0
    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
Beispiel #17
0
    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