Skip to content

Instantly share code, notes, and snippets.

@fabricesemti80
Last active January 31, 2024 12:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fabricesemti80/d4212367f0bcfc8cd8547ab805c9662e to your computer and use it in GitHub Desktop.
Save fabricesemti80/d4212367f0bcfc8cd8547ab805c9662e to your computer and use it in GitHub Desktop.
OctopusAnatomy#1

Analysis of an Octopus task

An analysis of the task http://pricing-deploy.sportingsolutions.com/deploy/app#/Spaces-1/projects/ipa-football-dal-simulation-service/deployments/process The task has 8 steps.

Tasks

  1. The first step uses PowerShell to stop and remove the service.
$name = $OctopusParameters['name']
# Kill any service management windows that are open as these can impact script
# http://support.microsoft.com/kb/823942
 Get-Process -n "mmc" -ErrorAction  SilentlyContinue | Stop-Process -ErrorAction SilentlyContinue
# Stop and delete existing service
$service = Get-Service $name -ErrorAction SilentlyContinue
if ($service -ne $null)
{
    Write-Host "Service exists, gracefully stopping"
    if($service.Status -eq "Running")
    {
  $servicePID = (Get-Wmiobject win32_Service | Where { $_.Name -eq $service.Name }).ProcessID
        $stopTries = 0
        do
        {
           $service | Stop-Service -Force -ErrorAction SilentlyContinue
            #allow service time to perform status change
            Start-Sleep -seconds 3
            #update status
            $service = Get-Service $name -ErrorAction SilentlyContinue
            $stopTries += 1
        } #keep trying until Servic is Stopped or Tries exceed 3
        until($service.Status -eq "Stopped" -or $stopTries -ge 3)
  $ProcessActive = Get-Process -Id $servicePID -ErrorAction SilentlyContinue
  if($ProcessActive -ne $null)
  {
   Write-Host "Service process exists, killing process"
   Stop-Process $servicePID -Force -erroraction 'silentlycontinue'
  }
    }
    Write-Host "Deleting service"
    $toDelete = Get-WmiObject -Class Win32_Service -Filter "name='$name'"
    $toDelete.delete()
}
else
{
    Write-Host "Service does not exist"
}
Write-Host "Complete"

This task is way overcomplicated. With Ansible all would be needed is:

# https://docs.ansible.com/ansible/latest/collections/ansible/windows/win_service_module.html
- name: Remove a service
  ansible.windows.win_service:
    name: service name
    state: absent
  1. This step is creating a local user account (to be used by this service). It again uses PowerShell.
$username = $OctopusParameters['Username']
$password = $OctopusParameters['Password']
$memberOf = $OctopusParameters['MemberOf']
$userRights = $OctopusParameters['UserRights']
# Add/Update User
$user = Get-WmiObject Win32_UserAccount -filter "LocalAccount=True AND Name='$username'"
if($user -eq $null)
{
    Write-Host "Adding user $username"
    net user "$username" "$password" /add /expires:never /passwordchg:no /yes
}
else
{
    Write-Host "User $username already exists, updating password"
    net user "$username" "$password" /expires:never /passwordchg:no
}
# Ensure password never expires
Write-Host "Ensuring password never expires"
net accounts /maxpwage:unlimited
WMIC USERACCOUNT WHERE "Name='$username'" SET PasswordExpires=FALSE
# Add/Update group membership
if($memberOf)
{
    $groups = $memberOf.Split(",")
    foreach($group in $groups)
    {
        $ntGroup = [ADSI]("WinNT://./$group")
        $members = $ntGroup.psbase.Invoke("Members") | %{$_.GetType().InvokeMember("Name", 'GetProperty', $null, $_, $null)}
        if($members -contains "$username")
        {
            Write-Host "User already a member of the $group group"
        }
        else
        {
            Write-Host "Adding to the $group group"
            net localgroup "$group" "$username" /add
        }
    }
}
# Add/Update user rights assignment
if($userRights)
{
    $userRightsArr = $userRights.Split(",")
    foreach($userRight in $userRightsArr)
    {
        Write-Host "Granting $userRight right"
        & "ntrights" +r $userRight -u "$username"
    }
}

This is again a massive overcomplication. Mixing cmd and powershell commands (as in the above script) is also not a great idea, not to mention the reliance on ntrights.exe', a file from an old and unsuported (like 20 years-old).exe` file. (Goes without saying using this old file is something even Microsoft does not endorse, as the kit it was part of no longer available from their site - it is a security risk that should be eliminated!)

