Windows Azure – Restore Encrypted VM – BEK and KEK

When restoring an Encrypted Virtual Machine that is managed by Windows Azure, you will need to use PowerShell.

This is a two stage process.

Restore-BackupImageToDisk … | Restore-EncryptedBekVM …

Stage 1

Retrieve the Backup Container – This will contain the encrypted disks and json files with the secret keys.

  • keyEncryptionKey
  • diskEncryptionKey

This will allow you to retrieve the disks and JSON metadata in step 2.

Restore-BackupImageToDisk.ps1

param(
    [Parameter(Mandatory=$true)]
    [string]
    $SubscriptionName="RangerRom",

    [Parameter(Mandatory=$true)]
    [string]
    $ResourceGroup="Ranger-Rom-Prod",

    [Parameter(Mandatory=$true)]
    [string]
    $StorageAccount="RangerRomStorage",

    [Parameter(Mandatory=$true)]
    [string]
    $RecoveryVaultName="RecoveryVault",

    [Parameter(Mandatory=$true)]
    [string]
    $VMName,

    [Parameter(Mandatory=$true)]
    [datetime]
    $FromDate
)

Login-AzureRmAccount
Get-AzureRmSubscription -SubscriptionName $SubscriptionName | Select-AzureRmSubscription
$endDate = Get-Date

$namedContainer = Get-AzureRmRecoveryServicesBackupContainer  -ContainerType "AzureVM" -Status "Registered" | Where { $_.FriendlyName -eq $VMName }
$backupitem = Get-AzureRmRecoveryServicesBackupItem -Container $namedContainer  -WorkloadType "AzureVM"

$rp = Get-AzureRmRecoveryServicesBackupRecoveryPoint -Item $backupitem -StartDate $FromDate.ToUniversalTime() -EndDate $enddate.ToUniversalTime()
$restorejob = Restore-AzureRmRecoveryServicesBackupItem -RecoveryPoint $rp[0] -StorageAccountName $StorageAccount -StorageAccountResourceGroupName $ResourceGroup
Wait-AzureRmRecoveryServicesBackupJob -Job $restorejob -Timeout 43200
$restorejob = Get-AzureRmRecoveryServicesBackupJob -Job $restorejob
$details = Get-AzureRmRecoveryServicesBackupJobDetails -Job $restorejob
$details

Stage 2

We will grab the job details via the pipeline if provided or go find the earliest Restore Job after the FromDate and then initiate a restore of all the encrypted disks to the new Virtual Machine.

Restore-EncryptedBekVM.ps1

[CmdletBinding()]
param(
    [Parameter(ValueFromPipeline=$True, Mandatory=$false, HelpMessage="Requires a AzureVmJobDetails object e.g. Get-AzureRmRecoveryServicesBackupJobDetails. Optional.")]
    $JobDetails,

    [Parameter(Mandatory=$true, HelpMessage="Assumes osDisks, DataDisk, RestorePoints, AvailbilitySet, VM, Nic are all in same resource group.")]
    [string]
    $DefaultResourceGroup="Ranger-Rom-Prod",
    
    [Parameter(Mandatory=$true)]
    [string]
    $SourceVMName="Lisha",

    [Parameter(Mandatory=$true)]
    [string]
    $TargetVMName,

    [Parameter(Mandatory=$true)]
    [string]
    $BackupVaultName="RecoveryVault",

    [Parameter(Mandatory=$true)]
    [datetime]
    $FromDate
)

Begin {
    $FromDate = $FromDate.ToUniversalTime()
    Write-Verbose "Started. $(Get-Date)"
}

Process {
	Write-Verbose "Retrieving Backup Vault."
	if(-not $JobDetails)
	{
		Get-AzureRmRecoveryServicesVault -Name $BackupVaultName -ResourceGroupName $DefaultResourceGroup | Set-AzureRmRecoveryServicesVaultContext
		$Job = Get-AzureRmRecoveryServicesBackupJob -Status Completed -Operation Restore -From $FromDate | Sort -Property StartTime | Where { $_.WorkloadName -eq $SourceVMName} | Select -Last 1
		if($Job -eq $null) {
			throw "Job $workLoadName not found."
		}
		$JobDetails = Get-AzureRmRecoveryServicesBackupJobDetails -Job $Job
	}

	Write-Verbose "Query the restored disk properties for the job details."
	$properties = $JobDetails.properties
	$storageAccountName = $properties["Target Storage Account Name"]
	$containerName = $properties["Config Blob Container Name"]
	$blobName = $properties["Config Blob Name"]

	Write-Verbose "Found Restore Blob Set at $($Properties['Config Blob Uri'])"
	Write-Verbose "Set the Azure storage context and restore the JSON configuration file."

	Set-AzureRmCurrentStorageAccount -Name $storageaccountname -ResourceGroupName $DefaultResourceGroup
	$folder = New-Item "C:\RangerRom" -ItemType Directory -Force
	$destination_path = "C:\$($folder.Name)\$SourceVMName.config.json"
	Get-AzureStorageBlobContent -Container $containerName -Blob $blobName -Destination $destination_path
	Write-Verbose "Restore config saved to file. $destination_path"
	$restoreConfig = ((Get-Content -Path $destination_path -Raw -Encoding Unicode)).TrimEnd([char]0x00) | ConvertFrom-Json

	# 3. Use the JSON configuration file to create the VM configuration.
	$oldVM = Get-AzureRmVM | Where { $_.Name -eq $SourceVMName }
	$vm = New-AzureRmVMConfig -VMSize $restoreConfig.'properties.hardwareProfile'.vmSize -VMName $TargetVMName -AvailabilitySetId $oldVM.AvailabilitySetReference.Id
	$vm.Location = $oldVM.Location

	# 4. Attach the OS disk and data disks - Managed, encrypted VMs (BEK only
	$bekUrl = $restoreConfig.'properties.storageProfile'.osDisk.encryptionSettings.diskEncryptionKey.secretUrl
	$keyVaultId = $restoreConfig.'properties.storageProfile'.osDisk.encryptionSettings.diskEncryptionKey.sourceVault.id
	$kekUrl = $restoreConfig.'properties.storageProfile'.osDisk.encryptionSettings.keyEncryptionKey.keyUrl
	$storageType = "StandardLRS"
	$osDiskName = $vm.Name + "_Restored_" + (Get-Date).ToString("yyyy-MM-dd-hh-mm-ss") + "_osdisk"
	$osVhdUri = $restoreConfig.'properties.storageProfile'.osDisk.vhd.uri
	$diskConfig = New-AzureRmDiskConfig -AccountType $storageType -Location $restoreConfig.location -CreateOption Import -SourceUri $osVhdUri
	$osDisk = New-AzureRmDisk -DiskName $osDiskName -Disk $diskConfig -ResourceGroupName $DefaultResourceGroup
	Set-AzureRmVMOSDisk -VM $vm -ManagedDiskId $osDisk.Id -DiskEncryptionKeyUrl $bekUrl -DiskEncryptionKeyVaultId $keyVaultId -KeyEncryptionKeyUrl $kekUrl -KeyEncryptionKeyVaultId $keyVaultId -CreateOption "Attach" -Windows

	$count = 0
	foreach($dd in $restoreConfig.'properties.storageProfile'.dataDisks)
	{
		$dataDiskName = $vm.Name + "_Restored_" + (Get-Date).ToString("yyyy-MM-dd-hh-mm-ss") + "_datadisk" + $count ;
		$dataVhdUri = $dd.vhd.uri;
		$dataDiskConfig = New-AzureRmDiskConfig -AccountType $storageType -Location $restoreConfig.location -CreateOption Import -SourceUri $dataVhdUri
		$dataDisk = New-AzureRmDisk -DiskName $dataDiskName -Disk $dataDiskConfig -ResourceGroupName $DefaultResourceGroup
		Add-AzureRmVMDataDisk -VM $vm -Name $dataDiskName -ManagedDiskId $dataDisk.Id -Lun $dd.Lun -CreateOption "Attach"
		$count += 1
	}

	Write-Verbose  "Setting the Network settings."

	$oldNicId = $oldVM.NetworkProfile.NetworkInterfaces[0].Id
	$oldNic = Get-AzureRmNetworkInterface -Name $oldNicId.Substring($oldNicId.LastIndexOf("/")+ 1) -ResourceGroupName $DefaultResourceGroup
	$subnetItems =  $oldNic.IpConfigurations[0].Subnet.Id.Split("/")
	$networkResourceGroup = $subnetItems[$subnetItems.IndexOf("resourceGroups")+1]
	$nicName= $vm.Name + "_nic_" + (New-Guid).Guid
	$nic = New-AzureRmNetworkInterface -Name $nicName -ResourceGroupName $networkResourceGroup -Location $oldNic.location -SubnetId $oldNic.IpConfigurations[0].Subnet.Id
	$vm=Add-AzureRmVMNetworkInterface -VM $vm -Id $nic.Id
	$vm.DiagnosticsProfile = $oldVM.DiagnosticsProfile

	Write-Verbose "Provisioning VM."
	New-AzureRmVM -ResourceGroupName $oldVM.ResourceGroupName -Location $oldVM.Location -VM $vm

}

