天天看點

使用shell,python,go來實作ansible的自定義子產品

作者:iefc
  • 一、自定義子產品運作原理
  • 二、自定義子產品實戰
  • 2.1 shell方式
  • 2.2 python方式
  • 2.3 golang方式
  • 三、測試驗證
  • 3.1 shell方式驗證
  • 3.2 python方式驗證
  • 3.3 golang方式驗證

ansible已經提供了非常多的子產品,涵蓋了系統、網絡、資料庫、容器、以及其他的方方面面的領域,幾乎可以不用重複造輪子,隻要你能想的到的,官方基本上都已經提供了,可以說極大的提高了我們的自動化效率,但是總會有些情況無法滿足我們的需求,是以我們需要學會如何去編寫一個自定義的子產品

在下文的介紹中,會介紹下自定義子產品的工作原理,分别以shell, python, golang等來實作自定義子產品,通過這三種寫法來實作修改主機的hosts檔案的功能。

一、自定義子產品運作原理

首先我們可以通過修改配置檔案來設定自定義子產品的位置,預設配置檔案位置:/etc/ansible/ansible.cfg,例如如下配置:

[defaults]

# some basic default values...
library        = /opt/workspace/ansible/library  # 此目錄可以随意設定
           

這裡我通過修改ansible的配置檔案,來配置我們存放自定義子產品的目錄,也就是說我們所編寫的自定義子產品,可以存放至此目錄中,在使用自定義子產品時就會從此目錄進行拷貝,注意是拷貝。

下面我們在/opt/workspace/ansible/library這個目錄中編寫一個shell腳本,看看ansible在運作時做了什麼?

#!/bin/bash

echo "hello world"
           

儲存為檔案:test_mod.sh,此時我們的子產品名稱就叫做:test_mod,接下來我們以ad-hoc的方式運作下這個子產品,看看會發生什麼?

# ansible localhost -m test_mod -a "name=tom age=18"
localhost | FAILED! => {
    "changed": false, 
    "module_stderr": "", 
    "module_stdout": "hello world\n", 
    "msg": "MODULE FAILURE\nSee stdout/stderr for the exact error", 
    "rc": 0
}
           

可以看到,我們在通過ad-hoc執行子產品時,傳入了兩個參數:name和age,這兩個參數是我随意設定的,執行之後ansible輸出了一段結果,看格式應該是一個json,其中module_stdout輸出了我們腳本裡的指令輸出,msg卻輸出了一段内容:MODULE FAILURE\nSee stdout/stderr for the exact error,關于為什麼會輸出這個錯誤,這裡先不說明,到後面大家就會明白。

接下來,我們開啟debug來看看會發生什麼?

使用shell,python,go來實作ansible的自定義子產品

總結大概經曆了這麼幾個步驟:

  1. 在被控端建立臨時目錄,用于存放自定義子產品、以及傳入的參數
  2. 将自定義子產品拷貝到被控端
  3. 将傳入的參數拷貝到被控端
  4. 給被控端的自定義子產品和傳入的參數設定可執行權限
  5. 以傳慘的方式在被控端執行自定義子產品
  6. 删除臨時目錄

看到這裡你應該就大概明白了原來ansible其實就把我們寫的腳本拷貝到了目标機器上執行而已。

回到上面那個報錯:MODULE FAILURE\nSee stdout/stderr for the exact error,這個報錯實際上是因為我們沒有輸出内容到标準輸出或标準錯誤,接下來我們改下test_mod.sh這個腳本,再來執行下試試

#!/bin/bash

_stdout() {
    local changed=$1
    local failed=$2
    local rc=$3
    local msg=$4
    cat <<EOF
    {
        "changed": ${changed},
        "failed": ${failed},
        "rc": ${rc},
        "msg": "${msg}"
    }
EOF
}

_stdout true false 0 "sucess"

           

執行就不會有任何錯誤資訊了

# ansible localhost -m test_mod -a "name=tom age=18"
localhost | CHANGED => {
    "changed": true, 
    "msg": "sucess", 
    "rc": 0
}
           

總結下如何自定義一個子產品:

  1. 編寫代碼邏輯實作我們所需的功能
  2. 代碼的執行結果必須輸出一個json,其中字段需要包括:changed, failed, rc, msg等字段,當然不是必須的。
  3. 編寫完成後,檔案名就是我們的子產品名稱
  4. 使用子產品時,設定的參數會以json字元串的方式傳給對應的子產品,也就是說代碼在去執行時,需要解析json内容,來擷取所傳入的參數内容

二、自定義子產品實戰

需求:修改本地hosts檔案,來添加自定義的解析,要求傳入兩個參數,分别為host和domain表示要設定的ip位址和主機名稱

2.1 shell方式

  • 子產品(檔案)名稱:set_hosts_by_shell
#!/bin/bash
# 導入變量檔案
source $1
 
# 定義一個全局的輸出格式
_stdout() {
    local changed=$1
    local failed=$2
    local rc=$3
    local msg=$4
    cat << EOF
    {
        "changed": ${changed},
        "failed": ${failed},
        "rc": ${rc},
        "msg": "${msg}"
    }
EOF
}
 
# 檢查傳參,沒有就報異常
_check_args() {
   if [[ x"$host" == x ]];then
       _stdout false true 1 "Missing args host"
       exit 1
   fi
   if [[ x"$domain" == x ]];then
       _stdout false true 1 "Missing args domain"
       exit 1
   fi
}
 
