DEV Community: Brandon Olin [1x Engineer] The latest articles on DEV Community by Brandon Olin [1x Engineer] (@devblackops). https://dev.to/devblackops https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F198151%2F065d21ed-fe19-4328-a2a4-0cd22061d029.png DEV Community: Brandon Olin [1x Engineer] https://dev.to/devblackops en 9 Tips for Writing Better PowerShell Functions Brandon Olin [1x Engineer] Sat, 07 Dec 2019 00:00:00 +0000 https://dev.to/devblackops/9-tips-for-writing-better-powershell-functions-4ai6 https://dev.to/devblackops/9-tips-for-writing-better-powershell-functions-4ai6 <p>One of the most common tasks in PowerShell is writing PowerShell functions. Functions are one of the basic building blocks we use to separate and abstract our code away. Without them, our scripts would be just a tangled mess of <code>if</code> statements, <code>while</code> and <code>for</code> loops, and duplicated code.</p> <p>Functions allow us to package our PowerShell logic into discreet blocks we can call, pass parameters to affect how they work, and reuse them so we can follow DRY (Don't Repeat Yourself) principals.</p> <p>PowerShell has a lot of functionality tucked away into functions that sometimes are not known, ignored, or forgotten about entirely. Let's talk about some basic things we can add to functions that improve our scripts and ultimately make us better tool makers.</p> <h2> TOC </h2> <ul> <li>Tip 1: Functions Should Do One Thing</li> <li>Tip 2: Functions Should Be Testable</li> <li>Tip 3: Functions Should Be Self-Contained</li> <li>Tip 4: Add Comment-Based Help</li> <li>Tip 5: Use The PowerShell Function Naming Convention</li> <li>Tip 6: Leverage Advanced Functions</li> <li>Tip 7: Support the Pipeline</li> <li>Tip 8: Support -WhatIf If Making Changes</li> <li>Tip 9: Support -Confirm if Making Changes</li> </ul> <h2> Tip #1: Functions Should Do One Thing </h2> <p>First off, let's make one thing clear. Functions should do one thing and one thing only.</p> <blockquote> <p>Give me a ping Vasili, one ping only please.<br> <br>- Capt. Marko Ramius - The Hunt for Red October</p> </blockquote> <p>I've seen countless PowerShell functions that try to cram in entirely too much logic and end up being an unwieldy mess. These large, unfocused functions end up being hard to understand as they have no clear purpose. They can perform in strange ways, utterly unrelated at all to what the user <em>thinks</em> the function does.</p> <p>I'll admit, I do this myself sometimes (hey nobody's perfect). This is especially true when you're writing code for the first time to perform a new task. It's easy to fall into the trap of handling this one edge case here, this other edge case there, maybe output this variable if <em>these</em> set of conditions are true, etc.</p> <p>We must recognize when this happens, and to correct the situation as soon as possible. If we don't, we'll end up struggling to support an unmaintainable function, our productivity suffers, and the users of our code become frustrated at the lack of focus.</p> <h2> Tip #2: Functions Should Be Testable </h2> <p>In short, single-purpose PowerShell functions are easier to write tests for in tools like <a href="proxy.php?url=https://github.com/pester/Pester">Pester</a>. We can create tests for the (hopefully one or just a few) different scenarios of expected input parameters and validate the function produced the expected output. If the function does too many things, writing unit tests becomes difficult or almost impossible.</p> <p>It's better to have a handful of small, discreet functions with quality unit tests than one large function with no or poor unit tests.</p> <h2> Tip #3: Functions Should Be Self-Contained </h2> <p>It's almost a certainty that your PowerShell function is working with variables in some way. A good practice is to supply the function with all the <strong>external</strong> variables it may need to perform its task as parameters into the function.</p> <p>If our function only <strong>reads</strong> external variables, it's a good idea to add parameters to the function, with the default value being the external variable.</p> <p>A big reason to do this is it helps make writing unit tests for functions easier.<br> We can confidently test a function in isolation, and verify the outputs if we supply all the required information directly into the function.</p> <p>When functions access external variables and make assumptions on their contents, hard to troubleshoot bugs can occur if those variables had their contents modified but another function.</p> <h2> Tip #4: Add Comment-Based Help </h2> <p>PowerShell has this excellent feature called <a href="proxy.php?url=https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_comment_based_help?view=powershell-6">comment-based help</a>. Use it to provide users of your code clear guidance on what it does and how to use it.</p> <p>When PowerShell parses your function, the comment-based help contained within is accessible via the builtin <a href="proxy.php?url=https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/get-help?view=powershell-6">Get-Help</a> cmdlet. The help text for a function doesn't need to be a novel, but clear and accurate help on what the function does, descriptions for each parameter, and useful examples for how to use the function is one of the best ways to delight the users of your function. Conversely, having no or inaccurate help is one of the easiest ways to frustrate the users of your software with the genuine possibility of them stopping to use it at all.</p> <h2> Tip #5: Use The PowerShell Function Naming Convention </h2> <p>PowerShell uses a <code>Verb</code>-<code>Noun</code> syntax for functions and cmdlets. You can see the list of approved verbs with descriptions about what they are intended for by running the <code>Get-Verb</code> command.</p> <p>A big reason for the approved verb list is consistency. The creators of PowerShell wanted it to provide a consistent experience to the user. This consistently also enhances PowerShell's readability.</p> <p>If a function is called <code>Get-IpAddress</code>, there is little ambiguity as to that the function does. It returns an IP address. It <strong>does not</strong> set the IP address or otherwise make changes to the system.</p> <p>If we wanted to set the IP address, it would be intuitive to have a <code>Set-IpAddress</code> function.</p> <p>I've seen many custom PowerShell functions use the <code>Get</code> verb but internally make non-obvious changes to the system. This not only causes confusion because you're wondering: "Hey! I just got my IP address. Why did my recycle bin get cleared out?" It also has the potential to cause harm, as the actions of the function does not match what the user expects.</p> <p><strong>This is a big no-no in PowerShell</strong>.</p> <h2> Tip #6: Leverage Advanced Functions </h2> <p>PowerShell gives you a excellent set of features you can use in functions for practically free <strong>if</strong> you make them <a href="proxy.php?url=https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_advanced?view=powershell-6">advanced functions</a>. By adding the <a href="proxy.php?url=https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_cmdletbindingattribute?view=powershell-6">[CmdletBinding()]</a> attribute to a function, you now have access to a set of <a href="proxy.php?url=https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_commonparameters?view=powershell-5.1">common parameters</a> you can utilize for things like:</p> <ul> <li>Writing verbose messages</li> <li>Supporting <code>-Confirm</code> and <code>-WhatIf</code> modes</li> <li>Parameter validation</li> <li>Support input from the pipeline</li> <li><a href="proxy.php?url=https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_advanced_parameters?view=powershell-6">Advanced parameters</a></li> </ul> <p>When creating new functions, it is a good practice to make them advanced functions as a matter of course. This allows you to support the features above easily should you need them.</p> <h2> Tip #7: Support the Pipeline </h2> <p>This is an extension of Tip #6, but I feel it deserves its own section. If you're adhering to Tip #1 and writing functions that do one thing only, then supporting the pipeline is an important feature to add so functions can be chained together to perform more complex logic.</p> <p>Advanced functions add support for the <a href="proxy.php?url=https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_advanced_parameters?view=powershell-6#valuefrompipeline-argument">ValueFromPipeline</a> argument attached to parameters via the <a href="proxy.php?url=https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_advanced_parameters?view=powershell-6#parameter-attribute">Parameter</a> attribute.</p> <p>Adding this argument indicates to PowerShell that the value of the parameter can come from a pipeline object. This allows you to stream the value of the parameter from the output of another function/cmdlet.</p> <p>For more information about the PowerShell pipeline, check out the <a href="proxy.php?url=https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_pipelines?view=powershell-6">about_pipelines</a> help doc or run <code>Get-Help about_pipelines</code> from PowerShell itself.</p> <h2> Tip #8: Support -WhatIf If Making Changes </h2> <p>PowerShell's support of the <code>-WhatIf</code> switch parameter is analogous to <code>--dry-run</code> or <code>--noop</code> in other tools. When specifying the <code>-WhatIf</code> switch, what you're essentially telling PowerShell is: "Tell me what you're going to do, but don't <em>actually</em> do it."</p> <p>It's a great tool to have when running PowerShell interactively to validate a series of actions that could potentially have serious effects <strong>without actually making them</strong>.</p> <p>Imagine you needed to run a command to delete files from a directory recursively. You want to delete all <code>.log</code> files from the <code>C:\tmp</code> directory but also want to be extra careful that you don't inadvertently delete other files, so you add the <code>-WhatIf</code> switch to <code>Remove-Item</code> like so:<br> </p> <div class="highlight"><pre class="highlight powershell"><code><span class="nf">Get-ChildItem</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nx">C:\tmp</span><span class="w"> </span><span class="nt">-File</span><span class="w"> </span><span class="o">*.</span><span class="nf">log</span><span class="w"> </span><span class="nt">-Recurse</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">Remove-Item</span><span class="w"> </span><span class="nt">-WhatIf</span><span class="w"> </span></code></pre></div> <p>With this, you can see all the files that <code>Remove-Item</code> <strong>would have</strong> deleted if you hadn't provided the <code>-WhatIf</code> switch. After validating the files displayed, you safely remove the <code>-WhatIf</code> switch and rerun the command to remove the files.</p> <p><code>-WhatIf</code> is only supported in advanced functions, so it’s another reason to implement Tip #6. When authoring functions that affect the system in some way, add <code>-WhatIf</code> support via the <code>SupportsShouldProcess</code> argument on the <a href="proxy.php?url=https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_cmdletbindingattribute?view=powershell-6">[CmdletBinding()]</a> attribute.</p> <p>Check out <a href="proxy.php?url=https://twitter.com/proxb">Boe Prox's</a> article about <a href="proxy.php?url=https://learn-powershell.net/2013/04/30/scripting-games-2013-use-of-supportsshouldprocess-in-functionsscripts/">Adding -WhatIf Support to Functions and Scripts</a>.</p> <h2> Tip #9: Support -Confirm if Making Changes </h2> <p>Related to Tip #8, the <code>-Confirm</code> switch indicates to PowerShell to pause before every action and display an interactive prompt asking you to <strong>confirm</strong> the action before it executes. It will do this for every object in the pipeline, allowing you to inspect the item before continuing.</p> <p>Just like with <code>-WhatIf</code> support, adding <code>-Confirm</code> support is only possible with advanced functions, so Tip #6 still applies.</p> <p>Check out <a href="proxy.php?url=https://twitter.com/vexx32">vex32's</a> excellent article about <a href="proxy.php?url=https://vexx32.github.io/2018/11/22/Implementing-ShouldProcess/">adding support for -Confirm</a> to your functions.</p> <h2> Closing </h2> <p>One of the problems with making a list of tips is knowing when to stop. There are many more tips I <strong>could</strong> have added about writing functions or even a list of things to definitely <strong>not</strong> do. Those will have to wait for another day.</p> <p>Cheers!</p> <p><em>Icon courtesy of <a href="proxy.php?url=https://icon-library.net/icon/tip-icon-3.html">Tip Icon #388700</a></em></p> powershell beginners codenewbie programming Infrastructure Testing with Pester and the Operation Validation Framework Brandon Olin [1x Engineer] Sat, 20 Jul 2019 04:41:42 +0000 https://dev.to/devblackops/infrastructure-testing-with-pester-and-the-operation-validation-framework-1a5p https://dev.to/devblackops/infrastructure-testing-with-pester-and-the-operation-validation-framework-1a5p <p>If you've been using PowerShell for any length of time in the past few years, you have undoubtedly heard of Pester. If not, then you're probably living in a strange parallel universe where the Zune is still a thing. In any case, Pester is <strong>THE</strong> testing framework for PowerShell and is a must-have tool in your Infrastructure Developer toolbox.</p> <p>I say Infrastructure Developer because that is what we are. If you write production code to automate your infrastructure, then you are not a Systems Engineer or Administrator, a SharePoint Engineer or anything else, you are a developer. <strong>Full stop</strong>.</p> <p>The fact that we write PowerShell code that defines or runs IT infrastructure is not any different than a web developer using CSS, JavaScript, and HTML, or a full-stack ninja rockstar slinging micro-services written in Go on <a href="proxy.php?url=https://kubernetes.io/" rel="noopener noreferrer">Kubernetes</a>.</p> <blockquote> <p>Everything involves working with code therefore <strong>everyone</strong> is a developer.<br> <br>- Sun Tzu</p> </blockquote> <p>Your traditional developer working with C# or Java tests their code. Your traditional Windows administrator using PowerShell to automate their jobs away (you won't) <strong>SHOULD</strong> test their code. Even if you're not writing tests in <a href="proxy.php?url=https://github.com/pester/Pester" rel="noopener noreferrer">Pester</a>, <a href="proxy.php?url=http://rspec.info/" rel="noopener noreferrer">RSpec</a>, <a href="proxy.php?url=http://specflow.org/" rel="noopener noreferrer">SpecFlow</a>, etc, you are testing your code. Or I should say, your <strong>users are testing your code for you</strong>.</p> <h2> What about your Infrastructure? </h2> <p>Do you test your infrastructure to verify it is working the way you expect? Are your services configured according to your specifications? Your infrastructure is not a static object; never changing and forever in the state you expect.</p> <p><strong>If you're not actively testing your infrastructure, don't worry, your users WILL let you know when it's not working :)</strong></p> <blockquote> <p>Things change. Always.<br> <br>- Abraham Lincoln</p> </blockquote> <p>Servers get provisioned or deprovisioned, new applications come online, or old ones fade away into the sunset. What about the gremlins chewing on the wires causing latency between services, or buggy code (cough...not yours of course) causing applications to crash? What about that outage last week where you manually tweaked a setting but forgot to backport it into your DSC configuration? All of these things create a living, breathing, dynamic environment. <strong>That environment must be tested to ensure your infrastructure reality matches your desired state.</strong></p> <p>This is where tools like Pester and the Operation Validation Framework can help.</p> <h2> Operation Validation? </h2> <p>You know what Pester is and know that you use it to test the functionality of your PowerShell scripts/modules. Did you know you can <strong>also</strong> use it to test your infrastructure? When you think about it, all Pester is doing at the end of the day is comparing a value on the left (<strong>your actual state</strong>), to a value on the right (<strong>your desired state</strong>) and raising an alarm when these don't match.</p> <h4> Get-Answer.ps1 </h4> <div class="highlight js-code-highlight"> <pre class="highlight powershell"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Get-Answer</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="kr">param</span><span class="p">(</span><span class="w"> </span><span class="p">[</span><span class="n">parameter</span><span class="p">(</span><span class="n">Mandatory</span><span class="p">)]</span><span class="w"> </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$Question</span><span class="w"> </span><span class="p">)</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$Question</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s1">'Answer to the Ultimate Question of Life, the Universe, and Everything'</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="mi">42</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre> </div> <h4> Get-Answer.tests.ps1 </h4> <div class="highlight js-code-highlight"> <pre class="highlight powershell"><code><span class="n">describe</span><span class="w"> </span><span class="s1">'Get-Answer'</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">context</span><span class="w"> </span><span class="s1">'Correct output'</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">it</span><span class="w"> </span><span class="s1">'Returns the correct value'</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">Get-Answer</span><span class="w"> </span><span class="nt">-Question</span><span class="w"> </span><span class="s1">'Answer to the Ultimate Question of Life, the Universe, and Everything'</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Should</span><span class="w"> </span><span class="nt">-Be</span><span class="w"> </span><span class="nx">42</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre> </div> <p>The test above is validating the output of our <code>Get-Answer</code> function. Now take a look at an infrastructure test:</p> <h4> os.services.tests.ps1 </h4> <div class="highlight js-code-highlight"> <pre class="highlight powershell"><code><span class="n">describe</span><span class="w"> </span><span class="s1">'Operating System'</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">context</span><span class="w"> </span><span class="s1">'Service Availability'</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">it</span><span class="w"> </span><span class="s1">'Eventlog is running'</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">$svc</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Service</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="nx">Eventlog</span><span class="w"> </span><span class="nv">$svc</span><span class="o">.</span><span class="nf">Status</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Should</span><span class="w"> </span><span class="nt">-Be</span><span class="w"> </span><span class="nx">running</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre> </div> <p>Looks similar to me 😊</p> <h2> Operation Validation Framework </h2> <p>Now that we know we can use Pester to test our infrastructure, what do we do with these tests and how do we execute them? What if we wanted to <strong>version</strong> these tests and <strong>publish</strong> them? <strong>Hey, that sounds like a PowerShell module!</strong></p> <blockquote> <p>Your ideas are intriguing to me and I wish to subscribe to your newsletter.<br> <br>- Sir Isaac Newton</p> </blockquote> <p>This is where the <a href="proxy.php?url=https://github.com/PowerShell/Operation-Validation-Framework" rel="noopener noreferrer">Operation Validation Framework</a> comes into play. It is a PowerShell module that searches for Pester tests contained in a defined folder structure in your other PowerShell modules and ...wait for it... executes them with Pester. That's it. By just putting our Pester tests in a module, we can now version them, publish them, and execute them!</p> <h3> Folder Structure </h3> <p>The Operation Validation Framework, or just OVF, expects Pester tests inside a known location in your module. If you put Pester tests inside a <code>Diagnostics\Simple</code> or <code>Diagnostics\Comprehensive</code> folder under your module, OVF can find these and execute them. The <code>Simple</code> folder is intended for tests that are quick and non-intrusive to execute. These tests could be executed every few minutes with little impact. The <code>Comprehensive</code> tests would be for more involved and take longer to execute. You’d probably run these every few hours or once a day.</p> <ul> <li>MyTestModule\ <ul> <li>MyTestModule.psd1</li> <li>Diagnostics\</li> <li>Simple\ <ul> <li>services.tests.ps1</li> <li>logicaldisks.tests.ps1</li> </ul> </li> <li>Comprehensive\ <ul> <li>performance.tests.ps1</li> </ul> </li> </ul> </li> </ul> <p>As an example, let’s say we have a PowerShell module with the structure above. This module includes the Pester tests below. Notice that both Pester test scripts have parameters that define some default values. Pester has this nifty feature where you can invoke a test script and inject parameters into it. This allows you to provide some sane defaults for your tests yet allow the user to override them if they need to. OVF also supports this feature. This means that you can write <strong>generic</strong> OVF modules designed to test a certain product or OS feature and publish them to the PowerShell Gallery! Users can then download and execute these, overriding the default parameters if necessary to fit their environment. We’d have common infrastructure tests that the whole community could use!</p> <p>Myself and others have already published a few OVF modules to the <a href="proxy.php?url=https://www.powershellgallery.com/packages?q=ovf&amp;x=0&amp;y=0" rel="noopener noreferrer">PowerShell Gallery</a>. Check out these simple OVF modules to test Windows Server, Active Directory, SharePoint, and Citrix ShareFile.</p> <ul> <li><a href="proxy.php?url=https://www.powershellgallery.com/packages/OVF.Windows.Server/" rel="noopener noreferrer">OVF.Windows.Server</a></li> <li><a href="proxy.php?url=https://www.powershellgallery.com/packages/OVF.Active.Directory/" rel="noopener noreferrer">OVF.Active.Directory</a></li> <li><a href="proxy.php?url=https://www.powershellgallery.com/packages/OVF.SharePoint/" rel="noopener noreferrer">OVF.SharePoint</a></li> <li><a href="proxy.php?url=https://www.powershellgallery.com/packages/OVF.ShareFile" rel="noopener noreferrer">OVF.ShareFile</a></li> </ul> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fxphvb6pi743w6wf01532.jpg" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fxphvb6pi743w6wf01532.jpg" alt="Do you know what this means?"></a></p> <h4> services.tests.ps1 </h4> <div class="highlight js-code-highlight"> <pre class="highlight powershell"><code><span class="kr">param</span><span class="p">(</span><span class="w"> </span><span class="nv">$Services</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="w"> </span><span class="s1">'DHCP'</span><span class="p">,</span><span class="w"> </span><span class="s1">'DNSCache'</span><span class="p">,</span><span class="s1">'Eventlog'</span><span class="p">,</span><span class="w"> </span><span class="s1">'PlugPlay'</span><span class="p">,</span><span class="w"> </span><span class="s1">'RpcSs'</span><span class="p">,</span><span class="w"> </span><span class="s1">'lanmanserver'</span><span class="p">,</span><span class="w"> </span><span class="s1">'LmHosts'</span><span class="p">,</span><span class="w"> </span><span class="s1">'Lanmanworkstation'</span><span class="p">,</span><span class="w"> </span><span class="s1">'MpsSvc'</span><span class="p">,</span><span class="w"> </span><span class="s1">'WinRM'</span><span class="w"> </span><span class="p">)</span><span class="w"> </span><span class="p">)</span><span class="w"> </span><span class="n">describe</span><span class="w"> </span><span class="s1">'Operating System'</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">context</span><span class="w"> </span><span class="s1">'Service Availability'</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">$Services</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ForEach-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">it</span><span class="w"> </span><span class="s2">"[</span><span class="bp">$_</span><span class="s2">] should be running"</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="p">(</span><span class="n">Get-Service</span><span class="w"> </span><span class="bp">$_</span><span class="p">)</span><span class="o">.</span><span class="nf">Status</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Should</span><span class="w"> </span><span class="nt">-Be</span><span class="w"> </span><span class="nx">running</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre> </div> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fck2zoquwi5bplognose9.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fck2zoquwi5bplognose9.png" alt="Service test results"></a></p> <h4> logicaldisk.tests.ps1 </h4> <div class="highlight js-code-highlight"> <pre class="highlight powershell"><code><span class="kr">param</span><span class="p">(</span><span class="w"> </span><span class="nv">$FreeSystemDriveMBytesThreshold</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">500</span><span class="p">,</span><span class="w"> </span><span class="nv">$FreeSystemDrivePctThreshold</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">.</span><span class="nf">05</span><span class="p">,</span><span class="w"> </span><span class="nv">$FreeNonSystemDriveMBytesThreshold</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1000</span><span class="p">,</span><span class="w"> </span><span class="nv">$FreeNonSystemDrivePctThreshold</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">.</span><span class="nf">05</span><span class="w"> </span><span class="p">)</span><span class="w"> </span><span class="n">describe</span><span class="w"> </span><span class="s1">'Logical Disks'</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">$vols</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Volume</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">DriveType</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s1">'Fixed'</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="o">-not</span><span class="w"> </span><span class="p">[</span><span class="n">string</span><span class="p">]::</span><span class="n">IsNullOrEmpty</span><span class="p">(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">DriveLetter</span><span class="p">)}</span><span class="w"> </span><span class="n">context</span><span class="w"> </span><span class="s1">'Availablity'</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">$vols</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ForEach-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">it</span><span class="w"> </span><span class="s2">"Volume [</span><span class="si">$(</span><span class="bp">$_</span><span class="o">.</span><span class="nf">DriveLetter</span><span class="si">)</span><span class="s2">] is healthy"</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">HealthStatus</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Should</span><span class="w"> </span><span class="nt">-Be</span><span class="w"> </span><span class="s1">'Healthy'</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="n">context</span><span class="w"> </span><span class="s1">'Capacity'</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">$systemDriveLetter</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">SystemDrive</span><span class="o">.</span><span class="nf">Substring</span><span class="p">(</span><span class="nx">0</span><span class="p">,</span><span class="w"> </span><span class="nx">1</span><span class="p">)</span><span class="w"> </span><span class="nv">$sysVol</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$vols</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="nx">DriveLetter</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="nv">$systemDriveLetter</span><span class="w"> </span><span class="nv">$nonSysVols</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$vols</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="nx">DriveLetter</span><span class="w"> </span><span class="o">-ne</span><span class="w"> </span><span class="nv">$systemDriveLetter</span><span class="w"> </span><span class="n">it</span><span class="w"> </span><span class="s2">"System drive [</span><span class="nv">$systemDriveLetter</span><span class="s2">] has </span><span class="nv">$FreeSystemDriveMBytesThreshold</span><span class="s2"> MB and </span><span class="si">$(</span><span class="s1">'{0:p0}'</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="nv">$FreeSystemDrivePctThreshold</span><span class="si">)</span><span class="s2"> free"</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="p">(</span><span class="nv">$sysVol</span><span class="o">.</span><span class="nf">SizeRemaining</span><span class="w"> </span><span class="n">/</span><span class="w"> </span><span class="nx">1MB</span><span class="p">)</span><span class="w"> </span><span class="o">-ge</span><span class="w"> </span><span class="nv">$FreeSystemDriveMBytesThreshold</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Should</span><span class="w"> </span><span class="nt">-Be</span><span class="w"> </span><span class="bp">$true</span><span class="w"> </span><span class="p">(</span><span class="nv">$sysVol</span><span class="o">.</span><span class="nf">SizeRemaining</span><span class="w"> </span><span class="n">/</span><span class="w"> </span><span class="nv">$sysVol</span><span class="o">.</span><span class="nf">Size</span><span class="p">)</span><span class="w"> </span><span class="o">-ge</span><span class="w"> </span><span class="nv">$FreeSystemDriveThresholdPct</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Should</span><span class="w"> </span><span class="nt">-Be</span><span class="w"> </span><span class="bp">$true</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$volume</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$nonSysVols</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">$driveLetter</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$volume</span><span class="o">.</span><span class="nf">DriveLetter</span><span class="w"> </span><span class="n">it</span><span class="w"> </span><span class="s2">"Non-System drive [</span><span class="nv">$driveLetter</span><span class="s2">] has greater than </span><span class="nv">$FreeNonSystemDriveMBytesThreshold</span><span class="s2"> MB and </span><span class="si">$(</span><span class="s1">'{0:p0}'</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="nv">$FreeNonSystemDrivePctThreshold</span><span class="si">)</span><span class="s2"> free"</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="p">(</span><span class="nv">$volume</span><span class="o">.</span><span class="nf">SizeRemaining</span><span class="w"> </span><span class="n">/</span><span class="w"> </span><span class="nx">1MB</span><span class="p">)</span><span class="w"> </span><span class="o">-ge</span><span class="w"> </span><span class="nv">$FreeNonSystemDriveThreshold</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Should</span><span class="w"> </span><span class="nt">-Be</span><span class="w"> </span><span class="bp">$true</span><span class="w"> </span><span class="p">(</span><span class="nv">$volume</span><span class="o">.</span><span class="nf">SizeRemaining</span><span class="w"> </span><span class="n">/</span><span class="w"> </span><span class="nv">$volume</span><span class="o">.</span><span class="nf">Size</span><span class="p">)</span><span class="w"> </span><span class="o">-ge</span><span class="w"> </span><span class="nv">$FreeNonSystemDriveThresholdPct</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Should</span><span class="w"> </span><span class="nt">-Be</span><span class="w"> </span><span class="bp">$true</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre> </div> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fc51mwidwyhpu5q53829v.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fc51mwidwyhpu5q53829v.png" alt="Logical disk tests"></a></p> <blockquote> <p>Always remember that you are absolutely unique. Just like everyone else.<br> <br>- Mister Rogers</p> </blockquote> <p>Now let’s see how to execute these same tests but using OVF. Since our test module has been installed into <code>$env:PSModulePath</code>, OVF will find it, inspect it, and return a collection of tests. These tests can then be executed with <code>Invoke-OperationValidation</code>. Imagine having your monitoring system running the simple script below and throwing alerts if any of the Pester tests have failed.<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight powershell"><code><span class="n">Import-Module</span><span class="w"> </span><span class="nx">OperationValidation</span><span class="w"> </span><span class="nv">$tests</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-OperationValidation</span><span class="w"> </span><span class="nt">-ModuleName</span><span class="w"> </span><span class="nx">MyTestModule</span><span class="w"> </span><span class="nv">$results</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$tests</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Invoke-OperationValidation</span><span class="w"> </span><span class="nv">$results</span><span class="w"> </span></code></pre> </div> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fsysc3z1g4jbsgajlpqvi.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fsysc3z1g4jbsgajlpqvi.png" alt="Running OVF tests"></a></p> <p>To execute the OVF tests and override the default parameters, we can use the <code>-Overrides</code> parameter. We can also show the Pester output as well. The cool thing about this framework is that you can develop a common module to test a certain technology and then tailor the settings per environment.</p> <p><a href="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fi9po1h59oss97miw7taz.png" class="article-body-image-wrapper"><img src="proxy.php?url=https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fi9po1h59oss97miw7taz.png" alt="Override OVF test parameters"></a></p> <h2> Wrapping Up </h2> <p>Using Pester to test your infrastructure should become a common practice for IT administrators. Everything is starting to be defined in code and like I said at the beginning, we are all developers now. We may be coding infrastructure, but the basics of software development still apply. Shouldn’t we be testing like a developer too?</p> <h2> Further reading </h2> <ul> <li><p><a href="proxy.php?url=https://sysnetdevops.com/2017/06/05/testing-infrastructure-with-pester/" rel="noopener noreferrer">https://sysnetdevops.com/2017/06/05/testing-infrastructure-with-pester/</a></p></li> <li><p><a href="proxy.php?url=https://4sysops.com/archives/an-introduction-to-infrastructure-testing-with-powershell-pester/" rel="noopener noreferrer">https://4sysops.com/archives/an-introduction-to-infrastructure-testing-with-powershell-pester/</a></p></li> <li><p><a href="proxy.php?url=http://www.brycematthew.net/powershell/pester/2017/04/13/Pester-Infrastructure-Testing.html" rel="noopener noreferrer">http://www.brycematthew.net/powershell/pester/2017/04/13/Pester-Infrastructure-Testing.html</a></p></li> <li><p><a href="proxy.php?url=http://wragg.io/getting-started-with-pester-for-operational-testing/" rel="noopener noreferrer">http://wragg.io/getting-started-with-pester-for-operational-testing/</a></p></li> </ul> <p>By the way, the quote attributions in this post may be inaccurate. I can't know for sure. I didn't test them. ;)</p> <p>Cheers</p> powershell infrastructure testing pester Using PoshBot Middleware for Rate-Limiting Notifications in Slack Brandon Olin [1x Engineer] Mon, 15 Jul 2019 00:00:00 +0000 https://dev.to/devblackops/using-poshbot-middleware-for-rate-limiting-notifications-in-slack-51n0 https://dev.to/devblackops/using-poshbot-middleware-for-rate-limiting-notifications-in-slack-51n0 <p>Recently, someone in the <strong>#ChatOps</strong> channel of the <a href="proxy.php?url=http://aks.ms/psslack">PowerShell Slack</a> workspace asked if it’s possible to use <a href="proxy.php?url=https://github.com/poshbotio/PoshBot">PoshBot</a> to send a message recommending people to use <a href="proxy.php?url=https://get.slack.help/hc/en-us/articles/115000769927-Use-threads-to-organize-discussions-">Slack threads</a> if they send over (x) amount of messages in (y) amount of time. I suppose he wanted to encourage threaded conversations to reduce the clutter in his Slack workspace. Here’s the solution I sent him that I adapted from a <a href="proxy.php?url=https://stackoverflow.com/questions/667508/whats-a-good-rate-limiting-algorithm">Stack Overflow question about rate-limiting</a>. What we’ll use to track user message rate is known as the <a href="proxy.php?url=https://en.wikipedia.org/wiki/Token_bucket">token bucket</a> algorithm.</p> <h2> PoshBot Middleware </h2> <p>PoshBot has the concept of <a href="proxy.php?url=http://docs.poshbot.io/en/latest/guides/middleware/#middleware-hooks">middleware hooks</a>, which is the ability to execute custom PowerShell scripts during certain events in the command processing lifecycle. These hooks can do pretty much anything you want. After all, they are just PowerShell scripts. If you follow the conventions outlined in the documentation, they are pretty straightforward to set up and can extend the utility of our ChatOps tooling.</p> <h2> Middleware Hook Stages </h2> <p>There are <strong>six</strong> different stages that middleware can execute. Middleware can modify the command received, the response back to the backend, or even to drop the message entirely and not allow it to execute. You pick the appropriate stage depending on what your middleware is doing.</p> <div class="table-wrapper-paragraph"><table> <thead> <tr> <th>Name</th> <th>Description</th> </tr> </thead> <tbody> <tr> <td>PreReceive</td> <td>Runs before PoshBot “receives” the message from the <a href="proxy.php?url=http://docs.poshbot.io/en/latest/tutorials/backend-development/overview/">backend</a> </td> </tr> <tr> <td>PostReceive</td> <td>Runs after the message is “received” from the backend, parsed, and matched with a registered bot command</td> </tr> <tr> <td>PreExecute</td> <td>Runs before a command is executed</td> </tr> <tr> <td>PostExecute</td> <td>Runs after a command has been executed but before responses are sent to the backend</td> </tr> <tr> <td>PreResponse</td> <td>Runs before responses are sent to the backend</td> </tr> <tr> <td>PostResponse</td> <td>Runs after responses have been sent to the backend</td> </tr> </tbody> </table></div> <h2> Adding the Middleware Hook </h2> <p>Middleware hooks are added to your <a href="proxy.php?url=http://docs.poshbot.io/en/latest/guides/configuration/">bot configuration</a> under the property called <code>MiddlewareConfiguration</code>. For the rate-limiting example I created, we’re going to use the <code>PreReceive</code> stage because this middleware is intended to count normal messages occurring in Slack, not necessary just bot commands. All the other stages run <strong>after</strong> a message has been parsed and matched to the bot command. If we used any other stage, we’d only be measuring the rate of bot commands, not normal Slack messages.</p> <p>In your bot configuration <code>.psd1</code> file, add the following to the <code>MiddlewareConfiguration</code> property. Adjust the hook name and path as desired.<br> </p> <div class="highlight"><pre class="highlight plaintext"><code>@{ # # Other sections omitted for brevity # MiddlewareConfiguration = @{ PreReceive = @{ Name = 'RateLimiter' Path = 'C:/Users/Brandon/.poshbot/middleware/rate_limiting_notice.ps1' } # PostReceive = @{ # Name = '' # Path = '' # } # PreExecute = @{ # Name = '' # Path = '' # } # PostExecute = @{ # Name = '' # Path = '' # } # PreResponse = @{ # Name = '' # Path = '' # } # PostResponse = @{ # Name = '' # Path = '' # } } } </code></pre></div> <p>Now let’s look at <code>rate_limiting_notice.ps1</code> and go through each section.</p> <h3> rate_limiting_notice.ps1 </h3> <blockquote> <p>The full script is found in this <a href="proxy.php?url=https://gist.github.com/devblackops/2b02fbb946a421de6efb5b9c8ce7d79b">GitHub gist</a>.</p> </blockquote> <p>PoshBot middleware hooks are just standard PowerShell scripts, but PoshBot expects <strong>two</strong> parameters to be available and passes specific objects to them.</p> <p><code>$Context</code> is a PowerShell object containing a ton of information about the incoming message received from the backend. Who sent the message, what channel it was in, the raw JSON message from the backend, etc. Our middleware needs to accept this object as the first parameter to the script.</p> <p><code>$Bot</code> is the main PoshBot instance object. It is essentially a PowerShell class instance with a bunch of methods implementing all the bot logic. Our middleware is given access to this object so we can perform deep modification of PoshBot internals. <strong>Remember, with great power comes great responsibility</strong>.<br> </p> <div class="highlight"><pre class="highlight powershell"><code><span class="cm">&lt;# </span><span class="cs">.SYNOPSIS</span><span class="cm"> Suggest Slack threads for talkative users. </span><span class="cs">.DESCRIPTION</span><span class="cm"> This middleware tracks how many messages (x) users send per (y) amount of time. If a user goes over the threshold, we'll send a message suggesting that Slack threads should be used. </span><span class="cs">.NOTES</span><span class="cm"> Based on https://stackoverflow.com/questions/667508/whats-a-good-rate-limiting-algorithm #&gt;</span><span class="w"> </span><span class="kr">param</span><span class="p">(</span><span class="w"> </span><span class="nv">$Context</span><span class="p">,</span><span class="w"> </span><span class="nv">$Bot</span><span class="w"> </span><span class="p">)</span><span class="w"> </span></code></pre></div> <p>This next section is where we’ll tell PoshBot to log a message that this middleware hook is starting and defining our rate-limiting values. We’ll also pull out the calling user ID from the context object. We define our rate-limiting window in seconds but to allow greater precision for the actual measurements, we’ll use milliseconds internally.</p> <p>We also need to be sure we DON’T measure messages already in a threaded conversation, or to count other messages PoshBot receives about updates to threaded conversations. If we don’t exclude these, our rate-limiting won’t work correctly and we’ll pester people to use threaded conversations when they already are and that would be…awkward.<br> </p> <div class="highlight"><pre class="highlight powershell"><code><span class="nv">$Bot</span><span class="o">.</span><span class="nf">LogDebug</span><span class="p">(</span><span class="s1">'Beginning message ratelimit middleware'</span><span class="p">)</span><span class="w"> </span><span class="c"># We'll allow (5) messages per user in a (60) second window before suggesting threads</span><span class="w"> </span><span class="nv">$maxMsgs</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">5</span><span class="w"> </span><span class="nv">$timePeriod</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">60</span><span class="w"> </span><span class="nv">$userId</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Context</span><span class="o">.</span><span class="nf">Message</span><span class="o">.</span><span class="nf">From</span><span class="w"> </span><span class="nv">$timePeriodMS</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$timePeriod</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">1000</span><span class="w"> </span><span class="c"># Only measure messages NOT already in a thread</span><span class="w"> </span><span class="c"># This middleware hook stage also receives extra messages whenever a user replies in a thread</span><span class="w"> </span><span class="c"># We need to ensure we DON'T count these against the rate-limiting</span><span class="w"> </span><span class="nv">$unThreadedMsg</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="w"> </span><span class="p">([</span><span class="kt">string</span><span class="p">]::</span><span class="nf">IsNullOrWhiteSpace</span><span class="p">(</span><span class="nv">$Context</span><span class="o">.</span><span class="nf">Message</span><span class="o">.</span><span class="nf">RawMessage</span><span class="o">.</span><span class="nf">thread_ts</span><span class="p">)</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="p">(</span><span class="nv">$Context</span><span class="o">.</span><span class="nf">Message</span><span class="o">.</span><span class="nf">RawMessage</span><span class="o">.</span><span class="nf">type</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s1">'message'</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="nv">$Context</span><span class="o">.</span><span class="nf">Message</span><span class="o">.</span><span class="nf">RawMessage</span><span class="o">.</span><span class="nf">subtype</span><span class="w"> </span><span class="o">-ne</span><span class="w"> </span><span class="s1">'message_replied'</span><span class="p">))</span><span class="w"> </span><span class="p">)</span><span class="w"> </span></code></pre></div> <p>Next, assuming we’re processing an <strong>unthreaded</strong> message, we’ll either load up a tracking object or create a new one if it doesn’t exist. We’re storing this data as a <code>CLIXML</code> file, so we need to use <code>Import-Clixml</code> to retrieve the object. This tracker is a hashtable with the user ID as the key, and a hashtable containing the user’s current message allowance and the last time they sent a message as the value.<br> </p> <div class="highlight"><pre class="highlight powershell"><code><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$unThreadedMsg</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="c"># Load the tracker</span><span class="w"> </span><span class="nv">$trackerPath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">Join-Path</span><span class="w"> </span><span class="nv">$Bot</span><span class="o">.</span><span class="nf">Configuration</span><span class="o">.</span><span class="nf">ConfigurationDirectory</span><span class="w"> </span><span class="s1">'msg_ratelimiting_tracking.clixml'</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nf">Test-Path</span><span class="w"> </span><span class="nv">$trackerPath</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">$tracker</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">Import-Clixml</span><span class="w"> </span><span class="nv">$trackerPath</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">$tracker</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w"> </span><span class="nv">$userId</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">@{</span><span class="w"> </span><span class="nx">Allowance</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$maxMsgs</span><span class="w"> </span><span class="nx">LastMsgTime</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">[</span><span class="nx">datetime</span><span class="err">]::</span><span class="nx">UtcNow</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div> <p>Next, we’ll get the current time and determine how many milliseconds it’s been since the user last sent a message. This value is then used to calculate our bucket allowance.<br> </p> <div class="highlight"><pre class="highlight powershell"><code><span class="nv">$now</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="kt">datetime</span><span class="p">]::</span><span class="nf">UtcNow</span><span class="w"> </span><span class="nv">$timePassed</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nv">$now</span><span class="w"> </span><span class="nf">-</span><span class="w"> </span><span class="nv">$tracker</span><span class="p">[</span><span class="nv">$userId</span><span class="p">]</span><span class="o">.</span><span class="nf">LastMsgTime</span><span class="p">)</span><span class="o">.</span><span class="nf">TotalSeconds</span><span class="w"> </span><span class="nv">$tracker</span><span class="p">[</span><span class="nv">$userId</span><span class="p">]</span><span class="o">.</span><span class="nf">LastMsgTime</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$now</span><span class="w"> </span><span class="nv">$tracker</span><span class="p">[</span><span class="nv">$userId</span><span class="p">]</span><span class="o">.</span><span class="nf">Allowance</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="nv">$timePassed</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="p">(</span><span class="nv">$maxMsgs</span><span class="w"> </span><span class="nf">/</span><span class="w"> </span><span class="nv">$timePeriodMS</span><span class="p">)</span><span class="w"> </span></code></pre></div> <p>We now need to look at our allowance and determine if we’ve breached the rate limit set. If we have <code>&lt;1</code> allowance, then we send a friendly message back to Slack information the user that perhaps they should use Slack threads. We do this by creating a <code>Response</code> object, which is a class internal to PoshBot that represents a message we want to send to the backend chat network. We then reset the user’s allowance so we won’t keep on sending this message unless they breach the limit again. Lastly, we’ll save this data back to disk with <code>Export-Clixml</code>.<br> </p> <div class="highlight"><pre class="highlight powershell"><code><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$tracker</span><span class="p">[</span><span class="nv">$userId</span><span class="p">]</span><span class="o">.</span><span class="nf">Allowance</span><span class="w"> </span><span class="o">-lt</span><span class="w"> </span><span class="mf">1.0</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">$Bot</span><span class="o">.</span><span class="nf">LogDebug</span><span class="p">(</span><span class="s2">"User [</span><span class="nv">$userId</span><span class="s2">] has breached ratelimit of [</span><span class="nv">$maxMsgs</span><span class="s2">] messages in [</span><span class="nv">$timePeriod</span><span class="s2">)] seconds. Sending thread reminder response"</span><span class="p">)</span><span class="w"> </span><span class="nv">$response</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="kt">Response</span><span class="p">]::</span><span class="nf">new</span><span class="p">()</span><span class="w"> </span><span class="nv">$response</span><span class="o">.</span><span class="nf">To</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Context</span><span class="o">.</span><span class="nf">Message</span><span class="o">.</span><span class="nf">To</span><span class="w"> </span><span class="nv">$response</span><span class="o">.</span><span class="nf">MessageFrom</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Context</span><span class="o">.</span><span class="nf">Message</span><span class="o">.</span><span class="nf">From</span><span class="w"> </span><span class="nv">$response</span><span class="o">.</span><span class="nf">OriginalMessage</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Context</span><span class="o">.</span><span class="nf">Message</span><span class="w"> </span><span class="nv">$mentionUser</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"&lt;@</span><span class="si">$(</span><span class="nv">$Context</span><span class="o">.</span><span class="nf">Message</span><span class="o">.</span><span class="nf">From</span><span class="si">)</span><span class="s2">&gt;"</span><span class="w"> </span><span class="nv">$text</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Hey </span><span class="nv">$mentionUser</span><span class="s2">, we noticed you have a lot to say. Perhaps creating a Slack thread would be useful."</span><span class="w"> </span><span class="nv">$response</span><span class="o">.</span><span class="nf">Data</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">New-PoshBotTextResponse</span><span class="w"> </span><span class="nt">-Text</span><span class="w"> </span><span class="nv">$text</span><span class="w"> </span><span class="nt">-AsCode</span><span class="w"> </span><span class="nv">$Bot</span><span class="o">.</span><span class="nf">SendMessage</span><span class="p">(</span><span class="nv">$response</span><span class="p">)</span><span class="w"> </span><span class="nv">$Bot</span><span class="o">.</span><span class="nf">LogDebug</span><span class="p">(</span><span class="s1">'Sending thread reminding response'</span><span class="p">)</span><span class="w"> </span><span class="c"># Reset so we don't send again until they breach the limit again</span><span class="w"> </span><span class="nv">$tracker</span><span class="p">[</span><span class="nv">$userId</span><span class="p">]</span><span class="o">.</span><span class="nf">Allowance</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$maxMsgs</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">$tracker</span><span class="p">[</span><span class="nv">$userId</span><span class="p">]</span><span class="o">.</span><span class="nf">Allowance</span><span class="w"> </span><span class="nf">-</span><span class="o">=</span><span class="w"> </span><span class="mf">1.0</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="nv">$tracker</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">Export-Clixml</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$trackerPath</span><span class="w"> </span></code></pre></div> <p>Finally, we’ll close out the <em>if/else</em> statement from above and log a debug message if we didn’t need to measure this message at all. We’ll then return the command context to PoshBot. Returning the <code>$Context</code> object to PoshBot tells it to continue with executing any other middleware hooks. If we wanted to tell PoshBot to stop processing this message, we return nothing from the script.<br> </p> <div class="highlight"><pre class="highlight powershell"><code><span class="p">}</span><span class="w"> </span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">$Bot</span><span class="o">.</span><span class="nf">LogDebug</span><span class="p">(</span><span class="s2">"Ignoring message. It's already in a threaded conversation."</span><span class="p">)</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c"># Return context back for any subsequent middleware</span><span class="w"> </span><span class="nv">$Bot</span><span class="o">.</span><span class="nf">LogDebug</span><span class="p">(</span><span class="s1">'Ending message ratelimit middleware'</span><span class="p">)</span><span class="w"> </span><span class="nv">$Context</span><span class="w"> </span></code></pre></div> <h2> Summary </h2> <blockquote> <p>The full script is found in this <a href="proxy.php?url=https://gist.github.com/devblackops/2b02fbb946a421de6efb5b9c8ce7d79b">GitHub gist</a>.</p> </blockquote> <p>I hope this post was informative and highlighted the power and flexibility you have with PoshBot middleware hooks. I’m sure this script didn’t account for some edge cases and may even contain a bug or two, but I’ll leave that as an exercise for the reader.</p> <p>Cheers</p> poshbot chatops slack powershell Joining Paths in PowerShell Brandon Olin [1x Engineer] Mon, 07 Jan 2019 00:00:00 +0000 https://dev.to/devblackops/joining-paths-in-powershell-3e4c https://dev.to/devblackops/joining-paths-in-powershell-3e4c <p>Frequently in PowerShell, you’ll be dealing with file paths and programmatically constructing them to either write or read files. There are a few different ways to build up file paths in PowerShell which I’ll go over below.</p> <h2> Using a hard-coded string </h2> <p>It is very common to see file paths being created by specifying the exact, hard-coded path.<br> </p> <div class="highlight"><pre class="highlight powershell"><code><span class="nv">$path</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'C:\mypath\to\foo.txt'</span><span class="w"> </span></code></pre></div> <p>This is problematic for a few reasons though. You’ll notice that we’re using <code>c:\</code> in the string. This immediately limits the use of this to Windows as the concept of the <code>C:</code> drive only exists there.</p> <p>In case you haven’t heard, PowerShell is <a href="proxy.php?url=https://blogs.msdn.microsoft.com/powershell/2016/08/18/powershell-on-linux-and-open-source-2/">cross-platform</a> now with version 6. We need to start thinking about how our scripts will run if they are executed on macOS or Linux. Assuming that they will only ever be executed on Windows will limit their usefulness, especially if we ever intend to publish it for others.</p> <p>The other issue with this example is the use of backslashes <code>\</code>. This may be the primary way to separate paths on Windows, but on Unix-like operating systems, the forward slash <code>/</code> is used. In most cases, PowerShell will normalize this for you on whatever OS you’re running on, and it is often forgotten that Windows <strong>also</strong> supports the forward slash <code>/</code> as a path separator, but this may not work in every scenario. To help ensure our scripts work in all environments, it is a good practice to use <code>/</code> if manually constructing a folder or file path.</p> <p>Also, if you need to access a path like <code>C:\mypath\to\foo.txt</code> on Windows or similar on macOS/Linux, it would be better to use a mix of environment and/or built-in PowerShell variables to make it more resilient. In PowerShell 6, the new boolean variables <code>$IsWindows</code>, <code>$IsLinux</code>, and <code>$IsMacOS</code> will tell you what operating system you’re on, and on Windows, we can rely on <code>$env:SYSTEMDRIVE</code> to return the system drive. This is usually <code>C:\</code> but in rare cases, could be another drive letter. On a Unix-like OS, we can use root <code>/</code>. We can use these variables to determine the correct path to use depending on the operating system.<br> </p> <div class="highlight"><pre class="highlight powershell"><code><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="bp">$IsWindows</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">$path</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"</span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">SYSTEMDRIVE</span><span class="s2">/mypath/to/foo.txt"</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">$path</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'/mypath/to/foo.txt'</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div> <h2> Using Join-Path </h2> <p>PowerShell includes the cmdlet <code>Join-Path</code> for taking multiple paths and returning a single path. This is a better method as <code>Join-Path</code> will ensure the correct path separator is used depending on the context.</p> <p>The example below will return <code>C:\foo</code> on Windows.<br> </p> <div class="highlight"><pre class="highlight powershell"><code><span class="nv">$path</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">Join-Path</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">SYSTEMDRIVE</span><span class="w"> </span><span class="nt">-ChildPath</span><span class="w"> </span><span class="s1">'foo'</span><span class="w"> </span></code></pre></div> <h2> Joining three or more paths </h2> <p>Often, we’ll run into situations where we need to join more than two paths together. We <strong>could</strong> do something like:<br> </p> <div class="highlight"><pre class="highlight powershell"><code><span class="nv">$path</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"</span><span class="nv">$path1</span><span class="s2">/</span><span class="nv">$path2</span><span class="s2">/</span><span class="nv">$path3</span><span class="s2">"</span><span class="w"> </span></code></pre></div> <p>or<br> </p> <div class="highlight"><pre class="highlight powershell"><code><span class="nv">$path</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">Join-Path</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$path1</span><span class="w"> </span><span class="nt">-ChildPath</span><span class="w"> </span><span class="s2">"</span><span class="nv">$path2</span><span class="s2">/</span><span class="nv">$path3</span><span class="s2">"</span><span class="w"> </span></code></pre></div> <p>or even worse:<br> </p> <div class="highlight"><pre class="highlight powershell"><code><span class="nv">$path</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">Join-Path</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$path1</span><span class="w"> </span><span class="nt">-ChildPath</span><span class="w"> </span><span class="p">(</span><span class="nf">Join-Path</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$path2</span><span class="w"> </span><span class="nt">-ChildPath</span><span class="w"> </span><span class="nv">$path3</span><span class="p">)</span><span class="w"> </span></code></pre></div> <p>None of these are particularly elegant though.</p> <p>Starting in PowerShell 6, <code>Join-Path</code> has a new parameter called <code>-AdditionalChildPaths</code>. This parameter takes a string array that you can use to include as many additional path sections as you need. With this, we can utilize the built-in cmdlet and not rely on any manual string concatenation.<br> </p> <div class="highlight"><pre class="highlight powershell"><code><span class="nv">$path</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">Join-Path</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$path1</span><span class="w"> </span><span class="nt">-ChildPath</span><span class="w"> </span><span class="nv">$path2</span><span class="w"> </span><span class="nt">-AdditionalChildPaths</span><span class="w"> </span><span class="p">(</span><span class="nv">$path3</span><span class="p">,</span><span class="w"> </span><span class="nv">$path4</span><span class="p">)</span><span class="w"> </span></code></pre></div> <p>These parameters also work positionally so you can skip specifying the parameters names if you desire. This technically goes against established PowerShell style guidelines for explicitly using parameter names but for common cmdlets, it is generally accepted.<br> </p> <div class="highlight"><pre class="highlight powershell"><code><span class="nv">$path</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">Join-Path</span><span class="w"> </span><span class="nv">$path1</span><span class="w"> </span><span class="nv">$path2</span><span class="w"> </span><span class="nv">$path3</span><span class="w"> </span><span class="nv">$path4</span><span class="w"> </span></code></pre></div> <h2> Dipping into .NET </h2> <p>One of the great things about PowerShell is the ability to dip into .Net when you need extra power or flexibility. My new favorite method of constructing file paths is using .NET’s <code>[System.IO.Path]</code> class and the <code>Combine()</code> method. This method accepts two or more strings which it will combine in one operation and not rely on any manual string concatenation.</p> <p>The great thing about this method is it works similar to PowerShell v6’s <code>Join-Path</code> and the <code>-AdditionalChildPaths</code> parameter, but works on lower versions of PowerShell as well, making your script or module even more portable.<br> </p> <div class="highlight"><pre class="highlight powershell"><code><span class="nv">$path</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="no">IO.</span><span class="kt">Path</span><span class="p">]::</span><span class="nf">Combine</span><span class="p">(</span><span class="nv">$path1</span><span class="p">,</span><span class="w"> </span><span class="nv">$path2</span><span class="p">,</span><span class="w"> </span><span class="nv">$path3</span><span class="p">,</span><span class="w"> </span><span class="nv">$path4</span><span class="p">)</span><span class="w"> </span></code></pre></div> <p>Happy <del>trails</del> paths :)</p> powershell files path Using a Powershell Azure Function to Send Automated Blog Post Tweets Brandon Olin [1x Engineer] Fri, 07 Dec 2018 00:00:00 +0000 https://dev.to/devblackops/using-a-powershell-azure-function-to-send-automated-blog-post-tweets-4mbo https://dev.to/devblackops/using-a-powershell-azure-function-to-send-automated-blog-post-tweets-4mbo <p>If you follow me on <a href="proxy.php?url=https://twitter.com/devblackops" rel="noopener noreferrer">Twitter</a>, you would have probably noticed that I occasionally send out tweets from random previous blog posts. I don’t want to have to remember to send these manually, and after reading how <a href="proxy.php?url=https://twitter.com/WindosNZ" rel="noopener noreferrer">Josh King</a> does it in his <a href="proxy.php?url=https://king.geek.nz/2018/05/30/automatic-blog-archive-tweets/" rel="noopener noreferrer">automated blog archive tweets</a> article, I thought I’d add my spin on it. For my implementation, I’m going to use an <a href="proxy.php?url=https://azure.microsoft.com/en-us/services/functions/" rel="noopener noreferrer">Azure Function</a> as well as a bit of blob storage to keep track of previous tweets. This way, I don’t depend on my local computer being up, and I can keep track of what posts I’ve already tweeted out, so I don’t repeat them. An example of one of these automated tweets is below:</p> <blockquote> <p>All the code for this process can be found in the <a href="proxy.php?url=https://github.com/devblackops/blog-archive-tweeter-example" rel="noopener noreferrer">GitHub repo</a>.</p> </blockquote> <p><iframe class="tweet-embed" id="tweet-1065853467795775488-344" src="proxy.php?url=https://platform.twitter.com/embed/Tweet.html?id=1065853467795775488"> </iframe> // Detect dark theme var iframe = document.getElementById('tweet-1065853467795775488-344'); if (document.body.className.includes('dark-theme')) { iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1065853467795775488&amp;theme=dark" } </p> <h2> TL;DR </h2> <p>This blog post tweeter works by consuming a JSON feed of previous blog posts, selecting one at random, generates a <a href="proxy.php?url=https://bitly.com/" rel="noopener noreferrer">Bitly</a> link to the post, then sends the tweet. A record of this tweet is then stored in Azure Storage, so subsequent invocations of the Function don’t re-send the same post until all available posts have been tweeted out. Once all available posts have been tweeted, the tracker is reset. For all the details about how this process works keep reading.</p> <h2> Blog Post JSON feed </h2> <p>I use the static site generator <a href="proxy.php?url=https://jekyllrb.com/" rel="noopener noreferrer">Jekyll</a> for my blog and create a JSON file containing all previous blog posts any time I update the blog with new content. This JSON file can be found at <a href="proxy.php?url=https://devblackops.io/feed.json" rel="noopener noreferrer">https://devblackops.io/feed.json</a>, and you can see how I generate it <a href="proxy.php?url=https://github.com/devblackops/devblackops.github.io/blob/master/jsonfeed.html" rel="noopener noreferrer">here</a>.</p> <p>A snippet of what this JSON looks like is below:<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight json"><code><span class="p">{</span><span class="w"> </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"DevBlackOps"</span><span class="p">,</span><span class="w"> </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Thoughts about DevOps and automation from a Windows guy"</span><span class="p">,</span><span class="w"> </span><span class="nl">"url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://devblackops.io/"</span><span class="p">,</span><span class="w"> </span><span class="nl">"date"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Fri, 07 Sep 2018 04:02:33 +0000"</span><span class="p">,</span><span class="w"> </span><span class="nl">"posts"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"The PowerShell Conference Book"</span><span class="p">,</span><span class="w"> </span><span class="nl">"url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://devblackops.io/the-powershell-conference-book/"</span><span class="p">,</span><span class="w"> </span><span class="nl">"date"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Mon, 09 Jul 2018 00:00:00 +0000"</span><span class="p">,</span><span class="w"> </span><span class="nl">"tags"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="s2">"PowerShell"</span><span class="w"> </span><span class="p">],</span><span class="w"> </span><span class="nl">"categories"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="s2">"PowerShell"</span><span class="w"> </span><span class="p">]</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"The Operation Validation Framework: Test your infrastructure using Pester"</span><span class="p">,</span><span class="w"> </span><span class="nl">"url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://devblackops.io/the-operation-validation-framework-test-your-infrastructure-using-pester/"</span><span class="p">,</span><span class="w"> </span><span class="nl">"date"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Mon, 25 Jun 2018 00:00:00 +0000"</span><span class="p">,</span><span class="w"> </span><span class="nl">"tags"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="s2">"PowerShell"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Infrastructure"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Testing"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Pester"</span><span class="p">,</span><span class="w"> </span><span class="s2">"OVF"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Operation Validation"</span><span class="w"> </span><span class="p">],</span><span class="w"> </span><span class="nl">"categories"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="s2">"PowerShell"</span><span class="w"> </span><span class="p">]</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">]</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre> </div> <h2> The Azure Function </h2> <p>This PowerShell-based Azure Function consumes the JSON feed from my blog, select a random post, then tweet it out. Hashtags are also created based on any tags defined in the blog post. I’m also borrowing some code from <a href="proxy.php?url=https://github.com/MeshkDevs" rel="noopener noreferrer">MeshkDevs</a> in the <a href="proxy.php?url=https://github.com/MeshkDevs/InvokeTwitterAPIs" rel="noopener noreferrer">InvokeTwitterAPIs</a> repository to send tweets using PowerShell.</p> <p>The Azure Function runs on a schedule and consumes the JSON feed from my blog. When the Function is triggered based on the schedule, a JSON-based tracker file hosted in Azure storage is passed as input. An array of available posts to tweet is created by taking the posts from the JSON feed and removing any previously tweeted posts contained in the tracker file. A random post is selected from whatever posts are left. This post is then tweeted out and added to the tracker file so it won’t be sent again until all available posts of been tweeted out.</p> <p>The relevant bits of the Function are below. To see the whole function including how to create short links using Bitly and send tweets to Twitter, check out the <a href="proxy.php?url=https://github.com/devblackops/blog-archive-tweeter-example/blob/master/sendblogtweet/run.ps1" rel="noopener noreferrer">whole file</a> in the GitHub <a href="proxy.php?url=https://github.com/devblackops/blog-archive-tweeter-example" rel="noopener noreferrer">repo</a>.</p> <h4> run.ps1 </h4> <div class="highlight js-code-highlight"> <pre class="highlight powershell"><code><span class="c"># I don't want these URLs tweeted out as they're not very relevant</span><span class="w"> </span><span class="nv">$excludedPosts</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@()</span><span class="w"> </span><span class="c"># Load tracker file</span><span class="w"> </span><span class="nv">$tracker</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Content</span><span class="w"> </span><span class="nv">$inBlob</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertFrom-Json</span><span class="w"> </span><span class="nx">Write-Output</span><span class="w"> </span><span class="s2">"Last tweeted: </span><span class="si">$(</span><span class="nv">$tracker</span><span class="o">.</span><span class="nf">lastTweetedTime</span><span class="si">)</span><span class="s2">"</span><span class="w"> </span><span class="c"># Get random blog post from feed</span><span class="w"> </span><span class="nv">$blog</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-RestMethod</span><span class="w"> </span><span class="nt">-Uri</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">BLOG_FEED_URL</span><span class="w"> </span><span class="nv">$candidatePosts</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$blog</span><span class="o">.</span><span class="nf">posts</span><span class="o">.</span><span class="nf">Where</span><span class="p">({</span><span class="bp">$_</span><span class="o">.</span><span class="nf">url</span><span class="w"> </span><span class="nt">-notin</span><span class="w"> </span><span class="nv">$excludedPosts</span><span class="p">})</span><span class="w"> </span><span class="c"># Get a post from the list of available posts that we haven't already tweeted</span><span class="w"> </span><span class="nv">$tweetedUrls</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$tracker</span><span class="o">.</span><span class="nf">tweetedPosts</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-Object</span><span class="w"> </span><span class="nt">-ExpandProperty</span><span class="w"> </span><span class="nx">url</span><span class="w"> </span><span class="nv">$availablePosts</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$candidatePosts</span><span class="o">.</span><span class="nf">Where</span><span class="p">({</span><span class="bp">$_</span><span class="o">.</span><span class="nf">url</span><span class="w"> </span><span class="nt">-notin</span><span class="w"> </span><span class="nv">$tweetedUrls</span><span class="p">})</span><span class="w"> </span><span class="nv">$post</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$availablePosts</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Get-Random</span><span class="w"> </span><span class="nv">$availablePosts</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$availablePosts</span><span class="o">.</span><span class="nf">Where</span><span class="p">({</span><span class="bp">$_</span><span class="o">.</span><span class="nf">url</span><span class="w"> </span><span class="o">-ne</span><span class="w"> </span><span class="nv">$post</span><span class="o">.</span><span class="nf">Url</span><span class="p">})</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">-not</span><span class="w"> </span><span class="nv">$post</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="c"># We've exhausted all available posts so reset</span><span class="w"> </span><span class="c"># the tracker and get a new post from the candidates</span><span class="w"> </span><span class="nv">$post</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$candidatePosts</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Get-Random</span><span class="w"> </span><span class="nv">$tracker</span><span class="o">.</span><span class="nf">tweetedPosts</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@()</span><span class="w"> </span><span class="nv">$availablePosts</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$candidatePosts</span><span class="o">.</span><span class="nf">Where</span><span class="p">({</span><span class="bp">$_</span><span class="o">.</span><span class="nf">url</span><span class="w"> </span><span class="o">-ne</span><span class="w"> </span><span class="nv">$post</span><span class="o">.</span><span class="nf">Url</span><span class="p">})</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="nv">$tracker</span><span class="o">.</span><span class="nf">candidatePostsCount</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$candidatePosts</span><span class="o">.</span><span class="nf">Count</span><span class="w"> </span><span class="nv">$tracker</span><span class="o">.</span><span class="nf">availablePostsCount</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$availablePosts</span><span class="o">.</span><span class="nf">Count</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$post</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">$postJson</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$post</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertTo-Json</span><span class="w"> </span><span class="nx">Write-Output</span><span class="w"> </span><span class="s2">"Retrieved post:</span><span class="se">`n</span><span class="nv">$postJson</span><span class="s2">"</span><span class="w"> </span><span class="c"># Create hashtags</span><span class="w"> </span><span class="nv">$hashtags</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">''</span><span class="w"> </span><span class="nv">$post</span><span class="o">.</span><span class="nf">tags</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Foreach-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">$tag</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$_</span><span class="w"> </span><span class="o">-replace</span><span class="w"> </span><span class="s1">' '</span><span class="p">,</span><span class="w"> </span><span class="s1">''</span><span class="w"> </span><span class="nv">$hashtags</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="p">(</span><span class="s1">' #'</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nv">$tag</span><span class="p">)</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="nv">$hashtags</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$hashtags</span><span class="o">.</span><span class="nf">Trim</span><span class="p">()</span><span class="w"> </span><span class="c"># Create tweet text</span><span class="w"> </span><span class="nv">$title</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$post</span><span class="o">.</span><span class="nf">title</span><span class="w"> </span><span class="nv">$link</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-ShortUrl</span><span class="w"> </span><span class="nt">-Url</span><span class="w"> </span><span class="nv">$post</span><span class="o">.</span><span class="nf">url</span><span class="w"> </span><span class="nt">-OAuthToken</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">BITLY_OAUTH_TOKEN</span><span class="w"> </span><span class="nv">$tweetText</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"From the blog archive: </span><span class="nv">$Title</span><span class="se">`n`n</span><span class="nv">$link</span><span class="se">`n`n</span><span class="nv">$hashtags</span><span class="s2">"</span><span class="w"> </span><span class="n">Write-Output</span><span class="w"> </span><span class="s2">"Sending tweet:</span><span class="se">`n</span><span class="nv">$tweetText</span><span class="s2">"</span><span class="w"> </span><span class="nv">$oAuth</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w"> </span><span class="nx">ApiKey</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">TWITTER_CONSUMER_KEY</span><span class="w"> </span><span class="nx">ApiSecret</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">TWITTER_CONSUMER_SECRET</span><span class="w"> </span><span class="nx">AccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">TWITTER_ACCESS_TOKEN</span><span class="w"> </span><span class="nx">AccessTokenSecret</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">TWITTER_ACCESS_SECRET</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="nv">$tweetParams</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w"> </span><span class="nx">ResourceURL</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'statuses/update.json'</span><span class="w"> </span><span class="nx">RestVerb</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'POST'</span><span class="w"> </span><span class="nx">Parameters</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w"> </span><span class="nx">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$tweetText</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="nx">OAuthSettings</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$oAuth</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="nv">$tweet</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-TwitterRestMethod</span><span class="w"> </span><span class="err">@</span><span class="nx">tweetParams</span><span class="w"> </span><span class="nv">$tweetJson</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$tweet</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertTo-Json</span><span class="w"> </span><span class="nx">Write-Output</span><span class="w"> </span><span class="s2">"Tweet sent:</span><span class="se">`n</span><span class="nv">$tweetJson</span><span class="s2">"</span><span class="w"> </span><span class="c"># Add tweeted post to tracker</span><span class="w"> </span><span class="nv">$now</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="n">Get-Date</span><span class="p">)</span><span class="o">.</span><span class="nf">ToUniversalTime</span><span class="p">()</span><span class="o">.</span><span class="nf">ToString</span><span class="p">(</span><span class="s1">'u'</span><span class="p">)</span><span class="w"> </span><span class="nv">$tracker</span><span class="o">.</span><span class="nf">lastTweetedTime</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$now</span><span class="w"> </span><span class="nv">$tweetedPost</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w"> </span><span class="nx">url</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$post</span><span class="err">.</span><span class="nx">Url</span><span class="w"> </span><span class="nx">lastTweeted</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$now</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="nv">$tracker</span><span class="o">.</span><span class="nf">lastTweetedPost</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$tweetedPost</span><span class="w"> </span><span class="nv">$tracker</span><span class="o">.</span><span class="nf">tweetedPosts</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="nv">$tweetedPost</span><span class="w"> </span><span class="nv">$tracker</span><span class="o">.</span><span class="nf">tweetedPostCount</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$tracker</span><span class="o">.</span><span class="nf">tweetedPosts</span><span class="o">.</span><span class="nf">Count</span><span class="w"> </span><span class="nv">$tracker</span><span class="o">.</span><span class="nf">candidatePostsCount</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$candidatePosts</span><span class="o">.</span><span class="nf">Count</span><span class="w"> </span><span class="nv">$tracker</span><span class="o">.</span><span class="nf">availablePostsCount</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$availablePosts</span><span class="o">.</span><span class="nf">Count</span><span class="w"> </span><span class="nv">$trackerJson</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$tracker</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertTo-Json</span><span class="w"> </span><span class="nx">Write-Output</span><span class="w"> </span><span class="s2">"Saving tracker to blob:</span><span class="se">`n</span><span class="nv">$trackerJson</span><span class="s2">"</span><span class="w"> </span><span class="nv">$trackerJson</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Out-File</span><span class="w"> </span><span class="nt">-Encoding</span><span class="w"> </span><span class="nx">UTF8</span><span class="w"> </span><span class="nt">-FilePath</span><span class="w"> </span><span class="nv">$outBlob</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre> </div> <p>The Function bindings are defined in <a href="proxy.php?url=https://github.com/devblackops/blog-archive-tweeter-example/blob/master/sendblogtweet/function.json" rel="noopener noreferrer">function.json</a>. You can see that I’ve set a timer-based trigger to fire this function. You can check out how <a href="proxy.php?url=https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-timer#cron-expressions" rel="noopener noreferrer">cron expressions</a> work in the Microsoft documentation. In this example, the function is triggered every <code>Monday, Wednesday, and Friday at 6:24am UTC</code>.</p> <p>I’m also defining an input binding to the tracker file contained in Azure storage. This same file is also defined as an output binding as the Function both reads and writes to it.</p> <h4> function.json </h4> <div class="highlight js-code-highlight"> <pre class="highlight json"><code><span class="p">{</span><span class="w"> </span><span class="nl">"bindings"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"timerTrigger"</span><span class="p">,</span><span class="w"> </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"myTimer"</span><span class="p">,</span><span class="w"> </span><span class="nl">"schedule"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0 24 6 * * 1,3,5"</span><span class="p">,</span><span class="w"> </span><span class="nl">"direction"</span><span class="p">:</span><span class="w"> </span><span class="s2">"in"</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"blob"</span><span class="p">,</span><span class="w"> </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"inBlob"</span><span class="p">,</span><span class="w"> </span><span class="nl">"path"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sendblogtwitter/posts.json"</span><span class="p">,</span><span class="w"> </span><span class="nl">"connection"</span><span class="p">:</span><span class="w"> </span><span class="s2">"blogarchivetweeter_STORAGE"</span><span class="p">,</span><span class="w"> </span><span class="nl">"direction"</span><span class="p">:</span><span class="w"> </span><span class="s2">"in"</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"blob"</span><span class="p">,</span><span class="w"> </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"outBlob"</span><span class="p">,</span><span class="w"> </span><span class="nl">"path"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sendblogtwitter/posts.json"</span><span class="p">,</span><span class="w"> </span><span class="nl">"connection"</span><span class="p">:</span><span class="w"> </span><span class="s2">"blogarchivetweeter_STORAGE"</span><span class="p">,</span><span class="w"> </span><span class="nl">"direction"</span><span class="p">:</span><span class="w"> </span><span class="s2">"out"</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">],</span><span class="w"> </span><span class="nl">"disabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre> </div> <h2> Deploying the Function </h2> <p>If you want to follow along at home, it is best if you clone the <a href="proxy.php?url=https://github.com/devblackops/blog-archive-tweeter-example" rel="noopener noreferrer">GitHub repo</a> and <code>cd</code> into it to run the deployment commands<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight plaintext"><code>git clone https://github.com/devblackops/blog-archive-tweeter-example cd ./blog-archive-tweeter-example </code></pre> </div> <p>To start, we’re going to define all our variables up front. Fill in these with your relevant information. For generating Twitter tokens used by this process, you can start <a href="proxy.php?url=https://developer.twitter.com/en/docs/basics/authentication/guides/access-tokens.html" rel="noopener noreferrer">here</a>. To generate a Bitly OAuth token, follow the directions <a href="proxy.php?url=https://dev.bitly.com/get_started.html" rel="noopener noreferrer">here</a>.<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight powershell"><code><span class="c"># Settings</span><span class="w"> </span><span class="nv">$subscription</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'&lt;YOUR-AZURE-SUBSCRIPTION&gt;'</span><span class="w"> </span><span class="nv">$resourceGroup</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'&lt;RESOURCE-GROUP-NAME&gt;'</span><span class="w"> </span><span class="nv">$region</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'&lt;AZURE-REGION&gt;'</span><span class="w"> </span><span class="nv">$storageAcct</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'&lt;STORAGE-ACCOUNT-NAME&gt;'</span><span class="w"> </span><span class="nv">$storageContainerName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'&lt;STORAGE-CONTAINER-NAME&gt;'</span><span class="w"> </span><span class="nv">$functionApp</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'&lt;FUNCTION-APP-NAME&gt;'</span><span class="w"> </span><span class="nv">$blogFeedUrl</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'&lt;YOUR-FEED-URL&gt;'</span><span class="w"> </span><span class="nv">$twitterAccessSecret</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'&lt;TWITTER-ACCESS-SECRET&gt;'</span><span class="w"> </span><span class="nv">$twitterAccessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'&lt;TWITTER-ACCESS-TOKEN&gt;'</span><span class="w"> </span><span class="nv">$twitterConsumerKey</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'&lt;TWITTER-CONSUMER-KEY&gt;'</span><span class="w"> </span><span class="nv">$twitterConsumeSecret</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'&lt;TWITTER-CONSUMER-SECRET&gt;'</span><span class="w"> </span><span class="nv">$bitlyOauthToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'&lt;BITLY-OAUTH-TOKEN&gt;'</span><span class="w"> </span></code></pre> </div> <p>Now we can log into Azure using <a href="proxy.php?url=https://docs.microsoft.com/en-us/cli/azure/?view=azure-cli-latest" rel="noopener noreferrer">AZ CLI</a> and create a resource group to hold our Function and storage account.<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight powershell"><code><span class="n">az</span><span class="w"> </span><span class="nx">login</span><span class="w"> </span><span class="n">az</span><span class="w"> </span><span class="nx">account</span><span class="w"> </span><span class="nx">set</span><span class="w"> </span><span class="nt">--subscription</span><span class="w"> </span><span class="nv">$subscription</span><span class="w"> </span><span class="n">az</span><span class="w"> </span><span class="nx">group</span><span class="w"> </span><span class="nx">create</span><span class="w"> </span><span class="nt">--name</span><span class="w"> </span><span class="nv">$resourceGroup</span><span class="w"> </span><span class="nt">--location</span><span class="w"> </span><span class="nv">$region</span><span class="w"> </span></code></pre> </div> <p>Create a new storage account and retrieve the connection string to it.<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight powershell"><code><span class="n">az</span><span class="w"> </span><span class="nx">storage</span><span class="w"> </span><span class="nx">account</span><span class="w"> </span><span class="nx">create</span><span class="w"> </span><span class="nt">--resource-group</span><span class="w"> </span><span class="nv">$resourceGroup</span><span class="w"> </span><span class="nt">--name</span><span class="w"> </span><span class="nv">$storageAcct</span><span class="w"> </span><span class="nt">--location</span><span class="w"> </span><span class="nv">$region</span><span class="w"> </span><span class="nt">--sku</span><span class="w"> </span><span class="nx">Standard_LRS</span><span class="w"> </span><span class="nv">$storageConnStr</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">az</span><span class="w"> </span><span class="nx">storage</span><span class="w"> </span><span class="nx">account</span><span class="w"> </span><span class="nx">show-connection-string</span><span class="w"> </span><span class="nt">--resource-group</span><span class="w"> </span><span class="nv">$resourceGroup</span><span class="w"> </span><span class="nt">--name</span><span class="w"> </span><span class="nv">$storageAcct</span><span class="w"> </span><span class="nt">--output</span><span class="w"> </span><span class="nx">tsv</span><span class="w"> </span></code></pre> </div> <p>Create a storage container and upload the empty tracker file. This file can be found in the GitHub repo <a href="proxy.php?url=https://github.com/devblackops/blog-archive-tweeter-example/blob/master/posts.json" rel="noopener noreferrer">here</a>.<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight powershell"><code><span class="n">az</span><span class="w"> </span><span class="nx">storage</span><span class="w"> </span><span class="nx">container</span><span class="w"> </span><span class="nx">create</span><span class="w"> </span><span class="nt">--account-name</span><span class="w"> </span><span class="nv">$storageAcct</span><span class="w"> </span><span class="nt">--name</span><span class="w"> </span><span class="nv">$storageContainerName</span><span class="w"> </span><span class="n">az</span><span class="w"> </span><span class="nx">storage</span><span class="w"> </span><span class="nx">blob</span><span class="w"> </span><span class="nx">upload</span><span class="w"> </span><span class="nt">--account-name</span><span class="w"> </span><span class="nv">$storageAcct</span><span class="w"> </span><span class="nt">--container-name</span><span class="w"> </span><span class="nv">$storageContainerName</span><span class="w"> </span><span class="nt">--name</span><span class="w"> </span><span class="nx">posts.json</span><span class="w"> </span><span class="nt">--file</span><span class="w"> </span><span class="o">.</span><span class="nx">/posts.json</span><span class="w"> </span></code></pre> </div> <p>Now create the Function App and set the application settings.<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight powershell"><code><span class="n">az</span><span class="w"> </span><span class="nx">functionapp</span><span class="w"> </span><span class="nx">create</span><span class="w"> </span><span class="nt">--resource-group</span><span class="w"> </span><span class="nv">$resourceGroup</span><span class="w"> </span><span class="nt">--name</span><span class="w"> </span><span class="nv">$functionApp</span><span class="w"> </span><span class="nt">--storage-account</span><span class="w"> </span><span class="nv">$storageAcct</span><span class="w"> </span><span class="nt">--consumption-plan-location</span><span class="w"> </span><span class="nv">$region</span><span class="w"> </span><span class="n">az</span><span class="w"> </span><span class="nx">functionapp</span><span class="w"> </span><span class="nx">config</span><span class="w"> </span><span class="nx">appsettings</span><span class="w"> </span><span class="nx">set</span><span class="w"> </span><span class="nt">--resource-group</span><span class="w"> </span><span class="nv">$resourceGroup</span><span class="w"> </span><span class="nt">--name</span><span class="w"> </span><span class="nv">$functionApp</span><span class="w"> </span><span class="nt">--settings</span><span class="w"> </span><span class="s2">"FUNCTIONS_EXTENSION_VERSION = ~1"</span><span class="w"> </span><span class="n">az</span><span class="w"> </span><span class="nx">functionapp</span><span class="w"> </span><span class="nx">config</span><span class="w"> </span><span class="nx">appsettings</span><span class="w"> </span><span class="nx">set</span><span class="w"> </span><span class="nt">--resource-group</span><span class="w"> </span><span class="nv">$resourceGroup</span><span class="w"> </span><span class="nt">--name</span><span class="w"> </span><span class="nv">$functionApp</span><span class="w"> </span><span class="nt">--settings</span><span class="w"> </span><span class="s2">"BLOG_FEED_URL = </span><span class="nv">$blogFeedUrl</span><span class="s2">"</span><span class="w"> </span><span class="n">az</span><span class="w"> </span><span class="nx">functionapp</span><span class="w"> </span><span class="nx">config</span><span class="w"> </span><span class="nx">appsettings</span><span class="w"> </span><span class="nx">set</span><span class="w"> </span><span class="nt">--resource-group</span><span class="w"> </span><span class="nv">$resourceGroup</span><span class="w"> </span><span class="nt">--name</span><span class="w"> </span><span class="nv">$functionApp</span><span class="w"> </span><span class="nt">--settings</span><span class="w"> </span><span class="s2">"TWITTER_ACCESS_SECRET = </span><span class="nv">$twitterAccessSecret</span><span class="s2">"</span><span class="w"> </span><span class="n">az</span><span class="w"> </span><span class="nx">functionapp</span><span class="w"> </span><span class="nx">config</span><span class="w"> </span><span class="nx">appsettings</span><span class="w"> </span><span class="nx">set</span><span class="w"> </span><span class="nt">--resource-group</span><span class="w"> </span><span class="nv">$resourceGroup</span><span class="w"> </span><span class="nt">--name</span><span class="w"> </span><span class="nv">$functionApp</span><span class="w"> </span><span class="nt">--settings</span><span class="w"> </span><span class="s2">"TWITTER_ACCESS_TOKEN = </span><span class="nv">$twitterAccessToken</span><span class="s2">"</span><span class="w"> </span><span class="n">az</span><span class="w"> </span><span class="nx">functionapp</span><span class="w"> </span><span class="nx">config</span><span class="w"> </span><span class="nx">appsettings</span><span class="w"> </span><span class="nx">set</span><span class="w"> </span><span class="nt">--resource-group</span><span class="w"> </span><span class="nv">$resourceGroup</span><span class="w"> </span><span class="nt">--name</span><span class="w"> </span><span class="nv">$functionApp</span><span class="w"> </span><span class="nt">--settings</span><span class="w"> </span><span class="s2">"TWITTER_CONSUMER_KEY = </span><span class="nv">$twitterConsumerKey</span><span class="s2">"</span><span class="w"> </span><span class="n">az</span><span class="w"> </span><span class="nx">functionapp</span><span class="w"> </span><span class="nx">config</span><span class="w"> </span><span class="nx">appsettings</span><span class="w"> </span><span class="nx">set</span><span class="w"> </span><span class="nt">--resource-group</span><span class="w"> </span><span class="nv">$resourceGroup</span><span class="w"> </span><span class="nt">--name</span><span class="w"> </span><span class="nv">$functionApp</span><span class="w"> </span><span class="nt">--settings</span><span class="w"> </span><span class="s2">"TWITTER_CONSUMER_SECRET = </span><span class="nv">$twitterConsumeSecret</span><span class="s2">"</span><span class="w"> </span><span class="n">az</span><span class="w"> </span><span class="nx">functionapp</span><span class="w"> </span><span class="nx">config</span><span class="w"> </span><span class="nx">appsettings</span><span class="w"> </span><span class="nx">set</span><span class="w"> </span><span class="nt">--resource-group</span><span class="w"> </span><span class="nv">$resourceGroup</span><span class="w"> </span><span class="nt">--name</span><span class="w"> </span><span class="nv">$functionApp</span><span class="w"> </span><span class="nt">--settings</span><span class="w"> </span><span class="s2">"BITLY_OAUTH_TOKEN = </span><span class="nv">$bitlyOauthToken</span><span class="s2">"</span><span class="w"> </span><span class="n">az</span><span class="w"> </span><span class="nx">functionapp</span><span class="w"> </span><span class="nx">config</span><span class="w"> </span><span class="nx">appsettings</span><span class="w"> </span><span class="nx">set</span><span class="w"> </span><span class="nt">--resource-group</span><span class="w"> </span><span class="nv">$resourceGroup</span><span class="w"> </span><span class="nt">--name</span><span class="w"> </span><span class="nv">$functionApp</span><span class="w"> </span><span class="nt">--settings</span><span class="w"> </span><span class="s2">"blogarchivetweeter_STORAGE = </span><span class="nv">$storageConnStr</span><span class="s2">"</span><span class="w"> </span></code></pre> </div> <p>Now we have to deploy the actual function. To do this, we’ll zip up the entire GitHub repository and deploy it into the Function App.<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight powershell"><code><span class="n">Compress-Archive</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nt">-DestinationPath</span><span class="w"> </span><span class="nx">function.zip</span><span class="w"> </span><span class="n">az</span><span class="w"> </span><span class="nx">functionapp</span><span class="w"> </span><span class="nx">deployment</span><span class="w"> </span><span class="nx">source</span><span class="w"> </span><span class="nx">config-zip</span><span class="w"> </span><span class="nt">--resource-group</span><span class="w"> </span><span class="nv">$resourceGroup</span><span class="w"> </span><span class="nt">--name</span><span class="w"> </span><span class="nv">$functionApp</span><span class="w"> </span><span class="nt">--src</span><span class="w"> </span><span class="o">.</span><span class="nx">/function.zip</span><span class="w"> </span></code></pre> </div> <h2> Summary </h2> <p>That’s it. At this point, you should have both a Function App and storage account deployed in the resource group with the function triggering based on a timer. Make sure to replace all the relevant settings for your Azure environment, Twitter/Bitly credentials, and blog feed URL.</p> <p>Now you have a serverless blog post tweeter happily sending out your past blog posts to your followers.</p> <p>Happy tweeting!</p> <p>Cheers.</p> powershell azure twitter blogging