End {
    Write-Verbose "Completed. $(Get-Date)"
}


 Usage

.\Restore-BackupImageToDisk … | .\Restore-EncryptedBekVM.ps1 …

Summary

Ensure you have Azure VM Backups and they are backed on on a regular schedule.

Use this script to backup a Restore Point from a Recovery Vault Container.

Remember it will pick the earliest date of a Restore Point relative to your From Date. If there are 4 Restores in the recovery vault from 1 May, it will pick the first one.

Remember to:

  • Decommission the old VM
  • Update DNS with the new ip address
  • Update the Load Balance Sets (If using a Load Balancer)
Advertisements

Update Management solution in Azure – Workspaces

Problem

Update Management in Azure does not support system workspace. If you are using Security Center in Azure, the chances are; all your VM’s are allocated to the system workspace that Security Center created.

The selected workspace is a system workspace and cannot be linked to this account

 

The other error you can get is when you try enable Update Management on VM, and that VM is in another Workspace.

The selected Automation account is already linked to a Long Analytics workspace that is not the selected workspace

Solution

Deallocate All VM’s that you want to use Update Management from the System Workspace to a New Workspace.

  1.  In OMS enable Update Management and Create a New Workspace
  2. Open the Workspace associated with Update Management and note the
    1. Workspace ID
    2. Workspace Primary Key
  3.  Run the following Powershell Scripts
    1. Disconnect-AllVirtualMachinesFromWorkspace
    2. Connect-AllVirtualMachinesToWorkspace -omsID “12345…” -omsKey  “12345…==”
function Disconnect-AllVirtualMachinesFromWorkspace {
        $typeWin = "MicrosoftMonitoringAgent"
        $typeLin = "OmsAgentForLinux"

		$windows = Get-AzureRmVm  | Where { $_.StorageProfile.OsDisk.OsType -eq "Windows" }
		foreach ($vm in $windows) {
			Remove-AzureRmVMExtension -ResourceGroupName "$($vm.ResourceGroupName)" -Name $typeWin -VMName "$($vm.Name)" -Force
        }

        $linux = Get-AzureRmVm  | Where { $_.StorageProfile.OsDisk.OsType -eq "Linux" }-Force
		foreach ($vm in $linux) {
			Remove-AzureRmVMExtension -ResourceGroupName "$($vm.ResourceGroupName)" -Name $typeLin -VMName "$($vm.Name)" -Force
		}
}

