def _getVIPNameAndAddress(self, interfaceId):
   """
     Return a tuple that is the Name tag value and the private IP address of
     the AWS::EC2::NetworkInterface with the given interfaceId
     
     The given interfaceId is an physical resource ID for an AWS::EC2::NetworkInterface resource.
     
     This method is a helper for _getVIPs().
   """
   
   interface = self.ec2.NetworkInterface(interfaceId)
   privateIP = interface.private_ip_address
   tags = interface.tag_set
   
   name = ''
   for tag in tags:
     key = tag.get('Key')
     if (key == 'Name'):
       name = tag.get('Value')
       break
     #endIf
   #endFor
   
   if (not name):
     raise ICPInstallationException("NetworkInterface: %s is expected to have a Name tag." % interfaceId)
   #endIf
  
   return  (name, privateIP)
 def _configureMgmtServices(self,configParameters,optionalServices,excludedServices):
   """
     Walk through the optionalServices list and set all that are not in the excludedServivces
     list to "enabled" in the configParameters dictionary.
     
     The incoming excludedServices parameter is assumed to have been "regularized" by the 
     _transformExcludeMgmtServices() method prior to the invocation of this method.
     
     The names in the optionalServices list are mapped to names that match the corresponding 
     parameter names used in the configParameters dictionary.
            
     NOTE: This method is used to support the inclusion/exclusion of management services in the 
     config.yaml file for ICP v3.1.0 or later.
   """
   methodName = "_configureMgmtServices"
   
   for service in optionalServices:
     serviceParameterName = ServiceNameParameterMap.get(service)
     if (not serviceParameterName):
       raise ICPInstallationException("Missing service name parameter in ServiceNameParameterMap for service: %s" % service)
     #endIf
       
     if (excludedServices and service in excludedServices):
       # Set the service parameter to be disabled in the configParmaeters
       configParameters[serviceParameterName] = 'disabled'
     else:
       configParameters[serviceParameterName] = 'enabled'
     #endIf
     if (TR.isLoggable(Level.FINEST)):
       TR.finest(methodName,"Management Service: %s: %s" % (serviceParameterName,configParameters[serviceParameterName]))
    def loadICPImages(self):
        """
      Load the IBM Cloud Pak images from the installation tar archive.
      
      Loading the ICP installation images on each node prior to kicking off the 
      inception install is an expediency that speeds up the installation process 
      dramatically.
      
      The AWS CloudFormation template downlaods the ICP installation tar ball from
      an S3 bucket to /tmp/icp-install-archive.tgz of each cluster node.  It turns 
      out that download is very fast: typically 3 to 4 minutes.
    """
        methodName = "loadICPImages"

        TR.info(methodName, "STARTED docker load of ICP installation images.")

        retcode = call(
            "tar -zxvf /tmp/icp-install-archive.tgz -O | docker load | tee /root/logs/load-icp-images.log",
            shell=True)
        if (retcode != 0):
            raise ICPInstallationException(
                "Error calling: 'tar -zxvf /tmp/icp-install-archive.tgz -O | docker load' - Return code: %s"
                % retcode)
        #endIf

        TR.info(methodName,
                "COMPLETED Docker load of ICP installation images.")
Example #4
0
 def getBootNodePublicKey(self):
   """
     Return the authorized key entry for the ~/.ssh/authorized_keys file.
     The returned string is intended to include the RSA public key as well as the root user
     and IP address of the boot node.  The returned string can be added directly to the
     authorized_keys file.
     
     NOTE: It is possible that the a given cluster node may be checking for the authorized
     key from the boot node, before the boot node has published it in its parameter.  When
     that happens a ParameterNotFound exception is raised by ssm.get_parameter().  That 
     exception is reported in the log, but ignored.
   """
   methodName = "getBootNodePublicKey"
   
   authorizedKeyEntry = ""
   parameterKey = "/%s/boot-public-key" % self.stackName
   tryCount = 1
   response = None
   
   while not response and tryCount <= 100:
     time.sleep(GetParameterSleepTime)
     TR.info(methodName,"Try: %d for getting parameter: %s" % (tryCount,parameterKey))
     try: 
       response = self.ssm.get_parameter(Name=parameterKey)
     except ClientError as e:
       etext = "%s" % e
       if (etext.find('ParameterNotFound') >= 0):
         if (TR.isLoggable(Level.FINEST)):
           TR.finest(methodName,"Ignoring ParameterNotFound ClientError on ssm.get_parameter() invocation")
         #endIf
       else:
         raise ICPInstallationException("Unexpected ClientError on ssm.get_parameter() invocation: %s" % etext)
       #endIf
     #endTry
     tryCount += 1
   #endWhile
   
   if (response and TR.isLoggable(Level.FINEST)):
     TR.finest(methodName,"Response: %s" % response)
   #endIf
   
   if (not response):
     TR.warning(methodName, "Failed to get a response for get_parameter: %s" % parameterKey)
   else:
     parameter = response.get('Parameter')
     if (not parameter):
       raise Exception("get_parameter response returned an empty Parameter.")
     #endIf
     authorizedKeyEntry = parameter.get('Value')
   #endIf
   
   return authorizedKeyEntry
    def loadInstallMap(self, version=None, region=None):
        """
      Return a dictionary that holds all the installation image information needed to 
      retrieve the installation images from S3. 
      
      Which install images to use is driven by the ICP version.
      Which S3 bucket to use is driven by the AWS region of the deployment.
      
      The source of the information is icp-install-artifact-map.yaml packaged with the
      boostrap script package.  The yaml file holds the specifics regarding which bucket
      to use and the S3 path for the ICP and Docker images as well as the Docker image
      name and the inception commands to use for the installation.          
    """
        methodName = "loadInstallMap"

        if (not version):
            raise MissingArgumentException("The ICP version must be provided.")
        #endIf

        if (not region):
            raise MissingArgumentException("The AWS region must be provided.")
        #endIf

        installDocPath = os.path.join(self.home, "maps",
                                      "icp-install-artifact-map.yaml")

        with open(installDocPath, 'r') as installDocFile:
            installDoc = yaml.load(installDocFile)
        #endWith

        if (TR.isLoggable(Level.FINEST)):
            TR.finest(methodName, "Install doc: %s" % installDoc)
        #endIf

        installMap = installDoc.get(version)
        if (not installMap):
            raise ICPInstallationException(
                "No ICP or Docker installation images defined for ICP version: %s"
                % version)
        #endIf

        # The version is needed to get to the proper folder in the region bucket.
        installMap['version'] = version
        installMap['s3bucket'] = self.ICPArchiveBucketName

        return installMap
 def createConfigFile(self, configFilePath, icpVersion):
   """
     Select the proper method to create the configuration file based on the 
     ICP version.  The differences in the format and content of the config.yaml
     from one version of ICP to the next are sufficient to warrant a specialized
     method for the creation of the configuration file.  This may settle out
     as the product matures.
     
   """
   
   if (icpVersion.startswith('2.1.')):
     self.createConfigFile_21(configFilePath)
   elif (icpVersion.startswith('3.1.')):
     self.createConfigFile_31(configFilePath)
   else:
     raise ICPInstallationException("Unexpected version of ICP: %s" % icpVersion)
   #endIf
   
   if (self.MasterNodeCount > 1):
     self.configureEtcHosts()
 def _transformExcludedMgmtServices(self,excludedServices):
   """
     Return a list of strings that are the names of the services to be excluded.
     
     The incoming excludedServices parameter may be a list of strings or the string
     representation of a list using commas to delimit the items in the list.
     (The value of ExcludedMgmtServices in the AWS CF template is a CommaDelimitedList 
     which is just such a string.)
     
     The items in the incoming list are converted to all lowercase characters and trimmed.
     
     If the incoming value in excludedServices is the empty string, then an empty list
     is returned.
     
     NOTE: This method is used to support the exclusion of management services in the 
     ICP v2.1 config.yaml file.  It is not used for excluding management services in 
     versions of ICP 3.1.0 and later.
   """
   result = []
   if (excludedServices):
     if (type(excludedServices) != type([])):
       # assume excludedServices is a string
       excludedServices = [x.strip() for x in excludedServices.split(',')]
     #endIf
     
     excludedServices = [x.lower() for x in excludedServices]
     
     for x in excludedServices:
       if (x not in OptionalManagementServices_21):
         raise ICPInstallationException("Service: %s is not an optional management service.  It must be one of: %s" % (x,OptionalManagementServices_21))
       #endIf
     #endFor
     
     result = excludedServices
   #endIf
   return result
Example #8
0
  def getSSMParameterValue(self,parameterKey,expectedValue=None):
    """
      Return the value from the given SSM parameter key.
      
      If an expectedValue is provided, then the wait loop for the SSM get_parameter()
      will continue until the expected value is seen or the try count is exceeded.
      
      NOTE: It is possible that the parameter is not  present in the SSM parameter
      cache when this method is invoked.   When that happens a ParameterNotFound 
      exception is raised by ssm.get_parameter().  Depending on the trace level,
      that  exception is reported in the log, but ignored.
    """
    methodName = "getSSMParameterValue"
    
    parameterValue = None

    tryCount = 1
    gotit = False
    while (not gotit and tryCount <= GetParameterMaxTryCount):
      if (expectedValue == None):
        TR.info(methodName,"Try: %d for getting parameter: %s" % (tryCount,parameterKey))
      else:
        TR.info(methodName,"Try: %d for getting parameter: %s with expected value: %s" % (tryCount,parameterKey,expectedValue))
      #endIf
      try: 
        response = self.ssm.get_parameter(Name=parameterKey)
        
        if (not response):
          if (TR.isLoggable(Level.FINEST)):
            TR.finest(methodName, "Failed to get a response for SSM get_parameter(): %s" % parameterKey)
          #endIf
        else:
          if (TR.isLoggable(Level.FINEST)):
            TR.finest(methodName,"Response: %s" % response)
          #endIf
        
          parameter = response.get('Parameter')
          if (not parameter):
            raise Exception("SSM get_parameter() response returned an empty Parameter.")
          #endIf
          
          parameterValue = parameter.get('Value')
          if (expectedValue == None):
            gotit = True
            break
          else:
            if (parameterValue == expectedValue):
              gotit = True
              break
            else:
              if (TR.isLoggable(Level.FINER)):
                TR.finer(methodName,"For key: %s ignoring value: %s waiting on value: %s" % (parameterKey,parameterValue,expectedValue))
              #endIf
            #endIf
          #endIf
        #endIf
      except ClientError as e:
        etext = "%s" % e
        if (etext.find('ParameterNotFound') >= 0):
          if (TR.isLoggable(Level.FINEST)):
            TR.finest(methodName,"Ignoring ParameterNotFound ClientError on ssm.get_parameter() invocation")
          #endIf
        else:
          raise ICPInstallationException("Unexpected ClientError on ssm.get_parameter() invocation: %s" % etext)
        #endIf
      #endTry
      time.sleep(GetParameterSleepTime)
      tryCount += 1
    #endWhile
    
    if (not gotit):
      if (expectedValue == None):
        raise ICPInstallationException("Failed to get parameter: %s " % parameterKey)
      else:
        raise ICPInstallationException("Failed to get parameter: %s with expected value: %s" % (parameterKey,expectedValue))
      #endIf
    #endIf
      
    return parameterValue
  def _init(self, stackIds=None, configTemplatePath=None, **restArgs):
    """
      Helper for the __init__() constructor.  
      
      All the heavy lifting for initialization of the class occurs in this method.
    """
    methodName = '_init'
    global StackParameters, StackParameterNames
    
    
    if (not stackIds):
      raise MissingArgumentException("The CloudFormation stack resource IDs must be provided.")
    #endIf
    
    self.rootStackId = stackIds[0]
    
    if (not configTemplatePath):
      raise MissingArgumentException("The path to the config.yaml template file must be provided.")
    #endIf
    
    self.configTemplatePath = configTemplatePath

    self.etcHostsPlaybookPath = restArgs.get('etcHostsPlaybookPath')
    
    self.SensitiveParameters = restArgs.get('sensitiveParameters')
    
    StackParameters = restArgs.get('stackParameters')
    if (not StackParameters):
      raise MissingArgumentException("The stack parameters must be provided.")
    #endIf
    
    StackParameterNames = StackParameters.keys()
    
    configParms = self.getConfigParameters(StackParameters)
    if (TR.isLoggable(Level.FINEST)):
      cleaned = Scrubber.dreplace(configParms, self.SensitiveParameters)
      TR.finest(methodName,"Scrubbed parameters defined in the stack:\n\t%s" % cleaned)
    #endIf
    
    self.configParameters = self.fillInDefaultValues(**configParms)

    self.masterELBAddress = self.getLoadBalancerIPAddress(stackIds,elbName="MasterNodeLoadBalancer")
    if (not self.masterELBAddress):
      raise ICPInstallationException("An ELB with a Name tag of MasterNodeLoadBalancer was not found.")
    #endIf

    # This next block supports different ways to set up the WhichClusterLBAddress.
    # It is a debugging ploy. I got tired of changing the script to try out different options.
    if (self.WhichClusterLBAddress == 'UseMasterELBAddress'):
      # NOTE: ICP 2.1.0.3 can't handle a DNS name in the cluster_lb_address config.yaml attribute.
      masterELB = self.masterELBAddress 
    elif (self.WhichClusterLBAddress == 'UseMasterELBName'):
      masterELB = self.getLoadBalancerDNSName(stackIds,elbName="MasterNodeLoadBalancer")
    elif (self.WhichClusterLBAddress == 'UseClusterName'):
      # In the root CloudFormation template, an alias entry is created in the Route53 DNS 
      # that maps the master ELB public DNS name to the cluster CN, i.e., the ClusterName.VPCDomain.
      # Setting the cluster_lb_address to the cluster_CA_domain avoids OAuth issues in mgmt console.
      masterELB = self.ClusterDNSName
    else:
      masterELB = self.ClusterDNSName
    #endIf
    
    self.configParameters['ClusterLBAddress'] = masterELB

    self.proxyELBAddress = self.getLoadBalancerIPAddress(stackIds,elbName="ProxyNodeLoadBalancer")
    if (not self.proxyELBAddress):
      raise ICPInstallationException("An ELB with a Name tag of ProxyNodeLoadBalancer was not found.")
    #endIf
    
    if (self.WhichProxyLBAddress == 'UseProxyELBAddress'):   
      # NOTE: ICP 2.1.0.3 can't handle a DNS name in the proxy_lb_address config.yaml attribute.
      proxyELB = self.proxyELBAddress
    elif (self.WhichProxyLBAddress == 'UseProxyELBName'):
      proxyELB = self.getLoadBalancerDNSName(stackIds,elbName="ProxyNodeLoadBalancer")  
    elif (self.WhichProxyLBAddress == 'UsePrimaryAppDomain'):
      # In the root CloudFormation template, an alias entry is created in the Route53 DNS 
      # that maps the proxy ELB public DNS name to the primary application domain.
      # The primary app domain is the first entry in the list of ApplicationDomains passed 
      # into the root stack.
      proxyELB = self.getPrimaryAppDomain()
    else:
      proxyELB = self.getPrimaryAppDomain()
    #endIf
      
    self.configParameters['ProxyLBAddress'] = proxyELB
    
    self.configParameters['ClusterCADomain'] = self.ClusterDNSName
    
    # VIPs are not supposed to be needed when load balancers are used.
    # WARNING: For an AWS deployment, VIPs have never worked. 
    # Using an EC2::NetworkInterface to get an extra IP doesn't work. 
    #self.vips = self._getVIPs(self.rootStackId)
    #self.configParameters['ClusterVIP'] = self.getVIPAddress("MasterVIP")
    #self.configParameters['ProxyVIP'] = self.getVIPAddress("ProxyVIP")
    
    self.configParameterNames = self.configParameters.keys()    

    if (TR.isLoggable(Level.FINEST)):
      cleaned = Scrubber.dreplace(self.configParameters,self.SensitiveParameters)
      TR.finest(methodName,"All configuration parameters, including defaults:\n\t%s" % cleaned)
    def addNodeToCluster(self):

        gotit = False
        tryCount = 1
        methodName = "addNodeToCluster"
        randomTime = random.randrange(60, 600)
        TR.info(methodName, "Sleeping for %s" % randomTime)
        time.sleep(randomTime)
        while (not gotit):
            parameterKey = "/%s/configureNodeStatus" % self.stackName
            TR.info(
                methodName,
                "Try: %d for getting parameter: %s" % (tryCount, parameterKey))
            try:
                response = self.getSSMParameterValue(parameterKey, count=3)
                TR.info(methodName, "Response returned = %s" % response)
                if (response == "True"):
                    time.sleep(GetParameterSleepTime * 5)
                elif (response == "False"):
                    gotit = True
            except ICPInstallationException as e:
                etext = "%s" % e
                if (etext.find('Failed to get parameter') >= 0):
                    if (TR.isLoggable(Level.FINEST)):
                        TR.finest(
                            methodName,
                            "Ignoring ParameterNotFound ClientError on ssm.get_parameter() invocation"
                        )
                    gotit = True
                    #endIf
                else:
                    raise ICPInstallationException(
                        "Unexpected ClientError on ssm.get_parameter() invocation: %s"
                        % etext)
                    #endIf
                #endTry
                tryCount += 1
        #endWhile
        if (gotit):
            ipValue = self.fqdn.split("-", 1)[1].split(".")
            workerNodeIP = re.sub("-", ".", ipValue[0])

            os.chdir(self.home + "/docker")
            os.chmod('icp-install-docker.bin', stat.S_IEXEC)
            call(
                './icp-install-docker.bin --install | tee /root/logs/install-docker.log',
                shell=True)
            os.chdir(self.home)
            # call ssm send command to execute docker run
            TR.info(methodName, "Execute docker Run script in bootnode")
            response = self.ssm.send_command(
                Targets=[{
                    "Key": "instanceids",
                    "Values": [self.bootInstanceId]
                }],
                DocumentName="AWS-RunShellScript",
                Parameters={
                    "commands": [
                        "python configureNode.py %s %s %s" %
                        (workerNodeIP, self.ICPVersion, self.role)
                    ],
                    "executionTimeout": ["1200"],
                    "workingDirectory": ["/root"]
                },
                Comment=
                "Execute script in bootnode to add newly inducted node to cluster",
                TimeoutSeconds=1500)
            time.sleep(GetParameterSleepTime * 10)
            command = response.get("Command")
            commandId = command.get("CommandId")
            TR.info(methodName, "Command Id : %s" % commandId)
            response = self.getExecutionStatus(commandId, self.bootInstanceId)
            TR.info(methodName, "response :%s" % response)
            if (response == "Success"):
                if (TR.isLoggable(Level.FINE)):
                    TR.fine(
                        methodName,
                        "Executed docker Run script in bootnode successfully")
            else:
                TR.fine(
                    methodName,
                    "Executed docker Run script in bootnode failed with error %s"
                    % response.get("StandardErrorContent"))
            parameterKey = "/%s/boot-public-key" % self.stackName
            TR.fine(methodName,
                    "delete ssm parameter key %s post setup" % parameterKey)
            self.ssm.delete_parameter(Name=parameterKey)