Интеграция Jira-AWX

от автора

«Деплой» виртуальных машин путем «тикета» в Jira

Всем привет, меня зовут Денис, я хотел бы поделиться опытом использования AWX в рамках одной из наших потребностей. Статья может быть полезна ребятам с «инфры», если в компании используется vmware и подобное cloud решение для частого деплоя, а для всяческой бюрократии и запросов вы используйте Jira.

Недавно @kuksepa выкладывала отличную статью про AWX поэтому я не стану много описывать конкретно его, а постараюсь кратко описать процесс. Прошу не обращать внимание на замазанные элементы, в них нет повествовательной нагрузки.

Схема использования данного решения

Схема использования данного решения

Jira

Для реализации в Jira создан новый проект (Deploy VM), пользовательские поля, 2 типа запроса (cloud и ЦОД) и 2 скрипта в ScriptRunner. Заранее говорю, что слабо знаком с динамикой полей и форм в jira и ввиду этого логика заполнения полей «тикета» пока что громоздкая

  • Проект Deploy VM имеет очередь заданий, созданы ассоциированные с ним поля, а также привязаны 2 типа запроса (Deploy и DeployCloud).

  • Скрипты (ScriptRunner) внутри jira запускаются при переводе «тикета» в статус «в работе» и посылают api запрос в AWX. Содержание скриптов в основном — это обработка полей и отправка POST запроса в требуемом AWX формате.

import com.atlassian.jira.component.ComponentAccessor import com.atlassian.jira.issue.IssueInputParametersImpl import com.atlassian.jira.user.ApplicationUser   //Далее идет мапинг пользовательских полей                                   ....                                      def apiUrl switch (field2Value) {     case "Ubuntu":         apiUrl = "https://Ваш url/v2/workflow_job_templates/121/launch/"         break     case "WindowsServer2019":            //тут ендпоинт в зависимости от выбора ОС           apiUrl = "https://Ваш url/v2/workflow_job_templates/122/launch/"         break     default:         log.error("Неверное значение для customfield_21402: ${field2Value}")         throw new IllegalArgumentException("Не удалось определить URL для значения: ${field2Value}") }  def requestBody = """{         //преобразование полей в тело запроса     "extra_vars":{          "computer_name": "${field3Value}", "VDC": "${field4Value}", "vapp_name": "${field5Value}",     "network": "${field6Value}", "ip": "${field7Value}", "gw": "${field8Value}", "ip_network": "${field9Value}",         "mask": "${field10Value}", "cpu": "${field11Value}", "memory": "${field12Value}", "disk": "${field13Value}", "group_access": "${field14Value}",         "disk2": "${field15Value}",         "size": "${field16Value}",         "mount_folder": "${field17Value}",         "description": "${field18Value}",         "JiraTicket": "${ticketNumber}" } }"""  //Далее формируем POST                                   ..... 
Варианты запроса

Варианты запроса

На данный момент поля заполнения cloud (слева) и vcenter (справа) выглядит следующим образом. Где существует выбор из предоставляемых ресурсов, выполнен каскадный список с единичным выбором.

Terraform-Git

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

Начну с примеров шаблона, которые выполняет непосредственно terraform.

1) Шаблон по деплою ubuntu в cloud, сеть и имя хоста передаем через скрипт, т.к. опции кастомизации не работают.

variable "vcd_url" {}  variable "org_name" {} variable "vcd_max_retry_timeout" { default = "1800" } variable "vcd_allow_unverified_ssl" { default = "true" } terraform {    backend "local" { path = "/home/dsoldatov/terraformstate/terraform.tfstate" }    required_providers {       vcd = {       source  = "Ваше/vmware/vcd"          version = ">=3.10.0"     }   } } provider "vcd" {   user                  = "none"   password              = "none"   auth_type             = "api_token_file"   api_token_file        = "token.json"   allow_api_token_file  = true   org                   = var.org_name   vdc                   = "Ваш тенант-{{VDC}}"   url                   = var.vcd_url   max_retry_timeout     = var.vcd_max_retry_timeout   allow_unverified_ssl  = var.vcd_allow_unverified_ssl } variable "attach_additional_disk" {   type = bool   default = "{{disk2}}" } resource "vcd_vapp_vm" "{{computer_name}}" {   vapp_name         = "{{vapp_name}}"   name              = "{{computer_name}}"   catalog_name      = "Ubuntu_new"   template_name     = "Ваш шаблон"   memory            = "{{memory}}"   cpus              = "{{cpu}}"   accept_all_eulas  = true   description       =  "{{description}}"   storage_profile   = "{{disk}}"   computer_name     = "{{computer_name}}"  network {     type               = "org"     name               = "{{network}}"     ip                 = "{{ip_network}}"     ip_allocation_mode = "MANUAL"     is_primary         = true }   customization {     allow_local_admin_password = "true"     auto_generate_password = "false"     admin_password = "Ваше"     enabled = true     initscript = <<-SCRIPT       #!/bin/bash       sudo awk '{gsub("Ваш IP шаблона", "{{ip}}")}1' /etc/netplan/00-installer-config.yaml > tmpfile && sudo mv tmpfile /etc/netplan/00-installer-config.yaml       sudo awk '{gsub("Ваш шлюз шаблона", "{{gw}}")}1' /etc/netplan/00-installer-config.yaml > tmpfile && sudo mv tmpfile /etc/netplan/00-installer-config.yaml       sudo awk '{gsub("Ваш шаблон шаблона", "{{computer_name}}")}1' /etc/hostname > tmpfile && sudo mv tmpfile /etc/hostname       netplan apply       reboot     SCRIPT   }  } resource "time_sleep" "wait_for_vm" { create_duration = "30s" } resource "vcd_vm_internal_disk" "additional_disk" { count         = var.attach_additional_disk ? 1 : 0 depends_on = [time_sleep.wait_for_vm] vapp_name     = "{{vapp_name}}" vm_name         = "{{computer_name}}" allow_vm_reboot = "true" bus_number        = 0 unit_number       = 1 bus_type        = "parallel" size_in_mb = "{{size}}" }

2) Шаблон по «деплою» windows server в vsphere использует 2 файла. В этом случае опции кастомизации работают и позволяют сменить сеть и авторизовать в домен. Также тут имеет место быть выбор между datastore и datacluster

provider "vsphere" {   user           = "${var.vsphere_user}"   password       = "${var.vsphere_password}"   vsphere_server = "${var.vsphere_server}"     allow_unverified_ssl = true }   data "vsphere_datacenter" "dc" {   name = "{{datacenter}}" } data "vsphere_datastore" "datastore" { count         = var.use_datastore_cluster ? 0 : 1 name         = "{{datastore}}" datacenter_id = data.vsphere_datacenter.dc.id } data "vsphere_datastore_cluster" "datastore_cluster" { count         = var.use_datastore_cluster ? 1 : 0 name         = "{{datastore}}" datacenter_id = data.vsphere_datacenter.dc.id }   data "vsphere_compute_cluster" "cluster" {   name          = "{{cluster}}"   datacenter_id = "${data.vsphere_datacenter.dc.id}" }   data "vsphere_network" "network" {   name          = "{{env}}_vlan{{vlan}}"   datacenter_id = "${data.vsphere_datacenter.dc.id}" }   data "vsphere_virtual_machine" "template" {  name          = "Ваш шаблон"   datacenter_id = "${data.vsphere_datacenter.dc.id}" } #data "vsphere_custom_attribute" "attribute" { #  name = "Описание" #} terraform {   backend "local" { path = "/home/dsoldatov/terraformstate/terraform.tfstate" }   required_providers {     Ваше = {       source  = "Ваше"     }   } }
variable "use_datastore_cluster" { description = "use datastore cluster" type        = bool default     = "{{datacluster}}" } locals { add_extra_disk = "{{disk2}}" selected_datastore = var.use_datastore_cluster ? data.vsphere_datastore_cluster.datastore_cluster[0].id : data.vsphere_datastore.datastore[0].id } resource "vsphere_virtual_machine" "vm1" {   name             = "{{computer_name}}"   resource_pool_id = "${data.vsphere_compute_cluster.cluster.resource_pool_id}"   datastore_id     =   local.selected_datastore   folder = "{{folder}}" #  custom_attributes = tomap({"${data.vsphere_custom_attribute.attribute.id}" = "{{description}}"})       num_cpus = "{{cpu}}"   memory   = "{{memory}}"   firmware = "efi"   guest_id = "${data.vsphere_virtual_machine.template.guest_id}"     scsi_type = "${data.vsphere_virtual_machine.template.scsi_type}"        network_interface {     network_id   = "${data.vsphere_network.network.id}"     adapter_type = "${data.vsphere_virtual_machine.template.network_interface_types[0]}"   }      disk {     label            = "disk0"     size             = "${data.vsphere_virtual_machine.template.disks.0.size}"     eagerly_scrub    = "${data.vsphere_virtual_machine.template.disks.0.eagerly_scrub}"     thin_provisioned = "${data.vsphere_virtual_machine.template.disks.0.thin_provisioned}" }      dynamic "disk" {     for_each = local.add_extra_disk ? [1] : []     content {        label            = "disk1"        size             = "{{size}}"        eagerly_scrub    = false        thin_provisioned = false        unit_number      = 1        controller_type = "scsi"      } }       clone {     template_uuid = "${data.vsphere_virtual_machine.template.id}"      customize {       windows_options {         computer_name         = "{{computer_name}}"         join_domain           = "Ваш домен"         domain_admin_user     = "{{login}}"         domain_admin_password = "{{pass}}" #        admin_password        = var.local_adminpass         run_once_command_list = [           "C:\\Distr\\installer.exe /s",           "msiexec /i C:\\Distr\\Ваше /quiet"         ]        }              network_interface {         ipv4_address = "{{ip_network}}"         ipv4_netmask = "{{mask}}"         dns_server_list     = ["Ваши dns"]       }         ipv4_gateway = "{{gw}}" }   } }

Запуск задания и передача данных в terraform инициируется ansible шаблоном. Пример шаблона.

  gather_facts: false   tasks:      - name: Wait   #Ожидание в случае параллельно работающего процесса деплоя     ansible.builtin.wait_for:       path: /home/dsoldatov/terraformstate/.terraform.tfstate.lock.info       state: absent    - name: Run state.sh script #Запуск скрипта на сброс состояния     command: /home/dsoldatov/templates_cloud_windows/state.sh     ignore_errors: yes     args:      chdir: /home/dsoldatov/templates_cloud_windows/         - name: Copy #Замена шаблона на деплой     template:       src: ../Playbooks/main_windows.j2       dest: /home/dsoldatov/templates_cloud_windows/main.tf        - name: Init     command: "terraform init"     args:       chdir: /home/dsoldatov/templates_cloud_windows     - name: Plan                                             #Инит и запуск деплоя     command: terraform plan -out=plan.out     args:       chdir: /home/dsoldatov/templates_cloud_windows    - name: Apply     command: terraform apply -input=false plan.out     args:       chdir: /home/dsoldatov/templates_cloud_windows    - name: Pause #Пауза для последеплойного состояния     ansible.builtin.pause:       seconds: 60

AWX

Для реализации получения на основании заполненного «тикета» в Jira готовой ВМ, авторизованной в домене, возможностью автоматической разметки второго диска с «мапингом» необходимого каталога, а также для автоматической смены статуса «тикета» в Jira, потребовалось сделать несколько дополнительных решений.

Этапы workflow деплоя ubuntu с добавлением в "инвентори" AWX, авторизацией в домене и добавлением и разметкой доп. диска.

Этапы workflow деплоя ubuntu с добавлением в «инвентори» AWX, авторизацией в домене и добавлением и разметкой доп. диска.

Для каждого из деплоев в AWX созданы workflow с цепочкой из ряда job_template, т.к. после деплоя ВМ нам необходимо, например, добавить ВМ в домен, a для этого после деплоя (terraform) потребуется выполнение ansible шаблона (кроме windows server в vsphere) с AWX (job_template) на авторизацию в домен, но мы не сможем его выполнить, если данного хоста нет в «инвентори». Поэтому первым делом во всех workflow мы выполняем именно этот ansible шаблон.

- hosts: localhost   tasks:   - name: Add new host     uri:        url: "ваш url"        method: POST        headers:          Content-Type: "application/json"          Authorization: "Bearer Ваш токен"        body: >              {              "name": "{{ computer_name }}",              "variables": "{\"ansible_host\": \"{{ ip_network }}\"}"              }        body_format: json            status_code: 201     register: result   - name: Debug result     debug:       var: result

Добавление хоста в «инвентори» на основании переданных Jira переменных из «тикета». Токен заранее прописан от технической jira УЗ с определенными правами в AWX.

Авторизация в домен реализована в рамках job_template ansible шаблонов идущих после основного деплоя.

- name: enter domain   hosts: "{{computer_name}}"   gather_facts: false   vars_files:     - ../Playbooks/credentials_win.yml   tasks:      - name: Pause        ansible.builtin.pause:         seconds: 90       - name: Join domain        win_domain_membership:         dns_domain_name: "Ваше домен"         hostname: "{{computer_name}}"         domain_admin_user: "{{login}}"         domain_admin_password: "{{pass}}"         state: domain

«Креды» для авторизации в домен подставляются из файла credentials_win.yml записанные в ansible vault формате. Без таймаута winrm не способен выполнить шаблон.

Для удобства использования «последеплойной» Ubuntu c доп. диском, в workflow добавлен шаблон ansible с процедурой разметки

- name: add_seconddisk   hosts: "{{ computer_name }}"   become: yes   vars:     disk2: "{{ lookup('env', 'DISK2') | default(false) }}"  #Переводится в true если потребовался второй диск и при составлении заявки вы выбрали true   tasks:     - name: reboot                   #Проводим ребут и ждем когда сервер перезапустится       shell: |        #!/bin/bash             /sbin/shutdown -r now             sleep 45       async: 1       poll: 0     - name: Wait       wait_for_connection:         delay: 30         timeout: 45      - name: Resize       block:         - name: Copy #Проводими создание LVM, разметку и мапинг папки к диску           copy:             dest: /home/ansible_usr/disk2.sh             content: |               #!/bin/bash               sudo echo -e "nn\np\n1\n\n\nw\nq" | sudo fdisk /dev/sdb               sudo pvcreate /dev/sdb1               sudo vgcreate datavolume /dev/sdb1               sudo lvcreate -l +100%FREE -n data datavolume               sudo mkfs.ext4 /dev/mapper/datavolume-data               sudo mkdir -p "{{ mount_folder }}"               sudo echo "/dev/datavolume/data {{ mount_folder }}     ext4    rw     1 1" >> /etc/fstab               sudo mount -a             mode: '0755'          - name: Execute           shell: /home/ansible_usr/disk2.sh          - name: Remove           file:             path: /home/ansible_usr/disk2.sh             state: absent       when: disk2 | bool     - name: Skip disk2             #Пропускаем процедуру, если false       debug:         msg: "Disk skipped."       when: not disk2 | bool

В данном случае mount_folder был заполнен как /var/postgres1

Так же реализована отправка webhook после окончания выполнения workflow

Настроенное уведомление в AWX

Настроенное уведомление в AWX

Уведомление отработает в случае успеха/провала выполнения workflow и отправит статус на локально стоящий flask сервер, который забирает значение статуса и смотрит на заполненную переменную JiraTicket. Данная переменная при создании «тикета» в jira заполняется автоматический и передается в AWX, чтобы отправился issue_key и сделал новый статус заявки.

По итогу, данный способ предоставления инфраструктурных мощностей вполне оправдан, особенно, если ваши рабочие процессы постоянно связаны с деплоем ВМ, а второе по трате времени — это jira с ее insight и прочими вещами. Остается сделать динамику полей и провести логические ассоциации с проектами.

В целом, даже если jira не входит в ваш рабочий стек, можно использовать web-интерфейс AWX с его Survey.


ссылка на оригинал статьи https://habr.com/ru/articles/876934/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *