Friday, May 1, 2015

Further automating GoToMeeting on a Windows client

To continue to build on my previous post I decided to add a bit of id10t proofing to my automation script.

The only thing that the script does not do is logon the user (I mean, who really wants to handle (secure) user credentials in the first place).

This is here for my benefit, but I am sure that this can be useful to someone else.  There is nothing here that I did not discover through internet search or my own discovery.

I don't register an event monitor for a simple reason, I cannot guarantee that the execution of this program has admin rights.  Admin access on the local computer is required to register for an event, though it would be nice for a user to be able to register for user mode process events.

Enjoy!

<#
.SYNOPSIS
    This will install the full G2M client for hosting meetings if it is not already installed prior to joining the meeting.
.PARAMETERS
    $g2mId - the meeting ID that will be joined. This can be any g2m ID, the user will be 'joining' an existing meeting (theirs or someone else's).
    $g2mKill - if there is a G2M already running that will be killed and the passed G2M will be joined.
.AUTHOR
    Brian Ehlert, Citrix Labs, Redmond, WA
#>


function startG2m
{

Param(
    [parameter(Mandatory = $false)]
    [ValidateNotNullOrEmpty()]
    [string]$g2mId,
    [parameter(Mandatory = $false)]
    [boolean]$g2mKill = $false
)

    # finds the path of g2mstart.exe
    # each version is in its own folder so you have to find the latest / most recent version
    $gtmFolder = Get-ChildItem -Path $env:LOCALAPPDATA\Citrix\GotoMeeting -ErrorAction SilentlyContinue | Sort-Object -Property Name -Descending | Select-Object -First 1

    if ( !($gtmFolder) ) {
        "G2M is _not_ installed. Installing the hosting client."
       
        # Install the GTM client
        Start-Process "https://global.gotomeeting.com/meeting/host"

        Start-Sleep -Seconds 2
        # close the IE tab that was opened
        $ie = (New-Object -COM "Shell.Application").Windows() | where { $_.Name -eq "Internet Explorer" }

        foreach ($tab in $ie) {
            if ( $tab.LocationURL -match "download.citrixonline.com" ) {
                do {
                    Start-Sleep -Seconds 1
                } until ( $tab.Busy -eq $false )
                $tab.Quit()
            }
        }

        # wait for the client logon prompt
        Do { Start-Sleep -Seconds 2 } until ( Get-Process -Name "g2mlauncher" -ErrorAction SilentlyContinue )

        startG2m -g2mId $g2mId
    } else {
        # is there an existing G2M that needs to be killed?
        # if we reconnect to the same meeting too quickly we will have ourself as an orphaned attendee.
        if ($g2mKill) {
            if ((Get-Process -Name "g2mui" -ErrorAction SilentlyContinue)) {
                "Killing the running G2M"
                Get-Process -Name "g2mui" | Stop-Process -Force
                Do { Start-Sleep -Seconds 2 } until ((Get-Process -Name g2mlauncher -ErrorAction SilentlyContinue).Handles -lt 500)
            } else {"The user was not joined to a G2M"}
        }

        "G2M is installed, using the installed client"
        # build the path
        $g2mstartPath = $gtmFolder.FullName + "\g2mstart.exe"

        # Define the arguments - double quotes is important for g2mstart.exe
        $g2mArgs = ("'/Action Join' '/MeetingID $g2mId'" ).Replace("'",'"')

        # is there a G2M video conference running?  We will collide if there is.
        if (!(Get-Process -Name "g2*conference")) {
            if ( $g2mId ){
                "Joining the meeting"
                Start-Process -FilePath $g2mstartPath -ArgumentList $g2mArgs
            } else { "No G2M ID was provided" }
        } else { "A G2M is already running" }
    }
}


#Example
startG2m -g2mId 000000000 -g2mKill $true
Start-Sleep -Seconds 5  # wait for the conference to initialize
$process = Get-Process -Name "g2*conference"
Wait-Process -InputObject $process # wait for the person to end the conference
# Do something else
"boo!"

Tuesday, April 28, 2015

PowerShell launching GoToMeeting on Windows

It isn't every day that you need to execute GUI application commands via command line. 
After all there are some GUI applications that simply don't design for it and don't document it. 
It really is not a use case that they expect.

GoToMeeting is just one of those applications.  It is never anticipated that a person might want to launch a session via a command line.

Well, I recently had such a case.  And I have two different ways to handle this.

First, a little bit about the application. 
GoToMeeting is a user mode application that is 100% in a user's profile.  It is web based, and has a web based launcher that always delivers the latest and greatest client.  The client side application is not found under %ProgramFiles%.

Launching / Joining a Meeting:
Method 1 (the modern way):

Start-Process http://www.gotomeeting.com/join/$g2mId

Method 2 (the legacy way):
This method was the most prevalent result when I searched.  Send arguments to g2mstart.exe.
However, this is also the most problematic.  GoToMeeting updates its client pretty frequently, and each client is in a new folder, so if you want the latest client you have to do a bit of discovery and you must format the string properly.

# each version is in its own folder so you have to find the latest / most recent version
$gtmFolder = Get-ChildItem -Path $env:LOCALAPPDATA\Citrix\GotoMeeting | Sort-Object -Property Name -Descending | Select-Object -First 1

# build the path
$g2mstartPath = $gtmFolder.FullName + "\g2mstart.exe"

# Define the arguments - double quotes is important for g2mstart.exe
$g2mArgs = ("'/Action Join' '/MeetingID $g2mId'" ).Replace("'",'"')

# start the meeting
Start-Process -FilePath $g2mstartPath -ArgumentList $g2mArgs


It would be improper of me not to focus on the modern way of doing this.  The way that is expected.  However, the one thing that doing it that way will accomplish is that the client might be forced into an update.  If you don't want your client updated, but instead want a nice seamless join, you might want to use the legacy method.

Allow me to present the modern and legacy functions:


function startG2m {
Param(
 [string]$g2mId
)

  # start the meeting
  Start-Process http://www.gotomeeting.com/join/$g2mId
  Start-Sleep -Seconds 2

  # close the IE tab that was opened
  $ie = (New-Object -COM "Shell.Application").Windows() | where { $_.Name -eq "Internet Explorer" }

  foreach ($tab in $ie) {
    if ( $tab.LocationURL -match "gotomeeting.com" ) {
      do { 
Start-Sleep -Seconds 1 } until ( $tab.Busy -eq $false )
      $tab.Quit()
    }

  }
}



function startG2mLegacy {

Param(
 [string]$g2mId
)

  # finds the path of g2mstart.exe
  # each version is in its own folder so you have to find the latest / most recent version

  $gtmFolder = Get-ChildItem -Path $env:LOCALAPPDATA\Citrix\GotoMeeting | Sort-Object -Property Name -Descending | Select-Object -First 1

  # build the path
  $g2mstartPath = $gtmFolder.FullName + "\g2mstart.exe"

  # Define the arguments - double quotes is important for g2mstart.exe
  $g2mArgs = ("'/Action Join' '/MeetingID $g2mId'" ).Replace("'",'"')

  # start the meeting
  Start-Process -FilePath $g2mstartPath -ArgumentList $g2mArgs

}


Now, using these two functions is identical.

startG2m $g2mId

But, being a person who does test I always like a bit of error handling, I hate popup dialogs.  And I especially hate dialogs if they interrupt my nice automation - a real buzz killer.

Here I have two checks - 1. is there an existing meeting that I need to kill to connect to a new meeting ("there can be only one"  - the Highlander) and 2. Don't start it if one is running.

# is there an existing G2M that we need to kill?
# Note - if we reconnect to the same meeting too quickly we will have ourself as an orphaned attendee.
if ($g2mKill) {
  if ((Get-Process -Name "g2mui" -ErrorAction SilentlyContinue)) {
    "Killing the running G2M"
    Get-Process -Name "g2mui" | Stop-Process -Force
    Do { Start-Sleep -Seconds 2 } until ((Get-Process -Name g2mlauncher).Handles -lt 500)
  } else {"The user was not joined to a G2M"}
}

# is there a G2M video conference running? We will collide if there is.
if (!(Get-Process -Name "g2*conference")) {
  if ( $g2mId ){

    # Start the G2M
    startG2m -g2mId $g2mId
  } else { "No G2M ID was provided" }
} else { "A G2M is already running" }


Monday, April 20, 2015

Ports and Docker containers

This is the feature of containers that the system administrator in me gets all excited about.

Ports.

This is more than security through obscurity, this is actually about isolated networking.
Docker has an internal network stack all to itself.

You can see it if you type ifconfig.  You see a Docker interface (not unlike a Hyper-V Internal network vNIC).
If you have a container running you can use sudo docker ps and one of the columns is 'ports'.

Lets begin with the intermediate of exposing ports.

For my examples I have been using my meshblu image and opening a bash shell and even mapping local file paths into it.  I am going to leave those out at the moment.

Now that I have a working model, I want to test it.  So I want to expose the listener port of the service running in my container.

sudo docker run -t -t -p 3000:3000 --name meshblu meshblu_appliance

The -p command allows the mapping of ports.
If I only defined -p 3000 I would be allowing port 3000 of the container to be mapped to some random port of the container host.  Instead I defined 3000:3000 - so as not to confuse myself.
What this does is map port 3000 of my container to port 3000 of my container host.

If I open any application on my container host or from a remote machine I can now access the application in my container on port 3000.  Just like opening a single port in a firewall.

Now.  My container has a number of supporting services such as Redis and MongoDB and other ports that the application will be listening on..  I would like to expose these as well.  They are there, in the container, running and responding and entirely hidden at the moment.
This is one that I did not consider intuitive.

sudo docker run -t -t -p 3000:3000 -p 1883:1883 -p 5683:5683 --name meshblu meshblu_appliance

Now I have mapped two additional ports.  Initially I tried using a single long string or an array (it just made sense) but you need to use individual port commands.

Just some other nifty stuff.  And I have not gotten beyond a single container yet.

Tuesday, April 14, 2015

Local file systems and Docker containers

Now I am going to move into the realm of areas that I consider fun.
What sort of trickery can I use containers for so that I can avoid installing 'stuff' on my development workstation?

Here is my scenario:  I am pulling some source from GitHub.  I might modify some scripts and need to test those.  I want to quickly test my changes within an installed and running instance of the application.  I don't want to 'install' the application on my workstation.

So, lets work thorough my real life example.  No modifications, just pulling the source and getting that into a container, without any need to build a custom container nor image.

Docker allows you to expose paths of the container host and map those into specific paths of the VM.  And this is where you can do some nifty things.

Say that you have some Node.js application.  And you want to run multiple instances of it, or you want to run and instance and have the logs write back to your development station. 
(This could be any number of combinations).

Lets run through an example:

Previously I ended with the example:
sudo docker run -i -t --name meshblu node:latest /bin/bash

If I extend that with this scenario we will see some new options.

sudo docker start -i -t -v "/home/brianeh/GitHub":"/home":ro --name meshblu node:latest /bin/bash

What I have added is the "-v" option.  this defines a path mapping.
"/home/brianeh/GitHub" is the GitHub folder of my user home path.  After the colon is the path in the container that this is mapped to.  "ro" means Read Only.  Or I could define that as "rw" - Read Write.

the neat thing is that once I run my container and enter its console I can type ls -l /home and I will see all of the files I have downloaded to my GitHub folder of my development machine.

This gives me a runspace within the container that is separate from my workstation where I can install applications but run the latest version of my code straight out of the development path.

(One reason why developers love containers)

Friday, April 10, 2015

Giving of your Octoblu devices

I realize that the title is not very searchable, and I am okay with that.

After all, this post is all about giving.  Actually moving ( handing over, granting, getting rid of ) a device on the Octoblu IoT platform.

To stay true to my Windows roots, I did this in PowerShell  ;-)