# 檢查是否已經存在行,并給出傳回碼
_check_line() {
    grep -Eo "$host\s+$domain" /etc/hosts >/dev/null
    return $?
}
 
# 開始添加行
add_line() {
    _check_args
    _check_line
    res=$?
    # 為了幂等,已存在的行不再添加
    if [[ $res -eq 1 ]];then
        echo "$host $domain" >> /etc/hosts
        _stdout true false 0 "Add $host $domain"
    elif [[ $res -eq 0 ]];then
        _stdout false false 0 "$host $domain existing!"
    fi
}
 
# 執行函數
add_line
           

在腳本的開頭我執行了一個source $1,這個是因為執行腳本傳參的時候,在shell中會以類似kv的方式傳入,也就是說内容類似于:

domain=www.baidu.com host=1.1.1.1 a=xxxx c=adsasda
           

是以當我執行source的時候,就會把這些參數注冊到環境變量中。

2.2 python方式

  • 子產品名稱:set_hosts_by_python
#!/usr/bin/env python


from __future__ import absolute_import, division, print_function
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_native


def add_lines(module, host, domain):
    option_file = "/etc/hosts"
    option_content = host + " " + domain
    with open(option_file, "r+") as f:
        contents = f.read()

        if option_content in contents:
            module.exit_json(changed=False, stdout="%s 已存在,無需添加" % option_content)
        else:
            f.write("%s\n" % option_content)
            module.exit_json(changed=True, stdout="%s 已添加" % option_content)
    return


def main():
    module = AnsibleModule(
        argument_spec=dict(
            host=dict(type="str", required=True),
            domain=dict(type="str", required=True),
        )
    )
    host = module.params["host"]
    domain = module.params["domain"]

    try:
        add_lines(module, host, domain)
    except Exception as e:
        module.fail_json(msg="Exception error: %s" % to_native(e))


if __name__ == "__main__":
    main()
           

通過python來實作時,ansible提供了已經封裝好的類AnsibleModule,通過執行個體化這個類,可以将所需的參數傳入進去,同時輸出執行結果時也提供了module.exit_json和module.fail_json的方法

2.3 golang方式

  • 子產品名稱:set_hosts_by_go
package main

import (
 "bufio"
 "bytes"
 "encoding/json"
 "fmt"
 "io"
 "io/ioutil"
 "os"
 "strings"
)

type ModuleResult struct {
 Changed bool   `json:"changed"`
 Fail    bool   `json:"fail"`
 Msg     string `json:"msg"`
 RC      int    `json:"rc"`
}

// 定義要傳入的參數
type AllArgs struct {
 Domain string `json:"domain"`
 Host   string `json:"host"`
}

func addLines(host, domain string) (string, error) {
 filename := "/etc/hosts"
 content := host + " " + domain
 data, err := ioutil.ReadFile(filename)
 if err != nil {
  return "", err
 }
 isExist := false

 scanner := bufio.NewScanner(bytes.NewReader(data))

 for scanner.Scan() {
  line := scanner.Text()
  if strings.Contains(line, content) {
   isExist = true
   return "已存在,無需添加", nil
  }
 }
 if !isExist {
  f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0644)
  if err != nil {
   return "未知錯誤", err
  }
  defer f.Close()
  if _, err := f.WriteString(content + "\n"); err != nil {
   return "write error", err
  }
 }
 return "write sucess", nil
}

func outPut(module ModuleResult, status int) {
 module.RC = status
 var out io.Writer
 if status == 0 {
  out = os.Stdout
 } else {
  out = os.Stderr
 }
 contents, _ := json.Marshal(module)
 fmt.Fprint(out, string(contents))
 os.Exit(status)
}

func parseArg(module ModuleResult, f string) (args AllArgs) {
 fobj, err := os.Open(f)
 if err != nil {
  module.Changed = false
  module.Fail = true
  module.Msg = ""
  outPut(module, 2)
 }

 defer fobj.Close()

 content, err := ioutil.ReadAll(fobj)

 if err != nil {
  module.Changed = false
  module.Fail = true
  module.Msg = ""
  outPut(module, 2)
 }

 err = json.Unmarshal(content, &args)
 if err != nil {
  module.Changed = false
  module.Fail = true
  module.Msg = ""
  outPut(module, 2)
 }
 return
}

func main() {
 module := ModuleResult{}
 var argfile = os.Args[1]
 var parsearg = parseArg(module, argfile)
 host := parsearg.Host
 domain := parsearg.Domain

 res, err := addLines(host, domain)
 if err != nil {
  module.Changed = false
  module.Fail = true
  module.Msg = "添加失敗 " + err.Error()
  outPut(module, 2)
 }
 module.Changed = true
 module.Fail = false
 module.Msg = res
 outPut(module, 0)
}

           

注意在使用go語言編寫自定義子產品時,不可以使用列印,例如使用fmt.Pringln來列印一些值,這種會造成執行中斷,因為這會誤導ansible,以為該子產品已經執行完成,因為ansible會捕獲标準輸出。

三、測試驗證

下面分别來驗證下分别使用shell, python, golang來實作的自定義子產品是否能完成我們的需求

3.1 shell方式驗證

使用shell,python,go來實作ansible的自定義子產品

3.2 python方式驗證

使用shell,python,go來實作ansible的自定義子產品

3.3 golang方式驗證

在使用golang編寫子產品時,需要先編譯成二進制檔案

使用shell,python,go來實作ansible的自定義子產品

歡迎關注公衆号:feelwow