介紹

最近在開發 Ansible Module 時,想要在 Pycharm 中進行 Debug,不然每次去猜測每個變數裡面現在的值真的是太痛苦了…於是就嘗試使用 Pycharm 的 Debug 功能來進行除錯。
因為最近在開發 collection 的 module 所以本篇主要會以 collection 的 module debug 為主。

這篇文章會介紹如何在 Pycharm 中進行 Ansible Module 的 Debug,並且提供一些小技巧來讓你更有效率地進行除錯。

前置作業

  1. 先確保你已經安裝了 Pycharm 和 Ansible。 (這邊建議您使用 venv 或是 conda 來建立虛擬環境,這樣可以避免跟系統的 Python 衝突)
  2. 在 Pycharm 中建立一個新的專案,並且將 Ansible Module 的程式碼放到專案中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 可以使用 ansible-galaxy init 來建立 collection 的基本架構
# 然後放在 collections 底下
.
├── collections
   └── ansible_collections
   └── shannonhung
   └── my_collection
   ├── docs
   ├── meta
      └── runtime.yml
   ├── plugins
      ├── inventory
      ├── modules
         └── my_test.py # 這是我們要 debug 的 module
      └── README.md
   ├── roles
      └── cluster_check
   ├── README.md
   └── galaxy.yml
├── ansible.cfg # Ansible 的設定檔
├── inventory.ini # Ansible 的 inventory 檔案
└── test-playbook.yml # Ansible 的 playbook 檔案

1.1 設定 Conda 環境

這邊建議您使用 conda 來建立虛擬環境,這樣可以避免跟系統的 Python 衝突。

1
2
3
4
5
6
# 建立 conda 環境
conda create -n <conda-env-name> python=3.13
# 進入 conda 環境
conda activate <conda-env-name>
# 安裝 Ansible 使用 conda 的 pip 套件安裝 ansible 如果單純使用 pip 會裝在本地而非 conda 環境
python -m pip install ansible

1.2 檔案內容的建置

那這邊介紹一一下每個檔案放些什麼東西。

ansible.cfg

  • ansible.cfg:Ansible 的設定檔,這邊可以設定一些 Ansible 的參數,例如 inventory 的路徑、module 的路徑等等。
1
2
3
4
5
6
[defaults]
interpreter_python = /Users/<user-name>/anaconda3/envs/<conda-env-name>/bin/python3.13 # 這邊請改成你自己的 python 路徑,才不會跳 warning
host_key_checking = False # 不檢查 host key

collections_path = ./collections # collections 的路徑
inventory = ./inventory.ini # inventory 的路徑

inventory.ini

  • inventory.ini:Ansible 的 inventory 檔案,這邊可以設定要連線的主機。
1
2
[local]
localhost ansible_connection=local
  • test-playbook.yml:Ansible 的 playbook 檔案,這邊可以設定要執行的 playbook。
1
2
3
4
5
6
7
8
9
- name: Test my collection
collections:
- shannonhung.my_collection
hosts: local
tasks:
- name: My Test
my_test:
name: hello world
new: true

test-playbook.py

  • my_test.py:Ansible 的 module 檔案,這邊可以設定要執行的 module。
1
2
3
4
5
6
7
8
9
10
11
12
13
- name: Test my collection
collections:
- shannonhung.my_collection
hosts: localhost
tasks:
- name: My Test
current_states:
name: hello world
new: true
register: testout
- name: dump test output
debug:
msg: '{{ testout }}'

my_test.py

要記得先使用 ansible-galaxy collection build 來建立 collection 的檔案,這樣才會有 shannonhung.my_collection 這個 collection。

  • my_test.py:Ansible 的 module 檔案,這邊可以設定要執行的 module。
1
2
3
4
5
6
7
8
9
10
11
12
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
#!/usr/bin/python

# Copyright: (c) 2018, Terry Jones <terry.jones@example.org>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)

__metaclass__ = type

DOCUMENTATION = r'''
---
module: my_test

short_description: This is my test module

# If this is part of a collection, you need to use semantic versioning,
# i.e. the version is of the form "2.5.0" and not "2.4".
version_added: "1.0.0"

description: This is my longer description explaining my test module.

options:
name:
description: This is the message to send to the test module.
required: true
type: str
new:
description:
- Control to demo if the result of this module is changed or not.
- Parameter description can be a list as well.
required: false
type: bool
# Specify this value according to your collection
# in format of namespace.collection.doc_fragment_name
# extends_documentation_fragment:
# - my_namespace.my_collection.my_doc_fragment_name

author:
- Your Name (@yourGitHubHandle)
'''