function Connect-AllVirtualMachinesToWorkspace {
    param(
        [Parameter(Mandatory=$true, HelpMessage="Workspace Id")]
        [string]
        $omsId,

        [Parameter(Mandatory=$true, HelpMessage="Workspace Primary Key")]
        [string]
        $omsKey
    )

    $typeWin = "MicrosoftMonitoringAgent"
    $typeLin = "OmsAgentForLinux"

    $PublicSettings = New-Object psobject | Add-Member -PassThru NoteProperty workspaceId $omsId | ConvertTo-Json
    $protectedSettings = New-Object psobject | Add-Member -PassThru NoteProperty workspaceKey $omsKey | ConvertTo-Json

    $windows = Get-AzureRmVm  | Where { $_.StorageProfile.OsDisk.OsType -eq "Windows" }
    foreach ($vm in $windows) {
        Set-AzureRmVMExtension -ExtensionName $typeWin -ResourceGroupName  "$($vm.ResourceGroupName)" -VMName "$($vm.Name)" -Publisher "Microsoft.EnterpriseCloud.Monitoring" -ExtensionType $typeWin -TypeHandlerVersion 1.0 -SettingString $PublicSettings  -ProtectedSettingString $protectedSettings  -Location $vm.location
    }

    $linux = Get-AzureRmVm  | Where { $_.StorageProfile.OsDisk.OsType -eq "Linux" }-Force
    foreach ($vm in $linux) {
        Set-AzureRmVMExtension -ExtensionName $typeLin -ResourceGroupName  "$($vm.ResourceGroupName)" -VMName "$($vm.Name)" -Publisher "Microsoft.EnterpriseCloud.Monitoring" -ExtensionType $typeLin -TypeHandlerVersion 1.0 -SettingString $PublicSettings  -ProtectedSettingString $protectedSettings  -Location $vm.location
    }
}

Octopus Deploy – AWS EC2 Web Servers and Elastic Load Balancer

Octopus Deploy has great add on template steps.  Using the

  • AWS – Register ELB Instance
  • AWS – Deregister ELB Instance

Octopus Steps Template

You can then leverage Environments to create Web Server Groups and thus automate deploying your web server instances.

  1. The first step is for Development and Staging, which do not use an ELB
  2. The next 4 steps is for Web Servers in Group 1
  3. The last 4 steps is for Web Servers in Group 2

A Powershell script is used to poll the web server, after the NUGET package is used to deploy the website. It will poll the Web Servers in the group until the homepage returns an HTTP 200 code for all of them. The homepage does various calls to the database, so this page is great to use. You could also create a custom up-time endpoint to query. A really cool way, is to have an API endpoint that returns which servers are up and use this to validate the step for polling.

 

 

Installing Puppet Enterprise on CentOS 7 in AWS EC2 with custom public HostName

Hey,

I ran into a few issues when I wanted to install Puppet Enterprise 2-17 in AWS as an EC2 instance. The main issues were

Summary

  • Need to use hostnamectl and cloud.cfg to change my hostname, as I wanted puppet on a public address, not private address, just for a POC
  • I was using a t2.nano and t2.micro, which will not work with Puppet Enterprise 2017 (puppet-enterprise-2017.2.2-el-7-x86_64). The error you get is just Failed to run PE Installer…… So I used a t2.medium to get around the issue.
  • The usual /etc/hosts file needs some settings and DNS registration (Route53 for me)
  • Disabled SELinux (We usually use a VPN)
  • Configure security groups and have 4433 as backup port (Probably not needed)

