{"id":7640,"date":"2024-01-24T11:40:04","date_gmt":"2024-01-24T17:40:04","guid":{"rendered":"https:\/\/gregsowell.com\/?p=7640"},"modified":"2024-01-24T11:40:04","modified_gmt":"2024-01-24T17:40:04","slug":"build-and-replace-linux-migration-via-ansible-ascender-awx-aap","status":"publish","type":"post","link":"https:\/\/gregsowell.com\/?p=7640","title":{"rendered":"&#8220;Build And Replace&#8221; Linux Migration Via Ansible, Ascender, AWX, AAP"},"content":{"rendered":"<p><a href=\"https:\/\/gregsowell.com\/wp-content\/uploads\/2024\/01\/thumbnail-template.jpg\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/gregsowell.com\/wp-content\/uploads\/2024\/01\/thumbnail-template-300x169.jpg\" alt=\"\" width=\"300\" height=\"169\" class=\"aligncenter size-medium wp-image-7641\" srcset=\"https:\/\/gregsowell.com\/wp-content\/uploads\/2024\/01\/thumbnail-template-300x169.jpg 300w, https:\/\/gregsowell.com\/wp-content\/uploads\/2024\/01\/thumbnail-template-768x432.jpg 768w, https:\/\/gregsowell.com\/wp-content\/uploads\/2024\/01\/thumbnail-template-1024x576.jpg 1024w, https:\/\/gregsowell.com\/wp-content\/uploads\/2024\/01\/thumbnail-template.jpg 1280w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><\/a><\/p>\n<p>Migrating from one Linux major version to another never seems to be a simple task, but through the magic of automation, it can be a lot simpler and reproducible. I\u2019m going to cover the Ansible playbooks I created to do the work, then I\u2019ll execute it using our enterprise automation platform called Ascender.<\/p>\n<p>Our recommended method is to:<br \/>\n&#8211; Backup configuration and data from the old server<br \/>\n&#8211; Provision a brand new server with the required apps<br \/>\n&#8211; Restore configurations and data to the new server<br \/>\n&#8211; Test services to the new server<br \/>\n&#8211; Sunset the old server<\/p>\n<h2>Video Demo<\/h2>\n<p><iframe loading=\"lazy\" width=\"560\" height=\"315\" src=\"https:\/\/www.youtube.com\/embed\/gblbYVr2Y70?si=KN2fws4FRPVzSjdz\" title=\"YouTube video player\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" allowfullscreen><\/iframe><\/p>\n<h2>Playbooks<\/h2>\n<p>First, I\u2019m using resources from the <a href=\"https:\/\/docs.ansible.com\/ansible\/latest\/collections\/community\/general\/index.html\">community.general collection found here<\/a>. I actually have a version of it included in my git repository.<\/p>\n<p>All of my playbooks can be found here in <a href=\"https:\/\/github.com\/gregsowell\/manual-system-migration-via-ansible\">my git repository<\/a>.<\/p>\n<p>I\u2019ll cover some of the playbooks here\u2026 mostly discussing the highlights. The <strong>discover-backup.yml<\/strong> playbook is the first playbook run:<\/p>\n<pre class=\"gs-code\"><code class=\"language-plaintext\">---\r\n- name: Discover\/backup hosts to be migrated\r\n  hosts: migration-hosts\r\n  gather_facts: false\r\n  vars:\r\n    # The host to store backup info to\r\n    backup_storage: backup-storage\r\n\r\n    # The location on the backup host to store info\r\n    backup_location: \/tmp\/migration\r\n  tasks:\r\n  - name: Execute rpm to get list of installed packages\r\n    ansible.builtin.command: rpm -qa --qf &quot;%{NAME} %{VERSION}-%{RELEASE}\\n&quot;\r\n    register: rpm_query\r\n\r\n  - name: Populate service facts - look for running services\r\n    ansible.builtin.service_facts:\r\n\r\n  # - name: Print service facts\r\n  #   ansible.builtin.debug:\r\n  #     var: ansible_facts.services\r\n\r\n  - name: Create backup directory on backup server - unique for each host\r\n    ansible.builtin.file:\r\n      path: &quot;{{ backup_location }}\/{{ inventory_hostname }}&quot;\r\n      state: directory\r\n      mode: &#039;0733&#039;\r\n    delegate_to: &quot;{{ backup_storage }}&quot;\r\n\r\n  # - name: Backup groups\r\n  #   ansible.builtin.include_tasks:\r\n  #     file: group-backup.yml\r\n\r\n  - name: Backup Apache when httpd is installed and enabled\r\n    when: item is search(&#039;httpd &#039;) and ansible_facts.services[&#039;httpd.service&#039;].status == &#039;enabled&#039;\r\n    ansible.builtin.include_tasks:\r\n      file: apache-backup.yml \r\n    loop: &quot;{{ rpm_query.stdout_lines }}&quot;<\/code><\/pre>\n<p>In the above, the first task I run uses the RPM command to gather information on all of the installed packages. Generally, I prefer to use a purpose-built module if one exists. In this instance, the ansible.builtin.package_facts module is designed to do this, but I found it didn\u2019t always report correctly for Centos7 servers, so I went with the RPM command as it always works. This list of apps will be used towards the bottom.<\/p>\n<p>Next, I create a directory for each host on a backup server. This will be the repository for all of my configs and data backed up from the old server.<\/p>\n<p>The last task is where the real work happens. I loop over the list of the installed packages on the server and check if one is the Apache service and if it is enabled. If those conditions are met, it will pull in the apache-backup.yml task file. This task file is something I created to backup things from my environment. If I had FTP services on some of my servers, I would also need an ftp-backup task file and an additional matching task, just like the apache-backup file.<\/p>\n<p>The <strong>apache-backup.yml<\/strong> file is actually fairly simple:<\/p>\n<pre class=\"gs-code\"><code class=\"language-plaintext\"># Task file for backing up apache\r\n\r\n# Backup apache config files\r\n- name: Create an archive of the config files\r\n  community.general.archive:\r\n    path: \/etc\/httpd\/con*\r\n    dest: &quot;\/tmp\/{{ inventory_hostname }}-httpd.tgz&quot;\r\n\r\n- name: Copy apache config files to ansible server\r\n  ansible.builtin.fetch:\r\n    src: &quot;\/tmp\/{{ inventory_hostname }}-httpd.tgz&quot;\r\n    dest: &quot;\/tmp\/{{ inventory_hostname }}-httpd.tgz&quot;\r\n    flat: true # Changes default fetch so it will save directly in destination\r\n\r\n- name: Copy config archive to backup server from local ansible server\r\n  ansible.builtin.copy:\r\n    src: &quot;\/tmp\/{{ inventory_hostname }}-httpd.tgz&quot;\r\n    dest: &quot;{{ backup_location }}\/{{ inventory_hostname }}\/{{ inventory_hostname }}-httpd.tgz&quot;\r\n  delegate_to: &quot;{{ backup_storage }}&quot;\r\n\r\n# Backup apache data files\r\n- name: Create an archive of the data directories\r\n  community.general.archive:\r\n    path: \/var\/www\r\n    dest: &quot;\/tmp\/{{ inventory_hostname }}-httpd-data.tgz&quot;\r\n\r\n- name: Copy apache data files to ansible server\r\n  ansible.builtin.fetch:\r\n    src: &quot;\/tmp\/{{ inventory_hostname }}-httpd-data.tgz&quot;\r\n    dest: &quot;\/tmp\/{{ inventory_hostname }}-httpd-data.tgz&quot;\r\n    flat: true # Changes default fetch so it will save directly in destination\r\n\r\n- name: Copy data archive to backup server from local ansible server\r\n  ansible.builtin.copy:\r\n    src: &quot;\/tmp\/{{ inventory_hostname }}-httpd-data.tgz&quot;\r\n    dest: &quot;{{ backup_location }}\/{{ inventory_hostname }}\/{{ inventory_hostname }}-httpd-data.tgz&quot;\r\n  delegate_to: &quot;{{ backup_storage }}&quot;<\/code><\/pre>\n<p>Taking a look at the above task file, you can see that it first creates an archive of the Apache configuration files. Really, it\u2019s more or less a zip file.<\/p>\n<p>It pulls the archive off the server, then pushes it over to a backup server.<\/p>\n<p>It then repeats these actions for the data directories.<\/p>\n<p>The next playbook is called provision-new-server.yml. I\u2019ll leave you to look at it if you like, but it:<br \/>\nConnects to vcenter and provisions a new server<br \/>\nWaits for the server to pull an IP address<br \/>\nAdds the new host to the inventory via the Ascender API<\/p>\n<p>Now that the old server is backed up and the new server has been provisioned, it\u2019s time to restore some services on the new. This is done with the <strong>restore.yml<\/strong> playbook:<\/p>\n<pre class=\"gs-code\"><code class=\"language-plaintext\">---\r\n- name: Playbook to restore configs on new servers\r\n  hosts: migration-hosts \r\n  gather_facts: false\r\n  vars:\r\n    # The host to store backup info to\r\n    backup_storage: backup-storage\r\n\r\n    # The location on the backup host to store info\r\n    backup_location: \/tmp\/migration\r\n\r\n  tasks:\r\n  - name: Set the restore server variables\r\n    ansible.builtin.set_fact:\r\n      restore_server: &quot;new-{{ inventory_hostname }}&quot;\r\n\r\n  # - name: Debug restore_server\r\n  #   ansible.builtin.debug:\r\n  #     var: restore_server\r\n\r\n  # grab a list of the files on the backup server for this host\r\n  - name: Find all files in hosts&#039; backup directories\r\n    ansible.builtin.find:\r\n      paths: &quot;{{ backup_location }}\/{{ inventory_hostname }}&quot;\r\n#      recurse: yes\r\n    delegate_to: &quot;{{ backup_storage }}&quot;\r\n    register: config_files\r\n\r\n  # - name: Debug config_files\r\n  #   when: item.path is search(inventory_hostname + &#039;-httpd.tgz&#039;)\r\n  #   ansible.builtin.debug:\r\n  #     var: config_files\r\n  #   loop: &quot;{{ config_files.files }}&quot;\r\n\r\n  # for each task type, loop through backup files and see if they exist - call restore task file\r\n  - name: If apache is installed, call install task file\r\n    when: item.path is search(inventory_hostname + &#039;-httpd.tgz&#039;)\r\n    ansible.builtin.include_tasks: \r\n      file: apache-restore.yml\r\n    loop: &quot;{{ config_files.files }}&quot;<\/code><\/pre>\n<p>The first task in the above sets a restore_server variable to the name of the new server. My playbooks I named the new server \u201cnew-{{ inventory_hostname }}\u201d. This means it\u2019s the name of the old server with \u201cnew-\u201d on the front\u2026 not overly complex, but it does the trick.<\/p>\n<p>The second task will search the backup folder\u2019s directory and find all files that have been backed up for each host.<\/p>\n<p>Somewhat similar to the backup procedure, the last task in the restore procedure is to loop over the files from the backup server, then calling task files for the various applications\/packages.  In this case, I\u2019m looking for the Apache backup files, and when found, running the <strong>apache-restore.yml<\/strong> task file.<\/p>\n<p>Next is to examine the <strong>apache-restore.yml<\/strong> file:<\/p>\n<pre class=\"gs-code\"><code class=\"language-plaintext\"># Task file for installing and configuring apache\r\n\r\n# - name: Debug restore_server\r\n#   ansible.builtin.debug:\r\n#     var: restore_server\r\n\r\n# Install apache\r\n- name: Install apache\r\n  ansible.builtin.dnf:\r\n    name: httpd\r\n    state: latest\r\n  delegate_to: &quot;{{ restore_server }}&quot;\r\n\r\n- name: Copy apache config files to ansible server\r\n  ansible.builtin.fetch:\r\n    src: &quot;{{ backup_location }}\/{{ inventory_hostname }}\/{{ inventory_hostname }}-httpd.tgz&quot;\r\n    dest: &quot;\/tmp\/{{ inventory_hostname }}-httpd.tgz&quot;\r\n    flat: true # Changes default fetch so it will save directly in destination\r\n  delegate_to: &quot;{{ backup_storage }}&quot;\r\n\r\n- name: Copy config archive to new server from local ansible server\r\n  ansible.builtin.copy:\r\n    src: &quot;\/tmp\/{{ inventory_hostname }}-httpd.tgz&quot;\r\n    dest: &quot;\/tmp\/{{ inventory_hostname }}-httpd.tgz&quot;\r\n  delegate_to: &quot;{{ restore_server }}&quot;\r\n\r\n- name: Extract config archive\r\n  ansible.builtin.unarchive:\r\n    src: &quot;\/tmp\/{{ inventory_hostname }}-httpd.tgz&quot;\r\n    dest: \/etc\/httpd\r\n    remote_src: true\r\n  delegate_to: &quot;{{ restore_server }}&quot;\r\n\r\n- name: Copy apache data files to ansible server\r\n  ansible.builtin.fetch:\r\n    src: &quot;{{ backup_location }}\/{{ inventory_hostname }}\/{{ inventory_hostname }}-httpd-data.tgz&quot;\r\n    dest: &quot;\/tmp\/{{ inventory_hostname }}-httpd-data.tgz&quot;\r\n    flat: true # Changes default fetch so it will save directly in destination\r\n  delegate_to: &quot;{{ backup_storage }}&quot;\r\n\r\n- name: Copy data archive to new server from local ansible server\r\n  ansible.builtin.copy:\r\n    src: &quot;\/tmp\/{{ inventory_hostname }}-httpd-data.tgz&quot;\r\n    dest: &quot;\/tmp\/{{ inventory_hostname }}-httpd-data.tgz&quot;\r\n  delegate_to: &quot;{{ restore_server }}&quot;\r\n\r\n- name: Extract config archive\r\n  ansible.builtin.unarchive:\r\n    src: &quot;\/tmp\/{{ inventory_hostname }}-httpd-data.tgz&quot;\r\n    dest: \/var\/www\r\n    remote_src: true\r\n  delegate_to: &quot;{{ restore_server }}&quot;\r\n\r\n- name: Start service httpd and enable it on boot\r\n  ansible.builtin.service:\r\n    name: httpd\r\n    state: started\r\n    enabled: yes\r\n  delegate_to: &quot;{{ restore_server }}&quot;<\/code><\/pre>\n<p>The above is quite simple. First things first, I install Apache. Next I connect to the backup server, copy the archive config files over, and extract them. I then do the same thing for the data files. Last, I start and enable the Apache service.<\/p>\n<p>After this, I run the <strong>suspend-old.yml<\/strong> playbook to pause the old VM.<\/p>\n<p>Very last, I\u2019ll run my testing playbooks that are designed for each app.<\/p>\n<h2>Ascender Configuration<\/h2>\n<p>I\u2019ve covered adding inventories, projects, and job templates in <a href=\"https:\/\/ciq.com\/blog\/updating-rocky-linux-with-ascender-on-mountain\/\">other blog posts<\/a>.<\/p>\n<p>I will show the workflow template I created to tie all of the job templates together, though:<br \/>\n<a href=\"https:\/\/gregsowell.com\/wp-content\/uploads\/2024\/01\/Screenshot_1.jpg\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/gregsowell.com\/wp-content\/uploads\/2024\/01\/Screenshot_1-300x34.jpg\" alt=\"\" width=\"300\" height=\"34\" class=\"aligncenter size-medium wp-image-7645\" srcset=\"https:\/\/gregsowell.com\/wp-content\/uploads\/2024\/01\/Screenshot_1-300x34.jpg 300w, https:\/\/gregsowell.com\/wp-content\/uploads\/2024\/01\/Screenshot_1-768x88.jpg 768w, https:\/\/gregsowell.com\/wp-content\/uploads\/2024\/01\/Screenshot_1-1024x117.jpg 1024w, https:\/\/gregsowell.com\/wp-content\/uploads\/2024\/01\/Screenshot_1.jpg 1893w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><\/a><br \/>\nA workflow allows me to take playbooks of all sorts and string them together with branching on success or on failure logic. It also allows me to make my playbooks flexible and reusable.<\/p>\n<h2>Conclusion<\/h2>\n<p>Migrating infrastructure is often complex and time consuming, and while we can\u2019t get more hours or employees to complete the task, we can employ our secret weapon, automation.  <\/p>\n<p>CIQ is ready to help you not only standup Ascender in your environment, but to also o experts at helping you migrate your infrastructure.  We have tools to assist and at the end you have the automations for your environment ready for continued and future use!<\/p>\n<p>As always, thanks for reading and I appreciate your feedback; happy migrating!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Migrating from one Linux major version to another never seems to be a simple task, but through the magic of automation, it can be\u2026<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[49,14,13,40],"tags":[],"class_list":["post-7640","post","type-post","status-publish","format-standard","hentry","category-ansible","category-linux","category-server","category-vmware"],"_links":{"self":[{"href":"https:\/\/gregsowell.com\/index.php?rest_route=\/wp\/v2\/posts\/7640","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/gregsowell.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/gregsowell.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/gregsowell.com\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/gregsowell.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=7640"}],"version-history":[{"count":4,"href":"https:\/\/gregsowell.com\/index.php?rest_route=\/wp\/v2\/posts\/7640\/revisions"}],"predecessor-version":[{"id":7646,"href":"https:\/\/gregsowell.com\/index.php?rest_route=\/wp\/v2\/posts\/7640\/revisions\/7646"}],"wp:attachment":[{"href":"https:\/\/gregsowell.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=7640"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/gregsowell.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=7640"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/gregsowell.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=7640"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}