Creating Custom License Entitlements for Office 365

0 Likes
NetIQ released an interesting driver, that supports the Office 365 environment. I am not entirely sure I understand all the subtleties of the Microsoft Online stuff, but that is ok, I know enough to be dangerous.

I wrote a review of the policies in the driver, goodness, almost 3 years ago. In it I reviewed the policies, and maybe I should go back now, and compare the mostly V2.1 packages dated 2012/2013 to the current packages (2.8 for base, and 2.6 for Default Config) and see what has changed. Designer has a nice feature that allows you to compare two versions of a package to see what content differs. Maybe I will if I have some time. The last review was 16 articles, so I expect a fair bit of changes to review.

Recently I got to use the Office 365 at a customer and I was trying to setup a custom license for them. I have to say the documentation there is basically the minimal information and is technically correct, if you understood everything involved. But if you are not sure how it works, it is really incomplete so that is something I can help fix with a better approach to describing how custom licenses work.

So here is what the documentation says about Custom License buried in this page:
https://www.netiq.com/documentation/idm45drivers/office365/data/btgxfb3.html

Office 365 Custom Licenses: Click the icon to create custom Office 365 licenses by disabling specific services. You must use License Entitlements to assign licenses to the Office 365 users.

Custom License Name: Specify the name with which a custom license should be created. This will appear as [domainname]:[license name (service to be disabled)] in the License Entitlements. If the name you entered contains spaces or a hyphen “-”, the driver cannot create a custom license.

Service Name to be Disabled: Specify the service names to be disabled. To disable more than one service, use a comma to separate the service names. For example, to disable Office 365 ProPlus and Lync Online services from your enterprise plan, use this string: OFFICESUBSCRIPTION,MCOSTANDARD.

This is correct, in its essence but confusing in its minimalness. The short answer is that when you edit the Driver Configuration (Driver Properties, Driver Configuration, Subscriber Options tab) there is a structured GCV where you can define multiple instances of custom licenses. First you must click the plus sign to setup an empty instance. Then name the custom license. Watch out, a bunch of characters are illegal like period, space, and dashes. So name it simply CUSTOM or the like.

Then from the list of features that you got via the PowerShell command: Get-MSolAccountSku

You build a comma separated list that is the list of services to be disabled. This is so very Microsoft in approach. Add overarching permissions then take away stuff. Very much the opposite of my normal approach of grant what you need, minimal at best. This is actually a problem since Microsoft has added new services over the years (Sway or Yammer I think) and that basically means all licensed users get the new service when it is added and you have to go back and disable it on all of them. Oh management joy!

Then restart the driver. What it does is call a Powershell that builds an environment variable it will be able to use. Then do a User App initiated Code Map Refresh. (User App Gui as Admin, Roles/Resources view, bottom left, Configure RBPM and there is a Entitlement Refresh section, click the arrow in a circle button).

Then you will see new license types appear when you go to assign a License entitlement to a Resource. A screen shot makes this very obvious.

The key to understand is that the driver makes the license in its memory, and returns it when you do a Code Map Refresh. It does not appear in the Microsoft list of licenses (You do not get to write to that list, go figure). It is kind of a pseudo license only the driver knows about. Also of interest, it does not allow you to say which of the license types you want your custom type to 'attach' to, instead it seems to apply them to all the available licenses, so for each license type you might have, you will get one with (CUSTOM) appended to it.

Also, it is not a persistent object in the sense that if you looked at the user, they would have the plain license in the O365 Admin interface, just certain services look manually turned off. This has consequences, it means that if you changed your custom license, nothing goes back and updates the existing users. In principle, you could probably go create a new resource with the entitlement of the new custom license, remove the old Resource assignment (however you added it to users, direct, via a Role, via a Role to Role to Resource inheritance) and then add the new one. The problem is that would leave your users in an unlicensed state for some period of time while the Roles and Resources Service Driver (RRSD) processed the changes in terms of who needs to remove a Resource and thus entitlement and then add them back in the second pass. Then the driver would have to chug through that backlog and there could be a serious delay.

So now that we understand the parts they kind of glossed over, let's look at some of the mechanics of stuff under the covers. This is the part you need to know, to understand how to troubleshoot it when something goes wrong.

First up, you need to see what you get in terms of a list of licenses. For a University, you might see a list like this of licenses:

            "DESKLESSPACK" = "Office 365 (Plan K1)"
