File 0003-Add-Uyuni-service-discovery.patch of Package golang-github-prometheus-prometheus

From 7009f54ee93f8f0a36a1a9320a56410ce6a11f44 Mon Sep 17 00:00:00 2001
From: Joao Cavalheiro <jcavalheiro@suse.de>
Date: Mon, 27 Jul 2020 17:42:33 +0200
Subject: [PATCH 3/3] Add Uyuni service discovery

---
 discovery/install/install.go                 |   1 +
 discovery/uyuni/uyuni.go                     | 397 ++++++++++++++++
 discovery/uyuni/uyuni_test.go                |  46 ++
 go.mod                                       |   1 +
 go.sum                                       |   2 +
 vendor/github.com/kolo/xmlrpc/LICENSE        |  19 +
 vendor/github.com/kolo/xmlrpc/README.md      |  90 ++++
 vendor/github.com/kolo/xmlrpc/client.go      | 161 +++++++
 vendor/github.com/kolo/xmlrpc/decoder.go     | 473 +++++++++++++++++++
 vendor/github.com/kolo/xmlrpc/encoder.go     | 181 +++++++
 vendor/github.com/kolo/xmlrpc/go.mod         |   5 +
 vendor/github.com/kolo/xmlrpc/go.sum         |   3 +
 vendor/github.com/kolo/xmlrpc/is_zero.go     |  44 ++
 vendor/github.com/kolo/xmlrpc/request.go     |  57 +++
 vendor/github.com/kolo/xmlrpc/response.go    |  42 ++
 vendor/github.com/kolo/xmlrpc/test_server.rb |  25 +
 vendor/modules.txt                           |   3 +
 17 files changed, 1550 insertions(+)
 create mode 100644 discovery/uyuni/uyuni.go
 create mode 100644 discovery/uyuni/uyuni_test.go
 create mode 100644 vendor/github.com/kolo/xmlrpc/LICENSE
 create mode 100644 vendor/github.com/kolo/xmlrpc/README.md
 create mode 100644 vendor/github.com/kolo/xmlrpc/client.go
 create mode 100644 vendor/github.com/kolo/xmlrpc/decoder.go
 create mode 100644 vendor/github.com/kolo/xmlrpc/encoder.go
 create mode 100644 vendor/github.com/kolo/xmlrpc/go.mod
 create mode 100644 vendor/github.com/kolo/xmlrpc/go.sum
 create mode 100644 vendor/github.com/kolo/xmlrpc/is_zero.go
 create mode 100644 vendor/github.com/kolo/xmlrpc/request.go
 create mode 100644 vendor/github.com/kolo/xmlrpc/response.go
 create mode 100644 vendor/github.com/kolo/xmlrpc/test_server.rb

diff --git a/discovery/install/install.go b/discovery/install/install.go
index d9394f270..7af209cac 100644
--- a/discovery/install/install.go
+++ b/discovery/install/install.go
@@ -30,5 +30,6 @@ import (
 	_ "github.com/prometheus/prometheus/discovery/marathon"     // register marathon
 	_ "github.com/prometheus/prometheus/discovery/openstack"    // register openstack
 	_ "github.com/prometheus/prometheus/discovery/triton"       // register triton
+	_ "github.com/prometheus/prometheus/discovery/uyuni"        // register uyuni
 	_ "github.com/prometheus/prometheus/discovery/zookeeper"    // register zookeeper
 )
