• Azure DevOps API PropertiesCollections

    I was looking at some of the Azure DevOps API documentation and noticed that some of the endpoints mention a properties object of type PropertiesCollection. Unfortunately, the details for that data structure are not particularly helpful, and I couldn't figure out how to use it. Some pages include examples, but none that I could find included an expanded properties object.

    To figure out how to use it, I created a simple .NET console application. I added references to the following NuGet packages:

    • Microsoft.TeamFoundationServer.Client
    • Microsoft.VisualStudio.Services.InteractiveClient
    • Microsoft.VisualStudio.Services.Release.Client
    using Microsoft.VisualStudio.Services.Common;
    using Microsoft.VisualStudio.Services.ReleaseManagement.WebApi.Clients;
    using Microsoft.VisualStudio.Services.WebApi;
    
    const string collectionUri = "https://dev.azure.com/organisation";
    const string projectName = "MyProject";
    const string pat = "YOUR-PAT-HERE";
    const int releaseId = 20;
    
    var creds = new VssBasicCredential(string.Empty, pat);
    
    // Connect to Azure DevOps Services
    var connection = new VssConnection(new Uri(collectionUri), creds);
    
    using var client = connection.GetClient<ReleaseHttpClient>();
    
    // Get data about a specific release
    var release = await client.GetReleaseAsync(projectName, releaseId);
    
    release.Properties.Add("Thing", "hey");
    
    // Send the updated release back to Azure DevOps Services
    var result = await client.UpdateReleaseAsync(release!, projectName, releaseId);
    
    Console.WriteLine();
    

    This allowed me to create a property key and value, that I could then examine by querying the item (in this case a 'classic' release), by calling the GET endpoint. eg.

    https://vsrm.dev.azure.com/{organization}/{project}/_apis/release/releases/{releaseId}?propertyFilters=Thing&api-version=7.0
    

    Note that you need to specify the propertyFilters parameter. Otherwise the properties` object will not be included in the response.

    And in doing that, we can see the JSON data structure!

        "properties": {
            "Thing": {
                "$type": "System.String",
                "$value": "hey"
            }
        }
    

    So, to add a property, you need to add a new key/value pair to the properties object, where the key is the name of the property, and the value is an object with two properties: $type and $value. The $type property is the type of the value, and the $value property is the value itself.

    The documentation clarifies the types supported:

    Values of type Byte[], Int32, Double, DateType and String preserve their type, other primitives are retuned as a String. Byte[] expected as base64 encoded string.

    (I think 'DateType' is a typo, and should be 'DateTime')

    Now that we know the shape of the data, I can jump back to PowerShell and use that to add a new property:

    $uri = "https://vsrm.dev.azure.com/$($organisation)/$($project)/_apis/release/releases/$($releaseId)?api-version=7.0&propertyFilters=Extra"
    
    $result = Invoke-RestMethod -Uri $uri -Method Get -Headers $headers
    
    if (-not ($result.properties.Extra)) {
        $result.properties | Add-Member -MemberType NoteProperty -Name "Extra" -Value @{
            "`$type" = "System.String"
            "`$value" = "haaaa"
        }
    }
    $body = $result | ConvertTo-Json -Depth 20
    
    "Updating via PUT"
    
    Invoke-RestMethod -Uri $uri -Method Put -Headers $headers -Body $body -ContentType "application/json"
    

  • New laptop - Dell XPS 9530

    At SixPivot we get a generous laptop allowance (currently $AU4,400!) that we can use every two years. Given I celebrated my 2-year anniversary earlier this year, I was keen to organise an upgrade to the laptop I'd purchased when I first started. Rather than order immediately, I knew that Dell had some new models coming out, so I held off until they became available in Australia before ordering. Dell XPS 9530, front left view I do agree with the adage "Use the right tool for the job", and as a software developer, I want to use the best computer possible.

    The Dell Latitude 14" laptop has done the job, but I wanted to return to a 15" laptop. The convenience of a full-size keyboard, with a little more room in the case to ensure better cooling should hopefully make for a more comfortable and quieter experience.

    View of Dell XPS 9530 showing the keyboard

    I selected the new Dell XPS 15 9530 and decked it out with the following options:

    • 13th Generation Intel(R) Core(TM) i9-13900H Processor (14-Core, 24MB Cache, up to 5.4 GHz)
    • NVIDIA(R) GeForce(R) RTX(TM) 4070 with 8 GB GDDR6
    • 64GB DDR5 4800MHz
    • 2TB M.2 PCIe NVMe Solid State Drive
    • 15.6" OLED 3.5K (3456x2160) InfinityEdge Touch Anti-Reflective 400-Nit Display

    The i9 CPU is the fastest they offer (and my first i9), and it's also the latest generation which I preferred. I wanted the higher res display over standard HD - it happens to be a touch display, which is a bonus. I don't use touch a lot (and would have been fine without it), but it can be handy sometimes.

    There were higher-capacity SSDs on offer, but I haven't really got close to filling the 2TB drive on the Latitude, so I think that should be fine. Upgrading later is always an option. 64GB of RAM is double what I had previously, but you can never have too much RAM!

    Cost

    If you head over to the Dell website and select similar options, you may notice the total cost is quite a bit more than my allowance, so how does that work? Well, there are a couple of extra factors working in my favour:

    1. SixPivot has a business account with Dell, so we get better pricing than regular retail, and
    2. SixPivot also has a "Good Vibes" points scheme, where we can each award points to our colleagues to show our appreciation for something great they've done. Those points can be spent in our SixPivot Rewards shop, and one thing you can spend them on is portable devices (like laptops). I had accrued enough points to cover the extra amount above the $4,400.

    Comparison

    It's interesting to see how my new device compares to my previous platforms, and how styles and ports have changed over the years.

    (top to bottom, newest to oldest) The XPS 9530, Latitude 7420, XPS 9550 and XPS 1645.

    Stack of laptops showing ports on left side Stack of laptops showing ports on right side

    Gone are the USB-A, HDMI, DisplayPort, VGA, eSATA ports and the like. Instead, just 3 USB-C ports, an SD card reader slot, and a 3.5mm audio jack are all you get. It finally feels like USB-C is becoming the de-facto standard for extensibility. If you didn't have an existing dock, Dell does throw in a handy compact USB-C to USB-A and HDMI adapter.

    USB-C adapter

    Unlike my last XPS (the 9550), the camera is now located on top of the screen (whatever were they thinking!), and interestingly the 9730 is actually a little bit narrower even though the display is still 15.6".

    This is a pretty beefy machine, and I think it should easily see me through the next 2 years!

  • Finding free subnets in an Azure Virtual Network

    An Azure Virtual Network (as the docs say) is "the fundamental building block for your private network in Azure". Often abbreviated to "VNet". When a VNet is created, you specify the available IP address range using CIDR notation. If you create a VNET through the Azure Portal, it defaults to 10.1.0.0/16, which equates to 65536 IP addresses (10.1.0.0 - 10.1.255.255).

    A VNet contains one or more subnets, where the IP range for each subnet is assigned from the VNet's allocation. One thing to note - you can't resize a VNet. Once it has been created, that's it. If you use up all the available IP addresses, your only options are to create a new VNet and peer it to the original VNet, or if the newer VNet is larger, migrate all your services over to it (which may not be trivial).

    If a VNet has been in use for some time or is used by multiple teams, you can end up with fragmentation - gaps between allocated subnets. This could happen because new subnets are allocated by choosing a 'nice' number to start on (rather than following immediately from the last allocated), or from a previously allocated subnet being deleted. e.g.

    Azure Virtual Network with a list of subnets

    In this VNet it turns out we have some gaps. While the temptation might be to allocate the next subnet starting at 10.0.2.0, depending on the size required, we might be able to use one of the available gaps instead.

    Now maybe you can read CIDR IP addresses in your sleep and can not only spot the gaps but know intuitively what ranges you could allocate. For the rest of us, I'd either resort to a pencil and paper or (more likely) see if I could script out the answer using PowerShell.

    And so I created a PowerShell script to query a VNet and list both the existing subnets and also the available gaps (and CIDR ranges that could use those gaps). I started sharing this script with a few of my SixPivot colleagues, as they were experiencing the same situation. I realised it would be good to make this more widely available, so the result is my first PowerShell module published to the PowerShell Gallery (under the SixPivot name) - SixPivot.Azure, which contains the Find-FreeSubnets function.

    Using the Find-FreeSubnets cmdlet

    First off, install the module:

    Install-Module SixPivot.Azure
    

    If you haven't previously connected to Azure then you'll need to do this:

    Connect-AzAccount
    

    Now you can use Find-FreeSubnets. You need to know the resource group and VNET name. eg.

    Find-FreeSubnets -ResourceGroup rg-freesubnet-australiaeast -VNetName vnet-freesubnet-australiaeast
    

    This will produce output similar to the following:

    VNet Start VNet End     Available      Subnets
    ---------- --------     ---------      -------
    10.0.0.0   10.0.255.255 {48, 8, 65184} {10.0.0.0/24, 10.0.1.0/28, 10.0.1.64/28, 10.0.1.88/29}
    

    The output is structured data. If you assign it to a variable, then you can dig down into the different parts.

    $vnet = Find-FreeSubnets -ResourceGroup rg-freesubnet-australiaeast -VNetName
    

    For the VNET itself, you can get the start and end addresses using VNetStart and VNetEnd properties.

    $vnet.VNetStart, $vnet.VNetEnd
    10.0.0.0
    10.0.255.255
    

    You can see the currently allocated subnets via the Subnets property:

    $vnet.Subnets
    
    Address space Range start Range end
    ------------- ----------- ---------
    10.0.0.0/24   10.0.0.0    10.0.0.255
    10.0.1.0/28   10.0.1.0    10.0.1.15
    10.0.1.64/28  10.0.1.64   10.0.1.79
    10.0.1.88/29  10.0.1.88   10.0.1.95
    

    And finally, (and this is the good bit!), the available subnets via the Available property

    $vnet.Available
    
    Start     End          Size  Available ranges
    -----     ---          ----  ----------------
    10.0.1.16 10.0.1.63    48    {10.0.1.16/28, 10.0.1.32/27, 10.0.1.32/28, 10.0.1.48/28}
    10.0.1.80 10.0.1.87    8
    10.0.1.96 10.0.255.255 65184 {10.0.1.96/27, 10.0.1.96/28, 10.0.1.112/28, 10.0.1.128/25…}
    

    For a particular Start and End, you can see potential CIDR ranges with the CIDRAvailable property:

    $vnet.Available[0].CIDRAvailable
    10.0.1.16/28
    10.0.1.32/27
    10.0.1.32/28
    10.0.1.48/28
    
    $vnet.Available[2].CIDRAvailable
    10.0.1.96/27
    10.0.1.96/28
    10.0.1.112/28
    10.0.1.128/25
    10.0.1.128/26
    10.0.1.128/27
    10.0.1.128/28
    10.0.1.144/28
    ...
    

    Possible prefix lengths of 25, 26, 27 or 28 are shown. The output for the second example actually scrolled way off the page, so watch out if the available Size is quite large.

    From the first available range, I could use either:

    • 10.0.1.16/28 and 10.0.1.32/27
    • or 10.0.1.16/28, 10.0.1.32/28 and 10.0.1.48/28

    Future enhancements

    The cmdlet is useful already, but one feature I'd like to add is to be able to pass in one or more CIDR prefix lengths (eg. 28,28,27) and allow it to find compatible non-overlapping ranges automatically.