"DESKLESSWOFFPACK" = "Office 365 (Plan K2)"
"LITEPACK" = "Office 365 (Plan P1)"
"EXCHANGESTANDARD" = "Office 365 Exchange Online Only"
"STANDARDPACK" = "Office 365 (Plan E1)"
"STANDARDWOFFPACK" = "Office 365 (Plan E2)"
"ENTERPRISEPACK" = "Office 365 (Plan E3)"
"ENTERPRISEPACKLRG" = "Office 365 (Plan E3)"
"ENTERPRISEWITHSCAL" = "Office 365 (Plan E4)"
"STANDARDPACK_STUDENT" = "Office 365 (Plan A1) for Students"
"STANDARDWOFFPACKPACK_STUDENT" = "Office 365 (Plan A2) for Students"
"ENTERPRISEPACK_STUDENT" = "Office 365 (Plan A3) for Students"
"ENTERPRISEWITHSCAL_STUDENT" = "Office 365 (Plan A4) for Students"
"STANDARDPACK_FACULTY" = "Office 365 (Plan A1) for Faculty"
"STANDARDWOFFPACKPACK_FACULTY" = "Office 365 (Plan A2) for Faculty"
"ENTERPRISEPACK_FACULTY" = "Office 365 (Plan A3) for Faculty"
"ENTERPRISEWITHSCAL_FACULTY" = "Office 365 (Plan A4) for Faculty"
"ENTERPRISEPACK_B_PILOT" = "Office 365 (Enterprise Preview)"
"STANDARD_B_PILOT" = "Office 365 (Small Business Preview)"


A non-educational output of licenses might look like this:

PS C:\Users\djt\Desktop> Get-MsolAccountSku | foreach {$_.AccountSkuId; $_.ServiceStatus.ServicePlan.ServiceName; ""}

acme:STREAM
EXCHANGE_S_FOUNDATION
Microsoft Stream

acme:POWERAPPS_INDIVIDUAL_USER
POWERVIDEOSFREE
POWERFLOWSFREE
POWERAPPSFREE

acme:ENTERPRISEPACK
FLOW_O365_P2
POWERAPPS_O365_P2
TEAMS1
PROJECTWORKMANAGEMENT
SWAY
INTUNE_O365
YAMMER_ENTERPRISE
RMS_S_ENTERPRISE
OFFICESUBSCRIPTION
MCOSTANDARD
SHAREPOINTWAC
SHAREPOINTENTERPRISE
EXCHANGE_S_ENTERPRISE

acme:POWER_BI_STANDARD
EXCHANGE_S_FOUNDATION
BI_AZURE_P0

acme:EMS
RMS_S_PREMIUM
INTUNE_A
RMS_S_ENTERPRISE
AAD_PREMIUM
MFA_PREMIUM

acme:SHAREPOINTSTANDARD
EXCHANGE_S_FOUNDATION
INTUNE_O365
SHAREPOINTSTANDARD

acme:ATP_ENTERPRISE
ATP_ENTERPRISE


The docs say to just run Get-MSolAccountSku which is not quite enough, you need to iterate over each license to see the services it includes to know what to exclude and the format of the name. The example above of the educational set of licenses is just the Get-MSolAccountSku command output, whereas the commercial example shows the proper output you need.

Thus if you were granting the ENTERPRISEPACK (E3) to users, which is the most common license, then you might wish to disable SWAY and FLOW_O365_P2 whatever those are. In which case, in the names of the disabled services in the Custom license definition you would include:

SWAY,FLOW_O365_P2

Keep adding values as you desire.

So what does this look like in the driver? Well you will not see it in the engine trace. Rather this a Remote Loader side thing which needs a .NET Remote Loader, so you need to edit the remote loader configuration to enable tracing, and set it to a high level, like 25. I am not sure the exact level of trace at which the underlying PowerShell commands are shown, but it is not in 3, and when you are troubleshooting, you shoot high and wide, at first then narrow it down later, so I started with level 25 trace.

My first attempt at this failed, because my test tenant had expired. So there were no licenses allowed to be assigned at all and that is visible in the engine trace.

 [02/06/17 11:39:05.485]:**O365** ST:
