<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Programming &#8211; Blog of Kliment Andreev &#8211; A place so I won&#039;t forget things</title>
	<atom:link href="https://blog.andreev.it/category/programming/feed/" rel="self" type="application/rss+xml" />
	<link>https://blog.andreev.it</link>
	<description></description>
	<lastBuildDate>Fri, 22 Nov 2024 16:36:12 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	
	<item>
		<title>FreeBSD: OpenVPN server in a LAN (local network) environment</title>
		<link>https://blog.andreev.it/2024/11/freebsd-openvpn-server-in-a-lan-local-network-environment/</link>
					<comments>https://blog.andreev.it/2024/11/freebsd-openvpn-server-in-a-lan-local-network-environment/#respond</comments>
		
		<dc:creator><![CDATA[Kliment Andreev]]></dc:creator>
		<pubDate>Fri, 22 Nov 2024 16:36:12 +0000</pubDate>
				<category><![CDATA[FreeBSD]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[freebsd]]></category>
		<category><![CDATA[LAN]]></category>
		<category><![CDATA[OpenVPN]]></category>
		<category><![CDATA[pf]]></category>
		<category><![CDATA[VPN]]></category>
		<guid isPermaLink="false">https://blog.andreev.it/?p=10074</guid>

					<description><![CDATA[In this post I&#8217;ll describe a use case that I am currently using. I&#8230;]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><p>In this post I&#8217;ll describe a use case that I am currently using. I have a standard Internet connection that goes to my Eero mesh router and behind that I have a homelab on 192.168.1.0/24 network as shown in the diagram. I have a port forwarding configured on the router so I can access one of my Linux servers, but if I want to access other servers, I have to either port forward SSH on some other-than-22/tcp port or use the Linux server as a jump-box. None of this is good, so I decided to create an OpenVPN server. Anytime I VPN to this server I can ssh/rdp to any of the internal servers. The OpenVPN server requires one nic.<br />
<strong>NOTE</strong>: This post is very FreeBSD specific so if you want to use some Linux server as OpenVPN server, this guide won&#8217;t help you at all.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2024/11/P177-01.png"><img fetchpriority="high" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2024/11/P177-01.png" alt="" width="721" height="491" class="aligncenter size-full wp-image-10078" srcset="https://blog.andreev.it/wp-content/uploads/2024/11/P177-01.png 721w, https://blog.andreev.it/wp-content/uploads/2024/11/P177-01-300x204.png 300w, https://blog.andreev.it/wp-content/uploads/2024/11/P177-01-585x398.png 585w" sizes="(max-width: 721px) 100vw, 721px" /></a><br />
<strong>IMPORTANT</strong>: On your home router port-forward port 1194/udp to your OpenVPN server.</p>
<h1>OpenVPN server</h1>
<p>Let&#8217;s install OpenVPN server first.</p>
<pre class="brush: bash; title: ; notranslate">
pkg install openvpn
</pre>
<p>Make sure it starts on boot and enable the NAT.</p>
<pre class="brush: bash; title: ; notranslate">
sysrc openvpn_enable=YES
sysrc gateway_enable=YES
</pre>
<p>Next, we need a directory where we&#8217;ll store our config files and the PKI.</p>
<pre class="brush: bash; title: ; notranslate">
mkdir /usr/local/etc/openvpn
cd /usr/local/etc/openvpn
</pre>
<p>Create the PKI structure.</p>
<pre class="brush: bash; title: ; notranslate">
easyrsa init-pki
</pre>
<p>Go to the newly created <em>pki </em>directory and make a copy of the <em>vars.example</em> file.</p>
<pre class="brush: bash; title: ; notranslate">
cd pki
cp vars.example vars
</pre>
<p>Now edit this file and uncomment the following lines and change to match your needs. It&#8217;s optional and you can keep everything-as-is.</p>
<pre class="brush: bash; title: ; notranslate">
set_var EASYRSA_REQ_COUNTRY     &quot;US&quot;
set_var EASYRSA_REQ_PROVINCE    &quot;NJ&quot;
set_var EASYRSA_REQ_CITY        &quot;Allentown&quot;
set_var EASYRSA_REQ_ORG         &quot;iAndreev&quot;
set_var EASYRSA_REQ_EMAIL       &quot;mail@somemail.com&quot;
set_var EASYRSA_REQ_OU          &quot;iAndreev&quot;
set_var EASYRSA_CA_EXPIRE       3650
set_var EASYRSA_CERT_EXPIRE     825
</pre>
<p>Now create the Certificate Authority based on these values.</p>
<pre class="brush: bash; title: ; notranslate">
cd /usr/local/etc/openvpn
easyrsa build-ca
</pre>
<p>This command will prompt you to create a password that you will you to sign the certificates for the server and the clients.<br />
Then create the DH parameters. It might take 30+ seconds.</p>
<pre class="brush: bash; title: ; notranslate">
easyrsa gen-dh
</pre>
<p>Create the server request and sign it.</p>
<pre class="brush: bash; title: ; notranslate">
easyrsa build-server-full server nopass
</pre>
<p>You have to type <em>yes </em>to confirm and then use the password that you created above to sign the cert.<br />
The command will create some files under the <em>pki </em>directory and we&#8217;ll need those later.<br />
Next, we&#8217;ll create the same for the client. In my case it&#8217;s a Dell laptop, so I use the word dell, but you can use client, client1, something-whatever&#8230;</p>
<pre class="brush: bash; title: ; notranslate">
easyrsa build-client-full dell nopass
</pre>
<p>Same thing, answer <em>yes </em>and then use the password to sign the certs.<br />
Copy the OpenVPN config example file that comes with the package.</p>
<pre class="brush: bash; title: ; notranslate">
cp /usr/local/share/examples/openvpn/sample-config-files/server.conf /usr/local/etc/openvpn/openvpn.conf
</pre>
<p>Edit<em> openvpn.conf</em> and make sure these values are changed and match the certs you created earlier.</p>
<pre class="brush: bash; title: ; notranslate">
ca /usr/local/etc/openvpn/pki/ca.crt
cert /usr/local/etc/openvpn/pki/issued/server.crt
key /usr/local/etc/openvpn/pki/private/server.key

dh /usr/local/etc/openvpn/pki/dh.pem
</pre>
<p>In addition to that, find the line that says push route and create a new one that matches your internal subnet. In my case it looks like this.</p>
<pre class="brush: bash; title: ; notranslate">
push &quot;route 192.168.1.0 255.255.255.0&quot;
</pre>
<p>If you have internal DNS and you want to send that as well, uncomment the following line and change to match your DNS IP.</p>
<pre class="brush: bash; title: ; notranslate">
push &quot;dhcp-option DNS 208.67.222.222&quot;
</pre>
<p>Now, you can start the OpenVPN server. Make sure you check the syslog for any errors.</p>
<pre class="brush: bash; title: ; notranslate">
service openvpn start
tail /var/log/messages
</pre>
<h1>OpenVPN client</h1>
<p>On your laptop that you want to use as a VPN client, download the OpenVPN client from <a href="https://openvpn.net/community-downloads/" target="_blank">here</a>. When you install the client, you&#8217;ll have a generic config file under <em>C:\Program Files\OpenVPN\sample-config</em> folder. Copy that file under <em>c:\users\[your_username]\OpenVPN\config</em>.<br />
Then, edit the file and change the following lines.</p>
<pre class="brush: bash; title: ; notranslate">
remote public-ip-or-hostname-of-your-router 1194
ca ca.crt
cert dell.crt
key dell.key
</pre>
<p>NOTE: Make sure you copy <em>ca.crt</em> from <em>/usr/local/etc/openvpn/pki</em>, <em>dell.crt</em> or client.crt or whatever.crt from <em>/usr/local/etc/openvpn/pki/issued</em> and<em> dell.key</em> or client.key or whatever.key from <em>/usr/local/etc/openvpn/pki/private</em> directories to the same <em>c:\users\[your-user-name]\OpenVPN\config</em> folder. There should be 4 files there. The config file, the CA certificate, the client certificate and the private key for the client.<br />
Start the OpenVPN client and connect. You shouldn&#8217;t have any problems and you&#8217;ll get 10.8.0.2 IP assigned to your laptop. If you try to ping 10.8.0.1 you&#8217;ll get a response. Do route print and you&#8217;ll see that the routes for 192.168.1.0 are going over 10.8.0.1. But, if you try to ping some server internally, e.g. 192.168.1.21, you won&#8217;t get any response. That&#8217;s because that server doesn&#8217;t know the route back. You can easily add the route to each server but it&#8217;s not practical. Let&#8217;s make one more change to the server.</p>
<h1>OpenVPN server</h1>
<p>Add these two lines to <em>/etc/rc.conf</em>. That&#8217;s the FreeBSD pf firewall.</p>
<pre class="brush: bash; title: ; notranslate">
pf_enable=&quot;YES&quot;
pf_rules=&quot;/etc/pf.conf&quot;
</pre>
<p>Now, create this config file in <em>/etc/pf.conf</em>. Many thanks to <a href="https://gundersen.net/openvpn-server-on-freebsd-with-pf-firewall/" target="_blank">this </a>guy. </p>
<pre class="brush: bash; title: ; notranslate">
vpnclients = &quot;10.8.0.0/24&quot;
# The name of your NIC. I am using a VM on ESXi and vmx0 is my NIC.
wanint = &quot;vmx0&quot;
# put your tunnel interface here, it is usually tun0
vpnint = &quot;tun0&quot;
# OpenVPN by default runs on udp port 1194
udpopen = &quot;{1194}&quot;
# Open the SSH port.
tcpopen = &quot;{22}&quot;
icmptypes = &quot;{echoreq, unreach}&quot;

set skip on lo
# the essential line
nat on $wanint inet from $vpnclients to any -&gt; $wanint

block in
pass in on $wanint proto udp from any to $wanint port $udpopen
pass in on $wanint proto tcp from any to $wanint port $tcpopen
# the following two lines could be made stricter if you don&#039;t trust the clients
pass out quick
pass in on $vpnint from any to any
pass in inet proto icmp all icmp-type $icmptypes
</pre>
<p>Reboot the OpenVPN server and if you reconnect from the client, you can use the internal IPs directly to connect.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.andreev.it/2024/11/freebsd-openvpn-server-in-a-lan-local-network-environment/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Visual Studio and BitBucket</title>
		<link>https://blog.andreev.it/2020/01/visual-studio-and-bitbucket/</link>
					<comments>https://blog.andreev.it/2020/01/visual-studio-and-bitbucket/#respond</comments>
		
		<dc:creator><![CDATA[Kliment Andreev]]></dc:creator>
		<pubDate>Sat, 11 Jan 2020 12:43:11 +0000</pubDate>
				<category><![CDATA[DevOps]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[BitBucket]]></category>
		<category><![CDATA[Visual Studio]]></category>
		<guid isPermaLink="false">https://blog.andreev.it/?p=6405</guid>

					<description><![CDATA[In this post I&#8217;ll explain how to use Visual Studio and BitBucket. I host&#8230;]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><p>In this post I&#8217;ll explain how to use Visual Studio and BitBucket. I host my own BitBucket server, but it should be the same.<br />
First, create a BitBucket repository and name it. The repo will have to be the same name as the project in Visual Studio.<br />
So, in my case, I&#8217;ll create a repo called <strong>test123</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P143-01.png"><img decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P143-01.png" alt="" width="586" height="355" class="aligncenter size-full wp-image-6407" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P143-01.png 586w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-01-300x182.png 300w" sizes="(max-width: 586px) 100vw, 586px" /></a><br />
Then, open Visual Studio 2019 and choose <strong>Continue without code</strong> from the bottom right.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P143-02.png"><img decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P143-02.png" alt="" width="956" height="616" class="aligncenter size-full wp-image-6408" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P143-02.png 956w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-02-300x193.png 300w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-02-768x495.png 768w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-02-585x377.png 585w" sizes="(max-width: 956px) 100vw, 956px" /></a><br />
If you don&#8217;t have the BitBucket plugin, go to <strong>Extensions | Manage Extensions</strong> from the menu and install <strong>Visual Studio Bitbucket Extension</strong>. Once installed, you&#8217;ll have to close and re-open Visual Studio.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P143-03.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P143-03.png" alt="" width="603" height="318" class="aligncenter size-full wp-image-6409" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P143-03.png 603w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-03-300x158.png 300w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-03-585x309.png 585w" sizes="(max-width: 603px) 100vw, 603px" /></a><br />
Go to <strong>View | Team Explorer</strong> from the menu and click <strong>Login </strong>for the BitBucket exptension.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P143-04.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P143-04.png" alt="" width="298" height="419" class="aligncenter size-full wp-image-6412" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P143-04.png 298w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-04-213x300.png 213w" sizes="(max-width: 298px) 100vw, 298px" /></a><br />
Depending on your BitBucket server (cloud or on-prem), enter the necessary parameters and click to <strong>Connect</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P143-05.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P143-05.png" alt="" width="408" height="320" class="aligncenter size-full wp-image-6413" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P143-05.png 408w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-05-300x235.png 300w" sizes="(max-width: 408px) 100vw, 408px" /></a><br />
Once logged in, click on <strong>Clone</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P143-06.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P143-06.png" alt="" width="308" height="148" class="aligncenter size-full wp-image-6414" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P143-06.png 308w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-06-300x144.png 300w" sizes="(max-width: 308px) 100vw, 308px" /></a><br />
Select the repo and click <strong>Clone</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P143-07.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P143-07.png" alt="" width="461" height="295" class="aligncenter size-full wp-image-6415" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P143-07.png 461w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-07-300x192.png 300w" sizes="(max-width: 461px) 100vw, 461px" /></a><br />
Create a new project (<strong>File | New | Project</strong>) and name it the same as the repo.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P143-08.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P143-08.png" alt="" width="834" height="324" class="aligncenter size-full wp-image-6416" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P143-08.png 834w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-08-300x117.png 300w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-08-768x298.png 768w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-08-585x227.png 585w" sizes="(max-width: 834px) 100vw, 834px" /></a><br />
Click <strong>Next</strong> and name the project the same again, then click <strong>Create</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P143-09.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P143-09.png" alt="" width="980" height="668" class="aligncenter size-full wp-image-6417" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P143-09.png 980w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-09-300x204.png 300w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-09-768x523.png 768w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-09-585x399.png 585w" sizes="(max-width: 980px) 100vw, 980px" /></a><br />
You can push the initial code now. First click <strong>File | Save All</strong>. Then in Team Explorer, click where it says <strong>Connect </strong>and switch it to <strong>Change</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P143-10.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P143-10.png" alt="" width="262" height="194" class="aligncenter size-full wp-image-6419" /></a><br />
Type something for the first commit and click on <strong>Commit All and Push</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P143-11.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P143-11.png" alt="" width="403" height="324" class="aligncenter size-full wp-image-6420" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P143-11.png 403w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-11-300x241.png 300w" sizes="(max-width: 403px) 100vw, 403px" /></a><br />
If you go back to BitBucket, you&#8217;ll see your code there.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P143-12.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P143-12.png" alt="" width="613" height="306" class="aligncenter size-full wp-image-6422" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P143-12.png 613w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-12-300x150.png 300w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-12-585x292.png 585w" sizes="(max-width: 613px) 100vw, 613px" /></a></p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.andreev.it/2020/01/visual-studio-and-bitbucket/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>DevOps: CI/CD using git, Jenkins, Docker, Kubernetes</title>
		<link>https://blog.andreev.it/2019/11/devops-ci-cd-using-git-jenkins-docker-kubernetes/</link>
					<comments>https://blog.andreev.it/2019/11/devops-ci-cd-using-git-jenkins-docker-kubernetes/#comments</comments>
		
		<dc:creator><![CDATA[Kliment Andreev]]></dc:creator>
		<pubDate>Sun, 03 Nov 2019 21:34:00 +0000</pubDate>
				<category><![CDATA[Containers]]></category>
		<category><![CDATA[DevOps]]></category>
		<category><![CDATA[Docker]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Node.js]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[chat]]></category>
		<category><![CDATA[containers]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[git]]></category>
		<category><![CDATA[Jenkins]]></category>
		<guid isPermaLink="false">https://blog.andreev.it/?p=5869</guid>

					<description><![CDATA[In this post I&#8217;ll explain how to create a Node.js chat program, put the&#8230;]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><p>In this post I&#8217;ll explain how to create a Node.js chat program, put the source on Github and then using Jenkins we&#8217;ll deploy the code to Docker Hub and from there to Docker containers that are managed by Kubernetes. There are many prerequisites for this. </p>
<ul>
&#8211; Linux workstation with Node.js and git installed (I&#8217;ll use CentOS 7)<br />
&#8211; Valid <a href="https://github.com/" rel="noopener noreferrer" target="_blank">GitHub </a>account. Create a repo called chitchat.<br />
&#8211; Valid <a href="https://hub.docker.com/" rel="noopener noreferrer" target="_blank">DockerHub </a>account. Create a repo called chitchat.<br />
&#8211; Jenkins server with a public IP, git and Docker installed (I&#8217;ll use CentOS 7)<br />
&#8211; Kubernetes cluster running Docker</ul>
<p>See my other posts on how to install <a href="https://blog.andreev.it/?p=5482" rel="noopener noreferrer" target="_blank">Jenkins </a>and <a href="https://blog.andreev.it/?p=5127" rel="noopener noreferrer" target="_blank">Kubernetes </a>cluster. </p>
<h1>The chat program</h1>
<p>For this purpose I&#8217;ve found a simple Node.js chat program that uses Express framework. The program can be found <a href="https://sabe.io/tutorials/how-to-build-real-time-chat-app-node-express-socket-io" rel="noopener noreferrer" target="_blank">here</a>, but we&#8217;ll modify it a bit. On a Linux workstation (I use CentOS, so some commands might differ), log as your regular user and create some necessary directories, a directory called chitchat and two subdirectories, public and test and two subdirectories for public, called js and css.</p>
<pre class="brush: bash; title: ; notranslate">
mkdir -p chitchat/test
mkdir -p chitchat/public/{js,css} 
cd chitchat
</pre>
<p>Create the source files for the chat program. First, let&#8217;s create the HTML. Click the (+) sign to expand. Just copy and paste everything. The first line (cat) will take care of the file creation.</p>
<pre class="brush: xml; collapse: true; light: false; title: ; toolbar: true; notranslate">
cat &lt;&lt; EOF &gt; index.html
&lt;!DOCTYPE html&gt;
&lt;html&gt;
    &lt;head&gt;
        &lt;title&gt;Chit Chat App&lt;/title&gt;
        &lt;link href=&quot;css/styles.css&quot; rel=&quot;stylesheet&quot; type=&quot;text/css&quot; media=&quot;screen&quot;&gt;
        &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width,minimum-scale=1,initial-scale=1&quot;&gt;
    &lt;/head&gt;
    &lt;body&gt;
        &lt;ul class=&quot;messages&quot;&gt;&lt;/ul&gt;
        &lt;form&gt;
            &lt;input type=&quot;text&quot; class=&quot;input&quot; autocomplete=&quot;off&quot; autofocus /&gt;
            &lt;button&gt;Send&lt;/button&gt;
        &lt;/form&gt;
        &lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.dev.js&quot;&gt;&lt;/script&gt;
        &lt;script src=&quot;js/app.js&quot;&gt;&lt;/script&gt;
    &lt;/body&gt;
&lt;/html&gt;
EOF
</pre>
<p>Then, the Node.js code.</p>
<pre class="brush: jscript; collapse: true; light: false; title: ; toolbar: true; notranslate">
cat &lt;&lt; EOF &gt; index.js
var express = require('express');
var app = express();
var http = require('http').Server(app);
var io = require('socket.io')(http);
var port = process.env.PORT || 4141;
var path = require('path');

app.get('/', function(req, res){
    res.sendFile(__dirname + '/index.html');
});

app.use(express.static(path.join(__dirname + '/public')));


io.on('connection', function(socket) {

    socket.on('user_join', function(data) {
        this.username = data;
        socket.broadcast.emit('user_join', data);
    });

    socket.on('chat_message', function(data) {
        data.username = this.username;
        socket.broadcast.emit('chat_message', data);
    });

    socket.on('disconnect', function(data) {
        socket.broadcast.emit('user_leave', this.username);
    });
});

http.listen(port, function() {
    console.log('Listening on *:' + port);
});
EOF
</pre>
<p>Then, the actual code for the app.</p>
<pre class="brush: jscript; collapse: true; light: false; title: ; toolbar: true; notranslate">
cat &lt;&lt; EOF &gt; public/js/app.js
const form = document.querySelector(&quot;form&quot;);
const input = document.querySelector(&quot;.input&quot;);
const messages = document.querySelector(&quot;.messages&quot;);
const username = prompt(&quot;Please enter a nickname: &quot;, &quot;&quot;);
const socket = io();

form.addEventListener(&quot;submit&quot;, function(event) {
        event.preventDefault();

        addMessage(username + &quot;: &quot; + input.value);

        socket.emit(&quot;chat_message&quot;, {
                        message: input.value
        });

        input.value = &quot;&quot;;
        return false;
}, false);

socket.on(&quot;chat_message&quot;, function(data) {
        addMessage(data.username + &quot;: &quot; + data.message);
});

socket.on(&quot;user_join&quot;, function(data) {
        addMessage(data + &quot; just joined the chat!&quot;);
});

socket.on(&quot;user_leave&quot;, function(data) {
        addMessage(data + &quot; has left the chat.&quot;);
});

addMessage(&quot;You have joined the chat as '&quot; + username  + &quot;'.&quot;);
socket.emit(&quot;user_join&quot;, username);

function addMessage(message) {
        const li = document.createElement(&quot;li&quot;);
        li.innerHTML = message;
        messages.appendChild(li);
        window.scrollTo(0, document.body.scrollHeight);
}
EOF
</pre>
<p>And finally the CSS stylesheet.</p>
<pre class="brush: css; collapse: true; light: false; title: ; toolbar: true; notranslate">
cat &lt;&lt; EOF &gt; public/css/styles.css
body {
    margin: 0;
    font-family: sans-serif;
}

form {
    position: fixed;
    bottom: 0;
    left: 0;
    width: 100%;
    display: flex;
    box-sizing: border-box;
    padding: 0.25rem;
}

form input {
    border: 0;
    padding: 0.5rem;
    width: 100%;
    outline: 0;
    margin-right: 0.5rem;
    border-radius: 0.25rem;
    background: #ccc;
}

form button {
    width: 6rem;
    background-color: #1b8c00;
    color: white;
    border: none;
    padding: 0.5rem;
    cursor: pointer;
    border-radius: 0.25rem;
    text-transform: uppercase;
}

form button:hover {
    background-color: #166d01;
}

.messages {
    margin: 0;
    padding: 0;
    margin-bottom: 3rem;
}

.messages li {
    padding: 0.5rem;
}

.messages li:nth-child(odd) {
    background: #eee;
}
EOF
</pre>
<p>If you don&#8217;t have Node.js installed, then install it. If you already have Node.js, skip this step. While still logged as the regular user, sudo to install Node.js and npm and check the versions. Make sure you are in chitchat directory.</p>
<pre class="brush: bash; title: ; notranslate">
curl -sL https://rpm.nodesource.com/setup_10.x | sudo bash -
sudo yum -y install nodejs
node -v &amp;&amp; npm -v
</pre>
<p>Initiate a new project. Make sure you are in chitchat directory.</p>
<pre class="brush: bash; title: ; notranslate">
npm init
</pre>
<p>Answer the questions for the initialization. Just hit Enter for every question, we&#8217;ll replace this file later. Then install Express and socket.io.</p>
<pre class="brush: bash; title: ; notranslate">
npm install -save express
npm install -save socket.io
</pre>
<p>Finally, start the program. If you have a firewall enabled, you have to poke a hole.</p>
<pre class="brush: bash; title: ; notranslate">
sudo firewall-cmd --add-port=4141/tcp --zone=public --permanent
sudo firewall-cmd --reload
node index.js
</pre>
<p>The program should listen on port 4141. Open up a browser and access the Node.js app. (<strong>http://workstation_ip:4141</strong>).<br />
I&#8217;ve opened two windows and logged as UserA and UserB to test the functionality. It looks like this.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/10/P138-01.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/10/P138-01.png" alt="" width="562" height="386" class="aligncenter size-full wp-image-5924" srcset="https://blog.andreev.it/wp-content/uploads/2019/10/P138-01.png 562w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-01-300x206.png 300w" sizes="(max-width: 562px) 100vw, 562px" /></a></p>
<h1>The GitHub repository</h1>
<p>Now that we have the program ready, let&#8217;s upload it on GitHub. Log with your account and create a new repository. In my case, I&#8217;ll name it chitchat.<br />
On your workstation, initiate the git repo, add the files, do the first commit and push the files to GitHub. It should look like this. Replace your username and the repo name in line 5.</p>
<pre class="brush: bash; highlight: [5]; title: ; notranslate">
cd chitchat
git init
git add .
git commit -m &quot;First commit.&quot;
git remote add origin https://github.com/&lt;your_username&gt;/&lt;your_repo&gt;
git push -u origin master
</pre>
<h1>Jenkins</h1>
<h2>Install Node.js plugin, Git and Docker</h2>
<p>If you installed Jenkins with the default settings, the Git and Docker plugins are probably installed. Node.js plugin is not installed by default so we&#8217;ll have to take care of that. Go to <strong>Manage Jenkins</strong> from the menu and then <strong>Manage Plugins</strong>. From the <strong>Available</strong> tab, find the NodeJS plugin and install it.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/10/P138-03.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/10/P138-03.png" alt="" width="495" height="83" class="aligncenter size-full wp-image-5927" srcset="https://blog.andreev.it/wp-content/uploads/2019/10/P138-03.png 495w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-03-300x50.png 300w" sizes="(max-width: 495px) 100vw, 495px" /></a><br />
Again, go to <strong>Manage Jenkins</strong>, <strong>Global Tool Configuration</strong> and click on <strong>Add NodeJS</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/11/P138-16.jpg"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/11/P138-16.jpg" alt="" width="438" height="116" class="aligncenter size-full wp-image-5965" srcset="https://blog.andreev.it/wp-content/uploads/2019/11/P138-16.jpg 438w, https://blog.andreev.it/wp-content/uploads/2019/11/P138-16-300x79.jpg 300w" sizes="(max-width: 438px) 100vw, 438px" /></a><br />
Enter a name and choose the the latest 10.x version. Make sure that the <strong>Install automatically</strong> is checked. Also, I named my Node.js installation &#8220;<strong>node</strong>&#8221; and I&#8217;ll reference it as &#8220;<strong>node</strong>&#8221; further in the pipeline script. If you name your installation differently, you&#8217;ll have to change that in line 4 in the script below.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/11/P138-17.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/11/P138-17.png" alt="" width="769" height="410" class="aligncenter size-full wp-image-5986" srcset="https://blog.andreev.it/wp-content/uploads/2019/11/P138-17.png 769w, https://blog.andreev.it/wp-content/uploads/2019/11/P138-17-300x160.png 300w, https://blog.andreev.it/wp-content/uploads/2019/11/P138-17-768x409.png 768w, https://blog.andreev.it/wp-content/uploads/2019/11/P138-17-585x312.png 585w" sizes="(max-width: 769px) 100vw, 769px" /></a><br />
Again, go to <strong>Manage Jenkins</strong> and then <strong>Manage Plugins</strong> in the center. Click on the <strong>Installed </strong>tab and look for Docker Pipeline plugin and Git plugin. They should be there and already installed (see the grayed checkmark on the left).<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/11/P138-18.jpg"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/11/P138-18.jpg" alt="" width="429" height="138" class="aligncenter size-full wp-image-5989" srcset="https://blog.andreev.it/wp-content/uploads/2019/11/P138-18.jpg 429w, https://blog.andreev.it/wp-content/uploads/2019/11/P138-18-300x97.jpg 300w" sizes="(max-width: 429px) 100vw, 429px" /></a><br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/11/P138-19.jpg"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/11/P138-19.jpg" alt="" width="692" height="62" class="aligncenter size-full wp-image-5990" srcset="https://blog.andreev.it/wp-content/uploads/2019/11/P138-19.jpg 692w, https://blog.andreev.it/wp-content/uploads/2019/11/P138-19-300x27.jpg 300w, https://blog.andreev.it/wp-content/uploads/2019/11/P138-19-585x52.jpg 585w" sizes="(max-width: 692px) 100vw, 692px" /></a><a href="https://blog.andreev.it/wp-content/uploads/2019/11/P138-20.jpg"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/11/P138-20.jpg" alt="" width="679" height="61" class="aligncenter size-full wp-image-5991" srcset="https://blog.andreev.it/wp-content/uploads/2019/11/P138-20.jpg 679w, https://blog.andreev.it/wp-content/uploads/2019/11/P138-20-300x27.jpg 300w, https://blog.andreev.it/wp-content/uploads/2019/11/P138-20-585x53.jpg 585w" sizes="(max-width: 679px) 100vw, 679px" /></a><br />
If not, click on the <strong>Available </strong>tab, search for these plugins and install them similarly to what you did with Node.js plugin. Once installed, SSH to the Jenkins server and install Git first. We&#8217;ll install it from the source. This is for CentOS 7 only.</p>
<pre class="brush: bash; title: ; notranslate">
yum -y groupinstall &quot;Development Tools&quot;
yum -y install gettext-devel openssl-devel perl-CPAN perl-devel zlib-devel wget
wget https://github.com/git/git/archive/v2.23.0.tar.gz -O /tmp/git.tgz
cd /tmp
tar xzvf git.tgz 
cd git*
make configure
./configure
make install
cd /tmp
rm -Rf git*
</pre>
<p>Run <strong>git &dash;&dash;version</strong> to check if it works properly.<br />
Then, install Docker.</p>
<pre class="brush: bash; title: ; notranslate">
yum install -y yum-utils device-mapper-persistent-data lvm2
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
yum -y install docker-ce
systemctl enable docker
systemctl start docker
usermod jenkins -g docker
systemctl restart jenkins
</pre>
<p>Now, Jenkins can execute Docker commands.</p>
<h2>Jenkins pipeline</h2>
<p>Once installed from the main menu, in the upper-left corner, click on <strong>New Item</strong>. Enter the project name and choose <strong>Pipeline</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/10/P138-02.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/10/P138-02.png" alt="" width="468" height="299" class="aligncenter size-full wp-image-5926" srcset="https://blog.andreev.it/wp-content/uploads/2019/10/P138-02.png 468w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-02-300x192.png 300w" sizes="(max-width: 468px) 100vw, 468px" /></a><br />
Scroll down until you find the <strong>Pipeline script</strong> and paste this code. We just want to test pulling the repo from Github. Make sure you replace your username and repo name in line 10.</p>
<pre class="brush: bash; highlight: [10]; title: ; notranslate">
pipeline {
    agent any

    tools {nodejs &quot;node&quot;}

    stages {

        stage('Cloning Git') {
            steps {
                git 'https://github.com/&lt;your_name&gt;/&lt;your_repo&gt;'
            }
        }
    }
}
</pre>
<p>It looks like this.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/10/P138-04.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/10/P138-04.png" alt="" width="806" height="561" class="aligncenter size-full wp-image-5928" srcset="https://blog.andreev.it/wp-content/uploads/2019/10/P138-04.png 806w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-04-300x209.png 300w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-04-768x535.png 768w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-04-585x407.png 585w" sizes="(max-width: 806px) 100vw, 806px" /></a><br />
Click on <strong>Save </strong>and once back, click on <strong>Build Now</strong> from the menu.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/10/P138-05.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/10/P138-05.png" alt="" width="257" height="375" class="aligncenter size-full wp-image-5929" srcset="https://blog.andreev.it/wp-content/uploads/2019/10/P138-05.png 257w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-05-206x300.png 206w" sizes="(max-width: 257px) 100vw, 257px" /></a><br />
Once completed, you&#8217;ll see that everything is running OK.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/10/P138-06.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/10/P138-06.png" alt="" width="604" height="303" class="aligncenter size-full wp-image-5930" srcset="https://blog.andreev.it/wp-content/uploads/2019/10/P138-06.png 604w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-06-300x150.png 300w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-06-585x293.png 585w" sizes="(max-width: 604px) 100vw, 604px" /></a><br />
Click on <strong>Console Output</strong> to see the actual logs from git.<br />
Now, that we have everything verified, we want to add another step of testing the application. We&#8217;ll have to install the pre-requisites for the Node.js and create a test scenario. On the workstation where your project is, create a test scenario. We&#8217;ll use <a href="https://mochajs.org/" rel="noopener noreferrer" target="_blank">mocha</a> and <a href="https://www.chaijs.com/" rel="noopener noreferrer" target="_blank">chai</a> for this. </p>
<pre class="brush: bash; title: ; notranslate">
cat &lt;&lt; EOF &gt; test/test.js
var chai = require('chai');
var io = require('socket.io-client')
var app = require('../index');
var client = io('http://localhost:4141');

console.log('Server started...Waiting for client connection...');
it('Client connected...', function(done) {
  client.on('connect', function (data) {
    done();
  });
});
EOF
</pre>
<p>We also need to define the pre-requisites. Change some of the lines here to suit your needs. This is the file that we are replacing, the one that <strong>npm init</strong> creates.</p>
<pre class="brush: xml; title: ; notranslate">
cat &lt;&lt; EOF &gt; package.json
{
  &quot;name&quot;: &quot;chitchat&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;description&quot;: &quot;A simple chat program&quot;,
  &quot;main&quot;: &quot;index.js&quot;,
  &quot;scripts&quot;: {
    &quot;start&quot;: &quot;node index.js&quot;,
    &quot;test&quot;: &quot;mocha -R spec test/test.js --exit&quot;
  },
  &quot;author&quot;: &quot;Kliment Andreev&quot;,
  &quot;license&quot;: &quot;ISC&quot;,
  &quot;dependencies&quot;: {
    &quot;express&quot;: &quot;^4.17.1&quot;,
    &quot;socket.io&quot;: &quot;^2.3.0&quot;
  },
  &quot;devDependencies&quot;: {
    &quot;chai&quot;: &quot;^4.2.0&quot;,
    &quot;mocha&quot;: &quot;^5.2.0&quot;
  }
}
EOF
</pre>
<p>As you can see the first file goes under chitchat/test directory and the package.json file is in the root of chitchat directory. Don&#8217;t forget to push these files to GitHub.</p>
<pre class="brush: bash; title: ; notranslate">
git add .
git commit -m &quot;Added package.json and test unit&quot;
git push -u origin master
</pre>
<p>Now, go back to your project in Jenkins (click <strong>Configure </strong>on the left) and replace the pipeline script with this one. Make sure you change your name and repo in line 10.</p>
<pre class="brush: bash; collapse: true; highlight: [10]; light: false; title: ; toolbar: true; notranslate">
pipeline {
    agent any

    tools {nodejs &quot;node&quot;}

    stages {

        stage('Cloning Git') {
            steps {
                git 'https://github.com/&lt;your_name&gt;/&lt;your_repo&gt;'
            }
        }

        stage('Install dependencies') {
            steps {
                sh 'npm install'
            }
        }

        stage('Test') {
            steps {
                sh 'npm test'
            }
        }
    }
}
</pre>
<p>Click on <strong>Build Now</strong> and you&#8217;ll see that all stages completed fine. But, we want to go a step further and automate the build process anytime we make a change in the source code. In order to do that we need a webhook between GitHub and our Jenkins server. Make sure your Jenkins server is publicly available, otherwise this integration won&#8217;t work.<br />
But before we do that, let&#8217;s add the script above as a file under our source code for the chat program. Make sure you change your name and repo in line 11. This file goes in the root of the chitchat directory.</p>
<pre class="brush: bash; collapse: true; highlight: [11]; light: false; title: ; toolbar: true; notranslate">
cat &lt;&lt; EOF &gt; Jenkinsfile
pipeline {
    agent any

    tools {nodejs &quot;node&quot;}

    stages {

        stage('Cloning Git') {
            steps {
                git 'https://github.com/&lt;your_name&gt;/&lt;your_repo&gt;'
            }
        }

        stage('Install dependencies') {
            steps {
                sh 'npm install'
            }
        }

        stage('Test') {
            steps {
                sh 'npm test'
            }
        }
    }
}
EOF
</pre>
<p>Add this file to Github.</p>
<pre class="brush: bash; title: ; notranslate">
git add .
git commit -m &quot;Added Jenkinsfile&quot;
git push -u origin master
</pre>
<h1>GitHub integration with Jenkins</h1>
<p>Log in to Github and from the upper right corner, click on your avatar and click on <strong>Settings</strong>. Then on the menu on the left side, all the way to the bottom click on <strong>Developer settings</strong>. Click on <strong>Personal access tokens</strong> and click on <strong>Generate new token</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/10/P138-07.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/10/P138-07.png" alt="" width="792" height="208" class="aligncenter size-full wp-image-5940" srcset="https://blog.andreev.it/wp-content/uploads/2019/10/P138-07.png 792w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-07-300x79.png 300w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-07-768x202.png 768w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-07-585x154.png 585w" sizes="(max-width: 792px) 100vw, 792px" /></a><br />
Give a name to your token (I&#8217;ll name mine Jenkins Token) and click on <strong>admin:repo_hook</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/10/P138-08.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/10/P138-08.png" alt="" width="458" height="100" class="aligncenter size-full wp-image-5942" srcset="https://blog.andreev.it/wp-content/uploads/2019/10/P138-08.png 458w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-08-300x66.png 300w" sizes="(max-width: 458px) 100vw, 458px" /></a><br />
Click on <strong>Generate token</strong> after. You&#8217;ll get a hex token, click on the clipboard icon next to it. You&#8217;ll need this token for the Jenkins configuration.<br />
Go to Jenkins and click on <strong>Manage Jenkins</strong> on the left followed by <strong>Configure System</strong> in the middle. Scroll down to the Git section, click on <strong>Add GitHub</strong> server, type <strong>GitHub </strong>for <strong>Name </strong>and next to <strong>Credentials </strong>click on <strong>Add</strong>, then <strong>Jenkins</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/10/P138-09.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/10/P138-09.png" alt="" width="1515" height="382" class="aligncenter size-full wp-image-5943" srcset="https://blog.andreev.it/wp-content/uploads/2019/10/P138-09.png 1515w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-09-300x76.png 300w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-09-768x194.png 768w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-09-1024x258.png 1024w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-09-1170x295.png 1170w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-09-585x148.png 585w" sizes="(max-width: 1515px) 100vw, 1515px" /></a><br />
When this window shows up, change the <strong>Kind </strong>to <strong>Secret text</strong>. Paste the token under <strong>Secret </strong>and enter something for <strong>ID </strong>and <strong>Description</strong>. Click <strong>Add</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/10/P138-10.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/10/P138-10.png" alt="" width="627" height="505" class="aligncenter size-full wp-image-5944" srcset="https://blog.andreev.it/wp-content/uploads/2019/10/P138-10.png 627w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-10-300x242.png 300w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-10-585x471.png 585w" sizes="(max-width: 627px) 100vw, 627px" /></a><br />
Once back, select the credential that we just created and click on <strong>Test connection</strong> on the right. You should get a message that the connection is fine. Make sure that <strong>Manage hooks</strong> is checked. Click on <strong>Save </strong>at the bottom once everything is OK.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/10/P138-11.jpg"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/10/P138-11.jpg" alt="" width="1113" height="230" class="aligncenter size-full wp-image-5948" srcset="https://blog.andreev.it/wp-content/uploads/2019/10/P138-11.jpg 1113w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-11-300x62.jpg 300w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-11-768x159.jpg 768w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-11-1024x212.jpg 1024w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-11-585x121.jpg 585w" sizes="(max-width: 1113px) 100vw, 1113px" /></a><br />
Go back to your project in Jenkins and click <strong>Configure </strong>on the left. Scroll down where the <strong>Pipeline </strong>is and change the <strong>Definition </strong>from <strong>Pipeline </strong>script to <strong>Pipeline script from SCM</strong>. SCM means Source Code Management. Change <strong>SCM </strong>to <strong>Git</strong>, enter the repo URL under <strong>Repositories </strong>. If your repo is not public, you&#8217;ll have to specify some credentials under <strong>Credentials</strong>. If your repo is public, you can leave them as <strong>none</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/10/P138-12.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/10/P138-12.png" alt="" width="1395" height="413" class="aligncenter size-full wp-image-5950" srcset="https://blog.andreev.it/wp-content/uploads/2019/10/P138-12.png 1395w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-12-300x89.png 300w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-12-768x227.png 768w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-12-1024x303.png 1024w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-12-1170x346.png 1170w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-12-585x173.png 585w" sizes="(max-width: 1395px) 100vw, 1395px" /></a><br />
Then scroll up a bit and check <strong>GitHub hook trigger for GitScm polling</strong> and then click <strong>Save</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/10/P138-13.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/10/P138-13.png" alt="" width="420" height="306" class="aligncenter size-full wp-image-5952" srcset="https://blog.andreev.it/wp-content/uploads/2019/10/P138-13.png 420w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-13-300x219.png 300w" sizes="(max-width: 420px) 100vw, 420px" /></a><br />
If you go back to GitHub, click on your project and all the way on the right click on <strong>Settings</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/10/P138-14.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/10/P138-14.png" alt="" width="812" height="114" class="aligncenter size-full wp-image-5953" srcset="https://blog.andreev.it/wp-content/uploads/2019/10/P138-14.png 812w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-14-300x42.png 300w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-14-768x108.png 768w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-14-585x82.png 585w" sizes="(max-width: 812px) 100vw, 812px" /></a><br />
Then click on <strong>Webhooks </strong>and you&#8217;ll see a green checkmark next to your Jenkins URL.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/10/P138-15.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/10/P138-15.png" alt="" width="1007" height="283" class="aligncenter size-full wp-image-5954" srcset="https://blog.andreev.it/wp-content/uploads/2019/10/P138-15.png 1007w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-15-300x84.png 300w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-15-768x216.png 768w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-15-585x164.png 585w" sizes="(max-width: 1007px) 100vw, 1007px" /></a><br />
Go to your workstation and edit the index.html file. Change the line 4 for example so instead of Chit Chat App, replace the blank with a hyphen. e.g. Chit Chat to Chit-Chat. Save it and push to GitHib.</p>
<pre class="brush: bash; title: ; notranslate">
git add .
git commit -m &quot;Minor change in index.html&quot;
git push -u origin master
</pre>
<p>If you go back to Jenkins, you&#8217;ll see that there is a build there triggered automatically. This part is <strong>CI (Continous Integration)</strong>. But, let&#8217;s go further and expand our CI with creating a Docker image.</p>
<h1>Docker image and Docker Hub</h1>
<p>In order to create a Docker image, we need a <strong>Dockerfile</strong>. I won&#8217;t explain the meaning of each line in the Dockerfile, so just copy &#038; paste the following snippet in the chitchat directory.</p>
<pre class="brush: bash; title: ; notranslate">
cat &lt;&lt; EOF &gt; Dockerfile
FROM node:10
WORKDIR /usr/src/app
COPY package*.json ./

RUN npm install
COPY . .

EXPOSE 4141
CMD &#x5B; &quot;npm&quot;, &quot;start&quot; ]
EOF
</pre>
<p>This file will tell Docker exactly what to do when creating the image. But, we also have to tell Jenkins that we need to build the image, test the image and deploy it to Docker Hub. So, your new Jenkinsfile will look like this. Change the URLs in lines 6 and 7 to suit your needs. Before you deploy the script, go to Jenkins, click on <strong>Credentials </strong>on the left, click on <strong>System </strong>below, click on <strong>Global credentials (unrestricted)</strong> in the middle and then <strong>Add Credentials</strong> on the left. Fill out the form with your Docker Hub credentials. Whatever you put as ID for these username/password pair is what you have to put in line 8 in the script.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/11/P138-21.jpg"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/11/P138-21.jpg" alt="" width="524" height="369" class="aligncenter size-full wp-image-5999" srcset="https://blog.andreev.it/wp-content/uploads/2019/11/P138-21.jpg 524w, https://blog.andreev.it/wp-content/uploads/2019/11/P138-21-300x211.jpg 300w" sizes="(max-width: 524px) 100vw, 524px" /></a><br />
And, now copy &#038; paste the script in the chitchat directory. If everything goes well, you&#8217;ll see your image deployed in Docker Hub and tagged as whatever your latest build was and the tag latest too.</p>
<pre class="brush: bash; highlight: [6,7]; title: ; notranslate">
cat &lt;&lt; 'EOF' &gt; Jenkinsfile
pipeline {

    environment {
        dockerregistry = 'https://registry.hub.docker.com'
        dockerhuburl = &quot;klimenta/chitchat&quot;
        githuburl = &quot;klimenta/chitchat&quot;
        dockerhubcrd = 'dockerhub'
    }

    agent any

    tools {nodejs &quot;node&quot;}

    stages {

        stage('Clone git repo') {
            steps {
                git 'https://github.com/' + githuburl
            }
        }

        stage('Install Node.js dependencies') {
            steps {
                sh 'npm install'
            }
        }

        stage('Test App') {
            steps {
                sh 'npm test'
            }
        }

        stage('Build image') {
          steps{
            script {
              dockerImage = docker.build(dockerhuburl + &quot;:$BUILD_NUMBER&quot;)
            }
          }
        }

        stage('Test image') {
            steps {
                sh 'docker run -i ' + dockerhuburl + ':$BUILD_NUMBER npm test'
            }
        }

        stage('Deploy image') {
          steps{
            script {
              docker.withRegistry(dockerregistry, dockerhubcrd ) {
                dockerImage.push(&quot;${env.BUILD_NUMBER}&quot;)
                dockerImage.push(&quot;latest&quot;)
              }
            }
          }
        }

        stage('Remove image') {
          steps{
            sh &quot;docker rmi $dockerhuburl:$BUILD_NUMBER&quot;
          }
        }
    }
}
EOF
</pre>
<p>Of course, you&#8217;ll have to tell GitHub that there is a change is a file.</p>
<pre class="brush: bash; title: ; notranslate">
git add .
git commit -m &quot;Added Jenkinsfile for Docker&quot;
git push -u origin master
</pre>
<h1>Deployment to Kubernetes using Jenkins</h1>
<p>Now that we are finished with the CI (Continuous Integration), it&#8217;s time for the CD (Continuous Deployment). We&#8217;ll use the existing Docker image that we put on Docker Hub. First thing first, we&#8217;ll have to install a plugin for a Kubernetes integration. Go to <strong>Manage Jenkins</strong> on the left, then click on <strong>Manage plugins</strong> in the center and click on <strong>Available </strong>tab. Install the <strong>Kubernetes Continuous Deploy</strong> plugin.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/11/P138-22.jpg"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/11/P138-22.jpg" alt="" width="510" height="71" class="aligncenter size-full wp-image-6003" srcset="https://blog.andreev.it/wp-content/uploads/2019/11/P138-22.jpg 510w, https://blog.andreev.it/wp-content/uploads/2019/11/P138-22-300x42.jpg 300w" sizes="(max-width: 510px) 100vw, 510px" /></a><br />
Once installed and Jenkins is restarted, click on <strong>Credentials </strong>on the left, then <strong>System </strong>just below and then click on <strong>Global credentials (unrestricted)</strong> in the middle. Click on <strong>Add Credentials</strong> on the left. When this window pops up, change the <strong>Kind </strong>to <strong>Kubernetes configuration (kubeconfig)</strong>, enter an <strong>ID</strong>, in my case it&#8217;s k8s. Remember the <strong>ID</strong>, you&#8217;ll reference it in the <strong>Jenkinsfile </strong>later, type a description and select <strong>Enter directly</strong>. Before you click OK, we&#8217;ll have to go to our Kubernetes cluster and log to it.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/11/P138-23.jpg"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/11/P138-23.jpg" alt="" width="627" height="509" class="aligncenter size-full wp-image-6004" srcset="https://blog.andreev.it/wp-content/uploads/2019/11/P138-23.jpg 627w, https://blog.andreev.it/wp-content/uploads/2019/11/P138-23-300x244.jpg 300w, https://blog.andreev.it/wp-content/uploads/2019/11/P138-23-585x475.jpg 585w" sizes="(max-width: 627px) 100vw, 627px" /></a><br />
Once you are logged to your master node, log as the user that you use to manage the k8s cluster and run this command.</p>
<pre class="brush: bash; title: ; notranslate">
cat .kube/config
</pre>
<p>You&#8217;ll get a long list of text. Paste this back in the Jenkins window under <strong>Content</strong> and click OK. This is how Jenkins will communicate with your Kubernetes cluster. We also need to tell Kubernetes how we want our app deployed. As you know this is done with a YAML file. So, we have to add that YAML file under the chitchat directory.Change the IP in line 25 to match your master k8s node IP. I am deploying a load balancer that will listen on port 8080 and 2 pods in a deployment.</p>
<pre class="brush: xml; title: ; notranslate">
cat &lt;&lt; 'EOF' &gt; k8s.yaml
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: chitchat
spec:
  replicas: 2
  template:
    metadata:
      name: chitchat
      labels:
        app: chitchat
    spec:
      containers:
      - image: $dockerhuburl:$BUILD_NUMBER
        name: nodejs
---
apiVersion: v1
kind: Service
metadata:
  name: loadbalancer
spec:
  type: LoadBalancer
  externalIPs:
  - 192.168.1.7
  selector:
    app: chitchat
  ports:
  - port: 8080
    targetPort: 4141
EOF
</pre>
<p>And we also want to tell Jenkins that we are adding an extra step to deploy it to the cluster, so the final Jenkins file looks like this.</p>
<pre class="brush: bash; title: ; notranslate">
cat &lt;&lt; 'EOF' &gt; Jenkinsfile
pipeline {

  environment {
    dockerregistry = 'https://registry.hub.docker.com'
    dockerhuburl = 'klimenta/chitchat'
    githuburl = 'klimenta/chitchat'
    dockerhubcrd = 'dockerhub'
    dockerImage = ''
  }

  agent any

  tools {nodejs &quot;node&quot;}

  stages {

    stage('Clone git repo') {
      steps {
         git 'https://github.com/' + githuburl
      }
    }

    stage('Install Node.js dependencies') {
      steps {
        sh 'npm install'
      }
    }

    stage('Test App') {
        steps {
            sh 'npm test'
        }
    }

    stage('Build image') {
      steps{
        script {
          dockerImage = docker.build(dockerhuburl + &quot;:$BUILD_NUMBER&quot;)
        }
      }
    }

    stage('Test image') {
      steps {
        sh 'docker run -i ' + dockerhuburl + ':$BUILD_NUMBER npm test'
      }
    }

    stage('Deploy image') {
      steps{
        script {
          docker.withRegistry(dockerregistry, dockerhubcrd ) {
            dockerImage.push(&quot;${env.BUILD_NUMBER}&quot;)
            dockerImage.push(&quot;latest&quot;)
          }
        }
      }
    }

    stage('Remove image') {
      steps{
        sh &quot;docker rmi $dockerhuburl:$BUILD_NUMBER&quot;
      }
    }

    stage('Deploy k8s') {
      steps {
        kubernetesDeploy(
          kubeconfigId: 'k8s',
          configs: 'k8s.yaml',
          enableConfigSubstitution: true
        )
      }
    }
  }
}
EOF
</pre>
<p>Trigger the change.</p>
<pre class="brush: bash; title: ; notranslate">
git add .
git commit -m &quot;Added Jenkinsfile for kubernetes and yaml&quot;
git push -u origin master
</pre>
<p>After a minute or so, you&#8217;ll see your pods running and if you go to your master node on port 8080 you&#8217;ll see the app happily running.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/11/P138-24.jpg"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/11/P138-24.jpg" alt="" width="424" height="344" class="aligncenter size-full wp-image-6025" srcset="https://blog.andreev.it/wp-content/uploads/2019/11/P138-24.jpg 424w, https://blog.andreev.it/wp-content/uploads/2019/11/P138-24-300x243.jpg 300w" sizes="(max-width: 424px) 100vw, 424px" /></a></p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.andreev.it/2019/11/devops-ci-cd-using-git-jenkins-docker-kubernetes/feed/</wfw:commentRss>
			<slash:comments>4</slash:comments>
		
		
			</item>
		<item>
		<title>Visual C#: GUI front-end for OpenSSH in Windows</title>
		<link>https://blog.andreev.it/2019/03/149-visual-c-gui-front-end-for-openssh-in-windows/</link>
					<comments>https://blog.andreev.it/2019/03/149-visual-c-gui-front-end-for-openssh-in-windows/#respond</comments>
		
		<dc:creator><![CDATA[Kliment Andreev]]></dc:creator>
		<pubDate>Mon, 04 Mar 2019 19:48:10 +0000</pubDate>
				<category><![CDATA[C#]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[front-end]]></category>
		<category><![CDATA[OpenSSH]]></category>
		<category><![CDATA[Visual C#]]></category>
		<guid isPermaLink="false">https://blog.andreev.it/?p=4573</guid>

					<description><![CDATA[As of late 2018, Windows 10 and Windows 2019 come up with OpenSSH installed.&#8230;]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><p>As of late 2018, Windows 10 and Windows 2019 come up with OpenSSH installed. I had to use OpenSSH in some of the scripts at work, but managing the connections became difficult. So, I created a small .NET program that manages SSH connections. It&#8217;s a very simple SSH GUI front-end for OpenSSH. It&#8217;s not meant to be a replacement for putty or any other SSH terminal programs.<br />
In case you get an error that your PEM certificate is wide open, change the permissions on that file. Do this from a command prompt, not from a Power Shell prompt.</p>
<pre class="brush: bash; title: ; notranslate">
set key=&quot;c:\folder\mykey.pem&quot;
cmd /c icacls %key% /c /t /inheritance:d
cmd /c icacls %key% /c /t /grant %username%:F
cmd /c icacls %key%  /c /t /remove Administrator BUILTIN\Administrators BUILTIN Everyone System Users &quot;Authenticated Users&quot;
cmd /c icacls %key%
</pre>
<p>Here is the <a href="https://github.com/klimenta/pusshy" rel="noopener noreferrer" target="_blank">link</a> to the GitHub repo.<br />
Here is a screenshot.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/03/P126-01.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/03/P126-01.png" alt="" width="699" height="160" class="aligncenter size-full wp-image-8461" srcset="https://blog.andreev.it/wp-content/uploads/2019/03/P126-01.png 699w, https://blog.andreev.it/wp-content/uploads/2019/03/P126-01-300x69.png 300w, https://blog.andreev.it/wp-content/uploads/2019/03/P126-01-585x134.png 585w" sizes="(max-width: 699px) 100vw, 699px" /></a><br />
Here is the C# source. Click + to expand. </p>
<pre class="brush: csharp; collapse: true; light: false; title: ; toolbar: true; notranslate">
using System;
using System.IO;
using System.Windows.Forms;
using System.Diagnostics;

namespace pusshy
{
    public partial class frmpusshy : Form
    {
        //It holds the name of the header column when a cell is right-clicked
        //Used for hiding columns
        string strColumnHeader; 
        //When a user hits enter, the next selected cell is on the right, not the bottom one
        //It allows horizontal scrolling when typing, not vertical
        protected override bool ProcessCmdKey(ref System.Windows.Forms.Message msg, System.Windows.Forms.Keys keyData)
        {
            int intColumn = gridHosts.CurrentCell.ColumnIndex;
            int intRow = gridHosts.CurrentCell.RowIndex;
            if (keyData == Keys.Enter)
            {
                if (intColumn == gridHosts.Columns.Count - 1)
                {
                    gridHosts.Rows.Add();
                    gridHosts.CurrentCell = gridHosts&#x5B;0, intRow + 1];
                }
                else
                {
                    gridHosts.CurrentCell = gridHosts&#x5B;intColumn + 1, intRow];
                }
                return true;
            }
            else
                return base.ProcessCmdKey(ref msg, keyData);
        }

        public frmpusshy()
        {
            InitializeComponent();
        }

        private void frmpusshy_FormClosed(object sender, FormClosedEventArgs e)
        {
            //Save the current size and position of the window
            //Used to remember these settings when the program is started next time
            Properties.Settings.Default&#x5B;&quot;Location&quot;] = this.Location;
            Properties.Settings.Default&#x5B;&quot;Size&quot;] = this.Size;
            Properties.Settings.Default.Save();
            //The local app data folder where settings and the data will be saved
            var strLocalAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
            //The data is in pusshy.csv file under C:\Users\&lt;username&gt;\AppData\Local\pusshy
            var strFileName = strLocalAppData + &quot;/pusshy/&quot; + &quot;pusshy.csv&quot;;
            string strCellValue;
            //Open the file for saving
            StreamWriter csvFileWriter = new StreamWriter(strFileName, false);
            int intColumnCount = gridHosts.ColumnCount - 1;
            int intColCount = gridHosts.Columns.Count;
            //Iterate through the grid and save
            foreach (DataGridViewRow dataRowObject in gridHosts.Rows)
            {
                if (!dataRowObject.IsNewRow)
                {
                    string strDataFromGrid = &quot;&quot;;                    
                    for (int i = 0; i &lt;= intColumnCount; i++)
                    {
                        try
                        {
                            strCellValue = dataRowObject.Cells&#x5B;i].Value.ToString();
                        }
                        //If the cell has no value, replace it with blank string instead of null
                        catch (System.NullReferenceException exception)
                        {
                            strCellValue = &quot;&quot;;
                        }
                        //Create a comma separated line
                        if (i != 0)
                        {
                            strDataFromGrid = strDataFromGrid + ',' + strCellValue;
                        }
                        else
                        {
                            strDataFromGrid = strCellValue;
                        }
                    }
                    csvFileWriter.WriteLine(strDataFromGrid);
                }
            }
            csvFileWriter.Flush();
            csvFileWriter.Close();
        }
        //Tag the whole row if there is a password in the password column (column=2). Used to hide password cells
        private void gridHosts_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e)
        {            
            if (gridHosts.CurrentRow.Tag != null &amp;&amp; gridHosts.CurrentCell.ColumnIndex == 2)
            {
                e.Control.Text = gridHosts.CurrentRow.Tag.ToString();
            }
        }
        //Replace the text in the password cell (column=2) with dots
        private void gridHosts_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e)
        {
            if (e.ColumnIndex == 2 &amp;&amp; e.Value != null)
            {                
                gridHosts.Rows&#x5B;e.RowIndex].Tag = e.Value;
                e.Value = new String('\u25CF', e.Value.ToString().Length);
            }
        }
        //Right-click on row header or column header
        private void gridHosts_MouseDown(object sender, MouseEventArgs e)
        {
            if (e.Button == MouseButtons.Right)
            {
                var hit = gridHosts.HitTest(e.X, e.Y);
                var intCurrentColumnIndex = hit.ColumnIndex;
                //If the user right-clicks the column header display this menu
                if ((hit.Type == DataGridViewHitTestType.ColumnHeader))
                {
                    strColumnHeader = gridHosts.Columns&#x5B;intCurrentColumnIndex].HeaderText;
                    menuRightColumnClick.Show(MousePosition);
                }
                //If the user right-clicks the column header display this menu
                if ((hit.Type == DataGridViewHitTestType.RowHeader))
                {
                    menuRightRowClick.Show(MousePosition);
                }
            }
        }
        //Hide the column that was selected
        private void hideToolStripMenuItem_Click(object sender, EventArgs e)
        {
            gridHosts.Columns&#x5B;strColumnHeader].Visible = false;
        }       
        //Delete a row
        private void deleteToolStripMenuItem_Click(object sender, EventArgs e)
        {
            foreach (DataGridViewRow row in gridHosts.SelectedRows)
            {
                try
                {
                    gridHosts.Rows.Remove(row);
                }
                //Can't delete the bottom row. Catch the exception
                catch (InvalidOperationException exception)
                {
                    return;
                }
            }
        }

        private void frmpusshy_Load(object sender, EventArgs e)
        {
            //The local app data folder where settings and the data will be saved
            var strLocalAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
            //The data is in pusshy.csv file under C:\Users\&lt;username&gt;\AppData\Local\pusshy
            var strFileName = strLocalAppData + &quot;/pusshy/&quot; + &quot;pusshy.csv&quot;;
            if (!File.Exists(strFileName))
            {
                return;
            }   
            //Clear the grid and load the pusshy.csv file
            gridHosts.Rows.Clear();
            StreamReader fileReader = new StreamReader(strFileName, false);
            int i = 0;
            while (fileReader.Peek() != -1)
            {
                var fileRow = fileReader.ReadLine();
                var fileDataField = fileRow.Split(',');
                gridHosts.Rows.Add(fileDataField);
                //var rd = fileDataField&#x5B;2];                
                i++;
            }
            fileReader.Dispose();
            fileReader.Close();
            //Change the size and the position of the application on load
            //With the settings that were saved last time the app was used
            this.Location = Properties.Settings.Default.Location;
            this.Size = Properties.Settings.Default.Size;
        }
        //When the user chooses Show All, all hidden columns will become visible
        private void showAllToolStripMenuItem_Click(object sender, EventArgs e)
        {            
            foreach (DataGridViewColumn column in gridHosts.Columns)
            {
                string strHeaderCaptionText = column.HeaderText;
                gridHosts.Columns&#x5B;strHeaderCaptionText].Visible = true;
            }                    
        }
        //When the user double-clicks a cell, launch the terminal
        private void gridHosts_CellDoubleClick(object sender, DataGridViewCellEventArgs e)
        {
            string strCommand = &quot;&quot;;
            int intRowIndex = gridHosts.CurrentCell.RowIndex;  
            //Get all values from the row
            string strHost = gridHosts.Rows&#x5B;intRowIndex].Cells&#x5B;0].Value.ToString();
            string strLogin = gridHosts.Rows&#x5B;intRowIndex].Cells&#x5B;1].Value.ToString();
            string strPwd = gridHosts.Rows&#x5B;intRowIndex].Cells&#x5B;2].Value.ToString();
            string strCert = gridHosts.Rows&#x5B;intRowIndex].Cells&#x5B;3].Value.ToString();
            //If the password cell is empty, start the terminal with certificate
            if (strPwd == &quot;&quot;)
            {
                strCommand = &quot;\&quot;C:/windows/sysnative/OpenSSH/ssh.exe\&quot; -l &quot; + strLogin + &quot; &quot; + &quot;-i &quot; + strCert + &quot; &quot; + strHost;
            } else
            //Or without password
            {
                strCommand = &quot;\&quot;C:/windows/sysnative/OpenSSH/ssh.exe\&quot; -l &quot; + strLogin + &quot; &quot; + strHost;
            }
            Process.Start(&quot;powershell.exe&quot;, strCommand);            
        }
    }
}
</pre>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.andreev.it/2019/03/149-visual-c-gui-front-end-for-openssh-in-windows/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>AWS: Snapshot Clean Up script in Lambda</title>
		<link>https://blog.andreev.it/2019/02/148-aws-lambda-snapshot-clean-up-script/</link>
					<comments>https://blog.andreev.it/2019/02/148-aws-lambda-snapshot-clean-up-script/#respond</comments>
		
		<dc:creator><![CDATA[Kliment Andreev]]></dc:creator>
		<pubDate>Wed, 13 Feb 2019 20:02:05 +0000</pubDate>
				<category><![CDATA[AWS]]></category>
		<category><![CDATA[Cloud]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[AMI]]></category>
		<category><![CDATA[Lambda]]></category>
		<category><![CDATA[Snapshot]]></category>
		<guid isPermaLink="false">https://blog.andreev.it/?p=4467</guid>

					<description><![CDATA[I have several scripts that I use to do some maintenance on my servers&#8230;]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><p>I have several scripts that I use to do some maintenance on my servers (snapshots, backups, cleanups etc) and they are all scattered on different servers. I&#8217;ve decided to consolidate them and use AWS Lambda to do the scheduling and execution for me. So, in this post I&#8217;ll explain how to create simple snapshot clean up script that deletes some old AWS snapshots and runs once a day. In addition, I&#8217;ll do the same Lambda script, but this time using the <a href="https://serverless.com" target="_blank" rel="noopener noreferrer">serverless</a> framework.</p>
<div style="border: 1px solid red; padding: 16px;">
<p style="text-align: center;"><strong><span style="color: #800000;">NOTE ABOUT SNAPSHOTS</span> </strong><br />
<i>The Lambda function deletes snapshots only. If the snapshot is associated with an AMI, you&#8217;ll get a message that the snapshot is in use.</i></p>
</div>
<p>The setup below requires Full Access to AWS.</p>
<h1>SNS</h1>
<p>First, let&#8217;s configure SNS that we&#8217;ll use to send us the notifications from the script. Go to <strong>SNS </strong>and click on <strong>Create topic</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/02/P125-01.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/02/P125-01.png" alt="" width="411" height="315" class="aligncenter size-full wp-image-8445" srcset="https://blog.andreev.it/wp-content/uploads/2019/02/P125-01.png 411w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-01-300x230.png 300w" sizes="(max-width: 411px) 100vw, 411px" /></a><br />
Name your topic and click on <strong>Create topic</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/02/P125-02.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/02/P125-02.png" alt="" width="419" height="313" class="aligncenter size-full wp-image-8446" srcset="https://blog.andreev.it/wp-content/uploads/2019/02/P125-02.png 419w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-02-300x224.png 300w" sizes="(max-width: 419px) 100vw, 419px" /></a><br />
Once there, click on <strong>Create subscription</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/02/P125-03.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/02/P125-03.png" alt="" width="276" height="208" class="aligncenter size-full wp-image-8447" /></a><br />
In my case, I want to receive an e-mail. Click on <strong>Create subscription</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/02/P125-04.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/02/P125-04.png" alt="" width="865" height="339" class="aligncenter size-full wp-image-8448" srcset="https://blog.andreev.it/wp-content/uploads/2019/02/P125-04.png 865w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-04-300x118.png 300w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-04-768x301.png 768w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-04-585x229.png 585w" sizes="(max-width: 865px) 100vw, 865px" /></a><br />
If you check your e-mail, you&#8217;ll see that you have to confirm the subscription. Click on <strong>Confirm subscription</strong> in the e-mail and you are done with this part.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/02/P125-05.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/02/P125-05.png" alt="" width="490" height="324" class="aligncenter size-full wp-image-8449" srcset="https://blog.andreev.it/wp-content/uploads/2019/02/P125-05.png 490w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-05-300x198.png 300w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-05-263x175.png 263w" sizes="(max-width: 490px) 100vw, 490px" /></a></p>
<h1>Lambda function</h1>
<p>We are ready to deploy the script now. Go to <strong>Lambda </strong>and click on <strong>Create a function</strong>. Fill out the form so it looks like this.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/02/P125-06.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/02/P125-06.png" alt="" width="1123" height="678" class="aligncenter size-full wp-image-8450" srcset="https://blog.andreev.it/wp-content/uploads/2019/02/P125-06.png 1123w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-06-300x181.png 300w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-06-1024x618.png 1024w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-06-768x464.png 768w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-06-585x353.png 585w" sizes="(max-width: 1123px) 100vw, 1123px" /></a><br />
Once you select <strong>Create a custom role</strong> a new window will pop up.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/02/P125-07.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/02/P125-07.png" alt="" width="509" height="294" class="aligncenter size-full wp-image-8451" srcset="https://blog.andreev.it/wp-content/uploads/2019/02/P125-07.png 509w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-07-300x173.png 300w" sizes="(max-width: 509px) 100vw, 509px" /></a><br />
Click <strong>Allow </strong>and the window will close. You&#8217;ll be back to the initial screen. Click <strong>Create function</strong> here.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/02/P125-08.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/02/P125-08.png" alt="" width="1119" height="303" class="aligncenter size-full wp-image-8452" srcset="https://blog.andreev.it/wp-content/uploads/2019/02/P125-08.png 1119w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-08-300x81.png 300w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-08-1024x277.png 1024w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-08-768x208.png 768w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-08-585x158.png 585w" sizes="(max-width: 1119px) 100vw, 1119px" /></a><br />
If you scroll down a bit, you&#8217;ll see a placeholder for a Python code. I already have my function ready, so I&#8217;ll just delete what&#8217;s in there and paste my script. Make sure you change the values in line numbers 8 and 10. Line number 8 is your AWS account and line number 10 is the amount of days that you want your snapshots to be kept. Also, if you have any snapshots that you want to keep no matter how many days they are old, you can specify their IDs in line 6. They&#8217;ll be skipped. Anything older than <em>diff_time</em> days will be deleted. Also, if your SNS topic was named differently, change the line number 53.</p>
<pre class="brush: python; title: ; notranslate">
import json, boto3, datetime

def lambda_handler(event, context):

    # Do not delete these snaps
    snap_ignore = &#x5B;'snap-12718f7046cc2add6','snap-0e738d4c47b2d6e14']    
    # Enter the AWS account number here
    accountId = '123456789012'
    # Delete snapshots older than diff_time days   
    diff_time = 30
    # Constant string
    no_snap = 'No snaps were found for clean up.'  
    # Constant string
    skip_str = 'was skipped because it is in use.'
    # Leave it empty
    mail_body = ''

    
    ec2 = boto3.resource('ec2')
    sns = boto3.client('sns')

    # Get all the snaps from the account
    snaps = ec2.snapshots.filter(OwnerIds=&#x5B;accountId])
    # Get the current time
    now_time = datetime.datetime.now().date()
    # Iterate through the snaps
    for snap in snaps:
        # Check for protected snaps, e.g the ones that we have to skip and ignore
        if (snap.id in snap_ignore): 
            continue
        # Get the creation time of the snap
        snap_time = snap.start_time.date()
        # Get the current time
        now_time = datetime.datetime.now().date()
        # Calculate the difference
        snapage_time = (now_time - snap_time).days
        # If the creation time is older than the required difference
        if(snapage_time &gt; diff_time):
            try:
                # Delete the snap
                snap.delete(snap.id)
                # Print the snap that will be deleted
                print(snap.id,'created on',snap_time,'was deleted')
                mail_body = mail_body + snap.id + ' created on ' + str(snap_time) + ' was deleted.\n'
            # Catch an exception if the snap is in use
            except:
                print(snap.id, skip_str)
                mail_body = mail_body + snap.id + ' ' + skip_str + '\n'
                continue
    if (mail_body == ''):
        mail_body = no_snap
    response = sns.publish(
        TopicArn = 'arn:aws:sns:us-east-1:' + accountId + ':topSnapCleanUp',
        Message = mail_body,
        Subject = 'Snapshot Clean up ' + str(now_time)
    )
</pre>
<p>Click <strong>Save </strong>in the upper right corner and click <strong>Test</strong>. Enter something for the Event name and click <strong>Create</strong>. Click <strong>Test </strong>again. You&#8217;ll get an error at the top of the screen.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/02/P125-09.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/02/P125-09.png" alt="" width="1083" height="423" class="aligncenter size-full wp-image-8453" srcset="https://blog.andreev.it/wp-content/uploads/2019/02/P125-09.png 1083w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-09-300x117.png 300w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-09-1024x400.png 1024w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-09-768x300.png 768w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-09-585x228.png 585w" sizes="(max-width: 1083px) 100vw, 1083px" /></a><br />
The error means that we don&#8217;t have rights. So, we have to add a policy to our role that we created earlier.</p>
<h1>IAM Roles and Policies</h1>
<p>If you go to the IAM service and search for the role that you&#8217;ve created, you&#8217;ll see that it&#8217;s there and that there is a simple Lambda policy attached.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/02/P125-10.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/02/P125-10.png" alt="" width="696" height="522" class="aligncenter size-full wp-image-8454" srcset="https://blog.andreev.it/wp-content/uploads/2019/02/P125-10.png 696w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-10-300x225.png 300w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-10-585x439.png 585w" sizes="(max-width: 696px) 100vw, 696px" /></a><br />
If you look at the policy, you&#8217;ll see that this policy gives access to CloudWatch logs. What we need in the policy is access to snapshots and SNS. Click on <strong>Edit policy</strong> and copy and paste this one. Replace your account number in line 16.</p>
<pre class="brush: bash; title: ; notranslate">
{
    &quot;Version&quot;: &quot;2012-10-17&quot;,
    &quot;Statement&quot;: &#x5B;
        {
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Action&quot;: &#x5B;
                &quot;ec2:DeleteSnapshot&quot;,
                &quot;ec2:DescribeVolumes&quot;,
                &quot;ec2:DescribeSnapshots&quot;
            ],
            &quot;Resource&quot;: &quot;*&quot;
        },
        {
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Action&quot;: &quot;sns:Publish&quot;,
            &quot;Resource&quot;: &quot;arn:aws:sns:us-east-1:123456789012:topSnapCleanUp&quot;
        }
    ]
}
</pre>
<p>If you save the policy and run the Lambda function, you&#8217;ll see that everything works as expected. But, we want to go a step further and automate the execution of the script.</p>
<h1>CloudWatch</h1>
<p>First, we need to decide when we want this script to start. In my case, I want the script to run at 1AM in the morning. Go to <strong>CloudWatch</strong>, click on <strong>Rules </strong>on the left side and click on <strong>Create rule</strong> button.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/02/P125-11.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/02/P125-11.png" alt="" width="425" height="321" class="aligncenter size-full wp-image-8455" srcset="https://blog.andreev.it/wp-content/uploads/2019/02/P125-11.png 425w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-11-300x227.png 300w" sizes="(max-width: 425px) 100vw, 425px" /></a><br />
Click on <strong>Cron expression</strong> and enter <em>0 1 * * ? *</em>. If you don&#8217;t know what this means, it&#8217;s a cron syntax. Google it and you&#8217;ll figure it out. It&#8217;s easy. On the right side, under <strong>Targets</strong>, choose our Lambda function from the drop-down. Then click <strong>Configure details</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/02/P125-12.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/02/P125-12.png" alt="" width="1060" height="653" class="aligncenter size-full wp-image-8456" srcset="https://blog.andreev.it/wp-content/uploads/2019/02/P125-12.png 1060w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-12-300x185.png 300w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-12-1024x631.png 1024w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-12-768x473.png 768w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-12-585x360.png 585w" sizes="(max-width: 1060px) 100vw, 1060px" /></a><br />
Under Step 2, enter the rule name, description and click on <strong>Create rule</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/02/P125-13.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/02/P125-13.png" alt="" width="1058" height="361" class="aligncenter size-full wp-image-8457" srcset="https://blog.andreev.it/wp-content/uploads/2019/02/P125-13.png 1058w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-13-300x102.png 300w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-13-1024x349.png 1024w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-13-768x262.png 768w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-13-585x200.png 585w" sizes="(max-width: 1058px) 100vw, 1058px" /></a><br />
In case your function times out because you have way too many snapshots, you can increase the time-out settings for the function from the main Lambda screen.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/02/P125-14.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/02/P125-14.png" alt="" width="417" height="347" class="aligncenter size-full wp-image-8458" srcset="https://blog.andreev.it/wp-content/uploads/2019/02/P125-14.png 417w, https://blog.andreev.it/wp-content/uploads/2019/02/P125-14-300x250.png 300w" sizes="(max-width: 417px) 100vw, 417px" /></a><br />
You are all set.</p>
<h1>Serverless</h1>
<p>You can deploy the same solution without even logging to the console. All you need is the <a href="https://serverless.com/" target="_blank" rel="noopener noreferrer">serverless </a>framework. I won&#8217;t explain how to install and configure serverless, so once you are done, let&#8217;s create our first serverless project.</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
sls create -t aws-python3 -p SnapCleanUp
Serverless: Generating boilerplate...
Serverless: Generating boilerplate in &quot;/home/klimenta/SnapCleanUp&quot;
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.37.1
 -------'

Serverless: Successfully generated boilerplate for template: &quot;aws-python3&quot;
</pre>
<p>Serverless will generate the skeleton code and YAML config file for you. If you go to that directory, you&#8217;ll see two files: <strong>handler.py</strong> which is our Python function and <strong>serverless.yml</strong> which is the AWS config written in YAML. You can take a look at the generic files to see what&#8217;s going on. In our case we&#8217;ll replace these two files with ours. So, here is the <strong>handler.py</strong> file. As you can see it&#8217;s pretty much the same as the Python file that we used initially.</p>
<pre class="brush: python; title: ; notranslate">
import json, boto3, datetime

def snapcleanup(event, context):

    # Do not delete these snaps
    snap_ignore = &#x5B;'snap-12718f7046cc2add6','snap-0e738d4c47b2d6e14']
    # Enter the AWS account number here
    accountId = '123456789012'
    # Delete snapshots older than diff_time days
    diff_time = 30
    # Constant string
    no_snap = 'No snaps were found for clean up.'
    # Constant string
    skip_str = 'was skipped because it is in use.'
    # Leave it empty
    mail_body = ''

    ec2 = boto3.resource('ec2')
    sns = boto3.client('sns')

    # Get all the snaps from the account
    snaps = ec2.snapshots.filter(OwnerIds=&#x5B;accountId])
    # Get the current time
    now_time = datetime.datetime.now().date()
    # Iterate through the snaps
    for snap in snaps:
        # Check for protected snaps, e.g the ones that we have to skip and ignore
        if (snap.id in snap_ignore):
            continue
        # Get the creation time of the snap
        snap_time = snap.start_time.date()
        # Get the current time
        now_time = datetime.datetime.now().date()
        # Calculate the difference
        snapage_time = (now_time - snap_time).days
        # If the creation time is older than the required difference
        if(snapage_time &gt; diff_time):
            try:
                # Delete the snap
                snap.delete(snap.id)
                # Print the snap that will be deleted
                print(snap.id,'created on',snap_time,'was deleted')
                mail_body = mail_body + snap.id + ' created on ' + str(snap_time) + ' was deleted.\n'
            # Catch an exception if the snap is in use
            except:
                print(snap.id, skip_str)
                mail_body = mail_body + snap.id + ' ' + skip_str + '\n'
                continue
    if (mail_body == ''):
        mail_body = no_snap
    response = sns.publish(
        TopicArn = 'arn:aws:sns:us-east-1:' + accountId + ':topSnapCleanUp',
        Message = mail_body,
        Subject = 'Snapshot Clean up ' + str(now_time)
    )
</pre>
<p>And here is the YAML file that does the heavy lifting of creating and gluing everything for you.</p>
<pre class="brush: xml; title: ; notranslate">
service: SnapCleanUp

provider:
  name: aws
  runtime: python3.7
  timeout: 10
  stage: prod
  region: us-east-1
  iamRoleStatements:
    - Effect: &quot;Allow&quot;
      Action:
      - ec2:DeleteSnapshot
      - ec2:DescribeVolumes
      - ec2:DescribeSnapshots
      Resource: &quot;*&quot;
    - Effect: Allow
      Action: sns:Publish
      Resource: {&quot;Ref&quot; : &quot;SNSTopic&quot;}

functions:
  snapcleanup:
    handler: handler.snapcleanup
    description: Deletes snaps older than XX days
    events:
      - schedule: cron(* 1 * * ? *)
        enabled: true
resources:
  Resources:
    SNSTopic:
      Type: AWS::SNS::Topic
      Properties:
        DisplayName: 'Snap Clean'
        TopicName: topSnapCleanUp

    SNSSubscription:
      Type: AWS::SNS::Subscription
      Properties:
        Endpoint: klimenta@iandreev.com
        Protocol: email
        TopicArn: {&quot;Ref&quot; : &quot;SNSTopic&quot;}
</pre>
<p>Here is a brief description. In the <strong>provider </strong>section you specify that you&#8217;ll use Python 3, the timeout for the execution will be 10 seconds and the region is us-east-1. We also have the role defined here. If you look the initial JSON script for the role, well, this is the same, except it&#8217;s in YAML. You can use any online converter to translate JSON to YAML. The <strong>function </strong>section is where I specify the name of the function and I also create the CloudWatch rule so the script runs at 1AM every day. Finally in the <strong>resources </strong>section I create the SNS topic and the subscription. For more information about the syntax, see the docs on the serverless site.<br />
Anyway, we can deploy the function now.</p>
<pre class="brush: bash; title: ; notranslate">
sls deploy -v
</pre>
<p>It takes about a minute for all resources to be completed. Once deployed, check your e-mail and look for the confirmation e-mail. You have to confirm your subscription. And pretty much that&#8217;s it. If you take a look at the console (Lambda, IAM roles, CLoudWatch rules and logs), you&#8217;ll see that pretty much every AWS resource looks the same as our first manual try.</p>
<h1>Helpful tips</h1>
<p>If you have to make a change in the function (handler.py), you don&#8217;t have to redeploy the whole solution. You can just deploy the function itself.</p>
<pre class="brush: bash; title: ; notranslate">
sls deploy function -f snapcleanup
</pre>
<p>Also, we have a scheduled that runs the function at a specified interval, but you can run the script manually.</p>
<pre class="brush: bash; title: ; notranslate">
sls invoke -f snapcleanup -l
</pre>
<p>Check the logs of the function in real-time.</p>
<pre class="brush: bash; title: ; notranslate">
sls logs -f snapcleanup -t
</pre>
<p>If you want to remove all resources and delete the function, use the remove parameter.</p>
<pre class="brush: bash; title: ; notranslate">
sls remove
</pre>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.andreev.it/2019/02/148-aws-lambda-snapshot-clean-up-script/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Visual C#: CmdDepot &#8211; small clipboard utility</title>
		<link>https://blog.andreev.it/2019/01/144-visual-c-cmddepot-small-clipboard-utility/</link>
					<comments>https://blog.andreev.it/2019/01/144-visual-c-cmddepot-small-clipboard-utility/#respond</comments>
		
		<dc:creator><![CDATA[Kliment Andreev]]></dc:creator>
		<pubDate>Sat, 26 Jan 2019 13:24:17 +0000</pubDate>
				<category><![CDATA[C#]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[Visual C#]]></category>
		<guid isPermaLink="false">https://blog.andreev.it/?p=4354</guid>

					<description><![CDATA[While I was playing with docker, I needed some 10-15 commands executed repeatedly. At&#8230;]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><p>While I was playing with docker, I needed some 10-15 commands executed repeatedly. At first, I was using the cursor up to search through the history, but at point there were so menu of them that I got lost. In bash, you can also use the command history and then execute the command from it if you know the line number by putting ! as a prefix, e.g. !20, but that didn&#8217;t work too. So, I&#8217;ve opened a notepad on a side and pasted my most used commands in a separate line. This worked better, but then while I was selecting and copying the lines, I would make mistakes and copy the line with a part of the next line or with the line feed character, so it executed immediately when pasted. I needed a small text edit where each line can be copied to the clipboard with a click of a button and can stay on top of the other windows. In addition, I needed many of these, so I needed something to spawn a new instance from the program itself. So, I made this utility that looks like this.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/01/P121-01.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/01/P121-01.png" alt="" width="300" height="81" class="aligncenter size-full wp-image-8405" /></a><br />
The program&#8217;s caption bar has been minimized so the program doesn&#8217;t take much real estate on the screen. The green bar on top is used to move the window. The buttons have captions to quickly display the function. Form left to right, button C copies from the text box to the clipboard, button P paste the text from clipboard into the text box, N means a new window &#8211; the program will start another instance of itself, the T button is a toggle &#8211; to stay on top of the other windows or not (if you have one monitor, this is very useful), and the last two buttons are minimize and exit. On the right bottom size there is a grip to resize the instance. This is how I use my program.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/01/P121-02.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/01/P121-02.png" alt="" width="497" height="365" class="aligncenter size-full wp-image-8406" srcset="https://blog.andreev.it/wp-content/uploads/2019/01/P121-02.png 497w, https://blog.andreev.it/wp-content/uploads/2019/01/P121-02-300x220.png 300w" sizes="(max-width: 497px) 100vw, 497px" /></a><br />
Anytime I need something, I just click on the C button and then right-click-Paste in my shell. No need to search through the history of your bash.<br />
The source and the exe are at github.</p>
<pre class="brush: bash; title: ; notranslate">
git clone https://www.github.com/klimenta/cmddepot.git
</pre>
<p>The EXE is under cmddepot\CmdDepot\bin\Release folder.<br />
The main source. Click to expand.</p>
<pre class="brush: bash; collapse: true; light: false; title: ; toolbar: true; notranslate">
using System;
using System.Drawing;
using System.Windows.Forms;

namespace CmdDepot
{
    public partial class CmdDepot : Form
    {
        public CmdDepot()
        {
            InitializeComponent();
            this.FormBorderStyle = FormBorderStyle.None;
            this.DoubleBuffered = true;
            this.SetStyle(ControlStyles.ResizeRedraw, true);
        }
        private const int cGrip = 16;      // Grip size
        private const int cCaption = 5;   // Caption bar height;

        protected override void OnPaint(PaintEventArgs e)
        {
            Rectangle rc = new Rectangle(this.ClientSize.Width - cGrip, this.ClientSize.Height - cGrip, cGrip, cGrip);
            ControlPaint.DrawSizeGrip(e.Graphics, this.BackColor, rc);
            rc = new Rectangle(0, 0, this.ClientSize.Width, cCaption);
            e.Graphics.FillRectangle(Brushes.Chartreuse, rc);
        }

        protected override void WndProc(ref Message m)
        {
            if (m.Msg == 0x84)
            {  // Trap WM_NCHITTEST
                Point pos = new Point(m.LParam.ToInt32());
                pos = this.PointToClient(pos);
                if (pos.Y &lt; cCaption)
                {
                    m.Result = (IntPtr)2;  // HTCAPTION
                    return;
                }
                if (pos.X &gt;= this.ClientSize.Width - cGrip &amp;&amp; pos.Y &gt;= this.ClientSize.Height - cGrip)
                {
                    m.Result = (IntPtr)17; // HTBOTTOMRIGHT
                    return;
                }
            }
            base.WndProc(ref m);
        }
    
        private void btnClose_Click(object sender, EventArgs e)
        {
            System.Windows.Forms.Application.Exit();
        }

        private void btnMinimize_Click(object sender, EventArgs e)
        {
            this.WindowState = FormWindowState.Minimized;
        }

        private void btnPaste_Click(object sender, EventArgs e)
        {
            tbCmd.Paste();
        }

        private void btnCopy_Click(object sender, EventArgs e)
        {
            Clipboard.SetText(tbCmd.Text);
        }

        private void btnNew_Click(object sender, EventArgs e)
        {
            System.Diagnostics.Process.Start(System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName);
        }

        private void btnStayOnTop_Click(object sender, EventArgs e)
        {
            this.TopMost = !this.TopMost;
            if (this.TopMost) {
                btnStayOnTop.BackColor = Color.Tomato;
            } else
            {
                btnStayOnTop.BackColor = btnCopy.BackColor;
            }            
        }
    }
}
</pre>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.andreev.it/2019/01/144-visual-c-cmddepot-small-clipboard-utility/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Programming: Go or Golang &#8211; Battleship game</title>
		<link>https://blog.andreev.it/2018/07/133-go-lang-battleship-game/</link>
					<comments>https://blog.andreev.it/2018/07/133-go-lang-battleship-game/#respond</comments>
		
		<dc:creator><![CDATA[Kliment Andreev]]></dc:creator>
		<pubDate>Sun, 29 Jul 2018 20:48:48 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[battleship]]></category>
		<category><![CDATA[game]]></category>
		<category><![CDATA[Go]]></category>
		<guid isPermaLink="false">http://blog.iandreev.com/?p=3971</guid>

					<description><![CDATA[I&#8217;ve decided to play with the Go or Golang language and what better way&#8230;]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><p>I&#8217;ve decided to play with the Go or Golang language and what better way to learn than to give yourself a small task and see if you can make it. So, I&#8217;ve made a small variant of the Battleship game. It&#8217;s my first project in Go and it can be much better optimized, but hey, it&#8217;s my first try. Save the source as <strong>battleship.go</strong> and start with go <strong>run battleship.go</strong>. Looks like this in a Linux terminal.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2018/07/P110-01.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2018/07/P110-01.png" alt="" width="295" height="439" class="aligncenter size-full wp-image-8248" /></a></p>
<pre class="brush: cpp; collapse: true; light: false; title: ; toolbar: true; notranslate">
//--------------------------------
// Battleship - K.Andreev 20182907
// BSD 2.0 License
// https://blog.andreev.it
//--------------------------------
package main

import (
	&quot;bufio&quot;
	&quot;fmt&quot;
	&quot;math/rand&quot;
	&quot;os&quot;
	&quot;strings&quot;
	&quot;time&quot;
)

//Size of the board 10 x 10
var intBoardSize = 10

//Total number of ships on each side
var intTotalShips = 15

//Player's name
var sYourName = &quot;&quot;

//Holder for &quot;press any key&quot;
var sAnyKey = &quot;&quot;

//Number of player's turns
var intPlayerTurns = 0

//Number of computer's turns
var intComputerTurns = 0

//Who plays first
var bPlayerFirst = true

//How many ships left for the player
var intPlayerShipsLeft = 15

//How many ships left for the computer
var intComputerShipsLeft = 15

//Define a two dimensional array for the player
var arrPlayerBoard &#x5B;10]&#x5B;10]int

//Define a two dimension array for the computer
var arrComputerBoard &#x5B;10]&#x5B;10]int

//Define Unicode elements for on-screen presentation
//If there are any issues with the display, change them here
//const emptyElement = &quot;\u25a2&quot;     //replace with &quot; &quot;
//const shipElement = &quot;\u25a3&quot;      //replace with &quot;X&quot;
//const shipWreckElement = &quot;\u25a4&quot; //replace with &quot;+&quot;
//const missedElement = &quot;\u25a9&quot;    //replace with &quot;o&quot;
const emptyElement = &quot; &quot;     
const shipElement = &quot;X&quot;      
const shipWreckElement = &quot;+&quot; 
const missedElement = &quot;o&quot;    

func main() {
	//Initialize the random generator
	rand.Seed(time.Now().UnixNano())
	//Print the initial greetings and instructions
	printInstructions()
	//Initialize the boards
	initBoard()
	//Get player's name
	inputYourName()
	//Print the boards
	printBoard()
	//Get the player's ships (coordinates)
	inputFleet()
	//Generate computer's fleet
	generateComputerFleet()
	//Print the boards
	printBoard()
	//Who plays first
	decideFirstPlayer()
	//MAIN GAME LOOP
	for {
		if bPlayerFirst {
			playerMove()
			computerMove()
		} else {
			computerMove()
			playerMove()
		}
	}
}

//Print's game instructions
func printInstructions() {
	fmt.Println(&quot;				==========&quot;)
	fmt.Println(&quot;				Battleship&quot;)
	fmt.Println(&quot;				==========&quot;)
	fmt.Println(&quot;&quot;)
	fmt.Println(&quot;The goal of this game is to sink all of the enemy's ships on the&quot;, intBoardSize, &quot;X&quot;, intBoardSize, &quot;board.&quot;)
	fmt.Println(&quot;You do that by guessing ship's location using coordinates (e.g. LAUNCH AT&gt; C3).&quot;)
	fmt.Println(&quot;If you or the computer hit a ship, another chance is given until you miss again.&quot;)
	fmt.Println(&quot;The game is over when you or the opponent sinks all&quot;, intTotalShips, &quot;ships.&quot;)
	fmt.Println(&quot;&quot;)
	fmt.Println(&quot;To make the game more difficult, you won't see where you missed on the computer board&quot;)
	fmt.Println(&quot;...and the computer won't hit the same place twice. HAVE FUN!!!&quot;)
	fmt.Println(&quot;&quot;)
	fmt.Println(&quot;K.Andreev - 20180729 - BSD Simplified License&quot;)
}

//Initialized both boards with zero values
//Sets the number of ships
func initBoard() {
	i := 0
	j := 0
	intPlayerShipsLeft = 15
	intComputerShipsLeft = 15
	intPlayerTurns = 0
	intComputerTurns = 0
	for i = 0; i &lt; intBoardSize; i++ {
		for j = 0; j &lt; intBoardSize; j++ {
			arrPlayerBoard&#x5B;i]&#x5B;j] = 0
			arrComputerBoard&#x5B;i]&#x5B;j] = 0
		}
	}
}

//Gets player's name
func inputYourName() {
	fmt.Println(&quot;&quot;)
	fmt.Print(&quot;Enter your name: &quot;)
	fmt.Scanln(&amp;sYourName)
	fmt.Println(&quot;&quot;)
	fmt.Println(&quot;Hello,&quot;, sYourName, &quot;!&quot;)
	fmt.Println(&quot;&quot;)
	fmt.Println(&quot;Please place your fleet using coordinates (e.g. D5)&quot;)
	fmt.Println(&quot;If you make a mistake, re-enter the same coordinate.&quot;)
	fmt.Println(&quot;&quot;)
	pressAnyKey()
}

//Ask the player to enter the coordinates of the fleet
func inputFleet() {
	intShipCounter := 0
	intXCoord := 0
	intYCoord := 0
	sPosition := &quot;&quot;
	fmt.Println(&quot;&quot;)
	for {
	enter:
		fmt.Print(&quot;SHIP AT&gt; &quot;)
		fmt.Scan(&amp;sPosition)
		//If the length is different than two, coordinate is wrong
		if len(sPosition) != 2 {
			fmt.Println(&quot;INVALID POSITION.&quot;)
			goto enter
		}
		//Convert the coordiantes in array position, e.g. A0 is (0,0), J9 is (9,9)
		sPosition = strings.ToUpper(sPosition)
		intXCoord = int(sPosition&#x5B;0] - 65)
		intYCoord = int(sPosition&#x5B;1] - 48)
		//If the array coordinate is not in &#x5B;(0,0)..(9,9)] range, coordinate is wrong
		if intXCoord &lt; 0 || intXCoord &gt; 9 {
			fmt.Println(&quot;INVALID POSITION.&quot;)
			goto enter
		}
		//If the player already entered a ship at a coordinate, toggle it, make it empty
		if arrPlayerBoard&#x5B;intXCoord]&#x5B;intYCoord] == 1 {
			arrPlayerBoard&#x5B;intXCoord]&#x5B;intYCoord] = 0
		} else {
			arrPlayerBoard&#x5B;intXCoord]&#x5B;intYCoord] = 1
			intShipCounter = intShipCounter + 1
		}
		printBoard()
		//Exit when all 15 ships are placed
		if intShipCounter == 15 {
			break
		}
	}
}

//Prints the boards on the screen
func printBoard() {
	fmt.Println(sYourName)
	i := 0
	j := 0
	intPlayerShipCounter := 0
	intComputerShipCounter := 0
	sChar := 'A'
	fmt.Println(&quot; 0123456789&quot;)
	//Prints player's board
	for i = 0; i &lt; intBoardSize; i++ {
		fmt.Print(string(sChar))
		sChar = sChar + 1
		for j = 0; j &lt; intBoardSize; j++ {
			switch arrPlayerBoard&#x5B;i]&#x5B;j] {
			case 0:
				fmt.Print(emptyElement)
			case 1:
				fmt.Print(shipElement)
				intPlayerShipCounter = intPlayerShipCounter + 1
			case 2:
				fmt.Print(shipWreckElement)
			case 3:
				fmt.Print(missedElement)
			}
		}
		fmt.Println(&quot;&quot;)
	}
	//Prints player's ships left and the total
	fmt.Println(&quot;SHIPS&quot;, intPlayerShipCounter, &quot;/&quot;, intTotalShips)
	fmt.Println(&quot;&quot;)
	//Prints computer's board
	fmt.Println(&quot;COMPUTER&quot;)
	i = 0
	j = 0
	sChar = 'A'
	fmt.Println(&quot; 0123456789&quot;)
	for i = 0; i &lt; intBoardSize; i++ {
		fmt.Print(string(sChar))
		sChar = sChar + 1
		for j = 0; j &lt; intBoardSize; j++ {
			switch arrComputerBoard&#x5B;i]&#x5B;j] {
			case 0:
				fmt.Print(emptyElement)
			case 1:
				//Uncomment the line below to see computer's ships (cheat)
				//fmt.Print(shipElement)
				intComputerShipCounter = intComputerShipCounter + 1
			case 2:
				fmt.Print(shipWreckElement)
			case 3:
				fmt.Print(missedElement)
			}
		}
		fmt.Println(&quot;&quot;)
	}
	//Prints computer's ships left and the total
	fmt.Println(&quot;SHIPS&quot;, intComputerShipCounter, &quot;/&quot;, intTotalShips)
}

//Places computer's fleet using random generator
func generateComputerFleet() {
	intShipCounter := 0
	intRandomShipatX := 0
	intRandomShipatY := 0
	for {
		intRandomShipatX = rand.Intn(10)
		intRandomShipatY = rand.Intn(10)
		if arrComputerBoard&#x5B;intRandomShipatX]&#x5B;intRandomShipatY] == 0 {
			arrComputerBoard&#x5B;intRandomShipatX]&#x5B;intRandomShipatY] = 1
			intShipCounter = intShipCounter + 1
		}
		if intShipCounter == 15 {
			break
		}
	}
}

//Who plays first. A number between 0 and 99 is drawn for both players.
//The bigger number plays first.
func decideFirstPlayer() {
	intPlayerFirst := 0
	intComputerFirst := 0
	fmt.Println(&quot;&quot;)
	fmt.Println(&quot;Prepare for the battle...&quot;)
	intPlayerFirst = rand.Intn(100)
	intComputerFirst = rand.Intn(100)
	if intPlayerFirst &gt;= intComputerFirst {
		bPlayerFirst = true
		fmt.Println(&quot;You play first. Random numbers say&quot;, intPlayerFirst, &quot;vs.&quot;, intComputerFirst)
		fmt.Scanln(&amp;sAnyKey)
		pressAnyKey()
	} else {
		bPlayerFirst = false
		fmt.Println(&quot;Computer plays first. Random numbers say&quot;, intComputerFirst, &quot;vs.&quot;, intPlayerFirst)
		fmt.Scanln(&amp;sAnyKey)
		pressAnyKey()
	}
}

//Get the player to input a coordinate
func playerMove() {
	intXCoord := 0
	intYCoord := 0
	sPosition := &quot;&quot;
	fmt.Println(&quot;&quot;)
enter:
	for {
		fmt.Print(&quot;LAUNCH AT&gt; &quot;)
		fmt.Scan(&amp;sPosition)
		//Same checks for invalid position
		if len(sPosition) != 2 {
			fmt.Println(&quot;INVALID POSITION.&quot;)
			goto enter
		}
		sPosition = strings.ToUpper(sPosition)
		intXCoord = int(sPosition&#x5B;0] - 65)
		intYCoord = int(sPosition&#x5B;1] - 48)
		if intXCoord &lt; 0 || intXCoord &gt; 9 {
			fmt.Println(&quot;INVALID POSITION.&quot;)
			goto enter
		}
		//Increase the turns number
		intPlayerTurns = intPlayerTurns + 1
		//This is a miss if the board has 0 value at the coordinate
		if arrComputerBoard&#x5B;intXCoord]&#x5B;intYCoord] == 0 {
			fmt.Print(&quot;You missed at &quot;, sPosition, &quot;.&quot;)
			fmt.Scanln(&amp;sAnyKey)
			pressAnyKey()
		} else {
			//This is a hit
			arrComputerBoard&#x5B;intXCoord]&#x5B;intYCoord] = 2
			fmt.Print(&quot;YOU HIT A SHIP AT &quot;, sPosition, &quot;.&quot;)
			fmt.Scanln(&amp;sAnyKey)
			pressAnyKey()
			printBoard()
			intComputerShipsLeft = intComputerShipsLeft - 1
			//Game over if all ships are hit
			if intComputerShipsLeft == 0 {
				fmt.Println(&quot;================&quot;)
				fmt.Println(&quot;&#x5B;   YOU WON    ]&quot;)
				fmt.Println(&quot;================&quot;)
				fmt.Println(intPlayerTurns, &quot;turns to complete the game...&quot;)
				os.Exit(0)
			}
			goto enter
		}
		printBoard()
		break
	}
}

//Computer logic for the play
func computerMove() {
	intRandomShipatX := 0
	intRandomShipatY := 0
enter:
	for {
		//Get a random coordinate
		intRandomShipatX = rand.Intn(10)
		intRandomShipatY = rand.Intn(10)
		//Increase the number of turns
		intComputerTurns = intComputerTurns + 1
		//If it's a miss, remember that position by placing &quot;3&quot; and never hit that position again
		if arrPlayerBoard&#x5B;intRandomShipatX]&#x5B;intRandomShipatY] == 0 {
			arrPlayerBoard&#x5B;intRandomShipatX]&#x5B;intRandomShipatY] = 3
			fmt.Println(&quot;Computer missed at&quot;, string(intRandomShipatX+65), string(intRandomShipatY+48))
			pressAnyKey()
			printBoard()
			break
		}
		//If it's a hit, mark the ship as hit by placing &quot;2&quot;
		if arrPlayerBoard&#x5B;intRandomShipatX]&#x5B;intRandomShipatY] == 1 {
			arrPlayerBoard&#x5B;intRandomShipatX]&#x5B;intRandomShipatY] = 2
			fmt.Println(&quot;COMPUTER HIT A SHIP at&quot;, string(intRandomShipatX+65), string(intRandomShipatY+48))
			pressAnyKey()
			printBoard()
			intPlayerShipsLeft = intPlayerShipsLeft - 1
			if intPlayerShipsLeft == 0 {
				fmt.Println(&quot;================&quot;)
				fmt.Println(&quot;&#x5B; COMPUTER WON ]&quot;)
				fmt.Println(&quot;================&quot;)
				fmt.Println(intComputerTurns, &quot;turns to complete the game...&quot;)
				os.Exit(0)
			} else {
				goto enter
			}
		}
		//If the random coordinate is where the ship was already hit, go guess again
		if arrPlayerBoard&#x5B;intRandomShipatX]&#x5B;intRandomShipatY] == 2 {
			goto enter
		}
		//If the random coordinate is where we missed, go guess again
		if arrPlayerBoard&#x5B;intRandomShipatX]&#x5B;intRandomShipatY] == 3 {
			goto enter
		}
	}
}

//Wait for any key to be pressed
func pressAnyKey() {
	in := bufio.NewReader(os.Stdin)
	line, _ := in.ReadString('\n')
	_ = line
}
</pre>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.andreev.it/2018/07/133-go-lang-battleship-game/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Visual C#: Screenshot Grabber &#8211; Take screenshots automatically</title>
		<link>https://blog.andreev.it/2018/04/127-c-screenshot-grabber-take-screenshots-automatically/</link>
					<comments>https://blog.andreev.it/2018/04/127-c-screenshot-grabber-take-screenshots-automatically/#respond</comments>
		
		<dc:creator><![CDATA[Kliment Andreev]]></dc:creator>
		<pubDate>Sat, 21 Apr 2018 23:48:16 +0000</pubDate>
				<category><![CDATA[C#]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[Visual C#]]></category>
		<guid isPermaLink="false">http://blog.iandreev.com/?p=3819</guid>

					<description><![CDATA[I needed a tool that takes screenshots at a regular interval. No bells and&#8230;]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><p>I needed a tool that takes screenshots at a regular interval. No bells and whistles, just simple tool where you can specify at what interval you can take the screesnhots, the destination folder and the monitor (if you have more than one). It looks like this. </p>
<p><a href="https://blog.andreev.it/wp-content/uploads/2018/04/P104-01.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2018/04/P104-01.png" alt="" width="303" height="237" class="aligncenter size-full wp-image-8219" /></a><br />
Click <strong>Start </strong>when you are ready and the tool will minimize itself. When you are done, double-click the icon in the tray or right-click on it and choose <strong>Open </strong>where you can <strong>Stop </strong>the screesnhot grabbing. </p>
<p>Here is the source for your review but you&#8217;ll need the whole C# solution in order to work. Get it (git clone) from <a href="https://github.com/klimenta/SSG" rel="noopener noreferrer" target="_blank">github</a>. You&#8217;ll need .NET 3.5. Here is the <a href="https://blog.andreev.it/wp-content/uploads/2018/04/SSG.7z">EXE</a>. </p>
<pre class="brush: cpp; collapse: true; light: false; title: ; toolbar: true; notranslate">
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Drawing.Imaging;
using System.IO;

namespace SSG
{
    public partial class Form1 : Form
    {

        //Get all available monitors
        Screen&#x5B;] scrScreens = Screen.AllScreens;

        public Form1()
        {
            InitializeComponent();
        }

        private void btnFolderBrowser_Click(object sender, EventArgs e)
        {
            //Shows up the browse dialog for the destination files
            DialogResult drResult = fbdBrowserDialog.ShowDialog();
            if (drResult == DialogResult.OK)
            {
                tbFolder.Text = fbdBrowserDialog.SelectedPath;
            }

        }

        private void btnRecord_Click(object sender, EventArgs e)
        {
            //Check if valid interval
            int intInterval = 1000;
            if (!(Int32.TryParse(tbInterval.Text, out intInterval)))
            {
                MessageBox.Show(&quot;Not a valid interval.&quot;, &quot;SSG&quot;, MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }
            if (intInterval &lt; 1)
            {
                MessageBox.Show(&quot;Not a valid interval.&quot;, &quot;SSG&quot;, MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }
            timTimer.Interval = intInterval * 1000;
            //Check if folder exists
            if (!(Directory.Exists(tbFolder.Text)))
            {
                MessageBox.Show(&quot;The folder does not exist.&quot;, &quot;SSG&quot;, MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }
            if (timTimer.Enabled)
            {
                timTimer.Enabled = false;
                btnRecord.Image = Properties.Resources.StartWithoutDebug_16x;
                return;
            }
            timTimer.Enabled = true;
            this.WindowState = FormWindowState.Minimized;
            btnRecord.Image = Properties.Resources.Stop_16x;            
        }

        private void timTimer_Tick(object sender, EventArgs e)
        {
            //Get the screenshots as JPG files
            Rectangle bounds = scrScreens&#x5B;cbMonitor.SelectedIndex].Bounds;
            using (Bitmap bitmap = new Bitmap(bounds.Width, bounds.Height))
            {
                using (Graphics g = Graphics.FromImage(bitmap))
                {
                    g.CopyFromScreen(bounds.Left, bounds.Top, 0, 0, bounds.Size);
                }
                string strDateTime = DateTime.Now.ToString(&quot;yyyyMMddHHmmss&quot;);
                bitmap.Save(tbFolder.Text + &quot;\\&quot; + strDateTime + &quot;.jpg&quot;, ImageFormat.Jpeg);
            }

        }

        private void Form1_Resize(object sender, EventArgs e)
        {
            //Minimize to tray
            if (this.WindowState == FormWindowState.Minimized)
            {
                Hide();
                niTrayIcon.Visible = true;
                if (timTimer.Enabled)
                {
                    niTrayIcon.Text = &quot;SSG - Recording&quot;;
                }
                else
                {
                    niTrayIcon.Text = &quot;SSG - Idle&quot;;
                }
            }
        }

        private void niTrayIcon_MouseDoubleClick(object sender, MouseEventArgs e)
        {
            Show();
            this.WindowState = FormWindowState.Normal;
            niTrayIcon.Visible = false;
        }

        private void tsmiExit_Click(object sender, EventArgs e)
        {
            Application.Exit();
        }

        private void tsmiOpen_Click(object sender, EventArgs e)
        {
            Show();
            this.WindowState = FormWindowState.Normal;
            niTrayIcon.Visible = false;
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            //Adds the number of monitors in a combox box
            int intMonitors = scrScreens.Length;
            int intCounter = 0;
            while (intCounter &lt; intMonitors)
            {
                cbMonitor.Items.Add((intCounter + 1).ToString());
                intCounter++;
            }
            cbMonitor.SelectedIndex = 0;
        }
    }
}
</pre>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.andreev.it/2018/04/127-c-screenshot-grabber-take-screenshots-automatically/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>PowerShell, Azure: Migrate a Windows VM from vCenter to Azure</title>
		<link>https://blog.andreev.it/2017/10/114-azure-migrate-vm-vcenter-azure/</link>
					<comments>https://blog.andreev.it/2017/10/114-azure-migrate-vm-vcenter-azure/#respond</comments>
		
		<dc:creator><![CDATA[Kliment Andreev]]></dc:creator>
		<pubDate>Wed, 04 Oct 2017 12:22:54 +0000</pubDate>
				<category><![CDATA[Azure]]></category>
		<category><![CDATA[Cloud]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[migration]]></category>
		<guid isPermaLink="false">http://blog.iandreev.com/?p=3246</guid>

					<description><![CDATA[Recently, I had to migrate a VM from vCenter to Azure. Unlike the migration&#8230;]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><p>Recently, I had to migrate a VM from vCenter to Azure. Unlike the migration from vCenter to AWS, this migration is a little bit trickier and involves many steps. There is probably a better way with Azure Site Recovery, but you have to pay for that. See my other <a href="https://blog.andreev.it/?p=5617" rel="noopener noreferrer" target="_blank">post </a>for that.<br />
We&#8217;ll use PowerShell for the migration. There are several prerequisites for the migration and they are listed in the comments section.<br />
I have a resource group called <strong>rgTest</strong>, a virtual network called <strong>vnetTest </strong>and a subnet that spans all of the virtual network address space called <strong>subTest</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2017/10/P092-01.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2017/10/P092-01.png" alt="" width="316" height="626" class="aligncenter size-full wp-image-7755" srcset="https://blog.andreev.it/wp-content/uploads/2017/10/P092-01.png 316w, https://blog.andreev.it/wp-content/uploads/2017/10/P092-01-151x300.png 151w" sizes="(max-width: 316px) 100vw, 316px" /></a><br />
In addition, I&#8217;ve created a storage account called <strong>sablogtest </strong>and a container called <strong>conttest</strong>.</p>
<p><a href="https://blog.andreev.it/wp-content/uploads/2017/10/P092-02.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2017/10/P092-02.png" alt="" width="999" height="711" class="aligncenter size-full wp-image-7756" srcset="https://blog.andreev.it/wp-content/uploads/2017/10/P092-02.png 999w, https://blog.andreev.it/wp-content/uploads/2017/10/P092-02-300x214.png 300w, https://blog.andreev.it/wp-content/uploads/2017/10/P092-02-768x547.png 768w, https://blog.andreev.it/wp-content/uploads/2017/10/P092-02-585x416.png 585w" sizes="(max-width: 999px) 100vw, 999px" /></a><br />
On my local machine, I have the VMDK files downloaded from vCenter in a folder called <strong>VM </strong>and I have another folder called <strong>VHD </strong>where the converted files will be stored. For the script, use the names of the VMDK files that are 1K in size.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2017/10/P092-03.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2017/10/P092-03.png" alt="" width="689" height="411" class="aligncenter size-full wp-image-7757" srcset="https://blog.andreev.it/wp-content/uploads/2017/10/P092-03.png 689w, https://blog.andreev.it/wp-content/uploads/2017/10/P092-03-300x179.png 300w, https://blog.andreev.it/wp-content/uploads/2017/10/P092-03-585x349.png 585w" sizes="(max-width: 689px) 100vw, 689px" /></a></p>
<p>The script works if the OS has one or two drives. If you have more drives, you&#8217;ll have to modify the script. It should be very straightforward.<br />
I&#8217;ve noticed that if I run the script in PowerShell cmd, then the script waits sometimes for the user to hit any key. In Windows ISE PowerShell, I didn&#8217;t have these problems. Make sure you are local admin on the machine. It&#8217;s needed because of the VMDK to VHD conversion.<br />
The VM will power up automatically. When you RDP to it, the data drive won&#8217;t be accessible. Make sure you run <strong>diskmgmt.msc</strong> and then online the drive. </p>
<pre class="brush: powershell; collapse: true; light: false; title: ; toolbar: true; notranslate">
################################################################################
## Kliment Andreev 2017 - (c) BSD License
################################################################################
## 1. Converts VMDK file(s) to VHD
## 2. Uploads the VHD(s) to an existing container
## 3. Creates a bootable disk and/or data disk
## 4. Creates a VM with a public and private IP
################################################################################
# Pre-req:
#       Microsoft CLI 2.0 for Azure
#       Microsoft Virtual Machine Converter 3.0
#       VMDK file(s) and enough storage to convert it/them to VHD
#       Existing subscription in azure
#       Known location of your subscription e.g East US
#       Existing resource group
#       Existing storage account and a container inside
#       Existing vNet where server will reside
#       Existing subnet where the server will reside
#       Existing security group that allows RDP
#       Make sure the OS is set for DHCP
#       Make sure the OS uses E1000, don't use vmxnet NIC
#       Make sure you know your local admin password
#       Make sure you have RDP enabled
################################################################################
# If for some reason the script fails 
# and you are starting from scratch,
# make sure you delete the VHD files.
# Otherwise the converter will create a new
# VHD file with (1) as suffix and the VM
# won't convert/upload properly
################################################################################
# Define your resources/variables here
# None of these can be empty except:
# $myDataVMDKFile and $myDataVHDFile
################################################################################
# Define where is your OS VMWare VMDK file, e.g. c:\users\name\desktop\server.vmdk
$myOSVMDKFile = &quot;C:\Users\klimenta\Desktop\VM\testvm.vmdk&quot;
# Define where is your DATA VMWare VMDK file, e.g. c:\users\name\desktop\server.vmdk
# If you have only C: drive, the string below can be empty e.g. &quot;&quot;
$myDataVMDKFile = &quot;C:\users\klimenta\desktop\VM\testvm_1.vmdk&quot;  
#$myDataVMDKFile = &quot;&quot; 
# Where do you want your OS VHD file created locally, e.g. c:\users\name\desktop\server.vhd
$myOSVHDFile = &quot;C:\Users\klimenta\Desktop\VHD\testvm.vhd&quot;   
# Where do you want your Data VHD file created locally, e.g. c:\users\name\desktop\server.vhd
$myDataVHDFile = &quot;C:\Users\klimenta\Desktop\VHD\testvm_1.vhd&quot;   
#$myDataVHDFile=&quot;&quot;
# Virtual Machine Converter PS module
$ConverterFile = &quot;C:\Program Files\Microsoft Virtual Machine Converter\MvmcCmdlet.psd1&quot;
# Existing subscription name
$mySubscription = &quot;Pay-As-You-Go&quot;
# Existing location
$myLocation = &quot;East US&quot;
# Existing resource group
$myResourceGroup = &quot;rgTest&quot;
# Name of the existing storage account (it shoould be unique)
$myStorageAcct = &quot;sablogtest&quot;
# Name of the existing container
$myContainer =&quot;conttest&quot;
# Name of the existing VNET
$myVNET = &quot;vnettest&quot;
# Name of the existing subnet
$mySubnet = &quot;subtest&quot;
# Name of the existing security group
$mySecurityGroup = &quot;sgAllowRDP&quot;
# VM size
$myVMSize = &quot;Standard_A2&quot;
################################################################################
# If Standard_A2
# doesn't suit your needs
# check other sizes in a region with:
# Get-AzureRmVMSize -Location &quot;East US&quot; | Sort-Object Name
################################################################################
# Start the counter
################################################################################
$StartDate = (Get-Date)
# Check if any of the variables are blank
if (!(($myOSVMDKFile) -and ($myOSVHDFile) -and ($ConverterFile) -and ($mySubscription) -and `
    ($myLocation) -and ($myResourceGroup) -and ($myStorageAcct) -and ($myContainer) -and ($myVNET) -and `
    ($mySubnet) -and ($mySecurityGroup) -and ($myVMSize))) {
    Write-Host &quot;Some of the required variables are empty. Exiting...&quot; -ForegroundCOlor Red
    Exit
}
# Check if the OS VMDK exists
if (-not(Test-Path -Path $myOSVMDKFile)) {
        Write-Host &quot;Error: VMDK file does not exist. Exiting...&quot; -ForegroundColor Red
        Exit
}
# Check if the Data VMDK exists but only if defined
if ($myDataVMDKFile -ne &quot;&quot;)
{
    if (-not(Test-Path -Path $myDataVMDKFile)) {
            Write-Host &quot;Error: VMDK file does not exist. Exiting...&quot; -ForegroundColor Red
            Exit
    }
}
################################################################################
# Check the VHD
################################################################################
$extension = (&#x5B;IO.Path]::GetExtension($myOSVHDFile)).ToUpper()
if ($extension -ne &quot;.VHD&quot;) {
    Write-Host &quot;Error: Not a VHD file. Exiting...&quot; -ForegroundColor Red
    Exit
}
################################################################################
# Import Virtual Machine Converter
################################################################################
if (-not(Test-Path -Path $ConverterFile)) {
        Write-Host &quot;Error: Microsoft Virtual Machine Converter not installed. Exiting...&quot; -ForegroundColor Red
        Exit
}
Import-Module $ConverterFile
# Get the VHD file name only and the URL of the container
$VHDOSFileName = &#x5B;io.path]::GetFileName($myOSVHDFile)
$VHDDataFileName = &#x5B;io.path]::GetFileName($myDataVHDFile)
$urlOS = &quot;https://&quot; + $myStorageAcct + &quot;.blob.core.windows.net/&quot; + $myContainer + &quot;/&quot; + $VHDOSFileName
$urlData = &quot;https://&quot; + $myStorageAcct + &quot;.blob.core.windows.net/&quot; + $myContainer + &quot;/&quot; + $VHDDataFileName
################################################################################
# Log in to your Azure account
################################################################################
Write-Host &quot;Waiting...Login prompt coming up...&quot; -ForegroundColor Yellow
try {
    Login-AzureRmAccount
}
catch {
    Write-Host $_.Exception.Message -ForegroundColor Red
    Exit
}
################################################################################
# Check if you can log to the specified subscription
################################################################################
try {
    if(Get-AzureRmSubscription -SubscriptionName $mySubscription -ErrorAction Stop) {}
}
catch {
    Write-Host $_.Exception.Message -ForegroundColor Red
    Write-Host &quot;Error: The name of the subscription is incorrect or you don't have rights. Exiting...&quot; -ForegroundColor Red
    Exit
}
Get-AzureRmSubscription -SubscriptionName $mySubscription | Select-AzureRmSubscription
Write-Host &quot;Your current subscription is set to&quot; $mySubscription -ForegroundColor Yellow
################################################################################
# Check if the resource group exists
################################################################################
try {
    if(Get-AzureRmResourceGroup -ResourceGroupName $myResourceGroup -ErrorAction Stop) {}
}
catch {
    Write-Host $_.Exception.Message -ForegroundColor Red
    Write-Host &quot;Error: The name of the resource group is incorrect or you don't have rights. Exiting...&quot; -ForegroundColor Red
    Exit
}
################################################################################ 
# Converting from VMDK to VHD (OS)
# If you get an error from Azure that the blob is corrupted, use 
# -VhdType FixedHardDisk
################################################################################
try {
    Write-Host &quot;Converting VMDK OS Disk to VHD in progress... Hit any key when no progress on top.&quot; -ForegroundColor Yellow
    if(ConvertTo-MvmcVirtualHardDisk -SourceLiteralPath $myOSVMDKFile -DestinationLiteralPath $myOSVHDFile `
    -VhdType DynamicHardDisk -VhdFormat Vhd -ErrorAction Stop) {}
}
catch {
    Write-Host $_.Exception.Message -ForegroundColor Red
    Write-Host &quot;Error: The VMDK file is most likely corrupt, access violation or incorrect VMDK format. Exiting...&quot; -ForegroundColor Red
    Exit
}
################################################################################
# Converting from VMDK to VHD (Data)
# If you get an error from Azure that the blob is corrupted, use 
# -VhdType FixedHardDisk
################################################################################
if ($myDataVMDKFile -ne &quot;&quot;)
{
    try {
        Write-Host &quot;Converting VMDK Data Disk to VHD in progress... Hit any key when no progress on top.&quot; -ForegroundColor Yellow
        if(ConvertTo-MvmcVirtualHardDisk -SourceLiteralPath $myDataVMDKFile -DestinationLiteralPath $myDataVHDFile `
        -VhdType DynamicHardDisk -VhdFormat Vhd -ErrorAction Stop) {}
    }
    catch {
        Write-Host $_.Exception.Message -ForegroundColor Red
        Write-Host &quot;Error: The VMDK file is most likely corrupt, access violation or incorrect VMDK format. Exiting...&quot; -ForegroundColor Red
        Exit
    }   
}   
################################################################################
# Upload the OS VHD to the existing blob/container
################################################################################
Write-Host &quot;Uploading OS VHD to&quot; $urlOS -Foreground Yellow
Add-AzureRmVhd -ResourceGroupName $myResourceGroup -Destination $urlOS -LocalFilePath $myOSVHDFile -OverWrite
# Upload the Data VHD to the existing blob/container
if ($myDataVMDKFile -ne &quot;&quot;)
{
    Write-Host &quot;Uploading Data VHD to&quot; $urlData -Foreground Yellow
    Add-AzureRmVhd -ResourceGroupName $myResourceGroup -Destination $urlData -LocalFilePath $myDataVHDFile -OverWrite
}
################################################################################
# Create the OS disk
################################################################################
Write-Host &quot;Creating OS disk from VHD...&quot; -Foreground Yellow
$VHDOSFileNameOnly = &#x5B;io.path]::GetFileNameWithoutExtension($VHDOSFileName)
try {
    $osDisk = New-AzureRmDisk -DiskName $VHDOSFileName -Disk `
        (New-AzureRmDiskConfig -AccountType StandardLRS  -Location $myLocation -CreateOption Import `
        -SourceUri $urlOS) `
        -ResourceGroupName $myResourceGroup
}
catch {
    Write-Host $_.Exception.Message -ForegroundColor Red
    Write-Host &quot;Error: Something went wrong when creating the OS disk. Exiting...&quot; -ForegroundColor Red
    Exit
}
################################################################################
# Create the Data disk
################################################################################
if ($myDataVMDKFile -ne &quot;&quot;)
{
    Write-Host &quot;Creating data disk from VHD...&quot; -Foreground Yellow
    $VHDDataFileNameOnly = &#x5B;io.path]::GetFileNameWithoutExtension($VHDDataFileName)
    try {
        $dataDisk = New-AzureRmDisk -DiskName $VHDDataFileName -Disk `
            (New-AzureRmDiskConfig -AccountType StandardLRS  -Location $myLocation -CreateOption Import `
            -SourceUri $urlData) `
            -ResourceGroupName $myResourceGroup
    }
    catch {
        Write-Host $_.Exception.Message -ForegroundColor Red
        Write-Host &quot;Error: Something went wrong when creating the data disk. Exiting...&quot; -ForegroundColor Red
        Exit
    }
}
################################################################################
# Create and assign a public IP
################################################################################
$ipName = &quot;ip&quot; + $VHDOSFileNameOnly
Write-Host &quot;Creating and assigning a public IP&quot; $ipName -Foreground Yellow
try {
    $pip = New-AzureRmPublicIpAddress -Name $ipName -ResourceGroupName $myResourceGroup -Location $myLocation `
        -AllocationMethod Dynamic
}
catch {
    Write-Host $_.Exception.Message -ForegroundColor Red
    Write-Host &quot;Error: Something went wrong with the IP. Exiting...&quot; -ForegroundColor Red
    Exit
}
################################################################################
# Create the NIC
################################################################################ 
$nicName = &quot;nic&quot; + $VHDOSFileNameOnly
Write-Host &quot;Creating the NIC&quot; $nicName -Foreground Yellow
try {
    $VNET = Get-AzureRmVirtualNetwork -Name $myVNET -ResourceGroupName $myResourceGroup -ErrorAction Stop
}
catch {
    Write-Host $_.Exception.Message -ForegroundColor Red
    Write-Host &quot;Error: Something went wrong with the NIC. Check the vNet name. Exiting...&quot; -ForegroundColor Red
    Exit
}
try {
    $nic = New-AzureRmNetworkInterface -Name $nicName -ResourceGroupName $myResourceGroup `
        -Location $myLocation -SubnetId $VNET.Subnets&#x5B;0].Id -PublicIpAddressId $pip.Id -NetworkSecurityGroupId $mySecurityGroup.Id
}
catch {
    Write-Host $_.Exception.Message -ForegroundColor Red
    Write-Host &quot;Error: Something went wrong with the NIC. Exiting...&quot; -ForegroundColor Red
    Exit
}
################################################################################
# Create the VM config
################################################################################
$vmName = $VHDOSFileNameOnly
Write-Host &quot;Creating the VM config for&quot; $VHDOSFileNameOnly -Foreground Yellow
try {
    $vmConfig = New-AzureRmVMConfig -VMName $vmName -VMSize $myVMSize -ErrorAction Stop
}
catch {
    Write-Host $_.Exception.Message -ForegroundColor Red
    Write-Host &quot;Error: Something went wrong with the VM Config. Exiting...&quot; -ForegroundColor Red
    Exit
}
################################################################################
# Attach the NIC
################################################################################
Write-Host &quot;Attaching the NIC to&quot; $VHDOSFileNameOnly -Foreground Yellow
$vm = Add-AzureRmVMNetworkInterface -VM $vmConfig -Id $nic.Id
################################################################################
# Add the OS disk
################################################################################
Write-Host &quot;Adding the OS disk to&quot; $VHDOSFileNameOnly -Foreground Yellow
try {
    $vm = Set-AzureRmVMOSDisk -VM $vm -ManagedDiskId $osDisk.Id -StorageAccountType StandardLRS `
         -CreateOption Attach -Windows
}
catch {
    Write-Host $_.Exception.Message -ForegroundColor Red
    Write-Host &quot;Error: Something went wrong with the OS disk. Exiting...&quot; -ForegroundColor Red
    Exit
}

################################################################################
# Create the VM
################################################################################
Write-Host &quot;Creating the VM. This will take some time...Hit any key.&quot; $VHDOSFileNameOnly -Foreground Yellow
try {
    New-AzureRmVM -ResourceGroupName $myResourceGroup -Location $myLocation -VM $vm
}
catch {
    Write-Host $_.Exception.Message -ForegroundColor Red
    Write-Host &quot;Error: Something went wrong with the VM creation. Exiting...&quot; -ForegroundColor Red
    Exit
}
################################################################################
# Add the Data disk
################################################################################
if ($myDataVMDKFile -ne &quot;&quot;)
{
    Write-Host &quot;Adding the Data disk to&quot; $VHDDataFileNameOnly -Foreground Yellow
    try {
        $vm = Get-AzureRmVM -Name $VHDOSFileNameOnly -ResourceGroupName $myResourceGroup
        $vm = Add-AzureRmVMDataDisk -VM $vm -ManagedDiskId $DataDisk.Id -StorageAccountType StandardLRS `
             -CreateOption Attach -Lun 1
        Update-AzureRmVM -VM $vm -ResourceGroupName $myResourceGroup
    }
    catch {
        Write-Host $_.Exception.Message -ForegroundColor Red
        Write-Host &quot;Error: Something went wrong with the Data disk. Exiting...&quot; -ForegroundColor Red
        Exit
    }
}
################################################################################
# Finalize
################################################################################
$publicIP = Get-AzureRmPublicIpAddress -Name $ipName -ResourceGroupName $myResourceGroup
$EndDate = (Get-Date)
Write-Host &quot;========================================================================&quot; -ForegroundColor Yellow
Write-Host &quot;CREATION OF THE VM COMPLETED. The IP address is&quot; $publicIP.ipaddress -ForegroundColor Yellow
Write-Host &quot;========================================================================&quot; -ForegroundColor Yellow
Write-Host &quot;=&gt; TOTAL TIME:&quot;($EndDate - $StartDate).Days &quot;days,&quot; ($EndDate - $StartDate).Hours &quot;hours,&quot; ($EndDate - $StartDate).Minutes &quot;minutes.&quot;
</pre>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.andreev.it/2017/10/114-azure-migrate-vm-vcenter-azure/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>PowerShell, shell: Remove comments from source (Windows/Linux/BSD)</title>
		<link>https://blog.andreev.it/2016/12/103-norem-utility-remove-comments-source/</link>
					<comments>https://blog.andreev.it/2016/12/103-norem-utility-remove-comments-source/#respond</comments>
		
		<dc:creator><![CDATA[Kliment Andreev]]></dc:creator>
		<pubDate>Wed, 14 Dec 2016 15:53:14 +0000</pubDate>
				<category><![CDATA[AIX]]></category>
		<category><![CDATA[CentOS]]></category>
		<category><![CDATA[FreeBSD]]></category>
		<category><![CDATA[OpenBSD]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[Unix shell]]></category>
		<category><![CDATA[Windows]]></category>
		<category><![CDATA[remove comments]]></category>
		<category><![CDATA[sh]]></category>
		<guid isPermaLink="false">http://blog.iandreev.com/?p=2990</guid>

					<description><![CDATA[norem for Linux/*BSD A small utility written in Bourne shell (compatible with both sh&#8230;]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><h1>norem for Linux/*BSD</h1>
<p>A small utility written in Bourne shell (compatible with both <strong>sh </strong>and <strong>bash</strong>, which means works without changes on all *BSD/Linux) that strips comments from a source file. E.g. I am too lazy to scroll through <strong>/etc/ssh/sshd_config</strong> file to look for  any valid directives.<br />
Here is <strong>/etc/ssh/sshd_config</strong> on FreeBSD.</p>
<pre class="brush: plain; collapse: true; light: false; title: ; toolbar: true; notranslate">
#       $OpenBSD: sshd_config,v 1.98 2016/02/17 05:29:04 djm Exp $
#       $FreeBSD: releng/10.3/crypto/openssh/sshd_config 296853 2016-03-14 13:05:13Z des $

# This is the sshd server system-wide configuration file.  See
# sshd_config(5) for more information.

# This sshd was compiled with PATH=/usr/bin:/bin:/usr/sbin:/sbin

# The strategy used for options in the default sshd_config shipped with
# OpenSSH is to specify options with their default value where
# possible, but leave them commented.  Uncommented options override the
# default value.

# Note that some of FreeBSD's defaults differ from OpenBSD's, and
# FreeBSD has a few additional options.

#Port 22
#AddressFamily any
#ListenAddress 0.0.0.0
#ListenAddress ::

# The default requires explicit activation of protocol 1
#Protocol 2

# HostKey for protocol version 1
#HostKey /etc/ssh/ssh_host_key
# HostKeys for protocol version 2
#HostKey /etc/ssh/ssh_host_rsa_key
#HostKey /etc/ssh/ssh_host_dsa_key
#HostKey /etc/ssh/ssh_host_ecdsa_key
#HostKey /etc/ssh/ssh_host_ed25519_key

# Lifetime and size of ephemeral version 1 server key
#KeyRegenerationInterval 1h
#ServerKeyBits 1024

# Ciphers and keying
#RekeyLimit default none

# Logging
# obsoletes QuietMode and FascistLogging
#SyslogFacility AUTH
#LogLevel INFO

# Authentication:

#LoginGraceTime 2m
#PermitRootLogin no
#StrictModes yes
#MaxAuthTries 6
#MaxSessions 10

#RSAAuthentication yes
#PubkeyAuthentication yes

# The default is to check both .ssh/authorized_keys and .ssh/authorized_keys2
#AuthorizedKeysFile .ssh/authorized_keys .ssh/authorized_keys2

#AuthorizedPrincipalsFile none

#AuthorizedKeysCommand none
#AuthorizedKeysCommandUser nobody

# For this to work you will also need host keys in /etc/ssh/ssh_known_hosts
#RhostsRSAAuthentication no
# similar for protocol version 2
#HostbasedAuthentication no
# Change to yes if you don't trust ~/.ssh/known_hosts for
# RhostsRSAAuthentication and HostbasedAuthentication
#IgnoreUserKnownHosts no
# Don't read the user's ~/.rhosts and ~/.shosts files
#IgnoreRhosts yes

# Change to yes to enable built-in password authentication.
#PasswordAuthentication no
#PermitEmptyPasswords no

# Change to no to disable PAM authentication
#ChallengeResponseAuthentication yes

# Kerberos options
#KerberosAuthentication no
#KerberosOrLocalPasswd yes
#KerberosTicketCleanup yes
#KerberosGetAFSToken no

# GSSAPI options
#GSSAPIAuthentication no
#GSSAPICleanupCredentials yes

# Set this to 'no' to disable PAM authentication, account processing,
# and session processing. If this is enabled, PAM authentication will
# be allowed through the ChallengeResponseAuthentication and
# PasswordAuthentication.  Depending on your PAM configuration,
# PAM authentication via ChallengeResponseAuthentication may bypass
# the setting of &quot;PermitRootLogin without-password&quot;.
# If you just want the PAM account and session checks to run without
# PAM authentication, then enable this but set PasswordAuthentication
# and ChallengeResponseAuthentication to 'no'.
#UsePAM yes

#AllowAgentForwarding yes
#AllowTcpForwarding yes
#GatewayPorts no
#X11Forwarding yes
#X11DisplayOffset 10
#X11UseLocalhost yes
#PermitTTY yes
#PrintMotd yes
#PrintLastLog yes
#TCPKeepAlive yes
#UseLogin no
#UsePrivilegeSeparation sandbox
#PermitUserEnvironment no
#Compression delayed
#ClientAliveInterval 0
#ClientAliveCountMax 3
#UseDNS yes
#PidFile /var/run/sshd.pid
#MaxStartups 10:30:100
#PermitTunnel no
#ChrootDirectory none
#VersionAddendum FreeBSD-20160310

# no default banner path
#Banner none

# override default of no subsystems
Subsystem       sftp    /usr/libexec/sftp-server

# Example of overriding settings on a per-user basis
#Match User anoncvs
#       X11Forwarding no
#       AllowTcpForwarding no
#       PermitTTY no
#       ForceCommand cvs server
</pre>
<p>With this utility, I can just do:</p>
<pre class="brush: bash; title: ; notranslate">
norem -f /etc/ssh/sshd_config
</pre>
<p>&#8230; and voila&#8230;You have the meat without the bones.</p>
<pre class="brush: plain; title: ; notranslate">
Subsystem       sftp    /usr/libexec/sftp-server
</pre>
<p>Almost all *nix utilities have &#8220;#&#8221; as a comment, but some languages such as Java and C++ use &#8220;//&#8221; for comments. In this case, we have to run:</p>
<pre class="brush: bash; title: ; notranslate">
norem -f file -c &quot;/&quot;
</pre>
<p>The utility is not smart enough for multi-line comments such as &#8220;/*&#8230;*/&#8221;<br />
Here is the source:</p>
<pre class="brush: bash; title: ; notranslate">
#!/bin/sh

usage()
{
    echo &quot;usage: norem &#x5B;-f file ] | &#x5B;-c char] | &#x5B;-e]] | &#x5B;-h]]&quot;
    echo &quot;Prints a file skipping the lines that start with -c&quot;
    echo &quot;By default empty lines are not printed, use -e yes to include them&quot;
    echo &quot;Kliment Andreev - 2016&quot;
}

if &#x5B; &quot;$#&quot; == &quot;0&quot; ]; then
        usage
        exit 1
fi

while &#x5B; $# -gt 0 ]; do
        key=&quot;$1&quot;

        case $key in
                -f|--file)
                        FILENAME=&quot;$2&quot;
                    shift
                        ;;
        -c|--char)
                        CHARACTER=&quot;$2&quot;
                        shift
                ;;
                -e|--empty)
                EMPTY=&quot;$2&quot;
                shift
                ;;
        *)
                usage
                        exit
                ;;
        esac
        shift
done

if &#x5B; -z &quot;${CHARACTER}&quot; ]; then
        CHARACTER=&quot;#&quot;
fi

if &#x5B; -z &quot;${EMPTY}&quot; ]; then
        cat ${FILENAME} | sed &quot;/^\\${CHARACTER}/d&quot; | awk /./
else
        cat ${FILENAME} |sed &quot;/^\\${CHARACTER}/d&quot;
fi
</pre>
<h1>norem for PowerShell</h1>
<p>The same utility for PowerShell. The input parameters are the same.</p>
<pre class="brush: powershell; title: ; notranslate">
Param(
	&#x5B;string]$fileName,
	&#x5B;string]$char=&quot;#&quot;,
	&#x5B;string]$empty
)

function usage {
    Write-Host &quot;usage: norem &#x5B;-f file ] | &#x5B;-c char] | &#x5B;-e]]&quot;
    Write-Host &quot;Prints a file skipping the lines that start with -c&quot;
    Write-Host &quot;By default empty lines are not printed, use -e yes to include them&quot;
    Write-Host &quot;Kliment Andreev - 2016&quot;
}

if ($psboundparameters.Count -eq 0) {
    usage
    exit
}

if ($empty.ToUpper().Contains(&quot;Y&quot;)) {
    Get-Content $fileName | Where { $_ -notmatch &quot;^&quot; + $char }
}
else {
    Get-Content $fileName | Where { $_ -notmatch &quot;^&quot; + $char } | Where {$_.trim() -ne &quot;&quot;}
}
</pre>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.andreev.it/2016/12/103-norem-utility-remove-comments-source/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
