From e55a27d2102a77ce3f01f3f36060843bb2b46fd2 Mon Sep 17 00:00:00 2001 From: Joao Cavalheiro 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 | 405 ++++++++++++++++ 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, 1558 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..2336d2746 --- /dev/null +++ b/discovery/uyuni/uyuni.go @@ -0,0 +1,405 @@ +// 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"` + PrimaryFQDN string `xmlrpc:"primary_fqdn"` + 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 + var hostname string + if combinedFormulaData.ProxyIsEnabled { + proxyPortNumber = fmt.Sprintf("%d", int(combinedFormulaData.ProxyPort)) + } + if len(networkInfo.PrimaryFQDN) > 0 { + hostname = networkInfo.PrimaryFQDN + } else { + hostname = networkInfo.Hostname + } + 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", hostname, labels[model.AddressLabel]) + labels[model.AddressLabel] = model.LabelValue(addr) + labels["hostname"] = model.LabelValue(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", + "PrimaryFQDN", networkInfoBySystemID[systemID].PrimaryFQDN, + "Hostname", 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 + } + + // + if err = dec.Skip(); err != nil { + return err + } + + break + } + } + } + + // + 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()) + } + + // + 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") + } + + // + 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(""), 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("%s", 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("%s", strconv.FormatInt(val.Int(), 10))) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + b = []byte(fmt.Sprintf("%s", strconv.FormatUint(val.Uint(), 10))) + case reflect.Float32, reflect.Float64: + b = []byte(fmt.Sprintf("%s", + strconv.FormatFloat(val.Float(), 'f', -1, val.Type().Bits()))) + case reflect.Bool: + if val.Bool() { + b = []byte("1") + } else { + b = []byte("0") + } + case reflect.String: + var buf bytes.Buffer + + xml.Escape(&buf, []byte(val.String())) + + if _, ok := val.Interface().(Base64); ok { + b = []byte(fmt.Sprintf("%s", buf.String())) + } else { + b = []byte(fmt.Sprintf("%s", buf.String())) + } + default: + return nil, fmt.Errorf("xmlrpc encode error: unsupported type") + } + + if err != nil { + return nil, err + } + + return []byte(fmt.Sprintf("%s", string(b))), nil +} + +func encodeStruct(structVal reflect.Value) ([]byte, error) { + var b bytes.Buffer + + b.WriteString("") + + 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("") + b.WriteString(fmt.Sprintf("%s", name)) + b.Write(p) + b.WriteString("") + } + + b.WriteString("") + + 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("") + + 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("") + b.WriteString(fmt.Sprintf("%s", key.String())) + + p, err := encodeValue(kval) + + if err != nil { + return nil, err + } + + b.Write(p) + b.WriteString("") + } + + b.WriteString("") + + return b.Bytes(), nil +} + +func encodeSlice(val reflect.Value) ([]byte, error) { + var b bytes.Buffer + + b.WriteString("") + + for i := 0; i < val.Len(); i++ { + p, err := encodeValue(val.Index(i)) + if err != nil { + return nil, err + } + + b.Write(p) + } + + b.WriteString("") + + 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(``) + b.WriteString(fmt.Sprintf("%s", method)) + + if args != nil { + b.WriteString("") + + for _, arg := range args { + p, err := marshal(arg) + if err != nil { + return nil, err + } + + b.WriteString(fmt.Sprintf("%s", string(p))) + } + + b.WriteString("") + } + + b.WriteString("") + + 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(`(\s|\S)+`) +) + +// 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.26.2