golang-github-prometheus-pr.../0003-Add-Uyuni-service-discovery.patch
Lars Vogdt f87217a1a3 Accepting request 809049 from home:jcavalheiro:monitoring
- Update to 2.18.0 
  + Features 
    * Tracing: Added experimental Jaeger support #7148
  + Changes
    * Federation: Only use local TSDB for federation (ignore remote read). #7096
    * Rules: `rule_evaluations_total` and `rule_evaluation_failures_total` have a `rule_group` label now. #7094
  + Enhancements
    * TSDB: Significantly reduce WAL size kept around after a block cut. #7098
    * Discovery: Add `architecture` meta label for EC2. #7000
  + Bug fixes
    * UI: Fixed wrong MinTime reported by /status. #7182
    * React UI: Fixed multiselect legend on OSX. #6880
    * Remote Write: Fixed blocked resharding edge case. #7122
    * Remote Write: Fixed remote write not updating on relabel configs change. #7073
- Changes from 2.17.2
  + Bug fixes
    * Federation: Register federation metrics #7081
    * PromQL: Fix panic in parser error handling #7132
    * Rules: Fix reloads hanging when deleting a rule group that is being evaluated #7138
    * TSDB: Fix a memory leak when prometheus starts with an empty TSDB WAL #7135
    * TSDB: Make isolation more robust to panics in web handlers #7129 #7136
- Changes from 2.17.1
  + Bug fixes
    * TSDB: Fix query performance regression that increased memory and CPU usage #7051
- Changes from 2.17.0
  + Features 
    * TSDB: Support isolation #6841
    * This release implements isolation in TSDB. API queries and recording rules are
      guaranteed to only see full scrapes and full recording rules. This comes with a
      certain overhead in resource usage. Depending on the situation, there might be
      some increase in memory usage, CPU usage, or query latency.
  + Enhancements
    * PromQL: Allow more keywords as metric names #6933
    * React UI: Add normalization of localhost URLs in targets page #6794
    * Remote read: Read from remote storage concurrently #6770
    * Rules: Mark deleted rule series as stale after a reload #6745
    * Scrape: Log scrape append failures as debug rather than warn #6852
    * TSDB: Improve query performance for queries that partially hit the head #6676
    * Consul SD: Expose service health as meta label #5313
    * EC2 SD: Expose EC2 instance lifecycle as meta label #6914
    * Kubernetes SD: Expose service type as meta label for K8s service role #6684
    * Kubernetes SD: Expose label_selector and field_selector #6807
    * Openstack SD: Expose hypervisor id as meta label #6962
  + Bug fixes
    * PromQL: Do not escape HTML-like chars in query log #6834 #6795
    * React UI: Fix data table matrix values #6896
    * React UI: Fix new targets page not loading when using non-ASCII characters #6892
    * Remote read: Fix duplication of metrics read from remote storage with external labels #6967 #7018
    * Remote write: Register WAL watcher and live reader metrics for all remotes, not just the first one #6998
    * Scrape: Prevent removal of metric names upon relabeling #6891
    * Scrape: Fix 'superfluous response.WriteHeader call' errors when scrape fails under some circonstances #6986
    * Scrape: Fix crash when reloads are separated by two scrape intervals #7011
- Changes from 2.16.0
  + Features 
    * React UI: Support local timezone on /graph #6692
    * PromQL: add absent_over_time query function #6490
    * Adding optional logging of queries to their own file #6520
  + Enhancements
    * React UI: Add support for rules page and "Xs ago" duration displays #6503
    * React UI: alerts page, replace filtering togglers tabs with checkboxes #6543
    * TSDB: Export metric for WAL write errors #6647
    * TSDB: Improve query performance for queries that only touch the most recent 2h of data. #6651
    * PromQL: Refactoring in parser errors to improve error messages #6634
    * PromQL: Support trailing commas in grouping opts #6480
    * Scrape: Reduce memory usage on reloads by reusing scrape cache #6670
    * Scrape: Add metrics to track bytes and entries in the metadata cache #6675
    * promtool: Add support for line-column numbers for invalid rules output #6533
    * Avoid restarting rule groups when it is unnecessary #6450
  + Bug fixes
    * React UI: Send cookies on fetch() on older browsers #6553
    * React UI: adopt grafana flot fix for stacked graphs #6603
    * React UI: broken graph page browser history so that back button works as expected #6659
    * TSDB: ensure compactionsSkipped metric is registered, and log proper error if one is returned from head.Init #6616
    * TSDB: return an error on ingesting series with duplicate labels #6664
    * PromQL: Fix unary operator precedence #6579
    * PromQL: Respect query.timeout even when we reach query.max-concurrency #6712
    * PromQL: Fix string and parentheses handling in engine, which affected React UI #6612
    * PromQL: Remove output labels returned by absent() if they are produced by multiple identical label matchers #6493
    * Scrape: Validate that OpenMetrics input ends with `# EOF` #6505
    * Remote read: return the correct error if configs can't be marshal'd to JSON #6622
    * Remote write: Make remote client `Store` use passed context, which can affect shutdown timing #6673
    * Remote write: Improve sharding calculation in cases where we would always be consistently behind by tracking pendingSamples #6511
    * Ensure prometheus_rule_group metrics are deleted when a rule group is removed #6693
- Changes from 2.15.2
  + Bug fixes
    * TSDB: Fixed support for TSDB blocks built with Prometheus before 2.1.0. #6564
    * TSDB: Fixed block compaction issues on Windows. #6547
