天天看點

探索ansible runner的源碼及執行api原理

前沿:

     ansible的文檔說的不清不楚,文檔一點也不實在,有些範例都做不通,走不通。今天中午吃飯的時候,和同僚 祖天彪(這名夠霸道吧),聊了很長時間ansible在實際項目中碰到的問題,尤其是運維平台頁面上。  下午的時候,找他去玩耍,這哥們正好在看ansible的api源碼,也就是 ansible runner ~    就這樣一塊交流一兩個小時,把runner 這部分的核心代碼給過了一遍。 中文的文檔真的很少, 我這邊就簡單的闡述下,看完後的一些總結。 可能有些地方不對,或者是沒有說明白,還請大家見諒。

1

2

3

4

5

6

7

8

9

10

11

12

<code>import ansible.runner</code>

<code>runner = ansible.runner.Runner(</code>

<code>   </code><code>host_list=</code><code>"/etc/ansible/xiaorui.py"</code>

<code>   </code><code>module_name=</code><code>'command'</code><code>,</code>

<code>   </code><code>module_args=</code><code>'ip a'</code><code>,</code>

<code>   </code><code>pattern=</code><code>'web'</code><code>,                    </code>

<code>)</code>

<code>datastructure = runner.run()</code>

<code>print</code> <code>'\n'</code>

<code>print</code> <code>datastructure</code>

上面是一個簡單的ansible api的執行的例子。 我們可以看到他調用的runner子產品。

runner目錄下面有個__init__.py檔案,__init__.py的作用, 相當于class中的def __init__(self):函數,用來初始化子產品。 把所在目錄當作一個package處理。不懂的去看,python包相關的基礎。 

下面講解下 Runner這個類。

class Runner(object):

    ''' core API interface to ansible '''

    # see bin/ansible for how this is used...

    def __init__(self,

        host_list=C.DEFAULT_HOST_LIST,      這裡不僅可以放 靜态的hosts檔案,也可以放 inventory的腳本,腳本要777權限。

        module_path=None,               這個是ansible的路徑,一般不用寫

        module_name=C.DEFAULT_MODULE_NAME,      子產品的名字,子產品的位置要標明在/usr/share/absible下,不然會識别不到。   要注意下~

        module_args=C.DEFAULT_MODULE_ARGS,  # ex: "src=/tmp/a dest=/tmp/b"   子產品的參數

        forks=C.DEFAULT_FORKS,              # parallelism level   程序的數目,他的邏輯是這樣,你如果填入了20個程序,他會判斷你的list_hosts是否有20個,沒有的話,他就會根據主機的數目來派生程序,如果超過20個,那就用multiprocess程序池pool來排程。mulitiprocess本身有個isalive的東西,來判斷分離出去程序的存活狀态。

        timeout=C.DEFAULT_TIMEOUT,       這個就是逾時的時間

        pattern=C.DEFAULT_PATTERN,          # which hosts?  ex: 'all', 'acme.example.org'   這個是做相關的比對,是關于inventory的比對

        remote_user=C.DEFAULT_REMOTE_USER,  # ex: 'username'  遠端使用者的選擇

        remote_pass=C.DEFAULT_REMOTE_PASS,  # ex: 'password123' or None if using key 遠端密碼的選擇

        remote_port=None,                   # if SSH on different ports   遠端端口的選擇

        private_key_file=C.DEFAULT_PRIVATE_KEY_FILE, # if not using keys/passwords  還可以用指定key

        sudo_pass=C.DEFAULT_SUDO_PASS,      # ex: 'password123' or None   sudo之後的密碼的推送

        background=0,                       # async poll every X seconds, else 0 for non-async  看字眼就知道他是做什麼的了,他非常的像 saltstack的 event, 當派生出了一個任務後,

        産生一個ansible_job_id,然後時不時的去拿資料

        transport=C.DEFAULT_TRANSPORT,      # 'ssh', 'paramiko', 'local'  這裡是選擇你的連結得到方式,預設是用的 paramiko

        conditional='True',                 # run only if this fact expression evals to true     這個是什麼呢?  相當與 puppet saltstack 裡面的require,狀态的判斷。  

        callbacks=None,                     # used for output  回調的輸出

        sudo=False,                         # whether to run sudo or not 是否是sudo

        sudo_user=C.DEFAULT_SUDO_USER,      # ex: 'root'  sudo的時候,用到的使用者名

        ):

