Skip to content

[feature] add MSI format installation package for Windows system #120

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,54 @@ jobs:
args: release --snapshot --skip-publish --clean
env:
PRIVATE_ACCESS_TOKEN: placeholder
- name: Upload Windows Artifacts
uses: actions/upload-artifact@v3
with:
name: windows_exporter_binaries
path: |
dist/nvidia_gpu_exporter_windows_*/
dist/checksums.txt
build-msi:
needs: build
runs-on: windows-2022
steps:
- name: Checkout
uses: actions/checkout@v3.5.3

- name: Setup Go
uses: actions/setup-go@v4.1.0
with:
go-version-file: go.mod

- name: Download Windows Artifacts
uses: actions/download-artifact@v3
with:
name: windows_exporter_binaries
path: .\dist\

- name: Build Release Artifacts
if: startsWith(github.ref, 'refs/tags/')
run: |
$ErrorActionPreference = "Stop"
$TagName = $env:GITHUB_REF -replace 'refs/tags/', ''
# The MSI version is not semver compliant, so just take the numerical parts
$MSIVersion = $TagName -replace '^v?([0-9\.]+).*$','$1'
Get-ChildItem -Path .\dist\nvidia_gpu_exporter_windows_* | ForEach-Object {
$Arch = ($_ -split "nvidia_gpu_exporter_windows_")[1]
Write-Verbose "Building windows_exporter ${MSIVersion} msi for $Arch"
.\install\build.ps1 -PathToExecutable ".\dist\nvidia_gpu_exporter_windows_${Arch}/nvidia_gpu_exporter.exe" -Version "${MSIVersion}" -Arch "$Arch"
Move-Item ".\install\Output\nvidia_gpu_exporter_${MSIVersion}_${Arch}.msi" .\dist\
}

- name: Display structure of downloaded files
run: ls -R .\dist\

- name: Generate checksums for msi
run: |
"`n" | Add-Content -Path ".\dist\checksums.txt" -NoNewline -Encoding UTF8
Get-FileHash -Algorithm SHA256 -Path .\dist\*.deb | ForEach-Object {
$filename = Split-Path -Leaf $_.Path
$hash = $_.Hash
Write-Output "$hash $filename" | Add-Content -Path ".\dist\checksums.txt" -Encoding UTF8
}

61 changes: 61 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ on:
push:
tags:
- "v*.*.*"

permissions:
contents: write

jobs:
release:
runs-on: ubuntu-22.04
Expand Down Expand Up @@ -32,3 +36,60 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PRIVATE_ACCESS_TOKEN: ${{ secrets.PRIVATE_ACCESS_TOKEN }}
- name: Upload Windows Artifacts
uses: actions/upload-artifact@v3
with:
name: windows_exporter_binaries
path: |
dist/nvidia_gpu_exporter_windows_*/
dist/checksums.txt
release-msi:
needs: release
runs-on: windows-2022
steps:
- name: Checkout
uses: actions/checkout@v3.5.3

- name: Setup Go
uses: actions/setup-go@v4.1.0
with:
go-version-file: go.mod

- name: Download Windows Artifacts
uses: actions/download-artifact@v3
with:
name: windows_exporter_binaries
path: .\dist\

- name: Build Release Artifacts
if: startsWith(github.ref, 'refs/tags/')
run: |
$ErrorActionPreference = "Stop"
$TagName = $env:GITHUB_REF -replace 'refs/tags/', ''
# The MSI version is not semver compliant, so just take the numerical parts
$MSIVersion = $TagName -replace '^v?([0-9\.]+).*$','$1'
Get-ChildItem -Path .\dist\nvidia_gpu_exporter_windows_* | ForEach-Object {
$Arch = ($_ -split "nvidia_gpu_exporter_windows_")[1]
Write-Verbose "Building windows_exporter ${MSIVersion} msi for $Arch"
.\install\build.ps1 -PathToExecutable ".\dist\nvidia_gpu_exporter_windows_${Arch}/nvidia_gpu_exporter.exe" -Version "${MSIVersion}" -Arch "$Arch"
Move-Item ".\install\Output\nvidia_gpu_exporter_${MSIVersion}_${Arch}.msi" .\dist\
}

