نوشتن callback اختصاصی برای ansible

توجه: برای درک بهتر این مطلب نیاز هستش که با ansible آشنایی مختصری داشته باشین.https://d2c.io/post/extending-ansible-plugins-part-1انسیبل(Ansible) یکی از ابزارهایی که کاربردهای زیادی داره و روز به روز هم داره به محبوبیتش اضافه میشه. یکی از دلایلی که، این ابزار رو دوست داشتنی کرده، امکان استفاده از پلاگین های مختلف هستش. پلاگین هایی که از یک طرف روند انجام کار رو برای ما راحت تر و شیرین تر می کنند و از طرف دیگه زمان انجام کار رو سرعت می بخشند. به عنوان مثال اگر خروجی playbook رو به صورت json نیاز داشته باشیم، کافیه تا پلاگین مربوط به اون رو استفاده کنیم(option زیر رو به کامندمون اضافه می کنیم یا اینکه توی تنظیمات فرمت خروجی رو به json تنظیم می کنیم).ANSIBLE_STDOUT_CALLBACK=jsonبا توجه به چیزی که می بینیم انسیبل از callback برای تولید خروجی json استفاده می کنه. خب سوالی که پیش میاد، اینکه آیا میشه callback اختصاصی خودمون رو داشته باشیم و اگر آره چطوری؟ با توجه به عنوان پست، جواب مشخصا اره ست! اما بریم ببینیم که چطوری باید این کار رو انجام بدیم.https://memegenerator.net/img/instances/62753961/im-ready-lets-go.jpgمقدمهبرای شروع باید بدونیم که callbackها خودشون یک نوع plugin هستند. پس باید با قواعد نوشتن پلاگین ها آشنا بشیم. از اونجایی که انسیبل مستندات خوبی داره، درنتیجه برای جواب دادن به این سوال از خود سایت انسیبل کمک می گیریم:‌ به گفته مستندات موجود در سایت برای نوشتن پلاگین باید چهارتا شرط رو رعایت کنیم. این چهار شرط عبارت اند از:be written in Pythonمورد اول به دلیل اینکه PluginLoader بتونه اون رو لود کنه و به عنوان یک object پایتونی برگردونه(حواستون باشه که این پلاگین باید با نسخه های ۲٫۷ یا ۳٫۵ به بالاتر پایتون نوشته شده باشه. چون انسیبل با این نسخه ها سازگاره)raise errorsاگر موقع اجرای پلاگین اروری غیرمنتظره ای رخ داد باید از AnsibleError برای raise کردن اون استفاده کنین. به عنوان مثال :from ansible.module_utils._text import to_native try: cause_an_exception() except Exception as e: raise AnsibleError('Something happened, this was original exception: %s' % to_native(e)) (چون ممکنه با نسخه های مختلف پایتون اجرا بشه به خاطر همین این شکلی نوشته شده)return strings in unicodeهر رشته ای که برگشت داده میشه باید در نوع unicode پایتون باشه تا بشه تو jinja2 استفاده ش کرد(برای این کار میشه از قابلیت خود ansible استفاده کرد):from ansible.module_utils._text import to_text result_string = to_text(result_string)conform to Ansible’s configuration and documentation standardsاگر هم بخوایم که آپشن هایی از پلاگین مون قابلیت کانفیگ کردن رو داشته باشه باید اون هارو تو بخش DOCUMENTATION فایل پلاگین قرار بدیم:DOCUMENTATION = ''' callback: timer callback_type: aggregate requirements: - whitelist in configuration short_description: Adds time to play stats version_added: &quot2.0&quot description: - This callback just adds total play duration to the play stats. options: format_string: description: format of the string shown to user at play end ini: - section: callback_timer key: format_string env: - name: ANSIBLE_CALLBACK_TIMER_FORMAT default: &quotPlaybook run took %s days, %s hours, %s minutes, %s seconds&quot '''پیش نیازهای نوشتن پلاگین رو گفتیم اما بد نیست با انواع پلاگین هایی که توی انسیبل داریم هم آشنا بشیم:Action pluginCache pluginCallback pluginConnection pluginFilter pluginInventory pluginLookup pluginTest pluginVars pluginاز اونجایی که این مطلب تمرکزش روی callback plugin هستش به خاطر همین مستقیم سراغ این مورد میریم.نوشتن Callbackhttps://pbs.twimg.com/media/DV1ynIsWkAEszlV.jpgاین نوع پلاگین ها می تونن، موقعی که به رخدادهای(event) مختلف جواب داده میشه، رفتارهای جدیدی رو به انسیبل اضافه کنن. منظور از رخداد چیه مثلا وقتی playbook یا یک تسک شروع به کار میکنه و چیزهایی از این قبیل.اولین نکته برای نوشتن callback اینکه که باید از CallbackBase ارث بری کنیم. یعنی به این شکل:from ansible.plugins.callback import CallbackBase class CallbackModule(CallbackBase): passتا جایی که من می دونم، اینکه چه رخدادهایی(events) رو در اختیار داریم رو انسیبل مشخص میکنه(یعنی برای هر چیزی نمیشه یا حداقل من اینطوری میدونم اگر اینطوری نیست خوشحال میشم تو کامنت ها به منم اطلاع بدین. البته ناگفته نمونه که انسیبل موارد خوبی و جامعی رو ارائه داده). حالا برای اینکه بتونیم رفتارهای مدنظرمون رو پیاده سازی کنیم باید توی callback خودمون توابع متناسب با اون رخداد رو شخصی سازی(customize) کنیم. لیست توابعی که در اختیار داریم رو می تونیم از فایل __init__.py تو lib/ansible/plugins/callback ببینیم. خوشبختانه اسم شون خیلی گویاست، به طوری که باعث شده موقع فراخوانی هر تابع و اینکه برای چه کاریه کاملا مشخص باشه.نکته: توابعی که با v2 شروع می شوند برای نسخه ۲٫۰ به بعد انسیبل هستند. https://github.com/ansible/ansible/blob/96b74d3e0b340f1bc6b3102d874f17516fe35e79/lib/ansible/plugins/callback/__init__.py مثال عملیفرض کنیم که یک playbook خیلی زمان گیر داریم برای همین اون رو اجرا و به امون خدا ولش می کنیم. حالا میخوایم بعد از تموم شدن اجرا، تسک های fail شده و unreachable برامون ایمیل بشه( خودِ انسیبل برای ایمیل یک پلاگین آماده داره ولی خب از اونجایی که این سناریو باعث درک بهتر قضیه میشه از همین موضوع استفاده می کنیم. هدف بیشتر اینکه با فرایند آشنا بشیم و بفهمیم چه کارهایی میشه انجام داد و ایده بگیریم).برای اینکه در زمان صرفه جویی بشه کد رو میزاریم و بعد میریم سراغ اینکه ببینیم هر بخش چیکار میکنه(البته بخش اصلی تا حد خیلی خوبی مشخصه).# These imports are defined for every callback plugin I've seen so far. # If you don't import `absolute_import` standard library modules may be # overriden by Ansible python modules with the same name. For example: I use the # standard library `json` module, but Ansible has a callback plugin with the # same name. When I excluded `absolute_import` and imported `json` the json # module I got was Ansible's json module and not the standard library one. # I'm not sure why `division` and `print_function` need to be imported. from __future__ import (absolute_import, division, print_function) import time from ansible.plugins.callback import CallbackBase __metaclass__ = type # Ansible documentation of the module. I'm also not sure why this is required, # but other plugins add documentation so it seems to be a standard. DOCUMENTATION = ''' callback: failed_tasks ''' class CallbackModule(CallbackBase): CALLBACK_VERSION = 2.0 CALLBACK_NAME = 'failed_tasks' CALLBACK_NEEDS_WHITELIST = True def __init__(self): super(CallbackModule, self).__init__() self.playbook_info = dict() self.playbook_info['duration'] = dict() self.failed_tasks = list() self.unreachable_tasks = list() def v2_playbook_on_play_start(self, play): self.playbook_info['name']: play.get_name(), self.playbook_info['id']: to_text(play._uuid), self.playbook_info['duration']['start'] = time.time() def v2_runner_on_failed(self, task_result, ignore_errors=False): self.failed_tasks.append(self._get_task_detail(task_result)) def v2_runner_on_unreachable(self, task_result): self.unreachable_tasks.append(self._get_task_detail(task_result)) def _get_task_detail(self, task_result): return { &quothost&quot: task_result._host.get_name(), &quotresult&quot: task_result._result } def playbook_on_stats(self, task_result, ignore_errors=False): self.playbook_info['duration']['end'] = time.time() self.send_email() def send_email(self): # do email stuffs.از اونجایی که ما با نسخه ۲٫۰ به بعد انسیبل کار می کنیم به خاطر همین فقط از این رفتیم سراغ توابعی که با v2 شروع می شوند.کلا تو این بخش از کد بهترین چیزی که میتونه به ما کمک کنه تا اینکه بفهمیم توی هر تابع(به کمک متغیرهای ورودی) به چه چیزهایی دسترسی داریم، دیباگ کردنه. به شخصِ از pdb و dir تو این مورد خیلی کمک گرفتم. به این نکته حواستون باشه.https://www.hackadda.com/media/blog/abhi/2020/02/07/debugging-meme.jpgتابع v2_playbook_on_play_start:این تابع موقع شروعِ اجراء ِ playbook فراخوانی میشه و آرگومان play رو هم به عنوان ورودی می گیره، که به کمک این آرگومان می تونیم به موارد مرتبط با playbook بدست پیدا کنیم. مواردی مثل vars و ... .تو کد ما، این تابع اسم playbook، به همراه id و زمان شروع به کار رو تنظیم کرده. نکته: تو این تابع برای دسترسی به موارد مختلف playbook و متعیرهاش از get_variable_manager زیاد استفاده می کنیم. یعنی:self.manager = play.get_variable_manager() vars = self.manager. .... تابع v2_runner_on_failed:وقتی که رانر یک تسک رو اجرا کنه و اجرای اون با خطا مواجه بشه این تابع فراخوانی میشه(بله میدونم که از اسمش مشخصه ولی باید توضیح داده بشه). ما هم اسم تسک و علت اینکه خطا رخ داده رو ذخیره می کنیم.تابع v2_runner_on_unreachable:این تابع هم وقتی که یک تسک unreachable باشه فراخوانی میشه(به جان عمه م میدونم اسمش مشخصه ول کن!). مثل تابع fail اطلاعات تسک رو می گیریم تا موقع ارسال ایمیل این اطلاعات رو داشته باشیم.طبیعتا اگر تسکی fail یا unreachable نشود، این دوتا هم تابع فراخوانی نمی شوند.تابع playbook_on_stats:(خب شما که کچل مون کرده بودی از اسمش مشخصه، از اسمش مشخصه، بگو ببینیم این کی فراخوانی میشه و به چه کاری میاد؟ نه بگو دیگه! چرا در و دیوار رو نیگا میکنی D:)این تابع بعد از همه فراخوانی میشه. به خاطر همین ما هم اومدیم زمان تموم شدن اجرا رو تنظیم کردیم و بعد هم گفتیم برو کارهای مرتبط با ارسال ایمیل رو شروع کن(از اونجایی که هدف نحوه ارسال ایمیل نیست کدش رو ننوشتیم).اما دوتا تابع وجود داره که توی کد ما نیستند ولی ممکنه زیاد استفاده بشوند به خاطر همین به اونا هم یک اشاره ی کوتاهی می کنیم.تابع playbook_on_task_start، طبیعتا موقع شروع هر تسک فراخوانی می شود. آرگومان های ورودیش هم به ترتیب task و بعدش is_conditional هستند.تابع v2_runner_on_start که از نسخه ۲٫۸ به بعد انسیبل اومد و موقعی که یک تسک تو یک host اجرا میشه فراخوانی میشه. یعنی اگر قرار باشه یک هاست ۴ تا تسک رو اجرا کنه، ۴ بار فراخوانی میشه. آرگومان های ورودیش هم به ترتیب host و پشت سر اون task هستش.کلی تابع دیگه هم هست. حتما یک نگاه به اون لینک بالایی بندازین.نکاتدرمورد callback ها چندتا نکته وجود داره که گفتنشون می تونه کمک کننده باشه و ما رو از ساعت ها دیباگ کردن و حرص خوردن(مخصوصا اینکه در ظاهر همه چی درست باشه ولی بازم کار نکنه D:) نجات بده.https://www.hackadda.com/media/blog/abhi/2020/02/07/when-you-finally-find-the-bug.jpgبرای stdout همزمان فقط یک دونه callback میشه تنظیم کرد. برای همین انسیبل متغیر CALLBACK_TYPE رو گذاشته تا این نوع callback رو از بقیه callback هارو تشخیص بده.متغیر های CALLBACK_NAME و CALLBACK_VERSION ضروری هستند و حتما باید باشند.اگر از callback_whitelist استفاده بشه، انسیبل به دنبال فایلی هم نام با چیزی که جلوی callback_whitelist نوشته شده می گرده و به مقدار متغیر CALLBACK_NAME داخل کد callback نگاه نمیکنه! یعنی اسم فایل هم تو این مورد مهم هستش.برای گرفتن اطلاعات از playbook یک همچین کارهایی رو می تونین بنویسین: manager = playbook.get_variable_manager() vars = manager.get_vars() host_vars = vars['hostvars']در نهایت اینکه تو دفعات اول و اینکه به چیزهایی دسترسی دارین، باید دیباگ کنید. پس با pdb و دیباگرها دوست باشین!همین. لبخند بزنین :)منابع:https://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.htmlhttps://docs.ansible.com/ansible/devel/plugins/callback.htmlhttps://dev.to/jrop/creating-an-alerting-callback-plugin-in-ansible---part-i-1h0nhttps://hodovi.ch/blog/extending-ansible-with-callback-plugins/https://github.com/ansible/ansible/blob/devel/lib/ansible/plugins/callback/__init__.py