Better ways to achieve the same results

  • If one prefers PowerShell, Microsoft has a complete module to do any CRUD operation on local users and groups --> New-LocalUser and Add-LocalGroupMember can do most of the work. For assigning the SeServiceLogonRight probably a custom module is needed, for example this one (haven't tested it, might need some refinements)
  • Alternatively Ansible is of course capable all of the above in a few simple steps:
## cat tasks/main.yaml
---
- name: "Ensure {{ local_group }} is present"
  ansible.windows.win_group:
    name: "{{ local_group }}"
    description: "{{  local_group_desc  }}"
    state: present
- name: "Ensure {{ user_account  }} is present"
  ansible.windows.win_user:
    name: "{{ local_user  }}"
    # password: B0bP4ssw0rd
    state: present
    groups:
      - "{{ local_group }}"
- name: "Add {{  local_right }} to {{ user_account  }}"
  win_user_right:
    name: "{{ local_right }}"
    users:
      - "{{ local_user  }}"
    action: add
##  cat defaults/main.yaml
---
local_user: "techops"
local_group: "ServiceAccounts"
local_group_desc: "Group for Service Accounts"
local_right: SeServiceLogonRight

BEFORE: Alt text

Running play: Alt text

AFTER: Alt text

This task does again a simple thing overcomplicated:

  • our script:
## I dont list the parameters here, they come from Octopus variables
$ruleName = $OctopusParameters['RuleName']
$localPort = $OctopusParameters['LocalPort']
$remotePort = $OctopusParameters['RemotePort']
$protocol = $OctopusParameters['Protocol']
$direction = $OctopusParameters['Direction']
# Remove any existing rule
Write-Host "Removing existing rule"
netsh advfirewall firewall delete rule name="$ruleName" dir=$direction
# Add new rule
Write-Host "Adding new rule"
netsh advfirewall firewall add rule name="$ruleName" dir=$direction action=allow protocol=$protocol localport=$localPort remoteport=$remotePort

what it should be really:

# https://docs.ansible.com/ansible/latest/collections/community/windows/win_firewall_rule_module.html
- name: <RuleName>
  community.windows.win_firewall_rule:
    name: <RuleName>
    localport: <LocalPort>
    action: allow
    direction: in
    protocol: <Protocol>
    # profiles: domain # optional; defaults to 'domain,private,public
    state: present
    enabled: yes
    ```

The current task is:

  • removing the firewall rule (needlessly)
  • than recreates the same rule (needlessly) It should really do a logical check of "is it exist - if not, create" --> indempotency is given with Ansible
  • it is also managed to - in just for lines of actual code! - mix cmd and powerhsell; totally unneccesarily, as PowerShell has a command to manage firewall rules, if that is something we want...
  1. Messaging Firewall Same as the previous step, just with a different port
  2. Deploy Service This task uses Octopus's built-in mechanisms to deploy an application from a NuGet feed. This is probably the only step where there are questions weather this could be done via Ansible for example. As I am rarely dealing with application development, can't say for certain, but there is a good chance this module or this one using Chocolatey would be more appropriate.
  3. File System - Permissions The last (active; there are some disabled steps) action step is setting some file permissions:
$item = $OctopusParameters['Item']
$readPermissionsTo = $OctopusParameters['ReadPermissionsTo']
$writePermissionsTo = $OctopusParameters['WritePermissionsTo']
# Check item exists
if(!(Test-Path $item))
{
    throw "$item does not exist"
}
# Assign read permissions
if($readPermissionsTo)
{
    $users = $readPermissionsTo.Split(",")
    foreach($user in $users)
    {
        Write-Host "Adding read permissions for $user"
        $acl = Get-Acl $item
        $acl.SetAccessRuleProtection($False, $False)
        $rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
                $user, "Read", "ContainerInherit, ObjectInherit", "None", "Allow")
        $acl.AddAccessRule($rule)
        Set-Acl $item $acl
    }
}
# Assign write permissions
if($writePermissionsTo)
{
    $users = $writePermissionsTo.Split(",")
    foreach($user in $users)
    {
        Write-Host "Adding write permissions for $user"
        $acl = Get-Acl $item
        $acl.SetAccessRuleProtection($False, $False)
        $rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
                $user, "Write", "ContainerInherit, ObjectInherit", "None", "Allow")
        $acl.AddAccessRule($rule)
        Set-Acl $item $acl
    }
}
Write-Host "Complete"

This can be done much simler way with this module

#random example
- name: Read ACL
  ansible.windows.win_acl:
    path: < Item >
    user: < ReadPermissionsTo >
    rights: Read
    type: allow
    state: present
- name: Write ACL
  ansible.windows.win_acl:
    path: < Item >
    user: < WritePermissionsTo >
    rights: Read
    type: allow
    state: present
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment