Статьи

Наша простая конфигурация и развертывание Jenkins

Первоначально эта статья была написана Денали Луммой в блоге Okta.

В Okta мы прошли много итераций использования Jenkins для создания и тестирования нашего программного обеспечения. Мы используем ряд инструментов, чтобы убедиться, что наш код работает правильно, и нам нравится, когда Дженкинс управляет ими. Список будет знаком любому, кто использует среду Java; PMD, Cobertura, модульные и функциональные тесты с JUnit, тесты Selenium с testNG, а также некоторые более экзотические инструменты, такие как сканер BURP Security, MogoTest и SLAMD.

Мы быстро обнаружили, что ручная установка и настройка Jenkins, когда нам требовался новый сервер для конкретной задачи или специального проекта, или перестройка существующего сервера, который был поврежден или съеден Amazon, была невероятно трудоемкой и подверженной ошибкам. Мы реализовали простой способ управления конфигурацией сервера Jenkins и конфигурациями заданий для данного экземпляра с помощью кода. Я собираюсь пройтись по подходу, который мы взяли (немного упростил), как пример того, как это сделать.

Мы используем Python и Jinja2, однако этот подход можно использовать с любым языком шаблонов, который имеет простую замену переменных и логические элементы управления для циклических и условных выражений.

Итак, начнем.

Необходимые условия для библиотеки и пакета

Это предполагает, что мы работаем на CentOS, однако мы можем и действительно запускать те же самые вещи на Windows и Mac.

Python 2.6

Мы стандартизируем эту версию, но другие тоже будут работать. Однако по учебным причинам я буду ссылаться на эти конкретные библиотеки, которые мы используем. Если у вас не установлен Python или у вас другая версия, вы можете получить 2.6 здесь: http://python.org/ftp/python/2.6.6/Python-2.6.6.tgz

 

wget http://python.org/ftp/python/2.6.6/Python-2.6.6.tgz
tar fxz Python-2.6.6.tgz
cd Python-2.6.6
./configure
make
sudo make altinstall
sudo ln -s /usr/local/bin/python2.6 /usr/bin/python2.6

 

Setuptools

setuptools необходим для установки Jinja2.

wget http://pypi.python.org/packages/2.6/s/setuptools/setuptools-0.6c11-py2.6.egg
sudo sh setuptools-0.6c11-py2.6.egg

 

Jinja2

Этот пакет можно найти здесь: http://pypi.python.org/packages/source/J/Jinja2/Jinja2-2.5.5.tar.gz

wget http://pypi.python.org/packages/source/J/Jinja2/Jinja2-2.5.5.tar.gz
tar fxz Jinja2-2.5.5.tar.gz
cd Jinja2-2.5.5
sudo python2.6 setup.py install

Дженкинс: сердце этого

Для динамического создания экземпляра Jenkins необходимо понять, какие файлы находятся и где они используются.

Jenkins — это отличное программное обеспечение, и его великолепием является простота использования через веб-интерфейс Jenkins, особенно для тех, кто только начинает. Но в своей основе Jenkins использует файлы конфигурации, определенные с помощью xml, для управления приложением. Редактирование этих файлов конфигурации xml имеет тот же эффект, что и редактирование Jenkins через графический интерфейс. Создавать XML-файлы с помощью кода проще, но обычно они менее сложны, чем сборка в API Jenkins. Так что это подход, который мы выбрали для управления нашими экземплярами Jenkins.

Когда Дженкинс запускается с помощью java -jar jenkins.war или какого-либо более сложного варианта этого, он создает каталог с именем .jenkins, на который ссылается переменная среды с именем JENKINS_HOME. Мы будем работать с файлами, созданными в этом каталоге, чтобы контролировать, как определяются сервер и соответствующие задания.

Загрузите последнюю стабильную войну Дженкинса из проекта с открытым исходным кодом здесь: http://jenkins-ci.org/