Preliminary Install Tasks

  1. Get the latest image from CentOS 7 (x86_64) – with Updates HVM
  2. Spin up an instance with at least 4GB memory, I had a lot of installation issues with applying the catalog with low memory. T2.Medium should work. Bigger is better!
    [puppet.rangerrom.com] Failed to run PE installer on puppet.rangerrom.com.
  3. If you not using a VPN then ensure you setup an elastic IP mapped to the instance for the public DNS name
    ElasticIP.PNG
  4. Register the hostname and elastic IP in DNS
    DNS.PNG
  5. Add you hostnames to /etc/hosts (Important!), note I also added puppet as this is the default for installs. This is a crucial step, so make sure you add your hostnames that you want to use. Put the public hostname first. As this is our primary hostname127.0.0.1  puppet.rangerrom.com puppet localhost
  6. Change the hostname of your EC2 Instance. We need to do the following

    #hostnamectl
    #sudo hostnamectl set-hostname puppet.rangerrom.com –static
    #sudo vi /etc/cloud/cloud.cfg

  7. Add the following to the end of cloud.cfg
    preserve_hostname: true
  8. This is the error I got when I first installed puppet (Due to low memory), therefore we will add port 4433 as well to the AWS security in the next step. I think this was due to insufficient memory, so use a T2.Medium instance size, so you have a minimum of 4GB of memory, else java kills itself. However I add it as a backup here in case you run some other service on 443.

    #sudo vi /var/log/puppetlabs/installer/2017-08-08T02.09.32+0000.install.log

    Failed to apply catalog: Connection refused – connect(2) for “puppet.rangerrom.com” port 4433

  9. Create a security group with the following ports open and also do the same for the Centos Firewall.
    PuppeSecurityGroups
  10. Run  netstat -anp | grep tcp to ensure no port conflicts.
  11. Disable SELinux or have it configured to work in a Puppet Master environment. Edit

    #sudo vi /etc/sysconfig/selinux

    set
    SELINUX=disabled

  12. Edit the sudo vi /etc/ssh/sshd_config and enable Root Logins
    PermitRootLogin yes
  13. Download Puppet Enterprise

    #curl -O https://s3.amazonaws.com/pe-builds/released/2017.2.2/puppet-enterprise-2017.2.2-el-7-x86_64.tar.gz
    #tar -xvf puppet-enterprise-2017.2.2-el-7-x86_64.tar.gz

  14. Install NC and use it to test if your ports are accessible.
    sudo yum install nc
    nc -nlvp 3000 (Run in one terminal) 
  15. nc puppet 3000 ( Run from another terminal)
    NC Test Firewalls.PNG
    This is a great way to ensure firewall rules are not restricting your installation. Secondly we testing that the local server can resolve itself, as it is important that you can resolve puppet and also your custom FQDN before running PE install.
  16. Reboot and run hostnamectl, the new hostname should be preserved.

    #sudo hostnamectl set-hostname puppet.rangerrom.com –static
    [centos@ip-172-31-13-233 ~]$ hostnamectl
    Static hostname: puppet.rangerrom.com
    Transient hostname: ip-172-31-13-233.ap-southeast-2.compute.internal
    Icon name: computer-vm
    Chassis: vm
    Machine ID: 8bd05758fdfc1903174c9fcaf82b71ca
    Boot ID: 0227f164ff23498cbd6a70fb71568745
    Virtualization: xen
    Operating System: CentOS Linux 7 (Core)
    CPE OS Name: cpe:/o:centos:centos:7
    Kernel: Linux 3.10.0-514.26.2.el7.x86_64
    Architecture: x86-64

