عبدالهادي الديوري

الأعضاء
  • المساهمات

    368
  • تاريخ الانضمام

  • تاريخ آخر زيارة

  • Days Won

    22

السُّمعة بالموقع

174 Excellent
  1. إن كانت لدينا آلاف المقالات فلا يُمكننا أن نعرضها جميعها في الصّفحة الرّئيسية، ولو كانت تحتوي على آلاف الصّور ومقاطع الفيديو فمن المؤكّد بأنّ الصّفحة ستأخذ عدّة دقائق لتنهي التّحميل، والحل الشّائع حاليّا في تطبيقات الويب هو تجميع عدد محدّد من السّجلات في كل صفحة، مثلا خمسة سجلّات في الصّفحة الأولى وخمسة أخرى في الصّفحة الثّانيّة، وهكذا… بهذه الطّريقة يُمكنك عرض شريط أسفل الصّفحة لتوفير طريقة للمُستخدم للتنقل بين الصّفحات، كذلك يُمكنك استعمال لغة جافا سكربت لوضع زر لتحميل المزيد من المقالات كما هو شائع في الكثير من المواقع على الانترنت. كيف تعمل خاصية الصفحات في Flask-SQLAlchemy قبل أن ننتقل إلى كيفيّة تقسيم الصّفحات إلى سجلّات لا بد أن تعرف المبدأ الرّئيسي للعمل، بدءًا من معنى كلمة Pagination (تحويل مجموعة من السّجلات إلى صفحات مُرقّمة، كل صفحة تحمل عددا مُعيّنا من السّجلات، والعدد الذي تُحدّده لكل صفحة يكون ثابتا إلى آخر مجموعة من السّجلات). فمثلا لنفترض بأنّ الأسماء التّاليّة مُسجلة لدينا في قاعدة البيانات: 1 عمر 2 عبد الرّحمن 3 خالد 4 يوسف 5 أحمد 6 إبراهيم 7 كمال 8 كريم 9 مُعاذ 10 جواد لنُقسّمها إلى صفحات مُرقّمة، كل صفحة ستحتوي على 3 أسماء أو أقل: الصّفحة 1: 1 عمر 2 عبد الرّحمن 3 خالد الصّفحة 2: 4 يوسف 5 أحمد 6 إبراهيم الصّفحة 3: 7 كمال 8 كريم 9 مُعاذ الصّفحة 4: 10 جواد بهذه الطّريقة، عندما تطلب من قاعدة البيانات السّجلات المُتواجدة في الصّفحة رقم 1، ستحصل على الأسماء: عمر، عبد الرّحمن، خالد. وإن طلبت السّجلات في الصّفحة رقم 4 فستحصل على الاسم "جواد"، وهكذا ستتمكّن من تقسيم السّجلات المُتواجدة في قاعدة البيانات لتفادي الاستعلام عنها كلّها في مرّة واحدة ومن ثم تقسيمها بنفسك باستعمال لغة Python، وهذا يكون بطيئا جدّا إذا ما كانت لديك الكثير من السّجلات في قاعدة البيانات، وعوضا عن الحصول عليها جميعا في مرّة واحدة، تطلب سجلّات صفحة واحدة فقط. إذا كنت تستخدم كلّا من مكتبة SQLAlchemy وإضافة Flask-SQLAlchemy للتّعامل مع قاعدة البيانات فيُمكنك استعمال خاصيّة تجميع عدد من السّجلات في عدد من الصّفحات عبر استعمال التّابع paginate الذي يأخذ مُعاملين أساسيّين، المُعامل page والذي سيُعبّر عن رقم الصّفحة، والمُعامل per_page الذي سيُعبّر عن عدد السّجلات التّي تتواجد بكل صفحة، وكمثال على هذين المعاملين، فقيمة per_page في مثال الأسماء سابقا هي 3 وكلّما غيّرت قيمة المُعامل page من 1 إلى 4 ستحصل على الأسماء المتواجدة بكل صفحة حسب رقم الصّفحة. السّطر التّالي مثال بسيط على كيفيّة استخدام التّابع paginate: >>> from project.models import Post >>> pagination = Post.query.paginate(page=1,per_page=3) >>> pagination <flask_sqlalchemy.Pagination object at 0x7f0b4a3cc910> ستُلاحظ بعد تنفيذك للأسطر السّابقة، أنّ المُتغيّر pagination عبارة عن كائن من الصّنف Pagination وليس عبارة عن قائمة سجلّات تُمثّل المقالات، ذلك أن SQLAlchemy توفّر لنا العديد من الخصائص والتّوابع التّي يُمكننا استخدامها مع أي كائن من الصّنف Pagination، فيُمكنك مثلا أن تتحقّق من أنّ الصّفحة الحاليّة هي آخر صفحة أو العكس أو أن تحصل على عدد الصّفحات الإجمالي والعديد من التّوابع والخصائص الأخرى التّي سنتطرّق إليها والتّي ستُساعدك حتما عندما ترغب في العمل مع صفحات مُتعدّدة أو استخدام تنسيقات مُحدّدة حسب شروط مُعيّنة. سنُغطّي في الفقرات التّاليّة بعضا من أهم الأمور التّي يُمكنك القيام بها مع خاصيّة ترقيم الصّفحات لتُقدّم لمُستخدمي تطبيقك أفضل تجربة استخدام مُمكنة. الوصول إلى السجلات الموجودة بالصفحة نفّذنا الأسطر التّالية قبل قليل لنحصل على كائن باسم pagination من الصّنف Pagination: >>> from project.models import Post >>> pagination = Post.query.paginate(page=1, per_page=3) ولكن الأهم أن نحصل على المقالات المُتواجدة في الصّفحة كي نتمكّن من عرض عنوان كل مقال ومحتواه أو مُعالجة بيانات كل مقال على حدة. وللوصول إلى السّجلات المُتواجدة بالصّفحة، أضف خاصيّة items للكائن pagination، وبالتّالي ستتمكّن من الدّوران حول المقالات الثّلاثة بحلقة for بسيطة ومنه ستتمكّن من الوصول إلى عنوان كل مقال ومُحواه وكاتبه وما إلى ذلك (تماما كما تتعامل مع المقالات التّي تحصل عليها عن طريق ()query.all أو أي استعلام مُشابه). والمثال التّالي يوضح هذا: >>> posts = pagination.items >>> posts [<title Post 1 | post 1 content >, <title a post from abdelhadi | content of a post >, <title another post from abdelhadi | some other content for the post >] كما ترى فقد تمكّنّا من الوصول إلى قائمة المقالات المُتواجدة في الصّفحة الأولى، ويُمكنك الآن التّعامل مع المُتغيّر posts كما تتعامل مع أي مُتغيّر آخر يُمثّل قائمة سجلّات في قاعدة البيانات. الانتقال إلى الصفحة التاليّة بعد أن تعرّفنا على كيفيّة الوصول إلى الصّفحة الأولى وعناصر الصّفحة، حان الوقت للانتقال إلى الصّفحة التّاليّة والوصول إلى عناصرها. للوصول إلى الصّفحة التّاليّة، كل ما عليك القيام به هو إضافة التّابع next إلى مُتغيّر الصّفحة الحاليّة والذي يجب أن يكون كائنا من الصّنف Pagination، انظر: >>> page_1 = Post.query.paginate(page=1, per_page=3) >>> page_1.items [<title Post 1 | post 1 content >, <title a post from abdelhadi | content of a post >, <title another post from abdelhadi | some other content for the post >] >>> page_2 = page_1.next() >>> page_2.items [<title a post from dyouri | other content >, <title Post from user4 | Content 4 >, <title Post from user5 | Content 5 >] في المثال السابق: نصل أولًا إلى الصّفحة الأولى ثم نسند القيمة إلى المُتغيّر page_1 وبعدها نطلع على عناصر الصّفحة للتأكّد، بعدها تأتي الفكرة الأساسيّة إذ نُنشئ مُتغيّرا جديدا page_2 بحيث نستخدم المُتغيّر page_1 مع التّابع next للحصول على الصّفحة الثّانيّة، وبعدها ننظر إلى عناصر الصّفحة. الصفحة السابقة تتشابه طريقة الوصول إلى الصّفحة السّابقة مع طريقة الوصول إلى الصّفحة التالية، فقط استعمل التّابع prev عوضا عن التّابع next، انظر: >>> page_2 = Post.query.paginate(page=2, per_page=3) >>> page_2.items [<title a post from dyouri | other content >, <title Post from user4 | Content 4 >, <title Post from user5 | Content 5 >] >>> page_1 = page_2.prev() >>> page_1.items [<title Post 1 | post 1 content >, <title a post from abdelhadi | content of a post >, <title another post from abdelhadi | some other content for the post >] نستعمل التّابع paginate أولا مع تمرير القيمة 2 إلى المُعامل page للحصول على الصّفحة الثّانيّة، ومن ثمّ نقوم بإنشاء مُتغيّر باسم page_1 بالقيمة ()page_2.prev (أي الصّفحة التّي تسبق الصّفحة الثّانيّة)، وهكذا نصل إلى الصّفحة الأولى مرة أخرى. التحقق من وجود صفحة تالية للصفحة الحالية قد ترغب أحيانا بمعرفة ما إذا كانت هذه الصّفحة التّي تتعامل معها هي الأخيرة أم لا، وتستطيع ذلك عن طريق الخاصيّة has_next التّي تُرجع إمّا القيمة المنطقيّة True أو القيمة False، بحيث يكون الشرط صحيحًا إذا كانت للصّفحة صفحة مُواليّة والعكس إذا كانت آخر صفحة. مثال على الحالة الأولى: >>> page_1 = Post.query.paginate(page=1, per_page=3) >>> page_1.items [<title Post 1 | post 1 content >, <title a post from abdelhadi | content of a post >, <title another post from abdelhadi | some other content for the post >] >>> page_1.has_next True كما تُلاحظ، فالصّفحة الأولى ليست آخر صفحة لأنّ عدد المقالات يتجاوز 3 مقالات، وعليه فالخاصيّة has_next تُرجع القيمة المنطقيّة True، وهذا يعني بأنّ القيمة كانت لتكون False لو كانت عدد المقالات المُتواجدة بقاعدة البيانات ثلاثة أو أقل من ذلك. مثال على الحالة الثّانيّة: >>> page_5 = Post.query.paginate(page=5, per_page=3) >>> page_5.items [<title New title for post from user8 | Content 8 >] >>> page_5.has_next False في هذه الحالة تكون الصّفحة الخامسة هي آخر صفحة وليس بعدها أية صفحة أخرى، حتى أنّ بها مقالا واحدا فقط، وبالتّالي فالنّتيجة هي القيمة False عند استعمالنا للخاصيّة has_next. التحقق من أن للصفحة الحالية صفحة تسبقها بما أنّ هناك طريقة لمعرفة ما إذا كانت للصّفحة الحاليّة صفحة أخرى بعدها، فلا بد أن تكون هناك طريقة للتّعرف على عكس ذلك، أي معرفة ما إذا كانت هذه الصّفحة أول صفحة بحيث لا صفحة تسبقها أم لا. ولمعرفة ما إذا كانت لهذه الصّفحة الحاليّة صفحة أخرى تسبقها يُمكنك استعمال الخاصيّة has_prev، انظر: >>> from project.models import Post >>> page_1 = Post.query.paginate(page=1, per_page=3) >>> page_1.items [<title Post 1 | post 1 content >, <title a post from abdelhadi | content of a post >, <title another post from abdelhadi | some other content for the post >] >>> page_1.has_prev False >>> page_1.has_next True نحصل هنا على الصّفحة الأولى ونتحقّق من أنّ لها صفحة تسبقها، والنّتيجة هي القيمة False كما هو مُتوقّع، بعدها نقوم بالتّحقق ممّا إذا كانت للصّفحة صفحة تالية بعدها، والنّتيجة مُتوقّعة كذلك. لنجمع الآن كلّا من التّابع next والخاصيّة has_prev ونرى النّتيجة: >>> page_1.next().has_prev True النّتيجة هي القيمة True لأنّ قيمة ()page_1.next هي الصّفحة الثّانيّة، ومن المُؤكّد بأنّ للصّفحة الثّانيّة صفحة تسبقها. عدد الصفحات وكيفية الدوران حول قائمة الصفحات للوصول إلى عدد الصّفحات الكليّ عند استخدام التّابع paginate، أضف الخاصيّة pages إلى الكائن، وسيعمل مهما كانت الصّفحة الحاليّة. مثال: >>> from project.models import Post >>> page_1 = Post.query.paginate(page=1, per_page=3) >>> page_1.pages 5 >>> page_3 = Post.query.paginate(page=3, per_page=3) >>> page_3.pages 5 في المثال أعلاه، نستورد الصّنف Post أولًا كالعادة ونحصل على الصّفحة الأولى ونُسند قيمتها إلى المُتغيّر page_1، وبعدها نستعمل الخاصيّة pages للتّعرف على العدد الإجماليّ للصّفحات الذي هو خمسة، وبعدها نقوم بنفس الشّيء مع الصّفحة الثّالثة، وذلك فقط لتوضيح أنّك تستطيع الحصول على العدد الإجمالي للصّفحات بغض النّظر عن الصّفحة التّي أنت فيها. الدوران حول قائمة أرقام الصفحات قد ترغب أحيانًا في الوصول إلى رقم كل صفحة على حدة، وأغلب الظّن أنّك تعمل على شريط لتمكين المُستخدم من الانتقال بين الصّفحات دون ترتيب مُحدّد (الانتقال من الصّفحة الثّالثة إلى الخامسة على سبيل المثال). يُمكنك استخدام التّابع iter_pages للحصول على كائن مُولّد (generator) والذي يسمح لك بالوصول إلى كل رقم على حدة، ويُمكنك التّعامل معه باستخدام حلقة for كما تتعامل مع قوائم python عاديّة، ويُمكنك كذلك الحصول على قائمة عبر استخدام الدّالة list لكنّ الأمر ليس ضروريا. انظر المثال التالي: >>> from project.models import Post >>> page_1 = Post.query.paginate(page=1, per_page=3) >>> page_1.iter_pages() <generator object iter_pages at 0x7f4d96115230> >>> for page in page_1.iter_pages(): ... print(page) ... 1 2 3 4 5 >>> list(page_1.iter_pages()) [1, 2, 3, 4, 5] عندما طبّقنا التّابع ()iter_pages على المُتغيّر page_1 حصلنا على معلومة بسيطة تُشير إلى أنّ هذا الكائن من النّوع generator، أي أنّه يُولّد كل عنصر عندما تحتاج إليه فقط، وكما تُلاحظ، فعندما استخدمنا حلقة for وطبعنا كل عنصر على حدة، استطعنا الوصول إلى أرقام الصّفحات من واحد إلى خمسة، وفي السّطر الأخير، ينتج استدعاء الدّالة list وتمرير المُولّد إليها قائمةً عاديّة، وهذا السّطر الأخير غير مفيد إلا في التّوضيح فقط، واستخدام المُولّد مُباشرة أمر طبيعي. فهذا المُولّد يُستخدم بشكل شائع لتوفير شريط تصفّح لتمكين الزّائر من الانتقال من صفحة إلى أخرى، ولا بدّ أنّك صادفت مثل هذا الشّريط من قبل. ولتوفير الشّريط لمُستخدمي تطبيقك، استخدم ماكرو خاص مع مُحرّك القوالب Jinja، وهو كالتّالي: {% macro render_pagination(pagination, endpoint) %} <div class=pagination> {%- for page in pagination.iter_pages() %} {% if page %} {% if page != pagination.page %} <a href="{{ url_for(endpoint, page=page) }}">{{ page }}</a> {% else %} <strong>{{ page }}</strong> {% endif %} {% else %} <span class=ellipsis>…</span> {% endif %} {%- endfor %} </div> {% endmacro %} يأخذ الماكرو render_pagination مُعاملين اثنين، الأول هو الكائن الذي يُمثّل صفحة مُعيّنة، ويجب أن يكون من الصّنف Pagination، مثل page_1 أو page_2 في الأمثلة السّابقة، أمّا المُعامل الثّاني فهو رابط المُوجّه الذي سيكون مسؤولا عن عرض الصّفحات في المُتصفّح، ويجب أن يكون الرّابط على شكل اسم الطّبعة الزّرقاء (إن وُجدت) ثمّ نقطة ثمّ اسم الدّالة المُرتبطة بالمُوجّه. فمثلا إن كان الموجّه الخاصّ بك يقع تحت طبعة زرقاء باسم posts وكانت اسم الدّالة المُرتبطة بهذا الموجّه هي (pages(page بحيث page رقم الصّفحة، فقيمة المُعامل endpoint في الماكرو أعلاه يجب أن تكون كما يلي: 'posts.pages'، والسّبب الرّئيسي لتوفير هذا المُعامل هو أنّ الماكرو يستخدم الدّالة url_for لتوليد رابط للصّفحة حسب رقمها، والتّالي بعض الرّوابط التّي يُمكن أن تُولّد: /posts/page/1 /posts/page/2 /posts/page/3 ... وعليه يكون الشريط في نفس المكان في كل مرّة تصل فيها إلى صفحة مُعيّنة. لاحظ بأنّنا نستعمل في الماكرو حلقة for مع التّابع ()iter_pages الذي تحدّثنا عنه سابقا، وذلك لعرض أرقام الصّفحات وإضافة رابط لكل صفحة، وهكذا سيكون الشّريط كما يلي: 1 2 3 4 5 مع رابط تحت كل رقم ليُوجّه إلى الصّفحة المُناسبة في التّطبيق. تتحقق الجملة الشّرطيّة if إن كانت الصّفحة الحاليّة تحمل نفس رقم الصّفحة المُتواجد في pagination.page وذلك لعرض الصّفحة الحاليّة بشكل مُميّز (في هذه الحالة سيكون خطّا عريضا مع إزالة الرابط) ليعرف الزّائر أية صفحة يتواجد فيها. تستطيع كذلك أن تنسقه كما تشاء، فقط أضف خاصيّة css وسيكون التّنسيق هو نفسه في جميع أنحاء التّطبيق ما دمت تستدعي هذا الماكرو في المكان الذي تُريد به الشّريط. إذا كنت تستخدم إطار العمل Bootstrap فإن الماكرو التّالي يُوفّر تنسيقا جميلا لشريط التّصفّح مع زرّين إضافيّين، واحد للانتقال إلى الصّفحة السّابقة والآخر للانتقال إلى الصّفحة التّاليّة. {% macro render_pagination(pagination, endpoint) %} <div class=pagination> <nav aria-label="Page navigation"> <ul class="pagination"> {# انتقل إلى الصّفحة السّابقة إن كانت مُتواجدة #} {% if pagination.has_prev %} <li> <a href="{{ url_for(endpoint, page=pagination.prev_num) }}" aria-label="Previous"> <span aria-hidden="true">«</span> </a> </li> {% else %} <li class='disabled'> <a href="#" aria-label="Previous"> <span aria-hidden="true">«</span> </a> </li> {% endif %} {# عرض أزرار الصّفحات (<< 1, 2, 3 >>)#} {%- for page in pagination.iter_pages() %} {% if page %} {% if page != pagination.page %} <li><a href="{{ url_for(endpoint, page=page) }}">{{ page }}</a></li> {% else %} <li class="active"><a href="{{ url_for(endpoint, page=page) }}">{{ page }}</a></li> {% endif %} {% else %} <span class=ellipsis>…</span> {% endif %} {%- endfor %} {# انتقل إلى الصّفحة المُواليّة إن كانت مُتواجدة #} {% if pagination.has_next %} <li> <a href="{{ url_for(endpoint, page=pagination.next_num) }}" aria-label="Next"> <span aria-hidden="true">»</span> </a> </li> {% else %} <li class='disabled'> <a href="#" aria-label="Next"> <span aria-hidden="true">»</span> </a> </li> {% endif %} </ul> </nav> </div> {% endmacro %} لاحظ جيّدا بأنّ هذا الماكرو يستخدم كذلك كلّا من الخاصيّتين has_next و has_prev، وذلك لتعطيل زرّ الانتقال إلى الصّفحة السّابقة إذا لم تكن تتواجد وتعطيل زر الانتقال إلى الصّفحة التّالية في نفس الحالة كذلك. كل ما عليك فعله لاستخدام الماكرو هو وضعه في أعلى ملفّ HTML واستدعائه كما يلي: {{ render_pagination(pagination, 'posts.pages') }} يفترض المثال أعلاه أنّ المُتغيّر الذي يحمل كائنا من الصّنف Pagination يُسمّى pagination والذي سيتوجّب عليك تمريره إلى ملفّ HTML عن طريق الدّالة render_template ورابط الموجّه المسؤول عن عرض الصّفحات هو posts.pages والذي سيُمرّر إلى الدّالة url_for لتوليد رابط كل صفحة على حدة. الحصول على رقم الصفحة التالية تطرّقنا من قبل إلى أنّك تستطيع الوصول إلى الكائن الذي يُمثّل الصّفحة التّاليّة عن طريق استخدام التّابع next، ولكن ماذا لو أردت أن تصل إلى رقم الصّفحة التّالية فقط؟ يُمكنك الحصول على رقم الصّفحة التّاليّة عن طريق استخدام الخاصيّة next_num، انظر: >>> from project.models import Post >>> page_1 = Post.query.paginate(page=1, per_page=3) >>> page_1.next_num 2 في المثال، نحصل على الصّفحة الأولى ثمّ نستخدم الخاصيّة next_num للحصول على رقم الصّفحة التالية للصّفحة الأولى والذي هو الرّقم 2 وهذا طبيعي لأنّ رقم الصّفحة الأولى هو 1. الحصول على رقم الصفحة الحالية للوصول إلى رقم الصّفحة الحاليّة كل ما عليك القيام به هو استخدام الخاصيّة page وستحصل على رقم الصّفحة المرتبطة بالكائن من الصّنف Pagination. انظر المثال التالي لكيفية الحصول على رقم الصّفحة الحاليّة: >>> from project.models import Post >>> page_1 = Post.query.paginate(page=1, per_page=3) >>> page_1.page 1 الحصول على رقم الصّفحة السّابقة تشبه طريقة الوصول إلى رقم الصّفحة السّابقة ما سبق، فقط استخدم الخاصيّة prev_num عوضا عن next_num و page. يغطي المثال التّالي كيفيّة الحصول على أرقام الصّفحات التّي ذكرناها إلى الآن. >>> from project.models import Post >>> page_2 = Post.query.paginate(page=2, per_page=3) >>> page_2.page 2 >>> page_2.prev_num 1 >>> page_2.next_num 3 خاتمة هكذا نكون قد وصلنا إلى جزء مُهم من مشوارنا لتعلّم كيفيّة التّعامل مع قاعدة بياناتنا باستخدام كل من مكتبة SQLAlchemy وإضافة Flask-SQLAlchemy، وتبقى لنا بضعة أمور مُهمّة لتعلّم كيفيّة إجراء عمليّات CRUD كلّها، إذ أنّنا لم نتعرّف بعد على كلّ من كيفيّة تعديل وحذف السّجلات إلّا أنّها أمور بسيطة لذا لن تأخذ الكثير من المساحة في هذه السّلسلة من الدّروس. لذا سنُنهي جزء التّعامل مع قاعدة البيانات لنتمكّن بعدها من العودة إلى تطبيق "كلمة" لنستمرّ في تطويره بإنشاء نظام لتسجيل مُستخدمين جدد وتسجيل دخولهم وتسجيل خروجهم، إضافة إلى عرض المقالات المتواجدة بقاعدة البيانات وكذا تمكين كل مُستخدم من إضافة مقالات خاصّة به، وسنستغلّ ما تعلّمناه من طرق لتقسيم السّجلات إلى صفحات مُتعدّدة ومُختلف المعلومات التّي اكتسبناها لتُعرض كل المقالات بشكل مُناسب للمُستخدمين مع شريط تصفّح للانتقال بين الصّفحات وما إلى ذلك.
  2. بعد أن تعرفنا على كيفيّة استعمال التّابع filter في مكتبة SQLAlchemy لترشيح السجلات عند الحصول عليها من قاعدة البيانات باستعمال شروط منطقيّة متقدمة في الدّرس السّابق، سنستمر في استكشاف ميّزات SQLAlchemy لبناء تطبيقات ويب أفضل، إذ سنتعرف في هذا الدّرس على كيفيّة استعمال التابع filter مع بقيّة التّوابع والدّوال المتوفّرة في المكتبة، إضافة إلى مقدمة بسيطة إلى كيفيّة التّعامل مع التّاريخ والوقت في بايثون للحصول على سجلّات من قاعدة البيانات حسب وقت وتاريخ محددين. مزج التابع filter ببقية التوابع، والدوال التي توفرها مكتبة SQLAlchemy يمكن مزج التّابع filter ببقيّة التّوابع، كما في حالة مزجه مع التّابع limit لتحديد عدد معيّن من النّتائج بعد ترشيحها وفق شرط أو شروط معيّنة، أو حتى مزج filter مع filter_by إن أردت ذلك (لكن مزجهما لا فائدة منه). لنحصل مثلًا على جميع المُستخدمين الذين تبدأ أسماءهم بالمقطع user ونحدّد عدد النّتائج في نتيجتين فقط: >>> User.query.filter(User.name.startswith('user')).limit(2).all() [<username: user4 | email: user4@example.com >, <username: user5 | email: user5@example.com >] كذلك يمكنك استعمال order_by بعد التابع filter لترتيب النّتائج التي تُحقّق الشروط المطروحة: >>> User.query.filter(User.name.in_(['user4', 'khalid', 'abdelhadi'])).order_by(User.name).all() [<username: abdelhadi | email: abdelhadi@example.com >, <username: khalid | email: khalid@example.com >, <username: user4 | email: user4@example.com >] وهذا مثال آخر على كيفيّة مزج كل من filter و order_by ثمّ limit في استعلام واحد: >>> User.query.filter(User.name.in_(['user4', 'khalid', 'abdelhadi'])).order_by(User.name).limit(2).all() [<username: abdelhadi | email: abdelhadi@example.com >, <username: khalid | email: khalid@example.com >] استعمال filter للحصول على نتائج حسب تاريخ إضافتها قد يحتاج التّعامل مع التّاريخ والوقت في بايثون إلى درس مُستقل لما فيه من التّعقيد والحالات المُختلفة، ورغم أنّني سأحاول ذكر معظم الأشياء المهمّة في هذه الفقرة، إلّا أنّني لن أذكر كل شيء، بل أهيب بك أن تجد درسا يشرح هذا الجزء بتفصيل أكبر. الهدف هدفنا من هذه الفقرة هو تقديم فكرة عن كيفيّة التّعامل مع السّجلات حسب تاريخ إضافتها (أي قيمة العمود created_date)، فبما أنّنا نمتلك معلومة مهمّة عن السّجل وهو وقت إضافته فسنتمكّن من الحصول على سجلّات حسب يوم إضافتها والسّجلات التّي أضيفت أثناء مقطع زمني معيّن (مثلا بين 5 أيام و10 أيام الماضيّة)، وعندما تفهم أساسيّات التّعامل مع التّاريخ والوقت فستتمكّن من إضافة ميّزات أخرى إلى تطبيقاتك كتحديد تاريخ آخر تعديل وكذلك أرشفة السّجلات حسب تاريخ إضافتها والعديد من الأفكار الأخرى. وحدة datetime الوحدة datetime هي وحدة مبنيّة مُسبقا بلغة بايثون، ويكفي للوصول إليها أن تقوم باستيرادها مُباشرة. تحتوي هذه الوحدة على العديد من الدّوال المُساعدة والوحدات الأخرى داخلها، وسنهتم حاليا بكل من الوحدة البنت datetime والدّالة timedelta فقط، لذا سنقوم باستيرادهما كما يلي: >>> from datetime import datetime, timedelta هنا نستدعي كلا من datetime وtimedelta من الوحدة datetime وبالتّالي فبغض النّظر عن التّشابه في الاسم فإن الوحدة التّي نستدعي منها هي الأصل والوحدة datetime الأخرى مجرّد فرع من الوحدة الأصليّة.هذا يعني بأنّنا إن أردنا الوصول إلى الوحدة الفرعيّة datetime عند استدعاء الوحدة الأصليّة datetime مُباشرة فعلينا إخبار بايثون بالأمر صراحة. بالمثال يتضح المقال: >>> import datetime >>> datetime <module 'datetime' (built-in)> >>> datetime.datetime <type 'datetime.datetime'> قارن الخرج بما يلي: >>> from datetime import datetime, timedelta >>> datetime <type 'datetime.datetime'> الاستنتاج هنا هو أنّ datetime هي الوحدة الأصلية وdatetime.datetime هي وحدة فرعيّة، وبينهما اختلاف واضح. والآن بعد أن حدّدنا الفرق بين كل من الوحدة datetime والوحدة البنت datetime، وبما أنّنا نستدعي هذه الأخيرة فسأشير إليها بـdatetime مُباشرة. إليك تذكيرا بسطر الاستيراد: >>> from datetime import datetime, timedelta لنحدّد الآن الفرق بين كل من datetime وtimedelta، وأبسط طريقة لشرح كل واحدة هي كالآتي: datetime تُمكّننا من التّعامل مع التاريخ (date) والوقت (time).timedelta تُحدّد الفرق بين مُدّتين زمنيّتين.` هذا يعني أنّك إن أردت الحصول على مدّة زمنّية معيّنة (الوقت الحالي حسب توقيتك المحلي أو توقيت UTC مثلا) فسيتوجّب عليك استخدام datetime مع مُختلف التّوابع التّي تُوفّرها الوحدة. وإن أردت الحصول على مقدار الوقت بين مدّتين زمنيّتين فسيتوجّب عليك استخدام timedelta، أمّا لو أردت الحصول مثلا على الفرق بين الوقت الحالي والوقت قبل 3 أيام فستستعمل كلا من datetime وtimedelta . سأشرح لك بعض الأمثلة على كيفيّة استخدام كل من datetime و timedelta للحصول على سجلات من قاعدة البيانات حسب تاريخ إضافتها وشروط أخرى بفضل التّابع filter الذي تُوفّره لنا مكتبة SQLAlchemy، وسنتعرّف في الفقرات التّاليّة على بضعة أساليب لترشيح السّجلات حسب النقطة الزمنيّة التي أضيفت فيها، وذلك عبر استخدام التّابع filter وخصائص الصّنف datetime. الحصول على سجلات حسب سنة إضافتها سنبدأ أولا بالتّعرف على كيفيّة الحصول على السجلات التي أضيفت في سنة مُعيّنة، كالمستخدمين الذين سجّلوا في الموقع في سنة 2018 أو المقالات التي أضيفت في سنة 2017 وغير ذلك من الإمكانيّات الأخرى التي يُمكنك التفكير فيها عند إنشاء تطبيقات تعتمد على ترتيب المحتوى حسب السنوات أو عرض بيانات خاصّة بسنة مُعيّنة. للحصول مثلا على جميع المُستخدمين الذين سجّلوا في التّطبيق في سنة 2017، سنستعمل دالّة في SQLAlchemy باسم extract لاستخراج السنة من قيمة التاريخ لمُقارنتها مع السنة التي نرغب في الحصول على السجلات المرتبطة بها، أي أنّنا سنستعمل التّابع filter للحصول على السّجلات التي يحتوي تاريخ إضافتها على السّنة 2017، والمثال التّالي توضيح على ما سبق: from sqlalchemy import extract from project.models import User users2017 = User.query.filter(extract('year', User.created_date) == 2017).all() في الشّيفرة أعلاه، نستورد الدّالة extract أولًا من حزمة sqlalchemy (لاحظ أنّ هذه الحزمة مُتعلّقة بمكتبة SQLAlchemy وليس بإضافة Flask-SQLAlchemy)، ثمّ نقوم باستيراد الصّنف User، بعدها نُنشئ المُتغيّر users2017 الذي سيحمل قائمة تحتوي على جميع المُستخدمين الذين تمّت إضافتهم إلى قاعدة البيانات في أي وقت من سنة 2017. الشّرط الذي نُمرّره للتّابع filter أعلاه هو كما يلي: extract('year', User.created_date) == 2017 لاحظ بأنّ السّطر عبارة عن مُقارنة بسيطة إذ نُقارن القيمة النّاتجة عن استدعاء الدّالة extract مع تمرير سلسة نصيّة ’year’ كمعامل أول وUser.created_date كمُعامل ثانِِ مع العدد الصّحيح 2017، ما يعني بأنّك لو أردت الحصول على المُستخدمين الذين أُضيفوا في سنة 2018 فكل ما عليك فعله هو تغيير 2017 بـ 2018 كما يلي: extract('year', User.created_date) == 2018 الحصول على سجلات أضيفت في شهر معين نتّبع نفس المنهج عند الرّغبة في الحصول على سجلّات أُضيفت في شهر مُعيّن من السّنة، علينا فقط استخراج الشّهر (month) من التاريخ عوضا عن السّنة، فللحصول على السّجلات التّي أضيفت في الشّهر الأول من كلّ سنة سنستعمل الشّرط التّالي: extract('month', User.created_date) == 1 ما يعني بأنّ الشيفرة الكليّة ستُصبح كما يلي: from sqlalchemy import extract from project.models import User jan_users = User.query.filter(extract('month', User.created_date) == 1).all() في هذه الحالة، سيحمل المُتغيّر jan_users قائمة بجميع المُستخدمين الذين سجّلوا في الشّهر الأول. لاحظ أنّنا لم نُحدّد السّنة هنا، ما يعني بأنّ النتيجة ستكون عبارة عن المُستخدمين الذين سجّلوا في الشّهر الأول من أي سنة كيفما كانت. الحصول على سجلات أضيفت في يوم معين من الشهر للحصول على سجلّ حسب اليوم من الشّهر الذي تمّت إضافته فيه إلى قاعدة البيانات، فكلّ ما عليك فعله هو استخدام الخيار day عوضا عن year للسّنة أو month للشّهر. مثال على كيفيّة الحصول على جميع المُستخدمين الذين سجّلوا في اليوم الأول من الشّهر: from sqlalchemy import extract from project.models import User users = User.query.filter(extract('day', User.created_date) == 1).all() الشّهر والسّنة لا يُهمّان هنا، المُهمّ أن يُضاف السّجل في اليوم الأول من أي شهر كان بغضّ النظر عن السّنة. الحصول على سجلات أضيفت في تاريخ معين (سنة، شهر، يوم) يُمكن جمع المعلومات السّابقة والدّالَّة db.and_ التي تعرّفنا عليها في درس سابق للحصول على سجلّات من قاعدة البيانات حسب تاريخ مُحدّد، أي في يوم وشهر مُحدّدين وسنة مُعيّنة. للحصول مثلا على جميع المُستخدمين الذين سجّلوا في اليوم الأول من الشّهر الأول من سنة 2018: from sqlalchemy import extract from project.models import User from project import db users2018 = extract('year', User.created_date) == 2018 first_jan_users = db.and_(extract('month', User.created_date) == 1, extract('day', User.created_date) == 1 ) users = User.query.filter(db.and_(users2018, first_jan_users)).all() هنا يحمل المُتغيّر users2018 شرطا يحدّد المُستخدمين الذين تمّت إضافتهم إلى قاعدة البيانات في سنة 2018، والمُتغيّر first_jan_users يحمل شرطا يُحدّد بأن على المستخدم أن يُسجَّل في الشهر الأول وكذا في أول يوم، ما يعني بأنّنا جمعنا شرطين في شرط واحد. بعدها نستعمل الشّرطين users2018 وfirst_jan_users للحصول على المستخدمين الذين يُحقّقون كلا الشّرطين في آن واحد، أي أن يُسجّلوا في سنة 2018 واليوم الأول من الشّهر الأول. يُمكنك تغيير القيم كيفما تشاء، وكذلك استعمال خصائص الوقت (السّاعة والدّقيقة على سبيل المثال) للحصول على سجلات حسب مُددِِ زمنيّة أكثر دقّة، وهذا ما سنتعرّف عليه في الفقرات التّاليّة من هذا الدّرس. الحصول على سجلات أضيفت في ساعة معينة من اليوم سنتّبع نفس المنهج الذي كنّا نستعمله عند التّعامل مع السنوات والأشهر والأيام للحصول على سجلّات حسب السّاعة من اليوم، أي الدّالة extract، وسنستخرج القيمة hour للعمل مع ساعات اليوم كما يلي (مع استبدال HOUR بالساعة في النظام الأربع والعشرينيّ 24h): extract('hour', User.created_date) == HOUR إن أردنا الحصول على جميع المُستخدمين الذين سجّلوا في الخامسة مساءً: from sqlalchemy import extract from project.models import User five_pm_users = extract('hour', User.created_date) == 17 users = User.query.filter(five_pm_users).all() الحصول على سجلات أضيفت في دقيقة معينة للحصول على سجلّات حسب الدقيقة التي أُضيفت فيها، استعمل minute مع الدّالة extract. وللحصول على المُستخدمين الذين سجّلوا في الدّقيقة الأولى من كلّ ساعة: from sqlalchemy import extract from project.models import User first_minute_users = extract('minute', User.created_date) == 1 users = User.query.filter(first_minute_users).all() الحصول على سجلات حسب وقت وتاريخ إضافتها لنجمع جميع المعلومات أعلاه للحصول على سجلّات حسب كلّ من تاريخ إضافتها (سنة، شهر، يوم) ووقت إضافتها (السّاعة والدّقيقة)، نحصل في المثال التّالي على أي مُستخدم سجّل في اليوم الأول من الشّهر الأول من سنة 2018 في الدّقيقة الأولى من السّاعة الخامسة مساءً: from sqlalchemy import extract from project.models import User from project import db users2018 = extract('year', User.created_date) == 2018 first_jan_users = db.and_(extract('month', User.created_date) == 1, extract('day', User.created_date) == 1 ) five_pm_first_minute_users = db.and_(extract('hour', User.created_date) == 17, extract('minute', User.created_date) == 1 ) first_jan_five_pm_first_minute = db.and_(first_jan_users, five_pm_first_minute_users ) users = User.query.filter(db.and_(users2018, first_jan_five_pm_first_minute ) ).all() لاحظ الشّروط التّي استخدمناها: users2018: المستخدمون الذين سجّلوا في سنة 2018 first_jan_users: المستخدمون الذين سجّلوا في اليوم الأول من الشّهر الأول five_pm_first_minute_users: المستخدمون الذين سجّلوا في الدّقيقة الأولى من السّاعة الخامسة first_jan_five_pm_first_minute: المستخدمون الذين سجّلوا في الدّقيقة الأولى من السّاعة الخامسة في 1 يناير. يُمكن كذلك مزج الشّروط الواحد داخل الآخر دون حفظها في مُتغيّرات خاصّة، لكنّ يفضل تسجيل الشّروط في مُتغيّرات لتسهيل قراءة الشّيفرة. التعامل مع الفروقات الزمنية يُمكن استعمال الدّالة timedelta من وحدة datetime للتّعامل مع الفروقات الزّمنية، إذ تُرجع timedelta فرقا زمنيّا بين نقطتين زمنيّتين مُحدّدتين بمقدار زمنيّ مُعيّن. وللحصول على فرق زمني مقدار يوم واحد، سنُمرّر عدد الأيام (1 في هذه الحالة) إلى المُعامل days كما يلي: >>> from datetime import timedelta >>> one_day = timedelta(days=1) >>> one_day datetime.timedelta(1) للحصول على فرق زمني مقداره 30 ثانيّة سنُمرّر القيمة إلى المُعامل seconds: >>> thirty_seconds = timedelta(seconds=30) >>> thirty_seconds datetime.timedelta(0, 30) للحصول على فرق زمني مقداره يوم وثلاثون ثانيّة: >>> timedelta(days=1, seconds=30) datetime.timedelta(1, 30) يُمكن كذلك الحصول على الفرق الزمني بين قيمتي datetime مُختلفتين، فللحصول على الفرق الزّمني بين سنتي 2017 و2018 مثلًا: >>> from datetime import datetime, timedelta >>> date1 = datetime(year=2017, month=1, day=1) >>> date2 = datetime(year=2018, month=1, day=1) >>> print(date1) 2017-01-01 00:00:00 >>> print(date2) 2018-01-01 00:00:00 >>> delta = date2 - date1 >>> delta datetime.timedelta(365) >>> print(delta) 365 days, 0:00:00 لاحظ بأنّ كل ما قمنا به هو عمليّة فرق بسيطة date2 – date1 وأسندنا القيمة إلى المتغيّر delta، والذي يُعطينا القيمة 365 days, 0:00:00 عند طباعته، أي أن الفرق الزمني بين السّنتين هو 365 يوما، صفر ساعة، صفر دقيقة وصفر ثانيّة. نستطيع استعمال قيمة الفرق الزمني في SQLAlchemy للحصول على سجلّات بشروط مثل أن تكون قد سُجّلت قبل شهر واحد، قبل سنة، ما بين الشّهر والشّهرين، إلى غير ذلك. يمكنّنا المثال التّالي من الحصول على جميع المُستخدمين الذين سجّلوا بعد أسبوعين مُنصرمين وقبل أسبوع واحد من الآن: from datetime import datetime, timedelta from project import db from project.models import User, Post # Get Users who signed up between # two weeks and one week ago datetime_now = datetime.utcnow() seven_days = timedelta(days=7) one_week_ago = datetime_now - seven_days fifteen_days = timedelta(days=15) two_weeks_ago = datetime_now - fifteen_days users = User.query.filter( db.and_(User.created_date >= two_weeks_ago, User.created_date <= one_week_ago) ).all() خاتمة تعرّفنا في هذا الدّرس على كيفيّة استعمال التّابع filter لإجراء عمليّات متقدّمة، ومزجه ببقيّة التوابع المتوفرة في SQLAlchemy، كما ألقينا نظرة على خصائص التّاريخ والوقت وكيفيّة الاعتماد عليها للحصول على سجلّات من قاعدة البيانات حسب شروط زمنيّة مُعيّنة.
  3. سنُتم في هذا الدرس ما بدأناه في الدرس السابق من تعلم لكيفيّة استعمال الشروط والمعاملات المنطقيّة المتقدمة في لغة بايثون، من أجل الحصول على نتائج دقيقة من قاعدة البيانات عند استعمال مكتبة SQLAlchemy وإضافة Flask-SQLAlchemy. التعامل مع السلاسل النصية قد تحتاج أحيانا إلى التّحقق من أنّ سلسلة نصيّة ما تبدأ أو تنتهي بكلمة أو حرف، أو أنّ كلمة أو حرفا معيّنا يتواجد في قائمة أو سلسلة نصيّة، لذا سنتعرّف في الفقرات التّاليّة على كيفيّة فعل ذلك في لغة بايثون. التابع startswith يُطبّق التّابع startswith على السّلسلة النّصيّة للتأكّد من أنّها تبدأ بمقطع معيّن، فمثلًا، سنتأكّد أنّ السّلسلة النّصيّة abdelhadi تبدأ بالمقطع abd: >>> name = 'abdelhadi' >>> name.startswith('abd') True يُمكنك تطبيق هذه الميّزة من أجل الحصول على جميع المقالات التّي يبدأ عنوانها بمقطع معيّن، فمثلا لنقل بأنّك تمتلك في مدوّنتك مقالات مُختلفة إضافة إلى مقالات تُشير إلى كل تحديث في تطبيقك لتعرض الميّزات الجديدة، ثم لنفترض زيادة حجم التطبيق بعد مدة إلى أن صارت له مُدونته المستقلة عن مُدوّنتك، سيتوجّب عليك الآن نقل المقالات المُخصّصة للتّحديثات إلى هذه المُدوّنة الجديدة، وعلى فرض أنّ تلك المقالات تبدأ بكلمة "تحديث"، فكيف يُمكنك الحصول على هذه المقالات لنقلها دون المقالات الأخرى؟ فكما ترى، تلك طريقة واحدة لاستعمال هذا التّابع، إذ يُمكنك استعمال startswith للحصول على جميع المقالات التّي تبدأ بالمقطع، ومن ثم تنقلها إلى مكان آخر بشكل آلي. العامل in يُستعمل العامل in للتّأكّد من أن سلسلة نصيّة أو قائمة في بايثون تحتوي على مقطع نصي معيّن. فمثلا، سنتأكّد من أن الحرف a يتواجد بكلمة abcd، وسنتأكّد في المثال الآخر من أنّ اسم المُستخدم khalid يتواجد ضمن قائمة usernames، بالإضافة إلى حالتين لعدم استيفاء الشّرط والحصول على القيمة المنطقيّة False: >>> 'a' in 'abcd' True >>> 'e' in 'abcd' False >>> usernames = ['abdelhadi', 'ahmed', 'ibrahim', 'ali', 'khalid', 'mohammed'] >>> 'khalid' in usernames True >>> 'youssuf' in usernames False إليك استخدامًا للعامل in: قلنا في الفقرة السّابقة أنك تستطيع نقل جميع المقالات التّي تبدأ بالمقطع "تحديث" إلى مدونة أخرى، لكن ماذا لو كانت مقالاتنا التّي تبدأ بهذا المقطع تُشير إلى أكثر من تطبيق واحد؟ لنقل بأنّ المدونة حاليا بها تحديثات لتطبيقين، تطبيق "كلمة" وتطبيق آخر، وكلاهما يحصلان على تحديثات ومقال لكل تحديث، هذا يعني بأنّ الحصول على المقالات التّي تبدأ بالمقطع "تحديث" ستكون مزيجا بين تحديثات التّطبيقين، ولكنّنا نرغب بنقل مقالات التطبيق "كلمة" إلى مدونة "كلمة" دون المقالات الخاصّة بالتّطبيق الآخر. نستطيع هنا مزج كل من startswith وin وand للوصول إلى مُرادنا، بحيث نتأكّد من أنّ عناوين المقالات التّي سنحصل عليها تبدأ بكلمة "تحديث" وفي نفس الوقت تحتوي على اسم التّطبيق "كلمة" وإليك مثالا بسيطا: >>> kalima_update = 'تحديث في تطبيق كلمة' >>> app_update = 'تحديث في تطبيق آخر' >>> kalima_update.startswith('تحديث') and 'كلمة' in kalima_update True >>> app_update.startswith('تحديث') and 'كلمة' in app_update False >>> app_update.startswith('تحديث') True >>> 'كلمة' in kalima_update True >>> 'كلمة' in app_update False كما تُلاحظ، استخدمنا كلًا من startswith مع and وin لنستطيع ترشيح عنوان المقال الذي يحمل تحديثا للتطبيق "كلمة" من العنوان الآخر. هذا مجرّد مثال على كيفيّة الاستفادة من مختلف الأدوات التّي توفّرها لنا لغة بايثون وكيفيّة مزجها لحل مشاكل تحدث على الواقع، وسنرى فيما بعد كيفيّة استخدام هذه الجمل التّي تُرجع حاليا القيمتين True وFalse فقط لترشيح بياناتنا في قاعدة البيانات باستعمال SQLAlchemy. التابع endswith يُطبّق التّابع endswith على السّلسلة النّصيّة للتأكّد من أنّ سلسلة نصيّة تنتهي بمقطع معيّن، على سبيل المثال، يُمكن أن نتأكّد من أنّ بريدا إلكترونيّا ينتهي بـexample.com كما يلي: >>> 'abdelhadi@example.com'.endswith('example.com') True >>> 'abdelhadi@gmail.com'.endswith('example.com') False لاحظ أن البريد الإلكتروني الأول ينتهي بـexample.com لذا تحقّق الشّرط وأرجع مُفسّر بايثون القيمة True، أمّا الثّاني فينتهي بـgmail.com وليس example.com لذا أُرجِعّت القيمة المنطقيّة False. إن تساءلت عن الفائدة من هذا الأمر على الواقع، فإليك السيناريو التّالي: لنقل مثلا بأنّ خدمة example.com ستلغي خدمات بريدها الإلكتروني وسيتوجّب عليك تنبيه جميع مُستخدميك لتنصحهم بالانتقال إلى خدمة بريد إلكتروني أخرى، يُمكنك مثلا أن تستعمل هذه الطّريقة للحصول على جميع المُستخدمين الذين سجّلوا ببريد إلكتروني من هذه الخدمة، ومن ثم تستطيع تنبيههم وحدهم دون المُستخدمين الآخرين. لا شك أن هناك أمثلة كثيرة أخرى على الواقع، وستجد تحديّات ومشاكل يُمكنك حلّها بمثل هذه الأدوات التّي توفّرها لغة بايثون. ترشيح البيانات التي نحصل عليها من قاعدة البيانات باستعمال شروط منطقية عبر مكتبة SQLAlchemy في السّابق، كنّا نستعمل التّابع filter_by للحصول على سجل أو عدّة سجلّات عند تَطابُقِ قيمة عمود معيّن مع القيمة المُمرّرة، فمثلا لو أردنا الحصول على مُستخدم يُطابق اسمه السّلسلة النّصيّة khalid يُمكن أن نستعمل التّابع filter_by مع تمرير السّلسلة النّصيّة إلى المعامل name والذي يمثّل اسم العمود في هذه الحالة: >>> from project.models import User >>> User.query.filter_by(name = 'khalid').first() <username: khalid | email: khalid@example.com > في الشّيفرة أعلاه، كل ما نفعله هو طلب سجل من قاعدة البيانات في كل مكان تُساوي فيه قيمة العمود name السّلسلة النّصيّة "khalid”. أي كأنّنا نقول: أعطني السّجل الذي يُرجع طَلبُه القيمة المنطقيّة True عند تنفيذ الجملة التّاليّة: User.name == 'khalid' وبالمثل يُمكن أن تستخدم filter_by مع بقيّة الأعمدة. يُعد التّابع fliter_by جيدًا في حالات كثيرة كالوصول إلى مستخدم عن طريق اسمه أو بريده الإلكتروني، أو حتى مقال عبر عنوانه، لكن ماذا لو أردنا إجراء عمليّات أكثر تقدّما، مثل الحصول على السّجلات التّي تبدأ أو تنتهي بمقطع نصي معيّن أو تلك التّي تحتوي بداخلها على مقطع ما، أو الوصول إلى جميع السّجلات التّي تحقّق مجموعة من الشّروط أو بعضها؟ التابع filter في SQLAlchemy يُمكنك استعمال التّابع filter للحصول على نتائج تستوفي شروطا معيّنة تُمرّر إلى هذا التّابع كما يلي: >>> Table.query.filter(Table.column == value) لدينا ثلاثة متغيّرات يُمكنك استبدالها هنا لتتحصّل على نتيجتك المرغوبة: Table هو اسم الصّنف المرتبط بالجدول الذي ترغب بالحصول على البيانات منه، وcolumn يُمثّل اسم العمود، أمّا value فتمثّل القيمة. وكما تلاحظ، فهذا التّعبير شبيه جدّا بالتّابع filter_by لأنّ الشّرط هو ما إذا كانت قيمة العمود تُساوي كذا. أمّا للوصول إلى نتائج متنوّعة فتستطيع استبدال العامل == لكتابة شروط منطقيّة أخرى، وإليك بعض الأمثلة: Table.query.filter(Table.column == value) Table.query.filter(Table.column < value) Table.query.filter(Table.column > value) Table.query.filter(Table.column.endswith(value)) Table.query.filter(Table.column.startswith(value)) يُمكنك أن تعوض المتغيّر value بقيمة عمود آخر من الجدول: Table.query.filter(Table.column == Table.column2) Table.query.filter(Table.column.startswith( Table.column2)) بعد ترشيح السّجلات، يُمكنك بعد ذلك تطبيق كل من all أو first على النّتيجة. لاحظ في هذا المثال كيف أنّ كلّا من filter و filter_by يرجعان نفس النّتيجة: >>> User.query.filter_by(name = 'khalid').first() <username: khalid | email: khalid@example.com > >>> User.query.filter(User.name == 'khalid').first() <username: khalid | email: khalid@example.com > وكذلك يتّبع get نفس المنهج لكنّه لا يحتاج إلى التّابع first لأنّ المعروف أنّ المفتاح الأولي لا يُمكن أن يحمل من طرف أكثر من سجل واحد: >>> User.query.filter_by(id = 3).first() <username: dyouri | email: dyouri@example.com > >>> User.query.get(3) <username: dyouri | email: dyouri@example.com > >>> User.query.filter(User.id == 3).first() <username: dyouri | email: dyouri@example.com > ترشيح السجلات عن طريق in قد تُفكّر في تطبيق العامل in للحصول على سجلات تتواجد إحدى قيم أعمدتها في قائمة ما، لكنّ SQLAlchemy لن يقبل العامل in الذي توفّره لنا لغة بايثون، وعليه لن تحصل على أيّة نتيجة. يُمكنك تنفيذ التّابع in_ في SQLAlchemy مع تمرير قائمة من القيم كما يلي: >>> User.query.filter(User.name.in_(['abdelhadi', 'user4', 'user5'])).all() [<username: abdelhadi | email: abdelhadi@example.com >, <username: user4 | email: user4@example.com >, <username: user5 | email: user5@example.com >] لاحظ كيف طبّقنا التّابع in_ كقيمة ممرّرة للتّابع filter: Table.column.in_(list) أمّا كل من startswith و endswith فلا تتغيّر ويُمكنك استخدامها كما وضّحت سابقا: >>> User.query.filter(User.name.startswith('abd')).first() <username: abdelhadi | email: abdelhadi@example.com > >>> User.query.filter(User.name.endswith('lid')).first() <username: khalid | email: khalid@example.com > تعلّمنا الآن كيفيّة الحصول على سجلّات تبدأ أو تنتهي أحد قيم أعمدتها بمقطع معيّن، لكن ماذا لو أردنا الحصول على السّجلات التي تحتوي على مقطع معيّن في أي مكان من النّص؟ توفّر لنا مكتبة SQLAlchemy طريقة للحصول على السّجلات التّي تحتوي على مقطع معيّن بغض النّظر عن مكان تواجد هذا المقطع (سواء كان في بداية السّلسلة النّصيّة أو وسطها أو نهايتها)، وذلك باستعمال contains: >>> User.query.filter(User.name.contains('d')).all() [<username: khalid | email: khalid@example.com >, <username: dyouri | email: dyouri@example.com >, <username: abdelhadi | email: abdelhadi@example.com >] كما تلاحظ فقد حصلنا على جميع المُستخدمين الذين تحتوي أسماءهم على الحرف d سواء أكان في بداية الاسم أو في آخره أو وسطه. ترشيح السجلات بأكثر من شرط واحد قد تود عند استعمال filter أن تحصل على نتائج تُحقّق أكثر من شرط واحد، وعلى سجل أو سجلّات تستوفي العديد من الشّروط (مثل الحصول على كل مُستخدم انضمّ قبل شهر واحد وأضاف أكثر من 10 مقالات). وغالبًا ستستعمل and التّي توفّرها لغة بايثون، لكن كما الحال مع in فمكتبة SQLAlchemy توفّر بديلا لها، والبديل هو الدّالة and_ في SQLAlchemy والتّي نصل إليها عن طريق الكائن db. إليك مثالا لطريقة القيام بالأمر: >>> user = User.query.filter(db.and_(condition1, condition2)) مع استبدال condition1 بالشّرط الأول و condition2 بالشّرط الثّاني. وبما أنّ and_ شبيهة بجملة and في لغة بايثون فالمتوقّع أنّ حالة تحقّق الشّرطين معا هي الحالة الوحيدة التي يُمكن أن تُرجع نتيجة من الجدول: >>> True and True True >>> False and False False >>> False and True False >>> True and False False لنفترض أنّنا نُريد الحصول على أول مُستخدم يبدأ اسمه بكلمة abd وفي نفس الوقت يكون رقم مُعرّفه أكبر من الرّقم 1: >>> from project import db >>> user = User.query.filter( ... db.and_(User.name.startswith('abd'), ... User.id > 1)).first() >>> user <username: abdelhadi | email: abdelhadi@example.com > كما تُلاحظ، حصلنا على المُستخدم عبدالهادي لأنّه يستوفي الشّرطين، فرقم المعرفّ الخاص بالمُستخدم هو 2 أي أنّه أكبر من 1، ثمّ إنّ اسم المُستخدم يبدأ بالمقطع abd بكل تأكيد. يُمكنك كذلك استخدام filter أكثر من مرّة لتطبيق العديد من الشّروط: >>> user = User.query.filter( ... User.name.startswith('abd')).filter( ... User.id > 1).first() >>> user <username: abdelhadi | email: abdelhadi@example.com > >>> user.id 2 لاحظ هنا بأنّ النّتيجة هي نفسها، أي أنّ كلّا من db.and_ و استعمال filter أكثر من مرّة له نفس المفعول وشخصيّا أرى أنّ استعمال db.and_ أفضل وأسهل. يُمكنك كذلك استخدام الدّالّة or_ للحصول على شرط يستوفي إمّا هذا الشّرط أو ذاك، وهو شبيه بجملة or في بايثون، وإليك تذكيرا بسيطا: >>> True or True True >>> False or False False >>> False or True True >>> True or False True يعني لو أردنا الحصول على مُستخدم ينتهي اسمه بالحرف d أو رقم مُعرّفه يُساوي 10 لكتبنا استعلامنا كما يلي: >>> user = User.query.filter( db.or_(User.name.endswith('d'), User.id == 10)).first() >>> user <username: khalid | email: khalid@example.com > >>> user.id 1 لاحظ بأنّ الشّرط الثّاني خاطئ، ومع ذلك حصلنا على المُستخدم خالد لأنّ الشّرط الأول صحيح، لو استخدمت and_ هنا لكانت النّتيجة عبارة عن القيمة None أو بمعنى آخر لا نتيجة لهذا الاستعلام وهذا لأنّ الشّرط الثّاني خاطئ. مزج and و or معًا كما رأينا سابقا، يُمكنك وضع شروط معقّدة بمزج and و or أكثر من مرّة داخل بعضها البعض كما يلي: >>> (True and True) or (False and True) True الشّرط الأول في الشيفرة أعلاه ينتِج القيمة True، والثّاني يُنتج القيمة False، يعني كأنّنا نقول: True or False ومن ثم فالخرج هو القيمة المنطقيّة True. يُمكنك أن تأخذ هذا المبدأ إلى أبعد الحدود لتكتب شروطًا أكثر تعقيدًا: >>> ((True and True) or (False and True)) and ((True and True) and (False and True)) False >>> (True and True) or (False and True) True >>> (True and True) and (False and True) False أعتقد بأنّني بالغت في مزج كل هذه الشّروط، لكنّني أشعر بأنّ فهم طريقة العمل هذه مُهمّة، خاصّة إن كنت تخطّط لقراءة شيفرة مشروع مفتوح المصدر. قد لا تحتاج أبدا إلى مزج كل من or و and لإنشاء شروط معقّدة في SQLAlchemy، لكنّي رأيت أن أمنحك مثالا لكيفيّة القيام بالأمر، ولا بأس إن لم تفهم المثال الآن إذ أنّه يختلف بعض الشّيء عمّا تطرّقنا إليه سابقا، وستستطيع أن تفهمه ببعض من المُمارسة. إليك مثالا على كيفيّة مزج and و or في SQLAlchemy: db.and_( db.or_( condition1, condition2 ), db.and_( condition3, condition4 ) ) لاحظ بأنّك تستطيع وضع الشّروط في مُتغيّرات لتعمل الشّيفرة السّابقة، مثلا: condition1 = User.name == 'khalid' condition2 = User.name == 'username' condition3 = User.id < 10 condition4 = User.email.endswith('example.com') هذا كأنّنا نقول: (condition1 or condition2) and (condition3 and condition4) إن لاحظت بيانات المُستخدم khalid فستدرك بأنّ هذه الشّروط تتحقّق فيه. وفي الشّيفرة التّاليّة، أضع الشّروط في مُتغيّر باسم condition لأمرّرها فيما بعد إلى التّابع filter. condition = db.and_( db.or_( User.name == 'khalid', # True User.name == 'username' # False ), db.and_( User.id < 10, # True User.email.endswith('example.com') # True ) ) لنجرّب الآن تمرير هذا الشّرط إلى مكتبة SQLAlchemy وللنَنظر هل هناك من مُستخدم يحقّق الشّروط المذكورة: >>> User.query.filter(condition).all() [<username: khalid | email: khalid@example.com >] جيد! كل شيء سار كما هو متوقّع، فالمُستخدم خالد هو الوحيد الذي يُحقّق جميع الشّروط. خاتمة تعرّفنا في هذا الدّرس على كيفيّة استعمال التابع filter في مكتبة SQLAlchemy لترشيح السّجلات عند الحصول عليها من قاعدة البيانات باستعمال شروط منطقيّة متقدّمة. وسنتعرّف في الدرس القادم على كيفيّة استغلال ما تعلّمناه من حيل للتّعامل مع قواعد البيانات بطريقة أفضل.
  4. بعد أن تعلمنا كيفية الحصول على السجلات على حدة باستعمال أرقام مُعرّفاتها إضافة إلى كيفية الحصول على جميع السجلات من جدول معيّن والحصول عليها حسب اسم عمود مُعيّن باستعمال التّابع filter_by، سنتعرّف في هذا الدّرس على كيفيّة ترتيب السّجلات، والحد من عدد السجلّات التي نحصل عليها من كل استعلام، وكذلك كيفيّة الحصول على سجلّات بشكل عشوائيّ، إضافة إلى مقدمة قصيرة في استعمال طرق مختلفة لترشيح النّتائج بشروط منطقيّة. ترتيب السجلات حسب قيم عمود معين تُرتَّب السجلات عادة في تطبيقات الويب حسب قيمة معيّنة، فقد تجد مقالات في مدونة ما مرتّبة حسب تاريخ إضافتها ومن ثم تحصل على آخر المقالات المُضافة أولا بأول، كذلك يمكن ترتيب المُستخدمين حسب أسمائهم أبجديا، وهناك العديد من الخيارات الأخرى لترتيب البيانات كما سترى بعد قليل. ولترتيب البيانات حسب عمود معيّن، يمكنك استعمال التّابع order_by بعد query مع تمرير اسم العمود في الجدول بإحدى طريقتين، إمّا كسلسلة نصيّة أو باستخدام الصّنف ثمّ اسم العمود مع فصلهما بنقطة كما يلي: >>> from project.models import User >>> users1 = User.query.order_by("created_date").all() >>> users2 = User.query.order_by(User.created_date).all() >>> users1 == users2 True كما تُلاحظ، نفّذنا استعلامًا عن المُستخدمين ثمّ رتبناهم حسب تاريخ الإضافة بكلتا الطّريقتين، ثمّ حصلنا على جميع النّتائج بالتّابع all، بعدها نتأكد أنّ الطّريقتين تؤديان إلى نفس النّتيجة، والنّتيجة أنّ هذا الأمر صحيح. لنحصل الآن على أول نتيجة ولننظر هل التّرتيب صحيح أم لا: >>> user = User.query.order_by(User.created_date).first() >>> user <username: khalid | email: khalid@example.com > التّرتيب صحيح كما ترى، فخالد هو أوّل مُستخدم أضفناه إلى قاعدة البيانات. طريقة التّرتيب حسب بقيّة الأعمدة بسيطة كذلك: >>> User.query.order_by(User.id).first() <username: khalid | email: khalid@example.com > >>> User.query.order_by(User.name).first() <username: abdelhadi | email: abdelhadi@example.com > >>> User.query.order_by(User.email).first() <username: abdelhadi | email: abdelhadi@example.com > لنطبّق على جدول المقالات كذلك: >>> from project.models import Post >>> Post.query.order_by(Post.id).first() <title Post 1 | post 1 content > >>> Post.query.order_by(Post.title).first() <title another post from abdelhadi | some other content for the post > >>> Post.query.order_by(Post.created_date).first() <title Post 1 | post 1 content > لاحظ أنّ ترتيب القيم وفق عمودٍ يحتوي على سلسلة نصيّة يكون أبجديا، أمّا الأعمدة التّي تحمل أعدادا كقيمة فتُرتّب من أصغر عدد إلى الأكبر، أمّا التّاريخ فحسب الأقدم إلى الأحدث. ملاحظة: أستخدمُ التّابع first فقط لكي لا يكون الخرج كبيرا، ولو أردتَ جميع السّجلات على شكل قائمة فتستطيع استخدام التّابع all. الترتيب عكسيّا يبدو بأنّك لاحظت بأنّنا لم نصل بعد إلى كيفيّة ترتيب المقالات حسب الأحدث، وذلك لأنّ التّرتيب حسب التّاريخ يُرجع المقالات الأقدم. يُمكننا عكس التّرتيب في SQLAlchemy للحصول على المقالات الأحدث والمُستخدمين المُنضمّين حديثا، وذلك من بإضافة التّابع desc بعد اسم العمود: >>> User.query.order_by(User.created_date.desc()).first() <username: user12 | email: user12@example.com > لاحظ بأنّنا حصلنا على المُستخدم user12 وهو آخر مُستخدم أضيف باستعمال حلقة التّكرار في درس سابق. إن استعملت الطّريقة الأخرى في التّرتيب فتستطيع عكسه كما يلي: User.query.order_by("created_date desc") هذا يعني أنّ السّطرين التّاليين سيُرجعان قائمتين، القائمة الأولى عبارة عن جميع المُستخدمين مُرتّبين حسب آخر مُستخدم أضيف إلى أقدم مُستخدم، والقائمة التّاليّة عبارة عن جميع المقالات المُضافة بنفس التّرتيب. >>> Post.query.order_by(Post.created_date.desc()).all() >>> User.query.order_by(User.created_date.desc()).all() هكذا استطعنا الوصول إلى قائمة بالمقالات المُضافة حديثا وآخر المُستخدمين المُنضمّين، لكنّنا لا نمتلك سوى طريقتين للحصول على النّتائج، إمّا بالحصول على أول نتيجة أو جميع النّتائج، فكيف يُمكن الحصول على عدد معيّن فقط من السّجلات؟ تحديد الناتج بعدد معين من السجلات لنقل بأنّ تطبيقنا وصل إلى آلاف المقالات والمُستخدمين، وبالتّالي فعرض جميع المُستخدمين المُنضمّين حديثا وأحدث المقالات سيجعل صفحتنا طويلة وقد يأخذ وقتا كبيرا، هذا بالإضافة إلى أنّه أمر غير ضروري. قطعًا يُمكنك الحصول على جميع المقالات على شكل قائمة واستعمال لغة بايثون لتحديد جزء منها فقط (أول خمسة عناصر مثلا)، لكنّ الحصول على جميع السّجلات وإسنادها إلى مُتغيّر قد يأخذ مساحة كبيرة، وكذلك ستستغرق مُعالجتها وقتًا طويلا. وبما أنّ قواعد بيانات SQL تُمكّننا من الحصول على عدد معيّن من السجلّات بجملة LIMIT فمن الأفضل أن نستخدمها عوضا عن معالجة آلاف البيانات لعرض جزء صغير منها. ولتحديد عدد معيّن من النّتائج في SQLAlchemy، استعمل التّابع limit مع تمرير عدد صحيح كمُعامل ليُمثّل عدد النّتائج، فمثلا للحصول على خمسة مقالات وخمسة مُستخدمين فقط يُمكنك استخدام التّابع limit() مع تمرير العدد 5: >>> five_posts = Post.query.limit(5).all() >>> five_users = User.query.limit(5).all() لاحظ أنّنا نستخدم التّابع all مرة أخرى للحصول على جميع النّتائج. سيحمل الآن كلا المُتغيّرينfive_posts و five_users خمسة سجلّات في كل متغيّر، ويُمكنك أن تصل إلى كل سجل وبياناته من خلال استخدام حلقة for كما في السّابق. كذلك يُمكنك تغيير المُعامل المُمرّر إلى التّابع limit إلى أي عدد تريده: Post.query.limit(10).all() Post.query.limit(7).all() Post.query.limit(8).all() Post.query.limit(1).first() لعلك لاحظت أننا نحصل في آخر سطر على سجل واحد فقط، يُمكنك استعمال هذه الطّريقة إن لم تكن مُهتما بالحصول على سجل مُعيّن من قاعدة البيانات (باستعمال إمّا get أو filter_by). مزج ما سبق لنتائج أكثر دقة الشّيء المُميّز في قواعد بيانات SQL هو أنّها تُوفّر لنا طريقة لمزج العديد من الشّروط في استعلام واحد، فمثلًا يُمكنك إخبار قاعدة البيانات أنّك تريد الحصول على مقالات مُستخدم رقم مُعرّفه كذا، لكن بدلًا من جلب جميع مقالاته، أعطني مقالين فقط. وتستطيع مزج التوابع في SQLAlchemy للحصول على هذه النّتيجة، فالسّطر التّالي مثلًا يستعلم عن المقالات من المُستخدم abdelhadi عن طريق رقم مُعرّفه، ثمّ يطبّق حدّا على النّتائج للحصول على سجلّين فقط: >>> posts_from_abdelhadi = Post.query.filter_by(author_id=2).limit(2).all() هنا مزجنا كلّا من التّابعين filter_by وlimit، الأول للحصول على المقالات التّي يحمل عمود author_id فيها القيمة 2 (أي رقم مُعرّف المُستخدم عبد الهادي)، والثاني لحدّ النّتائج في مقالين فقط. لاحظ أن التّابع all هو آخر ما يُضاف إلى الاستعلام للحصول على قائمة بجميع النّتائج، لذا لا تضعه والتّابع first في وسط الشّيفرة. وهذا مثال آخر نقوم فيه بترتيب المُستخدمين حسب تاريخ إضافتهم عكسيا ثمّ نُحدّد ثلاثة نتائج. >>> User.query.order_by(User.created_date.desc()).limit(3).all() [<username: user12 | email: user12@example.com >, <username: user11 | email: user11@example.com >, <username: user10 | email: user10@example.com >] تلاحظ في الشّيفرة أعلاه أنّ الخرج قد رُتّب عكسيا حسب تاريخ الإضافة ثمّ حُدّد بثلاثة سجلّات فقط، يُمكنك تطبيق هذه الطّريقة في عدّة مواضع لعرض عدد محدّد من المُستخدمين المُنضمّين حديثا أو ترتيب المقالات حسب تاريخ إضافتها وتحديد عددها، وكذلك يمكنك إضافة تقييم لكل مقال وترتب المقالات حسب الأفضل (أي الأكثر تقييما إيجابيا). يُمكنك مزج ما تشاء من التّوابع للحصول على نتائج أكثر دقّة من قاعدة بياناتك، لكن احترس، فمكتبة SQLAlchemy لا تسمح بتطبيق بعض التّوابع إلّا بترتيب خاص، فمثلا لا يُمكنك وضع حدّ لعدد النّتائج قبل ترتيبها، أي أنّ الشّيفرة التّاليّة خاطئة: User.query.limit(3).order_by(User.created_date).all() إن جرّبت تنفيذ الشّيفرة فستحصل على خطأ يُشبه نصّه ما يلي: sqlalchemy.exc.InvalidRequestError: Query.order_by() being called on a Query which already has LIMIT or OFFSET applied. To modify the row-limited results of a Query, call from_self() first. Otherwise, call order_by() before limit() or offset() are applied. لذا تأكّد دائما من أنّك تُطبّق التّابع limit في مكانه الصّحيح، والأغلب أنّك ستحتاج إليه في آخر الشّيفرة لتحديد عدد النّتائج بعد إجراء العمليّات الأخرى. يُمكن أن تواجهك بعض المشاكل من هذا القبيل، فإن واجهتك فاقرأ نص الخطأ أولا ثمّ ابحث عن حل له في الويب، فإن لم تجد فيمكنك طرح سؤال في قسم الأسئلة والأجوبة في الأكاديميّة، وتأكّد أن تضع نص الخطأ كاملا والجزء الذي سبّب الخطأ من شيفرتك. الحصول على سجلات بشكل عشوائي يُمكن أن تجد نفسك بحاجة إلى بناء تطبيق يتطلّب الحصول على سجلّات من قاعدة البيانات بشكل عشوائي، صحيح أنك تستطيع استخدام بايثون والوحدة random للقيام بالأمر، لكن تذكر قولي أن استخدام بايثون لمعالجة بيانات كبيرة فكرة سيّئة، لذا لا تقم بذلك. الحصول على سجلّات بشكل عشوائي ممكن، لكن للأسف فالطّريقة مُختلفة في قواعد بيانات SQL المُتعدّدة، وبما أنّنا نستعمل هنا قاعدة البيانات PostgreSQL فسأشرح طريقة القيام بالأمر فيها فقط، وإن أردت الحصول على كيفيّة القيام بالأمر في قواعد البيانات الأخرى فيُمكنك الرّجوع إلى توثيق SQLAlchemy. للحصول على سجلّات عشوائيّة في كلّ مرّة ننفّذ فيها الاستعلام في SQLAlchemy و PostgreSQL (نفس الطّريقة تعمل على SQLite كذلك) يُمكنك استعمال التّابع func المتواجد في الكائنdb الذي أنشأناه بمُساعدة إضافة Flask-SQLAlchemy ، لذا إن لم تستورده فلن تعمل الشّيفرة: >>> Post.query.order_by(db.func.random()).first() <title Post from user11 | Content 11 > >>> Post.query.order_by(db.func.random()).first() <title Post from user12 | Content 12 > >>> Post.query.order_by(db.func.random()).first() <title Post from user7 | Content 7 > الشّيفرة الأساسيّة لترتيب السّجلات بشكل عشوائي هي باستعمال التّابع func المتواجد في الكائن db ثمّ التّابع random مع تمرير كل شيء إلى التّابع order_by كما يلي: order_by(db.func.random()) لاحظ بأنّنا في كل مرّة نقوم فيها بتنفيذ الشّيفرة تختلف النّتيجة بشكل غير مُتوقّع. أيضًا، لعلك لاحظت أننا نقوم هنا بتطبيق التّابع first للحصول على أول سجل، ويُمكنك قطعًا الوصول إلى جميع السّجلات عبر التّابع all، وكذلك تحديد عدد من السّجلات بالتّابع limit بعد ترتيبها عشوائيّا: >>> User.query.order_by(db.func.random()).limit(3).all() [<username: user5 | email: user5@example.com >, <username: abdelhadi | email: abdelhadi@example.com >, <username: user11 | email: user11@example.com >] >>> User.query.order_by(db.func.random()).limit(3).all() [<username: user9 | email: user9@example.com >, <username: abdelhadi | email: abdelhadi@example.com >, <username: dyouri | email: dyouri@example.com >] >>> User.query.order_by(db.func.random()).limit(3).all() [<username: user8 | email: user8@example.com >, <username: user5 | email: user5@example.com >, <username: user6 | email: user6@example.com >] النّتائج عشوائيّة كما ترى، وكل نتيجة تختلف عن النّتيجة السّابقة، لذا تستطيع الآن التّفكير في إضافة قسم بالتّطبيق الذي سنبنيه لعرض مقالات ومُستخدمين بشكل عشوائي لتُساعد زوارك على اكتشاف المقالات والمُستخدمين الذين لا يُمكن أن تصل إليهم إلا بهذه الطّريقة (مثل المقالات التي مرت عليها سنوات أو مُستخدمون لم يُشاركوا منذ مدّة طويلة حتى اختفوا من الصّفحات الرّئيسية). ترشيح السجلات حسب شروط منطقية لم تعد تطبيقات الويب بسيطة كالسابق حين كان المستخدم يطلب صفحة المقال فيحصل عليها، أما الآن فقد كثرت الخيارات المنطقية في التطبيقات، فبعد أن كانت خيارات المستخدم محدودة جدًا بين الحصول على المقالات بترتيب معيّن أو مقالات كاتب محدد، صارت تطبيقات الويب تُتيح للمُستخدم حريّة كبيرة في كيفيّة حصوله على النّتائج. فالصّفحة والنّتائج المعروضة عليها يُمكن أن تتغيّر طبقا لعدّة شروط، وهناك خيارات كثيرة للحصول على نتائج دقيقة، فمثلا يُمكن أن يطلب المُستخدم مقالات أضيفت في شهر معيّن، أو الحصول على جميع المقالات المُضافة من السّنة الفلانيّة إلى يومنا الحالي، أو كل مقال يبدأ عنوانه بكلمة "تحديث" وفي نفس الوقت يكون كاتبه المُستخدم فلان بالإضافة إلى أنّه أضيف قبل 5 أيام مع وجوب تجاوزه المئة تعليق وهكذا…، وإن نظرنا إلى آخر مثال وحلّلناه فسنجد أنّه يحتوي على العديد من الشّروط المنطقيّة: فالمقال يبدأ بكلمة "تحديث” وكاتب المقال هو المُستخدم "فلان” وتاريخ إضافته قبل خمسة أيام وعدد تعليقاته مئة أو أكثر. لاحظ الآن كلّا من حرفي "و” و"أو” في التّحليل السّابق، هل يُذكّرك ذلك بشيء معيّن في لغة بايثون؟ الجواب هو المعاملات المنطقيّة and وor، التّي تعمل كما يلي: and: إن تحقّق هذا الشّرط و الشّرط الآخر فالجملة صحيحة، ولو تحقّق شرط دون آخر أو لم يتحقّق أي شرط فالجملة خاطئة. >>> 1 == 1 and 2 == 2 True >>> 1 == 2 and 2 == 2 False >>> 1 == 2 and 2 == 3 False or: إن تحقّق هذا الشّرط أو الشّرط الآخر فالجملة صحيحة إن كانت جميع الشّروط خاطئة فالجملة خاطئة. >>> 1 == 1 or 2 == 3 True >>> (1 == 1 or 2 == 3) True >>> 1 == 2 or 2 == 3 False لاحظ أن استعمال الأقواس أمر اختياري وستصل إلى نفس النّتيجة، لكن الأقواس مهمّة إن كنت تستعمل أكثر من عامل منطقي للتأكد من أنّ جملة تحتوي على العديد من الشّروط صحيحة بالفعل. يُمكنك استعمال أحدها أكثر من مرّة: >>> 1 == 2 or 2 == 3 or 3 == 3 >>> True >>> 1 == 2 or 2 == 3 or 3 == 4 >>> False >>> 2 == 2 and 3 == 3 and 4 == 4 >>> True يُمكن مزج هذه الشّروط المنطقيّة الواحد داخل الآخر لتصل إلى نتائج أكثر دقّة أو نتائج تستوفي شروطا عديدة والقوانين هنا كقوانين الرّياضيّات البسيطة (ما بداخل القوسين منعزل عن بقيّة الجملة): >>> (1 == 2 or 6 == 3) False >>> (1 == 1 and 2 == 2) True >>> (1 == 1 and 2 == 2) or 1 == 2 True >>> (1 == 1 and 2 == 2) and 1 == 2 False >>> (1 == 1 and 2 == 2) and (1 == 2 or 6 == 3) False >>> (1 == 1 and 2 == 2) and (1 == 2 or 6 == 6) True >>> (1 == 1 and 2 == 2) and (1 == 2 or 6 == 3) False >>> (1 == 1 and 2 == 2) or (1 == 2 or 6 == 3) True يُمكنك كذلك اختبار هذه القواعد عبر استعمال كل من القيمتين True و False فقط: >>> True or False True >>> True and True True >>> False or False False >>> False and False False خاتمة تعرّفنا في هذا الدّرس على كيفيّة استعمال مكتبة SQLAlchemy لترتيب السّجلات والحدّ من عددها والحصول على سجلات عشوائيّا من قاعدة البيانات، و كذلك ألقينا نظرة على كيفيّة استعمال المعامليْن and وor في لغة بايثون، واللذيْن يُعدان أساسًا سنبني عليه بقية معارفنا في الدّروس القادمة للحصول على نتائج أكثر دقّة من قاعدة البيانات، ومن ثم إتاحة إمكانية ترشيح النّتائج لتجربة استخدام أكثر أريحيّة.
  5. تعرّفنا في الدّروس السّابقة على ماهيّة أداة SQLAlchemy وكيفيّة الاعتماد عليها في تطبيقات Flask باستعمال إضافة Flask-SQLAlchemy للتعامل مع قواعد بيانات SQL، وسنتعرّف في هذا الدّرس على كيفيّة ملء قاعدة البيانات التي أنشأناها ببيانات تجريبيّة وكذلك كيفيّة الحصول على السّجلات من قاعدة البيانات باستعمال لغة بايثون. إضافة عدة سجلات في نفس الوقت قد ترغب أحيانا في إضافة أكثر من سجل في نفس الوقت، إمّا لأنّك تحصل على الكثير من السّجلات من نفس الزّائر في صفحتك أو أنّك ترغب في توفير بعض الوقت والأسطر البرمجيّة، ولإضافة أكثر من كائن إلى الجلسة، استعمل التّابع add_all عوضا عن add، وذلك عبر تمرير قائمة من الكائنات إلى التّابع كما يلي: >>> user2 = User('abdelhadi', 'abdelhadi@example.com', 'secret_pass') >>> user3 = User('dyouri', 'dyouri@example.com', 'p_not_found') >>> users_list = [user2, user3] >>> db.session.add_all(users_list) >>> db.session.commit() كذلك يُمكنك تمرير القائمة مُباشرة دون إسنادها إلى متغيّر سابق: db.session.add_all([user2, user3]) إن عُدت الآن إلى أداة psql ونفّذت الاستعلام select للحصول على جميع المُستخدمين فستجد أنّ المُستخدمَيْن abdelhadi و dyouri قد أُضيفا إلى قاعدة البيانات بنجاح. لِنُضف بضعة مقالات لكل مُستخدم من المُستخدمين اللّذين أضفناهما للتو: >>> post2 = Post('a post from abdelhadi', 'content of a post', author_id = 2) >>> post3 = Post('another post from abdelhadi', 'some other content for the post', author_id = 2) >>> post4 = Post('a post from dyouri', 'other content', author_id = 3) >>> posts_list = [post2, post3, post4] >>> db.session.add_all(posts_list) >>> db.session.commit() لاحظ أنك إن أضفت وحذفت بضعة مُستخدمين من قاعدة بياناتك قبل تنفيذ الشّيفرة، فقد يكون مُعرّف كلا المُستخدمَيْن مغايرا لما حدّدته بالشّيفرة أعلاه، لذا تأكّد من أنّ الأرقام صحيحة. كذلك يمكنك إضافة المقالات والمُستخدمين بشكل مُختلط كما يلي: >>> db.session.add_all([user2, user3, post2, post3]) استغلال حلقة For لإضافة عدة سجلات سنحتاج في هذا الدّرس إلى العديد من السّجلات في قاعدة البيانات من أجل التجربة عليها سواء تعديلا أو قراءة أو حتى حذفًا، بإمكاني أن أطلب منك إضافة بضعة مُستخدمين وبضعة مقالات مُستفيدا مما تعلّمته سابقا (أتمنى أنّك فعلت ذلك بالفعل) لكن الأمر سيأخذ بعض الوقت، ونحن لا يهمنا أسماء المُستخدمين ولا بيانات المقالات، بل كل ما نُريده هو بيانات مزيّفة لأغراض تجريبيّة، لذا لمَ لا نستغل البرمجة في شيء مُفيد ونُضيف المُستخدمين والمقالات بحلقة for بحيث يكون لكل سجل من البيانات رقم يميزه عن السّجلات الأخرى، أي سنضيف مُستخدمين بالأسماء التّاليّة: user1, user2, user3, user4 ... جرب حل الاختبار دون النظر إلى الشيفرة التالية: اكتب حلقة لإضافة مُستخدمين ومقالات بالطّريقة السّابقة. الحلّ الشّيفرة التّي سنُضيف بها المُستخدمين ستكون كما يلي: for u in range(4, 13): name = 'user{}'.format(u) email = 'user{}@example.com'.format(u) password = 'password{}'.format(u) user = User(name, email, password) db.session.add(user) db.session.commit() وهذه لإضافة مقال لكل مُستخدم من المُستخدمين : for p in range(4, 13): title = 'Post from user{}'.format(p) content = 'Content {}'.format(p) author_id = p post = Post(title, content, author_id) db.session.add(post) db.session.commit() تفترض شيفرة المقالات أنّ آخر مُستخدم تمت إضافته أو حذفه من جدول المُستخدمين يمتلك الرقم المُعرّف 3، مما يعني أنّ المُستخدم user4 الذي يُعد أول مُستخدم يُضاف باستخدام حلقة التّكرار سيكون رقمُ مُعرّفه 4. مُلاحظة: سنحذف كثيرًا من المُستخدمين والمقالات التّي أضفناها للتو فيما بعد، لذا توجّب علينا إضافة هذا العدد، كما يجب عليك إدراك أنّ تطبيقات الويب الحاليّة تستطيع جمع كم هائل من البيانات دون أن يؤثّر ذلك على الأداء بشكل ملحوظ، لذا لا تخف من اختبار قاعدة بياناتك بحلقات التّكرار لإضافة وحذف العديد من البيانات. الحصول على البيانات، والطرق المختلفة للتحكم بالنّتائج إن اتّبعت ما سبق وأضفت المُستخدمين والمقالات التّي أضفناها لقاعدة البيانات فسيكون لديك الآن في قاعدة بياناتك العديد من المقالات والمُستخدمين، وسنتمكّن من الحصول على هذه البيانات لقراءتها والتّعامل معها بعمليّات CRUD الأخرى. والبيانات التّي تتواجد في قاعدة بياناتنا الآن هي كالآتي: Users: id | name | ----+-----------+ 1 | khalid | 2 | abdelhadi | 3 | dyouri | 4 | user4 | 5 | user5 | 6 | user6 | 7 | user7 | 8 | user8 | 9 | user9 | 10 | user10 | 11 | user11 | 12 | user12 | ----------------- Posts: id | title | ----+-----------------------------+ 1 | Post 1 | 2 | a post from abdelhadi | 3 | another post from abdelhadi | 4 | a post from dyouri | 5 | Post from user4 | 6 | Post from user5 | 7 | Post from user6 | 8 | Post from user7 | 9 | Post from user8 | 10 | Post from user9 | 11 | Post from user10 | 12 | Post from user11 | 13 | Post from user12 | مُجدّدا، تجاهلت الأعمدة الأخرى لأنّها غير مهمّة، كما أنّ الجدول سيكون كبيرا إن لم أتجاهلها، وتستطيع الحصول على البيانات كاملة إن شئت عبر أداة psql وأمريْ select السّابقين. لا بأس إن لم تمتلك نفس البيانات في قاعدة بياناتك، فالمهم أن تفهم العلاقة الأساسيّة بين كلّ مقال والمُستخدم الذي أضافه. أمّا إن كنت ترغب اتّباع هذا الدّرس بحذافيره، فنفِّذ الشيفرة التالية لحذف جميع بياناتك وإضافة البيانات أعلاه. from project import db from project.models import Post, User db.drop_all() db.session.commit() db.create_all() db.session.commit() user = User('khalid', 'khalid@example.com', 'password') user2 = User('abdelhadi', 'abdelhadi@example.com', 'secret_pass') user3 = User('dyouri', 'dyouri@example.com', 'p_not_found') post = Post('Post 1', 'post 1 content', author_id=1) post2 = Post('a post from abdelhadi', 'content of a post', author_id = 2) post3 = Post('another post from abdelhadi', 'some other content for the post', author_id = 2) post4 = Post('a post from dyouri', 'other content', author_id = 3) db.session.add_all([user, user2, user3, post, post2, post3, post4, ]) db.session.commit() for u in range(4, 13): name = 'user{}'.format(u) email = 'user{}@example.com'.format(u) password = 'password{}'.format(u) user = User(name, email, password) db.session.add(user) db.session.commit() for p in range(4, 13): title = 'Post from user{}'.format(p) content = 'Content {}'.format(p) author_id = p post = Post(title, content, author_id) db.session.add(post) db.session.commit() طريقة العمل الآن وبعد أن أدخلنا بعض المُستخدمين والمقالات إلى قاعدة البيانات، صار بإمكاننا الحصول على كل سجل وبيانات أعمدته، وسنحصل في أغلب الحالات على السّجل أو السّجلّات إن كانت مُتعدّدة ومن ثم إسنادها إلى مُتغيّر، فإن كان سجلّا واحدا (مثلا المُستخدم خالد) يُمكننا الوصول إلى بيانات الأعمدة ببساطة بإلحاق اسم العمود بالمُتغيّر الذي يحمل السّجل وفصلهما بنُقطة (user.name للحصول على اسم المُستخدم على سبيل المثال)، أمّا إن كان المُتغيّر يحمل سجلّات عديدة، فسنستخدم حلقة for بسيطة للدّوران حول السّجلات واستخراج بيانات كلّ سجل على حدة. تلك الطّريقة هي التي سنعتمد عليها في ما بعد، أمّا الاختلاف فسيكون في كيفيّة الحصول على السّجل (أو السّجلات) لإسنادها إلى المُتغيّر. الحصول على سجل سنستعمل خاصيّة الاستعلام (querying) في SQL للحصول على السّجلات، ويُوفّر لنا SQLAlchemy طريقة بسيطة للوصول إلى الاستعلام وبقيّة العمليّات عليه. >>> User.query.method الآن يُمكنك استبدال method بالعديد من التّوابع التّي تُوفّرها لنا مكتبة SQLAlchemy مثل all للحصول على جميع السّجلات من الجدول و filter_by لترشيح السّجلات والوصول إلى سجل يوافق شروطا معيّنة والمزيد من الطّرق المُتعدّدة التّي توفّرها قواعد بيانات SQL، وسنتطرّق إلى بعض أهم تلك الطّرق بالتّفصيل، وكما قلت من قبل فإني لن أشرح كل شيء توفّره المكتبة، لذا إن أردت الحصول على معلومة لم أذكرها فيُمكنك طرح سؤال في الأكاديميّة. كيفيّة الوصول إلى جداول قاعدة البيانات من مفسر لغة بايثون ذكرنا في الدروس السابقة أنه من أجل التعامل مع جدول معين فإن علينا استعمال اسم الصنف الخاص بالجدول والذي سبق وأنشأناه في ملفّ models.py. وسنبدأ أولًا بقراءة البيانات من جدول المُستخدمين، لكن يجب أن تستدعي الصّنف User أولًا من وحدة models كي نستطيع الوصول إليه. تأكد من أن البيئة الوهمية مُفعّلة ثمّ افتح مُفسّر لغة بايثون واكتب ما يلي: >>> from project.models import User بعد استيراد الصّنف User، ستتمكن من استخدامه للتعامل مع جدول المُستخدمين. الحصول على سجل برقم مُعرّفه أول طريقة قد تطرأ على ذهنك للحصول على سجلّ معيّن هي استخدام مفتاحه الأولي، أي رقم المُعرّف في أغلب الحالات، ذلك أن رقم المُعرّف يعبّر عن كلّ سجل بشكل فريد، فمن المُستحيل أن يحمل أكثر من مُستخدم مثلا رقم المُعرّف 1، وبالمثل في حالة المقالات. وللحصول على المُستخدم الذي يحمل رقم المُعرّف 1، استعمل التّابع get() بعد User.query مع تمرير الرّقم 1: >>> User.query.get(1) <username: khalid | email: khalid@example.com > كما تُلاحظ، حصلنا على نبذة عن المُستخدم ذو رقم المُعرّف 1 بمُجرّد أن قمنا بتنفيذ الشّيفرة، لهذا من المُهم أن يتواجد التّابع __repr__ في جداولك ولو لم يكن إضافته ضروريا. ولأنّنا سنقوم بالعديد من العمليّات على المُستخدم "خالد"، لنقم بإسناده إلى مُتغيّر باسم user ثم لننظر إلى بعض من البيانات الخاصّة بخالد: >>> user = User.query.get(1) >>> user.name u'khalid' >>> user.email u'khalid@example.com' >>> user.password u'password' >>> user.password == "password" True >>> user.password == "wrong_password" False لاحظ آخر سطرين حيث نقوم بإجراء عمليّة تحقّق بسيطة لتفهم أنّ البيانات التّي تحصل عليها مجرّد سلاسل نصيّة عاديّة، وأنّك تستطيع القيام بمُختلف العمليّات التّي يُمكنك استعمال بايثون لإنجازها. وبالمُناسبة، إن كنت ترغب بالحصول على أول سجل من الجدول فقط بغض النّظر عن رقم مُعرّفه يُمكنك استخدام التّابع first: >>> User.query.first() <username: khalid | email: khalid@example.com> الحصول على عدد السجلات داخل جدول للحصول على العدد الإجمالي للسجلات في جدول مُعين، استعمل التّابع count() كما يلي: >>> User.query.count() 12 يوضح المثال التالي كيفية الحصول على العدد الإجمالي للمقالات في قاعدة بياناتنا (بعد استيراد الصّنف Post بالطّبع): >>> from project.models import Post >>> Post.query.count() 13 وكما تُلاحظ، فالنتيجة عبارة عن عدد صحيح (Integer)، لذا يُمكنك التعامل معه بلغة بايثون بشكل عادي. الحصول على سجل بقيمة عمود معين لا يكفي رقم المعرِّف وحده لإجراء العمليات الشّائعة في تطبيقات الويب، عمليات مثل الحصول على مُستخدم عن طريق اسمه، أو الحصول على مقالات حسب تاريخ إنشائها، أو ترتيب المُستخدمين وفق قيم أعمدة مختلفة -تاريخ الانضمام، ترتيب المُستخدمين حسب أسمائهم أبجديًا-، والعديد من العمليات الأخرى التّي تستطيع القيام بها بلغة SQL في قواعد البيانات التّي تستخدمها. يُمكنك الحصول على سجلّ أو عدّة سجلّات حسب قيمة عمود معيّن باستخدام التّابع filter_by كما يلي: >>> result = Table.query.filter_by(column=value) حصلنا هنا على نتيجة عبر ترشيح النّتائج وفق العمود وقيمته، أي أنّك لو أردت أن تحصل على مُستخدم حسب اسمه فتستطيع استبدال column بname و value باسم المُستخدم. وإن أردت الحصول على القيم الفعليّة فاستعمل إمّا التّابع first للحصول على أول نتيجة أو التّابع all للحصول على جميع السّجلات داخل قائمة. >>> result.first() >>> result.all() كتطبيق على هذا المفهوم، لنحصل على المُستخدم عبد الهادي ونُسنده إلى مُتغيّر باسم user: user = User.query.filter_by(name='abdelhadi').first() لاحظ بأنّنا نقوم بإضافة التّابع first لنحصل على المُستخدم الأول، وذلك لأنّ التّابع filter_by يُمكن أن يُرجع أكثر من سجل واحد. فمثلا لو كان لمقالين نفس العنوان -Post على سبيل المثال- فيُمكن أن يُرجع الاستعلام التّالي مقالين: >>> Post.query.filter_by(title='Post') لذا فمن أجل الحصول عليها جميعًا، عليك إلحاق ما سبق بالتّابع all، أمّا لو أردت أول مقال فقط، فيُمكنك استعمال التّابع first. بعد أن حصلنا على المُستخدم عبد الهادي وأسندناه إلى المُتغيّر user، نستطيع الحصول على اسمه وكلمة مروره وبقيّة بياناته كما ذكرنا سابقا، ويُمكننا كذلك الحصول على مقالاته، وقد أضفنا مقالين كما تعلم برقم المُعرّف الخاص بهذا المُستخدم، لذا لنحصل على مقالات المُستخدم عبد الهادي باستخدام التّابع posts الذي ذكرته سابقا. إليك الآن أهم طريقتين لمُعالجة المقالات: للوصول إلى المقال الأول وقيّمه: >>> post1 = user.posts.first() >>> post1 <title a post from abdelhadi | content of a post > >>> post1.title u'a post from abdelhadi' >>> post1.content u'content of a post' وللوصول إلى جميع المقالات على شكل قائمة يُمكنك الدّوران حولها باستعمال حلقة for، يُمكنك استخدام التّابع all كما يلي: >>> posts = user.posts.all() >>> posts [<title a post from abdelhadi | content of a post >, <title another post from abdelhadi | some other content for the post >] يُمكنك الآن الوصول إلى كل مقال على حدة بحلقة for بسيطة: for post in posts: print(post.title) print(post.content) print('-----------------------') يكون الخرج كما يلي: a post from abdelhadi content of a post ----------------------- another post from abdelhadi some other content for the post ----------------------- سنتطرّق إلى الدّوران حول العديد من السّجلات بتفصيل أكثر فيما بعد. للوصول إلى مقال معيّن يُمكنك إضافة علامات [] مع رقم العنصر، فللوصول إلى المقال الأول مثلًا: >>> user.posts[0] <title a post from abdelhadi | content of a post > والمقال الثّاني: >>> user.posts[1] <title another post from abdelhadi | some other content for the post > الدّوران حول قيم مجموعة من السجلات في هذا القسم، سنستعلم عن بعض القيم من قاعدة بياناتنا ومن ثم ندور حولها للوصول إلى كل سجلّ على حدة: سنبدأ بالحصول على جميع القيم في جدول معيّن، وسنحتاج في حالتنا إلى استخدام هذه الطّريقة من أجل الحصول على جميع المقالات لعرضها على الصّفحة الرّئيسيّة للمقالات. وللوصول إلى جميع السجلّات في جدول مُعيّن في Flask-SQLAlchemy يُمكنك ببساطة أن تستخدم كلّا من التّابعين query ثمّ all على الصّنف المرتبط بالجدول، فمثلا للوصول إلى جميع المقالات المُتواجدة في الجدول: >>> posts = Post.query.all() الآن، سيحمل المُتغيّر posts قائمةً بجميع المقالات، ونستطيع الوصول إلى كلّ قيمة من القيم في أعمدة الجدول بالإضافة إلى بيانات المُستخدم الذي أضاف المقال بحلقة for: for post in posts: print('-----------------------') print(post.title) print(post.content) print(post.created_date) print(post.author.name) print(post.author.email) print(post.author.password) print(post.author.created_date) تستطيع تُنفيذ حلقة التّكرار أعلاه لترى النّتيجة. وكما هو مُتوقّع، ستلاحظ أنّنا نستطيع الوصول إلى بيانات كل مقال على حدة وكذلك بيانات المُستخدم الذي أضاف المقال. كمثال إضافي، سنحصل على جميع المُستخدمين مع عرض بيانات كل مُستخدم ثمّ المقالات التّي كتبها: users = User.query.all() for user in users: print('-----------------------') print(user.name) print(user.email) print(user.password) print(user.created_date) for post in user.posts: print('----') print(post.title) print(post.content) print(post.created_date) إن أمعنت النّظر في الشّيفرة فستبدو لك بسيطة جدّا، مجرد حلقة للحصول إلى كل مُستخدم ثمّ حلقة أخرى للوصول إلى كل مقال من مقالاته. سنستخدم محرّك القوالب Jinja2 للدّوران حول البيانات وعرضها بطريقة مُشابهة من أجل الوصول إلى القيم وعرضها في ملفّات HTML داخل تطبيق الويب الذي نبنيه بإطار Flask، لذا أقترح أن تُحاول عرض المقالات وكاتب كلّ مقال منذ الآن لتختبر مدى فهمك للموضوع. في الدروس التالية، سنستخدم طرقًا أخرى للحصول على عدة سجلات وإسنادها إلى مُتغيّر، وستكون طريقة الوصول إلى كل قيمة من بيانات السّجل هي نفسها ما قد ذكرناه ها هنا.
  6. حاوية لينكس (Linux container) هي تجميع لمجموعة من عمليات تكون منفصلة عن باقي النظام عبر استعمال الخصائص الأمنية لنواة لينكس مثل مجالات الأسماء (Namespaces) ومجموعات المراقبة (Control groups). وهي بنية مُشابهة للأجهزة الوهميّة (Virtual Machines)، إلا أنها أكثر خفّة، بحيث أنك لن تحتاج إلى تشغيل نواة إضافية أو محاكاة العتاد، ما يعني أنه بالإمكان إنشاء حاويات متعددة على نفس الخادوم. ويُمكنك باستعمال حاويات لينكس تشغيل وحدات متعددة لأنظمة تشغيل بأكملها، كلّها محجوزة بنفس الخادوم. أو تحزيم تطبيقاتك الخاصة والملفات التابعة لها في حاوية خاصة دون التأثير على باقي مكونات النظام. على سبيل المثال تخيّل أنك تملك خادوما وتريد تجهيز عدد من الخدمات، بما فيها المواقع اﻹلكترونية لعملائك. في النظام التقليدي، كل موقع إلكتروني سيكون عبارة عن مضيف وهمي (virtual host) من نفس خادوم Nginx أو Apache، لكن مع حاويات لينكس، كل موقع إلكتروني يتم إعداده في حاويته الخاصة مع خادوم ويب خاصّ به. ﻹنشاء وتسيير هذه الحاويات يمكن استعمال LXD الذي يوفر خدمة مراقبة الأجهزة الافتراضية hypervisor لتسيير دورة حياة الحاويات بأكملها. سنقوم في هذا الدرس بتنصيب موقعين إلكترونيين مبنيين على Nginx في نفس الخادوم. كل منهما محجوز في حاويته الخاصة، ثم سنقوم بتنصيب HAProxy ليعمل على شكل وسيط عكسي (reverse proxy) على حاوية ثالثة، ثم سنقوم بتوجيه الزوار إلى حاوية HAProxy لجعل كلا الموقعين قابلين للولوج من خلال شبكة الإنترنت. المتطلّبات لإتمام المطلوب نحتاج إلى ما يلي: خادوم Ubuntu 16.04 معد بإتباع الخطوات المتواجدة في هذا الدّرس ومستخدم إداري بامتيازات sudo غير المستخدم الجذر وجدار ناري. اسما نطاق مؤهلان بالكامل (FQDNs)، مع كل سجل DNS A موجه نحو عنوان IP الخاصّ بالخادوم، لتحقيق ذلك يمكن إتباع هذا الدرس. اختياريا، قم بإضافة 20 GB من تخزين الكتل (Block storage) عن طريق إتباع هذا الدرس الذي يُمكن استعماله لتخزين جميع البيانات المتعلقة بالحاويات. الخطوة الأولى - إضافة المستخدم إلى مجموعة LXD نسجل الدخول إلى الخادوم عن طريق حساب المستخدم غير الجذر الذي سيُستعمل لتأدية جميع مهام تسيير الحاوية، وليُنجِز ذلك يجب إضافة هذا الحساب إلى مجموعة lxd عن طريق الأمر التّالي: sudo usermod --append --groups lxd sammy أبدل sammy باسم المستخدم الخاصّ بك. نقوم بتسجيل الخروج من الخادوم ثم تسجيل الدخول إليه لتحديث جلسة SHH بعضوية المجموعة الجديدة، بعد ذلك، يمكن البدء بإعداد LXD. الخطوة الثانية - إعداد LXD يحتاج LXD إلى إعدادات معينة ليعمل بالشكل المطلوب قبل استعماله، نوع التخزين على مستوى الواجهة الخلفية هو أهم إعداد يجب علينا تحديده، لتخزين الحاويات، التخزين المقترح لـLXD هو استعمال ملف من نوع ZFS مُخزّن إما داخل ملف مخصص مسبقا أو عن طريق استعمال تخزين الكتل. لدعم ملفات ZFS في LXD، سنقوم بتنصيب حزمة zfsutils-linux: sudo apt-get update sudo apt-get install zfsutils-linux بعد ذلك سنكون مستعدين لتهيئة LXD، أثناء التهيئة سيُطلَبُ منك تخصيص تفاصيل تخزين ZFS على مستوى النظام الخلفي. هناك طريقتان لذلك اعتمادا على إذا ما كنا سنستعمل ملفا معدا مسبقا أو تخزين الكتل، بعد تحديد آلية التخزين سنقوم بإعداد اختيارات تواصل الحاويات الخاصة بنا. لكن قبل ذلك، سنتعرّف على خياري تهيئة التّخزين. الخيار الأول - استعمال ملف مخصص مسبقا اتبع الخطوات التالية لإعداد LXD لاستعمال ملف مخصص مسبقا لتخزين الحاويات. أولا نفّذ اﻷمر التالي لبدأ عملية تهيئة LXD: sudo lxd init سيُطلب منا اﻹدلاء بمجموعة من المعلومات كما يظهر في النتائج التالية، سنقوم باختيار الاختيارات الافتراضية بما في ذلك حجم الملف المقترح والمسمى جهاز الحلقة (loop device). المُخرجات: Name of the storage backend to use (dir or zfs) [default=zfs]: zfs Create a new ZFS pool (yes/no) [default=yes]? yes Name of the new ZFS pool [default=lxd]: lxd Would you like to use an existing block device (yes/no) [default=no]? no Size in GB of the new loop device (1GB minimum) [default=15]: 15 Would you like LXD to be available over the network (yes/no) [default=no]? no Do you want to configure the LXD bridge (yes/no) [default=yes]? yes Warning: Stopping lxd.service, but it can still be activated by: lxd.socket LXD has been successfully configured. يتم حساب الحجم المقترح تلقائيا بناء على المساحة المتاحة في الخادوم. بعد إعداد الجهاز، سنقوم بضبط إعدادات التشبيك، لكن قبل ذلك، لنتعرّف على الخيار الثاني. الخيار الثاني - استعمال تخزين الكتل إذا كنت تريد استعمال تخزين الكتل، ستحتاج إلى البحث عن الجهاز الذي يُشير نحو حجم تخزين الكتل (block storage volume) الذي تمّ إنشاؤه لتخصيصه في إعدادات LXD، سنذهب إلى تبويبة Volumes في لوحة التحكم الخاصّة بـDigitalOcean. ثم نقوم بتحديد مكان للمساحة ثم النقر على النافذة المنبثقة More، ثم انقر على Config instructions. نقوم بتحديد مكان الجهاز عن طريق النّظر إلى مُخرج أمر تشكيل الحجم، ابحث بشكل خاص عن المسار المخصّص في أمر sudo mkfs.ext4 -F. تمثل الوثيقة التالية مثالا على هذا الحجم: الذي يهمنا هو الجزء المسطر بالأحمر. في هذه الحالة اسم الحجم هو: /dev/disk/by-id/scsi-0D0_Volume_volume-fra1-01 يمكن لهذا الاسم أن يختلف في حالتك. بعد تعريف الحجم سنعود إلى الطرفية وسننفّذ الأمر التالي لبدء عملية تهيئة LXD: sudo lxd init سوف تُطرَح علينا مجموعة من اﻷسئلة، أجب عليها بالأجوبة المُوضّحة أسفله: Name of the storage backend to use (dir or zfs) [default=zfs]: zfs Create a new ZFS pool (yes/no) [default=yes]? yes Name of the new ZFS pool [default=lxd]: lxd إذا طُلب منك استخدام جهاز كتل جاهز آنفا فاختَر نعم (yes)، مع اﻹدلاء بمساره. Would you like to use an existing block device (yes/no) [default=no]? yes Path to the existing block device: /dev/disk/by-id/scsi-0DO_Volume_volume-fra1-01 ثم اختر القيم الافتراضية لباقي اﻷسئلة. Would you like LXD to be available over the network (yes/no) [default=no]? no Do you want to configure the LXD bridge (yes/no) [default=yes]? yes Warning: Stopping lxd.service, but it can still be activated by: lxd.socket LXD has been successfully configured. بعد انتهاء العملية، سنقوم بإعداد الشبكة. إعداد التشبيك (Networking) ستُظهِر لنا عمليّة التّهيئة مجموعة من شاشات التّعليمات كما هو موضّح في الصورة أسفله، ستمكّننا هذه التّعليمات من إعداد جسر التشبيك للحاويات حتى تتمكن من الحصول على عناوين IP خاصّة بها، وتتمكن من التواصل بينها والاتصال بشبكة الأنترنت. قم باستعمال جميع القيم الافتراضية باستثناء تشبيك IPv6، قم باختيار الخيار No في هذه الحالة. إذ لن نستعمله في هذا الدرس. بعد الانتهاء من إعداد التّشبيك، ستكون جاهزا لإنشاء الحاويات. الخطوة الثّالثة - إنشاء الحاويات قمنا بإعداد LXD بنجاح، إذ قمنا بتخصيص موقع للتخزين على مستوى الواجهة الخلفية وقمنا بإعداد التشبيك الافتراضي ﻷي حاويات منشأة حديثا. ما يعني بأنّنا جاهزون ﻹنشاء وتسيير بعض الحاويات، وذلك باستعمال الأمر lxc. أول أمر سنُجرّبه هو الأمر lxc list، والذي يُعطينا لائحة الحاويات المُنصّبة المتوفرة: lxc list المُخرجات: Generating a client certificate. This may take a minute... If this is your first time using LXD, you should also run: sudo lxd init To start your first container, try: lxc launch ubuntu:16.04 +------+-------+------+------+------+-----------+ | NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS | +------+-------+------+------+------+-----------+ ولأنها المرة اﻷولى التي يتواصل فيها أمر lxc مع مراقب الأجهزة الافتراضية (Hypervisor) الخاص بـLXD،فإن المخرج يخبرنا بأن اﻷمر قام بإنشاء شهادة عميل تلقائيا وذلك للتواصل اﻵمن مع LXD. بعد ذلك، يخبرنا بمعلومات حول كيفيّة إطلاق الحاويات، ثمّ يعرِض لائحة فارغة من الحاويات ﻷننا لم ننشئ أيا منها بعد. لنَقُم بإنشاء ثلاثة حاويات، واحدة لكل خادوم ويب والثالثة للوسيط العكسي، دور هذا اﻷخير هو توجيه الاتصالات الواردة من اﻷنترنت نحو الخادوم المناسب داخل الحاوية. نقوم باستعمال أمر lxc launch لإنشاء وتشغيل حاوية (ubuntu:x) Ubuntu 16.04 باسم web1. الحرف x في ubuntu:x هو اختصار لكلمة Xenial الاسم الرمزي لـUbuntu 16.04، أمّا ubuntu: فهو مُعرّف المجلد المضبوط مسبقا الخاص بصور LXD. ملاحظة: يمكنك الوصول إلى اللائحة الكاملة لصور Ubuntu المتاحة عن طريق الأمر lxc image list ubuntu:، ولتوزيعات أخرى استعمل اﻷمر lxc image list images:. نفّذ الأوامر التالية لإنشاء الحاويات: lxc launch ubuntu:x web1 lxc launch ubuntu:x web2 lxc launch ubuntu:x haproxy وبما أنها المرة اﻷولى التي ننشئُ فيها الحاويات، فسيقوم اﻷمر اﻷول بتنزيل صور الحاويات من اﻷنترنت وسيخزنها محليا، ستُنشَئُ الحاويّتان المتبقيّتان بشكل أسرع. لاحظ بيانات المخرج جرّاء إنشاء الحاوية web1: Creating web1 Retrieving image: 100% Starting web1 بعد أن قمنا بإنشاء ثلاثة حاويات فارغة سنقوم باستعمال الأمر lxc list ﻹظهار المعلومات حولها: lxc list يُظهِر المخرج جدولا باسم الحاوية وحالتها الحالية، عنوان IP الخاص بها، نوعها وما إذا تم أخد أية لقطات (snapshots) لها: +---------+---------+-----------------------+------+------------+-----------+ | NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS | +---------+---------+-----------------------+------+------------+-----------+ | haproxy | RUNNING | 10.10.10.10 (eth0) | | PERSISTENT | 0 | +---------+---------+-----------------------+------+------------+-----------+ | web1 | RUNNING | 10.10.10.100 (eth0) | | PERSISTENT | 0 | +---------+---------+-----------------------+------+------------+-----------+ | web2 | RUNNING | 10.10.10.200 (eth0) | | PERSISTENT | 0 | +---------+---------+-----------------------+------+------------+-----------+ سَجِّل أسماء الحاويات وعناوين IPv4 الخاصة بها لأننا سنحتاجها لإعداد الخدمات. الخطوة الرابعة - إعداد حاويات Nginx لنقم بالاتصال مع الحاوية web1 وإعداد الخادوم اﻷول، وللقيام بذلك نستعمل الأمر lxc exec الذي يأخذ اسم الحاوية والأوامر التي تريد تنفيذها. استعمل اﻷمر التالي للاتصال بالحاوية: lxc exec web1 -- sudo --login --user ubuntu يَدُلّ المقطع -- على أنّ العوامل التي ستُمرَّرُ للأمر lxc ستنتهي وسيتم تمرير باقي السطر على شكل أوامر للتنفيذ داخل الحاوية، اﻷمر الذي سيُمرَّرُ في هذه الحالة هو sudo --login --user ubuntu والذي يوفر صدفة ولوج (login shell) للحساب ubuntu المعد مسبقا داخل الحاويّة. ملاحظة: إذا كنت تريد الاتصال بالحاوية بصلاحيات حساب الجذر، فقم باستعمال اﻷمر lxc exec web1 -- /bin/bash عوضا عمّا سبق. فور الدخول إلى الحاويّة، سيظهر لنا محثّ الصدفة على الشكل التالي: ubuntu@web1:~$ المُستخدم ubuntu هذا معد مسبقا بصلاحيات sudo داخل الحاوية ويستطيع إصدار أوامر sudo دون الحاجة إلى اﻹدلاء بكلمة المرور. هذه الصدفة محدودة داخل حدود الحاوية. أي أنّ أيّ أمر نُشغله داخل هذه الصدفة يبقى داخل الحاوية ولا يخرج إلى الخادوم المُضيف. لِنحدّث لائحة حزم Ubuntu داخل الحاوية ولنُنصِّب Nginx: sudo apt-get update sudo apt-get install nginx لنعدل صفحة الويب الافتراضية لهذا الموقع ولنُضِف نصّا يوضح أن هذا الموقع مستضاف على الحاوية web1. افتح الملف /var/www/html/index.nginx-debian.html: sudo nano /var/www/html/index.nginx-debian.html قم بالتعديلات التالية للملف: <!DOCTYPE html> <html> <head> <title>Welcome to nginx on LXD container web1!</title> <style> body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } </style> </head> <body> <h1>Welcome to nginx on LXD container web1!</h1> <p>If you see this page, the nginx web server is successfully installed and working. Further configuration is required.</p> ... قمنا بتعديل الملف في مكانين العنوان <title> والوسم <h1> وذلك بإضافة المقطع on LXD container web1. أغلق المحرر بعد الحفظ. واﻵن، سجّل الخروج من الحاوية لنَعودَ إلى الخادوم المُضيف: logout سنُكرر نفس اﻷمر بالنسبة لحاويّة web2، نسجل الدخول نُنصّب Nginx ثمّ نعدّل الملفّ /var/www/html/index.nginx-debian.html لذِكرِ web2، ثم نخرج من الحاويّة web2. لنستعمل curl للتأكد من أن الخواديم داخل الحاويات تعمل بشكل جيد. نحتاج إلى عناوين IP الخاصّة بالحاويات، والتي حصلنا عليها سابقا. curl http://10.10.10.100/ يجب على المُخرج أن يكون كما يلي: <!DOCTYPE html> <html> <head> <title>Welcome to nginx on LXD container web1!</title> <style> body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } </style> </head> <body> <h1>Welcome to nginx on LXD container web1!</h1> <p>If you see this page, the nginx web server is successfully installed and working. Further configuration is required.</p> ... تفقّد الحاوية الثانية كذلك باستعمال الأمر curl وعنوان IP الخاصّ بها للتّأكد من أنّها قد ضُبطت بالشكل الصحيح، يمكن المرور إلى ضبط HAProxy بعد ذلك. الخطوة الخامسة - إعداد حاوية HAProxy سنقوم بإعداد حاوية HAProxy لتعمل كوسيط أمام هاتين الحاويتين. ولمزيد من المعلومات حول طريقة عمل HAProxy يمكنك الرجوع إلى هذا الدرس. سنقوم بتوجيه حركة المرور نحو كل حاوية بناءا على اسم النطاق الذي نستعمله، سوف نستعمل اسم النطاق example.com في مثال الإعدادات الموالي، وهو نطاق خاص للتوثيقات التعليمية مثل هذا الدرس، سنقوم بإنشاء أول موقع إلكتروني متاح على اسمي النطاق example.com و www.example.com. الموقع اﻹلكتروني الثاني سيكون على اسم النّطاق www2.example.com. أبدِل أسماء النطاقات الخاصة بك مكان هذه اﻷسماء. سجل الدخول إلى حاوية haproxy: lxc exec haproxy -- sudo --login --user ubuntu حدّث لائحة الحزم ونصِّب HAProxy: sudo apt-get update sudo apt-get install haproxy بعد انتهاء التنصيب سننتقل إلى إعداد HAProxy. ملف إعدادات HAProxy مُتواجد في المسار ‎/etc/haproxy/haproxy.cfg. افتح هذا الملف بواسطة محرر النصوص المُفضّل لديك: sudo nano /etc/haproxy/haproxy.cfg أولا سنقوم ببعض التعديلات في قسم defaults. سنقوم بإضافة خيار forwardfor لكي نحتفظ بعنوان IP المصدري لعميل الويب، وسنضيف الخيار http-server-close، والذي سيقوم بتمكين إعادة استخدام الجلسة (session reuse) وتخفيض زمن الوصول (Latency). global ... defaults log global mode http option httplog option dontlognull option forwardfor option http-server-close timeout connect 5000 timeout client 50000 timeout server 50000 ... سنقوم بعد ذلك بإعداد الواجهة اﻷمامية للإشارة نحو حاويتي الواجهة الخلفية الخاصة بنا. أضف قسم frontend تحت اسم www_frontend كما يلي: frontend www_frontend bind *:80 # Bind to port 80 (www) on the container # It matches if the HTTP Host: field mentions any of the hostnames (after the '-i'). acl host_web1 hdr(host) -i example.com www.example.com acl host_web2 hdr(host) -i web2.example.com # Redirect the connection to the proper server cluster, depending on the match. use_backend web1_cluster if host_web1 use_backend web2_cluster if host_web2 توافق أوامر acl أسماء مُضيفات خواديم الويب ثم تقوم بإعادة توجيه الطلب إلى قسم backend المناسب. ثم نعرف قسمي backend جديدين واحد لكل خادوم ويب، ثم نقوم بتسميتهما web1_cluster وweb2_cluster تباعا. أضف الشفرة التالية للملف لتعريف الواجهات الخلفية: backend web1_cluster balance leastconn # We set the X-Client-IP HTTP header. This is useful if we want the web server to know the real client IP. http-request set-header X-Client-IP %[src] # This backend, named here "web1", directs to container "web1.lxd" (hostname). server web1 web1.lxd:80 check backend web2_cluster balance leastconn http-request set-header X-Client-IP %[src] server web2 web2.lxd:80 check يحدّد خيار balance استراتيجية موازنة الحمل (load-balancing). في هذه الحالة، نُفضّل اقل عدد من الاتصالات. خيار http-request يضبط ترويسة HTTP (HTTP header) مع عنوان IP الخاصّ بعميل الويب الحقيقي، إذا لم نحدد هذه الترويسة فإن خادوم الويب سيسجل عنوان HAProxy IP على انه العنوان المصدري لجميع الاتصالات مما سيصعب تحليل أماكن و أصول حركة المرور. يُخصّص خيار server اسما كيفيا (arbitrary name) للخادوم web1 متبوعا باسم المُضيف ومنفذ الخادوم. يُزوّد LXD الحاويات بخادوم DNS، وذلك لكي يُشير web1.lxd إلى عنوان IP التّابع للحاوية web1. الحاويات اﻷخرى لها أسماء مُضيفات خاصة بها مثل web2.lxd وhaproxy.lxd. يدفع المُعامل check HAProxy إلى أداء فحوصات السلامة (Health checks) على الخادوم للتأكد من توفره. لاختبار صحة اﻹعدادات، نفّذ اﻷمر التالي: /usr/sbin/haproxy -f /etc/haproxy/haproxy.cfg -c ينبغي على المخرج أن يكون كما يلي: Configuration file is valid لِنُعد تحميل Haproxy لكي يقرأ اﻹعدادات الجديدة: sudo systemctl reload haproxy والآن سجل الخروج من الحاوية للعودة إلى المُضيف: logout قمنا بتجهيز Haproxy ليتصرف على شكل وسيط عكسي يوجّه أي اتصال يتلقاه عبر المنفذ رقم 80 نحو الخادوم الملائِم داخل كلا الحاويتين. لنتحقق من أن haproxy يستطيع فعلا تحويل الطلبات نحو الحاوية الصحيحة. ولذلك نستعمل اﻷمر التّالي: curl --verbose --header 'Host: web2.example.com' http://10.10.10.10 يُرسل الأمر أعلاه طلبا لـHAProxy ويضبط ترويسة HTTP باسم host والتي سيستعملها HAProxy لتوجيه الاتصال نحو الخادوم الملائم. يجب على المخرج أن يكون كالتالي: ... > GET / HTTP/1.1 > Host: web2.example.com > User-Agent: curl/7.47.0 > Accept: */* > ... < <!DOCTYPE html> <html> <head> <title>Welcome to nginx on LXD container web2!</title> <style> ... تمكن Hproxy من فهم الطلب وتوجيهه نحو الحاوية web2 بنجاح. ومنه فإن الخادوم قدم لنا الصّفحة الرّئيسيّة الافتراضيّة التي قمنا بتعديلها سابقا وأظهر النص on LXD container web2. لنقم الآن بتوجيه الطلبات الخارجية إلى HAProxy حتى يتمكن بقية العالم من الولوج إلى مواقعنا الإلكترونية. الخطوة السادسة - توجيه الاتصالات الواردة نحو حاوية HAProxy القطعة اﻷخيرة من اﻷحجية هي توصيل الوسيط العكسي مع شبكة اﻷنترنت. نحتاج إلي ضبط الخادوم لتوجيه أي اتصال وارد إليه من اﻷنترنت عبر المنفذ 80 نحو حاوية HAProxy. نُصِّبَ HAProxy في حاوية خاصّة به، وبالتالي، فإنه غير قابل للولوج من اﻷنترنت افتراضيا ولحل هذه المشكلة، سنقوم بإنشاء قاعدة iptables لتوجيه الاتصالات. يحتاج اﻷمر iptables إلى عنواني IP، عنوان الخادوم العمومي (your_server_ip) وعنوان IP الخاصّ بالحاويّة haproxy (أي your_haproxy_ip أسفله)، والذي نحصل عليه من خلال اﻷمر lxc list. نفّذ هذا اﻷمر ﻹنشاء القاعدة: sudo iptables -t nat -I PREROUTING -i eth0 -p TCP -d your_server_ip/32 --dport 80 -j DNAT --to-destination your_haproxy_ip:80 وهذا شرح لتفاصيل هذا اﻷمر: المقطع -t nat يعني بأننا نستعمل الجدول nat. المقطع -I PREROUTING يعني أننا نضيف القاعدة إلى سلسلة التوجيه المسبَق (PREROUTING). المقطع -i eth0 يعني أن الواجهة eth0 هي التي ستُستَعمَل، وهي الواجهة العمومية الافتراضية على الخواديم. المقطع -p TCP يعني أننا نستعمل بروتوكول TCP. المقطع -d your_server_ip/32 يحدد عنوان IP الهدف للقاعدة. المقطع --dport 80 يُحدّد رقم المنفذ الهدف. المقطع -j DNAT يعني أننا نريد أن نؤدي قفزة نحو الهدف NAT (DNAT). المقطع --to-destination your_haproxy_ip:80 يدلّ على أننا نريد من الطلب الذهاب إلى عنوان IP الخاصّ بالحاوية من طريق HAProxy. للمزيد من المعلومات حول IPTables، تفقّد الدّرس ما هو الجدار الناري وكيف يعمل؟ والدّرس أساسيات IPTables - قواعد وأوامر شائعة للجدار الناري . أخيرا لحفظ أمر iptables لكي يُعاد تطبيقُه بعد إعادة التشغيل نقوم بتنصيب حزمة iptables-persistent: sudo apt-get install iptables-persistent أثناء تنصيب الحزم سيطلب منك حفظ قواعد iptables الحاليّة. وافق واحفظ جميع قواعد iptables الحاليّة. إذا قمت بإعداد اسمي نطاق FQDN، فسيمكنك أن تتصل بكل من الموقعين باستعمال متصفحك. لذا جرب ذلك. للتحقق من أن الخادومين يمكن الولوج لهما عن طريق اﻷنترنت، ادخل إلى كل منهما باستعمال حاسوبك المحلي عن طريق أمر curl بالشكل التالي: curl --verbose --header 'Host: example.com' 'http://your_server_ip' curl --verbose --header 'Host: web2.example.com' 'http://your_server_ip' هذه اﻷوامر تقوم باتصالات HTTP مع عنوان IP العمومي الخاصّ بالخادوم وتضيف حقل ترويسة HTTP مع خيار --header والذي سيستعمله HAProxy لتنفيذ الطلب كما فعلنا في الخطوة الخامسة. هذا هو المخرج بالنسبة لأمر curl الأول: * Trying your_server_ip... * Connected to your_server_ip (your_server_ip) port 80 (#0) > GET / HTTP/1.1 > Host: example.com > User-Agent: curl/7.47.0 > Accept: */* > < HTTP/1.1 200 OK < Server: nginx/1.10.0 (Ubuntu) ... <!DOCTYPE html> <html> <head> <title>Welcome to nginx on LXD container web1!</title> <style> body { ... وهذا هو مٌخرج الأمر الثّاني: * Trying your_server_ip... * Connected to your_server_ip (your_server_ip) port 80 (#0) > GET / HTTP/1.1 > Host: web2.example.com > User-Agent: curl/7.47.0 > Accept: */* > < HTTP/1.1 200 OK < Server: nginx/1.10.0 (Ubuntu) ... <!DOCTYPE html> <html> <head> <title>Welcome to nginx on LXD container web2!</title> <style> body { ... تم إظهار الموقع الصحيح في كلتا الحالتين. خاتمة قمت الآن بضبط موقعي ويب كل منهما داخل حاويته الخاصة مع HAProxy كموجه للمرور، يمكنك تكرار نفس العملية ﻹعداد مواقع ويب أخرى كل داخل حاويته الخاصة، يمكن كذلك إضافة MySQL في حاوية جديدة وتنصيب نظام إدارة محتوى مثل Wordpress لتسيير كل موقع ويب على حدة. يمكنك كذلك استعمال هذه العملية لدعم نُسخِِ أقدم من برمجية معينة، على سبيل المثال، إن كان نظام إدارة محتوى يتطلب برمجية قديمة مثل PHP5 فبإمكانك تنصيب Ubuntu 14.04 على حاوية (lxc launch ubuntu:t) عوضا عن محاولة تخفيض إصدارات مدير الحزم المتوفرة على Ubuntu 16.04. يوفر LXD القدرة على أخذ لقطات للحالة الكاملة للحاويات، ما يجعل إنشاء النسخ الاحتياطية وإرجاع الحاويات إلى الوراء في وقت لاحق أمرا سهلا. إضافة إلى ما سبق فتنصيب LXD على خادومين مختلفين يمكّن من الربط بينهما وتهجير الحاويات بين الخواديم عبر الإنترنت. ترجمة -بتصرّف- للمقال How to Host Multiple Web Sites with Nginx and HAProxy Using LXD on Ubuntu 16.04 لصاحبه Simos Xenitellis.
  7. إنَّ osquery عبارة عن أداة أمنية مفتوحة المصدر دورها تحويل نظام تشغيل بأكمله إلى قاعدة بيانات ضخمة، مع جداول يُمكنك استعلامها باستعمال جمل مُشابهة لجمل SQL. يُمكنك بهذه الاستعلامات مراقبة صلاحيّة الملفات، الاطلاع على حالة وإعدادات الجدار الناري، القيام بتدقيقات أمنية على الخادوم الهدف وغير ذلك. التطبيق عابر للمنصّات مع دعم للنسخ الجديدة من macOS، Windows 10، CentOS وUbuntu. تُوصف رسميا بأنّها "إطار عمل يحتوي على مجموعة من الأدوات المبنية على SQL لمراقبة نظام التّشغيل والحصول على الإحصائيات" وقد كانت بداية الإطار من شركة Facebook. يُمكنك باستخدام osquery تنفيذ أوامر مثل select * from logged_in_users ;‎ على الخادوم الخاصّ بك لتحصل على نتيجة مُشابهة لما يلي: +-----------+----------+-------+------------------+------------+------+ | type | user | tty | host | time | pid | +-----------+----------+-------+------------------+------------+------+ | login | LOGIN | ttyS0 | | 1483580429 | 1546 | | login | LOGIN | tty1 | | 1483580429 | 1549 | | user | root | pts/0 | 24.27.68.82 | 1483580584 | 1752 | | user | sammy | pts/1 | 11.11.11.11 | 1483580770 | 4057 | | boot_time | reboot | ~ | 4.4.0-57-generic | 1483580419 | 0 | | runlevel | runlevel | ~ | 4.4.0-57-generic | 1483580426 | 53 | +-----------+----------+-------+------------------+------------+------+ إن سرَّك ما سبق، فسيُعجبك استعمال osquery كأداة أمنية لمُراقبة النظام واستكشاف الوصول غير المُصرّح له على خادومك الخاصّ. يُوفّر تنصيب osquery ما يلي من المكونات: osqueryi: صدفة osquery التفاعليّة، للقيام باستعلامات ظرفيّة. osqueryd: عفريت (daemon) لتوقيت وتشغيل الاستعلامات في الخلفيّة. osqueryctl: سكربت مُساعد لاختبار نشرِِ (deployment) أو إعدادِِ لـosquery. يُمكن أن يُستعمَل كذلك عوضا عن مُدير خدمات نظام التّشغيل لتشغيل/إيقاف/إعادة تشغيل osqueryd. أداتا osqueryi و osqueryd مُستقلّتان عن بعضهما. إذ لا يحتاجان إلى التواصل بينهما. ولا يتواصلان، ويُمكنك استعمال الواحدة دون الأخرى. معظم المعامِلات والخيارات المطلوبة لتشغيل كل واحدة هي نفسها بين الأداتين، ويُمكنك تشغيل osqueryi باستعمال ملفّ إعدادات osqueryd لتتمكّن من تشخيص البيئة دون الحاجة إلى انتقال دائم بين أسطر الأوامر. سنقوم في هذا الدّرس بما يلي: تثبيت osquery ضبط الأجزاء التي يحتاج إليها osquery في نظام التّشغيل (مثل Rsyslog)، وذلك لكي يعمل osquery بشكل صحيح. ضبط ملفّ إعدادات يُمكن أن يُستعمل من طرف كل من osqueryi و osqueryd. العمل مع حِزمات (packs) osquery، وهي عبارة عن مجموعات من الاستعلامات المسبوقَةِ التّعريف يُمكنك إضافتها إلى المُؤقّت (schedule). تنفيذ استعلامات ظرفيّة باستعمال osqueryi للبحث عن مشاكل أمنيّة. تشغيل العفريت لكي يقوم بتنفيذ الاستعلامات آليّا. السّجلات المُولَّدة من طرف العفريت osqueryd مُراد بها أن تُنقَل إلى نقاط نهاية (endpoints) خارجيّة تحتاج إلى خبرات إضافيّة لضبطها واستعمالها بشكل صحيح. لن يُغطيّ هذا الدّرس هذا الإعداد، لكنّك ستتعلّم كيفيّة ضبط وتشغيل العفريت وحفظ النتائج محليّا. المُتطلّبات لمُتابعة هذا الدّرس، ستحتاج إلى ما يلي: خادوم Ubuntu 16.04 معد بإتباع الخطوات المتواجدة في هذا الدّرس ومستخدم إداري بامتيازات sudo غير المستخدم الجذر وجدار ناري. يجب عليك كذلك أن تمتلك فهما بسيطا لأساسيّات SQL ومعرفة أوليّة حول تأمين نظام لينكس . الخطوة الأولى: تثبيت osquery على الخادوم يُمكنك تنصيب osquery عبر تجميعه من الشيفرة المصدرية، أو عبر استعمال مدير الحزم. وبما أنّ المستودع الرسمي لـUbuntu لا يحتوي على حزمة تنصيب، فسيتوجب عليك إضافة المستودع الرسمي لـosquery إلى النظام. أضف أولًا المفتاح العمومي للمُستودع: sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 1484120AC4E9F8A1A577AEEE97A80C63C9D8B80B ثمّ أضف المُستودع: sudo add-apt-repository "deb [arch=amd64] https://osquery-packages.s3.amazonaws.com/xenial xenial main" حدّث قاعدة بيانات الحزم: sudo apt-get update وأخيرا، نصّب osquery: sudo apt-get install osquery افتراضيّا، لا يُمكن اعتبار osquery مُفيدا للغاية، إذ لا يُعتبر تطبيقا يُمكنك تنصيبه والاستفادة من كامل مزاياه مُباشرة. سواء رغبت باستعمال الصدفة التفاعليّة (interactive shell) أو العفريت، سيتوجّب عليك تمرير بعض المُعاملات والخيارات، إما عبر سطر الأوامر أو عبر ملفّ إعدادات. لعرض المُعاملات والخيارات المتوفّرة للعفريت، اكتب ما يلي: osqueryd --help سيحتوي المُخرج على عشرات المعاملات لسطر الأوامر وخيارات الضّبط. ما يلي جزء من المُخرج عند تجربة الأمر في الخادوم التجريبي الذي استعمِل من أجل هذا المقال: osquery 2.1.2, your OS as a high-performance relational database Usage: osqueryd [OPTION]... osquery command line flags: --flagfile PATH Line-delimited file of additional flags --config_check Check the format of an osquery config and exit --config_dump Dump the contents of the configuration --config_path VALUE Path to JSON config file --config_plugin VALUE Config plugin name --config_tls_endpoint VALUE TLS/HTTPS endpoint for config retrieval --config_tls_max_attempts VALUE Number of attempts to retry a TLS config/enroll request --config_tls_refresh VALUE Optional interval in seconds to re-read configuration --daemonize Run as daemon (osqueryd only) ... ... osquery configuration options (set by config or CLI flags): --audit_allow_config Allow the audit publisher to change auditing configuration --audit_allow_sockets Allow the audit publisher to install socket-related rules --audit_persist Attempt to retain control of audit --aws_access_key_id VALUE AWS access key ID --aws_firehose_period VALUE Seconds between flushing logs to Firehose (default 10) --aws_firehose_stream VALUE Name of Firehose stream for logging --aws_kinesis_period VALUE Seconds between flushing logs to Kinesis (default 10) --aws_kinesis_random_partition_key Enable random kinesis partition keys --aws_kinesis_stream VALUE Name of Kinesis stream for logging --aws_profile_name VALUE AWS profile for authentication and region configuration --aws_region VALUE AWS region للاطلاع على المعاملات الإضافيّة المُتوفرة للصدفة التفاعلية فقط، نفّذ ما يلي: osqueryi --help تشغيل osqueryi أسهل طريقة لعرض واستعلام جداول osquery المتوفرة افتراضيّا. على سبيل المثال، شغّل الصّدفة باستعمال الأمر التّالي: osqueryi --verbose سيضعك هذا في صدفة تفاعليّة، وستُلاحظ مُخرجا مُشابها لما يلي: I0105 01:52:54.987584 4761 init.cpp:364] osquery initialized [version=2.1.2] I0105 01:52:54.987808 4761 extensions.cpp:351] Could not autoload extensions: Failed reading: /etc/osquery/extensions.load I0105 01:52:54.987944 4761 extensions.cpp:364] Could not autoload modules: Failed reading: /etc/osquery/modules.load I0105 01:52:54.988209 4761 init.cpp:606] Error reading config: config file does not exist: /etc/osquery/osquery.conf I0105 01:52:54.988334 4761 events.cpp:886] Error registering subscriber: socket_events: Subscriber disabled via configuration I0105 01:52:54.993973 4763 interface.cpp:307] Extension manager service starting: /home/sammy/.osquery/shell.em Using a virtual database. Need help, type '.help' osquery> من رسائل المعلومات والأخطاء أعلاه، من الواضح بأنّ بعضا من أجزاء osquery لا تعمل كما يجب . بعض الاستعلامات مثل select * from yara ;‎ لن تُرجع أي شيء، ما يعني بأنّ الجدول لا يحتوي على أية بيانات. بعض الاستعلامات الأخرى مثل select time, severity, message from syslog ;‎ تُرجع رسالة كما يلي، ما يُوضّح بأنّنا بحاجة إلى بعض من العمل الإضافي: W1202 15:44:48.600539 1720 virtual_table.cpp:492] Table syslog is event-based but events are disabled W1202 15:44:48.600587 1720 virtual_table.cpp:499] Please see the table documentation: https://osquery.io/docs/#syslog سنقوم بتعديل إعدادات الخادوم الخاصّ بنا لحل هذه المُشكلة. اخرج من سطر أوامر osquery عبر كتابة ما يلي: .exit سنقوم في الفقرة التّالية بتعديل الأجزاء من نظام التّشغيل التي يحتاج إليها osquery للعمل بشكل صحيح. الخطوة الثّانيّة: السّماح لـosquery بالوصول إلى سجلّ النّظام سنقوم في هذه الخطوة بتعديل تطبيق syslog الخاص بنظام التشغيل لتمكين osquery من الحصول على واستعلام سجّل النّظام. وفي Ubuntu 16.04، هذا يعني تعديل ملفّ إعدادات Rsyslog. والتعديل الوحيد الذي سيتوجب عليك القيام به هو إضافة بضعة أسطر من الشيفرة إلى ملفّ الإعدادات. كبداية، افتح الملفّ ‎/etc/rsyslog.conf: sudo nano /etc/rsyslog.conf نحتاج إلى إضافة بضعة أسطر من الإعدادات التي ستُحدِّدُ لـRsyslog الأنبوب (pipe) الذي تجِبُ الكتابة إليه، وأيّا من مُعطيات (parameters) syslog يجب كتابتها للأنبوب. افتراضيّا، الأنبوب هو ‎/var/osquery/syslog_pipe. سيقوم osquery بعد ذلك بملء جدول syslog الخاصّ به من المعلومات المكتوبة لهذا الأنبوب. ألحِق ما يلي من الأسطر إلى نهاية الملفّ: template( name="OsqueryCsvFormat" type="string" string="%timestamp:::date-rfc3339,csv%,%hostname:::csv%,%syslogseverity:::csv%,%syslogfacility-text:::csv%,%syslogtag:::csv%,%msg:::csv%\n" ) *.* action(type="ompipe" Pipe="/var/osquery/syslog_pipe" template="OsqueryCsvFormat") احفظ وأغلق الملفّ. لتطبيق التغييرات، أعد تشغيل عفريت syslog: sudo systemctl restart rsyslog لنُنشئ الآن ملفّ إعدادات يضبط بعض الخيارات الافتراضية ويُوقِّتُ بعض الاستعلامات. الخطوة الثّالثة: إنشاء ملفّ إعدادات osquery إنشاء ملفّ إعدادات يُسهِّلُ من عمليّة تشغيل osqueryi.فعوضا عن تمرير عدد كبير من خيارات سطر الأوامر، يُمكن لـosqueryi قراءة هذه الخيارات من ملفّ إعدادات مُتواجد في المسار ‎/etc/osquery/osquery.conf. وبالطّبع، فملفّ الإعدادات سيكون مُتاحا للعفريت كذلك. يحتوي ملفّ الإعدادات على الاستعلامات التي تحتاج إلى تنفيذها حسب توقيت مُعيّن. لكنّ مُعظم الاستعلامات التي يُمكنك تشغيلها متوفّرة على شكل حزمات (packs). ملفّات الحزمات مُتوفرة في المُجلّد ‎/usr/share/osquery/packs. لا يأتي osquery مُجهّزا بملفّ إعدادات مُسبق، لكنّ هناك نموذج ملفّ إعدادات يُمكنك نسخه إلى /etc/osquery وتعديله. لكنّ ملفّ الإعدادات هذا لا يحتوي على جميع الخيارات التي تحتاج إليها لتشغيله على توزيعة لينكس مثل Ubuntu، لذا سنقوم بإنشاء ملفّنا الخاصّ. هناك ثلاثة أقسام لملفّ الإعدادات: قائمة بخيارات العفريت وإعدادات المزايا. يُمكن لهذه الإعدادات أن تُقرأ كذلك من طرف osqueryi. قائمة استعلامات موقوتة لتُشغَّل متى ما وَجَبَ ذلك. قائمة حزمات لتُستعمل للتعامل مع استعلامات موقوتة أكثر تحديدا. ما يلي قائمة من الخيارات التي سنستعملها في ملفّ الإعدادات الخاصّ بنا، ما يعنيه كلّ خيار، والقيم التي سنعيِّنها لهذه الخيارات. تكفي هذه القائمة من الخيارات لتشغيل كل من osqueryi وosqueryd على Ubuntu 16.04 وتوزيعات لينكس الأخرى. config_plugin: من أين نُريد osquery أن يقرأ الإعدادات الخاصّة به. بما أنّ الإعدادات تُقرأ من ملفّ على القرص افتراضيّا، فستكون القيمة filesystem. logger_plugin: يُحدّد هذا الخيار مكان كتابة نتائج الاستعلامات الموقوتة. سنستعمل القيمة filesystem مُجدّدا. logger_path: هذا هو المسار الذي يُؤدّي إلى مُجلّد السّجلات الذي ستجد به ملفّات تحتوي على المعلومات، التنبيهات والأخطاء ونتائج الاستعلامات الموقوتة. القيمة الافتراضيّة هي ‎/var/log/osquery. disable_logging: سنقوم بتفعيل التّسجيل عبر تحديد القيمة false لهذا الخيار. log_result_events: عبر تحديد القيمة true لهذا الخيار، سيُعبّر كل سطر من سجلات النّتائج عن تغيير في الحالة. schedule_splay_percent: في حالة تواجد عدد كبير من الاستعلامات الموقوتة في نفس المدة الزمنية، سيقوم هذا الخيار بتمديدها للحد من التأثيرات على أداء الخادوم. القيمة الافتراضيّة هي 10، وهي نسبة مئويّة. pidfile: المكان الذي سيُكتب فيه مُعرِّف العمليّة (process id) الخاصّ بعفريت osquery. القيمة الافتراضيّة هي ‎/var/osquery/osquery.pidfile. events_expiry: المُدة الزمنية بالثواني التي سيتم فيها الاحتفاظ بنتائج المُشترك في مخزن osquery. القيمة الافتراضية هي 3600. database_path: مسار قاعدة بيانات osquery. سنستعمل القيمة الافتراضية ‎/var/osquery/osquery.db. verbose: مع تفعيل التّسجيل، يُستعمل هذا الخيار لتفعيل أو تعطيل رسائل معلومات مُفصّلة. سنعيّن للخيار القيمة false. worker_threads: عدد السلاسل المُستعملة للتعامل مع الاستعلامات. سنترك القيمة الافتراضية 2. enable_monitor: تفعيل أو تعطيل مراقِب المُؤقّت. سنقوم بتفعيله، أي القيمة true. disable_events: يُستعمل هذا الخيار لضبط نظام osquery الخاصّ بالنّشر والاشتراك. نحتاج إلى تفعيله، أي القيمة false. disable_audit: يُستعمل لتعطيل استقبال الأحداث (events) من النظام الفرعي المسؤول عن التّدقيق في نظام التّشغيل. نحتاج إلى تفعيله، لذا فالقيمة التي سنستعملها هي false. audit_allow_config: السّماح لناشر التّدقيق بتغيير إعدادات التّدقيق. القيمة الافتراضية هي true. audit_allow_sockets: يقوم هذا الخيار بالسماح لناشر التّدقيق بتنصيب قواعد مُتعلّقة بالمقابس (socket). القيمة هي true. host_identifier: يُستعمَلُ لتعريف المُضيف الذي يُشغّل osquery. عند جمع نتائج من عدّة خوادم، فمن المُفيد التّمكن من التّعرف على الخادوم الذي جاء منه التّسجيل. القيمة تكون إمّا hostname أو uuid. القيمة الافتراضية التي سنعتمد عليها هي hostname. enable_syslog: يجب أن تكون قيمة هذا الخيار true ليتمكّن osquery من الحصول على معلومات syslog. schedule_default_interval: عند عدم توفير مُدّة لاستعلام موقوت، استعمِل هذه القيمة. سنُعيّن لهذا الخيار القيمَة 3600 ثانيّة. سبق لك وأن تعرّفت على كيفيّة عرض جميع مُعاملات سطر الأوامر وخيارات الضّبط المتوفّرة لكل من osqueryi و osqueryd، لكنّ الخيارات أعلاه كافيّة لتشغيل osquery على هذا الخادوم. أنشئ وافتح ملفّ الإعدادات باستخدام الأمر التّالي: sudo nano /etc/osquery/osquery.conf يعتمد ملفّ الإعدادات على صيغة JSON. انسخ ما يلي إلى الملفّ: { "options": { "config_plugin": "filesystem", "logger_plugin": "filesystem", "logger_path": "/var/log/osquery", "disable_logging": "false", "log_result_events": "true", "schedule_splay_percent": "10", "pidfile": "/var/osquery/osquery.pidfile", "events_expiry": "3600", "database_path": "/var/osquery/osquery.db", "verbose": "false", "worker_threads": "2", "enable_monitor": "true", "disable_events": "false", "disable_audit": "false", "audit_allow_config": "true", "host_identifier": "hostname", "enable_syslog": "true", "audit_allow_sockets": "true", "schedule_default_interval": "3600" }, القسم التّالي من ملفّ الإعدادات هو قسم التوقيت. يُعرَّف كل استعلام عبر مفتاح أو اسم يجب أن يكون فريدا في الملفّ، متبوعا بالاستعلام المُراد تنفيذه والمدّة الزمنية بالثواني. سنقوم بتوقيت استعلام لينظر إلى جدول crontab كلّ 300 ثانيّة. أضف ما يلي إلى ملفّ الإعدادات: "schedule": { "crontab": { "query": "SELECT * FROM crontab;", "interval": 300 } }, يُمكنك إضافة أي عدد من الاستعلامات تُريد، أبقِ فقط على الصّيغة الصحيحة لكي لا تحدث أخطاء في التدقيق. على سبيل المثال، لإضافة بضعة استعلامات أخرى، أضف ما يلي من الأسطر: "schedule": { "crontab": { "query": "SELECT * FROM crontab;", "interval": 300 }, "system_profile": { "query": "SELECT * FROM osquery_schedule;" }, "system_info": { "query": "SELECT hostname, cpu_brand, physical_memory FROM system_info;", "interval": 3600 } }, بعد الاستعلامات الموقوتة، يُمكنك إضافة استعلامات خاصّة تُدعى المُزخرفات (decorators)، وهي استعلامات تُضيف بيانات إلى بداية الاستعلامات الموقوتة الأخرى. الاستعلامات المُزخرفة التّالية ستقوم بإضافة المعرّف UUID الخاصّ بالمُضيف الذي يُشغّل osquery واسم المُستخدم في بداية كلّ استعلام موقوت. أضف ما يلي إلى نهاية الملفّ: "decorators": { "load": [ "SELECT uuid AS host_uuid FROM system_info;", "SELECT user AS username FROM logged_in_users ORDER BY time DESC LIMIT 1;" ] }, يُمكننا أخيرا توجيه osquery إلى قائمة من الحزمات التي تحتوي على استعلامات مُحدّدة. يوفّر osquery مجموعة افتراضيّة من الحزمات تجدها في المُجلّد ‎/usr/share/osquery/packs. أحد هذه الحزمات مُخصّصة لنظام macOS وبقيّتها لأنظمة لينكس. يُمكنك استعمال الحزمات من مساراتها الافتراضية، ويُمكنك كذلك نسخها إلى المُجلّد ‎/etc/osquery. أضف الأسطر التّالية إلى الملفّ لإنهاء الإعداد: "packs": { "osquery-monitoring": "/usr/share/osquery/packs/osquery-monitoring.conf", "incident-response": "/usr/share/osquery/packs/incident-response.conf", "it-compliance": "/usr/share/osquery/packs/it-compliance.conf", "vuln-management": "/usr/share/osquery/packs/vuln-management.conf" } } لاحظ معقوفة الإغلاق }في الأخير، هذه المعقوفة تُوافق معقوفة الفتح في السطر الأول من بداية الملفّ. يجب على ملفّ الإعدادات الكامل أن يبدو كما يلي: { "options": { "config_plugin": "filesystem", "logger_plugin": "filesystem", "logger_path": "/var/log/osquery", "disable_logging": "false", "log_result_events": "true", "schedule_splay_percent": "10", "pidfile": "/var/osquery/osquery.pidfile", "events_expiry": "3600", "database_path": "/var/osquery/osquery.db", "verbose": "false", "worker_threads": "2", "enable_monitor": "true", "disable_events": "false", "disable_audit": "false", "audit_allow_config": "true", "host_identifier": "hostname", "enable_syslog": "true", "audit_allow_sockets": "true", "schedule_default_interval": "3600" }, "schedule": { "crontab": { "query": "SELECT * FROM crontab;", "interval": 300 }, "system_profile": { "query": "SELECT * FROM osquery_schedule;" }, "system_info": { "query": "SELECT hostname, cpu_brand, physical_memory FROM system_info;", "interval": 3600 } }, "decorators": { "load": [ "SELECT uuid AS host_uuid FROM system_info;", "SELECT user AS username FROM logged_in_users ORDER BY time DESC LIMIT 1;" ] }, "packs": { "osquery-monitoring": "/usr/share/osquery/packs/osquery-monitoring.conf", "incident-response": "/usr/share/osquery/packs/incident-response.conf", "it-compliance": "/usr/share/osquery/packs/it-compliance.conf", "vuln-management": "/usr/share/osquery/packs/vuln-management.conf" } } احفظ وأغلق الملفّ ثمّ تحقّق من الإعدادات باستعمال الأمر التّالي: sudo osqueryctl config-check يجب على المُخرج أن يبدو كما يلي: I0104 11:11:46.022858 24501 rocksdb.cpp:187] Opening RocksDB handle: /var/osquery/osquery.db إن حدث خطأ ما، فسيحتوي المُخرج على موقع الخطأ لكي تتمكّن من إصلاحه. بعد إعداد ملفّ إعدادات سليم، يُمكنك الآن الانتقال إلى إعداد حزمة osquery المطلوبة لمُراقبة صلاحيّة الملفّات. الخطوة الرّابعة: إعداد حزمة osquery المطلوبة لمُراقبة صلاحيّة الملفّات مُراقبة صلاحيّة وسلامة الملفات على خادومك من الأجزاء المهمّة في مُراقبة أمن النّظام. ويُوفّر لنا osquery حلّا جاهزا في هذه المسألة. الحزمات التي أضفناها في ملفّ الإعدادات في القسم السّابق عبارة عن حزمات جاهزة. سنقوم في هذا الجزء من الدرس بإضافة حزمة واحدة إضافيّة إلى القائمة، والتي ستحتوي على الاستعلام والتّعليمات التي سيتم استعمالها لمُراقبة صلاحية الملفّات. سنقوم بتسمية الملفّ fim.conf. أنشئ الملفّ وافتحه باستعمال مُحرّر النّصوص الخاصّ بك: sudo nano /usr/share/osquery/packs/fim.conf سنقوم بإنشاء حزمة لمراقبة أحداث الملفّات في المُجلّدات /home، /etc و‎/tmp كلّ 300 ثانيّة. الضبط الكامل للحزمة متواجد أسفله، انسخه إلى الملفّ: { "queries": { "file_events": { "query": "select * from file_events;", "removed": false, "interval": 300 } }, "file_paths": { "homes": [ "/root/.ssh/%%", "/home/%/.ssh/%%" ], "etc": [ "/etc/%%" ], "home": [ "/home/%%" ], "tmp": [ "/tmp/%%" ] } } احفظ وأغلق الملفّ. لجعل الملفّ الجديد وقواعده مُتوفّرة لـosquery، أضفه إلى قائمة الحزمات في نهاية الملفّ ‎/etc/osquery/osquery.conf، افتح الملفّ للتّعديل: sudo nano /etc/osquery/osquery.conf بعدها عدّل قسم الحزمات لتشمل الملفّ الجديد (الصّف الأول ممّا يلي): ... "packs": { "fim": "/usr/share/osquery/packs/fim.conf", "osquery-monitoring": "/usr/share/osquery/packs/osquery-monitoring.conf", "incident-response": "/usr/share/osquery/packs/incident-response.conf", "it-compliance": "/usr/share/osquery/packs/it-compliance.conf", "vuln-management": "/usr/share/osquery/packs/vuln-management.conf" } احفظ وأغلق الملفّ. ولتتأكد من أنّنا لم نرتكب أي خطأ في تعديل الملفّ، تحقّق مُجدّدا: sudo osqueryctl config-check لنبدأ الآن استعمال osqueryi لاستعلام النّظام. الخطوة الخامسة: استعمال osqueryi لتنفيذ تدقيقات أمنية ظرفيّة هناك العديد من الحالات التي يُفيد فيها osquery. سنقوم في هذا القسم بالقيام بمجموعة من التدقيقات الأمنية على النّظام باستعمال الصدفة التّفاعليّة osqueryi. وتذكّر بأنّنا لم نقم بتشغيل عفريت osquery بعد. وهذا من ميّزات osquery الجميلة، إذ تستطيع تنفيذ استعلامات باستعمال osqueryi حتى ولو لم يكن العفريت مُفعّلا، مع استعمال ملفّ الإعدادات الذي أعددناه لضبط البيئة أيضا. لتشغيل osqueryi مع ملفّ الإعدادات، نفّذ ما يلي: sudo osqueryi --config_path /etc/osquery/osquery.conf --verbose مُلاحظة: تمرير خيار ‎--verbose إلى أمر تشغيل كلّ من osqueryi وosqueryd من أفضل الممارسات لأنّه يُظهر أية أخطاء أو تنبيهات يُمكن لها أن تُساعدك على استكشاف وحل مشاكل osquery. ويُمكن تشغيل osqueryi دون صلاحيات المُدير، لكنّك ستحتاج إلى تنفيذ الأمر بصلاحيات الجذر إن كنت ترغب في استعمال ملفّ الإعدادات الخاصّ بالعفريت. لنبدأ بتنفيذ تدقيقات أمنية بسيطة أولا ثمّ ننتقل إلى مُستويات أعلى خطوة بخطوة. على سبيل المثال، لنطّلع على من قد سجّل دخوله إلى النّظام في الوقت الحالي غيركَ أنت؟ يُمكن ذلك عبر الاستعلام التّالي: select * from logged_in_users ; يجب على المُخرج أن يبدو كالتّالي: +-----------+----------+-------+------------------+------------+------+ | type | user | tty | host | time | pid | +-----------+----------+-------+------------------+------------+------+ | boot_time | reboot | ~ | 4.4.0-57-generic | 1483580419 | 0 | | runlevel | runlevel | ~ | 4.4.0-57-generic | 1483580426 | 53 | | login | LOGIN | ttyS0 | | 1483580429 | 1546 | | login | LOGIN | tty1 | | 1483580429 | 1549 | | user | root | pts/0 | 11.11.11.11 | 1483580584 | 1752 | | user | sammy | pts/1 | 11.11.11.11 | 1483580770 | 4057 | +-----------+----------+-------+------------------+------------+------+ في المُخرج أعلاه حسابا مُستخدمين حقيقين قد سجّلا دخولهما إلى الجهاز، وكلاهما من نفس عنوان IP. يجب على عنوان IP هذا أن يكون معروفا. إن لم يكن كذلك، فسيتوجب عليك البحث عن مصدر تسجيل الدخول المثير للشبهات. يُخبرنا الاستعلام السّابق من قد سجّل دخوله في الوقت الحاليّ، لكن ماذا عن تسجيلات الدخول السّابقة؟ يُمكنك معرفة ذلك عبر استعلام الجدول last كما يلي: select * from last ; لا يُشير المُخرج إلى أي شيء غير اعتيادي، ما يعني بأنّه لم يُسجّل أي أحد غيرنا دخوله إلى الجهاز مُؤخّرا: +----------+-------+------+------+------------+------------------+ | username | tty | pid | type | time | host | +----------+-------+------+------+------------+------------------+ | reboot | ~ | 0 | 2 | 1483580419 | 4.4.0-57-generic | | runlevel | ~ | 53 | 1 | 1483580426 | 4.4.0-57-generic | | | ttyS0 | 1546 | 5 | 1483580429 | | | LOGIN | ttyS0 | 1546 | 6 | 1483580429 | | | | tty1 | 1549 | 5 | 1483580429 | | | LOGIN | tty1 | 1549 | 6 | 1483580429 | | | root | pts/0 | 1752 | 7 | 1483580584 | 11.11.11.11 | | sammy | pts/1 | 4057 | 7 | 1483580770 | 11.11.11.11 | +----------+-------+------+------+------------+------------------+ هل تم ضبط وتفعيل الجدار النّاري؟ هل لا يزال الجدار النّاري قيد التّشغيل؟ إن كنت في شك من أمرك، فنفّذ الاستعلام التّالي: select * from iptables ; إن لم تحصل على أي مُخرج، فهذا يعني بأنّ جدار IPTables النّاري لم يُضبَط. إن كان الخادوم مُتصلا بالأنترنت فهذا ليس جيّدا، لذا من المُفضّل أن تُعدّ الجدار النّاري الخاصّ بك. يُمكنك تعديل الاستعلام السّابق وتنفيذه لترشيح أعمدة مُحدّدة كما يلي: select chain, policy, src_ip, dst_ip from iptables ; يجب على الاستعلام أن يمنحك مُخرجا مُشابها لما يلي. ابحث عن أي مصدر مُثير للشبهات وعناوين IP الوجهة (destination IP addresses) التي لم تقم بضبطها: +---------+--------+---------+-----------+ | chain | policy | src_ip | dst_ip | +---------+--------+---------+-----------+ | INPUT | ACCEPT | 0.0.0.0 | 0.0.0.0 | | INPUT | ACCEPT | 0.0.0.0 | 127.0.0.0 | | INPUT | ACCEPT | 0.0.0.0 | 0.0.0.0 | | INPUT | ACCEPT | 0.0.0.0 | 0.0.0.0 | | INPUT | ACCEPT | 0.0.0.0 | 0.0.0.0 | | INPUT | ACCEPT | 0.0.0.0 | 0.0.0.0 | | INPUT | ACCEPT | 0.0.0.0 | 0.0.0.0 | | INPUT | ACCEPT | 0.0.0.0 | 0.0.0.0 | | INPUT | ACCEPT | 0.0.0.0 | 0.0.0.0 | | INPUT | ACCEPT | 0.0.0.0 | 0.0.0.0 | | FORWARD | ACCEPT | 0.0.0.0 | 0.0.0.0 | | FORWARD | ACCEPT | 0.0.0.0 | 0.0.0.0 | | OUTPUT | ACCEPT | 0.0.0.0 | 0.0.0.0 | | OUTPUT | ACCEPT | 0.0.0.0 | 0.0.0.0 | +---------+--------+---------+-----------+ ما نوع العمليّات الموقوتة في crontab؟ هل قُمت بتوقيتها بنفسك؟ سيُساعدك الاستعلام التّالي على اكتشاف البرمجيات الخبيثة التي تمّ توقيتها لتعمل في فترات زمنية مُعيّنة: select command, path from crontab ; يجب على المُخرج أن يكون على الشّكل التّالي. إن بدت أية أوامر مُثيرة للشبهات، فهذا يعني بأنّها تحتاج إلى تحقيق إضافي: +----------------------------------------------------------------------------------------------------------------------------------------+--------------------------------+ | command | path | +----------------------------------------------------------------------------------------------------------------------------------------+--------------------------------+ | root cd / && run-parts --report /etc/cron.hourly | /etc/crontab | | root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily ) | /etc/crontab | | root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly ) | /etc/crontab | | root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly ) | /etc/crontab | | root if [ -x /usr/share/mdadm/checkarray ] && [ $(date +\%d) -le 7 ]; then /usr/share/mdadm/checkarray --cron --all --idle --quiet; fi | /etc/cron.d/mdadm | | root test -x /etc/cron.daily/popularity-contest && /etc/cron.daily/popularity-contest --crond | /etc/cron.d/popularity-contest | +----------------------------------------------------------------------------------------------------------------------------------------+--------------------------------+ هل هناك من ملفّات على النّظام مع خاصيّة setuid مُفعّلة؟ هناك بضعة من هذه الأنواع من الملفّات على أي خادوم يعمل بـUbuntu 16.04، لكن أي من هذه هي؟ وهل هناك من ملفات لا يجب عليها أن تتواجد في النظام؟ يُمكن لإجابات هذه الأسئلة أن تُساعدك على اكتشاف ثنائيّات بأبواب خلفيّة (backdoored binaries). نفّذ الاستعلام التّالي بين الفينة والأخرى وقارن النتائج مع نتائج أقدم لتتمكن من اكتشاف أية إضافات أو تغييرات غير مرغوب فيها: select * from suid_bin ; مقطع من المُخرج يبدو كما يلي: +-------------------------------+----------+-----------+-------------+ | path | username | groupname | permissions | +-------------------------------+----------+-----------+-------------+ | /bin/ping6 | root | root | S | | /bin/su | root | root | S | | /bin/mount | root | root | S | | /bin/umount | root | root | S | | /bin/fusermount | root | root | S | | /bin/ntfs-3g | root | root | S | | /bin/ping | root | root | S | | /sbin/mount.ntfs-3g | root | root | S | | /sbin/mount.ntfs | root | root | S | | /sbin/unix_chkpwd | root | shadow | G | | /sbin/pam_extrausers_chkpwd | root | shadow | G | | /usr/bin/chage | root | shadow | G | | /usr/bin/locate | root | mlocate | G | | /usr/bin/chfn | root | root | S | | /usr/bin/chsh | root | root | S | | /usr/bin/newuidmap | root | root | S | | /usr/bin/write | root | tty | G | | /usr/bin/mlocate | root | mlocate | G | | /usr/bin/at | daemon | daemon | SG | | /usr/bin/sg | root | root | S | لعرض قائمة بوحدات النواة المُحمَّلة (loaded kernel modules)، نفّذ الاستعلام التّالي: select name, used_by, status from kernel_modules where status="Live" ; هذا استعلام آخر يجب تنفيذه بين الحين والآخر لمُقارنة مُخرجه مع نتائج أقدم للتحقق ممّا إذا كان هناك تغيير ما أو لا. عرض قائمة بجميع المنافذ المُنصتة (listening ports) من أحد الطرق الأخرى التي يُمكنك بها إيجاد أبواب خلفيّة على الخادوم. للقيام بذلك، نفّذ الأمر التّالي: select * from listening_ports ; يجب على المُخرج أن يكون كالتّالي على خادوم جديد مع SSH وحدها تعمل على المنفذ 22: +-------+------+----------+--------+---------+ | pid | port | protocol | family | address | +-------+------+----------+--------+---------+ | 1686 | 22 | 6 | 2 | 0.0.0.0 | | 1686 | 22 | 6 | 10 | :: | | 25356 | 0 | 0 | 0 | | +-------+------+----------+--------+---------+ إن كان المُخرج يحتوي على منافذ تعلم بأنّ الخادوم يُنصت منها، فلا داعي للقلق، أمّا إن كانت هناك منافذ أخرى مفتوحة، فسيتوجّب عليك التّحقيق في ماهيّة هذه المنافذ. لعرض نشاطات الملفّات على الخادوم، نفّذ الاستعلام التّالي: select target_path, action, uid from file_events ; يعرض المُخرج جميع نشاطات الملفات الحديثة على الخادوم، إضافة إلى مُعرّف المُستخدم المسؤول عن النّشاط: +---------------------------+---------+------+ | target_path | action | uid | +---------------------------+---------+------+ | /home/sammy/..bashrc.swp | CREATED | 1000 | | /home/sammy/..bashrc.swp | UPDATED | 1000 | | /home/sammy/..bashrc.swp | UPDATED | 1000 | | /home/sammy/.bashrc | UPDATED | 1000 | | /home/sammy/..bashrc.swp | DELETED | 1000 | | /home/sammy/..bashrc.swp | CREATED | 1000 | | /home/sammy/..bashrc.swp | UPDATED | 1000 | | /home/sammy/..bashrc.swp | UPDATED | 1000 | | /home/sammy/.bashrc | UPDATED | 1000 | | /home/sammy/.bashrc | UPDATED | 1000 | | /home/sammy/.bashrc | UPDATED | 1000 | | /home/sammy/..bashrc.swp | DELETED | | | /etc/test_file.txt | DELETED | | | /home/sammy/.bash_history | UPDATED | 1000 | | /home/sammy/.bash_history | UPDATED | 1000 | | /etc/secret_file.md | CREATED | 0 | | /etc/secret_file.md | UPDATED | 0 | | /etc/secret_file.md | UPDATED | 0 | +---------------------------+---------+------+ هناك العديد من الاستعلامات مُشابهة لما سبق يُمكنك بها أن تحصل على فكرة حول مشاكل أمنية مُحتملة. إن لم تكن مُتأكدا من مُخطّط (schema) جدول ما، يُمكنك استعمال الأمر التّالي لمعرفة مُخطّط الجدول: .schema name-of-table مع إبدال name-of-table باسم الجدول. ويُمكنك كذلك عرض قائمة بالجداول المُتوفرة بالأمر: .tables هناك العديد من الأمثلة الأخرى في الحزمات التي تأتي مع osquery، وقد صُمِّم العديد منها ليعمل بشكل دوري من طرف osqueryd. سنتعرّف في القسم التّالي على كيفيّة تشغيل العفريت لتنفيذ هذه الاستعلامات. ##الخطوة السّادسة: تشغيل osqueryd يسمح العفريت osqueryd بتنفيذ الاستعلامات في فترات زمنيّة مُحدّدة. ما يشمل كلّا من الاستعلامات التي ضبطناها في الخطوة الرّابعة، الاستعلامات المتواجدة في الحزمات التي أعددناها في تلك الخطوة، وحزمة FIM التي أعددناها في الخطوة الخامسة كذلك. إن لم تطّلع على الحزمات بعد، فهذا وقت مُناسب لإلقاء نظرة على مُحتويات ‎/usr/share/osquery/packs. تُكتَب النتائج المُولَّدة من طرف osqueryd إلى ملفّ باسم osqueryd.results.log في مُجلّد ‎/var/log/osquery. هذا الملفّ غير موجود افتراضيّا. ولا يتم إنشاؤه إلا بعد تشغيل العفريت وبدء توليد النّتائج من طرفه. يُمكنك تشغيل osqueryd إما بأداة systemctl أو osqueryctl. كلاهما يؤدي نفس المُهمّة، لذا لا يهم أيّ واحد منهما تستعمل. سيتحقّق osqueryd من تواجد ملفّ إعدادات عند تشغيله، وسيُنبّهك إن لم يجد واحدا. سيبقى مُشتغلا دون ملفّ إعدادات، إلا أن ذلك لن يكون ذا فائدة تُذكر. وبما أنّه قد سبق وأن أعددنا ملفّ إعدادات، فكل ما تحتاج إليه هو تشغيل العفريت: sudo systemctl start osqueryd أو يُمكن كتابة ما يلي: sudo osqueryctl start بعد بضعة دقائق من تشغيل العفريت، من المُفترض أن يزداد حجم الملفّ ‎/var/log/osquery/osqueryd.results.log. يُمكنك أن ترى ذلك بنفسك عبر تنفيذ الأمر التّالي مرارا وتكرارا: ls -lh /var/log/osquery/osqueryd.results.log ازدياد حجم الملفّ يدل على أن نتائج الاستعلامات الموقوتة تُكتَب على القرص. لا يمتلك osquery للأسف نظام تنبيهات مثل OSSEC، ما يعني بأنّك لن تستطيع رؤية نتائج الاستعلامات الموقوتة إلا عبر عرض ملفّ النتائج. ويُمكنك القيام بذلك عبر الأمر tail الذي سيقوم بعرض آخر 10 أسطر من الملفّ على الشّاشة بشكل مُستمر: sudo tail -f /var/log/osquery/osqueryd.results.log اضغط على CTRL+C لإيقاف العرض المُستمرّ للسّجل. قد ترغب على المدى البعيد بنقل نتائج الاستعلامات إلى منصّة تحليل خارجيّة يُمكنك العمل معها. بعض الخيارات مفتوحة المصدر تشمل كلّا من Doorman، Zentral و ElasticSearch. ختاما يعد osquery أداة قويّة مُفيدة لتشغيل استعلامات ظرفية وموقوتة باستعمال جمل SQL المألوفة. osqueryi هو المكون الذي يُمكّنك من تشغيل استعلامات سريعة، أما osqueryd فهو للاستعلامات الموقوتة. لتحليل نتائج الاستعلامات الموقوتة، سيتوجب عليك نقلها إلى منصّة خارجية لتحليل السّجلات. يُمكنك الحصول على المزيد من المعلومات حول osquery على osquery.io. ترجمة -بتصرّف- للمقال How To Monitor Your System Security with osquery on Ubuntu 16.04 لصاحبه finid.
  8. Cachet عبارة عن صفحة حالة (Status Page) ذاتية الاستضافة كبديل للخدمات المستضافة مثل StatusPage.io و Status.io. تُساعد على توفير معلومات حول وقت تشغيل (uptime) ووقت توقف (downtime) التطبيق الخاص بك ومشاركة المعلومات حول أي انقطاع. خير مثال عن ذلك هو صفحة حالة خدمات ومنتجات حسوب التي توضح ببساطة حالة جميع المواقع التابعة لها. code { background-color: rgb(250, 250, 250); border-radius: 3px; } وقد تمت برمجتُها بلغة PHP، لذا إن سبق لك تثبيت حزمة LAMP أو LEMP، فإنها سهلة التثبيت. تتميّز بواجهة نظيفة ومستجيبة ما يمكّن من تشغيلها على جميع اﻷجهزة. في هذا الدرس، سنقوم بضبط صفحة حالة بواسطة Cachet على Debian. مجموعة البرامج التي سنستعملها هي كالتالي: -Cachet من أجل صفحة الحالة نفسها. -Composer لإدارة اعتماديات PHP الخاصة بـCachet -SQLite كقاعدة بيانات لتخزين بيانات Cachet -Nginx لتقديم صفحة الحالة لاحظ أن Cachet لا تراقب المواقع والخوادم لوقت التوقف (downtime)، إذ تسجل اﻷحداث فقط، والتي يمكن تحديثها يدويا أو عن طريق الواجهة البرمجية الخاصة بـ Cachet. إذا كنت تبحث عن حلول لمراقبة تطبيقات الويب، تفقد هذا الدرس. المتطلبات تحتاج إلى ما يلي لمُتابعة هذا الدرس: خادوم Debian 8 معد عن طريق اتباع هذا الدّرس، مع مستخدم إداري بامتيازات sudo غير المستخدم الجذر. تعمل Cachet بـ 512MB من الذاكرة العشوائية، ويفضل 1GB أو اكثر من أجل أداء أفضل. اسم نطاق مؤهل بشكل كامل (FQDN) مع سجل A يوجه نطاقك نحو عنوان IPv4 خادومك. يمكن شراؤه على Namecheap أو الحصول عليه مجانا على Freenom، يمكنك اتباع درس اسم الاستضافة hostname هذا لتفاصيل إعداد سجلات DNS. Nginx مُنصّب ومُعدّ بواسطة Let's Encrypt. يمكن تثبيته عن طريق اتباع هذا الدرس بعدها أعدَّ Let's Encrypt باتباع الخطوتين اﻷوليين من هذا الدرس الخطوات المتبقية يمكن تجاوزها ﻷننا سنقوم بإنشاء ملف إعدادات Cachet خاص بنا. Composer مثبت باتباع الخطوات 1 و 2 من هذا الدرس. Git منصّب باتباع الخطوة الأولى من هذا الدّرس لسحب الشيفرة المصدرية الخاصة بـCachet من موقع GitHub. خادوم SMTP، لتتمكن Cachet من إرسال رسائل بريد إلكتروني حول الوقائع إلى المشتركين ومذكِّرات كلمة المرور إلى المستخدمين المنشئين في واجهة Cachet. يمكن استخدام Postfix كخادوم SMTP للإرسال فقط (Send-Only) بُمساعدة درس كيفية تثبيت وتهيئة Postfix كخادوم SMTP للإرسال فقط باستخدام أوبونتو 14.04. أو يمكن استخدام مزود طرف ثالث مثل Mailgun. الخطوة اﻷولى: إنشاء مستخدم Cachet أول ما يجب القيام به هو إنشاء حساب مستخدم مستقل لتشغيل Cachet. سيحمل هذا الإجراء حماية وعزلا إضافيين. sudo useradd --create-home --shell /bin/bash cachet سينشئ الأمر مستخدمًا باسم cachet مع مجلد المنزل في ‎/home/cachet، والذي ستُضبَط صدفته إلى ‎/bin/bash. الخيار الافتراضي هو ‎/bin/sh، إﻻ أنه لا يوفر ما يكفي من المعلومات في المحث (prompt) الخاص به. سيكون المُستخدم بلا كلمة مرور مع امتيازات حصرية للمكونات التي ستستعملها Cachet. بعد إنشاء المستخدم لنقم بتثبيت اعتماديات PHP. الخطوة الثانية: تثبيت اعتماديات PHP نحتاج إلى تثبيت اعتماديات Cachet، والتي هي عبارة عن عدد من حزم PHP إضافة إلى wget و unzip، والتي يستعملها Composer لتنزيل وفك ضغط مكتبات PHP: sudo apt-get install \ php5-fpm php5-curl php5-apcu php5-readline \ php5-mcrypt php5-apcu php5-cli php5-gd php5-sqlite\ wget unzip يمكنك معرفة المزيد حول أي من الحزم من خلال قائمة إضافات PHP الرّسميّة. لنقم اﻵن بضبط php-fpm، مسيّر عمليات FastCGI. والذي سيستعمله Nginx للتوسّط بين الطّلبات نحو Cachet. قم أولا بإنشاء الملف الذي سيستضيف المعلومات من أجل Cachet والتي يحتاجها php-fpm. افتح /etc/php5/fpm/pool.d/cachet.conf بالمحرّر nano أو أي محرر آخر: sudo nano /etc/php5/fpm/pool.d/cachet.conf قم بلصق التالي: [cachet] user = cachet group = cachet listen.owner = www-data listen.group = www-data listen = /var/run/php5-fpm-cachet.sock php_admin_value[disable_functions] = exec,passthru,shell_exec,system php_admin_flag[allow_url_fopen] = off request_terminate_timeout = 120s pm = ondemand pm.max_children = 5 pm.process_idle_timeout = 10s pm.max_requests = 500 chdir = / احفظ ثم أغلق الملف. يمكنك قراءة المزيد من المعلومات حول هذه اﻹعدادات في درس كيف تستضيف مجموعة مواقع بشكل آمن باستخدام Nginx و Php-fpm على أوبنتو 14.04 . ولكن إليك ما يقوم به كل سطر في هذا الملف: [cachet] هو اسم المُجمِّع (pool). إذ يجب على كل مُجمِّع أن يمتلك اسمًا فريدًا. user وgroup هما مستخدم لينكس والمجموعة التي سيشتغل الحوض الجديد تحتها. وهي نفسها ما أعددناه مع المستخدم الذي أنشأناه في الخطوة اﻷولى. listen.owner و listen.group تُعرِّف ملكِيّة المُنصت، أي مقبس المُجمِّع php-fpm الجديد. يجب على Nginx من أن يتمكن من قراءة هذا المقبس، ولذلك نستخدم مُستخدم ومجموعة www-data. listen يخصّص مسار ملف مقبس فريد لكل مُجمِّع. php_admin_value يسمح بضبط قيم إعدادات PHP مخصّصة. نستعمله هنا لتعطيل الدّوال التي يمكنها تشغيل أوامر لينكس (exec,passthru,shell_exec,system). php_admin_flag يشبه php_admin_value، إﻻ أنه مجرد مستبدل للقيم المنطقية (boolean switch) أي on و off. سنقوم بتعطيل الدّالة allow_url_fopen التي تمكّن برمجيات PHP من فتح ملفات بعيدة والتي قد تستعمل من طرف مهاجم. خيار pm يسمح بضبط أداء الحوض. نقوم بضبطه على ondemand والذي يوفر توازنا لإبقاء مُستويات استعمال الذاكرة منخفضة. إذا كنت تمتلك ذاكرة كبيرة، عندها يمكن ضبطه على القيمة static. إن كنت تمتلك معالجا قويا ذا سلاسل (threads) عديدة، فعندها قد يكون dynamic خيارا أفضل. يجب أن تكون قيمة الخيار chdir جذر نظام الملفّات /. ولا يجب تغيير قيمة هذا الخيار إلا إذا كنت تستخدم خيارا مهما آخر (مثل chroot). أعِد تشغيل php-fpm لتطبيق التغييرات: sudo systemctl restart php5-fpm مكّن خدمة php-fpm لكي تشتغل تلقائيا عند إعادة تشغيل الخادوم إن لم تفعل ذلك بعد: sudo systemctl enable php5-fpm بعد تثبيت حزم PHP العامة، لنقم بتنزيل Cachet. الخطوة الثالثة: تنزيل Cachet الشفرة المصدرية لـCachet مستضافة على GitHub ما يجعلها سهلة التنزيل، والتثبيت، وأيضا -كما سنرى لاحقا- ترقيتها باستخدام Git. يجب اتباع الخطوات المتبقية بصفة المستخدم cachet، لذلك قم بالانتقال له: sudo su - cachet انسخ شفرة Cachet المصدرية إلى مجلد جديد باسم www: git clone https://github.com/cachethq/Cachet.git www بعد ذلك، انتقل إلى المجلد الجديد الذي تتواجد به الشفرة المصدرية لـ Cachet: cd www ابتداءً من هذه المرحلة، لديك سجل تطور Cachet بأكمله، بما في ذلك فروع و وسوم Git. يمكنك تفقد آخر إصدار مستقر في صفحة إصدارات Cachet، ولكن يمكنك كذلك رؤية وسوم Git في هذا المجلد. في فترة نشر هذا الدرس، آخر إصدار مستقر ل Cachet هو v2.3.11. استعمل Git للانتقال إلى وسم هذا اﻹصدار: git checkout v2.3.11 بعد ذلك سنمر إلى التعرف على ملف إعدادات Cachet. الخطوة الرابعة: إعداد Cachet تتطلب Cachet ملف إعدادات يسمى env.، والذي يعتبر ضروريا لتشغيل Cachet. يمكنك بداخله ضبط متغيرات البيئة التي تستعملها Cachet لضبطها. لِنقُم بنسخ مثال اﻹعدادات الذي يأتي مع Cachet ونُبْقِ على الأصلي احتياطا: cp .env.example .env سنضيف إعدادين هنا، أحدهما لضبط قاعدة البيانات واﻵخر لضبط خادوم بريد إلكتروني. بالنسبة لقاعدة البيانات، سنستعمل SQLite. باعتبارها سهلة الاستخدام ولا تتطلب تثبيت أي مكونات إضافية على خادوم. لننشئ أولا الملف الفارغ الذي سيستضيف قاعدة البيانات: touch ./database/database.sqlite بعد ذلك، افتح الملف env. باستعمال nano أو محرر النصوص المُفضل لديك لضبط إعدادات قاعدة البيانات: nano .env ولأننا سنستعمل SQLite، فسنحتاج لحذف كثير من الإعدادات. حدد موقع مقطع الإعدادات الذي يبدأ بالمقطع DB_‎. جزء من الملف ‎.env الأصلي: . . . DB_DRIVER=mysql DB_HOST=localhost DB_DATABASE=cachet DB_USERNAME=homestead DB_PASSWORD=secret DB_PORT=null DB_PREFIX=null . . . احذف كل شيء باستثناء السطر DB_DRIVER، وغيّر قيمته من mysql إلى sqlite. بعد تعديل ملف‎.env: . . . DB_DRIVER=sqlite . . . ملاحظة: يمكنك تفقد خيارات قاعدة بيانات Cachet للحصول على جميع أسماء مُشغّلات قواعد البيانات الممكنة إذا كنت تستعمل قاعدة بيانات أخرى مثل MySQL أو PostgreSQL. بعدها، تحتاج إلى ملء تفاصيل خادوم SMTP الخاصّ بك في إعدادات MAIL_*‎ المُتعلّقة بضبط البريد الإلكتروني: . . . MAIL_HOST=smtp.example.com MAIL_PORT=25 MAIL_USERNAME=smtp_username MAIL_PASSWORD=smtp_password MAIL_ADDRESS=notifications@example.com MAIL_NAME="Status Page" . . . ما يعني: MAIL_HOST عنوان URL خادوم البريد اﻹلكتروني الخاصّ بك. MAIL_PORT المنفذ الذي ينصت عليه خادوم البريد اﻹلكتروني (عادة 25 ). MAIL_USERNAME اسم مستخدم حساب SMTP (عادة ما يكون البريد اﻹلكتروني بأكمله). MAIL_PASSWORD كلمة مرور حساب SMTP. MAIL_ADDRESS عنوان البريد الإلكتروني الذي سترسل منه التنبيهات للمشتركين. MAIL_NAME هو الاسم الذي سيظهر في الرسائل المرسلة إلى المشتركين. انتبه إلى أن أي قيمة بها مسافة يجب أن توضع بين علامتي تنصيص مزدوجتين. يمكنك الاطلاع على المزيد حول برامج تشغيل بريد Cachet على شيفرة mail.php المصدرية وتوثيق البريد الإلكتروني من Laravel. بعد الانتهاء من تحرير الملف، احفظه وأغلقه؛ بعد ذلك، تحتاج إلى ضبط قاعدة بيانات Cachet. الخطوة الخامسة: تهجير قاعدة البيانات تتم إدارة مكتبات PHP التي تعتمد عليها Cachet باستخدام Composer. أولا، كن متيقنا أنك في المجلد الصحيح: cd /home/cachet/www ثم قم بتشغيل Composer وثبّتب جميع المتطلبات باستثناء تلك التي تستعمل لأغراض التطوير. اعتمادا على سرعة الإنترنت لديك، قد يتطلب اﻷمر بعض الوقت: composer install --no-interaction --no-dev -o --no-scripts أنشئ مخطط قاعدة البيانات وشغل التهجير: php artisan migrate تنبيه: في الإصدار المستقر اﻷخير (2.3.11)، هناك علة عند استخدام SQLite والتي تلزمك بتشغيل أمر التهجير migrate قبل أي شيء آخر. اكتب yes عندما تُسأل عمّا إذا أردت تنفيذ الأمر بالفعل. سيكون المُخرج مُشابها لما يلي: ************************************** * Application In Production! * ************************************** Do you really wish to run this command? (yes/no) [no]: > yes Migration table created successfully. Migrated: 2015_01_05_201324_CreateComponentGroupsTable ... Migrated: 2016_06_02_075012_AlterTableMetricsAddOrderColumn Migrated: 2016_06_05_091615_create_cache_table اﻷمر التالي php artisan app:install، يأخذ نسخة احتياطية لقاعدة البيانات، يشغل التهجير، ويولد تلقائيا مفتاح التطبيق (أي قيمة APP_KEY في الملفّ .env) والذي تستعمله Cachet لجميع تشفيراتها. تحذير: لا تغّير أبدا قيمة APP_KEY التي توجد في الملف ‎.env بعد تثبيت وتشغيل Cachet في بيئة الإنتاج، إذ سيؤدي ذلك إلى فقدان كل بياناتك المشفرة. استعمل اﻷمر php artisan app:install مرة واحدة فقط. ولهذا السّبب يُفضّل الإبقاء على نسخة احتياطية من الملفّ ‎.env. أكمل التثبيت: php artisan app:install سيكون المُخرج مُشابها لما يلي: Clearing settings cache... Settings cache cleared! . . . Clearing cache... Application cache cleared! Cache cleared! كخطوة استباقية أخيرة، فرّغ مخزن Cachet المؤقت لتفادي أخطاء HTTP 500: rm -rf bootstrap/cache/* واﻵن بعد تهيئة قاعدة البيانات، يمكننا إعداد طابور مهام Cachet. الخطوة السادسة: إعداد طابور المهام (Task Queue) تستعمل Cachet طابورا لجدولة المهام التي تحتاج للعمل بشكل غير متزامن (asynchronously)، مثل إرسال رسائل البريد اﻹلكتروني. الطريقة المقترحة هي استعمال Supervisor، وهو مُسيّر عمليات يوفر واجهة ثابتة يُمكن من خلالها مُراقبة العمليات والتحكم بها. تأكد أولا من تسجيلك الخروج من جلسة المستخدم cachet وتحوّل إلى مستخدمك ذو صلاحيات sudo غير المُستخدم الجذر: exit ثبّت Supervisor: sudo apt-get install supervisor ثم أنشئ الملف الذي يحتوي على المعلومات التي يحتاجها Supervisor من Cachet. للقيام بذلك، افتح الملفّ /etc/supervisor/conf.d/cachet.conf: sudo nano /etc/supervisor/conf.d/cachet.conf هذا الملف يخبر Supervisor عن كيفية تشغيل وتسيير عملياته. يمكنك قراءة المزيد حول Supervisor في مقال كيفية تثبيت وإدارة Supervisor على خواديم أوبنتو ودبيان . أضف المحتوى التالي مع التّأكد من تعديل مجلد Cachet واسم المستخدم إذا استعملت أسماء أخرى (السّطران directory=/home/cachet/www/ و user=cachet): [program:cachet-queue] command=php artisan queue:work --daemon --delay=1 --sleep=1 --tries=3 directory=/home/cachet/www/ redirect_stderr=true autostart=true autorestart=true user=cachet احفظ وأغلق الملف، ثم أعد تشغيل Supervisor: sudo systemctl restart supervisor مكن خدمة Supervisor لكي تشتغل تلقائيا عند إعادة تشغيل الخادوم: sudo systemctl enable supervisor قاعدة البيانات وطابور المهام جاهزان، المكون التالي الذي يجب ضبطه هو خادوم الويب. الخطوة السابعة: ضبط Nginx سنستعمل Nginx على شكل خادوم ويب وسيط سيتواصل مع php-fpm. قسم المتطلبات أعلاه به درس لكيفية إعداد Nginx بشهادة TLS من Let's Encrypt. لنضف ملف إعدادات Nginx الذي يحتاج إليه Cachet. افتح ملفّ /etc/nginx/sites-available/cachet.conf بمُحرّر النصوص nano أو بأي محرر آخر: sudo nano /etc/nginx/sites-available/cachet.conf هذا هو النص الكامل للملف، والذي يجب عليك نسخه ولصقه. مع إبدال example.com باسم النطاق الخاص بك. وظيفة كل قسم موصوفة بشكل مُفصّل أسفله. server { server_name example.com; listen 80; return 301 https://$server_name$request_uri; } server { listen 443; server_name example.com; root /home/cachet/www/public; index index.php; ssl on; ## Location of the Let's Encrypt certificates ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; ## From https://cipherli.st/ ## and https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH"; ssl_ecdh_curve secp384r1; ssl_session_cache shared:SSL:10m; ssl_session_tickets off; ssl_stapling on; ssl_stapling_verify on; resolver 8.8.8.8 8.8.4.4 valid=300s; resolver_timeout 5s; ## Disable preloading HSTS for now. You can use the commented out header line that includes ## the "preload" directive if you understand the implications. #add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload"; add_header Strict-Transport-Security "max-age=63072000; includeSubdomains"; add_header X-Frame-Options DENY; add_header X-Content-Type-Options nosniff; ssl_buffer_size 1400; ssl_dhparam /etc/ssl/certs/dhparam.pem; location / { try_files $uri /index.php$is_args$args; } location ~ \.php$ { include fastcgi_params; fastcgi_pass unix:/var/run/php5-fpm-cachet.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_index index.php; fastcgi_keep_conn on; } } إليك ما يقوم به كل جزء من هذا الملف. كتلة server اﻷولى توجه حركة مرور HTTP نحو HTTPS: server { server_name example.com; listen 80; return 301 https://$server_name$request_uri; } . . . كتلة server الثانية تحتوي على معلومات خاصة حول هذا الضّبط، مثل تفاصيل SSL وإعدادات php-fpm. تعليمة root تُخبر Nginx عن مكان تواجد مجلد الجذر الخاصّ بـ Cachet. يجب أن تشير القيمة إلى مجلد public، وبما أننا نسخنا Cachet إلى /home/cachet/www/ فسيُصبح السّطر root /home/cachet/www/public;‎. . . . server { listen 443; server_name example.com; root /home/cachet/www/public; index index.php; . . . } شهادات SSL تتواجد داخل مجلد Let's Encrypt، والتي يجب أن تُسمى انطلاقا من اسم نطاقك: . . . server { . . . ssl on; ## Location of the Let's Encrypt certificates ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; . . . } باقي خيارات SSL مأخوذة مُباشرة من درس Nginx و Let's Encrypt: . . . server { . . . ## From https://cipherli.st/ ## and https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH"; ssl_ecdh_curve secp384r1; ssl_session_cache shared:SSL:10m; ssl_session_tickets off; ssl_stapling on; ssl_stapling_verify on; resolver 8.8.8.8 8.8.4.4 valid=300s; resolver_timeout 5s; ## Disable preloading HSTS for now. You can use the commented out header line that includes ## the "preload" directive if you understand the implications. #add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload"; add_header Strict-Transport-Security "max-age=63072000; includeSubdomains"; add_header X-Frame-Options DENY; add_header X-Content-Type-Options nosniff; ssl_buffer_size 1400; ssl_dhparam /etc/ssl/certs/dhparam.pem; . . . } قسم location ~ \.php$‎ يخبر Nginx عن كيفيّة تقديم ملفات PHP. الجزء اﻷهم هو اﻹشارة إلى ملف مقبس Unix الذي استعملناه عندما أنشأنا الملفّ ‎/etc/php5/fpm/pool.d/cachet.conf. أي الملفّ ‎/var/run/php5-fpm-cachet.sock بالتحديد. . . . server { . . . location / { try_files $uri /index.php$is_args$args; } location ~ \.php$ { include fastcgi_params; fastcgi_pass unix:/var/run/php5-fpm-cachet.sock; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_index index.php; fastcgi_keep_conn on; } } احفظ وأغلق الملف إن لم تفعل ذلك مسبقا. واﻵن بعد إنشاء إعدادات Cachet لـNginx، أنشئ وصلة رمزية (symlink) يُشير إلى المُجلد sites-enabled، إذ هنا يبحث Nginx عن ملفات اﻹعدادات لاستعملها: sudo ln -s /etc/nginx/sites-available/cachet.conf /etc/nginx/sites-enabled/cachet.conf أعد تشغيل Nginx لتطبَّق التّغييرات: sudo systemctl restart nginx ومكّن خدمة Nginx لكي تشتغل تلقائيا عند إعادة تشغيل الخادوم: sudo systemctl enable nginx هذا كلّ ما في اﻷمر، إذا كتبت الآن اسم نطاقك في المتصفح، سوف تظهر لك صفحة ضبط Cachet. لنُلقِ نظرة على هذه الصّفحة. الخطوة الثامنة: إنهاء الضّبط المبدئي لـCachet يتمّ إنهاء ما تبقى من خطواتِِ لإعداد Cachet في الواجهة الرسوميّة (GUI) على مُتصفّحك، ما يشمل إعداد اسم الموقع و المنطقة الزمنية باﻹضافة إلى إنشاء حساب المدير. هناك ثلاث خطوات (إعداد البيئة، صفحة الحالة، وحساب المدير )، ويمكنك تغيير اﻹعدادات لاحقا في لوحة التحكم الخاصّة بـ Cachet. ضبط البيئة ضبط البيئة هي أول خطوة من إعدادات الضّبط. تنبيه : إصدار Cachet الذي نستخدمه به علة تتمثل في عدم ظهور إعدادات البريد اﻹلكتروني على صفحة إعدادات البيئة، حتى لو قمت بضبطها مسبقا في env. سيتم حل هذا في اﻹصدار 2.4. يجب ملء الحقول كما يلي : Cache Driver يجب أن يكون (ACP(u. Session Driver يجب أن يكون (ACP(u. Mail Driver يجب أن يكون SMTP. Mail Host يجب أن يكون عنوان بريد خادومك اﻹلكتروني. -Mail From Address يجب أن يكون عنوان البريد اﻹلكتروني الذي سترسل منه التنبيهات إلى المشتركين. Mail Username يجب أن يكون مستخدم حساب SMTP (عادة بريدك اﻹلكتروني بأكمله). -Mail Password يجب أن يكون كلمة مرور حساب SMTP. اضغط Next للذهاب إلى الخطوة التالية. ضبط صفحة الحالة في هذا القسم، سنضبط اسم الموقع، نطاق الموقع، المنطقة الزمنية، واللغة. مُلاحظة : تدعم Cachet العديد من اللغات، لكنها مشروع مجتمعي، ما يعني أنه قد تكون هناك نصوصً غير مترجمة في اللغات المغايرة للإنجليزية. يمكنك إلقاء نظرة على لائحة اللغات المدعومة، والنسبة المئوية للمحتوى المُترجَم (للأسف، فقد تُرجم 34% فقط إلى اللغة العربية في وقت ترجمة هذا الدّرس). يجب ملء الحقول كما يلي: Site Name: الاسم الذي سيظهر على لوحة التحكم Site Domain: اسم النطاق FQDN الذي اخترته لـ Cachet Select your timezone: اختر منطقة زمنية بناء على جمهورك. UTC خيار افتراضي جيد. Select your language: اختر لغة واجهة Cachet. Show support for Cachet: إذا اخترت هذا الخيار، ستظهر عبارة "Powered by Cachet” على تذييل (footer) لوحة التحكم العامّة. اضغط Next للذهاب إلى الخطوة التالية. ضبط حساب المدير أخيرا، أنشئ حساب المدير. اختر اسم المستخدم، وأدخل بريدا إلكترونيا صحيحا، وكلمة مرور قوية. اضغط Complete Setup لحفظ جميع التغييرات. إنهاء الضبط على صفحة إنهاء الضبط Complete Setup، سيتم إعلامك بأنه قد تمّ إعداد Cachet بنجاح. يمكنك اﻵن الضغط على "Go to the dashboard” لتسجيل الدخول بمعلومات المدير خاصتك وزيارة لوحة تحكم Cachet. بعد إنهاء الخطوات السّابقة، فإنّCachet اﻵن مضبوطة وتعمل بشكل كامل. الخطوة اﻷخيرة توضِّح كيفية ترقيتها في المستقبل. الخطوة التاسعة: ترقية Cachet. استعمال Git يجعل أمر الترقية سهلا للغاية عند صدور إصدار جديد من ـ Cachet. إذ كل ما عليك فعله هو الانتقال إلى الوسم المناسب وتشغيل تهجير قاعدة البيانات. مُلاحظة: من الجيد دائما أخذ نُسخة احتياطيّة لـ Cachet وقاعدة بياناتها قبل محاولة ترقية اﻹصدار. وفي SQLite كل ما عليك فعله هو نسخ الملف database/database.sqlite إلى مكان آخر. أوﻻ، تحوَّل إلى المستخدم cachet وانتقل إلى المُجلد الذي نُصِّبتْ فيه Cachet: sudo su - cachet cd /home/cachet/www يمكنك تشغيل صفحة الصيانة إن اخترت ذلك: php artisan down اجلب شفرة Cachet الحديثة من GitHub: git fetch --all استعرض لائحة بجميع الوسوم المُتاحة: git tag -l سوف تظهر لك جميع الوسوم الحديثة التي تبدأ بحرف v. قد تلاحظ أنّ بعضا منها عبارة عن نُسخ تجريبيّة بيتا (beta) أو في حالة ترشيح للإطلاق (Release Candidate) أو ما يُعرف اختصارا بإصدارات RC. وﻷننا نعمل مع خادوم إنتاج (production server)، فيمكنك تجاهل هذه النّسخ واستعمال النّسخ المُستقرّة. يمكنك أيضا زيارة صفحة إصدارات Cachet لمعرفة أحدث وسم. إن رغبت في استعمال وسم للترقية، استعمل Git للانتقال إليه. على سبيل المثال، إذا رغبت في ترقيّة Cachet إلى اﻹصدار 2.4.0، فستسعمل الأمر التّالي: git checkout v2.4.0 أزل خبيئة Cachet قبل الاستمرار: rm -rf bootstrap/cache{,t}/* تاليا، قم بترقية اعتماديات Composer، والتي تحتوي عادة على إصلاح للعلل، تحسينات في اﻷداء، وميزات جديدة: composer install --no-interaction --no-dev -o --no-scripts أخيرا، شغّل التهجير: php artisan app:update إذا شغّلت صفحة الصيانة، فيمكنك اﻵن تمكين الوصول مجددا: php artisan up اﻹصدار الجديد من Cachet سيعمل كما يجب. خاتمة قمتَ اﻵن بإعداد Cachet مع SSL مدعومة بقاعدة بيانات SQLite وتعلمتَ كيفية صيانتها وترقيتها باستخدام Git. يمكنك كذلك اختيار قواعد بيانات أخرى مثل MySQL أو PostgreSQL. لتعلم المزيد حول خيارات Cachet، تفقّد التوثيق الرسمي. ترجمة -بتصرّف- للمقال How To Create a Status Page with Cachet on Debian 8 لصاحبته Achilleas Pipinellis.
  9. من المُستبعد احتمال أنّك ستعمل مع بيانات من جدول واحد فقط، لذا ستحتاج إلى الحصول على البيانات من جداول مُتعدّدة. استعمال الروابط (Joins) أحد الخيارات للقيام بالأمر، وفي قواعد البيانات العلائقيّة (Relational databases)، فالروابط هي التي تجعل قواعد البيانات علائقيّة. يوفّر هذا الدّرس تذكيرا سريعا بالأنواع الأربعة الأساسيّة لروابط SQL وثلاثة من فروعها. ما هو الرابط؟ الرابط طريقة للربط بين البيانات من جدولين أو أكثر اعتمادا على عمود مُشترك بين الجداول. على سبيل المثال، يُمكن ربط جدول عناوين مع جدول أرقام الهواتف حسب اسم شخص مُعيّن (أعطني عنوان ورقم هاتف الشّخص المُسمّى مُحمّد أحمد). لماذا تعد الروابط مهمة؟ تُمكّن الروابط من الحصول على البيانات من عدّة جداول باستعلام (query) واحد. من المُستبعد بأنّك ستعمل مع جدول واحد فقط. استعمال جدول واحد فقط يعني بأنّك إمّا تحدّ نفسك في كميّة البيانات التي تحصل عليها أو أنّك تمتلك الكثير من البيانات التي قد تجعل من الجدول صعب المراس. تُنشئ الروابط علاقة بين جدول وآخر (ومنه جاء مُصطلح “قواعد البيانات العلائقيّة”). ما هي أنواع روابط SQL الموجودة؟ ما يلي عبارة عن قائمة من أنواع روابط SQL الأربعة الرّئيسية (إضافة إلى ثلاثة أشكال فرعيّة)، مُرتبة من أكثرها حصرا إلى أكثرها شمولا. أضفنا كذلك عينات SQL إلى الشروحات المكتوبة لتجربتها والنظر إلى نتيجة استعمال رابط مُعيّن في الاستعلام الخاص بك. الرابط الدّاخلي Inner Join رُبما يعتبر الرّابط الدّاخلي من أكثر أنواع روابط SQL استعمالا. ويُرجِع الرابط الداخلي جميع الصفوف التي تُحقّق شرط الرّبط (join condition) من جدولين أو أكثر. عينة SQL SELECT columns FROM TableA INNER JOIN TableB ON A.columnName = B.columnName; الربط اليساري (الخارجي) يُرجع الرّبط اليساري الخارجي (أحيانا يُختصر إلى الرّبط اليساري) جميع الصفوف من الجدول المُحدّد يسار الشّرط ON والصفوف التي تًحقّق شرط الرّبط فقط من الجدول في اليمين. عينة SQL SELECT columns FROM TableA LEFT OUTER JOIN TableB ON A.columnName = B.columnName الربط اليساري (الخارجي) دون تقاطع هذا الرّبط فرع من فروع الرّبط اليساري، ويُرجع جميع الصفوف من الجدول على يسار الشّرط ON والتي تُحقّق شرط الرّبط كذلك، دون الصفوف التي تُحقّق شرط الرّبط من الجدول على اليمين. عينة SQL SELECT columns FROM TableA LEFT OUTER JOIN TableB ON A.columnName = B.columnName WHERE B.columnName IS NULL الربط اليميني (الخارجي) يُرجع الرّبط اليميني الخارجي (أحيانا يُختصر إلى الرّبط اليميني) جميع الصّفوف من يمين شرط ON والصفوف التي تحقّق شرط الرّبط فقط من الجدول على اليسار. عيّنة SQL SELECT columns FROM TableA RIGHT OUTER JOIN TableB ON A.columnName = B.columnName الربط اليميني (الخارجي) دون تقاطع هذا الرّبط فرع من فروع الرّبط اليميني، ويُرجع جميع الصفوف من الجدول على يمين الشّرط ON والتي تُحقّق شرط الرّبط كذلك، دون الصفوف التي تُحقّق شرط الرّبط من الجدول على اليسار. عيّنة SQL SELECT columns FROM TableA RIGHT OUTER JOIN TableB ON A.columnName = B.columnName WHERE A.columnName IS NULL الربط التام (الخارجي) يُرجع الرّبط التّام الخارجي (يُختصر أحيانا إلى الرّبط التّام) جميع الصفوف من كلا الجدولين المُحددين في الشّرط ON في حالة لم يتحقق شرط الرّبط (ما يشمل القيم الفارغة NULL). عيّنة SQL SELECT columns FROM TableA FULL JOIN TableB ON A.columnName = B.columnName الربط التام (الخارجي) دون تقاطع هذا فرع من الرّوابط التّامة الخارجية يُرجع جميع الصفوف من كلا الجدولين المُحدّدين في الشّرط ON في حالة لم يتحقّق شرط الرّبط (باستثناء القيم الفارغة NULL). عيّنة SQL SELECT columns FROM TableA FULL JOIN TableB ON A.columnName = B.columnName WHERE A.columnName IS NULL OR B.columnName IS NULL ختاما غطّى هذا الدّرس الأنواع الأربعة الأساسيّة لروابط SQL وثلاثة روابط مُتفرّعة منها. الروابط هي التي تجعل من قواعد البيانات العلائقيّة علاقية، لذا إن لم تحترف أنواع روابط SQL المختلفة، فاستعمل TeamSQL الآن واستخدم عينات SQL الموفَّرة في هذا الدّرس لتُلقي نظرة على كيفيّة الحصول على أجزاء مختلفة من البيانات الخاصة بك. ترجمة -بتصرّف- للمقال The Seven Types of SQL Joins لصاحبه Eren Baydemir.
  10. بايثون و Django زوج يعدّ بديلا كاملا للغة PHP، حاول تعلّم Django، إن وجدت أنّ تعلّمه صعب، فابدأ بإطار العمل Flask كبداية، فهو أبسط وأقرب إلى البرمجة بلغة بايثون دون إطار عمل. إن أردت تعلّم Flask، فابدأ بهذه الدروس التي كتبتُها، ابدأ من أول درس وأكمل صعودا، السلسة الثّانيّة (ذات الـ24 درسا) بطيئة نوعا ما، لأنّها تشرح الكثير من التّفاصيل التي قد لا تكون مُهمّة في البداية، إلّا أنّها ستكون مُهمّة عند تطوير تطبيق مُعقّد، وتهدف إلى أن تكون بديلا للتوثيقات الأجنبيّة، أي أنّك لا تحتاج إلى حفظ كل شيء في آن واحد، بل هي أقرب إلى المرجع الذي تبحث فيه عمّا تُريده في وقت الحاجة، لذا أنصحك بالصّبر. دروس Flask تشرح الكثير من التّفاصيل الدّقيقة التي ستُفيدك حتى عند انتقالك إلى Django أو لغة برمجة أخرى تتخصّص في تطوير الويب. الخلاصة أنّك لن تخسَر شيئا، سواء تعلّمت Flask أو Django فكلاهما يُغطّي ما تحتاج إليه. انظر الفرق بين Flask و Django.
  11. لغة بايثون لغة شاملة مطلوبة في مجالات عديدة عالميّا، مثل تطوير الويب (من الواجهة الخلفيّة، أي ما يتعلّق بالخوادم وقواعد البيانات)، تحليل البيانات وإدارة الخوادم وحتى تعلّم الآلة إلى غير ذلك من المجالات الجديدة نوعًا ما. ولأنّ هذه المجالات جديدة نسبيا في العالم العربي -عدا تطوير الويب- فمن الطبيعي أن لا تجد ما تُريده. وأتّفق مع مُحمّد في إجابته. لا تهُمّ لغة البرمجة التي تبدأ بها، المُهمّ أن تتعلّم أساسيّات البرمجة وتفهم كيفَ تنشئ تطبيقات بسيطة كخُطوة أولى. بعدها لن تحتاج إلى إجابة لسُؤالِك هذا، لأنّك ستفهم لمَ وكيفَ تستغلّ معرفتك آنذاك. لذا ما دُمتَ بدأت بلغة بايثون، فأنصحك أن تُكمِل فيها إلى أن تتعلّم أساسيّاتها على الأقل، فإن أردت الانتقال إلى لغة أخرى فسيسهُل عليك ذلك. وإن وجدت مشاريع تُريد العمل عليها بلغة بايثون فيما تنفَع فيه (تطوير تطبيقات الويب مثلًا) فافعَل ذلك. وفّقك الله.
  12. مقدّمة من الممكن أن ننخدع بفكرة أنّ الخوادم لن تُهاجَم ما دام الخادوم جديدا، زواره قلائل أو أنّ المخترقين لن يستفيدوا شيئا من اختراقه. لكنّ العديد من الهجمات تكون مُؤتمتة وتُصمَّمُ خصيصا للبحث عن الأخطاء الشّائعة التي تُرتَكب عند ضبط الخادوم. تقوم هذه البرمجيات بفحص الشبكات لاكتشاف الخوادم فقط، ولا تكثرت بمحتواها. تمكين الاتصالات الخارجيّة من أكثر الحالات الشّائعة التي قد تُؤدي إلى وصول غير مُصرّح إلى قاعدة بيانات PostgreSQL. يُمكن أن يحدث هذا لأنّ الإعدادات تسمح للبرمجيات باستكشاف الخوادم الضعيفة بسهولة. في هذا الدّرس، سنلقي نظرة على كيفيّة تقليل خطر الوصول غير المُصرّح الذي يطرحه تفعيل الاتّصالات البعيدة (remote connections). ورغم أنّ هذه خطوة أولى بغاية الأهميّة، وبما أنّ الخوادم قد تتعرّض للاختراق بطرق أخرى، فإنّنا ننصح باتّخاذ إجراءات إضافيّة لحماية بياناتك، والتي يُمكنك أن تجدها في جزء "إجراءات إضافيّة لمزيد من الحماية” من هذا الدّرس. الوضعيّة لفهم الخطر الذي نحاول تخفيفه، تخيّل الخادوم على أنّه متجر صغير. إن كان المتجر يُنصت (listening) على أي منفذ (port)، فهذا يُكافئ قلب لافتة تُشير إلى أنّ المتجر "مفتوح”. أي أنّ الخادوم يكون مرئيّا على الشّبكة، ما يُمكّن البرمجيات المؤتمتة من إيجاده. يُمكننا أن نتخيّل بأنّ كلّ منفذ عبارة عن طريقة للدّخول إلى المتجر، مثل باب أو نافذة مثلا. يُمكن لهذه المداخل أن تكون مفتوحة، مُغلقة، مُقفلة أو مُعطّلة حسب حالة البرمجيّة التي تقوم بالإنصات، لكنّ الإنصات على واجهة عامّة يعني بأنّ البرمجيات الخبيثة تستطيع مُحاولة الدّخول. فمثلا، يُمكن أن تُحاول البرمجيّة استعمال كلمة مرور افتراضيّة على أمل أنّها لم تتغيّر. يُمكن لها كذلك استغلال ثغرات أمنيّة موجودة في البرنامج الذي يُنصِتُ على أمل أنّها لم تُصلَح بعد. يُمكن مُحاولة العديد من الأساليب، إن تمكّنت البرمجيّة الخبيثة من إيجاد نقطة ضعف وقامت باستغلالها، فهذا يعني بأنّ الوصول إلى الخادوم سيتمّ بنجاح وسيتمكّن الهجوم من تخليف خسائر كبيرة. إن قُمنا بتقييد عفريت (daemon) معيّن مثل postgresql ليُنصت محليّا فقط، فهذا مُشابه لمحو الباب الذي يوصل إلى الخارج. ولن يُمكن مُحاولة أي شيء آخر للوصول إلى Postgres. تحمي الجدران النّاريّة (Firewalls) وشبكات VPN بطريقة مُشابهة. في هذا الدّرس، سنُركّز على حذف الباب العمومي الذي يوصل إلى PostgreSQL. لحماية العفريت أو البيانات أثناء نقلها أو تخزينها، انظر فقرة "إجراءات إضافيّة لمزيد من الحماية” من هذا الدّرس. المُتطلّبات سنستعمل في هذا الدّرس خادومي Ubuntu، الأول لمُضيف قاعدة البيانات والآخر ليعمل كعميل يتّصل بالمُضيف عن بُعد. يجب على كلّ خادوم أن يُجهَّز بمُستخدم sudo وجدار ناري مُفعّل. يُمكنك الاستعانة بدرس الإعداد البدئي لخادوم Ubuntu. مُضيف قاعدة البيانات PostgreSQL (Ubuntu 16.04) إن لم تقم بتنصيب PostgreSQL بعد، يُمكنك القيام بذلك باستخدام الأوامر التّاليّة: sudo apt-get update sudo apt-get install postgresql postgresql-contrib آلة العميل (Ubuntu 16.04) لاختبار تمكين الاتصالات البعيدة، سنستعمل عميل PostgreSQL psql. لتنصيبها، استعمل الأوامر التّاليّة: sudo apt-get update sudo apt-get install postgresql-client عند استيفاء هذه المُتطلبات، ستكون جاهزا لاتّباع هذا الدّرس. فهم الإعداد الافتراضيّ عند تنصيب PostgreSQL من مستودع حزم Ubuntu، فالخيار الافتراضيّ هو الانصات على المُضيف المحليّ (localhost). يُمكن تغيير هذا الخيار الافتراضي عبر تعديل مقطع listen_addresses على ملفّ postgresql.conf، لكنّ هذا الإعداد الافتراضي يمنع الخادوم من الانصات آليّا على واجهة عموميّة (public interface). علاوة على ما سبق، فالملفّ pg_hba.conf لا يسمح سوى لاتّصالات من مقابس أسماء نطاقات Unix/Linux (Unix/Linux domain sockets)، وعنوان الاسترجاع (loopback address) الخاصّ بالخادوم المحلي، ما يعني بأنّ الاتّصالات من مُضيفات خارجيّة لن تُقبَل: # Put your actual configuration here # ---------------------------------- # # If you want to allow non-local connections, you need to add more # "host" records. In that case you will also need to make PostgreSQL # listen on a non-local interface via the listen_addresses # configuration parameter, or via the -i or -h command line switches. # DO NOT DISABLE! # If you change this first entry you will need to make sure that the # database superuser can access the database using some other method. # Noninteractive access to all databases is required during automatic # maintenance (custom daily cronjobs, replication, and similar tasks). # # Database administrative login by Unix domain socket local all postgres peer # TYPE DATABASE USER ADDRESS METHOD # "local" is for Unix domain socket connections only local all all peer # IPv4 local connections: host all all 127.0.0.1/32 md5 # IPv6 local connections: host all all ::1/128 md5 هذه الإعدادات الافتراضيّة تُحقّق هدف منع الانصات على واجهة عموميّة. إن تركناها على حالها وأبقينا الجدار النّاري مُفعّلا، فهذا كلّ ما في الأمر! يُمكننا الآن الانتقال إلى قسم "إجراءات إضافيّة لمزيد من الحماية” للتّعرف على كيفيّة حماية البيانات أثناء نقلها. إن أردت الاتصال من مُضيف بعيد، فسنتطرّق إلى كيفيّة تعديل الإعدادات الافتراضيّة إضافة إلى الخطوات التي يجب اتّخاذها فورا لحماية الخادوم في الفقرة التّاليّة. إعداد الاتّصالات البعيدة (Remote Connections) لإعداد بيئة إنتاج قويّة، وقبل بدء العمل مع بيانات حسّاسة، من المُفضّل تشفير مرور (traffic) PostgreSQL باستخدام SSL، إضافة إلى حماية باستخدام جدار ناري خارجي أو شبكة افتراضيّة خاصّة (VPN). قبل القيام بالأمور السابقة ذكرها، يُمكننا اتّخاذ طريق أقل تعقيدا عبر تفعيل جدار ناريّ على خادوم قاعدة البيانات الخاص بنا وتقييد الوصول لتقبل فقط المُضيفات التي تحتاج إلى الوصول إلى الخادوم. الخطوة الأولى – إضافة مُستخدم وقاعدة بيانات سنبدأ بإضافة مُستخدم وقاعدة بيانات لأغراض تجريبيّة. للقيام بذلك، سنستعمل عميل PostgreSQL psql للاتصال بصفة المُستخدم الإداري postgres. عبر تمرير الخيار -i للأمر sudo سيتمّ تشغيل صدفة تسجيل الدّخول (login shell) الخاصّة بالمُستخدم postgres، ما يضمن بأنّ الخيارات في ملفّ .profile أو في موارد أخرى مُتعلّقة بتسجيل الدّخول ستُحمَّل. يقوم الخيار -u بتحديد المُستخدم postgres: sudo -i -u postgres psql بعدها، سنقوم بإنشاء مُستخدم بكلمة مرور. تأكّد من استعمال كلمة مرور جيّدة عوضا عن المقطع mypassword في المثال أسفله: CREATE USER sammy WITH PASSWORD 'mypassword'; إن تمّ إنشاء المُستخدم بنجاح، فسنستقبل المُخرج التّالي: CREATE ROLE مُلاحظة: منذ الإصدار 8.1 من PostgreSQL، فالأدوار (ROLES) والمُستخدمون (USERS) يشتركون في المعنى.لكنّ هناك اتّفاقا يقول بأنّه إن كان لدور كلمة مرور فإنّنا نُسمّيه مُستخدما، ونُسمّي الدّور عديم كلمة المرور دورا، لذا أحيانا ستحصل على ROLE في المُخرج رغم أنّك تتوقّع أن ترى USER. تاليّاََ، سنُنشئ قاعدة بيانات وسنمنح كامل صلاحيّات الوصول لمُستخدمنا الجديد. تقول أفضل الممارسات بمنح المُستخدمين صلاحيّات الوصول التي يجتاجونها فقط، وعلى الموارد التي يجب أن يحصلوا عليها فقط، لذا فاعتمادا على حالة الاستخدام ( use case)، قد يُفضّل تقييد أحقيّة الوصول للمُستخدم. . CREATE DATABASE sammydb OWNER sammy; عند إنشاء قاعدة البيانات بنجاح، سنستقبل التّأكيد التّالي: CREATE DATABASE بعد إنشاء المُستخدم وقاعدة البيانات، سنقوم بالخروج من سطر أوامر PostgreSQL: \q بعد الضغط على مفتاح ENTER، سنرجع إلى سطر الأوامر وسنكون جاهزين للمُتابعة. الخطورة الثّانيّة – إعداد UFW في درس الإعداد البدئي لخادوم Ubuntu ، قمنا بتفعيل UFW وسمحنا لاتّصالات SSH فقط. قبل بدء الإعداد، لنتحقّق من حالة UFW: sudo ufw status مُلاحظة: إن كان المخرج يدُلّ على أنّ الجدار النّاري غير مُفعّل (inactive)، يُمكننا تفعيله بالأمر التّالي: sudo ufw enable بعد التّفعيل، فإنّ إعادة تنفيذ الأمر sudo ufw status سيستعرض القواعد الحاليّة. فعّل SSH إن كان ذلك مطلوبا: sudo ufw allow OpenSSH في حالة لم تُغيِّر من المُتطلّبات، فمُخرج الأمر sudo ufw status سيُشير إلى أنّ OpenSSH هي الخدمة الوحيدة المُفعّلة: Status: active To Action From -- ------ ---- OpenSSH ALLOW Anywhere OpenSSH (v6) ALLOW Anywhere (v6) بعد التّحقّق من حالة الجدار النّاري، سنقوم بالسماح بالوصول إلى منفذ PostgreSQL وسنُقيّد الوصول لنسمح فقط للمُضيف أو المُضيفات المرغوبة. سيُضيف الأمر أسفله قاعدة للمنفذ الافتراضيّ لـPostgreSQL، أي المنفذ رقم 5432. إن غيّرت هذا المنفذ، فتأكّد من تعديل الأمر أسفله. تأكّد من استعمال عنوان IP الخاص بالخادوم الذي يحتاج إلى الوصول. أعد تنفيذ الأمر لإضافة كلّ عميل من العملاء الذين ترغب بإعطائهم أحقيّة الوصول إن كان ذلك لازما: sudo ufw allow from client_ip_address to any port 5432 استبدل client_ip_address بعنوان IP الخاصّ بالعميل. للتّحقّق من أنّ القاعدة قد طُبِّقت، يُمكنك تنفيذ الأمر ufw status مُجدّدا: sudo ufw status المُخرج: To Action From -- ------ ---- OpenSSH ALLOW Anywhere 5432 ALLOW client_ip_address OpenSSH (v6) ALLOW Anywhere (v6) مُلاحظة: إن لم تكن لديك دراية مُسبقة بأساسيّات UFW، يُمكنك تعلّم المزيد في درس أساسيات UFW: قواعد وأوامر شائعة للجدار الناري . بعد تجهيز قاعدة الجدار النّاريّ هذه، سنقوم الآن بإعداد PostgreSQL لتُنصت على عنوان IP العمومي. سنقوم بهذا عبر تعديل إعدادَيْن، خانة للمُضيف المُتصل في pg_hba.conf وإعداد listen_addresses في postgresql.conf. الخطوة الثّالثة – إعداد المُضيفات المسموح لها (Allowed Hosts) سنبدأ عبر إضافة خانة المُضيف في ملفّ pg_hba.conf. إن كنت تستعمل نسخة أخرى غيْرَ النُّسخةِ 9.5 من PostgreSQL فتأكّد من تعديل الأمر أسفله قبل تنفيذه: sudo nano /etc/postgresql/9.5/main/pg_hba.conf سنضع أسطر host تحت مقطع التّعليقات الذي يصف كيفيّة السّماح للاتصالات غير المحليّة. سنُضيف سطرا يحمل العنوان العمومي الخاص بخادوم قاعدة البيانات لاختبار ما إذا كان الجدار النّاري مُعدّا بشكل صحيح. استبدل المقطع client_ip_address بعنوان IP الخاص بآلة العميل الخاصّ بك: # If you want to allow non-local connections, you need to add more # "host" records. In that case you will also need to make PostgreSQL # listen on a non-local interface via the listen_addresses # configuration parameter, or via the -i or -h command line switches. host sammydb sammy client_ip_address/32 md5 قبل حفظ التغييرات، لننظر إلى كل قيمة من قيم السّطر الذي أضفناه في حالة كنت ترغب تعديل أي منها: المُضيف، المُعامل host يُحدّد بأنّ اتّصال TCP/IP سيُستَعمَل. قاعدة البيانات، العمود الثّاني، sammydb، يُحدّد أي قاعدة بيانات يُمكن للمُضيف أن يتّصل بها، يُمكنك تعيين أكثر من قاعدة بيانات واحدة عبر تفرقة أسمائها بالفاصلة ,. المُستخدم، sammy، يُحدّد المُستخدم المسموح له بالاتّصال. وكما مع عمود قاعدة البيانات، فتستطيع تحديد أكثر من مستخدم واحد باستعمال علامة الفاصلة. العنوان، يُحدّد عنوان آلة العميل ويُمكن أن يكون عبارة عن اسم مُضيف (hostname)، مجال عناوين IP (IP address range) أو كلمات مفتاحيّة خاصّة. في المثال أعلاه، قمنا بالسماح لعنوان IP الخاصّ بالعميل فقط. طريقة الاستيثاق (auth-method)، في الأخير، يُمكن تحديد طريقة استيثاق، يُشير md5 إلى كلمة مرور مزدوجة التّشفير بـMD5 ( double-MD5-hashed password ) لن تحتاج سوى كلمة المرور التي تم إنشاؤها للمُستخدم الذي سيقوم بالاتّصال. للمزيد من المعلومات وإعدادات إضافيّة راجع التوثيق الرّسمي لـPostgreSQL حول ملفّ pg_hba.conf. بعد الانتهاء من التّعديلات، احفظ وأغلق الملف. الخطوة الرّابعة – إعداد عنوان الإنصات (Listening Address) سنقوم الآن بضبط عنوان الإنصات في ملفّ postgresql.conf (تأكّد من تصحيح رقم النّسخة): sudo nano /etc/postgresql/9.5/main/postgresql.conf أضف عناوين الإنصات تحت سطر listen_addresses، تأكّد من استبدال server_ip_address بعنوان IP أو اسم مُضيف قاعدة البيانات الخاصّة بك وليس عنوان العميل الذي سيقوم بالاتصال: #listen_addresses = 'localhost' # what IP address(es) to listen on; listen_addresses = 'localhost,server_ip_address' احفظ وأغلق الملفّ عند الانتهاء من إجراء التّعديلات. الخطوة الخامسة – إعادة تشغيل PostgreSQL لن تُطبَّق التعديلات حتى نُعيد تشغيل عفريت (daemon) PostgreSQL، لذا سنقوم بهذا قبل أن نبدأ بالتجربة: sudo systemctl restart postgresql وبما أنّ systemctl لا يوفّر تغذيّة راجعة (feedback)، فسنتحقّق من نجاح إعادة تشغيل العفريت: sudo systemctl status postgresql إن احتوى المُخرج على Active: active وانتهى بمقطع مُشابه لما يلي، فهذا يعني بأنّ عفريتPostgreSQL مُفعّل. ... Jan 10 23:02:20 PostgreSQL systemd[1]: Started PostgreSQL RDBMS. بعد إعادة تشغيل العفريت، يُمكننا الآن التّجربة. الخطوة السّادسة – تجربة الاتّصال لنتحقّق من أنّنا نستطيع الاتّصال من جهاز العميل الخاص بنا. للقيام بهذا، سنستعمل الأمر psql مع الخيّار -U لتحديد المُستخدم، الخيار -h لتحديد عنوان IP الخاصّ بالعميل و -d لتحديد قاعدة البيانات، وذلك لأنّنا ضيّقنا الحماية لكي يتمكّن sammy فقط من الاتّصال بقاعدة بيانات واحدة فقط. psql -U sammy -h postgres_host_ip -d sammydb استبدل postgres_host_ip بعنوان IP الخاصّ بمُضيف PostgreSQL. إن تمّ إعداد كل شيء بشكل صحيح، فيجب أن تستقبل المحثَّ (prompt) التّالي: Password for user sammy: أدخل كلمة المرور التّي حدّدتها مسبقا عندما أضفت المُستخدم sammy في مرقاب (monitor) PostgreSQL. إن حصلت على المحثّ التّالي، فهذا يعني بأنّ الاتصال قد تمّ بنجاح: sammydb=> هذا يُؤكّد على أنّنا نستطيع تجاوز الجدار النّاري وأن نتّصل بقاعدة البيانات. سنقوم الآن بالخروج من المحثّ: \q بعد التّحقّق من أنّ الإعدادات قد ضُبِطت بنجاح، سنقوم بتنظيف مُخلّفات التّجربة. الخطوة السّابعة – حذف قاعدة البيانات والمُستخدم التّجريبيّين بعد اختبار الاتّصال، يُمكننا الآن العودة إلى المُضيف واستخدام الأمر التّالي لحذف قاعدة البيانات والمُستخدم. sudo -i -u postgres psql لحذف قاعدة البيانات: DROP DATABASE sammydb; عند نجاح العمليّة، ستستقبل المُخرج التّالي: DROP DATABASE لحذف المُستخدم: DROP USER sammy; المُخرج عند نجاح العمليّة: DROP ROLE سنقوم بإنهاء عمليّة التّنظيف بحذف خانة المُضيف الخاصّة بقاعدة البيانات sammydb من ملفّ pg_hba.conf لأنّنا لم نعد نحتاج إليها: sudo nano /etc/postgresql/9.5/main/pg_hba.conf استبدل 9.5 برقم النّسخة الخاصّة بك. السّطر الذي يجب عليك حذفه: host sammydb sammy client_ip_address/32 md5 ليُطبَّقَ التّعديل، سنقوم بحفظ وإغلاق الملفّ ومن ثمّ نُعيد تشغيل خادوم قاعدة البيانات: sudo systemctl restart postgresl للتحقّق من أنّ إعادة التّشغيل قد تمّت بنجاح، سنطّلع على الحالة: sudo systemctl status postgres إن كان المُخرج يحتوي على Active: active فهذا يعني بأنّ إعادة التّشغيل قد تمّت بنجاح. يُمكنك الآن ضبط التّطبيق أو الخدمة على العميل التي تحتاج إلى إمكانيّة الاتصال عن بعد. خاتمة اتّخذنا في هذا الدّرس الخطوات الأساسيّة لحماية PostgreSQL عبر إعداد الجدار النّاريّ الخاصّ بالخادوم ليسمح فقط للاتصالات من المُضيفات التي تحتاج إلى صلاحيّات الوصول وعبر ضبط PostgreSQL لتقبل الاتصالات من هذه المُضيفات فقط. هذا يُخفّف من خطر بعض من أنواع الهجمات. تعتبر هذه الإجراءات الخطوة الأولى فقط لحماية البيانات، وننصح بمراجعة واتّخاذ الإجراءات الأمنية الإضافيّة المذكورة أعلاه. ترجمة -بتصرّف- للمقال How To Secure PostgreSQL Against Automated Attacks لصاحبته Melissa Anderson.
  13. كتبت مؤخّرا برنامج Bash قصير لنسخ ملفّات MP3 من مفتاح USB من مُضيف شبكة (network host) إلى مُضيف شبكة آخر. تُنسَخ الملفّات إلى مجلّد خاصّ على خادوم أقوم بتشغيله لمؤسّسة تطوعيّة، ما يسمح بتشغيل وتنزيل الملفّات. يقوم برنامجي ببضعة أمور أخرى، مثل تعديل أسماء الملفّات قبل نسخها لتكون مرتّبة تلقائيّا حسب التّاريخ على صفحة الويب. كما تحذف جميع الملفّات على مفتاح USB بعد التّأكد من اكتمال النّقل بنجاح. يأتي هذا البُريْمِج ببضعة خيارات، مثل -h لعرض المُساعدة، و -t لنمط الاختبار (test mode) وبضعة خيارات أخرى. رغم أنّ برنامجي الصغير هذا جميل، إلّا أنّه يحتاج إلى تشغيله بالمُستخدم الجذر (root) للقيام بالعمليّات الأساسيّة. للأسف، لا تمتلك هذه المؤسّسة أشخاصا مهتمين بإدارة أنظمة الحواسيب، ما يدفعني للبحث عن أشخاص بقدرات تقنيّة متواضعة لتدريبهم على كيفيّة تسجيل الدّخول إلى الحاسوب الذي يعمل على نقل الملفّات وتشغيل هذا البرنامج. صحيح بأنّني أستطيع تشغيل البرنامج بنفسي، إلّا أنّ بضعة أسباب (كالمرض والسّفر) قد تحول دون ذلك. وحتى لو كنتُ متاحا، فبصفتي مدير نُظم كسول، أحب أن يقوم الآخرون بعملي من أجلي. لذا أكتب برامج لأتمتة (automate) هذه المهام وأستعمل Sudo لتمكين بضعة مُستخدمين من تشغيل البرامج.يتطلّب تنفيذ العديد من أوامر Linux صلاحيّات المُستخدم الجذر. هذا يحمي النظام من التخريب الخبيث أو غير المقصود. استعمال أداة Sudo يُمكنّ برنامج sudo مدراء النّظم ذوي صلاحيّات الجذر من تفويض المسؤوليّة لبضعة مهام أو جميعها لمستخدمين آخرين لنفس الحاسوب. كما يسمح لي بتنفيذ هذا التفويض دون توفير كلمة مرور المُستخدم الجذر، ما يوفّر مستوى عاليّا من الحماية على المُضيف. لنفترض على سبيل المثال بأنّني أعطيت للمُستخدم ruser أحقيّة الوصول إلى برنامج Bash خاص بي باسم myprog، والذي يحتاج إلى صلاحيّات المستخدم الجذر لتنفيذ بعض من وظائفه. يقوم المستخدم ruser بتسجيل الدّخول أولا باستعمال كلمة المرور الخاصّة به، وبعدها ينفّذ الأمر التّالي لتشغيل myprog. sudo myprog يقوم برنامج sudo بالاطّلاع على الملفّ /etc/sudoers ويتحقّق من أنّ لـruser إذنا يُمكّنه من تشغيل myprog. إن كان الأمر كذلك، يطلب sudo من المُستخدم كلمة مروره -وليس كلمة مرور المُستخدم الجذر-، بعد إدخال كلمة المرور، يتمّ تنفيذ البرنامج. يقوم sudo كذلك بتسجيل معلومات الوصول إلى myprog مع التاريخ والوقت الذي تمّ فيه تشغيل البرنامج إضافة إلى سطر الأمر والمُستخدم الذي قام بتنفيذه. تُسجّل هذه البيانات في ملفّ /var/log/security. أجد بأنّ سجلّ الأوامر التي تم تنفيذها مفيد عند التّدريب. إذ يسمح لي هذا بالتعرّف على من قام بماذا وما إن أدخل الأمر بشكل صحيح. قمت باستخدام هذه الميّزة لتفويض الصلاحيّات لي ولمُستخدم آخر للتمكّن من تشغيل برنامج واحد؛ لكن لـsudo إمكانيّات أكبر من ذلك. إذ يسمح لمدير النظام بتفويض السُّلطَة لإدارة وظائف الشّبكة أو خدمات مُعيّنة لشخص واحد أو مجموعة من المُستخدمين الموثوقين. يُمكّن هذا من تفويض أحقيّة تشغيل هذه الوظائف ويحمي كلمة مرور المُستخدم الجذر في نفس الوقت. ضبط ملفّ sudoers بصفتي مدير نُظم، يُمكنني استعمال ملفّ /etc/sudoers للسماح للمستخدمين أو مجموعات من المُستخدمين بالوصول إلى أمر مُعيّن، مجموعة محدّدة من الأوامر أو جميع الأوامر. هذه المرونة هي سرّ كل من قوّة وبساطة استعمال sudo للتفويض. بدا لي ملفّ sudoers معقّدا في البداية، لذا نسختُ وحلّلت ملفّ sudoers بالكامل من المُضيف الذي أستخدمه. على أمل أن تفهم الأساسيّات بعد قراءة هذا التّحليل. وجدت كذلك بأنّ ملفّات الإعدادات الافتراضيّة في التوزيعات المبنيّة على Red Hat تحتوي على الكثير من التّعليقات والأمثلة التي تُسهّل المأموريّة. لا تستعمِل محرّر النصوص الاعتياديّ عند تعديل ملفّ sudoers. استعمل الأمر visudo لتطبيق التّغييرات حالما تحفظ الملفّ وتخرجُ من المُحرّر. يُمكن استعمال محرّرات أخرى عوضا عن Vi بنفس الشكل الذي نستعمل فيه visudo. لنبدأ بتحليل هذا الملفّ من البداية مع بضعة أنواع من الأسماء المُستعارة (aliases). الأسماء المُستعارة الخاصّة بالمُضيفات Host aliases يُستعمل جزء الأسماء المُستعارة الخاصّة بالمُضيفات لإنشاء مجموعات من المُضيفات التي يُمكن عليها استعمال الأوامر أو الأسماء المستعارة للأوامر لمنح أحقيّة الوصول. الفكرة أن يُصان (maintain) هذا الملفّ الوحيد من أجل جميع المُضيفات في المؤسّسة ويُنسخ إلى مُجلّد /etc الخاص بكل مُضيف. وبالتالي يُمكن ضبط بعض المُضيفات (مثل الخوادم) لتُشكّل مجموعة لمنح بعض المُستخدمين أحقيّة الوصول إلى أوامر مُخصّصة، مثل إمكانيّة تشغيل أو إيقاف خدمات مثل HTTPD، DNS والتشبيك (networking) أو وصل (mount) أنظمة الملفّات وما إلى ذلك. يُمكن استعمال عناوين IP عوضا عن أسماء المُضيفات في الأسماء المُستعارة الخاصّة بالمُضيفات. ## Sudoers allows particular users to run various commands as ## the root user, without needing the root password. ## ## Examples are provided at the bottom of the file for collections ## of related commands, which can then be delegated out to particular ## users or groups. ## ## This file must be edited with the 'visudo' command. ## Host Aliases ## Groups of machines. You may prefer to use hostnames (perhaps using ## wildcards for entire domains) or IP addresses instead. # Host_Alias FILESERVERS = fs1, fs2 # Host_Alias MAILSERVERS = smtp, smtp2 ## User Aliases ## These aren't often necessary, as you can use regular groups ## (ie, from files, LDAP, NIS, etc) in this file - just use %groupname ## rather than USERALIAS # User_Alias ADMINS = jsmith, mikem User_Alias AUDIO = dboth, ruser ## Command Aliases ## These are groups of related commands... ## Networking # Cmnd_Alias NETWORKING = /sbin/route, /sbin/ifconfig, /bin/ping, /sbin/dhclient, /usr/bin/net, /sbin/iptables, /usr/bin/rfcomm, /usr/bin/wvdial, /sbin/iwconfig, /sbin/mii-tool ## Installation and management of software # Cmnd_Alias SOFTWARE = /bin/rpm, /usr/bin/up2date, /usr/bin/yum ## Services # Cmnd_Alias SERVICES = /sbin/service, /sbin/chkconfig ## Updating the locate database # Cmnd_Alias LOCATE = /usr/bin/updatedb ## Storage # Cmnd_Alias STORAGE = /sbin/fdisk, /sbin/sfdisk, /sbin/parted, /sbin/partprobe, /bin/mount, /bin/umount ## Delegating permissions # Cmnd_Alias DELEGATING = /usr/sbin/visudo, /bin/chown, /bin/chmod, /bin/chgrp ## Processes # Cmnd_Alias PROCESSES = /bin/nice, /bin/kill, /usr/bin/kill, /usr/bin/killall ## Drivers # Cmnd_Alias DRIVERS = /sbin/modprobe # Defaults specification # # Refuse to run if unable to disable echo on the tty. # Defaults !visiblepw Defaults env_reset Defaults env_keep = "COLORS DISPLAY HOSTNAME HISTSIZE KDEDIR LS_COLORS" Defaults env_keep += "MAIL PS1 PS2 QTDIR USERNAME LANG LC_ADDRESS LC_CTYPE" Defaults env_keep += "LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES" Defaults env_keep += "LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE" Defaults env_keep += "LC_TIME LC_ALL LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY" Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin ## Next comes the main part: which users can run what software on ## which machines (the sudoers file can be shared between multiple ## systems). ## Syntax: ## ## user MACHINE=COMMANDS ## ## The COMMANDS section may have other options added to it. ## ## Allow root to run any commands anywhere root ALL=(ALL) ALL ## Allows members of the 'sys' group to run networking, software, ## service management apps and more. # %sys ALL = NETWORKING, SOFTWARE, SERVICES, STORAGE, DELEGATING, PROCESSES, LOCATE, DRIVERS ## Allows people in group wheel to run all commands %wheel ALL=(ALL) ALL ## Same thing without a password # %wheel ALL=(ALL) NOPASSWD: ALL ## Allows members of the users group to mount and unmount the ## cdrom as root # %users ALL=/sbin/mount /mnt/cdrom, /sbin/umount /mnt/cdrom ## Allows members of the users group to shutdown this system # %users localhost=/sbin/shutdown -h now ## Read drop-in files from /etc/sudoers.d (the # here does not mean a comment) #includedir /etc/sudoers.d ################################################################################ # Added by David Both, 11/04/2017 to provide limited access to myprog # ################################################################################ # AUDIO guest1=/usr/local/bin/myprog الأسماء المستعارة الخاصّة بالمُستخدمين User aliases تُعطي الأسماء المستعارة الخاصّة بالمُستخدمين إمكانيّة ترتيب المُستخدمين إلى مجموعات ذات أسماء مُستعارة للمُستخدم الجذر، بهذه الطّريقة ستتمكّن مجموعة كاملة من المستخدمين من الوصول إلى صلاحيات مدير محدّدة. هذا هو الجزء الذي أضَفْتُ فيه السّطر User_Alias AUDIO = dboth, ruser، والذي يقوم بتعيين مُستخدمَيْنِ للاسم المُستعار AUDIO. يُمكن الاعتماد على المجموعات المنشأة مُسبقا في ملفّ /etc/groups عوضا عن الأسماء المستعارة. إن كانت أحد المجموعات في هذا الملفّ تفي بالغرض، مثل مجموعة audio فيُمكنك استخدام اسم المجموعة مسبوقا بعلامة % كما يلي: %audio عند تعيين الأوامر التي ستُوفَّرُ للمجموعات في ملفّ sudoers. الأسماء المستعارة للأوامر Command aliases في جزء الأسماء المستعارة للأوامر، نقوم بتوفير قائمة للأوامر ذات الصّلة، مثل أوامر التّشبيك أو الأوامر التي تقوم بتنصيب التّحديثات أو حزم RPM جديدة. تُمكّن هذه الأسماء المستعارة مدير النّظام من منح تصريح للوصول إلى مجموعة من الأوامر. تم مُسبقا إعداد عدد من هذه الأسماء المستعارة في هذا الجزء، ما يجعل تفويض أحقية الوصول لنوع محدد من الأوامر أمرا سهلا. القيم الافتراضيّة للبيئة Environment defaults يقوم الجزء التّالي بتعيين عدد من متغيّرات البيئة (environment variables). أكثر سطر مثير للاهتمام في هذا الجزء هو السّطر !visiblepw، والذي يمنع تشغيل sudo إن كانت بيئة المُستخدم تسمح بعرض كلمة المرور. هذا إجراء وقائي لا يجب تعديله. قسم الأوامر Command section قسم الأوامر هو الجزء الرّئيسي في ملفّ sudoers. يمكن القيام بأي شيء ترغب به دون الحاجة إلى الأسماء المستعارة عبر إضافة خانات هنا. لكنّ الأسماء المستعارة تجعل الأمر في غاية السّهولة. يقوم هذا القسم باستخدام الأسماء المُستعارة التي تم تعريفها أعلاه لإخبار sudo من لديه الحقّ للقيام بماذا وعلى أي مُضيف. الأمثلة تشرح نفسها ما دمت تفهم القواعد (Syntax) في هذا القسم. لننظر إلى القواعد في قسم الأوامر: ruser ALL=(ALL) ALL المثال أعلاه يقول بأنّ للمُستخدم ruser صلاحيات تُمكّنه من تنفيذ أي برنامج على أي مُضيف بصفة أي مُستخدم. هذه خانة عامّة للمُستخدم ruser. المقطع ALL الأول يدلّ على أنّ هذه القاعدة تُطبَّق على جميع المُضيفات. ALL الثّانيّة تسمح لـruser بتنفيذ الأوامر بصفة أي مُستخدم آخر. افتراضيّا، تُنفَّذُ الأوامر بصفة المُستخدم الجذر، لكنّ لـruser القدرة على انتحال صفة أي مُستخدم آخر عند استعمال الأمر sudo. المقطع ALL الأخير يعني بأنّ ruser يستطيع تنفيذ جميع الأوامر دون أية قيود. ما يمنح لـruser جميع صلاحيّات المُدير. لاحظ الخانة التي تمنح للمُستخدم root جميع صلاحيات الوصول لجميع الأوامر على جميع المُضيفات: root ALL=(ALL) ALL لتجربة هذا، قمت بتعليق (comment) السّطر أعلاه وحاولتُ تنفيذ الأمر chown بصفة المُستخدم الجذر دون sudo. تمّ تنفيذ الأمر. بعدها حاولت استخدام الأمر sudo chown، والذي فشل مع الرّسالة root غير موجود على ملفّ sudoers. سيتم الإبلاغ عن هذه الحادثة. ما يعني بأنّ المُستخدم الجذر قادر على تنفيذ أي أمر بصفته المُستخدم الجذر، لكن أي أمر سيفشل عند استعمال الأمر sudo. سيمنع هذا المُستخدمَ الجذر من تنفيذ أية أوامر بصفته مُستخدما آخر عبر الأمر sudo. لكنّ root يستطيع التحايل على هذا القيد بالعديد من الطّرق. الشّيفرة أسفله هي ما أضفته للتحكم بأحقية الوصول إلى برنامج myprog. يقول السّطر بأنّ المستخدمين الذين ينتمون إلى المجموعة AUDIO التي تم تعريفها أعلى الملفّ لديهم أحقيّة الوصول إلى برنامج واحد فقط (myprog) على مُضيف واحد (guest1). AUDIO guest1=/usr/local/bin/myprog سيُمكّن السّطر أعلاه المستخدمين المنتمين إلى مجموعة AUDIO من الوصول إلى البرنامج myprog على المُضيف guest1. لاحظ بأنّ السّطر أعلاه لا يُحدّد سوى المُضيف الذي يُمكن عليه تنفيذ البرنامج. ولا يُحدّد بأنّ للمُستخدم حريّة تنفيذ الأمر بصفة أي مُستخدم آخر. تجاوز كلمات المرور يُمكنك استخدام الكلمة المفتاحيّة NOPASSWORD لتمكين المُستخدمين المنتمين إلى المجموعة AUDIO من تشغيل برنامج myprog دون الحاجة إلى إدخال كلمة المرور الخاصّة بهم كما يلي: AUDIO guest1=NOPASSWORD : /usr/local/bin/myprog لم أُفعِّل هذا الخيار لبرنامجي، إذ يجب على مُستخدمي sudo التوقف والتفكير في ما يقومون به، وهذا يُساعد قليلا على ذلك. والسّطر أعلاه مُجرّد مثال توضيحيّ. المجموعة wheel معيار wheel المُحدّد في جزء الأوامر داخل ملفّ sudoers (كما هو موضّح أسفله) يقوم بالسماح لجميع المُستخدمين المُنتمين إلى المجموعة wheel بتنفيذ جميع الأوامر على أي مُضيف. تُعرَّف مجموعة wheel على الملفّ /etc/group، ومن الواجب إضافة المُستخدمين هناك. علامة % التي تسبق اسم المجموعة تعني بأنّ على sudo البحث عن هذه المجموعة في ملفّ /etc/group. %wheel ALL = (ALL) ALL يسمح السّطر أعلاه لجميع المُستخدمين الذين ينتمون إلى المجموعة wheel المعرّفة في ملفّ /etc/group بتشغيل جميع الأوامر على أي مُضيف. هذه طريقة جيّدة لتفويض كامل صلاحيّات المُستخدم الجذر لعدّة مُستخدمين دون توفير كلمة المرور الخاصّة بالمُستخدم الجذر. مجرّد إضافة مُستخدم إلى مجموعة wheel يُعطيهم كامل إمكانيّات المُستخدم الجذر. يسمح هذا كذلك بمُراقبة نشاطات المُستخدمين عبر مُدخلات التّسجيلات التي يقوم sudo بإنشائها. تُضيف بعض التوزيعات (مثل توزيعة Ubuntu) مُعرّفات المُستخدمين (user ID) إلى المجموعة wheel في ملفّ /etc/group، ما يسمح لهم باستخدام sudo لتنفيذ جميع الأوامر التي تتطلّب صلاحيّات المُستخدم الجذر. ختاما استعملت sudo هنا لغرض محدود (تمكين مستخدم أو مُستخدمَيْن من الوصول إلى أمر واحد). تمكّنت من تحقيق هذا الغرض عبر كتابة سطرَيْن فقط (دون احتساب التّعليقات). تفويض صلاحيات تنفيذ مهام مُحدّدة لمُستخدمين لا يمتلكون إمكانيّة الوصول إلى المستخدم الجذر عمليّة بسيطة يُمكن لها اقتصاد كم جيّد من الوقت لمُدير النظم. بالإضافة إلى ميّزة تسجيل نشاطات المُستخدمين التي تُساعد على إيجاد المشاكل. يُوفّر ملفّ sudoers كمًّا هائلًا من الإمكانيّات والخيارات. ألقِ نظرة على ملفّات man الخاصّة بـsudo وsudoers لمزيد من التّفاصيل. ترجمة -بتصرّف- للمقال Using sudo to delegate permissions in Linux لصاحبه David Both.
  14. يُستعمل Varnish Cache بكثرة لتخبئة محتوى مواقع الويب لتسريع أداء المواقع وتخفيض الحمولة على الخادوم الأصل (origin-server). لطالما شجعنا على استعمال التّخبئة لتسريع أداء تطبيقات الويب وتسهيل قابلية التوسع (Scalability) وضمان الاستقراريّة إضافة إلى الفوائد الأخرى التي تأتي مع هذه الإجراءات من تحسين لتجربة المُستخدم إلى التوفير في الموارد. لكن رغم ذلك فلا نزال بحاجة إلى التأكيد على أهميّة التّخبئة. وفي بعض الأحيان، قد يعني الأمر شرح كيفيّة استعمال Varnish Cache وما يُميّزه عن بقيّة التقنيات الأخرى. الأمور الغريبة التي تُميّز Varnish Cache Varnish Cache عبارة عن خادوم HTTP بنظام HTTP خلفي له قدرة على تقديم الملفّات. يعتمد على هندسة سلسليّة (threaded architecture)، دون حلقة أحداث (event loop). ولأن شيفرة الكتابة قادرة على استعمال استدعاءات النظام الحاصرة (blocking system calls)، فذلك يجعله أسهل استعمالا من Apache أو NGINX، التي يتطلّب استعمالها التعامل مع حلقة أحداث. يقوم Varnish Cache بالتسجيل (logging) على الذاكرة عوضا عن القرص الصلب، تم تصميمه بهذه الطّريقة لأن تسجيل 10000 من عمليات HTTP كل ثانية على القرص الصلب أمر يتطلب الكثير من الموارد. يقوم Varnish بتسجيل جميع التّفاصيل (حوالي 200 سطر لكل طلب) على الذاكرة مُباشرة. إن لم يتم طلب التفاصيل المُسجّلة فستتم الكتابة فوقها (overwrite). يعتبر Varnish التطبيق الوحيد الذي يتخذ هذا المسلك. المرونة: استعمال VCL الميّزة الكبرى لـ Varnish Cashe هي لغة الضّبط الخاصّة به. تمّ إنشاء لغةVCL (Varnish Configuration Language) منذ أكثر من 11 عاما لتدعم النسخة الأولى من Varnish Cache. على عكس Apache أو بقيّة البرمجيّات، فـVarnish Cache لا يعتمد على إعداد تقليدي.بل يعتمد عوضا عن ذلك على مجموعة من السّياسات التي تُكتب بهذه اللغة. تتم ترجمة هذه السيّاسات إلى شيفرة ثنائية، تُحمّل هذه الشيفرة ويتمّ تشغيلها بعد ذلك. يقول مهندس Varnish Cache Poul-Henning Kamp بأنVCL أُنشأت لتكون مجموعة أدوات يمكن استعمالها حسب الحاجة، وليس قطعة موحدة يُمكن إضافتها إلى بيئة العمل. في يومنا الحالي، هناك أكثر من 100 وحدة (modules) لـ VCL يمكن استعمالها ببساطة. في الحقيقة، ففي ملتقيات Varnish Summits هناك دائما ورشة عمل لتعليم الأشخاص كيفيّة كتابة وحدات خاصّة. تعتبر لغة VCL من أكبر الأسباب التي تجعل النّاس تستعمل وتتعلّق بـVarnish Cache. إذ تفتح المرونة التي توفرها اللغة الأبواب للقيام بأي شيء قد يرغب أحدهم بفعله، ما يمحو القيود التي عادة ما تأتي مع البرمجيّات التقليديّة. إذا، ما هي أهم الأدوات التي توفّرها VCL وكيف يُمكننا الاستفادة منها؟ التنطيف Purging لا يدعم Varnish تنظيف المحتوى عند إعداده لأول مرّة، إذ لا يُمكنك تنزيله وتنصيبه ثمّ استعمال نظام تنظيف دون أية خطوات إضافيّة. عوضا عن تمكين إعدادات افتراضيّة، يتوجب على مستخدمي Varnish Cache العمل على تفعيل الميزات المرغوبة كل على حدة، لكن إعداد نظام تنظيف أمر بسيط، ويمكنك الحصول على النتيجة التي ترغبها. sub vcl_recv { if (req.method == "PURGE") { return (purge); } } إن لم ترغب بتمكين الأشخاص على الأنترنت من تنظيف المحتوى على نظام تخبئتك، فسيتوجب عليك ضبط حدّ الوصول ACL كما يلي: acl purge { "localhost"; "192.168.55.0"/24; } sub vcl_recv { if (req.method == "PURGE") { if (!client.ip ~ purge) { return(synth(405,"Not allowed.")); } return (purge); } } إضافة ميزة لـVarnish Cache: خنق الرّبط السّاخن (Hot linking) يسمح إطار العمل الخاص بـVarnish للمستخدمين بإضافة أية ميزة يحتاجون إليها. أول ميّزة أضيفت هي استعمال VCL للحد من الرّبط السّاخن. الرّبط السّاخن هو عمليّة سرقة موارد أحدهم على الويب وكتابة مقال يستخدم صورا مرفوعة على خادوم الضّحية ليدفع تكاليف استخدام الموارد. يسمح Varnish للخوادم بإيقاف هذه العملية عند استعمال الرّبط السّاخن للموارد الخاصة بالخادم دون إذن. على سبيل المثال، يمكن لـVarnish وضع سقف لعدد مرّات حدوث هذا الأمر كل ثانيّة عبر استخدام وحدة من وحدات Varnish (VMOD) باسم vsthrottle لإضافة الخنق (throttling). يمكن القيام بالأمر عبر استيراد وتحميل الوحدة. شيفرة VCL أسفله تُوضّح كيفيّة الحدّ من الرّبط السّاخن. في هذا المثال، نقوم بتطبيق ثلاثة قواعد. القاعدة الأولى تتحقّق من أن عنوان URL المطلوب يبدأ بـ/assets/. القاعدتان الثانيّة والثّالثة تحميان مجلّد الملفات السّاكنة من الرّبط السّاخن. يتمّ هذا عبر التحقق ممّا إذا كان المُحيل referrer طرفا غير مُتوقّع، وما إن طلبت أسماء نطاق أخرى ملفّاتك لأكثر من 10 مرّات خلال 60 ثانيّة. ما سبق عبارة عن دالّة الخنق، يُستعمل رابط URL كمفتاح للخنق.الحدّ المسموح به هو 10 مرّات كلّ 60 ثانيّة. يُمكن تقديم الملفات السّاكنة غير المُشار إليها، وعندما يحصل ذلك، فسيتم إرسال خطأ 403 مع منع الربط السّاخن. يُمكن كذلك توسيع الخنق لاستعمال memcache، وذلك ليحصل المستخدم على المُحاسبة الرّئيسيّة (central accounting) في العنقود (cluster). import vsthrottle; (..) if (req.url ~ "^/assets/" && (req.http.referer !~ "^http://www.example.com/") && vsthrottle.is_denied(req.url, 10, 60s) { return(error(403,"Hotlinking prohibited"); } التّعامل مع ملفات تعريف الارتباط (cookies) لن يقوم Varnish بتخبئة المحتوى الذي يُطلَبُ باستعمال ملفّات تعريف الارتباط. ولن يستجيب إلى طلب محتوى مخبّأ إن كان الطّلب متعلّقا بملف تعريف ارتباط. عوضا عن هذا، يُمكن استعمال وحدة VMOD لحذف ملفّ تعريف الارتباط. المحترفون يستعملون تعبيرا نمطيّا (regular expression) لترشيح ملفات تعريف الارتباط. ولأنّنا لسنا محترفين، فـVarnish يوفّر وحدة باسم cookie تبدو كما يلي: import cookie; sub vcl_recv { cookie.parse ("cookie1: value1; cookie2: value2"); cookie.filter_except("cookie1"); // get_string() will now yield // "cookie1: cookie2: value2;"; } لا يقوم Varnish بإنشاء ترويسة (header) ملف تعريف ارتباط. وإن رأى بأن النظام الخلفي قد أرسل شيفرة إنشاء ملفّ تعريف ارتباط، فلن يقوم بتخبئته. الحل أن تحذف ترويسة Set-Cookie أو إصلاح الخادوم الخلفي. ترويسات Set-Cookie تقوم بتعطيل ملفّات تعريف الارتباط. الحلّ: احذف ترويسة Set-Cookie أو أصلح النّظام الخلفيّ. نمط الإمهال Grace Mode يُمكن نمط الإمهال Varnish من تقديم محتوى قديم إن لم يكن المحتوى الجديد جاهزا أو مُتوفّرا. يساهم هذا في تحسين الأداء عبر إعادة تحميل المحتوى من النظام الخلفيّ بشكل غير متزامن (asynchronous). في الوقت الذي تم فيه تقديم Varnish 1.0 كحل لنظام تخبئة لموقع الجريدة النرويجيّة Verdens Gang، كانت هناك مشاكل كبرى بسبب التكتّلات السلسليّة (threading pileups). كانت الصفحة الرّئيسيّة للموقع تُقدّم 3000 مرّة كلّ ثانيّة، لكن نظام إدارة المُحتوى (CMS) كان بطيئا، إذ كان يأخذ 3 ثوان لإعادة توليد الصّفحة الرّئيسيّة. في بعض المواقع، إن اتّبعت طلبات التّعليقات (RFC)، فسيقوم وسيط (proxy) التّخبئة بإلغاء الصّفحة الرّئيسيّة أو سيحدث انقضاء وقت (time out). عندها سيأتي مُستخدم آخر وسيتوجب جلب نسخة جديدة من الصّفحة الرّئيسيّة. يقوم Varnish بهذا عبر عمليّة التئام التّخبئة (coalescing). إذ يتم وضع المستخدمين الجدد عند وصولهم على قائمة انتظار. (أنظمة التّخبئة الأخرى تُرسل المُستخدمين إلى النظام الخلفيّ ما يقتل الخادم). إن تّمت إضافة 3000 مُستخدم إلى قائمة الانتظار كلّ ثانيّة، فبعد ثلاثة ثوان، سيكون الطابور عبارة عن 9000 مستخدم ينتظرون النظام الخلفي لجلب المحتوى. في البداية، كان Varnish يأخذ المحتوى المطلوب، ويقوم بالتواصل مع السّلاسل (Thread) الـ9000 التي تمّ إنشاؤها، ويُحاول دفع كل شيء دفعة واحدة. سمّي هذا الحدث بالقطيع الصّاخب (thundering herd). وكانت النّتيجة موت الخادوم فورا. لحلّ هذه المُشكلة، قرّر Varnish استعمال الصفحة الرّئيسيّة القديمة عوضا عن انتظار الخادوم لإعادة توليد المحتوى. لن يهتم أحد إن كان المحتوى قديما بـ20 ثانيّة، إلا في حالة كانت البيانات التي تقدّمها متعلّقة بمعلومات ماليّة وترغب عرضها في الوقت الفعلي. تغيّرت التّفاصيل المتعلّقة بـGrace خلال السنوات الماضيّة. في Varnish 4.0 (إن تمّ تفعيله) فستبدو كما يلي: sub vcl_backend_response { set beresp.grace = 2m; } يتم ربط Grace مع كائن الإجابة الخلفيّة (backend response object). إن تم تحديد القيمة في دقيقتين (2m) فسيُبقي Grace على الكائن لدقيقتين بعد انقضاء الـTPL. إن تم طلب الكائن خلال هذا الوقت، فسيتم تفضيل تقديم ما هو مُخبّأ عوضا عن إرسال طلب جديد إلى النظام الخلفيّ. سيتم استعمال الكائن وإعادة تحميله بعدها بشكل غير متزامن. تفاصيل Grace لا يتطلب فهم Varnish Cache النظر إلى الشّيفرة المصدريّة، عوضا عن ذلك، يتم توفير VCL افتراضيّ يُمكن الاعتماد عليه لإضافة أية تفاصيل أخرى حسب الحاجة. فبِميّزة مثل Grace يُمكن للمُستخدمين قراءة شيفرة VCL والنظر إلى التفاصيل: Insert code sub vcl_hit { if (obj.ttl >= 0s) { // A pure unadulterated hit, deliver it return (deliver); } if (obj.ttl + obj.grace > 0s) { // Object is in grace, deliver it // Automatically triggers a background fetch return (deliver); } // fetch & deliver once we get the result return (fetch); } إن كان الكائن VCL يحمل قيمة أكبر من 0 ثوان، هذا يعني بأنّ الـTTL موجب، سيتمّ الرجوع والتّقديم. إن كانتا كل من قيمتي TTL وGrace أكبر من 0 ثوان، فهذا يعني بأنّها ضمن Grace وسيقوم Varnish بتقديمها. إن كانتا كل من قيمتي TTL وGrace أصغر من 0 ثوان، فسيتّم طلب الصّفحة من النّظام الخلفيّ. تعديل كيفيّة عمل Grace يُمكن للمؤسّسات التي تكون بحاجة إلى تقديم معلومات ماليّة وتأبى عرض معلومات قديمة للمُستخدم أن تُغيّر من كيفيّة عمل Grace. في المثال التّالي، لم يتغيّر الجزء الأول، في الجزء الثّاني نقوم بالتقديم في حالة لم يكن الخادوم الخلفيّ على ما يرام وكانت كلا من قيمتي TTL و Grace أكبر من 0 ثانيّة. sub vcl_hit { if (obj.ttl >= 0s) { // A pure unadulterated hit, deliver it return (deliver); } if (!std.healthy(req.backend_hint) && (obj.ttl + obj.grace > 0s)) { return (deliver); } // fetch & deliver once we get the result return (fetch); } *بضعة معلومات إضافيّة: beresp: كائن الطّلب الخلفّي backend request object. req: كائن الطّلب request object. يُستعمل في vcl_recv. bereq: كائن الطّلب الخلفي backend request object. يُستعمل في vcl_backed_fetch. beresp: كائن الإجابة الخلفيّة. يُستعمل في vcl_backend_response. resp: كائن الإجابة response object. يُستعمل في vcl_deliver. obj: الكائن الأصلي في الذّاكرة. يُستعمل في vcl_hit. للمزيد من التّفاصيل استعمل man(7) vcl. آلة الحالة (state machine) يمرّ كل طلب عبر عدّة حالات. يُمكن تشغيل شيفرة مخصّصة في كل حالة لتعديل كائنات الطّلب. بعد تنفيذ الشّيفرة المخصّصة، يقوم Varnish باتّخاذ قرار ما إذا كان نجاحا (Hit) أو إخفاقا (Miss) بعدها يتم تشغيل إمّا VCL hit أو VCL Miss. في حالة النّجاح سيقوم Varnish بتقديم المحتوى. والإخفاقات تُعيد الطّلب من النظام الخلفي. 90% من التّغييرات التي يُحدثها المُستخدمون تحدث هنا. كيفيّة التّضبيط (Tuning) على Linux يتم تحديد الانضباطيات (Tunables) على Linux بشكل محافظ، ونجد SOMAXCONN و TCP_MAX_SYN_Backlog أكثرها مشقّة. عند الانصات (listen) إلى المقابس (socket)، يأخذ Varnish بضعة ثوان للانصات للنّداء. إن كانت السّلاسل مشغولة، فستبدأ النّواة (kernel) بالتّكتّل (queue). لا يُسمَحُ لهذا التّكتّل بأن ينمو إلى ما وراء قيمة SOMAXCONN. سيطلب Varnish 1024 اتّصالا على تكتّل عمق الانصات (listen depth queue)، لكنّ النواة تقوم بإبطال ذلك لتحصل على 928 فقط. ولأنّ Varnish يعمل بصلاحيات الجذر (root)، فمن الممكن تقرير عمق انصات خاصّ، لكنّ Linux أدرى بما هو أفضل. قد يرغب المُستخدمون بالرّفع من هذا الحدّ إن أرادوا تخفيض خطر رفض الاتّصالات عند انقضاء عمق الانصات. سيتوجّب على المستخدم اتخاذ قرار ما إذا كان عدم التّقديم وتقديم صفحة خطأ أفضل أو لا. يقوم TCP_MAX_SYN_Backlog بتعريف عدد الاتّصالات البارزة التي يُمكن لها أن تكون بداخل إقامة الاتّصال (handshake) الثّلاثي الأطراف قبل أن تقرّر النواة بأنّها تحت الهجوم. القيمة الافتراضيّة هي 128. لكن في حالة دخل جمع كبير من النّاس إلى موقعك دفعة واحدة فقد تحصل على أكثر من 128 اتّصال TCP كلّ ثانيّة. لا تقم بتعديل tcp_tw_recycle. تأكّد بأنّك تعرف ما تفعله قبل لمس الإعدادات وابحث على الويب قبل ذلك. إذ أنّ الكثير من العفاريت (demons) تعتمد على هذه القيّم. مساحات العمل (Work spaces) في Linux في Varnish، الذاكرة المحليّة متواجدة في كلّ سلسلة. ولأنّ التّراجع (rollback) يستنزف الموارد، فإنّنا لا نتراجع كلّما احتجنا إلى قليل من المساحة في الذّاكرة. عوضا عن ذلك، نقوم بتعيين مُسبق للذّاكرة على كلّ سلسلة إن كان للمستخدم العديد من سياسات VCL التي قد تستنزف مساحة العمل بأكملها. لا يقوم Varnish بتتبّع الاتّصال، وحدة التّعاقد (contract module) بطيئة للغاية. يتمّ افتراضيّا تشغيل خمسة سلاسل. يُمكن إنشاء سلاسل جديدة بسرعة نسبيّا، لكن إن كنت تخطّط لفتح ألف اتّصال كلّ ثانيّة أي ألف سلسلة، ففي هذه الحالة لديك مُعطى (parameter) لتخصيص سلاسل مُسبقا. خلاصة: احذر عند التّعامل مع مساحات العمل. لا تقم بتتبّع الاتّصالات (connection tracking). ارفع معدّل السّلاسل إلى طلب واحد في الثانيّة لكل سلسلة. الموازنة بين ما هو غريب وما هو رائع في Varnish Cache المرونة القويّة والمميّزات التّي يتمتّع بهاVarnish Cache لا تأتي في حزمة جاهزة (ما قد يجده البعض غريبا)، لذا فقد لا يكون الحلّ الأنسب للجميع. لكنّ المرونة التي يتمتّع بها قد تُغيّر من مجريات الأمور عند الرغبة في تخصيص والتّحكم بأداء المواقع وقابليّة توسيعها. ترجمة -بتصرّف- للمقال Getting started with web app accelerator Varnish Cache لصاحبه Per Buer.
  15. الوسيط العكسي عبارة عن نوع من الخواديم الوسيطة Proxy servers تستقبل طلبات HTTP(S) وتوزّعها بصورة شفّافة على خادوم خلفيّ Backend server واحد أو أكثر. الوسيط العكسي مُفيد لأنّ مُعظم تطبيقات الويب في أيّامنا هذه تستعمل خواديم لم تكن قد صُمّمت لأخذ الطّلبات مُباشرة من المُستخدم وفي العادة لا تدعم إلّا ميزات HTTP بدائيّة. يُمكنك استخدام وسيط عكسيّ لتجنّب وصول المستخدم إلى الخواديم الخلفيّة مُباشرة. يُمكن استخدامها كذلك لتوزيع الحمل Load balancing من الطّلبات على عدّة خواديم، ما يُحسّن الأداء ويُوفّر حماية من تعطّل التّطبيق. يُمكها كذلك أن تمنحك ميزات لا تمنحها خواديم التّطبيقات مثل التّخبئة Caching، ضغط الملفّات Compression، وحتى تشفير SSL. سنضبُط في هذا الدّرس Apache ليعمل على وسيطًا عكسيًّا بسيطًا باستخدام وحدة mod_proxy لإعادة توجيه الطّلبات القادمة إلى خادوم خلفيّ أو أكثر من خادوم يعمل على نفس الشّبكة. سنستعمل في هذا الدّرس واجهة خلفيّة بسيطة مكتوبة بإطار العمل Flask، لكنّك تستطيع استعمال أي خادوم خلفيّ تُريده وأي لغة برمجيّة مُناسبة لك. مُلاحظة: أبدِل your_server_ip في عناوين URL التي تجدها في هذا الدّرس بعنوان IP الخاصّ بخادومك. المُتطلّبات لاتّباع هذا الدّرس، ستحتاج إلى: خادوم أوبونتو 16.04 مضبوط باتّباع الخطوات في هذا الدّرس، وذلك يشمل مُستخدما ذا صلاحيّات sudo مع ضبط مُسبق لتمكينه من تنفيذ مهام إداريّة، غير المستخدم الجذر root، بالإضافة إلى جدار ناري. Apache 2 مُنصّب على خادومك باتّباع الخطوة الأولى من هذا الدّرس. الخطوة الأولى – تفعيل وحدات Apache اللازمة يمتلك Apache الكثير من الوحدات المبنيّة مُسبقا لكنّها لا تكون مُفعّلة عند التّنصيب. لذا سيتوجّب علينا تفعيل الوحدات التي سنستخدمها في هذا الدّرس. الوحدات التّي نحتاج إليها هي mod_proxy وبضعة إضافات خاصّة بها للحصول على بضعة ميزات أخرى لدعم بروتوكولات شبكيّة مُتعدّدة. سنستعمل ما يلي بالتّحديد: mod_proxy، الوحدة الرّئيسيّة لإعادة توجيه الطّلبات، وتُمكّننا من تحويل Apache إلى بوابّة لخواديم التّطبيقات المُعتمدة. وحدة mod_proxy_http لتمكيننا من توسيط اتّصالات HTTP. وحدتا mod_proxy_balancer وmod_lbmethod_byrequests لإضافة ميّزات مُوازنة الحمل على عدّة خواديم. لتفعيل الوحدات الأربع، نفّذ الأوامر التّاليّة: sudo a2enmod proxy sudo a2enmod proxy_http sudo a2enmod proxy_balancer sudo a2enmod lbmethod_byrequests لتطبيق التّغييرات، أعد تشغيل Apache: sudo systemctl restart apache2 يُمكن الآن استخدام Apache ليعمل وسيطًا عكسيًّا لطلبات HTTP. في الخطوة – الاختياريّة - التّاليّة، سننشئ خادومين خلفيّين بسيطين. ما سيُخوّلنا التّحقّق من أنّ الإعدادات تعمل جيّدا، لكن إن كانت لديك تطبيقات تعمل في الواجهة الخلفيّة، يُمكنك تخطي الخطوة التّاليّة والانتقال مُباشرة إلى الخطوة الثّالثة. الخطوة الثّانيّة – إعداد خواديم خلفيّة Backend Servers للاختبار إنشاء خواديم خلفيّة بسيطة طريقة سهلة لاختبار ما إذا كانت إعدادات Apache الخاصّة بك تعمل جيّدًا أو لا. في هذه الفقرة، سنعدّ خادومين يجيبان على طلبات HTTP بمقطع نصّي صغير. أحد الخادومين سيُجيب بالمقطع Hello world! والآخر بالمقطع Howdy world!. مُلاحظة: في الحالات غير الاختباريّة، عادة ما يجب على الخواديم أن تُجيب بنفس الإجابة. لكن بالنّسبة لهذا الاختبار، فامتلاك إجابتين مُختلفتين يُمكّننا من التّأكّد من أنّ خاصيّة مُوازنة الحمل تستخدم كلا الخادومين. Flask إطار عمل مُصغّر مبني بلغة بايثون لبناء تطبيقات الويب. استعملنا Flask لإنشاء خادوميْ الاختبار لأنّ تطبيقا بسيطا باستخدامه لا يتطلّب سوى بضعة أسطر برمجيّة. ليس من الضّروري أن تكون لديك معرفة بلغة بايثون لإعداد الخادومين، لكن إن أردت تعلّمها، فيُمكنك الاطّلاع على هذه الدّروس. أولا، حدّث قائمة الحزم: sudo apt-get update بعدها، نصّب أداة pip لإدارة حزم لغة بايثون: sudo apt-get -y install python3-pip استخدام pip لتنصيب أداة Flask: sudo pip3 install flask بعد تنصيب المُكوّنات المطلوبة، ابدأ بإنشاء ملفّ جديد ليحتوي على شفرة الخادوم الأول في المُجلّد الشخصي للمستخدم الحاليّ: nano ~/backend1.py انسخ ما يلي إلى الملفّ ثمّ احفظه وأغلقه: from flask import Flask app = Flask(__name__) @app.route('/') def home(): return 'Hello world!' السّطران الأول والثّاني عبارة عن تهيئة لإطار العمل Flask. لدينا دالّة واحدة باسم home() تُرجع النّص Hello world!. السّطر @app.route('/') المتواجد فوق الدّالة home() يُخبر Flask بالإجابة على أي طلب يصل إلى العنوان الجذر / بما تُرجعه الدّالّة. الخادوم الثّاني مُشابه للأول، الاختلاف الوحيد أنّ الجواب مُختلف قليلا. لذا انسخ الملفّ الأول: cp ~/backend1.py ~/backend2.py عدّل الملفّ الجديد: nano ~/backend2.py عدّل ما تُرجعه الدّالة من Hello world! إلى Howdy world! ثمّ احفظ وأغلق الملفّ. from flask import Flask app = Flask(__name__) @app.route('/') def home(): return 'Howdy world!' استخدم الأمر التّالي لتشغيل الخادوم الأول على المنفذ رقم 8080. سيقوم هذا الأمر بتحويل المخرجات إلى /dev/null كذلك لتفادي مُقاطعة المُخرج للطّرفيّة. FLASK_APP=~/backend1.py flask run --port=8080 >/dev/null 2>&1 & في السّطر أعلاه، عرّفنا مُتغيّر البيئة FLASK_APP ثمّ نفّذنا الأمر flask لتشغيل الخادوم في نفس السّطر. مُتغيّرات البيئة طريقة سهلة لتمرير المعلومات إلى العمليّات المبدوءة على الطّرفيّة. في هذه الحالة، باستعمال مُتغيّرات البيئة فإنّنا نتأكّد من أنّ الإعداد يُطبّق على الأمر الذي سيكون قيد التّشغيل فقط ولن يكون مُتوفّرا بعد ذلك، وهذا مُناسب لنا لأنّنا سنمرّر اسم ملفّ آخر بنفس الطّريقة لإخبار flask بتشغيل الخادوم الثّاني. استعمل الأمر التّالي لتشغيل الخادوم الثّاني على المنفذ 8081 بطريقة مُشابهة لما سبق. لاحظ بأنّ قيمة مُتغيّر البيئة FLASK_APP مُختلفة في هذه الحالة: FLASK_APP=~/backend2.py flask run --port=8081 >/dev/null 2>&1 & يُمكنك اختبار عمل الخادومين باستخدام أداة curl. اختبار الخادوم الأول: curl http://127.0.0.1:8080/ يجب على المُخرج أن يساوي Hello world!. الخادوم الثّاني: curl http://127.0.0.1:8081/ يجب على المُخرج أن يكون Howdy world!. مُلاحظة: لإغلاق كلا الخادومين بعد أن تنتهي من استخدامهما، عند إنهائك لهذا الدّرس مثلا، فيُمكنك تنفيذ الأمر killall flask. في الخطوة التّاليّة، سنعدّل ملفّ إعدادات Apache لتمكيننا من استخدامه وسيطًا عكسيًّا. الخطوة الثّالثة – تعديل الإعدادات المبدئيّة لجعل Apache يعمل وسيطًا عكسيًّا سنعدّ في هذه الفقرة مُضيفًا افتراضيًّا Virtual host على Apache للعمل وسيطًا عكسيًّا لخادوم خلفي واحد أو عدّة خواديم موزونة الحمل. مُلاحظة: سنُطبّق الإعدادات في هذا الدّرس على مُستوى المُضيف الافتراضي. يوجد في الإعداد المبدئي لـApache مُضيف افتراضي واحد مُفعّل. لكنّك تستطيع استعمال جميع أجزاء هذه الإعدادات على أي مُضيف افتراضي آخر. إن كان خادوم Apache الخاص بك يجيب على طلبات HTTP و HTTPS، فسيتوجّب عليك وضع إعدادات الوسيط العكسي في كلا المُضيفَين الافتراضيّيْن لـHTTP وHTTPS. افتح ملفّ إعدادات Apache المبدئي باستخدام nano أو أي مُحرّر مُفضّل لديك: sudo nano /etc/apache2/sites-available/000-default.conf ستجد داخل هذا الملفّ الجزء <VirtualHost *:80> بدءا من السّطر الأول. يشرح المثال الأول أسفله كيفيّة ضبط هذا الجزء لإعداد وسيط عكسي لخادوم واحد، وفي المثال الثّاني سنضبط وسيطا عكسيّا يوازن الحمل على أكثر من خادوم. المثال الأول – إعداد وسيط عكسي لخادوم خلفيّ واحد أبدل جميع المُحتويات داخل الوسم VirtualHost بما يلي ليصير ملفّ الإعدادات الخاصّ بك كما يلي: <VirtualHost *:80> ProxyPreserveHost On ProxyPass / http://127.0.0.1:8080/ ProxyPassReverse / http://127.0.0.1:8080/ </VirtualHost> إن تابعت الأمثلة في الخطوة الثّانيّة أعلاه، فاستخدم العنوان 127.0.0.1:8080 كما هو مكتوب أعلاه. إن كان لديك تطبيق ويب خاصّ بك، فاستعمل عنوان التّطبيق عوضا عمّا فعلنا. لدينا ثلاثة تعليمات في هذا الإعداد: ProxyPreserveHost مسؤولة عن جعل Apache يُمرّر الترويسة Header المسمَّاة Host الأصليّة إلى الخادوم الخلفيّ. هذا الأمر مُفيد لأنّه يُخبر الخادوم بالعنوان المُستخدَم للوصول إلى التّطبيق. ProxyPass عبارة عن تعليمة الإعداد الرّئيسيّة للوسيط. في هذه الحالة، نحدّد كلّ شيء تحت عنوان URL الجذر / ليُربط مع الخادوم الخلفيّ المرتبط بالعنوان المُعطى. على سبيل المثال، إن استقبل Apache طلبا للموجّه /example، فسيقوم بالاتّصال بالعنوان http://your_backend_server/example وإرجاع الإجابة إلى العميل. ينبغي على ProxyPassReverse أن يحمل نفس الإعداد الذي يحمله ProxyPass. ويُخبر Apache بتعديل ترويسة الجواب من طرف الخادوم الخلفيّ. بهذه الطّريقة سنتأكّد من إعادة توجيه العميل إلى عنوان الوسيط وليس الخادوم الخلفيّ في حالة وُجدت ترويسة إعادة توجيه في جواب الخادوم لتجنّب أخطاء إعادة التّوجيه. لتطبيق هذه التّغييرات، أعد تشغيل Apache: sudo systemctl restart apache2 إن حاولت الآن الوصول إلى http://your_server_ip في مُتصفّح ويب، ستجد جواب الخادوم الخلفيّ عوضا عن رسالة ترحيب Apache المألوفة. إن تابعت الخطوة الثّانيّة، فهذا يعني بأنّ النّتيجة ستكون Hello world!. المثال الثّاني – موازنة الحمل على عدّة خواديم خلفيّة إن كان لديك أكثر من خادوم خلفيّ واحد، فاستخدام ميزة مُوازنة الحمل في mod_proxy طريقة جيّدة لتوزيع الطّلبات على الخواديم. استبدل جميع مُحتويات الجزء VirtualHost بما يلي ليبدو ملفّ الإعدادات كما يلي: <VirtualHost *:80> <Proxy balancer://mycluster> BalancerMember http://127.0.0.1:8080 BalancerMember http://127.0.0.1:8081 </Proxy> ProxyPreserveHost On ProxyPass / balancer://mycluster/ ProxyPassReverse / balancer://mycluster/ </VirtualHost> الإعدادات هنا مُشابهة لما سبق، لكن عوضا عن تخصيص خادوم واحد مُباشرة، استعملنا وسم Proxy إضافيّا لتعيين خواديم مُتعدّدة.سمّينا الوسم بـbalancer://mycluster (يُمكنك تغيير الاسم إن شئت) ليحتوي على أكثر من تعليمة BalancerMember، والذي يُحدّد عناوين الخواديم الخلفيّة. هكذا تستعمل التّعليمتان ProxyPass وProxyPassReverse مجموعة موازنة الحمل المُسمّاة mycluster عوضا عن خادم مُحدّد. إن تابعت الخطوة الثّانيّة من هذا الدّرس، فاستعمل العنوانين 127.0.0.1:8080 و127.0.0.1:8081 للتّعليمة BalancerMember كما هو مُبيّن في الإعدادات أعلاه. أمّا إن كانت لديك خواديم خاصّة بك فاستعمل عناوينها. لتطبيق هذه التّغييرات، أعد تشغيل Apache: sudo systemctl restart apache2 إن حاولت الآن الوصول إلى http://your_server_ip في مُتصفّح ويب، ستجد جواب الخواديم الخلفيّة عوضا عن رسالة ترحيب Apache المألوفة. إن تابعت الخطوة الثّانيّة، فعند إعادة تحميل الصّفحة عدّة مرّات، ستُلاحظ بأنّ النّتيجة ستكون إمّا Hello world! أو Howdy world!، ما يعني بأنّ الوسيط العكسي يعمل على مُوازنة الحمل بين الخادومين. خاتمة لديك الآن معرفة بكيفيّة إعداد خادوم Apache ليعمل وسيطًا عكسيًّا لخادوم تطبيق أو أكثر. يُمكن استعمال mod_proxy لضبط وسيط عكسي لخواديم تطبيقات مكتوبة بلغات مثل Python مع Django أو Ruby مع Ruby on Rails. يُمكن كذلك استخدامها لموازنة الحمل على عدّة خواديم للتّطبيقات التي تستقبل العديد من الزّوار، بالإضافة إلى إمكانيّة استخدامه لتوفير حماية SSL للخواديم الخلفيّة التي لا تدعم SSL. رغم أنّ mod_proxy وmod_proxy_http أكثر تركيب يتمّ استخدامه، إلّا أنّ هناك العديد من الوحدات الأخرى التّي تدعم بروتوكولات اتّصال أخرى. إليك بعضا من الوحدات الشّهيرة التي لم نستخدمها في هذا الدّرس: mod_proxy_ftp لبروتوكول FTP. mod_proxy_connect لإعداد أنفاق SSL. mod_proxy_ajp لبروتوكول AJP (Apache JServ Protocol) لخواديم خلفيّة مبنيّة على Tomcat. mod_proxy_ftp لمقابس الويب Web sockets. يُمكنك قراءة التّوثيق الرّسمي للاستزادة حول mod_proxy. ترجمة – بتصرّف - للمقال How To Use Apache as a Reverse Proxy with mod_proxy on Ubuntu 16.04 لكاتبه Mateusz Papiernik.