diff --git a/discovery/uyuni/uyuni.go b/discovery/uyuni/uyuni.go
new file mode 100644
index 000000000..674fdb7d7
--- /dev/null
+++ b/discovery/uyuni/uyuni.go
@@ -0,0 +1,397 @@
+// Copyright 2019 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package uyuni
+
+import (
+	"context"
+	"fmt"
+	"net"
+	"net/url"
+	"regexp"
+	"strings"
+	"time"
+
+	"github.com/go-kit/kit/log"
+	"github.com/go-kit/kit/log/level"
+	"github.com/kolo/xmlrpc"
+	"github.com/pkg/errors"
+	"github.com/prometheus/common/model"
+
+	"github.com/prometheus/prometheus/discovery"
+	"github.com/prometheus/prometheus/discovery/refresh"
+	"github.com/prometheus/prometheus/discovery/targetgroup"
+)
+
+const (
+	monitoringEntitlementLabel    = "monitoring_entitled"
+	prometheusExporterFormulaName = "prometheus-exporters"
+	uyuniXMLRPCAPIPath            = "/rpc/api"
+)
+
+// DefaultSDConfig is the default Uyuni SD configuration.
+var DefaultSDConfig = SDConfig{
+	RefreshInterval: model.Duration(1 * time.Minute),
+}
+
+// Regular expression to extract port from formula data
+var monFormulaRegex = regexp.MustCompile(`--(?:telemetry\.address|web\.listen-address)=\":([0-9]*)\"`)
+
+func init() {
+	discovery.RegisterConfig(&SDConfig{})
+}
+
+// SDConfig is the configuration for Uyuni based service discovery.
+type SDConfig struct {
+	Host            string         `yaml:"host"`
+	User            string         `yaml:"username"`
+	Pass            string         `yaml:"password"`
+	RefreshInterval model.Duration `yaml:"refresh_interval,omitempty"`
+}
+
+// Uyuni API Response structures
+type systemGroupID struct {
+	GroupID   int    `xmlrpc:"id"`
+	GroupName string `xmlrpc:"name"`
+}
+
+type networkInfo struct {
+	SystemID int    `xmlrpc:"system_id"`
+	Hostname string `xmlrpc:"hostname"`
+	IP       string `xmlrpc:"ip"`
+}
+
+type exporterConfig struct {
+	Address string `xmlrpc:"address"`
+	Args    string `xmlrpc:"args"`
+	Enabled bool   `xmlrpc:"enabled"`
+}
+
+type proxiedExporterConfig struct {
+	ProxyIsEnabled   bool           `xmlrpc:"proxy_enabled"`
+	ProxyPort        float32        `xmlrpc:"proxy_port"`
+	NodeExporter     exporterConfig `xmlrpc:"node_exporter"`
+	ApacheExporter   exporterConfig `xmlrpc:"apache_exporter"`
+	PostgresExporter exporterConfig `xmlrpc:"postgres_exporter"`
+}
+
+// Discovery periodically performs Uyuni API requests. It implements the Discoverer interface.
+type Discovery struct {
+	*refresh.Discovery
+	interval time.Duration
+	sdConfig *SDConfig
+	logger   log.Logger
+}
+
+// Name returns the name of the Config.
+func (*SDConfig) Name() string { return "uyuni" }
+
+// NewDiscoverer returns a Discoverer for the Config.
+func (c *SDConfig) NewDiscoverer(opts discovery.DiscovererOptions) (discovery.Discoverer, error) {
+	return NewDiscovery(c, opts.Logger), nil
+}
+
+// UnmarshalYAML implements the yaml.Unmarshaler interface.
+func (c *SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
+	*c = DefaultSDConfig
+	type plain SDConfig
+	err := unmarshal((*plain)(c))
+
+	if err != nil {
+		return err
+	}
+	if c.Host == "" {
+		return errors.New("Uyuni SD configuration requires a Host")
+	}
+	if c.User == "" {
+		return errors.New("Uyuni SD configuration requires a Username")
+	}
+	if c.Pass == "" {
+		return errors.New("Uyuni SD configuration requires a Password")
+	}
+	if c.RefreshInterval <= 0 {
+		return errors.New("Uyuni SD configuration requires RefreshInterval to be a positive integer")
+	}
+	return nil
+}
+
+// Attempt to login in Uyuni Server and get an auth token
+func login(rpcclient *xmlrpc.Client, user string, pass string) (string, error) {
+	var result string
+	err := rpcclient.Call("auth.login", []interface{}{user, pass}, &result)
+	return result, err
+}
+
+// Logout from Uyuni API
+func logout(rpcclient *xmlrpc.Client, token string) error {
+	err := rpcclient.Call("auth.logout", token, nil)
+	return err
+}
+
+// Get the system groups information of monitored clients
+func getSystemGroupsInfoOfMonitoredClients(rpcclient *xmlrpc.Client, token string) (map[int][]systemGroupID, error) {
+	var systemGroupsInfos []struct {
+		SystemID     int             `xmlrpc:"id"`
+		SystemGroups []systemGroupID `xmlrpc:"system_groups"`
+	}
+	err := rpcclient.Call("system.listSystemGroupsForSystemsWithEntitlement", []interface{}{token, monitoringEntitlementLabel}, &systemGroupsInfos)
+	if err != nil {
+		return nil, err
+	}
+	result := make(map[int][]systemGroupID)
+	for _, systemGroupsInfo := range systemGroupsInfos {
+		result[systemGroupsInfo.SystemID] = systemGroupsInfo.SystemGroups
+	}
+	return result, nil
+}
+
+// GetSystemNetworkInfo lists client FQDNs
+func getNetworkInformationForSystems(rpcclient *xmlrpc.Client, token string, systemIDs []int) (map[int]networkInfo, error) {
+	var networkInfos []networkInfo
+	err := rpcclient.Call("system.getNetworkForSystems", []interface{}{token, systemIDs}, &networkInfos)
+	if err != nil {
+		return nil, err
+	}
+	result := make(map[int]networkInfo)
+	for _, networkInfo := range networkInfos {
+		result[networkInfo.SystemID] = networkInfo
+	}
+	return result, nil
+}
+
+// Get formula data for a given system
+func getExporterDataForSystems(
+	rpcclient *xmlrpc.Client,
+	token string,
+	systemIDs []int,
+) (map[int]proxiedExporterConfig, error) {
+	var combinedFormulaData []struct {
+		SystemID        int                   `xmlrpc:"system_id"`
+		ExporterConfigs proxiedExporterConfig `xmlrpc:"formula_values"`
+	}
+	err := rpcclient.Call(
+		"formula.getCombinedFormulaDataByServerIds",
+		[]interface{}{token, prometheusExporterFormulaName, systemIDs},
+		&combinedFormulaData)
+	if err != nil {
+		return nil, err
+	}
+	result := make(map[int]proxiedExporterConfig)
+	for _, combinedFormulaData := range combinedFormulaData {
+		result[combinedFormulaData.SystemID] = combinedFormulaData.ExporterConfigs
+	}
+	return result, nil
+}
+
+// extractPortFromFormulaData gets exporter port configuration from the formula.
+// args takes precedence over address.
+func extractPortFromFormulaData(args string, address string) (string, error) {
+	// first try args
+	var port string
+	tokens := monFormulaRegex.FindStringSubmatch(args)
+	if len(tokens) < 1 {
+		err := "Unable to find port in args: " + args
+		// now try address
+		_, addrPort, addrErr := net.SplitHostPort(address)
+		if addrErr != nil || len(addrPort) == 0 {
+			if addrErr != nil {
+				err = strings.Join([]string{addrErr.Error(), err}, " ")
+			}
+			return "", errors.New(err)
+		}
+		port = addrPort
+	} else {
+		port = tokens[1]
+	}
+
+	return port, nil
+}
+
+// NewDiscovery returns a new file discovery for the given paths.
+func NewDiscovery(conf *SDConfig, logger log.Logger) *Discovery {
+	d := &Discovery{
+		interval: time.Duration(conf.RefreshInterval),
+		sdConfig: conf,
+		logger:   logger,
+	}
+	d.Discovery = refresh.NewDiscovery(
+		logger,
+		"uyuni",
+		time.Duration(conf.RefreshInterval),
+		d.refresh,
+	)
+	return d
+}
+
+func initializeExporterTargets(
+	targets *[]model.LabelSet,
+	module string, config exporterConfig,
+	proxyPort string,
+	errors *[]error,
+) {
+	if !(config.Enabled) {
+		return
+	}
+	var port string
+	if len(proxyPort) == 0 {
+		exporterPort, err := extractPortFromFormulaData(config.Args, config.Address)
+		if err != nil {
+			*errors = append(*errors, err)
+			return
+		}
+		port = exporterPort
+	} else {
+		port = proxyPort
+	}
+
+	labels := model.LabelSet{}
+	labels["exporter"] = model.LabelValue(module + "_exporter")
+	// for now set only port number here
+	labels[model.AddressLabel] = model.LabelValue(port)
+	if len(proxyPort) > 0 {
+		labels[model.ParamLabelPrefix+"module"] = model.LabelValue(module)
+	}
+	*targets = append(*targets, labels)
+}
+
+func (d *Discovery) getTargetsForSystem(
+	systemID int,
+	systemGroupsIDs []systemGroupID,
+	networkInfo networkInfo,
+	combinedFormulaData proxiedExporterConfig,
+) []model.LabelSet {
+
+	var labelSets []model.LabelSet
+	var errors []error
+	var proxyPortNumber string
+	if combinedFormulaData.ProxyIsEnabled {
+		proxyPortNumber = fmt.Sprintf("%d", int(combinedFormulaData.ProxyPort))
+	}
+	initializeExporterTargets(&labelSets, "node", combinedFormulaData.NodeExporter, proxyPortNumber, &errors)
+	initializeExporterTargets(&labelSets, "apache", combinedFormulaData.ApacheExporter, proxyPortNumber, &errors)
+	initializeExporterTargets(&labelSets, "postgres", combinedFormulaData.PostgresExporter, proxyPortNumber, &errors)
+	managedGroupNames := getSystemGroupNames(systemGroupsIDs)
+	for _, labels := range labelSets {
+		// add hostname to the address label
+		addr := fmt.Sprintf("%s:%s", networkInfo.IP, labels[model.AddressLabel])
+		labels[model.AddressLabel] = model.LabelValue(addr)
+		labels["hostname"] = model.LabelValue(networkInfo.Hostname)
+		labels["groups"] = model.LabelValue(strings.Join(managedGroupNames, ","))
+		if combinedFormulaData.ProxyIsEnabled {
+			labels[model.MetricsPathLabel] = "/proxy"
+		}
+		_ = level.Debug(d.logger).Log("msg", "Configured target", "Labels", fmt.Sprintf("%+v", labels))
+	}
+	for _, err := range errors {
+		level.Error(d.logger).Log("msg", "Invalid exporter port", "clientId", systemID, "err", err)
+	}
+
+	return labelSets
+}
+
+func getSystemGroupNames(systemGroupsIDs []systemGroupID) []string {
+	managedGroupNames := make([]string, 0, len(systemGroupsIDs))
+	for _, systemGroupInfo := range systemGroupsIDs {
+		managedGroupNames = append(managedGroupNames, systemGroupInfo.GroupName)
+	}
+
+	if len(managedGroupNames) == 0 {
+		managedGroupNames = []string{"No group"}
+	}
+	return managedGroupNames
+}
+
+func (d *Discovery) getTargetsForSystems(
+	rpcClient *xmlrpc.Client,
+	token string,
+	systemGroupIDsBySystemID map[int][]systemGroupID,
+) ([]model.LabelSet, error) {
+
+	result := make([]model.LabelSet, 0)
+
+	systemIDs := make([]int, 0, len(systemGroupIDsBySystemID))
+	for systemID := range systemGroupIDsBySystemID {
+		systemIDs = append(systemIDs, systemID)
+	}
+
+	combinedFormulaDataBySystemID, err := getExporterDataForSystems(rpcClient, token, systemIDs)
+	if err != nil {
+		return nil, errors.Wrap(err, "Unable to get systems combined formula data")
+	}
+	networkInfoBySystemID, err := getNetworkInformationForSystems(rpcClient, token, systemIDs)
+	if err != nil {
+		return nil, errors.Wrap(err, "Unable to get the systems network information")
+	}
+
+	for _, systemID := range systemIDs {
+		targets := d.getTargetsForSystem(
+			systemID,
+			systemGroupIDsBySystemID[systemID],
+			networkInfoBySystemID[systemID],
+			combinedFormulaDataBySystemID[systemID])
+		result = append(result, targets...)
+
+		// Log debug information
+		if networkInfoBySystemID[systemID].IP != "" {
+			level.Debug(d.logger).Log("msg", "Found monitored system",
+				"Host", networkInfoBySystemID[systemID].Hostname,
+				"Network", fmt.Sprintf("%+v", networkInfoBySystemID[systemID]),
+				"Groups", fmt.Sprintf("%+v", systemGroupIDsBySystemID[systemID]),
+				"Formulas", fmt.Sprintf("%+v", combinedFormulaDataBySystemID[systemID]))
+		}
+	}
+	return result, nil
+}
+
+func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) {
+	config := d.sdConfig
+	apiURL := config.Host + uyuniXMLRPCAPIPath
+
+	startTime := time.Now()
+
+	// Check if the URL is valid and create rpc client
+	_, err := url.ParseRequestURI(apiURL)
+	if err != nil {
+		return nil, errors.Wrap(err, "Uyuni Server URL is not valid")
+	}
+
+	rpcClient, _ := xmlrpc.NewClient(apiURL, nil)
+
+	token, err := login(rpcClient, config.User, config.Pass)
+	if err != nil {
+		return nil, errors.Wrap(err, "Unable to login to Uyuni API")
+	}
+	systemGroupIDsBySystemID, err := getSystemGroupsInfoOfMonitoredClients(rpcClient, token)
+	if err != nil {
+		return nil, errors.Wrap(err, "Unable to get the managed system groups information of monitored clients")
+	}
+
+	targets := make([]model.LabelSet, 0)
+	if len(systemGroupIDsBySystemID) > 0 {
+		targetsForSystems, err := d.getTargetsForSystems(rpcClient, token, systemGroupIDsBySystemID)
+		if err != nil {
+			return nil, err
+		}
+		targets = append(targets, targetsForSystems...)
+		level.Info(d.logger).Log("msg", "Total discovery time", "time", time.Since(startTime))
+	} else {
+		fmt.Printf("\tFound 0 systems.\n")
+	}
+
+	err = logout(rpcClient, token)
+	if err != nil {
+		level.Warn(d.logger).Log("msg", "Failed to log out from Uyuni API", "err", err)
+	}
+	rpcClient.Close()
+	return []*targetgroup.Group{&targetgroup.Group{Targets: targets, Source: config.Host}}, nil
+}
diff --git a/discovery/uyuni/uyuni_test.go b/discovery/uyuni/uyuni_test.go
new file mode 100644
index 000000000..c5fa8cc9e
--- /dev/null
+++ b/discovery/uyuni/uyuni_test.go
@@ -0,0 +1,46 @@
+// Copyright 2019 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package uyuni
+
+import "testing"
+
+func TestExtractPortFromFormulaData(t *testing.T) {
+	type args struct {
+		args    string
+		address string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    string
+		wantErr bool
+	}{
+		{name: `TestArgs`, args: args{args: `--web.listen-address=":9100"`}, want: `9100`},
+		{name: `TestAddress`, args: args{address: `:9100`}, want: `9100`},
+		{name: `TestArgsAndAddress`, args: args{args: `--web.listen-address=":9100"`, address: `9999`}, want: `9100`},
+		{name: `TestMissingPort`, args: args{args: `localhost`}, wantErr: true},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := extractPortFromFormulaData(tt.args.args, tt.args.address)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("extractPortFromFormulaData() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if got != tt.want {
+				t.Errorf("extractPortFromFormulaData() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/go.mod b/go.mod
index 38982f449..9a261a5c9 100644
--- a/go.mod
+++ b/go.mod
@@ -38,6 +38,7 @@ require (
 	github.com/hetznercloud/hcloud-go v1.22.0
 	github.com/influxdata/influxdb v1.8.3
 	github.com/json-iterator/go v1.1.10
+	github.com/kolo/xmlrpc v0.0.0-20201022064351-38db28db192b
 	github.com/miekg/dns v1.1.31
 	github.com/mitchellh/mapstructure v1.2.2 // indirect
 	github.com/morikuni/aec v1.0.0 // indirect
diff --git a/go.sum b/go.sum
index 9a16a1838..be06c9453 100644
--- a/go.sum
+++ b/go.sum
@@ -524,6 +524,8 @@ github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0
 github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
 github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg=
 github.com/klauspost/pgzip v1.0.2-0.20170402124221-0bf5dcad4ada/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
+github.com/kolo/xmlrpc v0.0.0-20201022064351-38db28db192b h1:iNjcivnc6lhbvJA3LD622NPrUponluJrBWPIwGG/3Bg=
+github.com/kolo/xmlrpc v0.0.0-20201022064351-38db28db192b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
 github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
diff --git a/vendor/github.com/kolo/xmlrpc/LICENSE b/vendor/github.com/kolo/xmlrpc/LICENSE
new file mode 100644
index 000000000..8103dd139
--- /dev/null
+++ b/vendor/github.com/kolo/xmlrpc/LICENSE
@@ -0,0 +1,19 @@
+Copyright (C) 2012 Dmitry Maksimov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/vendor/github.com/kolo/xmlrpc/README.md b/vendor/github.com/kolo/xmlrpc/README.md
new file mode 100644
index 000000000..fecfcd839
--- /dev/null
+++ b/vendor/github.com/kolo/xmlrpc/README.md
@@ -0,0 +1,90 @@
+[![GoDoc](https://godoc.org/github.com/kolo/xmlrpc?status.svg)](https://godoc.org/github.com/kolo/xmlrpc)
+
+## Overview
+
+xmlrpc is an implementation of client side part of XMLRPC protocol in Go language.
+
+## Status
+
+This project is in minimal maintenance mode with no further development. Bug fixes
+are accepted, but it might take some time until they will be merged.
+
+## Installation
+
+To install xmlrpc package run `go get github.com/kolo/xmlrpc`. To use
+it in application add `"github.com/kolo/xmlrpc"` string to `import`
+statement.
+
+## Usage
+
+    client, _ := xmlrpc.NewClient("https://bugzilla.mozilla.org/xmlrpc.cgi", nil)
+    result := struct{
+      Version string `xmlrpc:"version"`
+    }{}
+    client.Call("Bugzilla.version", nil, &result)
+    fmt.Printf("Version: %s\n", result.Version) // Version: 4.2.7+
+
+Second argument of NewClient function is an object that implements
+[http.RoundTripper](http://golang.org/pkg/net/http/#RoundTripper)
+interface, it can be used to get more control over connection options.
+By default it initialized by http.DefaultTransport object.
+
+### Arguments encoding
+
+xmlrpc package supports encoding of native Go data types to method
+arguments.
+
+Data types encoding rules:
+
+* int, int8, int16, int32, int64 encoded to int;
+* float32, float64 encoded to double;
+* bool encoded to boolean;
+* string encoded to string;
+* time.Time encoded to datetime.iso8601;
+* xmlrpc.Base64 encoded to base64;
+* slice encoded to array;
+
+Structs encoded to struct by following rules:
+
+* all public field become struct members;
+* field name become member name;
+* if field has xmlrpc tag, its value become member name.
+* for fields tagged with `",omitempty"`, empty values are omitted;
+
+Server method can accept few arguments, to handle this case there is
+special approach to handle slice of empty interfaces (`[]interface{}`).
+Each value of such slice encoded as separate argument.
+
+### Result decoding
+
+Result of remote function is decoded to native Go data type.
+
+Data types decoding rules:
+
+* int, i4 decoded to int, int8, int16, int32, int64;
+* double decoded to float32, float64;
+* boolean decoded to bool;
+* string decoded to string;
+* array decoded to slice;
+* structs decoded following the rules described in previous section;
+* datetime.iso8601 decoded as time.Time data type;
+* base64 decoded to string.
+
+## Implementation details
+
+xmlrpc package contains clientCodec type, that implements [rpc.ClientCodec](http://golang.org/pkg/net/rpc/#ClientCodec)
+interface of [net/rpc](http://golang.org/pkg/net/rpc) package.
+
+xmlrpc package works over HTTP protocol, but some internal functions
+and data type were made public to make it easier to create another
+implementation of xmlrpc that works over another protocol. To encode
+request body there is EncodeMethodCall function. To decode server
+response Response data type can be used.
+
+## Contribution
+
+See [project status](#status).
+
+## Authors
+
+Dmitry Maksimov (dmtmax@gmail.com)
diff --git a/vendor/github.com/kolo/xmlrpc/client.go b/vendor/github.com/kolo/xmlrpc/client.go
new file mode 100644
index 000000000..643dc1c10
--- /dev/null
+++ b/vendor/github.com/kolo/xmlrpc/client.go
@@ -0,0 +1,161 @@
+package xmlrpc
+
+import (
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/http/cookiejar"
+	"net/rpc"
+	"net/url"
+	"sync"
+)
+
+type Client struct {
+	*rpc.Client
+}
+
+// clientCodec is rpc.ClientCodec interface implementation.
+type clientCodec struct {
+	// url presents url of xmlrpc service
+	url *url.URL
+
+	// httpClient works with HTTP protocol
+	httpClient *http.Client
+
+	// cookies stores cookies received on last request
+	cookies http.CookieJar
+
+	// responses presents map of active requests. It is required to return request id, that
+	// rpc.Client can mark them as done.
+	responses map[uint64]*http.Response
+	mutex     sync.Mutex
+
+	response Response
+
+	// ready presents channel, that is used to link request and it`s response.
+	ready chan uint64
+
+	// close notifies codec is closed.
+	close chan uint64
+}
+
+func (codec *clientCodec) WriteRequest(request *rpc.Request, args interface{}) (err error) {
+	httpRequest, err := NewRequest(codec.url.String(), request.ServiceMethod, args)
+
+	if err != nil {
+		return err
+	}
+
+	if codec.cookies != nil {
+		for _, cookie := range codec.cookies.Cookies(codec.url) {
+			httpRequest.AddCookie(cookie)
+		}
+	}
+
+	var httpResponse *http.Response
+	httpResponse, err = codec.httpClient.Do(httpRequest)
+
+	if err != nil {
+		return err
+	}
+
+	if codec.cookies != nil {
+		codec.cookies.SetCookies(codec.url, httpResponse.Cookies())
+	}
+
+	codec.mutex.Lock()
+	codec.responses[request.Seq] = httpResponse
+	codec.mutex.Unlock()
+
+	codec.ready <- request.Seq
+
+	return nil
+}
+
+func (codec *clientCodec) ReadResponseHeader(response *rpc.Response) (err error) {
+	var seq uint64
+	select {
+	case seq = <-codec.ready:
+	case <-codec.close:
+		return errors.New("codec is closed")
+	}
+	response.Seq = seq
+
+	codec.mutex.Lock()
+	httpResponse := codec.responses[seq]
+	delete(codec.responses, seq)
+	codec.mutex.Unlock()
+
+	defer httpResponse.Body.Close()
+
+	if httpResponse.StatusCode < 200 || httpResponse.StatusCode >= 300 {
+		response.Error = fmt.Sprintf("request error: bad status code - %d", httpResponse.StatusCode)
+		return nil
+	}
+
+	body, err := ioutil.ReadAll(httpResponse.Body)
+	if err != nil {
+		response.Error = err.Error()
+		return nil
+	}
+
+	resp := Response(body)
+	if err := resp.Err(); err != nil {
+		response.Error = err.Error()
+		return nil
+	}
+
+	codec.response = resp
+
+	return nil
+}
+
+func (codec *clientCodec) ReadResponseBody(v interface{}) (err error) {
+	if v == nil {
+		return nil
+	}
+	return codec.response.Unmarshal(v)
+}
+
+func (codec *clientCodec) Close() error {
+	if transport, ok := codec.httpClient.Transport.(*http.Transport); ok {
+		transport.CloseIdleConnections()
+	}
+
+	close(codec.close)
+
+	return nil
+}
+
+// NewClient returns instance of rpc.Client object, that is used to send request to xmlrpc service.
+func NewClient(requrl string, transport http.RoundTripper) (*Client, error) {
+	if transport == nil {
+		transport = http.DefaultTransport
+	}
+
+	httpClient := &http.Client{Transport: transport}
+
+	jar, err := cookiejar.New(nil)
+
+	if err != nil {
+		return nil, err
+	}
+
+	u, err := url.Parse(requrl)
+
+	if err != nil {
+		return nil, err
+	}
+
+	codec := clientCodec{
+		url:        u,
+		httpClient: httpClient,
+		close:      make(chan uint64),
+		ready:      make(chan uint64),
+		responses:  make(map[uint64]*http.Response),
+		cookies:    jar,
+	}
+
+	return &Client{rpc.NewClientWithCodec(&codec)}, nil
+}
diff --git a/vendor/github.com/kolo/xmlrpc/decoder.go b/vendor/github.com/kolo/xmlrpc/decoder.go
new file mode 100644
index 000000000..d4dcb19ad
--- /dev/null
+++ b/vendor/github.com/kolo/xmlrpc/decoder.go
@@ -0,0 +1,473 @@
+package xmlrpc
+
+import (
+	"bytes"
+	"encoding/xml"
+	"errors"
+	"fmt"
+	"io"
+	"reflect"
+	"strconv"
+	"strings"
+	"time"
+)
+
+const (
+	iso8601        = "20060102T15:04:05"
+	iso8601Z       = "20060102T15:04:05Z07:00"
+	iso8601Hyphen  = "2006-01-02T15:04:05"
+	iso8601HyphenZ = "2006-01-02T15:04:05Z07:00"
+)
+
+var (
+	// CharsetReader is a function to generate reader which converts a non UTF-8
+	// charset into UTF-8.
+	CharsetReader func(string, io.Reader) (io.Reader, error)
+
+	timeLayouts     = []string{iso8601, iso8601Z, iso8601Hyphen, iso8601HyphenZ}
+	invalidXmlError = errors.New("invalid xml")
+)
+
+type TypeMismatchError string
+
+func (e TypeMismatchError) Error() string { return string(e) }
+
+type decoder struct {
+	*xml.Decoder
+}
+
+func unmarshal(data []byte, v interface{}) (err error) {
+	dec := &decoder{xml.NewDecoder(bytes.NewBuffer(data))}
+
+	if CharsetReader != nil {
+		dec.CharsetReader = CharsetReader
+	}
+
+	var tok xml.Token
+	for {
+		if tok, err = dec.Token(); err != nil {
+			return err
+		}
+
+		if t, ok := tok.(xml.StartElement); ok {
+			if t.Name.Local == "value" {
+				val := reflect.ValueOf(v)
+				if val.Kind() != reflect.Ptr {
+					return errors.New("non-pointer value passed to unmarshal")
+				}
+				if err = dec.decodeValue(val.Elem()); err != nil {
+					return err
+				}
+
+				break
+			}
+		}
+	}
+
+	// read until end of document
+	err = dec.Skip()
+	if err != nil && err != io.EOF {
+		return err
+	}
+
+	return nil
+}
+
+func (dec *decoder) decodeValue(val reflect.Value) error {
+	var tok xml.Token
+	var err error
+
+	if val.Kind() == reflect.Ptr {
+		if val.IsNil() {
+			val.Set(reflect.New(val.Type().Elem()))
+		}
+		val = val.Elem()
+	}
+
+	var typeName string
+	for {
+		if tok, err = dec.Token(); err != nil {
+			return err
+		}
+
+		if t, ok := tok.(xml.EndElement); ok {
+			if t.Name.Local == "value" {
+				return nil
+			} else {
+				return invalidXmlError
+			}
+		}
+
+		if t, ok := tok.(xml.StartElement); ok {
+			typeName = t.Name.Local
+			break
+		}
+
+		// Treat value data without type identifier as string
+		if t, ok := tok.(xml.CharData); ok {
+			if value := strings.TrimSpace(string(t)); value != "" {
+				if err = checkType(val, reflect.String); err != nil {
+					return err
+				}
+
+				val.SetString(value)
+				return nil
+			}
+		}
+	}
+
+	switch typeName {
+	case "struct":
+		ismap := false
+		pmap := val
+		valType := val.Type()
+
+		if err = checkType(val, reflect.Struct); err != nil {
+			if checkType(val, reflect.Map) == nil {
+				if valType.Key().Kind() != reflect.String {
+					return fmt.Errorf("only maps with string key type can be unmarshalled")
+				}
+				ismap = true
+			} else if checkType(val, reflect.Interface) == nil && val.IsNil() {
+				var dummy map[string]interface{}
+				valType = reflect.TypeOf(dummy)
+				pmap = reflect.New(valType).Elem()
+				val.Set(pmap)
+				ismap = true
+			} else {
+				return err
+			}
+		}
+
+		var fields map[string]reflect.Value
+
+		if !ismap {
+			fields = make(map[string]reflect.Value)
+
+			for i := 0; i < valType.NumField(); i++ {
+				field := valType.Field(i)
+				fieldVal := val.FieldByName(field.Name)
+
+				if fieldVal.CanSet() {
+					if fn := field.Tag.Get("xmlrpc"); fn != "" {
+						fields[fn] = fieldVal
+					} else {
+						fields[field.Name] = fieldVal
+					}
+				}
+			}
+		} else {
+			// Create initial empty map
+			pmap.Set(reflect.MakeMap(valType))
+		}
+
+		// Process struct members.
+	StructLoop:
+		for {
+			if tok, err = dec.Token(); err != nil {
+				return err
+			}
+			switch t := tok.(type) {
+			case xml.StartElement:
+				if t.Name.Local != "member" {
+					return invalidXmlError
+				}
+
+				tagName, fieldName, err := dec.readTag()
+				if err != nil {
+					return err
+				}
+				if tagName != "name" {
+					return invalidXmlError
+				}
+
+				var fv reflect.Value
+				ok := true
+
+				if !ismap {
+					fv, ok = fields[string(fieldName)]
+				} else {
+					fv = reflect.New(valType.Elem())
+				}
+
+				if ok {
+					for {
+						if tok, err = dec.Token(); err != nil {
+							return err
+						}
+						if t, ok := tok.(xml.StartElement); ok && t.Name.Local == "value" {
+							if err = dec.decodeValue(fv); err != nil {
+								return err
+							}
+
+							// </value>
+							if err = dec.Skip(); err != nil {
+								return err
+							}
+
+							break
+						}
+					}
+				}
+
+				// </member>
+				if err = dec.Skip(); err != nil {
+					return err
+				}
+
+				if ismap {
+					pmap.SetMapIndex(reflect.ValueOf(string(fieldName)), reflect.Indirect(fv))
+					val.Set(pmap)
+				}
+			case xml.EndElement:
+				break StructLoop
+			}
+		}
+	case "array":
+		slice := val
+		if checkType(val, reflect.Interface) == nil && val.IsNil() {
+			slice = reflect.ValueOf([]interface{}{})
+		} else if err = checkType(val, reflect.Slice); err != nil {
+			return err
+		}
+
+	ArrayLoop:
+		for {
+			if tok, err = dec.Token(); err != nil {
+				return err
+			}
+
+			switch t := tok.(type) {
+			case xml.StartElement:
+				var index int
+				if t.Name.Local != "data" {
+					return invalidXmlError
+				}
+			DataLoop:
+				for {
+					if tok, err = dec.Token(); err != nil {
+						return err
+					}
+
+					switch tt := tok.(type) {
+					case xml.StartElement:
+						if tt.Name.Local != "value" {
+							return invalidXmlError
+						}
+
+						if index < slice.Len() {
+							v := slice.Index(index)
+							if v.Kind() == reflect.Interface {
+								v = v.Elem()
+							}
+							if v.Kind() != reflect.Ptr {
+								return errors.New("error: cannot write to non-pointer array element")
+							}
+							if err = dec.decodeValue(v); err != nil {
+								return err
+							}
+						} else {
+							v := reflect.New(slice.Type().Elem())
+							if err = dec.decodeValue(v); err != nil {
+								return err
+							}
+							slice = reflect.Append(slice, v.Elem())
+						}
+
+						// </value>
+						if err = dec.Skip(); err != nil {
+							return err
+						}
+						index++
+					case xml.EndElement:
+						val.Set(slice)
+						break DataLoop
+					}
+				}
+			case xml.EndElement:
+				break ArrayLoop
+			}
+		}
+	default:
+		if tok, err = dec.Token(); err != nil {
+			return err
+		}
+
+		var data []byte
+
+		switch t := tok.(type) {
+		case xml.EndElement:
+			return nil
+		case xml.CharData:
+			data = []byte(t.Copy())
+		default:
+			return invalidXmlError
+		}
+
+		switch typeName {
+		case "int", "i4", "i8":
+			if checkType(val, reflect.Interface) == nil && val.IsNil() {
+				i, err := strconv.ParseInt(string(data), 10, 64)
+				if err != nil {
+					return err
+				}
+
+				pi := reflect.New(reflect.TypeOf(i)).Elem()
+				pi.SetInt(i)
+				val.Set(pi)
+			} else if err = checkType(val, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64); err != nil {
+				return err
+			} else {
+				i, err := strconv.ParseInt(string(data), 10, val.Type().Bits())
+				if err != nil {
+					return err
+				}
+
+				val.SetInt(i)
+			}
+		case "string", "base64":
+			str := string(data)
+			if checkType(val, reflect.Interface) == nil && val.IsNil() {
+				pstr := reflect.New(reflect.TypeOf(str)).Elem()
+				pstr.SetString(str)
+				val.Set(pstr)
+			} else if err = checkType(val, reflect.String); err != nil {
+				return err
+			} else {
+				val.SetString(str)
+			}
+		case "dateTime.iso8601":
+			var t time.Time
+			var err error
+
+			for _, layout := range timeLayouts {
+				t, err = time.Parse(layout, string(data))
+				if err == nil {
+					break
+				}
+			}
+			if err != nil {
+				return err
+			}
+
+			if checkType(val, reflect.Interface) == nil && val.IsNil() {
+				ptime := reflect.New(reflect.TypeOf(t)).Elem()
+				ptime.Set(reflect.ValueOf(t))
+				val.Set(ptime)
+			} else if _, ok := val.Interface().(time.Time); !ok {
+				return TypeMismatchError(fmt.Sprintf("error: type mismatch error - can't decode %v to time", val.Kind()))
+			} else {
+				val.Set(reflect.ValueOf(t))
+			}
+		case "boolean":
+			v, err := strconv.ParseBool(string(data))
+			if err != nil {
+				return err
+			}
+
+			if checkType(val, reflect.Interface) == nil && val.IsNil() {
+				pv := reflect.New(reflect.TypeOf(v)).Elem()
+				pv.SetBool(v)
+				val.Set(pv)
+			} else if err = checkType(val, reflect.Bool); err != nil {
+				return err
+			} else {
+				val.SetBool(v)
+			}
+		case "double":
+			if checkType(val, reflect.Interface) == nil && val.IsNil() {
+				i, err := strconv.ParseFloat(string(data), 64)
+				if err != nil {
+					return err
+				}
+
+				pdouble := reflect.New(reflect.TypeOf(i)).Elem()
+				pdouble.SetFloat(i)
+				val.Set(pdouble)
+			} else if err = checkType(val, reflect.Float32, reflect.Float64); err != nil {
+				return err
+			} else {
+				i, err := strconv.ParseFloat(string(data), val.Type().Bits())
+				if err != nil {
+					return err
+				}
+
+				val.SetFloat(i)
+			}
+		default:
+			return errors.New("unsupported type")
+		}
+
+		// </type>
+		if err = dec.Skip(); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (dec *decoder) readTag() (string, []byte, error) {
+	var tok xml.Token
+	var err error
+
+	var name string
+	for {
+		if tok, err = dec.Token(); err != nil {
+			return "", nil, err
+		}
+
+		if t, ok := tok.(xml.StartElement); ok {
+			name = t.Name.Local
+			break
+		}
+	}
+
+	value, err := dec.readCharData()
+	if err != nil {
+		return "", nil, err
+	}
+
+	return name, value, dec.Skip()
+}
+
+func (dec *decoder) readCharData() ([]byte, error) {
+	var tok xml.Token
+	var err error
+
+	if tok, err = dec.Token(); err != nil {
+		return nil, err
+	}
+
+	if t, ok := tok.(xml.CharData); ok {
+		return []byte(t.Copy()), nil
+	} else {
+		return nil, invalidXmlError
+	}
+}
+
+func checkType(val reflect.Value, kinds ...reflect.Kind) error {
+	if len(kinds) == 0 {
+		return nil
+	}
+
+	if val.Kind() == reflect.Ptr {
+		val = val.Elem()
+	}
+
+	match := false
+
+	for _, kind := range kinds {
+		if val.Kind() == kind {
+			match = true
+			break
+		}
+	}
+
+	if !match {
+		return TypeMismatchError(fmt.Sprintf("error: type mismatch - can't unmarshal %v to %v",
+			val.Kind(), kinds[0]))
+	}
+
+	return nil
+}
diff --git a/vendor/github.com/kolo/xmlrpc/encoder.go b/vendor/github.com/kolo/xmlrpc/encoder.go
new file mode 100644
index 000000000..7ab271aa5
--- /dev/null
+++ b/vendor/github.com/kolo/xmlrpc/encoder.go
@@ -0,0 +1,181 @@
+package xmlrpc
+
+import (
+	"bytes"
+	"encoding/xml"
+	"fmt"
+	"reflect"
+	"sort"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// Base64 represents value in base64 encoding
+type Base64 string
+
+type encodeFunc func(reflect.Value) ([]byte, error)
+
+func marshal(v interface{}) ([]byte, error) {
+	if v == nil {
+		return []byte{}, nil
+	}
+
+	val := reflect.ValueOf(v)
+	return encodeValue(val)
+}
+
+func encodeValue(val reflect.Value) ([]byte, error) {
+	var b []byte
+	var err error
+
+	if val.Kind() == reflect.Ptr || val.Kind() == reflect.Interface {
+		if val.IsNil() {
+			return []byte("<value/>"), nil
+		}
+
+		val = val.Elem()
+	}
+
+	switch val.Kind() {
+	case reflect.Struct:
+		switch val.Interface().(type) {
+		case time.Time:
+			t := val.Interface().(time.Time)
+			b = []byte(fmt.Sprintf("<dateTime.iso8601>%s</dateTime.iso8601>", t.Format(iso8601)))
+		default:
+			b, err = encodeStruct(val)
+		}
+	case reflect.Map:
+		b, err = encodeMap(val)
+	case reflect.Slice:
+		b, err = encodeSlice(val)
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		b = []byte(fmt.Sprintf("<int>%s</int>", strconv.FormatInt(val.Int(), 10)))
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+		b = []byte(fmt.Sprintf("<i4>%s</i4>", strconv.FormatUint(val.Uint(), 10)))
+	case reflect.Float32, reflect.Float64:
+		b = []byte(fmt.Sprintf("<double>%s</double>",
+			strconv.FormatFloat(val.Float(), 'f', -1, val.Type().Bits())))
+	case reflect.Bool:
+		if val.Bool() {
+			b = []byte("<boolean>1</boolean>")
+		} else {
+			b = []byte("<boolean>0</boolean>")
+		}
+	case reflect.String:
+		var buf bytes.Buffer
+
+		xml.Escape(&buf, []byte(val.String()))
+
+		if _, ok := val.Interface().(Base64); ok {
+			b = []byte(fmt.Sprintf("<base64>%s</base64>", buf.String()))
+		} else {
+			b = []byte(fmt.Sprintf("<string>%s</string>", buf.String()))
+		}
+	default:
+		return nil, fmt.Errorf("xmlrpc encode error: unsupported type")
+	}
+
+	if err != nil {
+		return nil, err
+	}
+
+	return []byte(fmt.Sprintf("<value>%s</value>", string(b))), nil
+}
+
+func encodeStruct(structVal reflect.Value) ([]byte, error) {
+	var b bytes.Buffer
+
+	b.WriteString("<struct>")
+
+	structType := structVal.Type()
+	for i := 0; i < structType.NumField(); i++ {
+		fieldVal := structVal.Field(i)
+		fieldType := structType.Field(i)
+
+		name := fieldType.Tag.Get("xmlrpc")
+		// if the tag has the omitempty property, skip it
+		if strings.HasSuffix(name, ",omitempty") && isZero(fieldVal) {
+			continue
+		}
+		name = strings.TrimSuffix(name, ",omitempty")
+		if name == "" {
+			name = fieldType.Name
+		}
+
+		p, err := encodeValue(fieldVal)
+		if err != nil {
+			return nil, err
+		}
+
+		b.WriteString("<member>")
+		b.WriteString(fmt.Sprintf("<name>%s</name>", name))
+		b.Write(p)
+		b.WriteString("</member>")
+	}
+
+	b.WriteString("</struct>")
+
+	return b.Bytes(), nil
+}
+
+var sortMapKeys bool
+
+func encodeMap(val reflect.Value) ([]byte, error) {
+	var t = val.Type()
+
+	if t.Key().Kind() != reflect.String {
+		return nil, fmt.Errorf("xmlrpc encode error: only maps with string keys are supported")
+	}
+
+	var b bytes.Buffer
+
+	b.WriteString("<struct>")
+
+	keys := val.MapKeys()
+
+	if sortMapKeys {
+		sort.Slice(keys, func(i, j int) bool { return keys[i].String() < keys[j].String() })
+	}
+
+	for i := 0; i < val.Len(); i++ {
+		key := keys[i]
+		kval := val.MapIndex(key)
+
+		b.WriteString("<member>")
+		b.WriteString(fmt.Sprintf("<name>%s</name>", key.String()))
+
+		p, err := encodeValue(kval)
+
+		if err != nil {
+			return nil, err
+		}
+
+		b.Write(p)
+		b.WriteString("</member>")
+	}
+
+	b.WriteString("</struct>")
+
+	return b.Bytes(), nil
+}
+
+func encodeSlice(val reflect.Value) ([]byte, error) {
+	var b bytes.Buffer
+
+	b.WriteString("<array><data>")
+
+	for i := 0; i < val.Len(); i++ {
+		p, err := encodeValue(val.Index(i))
+		if err != nil {
+			return nil, err
+		}
+
+		b.Write(p)
+	}
+
+	b.WriteString("</data></array>")
+
+	return b.Bytes(), nil
+}
diff --git a/vendor/github.com/kolo/xmlrpc/go.mod b/vendor/github.com/kolo/xmlrpc/go.mod
new file mode 100644
index 000000000..42e7acd80
--- /dev/null
+++ b/vendor/github.com/kolo/xmlrpc/go.mod
@@ -0,0 +1,5 @@
+module github.com/kolo/xmlrpc
+
+go 1.14
+
+require golang.org/x/text v0.3.3
diff --git a/vendor/github.com/kolo/xmlrpc/go.sum b/vendor/github.com/kolo/xmlrpc/go.sum
new file mode 100644
index 000000000..fd5b10f79
--- /dev/null
+++ b/vendor/github.com/kolo/xmlrpc/go.sum
@@ -0,0 +1,3 @@
+golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/vendor/github.com/kolo/xmlrpc/is_zero.go b/vendor/github.com/kolo/xmlrpc/is_zero.go
new file mode 100644
index 000000000..65276d04a
--- /dev/null
+++ b/vendor/github.com/kolo/xmlrpc/is_zero.go
@@ -0,0 +1,44 @@
+package xmlrpc
+
+import (
+	"math"
+	. "reflect"
+)
+
+func isZero(v Value) bool {
+	switch v.Kind() {
+	case Bool:
+		return !v.Bool()
+	case Int, Int8, Int16, Int32, Int64:
+		return v.Int() == 0
+	case Uint, Uint8, Uint16, Uint32, Uint64, Uintptr:
+		return v.Uint() == 0
+	case Float32, Float64:
+		return math.Float64bits(v.Float()) == 0
+	case Complex64, Complex128:
+		c := v.Complex()
+		return math.Float64bits(real(c)) == 0 && math.Float64bits(imag(c)) == 0
+	case Array:
+		for i := 0; i < v.Len(); i++ {
+			if !isZero(v.Index(i)) {
+				return false
+			}
+		}
+		return true
+	case Chan, Func, Interface, Map, Ptr, Slice, UnsafePointer:
+		return v.IsNil()
+	case String:
+		return v.Len() == 0
+	case Struct:
+		for i := 0; i < v.NumField(); i++ {
+			if !isZero(v.Field(i)) {
+				return false
+			}
+		}
+		return true
+	default:
+		// This should never happens, but will act as a safeguard for
+		// later, as a default value doesn't makes sense here.
+		panic(&ValueError{"reflect.Value.IsZero", v.Kind()})
+	}
+}
diff --git a/vendor/github.com/kolo/xmlrpc/request.go b/vendor/github.com/kolo/xmlrpc/request.go
new file mode 100644
index 000000000..acb8251b2
--- /dev/null
+++ b/vendor/github.com/kolo/xmlrpc/request.go
@@ -0,0 +1,57 @@
+package xmlrpc
+
+import (
+	"bytes"
+	"fmt"
+	"net/http"
+)
+
+func NewRequest(url string, method string, args interface{}) (*http.Request, error) {
+	var t []interface{}
+	var ok bool
+	if t, ok = args.([]interface{}); !ok {
+		if args != nil {
+			t = []interface{}{args}
+		}
+	}
+
+	body, err := EncodeMethodCall(method, t...)
+	if err != nil {
+		return nil, err
+	}
+
+	request, err := http.NewRequest("POST", url, bytes.NewReader(body))
+	if err != nil {
+		return nil, err
+	}
+
+	request.Header.Set("Content-Type", "text/xml")
+	request.Header.Set("Content-Length", fmt.Sprintf("%d", len(body)))
+
+	return request, nil
+}
+
+func EncodeMethodCall(method string, args ...interface{}) ([]byte, error) {
+	var b bytes.Buffer
+	b.WriteString(`<?xml version="1.0" encoding="UTF-8"?>`)
+	b.WriteString(fmt.Sprintf("<methodCall><methodName>%s</methodName>", method))
+
+	if args != nil {
+		b.WriteString("<params>")
+
+		for _, arg := range args {
+			p, err := marshal(arg)
+			if err != nil {
+				return nil, err
+			}
+
+			b.WriteString(fmt.Sprintf("<param>%s</param>", string(p)))
+		}
+
+		b.WriteString("</params>")
+	}
+
+	b.WriteString("</methodCall>")
+
+	return b.Bytes(), nil
+}
diff --git a/vendor/github.com/kolo/xmlrpc/response.go b/vendor/github.com/kolo/xmlrpc/response.go
new file mode 100644
index 000000000..18e6d366c
--- /dev/null
+++ b/vendor/github.com/kolo/xmlrpc/response.go
@@ -0,0 +1,42 @@
+package xmlrpc
+
+import (
+	"fmt"
+	"regexp"
+)
+
+var (
+	faultRx = regexp.MustCompile(`<fault>(\s|\S)+</fault>`)
+)
+
+// FaultError is returned from the server when an invalid call is made
+type FaultError struct {
+	Code   int    `xmlrpc:"faultCode"`
+	String string `xmlrpc:"faultString"`
+}
+
+// Error implements the error interface
+func (e FaultError) Error() string {
+	return fmt.Sprintf("Fault(%d): %s", e.Code, e.String)
+}
+
+type Response []byte
+
+func (r Response) Err() error {
+	if !faultRx.Match(r) {
+		return nil
+	}
+	var fault FaultError
+	if err := unmarshal(r, &fault); err != nil {
+		return err
+	}
+	return fault
+}
+
+func (r Response) Unmarshal(v interface{}) error {
+	if err := unmarshal(r, v); err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/vendor/github.com/kolo/xmlrpc/test_server.rb b/vendor/github.com/kolo/xmlrpc/test_server.rb
new file mode 100644
index 000000000..1ccfc9ac4
--- /dev/null
+++ b/vendor/github.com/kolo/xmlrpc/test_server.rb
@@ -0,0 +1,25 @@
+# encoding: utf-8
+
+require "xmlrpc/server"
+
+class Service
+  def time
+    Time.now
+  end
+
+  def upcase(s)
+    s.upcase
+  end
+
+  def sum(x, y)
+    x + y
+  end
+
+  def error
+    raise XMLRPC::FaultException.new(500, "Server error")
+  end
+end
+
+server = XMLRPC::Server.new 5001, 'localhost'
+server.add_handler "service", Service.new
+server.serve
diff --git a/vendor/modules.txt b/vendor/modules.txt
index a2365b4c8..7f30086c3 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -312,6 +312,9 @@ github.com/jpillora/backoff
 github.com/json-iterator/go
 # github.com/julienschmidt/httprouter v1.3.0
 github.com/julienschmidt/httprouter
+# github.com/kolo/xmlrpc v0.0.0-20201022064351-38db28db192b
+## explicit
+github.com/kolo/xmlrpc
 # github.com/konsorten/go-windows-terminal-sequences v1.0.3
 github.com/konsorten/go-windows-terminal-sequences
 # github.com/mailru/easyjson v0.7.1
-- 
2.29.2

openSUSE Build Service is sponsored by