- Changes from 2.15.1
  + Bug fixes
    * TSDB: Fixed race on concurrent queries against same data. #6512
- Changes from 2.15.0
  + Features 
    * API: Added new endpoint for exposing per metric metadata `/metadata`. #6420 #6442
  + Changes
    * Discovery: Removed `prometheus_sd_kubernetes_cache_*` metrics. Additionally `prometheus_sd_kubernetes_workqueue_latency_seconds` and `prometheus_sd_kubernetes_workqueue_work_duration_seconds` metrics now show correct values in seconds. #6393
    * Remote write: Changed `query` label on `prometheus_remote_storage_*` metrics to `remote_name` and `url`. #6043
  + Enhancements
    * TSDB: Significantly reduced memory footprint of loaded TSDB blocks. #6418 #6461
    * TSDB: Significantly optimized what we buffer during compaction which should result in lower memory footprint during compaction. #6422 #6452 #6468 #6475
    * TSDB: Improve replay latency. #6230
    * TSDB: WAL size is now used for size based retention calculation. #5886
    * Remote read: Added query grouping and range hints to the remote read request #6401
    * Remote write: Added `prometheus_remote_storage_sent_bytes_total` counter per queue. #6344
    * promql: Improved PromQL parser performance. #6356
    * React UI: Implemented missing pages like `/targets` #6276, TSDB status page #6281 #6267 and many other fixes and performance improvements.
    * promql: Prometheus now accepts spaces between time range and square bracket. e.g `[ 5m]` #6065  
  + Bug fixes
    * Config: Fixed alertmanager configuration to not miss targets when configurations are similar. #6455
    * Remote write: Value of `prometheus_remote_storage_shards_desired` gauge shows raw value of desired shards and it's updated correctly. #6378
    * Rules: Prometheus now fails the evaluation of rules and alerts where metric results collide with labels specified in `labels` field. #6469
    * API: Targets Metadata API `/targets/metadata` now accepts empty `match_targets` parameter as in the spec. #6303
- Changes from 2.14.0
  + Features 
    * API: `/api/v1/status/runtimeinfo` and `/api/v1/status/buildinfo` endpoints added for use by the React UI. #6243
    * React UI: implement the new experimental React based UI. #5694 and many more
      * Can be found by under `/new`.
      * Not all pages are implemented yet.
    * Status: Cardinality statistics added to the Runtime & Build Information page. #6125
  + Enhancements
    * Remote write: fix delays in remote write after a compaction. #6021
    * UI: Alerts can be filtered by state. #5758
  + Bug fixes
    * Ensure warnings from the API are escaped. #6279
    * API: lifecycle endpoints return 403 when not enabled. #6057
    * Build: Fix Solaris build. #6149
    * Promtool: Remove false duplicate rule warnings when checking rule files with alerts. #6270
    * Remote write: restore use of deduplicating logger in remote write. #6113
    * Remote write: do not reshard when unable to send samples. #6111
    * Service discovery: errors are no longer logged on context cancellation. #6116, #6133
    * UI: handle null response from API properly. #6071
- Changes from 2.13.1
  + Bug fixes
    * Fix panic in ARM builds of Prometheus. #6110
    * promql: fix potential panic in the query logger. #6094
    * Multiple errors of http: superfluous response.WriteHeader call in the logs. #6145
- Changes from 2.13.0
  + Enhancements
    * Metrics: renamed prometheus_sd_configs_failed_total to prometheus_sd_failed_configs and changed to Gauge #5254
    * Include the tsdb tool in builds. #6089
    * Service discovery: add new node address types for kubernetes. #5902
    * UI: show warnings if query have returned some warnings. #5964
    * Remote write: reduce memory usage of the series cache. #5849
    * Remote read: use remote read streaming to reduce memory usage. #5703
    * Metrics: added metrics for remote write max/min/desired shards to queue manager. #5787
    * Promtool: show the warnings during label query. #5924
    * Promtool: improve error messages when parsing bad rules. #5965
    * Promtool: more promlint rules. #5515
  + Bug fixes
    * UI: Fix a Stored DOM XSS vulnerability with query history [CVE-2019-10215](http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-10215). #6098
    * Promtool: fix recording inconsistency due to duplicate labels. #6026
    * UI: fixes service-discovery view when accessed from unhealthy targets. #5915
    * Metrics format: OpenMetrics parser crashes on short input. #5939
    * UI: avoid truncated Y-axis values. #6014
- Changes from 2.12.0
  + Features 
    * Track currently active PromQL queries in a log file. #5794
    * Enable and provide binaries for `mips64` / `mips64le` architectures. #5792
  + Enhancements
    * Improve responsiveness of targets web UI and API endpoint. #5740
    * Improve remote write desired shards calculation. #5763
    * Flush TSDB pages more precisely. tsdb#660
    * Add `prometheus_tsdb_retention_limit_bytes` metric. tsdb#667
    * Add logging during TSDB WAL replay on startup. tsdb#662
    * Improve TSDB memory usage. tsdb#653, tsdb#643, tsdb#654, tsdb#642, tsdb#627
  + Bug fixes
    * Check for duplicate label names in remote read. #5829
    * Mark deleted rules' series as stale on next evaluation. #5759
    * Fix JavaScript error when showing warning about out-of-sync server time. #5833
    * Fix `promtool test rules` panic when providing empty `exp_labels`. #5774
    * Only check last directory when discovering checkpoint number. #5756
    * Fix error propagation in WAL watcher helper functions. #5741
    * Correctly handle empty labels from alert templates. #5845