再來關注下這個,我和同僚的關注點,不太一樣,他以前經常用salt,他不想寫hosts檔案,或者是inventory檔案,想直接 ansible  ip位址 -m shell "ip a" 類似這樣的使用,而我的想法是,想給inventory傳遞參數,  inventory在被ansbile調用的時候,是不能傳遞參數的, 他後面的那個參數,隻是擷取 json的那個key。 

簡單的說, ansible -i nima.py "bj_nginx" -m shell "dir"  這裡的bj_nginx 是不能傳遞給nima裡面的,而是擷取nima傳回的那片json的。

<code>self.inventory        = utils.</code><code>default</code><code>(inventory, lambda: ansible.inventory.Inventory(host_list))</code>

咱們進到,inventory的目錄

[root@devops-ruifengyun inventory ]$ pwd

/usr/lib/python2.7/dist-packages/ansible/inventory

試圖找下,官網支援不支援針對inventory的傳遞參數,host_list傳遞給了 Inventory類,他的邏輯也很幹練,如果 host_list  如果沒有傳入的話,預設走的是 /etc/ansible/hosts檔案,如果你指定了,他會從你的檔案讀入的,  首先如果是str,根據逗号來區分并且去除空格,放到list裡面。接着是list了,。。。  霹靂拉ye,oh。。

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

<code>      </code><code>if</code> <code>type</code><code>(host_list) </code><code>in</code> <code>[ </code><code>str</code><code>, </code><code>unicode</code> <code>]:</code>

<code>          </code><code>if</code> <code>host_list.find(</code><code>","</code><code>) !</code><code>=</code> <code>-</code><code>1</code><code>:</code>

<code>              </code><code>host_list </code><code>=</code> <code>host_list.split(</code><code>","</code><code>)</code>

<code>              </code><code>host_list </code><code>=</code> <code>[ h </code><code>for</code> <code>h </code><code>in</code> <code>host_list </code><code>if</code> <code>h </code><code>and</code> <code>h.strip() ]</code>

<code>      </code><code>if</code> <code>type</code><code>(host_list) </code><code>=</code><code>=</code> <code>list</code><code>:</code>

<code>          </code><code>self</code><code>.parser </code><code>=</code> <code>None</code>

<code>          </code><code>all</code> <code>=</code> <code>Group(</code><code>'all'</code><code>)</code>

<code>          </code><code>self</code><code>.groups </code><code>=</code> <code>[ </code><code>all</code> <code>]</code>

<code>          </code><code>for</code> <code>x </code><code>in</code> <code>host_list:</code>

<code>              </code><code>if</code> <code>x.find(</code><code>":"</code><code>) !</code><code>=</code> <code>-</code><code>1</code><code>:</code>

<code>                  </code><code>tokens </code><code>=</code> <code>x.split(</code><code>":"</code><code>,</code><code>1</code><code>)</code>

<code>                  </code><code>all</code><code>.add_host(Host(tokens[</code><code>0</code><code>], tokens[</code><code>1</code><code>]))</code>

<code>              </code><code>else</code><code>:</code>

<code>                  </code><code>all</code><code>.add_host(Host(x))</code>

<code>      </code><code>elif</code> <code>os.path.exists(host_list):</code>

<code>          </code><code>if</code> <code>os.path.isdir(host_list):</code>

<code>              </code><code># Ensure basedir is inside the directory</code>

<code>              </code><code>self</code><code>.host_list </code><code>=</code> <code>os.path.join(</code><code>self</code><code>.host_list, "")</code>

<code>              </code><code>self</code><code>.parser </code><code>=</code> <code>InventoryDirectory(filename</code><code>=</code><code>host_list)</code>

<code>              </code><code>self</code><code>.groups </code><code>=</code> <code>self</code><code>.parser.groups.values()</code>