Installation

  1. Now that we done all our preinstall checks, kick off the installer.

    #sudo ./puppet-enterprise-installer

  2. Enter 1 for a guided install.
  3. Wait until it asks you to connect to the server on https://<fqdn&gt;:3000
    This is what occurs if you did not configure your hostname correctly and you want a public hostname (EC2 internal is default):
    PuppetInstallStage1.PNG

    We want our public hostname.
    PuppetInstallStage1Correct
    Puppet will basically run a thin web server to complete the installation with the following command:
    RACK_ENV=production /opt/puppetlabs/puppet/share/installer/vendor/bundler/bin/thin start –debug -p 3000 -a 0.0.0.0 –ssl –ssl-disable-verify &> /dev/null

  4. Recall, we have the above FQDN in our host file, yours will be your hostname that you setup.
  5. Visit your Puppetmaster site at https://fqdn:3000
  6. Ensure in DNS Alias, you add puppet and all other DNS names you want to use. Otherwise the installation will fail.

    You should see the correct default hostname, if not, you got issues…. I added some alias names such as puppet and my internal and external ec2 addresses.

    PuppetWebDNSAlias.PNG

  7. Set an Admin password and click next
  8. Check and double check the settings to confirm.
    PuppetConfirm.PNG
  9. Check the validation rules, since this is for testing, I am happy with the warnings. It would be awesome if puppetlabs did DNS name resolution validation checks on the HostName. Anyways, here we get a warning about memory, 4GB is what is needed, so if you have install failures it may be due to memory!
    Validator.PNG
  10. I am feeling lucky, lets try with 3533MB of RAM 🙂SuccessInstall.PNG

T-SQL UpperCase first letter of word

I am amazed by the complex solutions out on the internet to upper case the first letter of a word in SQL. Here is a way I think is nice and simple.


-- Test Data

declare @word varchar(100)
with good as (select 'good' as a union select 'nice' union select 'fine')
select @word = (SELECT TOP 1 a FROM good ORDER BY NEWID())

-- Implementation

select substring(Upper(@word),1,1) + substring(@word, 2, LEN(@word))

Request.Browser.IsMobileDevice & Tablet Devices

Hi,

The problem with Request.Browser.IsMobileDevice is that it will classify a tablet as a mobile device.

If you need to discern between mobile, tablet and desktop. Then use the following extension method.

public static class HttpBrowserCapabilitiesBaseExtensions
{
 public static bool IsMobileNotTablet(this HttpBrowserCapabilitiesBase browser)
 {
  var userAgent = browser.Capabilities[""].ToString();
  var r = new Regex("ipad|(android(?!.*mobile))|xoom|sch-i800|playbook|tablet|kindle|nexus|silk", RegexOptions.IgnoreCase);
  var isTablet = r.IsMatch(userAgent) && browser.IsMobileDevice;
  return !isTablet && browser.IsMobileDevice;
 }
}

 

Then to use it is easy. Just import the namespace and reference the method.

using Web.Public.Helpers;
...
if (Request.Browser.IsMobileNotTablet() && !User.IsSubscribed)
....

 

 

 

 

JWPlayer – Customization

JWPlayer Cusomisation

You can do a lot of cusomisation with JWPlayer via CSS skinning with V7, but it has it’s limitations…

Sometimes you need to show an image on a hosted JWPlayer.. It is especially important for mobile devices, as autoplay is disabled on phones, so an image is great feature to have:


<script>
function setupPlayer(container,jwFeedUrl, cameraImage) {
  var xhttp = new XMLHttpRequest();
  xhttp.open("GET", jwFeedUrl, true);
  xhttp.send();
  xhttp.onreadystatechange = function () {
  if (xhttp.readyState == 4 && xhttp.status == 200) {
   var list = JSON.parse(xhttp.responseText).playlist;
   list[0].image = cameraImage;
   var playerone = jwplayer(container);
   playerone.setup({
    playlist: list,
     mute: 'true',
     autostart: 'true'
   });
  } else if (xhttp.readyState == 4 && xhttp.status == 404) {
     Console.log("Error loading player " + jwFeedUrl);
    }
 };
}

setupPlayer("myDivId","https://content.jwplatform.com/feeds/x12U4l67.json","http://assets.rangerrom.com/portrait/rangerrom.png");

</script>

JWPlayer Setup Timeout Issues

If you ever see get a Setup Timout error from JWPlayer. A useful workaround if you do not have time to optimise the website asset pipeline is to load the JWPlayer only once the html document is ready.


