في مرحلة ما، يتخرج كل سكريبت عمليات من سطر تكتبه يدوياً إلى أداة مشتركة يثبّتها زملاؤك ويشغّلونها يومياً. في اللحظة التي يحدث ذلك، يتداعى السكريبت الخام: لا يحتوي نصاً للمساعدة --help، ويفشل بشكل غامض حين يكون الوسيط المطلوب مفقوداً، ويخرج بـ 0 حتى عند الخطأ، ولا يمكن توصيله بشكل نظيف مع أوامر أخرى. أداة CLI صحيحة تحل كل هذا — وفي عالم DevOps، بناء CLIs هو مهارة هندسية أساسية. أدوات كـ AWS CLI وkubectl وgh وterraform كلها برامج CLI يثق بها المشغّلون في أنظمة الإنتاج كل يوم.
يغطي هذا الدرس نهجين — argparse من المكتبة القياسية للأدوات البسيطة، وclick من طرف ثالث لأي شيء تتوقع أن يتطور — بالإضافة إلى اتفاقيات رموز الخروج وخطوة التغليف في pyproject.toml التي تحول السكريبت إلى أمر قابل للتوزيع.
argparse: خط الأساس من المكتبة القياسية
argparse جزء من المكتبة القياسية لـ Python. يُحلّل sys.argv، ويتحقق من الأنواع، ويولّد --help تلقائياً، ويرفع أخطاء واضحة للوسيطات المطلوبة المفقودة. لأداة داخلية عشوائية لن تتجاوز أمراً واحداً، argparse هو الاختيار الصحيح لأنه لا يحتاج أي تبعيات.
#!/usr/bin/env python3
"""ops-check: verify that a set of services are reachable before a deploy."""
import argparse
import sys
import subprocess
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
prog="ops-check",
description="Pre-deploy service reachability checker",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
ops-check --hosts db.internal cache.internal --port 6379
ops-check --hosts api.example.com --timeout 5 --verbose
""",
)
parser.add_argument(
"--hosts",
nargs="+", # one or more values
required=True,
metavar="HOST",
help="Hostnames or IPs to check",
)
parser.add_argument(
"--port",
type=int,
default=80,
help="TCP port to probe (default: 80)",
)
parser.add_argument(
"--timeout",
type=float,
default=3.0,
help="Connection timeout in seconds (default: 3.0)",
)
parser.add_argument(
"--verbose", "-v",
action="store_true",
help="Print per-host results",
)
return parser.parse_args()
def check_host(host: str, port: int, timeout: float) -> bool:
result = subprocess.run(
["nc", "-z", "-w", str(int(timeout)), host, str(port)],
capture_output=True,
)
return result.returncode == 0
def main() -> None:
args = parse_args()
failures = []
for host in args.hosts:
ok = check_host(host, args.port, args.timeout)
if args.verbose:
status = "OK" if ok else "FAIL"
print(f" [{status}] {host}:{args.port}")
if not ok:
failures.append(host)
if failures:
print(f"UNREACHABLE: {', '.join(failures)}", file=sys.stderr)
sys.exit(1) # غير صفري — pipeline CI يراه فشلاً
print(f"All {len(args.hosts)} host(s) reachable on port {args.port}.")
sys.exit(0)
if __name__ == "__main__":
main()
الفكرة الأساسية — استدعِ sys.exit() دائماً بشكل صريح. حين تعود دالة بشكل طبيعي، تخرج Python برمز 0. هذا خطأ لأداة عمليات اكتشفت فشلاً. كل مسار كود في main() يجب أن ينتهي بـ sys.exit(code) صريح. أنظمة CI/CD والسكريبتات الـ shell ومغلّفات المراقبة كلها تعتمد على رموز الخروج لا على نص المخرجات.
اتفاقيات رموز الخروج لكل أداة عمليات
رموز الخروج هي واجهة برمجة Unix بين البرامج. أداة تخرج بـ 0 عند الفشل معطوبة بالتعريف — أي سكريبت shell أو مرحلة pipeline أو فحص مراقبة يستدعيها سيمضي بصمت. الاتفاقيات بسيطة وغير قابلة للتفاوض:
0 — نجاح، كل شيء عمل كما هو متوقع.
1 — فشل عمليات عام (خدمة غير قابلة للوصول، فشل التحقق، مورد غير موجود).
2 — سوء استخدام CLI نفسه (وسيطات خاطئة، علامة مطلوبة مفقودة). argparse وclick يخرجان بـ 2 تلقائياً لأخطاء الوسيطات — لا تتجاوز هذا.
130 — السكريبت مُقاطَع من المستخدم (Ctrl+C، SIGINT). اصطد KeyboardInterrupt واخرج بـ 130 حتى يعرف الـ shell أن المستخدم أوقفه عمداً.
لا تستخدم رموز خروج فوق 125 لأخطاء مستوى التطبيق — تلك الرموز محجوزة من الـ shell لإنهاء الإشارات. لا تخرج بغير صفري بعد إتمام المهمة بنجاح حتى لو طُبع تحذير.
click: الاختيار لمستوى الإنتاج للـ CLIs متعددة الأوامر
click (Command Line Interface Creation Kit) يُؤلّف الأوامر والأوامر الفرعية ومجموعات الخيارات بطريقة لا يستطيع argparse مضاهاتها. أي أداة داخلية تتجاوز فعلاً واحداً — ops deploy وops rollback وops status — يجب بناؤها على click. كما يتكامل بشكل طبيعي مع rich لمخرجات الطرفية الملونة، مما يهم حين تقرأ جداراً من النص في طرفية داكنة في 02:00.
#!/usr/bin/env python3
"""ops-cli: multi-command internal infrastructure CLI built with click."""
import sys
import click
# ── مجموعة المستوى الأعلى ───────────────────────────────────────────────────
@click.group()
@click.version_option(version="1.0.0", prog_name="ops-cli")
def cli() -> None:
"""Internal ops automation CLI. Run a subcommand with --help for details."""
# ── أمر deploy الفرعي ────────────────────────────────────────────────────────
@cli.command()
@click.argument("service")
@click.option("--env", "-e",
type=click.Choice(["staging", "production"], case_sensitive=False),
required=True,
help="Target environment")
@click.option("--image-tag", default="latest", show_default=True,
help="Docker image tag to deploy")
@click.option("--dry-run", is_flag=True,
help="Print what would happen without making changes")
@click.pass_context
def deploy(ctx: click.Context, service: str, env: str,
image_tag: str, dry_run: bool) -> None:
"""Deploy SERVICE to the target environment."""
if env == "production" and not dry_run:
# طلب تأكيد صريح للإنتاج
click.confirm(
f"Deploy {service}:{image_tag} to PRODUCTION?",
abort=True, # يرفع click.Abort -> يخرج بـ 1 عند 'n'
)
action = "[DRY-RUN] Would deploy" if dry_run else "Deploying"
click.echo(f"{action} {service}:{image_tag} -> {env}")
if not dry_run:
# منطق النشر الفعلي هنا (kubectl set image، إلخ)
click.secho(" Deploy complete.", fg="green", bold=True)
sys.exit(0)
# ── أمر status الفرعي ────────────────────────────────────────────────────────
@cli.command()
@click.argument("service")
@click.option("--namespace", "-n", default="default", show_default=True,
help="Kubernetes namespace")
def status(service: str, namespace: str) -> None:
"""Show running status for SERVICE."""
click.echo(f"Checking {service} in namespace {namespace} ...")
# التنفيذ الفعلي: استدعاءات kubectl / boto3
click.secho(" Running (3/3 replicas ready)", fg="green")
sys.exit(0)
if __name__ == "__main__":
cli()
ممارسة احترافية — استخدم click.secho() للمخرجات الملونة، لكن فقط حين الكتابة إلى طرفية. يُعطّل click تلقائياً الألوان حين لا يكون stdout طرفية (مثلاً حين توصَّل المخرجات إلى grep أو تُعاد توجيهها إلى ملف). لا تستخدم رموز ANSI escape يدوياً أبداً — فإنها تلوّث المخرجات الموصَّلة وتكسر محللات السجلات. الأمر نفسه ينطبق على أشرطة التقدم: استخدم click.progressbar() أو rich.progress.Progress، لا حيل إرجاع المؤشر المصنوعة يدوياً.
مخطط معمارية CLI
معمارية CLI متعددة الأوامر باستخدام click: المجموعة الجذرية تتولى العلامات العامة والتوجيه؛ كل أمر فرعي يمتلك وسيطاته ويخرج برمز ذي معنى.
التغليف: من سكريبت إلى أمر قابل للتثبيت
يصبح السكريبت أداة حقيقية حين يمكنك تثبيته بـ pip install . وتشغيله كـ ops-cli من أي مكان على النظام — بلا بادئة python script.py ولا تلاعب بالمسارات. يتم ذلك عبر نقاط الدخول في pyproject.toml التي رأيتها بالفعل في الدرس الأول. تخطيط المجلد لأداة CLI صغيرة يتبع تخطيط src/، الذي يمنع الحزمة من الاستيراد العرضي من جذر المشروع دون تثبيتها أولاً.
# تخطيط المشروع لـ ops-cli
ops-cli/
├── pyproject.toml
├── README.md
├── src/
│ └── ops_cli/
│ ├── __init__.py
│ ├── main.py # يعرّف cli() مع @click.group()
│ ├── deploy.py # أمر deploy الفرعي
│ └── status.py # أمر status الفرعي
└── tests/
├── test_deploy.py
└── test_status.py
# pyproject.toml — قسم [project.scripts] الحيوي
[project]
name = "ops-cli"
version = "1.0.0"
requires-python = ">=3.11"
dependencies = ["click>=8.1", "rich>=13.7"]
[project.scripts]
ops-cli = "ops_cli.main:cli" # يثبّت الأمر 'ops-cli' في PATH
# تثبيت في وضع قابل للتعديل أثناء التطوير (التغييرات تسري فوراً)
pip install -e .
# تثبيت من وسم إصدار في CI أو على جهاز زميل
pip install git+https://github.com/your-org/ops-cli.git@v1.0.0
# التحقق من أن نقطة الدخول المثبتة تعمل
ops-cli --help
ops-cli --version
مصيدة إنتاجية — لا تشحن أبداً أداة بأسماء بيئات أو معرفات حسابات مُرمَّزة في الكود. أداة مثبتة على أجهزة 20 مهندس يجب أن تقرأ إعداداتها من متغيرات البيئة أو ملف إعداد، لا من حرفيات في كود المصدر. استخدم os.environ.get("OPS_ENVIRONMENT", "staging") مع قيمة افتراضية آمنة، أو اقبل القيمة كخيار CLI (مفضّل، لأنه صريح وقابل للتدقيق في سجل الـ shell). معرفات حسابات الإنتاج المُرمَّزة في الكود هي حادثة أمنية P0 تنتظر حدوثها — شخص يُشغّل أمر "اختبار" ضد هدف خاطئ.
اختبار أدوات CLI مع click.testing
click يُرفق CliRunner يستدعي CLI الخاص بك داخل العملية دون إطلاق عملية فرعية. هذه هي الطريقة الصحيحة لاختبار وحدة أوامر CLI — تلتقط stdout وstderr ورمز الخروج، وتعمل داخل pytest بلا إعداد إضافي. اختبر دائماً على الأقل: المسار السعيد، ومسار الوسيط المطلوب المفقود (توقّع الخروج بـ 2)، ومسار الفشل (توقّع الخروج بـ 1 مع رسالة على stderr).
# tests/test_cli.py
from click.testing import CliRunner
from ops_cli.main import cli
def test_deploy_dry_run_exits_zero():
runner = CliRunner()
result = runner.invoke(cli, ["deploy", "api-gateway", "--env", "staging",
"--dry-run"])
assert result.exit_code == 0
assert "DRY-RUN" in result.output
def test_deploy_missing_env_exits_two():
runner = CliRunner()
result = runner.invoke(cli, ["deploy", "api-gateway"])
assert result.exit_code == 2 # خطأ استخدام argparse/click
assert "Missing option" in result.output
def test_status_shows_service_name():
runner = CliRunner()
result = runner.invoke(cli, ["status", "payment-service"])
assert result.exit_code == 0
assert "payment-service" in result.output
# التشغيل: pytest tests/ -v
ممارسة احترافية — أضف علامة --output json على كل أمر ينتج بيانات منظَّمة. الجداول المقروءة بشرياً جيدة للاستخدام التفاعلي، لكن وظائف CI والسكريبتات الأخرى التي تستدعي أداتك تحتاج مخرجات قابلة للتحليل آلياً. خيار --output واحد يُبدّل بين table (افتراضي) وjson يجعل أداتك مواطناً من الدرجة الأولى في pipeline. استخدم json.dumps(data, indent=2) واكتبها دائماً إلى stdout (لا stderr) حتى يستطيع المتصلون التقاطها بـ $(ops-cli status svc --output json).
قائمة تحقق الإنتاج لكل CLI عمليات
قبل تسليم CLI لبقية الفريق، تحقق من هذه النقاط. في شركات كـ Stripe وCloudflare، تمر الأدوات الداخلية بمراجعة موجزة تفحص بالضبط هذه القائمة قبل إضافتها إلى بيئة المطورين المشتركة:
--help على كل أمر وأمر فرعي — يولّدها click تلقائياً إن كتبت docstrings.
--version على المجموعة الجذرية — استخدم @click.version_option()؛ أدرجه في كل قالب تقرير أعطال.
رموز خروج ذات معنى على كل مسار كود — اختبر بـ echo $? بعد كل سيناريو.
لا بيانات اعتماد أو معرفات حسابات أو أسماء مضيفين مُرمَّزة في الكود — كل الإعداد من متغيرات البيئة أو ملف إعداد.
تسجيل منظَّم إلى stderr، النتائج إلى stdout — stderr للتشخيصات؛ stdout للمخرجات القابلة للتحليل آلياً. لا تخلط بينهما أبداً.
العمليات المدمِّرة ذات قابلية تكرار (Idempotent) — تشغيل ops-cli deploy مرتين يجب ألا يسبب حادثة.
علامة --dry-run على كل أمر يُعدّل الحالة — هذه هي أفضل شبكة أمان واحدة لأدوات العمليات.
نستخدم ملفات تعريف الارتباط لتشغيل هذا الموقع وتحليل الزيارات وعرض إعلانات مخصّصة. يمكنك قبول كل ملفات تعريف الارتباط أو رفض غير الأساسية منها.
سياسة الخصوصية