<code>          </code><code>elif</code> <code>utils.is_executable(host_list):</code>

<code>              </code><code>self</code><code>.parser </code><code>=</code> <code>InventoryScript(filename</code><code>=</code><code>host_list)</code>

<code>          </code><code>else</code><code>:</code>

<code>              </code><code>data </code><code>=</code> <code>file</code><code>(host_list).read()</code>

<code>              </code><code>if</code> <code>not</code> <code>data.startswith(</code><code>"---"</code><code>):</code>

<code>                  </code><code>self</code><code>.parser </code><code>=</code> <code>InventoryParser(filename</code><code>=</code><code>host_list)</code>

<code>                  </code><code>self</code><code>.groups </code><code>=</code> <code>self</code><code>.parser.groups.values()</code>

<code>                  </code><code>raise</code> <code>errors.AnsibleError(</code><code>"YAML inventory support is deprecated in 0.6 and removed in 0.7, see the migration script in examples/scripts in the git checkout"</code><code>)</code>

<code>          </code><code>utils.plugins.vars_loader.add_directory(</code><code>self</code><code>.basedir(), with_subdir</code><code>=</code><code>True</code><code>)</code>

<code>      </code><code>else</code><code>:</code>

<code>          </code><code>raise</code> <code>errors.AnsibleError(</code><code>"Unable to find an inventory file, specify one with -i ?"</code><code>)</code>

<code>  </code><code>def</code> <code>_match(</code><code>self</code><code>, </code><code>str</code><code>, pattern_str):</code>

<code>  </code><code>原文:http:</code><code>/</code><code>/</code><code>rfyiamcool.blog.</code><code>51cto</code><code>.com</code><code>/</code><code>1030776</code><code>/</code><code>1420147</code>

最後,咱們的流程跑到這裡。。。

 elif utils.is_executable(host_list):

     self.parser = InventoryScript(filename=host_list)

     self.groups = self.parser.groups.values()

咱們再來看看InventoryScript做了什麼東西。。。。

找到了,  這就是我給 host_list傳遞字元串的時候,會提示沒有該檔案,因為他的subprocess的shell狀态是 False,這樣的話,不能識别 分号和空格。 隻要在subprocess.Popen追加一個shell=True就可以了。

既然ansible runner代碼,基本過了一遍,那麼 inventory 子產品的參數傳遞,很明了了。。。

<code> </code><code>30</code>     <code>def</code> <code>__init__(</code><code>self</code><code>, filename</code><code>=</code><code>C.DEFAULT_HOST_LIST):</code>

<code> </code><code>31</code> 

<code> </code><code>32</code>         <code>self</code><code>.filename </code><code>=</code> <code>filename</code>

<code> </code><code>33</code>         <code>cmd </code><code>=</code> <code>[ </code><code>self</code><code>.filename, </code><code>"--list"</code> <code>]</code>

<code> </code><code>34</code>         <code>try</code><code>:</code>

<code> </code><code>35</code>             <code>sp </code><code>=</code> <code>subprocess.Popen(cmd, stdout</code><code>=</code><code>subprocess.PIPE, stderr</code><code>=</code><code>subprocess.PIPE)</code>

<code> </code><code>36</code>         <code>except</code> <code>OSError, e:</code>

<code> </code><code>37</code>             <code>raise</code> <code>errors.AnsibleError(</code><code>"problem running %s (%s)"</code> <code>%</code> <code>(</code><code>' '</code><code>.join(cmd), e))</code>

<code> </code><code>38</code>         <code>(stdout, stderr) </code><code>=</code> <code>sp.communicate()</code>

<code> </code><code>39</code>         <code>self</code><code>.data </code><code>=</code> <code>stdout</code>

<code> </code><code>40</code>         <code>self</code><code>.groups </code><code>=</code> <code>self</code><code>._parse()</code>

接着,他用調用了他自己類下的 _parse() 函數 ~

39

<code>def</code> <code>_parse(</code><code>self</code><code>):</code>

<code>    </code><code>all_hosts </code><code>=</code> <code>{}</code>