EXAMPLES = r'''
# Pass in a message
- name: Test with a message
my_namespace.my_collection.my_test:
name: hello world

# pass in a message and have changed true
- name: Test with a message and changed output
my_namespace.my_collection.my_test:
name: hello world
new: true

# fail the module
- name: Test failure of the module
my_namespace.my_collection.my_test:
name: fail me
'''

RETURN = r'''
# These are examples of possible return values, and in general should use other names for return values.
original_message:
description: The original name param that was passed in.
type: str
returned: always
sample: 'hello world'
message:
description: The output message that the test module generates.
type: str
returned: always
sample: 'goodbye'
'''

from ansible.module_utils.basic import AnsibleModule


def run_module():
# define available arguments/parameters a user can pass to the module
module_args = dict(
name=dict(type='str', required=True),
new=dict(type='bool', required=False, default=False),
# real
desired_missing=dict(type='bool', required=False, default=False),
)

# seed the result dict in the object
# we primarily care about changed and state
# changed is if this module effectively modified the target
# state will include any data that you want your module to pass back
# for consumption, for example, in a subsequent task
result = dict(
changed=False,
original_message='',
message=''
)

# the AnsibleModule object will be our abstraction working with Ansible
# this includes instantiation, a couple of common attr would be the
# args/params passed to the execution, as well as if the module
# supports check mode
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True
)

# if the user is working with this module in only check mode we do not
# want to make any changes to the environment, just return the current
# state with no modifications
if module.check_mode:
module.exit_json(**result)

# manipulate or modify the state as needed (this is going to be the
# part where your module will do what it needs to do)
result['original_message'] = module.params['name']
result['message'] = 'goodbye'

# use whatever logic you need to determine whether or not this module
# made any modifications to your target
if module.params['new']:
result['changed'] = True

# during the execution of the module, if there is an exception or a
# conditional state that effectively causes a failure, run
# AnsibleModule.fail_json() to pass in the message and the result
if module.params['name'] == 'fail me':
module.fail_json(msg='You requested this to fail', **result)

# in the event of a successful module execution, you will want to
# simple AnsibleModule.exit_json(), passing the key/value results
module.exit_json(**result)


def main():
run_module()


if __name__ == '__main__':
main()

基本上到這裡應該要可以執行以下指令:

1
ansible-playbook test-playbook.yml

就會出現以下畫面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
❯ ansible-playbook test-playbook.yml

PLAY [Test my collection] *********************************************************************************************************************************************************************************************************

TASK [Gathering Facts] ************************************************************************************************************************************************************************************************************
ok: [localhost]

TASK [My Test] ********************************************************************************************************************************************************************************************************************
changed: [localhost]

TASK [dump test output] ***********************************************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": {
"changed": true,
"failed": false,
"message": "goodbye",
"original_message": "hello world"
}
}

PLAY RECAP ************************************************************************************************************************************************************************************************************************
localhost : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Step1. Pycharm Debug 設定

在 Pycharm 中,打開 Run/Debug Configurations,然後點擊 +,選擇 Pythoon Debug Server。可以看到上圖畫面,然後設定以下參數:

  1. 選擇建立 Python Debug Server
  2. 然後設定名稱。
  3. 這邊會告訴你等等設定完之後要在本機的 python 環境中下載 pydevd-pycharm 這個套件
  4. 然後設定 port,隨便都可以。
  5. Path Mappings 設定成 <執行專案ansible的root目錄>=<debug server裡面的環境這裡使用tmp>,這邊是我們的 module 的路徑。

都設定好之後,按下 OK,然後會看到上面有一個 Python Debug Server 的選項。

然後準備下載 pydevd-pycharm 這個套件,這邊可以使用 pip 或是 conda 來下載。

1
python -m pip install -n <conda-env-name> pydevd-pycharm

Step2. 在 Ansible Module 中加入 Debug 的程式碼

在 Ansible Module 中加入以下程式碼,可以把這段程式碼放在你想要 debug 的地方,這樣啟動 debug server 的時候才會卡在這裡。

1
2
3
4
5
# 仔入 pydevd_pycharm 的套件
import pydevd_pycharm

# 把下面這行程式碼放在你想要 debug 的地方 這樣啟動 debug server 的時候才會卡在這裡
pydevd_pycharm.settrace('localhost', port=12345, stdoutToServer=True, stderrToServer=True)

Step3. 開啟 Debug Server

然後選擇蟲蟲的圖示,然後選擇 Python Debug Server,然後按下 Debug,這樣就會啟動 debug server。應該會看到以下畫面,就表示啟動 debug server 成功了。

最後都準備就緒了,只要按照正常的流程執行 ansible-playbook test-playbook.yml,然後就會進入 debug 的畫面了。
這樣就可以在 Pycharm 中進行 Ansible Module 的 Debug 了。

Reference