Here is my scenario, I have been working with a 'device' in Octoblu.  I have it all setup, it is working well, there is a flow tied to it, etc.  The world is a happy place.

Now, I am done with this.  But Joe wants to carry it on and take it to a show and demonstrate it, possibly even enhance the flow.
I developed the entire thing under my account, and I can't give Joe my password.

I could use the template feature and give Joe a device scrubbed version of my flow.  But that still requires that the device be deleted from my account and re-created under Joe's.  Or Joe has to take the device, reconfigure it and then personalize the flow.

Why not just give Joe my device - and I mean really give it.  Not just the physical device, but its representation on the platform and the flow that goes along with it.

To accomplish this I only have to do a few things:  I have to add Joe's user account to the device, then Joe needs to claim (take ownership) of that device.

In Octoblu everything is a 'device'; users, things, flows, running flows, etc.  A user is just a special type of device.

Octoblu also has a whitelist / blacklist security model that grants or denies access at the very granular device level.  You can literally have a device that can only talk to one other device (such as the flow it interacts with) and nothing else. 

By default, this is all locked down and your devices are only available to you and to the flows to link them to in the designer.

At the same time, there is no way to 'give this to Joe'.

Let me get to the meat and walk through a few things.

First of all, we will do all of this through the REST API.  And the REST API uses your user UUID and secret (they call a token) to authenticate you.

Using this you can authenticate and list your devices.
And get the UUID of the device (or UUIDs of devices) that you want to give.

The next piece of information you need is the UUID of the user you want to give the device to.

The rest is in the script (with some error handling of course).
You can find the source over here: https://github.com/brianehlert/moveOctobluDevice

And you can find the first version below:

# What is the UUID and token of the current device owner?
# and test that it is actually good
Do {
    Do {
        if (!$deviceOwner) { $deviceOwner = Read-Host -Prompt "What is the uuid of the current device owner? (your account)" }
        if (!$deviceOwnerSecret) { $deviceOwnerSecret = Read-Host -Prompt "What is the secret token of the current device owner? (your account)" }
        $meAuthHeader = @{
            meshblu_auth_uuid = $deviceOwner  
            meshblu_auth_token = $deviceOwnerSecret
        }
        $me = Invoke-RestMethod -URI ("http://meshblu.octoblu.com/devices/" + $deviceOwner) -Headers $meAuthHeader -Method Get -ErrorAction SilentlyContinue
    } Until ( $me )
    # echo back what you find
    "The current device owner is: " + $me.devices[0].octoblu.email
} until ( (Read-Host -Prompt "Is this correct? Type 'YES'") -eq 'YES' )

# What is the UUID of the user you will be moving your devices to?
# and test that it is actually good.
Do {
    Do {
        if (!$deviceNewOwner) { $deviceNewOwner = Read-Host -Prompt "What is the uuid of the new device owner? " }
        if (!$deviceNewOwnerSecret) { $deviceNewOwnerSecret = Read-Host -Prompt "What is the secret token of the new device owner? " }
        $youAuthHeader = @{
            meshblu_auth_uuid = $deviceNewOwner  
            meshblu_auth_token = $deviceNewOwnerSecret
        }
        If ($deviceNewOwnerSecret) {
            $you = Invoke-RestMethod -URI ("http://meshblu.octoblu.com/devices/" + $deviceNewOwner) -Headers $youAuthHeader -Method Get -ErrorAction SilentlyContinue
        } else { $you.devices.octoblu.email = "No token provided. Unable to validate" }
    } until ($you)
   
    # echo back what you find
    "The new device owner will be: " + $you.devices[0].octoblu.email
} until ( (Read-Host -Prompt "Is this the correct new device owner? Type 'YES'") -eq 'YES' )