<code>    </code><code>self</code><code>.raw  </code><code>=</code> <code>utils.parse_json(</code><code>self</code><code>.data)</code>

<code>    </code><code>all</code>       <code>=</code> <code>Group(</code><code>'all'</code><code>)</code>

<code>    </code><code>groups    </code><code>=</code> <code>dict</code><code>(</code><code>all</code><code>=</code><code>all</code><code>)</code>

<code>    </code><code>group     </code><code>=</code> <code>None</code>

<code>    </code><code>if</code> <code>'failed'</code> <code>in</code> <code>self</code><code>.raw:</code>

<code>        </code><code>raise</code> <code>errors.AnsibleError(</code><code>"failed to parse executable inventory script results"</code><code>)</code>

<code>    </code><code>for</code> <code>(group_name, data) </code><code>in</code> <code>self</code><code>.raw.items():</code>

<code>        </code><code>group </code><code>=</code> <code>groups[group_name] </code><code>=</code> <code>Group(group_name)</code>

<code>        </code><code>host </code><code>=</code> <code>None</code>

<code>        </code><code>if</code> <code>not</code> <code>isinstance</code><code>(data, </code><code>dict</code><code>):</code>

<code>            </code><code>data </code><code>=</code> <code>{</code><code>'hosts'</code><code>: data}</code>

<code>        </code><code>if</code> <code>'hosts'</code> <code>in</code> <code>data:</code>

<code>            </code><code>for</code> <code>hostname </code><code>in</code> <code>data[</code><code>'hosts'</code><code>]:</code>

<code>                </code><code>if</code> <code>not</code> <code>hostname </code><code>in</code> <code>all_hosts:</code>

<code>                    </code><code>all_hosts[hostname] </code><code>=</code> <code>Host(hostname)</code>

<code>                </code><code>host </code><code>=</code> <code>all_hosts[hostname]</code>

<code>                </code><code>group.add_host(host)</code>

<code>        </code><code>if</code> <code>'vars'</code> <code>in</code> <code>data:</code>

<code>            </code><code>for</code> <code>k, v </code><code>in</code> <code>data[</code><code>'vars'</code><code>].iteritems():</code>

<code>                </code><code>group.set_variable(k, v)</code>

<code>        </code><code>all</code><code>.add_child_group(group)</code>

<code>    </code><code># Separate loop to ensure all groups are defined</code>

<code>        </code><code>if</code> <code>isinstance</code><code>(data, </code><code>dict</code><code>) </code><code>and</code> <code>'children'</code> <code>in</code> <code>data:</code>

<code>            </code><code>for</code> <code>child_name </code><code>in</code> <code>data[</code><code>'children'</code><code>]:</code>

<code>                </code><code>if</code> <code>child_name </code><code>in</code> <code>groups:</code>

<code>                    </code><code>groups[group_name].add_child_group(groups[child_name])</code>

<code>    </code><code>return</code> <code>groups</code>

        上面的代碼已經很好了解了,就是把剛才通過subprocess擷取的資料,用json子產品,json.loads一下。這裡會取出兩個大key,一個是主機類型标簽的hosts 這個key,和vars ,這個vars是給模闆或者其他功能的資料。 

        有很多人會說,這也太搓了,太scha了,居然是通過外部調用擷取的資料,但是我覺得這也是很多人着迷ansible的地方,不管你會不會python,你隻要會任何一個語言,shell、php、java、nodejs、ruby隻要能标準的stdout輸出,那就行了。 

剩下的不需要你再度的介入,因為對于ansible來說,我拿到的資料都是标準的輸出,是以大家不管用怎麼語言,一定要做好異常的處理,讓他print幹幹淨淨的資料,這樣ansible 這個小孩才能完美的加載資料。

希望大家通過這篇文章,對于ansible api更有深度的了解,一些的幫助,如果文章描述的有問題,請即使的通知我,我會即使的更正修改。

 本文轉自 rfyiamcool 51CTO部落格,原文連結:http://blog.51cto.com/rfyiamcool/1420147,如需轉載請自行聯系原作者