Jekyll2020-05-12T12:38:28+00:00https://poshops.io/feed.xmlPoShOpsWrite an awesome description for your new site here. You can edit this line in _config.yml. It will appear in your document head meta (for Google search results) and in your feed.xml site description.Douglas FrancisGetting Started with Azure Virtual Desktop2019-08-27T00:00:00+00:002019-08-27T00:00:00+00:00https://poshops.io/blog/Getting-Started-With-Windows-Virtual-Desktop<p>Microsoft has announced the Preview release of Windows Virtual Desktop (WVD) running in Azure giving enterprises the ability to deploy Windows 10 VDI and applications. As this feature is in Preview it is not as polished as other Azure offerings but there is a lot of good features and options available and would be great for a Proof of Concept deployment with IT staff. Or if some of the unpolished management tools don’t phase you then this could be used for non-technical users as well. The end user experience is really good, especially when leveraging the Windows Virtual Desktop Remote Desktop Application.</p>
<!--more-->
<p>There are a few dependencies you’ll need to meet before getting started with WVD.</p>
<p>First you need access to <em>Active Directory</em>, this can either be provided by IaaS VM’s running Active Directory Domain Services or through an Azure Active Directory Domain Services deployment. In this environment I will be leveraging AADDS. The VM’s we will be deploying later <em>must</em> be domain joined, AAD join is not an option.</p>
<p>Second, you need rights to Azure AD to create and register Enterprise Applications and Service Principals along with the ability to deploy Azure resources to a subscription.</p>
<p>Additionally, you need the Azure AD Directory ID (GUID) of your tenant and of the Azure Subscription you will be deploying resources too. Keep that information handy, you’ll need it throughout the deployment process.</p>
<p>Finally, you need to install the PowerShell module for WVD</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w"> </span><span class="nf">Install-Module</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="nx">Microsoft.RDInfra.RDPowerShell</span><span class="w">
</span></code></pre></div></div>
<hr />
<p>Step one to deploying WVD is navigating to the <a href="https://rdweb.wvd.microsoft.com/">Windows Virtual Desktop Consent Page</a>. The consent page is where we are going to get two new Enterprise applications in the AAD tenant for managing the WVD servers and for client access.
<img src="https://poshops.io/assets/images/azurevdi/vdiConsentPage.png" alt="alt text" title="Windows Virtual Desktop Consent" /></p>
<p>Input the Azure Active Directory Directory ID (GUID) for the AAD tenant users will be authenticating with. Once the submit button is hit you’ll need to authenticate to Azure AD and consent to application permissions. This is read access to Azure AD and read assess to the Graph API.</p>
<p><img src="https://poshops.io/assets/images/azurevdi/wvdPermissions.png" alt="alt text" title="WVD Server Permissions" /></p>
<p>After consenting to the Server App go back and repeat the same process for the client application as well.
Please wait at least 30 seconds between granting consent so AAD can process the changes on your tenant.</p>
<p>After consenting to those applications to be a part of your AAD tenant you need to grant a user ‘TenantCreator’ access to the new Windows Virtual Desktop enterprise application. The name ‘TenantCreator’ here applies only to the Virtual Desktop Tenant, not your AAD tenant.</p>
<p>In the Azure Portal navigate to enterprise applications by going to ‘Azure Active Directory > Enterprise Applications > Search for Windows Virtual Desktop’</p>
<p>You will see the new applications you consented to, ‘Windows Virtual Desktop’ and ‘Windows Virtual Desktop Client’.</p>
<p><img src="https://poshops.io/assets/images/azurevdi/enterpriseAppSearch.png" alt="alt text" title="Enterprise Application Search" /></p>
<p>Add an administrator user ‘TenantCreator’ permissions, as of now in the preview this is the default and only permissions option that can be delegated.</p>
<p><img src="https://poshops.io/assets/images/azurevdi/wvdTenantCreator.png" alt="alt text" title="Tenant Creator Permissions" /></p>
<p>Now it’s time to drop into PowerShell and setup everything we need to go deploy the resources. Note: As of now you cannot use PowerShell Core and need to use Windows Powershell.</p>
<p>Add the deployment URL for the VDI environment. As of right now in Preview this is the only option. I expect the GA release to allow for custom domains here in the future.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">Add-RdsAccount</span><span class="w"> </span><span class="nt">-DeploymentUrl</span><span class="w"> </span><span class="s2">"https://rdbroker.wvd.microsoft.com"</span><span class="w">
</span></code></pre></div></div>
<p>Next the creation of the Windows Virtual Desktop Tenant, you need the Azure Active Directory Directory ID and the ID of the Azure subscription you’ll be adding resources in. I’m also going to create a variable here for the TenantName, it’ll be used throughout the setup.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$TenantName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'PoshOps.io'</span><span class="w">
</span><span class="nv">$AadDirectory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'</span><span class="w">
</span><span class="nv">$AzSubscription</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'</span><span class="w">
</span><span class="nf">New-RdsTenant</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="nv">$tenantName</span><span class="w"> </span><span class="nt">-AadTenantId</span><span class="w"> </span><span class="nv">$AadDirectory</span><span class="w"> </span><span class="nt">-AzureSubscriptionId</span><span class="w"> </span><span class="nv">$AzSubscription</span><span class="w">
</span></code></pre></div></div>
<p><img src="https://poshops.io/assets/images/azurevdi/powershellCreateRdsTenant.png" alt="alt text" title="Powershell - Create RDS Tenant" /></p>
<p>Next we need to connect to Azure AD</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$aadcontext</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">Connect-AzureAD</span><span class="w">
</span></code></pre></div></div>
<p><img src="https://poshops.io/assets/images/azurevdi/connectAAD.png" alt="alt text" title="Powershell - Connect Azure AD" /></p>
<p>Best practice is to create an AAD Service Principal for the WVD services to run under. You <em>could</em> create the deployment under a named user, but it is not recommended.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$svcPrincipal</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">New-AzureADApplication</span><span class="w"> </span><span class="nt">-AvailableToOtherTenants</span><span class="w"> </span><span class="bp">$true</span><span class="w"> </span><span class="nt">-DisplayName</span><span class="w"> </span><span class="s2">"Windows Virtual Desktop Svc Principal"</span><span class="w">
</span><span class="nv">$svcPrincipalCreds</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">New-AzureADApplicationPasswordCredential</span><span class="w"> </span><span class="nt">-ObjectId</span><span class="w"> </span><span class="nv">$svcPrincipal</span><span class="o">.</span><span class="nf">ObjectId</span><span class="w">
</span></code></pre></div></div>
<p><img src="https://poshops.io/assets/images/azurevdi/powershellcreatesp.png" alt="alt text" title="Powershell - Create AAD Service Principal" /></p>
<p>After creation of the Service Principal we are going to assign it RDS owner rights</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">New-RdsRoleAssignment</span><span class="w"> </span><span class="nt">-RoleDefinitionName</span><span class="w"> </span><span class="s2">"RDS Owner"</span><span class="w"> </span><span class="nt">-ApplicationId</span><span class="w"> </span><span class="nv">$svcPrincipal</span><span class="o">.</span><span class="nf">AppId</span><span class="w"> </span><span class="nt">-TenantName</span><span class="w"> </span><span class="nv">$tenantName</span><span class="w">
</span></code></pre></div></div>
<p><img src="https://poshops.io/assets/images/azurevdi/addRdsPermissions.png" alt="alt text" title="Powershell - Add RDS Permissions" /></p>
<p>Next up is creating a new credential object and updating that role.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$creds</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">New-Object</span><span class="w"> </span><span class="nx">System.Management.Automation.PSCredential</span><span class="p">(</span><span class="nv">$svcPrincipal</span><span class="o">.</span><span class="nf">AppId</span><span class="p">,</span><span class="w"> </span><span class="p">(</span><span class="nf">ConvertTo-SecureString</span><span class="w"> </span><span class="nv">$svcPrincipalCreds</span><span class="o">.</span><span class="nf">Value</span><span class="w"> </span><span class="nt">-AsPlainText</span><span class="w"> </span><span class="nt">-Force</span><span class="p">))</span><span class="w">
</span><span class="nf">Add-RdsAccount</span><span class="w"> </span><span class="nt">-DeploymentUrl</span><span class="w"> </span><span class="s2">"https://rdbroker.wvd.microsoft.com"</span><span class="w"> </span><span class="nt">-Credential</span><span class="w"> </span><span class="nv">$creds</span><span class="w"> </span><span class="nt">-ServicePrincipal</span><span class="w"> </span><span class="nt">-AadTenantId</span><span class="w"> </span><span class="nv">$aadContext</span><span class="o">.</span><span class="nf">TenantId</span><span class="o">.</span><span class="nf">Guid</span><span class="w">
</span></code></pre></div></div>
<p><img src="https://poshops.io/assets/images/azurevdi/powershellUpdatePermissions.png" alt="alt text" title="Powershell - Update RDS Role" /></p>
<p>We now need to make note of the Application ID and the Service Principal key, keep track of this because after you close this PowerShell session you will have to reset the Service Principal key to get access again. We will use this information in the portal for the WVD deployment.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$svcPrincipal</span><span class="o">.</span><span class="nf">appid</span><span class="w">
</span><span class="nv">$svcPrincipalCreds</span><span class="o">.</span><span class="nf">value</span><span class="w">
</span></code></pre></div></div>
<p><img src="https://poshops.io/assets/images/azurevdi/appsecrets.png" alt="alt text" title="Powershell - App Secrets" /></p>
<p>Okay we’re almost there! Time to drop into the Azure Portal and create our WVD deployment! This is where all of the infrastructure we need to leverage WVD gets created.</p>
<p>In the Portal click ‘create a resource and search for ‘Windows Virtual Desktop - Provision a Host Pool’ > Click create.</p>
<p><img src="https://poshops.io/assets/images/azurevdi/hostpool.png" alt="alt text" title="Azure Portal - Create WVD Host Pool" /></p>
<p>The deployment wizard will start, the first step is providing basic information about the HostPool that will host your VDI instances/applications.</p>
<p>Few things of note here. If you are going to host both VDI and applications you should create a separate host pool for VDI and another for applications. As of now you cannot have a user in an Application Group for VDI <em>and</em> in an Application Group for a hosted application within the same pool. Which means, if you want Jane User to be able to access a Windows 10 VDI instance and a published LoB application at the same time then the app has to be in a different pool than the VDI hosts. If you have no need for users to have access to both then feel free to create just one Hostpool.</p>
<p>In addition this is the step where you determine what type of Hostpool this will be. The options are ‘pooled’ or ‘personal.’ A personal pool is a dedicated, non-persistent VDI environment with a one-to-one mapping between user and VDI instance. The system will automatically assign the user to an instance when signing in. However, if you create a personal hostpool with only one VDI instance in it, and assign two users to it. Only the user that signs into the pool first will be able to get a VM. The second user will receive a no resources error, even if the first user is <em>not</em> signed into the VM at the time.</p>
<p>I highly recommend thinking through a solid naming convention that will survive changing needs of the organization and what you may be using VDI for. I’m going to create resources under this format
<em>env-purpose-pooltype-region-iteration</em></p>
<p>For example, a development pool, used for hosting personal VDI will be “dev-vdi-personalpool-ussc-01”, conversely a shared application hostpool would be “dev-app-sharedpool-ussc-01” I’m going to use the same convention with my resource groups, just adding an ‘rg’ in the name as there is a one-to-one relationship with pools and resource groups.</p>
<p>Speaking of user assignments, right now you can only grant access via a user’s UPN, not through an AAD group. Don’t worry about getting all of the user assignments added right now, you can add and remove user later via PowerShell or the management application.</p>
<p>Finally, the resource group must be empty and not contain any resources, it’s easier to then make a new resource group through this deployment wizard.</p>
<p><img src="https://poshops.io/assets/images/azurevdi/deploystep1.png" alt="alt text" title="Azure Portal - Deployment Basics" /></p>
<p>In step two we’re going to size our VDI deployment. There are 4 options for how the wizard is going to determine the compute requirements.</p>
<ul>
<li>Light - 6 users per vCPU</li>
<li>Medium - 4 users per vCPU</li>
<li>Heavy - 2 users per vCPU</li>
<li>Custom - you just provide a number of VM’s</li>
</ul>
<p>What the deployment wizard is creating is a Virtual Machine Availability set. Once created, VM Availability Sets <em>cannot</em> have VM’s added or removed. You will be able to scale the size of these VM’s up and down as your needs grow, and shut them down to save on host but to remove one you will need to remove them all.</p>
<p>By default the wizard selects D8s v3 VM’s for deployment. Right now that is ~$330/month per VM, plus networking/storage costs. You can select different types of VM’s, such as compute or memory optimized as your needs and budget require.</p>
<p>With the size of the VM you’ve choosen, the usage profile and number of users inputted the wizard will math out how many VM’s will be required to support that workload. There isn’t any additional buffer included other than what may be extra just to how the users to vCPU calculations compute out.</p>
<p>For example, if you do a light profile for 500 users on D8s v3 VM’s the wizard will want to deploy 11 VM’s, totalling 88 vCPU’s. 500 users, divided by 6 users/vCPU is 84 vCPU’s rounding up, giving 4 extra cores. In the event that a VM goes down with all users online, there would be a resource outage.</p>
<p>The last important piece on this step is the VM name prefix, this is in place to prevent name collision in AD/WVD. Anything you provide here will have a ‘-#’ appended to it. For example ‘wvdvm’ becomes ‘wvdvm-0’ and ‘wvdvm-1’.</p>
<p><img src="https://poshops.io/assets/images/azurevdi/deploystep2.png" alt="alt text" title="Azure Portal - Config Virtual Machines" /></p>
<p>Step 3 are the VM configuration settings. This is where you can choose what OS you want to deploy, and how/where you want it deployed. The quickest way to get started is deploying either the Windows 10 or Server 2016 images from the Gallery. Additonally, you can deploy an image from a VHD you have on blob storage or from a Managed Azure Instance. This is how you’d deploy custom applications if you’re going that route.</p>
<p>Next, select the disk type, provide a UPN to perform the domain-join, provide a specific OU for the computers to join if you’d like and select the Azure vNet and subnet for the VM’s to reside on.</p>
<p>The ‘gotchas’ in this section are ensuring you provide a domain-join account as a UPN, <em>not</em> as a ‘Contoso\Admin’ format as you may be used to. In addition to that the wizard by default wants to create a new vNet. <em>Do not do this.</em> The VM’s <em>have</em> to have the ability to talk to Active Directory services. If you create a new vNet then these machines will not be able to talk to your domain services and the deployment will fail. They can be on different subnets but if you deploy to a new vNet the deployment fails. By default there is not vNet to vNet traffic allowed.</p>
<p><img src="https://poshops.io/assets/images/azurevdi/deploystep3.png" alt="alt text" title="Azure Portal - VM Settings" /></p>
<p>Step Four is authentication to Windows Virtual Desktop. Leave the tenant group name default, provide the tenant name you created earlier in powershell and change RDS owner from UPN to Service Principal. This is where you provide the App ID, password and AAD tenant ID info we saved earlier.</p>
<p><img src="https://poshops.io/assets/images/azurevdi/deploystep4.png" alt="alt text" title="Azure Portal - VM Auth" /></p>
<p>Step Six is just a summary of the info you’ve provided so far, this is your chance to review all the settings you’ve provided. The first time you run this it may take a few minutes for the summary page to complete loading so you can proceed. This is so any needed resource providers that are needed but you don’t have in place can be registered.</p>
<p>Once validation has passed and you’re happy with the settings click ok.</p>Douglas FrancisMicrosoft has announced the Preview release of Windows Virtual Desktop (WVD) running in Azure giving enterprises the ability to deploy Windows 10 VDI and applications. As this feature is in Preview it is not as polished as other Azure offerings but there is a lot of good features and options available and would be great for a Proof of Concept deployment with IT staff. Or if some of the unpolished management tools don’t phase you then this could be used for non-technical users as well. The end user experience is really good, especially when leveraging the Windows Virtual Desktop Remote Desktop Application.Azure Active Directory Password Hash Sync2019-07-31T00:00:00+00:002019-07-31T00:00:00+00:00https://poshops.io/blog/Azure-Active-Directory-Password-Hash-Sync<p>I frequently have conversations with customers around what their authentication options are with Azure Active Directory. More specifically, the conversation leads to reasons why they should use Password Hash Synchronization over other options.</p>
<!--more-->
<p>Generally speaking, there are three options for authentication in the Azure AD space from Active Directory synced accounts.
- Password Hash Synchronization (PHS)
- Pass-thru authentication (PTA)
- Federation (Typically ADFS, other IDP’s are possible)</p>
<p>In short, PHS is authentication occurring in Azure AD-based off a hash of the hashed password value stored in Active Directory on-prem, synced by Azure AD Connect. With PTA authentication occurs through Azure AD communicating with an agent on-prem interacting with Active Directory. While using federation authentication requests are sent to the IDP for authentication off Active Directory.</p>
<p>The number one concern I receive from customers around password hash synchronization generally is “I don’t want my passwords stored in the cloud for ‘security.’” Although, I’ve yet to have a discussion with someone that can articulate what their security concerns are and why they are more concerned about those risks than currently deployed solutions.</p>
<p>I believe that part of this comes from a misconception on how PHS works. Commonly the people I talk to think that AD Connect is copying the hash value already stored in Active Directory and using that for Azure AD. PHS does not pull the password hash that is in Active Directory and upload it to Azure AD and make that the Azure AD password.</p>
<p>What occurs can be a blog post in of itself but does get into the cryptographic weeds a bit. Without digging into all that, what happens is: AD Connect takes the hash stored in Active Directory and places it through an <a href="https://docs.microsoft.com/en-us/azure/active-directory/hybrid/how-to-connect-password-hash-synchronization#detailed-description-of-how-password-hash-synchronization-works">MD4+salt+PBKDF2+HMAC+SHA256 cryptographic process</a>. At the end of this process is a new hash value stored in Azure AD for authentication purposes. The salt value is unique to the user and is not generic across the environment, this will become important later.</p>
<p>Overall this process accomplishes a few things that are important for the security of Azure accounts.
- It would not be possible for an attacker to obtain a copy of an Azure AD hash somehow and replay that hash to any on-prem resources as the user
- Any two users that may have the same password have different hashes. As the salt is done on a per-user basis two users with “password1” as a password would have different hashes
- Prevents rainbow tables from being effective solutions in brute-forcing attempts on the account as well.
- The hashed value in Azure AD is cryptographically much stronger than the mechanism that is used currently in Active Directory for generating hashes</p>
<p>Password Hash Sync is the preferred method for authentication users with Azure AD from Active Directory sourced identities, followed by PTA and federation.</p>
<p>The number one reason that companies start leveraging PHS is removing the dependency on on-prem infrastructure for authentication. With PTA and federation if any outages prevent Azure AD from communication to Active Directory users will not be able to authenticate. Note: It is possible to <a href="https://docs.microsoft.com/en-us/azure/active-directory/hybrid/tutorial-phs-backup">enable PHS as a backup authentication with ADFS</a>. I have at least one conversation a month with customers to assist them with migrating off of ADFS to Password Hash Sync. For the organizations that are only using ADFS for O365 authentication is a huge win. Less infrastructure on-premises that they need to manage and maintain, higher availability for authentication, and it is just one less thing they need to worry about. Even for the organizations using ADFS for other claims, they are moving those over to Azure AD as well for SAML authentication and are having a lot of success with it.</p>
<p>Azure AD is a global resource that operates out of all 54 Azure regions worldwide with a 99.9% <a href="https://azure.microsoft.com/en-us/support/legal/sla/active-directory/v1_0/">SLA guarantee</a>. Basic and Premium only, sorry no SLA guarantees for Azure AD free users. All writes and reads to Azure AD are distributed across multiple Azure regions. All writes to Azure AD are also copied over to the secondary region. The write attempt does not return successfully until the <a href="https://docs.microsoft.com/en-us/azure/active-directory/fundamentals/active-directory-architecture">secondary region acknowledges it has the data</a>, ensuring data integrity. Additionally, Azure performs all writes on the primary region, all read on the secondary region. This provides a balance of resources and spreads the load out.</p>
<p>You can leverage PHS for <a href="https://docs.microsoft.com/en-us/azure/active-directory/hybrid/how-to-connect-sso">seamless SSO</a>, automatically signing users into services when they are on corporate devices. (PTA also supports Seamless SSO, ADFS does not) This leads to limiting how often users need to enter passwords while still providing a secure method of authentication. You can even start leveraging password ban lists to prevent users from <a href="https://docs.microsoft.com/en-us/azure/active-directory/authentication/concept-password-ban-bad#custom-banned-password-list">setting common to guess passwords</a>.</p>
<p>However, all is not perfect and rosy in the PHS world. Like any technology, there are downsides to be aware of that should be considered before deployment into your environment.
First, on-prem lockout policies and restricted hour login settings do not apply to Azure AD. Password changes sync to Azure AD every two minutes from AD connect.</p>
<p>Additionally, passwords set in Azure AD are set never to expire, allowing users to sign in to Azure AD with passwords that have expired in Active Directory. This means if a user’s password expires in Active Directory. And if they have no need to sign into Active Directory again, the Azure AD password will continue working. The Azure AD password would only get updated the next time the password is changed on-premises and then synced back.</p>
<p>Also, if you leverage the ‘accountExpires’ attribute in Active Directory, this value does not get <a href="https://docs.microsoft.com/en-us/azure/active-directory/hybrid/how-to-connect-password-hash-synchronization#account-expiration">synced to Azure AD</a>. The recommendation for this is a PowerShell workflow or some other automated process to take action in Azure AD based on Active Directory. What I generally see is a PowerShell script that runs hourly against Active Directory querying for expired accounts and then using Set-AzureADUser to match on the on-premises value. Disabled accounts on the other hand, are synced and updated with Azure AD with AD Connect updates.</p>
<p>I hope you found this useful for managing identities in your environment and understanding one of the options available to you.</p>Douglas FrancisI frequently have conversations with customers around what their authentication options are with Azure Active Directory. More specifically, the conversation leads to reasons why they should use Password Hash Synchronization over other options.Start/Stop Maintenance Scripts for Exchange 20162018-06-19T00:00:00+00:002018-06-19T00:00:00+00:00https://poshops.io/blog/StartStop-Maintenance-Scripts-For-Exchange-2016<p>Coming from Exchange 2010 one of the things that I love/hate about Exchange 2016 is when a new CU is installed it is essentially a full install of Exchange. In some ways, this is nice because it gets rid of a lot of the weird bugs that would occur post update after CUs that had weird triggers, and you get a nice fresh new install of Exchange, for the most part, every quarter. However, on the downside of this, the CU update process now takes much longer to complete in 2016 because, well you’re getting a nice fresh install. Also deploying that ~5GB ISO file around is a tad bit annoying at times.</p>
<p>In my experience after the initial install, and the 3 or 4 CU’s I’ve done since, it takes approximately one hour per server for an install/CU upgrade from the moment that the setup program is executing its installation steps. Probably an hour and a half from setup launch/prereq check/install.</p>
<p>In addition, you need to place the Exchange host in maintenance before starting the CU upgrade which can take some more time, especially if you have to look up commands again because I don’t know about you, but I don’t do Exchange CU updates frequently enough to remember the exact syntax for all the Powershell cmdlets.</p>
<p>So that I could cut down on time spent interacting with the host during CU upgrade changes I made myself two small helper functions for maintenance tasks. First places the Exchange host in maintenance, drains active database copies and pauses cluster membership. Then the second reverses all of that. This probably saves me 15-20 minutes per host now, and that adds up when I have 4 Exchange hosts to patch. Also, with a bit more error handling and comment-based help, I eventually can turn these helper functions over to my GNOC and have them patch.</p>
<p>First off, I run an elevated Exchange PowerShell module session on the server I’m starting the maintenance from. Import this function to my profile, execute it providing the name of the Exchange server I am currently working on, and another Exchange server for roles to move over to.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm"><#
</span><span class="cs">.NOTES</span><span class="cm">
===========================================================================
Created with: SAPIEN Technologies, Inc., PowerShell Studio 2018 v5.5.151
Created on: 5/31/2018 2:10 PM
Created by: Douglas Francis
Organization: PoshOps.IO
Filename: Start-ExchangeMaintenance.ps1
===========================================================================
</span><span class="cs">.DESCRIPTION</span><span class="cm">
Places an exchange 2016 server in maintenance in prep for patching.
#></span><span class="w">
</span><span class="kr">function</span><span class="w"> </span><span class="nf">Start-ExchangeMaintenance</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="kr">param</span><span class="w">
</span><span class="p">(</span><span class="w">
</span><span class="p">[</span><span class="kt">parameter</span><span class="p">(</span><span class="kt">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="kt">validateNotNullOrEmpty</span><span class="p">()]</span><span class="w">
</span><span class="p">[</span><span class="kt">String</span><span class="p">]</span><span class="nv">$maintServer</span><span class="p">,</span><span class="w">
</span><span class="p">[</span><span class="kt">Parameter</span><span class="p">(</span><span class="kt">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="kt">ValidateNotNullOrEmpty</span><span class="p">()]</span><span class="w">
</span><span class="p">[</span><span class="kt">String</span><span class="p">]</span><span class="nv">$failoverServerFQDN</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="kr">BEGIN</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">PROCESS</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nf">Write-Verbose</span><span class="w"> </span><span class="s2">"Draining Mail Queues"</span><span class="w">
</span><span class="nf">Set-ServerComponentState</span><span class="w"> </span><span class="nv">$maintServer</span><span class="w"> </span><span class="nt">-Component</span><span class="w"> </span><span class="nx">HubTransport</span><span class="w"> </span><span class="nt">-State</span><span class="w"> </span><span class="nx">Draining</span><span class="w"> </span><span class="nt">-Requester</span><span class="w"> </span><span class="nx">Maintenance</span><span class="w">
</span><span class="nf">Write-Verbose</span><span class="w"> </span><span class="s2">"Restarting Transport Services"</span><span class="w">
</span><span class="nf">Restart-Service</span><span class="w"> </span><span class="nx">MSExchangeTransport</span><span class="w">
</span><span class="nf">Restart-Service</span><span class="w"> </span><span class="nx">MSExchangeFrontEndTransport</span><span class="w">
</span><span class="nf">Write-Verbose</span><span class="w"> </span><span class="s2">"Redicting pending mail to Failover Server"</span><span class="w">
</span><span class="nf">Redirect-Message</span><span class="w"> </span><span class="nt">-Server</span><span class="w"> </span><span class="nv">$maintServer</span><span class="w"> </span><span class="nt">-Target</span><span class="w"> </span><span class="nv">$failoverServerFQDN</span><span class="w">
</span><span class="nf">Write-Verbose</span><span class="w"> </span><span class="s2">"Suspending DAG activity"</span><span class="w">
</span><span class="nf">Suspend-ClusterNode</span><span class="w"> </span><span class="nv">$maintServer</span><span class="w">
</span><span class="nf">Write-Verbose</span><span class="w"> </span><span class="s2">"Moving any Active Database Ownership to other servers"</span><span class="w">
</span><span class="nf">Set-MailboxServer</span><span class="w"> </span><span class="nv">$maintServer</span><span class="w"> </span><span class="nt">-DatabaseCopyActivationDisabledAndMoveNow</span><span class="w"> </span><span class="nv">$True</span><span class="w">
</span><span class="nf">Write-Verbose</span><span class="w"> </span><span class="s2">"Blocking mmaintenance server from hosting active database copies"</span><span class="w">
</span><span class="nf">Set-MailboxServer</span><span class="w"> </span><span class="nv">$maintServer</span><span class="w"> </span><span class="nt">-DatabaseCopyAutoActivationPolicy</span><span class="w"> </span><span class="nx">Blocked</span><span class="w">
</span><span class="nf">Write-Verbose</span><span class="w"> </span><span class="s2">"Placing Server in maintenance"</span><span class="w">
</span><span class="nf">Set-ServerComponentState</span><span class="w"> </span><span class="nv">$maintServer</span><span class="w"> </span><span class="nt">-Component</span><span class="w"> </span><span class="nx">ServerWideOffline</span><span class="w"> </span><span class="nt">-State</span><span class="w"> </span><span class="nx">Inactive</span><span class="w"> </span><span class="nt">-Requester</span><span class="w"> </span><span class="nx">Maintenance</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">END</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></div>
<p>I frequently like to use the “Write-Verbose” cmdlet for small things like this instead of comments Verbose provides info during execution and reading in the script provides as much info as a comment would in simple things like this.</p>
<p>Everything that happens here is getting mail queues off the server, stopping membership in the DAG, moving over database ownership and placing exchange in maintenance.</p>
<p>Once maintenance on the server completes, I import and run this script that reverses the actions of the first one allowing the exchange server to function in the DAG again fully.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm"><#
</span><span class="cs">.NOTES</span><span class="cm">
===========================================================================
Created with: SAPIEN Technologies, Inc., PowerShell Studio 2018 v5.5.151
Created on: 5/31/2018 3:45 PM
Created by: Douglas Francis
Organization: Afni
Filename: Stop-ExchangeMaintenance.ps1
===========================================================================
</span><span class="cs">.DESCRIPTION</span><span class="cm">
Stops exchange 2016 server from being in maintenance and function in the DAG/mail flow again.
#></span><span class="w">
</span><span class="kr">function</span><span class="w"> </span><span class="nf">Stop-ExchangeMaintenance</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="kr">param</span><span class="w">
</span><span class="p">(</span><span class="w">
</span><span class="p">[</span><span class="kt">parameter</span><span class="p">(</span><span class="kt">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="p">)]</span><span class="w">
</span><span class="p">[</span><span class="kt">validateNotNullOrEmpty</span><span class="p">()]</span><span class="w">
</span><span class="p">[</span><span class="kt">String</span><span class="p">]</span><span class="nv">$maintServer</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="kr">BEGIN</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">PROCESS</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nf">Write-Verbose</span><span class="w"> </span><span class="s2">"Taking Server out of Maintenance"</span><span class="w">
</span><span class="nf">Set-ServerComponentState</span><span class="w"> </span><span class="nv">$maintServer</span><span class="w"> </span><span class="nt">-Component</span><span class="w"> </span><span class="nx">ServerWideOffline</span><span class="w"> </span><span class="nt">-State</span><span class="w"> </span><span class="nx">Active</span><span class="w"> </span><span class="nt">-Requester</span><span class="w"> </span><span class="nx">Maintenance</span><span class="w">
</span><span class="nf">Write-Verbose</span><span class="w"> </span><span class="s2">"Restart DAG activity"</span><span class="w">
</span><span class="nf">Resume-ClusterNode</span><span class="w"> </span><span class="nv">$maintServer</span><span class="w">
</span><span class="nf">Write-Verbose</span><span class="w"> </span><span class="s2">"Allow Database Activation"</span><span class="w">
</span><span class="nf">Set-MailboxServer</span><span class="w"> </span><span class="nv">$maintServer</span><span class="w"> </span><span class="nt">-DatabaseCopyActivationDisabledAndMoveNow</span><span class="w"> </span><span class="nv">$False</span><span class="w">
</span><span class="nf">Write-Verbose</span><span class="w"> </span><span class="s2">"Set database back to the original setting"</span><span class="w">
</span><span class="nf">Set-MailboxServer</span><span class="w"> </span><span class="nv">$maintServer</span><span class="w"> </span><span class="nt">-DatabaseCopyAutoActivationPolicy</span><span class="w"> </span><span class="nx">Unrestricted</span><span class="w">
</span><span class="nf">Write-Verbose</span><span class="w"> </span><span class="s2">"Reactivate hub transport"</span><span class="w">
</span><span class="nf">Set-ServerComponentstate</span><span class="w"> </span><span class="nv">$maintServer</span><span class="w"> </span><span class="nt">-Component</span><span class="w"> </span><span class="nx">HubTransport</span><span class="w"> </span><span class="nt">-State</span><span class="w"> </span><span class="nx">Active</span><span class="w"> </span><span class="nt">-Requester</span><span class="w"> </span><span class="nx">Maintenance</span><span class="w">
</span><span class="nf">Write-Verbose</span><span class="w"> </span><span class="s2">"Restart Transport Services"</span><span class="w">
</span><span class="nf">Restart-service</span><span class="w"> </span><span class="nx">msexchangetransport</span><span class="w">
</span><span class="nf">Restart-service</span><span class="w"> </span><span class="nx">msexchangefrontendtransport</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">END</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></div>
<p>Its quick, simple helper scripts like this that makes things go a bit quicker and while it still takes an hour to do a CU upgrade it cuts some of the pre/post prep work out of the equation and could also be modified for SCCM to use in patching maintenance as well to help automate that process.</p>Douglas FrancisComing from Exchange 2010 one of the things that I love/hate about Exchange 2016 is when a new CU is installed it is essentially a full install of Exchange. In some ways, this is nice because it gets rid of a lot of the weird bugs that would occur post update after CUs that had weird triggers, and you get a nice fresh new install of Exchange, for the most part, every quarter. However, on the downside of this, the CU update process now takes much longer to complete in 2016 because, well you’re getting a nice fresh install. Also deploying that ~5GB ISO file around is a tad bit annoying at times. In my experience after the initial install, and the 3 or 4 CU’s I’ve done since, it takes approximately one hour per server for an install/CU upgrade from the moment that the setup program is executing its installation steps. Probably an hour and a half from setup launch/prereq check/install. In addition, you need to place the Exchange host in maintenance before starting the CU upgrade which can take some more time, especially if you have to look up commands again because I don’t know about you, but I don’t do Exchange CU updates frequently enough to remember the exact syntax for all the Powershell cmdlets. So that I could cut down on time spent interacting with the host during CU upgrade changes I made myself two small helper functions for maintenance tasks. First places the Exchange host in maintenance, drains active database copies and pauses cluster membership. Then the second reverses all of that. This probably saves me 15-20 minutes per host now, and that adds up when I have 4 Exchange hosts to patch. Also, with a bit more error handling and comment-based help, I eventually can turn these helper functions over to my GNOC and have them patch. First off, I run an elevated Exchange PowerShell module session on the server I’m starting the maintenance from. Import this function to my profile, execute it providing the name of the Exchange server I am currently working on, and another Exchange server for roles to move over to. <# .NOTES =========================================================================== Created with: SAPIEN Technologies, Inc., PowerShell Studio 2018 v5.5.151 Created on: 5/31/2018 2:10 PM Created by: Douglas Francis Organization: PoshOps.IO Filename: Start-ExchangeMaintenance.ps1 =========================================================================== .DESCRIPTION Places an exchange 2016 server in maintenance in prep for patching. #> function Start-ExchangeMaintenance { param ( [parameter(Mandatory = $true)] [validateNotNullOrEmpty()] [String]$maintServer, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$failoverServerFQDN ) BEGIN { } PROCESS { Write-Verbose "Draining Mail Queues" Set-ServerComponentState $maintServer -Component HubTransport -State Draining -Requester Maintenance Write-Verbose "Restarting Transport Services" Restart-Service MSExchangeTransport Restart-Service MSExchangeFrontEndTransport Write-Verbose "Redicting pending mail to Failover Server" Redirect-Message -Server $maintServer -Target $failoverServerFQDN Write-Verbose "Suspending DAG activity" Suspend-ClusterNode $maintServer Write-Verbose "Moving any Active Database Ownership to other servers" Set-MailboxServer $maintServer -DatabaseCopyActivationDisabledAndMoveNow $True Write-Verbose "Blocking mmaintenance server from hosting active database copies" Set-MailboxServer $maintServer -DatabaseCopyAutoActivationPolicy Blocked Write-Verbose "Placing Server in maintenance" Set-ServerComponentState $maintServer -Component ServerWideOffline -State Inactive -Requester Maintenance } END { } } I frequently like to use the “Write-Verbose” cmdlet for small things like this instead of comments Verbose provides info during execution and reading in the script provides as much info as a comment would in simple things like this. Everything that happens here is getting mail queues off the server, stopping membership in the DAG, moving over database ownership and placing exchange in maintenance. Once maintenance on the server completes, I import and run this script that reverses the actions of the first one allowing the exchange server to function in the DAG again fully. <# .NOTES =========================================================================== Created with: SAPIEN Technologies, Inc., PowerShell Studio 2018 v5.5.151 Created on: 5/31/2018 3:45 PM Created by: Douglas Francis Organization: Afni Filename: Stop-ExchangeMaintenance.ps1 =========================================================================== .DESCRIPTION Stops exchange 2016 server from being in maintenance and function in the DAG/mail flow again. #> function Stop-ExchangeMaintenance { param ( [parameter(Mandatory = $true)] [validateNotNullOrEmpty()] [String]$maintServer ) BEGIN { } PROCESS { Write-Verbose "Taking Server out of Maintenance" Set-ServerComponentState $maintServer -Component ServerWideOffline -State Active -Requester Maintenance Write-Verbose "Restart DAG activity" Resume-ClusterNode $maintServer Write-Verbose "Allow Database Activation" Set-MailboxServer $maintServer -DatabaseCopyActivationDisabledAndMoveNow $False Write-Verbose "Set database back to the original setting" Set-MailboxServer $maintServer -DatabaseCopyAutoActivationPolicy Unrestricted Write-Verbose "Reactivate hub transport" Set-ServerComponentstate $maintServer -Component HubTransport -State Active -Requester Maintenance Write-Verbose "Restart Transport Services" Restart-service msexchangetransport Restart-service msexchangefrontendtransport } END { } } Its quick, simple helper scripts like this that makes things go a bit quicker and while it still takes an hour to do a CU upgrade it cuts some of the pre/post prep work out of the equation and could also be modified for SCCM to use in patching maintenance as well to help automate that process.3PAR - Relocating Virtual Volumes to Different CPGs2018-05-23T00:00:00+00:002018-05-23T00:00:00+00:00https://poshops.io/blog/3PAR-Relocating-Virtual-Volumes-to-different-CPGs<p>There may come a time when you wish to move a 3PAR Virtual volume over to a different CPG, either there was a hardware/OS change that has new features in the CPG level that you would like to take advantage of, you do a report based on CPG’s or maybe to get some dedup optimization.</p>
<p>There are two ways to initiate a VV migration to a different CPG, you can either use the CLI, or you can use the StorServ Management Console.</p>
<p>When using the CLI, you need the same of the new CPG and the VV that’s moved. Remember that the CLI is case sensitive.</p>
<p>In my case I wanted to move the snapshots over the same CPG with the full volume, where snp_cpg is the flag for relocating snapshot data, ‘Infra_SSD_Raid6’ is the CPG that I want to move the VV to and ‘ESXVV-1’ is the VV that I want to move.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tunevv snp_cpg Infra_SSD_Raid6 ESXVV-1
</code></pre></div></div>
<p>If you want to move the actual volume and not the snapshot, the command uses the usr_cpg flag instead.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tunevv usr_cpg Infra_SSD_Raid6 ESXVV-1
</code></pre></div></div>
<p>When using the SSMC find the volume</p>
<p>Actions > tune</p>
<p>Select either “user space or “copy space” > select either “current CPG” or “move to another CPG,” if moving to another CPG select the new one.</p>
<p>Click tune.</p>
<p>Once the migration completes, you should compact the CPG’s after your done. To reclaim the space, the CPG has expanded to that was taken up from the volume that you’ve migrated to the different CPG.</p>Douglas FrancisThere may come a time when you wish to move a 3PAR Virtual volume over to a different CPG, either there was a hardware/OS change that has new features in the CPG level that you would like to take advantage of, you do a report based on CPG’s or maybe to get some dedup optimization. There are two ways to initiate a VV migration to a different CPG, you can either use the CLI, or you can use the StorServ Management Console. When using the CLI, you need the same of the new CPG and the VV that’s moved. Remember that the CLI is case sensitive. In my case I wanted to move the snapshots over the same CPG with the full volume, where snp_cpg is the flag for relocating snapshot data, ‘Infra_SSD_Raid6’ is the CPG that I want to move the VV to and ‘ESXVV-1’ is the VV that I want to move. tunevv snp_cpg Infra_SSD_Raid6 ESXVV-1 If you want to move the actual volume and not the snapshot, the command uses the usr_cpg flag instead. tunevv usr_cpg Infra_SSD_Raid6 ESXVV-1 When using the SSMC find the volume Actions > tune Select either “user space or “copy space” > select either “current CPG” or “move to another CPG,” if moving to another CPG select the new one. Click tune. Once the migration completes, you should compact the CPG’s after your done. To reclaim the space, the CPG has expanded to that was taken up from the volume that you’ve migrated to the different CPG.Disabling License Features in O3652017-02-08T00:00:00+00:002017-02-08T00:00:00+00:00https://poshops.io/blog/Disabling-License-Features-in-O365<p>With the changes Microsoft has deployed to O365, adding new Skype plan options to K1 accounts and updating the plan on other license plans, I needed to remove this license for ~5000 users.</p>
<p>Some of my K1 users have licenses solely for Sharepoint Online while some have it for both Exchange and Sharepoint.</p>
<p>What I’ve done is get all the MSOL users in my domain storing that in $allusers</p>
<p>Then I loop through getting all of my K1 accounts, do a Get-Mailbox to determine if they have a mailbox so that I don’t remove the license options for Exchange for people that already have it.</p>
<p>Depending on that erroring out or not, I create a new MSOL license then assign the right permissions.</p>
<p>If you only need to remove the Skype plan, this would be the new license option.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$licensePlan</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">New-MsolLicenseOptions</span><span class="w"> </span><span class="nt">-AccountSkuId</span><span class="w"> </span><span class="nx">COMPANY:DESKLESSPACK</span><span class="w"> </span><span class="nt">-DisabledPlans</span><span class="w"> </span><span class="s2">"MCOIMP"</span><span class="w">
</span></code></pre></div></div>
<p>This isn’t the fastest way to run this but I have a server dedicated to running powershell scripts so I just kick it off and wait.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm"><#
</span><span class="cs">.NOTES</span><span class="cm">
===========================================================================
Created with: SAPIEN Technologies, Inc., PowerShell Studio 2016 v5.3.130
Created on: 1/10/2017 1:08 PM
Created by: Douglas Francis
Organization: PoshOps.io
Filename: Remove-SkypeK1License
===========================================================================
</span><span class="cs">.DESCRIPTION</span><span class="cm">
A description of the file.
#></span><span class="w">
</span><span class="c"># Getting all MSOL users</span><span class="w">
</span><span class="nv">$allusers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">Get-MsolUser</span><span class="w"> </span><span class="nt">-all</span><span class="w">
</span><span class="c"># getting all the k1 users from that</span><span class="w">
</span><span class="nv">$k1users</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$allusers</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">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">islicensed</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s1">'true'</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">licenses</span><span class="o">.</span><span class="nf">accountskuid</span><span class="w"> </span><span class="o">-contains</span><span class="w"> </span><span class="s1">'COMPANY:desklesspack'</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="c">#counter for displaying progress</span><span class="w">
</span><span class="nv">$totalcount</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$k1users</span><span class="o">.</span><span class="nf">count</span><span class="w">
</span><span class="nv">$count</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="w">
</span><span class="c">#looping through all users</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$user</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$k1users</span><span class="p">)</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nf">Write-Host</span><span class="w"> </span><span class="nt">-ForegroundColor</span><span class="w"> </span><span class="nx">Red</span><span class="w"> </span><span class="s2">"Getting user </span><span class="nv">$count</span><span class="s2"> out of </span><span class="nv">$totalcount</span><span class="s2">"</span><span class="w">
</span><span class="nf">Get-Mailbox</span><span class="w"> </span><span class="nv">$user</span><span class="o">.</span><span class="nf">userprincipalname</span><span class="w">
</span><span class="nx">if</span><span class="w"> </span><span class="p">(</span><span class="bp">$?</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="bp">$true</span><span class="p">)</span><span class="w"> </span><span class="c"># if has mailbox, setting disabled options leaving exchange</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nv">$user</span><span class="o">.</span><span class="nf">userprincipalname</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">Out-File</span><span class="w"> </span><span class="nx">C:\Scripts\Script_Results\k1mail.txt</span><span class="w"> </span><span class="nt">-Append</span><span class="w">
</span><span class="nv">$licensePlan</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">New-MsolLicenseOptions</span><span class="w"> </span><span class="nt">-AccountSkuId</span><span class="w"> </span><span class="nx">COMPANY:DESKLESSPACK</span><span class="w"> </span><span class="nt">-DisabledPlans</span><span class="w"> </span><span class="s2">"SWAY"</span><span class="p">,</span><span class="w"> </span><span class="s2">"INTUNE_O365"</span><span class="p">,</span><span class="w"> </span><span class="s2">"YAMMER_ENTERPRISE"</span><span class="p">,</span><span class="w"> </span><span class="s2">"MCOIMP"</span><span class="w">
</span><span class="nf">Set-MsolUserLicense</span><span class="w"> </span><span class="nt">-UserPrincipalName</span><span class="w"> </span><span class="nv">$Upn</span><span class="w"> </span><span class="nt">-RemoveLicenses</span><span class="w"> </span><span class="nv">$lic</span><span class="o">.</span><span class="nf">licenses</span><span class="o">.</span><span class="nf">accountskuid</span><span class="w"> </span><span class="nt">-AddLicenses</span><span class="w"> </span><span class="s1">'COMPANY:DESKLESSPACK'</span><span class="w"> </span><span class="nt">-LicenseOptions</span><span class="w"> </span><span class="nv">$licensePlan</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="c"># setting disabled options for users that only get sharepoint</span><span class="w">
</span><span class="nv">$user</span><span class="o">.</span><span class="nf">userprincipalname</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">Out-File</span><span class="w"> </span><span class="nx">C:\Scripts\Script_Results\k1nomail.txt</span><span class="w"> </span><span class="nt">-Append</span><span class="w">
</span><span class="nv">$licensePlan</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">New-MsolLicenseOptions</span><span class="w"> </span><span class="nt">-AccountSkuId</span><span class="w"> </span><span class="nx">COMPANY:DESKLESSPACK</span><span class="w"> </span><span class="nt">-DisabledPlans</span><span class="w"> </span><span class="s2">"SWAY"</span><span class="p">,</span><span class="w"> </span><span class="s2">"INTUNE_O365"</span><span class="p">,</span><span class="w"> </span><span class="s2">"YAMMER_ENTERPRISE"</span><span class="p">,</span><span class="w"> </span><span class="s2">"EXCHANGE_S_DESKLESS"</span><span class="p">,</span><span class="w"> </span><span class="s2">"MCOIMP"</span><span class="w">
</span><span class="nf">Set-MsolUserLicense</span><span class="w"> </span><span class="nt">-UserPrincipalName</span><span class="w"> </span><span class="nv">$Upn</span><span class="w"> </span><span class="nt">-RemoveLicenses</span><span class="w"> </span><span class="nv">$lic</span><span class="o">.</span><span class="nf">licenses</span><span class="o">.</span><span class="nf">accountskuid</span><span class="w"> </span><span class="nt">-AddLicenses</span><span class="w"> </span><span class="s1">'COMPANY:DESKLESSPACK'</span><span class="w"> </span><span class="nt">-LicenseOptions</span><span class="w"> </span><span class="nv">$licensePlan</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="nv">$count</span><span class="o">++</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>Douglas FrancisWith the changes Microsoft has deployed to O365, adding new Skype plan options to K1 accounts and updating the plan on other license plans, I needed to remove this license for ~5000 users. Some of my K1 users have licenses solely for Sharepoint Online while some have it for both Exchange and Sharepoint. What I’ve done is get all the MSOL users in my domain storing that in $allusers Then I loop through getting all of my K1 accounts, do a Get-Mailbox to determine if they have a mailbox so that I don’t remove the license options for Exchange for people that already have it. Depending on that erroring out or not, I create a new MSOL license then assign the right permissions. If you only need to remove the Skype plan, this would be the new license option. $licensePlan = New-MsolLicenseOptions -AccountSkuId COMPANY:DESKLESSPACK -DisabledPlans "MCOIMP" This isn’t the fastest way to run this but I have a server dedicated to running powershell scripts so I just kick it off and wait. <# .NOTES =========================================================================== Created with: SAPIEN Technologies, Inc., PowerShell Studio 2016 v5.3.130 Created on: 1/10/2017 1:08 PM Created by: Douglas Francis Organization: PoshOps.io Filename: Remove-SkypeK1License =========================================================================== .DESCRIPTION A description of the file. #> # Getting all MSOL users $allusers = Get-MsolUser -all # getting all the k1 users from that $k1users = $allusers | Where-Object { $_.islicensed -eq 'true' -and $_.licenses.accountskuid -contains 'COMPANY:desklesspack' } #counter for displaying progress $totalcount = $k1users.count $count = 1 #looping through all users foreach ($user in $k1users) { Write-Host -ForegroundColor Red "Getting user $count out of $totalcount" Get-Mailbox $user.userprincipalname if ($? -eq $true) # if has mailbox, setting disabled options leaving exchange { $user.userprincipalname | Out-File C:\Scripts\Script_Results\k1mail.txt -Append $licensePlan = New-MsolLicenseOptions -AccountSkuId COMPANY:DESKLESSPACK -DisabledPlans "SWAY", "INTUNE_O365", "YAMMER_ENTERPRISE", "MCOIMP" Set-MsolUserLicense -UserPrincipalName $Upn -RemoveLicenses $lic.licenses.accountskuid -AddLicenses 'COMPANY:DESKLESSPACK' -LicenseOptions $licensePlan } else { # setting disabled options for users that only get sharepoint $user.userprincipalname | Out-File C:\Scripts\Script_Results\k1nomail.txt -Append $licensePlan = New-MsolLicenseOptions -AccountSkuId COMPANY:DESKLESSPACK -DisabledPlans "SWAY", "INTUNE_O365", "YAMMER_ENTERPRISE", "EXCHANGE_S_DESKLESS", "MCOIMP" Set-MsolUserLicense -UserPrincipalName $Upn -RemoveLicenses $lic.licenses.accountskuid -AddLicenses 'COMPANY:DESKLESSPACK' -LicenseOptions $licensePlan } $count++ }Inactive Computer Account Cleanup2015-10-28T00:00:00+00:002015-10-28T00:00:00+00:00https://poshops.io/blog/Inactive-Computer-Account-Cleanup<p>Previously, I talked about cleaning up inactive user accounts. This is how I am automating the same process for computer accounts. Note: in my environment, some *nix based computers did not seem to consistently update the value we are looking at, which may cause some issues. Thankfully the only *nix based computers in my environment are servers, and I am excluding this from my automation.</p>
<p>My scenario is having a multitude of locations across a dozen locations where the local techs have access to add and remove computers from the domain. These computers don’t always get removed from the environment, and I needed a way to do some cleanup. I created this script to run as a scheduled task on my PoSH automation server every week.</p>
<p>What I’m doing is searching for all computer objects in Active Directory, excluding the OU’s that my servers reside in. Mostly because if the issue I noted with *nix-based computers and avoiding unnecessary outages is a good thing.</p>
<p>Once the computers that have been identified as inactive; in my case 60 days since checking in with the domain, they are disabled and moved to a term computers OU.</p>
<p>I am getting all computer objects, excluding those in my Server OU’s and the termed computer OU, assigning those to the $computers variable.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">Import-Module</span><span class="w"> </span><span class="nx">ActiveDirectory</span><span class="w">
</span><span class="nv">$computers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">Get-ADComputer</span><span class="w"> </span><span class="nt">-Filter</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nt">-Properties</span><span class="w"> </span><span class="nx">lastlogondate</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">where</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">distinguishedName</span><span class="w"> </span><span class="o">-notmatch</span><span class="w"> </span><span class="s2">"OU=Server_New,OU=Corporate,DC=domain,DC=net"</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">distinguishedName</span><span class="w"> </span><span class="o">-notmatch</span><span class="w"> </span><span class="s2">"OU=Servers,OU=Corporate,DC=domain,DC=net"</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">distinguishedName</span><span class="w"> </span><span class="o">-notmatch</span><span class="w"> </span><span class="s2">"OU=TermComputerAccounts,OU=Termed Accounts,DC=domain,DC=net"</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Now that I have all the computer objects, I filter those out by the ones that have not contacted the domain in over 60 days, while also excluding newly created objects made within the previous week.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$InactiveComputers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$computers</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">Where</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">LastLogonDate</span><span class="w"> </span><span class="o">-le</span><span class="w"> </span><span class="err">$</span><span class="p">(</span><span class="nf">Get-Date</span><span class="p">)</span><span class="o">.</span><span class="nf">AddDays</span><span class="p">(</span><span class="nt">-60</span><span class="p">)</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Created</span><span class="w"> </span><span class="o">-le</span><span class="w"> </span><span class="err">$</span><span class="p">(</span><span class="nf">Get-Date</span><span class="p">)</span><span class="o">.</span><span class="nf">AddDays</span><span class="p">(</span><span class="nt">-7</span><span class="p">)</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Next, I export those results to a CSV that I keep. If I ever need to know when a specific computer was removed along with what OU it was in I have the log.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$Path</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"C:\Scripts\Script_Results\TermComputers"</span><span class="w">
</span><span class="nv">$Filename</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nf">Get-date</span><span class="p">)</span><span class="o">.</span><span class="nf">ToString</span><span class="p">(</span><span class="err">“</span><span class="nf">MM-dd-yyyy-HH-mm-ss</span><span class="err">”</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="err">“</span><span class="w"> </span><span class="nf">-</span><span class="w"> </span><span class="nx">60DayInActiveComputers.csv</span><span class="err">”</span><span class="w">
</span><span class="nv">$InactiveComputers</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">select</span><span class="w"> </span><span class="nx">name</span><span class="p">,</span><span class="w"> </span><span class="nx">lastlogondate</span><span class="p">,</span><span class="w"> </span><span class="nx">distinguishedname</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">Export-Csv</span><span class="w"> </span><span class="nv">$Path</span><span class="nx">\</span><span class="nv">$Filename</span><span class="w"> </span><span class="nt">-NoTypeInformation</span><span class="w">
</span></code></pre></div></div>
<p>Now I will loop through all of those results, disable the AD account, and move it to the termed computers OU.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$computer</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$InactiveComputers</span><span class="p">)</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nf">Get-ADComputer</span><span class="w"> </span><span class="nv">$computer</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">Disable-ADAccount</span><span class="w"> </span><span class="nt">-Confirm</span><span class="p">:</span><span class="bp">$false</span><span class="w">
</span><span class="nf">Get-ADComputer</span><span class="w"> </span><span class="nv">$computer</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">Move-ADObject</span><span class="w"> </span><span class="nt">-TargetPath</span><span class="w"> </span><span class="s2">"OU=TermDesktops,OU=TermComputerAccounts,OU=Termed Accounts,DC=domain,DC=net"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>The rest of the script is for formatting the results into HTML and sending out an email notification.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Setting up email and HTML for sending the report on what was disabled. </span><span class="w">
</span><span class="nv">$html</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"<style>"</span><span class="w"> </span><span class="nv">$html</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$html</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"BODY{background-color:white;}"</span><span class="w"> </span><span class="nv">$html</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$html</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"TABLE{border-width: 1px;border-style: solid;border-color: black;border-collapse: collapse;}"</span><span class="w"> </span><span class="nv">$html</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$html</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"TH{border-width: 1px;padding: 10px;border-style: solid;border-color: black;background-color:Crimson}"</span><span class="w"> </span><span class="nv">$html</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$html</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"TD{border-width: 1px;padding: 10px;border-style: solid;border-color: black;background-color:Yellow}"</span><span class="w"> </span><span class="nv">$html</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$html</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"</style>"</span><span class="w"> </span><span class="nv">$InactiveComputers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$InactiveComputers</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">select</span><span class="w"> </span><span class="nx">name</span><span class="p">,</span><span class="w"> </span><span class="nx">lastlogondate</span><span class="p">,</span><span class="w"> </span><span class="nx">distinguishedname</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">ConvertTo-Html</span><span class="w"> </span><span class="nt">-Head</span><span class="w"> </span><span class="nv">$html</span><span class="w">
</span><span class="c"># emailing report</span><span class="w">
</span><span class="nv">$smtpServer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"smtp.domian.net"</span><span class="w"> </span><span class="nv">$smtpFrom</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"PoSH_Reporting@domain.com"</span><span class="w"> </span><span class="nv">$message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">New-Object</span><span class="w"> </span><span class="nx">System.Net.Mail.MailMessage</span><span class="w"> </span><span class="nv">$message</span><span class="o">.</span><span class="nf">From</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"PoSH_Reporting@domain.com"</span><span class="w"> </span><span class="nv">$message</span><span class="o">.</span><span class="nf">to</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"infosec@domain.com"</span><span class="p">)</span><span class="w"> </span><span class="nv">$Message</span><span class="o">.</span><span class="nf">To</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"serveradmin@domain.com"</span><span class="p">)</span><span class="w"> </span><span class="nv">$Message</span><span class="o">.</span><span class="nf">to</span><span class="o">.</span><span class="nf">add</span><span class="p">(</span><span class="s2">"DTS_Engineering@domain.com"</span><span class="p">)</span><span class="w"> </span><span class="nv">$message</span><span class="o">.</span><span class="nf">Attachments</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"</span><span class="nv">$Path</span><span class="s2">\</span><span class="nv">$Filename</span><span class="s2">"</span><span class="p">)</span><span class="w"> </span><span class="nv">$message</span><span class="o">.</span><span class="nf">IsBodyHtml</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w"> </span><span class="nv">$message</span><span class="o">.</span><span class="nf">Subject</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"60 Day InActive Computers Report"</span><span class="w"> </span><span class="nv">$message</span><span class="o">.</span><span class="nf">Body</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Computer Objects that have not signed into the domain in over 60 days. <br/> These objects have been disabled and moved to the TermDesktops OU"</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nv">$InactiveComputers</span><span class="w"> </span><span class="nv">$smtp</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">New-Object</span><span class="w"> </span><span class="nx">Net.Mail.SmtpClient</span><span class="p">(</span><span class="nv">$smtpServer</span><span class="p">)</span><span class="w"> </span><span class="nv">$smtp</span><span class="o">.</span><span class="nf">Send</span><span class="p">(</span><span class="nv">$message</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>
<p>Here is the entire thing beginning to end. Code on GitHub.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm"><#
</span><span class="cs">.NOTES</span><span class="cm">
===========================================================================
Created with: SAPIEN Technologies, Inc., PowerShell Studio 2015 v4.2.95
Created on: 10/9/2015 2:05 PM
Created by: Douglas Francis
Organization: PoshOps.io
Filename: 60DayInactiveComputers
Version: 1.0
===========================================================================
</span><span class="cs">.DESCRIPTION</span><span class="cm">
Searches AD for computer objects that have not signed into the domain in over 60 days.
Excluding the Servers & Servers_New OU
Creates a CSV file of the servers found for history.
Disables the computers moves them to the Termed Computers OU.
#></span><span class="w">
</span><span class="c"># Getting all computers, excluding Servers OU and Termed Computers OU</span><span class="w">
</span><span class="nf">Import-Module</span><span class="w"> </span><span class="nx">ActiveDirectory</span><span class="w">
</span><span class="nv">$computers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">Get-ADComputer</span><span class="w"> </span><span class="nt">-Filter</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nt">-Properties</span><span class="w"> </span><span class="nx">lastlogondate</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">where</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">distinguishedName</span><span class="w"> </span><span class="o">-notmatch</span><span class="w"> </span><span class="s2">"OU=Server_New,OU=Corporate,DC=contoso,DC=net"</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">distinguishedName</span><span class="w"> </span><span class="o">-notmatch</span><span class="w"> </span><span class="s2">"OU=Servers,OU=Corporate,DC=contoso,DC=net"</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">distinguishedName</span><span class="w"> </span><span class="o">-notmatch</span><span class="w"> </span><span class="s2">"OU=TermComputerAccounts,OU=Termed Accounts,DC=contoso,DC=net"</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="c"># Filtering all computers down to those that have not signed in in 60 days. Excluding computers created in the past 7 days</span><span class="w">
</span><span class="nv">$InactiveComputers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$computers</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">Where</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">LastLogonDate</span><span class="w"> </span><span class="o">-le</span><span class="w"> </span><span class="err">$</span><span class="p">(</span><span class="nf">Get-Date</span><span class="p">)</span><span class="o">.</span><span class="nf">AddDays</span><span class="p">(</span><span class="nt">-60</span><span class="p">)</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Created</span><span class="w"> </span><span class="o">-le</span><span class="w"> </span><span class="err">$</span><span class="p">(</span><span class="nf">Get-Date</span><span class="p">)</span><span class="o">.</span><span class="nf">AddDays</span><span class="p">(</span><span class="nt">-7</span><span class="p">)</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="c"># Exporting Results to CSV</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">"C:\Scripts\Script_Results\TermComputers"</span><span class="w">
</span><span class="nv">$Filename</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nf">Get-date</span><span class="p">)</span><span class="o">.</span><span class="nf">ToString</span><span class="p">(</span><span class="err">“</span><span class="nf">MM-dd-yyyy-HH-mm-ss</span><span class="err">”</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="err">“</span><span class="w"> </span><span class="nf">-</span><span class="w"> </span><span class="nx">60DayInActiveComputers.csv</span><span class="err">”</span><span class="w">
</span><span class="nv">$InactiveComputers</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">select</span><span class="w"> </span><span class="nx">name</span><span class="p">,</span><span class="w"> </span><span class="nx">lastlogondate</span><span class="p">,</span><span class="w"> </span><span class="nx">distinguishedname</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">Export-Csv</span><span class="w"> </span><span class="nv">$Path</span><span class="nx">\</span><span class="nv">$Filename</span><span class="w"> </span><span class="nt">-NoTypeInformation</span><span class="w">
</span><span class="c"># Disabling all accounts and moving to term computers OU</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$computer</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$InactiveComputers</span><span class="p">)</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nf">Get-ADComputer</span><span class="w"> </span><span class="nv">$computer</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">Disable-ADAccount</span><span class="w"> </span><span class="nt">-Confirm</span><span class="p">:</span><span class="bp">$false</span><span class="w">
</span><span class="nf">Get-ADComputer</span><span class="w"> </span><span class="nv">$computer</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">Move-ADObject</span><span class="w"> </span><span class="nt">-TargetPath</span><span class="w"> </span><span class="s2">"OU=TermDesktops,OU=TermComputerAccounts,OU=Termed Accounts,DC=contoso,DC=net"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Setting up email and HTML for sending the report on what was disabled.</span><span class="w">
</span><span class="nv">$html</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"<style>"</span><span class="w">
</span><span class="nv">$html</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$html</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"BODY{background-color:white;}"</span><span class="w">
</span><span class="nv">$html</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$html</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"TABLE{border-width: 1px;border-style: solid;border-color: black;border-collapse: collapse;}"</span><span class="w">
</span><span class="nv">$html</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$html</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"TH{border-width: 1px;padding: 10px;border-style: solid;border-color: black;background-color:Crimson}"</span><span class="w">
</span><span class="nv">$html</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$html</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"TD{border-width: 1px;padding: 10px;border-style: solid;border-color: black;background-color:Yellow}"</span><span class="w">
</span><span class="nv">$html</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$html</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"</style>"</span><span class="w">
</span><span class="nv">$InactiveComputers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$InactiveComputers</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">select</span><span class="w"> </span><span class="nx">name</span><span class="p">,</span><span class="w"> </span><span class="nx">lastlogondate</span><span class="p">,</span><span class="w"> </span><span class="nx">distinguishedname</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">ConvertTo-Html</span><span class="w"> </span><span class="nt">-Head</span><span class="w"> </span><span class="nv">$html</span><span class="w">
</span><span class="c"># emailing report </span><span class="w">
</span><span class="nv">$smtpServer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"smtp.contoso.net"</span><span class="w">
</span><span class="nv">$smtpFrom</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"PoSH_Reporting@contoso.com"</span><span class="w">
</span><span class="nv">$message</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">New-Object</span><span class="w"> </span><span class="nx">System.Net.Mail.MailMessage</span><span class="w">
</span><span class="nv">$message</span><span class="o">.</span><span class="nf">From</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"PoSH_Reporting@contoso.com"</span><span class="w">
</span><span class="nv">$message</span><span class="o">.</span><span class="nf">to</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"infosec@contoso.com"</span><span class="p">)</span><span class="w">
</span><span class="nv">$Message</span><span class="o">.</span><span class="nf">To</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"serveradmin@contoso.com"</span><span class="p">)</span><span class="w">
</span><span class="nv">$Message</span><span class="o">.</span><span class="nf">to</span><span class="o">.</span><span class="nf">add</span><span class="p">(</span><span class="s2">"DTS_Engineering@contoso.com"</span><span class="p">)</span><span class="w">
</span><span class="nv">$message</span><span class="o">.</span><span class="nf">Attachments</span><span class="o">.</span><span class="nf">Add</span><span class="p">(</span><span class="s2">"</span><span class="nv">$Path</span><span class="s2">\</span><span class="nv">$Filename</span><span class="s2">"</span><span class="p">)</span><span class="w">
</span><span class="nv">$message</span><span class="o">.</span><span class="nf">IsBodyHtml</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
</span><span class="nv">$message</span><span class="o">.</span><span class="nf">Subject</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"60 Day InActive Computers Report"</span><span class="w">
</span><span class="nv">$message</span><span class="o">.</span><span class="nf">Body</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Computer Objects that have not signed into the domain in over 60 days. <br/> These objects have been disabled and moved to the TermDesktops OU"</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nv">$InactiveComputers</span><span class="w">
</span><span class="nv">$smtp</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">New-Object</span><span class="w"> </span><span class="nx">Net.Mail.SmtpClient</span><span class="p">(</span><span class="nv">$smtpServer</span><span class="p">)</span><span class="w">
</span><span class="nv">$smtp</span><span class="o">.</span><span class="nf">Send</span><span class="p">(</span><span class="nv">$message</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>Douglas FrancisPreviously, I talked about cleaning up inactive user accounts. This is how I am automating the same process for computer accounts. Note: in my environment, some *nix based computers did not seem to consistently update the value we are looking at, which may cause some issues. Thankfully the only *nix based computers in my environment are servers, and I am excluding this from my automation. My scenario is having a multitude of locations across a dozen locations where the local techs have access to add and remove computers from the domain. These computers don’t always get removed from the environment, and I needed a way to do some cleanup. I created this script to run as a scheduled task on my PoSH automation server every week. What I’m doing is searching for all computer objects in Active Directory, excluding the OU’s that my servers reside in. Mostly because if the issue I noted with *nix-based computers and avoiding unnecessary outages is a good thing. Once the computers that have been identified as inactive; in my case 60 days since checking in with the domain, they are disabled and moved to a term computers OU. I am getting all computer objects, excluding those in my Server OU’s and the termed computer OU, assigning those to the $computers variable. Import-Module ActiveDirectory $computers = Get-ADComputer -Filter * -Properties lastlogondate | where { $_.distinguishedName -notmatch "OU=Server_New,OU=Corporate,DC=domain,DC=net" -and $_.distinguishedName -notmatch "OU=Servers,OU=Corporate,DC=domain,DC=net" -and $_.distinguishedName -notmatch "OU=TermComputerAccounts,OU=Termed Accounts,DC=domain,DC=net" } Now that I have all the computer objects, I filter those out by the ones that have not contacted the domain in over 60 days, while also excluding newly created objects made within the previous week. $InactiveComputers = $computers | Where { $_.LastLogonDate -le $(Get-Date).AddDays(-60) -and $_.Created -le $(Get-Date).AddDays(-7) } Next, I export those results to a CSV that I keep. If I ever need to know when a specific computer was removed along with what OU it was in I have the log. $Path = "C:\Scripts\Script_Results\TermComputers" $Filename = (Get-date).ToString(“MM-dd-yyyy-HH-mm-ss”) + “ - 60DayInActiveComputers.csv” $InactiveComputers | select name, lastlogondate, distinguishedname | Export-Csv $Path\$Filename -NoTypeInformation Now I will loop through all of those results, disable the AD account, and move it to the termed computers OU. foreach ($computer in $InactiveComputers) { Get-ADComputer $computer | Disable-ADAccount -Confirm:$false Get-ADComputer $computer | Move-ADObject -TargetPath "OU=TermDesktops,OU=TermComputerAccounts,OU=Termed Accounts,DC=domain,DC=net" } The rest of the script is for formatting the results into HTML and sending out an email notification. # Setting up email and HTML for sending the report on what was disabled. $html = "<style>" $html = $html + "BODY{background-color:white;}" $html = $html + "TABLE{border-width: 1px;border-style: solid;border-color: black;border-collapse: collapse;}" $html = $html + "TH{border-width: 1px;padding: 10px;border-style: solid;border-color: black;background-color:Crimson}" $html = $html + "TD{border-width: 1px;padding: 10px;border-style: solid;border-color: black;background-color:Yellow}" $html = $html + "</style>" $InactiveComputers = $InactiveComputers | select name, lastlogondate, distinguishedname | ConvertTo-Html -Head $html # emailing report $smtpServer = "smtp.domian.net" $smtpFrom = "PoSH_Reporting@domain.com" $message = New-Object System.Net.Mail.MailMessage $message.From = "PoSH_Reporting@domain.com" $message.to.Add("infosec@domain.com") $Message.To.Add("serveradmin@domain.com") $Message.to.add("DTS_Engineering@domain.com") $message.Attachments.Add("$Path\$Filename") $message.IsBodyHtml = $true $message.Subject = "60 Day InActive Computers Report" $message.Body = "Computer Objects that have not signed into the domain in over 60 days. <br/> These objects have been disabled and moved to the TermDesktops OU" + $InactiveComputers $smtp = New-Object Net.Mail.SmtpClient($smtpServer) $smtp.Send($message) Here is the entire thing beginning to end. Code on GitHub. <# .NOTES =========================================================================== Created with: SAPIEN Technologies, Inc., PowerShell Studio 2015 v4.2.95 Created on: 10/9/2015 2:05 PM Created by: Douglas Francis Organization: PoshOps.io Filename: 60DayInactiveComputers Version: 1.0 =========================================================================== .DESCRIPTION Searches AD for computer objects that have not signed into the domain in over 60 days. Excluding the Servers & Servers_New OU Creates a CSV file of the servers found for history. Disables the computers moves them to the Termed Computers OU. #> # Getting all computers, excluding Servers OU and Termed Computers OU Import-Module ActiveDirectory $computers = Get-ADComputer -Filter * -Properties lastlogondate | where { $_.distinguishedName -notmatch "OU=Server_New,OU=Corporate,DC=contoso,DC=net" -and $_.distinguishedName -notmatch "OU=Servers,OU=Corporate,DC=contoso,DC=net" -and $_.distinguishedName -notmatch "OU=TermComputerAccounts,OU=Termed Accounts,DC=contoso,DC=net" } # Filtering all computers down to those that have not signed in in 60 days. Excluding computers created in the past 7 days $InactiveComputers = $computers | Where { $_.LastLogonDate -le $(Get-Date).AddDays(-60) -and $_.Created -le $(Get-Date).AddDays(-7) } # Exporting Results to CSV $Path = "C:\Scripts\Script_Results\TermComputers" $Filename = (Get-date).ToString(“MM-dd-yyyy-HH-mm-ss”) + “ - 60DayInActiveComputers.csv” $InactiveComputers | select name, lastlogondate, distinguishedname | Export-Csv $Path\$Filename -NoTypeInformation # Disabling all accounts and moving to term computers OU foreach ($computer in $InactiveComputers) { Get-ADComputer $computer | Disable-ADAccount -Confirm:$false Get-ADComputer $computer | Move-ADObject -TargetPath "OU=TermDesktops,OU=TermComputerAccounts,OU=Termed Accounts,DC=contoso,DC=net" } # Setting up email and HTML for sending the report on what was disabled. $html = "<style>" $html = $html + "BODY{background-color:white;}" $html = $html + "TABLE{border-width: 1px;border-style: solid;border-color: black;border-collapse: collapse;}" $html = $html + "TH{border-width: 1px;padding: 10px;border-style: solid;border-color: black;background-color:Crimson}" $html = $html + "TD{border-width: 1px;padding: 10px;border-style: solid;border-color: black;background-color:Yellow}" $html = $html + "</style>" $InactiveComputers = $InactiveComputers | select name, lastlogondate, distinguishedname | ConvertTo-Html -Head $html # emailing report $smtpServer = "smtp.contoso.net" $smtpFrom = "PoSH_Reporting@contoso.com" $message = New-Object System.Net.Mail.MailMessage $message.From = "PoSH_Reporting@contoso.com" $message.to.Add("infosec@contoso.com") $Message.To.Add("serveradmin@contoso.com") $Message.to.add("DTS_Engineering@contoso.com") $message.Attachments.Add("$Path\$Filename") $message.IsBodyHtml = $true $message.Subject = "60 Day InActive Computers Report" $message.Body = "Computer Objects that have not signed into the domain in over 60 days. <br/> These objects have been disabled and moved to the TermDesktops OU" + $InactiveComputers $smtp = New-Object Net.Mail.SmtpClient($smtpServer) $smtp.Send($message)Automated User Account Removal and O365 Cleanup2015-09-23T00:00:00+00:002015-09-23T00:00:00+00:00https://poshops.io/blog/Automated-User-Account-Removal-and-O365-Cleanup<p>In my previous post, I talked about finding, disabling, and placing inactive users into a separate OU. In this post, I use PowerShell to remove users from that OU along with any child objects they have, such as mobile devices, along with removing their license for O365.</p>
<p>This script runs as a scheduled task on my Powershell automation server every Monday. It searches through all users in the Term30 OU, which is where our ID Team places terminated users and are also where my inactive user script places users. Any user that has not been modified in over 30 days gets picked up by this script. I went with modified date instead of the last login date because I would rather be on the side of caution. This typically means user accounts sit here for 30-60 days before they are removed. The script also creates a CSV report and creates a ticket for removal of their home directory if they have one. Sadly, there are years of bad habits and people putting in mappings to other shares in users’ AD profiles so I cannot have the script nuke them automatically.</p>
<p>I start by using the transcription feature of PowerShell to log what the script does with a file name based on the date/time the script ran. This way, I can go back and view logs either for error reporting, or knowing when an account is deleted. I highly recommend any automated scripts that you create have some logging. They do not take up much space and are better to have than not.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Powershell transcription setup</span><span class="w">
</span><span class="c"># Create a filename based on a time stamp.</span><span class="w">
</span><span class="nv">$Filename</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">((</span><span class="nf">Get-date</span><span class="p">)</span><span class="o">.</span><span class="nf">Month</span><span class="p">)</span><span class="o">.</span><span class="nf">ToString</span><span class="p">()</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"-"</span><span class="w"> </span><span class="o">+</span><span class="err">`</span><span class="w">
</span><span class="p">((</span><span class="nf">Get-date</span><span class="p">)</span><span class="o">.</span><span class="nf">Day</span><span class="p">)</span><span class="o">.</span><span class="nf">ToString</span><span class="p">()</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"-"</span><span class="w"> </span><span class="o">+</span><span class="err">`</span><span class="w">
</span><span class="p">((</span><span class="nf">Get-date</span><span class="p">)</span><span class="o">.</span><span class="nf">Year</span><span class="p">)</span><span class="o">.</span><span class="nf">ToString</span><span class="p">()</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"-"</span><span class="w"> </span><span class="o">+</span><span class="err">`</span><span class="w">
</span><span class="p">((</span><span class="nf">Get-date</span><span class="p">)</span><span class="o">.</span><span class="nf">Hour</span><span class="p">)</span><span class="o">.</span><span class="nf">ToString</span><span class="p">()</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"-"</span><span class="w"> </span><span class="o">+</span><span class="err">`</span><span class="w">
</span><span class="p">((</span><span class="nf">Get-date</span><span class="p">)</span><span class="o">.</span><span class="nf">Minute</span><span class="p">)</span><span class="o">.</span><span class="nf">ToString</span><span class="p">()</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"-"</span><span class="w"> </span><span class="o">+</span><span class="err">`</span><span class="w">
</span><span class="p">((</span><span class="nf">Get-date</span><span class="p">)</span><span class="o">.</span><span class="nf">Second</span><span class="p">)</span><span class="o">.</span><span class="nf">ToString</span><span class="p">()</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"-PurgeTerm30"</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">".txt"</span><span class="w">
</span><span class="c"># Set the storage path.</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">"C:\Scripts\PS Transcripts"</span><span class="w">
</span><span class="c"># Turn on PowerShell transcripting. </span><span class="w">
</span><span class="nf">Start-Transcript</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="s2">"</span><span class="nv">$Path</span><span class="s2">\</span><span class="nv">$Filename</span><span class="s2">"</span><span class="w">
</span></code></pre></div></div>
<p>UPDATE: as pointed out in the comments there is a much better way to create a file name for the transcript.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$Filename</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nf">Get-date</span><span class="p">)</span><span class="o">.</span><span class="nf">ToString</span><span class="p">(</span><span class="err">“</span><span class="nf">MM-dd-yyyy-HH-mm-ss</span><span class="err">”</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="err">“</span><span class="nt">-PurgeTerm30</span><span class="o">.</span><span class="nf">txt</span><span class="err">”</span><span class="w">
</span></code></pre></div></div>
<p>Now I connect to O365 with a service account created specifically for this purpose. Remember to set the account password not to expire or otherwise you need to update the script. Also, side note, this runs as a packaged .exe created with Powershell Studio, so I’m not overly concerned with having credentials stored in here.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># setting up modules and O365 connections</span><span class="w">
</span><span class="kr">Function</span><span class="w"> </span><span class="nf">New-Connection</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="c">#Credential setup for O365</span><span class="w">
</span><span class="nv">$O365User</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"deleting@domain4.onmicrosoft.com"</span><span class="w">
</span><span class="nv">$O365Pass</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"p@ssw0rd"</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">ConvertTo-SecureString</span><span class="w"> </span><span class="nt">-AsPlainText</span><span class="w"> </span><span class="nt">-Force</span><span class="w">
</span><span class="nv">$O365Creds</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">New-Object</span><span class="w"> </span><span class="nx">System.Management.Automation.PSCredential</span><span class="w"> </span><span class="p">(</span><span class="nv">$O365User</span><span class="p">,</span><span class="w"> </span><span class="nv">$O365Pass</span><span class="p">)</span><span class="w">
</span><span class="c"># O365 Function</span><span class="w">
</span><span class="kr">Function</span><span class="w"> </span><span class="nf">Connect-O365</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nv">$Session</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">New-PSSession</span><span class="w"> </span><span class="nt">-ConfigurationName</span><span class="w"> </span><span class="nx">Microsoft.Exchange</span><span class="w"> </span><span class="nt">-ConnectionUri</span><span class="w"> </span><span class="nx">https://ps.outlook.com/powershell/</span><span class="w"> </span><span class="nt">-Credential</span><span class="w"> </span><span class="nv">$O365Creds</span><span class="w"> </span><span class="nt">-Authentication</span><span class="w"> </span><span class="nx">Basic</span><span class="w"> </span><span class="nt">-AllowRedirection</span><span class="w">
</span><span class="nf">Import-PSSession</span><span class="w"> </span><span class="nv">$Session</span><span class="w"> </span><span class="nt">-AllowClobber</span><span class="w">
</span><span class="nf">Import-Module</span><span class="w"> </span><span class="nx">Msonline</span><span class="w">
</span><span class="nf">Connect-MSOLService</span><span class="w"> </span><span class="nt">-credential</span><span class="w"> </span><span class="nv">$O365Creds</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Connecting and improrting modules</span><span class="w">
</span><span class="nf">Connect-O365</span><span class="w">
</span><span class="nf">Import-Module</span><span class="w"> </span><span class="nx">ActiveDirectory</span><span class="w">
</span><span class="nf">Import-Module</span><span class="w"> </span><span class="nx">MSOnline</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="nf">New-Connection</span><span class="w">
</span></code></pre></div></div>
<p>Next, I search through the Term30 OU for user accounts that are disabled, in case someone accidentally put an active account in there, has not been modified for 30 days or greater and the homedirectory value. Then export that data to a CSV file</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$OldUsers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">Get-ADUser</span><span class="w"> </span><span class="nt">-Searchbase</span><span class="w"> </span><span class="s2">"OU=Term30, OU=Termed Accounts, DC=domain, DC=com"</span><span class="w"> </span><span class="nt">-Filter</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nt">-Properties</span><span class="w"> </span><span class="nx">samaccountname</span><span class="p">,</span><span class="w"> </span><span class="nx">Modified</span><span class="p">,</span><span class="w"> </span><span class="nx">homedirectory</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">Where</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Modified</span><span class="w"> </span><span class="o">-le</span><span class="w"> </span><span class="err">$</span><span class="p">(</span><span class="nf">Get-Date</span><span class="p">)</span><span class="o">.</span><span class="nf">AddDays</span><span class="p">(</span><span class="nt">-30</span><span class="p">)</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">enabled</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="bp">$false</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c"># | select samaccountname, Modified</span><span class="w">
</span><span class="c"># Exporting users to CSV for verifcation</span><span class="w">
</span><span class="nv">$OldUsers</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">Export-Csv</span><span class="w"> </span><span class="nx">C:\Scripts\Script_Results\Term30.csv</span><span class="w"> </span><span class="nt">-NoTypeInformation</span><span class="w">
</span></code></pre></div></div>
<p>Once I have my list of users into a CSV that gets emailed to the helpdesk to create a ticket for removal of the home directories. If you trust all the users have the correct value in their AD profile this could be automated as well, the service account this runs under would need the appropriate permissions on the file server to remove the users’ folder.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">Send-MailMessage</span><span class="w"> </span><span class="nt">-From</span><span class="w"> </span><span class="s2">"Term30Script <Term30Script@domain.com>"</span><span class="w"> </span><span class="nt">-To</span><span class="w"> </span><span class="s2">"helpdesk@domain.com"</span><span class="w"> </span><span class="nt">-Subject</span><span class="w"> </span><span class="s2">"Network Shares from Term30"</span><span class="w"> </span><span class="nt">-Body</span><span class="w"> </span><span class="s2">"Task to Server team. Please see attatched CSV file"</span><span class="w"> </span><span class="nt">-Attachments</span><span class="w"> </span><span class="s2">"C:\Scripts\Script_Results\Term30.csv"</span><span class="w"> </span><span class="nt">-SmtpServer</span><span class="w"> </span><span class="s2">"smtp.domain.net"</span><span class="w">
</span></code></pre></div></div>
<p>Now on to the O365 part, fair warning though. Once a license is removed, the mailbox is gone forever and cannot be recovered. You may wish to place mailboxes into a litigation hold depending on your companies requirements for retaining mail. Most of our users this is not needed, and if it is required, we receive the request to apply the litigation hold when the user is terminated/quits so I am not worried about it here.</p>
<p>I loop through each of the users in $oldusers removing the license and removing the mailbox from the recycle bin. I have the error action to silently continue because 90% of our employees only get internal email only through iMail from IPSwitch.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$user</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$OldUsers</span><span class="p">)</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nf">Remove-MsolUser</span><span class="w"> </span><span class="nt">-UserPrincipalName</span><span class="w"> </span><span class="s2">"</span><span class="nv">$User</span><span class="s2">"</span><span class="w"> </span><span class="nt">-Force</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="nx">SilentlyContinue</span><span class="w">
</span><span class="nf">Remove-MsolUser</span><span class="w"> </span><span class="nt">-UserPrincipalName</span><span class="w"> </span><span class="s2">"</span><span class="nv">$User</span><span class="s2">"</span><span class="w"> </span><span class="nt">-RemoveFromRecycleBin</span><span class="w"> </span><span class="nt">-Force</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="nx">SilentlyContinue</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Finally, the script removes the AD user object itself along with any child objects it has. This is the biggest difference, apart from the O365 license removal over our old solution as it would break on accounts with child objects.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$user</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$OldUsers</span><span class="p">)</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nf">Remove-ADobject</span><span class="w"> </span><span class="p">(</span><span class="nf">Get-ADUser</span><span class="w"> </span><span class="nv">$user</span><span class="p">)</span><span class="o">.</span><span class="nf">distinguishedname</span><span class="w"> </span><span class="nt">-Recursive</span><span class="w"> </span><span class="nt">-Confirm</span><span class="p">:</span><span class="bp">$false</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Here is the complete script, just note you will need the Azure/Microsoft Sign in assistant software installed for the O365 part to work.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm"><#
</span><span class="cs">.NOTES</span><span class="cm">
===========================================================================
Created with: SAPIEN Technologies, Inc., PowerShell Studio 2015 v4.2.86
Created on: 7/16/2015 10:20 AM
Created by: DouglasFrancis
Organization: PoshOps.io
Filename: PurgeTerm30_V_0_2
===========================================================================
</span><span class="cs">.DESCRIPTION</span><span class="cm">
Searches through the Term30 OU and find any accounts that have not been modified in the last 30 days.
It then deletes these accounts along with it's child objects and O365 license.
#></span><span class="w">
</span><span class="c"># Powershell transcription setup</span><span class="w">
</span><span class="c"># Create a filename based on a time stamp.</span><span class="w">
</span><span class="nv">$Filename</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">((</span><span class="nf">Get-date</span><span class="p">)</span><span class="o">.</span><span class="nf">Month</span><span class="p">)</span><span class="o">.</span><span class="nf">ToString</span><span class="p">()</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"-"</span><span class="w"> </span><span class="o">+</span><span class="err">`</span><span class="w">
</span><span class="p">((</span><span class="nf">Get-date</span><span class="p">)</span><span class="o">.</span><span class="nf">Day</span><span class="p">)</span><span class="o">.</span><span class="nf">ToString</span><span class="p">()</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"-"</span><span class="w"> </span><span class="o">+</span><span class="err">`</span><span class="w">
</span><span class="p">((</span><span class="nf">Get-date</span><span class="p">)</span><span class="o">.</span><span class="nf">Year</span><span class="p">)</span><span class="o">.</span><span class="nf">ToString</span><span class="p">()</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"-"</span><span class="w"> </span><span class="o">+</span><span class="err">`</span><span class="w">
</span><span class="p">((</span><span class="nf">Get-date</span><span class="p">)</span><span class="o">.</span><span class="nf">Hour</span><span class="p">)</span><span class="o">.</span><span class="nf">ToString</span><span class="p">()</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"-"</span><span class="w"> </span><span class="o">+</span><span class="err">`</span><span class="w">
</span><span class="p">((</span><span class="nf">Get-date</span><span class="p">)</span><span class="o">.</span><span class="nf">Minute</span><span class="p">)</span><span class="o">.</span><span class="nf">ToString</span><span class="p">()</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"-"</span><span class="w"> </span><span class="o">+</span><span class="err">`</span><span class="w">
</span><span class="p">((</span><span class="nf">Get-date</span><span class="p">)</span><span class="o">.</span><span class="nf">Second</span><span class="p">)</span><span class="o">.</span><span class="nf">ToString</span><span class="p">()</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"-PurgeTerm30"</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">".txt"</span><span class="w">
</span><span class="c"># Set the storage path.</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">"C:\Scripts\PS Transcripts"</span><span class="w">
</span><span class="c"># Turn on PowerShell transcripting. </span><span class="w">
</span><span class="nf">Start-Transcript</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="s2">"</span><span class="nv">$Path</span><span class="s2">\</span><span class="nv">$Filename</span><span class="s2">"</span><span class="w">
</span><span class="c"># setting up modules and O365 connections</span><span class="w">
</span><span class="kr">Function</span><span class="w"> </span><span class="nf">New-Connection</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="c">#Credential setup for O365</span><span class="w">
</span><span class="nv">$O365User</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"deleting@domain4.onmicrosoft.com"</span><span class="w">
</span><span class="nv">$O365Pass</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"p@ssw0rd"</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">ConvertTo-SecureString</span><span class="w"> </span><span class="nt">-AsPlainText</span><span class="w"> </span><span class="nt">-Force</span><span class="w">
</span><span class="nv">$O365Creds</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">New-Object</span><span class="w"> </span><span class="nx">System.Management.Automation.PSCredential</span><span class="w"> </span><span class="p">(</span><span class="nv">$O365User</span><span class="p">,</span><span class="w"> </span><span class="nv">$O365Pass</span><span class="p">)</span><span class="w">
</span><span class="c"># O365 Function</span><span class="w">
</span><span class="kr">Function</span><span class="w"> </span><span class="nf">Connect-O365</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nv">$Session</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">New-PSSession</span><span class="w"> </span><span class="nt">-ConfigurationName</span><span class="w"> </span><span class="nx">Microsoft.Exchange</span><span class="w"> </span><span class="nt">-ConnectionUri</span><span class="w"> </span><span class="nx">https://ps.outlook.com/powershell/</span><span class="w"> </span><span class="nt">-Credential</span><span class="w"> </span><span class="nv">$O365Creds</span><span class="w"> </span><span class="nt">-Authentication</span><span class="w"> </span><span class="nx">Basic</span><span class="w"> </span><span class="nt">-AllowRedirection</span><span class="w">
</span><span class="nf">Import-PSSession</span><span class="w"> </span><span class="nv">$Session</span><span class="w"> </span><span class="nt">-AllowClobber</span><span class="w">
</span><span class="nf">Import-Module</span><span class="w"> </span><span class="nx">Msonline</span><span class="w">
</span><span class="nf">Connect-MSOLService</span><span class="w"> </span><span class="nt">-credential</span><span class="w"> </span><span class="nv">$O365Creds</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Connecting and improrting modules</span><span class="w">
</span><span class="nf">Connect-O365</span><span class="w">
</span><span class="nf">Import-Module</span><span class="w"> </span><span class="nx">ActiveDirectory</span><span class="w">
</span><span class="nf">Import-Module</span><span class="w"> </span><span class="nx">MSOnline</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="nf">New-Connection</span><span class="w">
</span><span class="c"># Searching through term 30 OU for accounts that have not been modified in 30 days or greater.</span><span class="w">
</span><span class="c"># NOTE: uncomment out the end of the line and the export line to export data to CSV. </span><span class="w">
</span><span class="c"># However, with the select-object cmdlet in place the remove-adobject cmdlet will not work.</span><span class="w">
</span><span class="nv">$OldUsers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">Get-ADUser</span><span class="w"> </span><span class="nt">-Searchbase</span><span class="w"> </span><span class="s2">"OU=Term30, OU=Termed Accounts, DC=domain, DC=net"</span><span class="w"> </span><span class="nt">-Filter</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nt">-Properties</span><span class="w"> </span><span class="nx">samaccountname</span><span class="p">,</span><span class="w"> </span><span class="nx">Modified</span><span class="p">,</span><span class="w"> </span><span class="nx">homedirectory</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">Where</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Modified</span><span class="w"> </span><span class="o">-le</span><span class="w"> </span><span class="err">$</span><span class="p">(</span><span class="nf">Get-Date</span><span class="p">)</span><span class="o">.</span><span class="nf">AddDays</span><span class="p">(</span><span class="nt">-30</span><span class="p">)</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">enabled</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="bp">$false</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c"># | select samaccountname, Modified</span><span class="w">
</span><span class="c"># Exporting users to CSV for verifcation</span><span class="w">
</span><span class="nv">$OldUsers</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">Export-Csv</span><span class="w"> </span><span class="nx">C:\Scripts\Script_Results\Term30.csv</span><span class="w"> </span><span class="nt">-NoTypeInformation</span><span class="w">
</span><span class="c"># Emailing CSV</span><span class="w">
</span><span class="nf">Send-MailMessage</span><span class="w"> </span><span class="nt">-From</span><span class="w"> </span><span class="s2">"Term30Script <Term30Script@domain.com>"</span><span class="w"> </span><span class="nt">-To</span><span class="w"> </span><span class="s2">"domainitsupport@domain.com"</span><span class="w"> </span><span class="nt">-Subject</span><span class="w"> </span><span class="s2">"Network Shares from Term30"</span><span class="w"> </span><span class="nt">-Body</span><span class="w"> </span><span class="s2">"Task to Server team. Please see attached CSV file"</span><span class="w"> </span><span class="nt">-Attachments</span><span class="w"> </span><span class="s2">"C:\Scripts\Script_Results\Term30.csv"</span><span class="w"> </span><span class="nt">-SmtpServer</span><span class="w"> </span><span class="s2">"smtp.domain.net"</span><span class="w">
</span><span class="c"># Removing the user from O365</span><span class="w">
</span><span class="c"># NOTE: actions are commented out bc there is no -whatif flag for the Remove-msoluser cmdlet </span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$user</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$OldUsers</span><span class="p">)</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nf">Remove-MsolUser</span><span class="w"> </span><span class="nt">-UserPrincipalName</span><span class="w"> </span><span class="s2">"</span><span class="nv">$User</span><span class="s2">"</span><span class="w"> </span><span class="nt">-Force</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="nx">SilentlyContinue</span><span class="w">
</span><span class="nf">Remove-MsolUser</span><span class="w"> </span><span class="nt">-UserPrincipalName</span><span class="w"> </span><span class="s2">"</span><span class="nv">$User</span><span class="s2">"</span><span class="w"> </span><span class="nt">-RemoveFromRecycleBin</span><span class="w"> </span><span class="nt">-Force</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="nx">SilentlyContinue</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Removing the AD User accounts. and it's child objects</span><span class="w">
</span><span class="c"># NOTE: remove the -whatif flag for prod use</span><span class="w">
</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$user</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$OldUsers</span><span class="p">)</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nf">Remove-ADobject</span><span class="w"> </span><span class="p">(</span><span class="nf">Get-ADUser</span><span class="w"> </span><span class="nv">$user</span><span class="p">)</span><span class="o">.</span><span class="nf">distinguishedname</span><span class="w"> </span><span class="nt">-Recursive</span><span class="w"> </span><span class="nt">-Confirm</span><span class="p">:</span><span class="bp">$false</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>Douglas FrancisIn my previous post, I talked about finding, disabling, and placing inactive users into a separate OU. In this post, I use PowerShell to remove users from that OU along with any child objects they have, such as mobile devices, along with removing their license for O365. This script runs as a scheduled task on my Powershell automation server every Monday. It searches through all users in the Term30 OU, which is where our ID Team places terminated users and are also where my inactive user script places users. Any user that has not been modified in over 30 days gets picked up by this script. I went with modified date instead of the last login date because I would rather be on the side of caution. This typically means user accounts sit here for 30-60 days before they are removed. The script also creates a CSV report and creates a ticket for removal of their home directory if they have one. Sadly, there are years of bad habits and people putting in mappings to other shares in users’ AD profiles so I cannot have the script nuke them automatically. I start by using the transcription feature of PowerShell to log what the script does with a file name based on the date/time the script ran. This way, I can go back and view logs either for error reporting, or knowing when an account is deleted. I highly recommend any automated scripts that you create have some logging. They do not take up much space and are better to have than not. # Powershell transcription setup # Create a filename based on a time stamp. $Filename = ((Get-date).Month).ToString() + "-" +` ((Get-date).Day).ToString() + "-" +` ((Get-date).Year).ToString() + "-" +` ((Get-date).Hour).ToString() + "-" +` ((Get-date).Minute).ToString() + "-" +` ((Get-date).Second).ToString() + "-PurgeTerm30" + ".txt" # Set the storage path. $Path = "C:\Scripts\PS Transcripts" # Turn on PowerShell transcripting. Start-Transcript -Path "$Path\$Filename" UPDATE: as pointed out in the comments there is a much better way to create a file name for the transcript. $Filename = (Get-date).ToString(“MM-dd-yyyy-HH-mm-ss”) + “-PurgeTerm30.txt” Now I connect to O365 with a service account created specifically for this purpose. Remember to set the account password not to expire or otherwise you need to update the script. Also, side note, this runs as a packaged .exe created with Powershell Studio, so I’m not overly concerned with having credentials stored in here. # setting up modules and O365 connections Function New-Connection { #Credential setup for O365 $O365User = "deleting@domain4.onmicrosoft.com" $O365Pass = "p@ssw0rd" | ConvertTo-SecureString -AsPlainText -Force $O365Creds = New-Object System.Management.Automation.PSCredential ($O365User, $O365Pass) # O365 Function Function Connect-O365 { $Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://ps.outlook.com/powershell/ -Credential $O365Creds -Authentication Basic -AllowRedirection Import-PSSession $Session -AllowClobber Import-Module Msonline Connect-MSOLService -credential $O365Creds } # Connecting and improrting modules Connect-O365 Import-Module ActiveDirectory Import-Module MSOnline } New-Connection Next, I search through the Term30 OU for user accounts that are disabled, in case someone accidentally put an active account in there, has not been modified for 30 days or greater and the homedirectory value. Then export that data to a CSV file $OldUsers = Get-ADUser -Searchbase "OU=Term30, OU=Termed Accounts, DC=domain, DC=com" -Filter * -Properties samaccountname, Modified, homedirectory | Where { $_.Modified -le $(Get-Date).AddDays(-30) -and $_.enabled -eq $false } # | select samaccountname, Modified # Exporting users to CSV for verifcation $OldUsers | Export-Csv C:\Scripts\Script_Results\Term30.csv -NoTypeInformation Once I have my list of users into a CSV that gets emailed to the helpdesk to create a ticket for removal of the home directories. If you trust all the users have the correct value in their AD profile this could be automated as well, the service account this runs under would need the appropriate permissions on the file server to remove the users’ folder. Send-MailMessage -From "Term30Script <Term30Script@domain.com>" -To "helpdesk@domain.com" -Subject "Network Shares from Term30" -Body "Task to Server team. Please see attatched CSV file" -Attachments "C:\Scripts\Script_Results\Term30.csv" -SmtpServer "smtp.domain.net" Now on to the O365 part, fair warning though. Once a license is removed, the mailbox is gone forever and cannot be recovered. You may wish to place mailboxes into a litigation hold depending on your companies requirements for retaining mail. Most of our users this is not needed, and if it is required, we receive the request to apply the litigation hold when the user is terminated/quits so I am not worried about it here. I loop through each of the users in $oldusers removing the license and removing the mailbox from the recycle bin. I have the error action to silently continue because 90% of our employees only get internal email only through iMail from IPSwitch. foreach ($user in $OldUsers) { Remove-MsolUser -UserPrincipalName "$User" -Force -ErrorAction SilentlyContinue Remove-MsolUser -UserPrincipalName "$User" -RemoveFromRecycleBin -Force -ErrorAction SilentlyContinue } Finally, the script removes the AD user object itself along with any child objects it has. This is the biggest difference, apart from the O365 license removal over our old solution as it would break on accounts with child objects. foreach ($user in $OldUsers) { Remove-ADobject (Get-ADUser $user).distinguishedname -Recursive -Confirm:$false } Here is the complete script, just note you will need the Azure/Microsoft Sign in assistant software installed for the O365 part to work. <# .NOTES =========================================================================== Created with: SAPIEN Technologies, Inc., PowerShell Studio 2015 v4.2.86 Created on: 7/16/2015 10:20 AM Created by: DouglasFrancis Organization: PoshOps.io Filename: PurgeTerm30_V_0_2 =========================================================================== .DESCRIPTION Searches through the Term30 OU and find any accounts that have not been modified in the last 30 days. It then deletes these accounts along with it's child objects and O365 license. #> # Powershell transcription setup # Create a filename based on a time stamp. $Filename = ((Get-date).Month).ToString() + "-" +` ((Get-date).Day).ToString() + "-" +` ((Get-date).Year).ToString() + "-" +` ((Get-date).Hour).ToString() + "-" +` ((Get-date).Minute).ToString() + "-" +` ((Get-date).Second).ToString() + "-PurgeTerm30" + ".txt" # Set the storage path. $Path = "C:\Scripts\PS Transcripts" # Turn on PowerShell transcripting. Start-Transcript -Path "$Path\$Filename" # setting up modules and O365 connections Function New-Connection { #Credential setup for O365 $O365User = "deleting@domain4.onmicrosoft.com" $O365Pass = "p@ssw0rd" | ConvertTo-SecureString -AsPlainText -Force $O365Creds = New-Object System.Management.Automation.PSCredential ($O365User, $O365Pass) # O365 Function Function Connect-O365 { $Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://ps.outlook.com/powershell/ -Credential $O365Creds -Authentication Basic -AllowRedirection Import-PSSession $Session -AllowClobber Import-Module Msonline Connect-MSOLService -credential $O365Creds } # Connecting and improrting modules Connect-O365 Import-Module ActiveDirectory Import-Module MSOnline } New-Connection # Searching through term 30 OU for accounts that have not been modified in 30 days or greater. # NOTE: uncomment out the end of the line and the export line to export data to CSV. # However, with the select-object cmdlet in place the remove-adobject cmdlet will not work. $OldUsers = Get-ADUser -Searchbase "OU=Term30, OU=Termed Accounts, DC=domain, DC=net" -Filter * -Properties samaccountname, Modified, homedirectory | Where { $_.Modified -le $(Get-Date).AddDays(-30) -and $_.enabled -eq $false } # | select samaccountname, Modified # Exporting users to CSV for verifcation $OldUsers | Export-Csv C:\Scripts\Script_Results\Term30.csv -NoTypeInformation # Emailing CSV Send-MailMessage -From "Term30Script <Term30Script@domain.com>" -To "domainitsupport@domain.com" -Subject "Network Shares from Term30" -Body "Task to Server team. Please see attached CSV file" -Attachments "C:\Scripts\Script_Results\Term30.csv" -SmtpServer "smtp.domain.net" # Removing the user from O365 # NOTE: actions are commented out bc there is no -whatif flag for the Remove-msoluser cmdlet foreach ($user in $OldUsers) { Remove-MsolUser -UserPrincipalName "$User" -Force -ErrorAction SilentlyContinue Remove-MsolUser -UserPrincipalName "$User" -RemoveFromRecycleBin -Force -ErrorAction SilentlyContinue } # Removing the AD User accounts. and it's child objects # NOTE: remove the -whatif flag for prod use foreach ($user in $OldUsers) { Remove-ADobject (Get-ADUser $user).distinguishedname -Recursive -Confirm:$false }Active Directory Cleanup with Powershell: Finding inactive users2015-08-16T00:00:00+00:002015-08-16T00:00:00+00:00https://poshops.io/blog/Active-Directory-Cleanup-with-Powershell-Finding-Inactive-Users<p>Keeping Active Directory clean can be a very time-consuming task; this can be especially true in a company like mine. We add and remove 100’s of users a month from the environment. It also does not help that some of our clients have accounts with us for accessing tools and resources, and other shenanigans. With this in mind, I set about to write a script to run as a scheduled task every Monday morning, that searches through our AD environment. Minus a handful of specific OU’s such as Service Accounts, Leave of Absence users and the like and finds any account that has not signed into in over 90 days.</p>
<p>I also needed to get any account that has not been signed into at all. Thankfully when searching by the LastLogonDate property, an account that has not signed into will also show up. Sadly I had to do this because it isn’t uncommon for Vender/Client accounts to be requested and then never used. They all typically are set to expire but why worry about it or have them there if I don’t have to.</p>
<p>So now that I know, roughly, what I need. I’ll use a simple, but long, command to get all of the users in a variable to work with.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$Users</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">Get-ADUser</span><span class="w"> </span><span class="nt">-Properties</span><span class="w"> </span><span class="nx">LastLogonDate</span><span class="p">,</span><span class="w"> </span><span class="nx">created</span><span class="w"> </span><span class="nt">-Filter</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">where</span><span class="w"> </span><span class="p">{</span><span class="bp">$_</span><span class="o">.</span><span class="nf">distinguishedname</span><span class="w"> </span><span class="o">-notmatch</span><span class="w"> </span><span class="s2">"OU=External Accounts,DC=domain,DC=net"</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">distinguishedname</span><span class="w"> </span><span class="o">-notmatch</span><span class="w"> </span><span class="s2">"CN=Microsoft Exchange System Objects,DC=domain,DC=net"</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">distinguishedname</span><span class="w"> </span><span class="o">-notmatch</span><span class="w"> </span><span class="s2">"OU=Exchange Accounts,OU=Accounts,OU=Corporate,DC=domain,DC=net"</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">distinguishedname</span><span class="w"> </span><span class="o">-notmatch</span><span class="w"> </span><span class="s2">"CN=Builtin,DC=domain,DC=net"</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Next, I’ll search through those users for any users that have been inactive greater than 90 days.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$InactiveUsers</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Users</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">Where</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">LastLogonDate</span><span class="w"> </span><span class="o">-le</span><span class="w"> </span><span class="err">$</span><span class="p">(</span><span class="nf">Get-Date</span><span class="p">)</span><span class="o">.</span><span class="nf">AddDays</span><span class="p">(</span><span class="nt">-90</span><span class="p">)</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Created</span><span class="w"> </span><span class="o">-le</span><span class="w"> </span><span class="err">$</span><span class="p">(</span><span class="nf">Get-Date</span><span class="p">)</span><span class="o">.</span><span class="nf">AddDays</span><span class="p">(</span><span class="nt">-14</span><span class="p">)}</span><span class="w">
</span></code></pre></div></div>
<p>Then we are going to export that to a CSV file to ready it to be emailed into our ticketing system.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$InactiveUsers</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">select</span><span class="w"> </span><span class="nx">name</span><span class="p">,</span><span class="w"> </span><span class="nx">LastLogonDate</span><span class="p">,</span><span class="w"> </span><span class="nx">created</span><span class="p">,</span><span class="w"> </span><span class="nx">distinguishedname</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">Export-Csv</span><span class="w"> </span><span class="nx">C:\Scripts\Script_Results\90users.csv</span><span class="w"> </span><span class="nt">-NoTypeInformation</span><span class="w">
</span></code></pre></div></div>
<p>Tacking that into an email for tracking in our ticketing system.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">Send-MailMessage</span><span class="w"> </span><span class="nt">-From</span><span class="w"> </span><span class="s2">"InactiveUsersScript <InactiveUserScript@domain.com>"</span><span class="w"> </span><span class="nt">-To</span><span class="w"> </span><span class="s2">"domainitsupport@domain.com"</span><span class="w"> </span><span class="nt">-Cc</span><span class="w"> </span><span class="s2">"serverteam@domain.com"</span><span class="p">,</span><span class="w"> </span><span class="s2">"infosec@domain.com"</span><span class="p">,</span><span class="w"> </span><span class="s2">"ITSCSCTech@domain.com"</span><span class="p">,</span><span class="w"> </span><span class="s2">"ITSCSCIDs@domain.com"</span><span class="w"> </span><span class="nt">-Subject</span><span class="w"> </span><span class="s2">"Users inactive- 90 days"</span><span class="w"> </span><span class="nt">-Body</span><span class="w"> </span><span class="s2">"Please see attached CSV file"</span><span class="w"> </span><span class="nt">-Attachments</span><span class="w"> </span><span class="s2">"C:\Scripts\Script_Results\90users.csv"</span><span class="w"> </span><span class="nt">-SmtpServer</span><span class="w"> </span><span class="s2">"smtp.domain.net"</span><span class="w">
</span></code></pre></div></div>
<p>Finally, taking all of these accounts disabling them and moving them into an OU for termed employees. Accounts sit in this OU for 30 days after their last modified date and then another script cleans that up.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$user</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$InactiveUsers</span><span class="p">)</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nf">Disable-ADAccount</span><span class="w"> </span><span class="nt">-Identity</span><span class="w"> </span><span class="nv">$user</span><span class="w"> </span><span class="nt">-Confirm</span><span class="p">:</span><span class="bp">$false</span><span class="w">
</span><span class="nf">Get-ADUser</span><span class="w"> </span><span class="nv">$user</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="nf">Move-ADObject</span><span class="w"> </span><span class="nt">-TargetPath</span><span class="w"> </span><span class="s2">"OU=Term30,OU=Termed Accounts,DC=domain,DC=net"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Check out the full code on GitHub here.</p>Douglas FrancisKeeping Active Directory clean can be a very time-consuming task; this can be especially true in a company like mine. We add and remove 100’s of users a month from the environment. It also does not help that some of our clients have accounts with us for accessing tools and resources, and other shenanigans. With this in mind, I set about to write a script to run as a scheduled task every Monday morning, that searches through our AD environment. Minus a handful of specific OU’s such as Service Accounts, Leave of Absence users and the like and finds any account that has not signed into in over 90 days. I also needed to get any account that has not been signed into at all. Thankfully when searching by the LastLogonDate property, an account that has not signed into will also show up. Sadly I had to do this because it isn’t uncommon for Vender/Client accounts to be requested and then never used. They all typically are set to expire but why worry about it or have them there if I don’t have to. So now that I know, roughly, what I need. I’ll use a simple, but long, command to get all of the users in a variable to work with. $Users = Get-ADUser -Properties LastLogonDate, created -Filter * | where {$_.distinguishedname -notmatch "OU=External Accounts,DC=domain,DC=net" -and $_.distinguishedname -notmatch "CN=Microsoft Exchange System Objects,DC=domain,DC=net" -and $_.distinguishedname -notmatch "OU=Exchange Accounts,OU=Accounts,OU=Corporate,DC=domain,DC=net" -and $_.distinguishedname -notmatch "CN=Builtin,DC=domain,DC=net" } Next, I’ll search through those users for any users that have been inactive greater than 90 days. $InactiveUsers = $Users | Where { $_.LastLogonDate -le $(Get-Date).AddDays(-90) -and $_.Created -le $(Get-Date).AddDays(-14)} Then we are going to export that to a CSV file to ready it to be emailed into our ticketing system. $InactiveUsers | select name, LastLogonDate, created, distinguishedname | Export-Csv C:\Scripts\Script_Results\90users.csv -NoTypeInformation Tacking that into an email for tracking in our ticketing system. Send-MailMessage -From "InactiveUsersScript <InactiveUserScript@domain.com>" -To "domainitsupport@domain.com" -Cc "serverteam@domain.com", "infosec@domain.com", "ITSCSCTech@domain.com", "ITSCSCIDs@domain.com" -Subject "Users inactive- 90 days" -Body "Please see attached CSV file" -Attachments "C:\Scripts\Script_Results\90users.csv" -SmtpServer "smtp.domain.net" Finally, taking all of these accounts disabling them and moving them into an OU for termed employees. Accounts sit in this OU for 30 days after their last modified date and then another script cleans that up. foreach ($user in $InactiveUsers) { Disable-ADAccount -Identity $user -Confirm:$false Get-ADUser $user | Move-ADObject -TargetPath "OU=Term30,OU=Termed Accounts,DC=domain,DC=net" } Check out the full code on GitHub here.Microsoft Ignite 2015: Post Conference2015-05-17T00:00:00+00:002015-05-17T00:00:00+00:00https://poshops.io/blog/Microsoft-Ignite-2015-Post-Conference<p>Well, a week has passed since the end of Microsoft Ignite. After digging myself out all the things in the office that did not get done while I was away, I finally have time to write this.</p>
<p>Thinking back to Ignite, I am delighted that all sessions were recorded and posted on Channel9 along with the slide decks. And that a lot of the speakers also provided additional things like example scripts as well that are available.</p>
<p>With that in mind, I doubt that I will go to Ignite next year. It will be in the same location which I am not a huge fan of. Due to the issues that I had with things like there were not hotels within a short walking distance of the actual venue, the shuttle buses were only ran in the morning and evening along with the venue workers making everyone feel like cattle how they yelled and screamed to usher attendees from one location to the other.</p>
<p>Instead of going to Ignite, I will wait for all of the sessions to be posted online, and pick and choose the ones that look interesting to me and get the content that way. Right now TechMentor seems to be my current idea of replacing going to Ignite.</p>
<p>Also, things such as PowerShell Saturday’s and SQL Saturday’s are some excellent ideas for smaller training/industry events as well. But as for now, Ignite is not on my list of training/conference events for next year. However, I will be following what people say about next year Ignite and depending on what I hear from the community from it maybe Ignite will be back on my radar in 2017. That should give them plenty of time working out the conflict of bringing five conferences into one and the issues that occurred this year.</p>Douglas FrancisWell, a week has passed since the end of Microsoft Ignite. After digging myself out all the things in the office that did not get done while I was away, I finally have time to write this. Thinking back to Ignite, I am delighted that all sessions were recorded and posted on Channel9 along with the slide decks. And that a lot of the speakers also provided additional things like example scripts as well that are available. With that in mind, I doubt that I will go to Ignite next year. It will be in the same location which I am not a huge fan of. Due to the issues that I had with things like there were not hotels within a short walking distance of the actual venue, the shuttle buses were only ran in the morning and evening along with the venue workers making everyone feel like cattle how they yelled and screamed to usher attendees from one location to the other. Instead of going to Ignite, I will wait for all of the sessions to be posted online, and pick and choose the ones that look interesting to me and get the content that way. Right now TechMentor seems to be my current idea of replacing going to Ignite. Also, things such as PowerShell Saturday’s and SQL Saturday’s are some excellent ideas for smaller training/industry events as well. But as for now, Ignite is not on my list of training/conference events for next year. However, I will be following what people say about next year Ignite and depending on what I hear from the community from it maybe Ignite will be back on my radar in 2017. That should give them plenty of time working out the conflict of bringing five conferences into one and the issues that occurred this year.Microsoft Ignite 2015: Day 4 Thoughts2015-05-08T00:00:00+00:002015-05-08T00:00:00+00:00https://poshops.io/blog/Microsoft-Ignite-2015-Day-4-Thoughts<p>It is the end of another day of Microsoft Ignite, and I must say today was the best day I have had so far this week. The mobile app did not crash on me nearly as much; I think I only had to put my phone in airplane mode twice today to prevent the app from crashing. Lunch was even reasonably decent.</p>
<p>Anyway, on to the sessions, I had some excellent sessions about Powershell goodness with Jeffrey Snover. The first was about JEA, Just Enough Administration. The concept is, using Powershell JEA endpoints and JEA toolkits, administrators can have granular control over what functions and cmdlets users have access to. For example, instead of allowing a user to restart any processes, it can be controlled down to which processes that person can restart. If you are interested in checking out more about this the session video is posted online on the channel9 site; you can check it out here.</p>
<p>Also, Mark Minasi and Mark Russinovich had a good talk about cloud computing. It was probably one of my favorite talks as it was amusing, informative, and a laid back session. I highly recommend checking out the video for this session as well. Once again available on channel9 here.</p>
<p>I wrapped up the day with some more Powershell goodness — PowerShell unplugged by Jeffrey Snover and Don Jones. Don mostly just crashed it apparently but still good stuff. They discussed new things that are coming out in Powershell V5 such as the introduction of classes and the new package management. Check it out here.</p>Douglas FrancisIt is the end of another day of Microsoft Ignite, and I must say today was the best day I have had so far this week. The mobile app did not crash on me nearly as much; I think I only had to put my phone in airplane mode twice today to prevent the app from crashing. Lunch was even reasonably decent. Anyway, on to the sessions, I had some excellent sessions about Powershell goodness with Jeffrey Snover. The first was about JEA, Just Enough Administration. The concept is, using Powershell JEA endpoints and JEA toolkits, administrators can have granular control over what functions and cmdlets users have access to. For example, instead of allowing a user to restart any processes, it can be controlled down to which processes that person can restart. If you are interested in checking out more about this the session video is posted online on the channel9 site; you can check it out here. Also, Mark Minasi and Mark Russinovich had a good talk about cloud computing. It was probably one of my favorite talks as it was amusing, informative, and a laid back session. I highly recommend checking out the video for this session as well. Once again available on channel9 here. I wrapped up the day with some more Powershell goodness — PowerShell unplugged by Jeffrey Snover and Don Jones. Don mostly just crashed it apparently but still good stuff. They discussed new things that are coming out in Powershell V5 such as the introduction of classes and the new package management. Check it out here.