# List all of 'my devices' in a nice, neat way with the important bits - name, uuid, device type
$devices = Invoke-RestMethod -URI http://meshblu.octoblu.com/mydevices -Headers $meAuthHeader -Method Get
# Which device will you be moving to another user?
# base on device name as everything associated in the case of Gateblu needs to go.
Do {
    $devices.devices | Sort-Object Name | Format-Table -AutoSize name, uuid, type, subtype, online

    Do {
        if (!$nameToMove) { $nameToMove = Read-Host -Prompt "What is the name of the device you will be moving to the other user? (this is a match)" }
        $deviceToMove = $devices.devices -match $nameToMove }
    Until ( $deviceToMove )
    "The following device(s) matched: "
    $deviceToMove | Format-Table -AutoSize Name, UUID
} until ( (Read-Host -Prompt "proceed to move your device(s)? Type 'YES'") -eq 'YES' )
# The device only needs to be discoverable to take ownership.
foreach ( $device in $deviceToMove ) {
   
    If ( $device.discoverWhitelist ) {
        $device.discoverWhitelist += $deviceNewOwner
        $json = @{
            "discoverWhitelist" = $device.discoverWhitelist
        }
    } else {
        $json = @{
            "discoverWhitelist" = $deviceNewOwner
        }
    }
    $json = $json | ConvertTo-Json
    # make the device discoverable by the new owner
    Invoke-RestMethod -URI ( "http://meshblu.octoblu.com/devices/" + $device.uuid ) -ContentType "application/json" -Body $json -Headers $meAuthHeader -Method Put
    If ( $youAuthHeader.meshblu_auth_token ) {
        # claim the device as the new owner
        # only if you know the token - otherwise the other user will need to do that
        Invoke-RestMethod -URI ("http://meshblu.octoblu.com/claimdevice/" + $device.uuid ) -ContentType "application/json" -Headers $youAuthHeader -Method Put
    }
}