Friday, December 8, 2017

ConnectWise Manage Report Writer - Repeaters and Subtotals

If you're reading this you're probably aware that the support/documentation provided by ConnectWise for more difficult tasks in Report Writer is a bit... lacking. I ran into a few issues on Saturday and while i figured them all out the same day, i did a CW chat and submitted a ticket and have yet to hear back (4 full business days later). Basically, if you want to use Repeaters and/or Subtotals, there are very specific constraints that you need to keep in mind. This post is not for someone just getting into report writer, you need to already know how to build queries, use joins, and more.

Repeaters

These are a great solution to not having to use a sub-report. That said, they're not very intuitive and are in fact pretty basic in nature. The gist of it is that once you start the repeater, the first column you reference and all subsequent ones (unless you hit a nested a repeater) are repeated until the initial column changes. The documentation flip flops between using [repeater], <!--[repeater]-->, and <repeater>. The latter is never correct. The first two can be used interchangeably, but if you're wrapping this around HTML content then the middle option is the correct one.

As an example, say you are making a report that lists a client's agreements with their additions. The data in table form may look like this:

Agreement NameAnniversaryAddition NameAddition QtyAddition PriceExtended Price
Managed Services1/1/2019Antivirus30$5.00$150.00
Managed Services1/1/2019Agent30$1.00$30.00
Managed Services1/1/2019Spam Filtering35$3.00$105.00
Telecom Support2/1/2020Handset Fee20$10.00$200.00
Telecom Support2/1/2020PBX Maintenance1$100.00$100.00

