def copy_ssh_key(self, hostname, ip): """Copy SSH public key to remote system. This method will inject the public SSH key into remote system. It will create the public key content from the private key given. """ ssh_key = self.provider_params.get('ssh_key') username = self.provider_params.get('username') password = self.provider_params.get('password') # setup absolute path for private key private_key = os.path.join(self.workspace, ssh_key) # set permission of the private key try: os.chmod(private_key, stat.S_IRUSR | stat.S_IWUSR) except OSError as ex: raise BeakerProvisionerError( 'Error setting private key file permissions: %s' % ex) self.logger.info('Generating SSH public key from private..') # generate public key from private public_key = os.path.join(self.workspace, ssh_key + ".pub") rsa_key = paramiko.RSAKey(filename=private_key) with open(public_key, 'w') as f: f.write('%s %s\n' % (rsa_key.get_name(), rsa_key.get_base64())) self.logger.info('Successfully generated SSH public key from private!') self.logger.info('Send SSH key to remote system %s:%s' % (hostname, ip)) # send the key to the beaker machine ssh_con = paramiko.SSHClient() ssh_con.set_missing_host_key_policy(paramiko.AutoAddPolicy()) try: ssh_con.connect(hostname=ip, username=username, password=password) sftp = ssh_con.open_sftp() sftp.put(public_key, '/root/.ssh/authorized_keys') except paramiko.SSHException as ex: raise BeakerProvisionerError( 'Failed to connect to remote system: %s' % ex) except IOError as ex: raise BeakerProvisionerError('Failed sending public key: %s' % ex) finally: ssh_con.close() self.logger.debug("Successfully sent key: {0}, " "{1}".format(ip, hostname))
def get_machine_info(self, xmldata): """Get the remote system information from the beaker results XML. This method will parse the beaker results XML to get the hostname and IP address. :param xmldata: XML data of a beaker job status. :type xmldata: dict """ try: dom = parseString(xmldata) except Exception as ex: raise BeakerProvisionerError('Failed reading XML data: %s.' % ex) tasklist = dom.getElementsByTagName('task') for task in tasklist: cname = task.getAttribute('name') if cname == '/distribution/install' or cname == '/distribution/check-install': hostname = task.getElementsByTagName('system')[0]. \ getAttribute("value") addr = socket.gethostbyname(hostname) try: hostname = hostname.split('.')[0] except Exception: pass return hostname, addr
def submit_bkr_xml(self): """Submit a beaker job XML to Beaker. This method will upload (submit) a beaker job XML to Beaker. If the job was successfully uploaded, the beaker job id will be returned. """ # setup beaker client job submit commnand _cmd = "bkr job-submit --xml %s" % os.path.join( self.data_folder, self.job_xml) self.logger.info('Submitting beaker job XML..') self.logger.debug('Command to be run: %s' % _cmd) # submit beaker XML results = exec_local_cmd(_cmd) if results[0] != 0: self.logger.error(results[2]) raise BeakerProvisionerError('Failed to submit beaker job XML!') output = results[1] # post results tasks if output.find("Submitted:") != "-1": mod_output = output[output.find("Submitted:"):] # set the result as ascii instead of unicode job_id = mod_output[mod_output.find("[") + 2:mod_output.find("]") - 1] job_url = os.path.join(self.url, 'jobs', job_id[2:]) self.logger.info('Beaker job ID: %s.' % job_id) self.logger.info('Beaker job URL: %s.' % job_url) self.logger.info('Successfully submitted beaker XML!') return job_id, job_url
def _connect(self): """Connect to beaker.""" data = exec_local_cmd('bkr whoami') if data[0] != 0: self.logger.error(data[2]) raise BeakerProvisionerError('Connection to beaker failed!') self.logger.info('Connected to beaker!')
def get_job_status(self, xmldata): """Parse the beaker results. :param xmldata: XML data from beaker job status fetched. :type xmldata: str :return: Install (task) results :rtype: dict """ mydict = {} # parse xml data string try: dom = parseString(xmldata) except Exception as ex: raise BeakerProvisionerError('Failed reading XML data: %s.' % ex) # check job status joblist = dom.getElementsByTagName('job') # verify it is a length of 1 else exception if len(joblist) != 1: raise BeakerProvisionerError( 'Unable to parse job results from %s.' % self.provider_params.get('job_id')) mydict["job_result"] = joblist[0].getAttribute("result") mydict["job_status"] = joblist[0].getAttribute("status") tasklist = dom.getElementsByTagName('task') for task in tasklist: cname = task.getAttribute('name') if cname == '/distribution/install' or cname == '/distribution/check-install': mydict["install_result"] = task.getAttribute('result') mydict["install_status"] = task.getAttribute('status') if "install_status" in mydict and mydict["install_status"]: return mydict else: raise BeakerProvisionerError('Could not find install task status!')
def cancel_job(self, job_id): """Cancel a existing beaker job. This method will cancel a existing beaker job using the job id. """ # setup beaker job cancel command _cmd = "bkr job-cancel {0}".format(job_id) self.logger.info('Canceling beaker job..') # cancel beaker job results = exec_local_cmd(_cmd) if results[0] != 0: self.logger.error(results[2]) raise BeakerProvisionerError('Failed to cancel job.') output = results[1] if "Cancelled" in output: self.logger.info("Job %s cancelled." % job_id) else: raise BeakerProvisionerError('Failed to cancel beaker job!') self.logger.info('Successfully cancelled beaker job!')
def gen_bkr_xml(self): """Create beaker job xml based on host requirements. This method builds xml content and writes xml to file. """ # set beaker xml absolute file path bkr_xml_file = os.path.join(self.data_folder, self.job_xml) # set attributes for beaker xml object for key, value in self.provider_params.items(): if key is not 'name': if value: setattr(self.bkr_xml, key, value) # generate beaker job xml (workflow-simple command) self.bkr_xml.generate_beaker_xml(bkr_xml_file, kickstart_path=self.workspace, savefile=True) if 'force' in self.bkr_xml.hrname: self.logger.warning( 'Force was specified as a host_require_option.' 'Any other host_require_options will be ignored since ' 'force is a mutually exclusive option in beaker.') # format beaker client command to run # Latest version of beaker client fails to generate xml with this # replacement # _cmd = self.bkr_xml.cmd.replace('=', "\=") self.logger.info('Generating beaker job XML..') self.logger.debug('Command to be run: %s' % self.bkr_xml.cmd) # generate beaker job XML results = exec_local_cmd(self.bkr_xml.cmd) if results[0] != 0: self.logger.error(results[2]) raise BeakerProvisionerError('Failed to generate beaker job XML!') output = results[1] # generate complete beaker job XML self.bkr_xml.generate_xml_dom(bkr_xml_file, output, savefile=True) self.logger.info('Successfully generated beaker job XML!')
def analyze_results(self, resultsdict): """Analyze the beaker job install task status. return success, fail, or warn based on the job and install task statuses :param resultsdict: Beaker job of install task status. :rtype: dict :return: Action such as [wait, success or fail] :rtype: str """ # when is the job complete # TODO: explain what each beaker results analysis means if resultsdict["job_result"].strip().lower() == "new" and \ resultsdict["job_status"].strip().lower() in \ ["new", "waiting", "queued", "scheduled", "processed", "installing"]: return "wait" elif resultsdict["install_result"].strip().lower() == "new" and \ resultsdict["install_status"].strip().lower() in \ ["new", "waiting", "queued", "scheduled", "running", "processed"]: return "wait" elif resultsdict["job_status"].strip().lower() == "waiting" or \ resultsdict["install_status"].strip().lower() == "waiting": return "wait" elif resultsdict["job_result"].strip().lower() == "pass" and \ resultsdict["job_status"].strip().lower() == "running" and \ resultsdict["install_result"].strip().lower() == "pass" and \ resultsdict["install_status"].strip().lower() == "completed": return "success" elif resultsdict["job_result"].strip().lower() == "new" and \ resultsdict["job_status"].strip().lower() == "running" and \ resultsdict["install_result"].strip().lower() == "new" and \ resultsdict["install_status"].strip().lower() == "completed": return "success" elif resultsdict["job_result"].strip().lower() == "warn": return "fail" elif resultsdict["job_result"].strip().lower() == "fail": return "fail" else: raise BeakerProvisionerError('Unexpected job status: %s!' % resultsdict)
def wait_for_bkr_job(self, job_id): """Wait for submitted beaker job to have complete status. This method will wait for the beaker job to be complete depending on the timeout set. Users can define their own custom timeout or it will wait indefinitely for the machine to be provisioned. """ # set max wait time (default is 8 hours) wait = self.provider_params.get('bkr_timeout', None) if wait is None: wait = 28800 self.logger.debug('Beaker timeout limit: %s.' % wait) # check Beaker status every 60 seconds total_attempts = wait / 60 attempt = 0 while wait > 0: attempt += 1 self.logger.info('Waiting for machine to be ready, attempt %s of ' '%s.' % (attempt, total_attempts)) # setup beaker job results command _cmd = "bkr job-results %s" % job_id self.logger.debug('Fetching beaker job status..') # fetch beaker job status results = exec_local_cmd(_cmd) if results[0] != 0: self.logger.error(results[2]) raise BeakerProvisionerError('Failed to fetch job status!') xml_output = results[1] self.logger.debug('Successfully fetched beaker job status!') bkr_job_status_dict = self.get_job_status(xml_output) self.logger.debug("Beaker job status: %s" % bkr_job_status_dict) status = self.analyze_results(bkr_job_status_dict) self.logger.info('Beaker Job: id: %s status: %s.' % (job_id, status)) if status == "wait": wait -= 60 time.sleep(60) continue elif status == "success": self.logger.info("Machine is successfully provisioned from " "Beaker!") # get machine info return self.get_machine_info(xml_output) elif status == "fail": raise BeakerProvisionerError( 'Beaker job %s provision failed!' % job_id) else: raise BeakerProvisionerError( 'Beaker job %s has unknown status!' % job_id) # timeout reached for Beaker job self.logger.error('Maximum number of attempts reached!') # cancel job self.cancel_job(job_id) raise BeakerProvisionerError( 'Timeout reached waiting for beaker job to finish!')
def test_beaker_provisioner_error(): with pytest.raises(BeakerProvisionerError): raise BeakerProvisionerError('error message')