- Update Uyuni/SUSE Manager service discovery patch
  + Modified 0003-Add-Uyuni-service-discovery.patch:
  + Adapt service discovery to the new Uyuni API endpoints
  + Modified spec file: force golang 1.12 to fix build issues in SLE15SP2
- Update to Prometheus 2.11.2

OBS-URL: https://build.opensuse.org/request/show/809049
OBS-URL: https://build.opensuse.org/package/show/server:monitoring/golang-github-prometheus-prometheus?expand=0&rev=30
2020-06-02 20:45:18 +00:00

2058 lines
55 KiB
Diff
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

diff --git a/discovery/config/config.go b/discovery/config/config.go
index 820de1f..27d8c0c 100644
--- a/discovery/config/config.go
+++ b/discovery/config/config.go
@@ -27,6 +27,7 @@ import (
"github.com/prometheus/prometheus/discovery/openstack"
"github.com/prometheus/prometheus/discovery/targetgroup"
"github.com/prometheus/prometheus/discovery/triton"
+ "github.com/prometheus/prometheus/discovery/uyuni"
"github.com/prometheus/prometheus/discovery/zookeeper"
)
@@ -58,6 +59,8 @@ type ServiceDiscoveryConfig struct {
AzureSDConfigs []*azure.SDConfig `yaml:"azure_sd_configs,omitempty"`
// List of Triton service discovery configurations.
TritonSDConfigs []*triton.SDConfig `yaml:"triton_sd_configs,omitempty"`
+ // List of Uyuni service discovery configurations.
+ UyuniSDConfigs []*uyuni.SDConfig `yaml:"uyuni_sd_configs,omitempty"`
}
// Validate validates the ServiceDiscoveryConfig.
diff --git a/discovery/manager.go b/discovery/manager.go
index 66c0057..f65cd04 100644
--- a/discovery/manager.go
+++ b/discovery/manager.go
@@ -37,6 +37,7 @@ import (
"github.com/prometheus/prometheus/discovery/marathon"
"github.com/prometheus/prometheus/discovery/openstack"
"github.com/prometheus/prometheus/discovery/triton"
+ "github.com/prometheus/prometheus/discovery/uyuni"
"github.com/prometheus/prometheus/discovery/zookeeper"
)
@@ -414,6 +415,11 @@ func (m *Manager) registerProviders(cfg sd_config.ServiceDiscoveryConfig, setNam
return triton.New(log.With(m.logger, "discovery", "triton"), c)
})
}
+ for _, c := range cfg.UyuniSDConfigs {
+ add(c, func() (Discoverer, error) {
+ return uyuni.NewDiscovery(c, log.With(m.logger, "discovery", "uyuni")), nil
+ })
+ }
if len(cfg.StaticConfigs) > 0 {
add(setName, func() (Discoverer, error) {
return &StaticProvider{TargetGroups: cfg.StaticConfigs}, nil
diff --git a/discovery/uyuni/uyuni.go b/discovery/uyuni/uyuni.go
new file mode 100644
index 00000000..18e0cfce
--- /dev/null
+++ b/discovery/uyuni/uyuni.go
@@ -0,0 +1,298 @@
+// 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/http"
+ "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/refresh"
+ "github.com/prometheus/prometheus/discovery/targetgroup"
+)
+
+const (
+ uyuniLabel = model.MetaLabelPrefix + "uyuni_"
+ uyuniLabelEntitlements = uyuniLabel + "entitlements"
+ 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]*)\"`)
+
+// 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 {
+ Args string `xmlrpc:"args"`
+ Enabled bool `xmlrpc:"enabled"`
+}
+
+// Discovery periodically performs Uyuni API requests. It implements the Discoverer interface.
+type Discovery struct {
+ *refresh.Discovery
+ client *http.Client
+ interval time.Duration
+ sdConfig *SDConfig
+ logger log.Logger
+}
+
+// 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]map[string]exporterConfig, error) {
+ var combinedFormulaDatas []struct {
+ SystemID int `xmlrpc:"system_id"`
+ ExporterConfigs map[string]exporterConfig `xmlrpc:"formula_values"`
+ }
+ err := rpcclient.Call("formula.getCombinedFormulaDataByServerIds", []interface{}{token, prometheusExporterFormulaName, systemIDs}, &combinedFormulaDatas)
+ if err != nil {
+ return nil, err
+ }
+ result := make(map[int]map[string]exporterConfig)
+ for _, combinedFormulaData := range combinedFormulaDatas {
+ result[combinedFormulaData.SystemID] = combinedFormulaData.ExporterConfigs
+ }
+ return result, nil
+}
+
+// Get exporter port configuration from Formula
+func extractPortFromFormulaData(args string) (string, error) {
+ tokens := monFormulaRegex.FindStringSubmatch(args)
+ if len(tokens) < 1 {
+ return "", errors.New("Unable to find port in args: " + args)
+ }
+ return tokens[1], 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 (d *Discovery) getTargetsForSystem(systemID int, systemGroupsIDs []systemGroupID, networkInfo networkInfo, combinedFormulaData map[string]exporterConfig) []model.LabelSet {
+ labelSets := make([]model.LabelSet, 0)
+ for exporter, exporterConfig := range combinedFormulaData {
+ if exporterConfig.Enabled {
+ port, err := extractPortFromFormulaData(exporterConfig.Args)
+ if err == nil {
+ targets := model.LabelSet{}
+ addr := fmt.Sprintf("%s:%s", networkInfo.IP, port)
+ targets[model.AddressLabel] = model.LabelValue(addr)
+ targets["exporter"] = model.LabelValue(exporter)
+ targets["hostname"] = model.LabelValue(networkInfo.Hostname)
+
+ managedGroupNames := make([]string, 0, len(systemGroupsIDs))
+ for _, systemGroupInfo := range systemGroupsIDs {
+ managedGroupNames = append(managedGroupNames, systemGroupInfo.GroupName)
+ }
+
+ if len(managedGroupNames) == 0 {
+ managedGroupNames = []string{"No group"}
+ }
+
+ targets["groups"] = model.LabelValue(strings.Join(managedGroupNames, ","))
+ labelSets = append(labelSets, targets)
+
+ } else {
+ level.Error(d.logger).Log("msg", "Invalid exporter port", "clientId", systemID, "err", err)
+ }
+ }
+ }
+ return labelSets
+}
+
+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")
+ }
+
+ logout(rpcClient, token)
+ rpcClient.Close()
+ return []*targetgroup.Group{&targetgroup.Group{Targets: targets, Source: config.Host}}, nil
+}
diff --git a/go.mod b/go.mod
index 0b5a585..5a95ffb 100644
--- a/go.mod
+++ b/go.mod
@@ -41,6 +41,7 @@ require (
github.com/jpillora/backoff v1.0.0 // indirect
github.com/json-iterator/go v1.1.9
github.com/julienschmidt/httprouter v1.3.0 // indirect
+ github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b
github.com/mattn/go-colorable v0.1.6 // indirect
github.com/miekg/dns v1.1.29
github.com/mitchellh/mapstructure v1.2.2 // indirect
diff --git a/go.sum b/go.sum
index 7941bbe..9f31b87 100644
--- a/go.sum
+++ b/go.sum
@@ -505,6 +505,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-20200310150728-e0350524596b h1:DzHy0GlWeF0KAglaTMY7Q+khIFoG8toHP+wLFBVBQJc=
+github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ=
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/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
diff --git a/vendor/github.com/kolo/xmlrpc/LICENSE b/vendor/github.com/kolo/xmlrpc/LICENSE
new file mode 100644
index 00000000..8103dd13
--- /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 00000000..8113cfcc
--- /dev/null
+++ b/vendor/github.com/kolo/xmlrpc/README.md
@@ -0,0 +1,89 @@
+[![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 decoded 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.
+
+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 00000000..3aa86ce2
--- /dev/null
+++ b/vendor/github.com/kolo/xmlrpc/client.go
@@ -0,0 +1,170 @@
+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 codec.cookies != nil {
+ for _, cookie := range codec.cookies.Cookies(codec.url) {
+ httpRequest.AddCookie(cookie)
+ }
+ }
+
+ if err != nil {
+ return err
+ }
+
+ 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")
+ }
+
+ codec.mutex.Lock()
+ httpResponse := codec.responses[seq]
+ codec.mutex.Unlock()
+
+ if httpResponse.StatusCode < 200 || httpResponse.StatusCode >= 300 {
+ return fmt.Errorf("request error: bad status code - %d", httpResponse.StatusCode)
+ }
+
+ respData, err := ioutil.ReadAll(httpResponse.Body)
+
+ if err != nil {
+ return err
+ }
+
+ httpResponse.Body.Close()
+
+ resp := NewResponse(respData)
+
+ if resp.Failed() {
+ response.Error = fmt.Sprintf("%v", resp.Err())
+ }
+
+ codec.response = resp
+
+ response.Seq = seq
+
+ codec.mutex.Lock()
+ delete(codec.responses, seq)
+ codec.mutex.Unlock()
+
+ return nil
+}
+
+func (codec *clientCodec) ReadResponseBody(v interface{}) (err error) {
+ if v == nil {
+ return nil
+ }
+
+ if err = codec.response.Unmarshal(v); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+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/client_test.go b/vendor/github.com/kolo/xmlrpc/client_test.go
new file mode 100644
index 00000000..b429d4f8
--- /dev/null
+++ b/vendor/github.com/kolo/xmlrpc/client_test.go
@@ -0,0 +1,141 @@
+// +build integration
+
+package xmlrpc
+
+import (
+ "context"
+ "runtime"
+ "sync"
+ "testing"
+ "time"
+)
+
+func Test_CallWithoutArgs(t *testing.T) {
+ client := newClient(t)
+ defer client.Close()
+
+ var result time.Time
+ if err := client.Call("service.time", nil, &result); err != nil {
+ t.Fatalf("service.time call error: %v", err)
+ }
+}
+
+func Test_CallWithOneArg(t *testing.T) {
+ client := newClient(t)
+ defer client.Close()
+
+ var result string
+ if err := client.Call("service.upcase", "xmlrpc", &result); err != nil {
+ t.Fatalf("service.upcase call error: %v", err)
+ }
+
+ if result != "XMLRPC" {
+ t.Fatalf("Unexpected result of service.upcase: %s != %s", "XMLRPC", result)
+ }
+}
+
+func Test_CallWithTwoArgs(t *testing.T) {
+ client := newClient(t)
+ defer client.Close()
+
+ var sum int
+ if err := client.Call("service.sum", []interface{}{2, 3}, &sum); err != nil {
+ t.Fatalf("service.sum call error: %v", err)
+ }
+
+ if sum != 5 {
+ t.Fatalf("Unexpected result of service.sum: %d != %d", 5, sum)
+ }
+}
+
+func Test_TwoCalls(t *testing.T) {
+ client := newClient(t)
+ defer client.Close()
+
+ var upcase string
+ if err := client.Call("service.upcase", "xmlrpc", &upcase); err != nil {
+ t.Fatalf("service.upcase call error: %v", err)
+ }
+
+ var sum int
+ if err := client.Call("service.sum", []interface{}{2, 3}, &sum); err != nil {
+ t.Fatalf("service.sum call error: %v", err)
+ }
+
+}
+
+func Test_FailedCall(t *testing.T) {
+ client := newClient(t)
+ defer client.Close()
+
+ var result int
+ if err := client.Call("service.error", nil, &result); err == nil {
+ t.Fatal("expected service.error returns error, but it didn't")
+ }
+}
+
+func Test_ConcurrentCalls(t *testing.T) {
+ client := newClient(t)
+
+ call := func() {
+ var result time.Time
+ client.Call("service.time", nil, &result)
+ }
+
+ var wg sync.WaitGroup
+ for i := 0; i < 100; i++ {
+ wg.Add(1)
+ go func() {
+ call()
+ wg.Done()
+ }()
+ }
+
+ wg.Wait()
+ client.Close()
+}
+
+func Test_CloseMemoryLeak(t *testing.T) {
+ expected := runtime.NumGoroutine()
+
+ for i := 0; i < 3; i++ {
+ client := newClient(t)
+ client.Call("service.time", nil, nil)
+ client.Close()
+ }
+
+ var actual int
+
+ // It takes some time to stop running goroutinges. This function checks number of
+ // running goroutines. It finishes execution if number is same as expected or timeout
+ // has been reached.
+ func() {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ default:
+ actual = runtime.NumGoroutine()
+ if actual == expected {
+ return
+ }
+ }
+ }
+ }()
+
+ if actual != expected {
+ t.Errorf("expected number of running goroutines to be %d, but got %d", expected, actual)
+ }
+}
+
+func newClient(t *testing.T) *Client {
+ client, err := NewClient("http://localhost:5001", nil)
+ if err != nil {
+ t.Fatalf("Can't create client: %v", err)
+ }
+
+ return client
+}
diff --git a/vendor/github.com/kolo/xmlrpc/decoder.go b/vendor/github.com/kolo/xmlrpc/decoder.go
new file mode 100644
index 00000000..d4dcb19a
--- /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/decoder_test.go b/vendor/github.com/kolo/xmlrpc/decoder_test.go
new file mode 100644
index 00000000..3701d50a
--- /dev/null
+++ b/vendor/github.com/kolo/xmlrpc/decoder_test.go
@@ -0,0 +1,234 @@
+package xmlrpc
+
+import (
+ "fmt"
+ "io"
+ "io/ioutil"
+ "reflect"
+ "testing"
+ "time"
+
+ "golang.org/x/text/encoding/charmap"
+ "golang.org/x/text/transform"
+)
+
+type book struct {
+ Title string
+ Amount int
+}
+
+type bookUnexported struct {
+ title string
+ amount int
+}
+
+var unmarshalTests = []struct {
+ value interface{}
+ ptr interface{}
+ xml string
+}{
+ // int, i4, i8
+ {0, new(*int), "<value><int></int></value>"},
+ {100, new(*int), "<value><int>100</int></value>"},
+ {389451, new(*int), "<value><i4>389451</i4></value>"},
+ {int64(45659074), new(*int64), "<value><i8>45659074</i8></value>"},
+
+ // string
+ {"Once upon a time", new(*string), "<value><string>Once upon a time</string></value>"},
+ {"Mike & Mick <London, UK>", new(*string), "<value><string>Mike &amp; Mick &lt;London, UK&gt;</string></value>"},
+ {"Once upon a time", new(*string), "<value>Once upon a time</value>"},
+
+ // base64
+ {"T25jZSB1cG9uIGEgdGltZQ==", new(*string), "<value><base64>T25jZSB1cG9uIGEgdGltZQ==</base64></value>"},
+
+ // boolean
+ {true, new(*bool), "<value><boolean>1</boolean></value>"},
+ {false, new(*bool), "<value><boolean>0</boolean></value>"},
+
+ // double
+ {12.134, new(*float32), "<value><double>12.134</double></value>"},
+ {-12.134, new(*float32), "<value><double>-12.134</double></value>"},
+
+ // datetime.iso8601
+ {_time("2013-12-09T21:00:12Z"), new(*time.Time), "<value><dateTime.iso8601>20131209T21:00:12</dateTime.iso8601></value>"},
+ {_time("2013-12-09T21:00:12Z"), new(*time.Time), "<value><dateTime.iso8601>20131209T21:00:12Z</dateTime.iso8601></value>"},
+ {_time("2013-12-09T21:00:12-01:00"), new(*time.Time), "<value><dateTime.iso8601>20131209T21:00:12-01:00</dateTime.iso8601></value>"},
+ {_time("2013-12-09T21:00:12+01:00"), new(*time.Time), "<value><dateTime.iso8601>20131209T21:00:12+01:00</dateTime.iso8601></value>"},
+ {_time("2013-12-09T21:00:12Z"), new(*time.Time), "<value><dateTime.iso8601>2013-12-09T21:00:12</dateTime.iso8601></value>"},
+ {_time("2013-12-09T21:00:12Z"), new(*time.Time), "<value><dateTime.iso8601>2013-12-09T21:00:12Z</dateTime.iso8601></value>"},
+ {_time("2013-12-09T21:00:12-01:00"), new(*time.Time), "<value><dateTime.iso8601>2013-12-09T21:00:12-01:00</dateTime.iso8601></value>"},
+ {_time("2013-12-09T21:00:12+01:00"), new(*time.Time), "<value><dateTime.iso8601>2013-12-09T21:00:12+01:00</dateTime.iso8601></value>"},
+
+ // array
+ {[]int{1, 5, 7}, new(*[]int), "<value><array><data><value><int>1</int></value><value><int>5</int></value><value><int>7</int></value></data></array></value>"},
+ {[]interface{}{"A", "5"}, new(interface{}), "<value><array><data><value><string>A</string></value><value><string>5</string></value></data></array></value>"},
+ {[]interface{}{"A", int64(5)}, new(interface{}), "<value><array><data><value><string>A</string></value><value><int>5</int></value></data></array></value>"},
+
+ // struct
+ {book{"War and Piece", 20}, new(*book), "<value><struct><member><name>Title</name><value><string>War and Piece</string></value></member><member><name>Amount</name><value><int>20</int></value></member></struct></value>"},
+ {bookUnexported{}, new(*bookUnexported), "<value><struct><member><name>title</name><value><string>War and Piece</string></value></member><member><name>amount</name><value><int>20</int></value></member></struct></value>"},
+ {map[string]interface{}{"Name": "John Smith"}, new(interface{}), "<value><struct><member><name>Name</name><value><string>John Smith</string></value></member></struct></value>"},
+ {map[string]interface{}{}, new(interface{}), "<value><struct></struct></value>"},
+}
+
+func _time(s string) time.Time {
+ t, err := time.Parse(time.RFC3339, s)
+ if err != nil {
+ panic(fmt.Sprintf("time parsing error: %v", err))
+ }
+ return t
+}
+
+func Test_unmarshal(t *testing.T) {
+ for _, tt := range unmarshalTests {
+ v := reflect.New(reflect.TypeOf(tt.value))
+ if err := unmarshal([]byte(tt.xml), v.Interface()); err != nil {
+ t.Fatalf("unmarshal error: %v", err)
+ }
+
+ v = v.Elem()
+
+ if v.Kind() == reflect.Slice {
+ vv := reflect.ValueOf(tt.value)
+ if vv.Len() != v.Len() {
+ t.Fatalf("unmarshal error:\nexpected: %v\n got: %v", tt.value, v.Interface())
+ }
+ for i := 0; i < v.Len(); i++ {
+ if v.Index(i).Interface() != vv.Index(i).Interface() {
+ t.Fatalf("unmarshal error:\nexpected: %v\n got: %v", tt.value, v.Interface())
+ }
+ }
+ } else {
+ a1 := v.Interface()
+ a2 := interface{}(tt.value)
+
+ if !reflect.DeepEqual(a1, a2) {
+ t.Fatalf("unmarshal error:\nexpected: %v\n got: %v", tt.value, v.Interface())
+ }
+ }
+ }
+}
+
+func Test_unmarshalToNil(t *testing.T) {
+ for _, tt := range unmarshalTests {
+ if err := unmarshal([]byte(tt.xml), tt.ptr); err != nil {
+ t.Fatalf("unmarshal error: %v", err)
+ }
+ }
+}
+
+func Test_typeMismatchError(t *testing.T) {
+ var s string
+
+ encoded := "<value><int>100</int></value>"
+ var err error
+
+ if err = unmarshal([]byte(encoded), &s); err == nil {
+ t.Fatal("unmarshal error: expected error, but didn't get it")
+ }
+
+ if _, ok := err.(TypeMismatchError); !ok {
+ t.Fatal("unmarshal error: expected type mistmatch error, but didn't get it")
+ }
+}
+
+func Test_unmarshalEmptyValueTag(t *testing.T) {
+ var v int
+
+ if err := unmarshal([]byte("<value/>"), &v); err != nil {
+ t.Fatalf("unmarshal error: %v", err)
+ }
+}
+
+const structEmptyXML = `
+<value>
+ <struct>
+ </struct>
+</value>
+`
+
+func Test_unmarshalEmptyStruct(t *testing.T) {
+ var v interface{}
+ if err := unmarshal([]byte(structEmptyXML), &v); err != nil {
+ t.Fatal(err)
+ }
+ if v == nil {
+ t.Fatalf("got nil map")
+ }
+}
+
+const arrayValueXML = `
+<value>
+ <array>
+ <data>
+ <value><int>234</int></value>
+ <value><boolean>1</boolean></value>
+ <value><string>Hello World</string></value>
+ <value><string>Extra Value</string></value>
+ </data>
+ </array>
+</value>
+`
+
+func Test_unmarshalExistingArray(t *testing.T) {
+
+ var (
+ v1 int
+ v2 bool
+ v3 string
+
+ v = []interface{}{&v1, &v2, &v3}
+ )
+ if err := unmarshal([]byte(arrayValueXML), &v); err != nil {
+ t.Fatal(err)
+ }
+
+ // check pre-existing values
+ if want := 234; v1 != want {
+ t.Fatalf("want %d, got %d", want, v1)
+ }
+ if want := true; v2 != want {
+ t.Fatalf("want %t, got %t", want, v2)
+ }
+ if want := "Hello World"; v3 != want {
+ t.Fatalf("want %s, got %s", want, v3)
+ }
+ // check the appended result
+ if n := len(v); n != 4 {
+ t.Fatalf("missing appended result")
+ }
+ if got, ok := v[3].(string); !ok || got != "Extra Value" {
+ t.Fatalf("got %s, want %s", got, "Extra Value")
+ }
+}
+
+func Test_decodeNonUTF8Response(t *testing.T) {
+ data, err := ioutil.ReadFile("fixtures/cp1251.xml")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ CharsetReader = decode
+
+ var s string
+ if err = unmarshal(data, &s); err != nil {
+ fmt.Println(err)
+ t.Fatal("unmarshal error: cannot decode non utf-8 response")
+ }
+
+ expected := "Л.Н. Толстой - Война и Мир"
+
+ if s != expected {
+ t.Fatalf("unmarshal error:\nexpected: %v\n got: %v", expected, s)
+ }
+
+ CharsetReader = nil
+}
+
+func decode(charset string, input io.Reader) (io.Reader, error) {
+ if charset != "cp1251" {
+ return nil, fmt.Errorf("unsupported charset")
+ }
+
+ return transform.NewReader(input, charmap.Windows1251.NewDecoder()), nil
+}
diff --git a/vendor/github.com/kolo/xmlrpc/encoder.go b/vendor/github.com/kolo/xmlrpc/encoder.go
new file mode 100644
index 00000000..d585a7d3
--- /dev/null
+++ b/vendor/github.com/kolo/xmlrpc/encoder.go
@@ -0,0 +1,171 @@
+package xmlrpc
+
+import (
+ "bytes"
+ "encoding/xml"
+ "fmt"
+ "reflect"
+ "sort"
+ "strconv"
+ "time"
+)
+
+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(val reflect.Value) ([]byte, error) {
+ var b bytes.Buffer
+
+ b.WriteString("<struct>")
+
+ t := val.Type()
+ for i := 0; i < t.NumField(); i++ {
+ b.WriteString("<member>")
+ f := t.Field(i)
+
+ name := f.Tag.Get("xmlrpc")
+ if name == "" {
+ name = f.Name
+ }
+ b.WriteString(fmt.Sprintf("<name>%s</name>", name))
+
+ p, err := encodeValue(val.FieldByName(f.Name))
+ if err != nil {
+ return nil, err
+ }
+ 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/encoder_test.go b/vendor/github.com/kolo/xmlrpc/encoder_test.go
new file mode 100644
index 00000000..ca4ac706
--- /dev/null
+++ b/vendor/github.com/kolo/xmlrpc/encoder_test.go
@@ -0,0 +1,58 @@
+package xmlrpc
+
+import (
+ "testing"
+ "time"
+)
+
+var marshalTests = []struct {
+ value interface{}
+ xml string
+}{
+ {100, "<value><int>100</int></value>"},
+ {"Once upon a time", "<value><string>Once upon a time</string></value>"},
+ {"Mike & Mick <London, UK>", "<value><string>Mike &amp; Mick &lt;London, UK&gt;</string></value>"},
+ {Base64("T25jZSB1cG9uIGEgdGltZQ=="), "<value><base64>T25jZSB1cG9uIGEgdGltZQ==</base64></value>"},
+ {true, "<value><boolean>1</boolean></value>"},
+ {false, "<value><boolean>0</boolean></value>"},
+ {12.134, "<value><double>12.134</double></value>"},
+ {-12.134, "<value><double>-12.134</double></value>"},
+ {738777323.0, "<value><double>738777323</double></value>"},
+ {time.Unix(1386622812, 0).UTC(), "<value><dateTime.iso8601>20131209T21:00:12</dateTime.iso8601></value>"},
+ {[]interface{}{1, "one"}, "<value><array><data><value><int>1</int></value><value><string>one</string></value></data></array></value>"},
+ {&struct {
+ Title string
+ Amount int
+ }{"War and Piece", 20}, "<value><struct><member><name>Title</name><value><string>War and Piece</string></value></member><member><name>Amount</name><value><int>20</int></value></member></struct></value>"},
+ {&struct {
+ Value interface{} `xmlrpc:"value"`
+ }{}, "<value><struct><member><name>value</name><value/></member></struct></value>"},
+ {
+ map[string]interface{}{"title": "War and Piece", "amount": 20},
+ "<value><struct><member><name>amount</name><value><int>20</int></value></member><member><name>title</name><value><string>War and Piece</string></value></member></struct></value>",
+ },
+ {
+ map[string]interface{}{
+ "Name": "John Smith",
+ "Age": 6,
+ "Wight": []float32{66.67, 100.5},
+ "Dates": map[string]interface{}{"Birth": time.Date(1829, time.November, 10, 23, 0, 0, 0, time.UTC), "Death": time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)}},
+ "<value><struct><member><name>Age</name><value><int>6</int></value></member><member><name>Dates</name><value><struct><member><name>Birth</name><value><dateTime.iso8601>18291110T23:00:00</dateTime.iso8601></value></member><member><name>Death</name><value><dateTime.iso8601>20091110T23:00:00</dateTime.iso8601></value></member></struct></value></member><member><name>Name</name><value><string>John Smith</string></value></member><member><name>Wight</name><value><array><data><value><double>66.67</double></value><value><double>100.5</double></value></data></array></value></member></struct></value>",
+ },
+}
+
+func Test_marshal(t *testing.T) {
+ sortMapKeys = true
+
+ for _, tt := range marshalTests {
+ b, err := marshal(tt.value)
+ if err != nil {
+ t.Fatalf("unexpected marshal error: %v", err)
+ }
+
+ if string(b) != tt.xml {
+ t.Fatalf("marshal error:\nexpected: %s\n got: %s", tt.xml, string(b))
+ }
+
+ }
+}
diff --git a/vendor/github.com/kolo/xmlrpc/fixtures/cp1251.xml b/vendor/github.com/kolo/xmlrpc/fixtures/cp1251.xml
new file mode 100644
index 00000000..1d5e9bfc
--- /dev/null
+++ b/vendor/github.com/kolo/xmlrpc/fixtures/cp1251.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="cp1251" ?>
+<methodResponse>
+ <params>
+ <param><value><string><3E>.<2E>. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> - <20><><EFBFBD><EFBFBD><EFBFBD> <20> <20><><EFBFBD></string></value></param>
+ </params>
+</methodResponse>
\ No newline at end of file
diff --git a/vendor/github.com/kolo/xmlrpc/request.go b/vendor/github.com/kolo/xmlrpc/request.go
new file mode 100644
index 00000000..acb8251b
--- /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 00000000..6742a1c7
--- /dev/null
+++ b/vendor/github.com/kolo/xmlrpc/response.go
@@ -0,0 +1,52 @@
+package xmlrpc
+
+import (
+ "regexp"
+)
+
+var (
+ faultRx = regexp.MustCompile(`<fault>(\s|\S)+</fault>`)
+)
+
+type failedResponse struct {
+ Code int `xmlrpc:"faultCode"`
+ Error string `xmlrpc:"faultString"`
+}
+
+func (r *failedResponse) err() error {
+ return &xmlrpcError{
+ code: r.Code,
+ err: r.Error,
+ }
+}
+
+type Response struct {
+ data []byte
+}
+
+func NewResponse(data []byte) *Response {
+ return &Response{
+ data: data,
+ }
+}
+
+func (r *Response) Failed() bool {
+ return faultRx.Match(r.data)
+}
+
+func (r *Response) Err() error {
+ failedResp := new(failedResponse)
+ if err := unmarshal(r.data, failedResp); err != nil {
+ return err
+ }
+
+ return failedResp.err()
+}
+
+func (r *Response) Unmarshal(v interface{}) error {
+ if err := unmarshal(r.data, v); err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/vendor/github.com/kolo/xmlrpc/response_test.go b/vendor/github.com/kolo/xmlrpc/response_test.go
new file mode 100644
index 00000000..55095c24
--- /dev/null
+++ b/vendor/github.com/kolo/xmlrpc/response_test.go
@@ -0,0 +1,84 @@
+package xmlrpc
+
+import (
+ "testing"
+)
+
+const faultRespXml = `
+<?xml version="1.0" encoding="UTF-8"?>
+<methodResponse>
+ <fault>
+ <value>
+ <struct>
+ <member>
+ <name>faultString</name>
+ <value>
+ <string>You must log in before using this part of Bugzilla.</string>
+ </value>
+ </member>
+ <member>
+ <name>faultCode</name>
+ <value>
+ <int>410</int>
+ </value>
+ </member>
+ </struct>
+ </value>
+ </fault>
+</methodResponse>`
+
+func Test_failedResponse(t *testing.T) {
+ resp := NewResponse([]byte(faultRespXml))
+
+ if !resp.Failed() {
+ t.Fatal("Failed() error: expected true, got false")
+ }
+
+ if resp.Err() == nil {
+ t.Fatal("Err() error: expected error, got nil")
+ }
+
+ err := resp.Err().(*xmlrpcError)
+ if err.code != 410 && err.err != "You must log in before using this part of Bugzilla." {
+ t.Fatal("Err() error: got wrong error")
+ }
+}
+
+const emptyValResp = `
+<?xml version="1.0" encoding="UTF-8"?>
+<methodResponse>
+ <params>
+ <param>
+ <value>
+ <struct>
+ <member>
+ <name>user</name>
+ <value><string>Joe Smith</string></value>
+ </member>
+ <member>
+ <name>token</name>
+ <value/>
+ </member>
+ </struct>
+ </value>
+ </param>
+ </params>
+</methodResponse>`
+
+
+func Test_responseWithEmptyValue(t *testing.T) {
+ resp := NewResponse([]byte(emptyValResp))
+
+ result := struct{
+ User string `xmlrpc:"user"`
+ Token string `xmlrpc:"token"`
+ }{}
+
+ if err := resp.Unmarshal(&result); err != nil {
+ t.Fatalf("unmarshal error: %v", err)
+ }
+
+ if result.User != "Joe Smith" || result.Token != "" {
+ t.Fatalf("unexpected result: %v", result)
+ }
+}
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 00000000..1b1ff876
--- /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/github.com/kolo/xmlrpc/xmlrpc.go b/vendor/github.com/kolo/xmlrpc/xmlrpc.go
new file mode 100644
index 00000000..8766403a
--- /dev/null
+++ b/vendor/github.com/kolo/xmlrpc/xmlrpc.go
@@ -0,0 +1,19 @@
+package xmlrpc
+
+import (
+ "fmt"
+)
+
+// xmlrpcError represents errors returned on xmlrpc request.
+type xmlrpcError struct {
+ code int
+ err string
+}
+
+// Error() method implements Error interface
+func (e *xmlrpcError) Error() string {
+ return fmt.Sprintf("error: \"%s\" code: %d", e.err, e.code)
+}
+
+// Base64 represents value in base64 encoding
+type Base64 string