As you can see, the agreement name is duplicated for each time you have an addition on it, because this is how SQL works. So in Report Writer, on the Fields tab, you would click Design Form and then (if you're sane) edit the HTML directly. The code to put this together would look something like this:


[Company_Name]
<!--[repeater]-->
  <h3>[Agr_Name] - [Agr_Anniversary]</h3>
  <table>
    <tr><th>Item</th><th>Quantity</th><th>Cost</th><th>Ext Cost</th></tr>
    <!--[repeater]-->
      <tr>
        <td>[Line_Desc]</td>
        <td>[Qty]</td>
        <td>[Cost]</td>
        <td>[Ext]</td>
      </tr>
    <!--[/repeater]-->
  </table>
 <hr />
<!--[/repeater]-->

Note that there are nested repeaters. The first level is for the Agreement Name and Amount, the second level is for the items associated with each one. The resulting output looks something like this:


Company Name


Managed Services - 1/1/2019

ItemQuantityCostExt Cost
Antivirus30$5.00$150.00
Agent30$1.00$30.00
Spam Filtering35$3.00$105.00

Telecom - 2/1/2020

ItemQuantityCostExt Cost
Handset Fee20$10.00$200.00
PBX Maintenance1$100.00$100.00


Subtotals

Now let's say that you want to add up all of the addition amounts on the agreement and show that total. Since you still want the itemized list, you need to use the Subtotal Decorator. The CW Manage documentation flip flops on whether this is @Subtotal or #Subtotal, don't ask me why. The correct usage is @Subtotal. There are however a few constraints to consider:

  • Subtotal only makes sense in a repeater. If your data looks how you want it and you're not using a repeater, then this should be done in the query, not in the report designer.
  • You must check the VG (visual group) box on every column not part of the repeater that will be subtotaled. This will force them to be sorted as well, but that's not a big deal.
  • You must check the Add Subtotals box at the bottom of the Fields tab.
  • CW will tell you that you can only use Subtotal on one field. This is incorrect, you can subtotal as many fields in a repeater group as you want.
  • CW neglects to tell you that the column you are subtotaling cannot have spaces OR underscores in it.

Using the repeater example above, here is how you would add a subtotal of the overall ext costs of the additions:


[Company_Name]
<!--[repeater]-->
  <h3>[Agr_Name]</h3>
  <table>
    <tr><th>Item</th><th>Quantity</th><th>Cost</th><th>Ext Cost</th></tr>
    <!--[repeater]-->
      <tr>
        <td>[Line_Desc]</td>
        <td>[Qty]</td>
        <td>[Cost]</td>
        <td>[Ext]</td>
      </tr>
    <!--[/repeater]-->
    <tr><td colspan="3">Total:</td><td>[Ext@Subtotal]</td></tr>
  </table>
  <hr />
<!--[/repeater]-->

It's nice to note, the Subtotal can go above or below the repeater group. If you want to subtotal more than one column, just add that column with the @Subtotal decorator wherever you need.

Wednesday, November 15, 2017

Migrating VMkernel adapters on vSphere Distributed Virtual Switches

A chicken/egg scenario can sometimes occur when setting up DVS (distributed virtual switch) in a vCenter cluster. The issue being your vCenter is connected to your host over its' vmk0, so any network changes have to be confirmed or else they will be rolled back. In my case, i wanted to change the links to have my management network be a tagged VLAN instead of native. The problem here is that i would need to make the change in vCenter, then change the tagging on the switch ports, then hope that it picked up the changes. In almost all cases, it didn't, and reverted the settings. In a few cases i got lucky with the timing and it worked, but most of the time it didn't.


The solution is to set the Advanced vCenter setting of config.vpxd.network.rollback to false, then make the change. This causes it to not roll back the changes, so you better be sure that your configuration is correct. This should of course be set back to true when you're done.

Friday, June 23, 2017

Labtech and Corrupted Scripts

Update: The latest patch fixes this, or at least handles it more gracefully. v110.387 (Patch 15) is what is needed, otherwise see below.

Labtech (or Connectwise Automate, which i refuse to say outloud), has a current bug where you can quite easily corrupt a script. Since the scripts are not traditional text files, and are stored encoded in the database, there is no easy way to fix it if something does get corrupted. I'll outline how this happens, and how you can fix it, since their support has indicated that this is a low priority issue to them.

The Issue

For various reasons a script can get corrupted. When i say corrupted, it's all still there, but something is preventing it from getting loaded. For example, the other day my coworker added a new EDF and then asked me to take a look at a script. When i loaded it, the script functions did not show the EDF name but just showed the ID. Though i advised against it, we ended up making some changes and saving the script. After that, the script would no longer load with the error "Error loading script:Syntax error: Missing operand after '=' operator." (see below):



Now, you'd think that Labtech would still load the script and force you to fix it... NOPE. Any work you've put into that script is now lost, you need to restore from backups. This means either restoring your entire database, or grabbing the nightly .sql backup files, editing to grab the right data, and replacing via SQL. But what if you just put hours of work into the script and backups haven't run yet? Well, you're SOL in this case. Or are you?

The Fixes


Restoring from backup

If you just want to go to last night's backup, then the fix is not too difficult. You will need adequate knowledge of running SQL queries against the Labtech server, and the text editor of your choice.

  1. Take a new backup, in case you mess something up.
  2. Grab the backup that you want to restore. These are usually located in C:\Program Files\LabTech\Backup. You can go into the Tablebase folder or extract one of the zip files if you need an older copy. The file you need is lt_scripts.sql. Make a copy of this in a spot where you can edit it.
  3. Determine the ID of the script that is corrupted. Easiest method here is to show IDs in the Labtech Client (Tools > Show ID's). The ScriptID is the number in parenthesis next to the script in the Navigation Tree. For the sake of this guide, we will be using ID 5986.
  4. Edit the .sql file, i used notepad++. Search for the ID of the script, you should see something that looks like this. The first red square is what you're looking for with the script ID, the second is the actual content that you need to use. You want everything between the apostrophes, in my example starting with H4sI and ending in AAA===. I had to blur a lot of this for obvious reasons, but you should get the gist of it.
  5. Open your preferred SQL editor (Labtech staff usually install SQLyog on the server, you can also use the CLI or MySQL workbench)
  6. Craft a query that reads:
    UPDATE lt_scripts SET ScriptData='H4sI......AAA==' WHERE ScriptID=5986;
    (replace 5986 with your ScriptID, and paste the FULL content of what you selected into the ScriptData portion of the statement.)
  7. Run the query, and it will replace the body of the script with your backup copy.
  8. At this point you should be able to edit the script, no Refresh/Reload needed
  9. If not, you may need to go to an earlier backup

Fixing the script in-place

This is definitely the more difficult route. If your backup from last night is good enough then i'd use the previous section to restore from backup. Otherwise, read on. This requires knowledge of running SQL queries, minor XML knowledge, as well as the ability to perform Base64 encodes/decodes and gzip compressions/decompressions. I did this on a linux server, however you can also do it with other tools. I will gloss over various tasks that are covered in the previous section for the sake of brevity.

  1. Take a new backup, in case you mess something up.
  2. Extract the ScriptData for this script using this query:
    SELECT ScriptData FROM lt_scripts WHERE ScriptId=5986;
  3. Base64 Decode the result. You can use an online tool, or on a Linux shell:
    # base64 -d > output.gz
    After running that command, paste the data in and press Ctrl+D. However you do it, you need to end up with a .gz file containing the base64 decoded content.
  4. Unzip the file. On a Linux shell you can do:
    # gzip -d output.gz
    or
    # gunzip output.gz
  5. output is now an XML file containing the script. Edit this with a text editor (i use Notepad++) and find the section with the error. If you note, when you open the script editor and it errors out, it does load at least a few lines before dying. The last line it loads is where you want to start looking. If this has a variable name or something you can search for, use that. In my case of a missing EDF, i ended up with a section that looked like this:
    <ScriptSteps>
        <Action>2</Action>
        <FunctionId>103</FunctionId>
        <Param1 />
        <Param2>%computerid%</Param2>
        <Param3>DuoDefaults</Param3>
        <Param4 />
        <Param5 />
        <Sort>8</Sort>
        <Continue>1</Continue>
        <OsLimit>0</OsLimit>
        <Indentation>1</Indentation>
      </ScriptSteps>
      <ScriptSteps>
        <Action>2</Action>
        <FunctionId>103</FunctionId>
        <Param1>611</Param1>
        <Param2>%computerid%</Param2>
        <Param3>APIHostnameComputer</Param3>
        <Param4 />
        <Param5 />
        <Sort>9</Sort>
        <Continue>1</Continue>
        <OsLimit>0</OsLimit>
        <Indentation>1</Indentation>
      </ScriptSteps>
    
  6. If you notice, the second ScriptSteps block has an ID specified in Param1, while the first one does not. This is where LT stripped out the non-existent EDF and shot itself in the foot. In my case, i just needed to find the EDF ID and replace <Param1 /> with <Param1>1234</Param1>. If you're not up to it, just delete the entire ScriptSteps block.
  7. Once done, save the file, and gzip it back up again:
    # gzip output
  8. Then, base64 encode the file:
    # base64 output.gz > fixed.txt
  9. Finally, copy the contents of fixed.txt and run an update to replace the ScriptData with the fixed version:
    UPDATE lt_scripts SET ScriptData='H4sI......AAA==' WHERE ScriptID=5986;
    (replace 5986 with your ScriptID, and paste the FULL content of what you selected into the ScriptData portion of the statement.)
  10. Run the query, and it will replace the body of the script with your backup copy.
  11. At this point you should be able to edit the script, no Refresh/Reload needed

Final Notes

  • Do not update the ScriptData of one script with the contents of another. The ScriptData has the GUID of the script encoded in it so it will still be broken.
  • Don't have access to a linux server, or don't know linux? Here are some alternatives:
    • For Base64 encoding/decoding, you can use your programming/scripting language of choice. Powershell has the ability to do Base64 Encoding/Decoding, a quick google can tell you how to do that. You can't really use websites for this portion because you are dealing with binary data.
    • For gzip compression, you can install 7zip to take care of this for you.
  • It's a good practice to export your scripts after making large changes. If you can store these in version control (such as git), that's even better.

Wednesday, April 19, 2017

Exchange 2013 IMAP Session Limitations

I have various services that require the use of IMAP to access mailboxes. In one case, a mailbox has subfolders and the service maintains an IMAP connection for each subfolder that it needs to connect to. The issue that came up was that after the 16th connection was opened, new connections were being rejected with no useful error (NO LOGIN FAILED or NO AUTHENTICATE FAILED). This seemed like an easy task, as there is an IMAP limit, so i ran:


Set-ImapSettings -MaxConnectionsPerUser 32 -Server FRONTENDSERVERNAME
Set-ImapSettings -MaxConnectionsPerUser 32 -Server BACKENDSERVERNAME

After restarting the IMAP service on both servers (or both services on the same server if you have all roles on one), i expected it to be fixed, and it wasn't. I checked the settings, confirmed it took effect, still nothing. After turning on protocol logging, the back-end server's logs indicated that the mailbox couldn't be opened due to Microsoft.Exchange.Data.Storage.TooManyObjectsOpenedException/Microsoft.Mapi.MapiExceptionSessionLimit. I have had to adjust a similar setting before for other reasons, but figured "it's not a MAPI connection, so why would that matter." Well as it turns out, the IMAP backend proxy just opens a MAPI connection to the mailbox, so yes it is needed.


To raise these limits you create a new DWORD value in HKLM\SYSTEM\CurrentControlSet\Services\MSExchangeIS\ParametersSystem. After changing this, the Information Store service needs to be restarted. In my case, no one change actaully fixed the issue, it was a combination of the below values that worked:


  • Disable Session Limit - 0
  • Maximum Allowed Concurrent Exchange Sessions Per Service - 10000
  • Maximum Allowed Exchange Sessions Per Service - 10000
  • Maximum Allowed Service Sessions Per User - 10000
  • Maximum Allowed Sessions Per User - 10000

Probably a good idea to set these to something other than 10000, i'd gradually halve them until you run into issues again and then set it around there. It should also be noted that the IMAP service can take a little bit of time to stabilize, i wouldn't write off any of the changes until you've restarted the services and waited 10 minutes.

Friday, February 3, 2017

Xerox Scan to Folder via SMB with Windows Server 2016

Setting up a new Server 2016 file server, unable to get a Xerox WorkCentre 7220 to scan to it via SMB. This took a bit of troubleshooting, the copier was reporting invalid credentials and nothing was working. I had tried all combinations of IP vs Hostname, forward/backslashes, DOMAIN\username and username@domain, etc. There was a time mismatch between the server and copier, but after resolving that it was still not working. Ultimately i ended up running Wireshark on the server and found that the copier was trying to establish an SMB connection first to the IP address of the server, then to SMBSERVER (which is not the hostname of my server...).

Some googling came up with a few options, one of which would be to add SMBSERVER as an alternate NetBIOS name for the server. This included reducing some security levels and enabling some other things in the registry that didn't sound like a good idea. Instead, on a hunch, i switched the port from 139 (default) to 445. After switching the port, it worked as expected. Below are the settings that ended up working:

Server: SERVERNAME.domain.local : 445
Share: TheShareName
Document Path: Documents/Scans (no leading slash, and i used forward slashes)
Login Name: DOMAIN\username (we use a dedicated scans user)

See here: