Netværksingeniører står ofte over for opgaven med at kopiere bestemte fragmenter fra notesblokken til konsollen. Det drejer sig typisk om at kopiere flere parametre: Brugernavn/Adgangskode og muligvis noget andet. Brugen af scripts kan fremskynde denne proces. MEN opgaven med at skrive et script og udføre det samlet set skal tage mindre tid, end det ville tage at konfigurere manuelt, ellers er scripts ubrugelige.
Formålet med denne artikel. Denne artikel er en del af Fast Start-serien og har til formål at spare tid for netværksingeniører, når de konfigurerer udstyr (én opgave) på flere enheder. Den bruger softwaren SecureCRT og de indbyggede funktioner til at udføre scripts.
Indhold
Introduktion
SecureCRT-programmet har indbygget et mekanisme til udførelse af scripts. Hvad er formålet med scripts i terminalen:
- Automatiseret indtastning og udskrivning, samt minimal kontrol af korrekthed af indtastning/udskrivning.
- Fremskynde udførelsen af rutineopgaver – reduktion af pauser mellem konfigurationen af udstyr. (De facto reduktion af pauser forårsaget af tiden til at udføre handlinger som copy/paste på det samme udstyr, ved 3 eller flere fragmenter af kommandoer, der anvendes på udstyret.)
I dette dokument behandles følgende opgaver:
- Oprettelse af enkle scripts.
- Udførelse af scripts på SecureCRT.
- Eksempler på brug af enkle og avancerede scripts. (Praktik fra det virkelige liv.)
Oprettelse af enkle scripts.
Enkle scripts bruger kun to kommandoer: Send og WaitForString. Denne funktionalitet er tilstrækkelig til 90% (eller mere) af de opgaver, der udføres.
Scripts kan køres i Python, JS, VBS (Visual Basic), Perl osv.
Python
# $language = "Python"
# $interface = "1.0"
def main():
crt.Screen.Synchronous = True
crt.Screen.Send("r")
crt.Screen.WaitForString("name")
crt.Screen.Send("adminr")
crt.Screen.WaitForString("Password:")
crt.Screen.Send("Password")
crt.Screen.Synchronous = False
main()
Typisk en fil med udvidelsen «*.py»
VBS
# $language = "VBScript"
# $interface = "1.0"
Sub Main
crt.Screen.Synchronous = True
crt.Screen.Send vbcr
crt.Screen.WaitForString "name"
crt.Screen.Send "cisco" & vbcr
crt.Screen.WaitForString "assword"
crt.Screen.Send "cisco" & vbcr
crt.Screen.Synchronous = False
End Sub
Typisk en fil med udvidelsen «*.vbs»
Oprettelse af et script ved hjælp af optagelse af et script.
Muliggør automatisering af processen til at skrive et script. Du starter optagelsen af scriptet. SecureCRT optager kommandoer og den efterfølgende respons fra udstyret og giver dig et færdigt script.
a. Start optagelse af script:
Menu SecureCRT => Script => Start Recording Script
b. Udføre handlinger i konsollen (udføre handlinger til konfiguration i CLI).
c. Afslut optagelse af script:
Menu SecureCRT => Script => Stop Recording Script…
Gem filen med scriptet.
Eksempel på udførte kommandoer og det gemte script:

Udførelse af scripts på SecureCRT.
Efter oprettelsen/redigeringen af scriptet opstår det naturlige spørgsmål: Hvordan anvender man scriptet?
Der er flere måder:
- Manuel kørsel fra menuen Script
- Automatisk udførelse efter forbindelsen (logon script)
- Automatisk logon uden brug af script
- Manuel udførelse ved hjælp af en knap i SecureCRT (knappen skal stadig oprettes og tilføjes til SecureCRT)
Manuel kørsel fra menuen Script
Menu SecureCRT => Script => Run…
— De seneste 10 scripts huskes og er tilgængelige for hurtig kørsel:
Menu SecureCRT => Script => 1 «Filnavn på scriptet»
Menu SecureCRT => Script => 2 «Filnavn på scriptet»
Menu SecureCRT => Script => 3 «Filnavn på scriptet»
Menu SecureCRT => Script => 4 «Filnavn på scriptet»
Menu SecureCRT => Script => 5 «Filnavn på scriptet»
Automatisk udførelse efter forbindelsen (logon script)
Indstillinger for automatisk scriptlogon konfigureres for den gemte session: Connection => Logon Actions => Logon script

Automatisk logon uden brug af script
Det er muligt automatisk at indtaste brugernavn og kodeord uden at skrive et script, kun ved hjælp af SecureCRT's indbyggede funktionalitet. I indstillingerne for forbindelsen «Connection» => Logon Actions => Automate logon – skal flere par udfyldes – som repræsenterer par: «Forventet tekst» + «Sendte tegn til denne tekst» der kan være mange sådanne par. (Eksempel: 1. par forventning om indtastning af brugernavn, 2. forventning om indtastning af kodeord, 3. forventning om invitation til privilegeret tilstand, 4. kodeord for privilegeret tilstand.)
Eksempel på automatisk logon på Cisco ASA:

Manuel udførelse ved hjælp af en knap i SecureCRT (knappen skal stadig oprettes og tilføjes til SecureCRT)
I SecureCRT kan du tildele et script til en knap. Knappen tilføjes til en specielt oprettet panel til dette formål.
a. Tilføj panelet til grænsefladen: Menu SecureCRT => View => Button Bar
b. Tilføj en knap til panelet og tilføj scriptet. – Højreklik på panelet Button Bar og vælg «New button…» fra kontekstmenuen.
c. I dialogvinduet «Map Button» vælger du handlingen (function) «Run Script».
Angiver knappenote. Farve til ikonen på knappen. Afslut indstillingerne ved at trykke på Ok.

Bemærk:
Panelet med knapper er en meget nyttig funktion.
1. Det er muligt at vælge, hvilken panel der som standard åbnes til denne fane, når man logger ind på en bestemt session.
2. Det er muligt at tildele forudbestemte handlinger til standard handlinger med udstyret: vis show version, show running-config, gem konfigurationen.

Der er ikke knyttet noget script til disse knapper. Kun en linje med handlinger:

Indstillingen - for at åbne det nødvendige panel med knapper, når der skiftes til en session, foretages i sessionens indstillinger:

For kunden giver det mening at indstille individuelle scripts til Login og overgang til panelet med hyppige kommandoer for leverandøren.
![]()
Når man trykker på knappen Go Cisco - skifter panelet til Cisco Button Bar.