document.addEventListener("DOMContentLoaded", function (event) {
setupPlayer(...);

or with JQuery


$(function() {
  setupPlayer(...);
});

 

XCSoar on a Kobo Touch 2.0 with BlueFly GPS

Hi,

I decided it was time to get a Kobo Touch 2.0 and get XCSoar installed on it, complete with GPS. I chose the Touch 2.0 (Amazon.de).

  1. Buy a Kobo Touch 2.0
  2. Backup the SD Card
  3. You will need to purchase a BlueFlyVario_TTL_GPS_v11. I chose the TTL, as the USB version is prone to breaking the USB connector.
  4. Download and 3D print the cover from ThingVerse.
  5. A Soldering Iron Solder and a few tools.
  6. A spare circuit board to practice your soldering skills. You cannot afford to make a mistake when soldering.
  7. Star Phillips screw driver
  8. 3mm Drill bit
  9. 3M x 6mm bolts and nuts, used to fix the  cover to the case. It is tricky getting the nut to fit in the small gap on the Touch 2.0. I just drill holes on the side of the Kobo and slide the nut in like a coin slot.
  10. Software for the Kobo. You can download it here. That link has pictures and all the software to get the Kobo up and running with XCSoar 6.8.

There are detailed instructions you can follow here.

http://blueflyvario.blogspot.com.au/2014/11/kobo-glo-install.html

The reason for this post, is to compliment the above article with some pictures.

 

Inventory

Solder Station

Kobo Touch 2.0 Serial Port

Important: The RX on the Kobo Port goes to TX on the BlueFly. I use the screw to ground the circuit (yellow wire). Green Wire goes to V. Black and Red is TX and  RX on Kobo and switched around on BlueFly.

Soldered To Kobo 2.0

KoboRoot.tgz

Use a Linux Operating System to copy KoboRoot.tgz to the hidden .kobo folder and then reboot the Kobo. I just take out the SD Card, plug it into a machine with Linux or on Windows with a Virtual Machine running Linux (I use Kali Distro).

Customise XCSoar

Once rebooted and XCSoar is running, go to Nickle and then plug in the Kobo 2.0, then just copy additional files for XCSoar. E.g. Maps, Waypoints

Device Settings

Make sure you go into XCSoar Config -> Devices and set the device settings for the BlueFly Vario. Once done, use the monitor button to check feedback. (Ensure the Vario is turned on, by pushing the button. It will make an annoying sound continuously.)…it was music to my ears.

Events – BlueFly.xci

In XCSoar, register the BlyeFly.xci file as an Event in Language/Input. Ensure advance mode is on. This gives you a cool menu for BlueFly where you can set the volume of the beautiful sound it makes.

bluefly.xci

Go outside, and test your new navigation tool to compliment your flight deck. Happy flying with loads of battery time 🙂

Tips:

I made two holes on the bottom side of the Kobo, to get the 3mm Nuts in, so I could bolt down the cover. Maybe you can get it working with 2mm bolts and nuts….

Only remove the Circuit Board screws. Do not remove the screws that hold the screen in. Leave the screen in the device during the entire modification process.  Keep all wires above the circuit board, else the screen will not function correctly as it relies on a certain amount of pressure around the perimeter of the device. Look carefully under the circuit board, you will see lots of connectors around it. I initially tried to solder from under the circuit board (Easier to solder), but this cause a lot of issues with the screen touch sensitivity.

Nickly and the E-Reader still work after the modifications. If yours does not. Restore the SD card from a backup you made and go through the software modification again. e.g. 1. Copy the koboroot file to .kobo -> Reboot -> Customise XCSoar -> Nickel should work again.

if you ever reset the device with a long press on the power button, it may break Nickel and you can only use XCSoar without the e-reader. The symptom is Nickel will show a black screen.

JWPlayer .NET Client – Management API

Hi,

We recently migrated all our content from Ooyala to JWPlayer Hosted Platform. We needed a .NET tool to perform the following:

  1. Create Videos from remote sources
  2. Update Videos later e.g. Thumbnails etc
  3. List Videos in a Custom Application
  4. Other cools ideas that come in after adopting a new tool

Currently JWPlayer Management API only has a PHP and Python 2.7 Client as examples for Batch Migration.

To use in Visual Studio:

  1. Open Package Manager Console
  2. Run – Install-Package JWPlayer.NET

I have created an Open Source JWPlayer.NET library. Please feel free to improve on it e.g. Make it fluent.

Get Source Code (JWPlayer.NET)

Below is how you can use the API as at 29/06/2017.

Create Video

var jw = new Jw(ApiKey, ApiSecret);
var parameters = new Dictionary<string, string>
{
    {"sourceurl", "http://www.sample-videos.com/video/mp4/720/big_buck_bunny_720p_1mb.mp4"},
    {"sourceformat", "mp4"},
    {"sourcetype", "url"},
    {"title", "Test"},
    {"description", "Test Video"},
    {"tags", "foo, bar"},
    {"custom.LegacyId", Guid.NewGuid().ToString()}
};
var result = jw.CreateVideo(parameters);

Update Video

var jw = new Jw(ApiKey, ApiSecret);
var parameters = new Dictionary<string, string>
{
    {"video_key", "QxbbRMMP"},
    {"title", "Test Updated"},
    {"tags", "foo, bar, updated"},
};
var result = jw.UpdateVideo(parameters);

List Video

var jw = new Jw(ApiKey, ApiSecret);
var basicVideoSearch = new BasicVideoSearch {Search = "Foo", StartDate = DateTime.UtcNow.AddDays(-100)};
var result = jw.ListVideos(basicVideoSearch);
var count = result.Videos.Count;

 

Batch Migrations

Ensure you stick to the Rate Limit of 60 calls per minute, or call your JWPlayer Account Manager to increase it.

for(var i = 0; i < lines.count; i++)
{
   jw.CreateVideo(parameters);
   Thread.Sleep(TimeSpan.FromSeconds(1));
}

Clone JWPlayer.NET

Calculate Wind Direction and Wind Speed from Wind Vectors

Wind Vectors have a U (Eastward) and V (Northward) Component.

Below is the code in C# to calculate the resultant wind

public struct Wind
    {
        public Wind(float speed, float direction)
        {
            Speed = speed;
            Direction = direction;
        }
        public float Speed { get; set; }
        public float Direction { get; set; }
    }

public static Wind CalculateWindSpeedAndDirection(float u, float v)
        {
            if(Math.Abs(u) < 0.001 && Math.Abs(v) < 0.001)
                return new Wind(0,0);
            const double radianToDegree = (180 / Math.PI);

            return new Wind(
                Convert.ToSingle(Math.Sqrt(Math.Pow(u, 2) + Math.Pow(v, 2))),
                Convert.ToSingle(Math.Atan2(u, v) * radianToDegree + 180));
        }

Test Code

        [TestCase(-8.748f, 7.157f, 11.303f, 129.29f)]
        [TestCase(-4.641f, -3.049f, 5.553f, 56.696f)]
        [TestCase(10f, 0f, 10f, 270f)]
        [TestCase(-10f, 0f, 10f, 90)]
        [TestCase(0f, 10f, 10f, 180f)]
        [TestCase(0f, -10f, 10f, 360f)]
        [TestCase(0f,0f,0f,0f)]
        [TestCase(0.001f, 0.001f, 0.0014142f, 225f)]
        public void CanConvertWindVectorComponents(float u, float v, float expectedWindSpeed, float expectedWindDirection)
        {
            var result = MetraWaveForecastLocationModel.CalculateWindSpeedAndDirection(u, v);
            Assert.AreEqual(Math.Round(expectedWindDirection,2), Math.Round(result.Direction,2));
            Assert.AreEqual(Math.Round(expectedWindSpeed,2), Math.Round(result.Speed,2));
        }