Мы пройдемся по содержимому каталога, созданного Дженкинсом. Это ключ к пониманию того, как создать конкретный экземпляр Jenkins и связанные задания с кодом.

После загрузки установите переменную JENKINS_HOME в файл с именем .jenkins в текущем рабочем каталоге:

export JENKINS_HOME=`pwd`/.jenkins

 

По умолчанию он будет создан в файле с именем .jenkins в домашнем каталоге пользователей, но мы не хотим этого, мы хотим, чтобы он находился в текущем рабочем каталоге в скрытом каталоге с именем .jenkins, рядом с jenkins.war. ,

Запустите веб-приложение Jenkins:

java -jar jenkins.war

 

Давайте пройдемся по структуре.

Вы должны увидеть что-то вроде этого:

$ cd .jenkins
[.jenkins]$ ls -al
total 32
drwxr-xr-x 10 dlumma staff 340 Mar 19 15:22 .
drwxr-xr-x 4 dlumma staff 136 Mar 19 15:22 ..
-rw-r--r-- 1 dlumma staff 159 Mar 19 15:22 hudson.model.UpdateCenter.xml
-rw------- 1 dlumma staff 1675 Mar 19 15:22 identity.key
drwxr-xr-x 2 dlumma staff 68 Mar 19 15:22 jobs
-rw-r--r-- 1 dlumma staff 907 Mar 19 15:22 nodeMonitors.xml
drwxr-xr-x 16 dlumma staff 544 Mar 19 15:22 plugins
-rw-r--r-- 1 dlumma staff 64 Mar 19 15:22 secret.key
drwxr-xr-x 3 dlumma staff 102 Mar 19 15:22 userContent
drwxr-xr-x 30 dlumma staff 1020 Mar 19 15:22 war

 

Это скелетная структура каталогов, которую Jenkins использует для управления собой. Дополнительные файлы и каталоги добавляются по мере того, как указываются дополнительные функции и конфигурации. Каталоги, с которыми мы имеем дело и которые наиболее полезны, это: 1) каталог заданий (это потрясающе определяет все задания) и 2) каталоги плагинов (где определены все плагины, я тоже знаю, что это шокирует). По умолчанию в каталоге jobs ничего не определено, но уже установлено несколько плагинов, таких как ant maven, ssh и subversion.

Помимо вещей в каталогах заданий и плагинов, другой файл, которым мы часто манипулируем, называется config.xml. Этот файл находится на верхнем уровне под .jenkins. Это основной файл конфигурации для сервера Jenkins. Мы можем заставить Дженкинса сгенерировать этот файл, изменив страницу http: // jenkinsinstance / configure через пользовательский интерфейс. Перейдите на страницу конфигурации, добавьте системное сообщение «My Awesome Jenkins Server» и нажмите «Сохранить». Файл config.xml должен теперь существовать в каталоге .jenkins.

$ emacs .jenkins / config.xml показывает:

 

<?xml version='1.0' encoding='UTF-8'?>

<hudson>
<disabledAdministrativeMonitors/>
<version>1.456</version>
<numExecutors>2</numExecutors>
<mode>NORMAL</mode>
<useSecurity>true</useSecurity>
<authorizationStrategy/>
<securityRealm/>
<projectNamingStrategy/>
<workspaceDir>${ITEM_ROOTDIR}/workspace</workspaceDir>
<buildsDir>${ITEM_ROOTDIR}/builds</buildsDir>
<systemMessage>My Awesome Jenkins Server</systemMessage>
<jdks/>
<viewsTabBar/>
<myViewsTabBar/>
<clouds/>
<slaves/>
<quietPeriod>5</quietPeriod>
<scmCheckoutRetryCount>0</scmCheckoutRetryCount>
<views>
<hudson.model.AllView>
<owner reference="../../.."/>
<name>All</name>
<filterExecutors>false</filterExecutors>
<filterQueue>false</filterQueue>
<properties/>
</hudson.model.AllView>
</views>
<primaryView>All</primaryView>
<slaveAgentPort>0</slaveAgentPort>
<label></label>
<nodeProperties/>
<globalNodeProperties/>
</hudson>

 

После понимания базовой структуры файлов, которые Jenkins использует для определения его конфигурации и функциональности, можно приступить к определению его с помощью исходного кода.

Jenkinizer

Теперь не стесняйтесь сдуть свой экземпляр Jenkins и все связанные с ним файлы и клонировать настройку репозитория для этого сообщения в блоге Okta здесь:

git clone [email protected]: / okta / jenkinizer

В нашем репозитории jenkinizer есть четыре вида файлов: файлы конфигурации python, файлы шаблонов xml, файлы статических ресурсов и сценарии. Структура выглядит следующим образом (это упрощенная версия нашего фактического внутреннего кода и содержит только подмножество кода, связанного с тестированием селена для ясности):

jenkinizer /
— config /
—- серверы /
—— selenium /
——– selenium_server.py
—- шаблоны /
—— selenium /
——– selenium_server_config.xml
——– selenium_setup.xml
——– selenium_suite.xml
— установить /
—- .jenkins /
—— plugins /
——– (набор установленных нами плагинов)
—— hudson.plugins.emailext.ExtendedEmailPublisher.xml
—- jenkins-env.sh
—- jenkins.sh
—- jenkins.war
— mkconfig. ру

Справочники

 


конфиг

Этот каталог содержит все шаблоны и конфигурации заданий.

 


устанавливать

Статические ресурсы и сценарии инициализации для запуска Jenkins находятся в стадии установки.

Главный скрипт mkconfig.py находится в главном каталоге. Давайте пройдемся по тому, что это делает.

class ServerOptionParser(OptionParser):
    def __init__(self):
        OptionParser.__init__(self)
        self.add_option("-t", "--jenkins-type", action="store", dest="jenkins_type", default="selenium",
                         help="Type of server to generate." )
        self.add_option("-j", "--jenkins-jobset", dest="jenkins_jobset", default="integration",
                          help="Specific host this Jenkins will be deployed to.")
        self.add_option("-o", "--install-dir", dest="install_dir", default="/ebs/ci-build/tools/jenkins",
                          help="Location to install the newly generated server.")
        self.add_option("-b", "--jenkins-branch", dest="jenkins_branch", default="release",
                          help="Branch to use in testing.")
        self.add_option("-e", "--recipient-email", dest="recipient_email", default=" ",
                          help="Email recipient")
        self.add_option("-d", "--discard-old-builds", action="store_true", dest="discard_old_builds", default=False,
                          help="If set, old builds will be discarded.")

Это простой инструмент командной строки, созданный из прекрасной библиотеки OptionParser. Определим несколько параметров и аргументов. Наиболее важными из них являются jenkins-type и jenkins-jobset. В этом примере я показываю дистиллированную версию наших серверов селена. Таким образом, тип jenkins будет селеном, а набор jenkins-job будет либо интеграцией, либо предварительной интеграцией, либо последовательным. У нас есть много других видов серверов jenkins, которые я здесь не включаю для таких вещей, как сборка и тестирование мобильных устройств, артефакты Windows, модульные и функциональные сборки, конфигурации master / slave и информационные панели. Если вы хотите создавать серверы разных типов, вы можете следовать шаблону здесь, создав новый тип jenkins и назначив ему группу наборов заданий.

Следующие несколько методов используются для сохранения результатов теста при обновлении самого экземпляра Jenkins. Иногда нам все равно, но иногда мы хотим сохранить результаты испытаний, собранные до сих пор, и добавить новую работу или немного изменить настройку. Эти методы справляются с этим.

def make_selected_archive(backup_file_name, base_dir, files, gzip = True):
    backup_file = tarfile.open(backup_file_name, 'w:gz' if gzip else "w")
    current_dir = os.getcwd()
    os.chdir(base_dir)
    for artifact in files:
        if os.path.exists(artifact):
            backup_file.add(artifact)
    backup_file.close()
    os.chdir(current_dir)

def restore_archive(backup_file_name, base_dir, gzip = True):
    backup_file = tarfile.open(backup_file_name, "r:gz" if gzip else "r")
    backup_file.extractall(base_dir)
    backup_file.close()

def backup_old_builds(jobs_dir, job_names):
    archives = {}
    for job in job_names:
        job_dir = jobs_dir + "/" + job
        if os.path.exists(job_dir):
            print "Backing up old builds in %s" % (job_dir)
            backup_file_name = tempfile.gettempdir() + "/" + job + ".tar.gz"
            make_selected_archive(backup_file_name, job_dir, ["./builds", "./lastStableBuild", "./lastSuccessfulBuild", "./nextBuildNumber"])
            archives[job] = backup_file_name
    return archives

Затем идет основная функциональность, которая генерирует файлы конфигурации для этого сервера Jenkins и его заданий.
Мы получаем тип, jobset, installation_dir и email, которые будут использоваться в этом случае.

def build_server(options, base_path = os.getcwd()):

    # We categorize Jenkins instances depending on the jobs they run.  We have Jenkins instances that run selenium,
    # run unit and functional tests, build artifacts and deploy artifacts. This defined the kind of Jenkins we are
    # deploying.
    jenkins_type = options.jenkins_type

    # The set of jobs to deploy.
    jenkins_jobset = options.jenkins_jobset

    # The location to install this Jenkins instance to on the filesystem.
    install_dir = options.install_dir

    # Email.
    recipient_email = options.recipient_email

Мы загружаем файлы шаблонов, определенные для нашего типа сервера.

    # Make our python job and server configuration files reachable.
    sys.path.extend([base_path + "/config/servers"])

Мы импортируем определенный dict, определяющий указанный набор заданий и тип jenkins.

    # Define the template environment.
    template_env = Environment(loader=FileSystemLoader(base_path + '/config/templates/' + jenkins_type + '/'))

Мы определяем места установки.

    # Import the specific server and job data for this jenkins type
    # and this jobset.
    server_config = __import__(jenkins_type).__dict__[jenkins_jobset]
    server_config["env"]["EMAIL"] = recipient_email
    server_config["env"]["BRANCH"] = options.jenkins_branch

Если указано, мы создаем резервные копии существующих архивов, а затем удаляем текущую установку Jenkins.

    jenkins_dir = install_dir + "/.jenkins"
    jobs_dir = jenkins_dir + "/jobs"

    # Backup archived files and
    # remove the old instance if it exists.
    archives = {}
    if os.path.exists(install_dir):
        if not options.discard_old_builds:
            archives = backup_old_builds(jobs_dir, [job["job_name"] for job in server_config["jobs"]])
        shutil.rmtree(install_dir)

Мы копируем все файлы статических ресурсов в каталог установки и распечатываем информационное сообщение.

    # Copy all of the static Jenkins files to the installation directory
    shutil.copytree(base_path + "/install", install_dir)

    print "Generating configuration for jenkins type %s (writing to %s)" % (jenkins_type, install_dir)

Мы генерируем основной файл конфигурации Jenkins с заданным файлом конфигурации и конфигурацией сервера.

    # Create the jenkins main config file.
    with open(jenkins_dir + "/config.xml", "w") as f:
        t = template_env.get_template("server-config.xml")
        f.write(t.render(server_config))

И, наконец, мы генерируем файлы конфигурации заданий, записываем их в каталог установки и восстанавливаем любые заархивированные файлы.

    # Create the jobs config files.
    for job in server_config["jobs"]:
        job_name = job[ "job_name"]
        job_template = job["template_file" ]
        job.update(server_config)

        print "Creating job %s from %s" % (job_name, job_template)
        job_dir = jobs_dir + "/" + job_name
        os.makedirs(job_dir)
        with open(job_dir + "/config.xml", "w") as f:
            t = template_env.get_template(job_template)
            f.write(t.render(job))

        if archives.has_key(job_name):
            print "Restoring old builds for %s" % (job_name)
            restore_archive(archives[job_name], job_dir)

Теперь давайте посмотрим на используемые файлы. Файлы шаблонов в config / templates / selenium являются XML-файлами с простыми переменными, определенными в файле конфигурации selenium.py. Первоначально они были скопированы из установки Jenkins и отредактированы с помощью переменных для динамического рендеринга на основе данных, определенных в файлах Python.

Основные данные задания определены в config / servers / selenium.py. Каждый набор заданий определяется с помощью dict, созданного из get_default (), который содержит ключи env, os, chained, views и jobs. Ключ jobs ссылается на другой список, который содержит указания, какие конкретные данные задания, такие как job_name, template_file, max_builds_to_keep, repo и другие.

Обновление существующего сервера Jenkins selenium с помощью набора заданий интеграции будет выглядеть следующим образом:

$ python2.6 mkconfig.py -t selenium -j integration
Generating configuration for jenkins type selenium (writing to /ebs/ci-build/tools/jenkins)
Creating job okta.build from selenium-setup.xml
Creating job core.chrome from selenium-suite.xml
Creating job apps.chrome from selenium-suite.xml
Creating job plugin.apps.chrome from selenium-suite.xml
Creating job core.firefox.latest from selenium-suite.xml
Creating job apps.firefox.latest from selenium-suite.xml
Creating job plugin.apps.firefox.latest from selenium-suite.xml
Creating job core.ie.8 from selenium-suite.xml
Creating job apps.ie.8 from selenium-suite.xml
Creating job plugin.apps.ie.8 from selenium-suite.xml
Creating job core.firefox.3.6 from selenium-suite.xml
Creating job apps.firefox.3.6 from selenium-suite.xml
Creating job plugin.apps.firefox.3.6 from selenium-suite.xml
Creating job core.ie.9 from selenium-suite.xml
Creating job apps.ie.9 from selenium-suite.xml
Creating job plugin.apps.ie.9 from selenium-suite.xml

 

Теперь перейдите в каталог установки и запустите его.

 

$ cd /ebs/ci-build/tools/jenkins
$ ./jenkins.sh
JENKINS_HOME=/ebs/ci-build/tools/jenkins/.jenkins

 

Хвост журнал, чтобы убедиться, что все работает гладко.

 

$ tail -f jenkins.log
Running from: /ebs/ci-build/tools/jenkins/jenkins.war
webroot: EnvVars.masterEnvVars.get("JENKINS_HOME")
Apr 1, 2012 10:04:26 PM winstone.Logger logInternal
INFO: Beginning extraction from war file
Jenkins home directory: /ebs/ci-build/tools/jenkins/.jenkins found at: EnvVars.masterEnvVars.get("JENKINS_HOME")
Apr 1, 2012 10:04:29 PM winstone.Logger logInternal
INFO: HTTP Listener started: port=8080
Apr 1, 2012 10:04:29 PM winstone.Logger logInternal
INFO: AJP13 Listener started: port=8009
Apr 1, 2012 10:04:29 PM winstone.Logger logInternal
INFO: Winstone Servlet Engine v0.9.10 running: controlPort=disabled
Apr 1, 2012 10:04:30 PM jenkins.InitReactorRunner$1 onAttained
INFO: Started initialization
Apr 1, 2012 10:04:32 PM jenkins.InitReactorRunner$1 onAttained
INFO: Listed all plugins
Apr 1, 2012 10:04:32 PM jenkins.InitReactorRunner$1 onAttained
INFO: Prepared all plugins
Apr 1, 2012 10:04:32 PM jenkins.InitReactorRunner$1 onAttained
INFO: Started all plugins
Apr 1, 2012 10:04:32 PM jenkins.InitReactorRunner$1 onAttained
INFO: Augmented all extensions
Apr 1, 2012 10:04:36 PM org.apache.sshd.common.util.SecurityUtils$BouncyCastleRegistration run
INFO: Trying to register BouncyCastle as a JCE provider
Apr 1, 2012 10:04:36 PM org.apache.sshd.common.util.SecurityUtils$BouncyCastleRegistration run
INFO: Registration succeeded
Apr 1, 2012 10:04:36 PM jenkins.InitReactorRunner$1 onAttained
INFO: Loaded all jobs
Apr 1, 2012 10:04:36 PM org.jenkinsci.main.modules.sshd.SSHD start
INFO: Started SSHD at port 52810
Apr 1, 2012 10:04:36 PM jenkins.InitReactorRunner$1 onAttained
INFO: Completed initialization
Apr 1, 2012 10:04:37 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.web.context.support.StaticWebApplicationContext@2be3d80c: display name [Root WebApplicationContext]; startup date [Sun Apr 01 22:04:37 PDT 2012]; root of context hierarchy
Apr 1, 2012 10:04:37 PM org.springframework.context.support.AbstractApplicationContext obtainFreshBeanFactory
INFO: Bean factory for application context [org.springframework.web.context.support.StaticWebApplicationContext@2be3d80c]: org.springframework.beans.factory.support.DefaultListableBeanFactory@1e94b0ca
Apr 1, 2012 10:04:37 PM org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@1e94b0ca: defining beans [authenticationManager]; root of factory hierarchy
Apr 1, 2012 10:04:37 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.web.context.support.StaticWebApplicationContext@23a65a18: display name [Root WebApplicationContext]; startup date [Sun Apr 01 22:04:37 PDT 2012]; root of context hierarchy
Apr 1, 2012 10:04:37 PM org.springframework.context.support.AbstractApplicationContext obtainFreshBeanFactory
INFO: Bean factory for application context [org.springframework.web.context.support.StaticWebApplicationContext@23a65a18]: org.springframework.beans.factory.support.DefaultListableBeanFactory@29cc3436
Apr 1, 2012 10:04:37 PM org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@29cc3436: defining beans [filter,legacy]; root of factory hierarchy
Apr 1, 2012 10:04:37 PM hudson.TcpSlaveAgentListener
INFO: JNLP slave agent listener started on TCP port 7070
Apr 1, 2012 10:04:42 PM hudson.triggers.SCMTrigger$Runner run
INFO: SCM changes detected in okta.build. Triggering #1
Apr 1, 2012 10:04:48 PM hudson.WebAppMain$2 run
INFO: Jenkins is fully up and running
Apr 1, 2012 10:04:58 PM hudson.model.DownloadService$Downloadable doPostBack
INFO: Obtained the updated data file for hudson.tasks.Maven.MavenInstaller
Apr 1, 2012 10:04:58 PM hudson.model.DownloadService$Downloadable doPostBack
INFO: Obtained the updated data file for hudson.tasks.Ant.AntInstaller
Apr 1, 2012 10:04:58 PM hudson.model.DownloadService$Downloadable doPostBack
INFO: Obtained the updated data file for hudson.tools.JDKInstaller
Apr 1, 2012 10:05:01 PM hudson.model.UpdateSite doPostBack
INFO: Obtained the latest update center data file for UpdateSource default
Apr 1, 2012 10:05:42 PM hudson.triggers.SCMTrigger$Runner run
INFO: SCM changes detected in okta.build. Triggering #2
Apr 1, 2012 10:06:42 PM hudson.triggers.SCMTrigger$Runner run
INFO: SCM changes detected in okta.build. Triggering #3

 

Откройте ваш браузер и посмотрите недавно созданный экземпляр Jenkins по адресу: http: // localhost: 8080

Это простой и эффективный способ управления несколькими экземплярами Jenkins, предназначенными для выполнения различных задач. Это более легко обслуживаемо, чем настройка нового экземпляра через графический интерфейс, который очень подвержен ошибкам и занимает много времени. Тем не менее, это просто и не требует какого-либо сложного программирования. Для управления многими экземплярами Jenkins, делающими много разных вещей, этот подход хорошо сработал для нас.