הבלוג של ינון פרק

טיפים קצרים וחדשות למתכנתים

תנו צ'אנס לנווד - ארבע תכונות טובות של Nomad

05/03/2023

האשיקורפ נוסדה ב 2012 ושלוש שנים מאוחר יותר ב 2015 הוציאה את כלי ניהול הקונטיינרים שלה שנקרא Nomad. קוברנטיס היה אז בן שנה אבל כבר השיג אחיזה בשוק, ובנוסף נהנה מהגיבוי של גוגל כך שהסיכוי של נומד מלכתחילה היה נמוך.

אבל למרות שנומד לא ניצח בתחרות הפופולריות, הוא עדיין גדל, מתוחזק ונמצא בשימוש בעולם האמיתי. ואם אתם עדיין לא "נעולים" על קוברנטיס, הנה שלוש סיבות בגללן שווה לפחות לבחון את הנווד-

  1. קלות התקנה - בשביל להקים קלאסטר של נוודים כל מה שצריך זה להוריד קובץ בינארי אחד למחשב (התוכנה זמינה למק, חלונות ולינוקס). הפעלה של אותו Agent הופכת את המכונה שלכם לשרת Nomad. כשתפעילו את ה Agent על מחשב נוסף תוכלו לתת לו את כתובת ה IP של המחשב הראשון ויש לכם קלאסטר. נומד לא בררן ואפשר להפעיל Agent-ים שלו על כל דבר. ה Agent אוטומטית סורק את המכונה ומצרף אותה לקלאסטר עם היכולות שהוא מזהה.

  2. דוגמה נוספת היא ביצירת גמישות - קוברנטיס רוצה שליטה מלאה על הקלאסטר והמכונות שבו, ומוכן להריץ רק קונטיינרים. נומד הוא כבר הרבה יותר גמיש. יש לך קובץ בינארי להפעיל? קיבלת. תוכנית Java? מספיק טוב. קונטיינר? כמובן. הוא מצליח להפעיל כל דבר כי כל מכונה שמתחברת לקלאסטר מדווחת על היכולות שלה, למשל אם יש לה דוקר מותקן או אם היא יכולה להריץ קבצי exe או אם מותקן עליה Java, ואז כשהקלאסטר צריך להריץ משימה הוא יחפש איזה מכונה הכי תתאים להרצה לפי האילוצים ויתן לה את המשימה. דוגמה נוספת היא ב Storage: נומד מוכן להתחבר לכל התקן איחסון שיש לכם, בין אם זה כונן מקומי, תיקיית רשת או איחסון בענן ולחבר אותם לקונטיינרים.

  3. פיצ'רים קטנים שעושים את ההבדל - נכון אין לו את כל הפיצ'רים המסובכים של קוברנטיס, אבל בפינוקים קטנים נומד מצטיין. לדוגמה הפקודה nomad plan תקבל קובץ מניפסט (זה נקרא Job Specification שם) ותגיד לכם בדיוק ובצורה מפורטת מה הוא הולך לעשות כשתנסו להריץ את הג'וב הזה, וכמובן תזהה בעיות לפני שהן קורות. או התמיכה ב Canary Updates שבעזרת שורת הגדרה אחת נומד יריץ את המערכת שלכם במכונה חדשה בתור קנרית ורק אם היא עלתה כמו שצריך הוא ישדרג את כל המכונות הקיימות.

  4. אינטגרציה עם שאר הכלים של HashiCorp - במקום להכניס את הכל לכלי אחד האשיקורפ יצרו אוסף כלים עצמאיים שיכולים לעבוד יחד בתור מכפיל כח. לדוגמה Terraform יודע לבנות ארכיטקטורת רשת על כל ענן שתתנו לו (וכך יהיה קל להתקין את נומד על כל ענן ולעבור בקלות בין עננים), Vault יודע לנהל סודות ולכן קלאסטרים של נומד ישתמשו בו כדי להזריק סודות לקונטיינרים ו Consul מנהל את ה Service Discovery בתוך הקלאסטר. היתרון בבחירה לחלק את המשימות לכלים יחסית קטנים ועצמאיים היא שאפשר ללמוד כל כלי בפני עצמו ולהתחיל להשתמש למשל ב Nomad בלי הכלים האחרים, אבל אז כשלומדים על Terraform מגלים כמה קל לשלב בין השניים.