- name: Display structure of downloaded files
run: ls -R .\dist\

- name: Generate checksums for msi
run: |
Get-FileHash -Algorithm SHA256 -Path .\dist\*.msi | ForEach-Object {
$filename = Split-Path -Leaf $_.Path
$hash = $_.Hash
Write-Output "$hash $filename" | Add-Content -Path ".\dist\checksums.txt" -Encoding UTF8
}

- name: Release
if: startsWith(github.ref, 'refs/tags/')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$TagName = $env:GITHUB_REF -replace 'refs/tags/', ''
Get-ChildItem -Path .\dist\* -Include @('nvidia_gpu_exporter*.msi', 'checksums.txt') | Foreach-Object {gh release upload --clobber $TagName $_}
23 changes: 20 additions & 3 deletions cmd/nvidia_gpu_exporter/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"net"
"net/http"
"os"
"os/signal"
"syscall"

"github.com/alecthomas/kingpin/v2"
"github.com/coreos/go-systemd/v22/activation"
Expand All @@ -20,6 +22,7 @@ import (
webflag "github.com/prometheus/exporter-toolkit/web/kingpinflag"

"github.com/utkuozdemir/nvidia_gpu_exporter/internal/exporter"
"github.com/utkuozdemir/nvidia_gpu_exporter/internal/initiate"
)

const (
Expand Down Expand Up @@ -95,11 +98,25 @@ func main() {
IdleTimeout: *idleTimeout,
}

if err := listenAndServe(srv, webConfig, *network, logger); err != nil {
_ = level.Error(logger).Log("msg", "Error starting HTTP server", "err", err)
go func() {
if err := listenAndServe(srv, webConfig, *network, logger); err != nil {
_ = level.Error(logger).Log("msg", "Error starting HTTP server", "err", err)

os.Exit(1)
os.Exit(1)
}
}()

sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT)

select {
case <-initiate.StopCh:
_ = level.Info(logger).Log("msg", "Shutting down from service")
case <-sig:
_ = level.Info(logger).Log("msg", "Shutting down from signal")
}

os.Exit(0)
}

type RootHandler struct {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/prometheus/exporter-toolkit v0.10.0
github.com/stretchr/testify v1.8.4
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63
golang.org/x/sys v0.11.0
)