<nds dtdversion="3.5">
<source>
<product build="201601270714" instance="\CORP-IDMDEV\acme\system\idm\dset\O365" version="4.1.0.2">Identity Manager Driver for Microsoft Office365</product>
<contact>NetIQ, Corporation.</contact>
</source>
<output>
<status event-id="corp-idm101#20170203202621#1#3:377ed4e7-1cd0-490b-f8aa-e7d47e37d01c" level="error" type="driver-general"> Unable to assign this license because the number of allowed licenses
have been assigned.<operation-data AccountTracking-AppAccountStatus="-" AccountTracking-IdvAccountStatus="-" AccountTracking-Operation="modify" AccountTracking-association="1bed280f-e5e2-4e11-bb0a -6c97367c1a0d">
<entitlement-impl id="" name="License" qualified-src-dn="O=acme\OU=users\CN=JKirk" src="UA" src-dn="\CORP-IDMDEV\acme\users\JKirk" src-entry-id="122497" state="1">{"ID":"acme:ENTERPRISEPACK "}</entitlement-impl>
</operation-data>
</status>
</output>
</nds>
[02/06/17 11:39:05.486]:**O365** ST:Resolving association references.
[02/06/17 11:39:05.487]:**O365** ST:Processing returned document.
[02/06/17 11:39:05.487]:**O365** ST:Processing operation <status> for .
[02/06/17 11:39:05.487]:**O365** ST:
DirXML Log Event -------------------
Driver: \CORP-IDMDEV\acme\system\idm\dset\O365
Channel: Subscriber
Object: \CORP-IDMDEV\acme\users\JKirk
Status: Error
Message: Unable to assign this license because the number of allowed licenses have been assigned.


Good to know what that error means.

Next after paying for the tenant properly. You can see this in Remote Loader trace.

DirXML: [02/06/17 11:38:09.229]: TRACE:  SUB: Session Manager Initialized.
DirXML: [02/06/17 11:38:09.229]: TRACE: SUB: Connecting to Microsoft Online Services ...
DirXML: [02/06/17 11:38:09.229]: TRACE: SUB: $sec=ConvertTo-SecureString **** -AsPlainText -Force
DirXML: [02/06/17 11:38:09.244]: TRACE: SUB: $mycreds = New-Object System.Management.Automation.PSCredential("idmadmin@acme.net",$sec)
DirXML: [02/06/17 11:38:09.260]: TRACE: SUB: Connecting to Office 365 ...
DirXML: [02/06/17 11:38:09.260]: TRACE: SUB: Connect-MSolService -credential $mycreds
DirXML: [02/06/17 11:38:13.698]: TRACE: SUB: Connection Successful.
DirXML: [02/06/17 11:38:13.698]: TRACE: SUB: Connecting to Exchange Online Services ...
DirXML: [02/06/17 11:38:13.698]: TRACE: SUB: $Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://ps.outlook.com/powershell -Credential $mycreds -Authentication Basic -AllowRedirection
DirXML: [02/06/17 11:38:18.635]: TRACE: SUB: Import-PSSession $Session -AllowClobber
DirXML: [02/06/17 11:38:18.682]: TRACE: Remote Loader: Connection monitor thread waking up.
DirXML: [02/06/17 11:38:18.682]: TRACE: Remote Loader: 'keep-alive' packet sent.
DirXML: [02/06/17 11:38:18.682]: TRACE: Remote Loader: Connection monitor thread going to sleep.
DirXML: [02/06/17 11:38:27.073]: TRACE: SUB: Connection Successful.
DirXML: [02/06/17 11:38:27.135]: TRACE: SUB: Creating Custom Licenses
DirXML: [02/06/17 11:38:27.135]: TRACE: SUB: get-pssession
DirXML: [02/06/17 11:38:27.135]: TRACE: SUB: Get-MSolAccountSku
DirXML: [02/06/17 11:38:27.588]: TRACE: SUB: get-pssession
DirXML: [02/06/17 11:38:27.588]: TRACE: SUB: $acme_ENTERPRISEPACK_ACME-CUSTOM=New-MSolLicenseOptions -AccountSkuId acme:ENTERPRISEPACK -DisabledPlans FLOW_O365_P2,POWERAPPS_O365_P2,TEAMS1,PROJECTWORKMANAGEMENT,SWAY,YAMMER_ENTERPRISE,RMS_S_ENTERPRISE
DirXML: [02/06/17 11:38:27.604]: TRACE: SUB: Unable to create custom license. Exception of type 'DXACMEase.Xds.XdsRetryException' was thrown.
DirXML: [02/06/17 11:38:27.604]: TRACE: Remote Loader: SubscriptionShim.init() returned:
DirXML: [02/06/17 11:38:27.604]: TRACE:
<nds dtdversion="3.5">
<source>
<product instance="\CORP-IDMDEV\acme\system\idm\dset\O365" version="4.1.0.2" build="201601270714">Identity Manager Driver for Microsoft Office365</product>
<contact>NetIQ, Corporation.</contact>
</source>
<output>
<status level="success" />
</output>
</nds>


I had named my custom license ACME-CUSTOM, which led to a variable named, $acme_ENTERPRISEPACK_ACME-CUSTOM being created, but it has a dash in it, which is apparently illegal, so of course I got a very nice informative error message.

	Unable to create custom license. Exception of type 'DXACMEase.Xds.XdsRetryException' was thrown.


Did you find that helpful? Ya, me neither. The good news is the forums came to the rescue and someone had run into this before, posted that text into it, the great and wondrous Google (Oz?) found the text and I learned that periods were disallowed, spaces and what the heck, try it without a dash, you never know. To be fair a colleague suggested loosing the dash and I thought he was nuts, but what the heck, try anything once. Alas, he was correct and had to eat some crow. (Delicious with ketchup).

SUB: $acme_ENTERPRISEPACK_ACME-CUSTOM=New-MSolLicenseOptions  -AccountSkuId acme:ENTERPRISEPACK  -DisabledPlans FLOW_O365_P2,POWERAPPS_O365_P2,TEAMS1,PROJECTWORKMANAGEMENT,SWAY,YAMMER_ENTERPRISE,RMS_S_ENTERPRISE 


I thought at one point that maybe the length of the custom list, so I tried with just YAMMER excluded and got the same error on driver startup. What I had in fact done though was use the standard license, since it failed to create the custom license when I tried adding it to the user. In this case, the associated user was not there, since the User Account entitlement had failed to grant yet.

[02/06/17 17:28:48.018]:**O365** ST:
<nds dtdversion="4.0" ndsversion="8.x">
<source>
<product edition="Advanced" version="4.5.4.0">DirXML</product>
<contact>NetIQ Corporation</contact>
</source>
<input>
<modify cached-time="20170206222847.836Z" class-name="MSolUser" event-id="corp-idm101#20170206222847#1#4:bdbeafb3-1e0b-4e1a-ad8a-b3afbebd0b1e" qualified-src-dn="O=acme\OU=users\CN=MScott" src-dn="\CORP-IDMDEV\acme\users\MScott" src-entry-id="111090" timestamp="1486420127#4">
<association state="associated">a7a95e1f-8b65-4d84-b499-4aaa9a9b571d</association>
<modify-attr attr-name="LicenseAssignment">
<add-value>
<value type="string">acme:ENTERPRISEPACK</value>
</add-value>
</modify-attr>
<modify-attr attr-name="UsageLocation">
<add-value>
<value type="string">US</value>
</add-value>
<remove-all-values/>
<remove-all-values/>
</modify-attr>
</modify>
</input>
</nds>
[02/06/17 17:28:48.019]:**O365** ST:Remote Interface Driver: Document sent.
[02/06/17 17:28:49.298]:**O365** :Remote Interface Driver: Received.
[02/06/17 17:28:49.298]:**O365** :
<nds dtdversion="3.5">
<source>
<product build="201601270714" instance="\CORP-IDMDEV\acme\system\idm\dset\O365" version="4.1.0.2">Identity Manager Driver for Microsoft Office365</product>
<contact>NetIQ, Corporation.</contact>
</source>
<output>
<status event-id="corp-idm101#20170206222847#1#4:bdbeafb3-1e0b-4e1a-ad8a-b3afbebd0b1e" level="error" type="driver-general"> The operation couldn't be performed because object 'a7a95e1f-8b65-4d84-b499-4aaa9a9b571d' couldn't be found on 'CY1PR17A001DC01.NAMPR17A001.PROD.OUTLOOK.COM'.</status>
</output>
</nds>


The association value is what it complains about at the object ID and if you look at the DirXML-Associations value for the Active Directory driver, that is the correct value, it is the objectGUID from Active Directory being used in Office 365.

I tested with a fresh clean user, this time with just a single part of the license removed, Yammer in this case.

DirXML: [02/06/17 17:45:56.680]: TRACE:  
<nds dtdversion="4.0" ndsversion="8.x">
<source>
<product edition="Advanced" version="4.5.4.0">DirXML</product>
<contact>NetIQ Corporation</contact>
</source>
<input>
<add cached-time="20170206224554.332Z" class-name="MSolUser" event-id="corp-idm101#20170206224554#1#1:9a600231-efd4-4851-a181-3102609ad4ef" qualified-src-dn="O=acme\OU=users\CN=DSmith" src-dn="\CORP-IDMDEV\acme\users\DSmith" src-entry-id="114353" timestamp="1486421154#1">
<add-attr attr-name="DisplayName">
<value timestamp="1467312703#86" type="string">Smith, John M</value>
</add-attr>
<add-attr attr-name="FirstName">
<value timestamp="1467312703#85" type="string">John</value>
</add-attr>
<add-attr attr-name="Country">
<value timestamp="1483644833#319" type="string">New York</value>
</add-attr>
<add-attr attr-name="Department">
<value timestamp="1483644833#315" type="string">Corporate</value>
</add-attr>
<add-attr attr-name="State">
<value timestamp="1483644833#339" type="string">NY</value>
</add-attr>
<add-attr attr-name="LastName">
<value timestamp="1467312703#95" type="string">Smith</value>
</add-attr>
<add-attr attr-name="Title">
<value timestamp="1483644833#329" type="string">Intern-Paid</value>
</add-attr>
<add-attr attr-name="Office">
<value timestamp="1467312703#88" type="string">109574</value>
</add-attr>
<add-attr attr-name="ImmutableId">
<value type="string">EJV0</value>
</add-attr>
<add-attr attr-name="UserPrincipalName">
<value type="string">jsmith@acme.net</value>
</add-attr>
<add-attr attr-name="LicenseAssignment">
<value type="string">acme:ENTERPRISEPACK</value>
</add-attr>
<add-attr attr-name="UsageLocation">
<value type="string">US</value>
</add-attr>
<add-attr attr-name="BlockCredential">
<value type="string">False</value>
</add-attr>
<add-attr attr-name="StrongPasswordRequired">
<value type="state">true</value>
</add-attr>
<password>
<!-- content suppressed -->
</password>
</add>
</input>
</nds>
DirXML: [02/06/17 17:45:56.680]: TRACE: SUB: get-pssession
DirXML: [02/06/17 17:45:56.680]: TRACE: : New-MSolUser -DisplayName 'Smith, John M' -FirstName 'John' -Country 'NYC Chelsea - Non-NYC' -Department 'Corporate' -PostalCode '11793' -State 'NY' -LastName 'Smith' -Title 'Intern-Paid' -Office '109574' -ImmutableId 'EJV0' -UserPrincipalName 'jsmith@acme.net' -UsageLocation 'US' -BlockCredential $False -StrongPasswordRequired $true -Password ****
DirXML: [02/06/17 17:45:57.227]: TRACE: SUB: get-pssession
DirXML: [02/06/17 17:45:57.242]: TRACE: : Set-MSolUserLicense -UserPrincipalName 'jsmith@acme.net' -AddLicenses 'acme:ENTERPRISEPACK'
DirXML: [02/06/17 17:45:57.570]: TRACE: Remote Loader: SubscriptionShim.execute() returned:
DirXML: [02/06/17 17:45:57.570]: TRACE:
<nds dtdversion="3.5">
<source>
<product instance="\CORP-IDMDEV\acme\system\idm\dset\O365" version="4.1.0.2" build="201601270714">Identity Manager Driver for Microsoft Office365</product>
<contact>NetIQ, Corporation.</contact>
</source>
<output>
<status level="success" event-id="corp-idm101#20170206224554#1#1:9a600231-efd4-4851-a181-3102609ad4ef" />
<add-association dest-entry-id="114353" dest-dn="\CORP-IDMDEV\acme\users\DSmith" event-id="corp-idm101#20170206224554#1#1:9a600231-efd4-4851-a181-3102609ad4ef">acdd4b56-d2f2-4a90-adbe-640e11a71d85</add-association>
</output>
</nds>
DirXML: [02/06/17 17:45:57.570]: TRACE: Remote Loader: Sending...
DirXML: [02/06/17 17:45:57.570]: TRACE:
<nds dtdversion="3.5">
<source>
<product instance="\CORP-IDMDEV\acme\system\idm\dset\O365" version="4.1.0.2" build="201601270714">Identity Manager Driver for Microsoft Office365</product>
<contact>NetIQ, Corporation.</contact>
</source>
<output>
<status level="success" event-id="corp-idm101#20170206224554#1#1:9a600231-efd4-4851-a181-3102609ad4ef" />
<add-association dest-entry-id="114353" dest-dn="\CORP-IDMDEV\acme\users\DSmith" event-id="corp-idm101#20170206224554#1#1:9a600231-efd4-4851-a181-3102609ad4ef">acdd4b56-d2f2-4a90-adbe-640e11a71d85</add-association>
</output>
</nds>
DirXML: [02/06/17 17:45:57.570]: TRACE: Remote Loader: Document sent.
DirXML: [02/06/17 17:45:57.570]:
DirXML Log Event -------------------
Driver = \CORP-IDMDEV\acme\system\idm\dset\O365
Thread = Subscriber
Object = \CORP-IDMDEV\acme\users\DSmith



Looks ok. But the license was still the proper full license so that did not actually work, drat.

Finally I tried it without the dash in the custom license name, and just called it CUSTOM, not ACME-CUSTOM and this time, it worked at startup without an error.

Then the trick was to do a Code Map Refresh from User Application. Now instead of showing us just a single E3 Enterprise license, it shows two, the E3 and my Custom license. I can then select that and use it to assign to a user.

There are some downsides to this design (from the Microsoft side, not the Microfocus side). If Microsoft adds a new product, you will have to go back via PowerShell and change everyone if you do not want it enabled by default. In principle you could make a new Resource to use a new Custom license that excludes it, but alas updating the current custom license does not go out and fix everyone with it already assigned. This is a consequence of Microsoft's design of their license model. You would have to remove then add the new Resource to everyone which is probably not a good idea to do in production. Especially since you would be removing in bulk from everyone first, which takes a fair bit of time to process everyone. Then adding them back so there would be a potentially large time gap in between. You could potentially do it pair wise, for any user, remove then grant to keep the time window down, but that would be harder than just simply changing a Resource to Role assignment which is just a few clicks in the GUI but would cause this problem when implemented.

One other interesting thing to look at is visible in the remote loader trace and a high enough level. (It was higher than 3).

When you send in an <add> event as XDS, it parses it and generates the PowerShell command needed. This is useful for testing when it is failing, since you can try it from the Remote Loader servers PowerShell command line to troubleshoot permissions and connectivity.

Thus a full <add> event came in as seen above in the last example, and the driver output:

DirXML: [02/06/17 17:45:56.680]: TRACE:  SUB: get-pssession
DirXML: [02/06/17 17:45:56.680]: TRACE: : New-MSolUser -DisplayName 'Smith, John M' -FirstName 'John' -Country 'NYC Chelsea - Non-NYC' -Department 'Corporate' -PostalCode '11793' -State 'NY' -LastName 'Smith' -Title 'Intern-Paid' -Office '109574' -ImmutableId 'EJV0' -UserPrincipalName 'jsmith@acme.net' -UsageLocation 'US' -BlockCredential $False -StrongPasswordRequired $true -Password ****
DirXML: [02/06/17 17:45:57.227]: TRACE: SUB: get-pssession
DirXML: [02/06/17 17:45:57.242]: TRACE: : Set-MSolUserLicense -UserPrincipalName 'jsmith@acme.net' -AddLicenses 'acme:ENTERPRISEPACK'


There are 4 PowerShell commands issued. get-pssession twice, once before each actual call to connect to the existing session and get a handle to work with it. Then the New-MsolUser command to create the user (implement the UserAccount entitlement at a higher level of abstraction, and finally the Set-MsolUserLicense to implement the License entitlement.

You can see in the New-MsolUser call that each attribute in the XDS document is mapped to a parameter field in the command. This is sort of an interesting approach from a driver level. Parse a set of attributes into a command that takes most of them. Of course where it gets a bit odd and complex is when the command set to do it is spanned across multiple commands.

Labels:

How To-Best Practice
Comment List
Related
Recommended