
{"id":5430,"date":"2025-03-20T14:21:39","date_gmt":"2025-03-20T14:21:39","guid":{"rendered":"https:\/\/blog.gordonbuchan.com\/blog\/?p=5430"},"modified":"2025-03-22T00:02:25","modified_gmt":"2025-03-22T00:02:25","slug":"using-a-large-language-model-llm-to-generate-ansible-playbooks-for-the-management-of-one-or-more-linux-servers","status":"publish","type":"post","link":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/2025\/03\/20\/using-a-large-language-model-llm-to-generate-ansible-playbooks-for-the-management-of-one-or-more-linux-servers\/","title":{"rendered":"Using a large language model (LLM) to generate Ansible playbooks for the management of one or more Linux servers"},"content":{"rendered":"\n<p>In this procedure, we create a Python script that connects to a large language model (LLM) to facilitate the creation of Ansible playbooks using natural language queries. We begin by creating the Ansible deployment environment on a staging server, with a hosts file and inventory.ini file that correspond to a group of servers. We enable passwordless entry from the staging server to the servers, and ensure that visudo is configured for passwordless escalation to root using sudo.<\/p>\n\n\n\n<p>The system does not run the generated playbook; the human operator must review it and then execute it manually.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">A preview of the system in operation<\/h1>\n\n\n\n<p>To use the system, an operator starts the playbook generator at the command line:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\npython3 playbook_generator.py\n<\/pre><\/div>\n\n\n<p>The operator enters a natural language query, such as:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nCLI&gt; install net-tools on node03\n<\/pre><\/div>\n\n\n<p>The system generates a playbook:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n\ud83d\udccc Saving Playbook: playbook_20250320123755.yml\n\u2705 Playbook saved: playbook_20250320123755.yml\nCLI&gt; quit\n<\/pre><\/div>\n\n\n<p>The operator runs the playbook:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n(llmansible_env) root@node01:\/opt\/llmansible# ansible-playbook -i inventory.ini playbooks\/playbook_20250320123755.yml\n\nPLAY &#x5B;Install net-tools on node03] ***\n\nTASK &#x5B;Gathering Facts] *\nok: &#x5B;node03]\n\nTASK &#x5B;Install net-tools] *\nchanged: &#x5B;node03]\n\nPLAY RECAP *\nnode03 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0\n<\/pre><\/div>\n\n\n<h1 class=\"wp-block-heading\">Escalating to root using sudo su<\/h1>\n\n\n\n<p>Enter the following command:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nsudo su\n<\/pre><\/div>\n\n\n<h1 class=\"wp-block-heading\">Configuring the staging server<\/h1>\n\n\n\n<p>On the staging server, enter the following commands:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\napt clean &amp;amp;&amp;amp; update &amp;amp;&amp;amp; sudo apt upgrade -y&amp;lt;br&gt;reboot\n<\/pre><\/div>\n\n\n<p>Enter the following commands:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nsudo su\napt install -y ansible python3-pip tmux python3-venv \\\nopenssh-client pkg-config\n\ncd \/etc\nnotepad hosts\n<\/pre><\/div>\n\n\n<p>Use the nano editor to add the following text to the file. Modify IP address values as appropriate for your setup:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n127.0.1.1 node01\n192.168.56.82 node02\n192.168.56.83 node03\n192.168.56.84 node04\n192.168.56.85 node05\n<\/pre><\/div>\n\n\n<p>Save and exit the file.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Preparing the nodes for automation<\/h1>\n\n\n\n<p>On each node, ensure that the server has a host name, static IP address, and has visudo configured for passwordless escalation to root with sudo su.<\/p>\n\n\n\n<p>For each node:<\/p>\n\n\n\n<p>Enter the following commands:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nsudo su\nvisudo\n<\/pre><\/div>\n\n\n<p>Use the nano editor to add the following text to the bottom of the file (substitute your login name):<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\ndesktop ALL=(ALL:ALL) NOPASSWD:ALL\n<\/pre><\/div>\n\n\n<p>Save and exit the file.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Generating SSH keys<\/h1>\n\n\n\n<p>On the staging server, enter the following commands:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nsudo su\nssh-keygen -t rsa -b 4096 -f ~\/.ssh\/id_rsa -N &quot;&quot;\n<\/pre><\/div>\n\n\n<h1 class=\"wp-block-heading\">Copying the public SSH key to all nodes<\/h1>\n\n\n\n<p>Enter the following commands (substitute login username and server name as appropriate for your installation.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nssh-copy-id -i ~\/.ssh\/id_rsa.pub desktop@node02\nssh-copy-id -i ~\/.ssh\/id_rsa.pub desktop@node03\nssh-copy-id -i ~\/.ssh\/id_rsa.pub desktop@node04\nssh-copy-id -i ~\/.ssh\/id_rsa.pub desktop@node05\n<\/pre><\/div>\n\n\n<h1 class=\"wp-block-heading\">Verifying that passwordless login via SSH is working<\/h1>\n\n\n\n<p>Enter the following commands (modify login user name and host name as appropriate to match your installation):<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nssh desktop@node02\nexit\n<\/pre><\/div>\n\n\n<h1 class=\"wp-block-heading\">Creating the inventory.ini file<\/h1>\n\n\n\n<p>Enter the following commands:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\ncd \/opt\nmkdir llmansible\ncd llmansible\nnano inventory.ini\n<\/pre><\/div>\n\n\n<p>Use the nano editor to add the following text to the file:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n&#x5B;all]\nnode02 ansible_host=192.168.56.82 ansible_user=desktop\nnode03 ansible_host=192.168.56.83 ansible_user=desktop\nnode04 ansible_host=192.168.56.84 ansible_user=desktop\nnode05 ansible_host=192.168.56.85 ansible_user=desktop\n&#x5B;all:vars]\nansible_ssh_private_key_file=~\/.ssh\/id_rsa\nansible_python_interpreter=\/usr\/bin\/python3\n<\/pre><\/div>\n\n\n<p>Save and exit the file.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Creating the config.ini file<\/h1>\n\n\n\n<p>Enter the following command:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nnano config.ini\n<\/pre><\/div>\n\n\n<p>Use the nano editor to add the following text to the file (modify values as appropriate to match your installation):<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n&#x5B;LLM]\nsystem_prompt = You are an AI that generates structured Ansible playbooks. Ensure: The playbook is idempotent. It installs required packages using apt (for Ubuntu) or yum (for CentOS). It does not execute shell commands directly. It follows proper YAML formatting. assume that OS is ubuntu unless otherwise stated. only respond with the contents of the ansible playbook, nothing more. do not offer multiple playbooks. do not add commentary.\napi_url = https:\/\/api.lemonfox.ai\/v1\/chat\/completions\napi_token = your-api-token\nmodel_name = llama-8b-chat\n<\/pre><\/div>\n\n\n<p>Save and exit the file.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Creating a Python virtual environment (venv), and adding Python dependencies<\/h1>\n\n\n\n<p>Enter the following commands:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\ncd \/opt\/llmansible\npython3 -m venv llmansible_env\nsource llmansible_env\/bin\/activate\n<\/pre><\/div>\n\n\n<h1 class=\"wp-block-heading\">Adding Python dependencies using pip<\/h1>\n\n\n\n<p>Enter the following command:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\npip install PyYAML requests\n<\/pre><\/div>\n\n\n<h1 class=\"wp-block-heading\">Creating the playbook_generator.py file<\/h1>\n\n\n\n<p>Enter the following command:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nnano playbook_generator.py\n<\/pre><\/div>\n\n\n<p>Use the nano editor to add the following text to the file:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n# MIT license Gordon Buchan 2025\n# see https:\/\/opensource.org\/license\/mit\n# some of the code was generated with the assistance of AI tools.\n\nimport requests\nimport configparser\nimport os\nimport re\nimport yaml\nfrom datetime import datetime\n\n# Load Configuration\nconfig = configparser.ConfigParser()\nconfig.read(&quot;config.ini&quot;)\n\nLLM_API_URL = config.get(&quot;LLM&quot;, &quot;api_url&quot;)\nLLM_API_TOKEN = config.get(&quot;LLM&quot;, &quot;api_token&quot;)\nLLM_MODEL = config.get(&quot;LLM&quot;, &quot;model_name&quot;)\nSYSTEM_PROMPT = config.get(&quot;LLM&quot;, &quot;system_prompt&quot;)\n\nPLAYBOOK_DIR = &quot;\/opt\/llmansible\/playbooks&quot;\n\ndef extract_yaml(text):\n    &quot;&quot;&quot;Extracts valid YAML content and removes explanations or malformed sections.&quot;&quot;&quot;\n\n    # Remove Markdown-style code block markers (e.g., ```yaml)\n    text = re.sub(r&quot;```(yaml|yml)?&quot;, &quot;&quot;, text, flags=re.IGNORECASE).strip()\n\n    # Capture the first YAML block (ensuring it&#039;s well-formed)\n    match = re.search(r&quot;(?s)(---\\n.+?)(?=\\n\\S|\\Z)&quot;, text)\n\n    if match:\n        yaml_content = match.group(1).strip()\n\n        # Remove trailing incomplete YAML lines or explanations\n        yaml_content = re.sub(r&quot;\\n\\w+:\\s*\\&quot;?&#x5B;^\\n]*$&quot;, &quot;&quot;, yaml_content).strip()\n\n        # Validate extracted YAML before returning\n        try:\n            yaml.safe_load(yaml_content)  # If this fails, the YAML is invalid\n            return yaml_content\n        except yaml.YAMLError as e:\n            print(f&quot;\u274c YAML Validation Error: {e}&quot;)\n            return &quot;&quot;\n\n    print(&quot;\u274c Error: No valid YAML found.&quot;)\n    return &quot;&quot;\n\ndef query_llm(prompt):\n    &quot;&quot;&quot;Queries the LLM API and extracts a valid Ansible playbook.&quot;&quot;&quot;\n    payload = {\n        &quot;model&quot;: LLM_MODEL,\n        &quot;messages&quot;: &#x5B;\n            {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: SYSTEM_PROMPT},\n            {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: prompt}\n        ],\n        &quot;max_tokens&quot;: 2048\n    }\n\n    headers = {\n        &quot;Authorization&quot;: f&quot;Bearer {LLM_API_TOKEN}&quot;,\n        &quot;Content-Type&quot;: &quot;application\/json&quot;\n    }\n\n    response = requests.post(LLM_API_URL, json=payload, headers=headers)\n\n    print(&quot;\\n\ud83d\udd0d API RAW RESPONSE:\\n&quot;, response.text)\n\n    try:\n        response_json = response.json()\n        llm_response = response_json&#x5B;&quot;choices&quot;]&#x5B;0]&#x5B;&quot;message&quot;]&#x5B;&quot;content&quot;]\n        yaml_content = extract_yaml(llm_response)\n\n        if not yaml_content:\n            print(&quot;\u274c Error: No valid YAML extracted.&quot;)\n            return &quot;&quot;\n\n        print(&quot;\\n\u2705 Extracted YAML Playbook:\\n&quot;, yaml_content)\n        return yaml_content\n\n    except (KeyError, IndexError):\n        print(&quot;\u274c Error: Unexpected API response format.&quot;)\n        return &quot;&quot;\n\ndef save_playbook(machine, command, playbook_content):\n    &quot;&quot;&quot;Saves the extracted YAML playbook if it&#039;s valid.&quot;&quot;&quot;\n    if not playbook_content.strip().startswith(&quot;---&quot;):\n        print(&quot;\u274c Error: Extracted content is not a valid Ansible playbook. Skipping save.&quot;)\n        return None\n\n    timestamp = datetime.now().strftime(&quot;%Y%m%d%H%M%S&quot;)\n    playbook_name = f&quot;playbook_{timestamp}.yml&quot;\n    playbook_path = os.path.join(PLAYBOOK_DIR, playbook_name)\n\n    os.makedirs(PLAYBOOK_DIR, exist_ok=True)\n\n    print(f&quot;\\n\ud83d\udccc Saving Playbook: {playbook_name}&quot;)\n\n    with open(playbook_path, &quot;w&quot;) as f:\n        f.write(playbook_content)\n\n    return playbook_name\n\ndef main():\n    &quot;&quot;&quot;CLI Operator Console.&quot;&quot;&quot;\n    while True:\n        user_input = input(&quot;CLI&gt; &quot;)\n        if user_input.lower() in &#x5B;&quot;exit&quot;, &quot;quit&quot;]:\n            break\n\n        llm_response = query_llm(user_input)\n        if llm_response:\n            playbook_name = save_playbook(&quot;all&quot;, user_input, llm_response)\n            if playbook_name:\n                print(f&quot;\u2705 Playbook saved: {playbook_name}&quot;)\n\nif __name__ == &quot;__main__&quot;:\n    main()\n\n<\/pre><\/div>\n\n\n<p>Save and exit the file.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Creating an Ansible playbook using the playbook generator<\/h1>\n\n\n\n<h2 class=\"wp-block-heading\">Ensuring that you are in the Python venv<\/h2>\n\n\n\n<p>Ensure that you are already in the Python venv. If not, enter the following commands:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\ncd \/opt\/llmansible\nsource llmansible_env\/bin\/activate\n<\/pre><\/div>\n\n\n<p>Enter the following command:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\npython3 playbook_generator.py\n<\/pre><\/div>\n\n\n<p>The operator enters a natural language query, such as:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nCLI&gt; install net-tools on node04\n\ud83d\udd0d API RAW RESPONSE:\n{&quot;id&quot;:&quot;chatcmpl-923543777fb7a294&quot;,&quot;object&quot;:&quot;chat.completion&quot;,&quot;created&quot;:1742474275313,&quot;model&quot;:&quot;llama-8b-chat&quot;,&quot;choices&quot;:&#x5B;{&quot;index&quot;:0,&quot;message&quot;:{&quot;content&quot;:&quot;---\\n- name: Install net-tools on node04\\n hosts: node04\\n become: yes\\n tasks:\\n - name: Install net-tools\\n apt:\\n name: net-tools\\n state: present&quot;,&quot;role&quot;:&quot;assistant&quot;},&quot;finish_reason&quot;:&quot;stop&quot;}],&quot;usage&quot;:{&quot;prompt_tokens&quot;:130,&quot;completion_tokens&quot;:47,&quot;total_tokens&quot;:177}}\n\n\u2705 Extracted YAML Playbook:\n\nname: Install net-tools on node04\nhosts: node04\nbecome: yes\ntasks:\n\nname: Install net-tools\napt:\nname: net-tools\nstate: present\n\n\ud83d\udccc Saving Playbook: playbook_20250320135023.yml\n\u2705 Playbook saved: playbook_20250320135023.yml\nCLI&gt; quit\n<\/pre><\/div>\n\n\n<p>The operator runs the playbook:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n(llmansible_env) root@node01:\/opt\/llmansible# ansible-playbook -i inventory.ini playbooks\/playbook_20250320135023.yml\n\nPLAY &#x5B;Install net-tools on node04] ***\n\nTASK &#x5B;Gathering Facts] *\nok: &#x5B;node04]\n\nTASK &#x5B;Install net-tools] *\nchanged: &#x5B;node04]\n\nPLAY RECAP *\nnode04 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0\n<\/pre><\/div>\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"957\" src=\"https:\/\/blog.gordonbuchan.com\/blog\/wp-content\/uploads\/2025\/03\/image-1024x957.png\" alt=\"\" class=\"wp-image-5450\" srcset=\"https:\/\/blog.gordonbuchan.com\/blog\/wp-content\/uploads\/2025\/03\/image-1024x957.png 1024w, https:\/\/blog.gordonbuchan.com\/blog\/wp-content\/uploads\/2025\/03\/image-300x280.png 300w, https:\/\/blog.gordonbuchan.com\/blog\/wp-content\/uploads\/2025\/03\/image-768x718.png 768w, https:\/\/blog.gordonbuchan.com\/blog\/wp-content\/uploads\/2025\/03\/image.png 1356w\" sizes=\"auto, (max-width: 767px) 89vw, (max-width: 1000px) 54vw, (max-width: 1071px) 543px, 580px\" \/><\/figure>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>In this procedure, we create a Python script that connects to a large language model (LLM) to facilitate the creation of Ansible playbooks using natural language queries. We begin by creating the Ansible deployment environment on a staging server, with a hosts file and inventory.ini file that correspond to a group of servers. We enable &hellip; <\/p>\n<p class=\"link-more\"><a href=\"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/2025\/03\/20\/using-a-large-language-model-llm-to-generate-ansible-playbooks-for-the-management-of-one-or-more-linux-servers\/\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;Using a large language model (LLM) to generate Ansible playbooks for the management of one or more Linux servers&#8221;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[],"class_list":["post-5430","post","type-post","status-publish","format-standard","hentry","category-linux"],"_links":{"self":[{"href":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/wp-json\/wp\/v2\/posts\/5430","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/wp-json\/wp\/v2\/comments?post=5430"}],"version-history":[{"count":39,"href":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/wp-json\/wp\/v2\/posts\/5430\/revisions"}],"predecessor-version":[{"id":5476,"href":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/wp-json\/wp\/v2\/posts\/5430\/revisions\/5476"}],"wp:attachment":[{"href":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/wp-json\/wp\/v2\/media?parent=5430"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/wp-json\/wp\/v2\/categories?post=5430"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.gordonbuchan.com\/blog\/index.php\/wp-json\/wp\/v2\/tags?post=5430"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}