סך הכל נומד אולי לא ניצח בתחרות פופולריות, אבל אם גם אתם קצת עייפים מהסיבוכיות או הקיבעונות של קוברנטיס, ומחפשים לדוגמה משהו שיהיה קל להתקנה, יאפשר לכם לעבוד גם עם התשתית הקיימת וגם לשדרג לקונטיינרים רק חלקים מהמערכת בהדרגה אז נומד יכול להיות פיתרון מעניין.

נ.ב. נקודת התחלה טובה לעבודה איתו היא אוסף ה Tutorials באתר שלהם כאן: https://developer.hashicorp.com/nomad/tutorials/get-started.

האם ריאקט הוא סקאם?

04/03/2023

כשמישהו שואל "האם טכנולוגיה X היא Scam" הוא בדרך כלל מתכוון לשילוב של הגורמים הבאים:

  1. יש טכנולוגיות יותר פשוטות שפותרות את אותה בעיה.

  2. הרבה אנשים בוחרים בטכנולוגיה המסובכת בגלל יחסי הציבור שלה ולא בגלל הערך האמיתי.

וכך השאלה ממשיכה לחזור לגבי שיטות עבודה וטכנולוגיה חדשות - האם הענן הוא רמאות (יותר זול להחזיק שרת שלכם), האם אג'ייל הוא רמאות (זה רק גורם לנו לעבוד יותר לאט), האם בדיקות יחידה הן רמאות (אני עובד כל היום לכתוב בדיקות והן אף פעם לא מצאו באגים), וכמובן האם JavaScript Frameworks הן רמאות.

אנחנו בוחרים טכנולוגיה שאנחנו חושבים שתפתור לנו בעיות שכבר יש לנו עם הטכנולוגיה שעבדנו איתה בפרויקט הקודם. מי שבחר ריאקט ידע טוב מאוד מה זה jQuery או AngularJS ואיך ריאקט יפתור את הבעיות הספציפיות שהיו בטכנולוגיות הקודמות. מי שבחר בענן ידע בדיוק מה זה לנהל שרת לבד, כולל את כל הבלאגן של להרים עוד ועוד שרתים ולעקוב אחריהם. לאנשים האלה היתה בעיה אמיתית והטכנולוגיה החדשה פתרה אותה.

בנוסף אליהם יש קבוצה מאוד גדולה של אנשים שמסתכלים מה עושים בפרויקטים אחרים ובוחרים את הטכנולוגיה שכולם לוקחים כי "אם זה מספיק טוב לפייסבוק זה בטח מספיק טוב גם בשבילי". אלה האנשים שבוחרים את הפיתרון לפני שנתקלו בבעיה כדי למנוע אותה. גם זאת לא התנהגות כל כך מוזרה, אם ניזכר שפרויקטי תוכנה הם לא צפויים ואין לך מושג באיזה בעיות תיתקל. לבחור טכנולוגיה שכולם משתמשים בה מבטיח שמגבלה טכנולוגית לא תכריח אותך לשכתב את כל הקוד בעוד שנה כשהפרויקט יגדל.

כן ריאקט מסובך, האקוסיסטם עמוס וכל הזמן צריך להתעדכן בשינויים ושיטות עבודה חדשות. כן הרבה אנשים שעובדים בריאקט היום רואים את הבעיות שבו. אבל לא, זה לא Scam ונכון להיום בפרויקט גדול יש עדיין הרבה יתרונות בחלוקת המערכת לקוד צד שרת שמחזיר JSON-ים דרך API (לא משנה אם REST או GraphQL) וקוד צד לקוח עצמאי שאחראי על התצוגה.

תפסו מחסה, הדקורטורים חוזרים!

03/03/2023

טייפסקריפט 5 תכף יוצאת ואחד הפיצ'רים המטרידים בה הוא התמיכה המחודשת בדקורטורים. מסתבר שבזמן שלא שמנו לב סימני הכרוכית הצליחו לגייס תמיכה ב TC39 ועושים את דרכם במהירות לדפדפנים שלנו, כשטייפסקריפט היא הסנונית הראשונה. בואו ניזכר מהם ולמה בפעם הקודמת הם לא עבדו טוב.

המשך קריאה

היום למדתי: כבר מזמן לא צריך להוסיף קו תחתי אחרי exec ב Qt for Python

02/03/2023

לפמעים צריך לחכות שמשהו יהפוך ל Deprecated עד שאני אשים לב שכבר הרבה זמן הוא לא היה הכרחי. זה לפחות היה הסיפור עם PyQt ו PySide והפקודה exec.

בפייתון 2 המילה exec היתה מילה שמורה. הפקודה exec של Qt היא זאת שמפעילה את הלולאה הראשית של היישום, והשם exec הוא בדיוק שם הפונקציה ב C++, לכן ובשביל לא לבלבל יותר מדי מתכנתים שכבר מכירים Qt מ C++, בפייתון בחרו לאמץ את השם אבל להוסיף _ בסוף. תוכנית PyQt פשוטה של שלום עולם לכן נראתה כך:

import sys
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QApplication, QLabel

if __name__ == "__main__":
    app = QApplication(sys.argv)
    label = QLabel("Hello World", alignment=Qt.AlignCenter)
    label.show()
    sys.exit(app.exec_())

אבל בפייתון 3 המילה exec כבר לא היתה מילה שמורה, ולכן החל מ Qt5 אפשר להשתמש באותו exec של C++. בגירסה 6 אנחנו כבר מקבלים Deprecation Warning על ה exec עם הקו התחתי בסוף. אגב גם על sys.exit שמופיע בתוכנית הדוגמה אפשר לוותר והקוד הבא יעשה בדיוק את אותו דבר ובלי להתלונן:

import sys
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QApplication, QLabel

if __name__ == "__main__":
    app = QApplication(sys.argv)
    label = QLabel("Hello World", alignment=Qt.AlignCenter)
    label.show()
    app.exec()

נ.ב. רוצים להפעיל אותו אצלכם? כל מה שצריך זה להתקין לפני את החבילות של PySide ואתם מסודרים:

pip install PySide6

טיפ גיטהאב: איך להדפיס את הלוג של ההרצה האחרונה דרך שורת הפקודה

01/03/2023

לגיטהאב יש כלי שורת פקודה שנקרא gh שהוא סוג של קסם, כי הוא מאפשר לעשות כל דבר שאפשר לעשות דרך האתר, משורת הפקודה ועם פלט JSON שמאוד קל לפענוח. בעבר כתבתי עליו בפוסט איך ליצור Pull Request משורת הפקודה והיום הוא חוזר בשביל לענות על עוד שאלה מתלמיד - איך להדפיס את הלוג של ההרצה האחרונה של Github Action משורת הפקודה? ופה עלינו בסך הכל לשלב שתי פקודות.

הפקודה הראשונה שצריך להכיר היא gh run list. כשאני מריץ את הפקודה מתיקיה עם ריפוזיטורי, היא מדפיסה למסך את כל ההרצות של ה Actions מהריפוזיטורי. בעזרת המתג --json אני יכול לבקש רק פרטים מסוימים על כל הרצה ובעזרת -L אני מגביל את מספר התוצאות. ככה זה נראה עם ריפו שלי בהגבלה ל-3 הרצות אחרונות:

$ PAGER= gh run list --json name,databaseId,startedAt -L 3

[
  {
    "databaseId": 4289773851,
    "name": "publish-daily-post-to-telegram",
    "startedAt": "2023-02-28T05:11:50Z"
  },
  {
    "databaseId": 4279130715,
    "name": "publish-daily-post-to-telegram",
    "startedAt": "2023-02-27T05:11:47Z"
  },
  {
    "databaseId": 4273470712,
    "name": "publish-daily-post-to-telegram",
    "startedAt": "2023-02-26T05:10:54Z"
  }
]

הפקודה השניה שצריך להכיר היא gh run view שמקבלת מספר ריצה ומדפיסה פרטים עליה. המתג --log יגרום ל view להציג את הלוג המלא של ההרצה. ככה זה נראה אצלי עם ההרצה העדכנית ביותר:

$ PAGER= gh run view --log 4289773851 | head

publish Set up job      2023-02-28T05:12:00.4187561Z Current runner version: '2.301.1'
publish Set up job      2023-02-28T05:12:00.4215234Z ##[group]Operating System
publish Set up job      2023-02-28T05:12:00.4215881Z Ubuntu
publish Set up job      2023-02-28T05:12:00.4216132Z 22.04.2
publish Set up job      2023-02-28T05:12:00.4216414Z LTS
publish Set up job      2023-02-28T05:12:00.4216728Z ##[endgroup]
publish Set up job      2023-02-28T05:12:00.4217052Z ##[group]Runner Image
publish Set up job      2023-02-28T05:12:00.4217433Z Image: ubuntu-22.04
publish Set up job      2023-02-28T05:12:00.4217721Z Version: 20230219.1
publish Set up job      2023-02-28T05:12:00.4218252Z Included Software: https://github.com/actions/runner-images/blob/ubuntu22/20230219.1/images/linux/Ubuntu2204-Readme.md

וזה כמובן ממשיך לכל הלוג.

שילוב של שתי הפקודות יאפשר לי בשורה אחת לקבל את הלוג המלא של ההרצה האחרונה של Action בריפו:

$ PAGER= gh run view --log $(gh run list --json databaseId -q '.[].databaseId' -L 1)

נ.ב. המתג -q מקבל שאילתה בפורמט של jq. בזכותו פלט הפקודה היה רק מזהה הריצה במקום ה JSON המלא.

מה הסיפור עם הלוכסנים אחרי כתובת IP?

28/02/2023

נתקלתם פעם בכתובת 10.10.64.0/27? ומה לגבי 192.168.0.0/24? כשאנחנו מדברים על אוסף כתובות IP ברשת מסוימת הרבה פעמים נבחר לכתוב אותן בתור כתובת ואחריה לוכסן ואז מספר, בכתיב שנקרא CIDR Block. האותיות CIDR הן ראשי תיבות של המילים Classless Inter-Domain Routing. השיטה עצמה הוצגה כבר ב 1993 במטרה לחסוך בכתובת IP ולספק גמישות לארגונים שרצו לחלק את הרשת הפנימית שלהם למקטעים.

חלוקת רשת למקטעים חוסכת בכתובות IP כי לא כל מכונה ברשת הפנימית צריכה כתובת IP ציבורית. חישבו על הרשת הפנימית שלכם בבית - המחשבים יכולים לקבל כתובת IP כמו 192.168.0.124, ובעזרתה המכונות ברשת הפנימית מתקשרות אחת עם השניה, אבל רק כתובת ה IP החיצונית של הראוטר תהיה נגישה מהאינטרנט. בניית רשת פנימית חסכה לספק האינטרנט שלכם כתובות IP, כי עכשיו הם יכולים לתת לכם רק כתובת IP אחת בשביל הראוטר ולא צריכים להקצות כתובת לכל מכונה ברשת הביתית.

כתיב CIDR הוא פשוט דרך לייצג אוסף של כתובות IP עוקבות. כשהראוטר שלכם רואה הודעה שנשלחת בין שתי מכונות בתוך אותו בלוק שהוא מנהל, הוא לא צריך לשלוח את ההודעה החוצה ל ISP, ויכול מיד להעביר אותה למכונת היעד.

לכן כשאני כותב 192.168.0.0/30 אני בעצם כותב את 4 כתובות ה IP:

192.168.0.0
192.168.0.1
192.168.0.2
192.168.0.3

ובאופן כללי כתיב של כתובת IP שאחריה לוכסן ואז מספר מפורש בתור בלוק של כתובות IP צמודות.

איך יודעים איזה כתובות IP מתאימות ל CIDR Block מסוים? טוב ששאלתם. קודם כל יש תוכנות שפורסות רשימות כאלה, לדוגמה אם יש לכם nmap מותקן תוכלו לכתוב:

$ nmap -sL -n 192.168.0.0/30| awk '/for/{print $NF}'

ולקבל בדיוק את הרשימה (ה awk בסוף בא לסנן כמה שורות לא חשובות ש nmap מדפיס). ואפשר גם לחשב את הרשימה יחסית בקלות כשמבינים מה הכתיב מייצג. כל כתובת IP ניתנת לייצוג בגירסה בינארית באמצעות המרה לבינארי של כל אחד מהמספרים שמרכיב אותה. לדוגמה הכתובת 172.16.21.9 מיוצגת בבינארית על ידי המספר:

10101100.00010000.00010101.00001001

בכתיב CIDR המספר שאחרי הלוכסן מייצג כמה ביטים משמשים לייצוג הרשת, ובעצם יישארו קבועים בכל כתובת IP שנכתוב. המספר הזה נע בין 0, המייצג בלוק של כל כתובות ה IP האפשריות, ל 32 המייצג בלוק שמורכב מכתובת IP בודדת. בשביל שיהיה נוח לכתוב, כתובת הבסיס של בלוק CIDR מגדירה את הביטים של ה Host להיות מאופסים.

אז אם נשאר עם הכתובת 172.16.21.9 אני יכול לדבר על הבלוק 172.16.21.9/31 בו נמצאות רק שתי הכתובת:

172.16.21.8
172.16.21.9

או בכתיב בינארי:

10101100.00010000.00010101.00001000
10101100.00010000.00010101.00001001

ושימו לב איך ה 31 ביטים הראשונים זהים ורק הביט האחרון משתנה מ 0 ל 1. ככל שאוריד את המספר שאחרי הלוכסן (מספר הביטים של הרשת), כך יהיו לי יותר כתובות IP שונות בבלוק. הבלוק 172.16.0.0/24 כבר מכיל 256 כתובות - כל הכתובות בהן שלושת המספרים הראשונים הם 172.16.0. הבלוק 172.16.0.0/28 מכיל רק 16 כתובות, הראשונה היא 172.16.0.0 והאחרונה היא 172.16.0.15. בשביל להגיע לבלוק הבא של 16 כתובות אני מגדיל את המספר האחרון בכתובת, כלומר הבלוק 172.16.0.16/28 מכיל גם הוא 16 כתובות אבל הפעם הכתובת הראשונה היא 172.16.0.16 והאחרונה היא 172.16.0.31.

באגים בלתי נראים

27/02/2023

כלי פיתוח טובים הם קודם כל כלים שיודעים להצביע (ברמת דיוק גבוהה) על קוד שנראה כמו באג. וכן, כלי פיתוח קשורים לשפה ועובדים יחד איתה. בדוגמה קלאסית מ JavaScript, הקוד הזה שגוי אבל ל eslint לא אכפת:

function countUntil(n) {
  let i=0;
  while(i < n)
    console.log(i);
    i++;
}

countUntil(10);

קל לראות שהשורה השניה ב"בלוק" ה while היא בעצם בכלל לא בבלוק ה while, ולכן הלולאה תרוץ עד אינסוף. בשביל לתקן אני לא צריך לעבור לפייתון, מספיק להוסיף ל eslint את הכלל indent כדי שהוא יצעק שהשורה i++ לא במקום.

אחת העבודות החשובות שלנו כמתכנתים היא למצוא את הכלים הנכונים - לא הכלים שיכתבו במקומנו את הקוד, אלא קודם כל הכלים שיצביעו על שגיאות שאולי לעין קל לפספס.

המסמך הכי חשוב שאתם לא כותבים

26/02/2023

שידרוגי תוכנה זה קשה וככל שהמערכת בשלה ומורכבת יותר זה רק נהיה יותר מסובך. בעיה ראשונה ומוכרת היא שגירסאות חדשות של תלויות יוצאות כל הזמן, לפעמים באמת מתקנות בעיות אבל הרבה פעמים גם שוברות תאימות API. שידרוג תלויות יכול לשבור הרבה דברים במערכת.

בעיה שניה ואפילו יותר מטרידה היא שכמות המערכות שיש להן תלויות גדלה, וקורה שאנחנו מאבדים קשר עם כל התלויות שצריך לשדרג ואיך לשדרג כל דבר.

במערכת Full Stack טיפוסית יהיו לנו את מערכת ההפעלה של השרת, גירסאות של כל הספריות המובנות במערכת ההפעלה, בסיס הנתונים, מעל זה תוכנות נלוות שהתקנו בשביל האפליקציה, כשהאפליקציה רצה בתוך Docker Container אז קבצי ה Dockerfile מתיחסים לתוכנות בגירסאות מסוימות (וגם אותן צריך לשדרג), וברמת האפליקציה ה requirements.txt, Gemfile או package.json צריכים טיפול ושדרוג.