require (
Expand All @@ -33,7 +34,6 @@ require (
golang.org/x/net v0.10.0 // indirect
golang.org/x/oauth2 v0.8.0 // indirect
golang.org/x/sync v0.2.0 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.30.0 // indirect
Expand Down
60 changes: 60 additions & 0 deletions install/build.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
[CmdletBinding()]
Param (
[Parameter(Mandatory = $true)]
[String] $PathToExecutable,
[Parameter(Mandatory = $true)]
[String] $Version,
[Parameter(Mandatory = $false)]
[ValidateSet("amd64", "386", "amd64_v1")]
[String] $Arch = "amd64"
)
$ErrorActionPreference = "Stop"

# Get absolute path to executable before switching directories
$PathToExecutable = Resolve-Path $PathToExecutable
# Set working dir to this directory, reset previous on exit
Push-Location $PSScriptRoot
Trap {
# Reset working dir on error
Pop-Location
}

if ($PSVersionTable.PSVersion.Major -lt 5) {
Write-Error "Powershell version 5 required"
exit 1
}

$wc = New-Object System.Net.WebClient
function Get-FileIfNotExists {
Param (
$Url,
$Destination
)
if (-not (Test-Path $Destination)) {
Write-Verbose "Downloading $Url"
$wc.DownloadFile($Url, $Destination)
}
else {
Write-Verbose "${Destination} already exists. Skipping."
}
}

$sourceDir = mkdir -Force Source
mkdir -Force Work, Output | Out-Null

Write-Verbose "Downloading WiX..."
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Get-FileIfNotExists "https://github.com/wixtoolset/wix3/releases/download/wix311rtm/wix311-binaries.zip" "$sourceDir\wix-binaries.zip"
mkdir -Force WiX | Out-Null
Expand-Archive -Path "${sourceDir}\wix-binaries.zip" -DestinationPath WiX -Force

Copy-Item -Force $PathToExecutable Work/nvidia_gpu_exporter.exe

Write-Verbose "Creating nvidia_gpu_exporter_${Version}_${Arch}.msi"
$wixArch = @{"amd64" = "x64"; "amd64_v1" = "x64"; "386" = "x86"}[$Arch]
$wixOpts = "-ext WixFirewallExtension -ext WixUtilExtension"
Invoke-Expression "WiX\candle.exe -nologo -arch $wixArch $wixOpts -out Work\nvidia_gpu_exporter.wixobj -dVersion=`"$Version`" nvidia_gpu_exporter.wxs"
Invoke-Expression "WiX\light.exe -nologo -spdb $wixOpts -out `"Output\nvidia_gpu_exporter_${Version}_${Arch}.msi`" Work\nvidia_gpu_exporter.wixobj"

Write-Verbose "Done!"
Pop-Location
71 changes: 71 additions & 0 deletions install/nvidia_gpu_exporter.wxs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
xmlns:fw="http://schemas.microsoft.com/wix/FirewallExtension"
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
<?if $(sys.BUILDARCH)=x64 ?>
<?define PlatformProgramFiles = "ProgramFiles64Folder" ?>
<?else ?>
<?define PlatformProgramFiles = "ProgramFilesFolder" ?>
<?endif ?>
<Product Id="*" UpgradeCode="66a6eb5b-1fc2-4b14-a362-6ceec6413302"
Name="nvidia_gpu_exporter" Version="$(var.Version)" Manufacturer="lonewayren"
Language="1033" Codepage="1252">
<Package Id="*" Manufacturer="lonewayren" InstallScope="perMachine" InstallerVersion="500"
Description="nvidia_gpu_exporter $(var.Version) installer" Compressed="yes" />
<Media Id="1" Cabinet="nvidia_gpu_exporter.cab" EmbedCab="yes"/>
<MajorUpgrade Schedule="afterInstallInitialize" DowngradeErrorMessage="A later version of [ProductName] is already installed. Setup will now exit." />

<Property Id="EXTRA_FLAGS" Secure="yes"/>
<SetProperty Id="ExtraFlags" After="InstallFiles" Sequence="execute" Value="[EXTRA_FLAGS]">EXTRA_FLAGS</SetProperty>

<Property Id="LISTEN_ADDR" Secure="yes" />
<Property Id="LISTEN_PORT" Secure="yes" />
<SetProperty Id="ListenFlagBoth" After="InstallFiles" Sequence="execute" Value="--web.listen-address=[LISTEN_ADDR]:[LISTEN_PORT]">LISTEN_ADDR AND LISTEN_PORT</SetProperty>
<SetProperty Id="ListenFlagAddr" After="InstallFiles" Sequence="execute" Value="--web.listen-address=[LISTEN_ADDR]:9835">LISTEN_ADDR AND (NOT LISTEN_PORT)</SetProperty>
<SetProperty Id="ListenFlagPort" After="InstallFiles" Sequence="execute" Value="--web.listen-address=0.0.0.0:[LISTEN_PORT]">LISTEN_PORT AND (NOT LISTEN_ADDR)</SetProperty>

<Property Id="METRICS_PATH" Secure="yes"/>
<SetProperty Id="MetricsPathFlag" After="InstallFiles" Sequence="execute" Value="--telemetry.path [METRICS_PATH]">METRICS_PATH</SetProperty>

<Property Id="REMOTE_ADDR" Secure="yes" />
<SetProperty Id="RemoteAddressFlag" After="InstallFiles" Sequence="execute" Value="[REMOTE_ADDR]">REMOTE_ADDR</SetProperty>


<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="$(var.PlatformProgramFiles)">
<Directory Id="APPLICATIONROOTDIRECTORY" Name="nvidia_gpu_exporter">
<Directory Id="bin" Name="bin" />
</Directory>
</Directory>
</Directory>


<ComponentGroup Id="Files">
<Component Id="BinDirectory" Directory="bin">
<CreateFolder />
<RemoveFolder Id='BinDirectory' On='uninstall' />
<File Id="nvidia_gpu_exporter.exe" Name="nvidia_gpu_exporter.exe"
Source="nvidia_gpu_exporter.exe" KeyPath="yes">
<fw:FirewallException Id="GpuMetricsEndpoint" Name="nvidia_gpu_exporter"
Description="nvidia_gpu_exporter HTTP endpoint" Scope="any"
Protocol="tcp" IgnoreFailure="yes" />
</File>
<ServiceInstall Id="GpuInstallExporterService" Name="nvidia_gpu_exporter"
DisplayName="nvidia_gpu_exporter"
Description="Exports Prometheus metrics about the system, Version: $(var.Version)"
ErrorControl="normal" Start="auto" Account="LocalSystem" Type="ownProcess"
Arguments='[ListenFlagBoth] [ListenFlagAddr] [ListenFlagPort] [ExtraFlags]'>
<util:ServiceConfig FirstFailureActionType="restart" SecondFailureActionType="restart"
ThirdFailureActionType="restart" RestartServiceDelayInSeconds="60" />
<ServiceDependency Id="wmiApSrv" />
</ServiceInstall>
<ServiceControl Id="GpuServiceStateControl" Name="nvidia_gpu_exporter" Remove="uninstall" Start="install" Stop="both" />
</Component>
</ComponentGroup>

<Feature Id="GpuDefaultFeature" Level="1">
<ComponentGroupRef Id="Files" />
</Feature>
</Product>
</Wix>

9 changes: 9 additions & 0 deletions internal/initiate/initiate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Package initiate This package allows us to initiate Time
// Sensitive components (Like registering the windows service)
// as early as possible in the startup process
package initiate

// StopCh is used by Windows service.
//
//nolint:gochecknoglobals
var StopCh = make(chan bool)
65 changes: 65 additions & 0 deletions internal/initiate/initiate_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//go:build windows
// +build windows

// Package initiate This package allows us to initiate Time Sensitive components (Like registering the windows service) as early as possible in the startup process
package initiate

import (
"fmt"
"os"

"github.com/go-kit/log"
"github.com/go-kit/log/level"
"golang.org/x/sys/windows/svc"
)

const (
serviceName = "nvidia_gpu_exporter"
)

var logger = log.NewLogfmtLogger(os.Stdout)

type windowsExporterService struct {
stopCh chan<- bool
}

func (s *windowsExporterService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) {
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
changes <- svc.Status{State: svc.StartPending}
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
loop:
for {
select {
case c := <-r:
switch c.Cmd {
case svc.Interrogate:
changes <- c.CurrentStatus
case svc.Stop, svc.Shutdown:
level.Debug(logger).Log("msg", "Service Stop Received")
s.stopCh <- true
break loop
default:
level.Error(logger).Log("msg", fmt.Sprintf("unexpected control request #%d", c))
}
}
}
changes <- svc.Status{State: svc.StopPending}
return
}

func init() {
level.Debug(logger).Log("msg", "Checking if We are a service")
isService, err := svc.IsWindowsService()
if err != nil {
level.Error(logger).Log("msg", err)
}
level.Debug(logger).Log("msg", "Attempting to start exporter service")
if isService {
go func() {
err = svc.Run(serviceName, &windowsExporterService{stopCh: StopCh})
if err != nil {
level.Error(logger).Log("msg", "Failed to start service: ", "error", err)
}
}()
}
}