Eksempler på brug af enkle og avancerede scripts. (Praktik fra det virkelige liv.)
Enkelte scripts er tilstrækkelige til næsten alle tilfælde. Men en gang havde jeg brug for at komplicere scriptet en smule – for at fremskynde arbejdet. Denne komplicering - krævede kun at anmode om ekstra data i et dialogvindue fra brugeren.
Anmodning om brugerdata via dialogboks
Jeg havde to dataanmodninger i mit script. Det var Hostname og det 4. oktet af IP-adressen. For at udføre denne handling - googlede jeg, hvordan man gjorde det og fandt det på det officielle SecureCRT-websted (vandyke) - funktionen kaldes prompt.
crt.Screen.WaitForString("-Vlanif200]")
hostnamestr = crt.Dialog.Prompt("Indtast hostname:", "hostname", "", False)
ipaddressstr = crt.Dialog.Prompt("Indtast ip adresse:", "ip", "", False)
crt.Screen.Send("ip address 10.10.10.")
crt.Screen.Send(ipaddressstr)
crt.Screen.Send(" 23r")
crt.Screen.Send("quitr")
crt.Screen.Send("sysname ")
crt.Screen.Send(hostnamestr)
crt.Screen.Send("r")
Denne del af scriptet anmodede om Hostname og cifrene fra det sidste oktet. Da der var 15 enheder, og dataene blev præsenteret i et tabel, kopierede jeg værdierne fra tabellen og indsatte dem i dialogvinduerne. Derefter fungerede scriptet automatisk.
FTP-kopiering til netværksudstyr.
Dette script åbnede et kommandovindue (shell) og kopierede data via FTP. Når det var færdigt, lukkede det sessionen. Det er umuligt at bruge notesblokken til dette, da kopieringen tager meget lang tid, og dataene i FTP-bufferen ikke vil forblive der så længe:
# $language = "Python"
# $interface = "1.0"
# Connect to a telnet server and automate the initial login sequence.
# Note that synchronous mode is enabled to prevent server output from
# potentially being missed.
def main():
crt.Screen.Synchronous = True
crt.Screen.Send("ftp 192.168.1.1r")
crt.Screen.WaitForString("Name")
crt.Screen.Send("adminr")
crt.Screen.WaitForString("Password:")
crt.Screen.Send("Passwordr")
crt.Screen.WaitForString("ftp")
crt.Screen.Send("binaryr")
crt.Screen.WaitForString("ftp")
crt.Screen.Send("put S5720LI-V200R011SPH016.patr")
crt.Screen.WaitForString("ftp")
crt.Screen.Send("quitr")
crt.Screen.Synchronous = False
main()
Indtastning af brugernavn/adgangskode ved hjælp af script
En af kunderne havde ikke direkte adgang til netværksudstyret. Man kunne først få adgang til udstyret ved at tilslutte sig Gateway'en, hvorefter man kunne oprette forbindelse til udstyret, der var tilsluttet det. Til forbindelsen blev der brugt en SSH-klient, der er indbygget i IOS/udstyrets software. Derfor blev brugernavn og adgangskode anmodet om i konsollen. Med nedenstående script blev brugernavn og adgangskode indtastet automatisk:
# $language = "Python"
# $interface = "1.0"
# Connect to a telnet server and automate the initial login sequence.
# Note that synchronous mode is enabled to prevent server output from
# potentially being missed.
def main():
crt.Screen.Synchronous = True
crt.Screen.Send("snmpadminr")
crt.Screen.WaitForString("assword:")
crt.Screen.Send("Passwordr")
crt.Screen.Synchronous = False
main()
Bemærk: Der var to scripts. Et til administratorkontoen, det andet til eSIGHT-kontoen.
Scriptet med mulighed for at tilføje data direkte under udførelsen af scriptet.
Opgaven var at tilføje en statisk rute på alt netværksudstyr. Men gateway'en til internettet var forskellig på hvert udstyr (og den adskilte sig fra gateway'en som standard). Det følgende script viste routingtabellen, gik ind i konfigurationsmodus og indtastede ikke hele kommandoen (IP-adressen på gateway'en til internettet) – denne del tilføjede jeg. Efter jeg trykkede Enter, fortsatte scriptet udførelsen af kommandoen.
# $language = "Python"
# $interface = "1.0"
# Connect to a telnet server and automate the initial login sequence.
# Note that synchronous mode is enabled to prevent server output from
# potentially being missed.
def main():
crt.Screen.Synchronous = True
crt.Screen.Send("Zdes-mogla-bit-vasha-reklamar")
crt.Screen.WaitForString("#")
crt.Screen.Send("show run | inc ip router")
crt.Screen.WaitForString("#")
crt.Screen.Send("conf tr")
crt.Screen.WaitForString("(config)#")
crt.Screen.Send("ip route 10.10.10.8 255.255.255.252 ")
crt.Screen.WaitForString("(config)#")
crt.Screen.Send("endr")
crt.Screen.WaitForString("#")
crt.Screen.Send("copy run star")
crt.Screen.WaitForString("[startup-config]?")
crt.Screen.Send("r")
crt.Screen.WaitForString("#")
crt.Screen.Send("exitr")
crt.Screen.Synchronous = False
main()
I dette script er linjen: crt.Screen.Send(«ip route 10.10.10.8 255.255.255.252 „) ikke komplet med IP-adressen på gateway'en og der mangler et linjeskift-tegn. Scriptet venter på at næste linje med tegnene «(config)#» dukker op. Disse tegn dukkede op efter jeg indtastede IP-adressen og trykkede enter.
Konklusion:
Når man skriver og udfører et script, skal man nødvendigvis følge reglen: Tiden brugt på at skrive scriptet og udføre det må aldrig være længere end den tid, der teoretisk ville blive brugt på at udføre det samme arbejde manuelt (copy/paste fra notesblok, skrive og debugge playbook til ansible, skrive og debugge et python-script). Det vil sige, at brugen af scriptet skal spare tid, ikke bruge tid på engangsautomatisering af processer (det vil sige, når scriptet er unikt og repetitioner ikke vil finde sted). Men hvis scriptet er unikt, og automatiseringen med scriptet samt skrivningen/debugging af scriptet tager mindre tid end at udføre det på enhver anden måde (ansible, kommandovindue), så er scriptet den bedste løsning.
Debugging af scriptet. Scriptet vokser gradvist, debugging foregår i praksis på det første, andet og tredje apparat, og til det fjerde vil scriptet sandsynligvis være helt funktionelt.
At køre et script (med indtastning af brugernavn og adgangskode) med musen sker normalt hurtigere end at kopiere fra notesbogen Username og Password. Men det er ikke sikkert ud fra et sikkerhedsperspektiv.
Et andet (virkeligt) eksempel, når brugen af et script er: Du har ikke direkte adgang til netværksenhederne. Men du har brug for at justere alle netværksenheder (føje til overvågningssystemet, justere yderligere Username/password/snmpv3username/password). Der er adgang, når du logger ind på Core-switchen, og derfra åbner SSH til de andre enheder. Hvorfor kan man ikke bruge Ansible-værktøjer? – Fordi vi støder på begrænsningen i antallet af tilladte samtidige sessioner på netværksenhederne (line vty 0 4, user-interface vty 0 4) (det er et andet spørgsmål, hvordan man i Ansible håndterer forskellige enheder med den samme SSH som første hop).
Scriptet reducerer tiden ved langvarige operationer – for eksempel kopiering af filer via FTP. Efter kopieringen er afsluttet, begynder scriptet straks at køre. En person skal vente på, at kopieringen slutter, derefter erkende afslutningen af kopieringen og derefter indtaste de relevante kommandoer. Scriptet gør dette objektivt hurtigere.
Scripts er anvendelige der, hvor det ikke er muligt at bruge masseoverførselsmidler: Konsol. Eller når dele af dataene til udstyret er unikke: hostname, management ip address. Eller når det er sværere at skrive et program og fejlsøge det, end at tilføje de data, der er indhentet fra udstyret under scriptets kørsel. – Et eksempel med scriptet til at skrive ruter, når hver enhed har sin egen IP-adresse fra internetudbyderen. (Mine kolleger skrev sådanne scripts – når DMVPN-spoke var over 300. Der var brug for at ændre DMVPN-indstillingerne).
Praktisk eksempel: Indstilling af de første indstillinger på en ny switch via konsolporte:
A. Indsatte konsolkablet i enheden.
B. Startede scriptet
C. Ventede på, at scriptet blev udført
D. Flyttede konsolkablet til næste enhed.
E. Hvis switchen ikke er den sidste, gå tilbage til punkt B.
Resultatet af scriptets arbejde:
- en indledende adgangskode er indstillet på udstyret.
- Username er indtastet
- en unik IP-adresse for enheden er indtastet.
P.S. operationen måtte gentages. Fordi ssh var standardmæssigt ikke konfigureret/udtaget. (Ja, det er min fejl.)
Kilder anvendt.
1.
2.
Bilag 1: Eksempler på scripts.
Et eksempel på et langt script med to forespørgsler: Værtsnavn og IP-adresse. Det blev oprettet til forudkonfigurering af udstyr via konsollen (9600 baud). Desuden til forberedelse af forbindelsen af udstyret til netværket.
# $language = "Python"
# $interface = "1.0"
# Connect to a telnet server and automate the initial login sequence.
# Note that synchronous mode is enabled to prevent server output from
# potentially being missed.
def main():
crt.Screen.Synchronous = True
crt.Screen.Send("r")
crt.Screen.WaitForString("name")
crt.Screen.Send("adminr")
crt.Screen.WaitForString("Password:")
crt.Screen.Send("Passwordr")
crt.Screen.Send("sysr")
crt.Screen.WaitForString("]")
crt.Screen.Send("interface Vlanif 1r")
crt.Screen.WaitForString("Vlanif1]")
crt.Screen.Send("undo ip addressr")
crt.Screen.Send("shutdownr")
crt.Screen.Send("vlan 100r")
crt.Screen.Send(" description description1r")
crt.Screen.Send(" name description1r")
crt.Screen.Send("vlan 110r")
crt.Screen.Send(" description description2r")
crt.Screen.Send(" name description2r")
crt.Screen.Send("vlan 120r")
crt.Screen.Send(" description description3r")
crt.Screen.Send(" name description3r")
crt.Screen.Send("vlan 130r")
crt.Screen.Send(" description description4r")
crt.Screen.Send(" name description4r")
crt.Screen.Send("vlan 140r")
crt.Screen.Send(" description description5r")
crt.Screen.Send(" name description5r")
crt.Screen.Send("vlan 150r")
crt.Screen.Send(" description description6r")
crt.Screen.Send(" name description6r")
crt.Screen.Send("vlan 160r")
crt.Screen.Send(" description description7r")
crt.Screen.Send(" name description7r")
crt.Screen.Send("vlan 170r")
crt.Screen.Send(" description description8r")
crt.Screen.Send(" name description8r")
crt.Screen.Send("vlan 180r")
crt.Screen.Send(" description description9r")
crt.Screen.Send(" name description9r")
crt.Screen.Send("vlan 200r")
crt.Screen.Send(" description description10r")
crt.Screen.Send(" name description10r")
crt.Screen.Send("vlan 300r")
crt.Screen.Send(" description description11r")
crt.Screen.Send(" name description11r")
crt.Screen.Send("quitr")
crt.Screen.WaitForString("]")
crt.Screen.Send("stp region-configurationr")
crt.Screen.Send("region-name descr")
crt.Screen.Send("active region-configurationr")
crt.Screen.WaitForString("mst-region]")
crt.Screen.Send("quitr")
crt.Screen.Send("stp instance 0 priority 57344r")
crt.Screen.WaitForString("]")
crt.Screen.Send("interface range GigabitEthernet 0/0/1 to GigabitEthernet 0/0/42r")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("description Usersr")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("port link-type hybridr")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("voice-vlan 100 enabler")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("voice-vlan legacy enabler")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("port hybrid pvid vlan 120r")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("port hybrid tagged vlan 100r")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("port hybrid untagged vlan 120r")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("stp edged-port enabler")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("trust 8021pr")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("storm-control broadcast min-rate 1000 max-rate 1500r")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("storm-control multicast min-rate 1000 max-rate 1500r")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("storm-control action blockr")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("storm-control enable trapr")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("quitr")
crt.Screen.Send("interface range GigabitEthernet 0/0/43 to GigabitEthernet 0/0/48r")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("description Printersr")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("port link-type accessr")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("port default vlan 130r")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("stp edged-port enabler")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("trust 8021pr")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("storm-control broadcast min-rate 1000 max-rate 1500r")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("storm-control multicast min-rate 1000 max-rate 1500r")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("storm-control action blockr")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("storm-control enable trapr")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("quitr")
crt.Screen.Send("interface range XGigabitEthernet 0/0/1 to XGigabitEthernet 0/0/2r")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("description uplinkr")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("port link-type trunkr")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("port trunk allow-pass vlan 100 110 120 130 140 150 160 170 180 200r")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("port trunk allow-pass vlan 300r")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("storm-control broadcast min-rate 1000 max-rate 1500r")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("storm-control multicast min-rate 1000 max-rate 1500r")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("storm-control action blockr")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("storm-control enable trapr")
crt.Screen.WaitForString("port-group]")
crt.Screen.Send("quitr")
crt.Screen.Send("ntp-service unicast-server 10.10.10.4r")
crt.Screen.Send("ntp-service unicast-server 10.10.10.2r")
crt.Screen.Send("ntp-service unicast-server 10.10.10.134r")
crt.Screen.Send("ip route-static 0.0.0.0 0.0.0.0 10.10.10.254r")
crt.Screen.Send("interface Vlanif 200r")
crt.Screen.WaitForString("-Vlanif200]")
crt.Screen.Send("r")
crt.Screen.WaitForString("-Vlanif200]")
crt.Screen.Send("r")
crt.Screen.WaitForString("-Vlanif200]")
crt.Screen.Send("r")
crt.Screen.WaitForString("-Vlanif200]")
crt.Screen.Send("r")
crt.Screen.WaitForString("-Vlanif200]")
crt.Screen.Send("r")
crt.Screen.WaitForString("-Vlanif200]")
crt.Screen.Send("r")
crt.Screen.WaitForString("-Vlanif200]")
crt.Screen.Send("r")
crt.Screen.WaitForString("-Vlanif200]")
hostnamestr = crt.Dialog.Prompt("Enter hostname:", "hostname", "", False)
ipaddressstr = crt.Dialog.Prompt("Enter ip address:", "ip", "", False)
crt.Screen.Send("ip address 10.10.10.")
crt.Screen.Send(ipaddressstr)
crt.Screen.Send(" 24r")
crt.Screen.Send("quitr")
crt.Screen.Send("sysname ")
crt.Screen.Send(hostnamestr)
crt.Screen.Send("r")
crt.Screen.WaitForString("]")
crt.Screen.Synchronous = False
main()
Sådanne scripts er normalt ikke nødvendige, men med 15 enheder gjorde det konfigurationen hurtigere. Derefter var det hurtigere at konfigurere udstyret ved hjælp af SecureCRT Command-vinduet.
Opsætning af en konto til ssh.
Et andet eksempel. Opsætning også via konsollen.
# $language = "Python"
# $interface = "1.0"
# Connect to a telnet server and automate the initial login sequence.
# Note that synchronous mode is enabled to prevent server output from
# potentially being missed.
def main():
crt.Screen.Synchronous = True
crt.Screen.Send("r")
crt.Screen.WaitForString("name")
crt.Screen.Send("adminr")
crt.Screen.WaitForString("Password:")
crt.Screen.Send("Passwordr")
crt.Screen.WaitForString(">")
crt.Screen.Send("sysr")
crt.Screen.Send("stelnet server enabler")
crt.Screen.Send("aaar")
crt.Screen.Send("local-user admin service-type terminal ftp http sshr")
crt.Screen.Send("quitr")
crt.Screen.Send("user-interface vty 0 4r")
crt.Screen.Send("authentication-mode aaar")
crt.Screen.Send("quitr")
crt.Screen.Send("quitr")
crt.Screen.Synchronous = False
main()
Om SecureCRT:Betalt software: fra 99$ (den laveste pris kun for SecureCRT i et år)
Licensen til softwaren købes én gang, med support (til opdateringer), derefter bruges softwaren med denne licens uden tidsbegrænsning.
Arbejder på Mac OS X og Windows operativsystemer.
Der er support for scripts (denne artikel)
Der er
Serial/Telnet/SSH1/SSH2/Shell i operativsystemet
Kilde: habr.com