לכן במקום להגיד "נשדרג כשיהיה זמן" או לשדרג כשמשהו נשבר, מומלץ לקבע את השידרוגים במסמך אחד שאומר הכל. טבלת אקסל תעבוד פה טוב, אבל אפשר גם קובץ טקסט פשוט. העמודות יהיו "מה", "איך לשדרג", "מתי שדרגנו פעם אחרונה", "תאריך שידרוג הבא", לדוגמה:

  1. בסיס הנתונים, קישור למדריך שמסביר איך לשדרג את בסיס הנתונים, שודרג לאחרונה ב XXX, ישודרג פעם הבאה ב YYY.

  2. קובץ ה Dockerfile של השרת, קישור למסמך שמסביר איך לשדרג, שודרג לאחרונה ב XXX ישודרג פעם הבאה ב YYY.

  3. תלויות צד-לקוח של האפליקציה הראשית, קישור למסמך שמסביר איך לשדרג, שודרג לאחרונה ב XXX, ישודרג פעם הבאה ב YYY.

טבלת אקסל מסודרת עם לינקים שתעודכן בכל שידרוג יכולה להפוך את השידרוגים לחוויה הרבה פחות מלחיצה ובלי לשכוח אף חלק במערכת.

שלוש סיבות לבחור ב Tauri לבניית אפליקציית ה Desktop הבאה שלכם

25/02/2023

טאורי הוא כלי שמאפשר לכתוב אפליקציית Desktop בטכנולוגיות ווב. ואם המשפט הזה מזכיר לכם כלי אחר שמתחרז עם "עפרון", הנה שלוש סיבות שבגללן אני מתכנן לבחור בטאורי לפרויקט ה Desktop הבא שלי:

  1. גודל הקובץ - יישום אלקטרון פשוט יתחיל ב 50 מגה ויכול להגיע בקלות לכמה מאות. טאורי לא כולל את כרומיום ומשתמש ב Web View שכבר קיים במערכת ההפעלה, לכן יישומים פשוטים לוקחים בסך הכל 3-4 מגה.

  2. ראסט - טאורי משתמש ב Rust בשביל לתקשר עם מערכת ההפעלה ובנוי על תהליך Rust שמפעיל את ה Web View של מערכת ההפעלה ושם פותח את קבצי ה HTML/CSS/JS שלכם.

  3. אבטחת מידע - לבעיות XSS באפליקציות אלקטרון היו השלכות הרסניות, כיוון שקוד ה JavaScript קיבל גישה מלאה למחשב (דרך APIs של אלקטרון). טאורי לוקח גישה יותר מאובטחת ונותן לקוד ה JavaScript גישה רק ל APIs שבחרנו בצורה מפורשת.

רוצים לראות איך האפליקציה שלכם תעבוד בתור Desktop App? המדריך כאן מסביר איך להוסיף את טאורי לכל Web Application כדי להפוך אותו לאפליקציית Desktop, ולדעתי הוא מקום טוב להתחיל לשחק עם הכלי: https://tauri.app/v1/guides/getting-started/setup/integrate

שתי מוטיבציות

24/02/2023

יש מוטיבציה שנוצרת מפחד- לדוגמה הרבה אנשים אחרי ראיון עבודה יוצאים עם הרגשה של "אני לא יודע כלום", ואז כמו מקבלים זריקת מרץ ללמוד כמה שיותר כדי לבוא מוכנים לראיון הבא.

הבעיה שזריקת המרץ הזאת לא מחזיקה מעמד הרבה זמן. מוטיבציה שנובעת מפחד נגמרת כשהפחד נגמר, ופחד הוא רגש זמני. (כשהפחד הופך לכרוני הוא כבר נקרא חרדה, וחרדה לא מניעה אף אחד לפעולה).

הסוג השני של מוטיבציה נוצר מהשראה (Inspiration)- אנשים כמונו עושים דברים בצורה כזאת. הבחירה של מתכנתים רבים ללמוד טכנולוגיה חדשה כי "זה מעניין" או "זה כלי שחשוב להכיר" היא סוג כזה של מוטיבציה. אף אחד לא יפטר אותך אם לא תלמד Web Assembly, אבל הרבה מתכנתי ווב כבר רואים את הפוטנציאל ומתחילים ללמוד את הטכנולוגיה.

שני הסוגים חשובים אבל רק את הסוג השני אנחנו צריכים לחפש באופן אקטיבי. כשאתם רוצים להתקדם, שווה להתאמץ ולמצוא השראה טוב יותר.