diff --git a/CHANGELOG.md b/CHANGELOG.md index bc41e3c7b4..5680450eac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,18 +1,18 @@ -## v1.6.3 / 2017-05-18 +## 1.6.3 / 2017-05-18 * [BUGFIX] Fix disappearing Alertmanger targets in Alertmanager discovery. * [BUGFIX] Fix panic with remote_write on ARMv7. * [BUGFIX] Fix stacked graphs to adapt min/max values. -## v1.6.2 / 2017-05-11 +## 1.6.2 / 2017-05-11 * [BUGFIX] Fix potential memory leak in Kubernetes service discovery -## v1.6.1 / 2017-04-19 +## 1.6.1 / 2017-04-19 * [BUGFIX] Don't panic if storage has no FPs even after initial wait -## v1.6.0 / 2017-04-14 +## 1.6.0 / 2017-04-14 * [CHANGE] Replaced the remote write implementations for various backends by a generic write interface with example adapter implementation for various @@ -77,11 +77,11 @@ * [BUGFIX] Fix deadlock in Zookeeper SD. * [BUGFIX] Fix fuzzy search problems in the web-UI auto-completion. -## v1.5.3 / 2017-05-11 +## 1.5.3 / 2017-05-11 * [BUGFIX] Fix potential memory leak in Kubernetes service discovery -## v1.5.2 / 2017-02-10 +## 1.5.2 / 2017-02-10 * [BUGFIX] Fix series corruption in a special case of series maintenance where the minimum series-file-shrink-ratio kicks in. @@ -89,14 +89,14 @@ scheduled to be quarantined. * [ENHANCEMENT] Binaries built with Go1.7.5. -## v1.5.1 / 2017-02-07 +## 1.5.1 / 2017-02-07 * [BUGFIX] Don't lose fully persisted memory series during checkpointing. * [BUGFIX] Fix intermittently failing relabeling. * [BUGFIX] Make `-storage.local.series-file-shrink-ratio` work. * [BUGFIX] Remove race condition from TestLoop. -## v1.5.0 / 2017-01-23 +## 1.5.0 / 2017-01-23 * [CHANGE] Use lexicographic order to sort alerts by name. * [FEATURE] Add Joyent Triton discovery. @@ -113,11 +113,11 @@ * [BUGFIX] Ignore dotfiles in data directory. * [BUGFIX] Abort on intermediate federation errors. -## v1.4.1 / 2016-11-28 +## 1.4.1 / 2016-11-28 * [BUGFIX] Fix Consul service discovery -## v1.4.0 / 2016-11-25 +## 1.4.0 / 2016-11-25 * [FEATURE] Allow configuring Alertmanagers via service discovery * [FEATURE] Display used Alertmanagers on runtime page in the UI @@ -130,16 +130,27 @@ * [BUGFIX] Use proper float64 modulo in PromQL `%` binary operations * [BUGFIX] Fix crash bug in Kubernetes service discovery -## v1.3.1 / 2016-11-04 +## 1.3.1 / 2016-11-04 -This bug-fix release pulls in the fixes from the v1.2.3 release. +This bug-fix release pulls in the fixes from the 1.2.3 release. * [BUGFIX] Correctly handle empty Regex entry in relabel config. * [BUGFIX] MOD (`%`) operator doesn't panic with small floating point numbers. * [BUGFIX] Updated miekg/dns vendoring to pick up upstream bug fixes. * [ENHANCEMENT] Improved DNS error reporting. -## v1.3.0 / 2016-11-01 +## 1.2.3 / 2016-11-04 + +Note that this release is chronologically after 1.3.0. + +* [BUGFIX] Correctly handle end time before start time in range queries. +* [BUGFIX] Error on negative `-storage.staleness-delta` +* [BUGFIX] Correctly handle empty Regex entry in relabel config. +* [BUGFIX] MOD (`%`) operator doesn't panic with small floating point numbers. +* [BUGFIX] Updated miekg/dns vendoring to pick up upstream bug fixes. +* [ENHANCEMENT] Improved DNS error reporting. + +## 1.3.0 / 2016-11-01 This is a breaking change to the Kubernetes service discovery. @@ -153,16 +164,7 @@ This is a breaking change to the Kubernetes service discovery. * [BUGFIX] Validate query end time is not before start time. * [BUGFIX] Error on negative `-storage.staleness-delta` -## v1.2.3 / 2016-11-04 - -* [BUGFIX] Correctly handle end time before start time in range queries. -* [BUGFIX] Error on negative `-storage.staleness-delta` -* [BUGFIX] Correctly handle empty Regex entry in relabel config. -* [BUGFIX] MOD (`%`) operator doesn't panic with small floating point numbers. -* [BUGFIX] Updated miekg/dns vendoring to pick up upstream bug fixes. -* [ENHANCEMENT] Improved DNS error reporting. - -## v1.2.2 / 2016-10-30 +## 1.2.2 / 2016-10-30 * [BUGFIX] Correctly handle on() in alerts. * [BUGFIX] UI: Deal properly with aborted requests. @@ -171,13 +173,13 @@ This is a breaking change to the Kubernetes service discovery. * [BUGFIX] Remote storage: Re-add accidentally removed timeout flag. * [BUGFIX] Updated a number of vendored packages to pick up upstream bug fixes. -## v1.2.1 / 2016-10-10 +## 1.2.1 / 2016-10-10 * [BUGFIX] Count chunk evictions properly so that the server doesn't assume it runs out of memory and subsequencly throttles ingestion. * [BUGFIX] Use Go1.7.1 for prebuilt binaries to fix issues on MacOS Sierra. -## v1.2.0 / 2016-10-07 +## 1.2.0 / 2016-10-07 * [FEATURE] Cleaner encoding of query parameters in `/graph` URLs. * [FEATURE] PromQL: Add `minute()` function. @@ -200,22 +202,22 @@ This is a breaking change to the Kubernetes service discovery. * [FEATURE] **Experimental** remote write path: Add HTTP basic auth and TLS. * [FEATURE] **Experimental** remote write path: Support for relabelling. -## v1.1.3 / 2016-09-16 +## 1.1.3 / 2016-09-16 * [ENHANCEMENT] Use golang-builder base image for tests in CircleCI. * [ENHANCEMENT] Added unit tests for federation. * [BUGFIX] Correctly de-dup metric families in federation output. -## v1.1.2 / 2016-09-08 +## 1.1.2 / 2016-09-08 * [BUGFIX] Allow label names that coincide with keywords. -## v1.1.1 / 2016-09-07 +## 1.1.1 / 2016-09-07 * [BUGFIX] Fix IPv6 escaping in service discovery integrations * [BUGFIX] Fix default scrape port assignment for IPv6 -## v1.1.0 / 2016-09-03 +## 1.1.0 / 2016-09-03 * [FEATURE] Add `quantile` and `quantile_over_time`. * [FEATURE] Add `stddev_over_time` and `stdvar_over_time`. @@ -245,17 +247,17 @@ This is a breaking change to the Kubernetes service discovery. * [BUGFIX] Fix rule HTML escaping issues. * [BUGFIX] Remove internal labels from alerts sent to AM. -## v1.0.2 / 2016-08-24 +## 1.0.2 / 2016-08-24 * [BUGFIX] Clean up old targets after config reload. -## v1.0.1 / 2016-07-21 +## 1.0.1 / 2016-07-21 * [BUGFIX] Exit with error on non-flag command-line arguments. * [BUGFIX] Update example console templates to new HTTP API. * [BUGFIX] Re-add logging flags. -## v1.0.0 / 2016-07-18 +## 1.0.0 / 2016-07-18 * [CHANGE] Remove deprecated query language keywords * [CHANGE] Change Kubernetes SD to require specifying Kubernetes role @@ -274,7 +276,7 @@ This is a breaking change to the Kubernetes service discovery. * [BUGFIX] Fix edge case handling in crash recovery * [BUGFIX] Hide testing package flags from help output -## v0.20.0 / 2016-06-15 +## 0.20.0 / 2016-06-15 This release contains multiple breaking changes to the configuration schema. @@ -292,23 +294,23 @@ This release contains multiple breaking changes to the configuration schema. * [CHANGE] Rename `names` to `files` in file SD configuration * [CHANGE] Remove kubelet port config option in Kubernetes SD configuration -## v0.19.3 / 2016-06-14 +## 0.19.3 / 2016-06-14 * [BUGFIX] Handle Marathon apps with zero ports * [BUGFIX] Fix startup panic in retrieval layer -## v0.19.2 / 2016-05-29 +## 0.19.2 / 2016-05-29 * [BUGFIX] Correctly handle `GROUP_LEFT` and `GROUP_RIGHT` without labels in string representation of expressions and in rules. * [BUGFIX] Use `-web.external-url` for new status endpoints. -## v0.19.1 / 2016-05-25 +## 0.19.1 / 2016-05-25 * [BUGFIX] Handle service discovery panic affecting Kubernetes SD * [BUGFIX] Fix web UI display issue in some browsers -## v0.19.0 / 2016-05-24 +## 0.19.0 / 2016-05-24 This version contains a breaking change to the query language. Please read the documentation on the grouping behavior of vector matching: @@ -323,7 +325,7 @@ https://prometheus.io/docs/querying/operators/#vector-matching * [ENHANCEMENT] Partition status page into individual pages * [BUGFIX] Fix issue of hanging target scrapes -## v0.18.0 / 2016-04-18 +## 0.18.0 / 2016-04-18 * [BUGFIX] Fix operator precedence in PromQL * [BUGFIX] Never drop still open head chunk @@ -343,16 +345,16 @@ https://prometheus.io/docs/querying/operators/#vector-matching * [ENHANCEMENT] Instrument retrieval layer * [ENHANCEMENT] Add Go version to `prometheus_build_info` metric -## v0.17.0 / 2016-03-02 +## 0.17.0 / 2016-03-02 -This version no longer works with Alertmanager v0.0.4 and earlier! +This version no longer works with Alertmanager 0.0.4 and earlier! The alerting rule syntax has changed as well but the old syntax is supported -up until version v0.18. +up until version 0.18. All regular expressions in PromQL are anchored now, matching the behavior of regular expressions in config files. -* [CHANGE] Integrate with Alertmanager v0.1.0 and higher +* [CHANGE] Integrate with Alertmanager 0.1.0 and higher * [CHANGE] Degraded storage mode renamed to rushed mode * [CHANGE] New alerting rule syntax * [CHANGE] Add label validation on ingestion @@ -373,7 +375,7 @@ regular expressions in config files. * [BUGFIX] Properly handle creation of target with bad TLS config * [BUGFIX] Fix of checkpoint timing issue -## v0.16.2 / 2016-01-18 +## 0.16.2 / 2016-01-18 * [FEATURE] Multiple authentication options for EC2 discovery added * [FEATURE] Several meta labels for EC2 discovery added @@ -404,14 +406,14 @@ regular expressions in config files. Some changes to the Kubernetes service discovery were integration since it was released as a beta feature. -## v0.16.1 / 2015-10-16 +## 0.16.1 / 2015-10-16 * [FEATURE] Add `irate()` function. * [ENHANCEMENT] Improved auto-completion in expression browser. * [CHANGE] Kubernetes SD moves node label to instance label. * [BUGFIX] Escape regexes in console templates. -## v0.16.0 / 2015-10-09 +## 0.16.0 / 2015-10-09 BREAKING CHANGES: @@ -574,14 +576,13 @@ All changes: the same series upon append. * [CLEANUP] Resolve relative paths during configuration loading. -## v0.15.1 / 2015-07-27 - +## 0.15.1 / 2015-07-27 * [BUGFIX] Fix vector matching behavior when there is a mix of equality and non-equality matchers in a vector selector and one matcher matches no series. * [ENHANCEMENT] Allow overriding `GOARCH` and `GOOS` in Makefile.INCLUDE. * [ENHANCEMENT] Update vendored dependencies. -## v0.15.0 / 2015-07-21 +## 0.15.0 / 2015-07-21 BREAKING CHANGES: @@ -692,8 +693,7 @@ All changes: * [CLEANUP] Use `templates.TemplateExpander` for all page templates. * [CLEANUP] Use new v1 HTTP API for querying and graphing. -## v0.14.0 / 2015-06-01 - +## 0.14.0 / 2015-06-01 * [CHANGE] Configuration format changed and switched to YAML. (See the provided [migration tool](https://github.com/prometheus/migrate/releases).) * [ENHANCEMENT] Redesign of state-preserving target discovery. @@ -718,12 +718,10 @@ All changes: * [FEATURE] Add increase() query function to calculate a counter's increase. * [ENHANCEMENT] Limit retrievable samples to the storage's retention window. -## v0.13.4 / 2015-05-23 - +## 0.13.4 / 2015-05-23 * [BUGFIX] Fix a race while checkpointing fingerprint mappings. -## v0.13.3 / 2015-05-11 - +## 0.13.3 / 2015-05-11 * [BUGFIX] Handle fingerprint collisions properly. * [CHANGE] Comments in rules file must start with `#`. (The undocumented `//` and `/*...*/` comment styles are no longer supported.) @@ -733,8 +731,7 @@ All changes: * [ENHANCEMENT] Limit maximum number of concurrent queries. * [ENHANCEMENT] Terminate running queries during shutdown. -## v0.13.2 / 2015-05-05 - +## 0.13.2 / 2015-05-05 * [MAINTENANCE] Updated vendored dependencies to their newest versions. * [MAINTENANCE] Include rule_checker and console templates in release tarball. * [BUGFIX] Sort NaN as the lowest value. @@ -744,13 +741,11 @@ All changes: reading from disk. * [BUGFIX] Show correct error on wrong DNS response. -## v0.13.1 / 2015-04-09 - +## 0.13.1 / 2015-04-09 * [BUGFIX] Treat memory series with zero chunks correctly in series maintenance. * [ENHANCEMENT] Improve readability of usage text even more. -## v0.13.0 / 2015-04-08 - +## 0.13.0 / 2015-04-08 * [ENHANCEMENT] Double-delta encoding for chunks, saving typically 40% of space, both in RAM and on disk. * [ENHANCEMENT] Redesign of chunk persistence queuing, increasing performance @@ -777,8 +772,7 @@ All changes: * [CLEANUP] Misc. other code cleanups. * [MAINTENANCE] Updated vendored dependcies to their newest versions. -## v0.12.0 / 2015-03-04 - +## 0.12.0 / 2015-03-04 * [CHANGE] Use client_golang v0.3.1. THIS CHANGES FINGERPRINTING AND INVALIDATES ALL PERSISTED FINGERPRINTS. You have to wipe your storage to use this or later versions. There is a version guard in place that will prevent you to @@ -793,9 +787,8 @@ All changes: (rather than /tmp/metrics). * [CHANGE] Makefile uses Go 1.4.2. -## v0.11.1 / 2015-02-27 - -* [BUGFIX] Make series maintenance complete again. (Ever since v0.9.0rc4, +## 0.11.1 / 2015-02-27 +* [BUGFIX] Make series maintenance complete again. (Ever since 0.9.0rc4, or commit 0851945, series would not be archived, chunk descriptors would not be evicted, and stale head chunks would never be closed. This happened due to accidental deletion of a line calling a (well tested :) function. @@ -806,8 +799,7 @@ All changes: * [CLEANUP] Code cleanups. * [ENHANCEMENT] Limit the number of 'dirty' series counted during checkpointing. -## v0.11.0 / 2015-02-23 - +## 0.11.0 / 2015-02-23 * [FEATURE] Introduce new metric type Histogram with server-side aggregation. * [FEATURE] Add offset operator. * [FEATURE] Add floor, ceil and round functions. @@ -831,8 +823,7 @@ All changes: * [BUGFIX] Fix Rickshaw/D3 version mismatch. * [CLEANUP] Various code cleanups. -## v0.10.0 / 2015-01-26 - +## 0.10.0 / 2015-01-26 * [CHANGE] More efficient JSON result format in query API. This requires up-to-date versions of PromDash and prometheus_cli, too. * [ENHANCEMENT] Excluded non-minified Bootstrap assets and the Bootstrap maps @@ -844,8 +835,7 @@ All changes: * [BUGFIX] Several fixes to graphs in consoles. * [CLEANUP] Removed a file size check that did not check anything. -## v0.9.0 / 2015-01-23 - +## 0.9.0 / 2015-01-23 * [CHANGE] Reworked command line flags, now more consistent and taking into account needs of the new storage backend (see below). * [CHANGE] Metric names are dropped after certain transformations. @@ -879,20 +869,18 @@ All changes: * [ENHANCEMENT] Switched from Go 1.3 to Go 1.4. * [ENHANCEMENT] Vendored external dependencies with godeps. * [ENHANCEMENT] Numerous Web UI improvements, moved to Bootstrap3 and - Rickshaw v1.5.1. + Rickshaw 1.5.1. * [ENHANCEMENT] Improved Docker integration. * [ENHANCEMENT] Simplified the Makefile contraption. * [CLEANUP] Put meta-data files into proper shape (LICENSE, README.md etc.) * [CLEANUP] Removed all legitimate 'go vet' and 'golint' warnings. * [CLEANUP] Removed dead code. -## v0.8.0 / 2014-09-04 - +## 0.8.0 / 2014-09-04 * [ENHANCEMENT] Stagger scrapes to spread out load. * [BUGFIX] Correctly quote HTTP Accept header. -## v0.7.0 / 2014-08-06 - +## 0.7.0 / 2014-08-06 * [FEATURE] Added new functions: abs(), topk(), bottomk(), drop_common_labels(). * [FEATURE] Let console templates get graph links from expressions. * [FEATURE] Allow console templates to dynamically include other templates. @@ -906,8 +894,7 @@ All changes: * [ENHANCEMENT] Removed incremental backoffs for unhealthy targets. * [ENHANCEMENT] Dockerfile also builds Prometheus support tools now. -## v0.6.0 / 2014-06-30 - +## 0.6.0 / 2014-06-30 * [FEATURE] Added console and alert templates support, along with various template functions. * [PERFORMANCE] Much faster and more memory-efficient flushing to disk. * [ENHANCEMENT] Query results are now only logged when debugging. @@ -917,7 +904,7 @@ All changes: * [BUGFIX] Added installation step for missing dependency to Dockerfile. * [BUGFIX] Removed broken and unused "User Dashboard" link. -## v0.5.0 / 2014-05-28 +## 0.5.0 / 2014-05-28 * [BUGFIX] Fixed next retrieval time display on status page. * [BUGFIX] Updated some variable references in tools subdir. @@ -927,7 +914,7 @@ All changes: * [ENHANCEMENT] Added internal check to verify temporal order of streams. * [ENHANCEMENT] Some internal refactorings. -## v0.4.0 / 2014-04-17 +## 0.4.0 / 2014-04-17 * [FEATURE] Vectors and scalars may now be reversed in binary operations (` `). * [FEATURE] It's possible to shutdown Prometheus via a `/-/quit` web endpoint now. diff --git a/Dockerfile b/Dockerfile index a522c07c52..2b73e43b49 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ FROM quay.io/prometheus/busybox:latest -MAINTAINER The Prometheus Authors +LABEL maintainer "The Prometheus Authors " COPY prometheus /bin/prometheus COPY promtool /bin/promtool diff --git a/cmd/prometheus/config.go b/cmd/prometheus/config.go index 47459fc432..3a78c8fefb 100644 --- a/cmd/prometheus/config.go +++ b/cmd/prometheus/config.go @@ -291,7 +291,7 @@ func parseAlertmanagerURLToConfig(us string) (*config.AlertmanagerConfig, error) } if password, isSet := u.User.Password(); isSet { - acfg.HTTPClientConfig.BasicAuth.Password = password + acfg.HTTPClientConfig.BasicAuth.Password = config.Secret(password) } } diff --git a/cmd/prometheus/main.go b/cmd/prometheus/main.go index ec374fbd3b..7dce4aa599 100644 --- a/cmd/prometheus/main.go +++ b/cmd/prometheus/main.go @@ -95,8 +95,8 @@ func Main() int { // reloadables = append(reloadables, remoteStorage) var ( - notifier = notifier.New(&cfg.notifier) - targetManager = retrieval.NewTargetManager(localStorage) + notifier = notifier.New(&cfg.notifier, log.Base()) + targetManager = retrieval.NewTargetManager(localStorage, log.Base()) queryEngine = promql.NewEngine(localStorage, &cfg.queryEngine) ctx, cancelCtx = context.WithCancel(context.Background()) ) diff --git a/config/config.go b/config/config.go index 88abbb9f4d..05f17350ca 100644 --- a/config/config.go +++ b/config/config.go @@ -30,7 +30,6 @@ import ( var ( patFileSDName = regexp.MustCompile(`^[^*]*(\*[^/]*)?\.(json|yml|yaml|JSON|YML|YAML)$`) patRulePath = regexp.MustCompile(`^[^*]*(\*[^/]*)?$`) - patAuthLine = regexp.MustCompile(`((?:password|bearer_token|secret_key|client_secret):\s+)(".+"|'.+'|[^\s]+)`) relabelTarget = regexp.MustCompile(`^(?:(?:[a-zA-Z_]|\$(?:\{\w+\}|\w+))+\w*)+$`) ) @@ -150,6 +149,12 @@ var ( RefreshInterval: model.Duration(60 * time.Second), } + // DefaultOpenstackSDConfig is the default OpenStack SD configuration. + DefaultOpenstackSDConfig = OpenstackSDConfig{ + Port: 80, + RefreshInterval: model.Duration(60 * time.Second), + } + // DefaultAzureSDConfig is the default Azure SD configuration. DefaultAzureSDConfig = AzureSDConfig{ Port: 80, @@ -219,6 +224,23 @@ type Config struct { original string } +// Secret special type for storing secrets. +type Secret string + +// UnmarshalYAML implements the yaml.Unmarshaler interface for Secrets. +func (s *Secret) UnmarshalYAML(unmarshal func(interface{}) error) error { + type plain Secret + return unmarshal((*plain)(s)) +} + +// MarshalYAML implements the yaml.Marshaler interface for Secrets. +func (s Secret) MarshalYAML() (interface{}, error) { + if s != "" { + return "", nil + } + return nil, nil +} + // resolveFilepaths joins all relative paths in a configuration // with a given base directory. func resolveFilepaths(baseDir string, cfg *Config) { @@ -281,17 +303,11 @@ func checkOverflow(m map[string]interface{}, ctx string) error { } func (c Config) String() string { - var s string - if c.original != "" { - s = c.original - } else { - b, err := yaml.Marshal(c) - if err != nil { - return fmt.Sprintf("", err) - } - s = string(b) + b, err := yaml.Marshal(c) + if err != nil { + return fmt.Sprintf("", err) } - return patAuthLine.ReplaceAllString(s, "${1}") + return string(b) } // UnmarshalYAML implements the yaml.Unmarshaler interface. @@ -370,7 +386,7 @@ func (c *GlobalConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { if err := unmarshal((*plain)(gc)); err != nil { return err } - if err := checkOverflow(c.XXX, "global config"); err != nil { + if err := checkOverflow(gc.XXX, "global config"); err != nil { return err } // First set the correct scrape interval, then check that the timeout @@ -454,6 +470,8 @@ type ServiceDiscoveryConfig struct { GCESDConfigs []*GCESDConfig `yaml:"gce_sd_configs,omitempty"` // List of EC2 service discovery configurations. EC2SDConfigs []*EC2SDConfig `yaml:"ec2_sd_configs,omitempty"` + // List of OpenStack service discovery configurations. + OpenstackSDConfigs []*OpenstackSDConfig `yaml:"openstack_sd_configs,omitempty"` // List of Azure service discovery configurations. AzureSDConfigs []*AzureSDConfig `yaml:"azure_sd_configs,omitempty"` // List of Triton service discovery configurations. @@ -480,7 +498,7 @@ type HTTPClientConfig struct { // The HTTP basic authentication credentials for the targets. BasicAuth *BasicAuth `yaml:"basic_auth,omitempty"` // The bearer token for the targets. - BearerToken string `yaml:"bearer_token,omitempty"` + BearerToken Secret `yaml:"bearer_token,omitempty"` // The bearer token file for the targets. BearerTokenFile string `yaml:"bearer_token_file,omitempty"` // HTTP proxy server to use to connect to the targets. @@ -544,7 +562,7 @@ func (c *ScrapeConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { if err != nil { return err } - if err := checkOverflow(c.XXX, "scrape_config"); err != nil { + if err = checkOverflow(c.XXX, "scrape_config"); err != nil { return err } if len(c.JobName) == 0 { @@ -554,7 +572,7 @@ func (c *ScrapeConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { // The UnmarshalYAML method of HTTPClientConfig is not being called because it's not a pointer. // We cannot make it a pointer as the parser panics for inlined pointer structs. // Thus we just do its validation here. - if err := c.HTTPClientConfig.validate(); err != nil { + if err = c.HTTPClientConfig.validate(); err != nil { return err } @@ -660,7 +678,7 @@ func CheckTargetAddress(address model.LabelValue) error { // BasicAuth contains basic HTTP authentication credentials. type BasicAuth struct { Username string `yaml:"username"` - Password string `yaml:"password"` + Password Secret `yaml:"password"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline"` @@ -669,7 +687,7 @@ type BasicAuth struct { // ClientCert contains client cert credentials. type ClientCert struct { Cert string `yaml:"cert"` - Key string `yaml:"key"` + Key Secret `yaml:"key"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline"` @@ -718,7 +736,7 @@ func (tg *TargetGroup) UnmarshalYAML(unmarshal func(interface{}) error) error { }) } tg.Labels = g.Labels - return checkOverflow(g.XXX, "target_group") + return checkOverflow(g.XXX, "static_config") } // MarshalYAML implements the yaml.Marshaler interface. @@ -825,12 +843,12 @@ func (c *FileSDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { // ConsulSDConfig is the configuration for Consul service discovery. type ConsulSDConfig struct { Server string `yaml:"server"` - Token string `yaml:"token,omitempty"` + Token Secret `yaml:"token,omitempty"` Datacenter string `yaml:"datacenter,omitempty"` TagSeparator string `yaml:"tag_separator,omitempty"` Scheme string `yaml:"scheme,omitempty"` Username string `yaml:"username,omitempty"` - Password string `yaml:"password,omitempty"` + Password Secret `yaml:"password,omitempty"` // The list of services for which targets are discovered. // Defaults to all services if empty. Services []string `yaml:"services"` @@ -933,7 +951,7 @@ type MarathonSDConfig struct { Timeout model.Duration `yaml:"timeout,omitempty"` RefreshInterval model.Duration `yaml:"refresh_interval,omitempty"` TLSConfig TLSConfig `yaml:"tls_config,omitempty"` - BearerToken string `yaml:"bearer_token,omitempty"` + BearerToken Secret `yaml:"bearer_token,omitempty"` BearerTokenFile string `yaml:"bearer_token_file,omitempty"` // Catches all undefined fields and must be empty after parsing. @@ -990,7 +1008,7 @@ type KubernetesSDConfig struct { APIServer URL `yaml:"api_server"` Role KubernetesRole `yaml:"role"` BasicAuth *BasicAuth `yaml:"basic_auth,omitempty"` - BearerToken string `yaml:"bearer_token,omitempty"` + BearerToken Secret `yaml:"bearer_token,omitempty"` BearerTokenFile string `yaml:"bearer_token_file,omitempty"` TLSConfig TLSConfig `yaml:"tls_config,omitempty"` NamespaceDiscovery KubernetesNamespaceDiscovery `yaml:"namespaces"` @@ -1095,7 +1113,7 @@ func (c *GCESDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { type EC2SDConfig struct { Region string `yaml:"region"` AccessKey string `yaml:"access_key,omitempty"` - SecretKey string `yaml:"secret_key,omitempty"` + SecretKey Secret `yaml:"secret_key,omitempty"` Profile string `yaml:"profile,omitempty"` RefreshInterval model.Duration `yaml:"refresh_interval,omitempty"` Port int `yaml:"port"` @@ -1121,13 +1139,42 @@ func (c *EC2SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { return nil } +// OpenstackSDConfig is the configuration for OpenStack based service discovery. +type OpenstackSDConfig struct { + IdentityEndpoint string `yaml:"identity_endpoint"` + Username string `yaml:"username"` + UserID string `yaml:"userid"` + Password Secret `yaml:"password"` + ProjectName string `yaml:"project_name"` + ProjectID string `yaml:"project_id"` + DomainName string `yaml:"domain_name"` + DomainID string `yaml:"domain_id"` + Region string `yaml:"region"` + RefreshInterval model.Duration `yaml:"refresh_interval,omitempty"` + Port int `yaml:"port"` + + // Catches all undefined fields and must be empty after parsing. + XXX map[string]interface{} `yaml:",inline"` +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (c *OpenstackSDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + *c = DefaultOpenstackSDConfig + type plain OpenstackSDConfig + err := unmarshal((*plain)(c)) + if err != nil { + return err + } + return checkOverflow(c.XXX, "openstack_sd_config") +} + // AzureSDConfig is the configuration for Azure based service discovery. type AzureSDConfig struct { Port int `yaml:"port"` SubscriptionID string `yaml:"subscription_id"` TenantID string `yaml:"tenant_id,omitempty"` ClientID string `yaml:"client_id,omitempty"` - ClientSecret string `yaml:"client_secret,omitempty"` + ClientSecret Secret `yaml:"client_secret,omitempty"` RefreshInterval model.Duration `yaml:"refresh_interval,omitempty"` // Catches all undefined fields and must be empty after parsing. diff --git a/config/config_test.go b/config/config_test.go index c0ed1e40be..0282deb0e5 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -18,6 +18,7 @@ import ( "io/ioutil" "net/url" "reflect" + "regexp" "strings" "testing" "time" @@ -144,6 +145,7 @@ var expectedConf = &Config{ }, }, { + JobName: "service-x", ScrapeInterval: model.Duration(50 * time.Second), @@ -153,7 +155,7 @@ var expectedConf = &Config{ HTTPClientConfig: HTTPClientConfig{ BasicAuth: &BasicAuth{ Username: "admin_name", - Password: "admin_password", + Password: "multiline\nmysecret\ntest", }, }, MetricsPath: "/my_path", @@ -245,6 +247,7 @@ var expectedConf = &Config{ ConsulSDConfigs: []*ConsulSDConfig{ { Server: "localhost:1234", + Token: "mysecret", Services: []string{"nginx", "cache", "mysql"}, TagSeparator: DefaultConsulSDConfig.TagSeparator, Scheme: "https", @@ -284,7 +287,7 @@ var expectedConf = &Config{ KeyFile: "testdata/valid_key_file", }, - BearerToken: "avalidtoken", + BearerToken: "mysecret", }, }, { @@ -303,7 +306,7 @@ var expectedConf = &Config{ Role: KubernetesRoleEndpoint, BasicAuth: &BasicAuth{ Username: "myusername", - Password: "mypassword", + Password: "mysecret", }, NamespaceDiscovery: KubernetesNamespaceDiscovery{}, }, @@ -372,7 +375,7 @@ var expectedConf = &Config{ { Region: "us-east-1", AccessKey: "access", - SecretKey: "secret", + SecretKey: "mysecret", Profile: "profile", RefreshInterval: model.Duration(60 * time.Second), Port: 80, @@ -395,7 +398,7 @@ var expectedConf = &Config{ SubscriptionID: "11AAAA11-A11A-111A-A111-1111A1111A11", TenantID: "BBBB222B-B2B2-2B22-B222-2BB2222BB2B2", ClientID: "333333CC-3C33-3333-CCC3-33C3CCCCC33C", - ClientSecret: "nAdvAK2oBuVym4IXix", + ClientSecret: "mysecret", RefreshInterval: model.Duration(5 * time.Minute), Port: 9100, }, @@ -538,9 +541,12 @@ func TestLoadConfig(t *testing.T) { // String method must not reveal authentication credentials. s := c.String() - if strings.Contains(s, "admin_password") { + secretRe := regexp.MustCompile("") + matches := secretRe.FindAllStringIndex(s, -1) + if len(matches) != 6 || strings.Contains(s, "mysecret") { t.Fatalf("config's String method reveals authentication credentials.") } + } var expectedErrors = []struct { @@ -634,6 +640,9 @@ var expectedErrors = []struct { }, { filename: "target_label_hashmod_missing.bad.yml", errMsg: "relabel configuration for hashmod action requires 'target_label' value", + }, { + filename: "unknown_global_attr.bad.yml", + errMsg: "unknown fields in global config: nonexistent_field", }, } diff --git a/config/testdata/conf.good.yml b/config/testdata/conf.good.yml index 1825edd9b7..69c9cdc880 100644 --- a/config/testdata/conf.good.yml +++ b/config/testdata/conf.good.yml @@ -67,7 +67,7 @@ scrape_configs: basic_auth: username: admin_name - password: admin_password + password: "multiline\nmysecret\ntest" scrape_interval: 50s scrape_timeout: 5s @@ -113,6 +113,7 @@ scrape_configs: consul_sd_configs: - server: 'localhost:1234' + token: mysecret services: ['nginx', 'cache', 'mysql'] scheme: https tls_config: @@ -134,7 +135,7 @@ scrape_configs: cert_file: valid_cert_file key_file: valid_key_file - bearer_token: avalidtoken + bearer_token: mysecret - job_name: service-kubernetes @@ -144,7 +145,7 @@ scrape_configs: basic_auth: username: 'myusername' - password: 'mypassword' + password: 'mysecret' - job_name: service-kubernetes-namespaces @@ -168,7 +169,7 @@ scrape_configs: ec2_sd_configs: - region: us-east-1 access_key: access - secret_key: secret + secret_key: mysecret profile: profile - job_name: service-azure @@ -176,7 +177,7 @@ scrape_configs: - subscription_id: 11AAAA11-A11A-111A-A111-1111A1111A11 tenant_id: BBBB222B-B2B2-2B22-B222-2BB2222BB2B2 client_id: 333333CC-3C33-3333-CCC3-33C3CCCCC33C - client_secret: nAdvAK2oBuVym4IXix + client_secret: mysecret port: 9100 - job_name: service-nerve diff --git a/config/testdata/unknown_global_attr.bad.yml b/config/testdata/unknown_global_attr.bad.yml new file mode 100644 index 0000000000..169391f9c6 --- /dev/null +++ b/config/testdata/unknown_global_attr.bad.yml @@ -0,0 +1,2 @@ +global: + nonexistent_field: test diff --git a/discovery/azure/azure.go b/discovery/azure/azure.go index 34ee5de4de..4f96e5a216 100644 --- a/discovery/azure/azure.go +++ b/discovery/azure/azure.go @@ -66,14 +66,16 @@ type Discovery struct { cfg *config.AzureSDConfig interval time.Duration port int + logger log.Logger } // NewDiscovery returns a new AzureDiscovery which periodically refreshes its targets. -func NewDiscovery(cfg *config.AzureSDConfig) *Discovery { +func NewDiscovery(cfg *config.AzureSDConfig, logger log.Logger) *Discovery { return &Discovery{ cfg: cfg, interval: time.Duration(cfg.RefreshInterval), port: cfg.Port, + logger: logger, } } @@ -91,7 +93,7 @@ func (d *Discovery) Run(ctx context.Context, ch chan<- []*config.TargetGroup) { tg, err := d.refresh() if err != nil { - log.Errorf("unable to refresh during Azure discovery: %s", err) + d.logger.Errorf("unable to refresh during Azure discovery: %s", err) } else { select { case <-ctx.Done(): @@ -120,7 +122,7 @@ func createAzureClient(cfg config.AzureSDConfig) (azureClient, error) { if err != nil { return azureClient{}, err } - spt, err := azure.NewServicePrincipalToken(*oauthConfig, cfg.ClientID, cfg.ClientSecret, azure.PublicCloud.ResourceManagerEndpoint) + spt, err := azure.NewServicePrincipalToken(*oauthConfig, cfg.ClientID, string(cfg.ClientSecret), azure.PublicCloud.ResourceManagerEndpoint) if err != nil { return azureClient{}, err } @@ -141,13 +143,13 @@ type azureResource struct { } // Create a new azureResource object from an ID string. -func newAzureResourceFromID(id string) (azureResource, error) { +func newAzureResourceFromID(id string, logger log.Logger) (azureResource, error) { // Resource IDs have the following format. // /subscriptions/SUBSCRIPTION_ID/resourceGroups/RESOURCE_GROUP/providers/PROVIDER/TYPE/NAME s := strings.Split(id, "/") if len(s) != 9 { err := fmt.Errorf("invalid ID '%s'. Refusing to create azureResource", id) - log.Error(err) + logger.Error(err) return azureResource{}, err } return azureResource{ @@ -185,7 +187,7 @@ func (d *Discovery) refresh() (tg *config.TargetGroup, err error) { } machines = append(machines, *result.Value...) } - log.Debugf("Found %d virtual machines during Azure discovery.", len(machines)) + d.logger.Debugf("Found %d virtual machines during Azure discovery.", len(machines)) // We have the slice of machines. Now turn them into targets. // Doing them in go routines because the network interface calls are slow. @@ -197,7 +199,7 @@ func (d *Discovery) refresh() (tg *config.TargetGroup, err error) { ch := make(chan target, len(machines)) for i, vm := range machines { go func(i int, vm compute.VirtualMachine) { - r, err := newAzureResourceFromID(*vm.ID) + r, err := newAzureResourceFromID(*vm.ID, d.logger) if err != nil { ch <- target{labelSet: nil, err: err} return @@ -219,14 +221,14 @@ func (d *Discovery) refresh() (tg *config.TargetGroup, err error) { // Get the IP address information via separate call to the network provider. for _, nic := range *vm.Properties.NetworkProfile.NetworkInterfaces { - r, err := newAzureResourceFromID(*nic.ID) + r, err := newAzureResourceFromID(*nic.ID, d.logger) if err != nil { ch <- target{labelSet: nil, err: err} return } networkInterface, err := client.nic.Get(r.ResourceGroup, r.Name, "") if err != nil { - log.Errorf("Unable to get network interface %s: %s", r.Name, err) + d.logger.Errorf("Unable to get network interface %s: %s", r.Name, err) ch <- target{labelSet: nil, err: err} // Get out of this routine because we cannot continue without a network interface. return @@ -237,7 +239,7 @@ func (d *Discovery) refresh() (tg *config.TargetGroup, err error) { // yet support this. On deallocated machines, this value happens to be nil so it // is a cheap and easy way to determine if a machine is allocated or not. if networkInterface.Properties.Primary == nil { - log.Debugf("Virtual machine %s is deallocated. Skipping during Azure SD.", *vm.Name) + d.logger.Debugf("Virtual machine %s is deallocated. Skipping during Azure SD.", *vm.Name) ch <- target{} return } @@ -272,6 +274,6 @@ func (d *Discovery) refresh() (tg *config.TargetGroup, err error) { } } - log.Debugf("Azure discovery completed.") + d.logger.Debugf("Azure discovery completed.") return tg, nil } diff --git a/discovery/consul/consul.go b/discovery/consul/consul.go index 562a2700d3..d842ca2813 100644 --- a/discovery/consul/consul.go +++ b/discovery/consul/consul.go @@ -89,10 +89,11 @@ type Discovery struct { clientDatacenter string tagSeparator string watchedServices []string // Set of services which will be discovered. + logger log.Logger } // NewDiscovery returns a new Discovery for the given config. -func NewDiscovery(conf *config.ConsulSDConfig) (*Discovery, error) { +func NewDiscovery(conf *config.ConsulSDConfig, logger log.Logger) (*Discovery, error) { tls, err := httputil.NewTLSConfig(conf.TLSConfig) if err != nil { return nil, err @@ -104,10 +105,10 @@ func NewDiscovery(conf *config.ConsulSDConfig) (*Discovery, error) { Address: conf.Server, Scheme: conf.Scheme, Datacenter: conf.Datacenter, - Token: conf.Token, + Token: string(conf.Token), HttpAuth: &consul.HttpBasicAuth{ Username: conf.Username, - Password: conf.Password, + Password: string(conf.Password), }, HttpClient: wrapper, } @@ -121,6 +122,7 @@ func NewDiscovery(conf *config.ConsulSDConfig) (*Discovery, error) { tagSeparator: conf.TagSeparator, watchedServices: conf.Services, clientDatacenter: clientConf.Datacenter, + logger: logger, } return cd, nil } @@ -163,7 +165,7 @@ func (d *Discovery) Run(ctx context.Context, ch chan<- []*config.TargetGroup) { } if err != nil { - log.Errorf("Error refreshing service list: %s", err) + d.logger.Errorf("Error refreshing service list: %s", err) rpcFailuresCount.Inc() time.Sleep(retryInterval) continue @@ -179,7 +181,7 @@ func (d *Discovery) Run(ctx context.Context, ch chan<- []*config.TargetGroup) { if d.clientDatacenter == "" { info, err := d.client.Agent().Self() if err != nil { - log.Errorf("Error retrieving datacenter name: %s", err) + d.logger.Errorf("Error retrieving datacenter name: %s", err) time.Sleep(retryInterval) continue } @@ -203,6 +205,7 @@ func (d *Discovery) Run(ctx context.Context, ch chan<- []*config.TargetGroup) { datacenterLabel: model.LabelValue(d.clientDatacenter), }, tagSeparator: d.tagSeparator, + logger: d.logger, } wctx, cancel := context.WithCancel(ctx) @@ -235,6 +238,7 @@ type consulService struct { labels model.LabelSet client *consul.Client tagSeparator string + logger log.Logger } func (srv *consulService) watch(ctx context.Context, ch chan<- []*config.TargetGroup) { @@ -258,7 +262,7 @@ func (srv *consulService) watch(ctx context.Context, ch chan<- []*config.TargetG } if err != nil { - log.Errorf("Error refreshing service %s: %s", srv.name, err) + srv.logger.Errorf("Error refreshing service %s: %s", srv.name, err) rpcFailuresCount.Inc() time.Sleep(retryInterval) continue diff --git a/discovery/consul/consul_test.go b/discovery/consul/consul_test.go index 2040879b90..1e7cfd5292 100644 --- a/discovery/consul/consul_test.go +++ b/discovery/consul/consul_test.go @@ -16,13 +16,14 @@ package consul import ( "testing" + "github.com/prometheus/common/log" "github.com/prometheus/prometheus/config" ) func TestConfiguredService(t *testing.T) { conf := &config.ConsulSDConfig{ Services: []string{"configuredServiceName"}} - consulDiscovery, err := NewDiscovery(conf) + consulDiscovery, err := NewDiscovery(conf, log.Base()) if err != nil { t.Errorf("Unexpected error when initialising discovery %v", err) @@ -37,7 +38,7 @@ func TestConfiguredService(t *testing.T) { func TestNonConfiguredService(t *testing.T) { conf := &config.ConsulSDConfig{} - consulDiscovery, err := NewDiscovery(conf) + consulDiscovery, err := NewDiscovery(conf, log.Base()) if err != nil { t.Errorf("Unexpected error when initialising discovery %v", err) diff --git a/discovery/discovery.go b/discovery/discovery.go index 91d7eb7ab5..bf2e1cecdd 100644 --- a/discovery/discovery.go +++ b/discovery/discovery.go @@ -28,6 +28,7 @@ import ( "github.com/prometheus/prometheus/discovery/gce" "github.com/prometheus/prometheus/discovery/kubernetes" "github.com/prometheus/prometheus/discovery/marathon" + "github.com/prometheus/prometheus/discovery/openstack" "github.com/prometheus/prometheus/discovery/triton" "github.com/prometheus/prometheus/discovery/zookeeper" "golang.org/x/net/context" @@ -50,7 +51,7 @@ type TargetProvider interface { } // ProvidersFromConfig returns all TargetProviders configured in cfg. -func ProvidersFromConfig(cfg config.ServiceDiscoveryConfig) map[string]TargetProvider { +func ProvidersFromConfig(cfg config.ServiceDiscoveryConfig, logger log.Logger) map[string]TargetProvider { providers := map[string]TargetProvider{} app := func(mech string, i int, tp TargetProvider) { @@ -58,59 +59,68 @@ func ProvidersFromConfig(cfg config.ServiceDiscoveryConfig) map[string]TargetPro } for i, c := range cfg.DNSSDConfigs { - app("dns", i, dns.NewDiscovery(c)) + app("dns", i, dns.NewDiscovery(c, logger)) } for i, c := range cfg.FileSDConfigs { - app("file", i, file.NewDiscovery(c)) + app("file", i, file.NewDiscovery(c, logger)) } for i, c := range cfg.ConsulSDConfigs { - k, err := consul.NewDiscovery(c) + k, err := consul.NewDiscovery(c, logger) if err != nil { - log.Errorf("Cannot create Consul discovery: %s", err) + logger.Errorf("Cannot create Consul discovery: %s", err) continue } app("consul", i, k) } for i, c := range cfg.MarathonSDConfigs { - m, err := marathon.NewDiscovery(c) + m, err := marathon.NewDiscovery(c, logger) if err != nil { - log.Errorf("Cannot create Marathon discovery: %s", err) + logger.Errorf("Cannot create Marathon discovery: %s", err) continue } app("marathon", i, m) } for i, c := range cfg.KubernetesSDConfigs { - k, err := kubernetes.New(log.Base(), c) + k, err := kubernetes.New(logger, c) if err != nil { - log.Errorf("Cannot create Kubernetes discovery: %s", err) + logger.Errorf("Cannot create Kubernetes discovery: %s", err) continue } app("kubernetes", i, k) } for i, c := range cfg.ServersetSDConfigs { - app("serverset", i, zookeeper.NewServersetDiscovery(c)) + app("serverset", i, zookeeper.NewServersetDiscovery(c, logger)) } for i, c := range cfg.NerveSDConfigs { - app("nerve", i, zookeeper.NewNerveDiscovery(c)) + app("nerve", i, zookeeper.NewNerveDiscovery(c, logger)) } for i, c := range cfg.EC2SDConfigs { - app("ec2", i, ec2.NewDiscovery(c)) + app("ec2", i, ec2.NewDiscovery(c, logger)) } - for i, c := range cfg.GCESDConfigs { - gced, err := gce.NewDiscovery(c) + for i, c := range cfg.OpenstackSDConfigs { + openstackd, err := openstack.NewDiscovery(c) if err != nil { - log.Errorf("Cannot initialize GCE discovery: %s", err) + log.Errorf("Cannot initialize OpenStack discovery: %s", err) + continue + } + app("openstack", i, openstackd) + } + + for i, c := range cfg.GCESDConfigs { + gced, err := gce.NewDiscovery(c, logger) + if err != nil { + logger.Errorf("Cannot initialize GCE discovery: %s", err) continue } app("gce", i, gced) } for i, c := range cfg.AzureSDConfigs { - app("azure", i, azure.NewDiscovery(c)) + app("azure", i, azure.NewDiscovery(c, logger)) } for i, c := range cfg.TritonSDConfigs { - t, err := triton.New(log.With("sd", "triton"), c) + t, err := triton.New(logger.With("sd", "triton"), c) if err != nil { - log.Errorf("Cannot create Triton discovery: %s", err) + logger.Errorf("Cannot create Triton discovery: %s", err) continue } app("triton", i, t) diff --git a/discovery/discovery_test.go b/discovery/discovery_test.go index d74b1df5e2..b98191c72f 100644 --- a/discovery/discovery_test.go +++ b/discovery/discovery_test.go @@ -16,6 +16,7 @@ package discovery import ( "testing" + "github.com/prometheus/common/log" "github.com/prometheus/prometheus/config" "golang.org/x/net/context" yaml "gopkg.in/yaml.v2" @@ -53,7 +54,7 @@ static_configs: go ts.Run(ctx) - ts.UpdateProviders(ProvidersFromConfig(*cfg)) + ts.UpdateProviders(ProvidersFromConfig(*cfg, log.Base())) <-called verifyPresence(ts.tgroups, "static/0/0", true) @@ -67,7 +68,7 @@ static_configs: t.Fatalf("Unable to load YAML config sTwo: %s", err) } - ts.UpdateProviders(ProvidersFromConfig(*cfg)) + ts.UpdateProviders(ProvidersFromConfig(*cfg, log.Base())) <-called verifyPresence(ts.tgroups, "static/0/0", true) diff --git a/discovery/dns/dns.go b/discovery/dns/dns.go index d5d506dedc..61ab2dd3a3 100644 --- a/discovery/dns/dns.go +++ b/discovery/dns/dns.go @@ -66,10 +66,11 @@ type Discovery struct { interval time.Duration port int qtype uint16 + logger log.Logger } // NewDiscovery returns a new Discovery which periodically refreshes its targets. -func NewDiscovery(conf *config.DNSSDConfig) *Discovery { +func NewDiscovery(conf *config.DNSSDConfig, logger log.Logger) *Discovery { qtype := dns.TypeSRV switch strings.ToUpper(conf.Type) { case "A": @@ -84,6 +85,7 @@ func NewDiscovery(conf *config.DNSSDConfig) *Discovery { interval: time.Duration(conf.RefreshInterval), qtype: qtype, port: conf.Port, + logger: logger, } } @@ -112,7 +114,7 @@ func (d *Discovery) refreshAll(ctx context.Context, ch chan<- []*config.TargetGr for _, name := range d.names { go func(n string) { if err := d.refresh(ctx, n, ch); err != nil { - log.Errorf("Error refreshing DNS targets: %s", err) + d.logger.Errorf("Error refreshing DNS targets: %s", err) } wg.Done() }(name) @@ -122,7 +124,7 @@ func (d *Discovery) refreshAll(ctx context.Context, ch chan<- []*config.TargetGr } func (d *Discovery) refresh(ctx context.Context, name string, ch chan<- []*config.TargetGroup) error { - response, err := lookupAll(name, d.qtype) + response, err := lookupAll(name, d.qtype, d.logger) dnsSDLookupsCount.Inc() if err != nil { dnsSDLookupFailuresCount.Inc() @@ -147,7 +149,7 @@ func (d *Discovery) refresh(ctx context.Context, name string, ch chan<- []*confi case *dns.AAAA: target = hostPort(addr.AAAA.String(), d.port) default: - log.Warnf("%q is not a valid SRV record", record) + d.logger.Warnf("%q is not a valid SRV record", record) continue } @@ -167,7 +169,7 @@ func (d *Discovery) refresh(ctx context.Context, name string, ch chan<- []*confi return nil } -func lookupAll(name string, qtype uint16) (*dns.Msg, error) { +func lookupAll(name string, qtype uint16, logger log.Logger) (*dns.Msg, error) { conf, err := dns.ClientConfigFromFile(resolvConf) if err != nil { return nil, fmt.Errorf("could not load resolv.conf: %s", err) @@ -181,7 +183,7 @@ func lookupAll(name string, qtype uint16) (*dns.Msg, error) { for _, lname := range conf.NameList(name) { response, err = lookup(lname, qtype, client, servAddr, false) if err != nil { - log. + logger. With("server", server). With("name", name). With("reason", err). diff --git a/discovery/ec2/ec2.go b/discovery/ec2/ec2.go index 56fa07f9a3..99cee5be3d 100644 --- a/discovery/ec2/ec2.go +++ b/discovery/ec2/ec2.go @@ -72,11 +72,12 @@ type Discovery struct { interval time.Duration profile string port int + logger log.Logger } // NewDiscovery returns a new EC2Discovery which periodically refreshes its targets. -func NewDiscovery(conf *config.EC2SDConfig) *Discovery { - creds := credentials.NewStaticCredentials(conf.AccessKey, conf.SecretKey, "") +func NewDiscovery(conf *config.EC2SDConfig, logger log.Logger) *Discovery { + creds := credentials.NewStaticCredentials(conf.AccessKey, string(conf.SecretKey), "") if conf.AccessKey == "" && conf.SecretKey == "" { creds = nil } @@ -88,6 +89,7 @@ func NewDiscovery(conf *config.EC2SDConfig) *Discovery { profile: conf.Profile, interval: time.Duration(conf.RefreshInterval), port: conf.Port, + logger: logger, } } @@ -99,7 +101,7 @@ func (d *Discovery) Run(ctx context.Context, ch chan<- []*config.TargetGroup) { // Get an initial set right away. tg, err := d.refresh() if err != nil { - log.Error(err) + d.logger.Error(err) } else { select { case ch <- []*config.TargetGroup{tg}: @@ -113,7 +115,7 @@ func (d *Discovery) Run(ctx context.Context, ch chan<- []*config.TargetGroup) { case <-ticker.C: tg, err := d.refresh() if err != nil { - log.Error(err) + d.logger.Error(err) continue } diff --git a/discovery/file/file.go b/discovery/file/file.go index 7e4d11eb42..b74c6e5102 100644 --- a/discovery/file/file.go +++ b/discovery/file/file.go @@ -63,13 +63,15 @@ type Discovery struct { // and how many target groups they contained. // This is used to detect deleted target groups. lastRefresh map[string]int + logger log.Logger } // NewDiscovery returns a new file discovery for the given paths. -func NewDiscovery(conf *config.FileSDConfig) *Discovery { +func NewDiscovery(conf *config.FileSDConfig, logger log.Logger) *Discovery { return &Discovery{ paths: conf.Files, interval: time.Duration(conf.RefreshInterval), + logger: logger, } } @@ -79,7 +81,7 @@ func (d *Discovery) listFiles() []string { for _, p := range d.paths { files, err := filepath.Glob(p) if err != nil { - log.Errorf("Error expanding glob %q: %s", p, err) + d.logger.Errorf("Error expanding glob %q: %s", p, err) continue } paths = append(paths, files...) @@ -100,7 +102,7 @@ func (d *Discovery) watchFiles() { p = "./" } if err := d.watcher.Add(p); err != nil { - log.Errorf("Error adding file watch for %q: %s", p, err) + d.logger.Errorf("Error adding file watch for %q: %s", p, err) } } } @@ -111,7 +113,7 @@ func (d *Discovery) Run(ctx context.Context, ch chan<- []*config.TargetGroup) { watcher, err := fsnotify.NewWatcher() if err != nil { - log.Errorf("Error creating file watcher: %s", err) + d.logger.Errorf("Error creating file watcher: %s", err) return } d.watcher = watcher @@ -149,7 +151,7 @@ func (d *Discovery) Run(ctx context.Context, ch chan<- []*config.TargetGroup) { case err := <-d.watcher.Errors: if err != nil { - log.Errorf("Error on file watch: %s", err) + d.logger.Errorf("Error on file watch: %s", err) } } } @@ -157,7 +159,7 @@ func (d *Discovery) Run(ctx context.Context, ch chan<- []*config.TargetGroup) { // stop shuts down the file watcher. func (d *Discovery) stop() { - log.Debugf("Stopping file discovery for %s...", d.paths) + d.logger.Debugf("Stopping file discovery for %s...", d.paths) done := make(chan struct{}) defer close(done) @@ -175,10 +177,10 @@ func (d *Discovery) stop() { } }() if err := d.watcher.Close(); err != nil { - log.Errorf("Error closing file watcher for %s: %s", d.paths, err) + d.logger.Errorf("Error closing file watcher for %s: %s", d.paths, err) } - log.Debugf("File discovery for %s stopped.", d.paths) + d.logger.Debugf("File discovery for %s stopped.", d.paths) } // refresh reads all files matching the discovery's patterns and sends the respective @@ -194,7 +196,7 @@ func (d *Discovery) refresh(ctx context.Context, ch chan<- []*config.TargetGroup tgroups, err := readFile(p) if err != nil { fileSDReadErrorsCount.Inc() - log.Errorf("Error reading file %q: %s", p, err) + d.logger.Errorf("Error reading file %q: %s", p, err) // Prevent deletion down below. ref[p] = d.lastRefresh[p] continue diff --git a/discovery/file/file_test.go b/discovery/file/file_test.go index 39ce91f476..f977e3b228 100644 --- a/discovery/file/file_test.go +++ b/discovery/file/file_test.go @@ -20,6 +20,7 @@ import ( "testing" "time" + "github.com/prometheus/common/log" "github.com/prometheus/common/model" "golang.org/x/net/context" @@ -41,7 +42,7 @@ func testFileSD(t *testing.T, ext string) { conf.RefreshInterval = model.Duration(1 * time.Hour) var ( - fsd = NewDiscovery(&conf) + fsd = NewDiscovery(&conf, log.Base()) ch = make(chan []*config.TargetGroup) ctx, cancel = context.WithCancel(context.Background()) ) @@ -60,7 +61,7 @@ func testFileSD(t *testing.T, ext string) { } defer newf.Close() - f, err := os.Open("fixtures/target_groups" + ext) + f, err := os.Open("fixtures/valid" + ext) if err != nil { t.Fatal(err) } diff --git a/discovery/file/fixtures/target_groups.json b/discovery/file/fixtures/valid.json similarity index 100% rename from discovery/file/fixtures/target_groups.json rename to discovery/file/fixtures/valid.json diff --git a/discovery/file/fixtures/target_groups.yml b/discovery/file/fixtures/valid.yml similarity index 100% rename from discovery/file/fixtures/target_groups.yml rename to discovery/file/fixtures/valid.yml diff --git a/discovery/gce/gce.go b/discovery/gce/gce.go index 44ac349fea..2b6df1b916 100644 --- a/discovery/gce/gce.go +++ b/discovery/gce/gce.go @@ -76,10 +76,11 @@ type Discovery struct { interval time.Duration port int tagSeparator string + logger log.Logger } // NewDiscovery returns a new Discovery which periodically refreshes its targets. -func NewDiscovery(conf *config.GCESDConfig) (*Discovery, error) { +func NewDiscovery(conf *config.GCESDConfig, logger log.Logger) (*Discovery, error) { gd := &Discovery{ project: conf.Project, zone: conf.Zone, @@ -87,6 +88,7 @@ func NewDiscovery(conf *config.GCESDConfig) (*Discovery, error) { interval: time.Duration(conf.RefreshInterval), port: conf.Port, tagSeparator: conf.TagSeparator, + logger: logger, } var err error gd.client, err = google.DefaultClient(oauth2.NoContext, compute.ComputeReadonlyScope) @@ -106,7 +108,7 @@ func (d *Discovery) Run(ctx context.Context, ch chan<- []*config.TargetGroup) { // Get an initial set right away. tg, err := d.refresh() if err != nil { - log.Error(err) + d.logger.Error(err) } else { select { case ch <- []*config.TargetGroup{tg}: @@ -122,7 +124,7 @@ func (d *Discovery) Run(ctx context.Context, ch chan<- []*config.TargetGroup) { case <-ticker.C: tg, err := d.refresh() if err != nil { - log.Error(err) + d.logger.Error(err) continue } select { diff --git a/discovery/kubernetes/kubernetes.go b/discovery/kubernetes/kubernetes.go index f468b63358..2b7208c3e3 100644 --- a/discovery/kubernetes/kubernetes.go +++ b/discovery/kubernetes/kubernetes.go @@ -124,7 +124,7 @@ func New(l log.Logger, conf *config.KubernetesSDConfig) (*Discovery, error) { Insecure: conf.TLSConfig.InsecureSkipVerify, }, } - token := conf.BearerToken + token := string(conf.BearerToken) if conf.BearerTokenFile != "" { bf, err := ioutil.ReadFile(conf.BearerTokenFile) if err != nil { @@ -136,7 +136,7 @@ func New(l log.Logger, conf *config.KubernetesSDConfig) (*Discovery, error) { if conf.BasicAuth != nil { kcfg.Username = conf.BasicAuth.Username - kcfg.Password = conf.BasicAuth.Password + kcfg.Password = string(conf.BasicAuth.Password) } } diff --git a/discovery/marathon/marathon.go b/discovery/marathon/marathon.go index 5d715d2b77..d1f2b2f6cd 100644 --- a/discovery/marathon/marathon.go +++ b/discovery/marathon/marathon.go @@ -85,16 +85,17 @@ type Discovery struct { lastRefresh map[string]*config.TargetGroup appsClient AppListClient token string + logger log.Logger } // NewDiscovery returns a new Marathon Discovery. -func NewDiscovery(conf *config.MarathonSDConfig) (*Discovery, error) { +func NewDiscovery(conf *config.MarathonSDConfig, logger log.Logger) (*Discovery, error) { tls, err := httputil.NewTLSConfig(conf.TLSConfig) if err != nil { return nil, err } - token := conf.BearerToken + token := string(conf.BearerToken) if conf.BearerTokenFile != "" { bf, err := ioutil.ReadFile(conf.BearerTokenFile) if err != nil { @@ -116,6 +117,7 @@ func NewDiscovery(conf *config.MarathonSDConfig) (*Discovery, error) { refreshInterval: time.Duration(conf.RefreshInterval), appsClient: fetchApps, token: token, + logger: logger, }, nil } @@ -128,7 +130,7 @@ func (d *Discovery) Run(ctx context.Context, ch chan<- []*config.TargetGroup) { case <-time.After(d.refreshInterval): err := d.updateServices(ctx, ch) if err != nil { - log.Errorf("Error while updating services: %s", err) + d.logger.Errorf("Error while updating services: %s", err) } } } @@ -167,7 +169,7 @@ func (d *Discovery) updateServices(ctx context.Context, ch chan<- []*config.Targ case <-ctx.Done(): return ctx.Err() case ch <- []*config.TargetGroup{{Source: source}}: - log.Debugf("Removing group for %s", source) + d.logger.Debugf("Removing group for %s", source) } } } diff --git a/discovery/marathon/marathon_test.go b/discovery/marathon/marathon_test.go index 913380ad61..95e420dd88 100644 --- a/discovery/marathon/marathon_test.go +++ b/discovery/marathon/marathon_test.go @@ -19,6 +19,7 @@ import ( "testing" "time" + "github.com/prometheus/common/log" "github.com/prometheus/common/model" "golang.org/x/net/context" @@ -32,7 +33,7 @@ var ( ) func testUpdateServices(client AppListClient, ch chan []*config.TargetGroup) error { - md, err := NewDiscovery(&conf) + md, err := NewDiscovery(&conf, log.Base()) if err != nil { return err } @@ -140,7 +141,7 @@ func TestMarathonSDSendGroup(t *testing.T) { func TestMarathonSDRemoveApp(t *testing.T) { var ch = make(chan []*config.TargetGroup, 1) - md, err := NewDiscovery(&conf) + md, err := NewDiscovery(&conf, log.Base()) if err != nil { t.Fatalf("%s", err) } @@ -176,7 +177,7 @@ func TestMarathonSDRunAndStop(t *testing.T) { ch = make(chan []*config.TargetGroup) doneCh = make(chan error) ) - md, err := NewDiscovery(&conf) + md, err := NewDiscovery(&conf, log.Base()) if err != nil { t.Fatalf("%s", err) } diff --git a/discovery/openstack/mock.go b/discovery/openstack/mock.go new file mode 100644 index 0000000000..9cb53f5992 --- /dev/null +++ b/discovery/openstack/mock.go @@ -0,0 +1,435 @@ +// Copyright 2017 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 openstack + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" +) + +// SDMock is the interface for the OpenStack mock +type SDMock struct { + t *testing.T + Server *httptest.Server + Mux *http.ServeMux +} + +// NewSDMock returns a new SDMock. +func NewSDMock(t *testing.T) *SDMock { + return &SDMock{ + t: t, + } +} + +// Endpoint returns the URI to the mock server +func (m *SDMock) Endpoint() string { + return m.Server.URL + "/" +} + +// Setup creates the mock server +func (m *SDMock) Setup() { + m.Mux = http.NewServeMux() + m.Server = httptest.NewServer(m.Mux) +} + +// ShutdownServer creates the mock server +func (m *SDMock) ShutdownServer() { + m.Server.Close() +} + +const tokenID = "cbc36478b0bd8e67e89469c7749d4127" + +func testMethod(t *testing.T, r *http.Request, expected string) { + if expected != r.Method { + t.Errorf("Request method = %v, expected %v", r.Method, expected) + } +} + +func testHeader(t *testing.T, r *http.Request, header string, expected string) { + if actual := r.Header.Get(header); expected != actual { + t.Errorf("Header %s = %s, expected %s", header, actual, expected) + } +} + +// HandleVersionsSuccessfully mocks version call +func (m *SDMock) HandleVersionsSuccessfully() { + m.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, ` + { + "versions": { + "values": [ + { + "status": "stable", + "id": "v3.0", + "links": [ + { "href": "%s", "rel": "self" } + ] + }, + { + "status": "stable", + "id": "v2.0", + "links": [ + { "href": "%s", "rel": "self" } + ] + } + ] + } + } + `, m.Endpoint()+"v3/", m.Endpoint()+"v2.0/") + }) +} + +// HandleAuthSuccessfully mocks auth call +func (m *SDMock) HandleAuthSuccessfully() { + m.Mux.HandleFunc("/v3/auth/tokens", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("X-Subject-Token", tokenID) + + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, ` + { + "token": { + "audit_ids": ["VcxU2JYqT8OzfUVvrjEITQ", "qNUTIJntTzO1-XUk5STybw"], + "catalog": [ + { + "endpoints": [ + { + "id": "39dc322ce86c4111b4f06c2eeae0841b", + "interface": "public", + "region": "RegionOne", + "url": "http://localhost:5000" + }, + { + "id": "ec642f27474842e78bf059f6c48f4e99", + "interface": "internal", + "region": "RegionOne", + "url": "http://localhost:5000" + }, + { + "id": "c609fc430175452290b62a4242e8a7e8", + "interface": "admin", + "region": "RegionOne", + "url": "http://localhost:35357" + } + ], + "id": "4363ae44bdf34a3981fde3b823cb9aa2", + "type": "identity", + "name": "keystone" + }, + { + "endpoints": [ + { + "id": "e2ffee808abc4a60916715b1d4b489dd", + "interface": "public", + "region": "RegionOne", + "region_id": "RegionOne", + "url": "%s" + } + ], + "id": "b7f2a5b1a019459cb956e43a8cb41e31", + "type": "compute" + } + + ], + "expires_at": "2013-02-27T18:30:59.999999Z", + "is_domain": false, + "issued_at": "2013-02-27T16:30:59.999999Z", + "methods": [ + "password" + ], + "project": { + "domain": { + "id": "1789d1", + "name": "example.com" + }, + "id": "263fd9", + "name": "project-x" + }, + "roles": [ + { + "id": "76e72a", + "name": "admin" + }, + { + "id": "f4f392", + "name": "member" + } + ], + "user": { + "domain": { + "id": "1789d1", + "name": "example.com" + }, + "id": "0ca8f6", + "name": "Joe", + "password_expires_at": "2016-11-06T15:32:17.000000" + } + } +} + `, m.Endpoint()) + }) +} + +const serverListBody = ` +{ + "servers": [ + { + "status": "ACTIVE", + "updated": "2014-09-25T13:10:10Z", + "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + "OS-EXT-SRV-ATTR:host": "devstack", + "addresses": { + "private": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:7c:1b:2b", + "version": 4, + "addr": "10.0.0.32", + "OS-EXT-IPS:type": "fixed" + } + ] + }, + "links": [ + { + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + "rel": "self" + }, + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + "rel": "bookmark" + } + ], + "key_name": null, + "image": { + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "rel": "bookmark" + } + ] + }, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-EXT-SRV-ATTR:instance_name": "instance-0000001e", + "OS-SRV-USG:launched_at": "2014-09-25T13:10:10.000000", + "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack", + "flavor": { + "id": "1", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark" + } + ] + }, + "id": "ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + "security_groups": [ + { + "name": "default" + } + ], + "OS-SRV-USG:terminated_at": null, + "OS-EXT-AZ:availability_zone": "nova", + "user_id": "9349aff8be7545ac9d2f1d00999a23cd", + "name": "herp", + "created": "2014-09-25T13:10:02Z", + "tenant_id": "fcad67a6189847c4aecfa3c81a05783b", + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "", + "metadata": {} + }, + { + "status": "ACTIVE", + "updated": "2014-09-25T13:04:49Z", + "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + "OS-EXT-SRV-ATTR:host": "devstack", + "addresses": { + "private": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be", + "version": 4, + "addr": "10.0.0.31", + "OS-EXT-IPS:type": "fixed" + } + ] + }, + "links": [ + { + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "self" + }, + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "bookmark" + } + ], + "key_name": null, + "image": { + "id": "f90f6034-2570-4974-8351-6b49732ef2eb", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/images/f90f6034-2570-4974-8351-6b49732ef2eb", + "rel": "bookmark" + } + ] + }, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-EXT-SRV-ATTR:instance_name": "instance-0000001d", + "OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000", + "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack", + "flavor": { + "id": "1", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark" + } + ] + }, + "id": "9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "security_groups": [ + { + "name": "default" + } + ], + "OS-SRV-USG:terminated_at": null, + "OS-EXT-AZ:availability_zone": "nova", + "user_id": "9349aff8be7545ac9d2f1d00999a23cd", + "name": "derp", + "created": "2014-09-25T13:04:41Z", + "tenant_id": "fcad67a6189847c4aecfa3c81a05783b", + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "", + "metadata": {} + }, + { + "status": "ACTIVE", + "updated": "2014-09-25T13:04:49Z", + "hostId": "29d3c8c896a45aa4c34e52247875d7fefc3d94bbcc9f622b5d204362", + "OS-EXT-SRV-ATTR:host": "devstack", + "addresses": { + "private": [ + { + "OS-EXT-IPS-MAC:mac_addr": "fa:16:3e:9e:89:be", + "version": 4, + "addr": "10.0.0.31", + "OS-EXT-IPS:type": "fixed" + } + ] + }, + "links": [ + { + "href": "http://104.130.131.164:8774/v2/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "self" + }, + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/servers/9e5476bd-a4ec-4653-93d6-72c93aa682ba", + "rel": "bookmark" + } + ], + "key_name": null, + "image": "", + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-EXT-SRV-ATTR:instance_name": "instance-0000001d", + "OS-SRV-USG:launched_at": "2014-09-25T13:04:49.000000", + "OS-EXT-SRV-ATTR:hypervisor_hostname": "devstack", + "flavor": { + "id": "1", + "links": [ + { + "href": "http://104.130.131.164:8774/fcad67a6189847c4aecfa3c81a05783b/flavors/1", + "rel": "bookmark" + } + ] + }, + "id": "9e5476bd-a4ec-4653-93d6-72c93aa682bb", + "security_groups": [ + { + "name": "default" + } + ], + "OS-SRV-USG:terminated_at": null, + "OS-EXT-AZ:availability_zone": "nova", + "user_id": "9349aff8be7545ac9d2f1d00999a23cd", + "name": "merp", + "created": "2014-09-25T13:04:41Z", + "tenant_id": "fcad67a6189847c4aecfa3c81a05783b", + "OS-DCF:diskConfig": "MANUAL", + "os-extended-volumes:volumes_attached": [], + "accessIPv4": "", + "accessIPv6": "", + "progress": 0, + "OS-EXT-STS:power_state": 1, + "config_drive": "", + "metadata": {} + } + ] +} +` + +// HandleServerListSuccessfully mocks server detail call +func (m *SDMock) HandleServerListSuccessfully() { + m.Mux.HandleFunc("/servers/detail", func(w http.ResponseWriter, r *http.Request) { + testMethod(m.t, r, "GET") + testHeader(m.t, r, "X-Auth-Token", tokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, serverListBody) + }) +} + +const listOutput = ` +{ + "floating_ips": [ + { + "fixed_ip": null, + "id": "1", + "instance_id": null, + "ip": "10.10.10.1", + "pool": "nova" + }, + { + "fixed_ip": "166.78.185.201", + "id": "2", + "instance_id": "ef079b0c-e610-4dfb-b1aa-b49f07ac48e5", + "ip": "10.10.10.2", + "pool": "nova" + } + ] +} +` + +// HandleFloatingIPListSuccessfully mocks floating ips call +func (m *SDMock) HandleFloatingIPListSuccessfully() { + m.Mux.HandleFunc("/os-floating-ips", func(w http.ResponseWriter, r *http.Request) { + testMethod(m.t, r, "GET") + testHeader(m.t, r, "X-Auth-Token", tokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, listOutput) + }) +} diff --git a/discovery/openstack/openstack.go b/discovery/openstack/openstack.go new file mode 100644 index 0000000000..4011b9253b --- /dev/null +++ b/discovery/openstack/openstack.go @@ -0,0 +1,256 @@ +// Copyright 2017 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 openstack + +import ( + "fmt" + "net" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + "github.com/gophercloud/gophercloud/pagination" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/common/log" + "github.com/prometheus/common/model" + "golang.org/x/net/context" + + "github.com/prometheus/prometheus/config" + "github.com/prometheus/prometheus/util/strutil" +) + +const ( + openstackLabelPrefix = model.MetaLabelPrefix + "openstack_" + openstackLabelInstanceID = openstackLabelPrefix + "instance_id" + openstackLabelInstanceName = openstackLabelPrefix + "instance_name" + openstackLabelInstanceStatus = openstackLabelPrefix + "instance_status" + openstackLabelInstanceFlavor = openstackLabelPrefix + "instance_flavor" + openstackLabelPublicIP = openstackLabelPrefix + "public_ip" + openstackLabelPrivateIP = openstackLabelPrefix + "private_ip" + openstackLabelTagPrefix = openstackLabelPrefix + "tag_" +) + +var ( + refreshFailuresCount = prometheus.NewCounter( + prometheus.CounterOpts{ + Name: "prometheus_sd_openstack_refresh_failures_total", + Help: "The number of OpenStack-SD scrape failures.", + }) + refreshDuration = prometheus.NewSummary( + prometheus.SummaryOpts{ + Name: "prometheus_sd_openstack_refresh_duration_seconds", + Help: "The duration of an OpenStack-SD refresh in seconds.", + }) +) + +func init() { + prometheus.MustRegister(refreshFailuresCount) + prometheus.MustRegister(refreshDuration) +} + +// Discovery periodically performs OpenStack-SD requests. It implements +// the TargetProvider interface. +type Discovery struct { + authOpts *gophercloud.AuthOptions + region string + interval time.Duration + port int +} + +// NewDiscovery returns a new OpenStackDiscovery which periodically refreshes its targets. +func NewDiscovery(conf *config.OpenstackSDConfig) (*Discovery, error) { + opts := gophercloud.AuthOptions{ + IdentityEndpoint: conf.IdentityEndpoint, + Username: conf.Username, + UserID: conf.UserID, + Password: string(conf.Password), + TenantName: conf.ProjectName, + TenantID: conf.ProjectID, + DomainName: conf.DomainName, + DomainID: conf.DomainID, + } + + return &Discovery{ + authOpts: &opts, + region: conf.Region, + interval: time.Duration(conf.RefreshInterval), + port: conf.Port, + }, nil +} + +// Run implements the TargetProvider interface. +func (d *Discovery) Run(ctx context.Context, ch chan<- []*config.TargetGroup) { + // Get an initial set right away. + tg, err := d.refresh() + if err != nil { + log.Error(err) + } else { + select { + case ch <- []*config.TargetGroup{tg}: + case <-ctx.Done(): + return + } + } + + ticker := time.NewTicker(d.interval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + tg, err := d.refresh() + if err != nil { + log.Error(err) + continue + } + + select { + case ch <- []*config.TargetGroup{tg}: + case <-ctx.Done(): + return + } + case <-ctx.Done(): + return + } + } +} + +func (d *Discovery) refresh() (tg *config.TargetGroup, err error) { + t0 := time.Now() + defer func() { + refreshDuration.Observe(time.Since(t0).Seconds()) + if err != nil { + refreshFailuresCount.Inc() + } + }() + + provider, err := openstack.AuthenticatedClient(*d.authOpts) + + if err != nil { + return nil, fmt.Errorf("could not create OpenStack session: %s", err) + } + client, err := openstack.NewComputeV2(provider, gophercloud.EndpointOpts{ + Region: d.region, + }) + + if err != nil { + return nil, fmt.Errorf("could not create OpenStack compute session: %s", err) + } + + opts := servers.ListOpts{} + pager := servers.List(client, opts) + + tg = &config.TargetGroup{ + Source: fmt.Sprintf("OS_%s", d.region), + } + + pagerFIP := floatingips.List(client) + floatingIPList := make(map[string][]string) + + err = pagerFIP.EachPage(func(page pagination.Page) (bool, error) { + result, err := floatingips.ExtractFloatingIPs(page) + if err != nil { + log.Warn(err) + } + for _, ip := range result { + // Skip not associated ips + if ip.InstanceID != "" { + floatingIPList[ip.InstanceID] = append(floatingIPList[ip.InstanceID], ip.IP) + } + } + return true, nil + }) + + if err != nil { + return nil, fmt.Errorf("could not describe floating IPs: %s", err) + } + + err = pager.EachPage(func(page pagination.Page) (bool, error) { + serverList, err := servers.ExtractServers(page) + if err != nil { + return false, fmt.Errorf("could not extract servers: %s", err) + } + + for _, s := range serverList { + labels := model.LabelSet{ + openstackLabelInstanceID: model.LabelValue(s.ID), + } + + for _, address := range s.Addresses { + md, ok := address.([]interface{}) + if !ok { + log.Warn("Invalid type for address, expected array") + continue + } + + if len(md) == 0 { + log.Debugf("Got no IP address for instance %s", s.ID) + continue + } + + md1, ok := md[0].(map[string]interface{}) + if !ok { + log.Warn("Invalid type for address, expected dict") + continue + } + + addr, ok := md1["addr"].(string) + if !ok { + log.Warn("Invalid type for address, expected string") + continue + } + + labels[openstackLabelPrivateIP] = model.LabelValue(addr) + + addr = net.JoinHostPort(addr, fmt.Sprintf("%d", d.port)) + + labels[model.AddressLabel] = model.LabelValue(addr) + + // Only use first private IP + break + } + + if val, ok := floatingIPList[s.ID]; ok { + if len(val) > 0 { + labels[openstackLabelPublicIP] = model.LabelValue(val[0]) + } + } + + labels[openstackLabelInstanceStatus] = model.LabelValue(s.Status) + labels[openstackLabelInstanceName] = model.LabelValue(s.Name) + id, ok := s.Flavor["id"].(string) + if !ok { + log.Warn("Invalid type for instance id, excepted string") + continue + } + labels[openstackLabelInstanceFlavor] = model.LabelValue(id) + + for k, v := range s.Metadata { + name := strutil.SanitizeLabelName(k) + labels[openstackLabelTagPrefix+model.LabelName(name)] = model.LabelValue(v) + } + + tg.Targets = append(tg.Targets, labels) + } + return true, nil + }) + + if err != nil { + return nil, fmt.Errorf("could not describe instances: %s", err) + } + + return tg, nil +} diff --git a/discovery/openstack/openstack_test.go b/discovery/openstack/openstack_test.go new file mode 100644 index 0000000000..a6285be2da --- /dev/null +++ b/discovery/openstack/openstack_test.go @@ -0,0 +1,87 @@ +// Copyright 2017 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 openstack + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/config" +) + +type OpenstackSDTestSuite struct { + suite.Suite + Mock *SDMock +} + +func (s *OpenstackSDTestSuite) TearDownSuite() { + s.Mock.ShutdownServer() +} + +func (s *OpenstackSDTestSuite) SetupTest() { + s.Mock = NewSDMock(s.T()) + s.Mock.Setup() + + s.Mock.HandleServerListSuccessfully() + s.Mock.HandleFloatingIPListSuccessfully() + + s.Mock.HandleVersionsSuccessfully() + s.Mock.HandleAuthSuccessfully() +} + +func TestOpenstackSDSuite(t *testing.T) { + suite.Run(t, new(OpenstackSDTestSuite)) +} + +func (s *OpenstackSDTestSuite) openstackAuthSuccess() (*Discovery, error) { + conf := config.OpenstackSDConfig{ + IdentityEndpoint: s.Mock.Endpoint(), + Password: "test", + Username: "test", + DomainName: "12345", + Region: "RegionOne", + } + + return NewDiscovery(&conf) +} + +func (s *OpenstackSDTestSuite) TestOpenstackSDRefresh() { + d, _ := s.openstackAuthSuccess() + + tg, err := d.refresh() + assert.Nil(s.T(), err) + require.NotNil(s.T(), tg) + require.NotNil(s.T(), tg.Targets) + require.Len(s.T(), tg.Targets, 3) + + assert.Equal(s.T(), tg.Targets[0]["__address__"], model.LabelValue("10.0.0.32:0")) + assert.Equal(s.T(), tg.Targets[0]["__meta_openstack_instance_flavor"], model.LabelValue("1")) + assert.Equal(s.T(), tg.Targets[0]["__meta_openstack_instance_id"], model.LabelValue("ef079b0c-e610-4dfb-b1aa-b49f07ac48e5")) + assert.Equal(s.T(), tg.Targets[0]["__meta_openstack_instance_name"], model.LabelValue("herp")) + assert.Equal(s.T(), tg.Targets[0]["__meta_openstack_instance_status"], model.LabelValue("ACTIVE")) + assert.Equal(s.T(), tg.Targets[0]["__meta_openstack_private_ip"], model.LabelValue("10.0.0.32")) + assert.Equal(s.T(), tg.Targets[0]["__meta_openstack_public_ip"], model.LabelValue("10.10.10.2")) + + assert.Equal(s.T(), tg.Targets[1]["__address__"], model.LabelValue("10.0.0.31:0")) + assert.Equal(s.T(), tg.Targets[1]["__meta_openstack_instance_flavor"], model.LabelValue("1")) + assert.Equal(s.T(), tg.Targets[1]["__meta_openstack_instance_id"], model.LabelValue("9e5476bd-a4ec-4653-93d6-72c93aa682ba")) + assert.Equal(s.T(), tg.Targets[1]["__meta_openstack_instance_name"], model.LabelValue("derp")) + assert.Equal(s.T(), tg.Targets[1]["__meta_openstack_instance_status"], model.LabelValue("ACTIVE")) + assert.Equal(s.T(), tg.Targets[1]["__meta_openstack_private_ip"], model.LabelValue("10.0.0.31")) + +} diff --git a/discovery/zookeeper/zookeeper.go b/discovery/zookeeper/zookeeper.go index 0b8f1943b2..ba6e9d5fa3 100644 --- a/discovery/zookeeper/zookeeper.go +++ b/discovery/zookeeper/zookeeper.go @@ -24,6 +24,7 @@ import ( "github.com/samuel/go-zookeeper/zk" "golang.org/x/net/context" + "github.com/prometheus/common/log" "github.com/prometheus/prometheus/config" "github.com/prometheus/prometheus/util/strutil" "github.com/prometheus/prometheus/util/treecache" @@ -39,17 +40,18 @@ type Discovery struct { updates chan treecache.ZookeeperTreeCacheEvent treeCaches []*treecache.ZookeeperTreeCache - parse func(data []byte, path string) (model.LabelSet, error) + parse func(data []byte, path string) (model.LabelSet, error) + logger log.Logger } // NewNerveDiscovery returns a new Discovery for the given Nerve config. -func NewNerveDiscovery(conf *config.NerveSDConfig) *Discovery { - return NewDiscovery(conf.Servers, time.Duration(conf.Timeout), conf.Paths, parseNerveMember) +func NewNerveDiscovery(conf *config.NerveSDConfig, logger log.Logger) *Discovery { + return NewDiscovery(conf.Servers, time.Duration(conf.Timeout), conf.Paths, logger, parseNerveMember) } // NewServersetDiscovery returns a new Discovery for the given serverset config. -func NewServersetDiscovery(conf *config.ServersetSDConfig) *Discovery { - return NewDiscovery(conf.Servers, time.Duration(conf.Timeout), conf.Paths, parseServersetMember) +func NewServersetDiscovery(conf *config.ServersetSDConfig, logger log.Logger) *Discovery { + return NewDiscovery(conf.Servers, time.Duration(conf.Timeout), conf.Paths, logger, parseServersetMember) } // NewDiscovery returns a new discovery along Zookeeper parses with @@ -58,6 +60,7 @@ func NewDiscovery( srvs []string, timeout time.Duration, paths []string, + logger log.Logger, pf func(data []byte, path string) (model.LabelSet, error), ) *Discovery { conn, _, err := zk.Connect(srvs, timeout) @@ -71,6 +74,7 @@ func NewDiscovery( updates: updates, sources: map[string]*config.TargetGroup{}, parse: pf, + logger: logger, } for _, path := range paths { sd.treeCaches = append(sd.treeCaches, treecache.NewZookeeperTreeCache(conn, path, updates)) diff --git a/documentation/examples/remote_storage/remote_storage_adapter/influxdb/client.go b/documentation/examples/remote_storage/remote_storage_adapter/influxdb/client.go index edd204abc6..9b446a12c8 100644 --- a/documentation/examples/remote_storage/remote_storage_adapter/influxdb/client.go +++ b/documentation/examples/remote_storage/remote_storage_adapter/influxdb/client.go @@ -104,7 +104,7 @@ func (c *Client) Write(samples model.Samples) error { func (c *Client) Read(req *remote.ReadRequest) (*remote.ReadResponse, error) { labelsToSeries := map[string]*remote.TimeSeries{} for _, q := range req.Queries { - command, err := buildCommand(q) + command, err := c.buildCommand(q) if err != nil { return nil, err } @@ -134,7 +134,7 @@ func (c *Client) Read(req *remote.ReadRequest) (*remote.ReadResponse, error) { return &resp, nil } -func buildCommand(q *remote.Query) (string, error) { +func (c *Client) buildCommand(q *remote.Query) (string, error) { matchers := make([]string, 0, len(q.Matchers)) // If we don't find a metric name matcher, query all metrics // (InfluxDB measurements) by default. @@ -143,9 +143,9 @@ func buildCommand(q *remote.Query) (string, error) { if m.Name == model.MetricNameLabel { switch m.Type { case remote.MatchType_EQUAL: - from = fmt.Sprintf("FROM %q", m.Value) + from = fmt.Sprintf("FROM %q.%q", c.retentionPolicy, m.Value) case remote.MatchType_REGEX_MATCH: - from = fmt.Sprintf("FROM /^%s$/", escapeSlashes(m.Value)) + from = fmt.Sprintf("FROM %q./^%s$/", c.retentionPolicy, escapeSlashes(m.Value)) default: // TODO: Figure out how to support these efficiently. return "", fmt.Errorf("non-equal or regex-non-equal matchers are not supported on the metric name yet") diff --git a/notifier/notifier.go b/notifier/notifier.go index 90fe52bcd7..30279fb28f 100644 --- a/notifier/notifier.go +++ b/notifier/notifier.go @@ -113,6 +113,7 @@ type Notifier struct { alertmanagers []*alertmanagerSet cancelDiscovery func() + logger log.Logger } // Options are the configurable parameters of a Handler. @@ -204,7 +205,7 @@ func newAlertMetrics(r prometheus.Registerer, queueCap int, queueLen, alertmanag } // New constructs a new Notifier. -func New(o *Options) *Notifier { +func New(o *Options, logger log.Logger) *Notifier { ctx, cancel := context.WithCancel(context.Background()) if o.Do == nil { @@ -217,6 +218,7 @@ func New(o *Options) *Notifier { cancel: cancel, more: make(chan struct{}, 1), opts: o, + logger: logger, } queueLenFunc := func() float64 { return float64(n.queueLen()) } @@ -237,7 +239,7 @@ func (n *Notifier) ApplyConfig(conf *config.Config) error { ctx, cancel := context.WithCancel(n.ctx) for _, cfg := range conf.AlertingConfig.AlertmanagerConfigs { - ams, err := newAlertmanagerSet(cfg) + ams, err := newAlertmanagerSet(cfg, n.logger) if err != nil { return err } @@ -251,7 +253,7 @@ func (n *Notifier) ApplyConfig(conf *config.Config) error { // old ones. for _, ams := range amSets { go ams.ts.Run(ctx) - ams.ts.UpdateProviders(discovery.ProvidersFromConfig(ams.cfg.ServiceDiscoveryConfig)) + ams.ts.UpdateProviders(discovery.ProvidersFromConfig(ams.cfg.ServiceDiscoveryConfig, n.logger)) } if n.cancelDiscovery != nil { n.cancelDiscovery() @@ -335,7 +337,7 @@ func (n *Notifier) Send(alerts ...*Alert) { if d := len(alerts) - n.opts.QueueCapacity; d > 0 { alerts = alerts[d:] - log.Warnf("Alert batch larger than queue capacity, dropping %d alerts", d) + n.logger.Warnf("Alert batch larger than queue capacity, dropping %d alerts", d) n.metrics.dropped.Add(float64(d)) } @@ -344,7 +346,7 @@ func (n *Notifier) Send(alerts ...*Alert) { if d := (len(n.queue) + len(alerts)) - n.opts.QueueCapacity; d > 0 { n.queue = n.queue[d:] - log.Warnf("Alert notification queue full, dropping %d alerts", d) + n.logger.Warnf("Alert notification queue full, dropping %d alerts", d) n.metrics.dropped.Add(float64(d)) } n.queue = append(n.queue, alerts...) @@ -402,7 +404,7 @@ func (n *Notifier) sendAll(alerts ...*Alert) bool { b, err := json.Marshal(alerts) if err != nil { - log.Errorf("Encoding alerts failed: %s", err) + n.logger.Errorf("Encoding alerts failed: %s", err) return false } @@ -427,7 +429,7 @@ func (n *Notifier) sendAll(alerts ...*Alert) bool { u := am.url().String() if err := n.sendOne(ctx, ams.client, u, b); err != nil { - log.With("alertmanager", u).With("count", len(alerts)).Errorf("Error sending alerts: %s", err) + n.logger.With("alertmanager", u).With("count", len(alerts)).Errorf("Error sending alerts: %s", err) n.metrics.errors.WithLabelValues(u).Inc() } else { atomic.AddUint64(&numSuccess, 1) @@ -466,7 +468,7 @@ func (n *Notifier) sendOne(ctx context.Context, c *http.Client, url string, b [] // Stop shuts down the notification handler. func (n *Notifier) Stop() { - log.Info("Stopping notification handler...") + n.logger.Info("Stopping notification handler...") n.cancel() } @@ -496,11 +498,12 @@ type alertmanagerSet struct { metrics *alertMetrics - mtx sync.RWMutex - ams []alertmanager + mtx sync.RWMutex + ams []alertmanager + logger log.Logger } -func newAlertmanagerSet(cfg *config.AlertmanagerConfig) (*alertmanagerSet, error) { +func newAlertmanagerSet(cfg *config.AlertmanagerConfig, logger log.Logger) (*alertmanagerSet, error) { client, err := httputil.NewClientFromConfig(cfg.HTTPClientConfig) if err != nil { return nil, err @@ -508,6 +511,7 @@ func newAlertmanagerSet(cfg *config.AlertmanagerConfig) (*alertmanagerSet, error s := &alertmanagerSet{ client: client, cfg: cfg, + logger: logger, } s.ts = discovery.NewTargetSet(s) @@ -522,7 +526,7 @@ func (s *alertmanagerSet) Sync(tgs []*config.TargetGroup) { for _, tg := range tgs { ams, err := alertmanagerFromGroup(tg, s.cfg) if err != nil { - log.With("err", err).Error("generating discovered Alertmanagers failed") + s.logger.With("err", err).Error("generating discovered Alertmanagers failed") continue } all = append(all, ams...) diff --git a/notifier/notifier_test.go b/notifier/notifier_test.go index c21d58f0d2..29a6286e14 100644 --- a/notifier/notifier_test.go +++ b/notifier/notifier_test.go @@ -26,6 +26,7 @@ import ( "golang.org/x/net/context" + "github.com/prometheus/common/log" "github.com/prometheus/common/model" "github.com/prometheus/prometheus/config" "github.com/prometheus/prometheus/pkg/labels" @@ -64,7 +65,7 @@ func TestPostPath(t *testing.T) { } func TestHandlerNextBatch(t *testing.T) { - h := New(&Options{}) + h := New(&Options{}, log.Base()) for i := range make([]struct{}, 2*maxBatchSize+1) { h.queue = append(h.queue, &Alert{ @@ -151,7 +152,7 @@ func TestHandlerSendAll(t *testing.T) { defer server1.Close() defer server2.Close() - h := New(&Options{}) + h := New(&Options{}, log.Base()) h.alertmanagers = append(h.alertmanagers, &alertmanagerSet{ ams: []alertmanager{ alertmanagerMock{ @@ -214,7 +215,7 @@ func TestCustomDo(t *testing.T) { Body: ioutil.NopCloser(nil), }, nil }, - }) + }, log.Base()) h.sendOne(context.Background(), nil, testURL, []byte(testBody)) @@ -236,7 +237,7 @@ func TestExternalLabels(t *testing.T) { Replacement: "c", }, }, - }) + }, log.Base()) // This alert should get the external label attached. h.Send(&Alert{ @@ -276,7 +277,7 @@ func TestHandlerRelabel(t *testing.T) { Replacement: "renamed", }, }, - }) + }, log.Base()) // This alert should be dropped due to the configuration h.Send(&Alert{ @@ -322,7 +323,9 @@ func TestHandlerQueueing(t *testing.T) { h := New(&Options{ QueueCapacity: 3 * maxBatchSize, - }) + }, + log.Base(), + ) h.alertmanagers = append(h.alertmanagers, &alertmanagerSet{ ams: []alertmanager{ alertmanagerMock{ diff --git a/retrieval/scrape.go b/retrieval/scrape.go index 1812854bff..a55ffa9c3c 100644 --- a/retrieval/scrape.go +++ b/retrieval/scrape.go @@ -115,7 +115,7 @@ type scrapePool struct { } func newScrapePool(ctx context.Context, cfg *config.ScrapeConfig, app Appendable) *scrapePool { - client, err := NewHTTPClient(cfg.HTTPClientConfig) + client, err := httputil.NewClientFromConfig(cfg.HTTPClientConfig) if err != nil { // Any errors that could occur here should be caught during config validation. log.Errorf("Error creating HTTP client for job %q: %s", cfg.JobName, err) diff --git a/retrieval/target.go b/retrieval/target.go index 8ba6f46d31..7abaafffdc 100644 --- a/retrieval/target.go +++ b/retrieval/target.go @@ -17,9 +17,7 @@ import ( "errors" "fmt" "hash/fnv" - "io/ioutil" "net" - "net/http" "net/url" "strings" "sync" @@ -32,7 +30,6 @@ import ( "github.com/prometheus/prometheus/pkg/relabel" "github.com/prometheus/prometheus/pkg/value" "github.com/prometheus/prometheus/storage" - "github.com/prometheus/prometheus/util/httputil" ) // TargetHealth describes the health state of a target. @@ -70,44 +67,6 @@ func NewTarget(labels, discoveredLabels labels.Labels, params url.Values) *Targe } } -// NewHTTPClient returns a new HTTP client configured for the given scrape configuration. -func NewHTTPClient(cfg config.HTTPClientConfig) (*http.Client, error) { - tlsConfig, err := httputil.NewTLSConfig(cfg.TLSConfig) - if err != nil { - return nil, err - } - // The only timeout we care about is the configured scrape timeout. - // It is applied on request. So we leave out any timings here. - var rt http.RoundTripper = &http.Transport{ - Proxy: http.ProxyURL(cfg.ProxyURL.URL), - MaxIdleConns: 10000, - TLSClientConfig: tlsConfig, - DisableCompression: true, - } - - // If a bearer token is provided, create a round tripper that will set the - // Authorization header correctly on each request. - bearerToken := cfg.BearerToken - if len(bearerToken) == 0 && len(cfg.BearerTokenFile) > 0 { - b, err := ioutil.ReadFile(cfg.BearerTokenFile) - if err != nil { - return nil, fmt.Errorf("unable to read bearer token file %s: %s", cfg.BearerTokenFile, err) - } - bearerToken = strings.TrimSpace(string(b)) - } - - if len(bearerToken) > 0 { - rt = httputil.NewBearerAuthRoundTripper(bearerToken, rt) - } - - if cfg.BasicAuth != nil { - rt = httputil.NewBasicAuthRoundTripper(cfg.BasicAuth.Username, cfg.BasicAuth.Password, rt) - } - - // Return a new client with the configured round tripper. - return httputil.NewClient(rt), nil -} - func (t *Target) String() string { return t.URL().String() } diff --git a/retrieval/targetmanager.go b/retrieval/targetmanager.go index ede8c79f0a..aa8706db5c 100644 --- a/retrieval/targetmanager.go +++ b/retrieval/targetmanager.go @@ -38,6 +38,7 @@ type TargetManager struct { // Set of unqiue targets by scrape configuration. targetSets map[string]*targetSet + logger log.Logger } type targetSet struct { @@ -53,16 +54,17 @@ type Appendable interface { } // NewTargetManager creates a new TargetManager. -func NewTargetManager(app Appendable) *TargetManager { +func NewTargetManager(app Appendable, logger log.Logger) *TargetManager { return &TargetManager{ append: app, targetSets: map[string]*targetSet{}, + logger: logger, } } // Run starts background processing to handle target updates. func (tm *TargetManager) Run() { - log.Info("Starting target manager...") + tm.logger.Info("Starting target manager...") tm.mtx.Lock() @@ -76,7 +78,7 @@ func (tm *TargetManager) Run() { // Stop all background processing. func (tm *TargetManager) Stop() { - log.Infoln("Stopping target manager...") + tm.logger.Infoln("Stopping target manager...") tm.mtx.Lock() // Cancel the base context, this will cause all target providers to shut down @@ -88,7 +90,7 @@ func (tm *TargetManager) Stop() { // Wait for all scrape inserts to complete. tm.wg.Wait() - log.Debugln("Target manager stopped") + tm.logger.Debugln("Target manager stopped") } func (tm *TargetManager) reload() { @@ -122,7 +124,7 @@ func (tm *TargetManager) reload() { } else { ts.sp.reload(scfg) } - ts.ts.UpdateProviders(discovery.ProvidersFromConfig(scfg.ServiceDiscoveryConfig)) + ts.ts.UpdateProviders(discovery.ProvidersFromConfig(scfg.ServiceDiscoveryConfig, tm.logger)) } // Remove old target sets. Waiting for scrape pools to complete pending diff --git a/util/httputil/client.go b/util/httputil/client.go index 4123814328..2c01fd0fd7 100644 --- a/util/httputil/client.go +++ b/util/httputil/client.go @@ -42,14 +42,16 @@ func NewClientFromConfig(cfg config.HTTPClientConfig) (*http.Client, error) { // The only timeout we care about is the configured scrape timeout. // It is applied on request. So we leave out any timings here. var rt http.RoundTripper = &http.Transport{ - Proxy: http.ProxyURL(cfg.ProxyURL.URL), - DisableKeepAlives: true, - TLSClientConfig: tlsConfig, + Proxy: http.ProxyURL(cfg.ProxyURL.URL), + MaxIdleConns: 10000, + DisableKeepAlives: false, + TLSClientConfig: tlsConfig, + DisableCompression: true, } // If a bearer token is provided, create a round tripper that will set the // Authorization header correctly on each request. - bearerToken := cfg.BearerToken + bearerToken := string(cfg.BearerToken) if len(bearerToken) == 0 && len(cfg.BearerTokenFile) > 0 { b, err := ioutil.ReadFile(cfg.BearerTokenFile) if err != nil { @@ -63,7 +65,7 @@ func NewClientFromConfig(cfg config.HTTPClientConfig) (*http.Client, error) { } if cfg.BasicAuth != nil { - rt = NewBasicAuthRoundTripper(cfg.BasicAuth.Username, cfg.BasicAuth.Password, rt) + rt = NewBasicAuthRoundTripper(cfg.BasicAuth.Username, string(cfg.BasicAuth.Password), rt) } // Return a new client with the configured round tripper. diff --git a/vendor/github.com/gophercloud/gophercloud/CHANGELOG.md b/vendor/github.com/gophercloud/gophercloud/CHANGELOG.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/vendor/github.com/gophercloud/gophercloud/FAQ.md b/vendor/github.com/gophercloud/gophercloud/FAQ.md new file mode 100644 index 0000000000..88a366a288 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/FAQ.md @@ -0,0 +1,148 @@ +# Tips + +## Implementing default logging and re-authentication attempts + +You can implement custom logging and/or limit re-auth attempts by creating a custom HTTP client +like the following and setting it as the provider client's HTTP Client (via the +`gophercloud.ProviderClient.HTTPClient` field): + +```go +//... + +// LogRoundTripper satisfies the http.RoundTripper interface and is used to +// customize the default Gophercloud RoundTripper to allow for logging. +type LogRoundTripper struct { + rt http.RoundTripper + numReauthAttempts int +} + +// newHTTPClient return a custom HTTP client that allows for logging relevant +// information before and after the HTTP request. +func newHTTPClient() http.Client { + return http.Client{ + Transport: &LogRoundTripper{ + rt: http.DefaultTransport, + }, + } +} + +// RoundTrip performs a round-trip HTTP request and logs relevant information about it. +func (lrt *LogRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) { + glog.Infof("Request URL: %s\n", request.URL) + + response, err := lrt.rt.RoundTrip(request) + if response == nil { + return nil, err + } + + if response.StatusCode == http.StatusUnauthorized { + if lrt.numReauthAttempts == 3 { + return response, fmt.Errorf("Tried to re-authenticate 3 times with no success.") + } + lrt.numReauthAttempts++ + } + + glog.Debugf("Response Status: %s\n", response.Status) + + return response, nil +} + +endpoint := "https://127.0.0.1/auth" +pc := openstack.NewClient(endpoint) +pc.HTTPClient = newHTTPClient() + +//... +``` + + +## Implementing custom objects + +OpenStack request/response objects may differ among variable names or types. + +### Custom request objects + +To pass custom options to a request, implement the desired `OptsBuilder` interface. For +example, to pass in + +```go +type MyCreateServerOpts struct { + Name string + Size int +} +``` + +to `servers.Create`, simply implement the `servers.CreateOptsBuilder` interface: + +```go +func (o MyCreateServeropts) ToServerCreateMap() (map[string]interface{}, error) { + return map[string]interface{}{ + "name": o.Name, + "size": o.Size, + }, nil +} +``` + +create an instance of your custom options object, and pass it to `servers.Create`: + +```go +// ... +myOpts := MyCreateServerOpts{ + Name: "s1", + Size: "100", +} +server, err := servers.Create(computeClient, myOpts).Extract() +// ... +``` + +### Custom response objects + +Some OpenStack services have extensions. Extensions that are supported in Gophercloud can be +combined to create a custom object: + +```go +// ... +type MyVolume struct { + volumes.Volume + tenantattr.VolumeExt +} + +var v struct { + MyVolume `json:"volume"` +} + +err := volumes.Get(client, volID).ExtractInto(&v) +// ... +``` + +## Overriding default `UnmarshalJSON` method + +For some response objects, a field may be a custom type or may be allowed to take on +different types. In these cases, overriding the default `UnmarshalJSON` method may be +necessary. To do this, declare the JSON `struct` field tag as "-" and create an `UnmarshalJSON` +method on the type: + +```go +// ... +type MyVolume struct { + ID string `json: "id"` + TimeCreated time.Time `json: "-"` +} + +func (r *MyVolume) UnmarshalJSON(b []byte) error { + type tmp MyVolume + var s struct { + tmp + TimeCreated gophercloud.JSONRFC3339MilliNoZ `json:"created_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Volume(s.tmp) + + r.TimeCreated = time.Time(s.CreatedAt) + + return err +} +// ... +``` diff --git a/vendor/github.com/gophercloud/gophercloud/LICENSE b/vendor/github.com/gophercloud/gophercloud/LICENSE new file mode 100644 index 0000000000..fbbbc9e4cb --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/LICENSE @@ -0,0 +1,191 @@ +Copyright 2012-2013 Rackspace, Inc. + +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. + +------ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/vendor/github.com/gophercloud/gophercloud/MIGRATING.md b/vendor/github.com/gophercloud/gophercloud/MIGRATING.md new file mode 100644 index 0000000000..aa383c9cc9 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/MIGRATING.md @@ -0,0 +1,32 @@ +# Compute + +## Floating IPs + +* `github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingip` is now `github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips` +* `floatingips.Associate` and `floatingips.Disassociate` have been removed. +* `floatingips.DisassociateOpts` is now required to disassociate a Floating IP. + +## Security Groups + +* `secgroups.AddServerToGroup` is now `secgroups.AddServer`. +* `secgroups.RemoveServerFromGroup` is now `secgroups.RemoveServer`. + +## Servers + +* `servers.Reboot` now requires a `servers.RebootOpts` struct: + + ```golang + rebootOpts := &servers.RebootOpts{ + Type: servers.SoftReboot, + } + res := servers.Reboot(client, server.ID, rebootOpts) + ``` + +# Identity + +## V3 + +### Tokens + +* `Token.ExpiresAt` is now of type `gophercloud.JSONRFC3339Milli` instead of + `time.Time` diff --git a/vendor/github.com/gophercloud/gophercloud/README.md b/vendor/github.com/gophercloud/gophercloud/README.md new file mode 100644 index 0000000000..60ca479de8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/README.md @@ -0,0 +1,143 @@ +# Gophercloud: an OpenStack SDK for Go +[![Build Status](https://travis-ci.org/gophercloud/gophercloud.svg?branch=master)](https://travis-ci.org/gophercloud/gophercloud) +[![Coverage Status](https://coveralls.io/repos/github/gophercloud/gophercloud/badge.svg?branch=master)](https://coveralls.io/github/gophercloud/gophercloud?branch=master) + +Gophercloud is an OpenStack Go SDK. + +## Useful links + +* [Reference documentation](http://godoc.org/github.com/gophercloud/gophercloud) +* [Effective Go](https://golang.org/doc/effective_go.html) + +## How to install + +Before installing, you need to ensure that your [GOPATH environment variable](https://golang.org/doc/code.html#GOPATH) +is pointing to an appropriate directory where you want to install Gophercloud: + +```bash +mkdir $HOME/go +export GOPATH=$HOME/go +``` + +To protect yourself against changes in your dependencies, we highly recommend choosing a +[dependency management solution](https://github.com/golang/go/wiki/PackageManagementTools) for +your projects, such as [godep](https://github.com/tools/godep). Once this is set up, you can install +Gophercloud as a dependency like so: + +```bash +go get github.com/gophercloud/gophercloud + +# Edit your code to import relevant packages from "github.com/gophercloud/gophercloud" + +godep save ./... +``` + +This will install all the source files you need into a `Godeps/_workspace` directory, which is +referenceable from your own source files when you use the `godep go` command. + +## Getting started + +### Credentials + +Because you'll be hitting an API, you will need to retrieve your OpenStack +credentials and either store them as environment variables or in your local Go +files. The first method is recommended because it decouples credential +information from source code, allowing you to push the latter to your version +control system without any security risk. + +You will need to retrieve the following: + +* username +* password +* a valid Keystone identity URL + +For users that have the OpenStack dashboard installed, there's a shortcut. If +you visit the `project/access_and_security` path in Horizon and click on the +"Download OpenStack RC File" button at the top right hand corner, you will +download a bash file that exports all of your access details to environment +variables. To execute the file, run `source admin-openrc.sh` and you will be +prompted for your password. + +### Authentication + +Once you have access to your credentials, you can begin plugging them into +Gophercloud. The next step is authentication, and this is handled by a base +"Provider" struct. To get one, you can either pass in your credentials +explicitly, or tell Gophercloud to use environment variables: + +```go +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack" + "github.com/gophercloud/gophercloud/openstack/utils" +) + +// Option 1: Pass in the values yourself +opts := gophercloud.AuthOptions{ + IdentityEndpoint: "https://openstack.example.com:5000/v2.0", + Username: "{username}", + Password: "{password}", +} + +// Option 2: Use a utility function to retrieve all your environment variables +opts, err := openstack.AuthOptionsFromEnv() +``` + +Once you have the `opts` variable, you can pass it in and get back a +`ProviderClient` struct: + +```go +provider, err := openstack.AuthenticatedClient(opts) +``` + +The `ProviderClient` is the top-level client that all of your OpenStack services +derive from. The provider contains all of the authentication details that allow +your Go code to access the API - such as the base URL and token ID. + +### Provision a server + +Once we have a base Provider, we inject it as a dependency into each OpenStack +service. In order to work with the Compute API, we need a Compute service +client; which can be created like so: + +```go +client, err := openstack.NewComputeV2(provider, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), +}) +``` + +We then use this `client` for any Compute API operation we want. In our case, +we want to provision a new server - so we invoke the `Create` method and pass +in the flavor ID (hardware specification) and image ID (operating system) we're +interested in: + +```go +import "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + +server, err := servers.Create(client, servers.CreateOpts{ + Name: "My new server!", + FlavorRef: "flavor_id", + ImageRef: "image_id", +}).Extract() +``` + +The above code sample creates a new server with the parameters, and embodies the +new resource in the `server` variable (a +[`servers.Server`](http://godoc.org/github.com/gophercloud/gophercloud) struct). + +## Advanced Usage + +Have a look at the [FAQ](./FAQ.md) for some tips on customizing the way Gophercloud works. + +## Backwards-Compatibility Guarantees + +None. Vendor it and write tests covering the parts you use. + +## Contributing + +See the [contributing guide](./.github/CONTRIBUTING.md). + +## Help and feedback + +If you're struggling with something or have spotted a potential bug, feel free +to submit an issue to our [bug tracker](/issues). diff --git a/vendor/github.com/gophercloud/gophercloud/STYLEGUIDE.md b/vendor/github.com/gophercloud/gophercloud/STYLEGUIDE.md new file mode 100644 index 0000000000..5b49ef4882 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/STYLEGUIDE.md @@ -0,0 +1,74 @@ + +## On Pull Requests + +- Before you start a PR there needs to be a Github issue and a discussion about it + on that issue with a core contributor, even if it's just a 'SGTM'. + +- A PR's description must reference the issue it closes with a `For ` (e.g. For #293). + +- A PR's description must contain link(s) to the line(s) in the OpenStack + source code (on Github) that prove(s) the PR code to be valid. Links to documentation + are not good enough. The link(s) should be to a non-`master` branch. For example, + a pull request implementing the creation of a Neutron v2 subnet might put the + following link in the description: + + https://github.com/openstack/neutron/blob/stable/mitaka/neutron/api/v2/attributes.py#L749 + + From that link, a reviewer (or user) can verify the fields in the request/response + objects in the PR. + +- A PR that is in-progress should have `[wip]` in front of the PR's title. When + ready for review, remove the `[wip]` and ping a core contributor with an `@`. + +- Forcing PRs to be small can have the effect of users submitting PRs in a hierarchical chain, with + one depending on the next. If a PR depends on another one, it should have a [Pending #PRNUM] + prefix in the PR title. In addition, it will be the PR submitter's responsibility to remove the + [Pending #PRNUM] tag once the PR has been updated with the merged, dependent PR. That will + let reviewers know it is ready to review. + +- A PR should be small. Even if you intend on implementing an entire + service, a PR should only be one route of that service + (e.g. create server or get server, but not both). + +- Unless explicitly asked, do not squash commits in the middle of a review; only + append. It makes it difficult for the reviewer to see what's changed from one + review to the next. + +## On Code + +- In re design: follow as closely as is reasonable the code already in the library. + Most operations (e.g. create, delete) admit the same design. + +- Unit tests and acceptance (integration) tests must be written to cover each PR. + Tests for operations with several options (e.g. list, create) should include all + the options in the tests. This will allow users to verify an operation on their + own infrastructure and see an example of usage. + +- If in doubt, ask in-line on the PR. + +### File Structure + +- The following should be used in most cases: + + - `requests.go`: contains all the functions that make HTTP requests and the + types associated with the HTTP request (parameters for URL, body, etc) + - `results.go`: contains all the response objects and their methods + - `urls.go`: contains the endpoints to which the requests are made + +### Naming + +- For methods on a type in `response.go`, the receiver should be named `r` and the + variable into which it will be unmarshalled `s`. + +- Functions in `requests.go`, with the exception of functions that return a + `pagination.Pager`, should be named returns of the name `r`. + +- Functions in `requests.go` that accept request bodies should accept as their + last parameter an `interface` named `OptsBuilder` (eg `CreateOptsBuilder`). + This `interface` should have at the least a method named `ToMap` + (eg `ToPortCreateMap`). + +- Functions in `requests.go` that accept query strings should accept as their + last parameter an `interface` named `OptsBuilder` (eg `ListOptsBuilder`). + This `interface` should have at the least a method named `ToQuery` + (eg `ToServerListQuery`). diff --git a/vendor/github.com/gophercloud/gophercloud/auth_options.go b/vendor/github.com/gophercloud/gophercloud/auth_options.go new file mode 100644 index 0000000000..eabf182075 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/auth_options.go @@ -0,0 +1,327 @@ +package gophercloud + +/* +AuthOptions stores information needed to authenticate to an OpenStack Cloud. +You can populate one manually, or use a provider's AuthOptionsFromEnv() function +to read relevant information from the standard environment variables. Pass one +to a provider's AuthenticatedClient function to authenticate and obtain a +ProviderClient representing an active session on that provider. + +Its fields are the union of those recognized by each identity implementation and +provider. +*/ +type AuthOptions struct { + // IdentityEndpoint specifies the HTTP endpoint that is required to work with + // the Identity API of the appropriate version. While it's ultimately needed by + // all of the identity services, it will often be populated by a provider-level + // function. + IdentityEndpoint string `json:"-"` + + // Username is required if using Identity V2 API. Consult with your provider's + // control panel to discover your account's username. In Identity V3, either + // UserID or a combination of Username and DomainID or DomainName are needed. + Username string `json:"username,omitempty"` + UserID string `json:"id,omitempty"` + + Password string `json:"password,omitempty"` + + // At most one of DomainID and DomainName must be provided if using Username + // with Identity V3. Otherwise, either are optional. + DomainID string `json:"id,omitempty"` + DomainName string `json:"name,omitempty"` + + // The TenantID and TenantName fields are optional for the Identity V2 API. + // The same fields are known as project_id and project_name in the Identity + // V3 API, but are collected as TenantID and TenantName here in both cases. + // Some providers allow you to specify a TenantName instead of the TenantId. + // Some require both. Your provider's authentication policies will determine + // how these fields influence authentication. + // If DomainID or DomainName are provided, they will also apply to TenantName. + // It is not currently possible to authenticate with Username and a Domain + // and scope to a Project in a different Domain by using TenantName. To + // accomplish that, the ProjectID will need to be provided to the TenantID + // option. + TenantID string `json:"tenantId,omitempty"` + TenantName string `json:"tenantName,omitempty"` + + // AllowReauth should be set to true if you grant permission for Gophercloud to + // cache your credentials in memory, and to allow Gophercloud to attempt to + // re-authenticate automatically if/when your token expires. If you set it to + // false, it will not cache these settings, but re-authentication will not be + // possible. This setting defaults to false. + // + // NOTE: The reauth function will try to re-authenticate endlessly if left unchecked. + // The way to limit the number of attempts is to provide a custom HTTP client to the provider client + // and provide a transport that implements the RoundTripper interface and stores the number of failed retries. + // For an example of this, see here: https://github.com/rackspace/rack/blob/1.0.0/auth/clients.go#L311 + AllowReauth bool `json:"-"` + + // TokenID allows users to authenticate (possibly as another user) with an + // authentication token ID. + TokenID string `json:"-"` +} + +// ToTokenV2CreateMap allows AuthOptions to satisfy the AuthOptionsBuilder +// interface in the v2 tokens package +func (opts AuthOptions) ToTokenV2CreateMap() (map[string]interface{}, error) { + // Populate the request map. + authMap := make(map[string]interface{}) + + if opts.Username != "" { + if opts.Password != "" { + authMap["passwordCredentials"] = map[string]interface{}{ + "username": opts.Username, + "password": opts.Password, + } + } else { + return nil, ErrMissingInput{Argument: "Password"} + } + } else if opts.TokenID != "" { + authMap["token"] = map[string]interface{}{ + "id": opts.TokenID, + } + } else { + return nil, ErrMissingInput{Argument: "Username"} + } + + if opts.TenantID != "" { + authMap["tenantId"] = opts.TenantID + } + if opts.TenantName != "" { + authMap["tenantName"] = opts.TenantName + } + + return map[string]interface{}{"auth": authMap}, nil +} + +func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[string]interface{}, error) { + type domainReq struct { + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + } + + type projectReq struct { + Domain *domainReq `json:"domain,omitempty"` + Name *string `json:"name,omitempty"` + ID *string `json:"id,omitempty"` + } + + type userReq struct { + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + Password string `json:"password"` + Domain *domainReq `json:"domain,omitempty"` + } + + type passwordReq struct { + User userReq `json:"user"` + } + + type tokenReq struct { + ID string `json:"id"` + } + + type identityReq struct { + Methods []string `json:"methods"` + Password *passwordReq `json:"password,omitempty"` + Token *tokenReq `json:"token,omitempty"` + } + + type authReq struct { + Identity identityReq `json:"identity"` + } + + type request struct { + Auth authReq `json:"auth"` + } + + // Populate the request structure based on the provided arguments. Create and return an error + // if insufficient or incompatible information is present. + var req request + + if opts.Password == "" { + if opts.TokenID != "" { + // Because we aren't using password authentication, it's an error to also provide any of the user-based authentication + // parameters. + if opts.Username != "" { + return nil, ErrUsernameWithToken{} + } + if opts.UserID != "" { + return nil, ErrUserIDWithToken{} + } + if opts.DomainID != "" { + return nil, ErrDomainIDWithToken{} + } + if opts.DomainName != "" { + return nil, ErrDomainNameWithToken{} + } + + // Configure the request for Token authentication. + req.Auth.Identity.Methods = []string{"token"} + req.Auth.Identity.Token = &tokenReq{ + ID: opts.TokenID, + } + } else { + // If no password or token ID are available, authentication can't continue. + return nil, ErrMissingPassword{} + } + } else { + // Password authentication. + req.Auth.Identity.Methods = []string{"password"} + + // At least one of Username and UserID must be specified. + if opts.Username == "" && opts.UserID == "" { + return nil, ErrUsernameOrUserID{} + } + + if opts.Username != "" { + // If Username is provided, UserID may not be provided. + if opts.UserID != "" { + return nil, ErrUsernameOrUserID{} + } + + // Either DomainID or DomainName must also be specified. + if opts.DomainID == "" && opts.DomainName == "" { + return nil, ErrDomainIDOrDomainName{} + } + + if opts.DomainID != "" { + if opts.DomainName != "" { + return nil, ErrDomainIDOrDomainName{} + } + + // Configure the request for Username and Password authentication with a DomainID. + req.Auth.Identity.Password = &passwordReq{ + User: userReq{ + Name: &opts.Username, + Password: opts.Password, + Domain: &domainReq{ID: &opts.DomainID}, + }, + } + } + + if opts.DomainName != "" { + // Configure the request for Username and Password authentication with a DomainName. + req.Auth.Identity.Password = &passwordReq{ + User: userReq{ + Name: &opts.Username, + Password: opts.Password, + Domain: &domainReq{Name: &opts.DomainName}, + }, + } + } + } + + if opts.UserID != "" { + // If UserID is specified, neither DomainID nor DomainName may be. + if opts.DomainID != "" { + return nil, ErrDomainIDWithUserID{} + } + if opts.DomainName != "" { + return nil, ErrDomainNameWithUserID{} + } + + // Configure the request for UserID and Password authentication. + req.Auth.Identity.Password = &passwordReq{ + User: userReq{ID: &opts.UserID, Password: opts.Password}, + } + } + } + + b, err := BuildRequestBody(req, "") + if err != nil { + return nil, err + } + + if len(scope) != 0 { + b["auth"].(map[string]interface{})["scope"] = scope + } + + return b, nil +} + +func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) { + + var scope struct { + ProjectID string + ProjectName string + DomainID string + DomainName string + } + + if opts.TenantID != "" { + scope.ProjectID = opts.TenantID + } else { + if opts.TenantName != "" { + scope.ProjectName = opts.TenantName + scope.DomainID = opts.DomainID + scope.DomainName = opts.DomainName + } + } + + if scope.ProjectName != "" { + // ProjectName provided: either DomainID or DomainName must also be supplied. + // ProjectID may not be supplied. + if scope.DomainID == "" && scope.DomainName == "" { + return nil, ErrScopeDomainIDOrDomainName{} + } + if scope.ProjectID != "" { + return nil, ErrScopeProjectIDOrProjectName{} + } + + if scope.DomainID != "" { + // ProjectName + DomainID + return map[string]interface{}{ + "project": map[string]interface{}{ + "name": &scope.ProjectName, + "domain": map[string]interface{}{"id": &scope.DomainID}, + }, + }, nil + } + + if scope.DomainName != "" { + // ProjectName + DomainName + return map[string]interface{}{ + "project": map[string]interface{}{ + "name": &scope.ProjectName, + "domain": map[string]interface{}{"name": &scope.DomainName}, + }, + }, nil + } + } else if scope.ProjectID != "" { + // ProjectID provided. ProjectName, DomainID, and DomainName may not be provided. + if scope.DomainID != "" { + return nil, ErrScopeProjectIDAlone{} + } + if scope.DomainName != "" { + return nil, ErrScopeProjectIDAlone{} + } + + // ProjectID + return map[string]interface{}{ + "project": map[string]interface{}{ + "id": &scope.ProjectID, + }, + }, nil + } else if scope.DomainID != "" { + // DomainID provided. ProjectID, ProjectName, and DomainName may not be provided. + if scope.DomainName != "" { + return nil, ErrScopeDomainIDOrDomainName{} + } + + // DomainID + return map[string]interface{}{ + "domain": map[string]interface{}{ + "id": &scope.DomainID, + }, + }, nil + } else if scope.DomainName != "" { + return nil, ErrScopeDomainName{} + } + + return nil, nil +} + +func (opts AuthOptions) CanReauth() bool { + return opts.AllowReauth +} diff --git a/vendor/github.com/gophercloud/gophercloud/doc.go b/vendor/github.com/gophercloud/gophercloud/doc.go new file mode 100644 index 0000000000..b559516f91 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/doc.go @@ -0,0 +1,69 @@ +/* +Package gophercloud provides a multi-vendor interface to OpenStack-compatible +clouds. The library has a three-level hierarchy: providers, services, and +resources. + +Provider structs represent the service providers that offer and manage a +collection of services. The IdentityEndpoint is typically refered to as +"auth_url" in information provided by the cloud operator. Additionally, +the cloud may refer to TenantID or TenantName as project_id and project_name. +These are defined like so: + + opts := gophercloud.AuthOptions{ + IdentityEndpoint: "https://openstack.example.com:5000/v2.0", + Username: "{username}", + Password: "{password}", + TenantID: "{tenant_id}", + } + + provider, err := openstack.AuthenticatedClient(opts) + +Service structs are specific to a provider and handle all of the logic and +operations for a particular OpenStack service. Examples of services include: +Compute, Object Storage, Block Storage. In order to define one, you need to +pass in the parent provider, like so: + + opts := gophercloud.EndpointOpts{Region: "RegionOne"} + + client := openstack.NewComputeV2(provider, opts) + +Resource structs are the domain models that services make use of in order +to work with and represent the state of API resources: + + server, err := servers.Get(client, "{serverId}").Extract() + +Intermediate Result structs are returned for API operations, which allow +generic access to the HTTP headers, response body, and any errors associated +with the network transaction. To turn a result into a usable resource struct, +you must call the Extract method which is chained to the response, or an +Extract function from an applicable extension: + + result := servers.Get(client, "{serverId}") + + // Attempt to extract the disk configuration from the OS-DCF disk config + // extension: + config, err := diskconfig.ExtractGet(result) + +All requests that enumerate a collection return a Pager struct that is used to +iterate through the results one page at a time. Use the EachPage method on that +Pager to handle each successive Page in a closure, then use the appropriate +extraction method from that request's package to interpret that Page as a slice +of results: + + err := servers.List(client, nil).EachPage(func (page pagination.Page) (bool, error) { + s, err := servers.ExtractServers(page) + if err != nil { + return false, err + } + + // Handle the []servers.Server slice. + + // Return "false" or an error to prematurely stop fetching new pages. + return true, nil + }) + +This top-level package contains utility functions and data types that are used +throughout the provider and service packages. Of particular note for end users +are the AuthOptions and EndpointOpts structs. +*/ +package gophercloud diff --git a/vendor/github.com/gophercloud/gophercloud/endpoint_search.go b/vendor/github.com/gophercloud/gophercloud/endpoint_search.go new file mode 100644 index 0000000000..9887947f61 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/endpoint_search.go @@ -0,0 +1,76 @@ +package gophercloud + +// Availability indicates to whom a specific service endpoint is accessible: +// the internet at large, internal networks only, or only to administrators. +// Different identity services use different terminology for these. Identity v2 +// lists them as different kinds of URLs within the service catalog ("adminURL", +// "internalURL", and "publicURL"), while v3 lists them as "Interfaces" in an +// endpoint's response. +type Availability string + +const ( + // AvailabilityAdmin indicates that an endpoint is only available to + // administrators. + AvailabilityAdmin Availability = "admin" + + // AvailabilityPublic indicates that an endpoint is available to everyone on + // the internet. + AvailabilityPublic Availability = "public" + + // AvailabilityInternal indicates that an endpoint is only available within + // the cluster's internal network. + AvailabilityInternal Availability = "internal" +) + +// EndpointOpts specifies search criteria used by queries against an +// OpenStack service catalog. The options must contain enough information to +// unambiguously identify one, and only one, endpoint within the catalog. +// +// Usually, these are passed to service client factory functions in a provider +// package, like "rackspace.NewComputeV2()". +type EndpointOpts struct { + // Type [required] is the service type for the client (e.g., "compute", + // "object-store"). Generally, this will be supplied by the service client + // function, but a user-given value will be honored if provided. + Type string + + // Name [optional] is the service name for the client (e.g., "nova") as it + // appears in the service catalog. Services can have the same Type but a + // different Name, which is why both Type and Name are sometimes needed. + Name string + + // Region [required] is the geographic region in which the endpoint resides, + // generally specifying which datacenter should house your resources. + // Required only for services that span multiple regions. + Region string + + // Availability [optional] is the visibility of the endpoint to be returned. + // Valid types include the constants AvailabilityPublic, AvailabilityInternal, + // or AvailabilityAdmin from this package. + // + // Availability is not required, and defaults to AvailabilityPublic. Not all + // providers or services offer all Availability options. + Availability Availability +} + +/* +EndpointLocator is an internal function to be used by provider implementations. + +It provides an implementation that locates a single endpoint from a service +catalog for a specific ProviderClient based on user-provided EndpointOpts. The +provider then uses it to discover related ServiceClients. +*/ +type EndpointLocator func(EndpointOpts) (string, error) + +// ApplyDefaults is an internal method to be used by provider implementations. +// +// It sets EndpointOpts fields if not already set, including a default type. +// Currently, EndpointOpts.Availability defaults to the public endpoint. +func (eo *EndpointOpts) ApplyDefaults(t string) { + if eo.Type == "" { + eo.Type = t + } + if eo.Availability == "" { + eo.Availability = AvailabilityPublic + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/errors.go b/vendor/github.com/gophercloud/gophercloud/errors.go new file mode 100644 index 0000000000..e0fe7c1e08 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/errors.go @@ -0,0 +1,408 @@ +package gophercloud + +import "fmt" + +// BaseError is an error type that all other error types embed. +type BaseError struct { + DefaultErrString string + Info string +} + +func (e BaseError) Error() string { + e.DefaultErrString = "An error occurred while executing a Gophercloud request." + return e.choseErrString() +} + +func (e BaseError) choseErrString() string { + if e.Info != "" { + return e.Info + } + return e.DefaultErrString +} + +// ErrMissingInput is the error when input is required in a particular +// situation but not provided by the user +type ErrMissingInput struct { + BaseError + Argument string +} + +func (e ErrMissingInput) Error() string { + e.DefaultErrString = fmt.Sprintf("Missing input for argument [%s]", e.Argument) + return e.choseErrString() +} + +// ErrInvalidInput is an error type used for most non-HTTP Gophercloud errors. +type ErrInvalidInput struct { + ErrMissingInput + Value interface{} +} + +func (e ErrInvalidInput) Error() string { + e.DefaultErrString = fmt.Sprintf("Invalid input provided for argument [%s]: [%+v]", e.Argument, e.Value) + return e.choseErrString() +} + +// ErrUnexpectedResponseCode is returned by the Request method when a response code other than +// those listed in OkCodes is encountered. +type ErrUnexpectedResponseCode struct { + BaseError + URL string + Method string + Expected []int + Actual int + Body []byte +} + +func (e ErrUnexpectedResponseCode) Error() string { + e.DefaultErrString = fmt.Sprintf( + "Expected HTTP response code %v when accessing [%s %s], but got %d instead\n%s", + e.Expected, e.Method, e.URL, e.Actual, e.Body, + ) + return e.choseErrString() +} + +// ErrDefault400 is the default error type returned on a 400 HTTP response code. +type ErrDefault400 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault401 is the default error type returned on a 401 HTTP response code. +type ErrDefault401 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault404 is the default error type returned on a 404 HTTP response code. +type ErrDefault404 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault405 is the default error type returned on a 405 HTTP response code. +type ErrDefault405 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault408 is the default error type returned on a 408 HTTP response code. +type ErrDefault408 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault429 is the default error type returned on a 429 HTTP response code. +type ErrDefault429 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault500 is the default error type returned on a 500 HTTP response code. +type ErrDefault500 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault503 is the default error type returned on a 503 HTTP response code. +type ErrDefault503 struct { + ErrUnexpectedResponseCode +} + +func (e ErrDefault400) Error() string { + return "Invalid request due to incorrect syntax or missing required parameters." +} +func (e ErrDefault401) Error() string { + return "Authentication failed" +} +func (e ErrDefault404) Error() string { + return "Resource not found" +} +func (e ErrDefault405) Error() string { + return "Method not allowed" +} +func (e ErrDefault408) Error() string { + return "The server timed out waiting for the request" +} +func (e ErrDefault429) Error() string { + return "Too many requests have been sent in a given amount of time. Pause" + + " requests, wait up to one minute, and try again." +} +func (e ErrDefault500) Error() string { + return "Internal Server Error" +} +func (e ErrDefault503) Error() string { + return "The service is currently unable to handle the request due to a temporary" + + " overloading or maintenance. This is a temporary condition. Try again later." +} + +// Err400er is the interface resource error types implement to override the error message +// from a 400 error. +type Err400er interface { + Error400(ErrUnexpectedResponseCode) error +} + +// Err401er is the interface resource error types implement to override the error message +// from a 401 error. +type Err401er interface { + Error401(ErrUnexpectedResponseCode) error +} + +// Err404er is the interface resource error types implement to override the error message +// from a 404 error. +type Err404er interface { + Error404(ErrUnexpectedResponseCode) error +} + +// Err405er is the interface resource error types implement to override the error message +// from a 405 error. +type Err405er interface { + Error405(ErrUnexpectedResponseCode) error +} + +// Err408er is the interface resource error types implement to override the error message +// from a 408 error. +type Err408er interface { + Error408(ErrUnexpectedResponseCode) error +} + +// Err429er is the interface resource error types implement to override the error message +// from a 429 error. +type Err429er interface { + Error429(ErrUnexpectedResponseCode) error +} + +// Err500er is the interface resource error types implement to override the error message +// from a 500 error. +type Err500er interface { + Error500(ErrUnexpectedResponseCode) error +} + +// Err503er is the interface resource error types implement to override the error message +// from a 503 error. +type Err503er interface { + Error503(ErrUnexpectedResponseCode) error +} + +// ErrTimeOut is the error type returned when an operations times out. +type ErrTimeOut struct { + BaseError +} + +func (e ErrTimeOut) Error() string { + e.DefaultErrString = "A time out occurred" + return e.choseErrString() +} + +// ErrUnableToReauthenticate is the error type returned when reauthentication fails. +type ErrUnableToReauthenticate struct { + BaseError + ErrOriginal error +} + +func (e ErrUnableToReauthenticate) Error() string { + e.DefaultErrString = fmt.Sprintf("Unable to re-authenticate: %s", e.ErrOriginal) + return e.choseErrString() +} + +// ErrErrorAfterReauthentication is the error type returned when reauthentication +// succeeds, but an error occurs afterword (usually an HTTP error). +type ErrErrorAfterReauthentication struct { + BaseError + ErrOriginal error +} + +func (e ErrErrorAfterReauthentication) Error() string { + e.DefaultErrString = fmt.Sprintf("Successfully re-authenticated, but got error executing request: %s", e.ErrOriginal) + return e.choseErrString() +} + +// ErrServiceNotFound is returned when no service in a service catalog matches +// the provided EndpointOpts. This is generally returned by provider service +// factory methods like "NewComputeV2()" and can mean that a service is not +// enabled for your account. +type ErrServiceNotFound struct { + BaseError +} + +func (e ErrServiceNotFound) Error() string { + e.DefaultErrString = "No suitable service could be found in the service catalog." + return e.choseErrString() +} + +// ErrEndpointNotFound is returned when no available endpoints match the +// provided EndpointOpts. This is also generally returned by provider service +// factory methods, and usually indicates that a region was specified +// incorrectly. +type ErrEndpointNotFound struct { + BaseError +} + +func (e ErrEndpointNotFound) Error() string { + e.DefaultErrString = "No suitable endpoint could be found in the service catalog." + return e.choseErrString() +} + +// ErrResourceNotFound is the error when trying to retrieve a resource's +// ID by name and the resource doesn't exist. +type ErrResourceNotFound struct { + BaseError + Name string + ResourceType string +} + +func (e ErrResourceNotFound) Error() string { + e.DefaultErrString = fmt.Sprintf("Unable to find %s with name %s", e.ResourceType, e.Name) + return e.choseErrString() +} + +// ErrMultipleResourcesFound is the error when trying to retrieve a resource's +// ID by name and multiple resources have the user-provided name. +type ErrMultipleResourcesFound struct { + BaseError + Name string + Count int + ResourceType string +} + +func (e ErrMultipleResourcesFound) Error() string { + e.DefaultErrString = fmt.Sprintf("Found %d %ss matching %s", e.Count, e.ResourceType, e.Name) + return e.choseErrString() +} + +// ErrUnexpectedType is the error when an unexpected type is encountered +type ErrUnexpectedType struct { + BaseError + Expected string + Actual string +} + +func (e ErrUnexpectedType) Error() string { + e.DefaultErrString = fmt.Sprintf("Expected %s but got %s", e.Expected, e.Actual) + return e.choseErrString() +} + +func unacceptedAttributeErr(attribute string) string { + return fmt.Sprintf("The base Identity V3 API does not accept authentication by %s", attribute) +} + +func redundantWithTokenErr(attribute string) string { + return fmt.Sprintf("%s may not be provided when authenticating with a TokenID", attribute) +} + +func redundantWithUserID(attribute string) string { + return fmt.Sprintf("%s may not be provided when authenticating with a UserID", attribute) +} + +// ErrAPIKeyProvided indicates that an APIKey was provided but can't be used. +type ErrAPIKeyProvided struct{ BaseError } + +func (e ErrAPIKeyProvided) Error() string { + return unacceptedAttributeErr("APIKey") +} + +// ErrTenantIDProvided indicates that a TenantID was provided but can't be used. +type ErrTenantIDProvided struct{ BaseError } + +func (e ErrTenantIDProvided) Error() string { + return unacceptedAttributeErr("TenantID") +} + +// ErrTenantNameProvided indicates that a TenantName was provided but can't be used. +type ErrTenantNameProvided struct{ BaseError } + +func (e ErrTenantNameProvided) Error() string { + return unacceptedAttributeErr("TenantName") +} + +// ErrUsernameWithToken indicates that a Username was provided, but token authentication is being used instead. +type ErrUsernameWithToken struct{ BaseError } + +func (e ErrUsernameWithToken) Error() string { + return redundantWithTokenErr("Username") +} + +// ErrUserIDWithToken indicates that a UserID was provided, but token authentication is being used instead. +type ErrUserIDWithToken struct{ BaseError } + +func (e ErrUserIDWithToken) Error() string { + return redundantWithTokenErr("UserID") +} + +// ErrDomainIDWithToken indicates that a DomainID was provided, but token authentication is being used instead. +type ErrDomainIDWithToken struct{ BaseError } + +func (e ErrDomainIDWithToken) Error() string { + return redundantWithTokenErr("DomainID") +} + +// ErrDomainNameWithToken indicates that a DomainName was provided, but token authentication is being used instead.s +type ErrDomainNameWithToken struct{ BaseError } + +func (e ErrDomainNameWithToken) Error() string { + return redundantWithTokenErr("DomainName") +} + +// ErrUsernameOrUserID indicates that neither username nor userID are specified, or both are at once. +type ErrUsernameOrUserID struct{ BaseError } + +func (e ErrUsernameOrUserID) Error() string { + return "Exactly one of Username and UserID must be provided for password authentication" +} + +// ErrDomainIDWithUserID indicates that a DomainID was provided, but unnecessary because a UserID is being used. +type ErrDomainIDWithUserID struct{ BaseError } + +func (e ErrDomainIDWithUserID) Error() string { + return redundantWithUserID("DomainID") +} + +// ErrDomainNameWithUserID indicates that a DomainName was provided, but unnecessary because a UserID is being used. +type ErrDomainNameWithUserID struct{ BaseError } + +func (e ErrDomainNameWithUserID) Error() string { + return redundantWithUserID("DomainName") +} + +// ErrDomainIDOrDomainName indicates that a username was provided, but no domain to scope it. +// It may also indicate that both a DomainID and a DomainName were provided at once. +type ErrDomainIDOrDomainName struct{ BaseError } + +func (e ErrDomainIDOrDomainName) Error() string { + return "You must provide exactly one of DomainID or DomainName to authenticate by Username" +} + +// ErrMissingPassword indicates that no password was provided and no token is available. +type ErrMissingPassword struct{ BaseError } + +func (e ErrMissingPassword) Error() string { + return "You must provide a password to authenticate" +} + +// ErrScopeDomainIDOrDomainName indicates that a domain ID or Name was required in a Scope, but not present. +type ErrScopeDomainIDOrDomainName struct{ BaseError } + +func (e ErrScopeDomainIDOrDomainName) Error() string { + return "You must provide exactly one of DomainID or DomainName in a Scope with ProjectName" +} + +// ErrScopeProjectIDOrProjectName indicates that both a ProjectID and a ProjectName were provided in a Scope. +type ErrScopeProjectIDOrProjectName struct{ BaseError } + +func (e ErrScopeProjectIDOrProjectName) Error() string { + return "You must provide at most one of ProjectID or ProjectName in a Scope" +} + +// ErrScopeProjectIDAlone indicates that a ProjectID was provided with other constraints in a Scope. +type ErrScopeProjectIDAlone struct{ BaseError } + +func (e ErrScopeProjectIDAlone) Error() string { + return "ProjectID must be supplied alone in a Scope" +} + +// ErrScopeDomainName indicates that a DomainName was provided alone in a Scope. +type ErrScopeDomainName struct{ BaseError } + +func (e ErrScopeDomainName) Error() string { + return "DomainName must be supplied with a ProjectName or ProjectID in a Scope" +} + +// ErrScopeEmpty indicates that no credentials were provided in a Scope. +type ErrScopeEmpty struct{ BaseError } + +func (e ErrScopeEmpty) Error() string { + return "You must provide either a Project or Domain in a Scope" +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/auth_env.go b/vendor/github.com/gophercloud/gophercloud/openstack/auth_env.go new file mode 100644 index 0000000000..f6d2eb194b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/auth_env.go @@ -0,0 +1,52 @@ +package openstack + +import ( + "os" + + "github.com/gophercloud/gophercloud" +) + +var nilOptions = gophercloud.AuthOptions{} + +// AuthOptionsFromEnv fills out an identity.AuthOptions structure with the settings found on the various OpenStack +// OS_* environment variables. The following variables provide sources of truth: OS_AUTH_URL, OS_USERNAME, +// OS_PASSWORD, OS_TENANT_ID, and OS_TENANT_NAME. Of these, OS_USERNAME, OS_PASSWORD, and OS_AUTH_URL must +// have settings, or an error will result. OS_TENANT_ID and OS_TENANT_NAME are optional. +func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) { + authURL := os.Getenv("OS_AUTH_URL") + username := os.Getenv("OS_USERNAME") + userID := os.Getenv("OS_USERID") + password := os.Getenv("OS_PASSWORD") + tenantID := os.Getenv("OS_TENANT_ID") + tenantName := os.Getenv("OS_TENANT_NAME") + domainID := os.Getenv("OS_DOMAIN_ID") + domainName := os.Getenv("OS_DOMAIN_NAME") + + if authURL == "" { + err := gophercloud.ErrMissingInput{Argument: "authURL"} + return nilOptions, err + } + + if username == "" && userID == "" { + err := gophercloud.ErrMissingInput{Argument: "username"} + return nilOptions, err + } + + if password == "" { + err := gophercloud.ErrMissingInput{Argument: "password"} + return nilOptions, err + } + + ao := gophercloud.AuthOptions{ + IdentityEndpoint: authURL, + UserID: userID, + Username: username, + Password: password, + TenantID: tenantID, + TenantName: tenantName, + DomainID: domainID, + DomainName: domainName, + } + + return ao, nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/client.go b/vendor/github.com/gophercloud/gophercloud/openstack/client.go new file mode 100644 index 0000000000..2d30cc60ad --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/client.go @@ -0,0 +1,336 @@ +package openstack + +import ( + "fmt" + "net/url" + "reflect" + + "github.com/gophercloud/gophercloud" + tokens2 "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens" + tokens3 "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" + "github.com/gophercloud/gophercloud/openstack/utils" +) + +const ( + v20 = "v2.0" + v30 = "v3.0" +) + +// NewClient prepares an unauthenticated ProviderClient instance. +// Most users will probably prefer using the AuthenticatedClient function instead. +// This is useful if you wish to explicitly control the version of the identity service that's used for authentication explicitly, +// for example. +func NewClient(endpoint string) (*gophercloud.ProviderClient, error) { + u, err := url.Parse(endpoint) + if err != nil { + return nil, err + } + hadPath := u.Path != "" + u.Path, u.RawQuery, u.Fragment = "", "", "" + base := u.String() + + endpoint = gophercloud.NormalizeURL(endpoint) + base = gophercloud.NormalizeURL(base) + + if hadPath { + return &gophercloud.ProviderClient{ + IdentityBase: base, + IdentityEndpoint: endpoint, + }, nil + } + + return &gophercloud.ProviderClient{ + IdentityBase: base, + IdentityEndpoint: "", + }, nil +} + +// AuthenticatedClient logs in to an OpenStack cloud found at the identity endpoint specified by options, acquires a token, and +// returns a Client instance that's ready to operate. +// It first queries the root identity endpoint to determine which versions of the identity service are supported, then chooses +// the most recent identity service available to proceed. +func AuthenticatedClient(options gophercloud.AuthOptions) (*gophercloud.ProviderClient, error) { + client, err := NewClient(options.IdentityEndpoint) + if err != nil { + return nil, err + } + + err = Authenticate(client, options) + if err != nil { + return nil, err + } + return client, nil +} + +// Authenticate or re-authenticate against the most recent identity service supported at the provided endpoint. +func Authenticate(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error { + versions := []*utils.Version{ + {ID: v20, Priority: 20, Suffix: "/v2.0/"}, + {ID: v30, Priority: 30, Suffix: "/v3/"}, + } + + chosen, endpoint, err := utils.ChooseVersion(client, versions) + if err != nil { + return err + } + + switch chosen.ID { + case v20: + return v2auth(client, endpoint, options, gophercloud.EndpointOpts{}) + case v30: + return v3auth(client, endpoint, &options, gophercloud.EndpointOpts{}) + default: + // The switch statement must be out of date from the versions list. + return fmt.Errorf("Unrecognized identity version: %s", chosen.ID) + } +} + +// AuthenticateV2 explicitly authenticates against the identity v2 endpoint. +func AuthenticateV2(client *gophercloud.ProviderClient, options gophercloud.AuthOptions, eo gophercloud.EndpointOpts) error { + return v2auth(client, "", options, eo) +} + +func v2auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions, eo gophercloud.EndpointOpts) error { + v2Client, err := NewIdentityV2(client, eo) + if err != nil { + return err + } + + if endpoint != "" { + v2Client.Endpoint = endpoint + } + + v2Opts := tokens2.AuthOptions{ + IdentityEndpoint: options.IdentityEndpoint, + Username: options.Username, + Password: options.Password, + TenantID: options.TenantID, + TenantName: options.TenantName, + AllowReauth: options.AllowReauth, + TokenID: options.TokenID, + } + + result := tokens2.Create(v2Client, v2Opts) + + token, err := result.ExtractToken() + if err != nil { + return err + } + + catalog, err := result.ExtractServiceCatalog() + if err != nil { + return err + } + + if options.AllowReauth { + client.ReauthFunc = func() error { + client.TokenID = "" + return v2auth(client, endpoint, options, eo) + } + } + client.TokenID = token.ID + client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) { + return V2EndpointURL(catalog, opts) + } + + return nil +} + +// AuthenticateV3 explicitly authenticates against the identity v3 service. +func AuthenticateV3(client *gophercloud.ProviderClient, options tokens3.AuthOptionsBuilder, eo gophercloud.EndpointOpts) error { + return v3auth(client, "", options, eo) +} + +func v3auth(client *gophercloud.ProviderClient, endpoint string, opts tokens3.AuthOptionsBuilder, eo gophercloud.EndpointOpts) error { + // Override the generated service endpoint with the one returned by the version endpoint. + v3Client, err := NewIdentityV3(client, eo) + if err != nil { + return err + } + + if endpoint != "" { + v3Client.Endpoint = endpoint + } + + result := tokens3.Create(v3Client, opts) + + token, err := result.ExtractToken() + if err != nil { + return err + } + + catalog, err := result.ExtractServiceCatalog() + if err != nil { + return err + } + + client.TokenID = token.ID + + if opts.CanReauth() { + client.ReauthFunc = func() error { + client.TokenID = "" + return v3auth(client, endpoint, opts, eo) + } + } + client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) { + return V3EndpointURL(catalog, opts) + } + + return nil +} + +// NewIdentityV2 creates a ServiceClient that may be used to interact with the v2 identity service. +func NewIdentityV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + endpoint := client.IdentityBase + "v2.0/" + var err error + if !reflect.DeepEqual(eo, gophercloud.EndpointOpts{}) { + eo.ApplyDefaults("identity") + endpoint, err = client.EndpointLocator(eo) + if err != nil { + return nil, err + } + } + + return &gophercloud.ServiceClient{ + ProviderClient: client, + Endpoint: endpoint, + }, nil +} + +// NewIdentityV3 creates a ServiceClient that may be used to access the v3 identity service. +func NewIdentityV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + endpoint := client.IdentityBase + "v3/" + var err error + if !reflect.DeepEqual(eo, gophercloud.EndpointOpts{}) { + eo.ApplyDefaults("identity") + endpoint, err = client.EndpointLocator(eo) + if err != nil { + return nil, err + } + } + + return &gophercloud.ServiceClient{ + ProviderClient: client, + Endpoint: endpoint, + }, nil +} + +// NewObjectStorageV1 creates a ServiceClient that may be used with the v1 object storage package. +func NewObjectStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("object-store") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil +} + +// NewComputeV2 creates a ServiceClient that may be used with the v2 compute package. +func NewComputeV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("compute") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil +} + +// NewNetworkV2 creates a ServiceClient that may be used with the v2 network package. +func NewNetworkV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("network") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ + ProviderClient: client, + Endpoint: url, + ResourceBase: url + "v2.0/", + }, nil +} + +// NewBlockStorageV1 creates a ServiceClient that may be used to access the v1 block storage service. +func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("volume") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil +} + +// NewBlockStorageV2 creates a ServiceClient that may be used to access the v2 block storage service. +func NewBlockStorageV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("volumev2") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil +} + +// NewSharedFileSystemV2 creates a ServiceClient that may be used to access the v2 shared file system service. +func NewSharedFileSystemV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("sharev2") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil +} + +// NewCDNV1 creates a ServiceClient that may be used to access the OpenStack v1 +// CDN service. +func NewCDNV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("cdn") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil +} + +// NewOrchestrationV1 creates a ServiceClient that may be used to access the v1 orchestration service. +func NewOrchestrationV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("orchestration") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil +} + +// NewDBV1 creates a ServiceClient that may be used to access the v1 DB service. +func NewDBV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("database") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil +} + +// NewDNSV2 creates a ServiceClient that may be used to access the v2 DNS service. +func NewDNSV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("dns") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ + ProviderClient: client, + Endpoint: url, + ResourceBase: url + "v2/"}, nil +} + +// NewImageServiceV2 creates a ServiceClient that may be used to access the v2 image service. +func NewImageServiceV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + eo.ApplyDefaults("image") + url, err := client.EndpointLocator(eo) + if err != nil { + return nil, err + } + return &gophercloud.ServiceClient{ProviderClient: client, + Endpoint: url, + ResourceBase: url + "v2/"}, nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/delegate.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/delegate.go new file mode 100644 index 0000000000..00e7c3becf --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/delegate.go @@ -0,0 +1,23 @@ +package extensions + +import ( + "github.com/gophercloud/gophercloud" + common "github.com/gophercloud/gophercloud/openstack/common/extensions" + "github.com/gophercloud/gophercloud/pagination" +) + +// ExtractExtensions interprets a Page as a slice of Extensions. +func ExtractExtensions(page pagination.Page) ([]common.Extension, error) { + return common.ExtractExtensions(page) +} + +// Get retrieves information for a specific extension using its alias. +func Get(c *gophercloud.ServiceClient, alias string) common.GetResult { + return common.Get(c, alias) +} + +// List returns a Pager which allows you to iterate over the full collection of extensions. +// It does not accept query parameters. +func List(c *gophercloud.ServiceClient) pagination.Pager { + return common.List(c) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/doc.go new file mode 100644 index 0000000000..2b447da1d6 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/doc.go @@ -0,0 +1,3 @@ +// Package extensions provides information and interaction with the +// different extensions available for the OpenStack Compute service. +package extensions diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/doc.go new file mode 100644 index 0000000000..6682fa6290 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/doc.go @@ -0,0 +1,3 @@ +// Package floatingips provides the ability to manage floating ips through +// nova-network +package floatingips diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/requests.go new file mode 100644 index 0000000000..b36aeba59c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/requests.go @@ -0,0 +1,112 @@ +package floatingips + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// List returns a Pager that allows you to iterate over a collection of FloatingIPs. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page { + return FloatingIPPage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder describes struct types that can be accepted by the Create call. Notable, the +// CreateOpts struct in this package does. +type CreateOptsBuilder interface { + ToFloatingIPCreateMap() (map[string]interface{}, error) +} + +// CreateOpts specifies a Floating IP allocation request +type CreateOpts struct { + // Pool is the pool of floating IPs to allocate one from + Pool string `json:"pool" required:"true"` +} + +// ToFloatingIPCreateMap constructs a request body from CreateOpts. +func (opts CreateOpts) ToFloatingIPCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "") +} + +// Create requests the creation of a new floating IP +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToFloatingIPCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Get returns data about a previously created FloatingIP. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// Delete requests the deletion of a previous allocated FloatingIP. +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} + +// AssociateOptsBuilder is the interface types must satfisfy to be used as +// Associate options +type AssociateOptsBuilder interface { + ToFloatingIPAssociateMap() (map[string]interface{}, error) +} + +// AssociateOpts specifies the required information to associate a floating IP with an instance +type AssociateOpts struct { + // FloatingIP is the floating IP to associate with an instance + FloatingIP string `json:"address" required:"true"` + // FixedIP is an optional fixed IP address of the server + FixedIP string `json:"fixed_address,omitempty"` +} + +// ToFloatingIPAssociateMap constructs a request body from AssociateOpts. +func (opts AssociateOpts) ToFloatingIPAssociateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "addFloatingIp") +} + +// AssociateInstance pairs an allocated floating IP with an instance. +func AssociateInstance(client *gophercloud.ServiceClient, serverID string, opts AssociateOptsBuilder) (r AssociateResult) { + b, err := opts.ToFloatingIPAssociateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(associateURL(client, serverID), b, nil, nil) + return +} + +// DisassociateOptsBuilder is the interface types must satfisfy to be used as +// Disassociate options +type DisassociateOptsBuilder interface { + ToFloatingIPDisassociateMap() (map[string]interface{}, error) +} + +// DisassociateOpts specifies the required information to disassociate a floating IP with an instance +type DisassociateOpts struct { + FloatingIP string `json:"address" required:"true"` +} + +// ToFloatingIPDisassociateMap constructs a request body from AssociateOpts. +func (opts DisassociateOpts) ToFloatingIPDisassociateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "removeFloatingIp") +} + +// DisassociateInstance decouples an allocated floating IP from an instance +func DisassociateInstance(client *gophercloud.ServiceClient, serverID string, opts DisassociateOptsBuilder) (r DisassociateResult) { + b, err := opts.ToFloatingIPDisassociateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(disassociateURL(client, serverID), b, nil, nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/results.go new file mode 100644 index 0000000000..2f5b33844e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/results.go @@ -0,0 +1,117 @@ +package floatingips + +import ( + "encoding/json" + "strconv" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// A FloatingIP is an IP that can be associated with an instance +type FloatingIP struct { + // ID is a unique ID of the Floating IP + ID string `json:"-"` + + // FixedIP is the IP of the instance related to the Floating IP + FixedIP string `json:"fixed_ip,omitempty"` + + // InstanceID is the ID of the instance that is using the Floating IP + InstanceID string `json:"instance_id"` + + // IP is the actual Floating IP + IP string `json:"ip"` + + // Pool is the pool of floating IPs that this floating IP belongs to + Pool string `json:"pool"` +} + +func (r *FloatingIP) UnmarshalJSON(b []byte) error { + type tmp FloatingIP + var s struct { + tmp + ID interface{} `json:"id"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = FloatingIP(s.tmp) + + switch t := s.ID.(type) { + case float64: + r.ID = strconv.FormatFloat(t, 'f', -1, 64) + case string: + r.ID = t + } + + return err +} + +// FloatingIPPage stores a single, only page of FloatingIPs +// results from a List call. +type FloatingIPPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a FloatingIPsPage is empty. +func (page FloatingIPPage) IsEmpty() (bool, error) { + va, err := ExtractFloatingIPs(page) + return len(va) == 0, err +} + +// ExtractFloatingIPs interprets a page of results as a slice of +// FloatingIPs. +func ExtractFloatingIPs(r pagination.Page) ([]FloatingIP, error) { + var s struct { + FloatingIPs []FloatingIP `json:"floating_ips"` + } + err := (r.(FloatingIPPage)).ExtractInto(&s) + return s.FloatingIPs, err +} + +// FloatingIPResult is the raw result from a FloatingIP request. +type FloatingIPResult struct { + gophercloud.Result +} + +// Extract is a method that attempts to interpret any FloatingIP resource +// response as a FloatingIP struct. +func (r FloatingIPResult) Extract() (*FloatingIP, error) { + var s struct { + FloatingIP *FloatingIP `json:"floating_ip"` + } + err := r.ExtractInto(&s) + return s.FloatingIP, err +} + +// CreateResult is the response from a Create operation. Call its Extract method to interpret it +// as a FloatingIP. +type CreateResult struct { + FloatingIPResult +} + +// GetResult is the response from a Get operation. Call its Extract method to interpret it +// as a FloatingIP. +type GetResult struct { + FloatingIPResult +} + +// DeleteResult is the response from a Delete operation. Call its Extract method to determine if +// the call succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// AssociateResult is the response from a Delete operation. Call its Extract method to determine if +// the call succeeded or failed. +type AssociateResult struct { + gophercloud.ErrResult +} + +// DisassociateResult is the response from a Delete operation. Call its Extract method to determine if +// the call succeeded or failed. +type DisassociateResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/urls.go new file mode 100644 index 0000000000..4768e5a897 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips/urls.go @@ -0,0 +1,37 @@ +package floatingips + +import "github.com/gophercloud/gophercloud" + +const resourcePath = "os-floating-ips" + +func resourceURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func listURL(c *gophercloud.ServiceClient) string { + return resourceURL(c) +} + +func createURL(c *gophercloud.ServiceClient) string { + return resourceURL(c) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return getURL(c, id) +} + +func serverURL(c *gophercloud.ServiceClient, serverID string) string { + return c.ServiceURL("servers/" + serverID + "/action") +} + +func associateURL(c *gophercloud.ServiceClient, serverID string) string { + return serverURL(c, serverID) +} + +func disassociateURL(c *gophercloud.ServiceClient, serverID string) string { + return serverURL(c, serverID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/doc.go new file mode 100644 index 0000000000..5822e1bcf6 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/doc.go @@ -0,0 +1,7 @@ +// Package flavors provides information and interaction with the flavor API +// resource in the OpenStack Compute service. +// +// A flavor is an available hardware configuration for a server. Each flavor +// has a unique combination of disk space, memory capacity and priority for CPU +// time. +package flavors diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/requests.go new file mode 100644 index 0000000000..d5d571c3d6 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/requests.go @@ -0,0 +1,141 @@ +package flavors + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToFlavorListQuery() (string, error) +} + +// ListOpts helps control the results returned by the List() function. +// For example, a flavor with a minDisk field of 10 will not be returned if you specify MinDisk set to 20. +// Typically, software will use the last ID of the previous call to List to set the Marker for the current call. +type ListOpts struct { + + // ChangesSince, if provided, instructs List to return only those things which have changed since the timestamp provided. + ChangesSince string `q:"changes-since"` + + // MinDisk and MinRAM, if provided, elides flavors which do not meet your criteria. + MinDisk int `q:"minDisk"` + MinRAM int `q:"minRam"` + + // Marker and Limit control paging. + // Marker instructs List where to start listing from. + Marker string `q:"marker"` + + // Limit instructs List to refrain from sending excessively large lists of flavors. + Limit int `q:"limit"` +} + +// ToFlavorListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToFlavorListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// ListDetail instructs OpenStack to provide a list of flavors. +// You may provide criteria by which List curtails its results for easier processing. +// See ListOpts for more details. +func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToFlavorListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return FlavorPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +type CreateOptsBuilder interface { + ToFlavorCreateMap() (map[string]interface{}, error) +} + +// CreateOpts is passed to Create to create a flavor +// Source: +// https://github.com/openstack/nova/blob/stable/newton/nova/api/openstack/compute/schemas/flavor_manage.py#L20 +type CreateOpts struct { + Name string `json:"name" required:"true"` + // memory size, in MBs + RAM int `json:"ram" required:"true"` + VCPUs int `json:"vcpus" required:"true"` + // disk size, in GBs + Disk *int `json:"disk" required:"true"` + ID string `json:"id,omitempty"` + // non-zero, positive + Swap *int `json:"swap,omitempty"` + RxTxFactor float64 `json:"rxtx_factor,omitempty"` + IsPublic *bool `json:"os-flavor-access:is_public,omitempty"` + // ephemeral disk size, in GBs, non-zero, positive + Ephemeral *int `json:"OS-FLV-EXT-DATA:ephemeral,omitempty"` +} + +// ToFlavorCreateMap satisfies the CreateOptsBuilder interface +func (opts *CreateOpts) ToFlavorCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "flavor") +} + +// Create a flavor +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToFlavorCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +// Get instructs OpenStack to provide details on a single flavor, identified by its ID. +// Use ExtractFlavor to convert its result into a Flavor. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// IDFromName is a convienience function that returns a flavor's ID given its name. +func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + allPages, err := ListDetail(client, nil).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractFlavors(allPages) + if err != nil { + return "", err + } + + for _, f := range all { + if f.Name == name { + count++ + id = f.ID + } + } + + switch count { + case 0: + err := &gophercloud.ErrResourceNotFound{} + err.ResourceType = "flavor" + err.Name = name + return "", err + case 1: + return id, nil + default: + err := &gophercloud.ErrMultipleResourcesFound{} + err.ResourceType = "flavor" + err.Name = name + err.Count = count + return "", err + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/results.go new file mode 100644 index 0000000000..18b8434055 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/results.go @@ -0,0 +1,113 @@ +package flavors + +import ( + "encoding/json" + "strconv" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +type CreateResult struct { + commonResult +} + +// GetResult temporarily holds the response from a Get call. +type GetResult struct { + commonResult +} + +// Extract provides access to the individual Flavor returned by the Get and Create functions. +func (r commonResult) Extract() (*Flavor, error) { + var s struct { + Flavor *Flavor `json:"flavor"` + } + err := r.ExtractInto(&s) + return s.Flavor, err +} + +// Flavor records represent (virtual) hardware configurations for server resources in a region. +type Flavor struct { + // The Id field contains the flavor's unique identifier. + // For example, this identifier will be useful when specifying which hardware configuration to use for a new server instance. + ID string `json:"id"` + // The Disk and RA< fields provide a measure of storage space offered by the flavor, in GB and MB, respectively. + Disk int `json:"disk"` + RAM int `json:"ram"` + // The Name field provides a human-readable moniker for the flavor. + Name string `json:"name"` + RxTxFactor float64 `json:"rxtx_factor"` + // Swap indicates how much space is reserved for swap. + // If not provided, this field will be set to 0. + Swap int `json:"swap"` + // VCPUs indicates how many (virtual) CPUs are available for this flavor. + VCPUs int `json:"vcpus"` +} + +func (r *Flavor) UnmarshalJSON(b []byte) error { + type tmp Flavor + var s struct { + tmp + Swap interface{} `json:"swap"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = Flavor(s.tmp) + + switch t := s.Swap.(type) { + case float64: + r.Swap = int(t) + case string: + switch t { + case "": + r.Swap = 0 + default: + swap, err := strconv.ParseFloat(t, 64) + if err != nil { + return err + } + r.Swap = int(swap) + } + } + + return nil +} + +// FlavorPage contains a single page of the response from a List call. +type FlavorPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines if a page contains any results. +func (page FlavorPage) IsEmpty() (bool, error) { + flavors, err := ExtractFlavors(page) + return len(flavors) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the next page of results. +func (page FlavorPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"flavors_links"` + } + err := page.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// ExtractFlavors provides access to the list of flavors in a page acquired from the List operation. +func ExtractFlavors(r pagination.Page) ([]Flavor, error) { + var s struct { + Flavors []Flavor `json:"flavors"` + } + err := (r.(FlavorPage)).ExtractInto(&s) + return s.Flavors, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/urls.go new file mode 100644 index 0000000000..2fc21796f7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/urls.go @@ -0,0 +1,17 @@ +package flavors + +import ( + "github.com/gophercloud/gophercloud" +) + +func getURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id) +} + +func listURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("flavors", "detail") +} + +func createURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("flavors") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/doc.go new file mode 100644 index 0000000000..0edaa3f025 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/doc.go @@ -0,0 +1,7 @@ +// Package images provides information and interaction with the image API +// resource in the OpenStack Compute service. +// +// An image is a collection of files used to create or rebuild a server. +// Operators provide a number of pre-built OS images by default. You may also +// create custom images from cloud servers you have launched. +package images diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/requests.go new file mode 100644 index 0000000000..df9f1da8f6 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/requests.go @@ -0,0 +1,102 @@ +package images + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToImageListQuery() (string, error) +} + +// ListOpts contain options for limiting the number of Images returned from a call to ListDetail. +type ListOpts struct { + // When the image last changed status (in date-time format). + ChangesSince string `q:"changes-since"` + // The number of Images to return. + Limit int `q:"limit"` + // UUID of the Image at which to set a marker. + Marker string `q:"marker"` + // The name of the Image. + Name string `q:"name"` + // The name of the Server (in URL format). + Server string `q:"server"` + // The current status of the Image. + Status string `q:"status"` + // The value of the type of image (e.g. BASE, SERVER, ALL) + Type string `q:"type"` +} + +// ToImageListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToImageListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// ListDetail enumerates the available images. +func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listDetailURL(client) + if opts != nil { + query, err := opts.ToImageListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ImagePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get acquires additional detail about a specific image by ID. +// Use ExtractImage() to interpret the result as an openstack Image. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// Delete deletes the specified image ID. +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} + +// IDFromName is a convienience function that returns an image's ID given its name. +func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + allPages, err := ListDetail(client, nil).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractImages(allPages) + if err != nil { + return "", err + } + + for _, f := range all { + if f.Name == name { + count++ + id = f.ID + } + } + + switch count { + case 0: + err := &gophercloud.ErrResourceNotFound{} + err.ResourceType = "image" + err.Name = name + return "", err + case 1: + return id, nil + default: + err := &gophercloud.ErrMultipleResourcesFound{} + err.ResourceType = "image" + err.Name = name + err.Count = count + return "", err + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/results.go new file mode 100644 index 0000000000..f9ebc69e98 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/results.go @@ -0,0 +1,83 @@ +package images + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// GetResult temporarily stores a Get response. +type GetResult struct { + gophercloud.Result +} + +// DeleteResult represents the result of an image.Delete operation. +type DeleteResult struct { + gophercloud.ErrResult +} + +// Extract interprets a GetResult as an Image. +func (r GetResult) Extract() (*Image, error) { + var s struct { + Image *Image `json:"image"` + } + err := r.ExtractInto(&s) + return s.Image, err +} + +// Image is used for JSON (un)marshalling. +// It provides a description of an OS image. +type Image struct { + // ID contains the image's unique identifier. + ID string + + Created string + + // MinDisk and MinRAM specify the minimum resources a server must provide to be able to install the image. + MinDisk int + MinRAM int + + // Name provides a human-readable moniker for the OS image. + Name string + + // The Progress and Status fields indicate image-creation status. + // Any usable image will have 100% progress. + Progress int + Status string + + Updated string + + Metadata map[string]interface{} +} + +// ImagePage contains a single page of results from a List operation. +// Use ExtractImages to convert it into a slice of usable structs. +type ImagePage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a page contains no Image results. +func (page ImagePage) IsEmpty() (bool, error) { + images, err := ExtractImages(page) + return len(images) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the next page of results. +func (page ImagePage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"images_links"` + } + err := page.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// ExtractImages converts a page of List results into a slice of usable Image structs. +func ExtractImages(r pagination.Page) ([]Image, error) { + var s struct { + Images []Image `json:"images"` + } + err := (r.(ImagePage)).ExtractInto(&s) + return s.Images, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/urls.go new file mode 100644 index 0000000000..57787fb725 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/urls.go @@ -0,0 +1,15 @@ +package images + +import "github.com/gophercloud/gophercloud" + +func listDetailURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("images", "detail") +} + +func getURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("images", id) +} + +func deleteURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("images", id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/doc.go new file mode 100644 index 0000000000..fe4567120c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/doc.go @@ -0,0 +1,6 @@ +// Package servers provides information and interaction with the server API +// resource in the OpenStack Compute service. +// +// A server is a virtual machine instance in the compute system. In order for +// one to be provisioned, a valid flavor and image are required. +package servers diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/errors.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/errors.go new file mode 100644 index 0000000000..c9f0e3c20b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/errors.go @@ -0,0 +1,71 @@ +package servers + +import ( + "fmt" + + "github.com/gophercloud/gophercloud" +) + +// ErrNeitherImageIDNorImageNameProvided is the error when neither the image +// ID nor the image name is provided for a server operation +type ErrNeitherImageIDNorImageNameProvided struct{ gophercloud.ErrMissingInput } + +func (e ErrNeitherImageIDNorImageNameProvided) Error() string { + return "One and only one of the image ID and the image name must be provided." +} + +// ErrNeitherFlavorIDNorFlavorNameProvided is the error when neither the flavor +// ID nor the flavor name is provided for a server operation +type ErrNeitherFlavorIDNorFlavorNameProvided struct{ gophercloud.ErrMissingInput } + +func (e ErrNeitherFlavorIDNorFlavorNameProvided) Error() string { + return "One and only one of the flavor ID and the flavor name must be provided." +} + +type ErrNoClientProvidedForIDByName struct{ gophercloud.ErrMissingInput } + +func (e ErrNoClientProvidedForIDByName) Error() string { + return "A service client must be provided to find a resource ID by name." +} + +// ErrInvalidHowParameterProvided is the error when an unknown value is given +// for the `how` argument +type ErrInvalidHowParameterProvided struct{ gophercloud.ErrInvalidInput } + +// ErrNoAdminPassProvided is the error when an administrative password isn't +// provided for a server operation +type ErrNoAdminPassProvided struct{ gophercloud.ErrMissingInput } + +// ErrNoImageIDProvided is the error when an image ID isn't provided for a server +// operation +type ErrNoImageIDProvided struct{ gophercloud.ErrMissingInput } + +// ErrNoIDProvided is the error when a server ID isn't provided for a server +// operation +type ErrNoIDProvided struct{ gophercloud.ErrMissingInput } + +// ErrServer is a generic error type for servers HTTP operations. +type ErrServer struct { + gophercloud.ErrUnexpectedResponseCode + ID string +} + +func (se ErrServer) Error() string { + return fmt.Sprintf("Error while executing HTTP request for server [%s]", se.ID) +} + +// Error404 overrides the generic 404 error message. +func (se ErrServer) Error404(e gophercloud.ErrUnexpectedResponseCode) error { + se.ErrUnexpectedResponseCode = e + return &ErrServerNotFound{se} +} + +// ErrServerNotFound is the error when a 404 is received during server HTTP +// operations. +type ErrServerNotFound struct { + ErrServer +} + +func (e ErrServerNotFound) Error() string { + return fmt.Sprintf("I couldn't find server [%s]", e.ID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/requests.go new file mode 100644 index 0000000000..9618637317 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/requests.go @@ -0,0 +1,741 @@ +package servers + +import ( + "encoding/base64" + "encoding/json" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" + "github.com/gophercloud/gophercloud/openstack/compute/v2/images" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToServerListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the server attributes you want to see returned. Marker and Limit are used +// for pagination. +type ListOpts struct { + // A time/date stamp for when the server last changed status. + ChangesSince string `q:"changes-since"` + + // Name of the image in URL format. + Image string `q:"image"` + + // Name of the flavor in URL format. + Flavor string `q:"flavor"` + + // Name of the server as a string; can be queried with regular expressions. + // Realize that ?name=bob returns both bob and bobb. If you need to match bob + // only, you can use a regular expression matching the syntax of the + // underlying database server implemented for Compute. + Name string `q:"name"` + + // Value of the status of the server so that you can filter on "ACTIVE" for example. + Status string `q:"status"` + + // Name of the host as a string. + Host string `q:"host"` + + // UUID of the server at which you want to set a marker. + Marker string `q:"marker"` + + // Integer value for the limit of values to return. + Limit int `q:"limit"` + + // Bool to show all tenants + AllTenants bool `q:"all_tenants"` +} + +// ToServerListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToServerListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List makes a request against the API to list servers accessible to you. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listDetailURL(client) + if opts != nil { + query, err := opts.ToServerListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ServerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder describes struct types that can be accepted by the Create call. +// The CreateOpts struct in this package does. +type CreateOptsBuilder interface { + ToServerCreateMap() (map[string]interface{}, error) +} + +// Network is used within CreateOpts to control a new server's network attachments. +type Network struct { + // UUID of a nova-network to attach to the newly provisioned server. + // Required unless Port is provided. + UUID string + + // Port of a neutron network to attach to the newly provisioned server. + // Required unless UUID is provided. + Port string + + // FixedIP [optional] specifies a fixed IPv4 address to be used on this network. + FixedIP string +} + +// Personality is an array of files that are injected into the server at launch. +type Personality []*File + +// File is used within CreateOpts and RebuildOpts to inject a file into the server at launch. +// File implements the json.Marshaler interface, so when a Create or Rebuild operation is requested, +// json.Marshal will call File's MarshalJSON method. +type File struct { + // Path of the file + Path string + // Contents of the file. Maximum content size is 255 bytes. + Contents []byte +} + +// MarshalJSON marshals the escaped file, base64 encoding the contents. +func (f *File) MarshalJSON() ([]byte, error) { + file := struct { + Path string `json:"path"` + Contents string `json:"contents"` + }{ + Path: f.Path, + Contents: base64.StdEncoding.EncodeToString(f.Contents), + } + return json.Marshal(file) +} + +// CreateOpts specifies server creation parameters. +type CreateOpts struct { + // Name is the name to assign to the newly launched server. + Name string `json:"name" required:"true"` + + // ImageRef [optional; required if ImageName is not provided] is the ID or full + // URL to the image that contains the server's OS and initial state. + // Also optional if using the boot-from-volume extension. + ImageRef string `json:"imageRef"` + + // ImageName [optional; required if ImageRef is not provided] is the name of the + // image that contains the server's OS and initial state. + // Also optional if using the boot-from-volume extension. + ImageName string `json:"-"` + + // FlavorRef [optional; required if FlavorName is not provided] is the ID or + // full URL to the flavor that describes the server's specs. + FlavorRef string `json:"flavorRef"` + + // FlavorName [optional; required if FlavorRef is not provided] is the name of + // the flavor that describes the server's specs. + FlavorName string `json:"-"` + + // SecurityGroups lists the names of the security groups to which this server should belong. + SecurityGroups []string `json:"-"` + + // UserData contains configuration information or scripts to use upon launch. + // Create will base64-encode it for you, if it isn't already. + UserData []byte `json:"-"` + + // AvailabilityZone in which to launch the server. + AvailabilityZone string `json:"availability_zone,omitempty"` + + // Networks dictates how this server will be attached to available networks. + // By default, the server will be attached to all isolated networks for the tenant. + Networks []Network `json:"-"` + + // Metadata contains key-value pairs (up to 255 bytes each) to attach to the server. + Metadata map[string]string `json:"metadata,omitempty"` + + // Personality includes files to inject into the server at launch. + // Create will base64-encode file contents for you. + Personality Personality `json:"personality,omitempty"` + + // ConfigDrive enables metadata injection through a configuration drive. + ConfigDrive *bool `json:"config_drive,omitempty"` + + // AdminPass sets the root user password. If not set, a randomly-generated + // password will be created and returned in the rponse. + AdminPass string `json:"adminPass,omitempty"` + + // AccessIPv4 specifies an IPv4 address for the instance. + AccessIPv4 string `json:"accessIPv4,omitempty"` + + // AccessIPv6 pecifies an IPv6 address for the instance. + AccessIPv6 string `json:"accessIPv6,omitempty"` + + // ServiceClient will allow calls to be made to retrieve an image or + // flavor ID by name. + ServiceClient *gophercloud.ServiceClient `json:"-"` +} + +// ToServerCreateMap assembles a request body based on the contents of a CreateOpts. +func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) { + sc := opts.ServiceClient + opts.ServiceClient = nil + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if opts.UserData != nil { + var userData string + if _, err := base64.StdEncoding.DecodeString(string(opts.UserData)); err != nil { + userData = base64.StdEncoding.EncodeToString(opts.UserData) + } else { + userData = string(opts.UserData) + } + b["user_data"] = &userData + } + + if len(opts.SecurityGroups) > 0 { + securityGroups := make([]map[string]interface{}, len(opts.SecurityGroups)) + for i, groupName := range opts.SecurityGroups { + securityGroups[i] = map[string]interface{}{"name": groupName} + } + b["security_groups"] = securityGroups + } + + if len(opts.Networks) > 0 { + networks := make([]map[string]interface{}, len(opts.Networks)) + for i, net := range opts.Networks { + networks[i] = make(map[string]interface{}) + if net.UUID != "" { + networks[i]["uuid"] = net.UUID + } + if net.Port != "" { + networks[i]["port"] = net.Port + } + if net.FixedIP != "" { + networks[i]["fixed_ip"] = net.FixedIP + } + } + b["networks"] = networks + } + + // If ImageRef isn't provided, check if ImageName was provided to ascertain + // the image ID. + if opts.ImageRef == "" { + if opts.ImageName != "" { + if sc == nil { + err := ErrNoClientProvidedForIDByName{} + err.Argument = "ServiceClient" + return nil, err + } + imageID, err := images.IDFromName(sc, opts.ImageName) + if err != nil { + return nil, err + } + b["imageRef"] = imageID + } + } + + // If FlavorRef isn't provided, use FlavorName to ascertain the flavor ID. + if opts.FlavorRef == "" { + if opts.FlavorName == "" { + err := ErrNeitherFlavorIDNorFlavorNameProvided{} + err.Argument = "FlavorRef/FlavorName" + return nil, err + } + if sc == nil { + err := ErrNoClientProvidedForIDByName{} + err.Argument = "ServiceClient" + return nil, err + } + flavorID, err := flavors.IDFromName(sc, opts.FlavorName) + if err != nil { + return nil, err + } + b["flavorRef"] = flavorID + } + + return map[string]interface{}{"server": b}, nil +} + +// Create requests a server to be provisioned to the user in the current tenant. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + reqBody, err := opts.ToServerCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(listURL(client), reqBody, &r.Body, nil) + return +} + +// Delete requests that a server previously provisioned be removed from your account. +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, id), nil) + return +} + +// ForceDelete forces the deletion of a server +func ForceDelete(client *gophercloud.ServiceClient, id string) (r ActionResult) { + _, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"forceDelete": ""}, nil, nil) + return +} + +// Get requests details on a single server, by ID. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 203}, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional attributes to the Update request. +type UpdateOptsBuilder interface { + ToServerUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts specifies the base attributes that may be updated on an existing server. +type UpdateOpts struct { + // Name changes the displayed name of the server. + // The server host name will *not* change. + // Server names are not constrained to be unique, even within the same tenant. + Name string `json:"name,omitempty"` + + // AccessIPv4 provides a new IPv4 address for the instance. + AccessIPv4 string `json:"accessIPv4,omitempty"` + + // AccessIPv6 provides a new IPv6 address for the instance. + AccessIPv6 string `json:"accessIPv6,omitempty"` +} + +// ToServerUpdateMap formats an UpdateOpts structure into a request body. +func (opts UpdateOpts) ToServerUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "server") +} + +// Update requests that various attributes of the indicated server be changed. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToServerUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// ChangeAdminPassword alters the administrator or root password for a specified server. +func ChangeAdminPassword(client *gophercloud.ServiceClient, id, newPassword string) (r ActionResult) { + b := map[string]interface{}{ + "changePassword": map[string]string{ + "adminPass": newPassword, + }, + } + _, r.Err = client.Post(actionURL(client, id), b, nil, nil) + return +} + +// RebootMethod describes the mechanisms by which a server reboot can be requested. +type RebootMethod string + +// These constants determine how a server should be rebooted. +// See the Reboot() function for further details. +const ( + SoftReboot RebootMethod = "SOFT" + HardReboot RebootMethod = "HARD" + OSReboot = SoftReboot + PowerCycle = HardReboot +) + +// RebootOptsBuilder is an interface that options must satisfy in order to be +// used when rebooting a server instance +type RebootOptsBuilder interface { + ToServerRebootMap() (map[string]interface{}, error) +} + +// RebootOpts satisfies the RebootOptsBuilder interface +type RebootOpts struct { + Type RebootMethod `json:"type" required:"true"` +} + +// ToServerRebootMap allows RebootOpts to satisfiy the RebootOptsBuilder +// interface +func (opts *RebootOpts) ToServerRebootMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "reboot") +} + +// Reboot requests that a given server reboot. +// Two methods exist for rebooting a server: +// +// HardReboot (aka PowerCycle) starts the server instance by physically cutting power to the machine, or if a VM, +// terminating it at the hypervisor level. +// It's done. Caput. Full stop. +// Then, after a brief while, power is rtored or the VM instance rtarted. +// +// SoftReboot (aka OSReboot) simply tells the OS to rtart under its own procedur. +// E.g., in Linux, asking it to enter runlevel 6, or executing "sudo shutdown -r now", or by asking Windows to rtart the machine. +func Reboot(client *gophercloud.ServiceClient, id string, opts RebootOptsBuilder) (r ActionResult) { + b, err := opts.ToServerRebootMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(actionURL(client, id), b, nil, nil) + return +} + +// RebuildOptsBuilder is an interface that allows extensions to override the +// default behaviour of rebuild options +type RebuildOptsBuilder interface { + ToServerRebuildMap() (map[string]interface{}, error) +} + +// RebuildOpts represents the configuration options used in a server rebuild +// operation +type RebuildOpts struct { + // The server's admin password + AdminPass string `json:"adminPass,omitempty"` + // The ID of the image you want your server to be provisioned on + ImageID string `json:"imageRef"` + ImageName string `json:"-"` + // Name to set the server to + Name string `json:"name,omitempty"` + // AccessIPv4 [optional] provides a new IPv4 address for the instance. + AccessIPv4 string `json:"accessIPv4,omitempty"` + // AccessIPv6 [optional] provides a new IPv6 address for the instance. + AccessIPv6 string `json:"accessIPv6,omitempty"` + // Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server. + Metadata map[string]string `json:"metadata,omitempty"` + // Personality [optional] includes files to inject into the server at launch. + // Rebuild will base64-encode file contents for you. + Personality Personality `json:"personality,omitempty"` + ServiceClient *gophercloud.ServiceClient `json:"-"` +} + +// ToServerRebuildMap formats a RebuildOpts struct into a map for use in JSON +func (opts RebuildOpts) ToServerRebuildMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + // If ImageRef isn't provided, check if ImageName was provided to ascertain + // the image ID. + if opts.ImageID == "" { + if opts.ImageName != "" { + if opts.ServiceClient == nil { + err := ErrNoClientProvidedForIDByName{} + err.Argument = "ServiceClient" + return nil, err + } + imageID, err := images.IDFromName(opts.ServiceClient, opts.ImageName) + if err != nil { + return nil, err + } + b["imageRef"] = imageID + } + } + + return map[string]interface{}{"rebuild": b}, nil +} + +// Rebuild will reprovision the server according to the configuration options +// provided in the RebuildOpts struct. +func Rebuild(client *gophercloud.ServiceClient, id string, opts RebuildOptsBuilder) (r RebuildResult) { + b, err := opts.ToServerRebuildMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(actionURL(client, id), b, &r.Body, nil) + return +} + +// ResizeOptsBuilder is an interface that allows extensions to override the default structure of +// a Resize request. +type ResizeOptsBuilder interface { + ToServerResizeMap() (map[string]interface{}, error) +} + +// ResizeOpts represents the configuration options used to control a Resize operation. +type ResizeOpts struct { + // FlavorRef is the ID of the flavor you wish your server to become. + FlavorRef string `json:"flavorRef" required:"true"` +} + +// ToServerResizeMap formats a ResizeOpts as a map that can be used as a JSON request body for the +// Resize request. +func (opts ResizeOpts) ToServerResizeMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "resize") +} + +// Resize instructs the provider to change the flavor of the server. +// Note that this implies rebuilding it. +// Unfortunately, one cannot pass rebuild parameters to the resize function. +// When the resize completes, the server will be in RESIZE_VERIFY state. +// While in this state, you can explore the use of the new server's configuration. +// If you like it, call ConfirmResize() to commit the resize permanently. +// Otherwise, call RevertResize() to restore the old configuration. +func Resize(client *gophercloud.ServiceClient, id string, opts ResizeOptsBuilder) (r ActionResult) { + b, err := opts.ToServerResizeMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(actionURL(client, id), b, nil, nil) + return +} + +// ConfirmResize confirms a previous resize operation on a server. +// See Resize() for more details. +func ConfirmResize(client *gophercloud.ServiceClient, id string) (r ActionResult) { + _, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"confirmResize": nil}, nil, &gophercloud.RequestOpts{ + OkCodes: []int{201, 202, 204}, + }) + return +} + +// RevertResize cancels a previous resize operation on a server. +// See Resize() for more details. +func RevertResize(client *gophercloud.ServiceClient, id string) (r ActionResult) { + _, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"revertResize": nil}, nil, nil) + return +} + +// RescueOptsBuilder is an interface that allows extensions to override the +// default structure of a Rescue request. +type RescueOptsBuilder interface { + ToServerRescueMap() (map[string]interface{}, error) +} + +// RescueOpts represents the configuration options used to control a Rescue +// option. +type RescueOpts struct { + // AdminPass is the desired administrative password for the instance in + // RESCUE mode. If it's left blank, the server will generate a password. + AdminPass string `json:"adminPass,omitempty"` +} + +// ToServerRescueMap formats a RescueOpts as a map that can be used as a JSON +// request body for the Rescue request. +func (opts RescueOpts) ToServerRescueMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "rescue") +} + +// Rescue instructs the provider to place the server into RESCUE mode. +func Rescue(client *gophercloud.ServiceClient, id string, opts RescueOptsBuilder) (r RescueResult) { + b, err := opts.ToServerRescueMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(actionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// ResetMetadataOptsBuilder allows extensions to add additional parameters to the +// Reset request. +type ResetMetadataOptsBuilder interface { + ToMetadataResetMap() (map[string]interface{}, error) +} + +// MetadataOpts is a map that contains key-value pairs. +type MetadataOpts map[string]string + +// ToMetadataResetMap assembles a body for a Reset request based on the contents of a MetadataOpts. +func (opts MetadataOpts) ToMetadataResetMap() (map[string]interface{}, error) { + return map[string]interface{}{"metadata": opts}, nil +} + +// ToMetadataUpdateMap assembles a body for an Update request based on the contents of a MetadataOpts. +func (opts MetadataOpts) ToMetadataUpdateMap() (map[string]interface{}, error) { + return map[string]interface{}{"metadata": opts}, nil +} + +// ResetMetadata will create multiple new key-value pairs for the given server ID. +// Note: Using this operation will erase any already-existing metadata and create +// the new metadata provided. To keep any already-existing metadata, use the +// UpdateMetadatas or UpdateMetadata function. +func ResetMetadata(client *gophercloud.ServiceClient, id string, opts ResetMetadataOptsBuilder) (r ResetMetadataResult) { + b, err := opts.ToMetadataResetMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(metadataURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Metadata requests all the metadata for the given server ID. +func Metadata(client *gophercloud.ServiceClient, id string) (r GetMetadataResult) { + _, r.Err = client.Get(metadataURL(client, id), &r.Body, nil) + return +} + +// UpdateMetadataOptsBuilder allows extensions to add additional parameters to the +// Create request. +type UpdateMetadataOptsBuilder interface { + ToMetadataUpdateMap() (map[string]interface{}, error) +} + +// UpdateMetadata updates (or creates) all the metadata specified by opts for the given server ID. +// This operation does not affect already-existing metadata that is not specified +// by opts. +func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts UpdateMetadataOptsBuilder) (r UpdateMetadataResult) { + b, err := opts.ToMetadataUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(metadataURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// MetadatumOptsBuilder allows extensions to add additional parameters to the +// Create request. +type MetadatumOptsBuilder interface { + ToMetadatumCreateMap() (map[string]interface{}, string, error) +} + +// MetadatumOpts is a map of length one that contains a key-value pair. +type MetadatumOpts map[string]string + +// ToMetadatumCreateMap assembles a body for a Create request based on the contents of a MetadataumOpts. +func (opts MetadatumOpts) ToMetadatumCreateMap() (map[string]interface{}, string, error) { + if len(opts) != 1 { + err := gophercloud.ErrInvalidInput{} + err.Argument = "servers.MetadatumOpts" + err.Info = "Must have 1 and only 1 key-value pair" + return nil, "", err + } + metadatum := map[string]interface{}{"meta": opts} + var key string + for k := range metadatum["meta"].(MetadatumOpts) { + key = k + } + return metadatum, key, nil +} + +// CreateMetadatum will create or update the key-value pair with the given key for the given server ID. +func CreateMetadatum(client *gophercloud.ServiceClient, id string, opts MetadatumOptsBuilder) (r CreateMetadatumResult) { + b, key, err := opts.ToMetadatumCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Put(metadatumURL(client, id, key), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Metadatum requests the key-value pair with the given key for the given server ID. +func Metadatum(client *gophercloud.ServiceClient, id, key string) (r GetMetadatumResult) { + _, r.Err = client.Get(metadatumURL(client, id, key), &r.Body, nil) + return +} + +// DeleteMetadatum will delete the key-value pair with the given key for the given server ID. +func DeleteMetadatum(client *gophercloud.ServiceClient, id, key string) (r DeleteMetadatumResult) { + _, r.Err = client.Delete(metadatumURL(client, id, key), nil) + return +} + +// ListAddresses makes a request against the API to list the servers IP addresses. +func ListAddresses(client *gophercloud.ServiceClient, id string) pagination.Pager { + return pagination.NewPager(client, listAddressesURL(client, id), func(r pagination.PageResult) pagination.Page { + return AddressPage{pagination.SinglePageBase(r)} + }) +} + +// ListAddressesByNetwork makes a request against the API to list the servers IP addresses +// for the given network. +func ListAddressesByNetwork(client *gophercloud.ServiceClient, id, network string) pagination.Pager { + return pagination.NewPager(client, listAddressesByNetworkURL(client, id, network), func(r pagination.PageResult) pagination.Page { + return NetworkAddressPage{pagination.SinglePageBase(r)} + }) +} + +// CreateImageOptsBuilder is the interface types must satisfy in order to be +// used as CreateImage options +type CreateImageOptsBuilder interface { + ToServerCreateImageMap() (map[string]interface{}, error) +} + +// CreateImageOpts satisfies the CreateImageOptsBuilder +type CreateImageOpts struct { + // Name of the image/snapshot + Name string `json:"name" required:"true"` + // Metadata contains key-value pairs (up to 255 bytes each) to attach to the created image. + Metadata map[string]string `json:"metadata,omitempty"` +} + +// ToServerCreateImageMap formats a CreateImageOpts structure into a request body. +func (opts CreateImageOpts) ToServerCreateImageMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "createImage") +} + +// CreateImage makes a request against the nova API to schedule an image to be created of the server +func CreateImage(client *gophercloud.ServiceClient, id string, opts CreateImageOptsBuilder) (r CreateImageResult) { + b, err := opts.ToServerCreateImageMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + r.Err = err + r.Header = resp.Header + return +} + +// IDFromName is a convienience function that returns a server's ID given its name. +func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) { + count := 0 + id := "" + allPages, err := List(client, nil).AllPages() + if err != nil { + return "", err + } + + all, err := ExtractServers(allPages) + if err != nil { + return "", err + } + + for _, f := range all { + if f.Name == name { + count++ + id = f.ID + } + } + + switch count { + case 0: + return "", gophercloud.ErrResourceNotFound{Name: name, ResourceType: "server"} + case 1: + return id, nil + default: + return "", gophercloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "server"} + } +} + +// GetPassword makes a request against the nova API to get the encrypted administrative password. +func GetPassword(client *gophercloud.ServiceClient, serverId string) (r GetPasswordResult) { + _, r.Err = client.Get(passwordURL(client, serverId), &r.Body, nil) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/results.go new file mode 100644 index 0000000000..1ae1e91c78 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/results.go @@ -0,0 +1,350 @@ +package servers + +import ( + "crypto/rsa" + "encoding/base64" + "encoding/json" + "fmt" + "net/url" + "path" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type serverResult struct { + gophercloud.Result +} + +// Extract interprets any serverResult as a Server, if possible. +func (r serverResult) Extract() (*Server, error) { + var s Server + err := r.ExtractInto(&s) + return &s, err +} + +func (r serverResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "server") +} + +func ExtractServersInto(r pagination.Page, v interface{}) error { + return r.(ServerPage).Result.ExtractIntoSlicePtr(v, "servers") +} + +// CreateResult temporarily contains the response from a Create call. +type CreateResult struct { + serverResult +} + +// GetResult temporarily contains the response from a Get call. +type GetResult struct { + serverResult +} + +// UpdateResult temporarily contains the response from an Update call. +type UpdateResult struct { + serverResult +} + +// DeleteResult temporarily contains the response from a Delete call. +type DeleteResult struct { + gophercloud.ErrResult +} + +// RebuildResult temporarily contains the response from a Rebuild call. +type RebuildResult struct { + serverResult +} + +// ActionResult represents the result of server action operations, like reboot +type ActionResult struct { + gophercloud.ErrResult +} + +// RescueResult represents the result of a server rescue operation +type RescueResult struct { + ActionResult +} + +// CreateImageResult represents the result of an image creation operation +type CreateImageResult struct { + gophercloud.Result +} + +// GetPasswordResult represent the result of a get os-server-password operation. +type GetPasswordResult struct { + gophercloud.Result +} + +// ExtractPassword gets the encrypted password. +// If privateKey != nil the password is decrypted with the private key. +// If privateKey == nil the encrypted password is returned and can be decrypted with: +// echo '' | base64 -D | openssl rsautl -decrypt -inkey +func (r GetPasswordResult) ExtractPassword(privateKey *rsa.PrivateKey) (string, error) { + var s struct { + Password string `json:"password"` + } + err := r.ExtractInto(&s) + if err == nil && privateKey != nil && s.Password != "" { + return decryptPassword(s.Password, privateKey) + } + return s.Password, err +} + +func decryptPassword(encryptedPassword string, privateKey *rsa.PrivateKey) (string, error) { + b64EncryptedPassword := make([]byte, base64.StdEncoding.DecodedLen(len(encryptedPassword))) + + n, err := base64.StdEncoding.Decode(b64EncryptedPassword, []byte(encryptedPassword)) + if err != nil { + return "", fmt.Errorf("Failed to base64 decode encrypted password: %s", err) + } + password, err := rsa.DecryptPKCS1v15(nil, privateKey, b64EncryptedPassword[0:n]) + if err != nil { + return "", fmt.Errorf("Failed to decrypt password: %s", err) + } + + return string(password), nil +} + +// ExtractImageID gets the ID of the newly created server image from the header +func (r CreateImageResult) ExtractImageID() (string, error) { + if r.Err != nil { + return "", r.Err + } + // Get the image id from the header + u, err := url.ParseRequestURI(r.Header.Get("Location")) + if err != nil { + return "", err + } + imageID := path.Base(u.Path) + if imageID == "." || imageID == "/" { + return "", fmt.Errorf("Failed to parse the ID of newly created image: %s", u) + } + return imageID, nil +} + +// Extract interprets any RescueResult as an AdminPass, if possible. +func (r RescueResult) Extract() (string, error) { + var s struct { + AdminPass string `json:"adminPass"` + } + err := r.ExtractInto(&s) + return s.AdminPass, err +} + +// Server exposes only the standard OpenStack fields corresponding to a given server on the user's account. +type Server struct { + // ID uniquely identifies this server amongst all other servers, including those not accessible to the current tenant. + ID string `json:"id"` + // TenantID identifies the tenant owning this server resource. + TenantID string `json:"tenant_id"` + // UserID uniquely identifies the user account owning the tenant. + UserID string `json:"user_id"` + // Name contains the human-readable name for the server. + Name string `json:"name"` + // Updated and Created contain ISO-8601 timestamps of when the state of the server last changed, and when it was created. + Updated time.Time `json:"updated"` + Created time.Time `json:"created"` + HostID string `json:"hostid"` + // Status contains the current operational status of the server, such as IN_PROGRESS or ACTIVE. + Status string `json:"status"` + // Progress ranges from 0..100. + // A request made against the server completes only once Progress reaches 100. + Progress int `json:"progress"` + // AccessIPv4 and AccessIPv6 contain the IP addresses of the server, suitable for remote access for administration. + AccessIPv4 string `json:"accessIPv4"` + AccessIPv6 string `json:"accessIPv6"` + // Image refers to a JSON object, which itself indicates the OS image used to deploy the server. + Image map[string]interface{} `json:"-"` + // Flavor refers to a JSON object, which itself indicates the hardware configuration of the deployed server. + Flavor map[string]interface{} `json:"flavor"` + // Addresses includes a list of all IP addresses assigned to the server, keyed by pool. + Addresses map[string]interface{} `json:"addresses"` + // Metadata includes a list of all user-specified key-value pairs attached to the server. + Metadata map[string]string `json:"metadata"` + // Links includes HTTP references to the itself, useful for passing along to other APIs that might want a server reference. + Links []interface{} `json:"links"` + // KeyName indicates which public key was injected into the server on launch. + KeyName string `json:"key_name"` + // AdminPass will generally be empty (""). However, it will contain the administrative password chosen when provisioning a new server without a set AdminPass setting in the first place. + // Note that this is the ONLY time this field will be valid. + AdminPass string `json:"adminPass"` + // SecurityGroups includes the security groups that this instance has applied to it + SecurityGroups []map[string]interface{} `json:"security_groups"` +} + +func (r *Server) UnmarshalJSON(b []byte) error { + type tmp Server + var s struct { + tmp + Image interface{} `json:"image"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = Server(s.tmp) + + switch t := s.Image.(type) { + case map[string]interface{}: + r.Image = t + case string: + switch t { + case "": + r.Image = nil + } + } + + return err +} + +// ServerPage abstracts the raw results of making a List() request against the API. +// As OpenStack extensions may freely alter the response bodies of structures returned to the client, you may only safely access the +// data provided through the ExtractServers call. +type ServerPage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a page contains no Server results. +func (r ServerPage) IsEmpty() (bool, error) { + s, err := ExtractServers(r) + return len(s) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the next page of results. +func (r ServerPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"servers_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// ExtractServers interprets the results of a single page from a List() call, producing a slice of Server entities. +func ExtractServers(r pagination.Page) ([]Server, error) { + var s []Server + err := ExtractServersInto(r, &s) + return s, err +} + +// MetadataResult contains the result of a call for (potentially) multiple key-value pairs. +type MetadataResult struct { + gophercloud.Result +} + +// GetMetadataResult temporarily contains the response from a metadata Get call. +type GetMetadataResult struct { + MetadataResult +} + +// ResetMetadataResult temporarily contains the response from a metadata Reset call. +type ResetMetadataResult struct { + MetadataResult +} + +// UpdateMetadataResult temporarily contains the response from a metadata Update call. +type UpdateMetadataResult struct { + MetadataResult +} + +// MetadatumResult contains the result of a call for individual a single key-value pair. +type MetadatumResult struct { + gophercloud.Result +} + +// GetMetadatumResult temporarily contains the response from a metadatum Get call. +type GetMetadatumResult struct { + MetadatumResult +} + +// CreateMetadatumResult temporarily contains the response from a metadatum Create call. +type CreateMetadatumResult struct { + MetadatumResult +} + +// DeleteMetadatumResult temporarily contains the response from a metadatum Delete call. +type DeleteMetadatumResult struct { + gophercloud.ErrResult +} + +// Extract interprets any MetadataResult as a Metadata, if possible. +func (r MetadataResult) Extract() (map[string]string, error) { + var s struct { + Metadata map[string]string `json:"metadata"` + } + err := r.ExtractInto(&s) + return s.Metadata, err +} + +// Extract interprets any MetadatumResult as a Metadatum, if possible. +func (r MetadatumResult) Extract() (map[string]string, error) { + var s struct { + Metadatum map[string]string `json:"meta"` + } + err := r.ExtractInto(&s) + return s.Metadatum, err +} + +// Address represents an IP address. +type Address struct { + Version int `json:"version"` + Address string `json:"addr"` +} + +// AddressPage abstracts the raw results of making a ListAddresses() request against the API. +// As OpenStack extensions may freely alter the response bodies of structures returned +// to the client, you may only safely access the data provided through the ExtractAddresses call. +type AddressPage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if an AddressPage contains no networks. +func (r AddressPage) IsEmpty() (bool, error) { + addresses, err := ExtractAddresses(r) + return len(addresses) == 0, err +} + +// ExtractAddresses interprets the results of a single page from a ListAddresses() call, +// producing a map of addresses. +func ExtractAddresses(r pagination.Page) (map[string][]Address, error) { + var s struct { + Addresses map[string][]Address `json:"addresses"` + } + err := (r.(AddressPage)).ExtractInto(&s) + return s.Addresses, err +} + +// NetworkAddressPage abstracts the raw results of making a ListAddressesByNetwork() request against the API. +// As OpenStack extensions may freely alter the response bodies of structures returned +// to the client, you may only safely access the data provided through the ExtractAddresses call. +type NetworkAddressPage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a NetworkAddressPage contains no addresses. +func (r NetworkAddressPage) IsEmpty() (bool, error) { + addresses, err := ExtractNetworkAddresses(r) + return len(addresses) == 0, err +} + +// ExtractNetworkAddresses interprets the results of a single page from a ListAddressesByNetwork() call, +// producing a slice of addresses. +func ExtractNetworkAddresses(r pagination.Page) ([]Address, error) { + var s map[string][]Address + err := (r.(NetworkAddressPage)).ExtractInto(&s) + if err != nil { + return nil, err + } + + var key string + for k := range s { + key = k + } + + return s[key], err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/urls.go new file mode 100644 index 0000000000..e892e8d925 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/urls.go @@ -0,0 +1,51 @@ +package servers + +import "github.com/gophercloud/gophercloud" + +func createURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("servers") +} + +func listURL(client *gophercloud.ServiceClient) string { + return createURL(client) +} + +func listDetailURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("servers", "detail") +} + +func deleteURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id) +} + +func getURL(client *gophercloud.ServiceClient, id string) string { + return deleteURL(client, id) +} + +func updateURL(client *gophercloud.ServiceClient, id string) string { + return deleteURL(client, id) +} + +func actionURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "action") +} + +func metadatumURL(client *gophercloud.ServiceClient, id, key string) string { + return client.ServiceURL("servers", id, "metadata", key) +} + +func metadataURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "metadata") +} + +func listAddressesURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "ips") +} + +func listAddressesByNetworkURL(client *gophercloud.ServiceClient, id, network string) string { + return client.ServiceURL("servers", id, "ips", network) +} + +func passwordURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "os-server-password") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/util.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/util.go new file mode 100644 index 0000000000..494a0e4dc4 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/util.go @@ -0,0 +1,20 @@ +package servers + +import "github.com/gophercloud/gophercloud" + +// WaitForStatus will continually poll a server until it successfully transitions to a specified +// status. It will do this for at most the number of seconds specified. +func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error { + return gophercloud.WaitFor(secs, func() (bool, error) { + current, err := Get(c, id).Extract() + if err != nil { + return false, err + } + + if current.Status == status { + return true, nil + } + + return false, nil + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/endpoint_location.go b/vendor/github.com/gophercloud/gophercloud/openstack/endpoint_location.go new file mode 100644 index 0000000000..ea37f5b271 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/endpoint_location.go @@ -0,0 +1,99 @@ +package openstack + +import ( + "github.com/gophercloud/gophercloud" + tokens2 "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens" + tokens3 "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" +) + +// V2EndpointURL discovers the endpoint URL for a specific service from a ServiceCatalog acquired +// during the v2 identity service. The specified EndpointOpts are used to identify a unique, +// unambiguous endpoint to return. It's an error both when multiple endpoints match the provided +// criteria and when none do. The minimum that can be specified is a Type, but you will also often +// need to specify a Name and/or a Region depending on what's available on your OpenStack +// deployment. +func V2EndpointURL(catalog *tokens2.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) { + // Extract Endpoints from the catalog entries that match the requested Type, Name if provided, and Region if provided. + var endpoints = make([]tokens2.Endpoint, 0, 1) + for _, entry := range catalog.Entries { + if (entry.Type == opts.Type) && (opts.Name == "" || entry.Name == opts.Name) { + for _, endpoint := range entry.Endpoints { + if opts.Region == "" || endpoint.Region == opts.Region { + endpoints = append(endpoints, endpoint) + } + } + } + } + + // Report an error if the options were ambiguous. + if len(endpoints) > 1 { + err := &ErrMultipleMatchingEndpointsV2{} + err.Endpoints = endpoints + return "", err + } + + // Extract the appropriate URL from the matching Endpoint. + for _, endpoint := range endpoints { + switch opts.Availability { + case gophercloud.AvailabilityPublic: + return gophercloud.NormalizeURL(endpoint.PublicURL), nil + case gophercloud.AvailabilityInternal: + return gophercloud.NormalizeURL(endpoint.InternalURL), nil + case gophercloud.AvailabilityAdmin: + return gophercloud.NormalizeURL(endpoint.AdminURL), nil + default: + err := &ErrInvalidAvailabilityProvided{} + err.Argument = "Availability" + err.Value = opts.Availability + return "", err + } + } + + // Report an error if there were no matching endpoints. + err := &gophercloud.ErrEndpointNotFound{} + return "", err +} + +// V3EndpointURL discovers the endpoint URL for a specific service from a Catalog acquired +// during the v3 identity service. The specified EndpointOpts are used to identify a unique, +// unambiguous endpoint to return. It's an error both when multiple endpoints match the provided +// criteria and when none do. The minimum that can be specified is a Type, but you will also often +// need to specify a Name and/or a Region depending on what's available on your OpenStack +// deployment. +func V3EndpointURL(catalog *tokens3.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) { + // Extract Endpoints from the catalog entries that match the requested Type, Interface, + // Name if provided, and Region if provided. + var endpoints = make([]tokens3.Endpoint, 0, 1) + for _, entry := range catalog.Entries { + if (entry.Type == opts.Type) && (opts.Name == "" || entry.Name == opts.Name) { + for _, endpoint := range entry.Endpoints { + if opts.Availability != gophercloud.AvailabilityAdmin && + opts.Availability != gophercloud.AvailabilityPublic && + opts.Availability != gophercloud.AvailabilityInternal { + err := &ErrInvalidAvailabilityProvided{} + err.Argument = "Availability" + err.Value = opts.Availability + return "", err + } + if (opts.Availability == gophercloud.Availability(endpoint.Interface)) && + (opts.Region == "" || endpoint.Region == opts.Region) { + endpoints = append(endpoints, endpoint) + } + } + } + } + + // Report an error if the options were ambiguous. + if len(endpoints) > 1 { + return "", ErrMultipleMatchingEndpointsV3{Endpoints: endpoints} + } + + // Extract the URL from the matching Endpoint. + for _, endpoint := range endpoints { + return gophercloud.NormalizeURL(endpoint.URL), nil + } + + // Report an error if there were no matching endpoints. + err := &gophercloud.ErrEndpointNotFound{} + return "", err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/errors.go b/vendor/github.com/gophercloud/gophercloud/openstack/errors.go new file mode 100644 index 0000000000..df410b1c61 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/errors.go @@ -0,0 +1,71 @@ +package openstack + +import ( + "fmt" + + "github.com/gophercloud/gophercloud" + tokens2 "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens" + tokens3 "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" +) + +// ErrEndpointNotFound is the error when no suitable endpoint can be found +// in the user's catalog +type ErrEndpointNotFound struct{ gophercloud.BaseError } + +func (e ErrEndpointNotFound) Error() string { + return "No suitable endpoint could be found in the service catalog." +} + +// ErrInvalidAvailabilityProvided is the error when an invalid endpoint +// availability is provided +type ErrInvalidAvailabilityProvided struct{ gophercloud.ErrInvalidInput } + +func (e ErrInvalidAvailabilityProvided) Error() string { + return fmt.Sprintf("Unexpected availability in endpoint query: %s", e.Value) +} + +// ErrMultipleMatchingEndpointsV2 is the error when more than one endpoint +// for the given options is found in the v2 catalog +type ErrMultipleMatchingEndpointsV2 struct { + gophercloud.BaseError + Endpoints []tokens2.Endpoint +} + +func (e ErrMultipleMatchingEndpointsV2) Error() string { + return fmt.Sprintf("Discovered %d matching endpoints: %#v", len(e.Endpoints), e.Endpoints) +} + +// ErrMultipleMatchingEndpointsV3 is the error when more than one endpoint +// for the given options is found in the v3 catalog +type ErrMultipleMatchingEndpointsV3 struct { + gophercloud.BaseError + Endpoints []tokens3.Endpoint +} + +func (e ErrMultipleMatchingEndpointsV3) Error() string { + return fmt.Sprintf("Discovered %d matching endpoints: %#v", len(e.Endpoints), e.Endpoints) +} + +// ErrNoAuthURL is the error when the OS_AUTH_URL environment variable is not +// found +type ErrNoAuthURL struct{ gophercloud.ErrInvalidInput } + +func (e ErrNoAuthURL) Error() string { + return "Environment variable OS_AUTH_URL needs to be set." +} + +// ErrNoUsername is the error when the OS_USERNAME environment variable is not +// found +type ErrNoUsername struct{ gophercloud.ErrInvalidInput } + +func (e ErrNoUsername) Error() string { + return "Environment variable OS_USERNAME needs to be set." +} + +// ErrNoPassword is the error when the OS_PASSWORD environment variable is not +// found +type ErrNoPassword struct{ gophercloud.ErrInvalidInput } + +func (e ErrNoPassword) Error() string { + return "Environment variable OS_PASSWORD needs to be set." +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/doc.go new file mode 100644 index 0000000000..0c2d49d567 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/doc.go @@ -0,0 +1,7 @@ +// Package tenants provides information and interaction with the +// tenants API resource for the OpenStack Identity service. +// +// See http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2 +// and http://developer.openstack.org/api-ref-identity-v2.html#admin-tenants +// for more information. +package tenants diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/requests.go new file mode 100644 index 0000000000..b9d7de65fa --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/requests.go @@ -0,0 +1,29 @@ +package tenants + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOpts filters the Tenants that are returned by the List call. +type ListOpts struct { + // Marker is the ID of the last Tenant on the previous page. + Marker string `q:"marker"` + // Limit specifies the page size. + Limit int `q:"limit"` +} + +// List enumerates the Tenants to which the current token has access. +func List(client *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager { + url := listURL(client) + if opts != nil { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return pagination.Pager{Err: err} + } + url += q.String() + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return TenantPage{pagination.LinkedPageBase{PageResult: r}} + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/results.go new file mode 100644 index 0000000000..3ce1e67736 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/results.go @@ -0,0 +1,53 @@ +package tenants + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Tenant is a grouping of users in the identity service. +type Tenant struct { + // ID is a unique identifier for this tenant. + ID string `json:"id"` + + // Name is a friendlier user-facing name for this tenant. + Name string `json:"name"` + + // Description is a human-readable explanation of this Tenant's purpose. + Description string `json:"description"` + + // Enabled indicates whether or not a tenant is active. + Enabled bool `json:"enabled"` +} + +// TenantPage is a single page of Tenant results. +type TenantPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Tenants contains any results. +func (r TenantPage) IsEmpty() (bool, error) { + tenants, err := ExtractTenants(r) + return len(tenants) == 0, err +} + +// NextPageURL extracts the "next" link from the tenants_links section of the result. +func (r TenantPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"tenants_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// ExtractTenants returns a slice of Tenants contained in a single page of results. +func ExtractTenants(r pagination.Page) ([]Tenant, error) { + var s struct { + Tenants []Tenant `json:"tenants"` + } + err := (r.(TenantPage)).ExtractInto(&s) + return s.Tenants, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/urls.go new file mode 100644 index 0000000000..101599bc94 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/urls.go @@ -0,0 +1,7 @@ +package tenants + +import "github.com/gophercloud/gophercloud" + +func listURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("tenants") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/doc.go new file mode 100644 index 0000000000..31cacc5e17 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/doc.go @@ -0,0 +1,5 @@ +// Package tokens provides information and interaction with the token API +// resource for the OpenStack Identity service. +// For more information, see: +// http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2 +package tokens diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/requests.go new file mode 100644 index 0000000000..4983031e7f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/requests.go @@ -0,0 +1,99 @@ +package tokens + +import "github.com/gophercloud/gophercloud" + +type PasswordCredentialsV2 struct { + Username string `json:"username" required:"true"` + Password string `json:"password" required:"true"` +} + +type TokenCredentialsV2 struct { + ID string `json:"id,omitempty" required:"true"` +} + +// AuthOptionsV2 wraps a gophercloud AuthOptions in order to adhere to the AuthOptionsBuilder +// interface. +type AuthOptionsV2 struct { + PasswordCredentials *PasswordCredentialsV2 `json:"passwordCredentials,omitempty" xor:"TokenCredentials"` + + // The TenantID and TenantName fields are optional for the Identity V2 API. + // Some providers allow you to specify a TenantName instead of the TenantId. + // Some require both. Your provider's authentication policies will determine + // how these fields influence authentication. + TenantID string `json:"tenantId,omitempty"` + TenantName string `json:"tenantName,omitempty"` + + // TokenCredentials allows users to authenticate (possibly as another user) with an + // authentication token ID. + TokenCredentials *TokenCredentialsV2 `json:"token,omitempty" xor:"PasswordCredentials"` +} + +// AuthOptionsBuilder describes any argument that may be passed to the Create call. +type AuthOptionsBuilder interface { + // ToTokenCreateMap assembles the Create request body, returning an error if parameters are + // missing or inconsistent. + ToTokenV2CreateMap() (map[string]interface{}, error) +} + +// AuthOptions are the valid options for Openstack Identity v2 authentication. +// For field descriptions, see gophercloud.AuthOptions. +type AuthOptions struct { + IdentityEndpoint string `json:"-"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + TenantID string `json:"tenantId,omitempty"` + TenantName string `json:"tenantName,omitempty"` + AllowReauth bool `json:"-"` + TokenID string +} + +// ToTokenV2CreateMap allows AuthOptions to satisfy the AuthOptionsBuilder +// interface in the v2 tokens package +func (opts AuthOptions) ToTokenV2CreateMap() (map[string]interface{}, error) { + v2Opts := AuthOptionsV2{ + TenantID: opts.TenantID, + TenantName: opts.TenantName, + } + + if opts.Password != "" { + v2Opts.PasswordCredentials = &PasswordCredentialsV2{ + Username: opts.Username, + Password: opts.Password, + } + } else { + v2Opts.TokenCredentials = &TokenCredentialsV2{ + ID: opts.TokenID, + } + } + + b, err := gophercloud.BuildRequestBody(v2Opts, "auth") + if err != nil { + return nil, err + } + return b, nil +} + +// Create authenticates to the identity service and attempts to acquire a Token. +// If successful, the CreateResult +// Generally, rather than interact with this call directly, end users should call openstack.AuthenticatedClient(), +// which abstracts all of the gory details about navigating service catalogs and such. +func Create(client *gophercloud.ServiceClient, auth AuthOptionsBuilder) (r CreateResult) { + b, err := auth.ToTokenV2CreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(CreateURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 203}, + MoreHeaders: map[string]string{"X-Auth-Token": ""}, + }) + return +} + +// Get validates and retrieves information for user's token. +func Get(client *gophercloud.ServiceClient, token string) (r GetResult) { + _, r.Err = client.Get(GetURL(client, token), &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 203}, + }) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/results.go new file mode 100644 index 0000000000..6b36493706 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/results.go @@ -0,0 +1,144 @@ +package tokens + +import ( + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/identity/v2/tenants" +) + +// Token provides only the most basic information related to an authentication token. +type Token struct { + // ID provides the primary means of identifying a user to the OpenStack API. + // OpenStack defines this field as an opaque value, so do not depend on its content. + // It is safe, however, to compare for equality. + ID string + + // ExpiresAt provides a timestamp in ISO 8601 format, indicating when the authentication token becomes invalid. + // After this point in time, future API requests made using this authentication token will respond with errors. + // Either the caller will need to reauthenticate manually, or more preferably, the caller should exploit automatic re-authentication. + // See the AuthOptions structure for more details. + ExpiresAt time.Time + + // Tenant provides information about the tenant to which this token grants access. + Tenant tenants.Tenant +} + +// Role is a role for a user. +type Role struct { + Name string `json:"name"` +} + +// User is an OpenStack user. +type User struct { + ID string `json:"id"` + Name string `json:"name"` + UserName string `json:"username"` + Roles []Role `json:"roles"` +} + +// Endpoint represents a single API endpoint offered by a service. +// It provides the public and internal URLs, if supported, along with a region specifier, again if provided. +// The significance of the Region field will depend upon your provider. +// +// In addition, the interface offered by the service will have version information associated with it +// through the VersionId, VersionInfo, and VersionList fields, if provided or supported. +// +// In all cases, fields which aren't supported by the provider and service combined will assume a zero-value (""). +type Endpoint struct { + TenantID string `json:"tenantId"` + PublicURL string `json:"publicURL"` + InternalURL string `json:"internalURL"` + AdminURL string `json:"adminURL"` + Region string `json:"region"` + VersionID string `json:"versionId"` + VersionInfo string `json:"versionInfo"` + VersionList string `json:"versionList"` +} + +// CatalogEntry provides a type-safe interface to an Identity API V2 service catalog listing. +// Each class of service, such as cloud DNS or block storage services, will have a single +// CatalogEntry representing it. +// +// Note: when looking for the desired service, try, whenever possible, to key off the type field. +// Otherwise, you'll tie the representation of the service to a specific provider. +type CatalogEntry struct { + // Name will contain the provider-specified name for the service. + Name string `json:"name"` + + // Type will contain a type string if OpenStack defines a type for the service. + // Otherwise, for provider-specific services, the provider may assign their own type strings. + Type string `json:"type"` + + // Endpoints will let the caller iterate over all the different endpoints that may exist for + // the service. + Endpoints []Endpoint `json:"endpoints"` +} + +// ServiceCatalog provides a view into the service catalog from a previous, successful authentication. +type ServiceCatalog struct { + Entries []CatalogEntry +} + +// CreateResult defers the interpretation of a created token. +// Use ExtractToken() to interpret it as a Token, or ExtractServiceCatalog() to interpret it as a service catalog. +type CreateResult struct { + gophercloud.Result +} + +// GetResult is the deferred response from a Get call, which is the same with a Created token. +// Use ExtractUser() to interpret it as a User. +type GetResult struct { + CreateResult +} + +// ExtractToken returns the just-created Token from a CreateResult. +func (r CreateResult) ExtractToken() (*Token, error) { + var s struct { + Access struct { + Token struct { + Expires string `json:"expires"` + ID string `json:"id"` + Tenant tenants.Tenant `json:"tenant"` + } `json:"token"` + } `json:"access"` + } + + err := r.ExtractInto(&s) + if err != nil { + return nil, err + } + + expiresTs, err := time.Parse(gophercloud.RFC3339Milli, s.Access.Token.Expires) + if err != nil { + return nil, err + } + + return &Token{ + ID: s.Access.Token.ID, + ExpiresAt: expiresTs, + Tenant: s.Access.Token.Tenant, + }, nil +} + +// ExtractServiceCatalog returns the ServiceCatalog that was generated along with the user's Token. +func (r CreateResult) ExtractServiceCatalog() (*ServiceCatalog, error) { + var s struct { + Access struct { + Entries []CatalogEntry `json:"serviceCatalog"` + } `json:"access"` + } + err := r.ExtractInto(&s) + return &ServiceCatalog{Entries: s.Access.Entries}, err +} + +// ExtractUser returns the User from a GetResult. +func (r GetResult) ExtractUser() (*User, error) { + var s struct { + Access struct { + User User `json:"user"` + } `json:"access"` + } + err := r.ExtractInto(&s) + return &s.Access.User, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/urls.go new file mode 100644 index 0000000000..ee0a28f200 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/urls.go @@ -0,0 +1,13 @@ +package tokens + +import "github.com/gophercloud/gophercloud" + +// CreateURL generates the URL used to create new Tokens. +func CreateURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("tokens") +} + +// GetURL generates the URL used to Validate Tokens. +func GetURL(client *gophercloud.ServiceClient, token string) string { + return client.ServiceURL("tokens", token) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/doc.go new file mode 100644 index 0000000000..76ff5f4738 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/doc.go @@ -0,0 +1,6 @@ +// Package tokens provides information and interaction with the token API +// resource for the OpenStack Identity service. +// +// For more information, see: +// http://developer.openstack.org/api-ref-identity-v3.html#tokens-v3 +package tokens diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/requests.go new file mode 100644 index 0000000000..ba4363b2b9 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/requests.go @@ -0,0 +1,200 @@ +package tokens + +import "github.com/gophercloud/gophercloud" + +// Scope allows a created token to be limited to a specific domain or project. +type Scope struct { + ProjectID string `json:"scope.project.id,omitempty" not:"ProjectName,DomainID,DomainName"` + ProjectName string `json:"scope.project.name,omitempty"` + DomainID string `json:"scope.project.id,omitempty" not:"ProjectName,ProjectID,DomainName"` + DomainName string `json:"scope.project.id,omitempty"` +} + +// AuthOptionsBuilder describes any argument that may be passed to the Create call. +type AuthOptionsBuilder interface { + // ToTokenV3CreateMap assembles the Create request body, returning an error if parameters are + // missing or inconsistent. + ToTokenV3CreateMap(map[string]interface{}) (map[string]interface{}, error) + ToTokenV3ScopeMap() (map[string]interface{}, error) + CanReauth() bool +} + +type AuthOptions struct { + // IdentityEndpoint specifies the HTTP endpoint that is required to work with + // the Identity API of the appropriate version. While it's ultimately needed by + // all of the identity services, it will often be populated by a provider-level + // function. + IdentityEndpoint string `json:"-"` + + // Username is required if using Identity V2 API. Consult with your provider's + // control panel to discover your account's username. In Identity V3, either + // UserID or a combination of Username and DomainID or DomainName are needed. + Username string `json:"username,omitempty"` + UserID string `json:"id,omitempty"` + + Password string `json:"password,omitempty"` + + // At most one of DomainID and DomainName must be provided if using Username + // with Identity V3. Otherwise, either are optional. + DomainID string `json:"id,omitempty"` + DomainName string `json:"name,omitempty"` + + // AllowReauth should be set to true if you grant permission for Gophercloud to + // cache your credentials in memory, and to allow Gophercloud to attempt to + // re-authenticate automatically if/when your token expires. If you set it to + // false, it will not cache these settings, but re-authentication will not be + // possible. This setting defaults to false. + AllowReauth bool `json:"-"` + + // TokenID allows users to authenticate (possibly as another user) with an + // authentication token ID. + TokenID string `json:"-"` + + Scope Scope `json:"-"` +} + +func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[string]interface{}, error) { + gophercloudAuthOpts := gophercloud.AuthOptions{ + Username: opts.Username, + UserID: opts.UserID, + Password: opts.Password, + DomainID: opts.DomainID, + DomainName: opts.DomainName, + AllowReauth: opts.AllowReauth, + TokenID: opts.TokenID, + } + + return gophercloudAuthOpts.ToTokenV3CreateMap(scope) +} + +func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) { + if opts.Scope.ProjectName != "" { + // ProjectName provided: either DomainID or DomainName must also be supplied. + // ProjectID may not be supplied. + if opts.Scope.DomainID == "" && opts.Scope.DomainName == "" { + return nil, gophercloud.ErrScopeDomainIDOrDomainName{} + } + if opts.Scope.ProjectID != "" { + return nil, gophercloud.ErrScopeProjectIDOrProjectName{} + } + + if opts.Scope.DomainID != "" { + // ProjectName + DomainID + return map[string]interface{}{ + "project": map[string]interface{}{ + "name": &opts.Scope.ProjectName, + "domain": map[string]interface{}{"id": &opts.Scope.DomainID}, + }, + }, nil + } + + if opts.Scope.DomainName != "" { + // ProjectName + DomainName + return map[string]interface{}{ + "project": map[string]interface{}{ + "name": &opts.Scope.ProjectName, + "domain": map[string]interface{}{"name": &opts.Scope.DomainName}, + }, + }, nil + } + } else if opts.Scope.ProjectID != "" { + // ProjectID provided. ProjectName, DomainID, and DomainName may not be provided. + if opts.Scope.DomainID != "" { + return nil, gophercloud.ErrScopeProjectIDAlone{} + } + if opts.Scope.DomainName != "" { + return nil, gophercloud.ErrScopeProjectIDAlone{} + } + + // ProjectID + return map[string]interface{}{ + "project": map[string]interface{}{ + "id": &opts.Scope.ProjectID, + }, + }, nil + } else if opts.Scope.DomainID != "" { + // DomainID provided. ProjectID, ProjectName, and DomainName may not be provided. + if opts.Scope.DomainName != "" { + return nil, gophercloud.ErrScopeDomainIDOrDomainName{} + } + + // DomainID + return map[string]interface{}{ + "domain": map[string]interface{}{ + "id": &opts.Scope.DomainID, + }, + }, nil + } else if opts.Scope.DomainName != "" { + return nil, gophercloud.ErrScopeDomainName{} + } + + return nil, nil +} + +func (opts *AuthOptions) CanReauth() bool { + return opts.AllowReauth +} + +func subjectTokenHeaders(c *gophercloud.ServiceClient, subjectToken string) map[string]string { + return map[string]string{ + "X-Subject-Token": subjectToken, + } +} + +// Create authenticates and either generates a new token, or changes the Scope of an existing token. +func Create(c *gophercloud.ServiceClient, opts AuthOptionsBuilder) (r CreateResult) { + scope, err := opts.ToTokenV3ScopeMap() + if err != nil { + r.Err = err + return + } + + b, err := opts.ToTokenV3CreateMap(scope) + if err != nil { + r.Err = err + return + } + + resp, err := c.Post(tokenURL(c), b, &r.Body, &gophercloud.RequestOpts{ + MoreHeaders: map[string]string{"X-Auth-Token": ""}, + }) + r.Err = err + if resp != nil { + r.Header = resp.Header + } + return +} + +// Get validates and retrieves information about another token. +func Get(c *gophercloud.ServiceClient, token string) (r GetResult) { + resp, err := c.Get(tokenURL(c), &r.Body, &gophercloud.RequestOpts{ + MoreHeaders: subjectTokenHeaders(c, token), + OkCodes: []int{200, 203}, + }) + if resp != nil { + r.Err = err + r.Header = resp.Header + } + return +} + +// Validate determines if a specified token is valid or not. +func Validate(c *gophercloud.ServiceClient, token string) (bool, error) { + resp, err := c.Request("HEAD", tokenURL(c), &gophercloud.RequestOpts{ + MoreHeaders: subjectTokenHeaders(c, token), + OkCodes: []int{204, 404}, + }) + if err != nil { + return false, err + } + + return resp.StatusCode == 204, nil +} + +// Revoke immediately makes specified token invalid. +func Revoke(c *gophercloud.ServiceClient, token string) (r RevokeResult) { + _, r.Err = c.Delete(tokenURL(c), &gophercloud.RequestOpts{ + MoreHeaders: subjectTokenHeaders(c, token), + }) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/results.go new file mode 100644 index 0000000000..0f1e8c2ba7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/results.go @@ -0,0 +1,103 @@ +package tokens + +import ( + "time" + + "github.com/gophercloud/gophercloud" +) + +// Endpoint represents a single API endpoint offered by a service. +// It matches either a public, internal or admin URL. +// If supported, it contains a region specifier, again if provided. +// The significance of the Region field will depend upon your provider. +type Endpoint struct { + ID string `json:"id"` + Region string `json:"region"` + Interface string `json:"interface"` + URL string `json:"url"` +} + +// CatalogEntry provides a type-safe interface to an Identity API V3 service catalog listing. +// Each class of service, such as cloud DNS or block storage services, could have multiple +// CatalogEntry representing it (one by interface type, e.g public, admin or internal). +// +// Note: when looking for the desired service, try, whenever possible, to key off the type field. +// Otherwise, you'll tie the representation of the service to a specific provider. +type CatalogEntry struct { + // Service ID + ID string `json:"id"` + // Name will contain the provider-specified name for the service. + Name string `json:"name"` + // Type will contain a type string if OpenStack defines a type for the service. + // Otherwise, for provider-specific services, the provider may assign their own type strings. + Type string `json:"type"` + // Endpoints will let the caller iterate over all the different endpoints that may exist for + // the service. + Endpoints []Endpoint `json:"endpoints"` +} + +// ServiceCatalog provides a view into the service catalog from a previous, successful authentication. +type ServiceCatalog struct { + Entries []CatalogEntry `json:"catalog"` +} + +// commonResult is the deferred result of a Create or a Get call. +type commonResult struct { + gophercloud.Result +} + +// Extract is a shortcut for ExtractToken. +// This function is deprecated and still present for backward compatibility. +func (r commonResult) Extract() (*Token, error) { + return r.ExtractToken() +} + +// ExtractToken interprets a commonResult as a Token. +func (r commonResult) ExtractToken() (*Token, error) { + var s Token + err := r.ExtractInto(&s) + if err != nil { + return nil, err + } + + // Parse the token itself from the stored headers. + s.ID = r.Header.Get("X-Subject-Token") + + return &s, err +} + +// ExtractServiceCatalog returns the ServiceCatalog that was generated along with the user's Token. +func (r CreateResult) ExtractServiceCatalog() (*ServiceCatalog, error) { + var s ServiceCatalog + err := r.ExtractInto(&s) + return &s, err +} + +// CreateResult defers the interpretation of a created token. +// Use ExtractToken() to interpret it as a Token, or ExtractServiceCatalog() to interpret it as a service catalog. +type CreateResult struct { + commonResult +} + +// GetResult is the deferred response from a Get call. +type GetResult struct { + commonResult +} + +// RevokeResult is the deferred response from a Revoke call. +type RevokeResult struct { + commonResult +} + +// Token is a string that grants a user access to a controlled set of services in an OpenStack provider. +// Each Token is valid for a set length of time. +type Token struct { + // ID is the issued token. + ID string `json:"id"` + // ExpiresAt is the timestamp at which this token will no longer be accepted. + ExpiresAt time.Time `json:"expires_at"` +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.ExtractIntoStructPtr(v, "token") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/urls.go new file mode 100644 index 0000000000..2f864a31c8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/urls.go @@ -0,0 +1,7 @@ +package tokens + +import "github.com/gophercloud/gophercloud" + +func tokenURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("auth", "tokens") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/utils/choose_version.go b/vendor/github.com/gophercloud/gophercloud/openstack/utils/choose_version.go new file mode 100644 index 0000000000..c605d08444 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/utils/choose_version.go @@ -0,0 +1,114 @@ +package utils + +import ( + "fmt" + "strings" + + "github.com/gophercloud/gophercloud" +) + +// Version is a supported API version, corresponding to a vN package within the appropriate service. +type Version struct { + ID string + Suffix string + Priority int +} + +var goodStatus = map[string]bool{ + "current": true, + "supported": true, + "stable": true, +} + +// ChooseVersion queries the base endpoint of an API to choose the most recent non-experimental alternative from a service's +// published versions. +// It returns the highest-Priority Version among the alternatives that are provided, as well as its corresponding endpoint. +func ChooseVersion(client *gophercloud.ProviderClient, recognized []*Version) (*Version, string, error) { + type linkResp struct { + Href string `json:"href"` + Rel string `json:"rel"` + } + + type valueResp struct { + ID string `json:"id"` + Status string `json:"status"` + Links []linkResp `json:"links"` + } + + type versionsResp struct { + Values []valueResp `json:"values"` + } + + type response struct { + Versions versionsResp `json:"versions"` + } + + normalize := func(endpoint string) string { + if !strings.HasSuffix(endpoint, "/") { + return endpoint + "/" + } + return endpoint + } + identityEndpoint := normalize(client.IdentityEndpoint) + + // If a full endpoint is specified, check version suffixes for a match first. + for _, v := range recognized { + if strings.HasSuffix(identityEndpoint, v.Suffix) { + return v, identityEndpoint, nil + } + } + + var resp response + _, err := client.Request("GET", client.IdentityBase, &gophercloud.RequestOpts{ + JSONResponse: &resp, + OkCodes: []int{200, 300}, + }) + + if err != nil { + return nil, "", err + } + + byID := make(map[string]*Version) + for _, version := range recognized { + byID[version.ID] = version + } + + var highest *Version + var endpoint string + + for _, value := range resp.Versions.Values { + href := "" + for _, link := range value.Links { + if link.Rel == "self" { + href = normalize(link.Href) + } + } + + if matching, ok := byID[value.ID]; ok { + // Prefer a version that exactly matches the provided endpoint. + if href == identityEndpoint { + if href == "" { + return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", value.ID, client.IdentityBase) + } + return matching, href, nil + } + + // Otherwise, find the highest-priority version with a whitelisted status. + if goodStatus[strings.ToLower(value.Status)] { + if highest == nil || matching.Priority > highest.Priority { + highest = matching + endpoint = href + } + } + } + } + + if highest == nil { + return nil, "", fmt.Errorf("No supported version available from endpoint %s", client.IdentityBase) + } + if endpoint == "" { + return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", highest.ID, client.IdentityBase) + } + + return highest, endpoint, nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/pagination/http.go b/vendor/github.com/gophercloud/gophercloud/pagination/http.go new file mode 100644 index 0000000000..cb4b4ae6b1 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/pagination/http.go @@ -0,0 +1,60 @@ +package pagination + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "github.com/gophercloud/gophercloud" +) + +// PageResult stores the HTTP response that returned the current page of results. +type PageResult struct { + gophercloud.Result + url.URL +} + +// PageResultFrom parses an HTTP response as JSON and returns a PageResult containing the +// results, interpreting it as JSON if the content type indicates. +func PageResultFrom(resp *http.Response) (PageResult, error) { + var parsedBody interface{} + + defer resp.Body.Close() + rawBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return PageResult{}, err + } + + if strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") { + err = json.Unmarshal(rawBody, &parsedBody) + if err != nil { + return PageResult{}, err + } + } else { + parsedBody = rawBody + } + + return PageResultFromParsed(resp, parsedBody), err +} + +// PageResultFromParsed constructs a PageResult from an HTTP response that has already had its +// body parsed as JSON (and closed). +func PageResultFromParsed(resp *http.Response, body interface{}) PageResult { + return PageResult{ + Result: gophercloud.Result{ + Body: body, + Header: resp.Header, + }, + URL: *resp.Request.URL, + } +} + +// Request performs an HTTP request and extracts the http.Response from the result. +func Request(client *gophercloud.ServiceClient, headers map[string]string, url string) (*http.Response, error) { + return client.Get(url, nil, &gophercloud.RequestOpts{ + MoreHeaders: headers, + OkCodes: []int{200, 204}, + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/pagination/linked.go b/vendor/github.com/gophercloud/gophercloud/pagination/linked.go new file mode 100644 index 0000000000..3656fb7f8f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/pagination/linked.go @@ -0,0 +1,92 @@ +package pagination + +import ( + "fmt" + "reflect" + + "github.com/gophercloud/gophercloud" +) + +// LinkedPageBase may be embedded to implement a page that provides navigational "Next" and "Previous" links within its result. +type LinkedPageBase struct { + PageResult + + // LinkPath lists the keys that should be traversed within a response to arrive at the "next" pointer. + // If any link along the path is missing, an empty URL will be returned. + // If any link results in an unexpected value type, an error will be returned. + // When left as "nil", []string{"links", "next"} will be used as a default. + LinkPath []string +} + +// NextPageURL extracts the pagination structure from a JSON response and returns the "next" link, if one is present. +// It assumes that the links are available in a "links" element of the top-level response object. +// If this is not the case, override NextPageURL on your result type. +func (current LinkedPageBase) NextPageURL() (string, error) { + var path []string + var key string + + if current.LinkPath == nil { + path = []string{"links", "next"} + } else { + path = current.LinkPath + } + + submap, ok := current.Body.(map[string]interface{}) + if !ok { + err := gophercloud.ErrUnexpectedType{} + err.Expected = "map[string]interface{}" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body)) + return "", err + } + + for { + key, path = path[0], path[1:len(path)] + + value, ok := submap[key] + if !ok { + return "", nil + } + + if len(path) > 0 { + submap, ok = value.(map[string]interface{}) + if !ok { + err := gophercloud.ErrUnexpectedType{} + err.Expected = "map[string]interface{}" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(value)) + return "", err + } + } else { + if value == nil { + // Actual null element. + return "", nil + } + + url, ok := value.(string) + if !ok { + err := gophercloud.ErrUnexpectedType{} + err.Expected = "string" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(value)) + return "", err + } + + return url, nil + } + } +} + +// IsEmpty satisifies the IsEmpty method of the Page interface +func (current LinkedPageBase) IsEmpty() (bool, error) { + if b, ok := current.Body.([]interface{}); ok { + return len(b) == 0, nil + } + err := gophercloud.ErrUnexpectedType{} + err.Expected = "[]interface{}" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body)) + return true, err +} + +// GetBody returns the linked page's body. This method is needed to satisfy the +// Page interface. +func (current LinkedPageBase) GetBody() interface{} { + return current.Body +} diff --git a/vendor/github.com/gophercloud/gophercloud/pagination/marker.go b/vendor/github.com/gophercloud/gophercloud/pagination/marker.go new file mode 100644 index 0000000000..52e53bae85 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/pagination/marker.go @@ -0,0 +1,58 @@ +package pagination + +import ( + "fmt" + "reflect" + + "github.com/gophercloud/gophercloud" +) + +// MarkerPage is a stricter Page interface that describes additional functionality required for use with NewMarkerPager. +// For convenience, embed the MarkedPageBase struct. +type MarkerPage interface { + Page + + // LastMarker returns the last "marker" value on this page. + LastMarker() (string, error) +} + +// MarkerPageBase is a page in a collection that's paginated by "limit" and "marker" query parameters. +type MarkerPageBase struct { + PageResult + + // Owner is a reference to the embedding struct. + Owner MarkerPage +} + +// NextPageURL generates the URL for the page of results after this one. +func (current MarkerPageBase) NextPageURL() (string, error) { + currentURL := current.URL + + mark, err := current.Owner.LastMarker() + if err != nil { + return "", err + } + + q := currentURL.Query() + q.Set("marker", mark) + currentURL.RawQuery = q.Encode() + + return currentURL.String(), nil +} + +// IsEmpty satisifies the IsEmpty method of the Page interface +func (current MarkerPageBase) IsEmpty() (bool, error) { + if b, ok := current.Body.([]interface{}); ok { + return len(b) == 0, nil + } + err := gophercloud.ErrUnexpectedType{} + err.Expected = "[]interface{}" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body)) + return true, err +} + +// GetBody returns the linked page's body. This method is needed to satisfy the +// Page interface. +func (current MarkerPageBase) GetBody() interface{} { + return current.Body +} diff --git a/vendor/github.com/gophercloud/gophercloud/pagination/pager.go b/vendor/github.com/gophercloud/gophercloud/pagination/pager.go new file mode 100644 index 0000000000..6f1609ef2e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/pagination/pager.go @@ -0,0 +1,238 @@ +package pagination + +import ( + "errors" + "fmt" + "net/http" + "reflect" + "strings" + + "github.com/gophercloud/gophercloud" +) + +var ( + // ErrPageNotAvailable is returned from a Pager when a next or previous page is requested, but does not exist. + ErrPageNotAvailable = errors.New("The requested page does not exist.") +) + +// Page must be satisfied by the result type of any resource collection. +// It allows clients to interact with the resource uniformly, regardless of whether or not or how it's paginated. +// Generally, rather than implementing this interface directly, implementors should embed one of the concrete PageBase structs, +// instead. +// Depending on the pagination strategy of a particular resource, there may be an additional subinterface that the result type +// will need to implement. +type Page interface { + + // NextPageURL generates the URL for the page of data that follows this collection. + // Return "" if no such page exists. + NextPageURL() (string, error) + + // IsEmpty returns true if this Page has no items in it. + IsEmpty() (bool, error) + + // GetBody returns the Page Body. This is used in the `AllPages` method. + GetBody() interface{} +} + +// Pager knows how to advance through a specific resource collection, one page at a time. +type Pager struct { + client *gophercloud.ServiceClient + + initialURL string + + createPage func(r PageResult) Page + + Err error + + // Headers supplies additional HTTP headers to populate on each paged request. + Headers map[string]string +} + +// NewPager constructs a manually-configured pager. +// Supply the URL for the first page, a function that requests a specific page given a URL, and a function that counts a page. +func NewPager(client *gophercloud.ServiceClient, initialURL string, createPage func(r PageResult) Page) Pager { + return Pager{ + client: client, + initialURL: initialURL, + createPage: createPage, + } +} + +// WithPageCreator returns a new Pager that substitutes a different page creation function. This is +// useful for overriding List functions in delegation. +func (p Pager) WithPageCreator(createPage func(r PageResult) Page) Pager { + return Pager{ + client: p.client, + initialURL: p.initialURL, + createPage: createPage, + } +} + +func (p Pager) fetchNextPage(url string) (Page, error) { + resp, err := Request(p.client, p.Headers, url) + if err != nil { + return nil, err + } + + remembered, err := PageResultFrom(resp) + if err != nil { + return nil, err + } + + return p.createPage(remembered), nil +} + +// EachPage iterates over each page returned by a Pager, yielding one at a time to a handler function. +// Return "false" from the handler to prematurely stop iterating. +func (p Pager) EachPage(handler func(Page) (bool, error)) error { + if p.Err != nil { + return p.Err + } + currentURL := p.initialURL + for { + currentPage, err := p.fetchNextPage(currentURL) + if err != nil { + return err + } + + empty, err := currentPage.IsEmpty() + if err != nil { + return err + } + if empty { + return nil + } + + ok, err := handler(currentPage) + if err != nil { + return err + } + if !ok { + return nil + } + + currentURL, err = currentPage.NextPageURL() + if err != nil { + return err + } + if currentURL == "" { + return nil + } + } +} + +// AllPages returns all the pages from a `List` operation in a single page, +// allowing the user to retrieve all the pages at once. +func (p Pager) AllPages() (Page, error) { + // pagesSlice holds all the pages until they get converted into as Page Body. + var pagesSlice []interface{} + // body will contain the final concatenated Page body. + var body reflect.Value + + // Grab a test page to ascertain the page body type. + testPage, err := p.fetchNextPage(p.initialURL) + if err != nil { + return nil, err + } + // Store the page type so we can use reflection to create a new mega-page of + // that type. + pageType := reflect.TypeOf(testPage) + + // if it's a single page, just return the testPage (first page) + if _, found := pageType.FieldByName("SinglePageBase"); found { + return testPage, nil + } + + // Switch on the page body type. Recognized types are `map[string]interface{}`, + // `[]byte`, and `[]interface{}`. + switch pb := testPage.GetBody().(type) { + case map[string]interface{}: + // key is the map key for the page body if the body type is `map[string]interface{}`. + var key string + // Iterate over the pages to concatenate the bodies. + err = p.EachPage(func(page Page) (bool, error) { + b := page.GetBody().(map[string]interface{}) + for k, v := range b { + // If it's a linked page, we don't want the `links`, we want the other one. + if !strings.HasSuffix(k, "links") { + // check the field's type. we only want []interface{} (which is really []map[string]interface{}) + switch vt := v.(type) { + case []interface{}: + key = k + pagesSlice = append(pagesSlice, vt...) + } + } + } + return true, nil + }) + if err != nil { + return nil, err + } + // Set body to value of type `map[string]interface{}` + body = reflect.MakeMap(reflect.MapOf(reflect.TypeOf(key), reflect.TypeOf(pagesSlice))) + body.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(pagesSlice)) + case []byte: + // Iterate over the pages to concatenate the bodies. + err = p.EachPage(func(page Page) (bool, error) { + b := page.GetBody().([]byte) + pagesSlice = append(pagesSlice, b) + // seperate pages with a comma + pagesSlice = append(pagesSlice, []byte{10}) + return true, nil + }) + if err != nil { + return nil, err + } + if len(pagesSlice) > 0 { + // Remove the trailing comma. + pagesSlice = pagesSlice[:len(pagesSlice)-1] + } + var b []byte + // Combine the slice of slices in to a single slice. + for _, slice := range pagesSlice { + b = append(b, slice.([]byte)...) + } + // Set body to value of type `bytes`. + body = reflect.New(reflect.TypeOf(b)).Elem() + body.SetBytes(b) + case []interface{}: + // Iterate over the pages to concatenate the bodies. + err = p.EachPage(func(page Page) (bool, error) { + b := page.GetBody().([]interface{}) + pagesSlice = append(pagesSlice, b...) + return true, nil + }) + if err != nil { + return nil, err + } + // Set body to value of type `[]interface{}` + body = reflect.MakeSlice(reflect.TypeOf(pagesSlice), len(pagesSlice), len(pagesSlice)) + for i, s := range pagesSlice { + body.Index(i).Set(reflect.ValueOf(s)) + } + default: + err := gophercloud.ErrUnexpectedType{} + err.Expected = "map[string]interface{}/[]byte/[]interface{}" + err.Actual = fmt.Sprintf("%T", pb) + return nil, err + } + + // Each `Extract*` function is expecting a specific type of page coming back, + // otherwise the type assertion in those functions will fail. pageType is needed + // to create a type in this method that has the same type that the `Extract*` + // function is expecting and set the Body of that object to the concatenated + // pages. + page := reflect.New(pageType) + // Set the page body to be the concatenated pages. + page.Elem().FieldByName("Body").Set(body) + // Set any additional headers that were pass along. The `objectstorage` pacakge, + // for example, passes a Content-Type header. + h := make(http.Header) + for k, v := range p.Headers { + h.Add(k, v) + } + page.Elem().FieldByName("Header").Set(reflect.ValueOf(h)) + // Type assert the page to a Page interface so that the type assertion in the + // `Extract*` methods will work. + return page.Elem().Interface().(Page), err +} diff --git a/vendor/github.com/gophercloud/gophercloud/pagination/pkg.go b/vendor/github.com/gophercloud/gophercloud/pagination/pkg.go new file mode 100644 index 0000000000..912daea364 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/pagination/pkg.go @@ -0,0 +1,4 @@ +/* +Package pagination contains utilities and convenience structs that implement common pagination idioms within OpenStack APIs. +*/ +package pagination diff --git a/vendor/github.com/gophercloud/gophercloud/pagination/single.go b/vendor/github.com/gophercloud/gophercloud/pagination/single.go new file mode 100644 index 0000000000..4251d6491e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/pagination/single.go @@ -0,0 +1,33 @@ +package pagination + +import ( + "fmt" + "reflect" + + "github.com/gophercloud/gophercloud" +) + +// SinglePageBase may be embedded in a Page that contains all of the results from an operation at once. +type SinglePageBase PageResult + +// NextPageURL always returns "" to indicate that there are no more pages to return. +func (current SinglePageBase) NextPageURL() (string, error) { + return "", nil +} + +// IsEmpty satisifies the IsEmpty method of the Page interface +func (current SinglePageBase) IsEmpty() (bool, error) { + if b, ok := current.Body.([]interface{}); ok { + return len(b) == 0, nil + } + err := gophercloud.ErrUnexpectedType{} + err.Expected = "[]interface{}" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body)) + return true, err +} + +// GetBody returns the single page's body. This method is needed to satisfy the +// Page interface. +func (current SinglePageBase) GetBody() interface{} { + return current.Body +} diff --git a/vendor/github.com/gophercloud/gophercloud/params.go b/vendor/github.com/gophercloud/gophercloud/params.go new file mode 100644 index 0000000000..e484fe1c1e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/params.go @@ -0,0 +1,445 @@ +package gophercloud + +import ( + "encoding/json" + "fmt" + "net/url" + "reflect" + "strconv" + "strings" + "time" +) + +// BuildRequestBody builds a map[string]interface from the given `struct`. If +// parent is not the empty string, the final map[string]interface returned will +// encapsulate the built one +// +func BuildRequestBody(opts interface{}, parent string) (map[string]interface{}, error) { + optsValue := reflect.ValueOf(opts) + if optsValue.Kind() == reflect.Ptr { + optsValue = optsValue.Elem() + } + + optsType := reflect.TypeOf(opts) + if optsType.Kind() == reflect.Ptr { + optsType = optsType.Elem() + } + + optsMap := make(map[string]interface{}) + if optsValue.Kind() == reflect.Struct { + //fmt.Printf("optsValue.Kind() is a reflect.Struct: %+v\n", optsValue.Kind()) + for i := 0; i < optsValue.NumField(); i++ { + v := optsValue.Field(i) + f := optsType.Field(i) + + if f.Name != strings.Title(f.Name) { + //fmt.Printf("Skipping field: %s...\n", f.Name) + continue + } + + //fmt.Printf("Starting on field: %s...\n", f.Name) + + zero := isZero(v) + //fmt.Printf("v is zero?: %v\n", zero) + + // if the field has a required tag that's set to "true" + if requiredTag := f.Tag.Get("required"); requiredTag == "true" { + //fmt.Printf("Checking required field [%s]:\n\tv: %+v\n\tisZero:%v\n", f.Name, v.Interface(), zero) + // if the field's value is zero, return a missing-argument error + if zero { + // if the field has a 'required' tag, it can't have a zero-value + err := ErrMissingInput{} + err.Argument = f.Name + return nil, err + } + } + + if xorTag := f.Tag.Get("xor"); xorTag != "" { + //fmt.Printf("Checking `xor` tag for field [%s] with value %+v:\n\txorTag: %s\n", f.Name, v, xorTag) + xorField := optsValue.FieldByName(xorTag) + var xorFieldIsZero bool + if reflect.ValueOf(xorField.Interface()) == reflect.Zero(xorField.Type()) { + xorFieldIsZero = true + } else { + if xorField.Kind() == reflect.Ptr { + xorField = xorField.Elem() + } + xorFieldIsZero = isZero(xorField) + } + if !(zero != xorFieldIsZero) { + err := ErrMissingInput{} + err.Argument = fmt.Sprintf("%s/%s", f.Name, xorTag) + err.Info = fmt.Sprintf("Exactly one of %s and %s must be provided", f.Name, xorTag) + return nil, err + } + } + + if orTag := f.Tag.Get("or"); orTag != "" { + //fmt.Printf("Checking `or` tag for field with:\n\tname: %+v\n\torTag:%s\n", f.Name, orTag) + //fmt.Printf("field is zero?: %v\n", zero) + if zero { + orField := optsValue.FieldByName(orTag) + var orFieldIsZero bool + if reflect.ValueOf(orField.Interface()) == reflect.Zero(orField.Type()) { + orFieldIsZero = true + } else { + if orField.Kind() == reflect.Ptr { + orField = orField.Elem() + } + orFieldIsZero = isZero(orField) + } + if orFieldIsZero { + err := ErrMissingInput{} + err.Argument = fmt.Sprintf("%s/%s", f.Name, orTag) + err.Info = fmt.Sprintf("At least one of %s and %s must be provided", f.Name, orTag) + return nil, err + } + } + } + + if v.Kind() == reflect.Struct || (v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct) { + if zero { + //fmt.Printf("value before change: %+v\n", optsValue.Field(i)) + if jsonTag := f.Tag.Get("json"); jsonTag != "" { + jsonTagPieces := strings.Split(jsonTag, ",") + if len(jsonTagPieces) > 1 && jsonTagPieces[1] == "omitempty" { + if v.CanSet() { + if !v.IsNil() { + if v.Kind() == reflect.Ptr { + v.Set(reflect.Zero(v.Type())) + } + } + //fmt.Printf("value after change: %+v\n", optsValue.Field(i)) + } + } + } + continue + } + + //fmt.Printf("Calling BuildRequestBody with:\n\tv: %+v\n\tf.Name:%s\n", v.Interface(), f.Name) + _, err := BuildRequestBody(v.Interface(), f.Name) + if err != nil { + return nil, err + } + } + } + + //fmt.Printf("opts: %+v \n", opts) + + b, err := json.Marshal(opts) + if err != nil { + return nil, err + } + + //fmt.Printf("string(b): %s\n", string(b)) + + err = json.Unmarshal(b, &optsMap) + if err != nil { + return nil, err + } + + //fmt.Printf("optsMap: %+v\n", optsMap) + + if parent != "" { + optsMap = map[string]interface{}{parent: optsMap} + } + //fmt.Printf("optsMap after parent added: %+v\n", optsMap) + return optsMap, nil + } + // Return an error if the underlying type of 'opts' isn't a struct. + return nil, fmt.Errorf("Options type is not a struct.") +} + +// EnabledState is a convenience type, mostly used in Create and Update +// operations. Because the zero value of a bool is FALSE, we need to use a +// pointer instead to indicate zero-ness. +type EnabledState *bool + +// Convenience vars for EnabledState values. +var ( + iTrue = true + iFalse = false + + Enabled EnabledState = &iTrue + Disabled EnabledState = &iFalse +) + +// IPVersion is a type for the possible IP address versions. Valid instances +// are IPv4 and IPv6 +type IPVersion int + +const ( + // IPv4 is used for IP version 4 addresses + IPv4 IPVersion = 4 + // IPv6 is used for IP version 6 addresses + IPv6 IPVersion = 6 +) + +// IntToPointer is a function for converting integers into integer pointers. +// This is useful when passing in options to operations. +func IntToPointer(i int) *int { + return &i +} + +/* +MaybeString is an internal function to be used by request methods in individual +resource packages. + +It takes a string that might be a zero value and returns either a pointer to its +address or nil. This is useful for allowing users to conveniently omit values +from an options struct by leaving them zeroed, but still pass nil to the JSON +serializer so they'll be omitted from the request body. +*/ +func MaybeString(original string) *string { + if original != "" { + return &original + } + return nil +} + +/* +MaybeInt is an internal function to be used by request methods in individual +resource packages. + +Like MaybeString, it accepts an int that may or may not be a zero value, and +returns either a pointer to its address or nil. It's intended to hint that the +JSON serializer should omit its field. +*/ +func MaybeInt(original int) *int { + if original != 0 { + return &original + } + return nil +} + +/* +func isUnderlyingStructZero(v reflect.Value) bool { + switch v.Kind() { + case reflect.Ptr: + return isUnderlyingStructZero(v.Elem()) + default: + return isZero(v) + } +} +*/ + +var t time.Time + +func isZero(v reflect.Value) bool { + //fmt.Printf("\n\nchecking isZero for value: %+v\n", v) + switch v.Kind() { + case reflect.Ptr: + if v.IsNil() { + return true + } + return false + case reflect.Func, reflect.Map, reflect.Slice: + return v.IsNil() + case reflect.Array: + z := true + for i := 0; i < v.Len(); i++ { + z = z && isZero(v.Index(i)) + } + return z + case reflect.Struct: + if v.Type() == reflect.TypeOf(t) { + if v.Interface().(time.Time).IsZero() { + return true + } + return false + } + z := true + for i := 0; i < v.NumField(); i++ { + z = z && isZero(v.Field(i)) + } + return z + } + // Compare other types directly: + z := reflect.Zero(v.Type()) + //fmt.Printf("zero type for value: %+v\n\n\n", z) + return v.Interface() == z.Interface() +} + +/* +BuildQueryString is an internal function to be used by request methods in +individual resource packages. + +It accepts a tagged structure and expands it into a URL struct. Field names are +converted into query parameters based on a "q" tag. For example: + + type struct Something { + Bar string `q:"x_bar"` + Baz int `q:"lorem_ipsum"` + } + + instance := Something{ + Bar: "AAA", + Baz: "BBB", + } + +will be converted into "?x_bar=AAA&lorem_ipsum=BBB". + +The struct's fields may be strings, integers, or boolean values. Fields left at +their type's zero value will be omitted from the query. +*/ +func BuildQueryString(opts interface{}) (*url.URL, error) { + optsValue := reflect.ValueOf(opts) + if optsValue.Kind() == reflect.Ptr { + optsValue = optsValue.Elem() + } + + optsType := reflect.TypeOf(opts) + if optsType.Kind() == reflect.Ptr { + optsType = optsType.Elem() + } + + params := url.Values{} + + if optsValue.Kind() == reflect.Struct { + for i := 0; i < optsValue.NumField(); i++ { + v := optsValue.Field(i) + f := optsType.Field(i) + qTag := f.Tag.Get("q") + + // if the field has a 'q' tag, it goes in the query string + if qTag != "" { + tags := strings.Split(qTag, ",") + + // if the field is set, add it to the slice of query pieces + if !isZero(v) { + loop: + switch v.Kind() { + case reflect.Ptr: + v = v.Elem() + goto loop + case reflect.String: + params.Add(tags[0], v.String()) + case reflect.Int: + params.Add(tags[0], strconv.FormatInt(v.Int(), 10)) + case reflect.Bool: + params.Add(tags[0], strconv.FormatBool(v.Bool())) + case reflect.Slice: + switch v.Type().Elem() { + case reflect.TypeOf(0): + for i := 0; i < v.Len(); i++ { + params.Add(tags[0], strconv.FormatInt(v.Index(i).Int(), 10)) + } + default: + for i := 0; i < v.Len(); i++ { + params.Add(tags[0], v.Index(i).String()) + } + } + } + } else { + // Otherwise, the field is not set. + if len(tags) == 2 && tags[1] == "required" { + // And the field is required. Return an error. + return nil, fmt.Errorf("Required query parameter [%s] not set.", f.Name) + } + } + } + } + + return &url.URL{RawQuery: params.Encode()}, nil + } + // Return an error if the underlying type of 'opts' isn't a struct. + return nil, fmt.Errorf("Options type is not a struct.") +} + +/* +BuildHeaders is an internal function to be used by request methods in +individual resource packages. + +It accepts an arbitrary tagged structure and produces a string map that's +suitable for use as the HTTP headers of an outgoing request. Field names are +mapped to header names based in "h" tags. + + type struct Something { + Bar string `h:"x_bar"` + Baz int `h:"lorem_ipsum"` + } + + instance := Something{ + Bar: "AAA", + Baz: "BBB", + } + +will be converted into: + + map[string]string{ + "x_bar": "AAA", + "lorem_ipsum": "BBB", + } + +Untagged fields and fields left at their zero values are skipped. Integers, +booleans and string values are supported. +*/ +func BuildHeaders(opts interface{}) (map[string]string, error) { + optsValue := reflect.ValueOf(opts) + if optsValue.Kind() == reflect.Ptr { + optsValue = optsValue.Elem() + } + + optsType := reflect.TypeOf(opts) + if optsType.Kind() == reflect.Ptr { + optsType = optsType.Elem() + } + + optsMap := make(map[string]string) + if optsValue.Kind() == reflect.Struct { + for i := 0; i < optsValue.NumField(); i++ { + v := optsValue.Field(i) + f := optsType.Field(i) + hTag := f.Tag.Get("h") + + // if the field has a 'h' tag, it goes in the header + if hTag != "" { + tags := strings.Split(hTag, ",") + + // if the field is set, add it to the slice of query pieces + if !isZero(v) { + switch v.Kind() { + case reflect.String: + optsMap[tags[0]] = v.String() + case reflect.Int: + optsMap[tags[0]] = strconv.FormatInt(v.Int(), 10) + case reflect.Bool: + optsMap[tags[0]] = strconv.FormatBool(v.Bool()) + } + } else { + // Otherwise, the field is not set. + if len(tags) == 2 && tags[1] == "required" { + // And the field is required. Return an error. + return optsMap, fmt.Errorf("Required header not set.") + } + } + } + + } + return optsMap, nil + } + // Return an error if the underlying type of 'opts' isn't a struct. + return optsMap, fmt.Errorf("Options type is not a struct.") +} + +// IDSliceToQueryString takes a slice of elements and converts them into a query +// string. For example, if name=foo and slice=[]int{20, 40, 60}, then the +// result would be `?name=20&name=40&name=60' +func IDSliceToQueryString(name string, ids []int) string { + str := "" + for k, v := range ids { + if k == 0 { + str += "?" + } else { + str += "&" + } + str += fmt.Sprintf("%s=%s", name, strconv.Itoa(v)) + } + return str +} + +// IntWithinRange returns TRUE if an integer falls within a defined range, and +// FALSE if not. +func IntWithinRange(val, min, max int) bool { + return val > min && val < max +} diff --git a/vendor/github.com/gophercloud/gophercloud/provider_client.go b/vendor/github.com/gophercloud/gophercloud/provider_client.go new file mode 100644 index 0000000000..f88682381d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/provider_client.go @@ -0,0 +1,307 @@ +package gophercloud + +import ( + "bytes" + "encoding/json" + "io" + "io/ioutil" + "net/http" + "strings" +) + +// DefaultUserAgent is the default User-Agent string set in the request header. +const DefaultUserAgent = "gophercloud/2.0.0" + +// UserAgent represents a User-Agent header. +type UserAgent struct { + // prepend is the slice of User-Agent strings to prepend to DefaultUserAgent. + // All the strings to prepend are accumulated and prepended in the Join method. + prepend []string +} + +// Prepend prepends a user-defined string to the default User-Agent string. Users +// may pass in one or more strings to prepend. +func (ua *UserAgent) Prepend(s ...string) { + ua.prepend = append(s, ua.prepend...) +} + +// Join concatenates all the user-defined User-Agend strings with the default +// Gophercloud User-Agent string. +func (ua *UserAgent) Join() string { + uaSlice := append(ua.prepend, DefaultUserAgent) + return strings.Join(uaSlice, " ") +} + +// ProviderClient stores details that are required to interact with any +// services within a specific provider's API. +// +// Generally, you acquire a ProviderClient by calling the NewClient method in +// the appropriate provider's child package, providing whatever authentication +// credentials are required. +type ProviderClient struct { + // IdentityBase is the base URL used for a particular provider's identity + // service - it will be used when issuing authenticatation requests. It + // should point to the root resource of the identity service, not a specific + // identity version. + IdentityBase string + + // IdentityEndpoint is the identity endpoint. This may be a specific version + // of the identity service. If this is the case, this endpoint is used rather + // than querying versions first. + IdentityEndpoint string + + // TokenID is the ID of the most recently issued valid token. + TokenID string + + // EndpointLocator describes how this provider discovers the endpoints for + // its constituent services. + EndpointLocator EndpointLocator + + // HTTPClient allows users to interject arbitrary http, https, or other transit behaviors. + HTTPClient http.Client + + // UserAgent represents the User-Agent header in the HTTP request. + UserAgent UserAgent + + // ReauthFunc is the function used to re-authenticate the user if the request + // fails with a 401 HTTP response code. This a needed because there may be multiple + // authentication functions for different Identity service versions. + ReauthFunc func() error + + Debug bool +} + +// AuthenticatedHeaders returns a map of HTTP headers that are common for all +// authenticated service requests. +func (client *ProviderClient) AuthenticatedHeaders() map[string]string { + if client.TokenID == "" { + return map[string]string{} + } + return map[string]string{"X-Auth-Token": client.TokenID} +} + +// RequestOpts customizes the behavior of the provider.Request() method. +type RequestOpts struct { + // JSONBody, if provided, will be encoded as JSON and used as the body of the HTTP request. The + // content type of the request will default to "application/json" unless overridden by MoreHeaders. + // It's an error to specify both a JSONBody and a RawBody. + JSONBody interface{} + // RawBody contains an io.Reader that will be consumed by the request directly. No content-type + // will be set unless one is provided explicitly by MoreHeaders. + RawBody io.Reader + // JSONResponse, if provided, will be populated with the contents of the response body parsed as + // JSON. + JSONResponse interface{} + // OkCodes contains a list of numeric HTTP status codes that should be interpreted as success. If + // the response has a different code, an error will be returned. + OkCodes []int + // MoreHeaders specifies additional HTTP headers to be provide on the request. If a header is + // provided with a blank value (""), that header will be *omitted* instead: use this to suppress + // the default Accept header or an inferred Content-Type, for example. + MoreHeaders map[string]string + // ErrorContext specifies the resource error type to return if an error is encountered. + // This lets resources override default error messages based on the response status code. + ErrorContext error +} + +var applicationJSON = "application/json" + +// Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication +// header will automatically be provided. +func (client *ProviderClient) Request(method, url string, options *RequestOpts) (*http.Response, error) { + var body io.Reader + var contentType *string + + // Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided + // io.ReadSeeker as-is. Default the content-type to application/json. + if options.JSONBody != nil { + if options.RawBody != nil { + panic("Please provide only one of JSONBody or RawBody to gophercloud.Request().") + } + + rendered, err := json.Marshal(options.JSONBody) + if err != nil { + return nil, err + } + + body = bytes.NewReader(rendered) + contentType = &applicationJSON + } + + if options.RawBody != nil { + body = options.RawBody + } + + // Construct the http.Request. + req, err := http.NewRequest(method, url, body) + if err != nil { + return nil, err + } + + // Populate the request headers. Apply options.MoreHeaders last, to give the caller the chance to + // modify or omit any header. + if contentType != nil { + req.Header.Set("Content-Type", *contentType) + } + req.Header.Set("Accept", applicationJSON) + + for k, v := range client.AuthenticatedHeaders() { + req.Header.Add(k, v) + } + + // Set the User-Agent header + req.Header.Set("User-Agent", client.UserAgent.Join()) + + if options.MoreHeaders != nil { + for k, v := range options.MoreHeaders { + if v != "" { + req.Header.Set(k, v) + } else { + req.Header.Del(k) + } + } + } + + // Set connection parameter to close the connection immediately when we've got the response + req.Close = true + + // Issue the request. + resp, err := client.HTTPClient.Do(req) + if err != nil { + return nil, err + } + + // Allow default OkCodes if none explicitly set + if options.OkCodes == nil { + options.OkCodes = defaultOkCodes(method) + } + + // Validate the HTTP response status. + var ok bool + for _, code := range options.OkCodes { + if resp.StatusCode == code { + ok = true + break + } + } + + if !ok { + body, _ := ioutil.ReadAll(resp.Body) + resp.Body.Close() + //pc := make([]uintptr, 1) + //runtime.Callers(2, pc) + //f := runtime.FuncForPC(pc[0]) + respErr := ErrUnexpectedResponseCode{ + URL: url, + Method: method, + Expected: options.OkCodes, + Actual: resp.StatusCode, + Body: body, + } + //respErr.Function = "gophercloud.ProviderClient.Request" + + errType := options.ErrorContext + switch resp.StatusCode { + case http.StatusBadRequest: + err = ErrDefault400{respErr} + if error400er, ok := errType.(Err400er); ok { + err = error400er.Error400(respErr) + } + case http.StatusUnauthorized: + if client.ReauthFunc != nil { + err = client.ReauthFunc() + if err != nil { + e := &ErrUnableToReauthenticate{} + e.ErrOriginal = respErr + return nil, e + } + if options.RawBody != nil { + if seeker, ok := options.RawBody.(io.Seeker); ok { + seeker.Seek(0, 0) + } + } + resp, err = client.Request(method, url, options) + if err != nil { + switch err.(type) { + case *ErrUnexpectedResponseCode: + e := &ErrErrorAfterReauthentication{} + e.ErrOriginal = err.(*ErrUnexpectedResponseCode) + return nil, e + default: + e := &ErrErrorAfterReauthentication{} + e.ErrOriginal = err + return nil, e + } + } + return resp, nil + } + err = ErrDefault401{respErr} + if error401er, ok := errType.(Err401er); ok { + err = error401er.Error401(respErr) + } + case http.StatusNotFound: + err = ErrDefault404{respErr} + if error404er, ok := errType.(Err404er); ok { + err = error404er.Error404(respErr) + } + case http.StatusMethodNotAllowed: + err = ErrDefault405{respErr} + if error405er, ok := errType.(Err405er); ok { + err = error405er.Error405(respErr) + } + case http.StatusRequestTimeout: + err = ErrDefault408{respErr} + if error408er, ok := errType.(Err408er); ok { + err = error408er.Error408(respErr) + } + case 429: + err = ErrDefault429{respErr} + if error429er, ok := errType.(Err429er); ok { + err = error429er.Error429(respErr) + } + case http.StatusInternalServerError: + err = ErrDefault500{respErr} + if error500er, ok := errType.(Err500er); ok { + err = error500er.Error500(respErr) + } + case http.StatusServiceUnavailable: + err = ErrDefault503{respErr} + if error503er, ok := errType.(Err503er); ok { + err = error503er.Error503(respErr) + } + } + + if err == nil { + err = respErr + } + + return resp, err + } + + // Parse the response body as JSON, if requested to do so. + if options.JSONResponse != nil { + defer resp.Body.Close() + if err := json.NewDecoder(resp.Body).Decode(options.JSONResponse); err != nil { + return nil, err + } + } + + return resp, nil +} + +func defaultOkCodes(method string) []int { + switch { + case method == "GET": + return []int{200} + case method == "POST": + return []int{201, 202} + case method == "PUT": + return []int{201, 202} + case method == "PATCH": + return []int{200, 204} + case method == "DELETE": + return []int{202, 204} + } + + return []int{} +} diff --git a/vendor/github.com/gophercloud/gophercloud/results.go b/vendor/github.com/gophercloud/gophercloud/results.go new file mode 100644 index 0000000000..76c16ef8ff --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/results.go @@ -0,0 +1,336 @@ +package gophercloud + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "reflect" + "strconv" + "time" +) + +/* +Result is an internal type to be used by individual resource packages, but its +methods will be available on a wide variety of user-facing embedding types. + +It acts as a base struct that other Result types, returned from request +functions, can embed for convenience. All Results capture basic information +from the HTTP transaction that was performed, including the response body, +HTTP headers, and any errors that happened. + +Generally, each Result type will have an Extract method that can be used to +further interpret the result's payload in a specific context. Extensions or +providers can then provide additional extraction functions to pull out +provider- or extension-specific information as well. +*/ +type Result struct { + // Body is the payload of the HTTP response from the server. In most cases, + // this will be the deserialized JSON structure. + Body interface{} + + // Header contains the HTTP header structure from the original response. + Header http.Header + + // Err is an error that occurred during the operation. It's deferred until + // extraction to make it easier to chain the Extract call. + Err error +} + +// ExtractInto allows users to provide an object into which `Extract` will extract +// the `Result.Body`. This would be useful for OpenStack providers that have +// different fields in the response object than OpenStack proper. +func (r Result) ExtractInto(to interface{}) error { + if r.Err != nil { + return r.Err + } + + if reader, ok := r.Body.(io.Reader); ok { + if readCloser, ok := reader.(io.Closer); ok { + defer readCloser.Close() + } + return json.NewDecoder(reader).Decode(to) + } + + b, err := json.Marshal(r.Body) + if err != nil { + return err + } + err = json.Unmarshal(b, to) + + return err +} + +func (r Result) extractIntoPtr(to interface{}, label string) error { + if label == "" { + return r.ExtractInto(&to) + } + + var m map[string]interface{} + err := r.ExtractInto(&m) + if err != nil { + return err + } + + b, err := json.Marshal(m[label]) + if err != nil { + return err + } + + err = json.Unmarshal(b, &to) + return err +} + +// ExtractIntoStructPtr will unmarshal the Result (r) into the provided +// interface{} (to). +// +// NOTE: For internal use only +// +// `to` must be a pointer to an underlying struct type +// +// If provided, `label` will be filtered out of the response +// body prior to `r` being unmarshalled into `to`. +func (r Result) ExtractIntoStructPtr(to interface{}, label string) error { + if r.Err != nil { + return r.Err + } + + t := reflect.TypeOf(to) + if k := t.Kind(); k != reflect.Ptr { + return fmt.Errorf("Expected pointer, got %v", k) + } + switch t.Elem().Kind() { + case reflect.Struct: + return r.extractIntoPtr(to, label) + default: + return fmt.Errorf("Expected pointer to struct, got: %v", t) + } +} + +// ExtractIntoSlicePtr will unmarshal the Result (r) into the provided +// interface{} (to). +// +// NOTE: For internal use only +// +// `to` must be a pointer to an underlying slice type +// +// If provided, `label` will be filtered out of the response +// body prior to `r` being unmarshalled into `to`. +func (r Result) ExtractIntoSlicePtr(to interface{}, label string) error { + if r.Err != nil { + return r.Err + } + + t := reflect.TypeOf(to) + if k := t.Kind(); k != reflect.Ptr { + return fmt.Errorf("Expected pointer, got %v", k) + } + switch t.Elem().Kind() { + case reflect.Slice: + return r.extractIntoPtr(to, label) + default: + return fmt.Errorf("Expected pointer to slice, got: %v", t) + } +} + +// PrettyPrintJSON creates a string containing the full response body as +// pretty-printed JSON. It's useful for capturing test fixtures and for +// debugging extraction bugs. If you include its output in an issue related to +// a buggy extraction function, we will all love you forever. +func (r Result) PrettyPrintJSON() string { + pretty, err := json.MarshalIndent(r.Body, "", " ") + if err != nil { + panic(err.Error()) + } + return string(pretty) +} + +// ErrResult is an internal type to be used by individual resource packages, but +// its methods will be available on a wide variety of user-facing embedding +// types. +// +// It represents results that only contain a potential error and +// nothing else. Usually, if the operation executed successfully, the Err field +// will be nil; otherwise it will be stocked with a relevant error. Use the +// ExtractErr method +// to cleanly pull it out. +type ErrResult struct { + Result +} + +// ExtractErr is a function that extracts error information, or nil, from a result. +func (r ErrResult) ExtractErr() error { + return r.Err +} + +/* +HeaderResult is an internal type to be used by individual resource packages, but +its methods will be available on a wide variety of user-facing embedding types. + +It represents a result that only contains an error (possibly nil) and an +http.Header. This is used, for example, by the objectstorage packages in +openstack, because most of the operations don't return response bodies, but do +have relevant information in headers. +*/ +type HeaderResult struct { + Result +} + +// ExtractHeader will return the http.Header and error from the HeaderResult. +// +// header, err := objects.Create(client, "my_container", objects.CreateOpts{}).ExtractHeader() +func (r HeaderResult) ExtractInto(to interface{}) error { + if r.Err != nil { + return r.Err + } + + tmpHeaderMap := map[string]string{} + for k, v := range r.Header { + if len(v) > 0 { + tmpHeaderMap[k] = v[0] + } + } + + b, err := json.Marshal(tmpHeaderMap) + if err != nil { + return err + } + err = json.Unmarshal(b, to) + + return err +} + +// RFC3339Milli describes a common time format used by some API responses. +const RFC3339Milli = "2006-01-02T15:04:05.999999Z" + +type JSONRFC3339Milli time.Time + +func (jt *JSONRFC3339Milli) UnmarshalJSON(data []byte) error { + b := bytes.NewBuffer(data) + dec := json.NewDecoder(b) + var s string + if err := dec.Decode(&s); err != nil { + return err + } + t, err := time.Parse(RFC3339Milli, s) + if err != nil { + return err + } + *jt = JSONRFC3339Milli(t) + return nil +} + +const RFC3339MilliNoZ = "2006-01-02T15:04:05.999999" + +type JSONRFC3339MilliNoZ time.Time + +func (jt *JSONRFC3339MilliNoZ) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + t, err := time.Parse(RFC3339MilliNoZ, s) + if err != nil { + return err + } + *jt = JSONRFC3339MilliNoZ(t) + return nil +} + +type JSONRFC1123 time.Time + +func (jt *JSONRFC1123) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + t, err := time.Parse(time.RFC1123, s) + if err != nil { + return err + } + *jt = JSONRFC1123(t) + return nil +} + +type JSONUnix time.Time + +func (jt *JSONUnix) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + unix, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return err + } + t = time.Unix(unix, 0) + *jt = JSONUnix(t) + return nil +} + +// RFC3339NoZ is the time format used in Heat (Orchestration). +const RFC3339NoZ = "2006-01-02T15:04:05" + +type JSONRFC3339NoZ time.Time + +func (jt *JSONRFC3339NoZ) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + t, err := time.Parse(RFC3339NoZ, s) + if err != nil { + return err + } + *jt = JSONRFC3339NoZ(t) + return nil +} + +/* +Link is an internal type to be used in packages of collection resources that are +paginated in a certain way. + +It's a response substructure common to many paginated collection results that is +used to point to related pages. Usually, the one we care about is the one with +Rel field set to "next". +*/ +type Link struct { + Href string `json:"href"` + Rel string `json:"rel"` +} + +/* +ExtractNextURL is an internal function useful for packages of collection +resources that are paginated in a certain way. + +It attempts to extract the "next" URL from slice of Link structs, or +"" if no such URL is present. +*/ +func ExtractNextURL(links []Link) (string, error) { + var url string + + for _, l := range links { + if l.Rel == "next" { + url = l.Href + } + } + + if url == "" { + return "", nil + } + + return url, nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/service_client.go b/vendor/github.com/gophercloud/gophercloud/service_client.go new file mode 100644 index 0000000000..7484c67e57 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/service_client.go @@ -0,0 +1,141 @@ +package gophercloud + +import ( + "io" + "net/http" + "strings" +) + +// ServiceClient stores details required to interact with a specific service API implemented by a provider. +// Generally, you'll acquire these by calling the appropriate `New` method on a ProviderClient. +type ServiceClient struct { + // ProviderClient is a reference to the provider that implements this service. + *ProviderClient + + // Endpoint is the base URL of the service's API, acquired from a service catalog. + // It MUST end with a /. + Endpoint string + + // ResourceBase is the base URL shared by the resources within a service's API. It should include + // the API version and, like Endpoint, MUST end with a / if set. If not set, the Endpoint is used + // as-is, instead. + ResourceBase string + + Microversion string +} + +// ResourceBaseURL returns the base URL of any resources used by this service. It MUST end with a /. +func (client *ServiceClient) ResourceBaseURL() string { + if client.ResourceBase != "" { + return client.ResourceBase + } + return client.Endpoint +} + +// ServiceURL constructs a URL for a resource belonging to this provider. +func (client *ServiceClient) ServiceURL(parts ...string) string { + return client.ResourceBaseURL() + strings.Join(parts, "/") +} + +// Get calls `Request` with the "GET" HTTP verb. +func (client *ServiceClient) Get(url string, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = &RequestOpts{} + } + if JSONResponse != nil { + opts.JSONResponse = JSONResponse + } + + if opts.MoreHeaders == nil { + opts.MoreHeaders = make(map[string]string) + } + opts.MoreHeaders["X-OpenStack-Nova-API-Version"] = client.Microversion + + return client.Request("GET", url, opts) +} + +// Post calls `Request` with the "POST" HTTP verb. +func (client *ServiceClient) Post(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = &RequestOpts{} + } + + if v, ok := (JSONBody).(io.Reader); ok { + opts.RawBody = v + } else if JSONBody != nil { + opts.JSONBody = JSONBody + } + + if JSONResponse != nil { + opts.JSONResponse = JSONResponse + } + + if opts.MoreHeaders == nil { + opts.MoreHeaders = make(map[string]string) + } + opts.MoreHeaders["X-OpenStack-Nova-API-Version"] = client.Microversion + + return client.Request("POST", url, opts) +} + +// Put calls `Request` with the "PUT" HTTP verb. +func (client *ServiceClient) Put(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = &RequestOpts{} + } + + if v, ok := (JSONBody).(io.Reader); ok { + opts.RawBody = v + } else if JSONBody != nil { + opts.JSONBody = JSONBody + } + + if JSONResponse != nil { + opts.JSONResponse = JSONResponse + } + + if opts.MoreHeaders == nil { + opts.MoreHeaders = make(map[string]string) + } + opts.MoreHeaders["X-OpenStack-Nova-API-Version"] = client.Microversion + + return client.Request("PUT", url, opts) +} + +// Patch calls `Request` with the "PATCH" HTTP verb. +func (client *ServiceClient) Patch(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = &RequestOpts{} + } + + if v, ok := (JSONBody).(io.Reader); ok { + opts.RawBody = v + } else if JSONBody != nil { + opts.JSONBody = JSONBody + } + + if JSONResponse != nil { + opts.JSONResponse = JSONResponse + } + + if opts.MoreHeaders == nil { + opts.MoreHeaders = make(map[string]string) + } + opts.MoreHeaders["X-OpenStack-Nova-API-Version"] = client.Microversion + + return client.Request("PATCH", url, opts) +} + +// Delete calls `Request` with the "DELETE" HTTP verb. +func (client *ServiceClient) Delete(url string, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = &RequestOpts{} + } + + if opts.MoreHeaders == nil { + opts.MoreHeaders = make(map[string]string) + } + opts.MoreHeaders["X-OpenStack-Nova-API-Version"] = client.Microversion + + return client.Request("DELETE", url, opts) +} diff --git a/vendor/github.com/gophercloud/gophercloud/util.go b/vendor/github.com/gophercloud/gophercloud/util.go new file mode 100644 index 0000000000..68f9a5d3ec --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/util.go @@ -0,0 +1,102 @@ +package gophercloud + +import ( + "fmt" + "net/url" + "path/filepath" + "strings" + "time" +) + +// WaitFor polls a predicate function, once per second, up to a timeout limit. +// This is useful to wait for a resource to transition to a certain state. +// To handle situations when the predicate might hang indefinitely, the +// predicate will be prematurely cancelled after the timeout. +// Resource packages will wrap this in a more convenient function that's +// specific to a certain resource, but it can also be useful on its own. +func WaitFor(timeout int, predicate func() (bool, error)) error { + type WaitForResult struct { + Success bool + Error error + } + + start := time.Now().Unix() + + for { + // If a timeout is set, and that's been exceeded, shut it down. + if timeout >= 0 && time.Now().Unix()-start >= int64(timeout) { + return fmt.Errorf("A timeout occurred") + } + + time.Sleep(1 * time.Second) + + var result WaitForResult + ch := make(chan bool, 1) + go func() { + defer close(ch) + satisfied, err := predicate() + result.Success = satisfied + result.Error = err + }() + + select { + case <-ch: + if result.Error != nil { + return result.Error + } + if result.Success { + return nil + } + // If the predicate has not finished by the timeout, cancel it. + case <-time.After(time.Duration(timeout) * time.Second): + return fmt.Errorf("A timeout occurred") + } + } +} + +// NormalizeURL is an internal function to be used by provider clients. +// +// It ensures that each endpoint URL has a closing `/`, as expected by +// ServiceClient's methods. +func NormalizeURL(url string) string { + if !strings.HasSuffix(url, "/") { + return url + "/" + } + return url +} + +// NormalizePathURL is used to convert rawPath to a fqdn, using basePath as +// a reference in the filesystem, if necessary. basePath is assumed to contain +// either '.' when first used, or the file:// type fqdn of the parent resource. +// e.g. myFavScript.yaml => file://opt/lib/myFavScript.yaml +func NormalizePathURL(basePath, rawPath string) (string, error) { + u, err := url.Parse(rawPath) + if err != nil { + return "", err + } + // if a scheme is defined, it must be a fqdn already + if u.Scheme != "" { + return u.String(), nil + } + // if basePath is a url, then child resources are assumed to be relative to it + bu, err := url.Parse(basePath) + if err != nil { + return "", err + } + var basePathSys, absPathSys string + if bu.Scheme != "" { + basePathSys = filepath.FromSlash(bu.Path) + absPathSys = filepath.Join(basePathSys, rawPath) + bu.Path = filepath.ToSlash(absPathSys) + return bu.String(), nil + } + + absPathSys = filepath.Join(basePath, rawPath) + u.Path = filepath.ToSlash(absPathSys) + if err != nil { + return "", err + } + u.Scheme = "file" + return u.String(), nil + +} diff --git a/vendor/github.com/samuel/go-zookeeper/zk/conn.go b/vendor/github.com/samuel/go-zookeeper/zk/conn.go index b46467025a..b6b8dbc1a2 100644 --- a/vendor/github.com/samuel/go-zookeeper/zk/conn.go +++ b/vendor/github.com/samuel/go-zookeeper/zk/conn.go @@ -44,9 +44,9 @@ const ( type watchType int const ( - watchTypeData = iota - watchTypeExist = iota - watchTypeChild = iota + watchTypeData = iota + watchTypeExist + watchTypeChild ) type watchPathType struct { @@ -61,37 +61,52 @@ type Logger interface { Printf(string, ...interface{}) } -type Conn struct { - lastZxid int64 - sessionID int64 - state State // must be 32-bit aligned - xid uint32 - timeout int32 // session timeout in milliseconds - passwd []byte +type authCreds struct { + scheme string + auth []byte +} - dialer Dialer - servers []string - serverIndex int // remember last server that was tried during connect to round-robin attempts to servers - lastServerIndex int // index of the last server that was successfully connected to and authenticated with - conn net.Conn - eventChan chan Event - shouldQuit chan struct{} - pingInterval time.Duration - recvTimeout time.Duration - connectTimeout time.Duration +type Conn struct { + lastZxid int64 + sessionID int64 + state State // must be 32-bit aligned + xid uint32 + sessionTimeoutMs int32 // session timeout in milliseconds + passwd []byte + + dialer Dialer + hostProvider HostProvider + serverMu sync.Mutex // protects server + server string // remember the address/port of the current server + conn net.Conn + eventChan chan Event + eventCallback EventCallback // may be nil + shouldQuit chan struct{} + pingInterval time.Duration + recvTimeout time.Duration + connectTimeout time.Duration + + creds []authCreds + credsMu sync.Mutex // protects server sendChan chan *request requests map[int32]*request // Xid -> pending request requestsLock sync.Mutex watchers map[watchPathType][]chan Event watchersLock sync.Mutex + closeChan chan struct{} // channel to tell send loop stop // Debug (used by unit tests) reconnectDelay time.Duration logger Logger + + buf []byte } +// connOption represents a connection option. +type connOption func(c *Conn) + type request struct { xid int32 opcode int32 @@ -122,26 +137,39 @@ type Event struct { Server string // For connection events } -// Connect establishes a new connection to a pool of zookeeper servers -// using the default net.Dialer. See ConnectWithDialer for further -// information about session timeout. -func Connect(servers []string, sessionTimeout time.Duration) (*Conn, <-chan Event, error) { - return ConnectWithDialer(servers, sessionTimeout, nil) +// HostProvider is used to represent a set of hosts a ZooKeeper client should connect to. +// It is an analog of the Java equivalent: +// http://svn.apache.org/viewvc/zookeeper/trunk/src/java/main/org/apache/zookeeper/client/HostProvider.java?view=markup +type HostProvider interface { + // Init is called first, with the servers specified in the connection string. + Init(servers []string) error + // Len returns the number of servers. + Len() int + // Next returns the next server to connect to. retryStart will be true if we've looped through + // all known servers without Connected() being called. + Next() (server string, retryStart bool) + // Notify the HostProvider of a successful connection. + Connected() } -// ConnectWithDialer establishes a new connection to a pool of zookeeper +// ConnectWithDialer establishes a new connection to a pool of zookeeper servers +// using a custom Dialer. See Connect for further information about session timeout. +// This method is deprecated and provided for compatibility: use the WithDialer option instead. +func ConnectWithDialer(servers []string, sessionTimeout time.Duration, dialer Dialer) (*Conn, <-chan Event, error) { + return Connect(servers, sessionTimeout, WithDialer(dialer)) +} + +// Connect establishes a new connection to a pool of zookeeper // servers. The provided session timeout sets the amount of time for which // a session is considered valid after losing connection to a server. Within // the session timeout it's possible to reestablish a connection to a different // server and keep the same session. This is means any ephemeral nodes and // watches are maintained. -func ConnectWithDialer(servers []string, sessionTimeout time.Duration, dialer Dialer) (*Conn, <-chan Event, error) { +func Connect(servers []string, sessionTimeout time.Duration, options ...connOption) (*Conn, <-chan Event, error) { if len(servers) == 0 { return nil, nil, errors.New("zk: server list must not be empty") } - recvTimeout := sessionTimeout * 2 / 3 - srvs := make([]string, len(servers)) for i, addr := range servers { @@ -156,38 +184,69 @@ func ConnectWithDialer(servers []string, sessionTimeout time.Duration, dialer Di stringShuffle(srvs) ec := make(chan Event, eventChanSize) - if dialer == nil { - dialer = net.DialTimeout - } - conn := Conn{ - dialer: dialer, - servers: srvs, - serverIndex: 0, - lastServerIndex: -1, - conn: nil, - state: StateDisconnected, - eventChan: ec, - shouldQuit: make(chan struct{}), - recvTimeout: recvTimeout, - pingInterval: recvTimeout / 2, - connectTimeout: 1 * time.Second, - sendChan: make(chan *request, sendChanSize), - requests: make(map[int32]*request), - watchers: make(map[watchPathType][]chan Event), - passwd: emptyPassword, - timeout: int32(sessionTimeout.Nanoseconds() / 1e6), - logger: DefaultLogger, + conn := &Conn{ + dialer: net.DialTimeout, + hostProvider: &DNSHostProvider{}, + conn: nil, + state: StateDisconnected, + eventChan: ec, + shouldQuit: make(chan struct{}), + connectTimeout: 1 * time.Second, + sendChan: make(chan *request, sendChanSize), + requests: make(map[int32]*request), + watchers: make(map[watchPathType][]chan Event), + passwd: emptyPassword, + logger: DefaultLogger, + buf: make([]byte, bufferSize), // Debug reconnectDelay: 0, } + + // Set provided options. + for _, option := range options { + option(conn) + } + + if err := conn.hostProvider.Init(srvs); err != nil { + return nil, nil, err + } + + conn.setTimeouts(int32(sessionTimeout / time.Millisecond)) + go func() { conn.loop() conn.flushRequests(ErrClosing) conn.invalidateWatches(ErrClosing) close(conn.eventChan) }() - return &conn, ec, nil + return conn, ec, nil +} + +// WithDialer returns a connection option specifying a non-default Dialer. +func WithDialer(dialer Dialer) connOption { + return func(c *Conn) { + c.dialer = dialer + } +} + +// WithHostProvider returns a connection option specifying a non-default HostProvider. +func WithHostProvider(hostProvider HostProvider) connOption { + return func(c *Conn) { + c.hostProvider = hostProvider + } +} + +// EventCallback is a function that is called when an Event occurs. +type EventCallback func(Event) + +// WithEventCallback returns a connection option that specifies an event +// callback. +// The callback must not block - doing so would delay the ZK go routines. +func WithEventCallback(cb EventCallback) connOption { + return func(c *Conn) { + c.eventCallback = cb + } } func (c *Conn) Close() { @@ -199,31 +258,54 @@ func (c *Conn) Close() { } } -// States returns the current state of the connection. +// State returns the current state of the connection. func (c *Conn) State() State { return State(atomic.LoadInt32((*int32)(&c.state))) } +// SessionID returns the current session id of the connection. +func (c *Conn) SessionID() int64 { + return atomic.LoadInt64(&c.sessionID) +} + // SetLogger sets the logger to be used for printing errors. // Logger is an interface provided by this package. func (c *Conn) SetLogger(l Logger) { c.logger = l } +func (c *Conn) setTimeouts(sessionTimeoutMs int32) { + c.sessionTimeoutMs = sessionTimeoutMs + sessionTimeout := time.Duration(sessionTimeoutMs) * time.Millisecond + c.recvTimeout = sessionTimeout * 2 / 3 + c.pingInterval = c.recvTimeout / 2 +} + func (c *Conn) setState(state State) { atomic.StoreInt32((*int32)(&c.state), int32(state)) + c.sendEvent(Event{Type: EventSession, State: state, Server: c.Server()}) +} + +func (c *Conn) sendEvent(evt Event) { + if c.eventCallback != nil { + c.eventCallback(evt) + } + select { - case c.eventChan <- Event{Type: EventSession, State: state, Server: c.servers[c.serverIndex]}: + case c.eventChan <- evt: default: // panic("zk: event channel full - it must be monitored and never allowed to be full") } } func (c *Conn) connect() error { - c.setState(StateConnecting) + var retryStart bool for { - c.serverIndex = (c.serverIndex + 1) % len(c.servers) - if c.serverIndex == c.lastServerIndex { + c.serverMu.Lock() + c.server, retryStart = c.hostProvider.Next() + c.serverMu.Unlock() + c.setState(StateConnecting) + if retryStart { c.flushUnsentRequests(ErrNoServer) select { case <-time.After(time.Second): @@ -233,22 +315,79 @@ func (c *Conn) connect() error { c.flushUnsentRequests(ErrClosing) return ErrClosing } - } else if c.lastServerIndex < 0 { - // lastServerIndex defaults to -1 to avoid a delay on the initial connect - c.lastServerIndex = 0 } - zkConn, err := c.dialer("tcp", c.servers[c.serverIndex], c.connectTimeout) + zkConn, err := c.dialer("tcp", c.Server(), c.connectTimeout) if err == nil { c.conn = zkConn c.setState(StateConnected) + c.logger.Printf("Connected to %s", c.Server()) return nil } - c.logger.Printf("Failed to connect to %s: %+v", c.servers[c.serverIndex], err) + c.logger.Printf("Failed to connect to %s: %+v", c.Server(), err) } } +func (c *Conn) resendZkAuth(reauthReadyChan chan struct{}) { + c.credsMu.Lock() + defer c.credsMu.Unlock() + + defer close(reauthReadyChan) + + c.logger.Printf("Re-submitting `%d` credentials after reconnect", + len(c.creds)) + + for _, cred := range c.creds { + resChan, err := c.sendRequest( + opSetAuth, + &setAuthRequest{Type: 0, + Scheme: cred.scheme, + Auth: cred.auth, + }, + &setAuthResponse{}, + nil) + + if err != nil { + c.logger.Printf("Call to sendRequest failed during credential resubmit: %s", err) + // FIXME(prozlach): lets ignore errors for now + continue + } + + res := <-resChan + if res.err != nil { + c.logger.Printf("Credential re-submit failed: %s", res.err) + // FIXME(prozlach): lets ignore errors for now + continue + } + } +} + +func (c *Conn) sendRequest( + opcode int32, + req interface{}, + res interface{}, + recvFunc func(*request, *responseHeader, error), +) ( + <-chan response, + error, +) { + rq := &request{ + xid: c.nextXid(), + opcode: opcode, + pkt: req, + recvStruct: res, + recvChan: make(chan response, 1), + recvFunc: recvFunc, + } + + if err := c.sendData(rq); err != nil { + return nil, err + } + + return rq.recvChan, nil +} + func (c *Conn) loop() { for { if err := c.connect(); err != nil { @@ -259,41 +398,46 @@ func (c *Conn) loop() { err := c.authenticate() switch { case err == ErrSessionExpired: + c.logger.Printf("Authentication failed: %s", err) c.invalidateWatches(err) case err != nil && c.conn != nil: + c.logger.Printf("Authentication failed: %s", err) c.conn.Close() case err == nil: - c.lastServerIndex = c.serverIndex - closeChan := make(chan struct{}) // channel to tell send loop stop - var wg sync.WaitGroup + c.logger.Printf("Authenticated: id=%d, timeout=%d", c.SessionID(), c.sessionTimeoutMs) + c.hostProvider.Connected() // mark success + c.closeChan = make(chan struct{}) // channel to tell send loop stop + reauthChan := make(chan struct{}) // channel to tell send loop that authdata has been resubmitted + var wg sync.WaitGroup wg.Add(1) go func() { - c.sendLoop(c.conn, closeChan) + <-reauthChan + err := c.sendLoop() + c.logger.Printf("Send loop terminated: err=%v", err) c.conn.Close() // causes recv loop to EOF/exit wg.Done() }() wg.Add(1) go func() { - err = c.recvLoop(c.conn) + err := c.recvLoop(c.conn) + c.logger.Printf("Recv loop terminated: err=%v", err) if err == nil { panic("zk: recvLoop should never return nil error") } - close(closeChan) // tell send loop to exit + close(c.closeChan) // tell send loop to exit wg.Done() }() + c.resendZkAuth(reauthChan) + + c.sendSetWatches() wg.Wait() } c.setState(StateDisconnected) - // Yeesh - if err != io.EOF && err != ErrSessionExpired && !strings.Contains(err.Error(), "use of closed network connection") { - c.logger.Printf(err.Error()) - } - select { case <-c.shouldQuit: c.flushRequests(ErrClosing) @@ -399,13 +543,12 @@ func (c *Conn) sendSetWatches() { func (c *Conn) authenticate() error { buf := make([]byte, 256) - // connect request - + // Encode and send a connect request. n, err := encodePacket(buf[4:], &connectRequest{ ProtocolVersion: protocolVersion, LastZxidSeen: c.lastZxid, - TimeOut: c.timeout, - SessionID: c.sessionID, + TimeOut: c.sessionTimeoutMs, + SessionID: c.SessionID(), Passwd: c.passwd, }) if err != nil { @@ -421,23 +564,12 @@ func (c *Conn) authenticate() error { return err } - c.sendSetWatches() - - // connect response - - // package length + // Receive and decode a connect response. c.conn.SetReadDeadline(time.Now().Add(c.recvTimeout * 10)) _, err = io.ReadFull(c.conn, buf[:4]) c.conn.SetReadDeadline(time.Time{}) if err != nil { - // Sometimes zookeeper just drops connection on invalid session data, - // we prefer to drop session and start from scratch when that event - // occurs instead of dropping into loop of connect/disconnect attempts - c.sessionID = 0 - c.passwd = emptyPassword - c.lastZxid = 0 - c.setState(StateExpired) - return ErrSessionExpired + return err } blen := int(binary.BigEndian.Uint32(buf[:4])) @@ -456,81 +588,88 @@ func (c *Conn) authenticate() error { return err } if r.SessionID == 0 { - c.sessionID = 0 + atomic.StoreInt64(&c.sessionID, int64(0)) c.passwd = emptyPassword c.lastZxid = 0 c.setState(StateExpired) return ErrSessionExpired } - c.timeout = r.TimeOut - c.sessionID = r.SessionID + atomic.StoreInt64(&c.sessionID, r.SessionID) + c.setTimeouts(r.TimeOut) c.passwd = r.Passwd c.setState(StateHasSession) return nil } -func (c *Conn) sendLoop(conn net.Conn, closeChan <-chan struct{}) error { +func (c *Conn) sendData(req *request) error { + header := &requestHeader{req.xid, req.opcode} + n, err := encodePacket(c.buf[4:], header) + if err != nil { + req.recvChan <- response{-1, err} + return nil + } + + n2, err := encodePacket(c.buf[4+n:], req.pkt) + if err != nil { + req.recvChan <- response{-1, err} + return nil + } + + n += n2 + + binary.BigEndian.PutUint32(c.buf[:4], uint32(n)) + + c.requestsLock.Lock() + select { + case <-c.closeChan: + req.recvChan <- response{-1, ErrConnectionClosed} + c.requestsLock.Unlock() + return ErrConnectionClosed + default: + } + c.requests[req.xid] = req + c.requestsLock.Unlock() + + c.conn.SetWriteDeadline(time.Now().Add(c.recvTimeout)) + _, err = c.conn.Write(c.buf[:n+4]) + c.conn.SetWriteDeadline(time.Time{}) + if err != nil { + req.recvChan <- response{-1, err} + c.conn.Close() + return err + } + + return nil +} + +func (c *Conn) sendLoop() error { pingTicker := time.NewTicker(c.pingInterval) defer pingTicker.Stop() - buf := make([]byte, bufferSize) for { select { case req := <-c.sendChan: - header := &requestHeader{req.xid, req.opcode} - n, err := encodePacket(buf[4:], header) - if err != nil { - req.recvChan <- response{-1, err} - continue - } - - n2, err := encodePacket(buf[4+n:], req.pkt) - if err != nil { - req.recvChan <- response{-1, err} - continue - } - - n += n2 - - binary.BigEndian.PutUint32(buf[:4], uint32(n)) - - c.requestsLock.Lock() - select { - case <-closeChan: - req.recvChan <- response{-1, ErrConnectionClosed} - c.requestsLock.Unlock() - return ErrConnectionClosed - default: - } - c.requests[req.xid] = req - c.requestsLock.Unlock() - - conn.SetWriteDeadline(time.Now().Add(c.recvTimeout)) - _, err = conn.Write(buf[:n+4]) - conn.SetWriteDeadline(time.Time{}) - if err != nil { - req.recvChan <- response{-1, err} - conn.Close() + if err := c.sendData(req); err != nil { return err } case <-pingTicker.C: - n, err := encodePacket(buf[4:], &requestHeader{Xid: -2, Opcode: opPing}) + n, err := encodePacket(c.buf[4:], &requestHeader{Xid: -2, Opcode: opPing}) if err != nil { panic("zk: opPing should never fail to serialize") } - binary.BigEndian.PutUint32(buf[:4], uint32(n)) + binary.BigEndian.PutUint32(c.buf[:4], uint32(n)) - conn.SetWriteDeadline(time.Now().Add(c.recvTimeout)) - _, err = conn.Write(buf[:n+4]) - conn.SetWriteDeadline(time.Time{}) + c.conn.SetWriteDeadline(time.Now().Add(c.recvTimeout)) + _, err = c.conn.Write(c.buf[:n+4]) + c.conn.SetWriteDeadline(time.Time{}) if err != nil { - conn.Close() + c.conn.Close() return err } - case <-closeChan: + case <-c.closeChan: return nil } } @@ -565,7 +704,7 @@ func (c *Conn) recvLoop(conn net.Conn) error { if res.Xid == -1 { res := &watcherEvent{} - _, err := decodePacket(buf[16:16+blen], res) + _, err := decodePacket(buf[16:blen], res) if err != nil { return err } @@ -575,10 +714,7 @@ func (c *Conn) recvLoop(conn net.Conn) error { Path: res.Path, Err: nil, } - select { - case c.eventChan <- ev: - default: - } + c.sendEvent(ev) wTypes := make([]watchType, 0, 2) switch res.Type { case EventNodeCreated: @@ -622,7 +758,7 @@ func (c *Conn) recvLoop(conn net.Conn) error { if res.Err != 0 { err = res.Err.toError() } else { - _, err = decodePacket(buf[16:16+blen], req.recvStruct) + _, err = decodePacket(buf[16:blen], req.recvStruct) } if req.recvFunc != nil { req.recvFunc(req, &res, err) @@ -670,7 +806,28 @@ func (c *Conn) request(opcode int32, req interface{}, res interface{}, recvFunc func (c *Conn) AddAuth(scheme string, auth []byte) error { _, err := c.request(opSetAuth, &setAuthRequest{Type: 0, Scheme: scheme, Auth: auth}, &setAuthResponse{}, nil) - return err + + if err != nil { + return err + } + + // Remember authdata so that it can be re-submitted on reconnect + // + // FIXME(prozlach): For now we treat "userfoo:passbar" and "userfoo:passbar2" + // as two different entries, which will be re-submitted on reconnet. Some + // research is needed on how ZK treats these cases and + // then maybe switch to something like "map[username] = password" to allow + // only single password for given user with users being unique. + obj := authCreds{ + scheme: scheme, + auth: auth, + } + + c.credsMu.Lock() + c.creds = append(c.creds, obj) + c.credsMu.Unlock() + + return nil } func (c *Conn) Children(path string) ([]string, *Stat, error) { @@ -816,7 +973,6 @@ func (c *Conn) GetACL(path string) ([]ACL, *Stat, error) { _, err := c.request(opGetAcl, &getAclRequest{Path: path}, res, nil) return res.Acl, &res.Stat, err } - func (c *Conn) SetACL(path string, acl []ACL, version int32) (*Stat, error) { res := &setAclResponse{} _, err := c.request(opSetAcl, &setAclRequest{Path: path, Acl: acl, Version: version}, res, nil) @@ -832,6 +988,7 @@ func (c *Conn) Sync(path string) (string, error) { type MultiResponse struct { Stat *Stat String string + Error error } // Multi executes multiple ZooKeeper operations or none of them. The provided @@ -854,7 +1011,7 @@ func (c *Conn) Multi(ops ...interface{}) ([]MultiResponse, error) { case *CheckVersionRequest: opCode = opCheck default: - return nil, fmt.Errorf("uknown operation type %T", op) + return nil, fmt.Errorf("unknown operation type %T", op) } req.Ops = append(req.Ops, multiRequestOp{multiHeader{opCode, false, -1}, op}) } @@ -862,7 +1019,14 @@ func (c *Conn) Multi(ops ...interface{}) ([]MultiResponse, error) { _, err := c.request(opMulti, req, res, nil) mr := make([]MultiResponse, len(res.Ops)) for i, op := range res.Ops { - mr[i] = MultiResponse{Stat: op.Stat, String: op.String} + mr[i] = MultiResponse{Stat: op.Stat, String: op.String, Error: op.Err.toError()} } return mr, err } + +// Server returns the current or last-connected server name. +func (c *Conn) Server() string { + c.serverMu.Lock() + defer c.serverMu.Unlock() + return c.server +} diff --git a/vendor/github.com/samuel/go-zookeeper/zk/constants.go b/vendor/github.com/samuel/go-zookeeper/zk/constants.go index f9b39b904f..33b5563b9f 100644 --- a/vendor/github.com/samuel/go-zookeeper/zk/constants.go +++ b/vendor/github.com/samuel/go-zookeeper/zk/constants.go @@ -28,18 +28,19 @@ const ( opClose = -11 opSetAuth = 100 opSetWatches = 101 + opError = -1 // Not in protocol, used internally opWatcherEvent = -2 ) const ( - EventNodeCreated = EventType(1) - EventNodeDeleted = EventType(2) - EventNodeDataChanged = EventType(3) - EventNodeChildrenChanged = EventType(4) + EventNodeCreated EventType = 1 + EventNodeDeleted EventType = 2 + EventNodeDataChanged EventType = 3 + EventNodeChildrenChanged EventType = 4 - EventSession = EventType(-1) - EventNotWatching = EventType(-2) + EventSession EventType = -1 + EventNotWatching EventType = -2 ) var ( @@ -54,14 +55,13 @@ var ( ) const ( - StateUnknown = State(-1) - StateDisconnected = State(0) - StateConnecting = State(1) - StateAuthFailed = State(4) - StateConnectedReadOnly = State(5) - StateSaslAuthenticated = State(6) - StateExpired = State(-112) - // StateAuthFailed = State(-113) + StateUnknown State = -1 + StateDisconnected State = 0 + StateConnecting State = 1 + StateAuthFailed State = 4 + StateConnectedReadOnly State = 5 + StateSaslAuthenticated State = 6 + StateExpired State = -112 StateConnected = State(100) StateHasSession = State(101) @@ -154,20 +154,20 @@ const ( errBadArguments = -8 errInvalidState = -9 // API errors - errAPIError = ErrCode(-100) - errNoNode = ErrCode(-101) // * - errNoAuth = ErrCode(-102) - errBadVersion = ErrCode(-103) // * - errNoChildrenForEphemerals = ErrCode(-108) - errNodeExists = ErrCode(-110) // * - errNotEmpty = ErrCode(-111) - errSessionExpired = ErrCode(-112) - errInvalidCallback = ErrCode(-113) - errInvalidAcl = ErrCode(-114) - errAuthFailed = ErrCode(-115) - errClosing = ErrCode(-116) - errNothing = ErrCode(-117) - errSessionMoved = ErrCode(-118) + errAPIError ErrCode = -100 + errNoNode ErrCode = -101 // * + errNoAuth ErrCode = -102 + errBadVersion ErrCode = -103 // * + errNoChildrenForEphemerals ErrCode = -108 + errNodeExists ErrCode = -110 // * + errNotEmpty ErrCode = -111 + errSessionExpired ErrCode = -112 + errInvalidCallback ErrCode = -113 + errInvalidAcl ErrCode = -114 + errAuthFailed ErrCode = -115 + errClosing ErrCode = -116 + errNothing ErrCode = -117 + errSessionMoved ErrCode = -118 ) // Constants for ACL permissions diff --git a/vendor/github.com/samuel/go-zookeeper/zk/dnshostprovider.go b/vendor/github.com/samuel/go-zookeeper/zk/dnshostprovider.go new file mode 100644 index 0000000000..f4bba8d0b5 --- /dev/null +++ b/vendor/github.com/samuel/go-zookeeper/zk/dnshostprovider.go @@ -0,0 +1,88 @@ +package zk + +import ( + "fmt" + "net" + "sync" +) + +// DNSHostProvider is the default HostProvider. It currently matches +// the Java StaticHostProvider, resolving hosts from DNS once during +// the call to Init. It could be easily extended to re-query DNS +// periodically or if there is trouble connecting. +type DNSHostProvider struct { + mu sync.Mutex // Protects everything, so we can add asynchronous updates later. + servers []string + curr int + last int + lookupHost func(string) ([]string, error) // Override of net.LookupHost, for testing. +} + +// Init is called first, with the servers specified in the connection +// string. It uses DNS to look up addresses for each server, then +// shuffles them all together. +func (hp *DNSHostProvider) Init(servers []string) error { + hp.mu.Lock() + defer hp.mu.Unlock() + + lookupHost := hp.lookupHost + if lookupHost == nil { + lookupHost = net.LookupHost + } + + found := []string{} + for _, server := range servers { + host, port, err := net.SplitHostPort(server) + if err != nil { + return err + } + addrs, err := lookupHost(host) + if err != nil { + return err + } + for _, addr := range addrs { + found = append(found, net.JoinHostPort(addr, port)) + } + } + + if len(found) == 0 { + return fmt.Errorf("No hosts found for addresses %q", servers) + } + + // Randomize the order of the servers to avoid creating hotspots + stringShuffle(found) + + hp.servers = found + hp.curr = -1 + hp.last = -1 + + return nil +} + +// Len returns the number of servers available +func (hp *DNSHostProvider) Len() int { + hp.mu.Lock() + defer hp.mu.Unlock() + return len(hp.servers) +} + +// Next returns the next server to connect to. retryStart will be true +// if we've looped through all known servers without Connected() being +// called. +func (hp *DNSHostProvider) Next() (server string, retryStart bool) { + hp.mu.Lock() + defer hp.mu.Unlock() + hp.curr = (hp.curr + 1) % len(hp.servers) + retryStart = hp.curr == hp.last + if hp.last == -1 { + hp.last = 0 + } + return hp.servers[hp.curr], retryStart +} + +// Connected notifies the HostProvider of a successful connection. +func (hp *DNSHostProvider) Connected() { + hp.mu.Lock() + defer hp.mu.Unlock() + hp.last = hp.curr +} diff --git a/vendor/github.com/samuel/go-zookeeper/zk/flw.go b/vendor/github.com/samuel/go-zookeeper/zk/flw.go index 1045c98cfd..3e97f96876 100644 --- a/vendor/github.com/samuel/go-zookeeper/zk/flw.go +++ b/vendor/github.com/samuel/go-zookeeper/zk/flw.go @@ -5,10 +5,10 @@ import ( "bytes" "fmt" "io/ioutil" - "math/big" "net" "regexp" "strconv" + "strings" "time" ) @@ -22,7 +22,7 @@ import ( // which server had the issue. func FLWSrvr(servers []string, timeout time.Duration) ([]*ServerStats, bool) { // different parts of the regular expression that are required to parse the srvr output - var ( + const ( zrVer = `^Zookeeper version: ([A-Za-z0-9\.\-]+), built on (\d\d/\d\d/\d\d\d\d \d\d:\d\d [A-Za-z0-9:\+\-]+)` zrLat = `^Latency min/avg/max: (\d+)/(\d+)/(\d+)` zrNet = `^Received: (\d+).*\n^Sent: (\d+).*\n^Connections: (\d+).*\n^Outstanding: (\d+)` @@ -31,7 +31,6 @@ func FLWSrvr(servers []string, timeout time.Duration) ([]*ServerStats, bool) { // build the regex from the pieces above re, err := regexp.Compile(fmt.Sprintf(`(?m:\A%v.*\n%v.*\n%v.*\n%v)`, zrVer, zrLat, zrNet, zrState)) - if err != nil { return nil, false } @@ -152,14 +151,13 @@ func FLWRuok(servers []string, timeout time.Duration) []bool { // As with FLWSrvr, the boolean value indicates whether one of the requests had // an issue. The Clients struct has an Error value that can be checked. func FLWCons(servers []string, timeout time.Duration) ([]*ServerClients, bool) { - var ( + const ( zrAddr = `^ /((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?):(?:\d+))\[\d+\]` zrPac = `\(queued=(\d+),recved=(\d+),sent=(\d+),sid=(0x[A-Za-z0-9]+),lop=(\w+),est=(\d+),to=(\d+),` zrSesh = `lcxid=(0x[A-Za-z0-9]+),lzxid=(0x[A-Za-z0-9]+),lresp=(\d+),llat=(\d+),minlat=(\d+),avglat=(\d+),maxlat=(\d+)\)` ) re, err := regexp.Compile(fmt.Sprintf("%v%v%v", zrAddr, zrPac, zrSesh)) - if err != nil { return nil, false } @@ -205,41 +203,21 @@ func FLWCons(servers []string, timeout time.Duration) ([]*ServerClients, bool) { sid, _ := strconv.ParseInt(match[4], 0, 64) est, _ := strconv.ParseInt(match[6], 0, 64) timeout, _ := strconv.ParseInt(match[7], 0, 32) + lcxid, _ := parseInt64(match[8]) + lzxid, _ := parseInt64(match[9]) lresp, _ := strconv.ParseInt(match[10], 0, 64) llat, _ := strconv.ParseInt(match[11], 0, 32) minlat, _ := strconv.ParseInt(match[12], 0, 32) avglat, _ := strconv.ParseInt(match[13], 0, 32) maxlat, _ := strconv.ParseInt(match[14], 0, 32) - // zookeeper returns a value, '0xffffffffffffffff', as the - // Lzxid for PING requests in the 'cons' output. - // unfortunately, in Go that is an invalid int64 and is not represented - // as -1. - // However, converting the string value to a big.Int and then back to - // and int64 properly sets the value to -1 - lzxid, ok := new(big.Int).SetString(match[9], 0) - - var errVal error - - if !ok { - errVal = fmt.Errorf("failed to convert lzxid value to big.Int") - imOk = false - } - - lcxid, ok := new(big.Int).SetString(match[8], 0) - - if !ok && errVal == nil { - errVal = fmt.Errorf("failed to convert lcxid value to big.Int") - imOk = false - } - clients = append(clients, &ServerClient{ Queued: queued, Received: recvd, Sent: sent, SessionID: sid, - Lcxid: lcxid.Int64(), - Lzxid: lzxid.Int64(), + Lcxid: int64(lcxid), + Lzxid: int64(lzxid), Timeout: int32(timeout), LastLatency: int32(llat), MinLatency: int32(minlat), @@ -249,7 +227,6 @@ func FLWCons(servers []string, timeout time.Duration) ([]*ServerClients, bool) { LastResponse: time.Unix(lresp, 0), Addr: match[0], LastOperation: match[5], - Error: errVal, }) } @@ -259,9 +236,17 @@ func FLWCons(servers []string, timeout time.Duration) ([]*ServerClients, bool) { return sc, imOk } +// parseInt64 is similar to strconv.ParseInt, but it also handles hex values that represent negative numbers +func parseInt64(s string) (int64, error) { + if strings.HasPrefix(s, "0x") { + i, err := strconv.ParseUint(s, 0, 64) + return int64(i), err + } + return strconv.ParseInt(s, 0, 64) +} + func fourLetterWord(server, command string, timeout time.Duration) ([]byte, error) { conn, err := net.DialTimeout("tcp", server, timeout) - if err != nil { return nil, err } @@ -271,20 +256,11 @@ func fourLetterWord(server, command string, timeout time.Duration) ([]byte, erro defer conn.Close() conn.SetWriteDeadline(time.Now().Add(timeout)) - _, err = conn.Write([]byte(command)) - if err != nil { return nil, err } conn.SetReadDeadline(time.Now().Add(timeout)) - - resp, err := ioutil.ReadAll(conn) - - if err != nil { - return nil, err - } - - return resp, nil + return ioutil.ReadAll(conn) } diff --git a/vendor/github.com/samuel/go-zookeeper/zk/lock.go b/vendor/github.com/samuel/go-zookeeper/zk/lock.go index f13a8b0ba6..3c35a427c8 100644 --- a/vendor/github.com/samuel/go-zookeeper/zk/lock.go +++ b/vendor/github.com/samuel/go-zookeeper/zk/lock.go @@ -58,8 +58,16 @@ func (l *Lock) Lock() error { parts := strings.Split(l.path, "/") pth := "" for _, p := range parts[1:] { + var exists bool pth += "/" + p - _, err := l.c.Create(pth, []byte{}, 0, l.acl) + exists, _, err = l.c.Exists(pth) + if err != nil { + return err + } + if exists == true { + continue + } + _, err = l.c.Create(pth, []byte{}, 0, l.acl) if err != nil && err != ErrNodeExists { return err } @@ -86,7 +94,7 @@ func (l *Lock) Lock() error { } lowestSeq := seq - prevSeq := 0 + prevSeq := -1 prevSeqPath := "" for _, p := range children { s, err := parseSeq(p) diff --git a/vendor/github.com/samuel/go-zookeeper/zk/server_help.go b/vendor/github.com/samuel/go-zookeeper/zk/server_help.go index 4a53772bde..3663064cae 100644 --- a/vendor/github.com/samuel/go-zookeeper/zk/server_help.go +++ b/vendor/github.com/samuel/go-zookeeper/zk/server_help.go @@ -7,9 +7,14 @@ import ( "math/rand" "os" "path/filepath" + "strings" "time" ) +func init() { + rand.Seed(time.Now().UnixNano()) +} + type TestServer struct { Port int Path string @@ -87,33 +92,125 @@ func StartTestCluster(size int, stdout, stderr io.Writer) (*TestCluster, error) Srv: srv, }) } + if err := cluster.waitForStart(10, time.Second); err != nil { + return nil, err + } success = true - time.Sleep(time.Second) // Give the server time to become active. Should probably actually attempt to connect to verify. return cluster, nil } -func (ts *TestCluster) Connect(idx int) (*Conn, error) { - zk, _, err := Connect([]string{fmt.Sprintf("127.0.0.1:%d", ts.Servers[idx].Port)}, time.Second*15) +func (tc *TestCluster) Connect(idx int) (*Conn, error) { + zk, _, err := Connect([]string{fmt.Sprintf("127.0.0.1:%d", tc.Servers[idx].Port)}, time.Second*15) return zk, err } -func (ts *TestCluster) ConnectAll() (*Conn, <-chan Event, error) { - return ts.ConnectAllTimeout(time.Second * 15) +func (tc *TestCluster) ConnectAll() (*Conn, <-chan Event, error) { + return tc.ConnectAllTimeout(time.Second * 15) } -func (ts *TestCluster) ConnectAllTimeout(sessionTimeout time.Duration) (*Conn, <-chan Event, error) { - hosts := make([]string, len(ts.Servers)) - for i, srv := range ts.Servers { +func (tc *TestCluster) ConnectAllTimeout(sessionTimeout time.Duration) (*Conn, <-chan Event, error) { + return tc.ConnectWithOptions(sessionTimeout) +} + +func (tc *TestCluster) ConnectWithOptions(sessionTimeout time.Duration, options ...connOption) (*Conn, <-chan Event, error) { + hosts := make([]string, len(tc.Servers)) + for i, srv := range tc.Servers { hosts[i] = fmt.Sprintf("127.0.0.1:%d", srv.Port) } - zk, ch, err := Connect(hosts, sessionTimeout) + zk, ch, err := Connect(hosts, sessionTimeout, options...) return zk, ch, err } -func (ts *TestCluster) Stop() error { - for _, srv := range ts.Servers { +func (tc *TestCluster) Stop() error { + for _, srv := range tc.Servers { srv.Srv.Stop() } - defer os.RemoveAll(ts.Path) + defer os.RemoveAll(tc.Path) + return tc.waitForStop(5, time.Second) +} + +// waitForStart blocks until the cluster is up +func (tc *TestCluster) waitForStart(maxRetry int, interval time.Duration) error { + // verify that the servers are up with SRVR + serverAddrs := make([]string, len(tc.Servers)) + for i, s := range tc.Servers { + serverAddrs[i] = fmt.Sprintf("127.0.0.1:%d", s.Port) + } + + for i := 0; i < maxRetry; i++ { + _, ok := FLWSrvr(serverAddrs, time.Second) + if ok { + return nil + } + time.Sleep(interval) + } + return fmt.Errorf("unable to verify health of servers") +} + +// waitForStop blocks until the cluster is down +func (tc *TestCluster) waitForStop(maxRetry int, interval time.Duration) error { + // verify that the servers are up with RUOK + serverAddrs := make([]string, len(tc.Servers)) + for i, s := range tc.Servers { + serverAddrs[i] = fmt.Sprintf("127.0.0.1:%d", s.Port) + } + + var success bool + for i := 0; i < maxRetry && !success; i++ { + success = true + for _, ok := range FLWRuok(serverAddrs, time.Second) { + if ok { + success = false + } + } + if !success { + time.Sleep(interval) + } + } + if !success { + return fmt.Errorf("unable to verify servers are down") + } + return nil +} + +func (tc *TestCluster) StartServer(server string) { + for _, s := range tc.Servers { + if strings.HasSuffix(server, fmt.Sprintf(":%d", s.Port)) { + s.Srv.Start() + return + } + } + panic(fmt.Sprintf("Unknown server: %s", server)) +} + +func (tc *TestCluster) StopServer(server string) { + for _, s := range tc.Servers { + if strings.HasSuffix(server, fmt.Sprintf(":%d", s.Port)) { + s.Srv.Stop() + return + } + } + panic(fmt.Sprintf("Unknown server: %s", server)) +} + +func (tc *TestCluster) StartAllServers() error { + for _, s := range tc.Servers { + if err := s.Srv.Start(); err != nil { + return fmt.Errorf( + "Failed to start server listening on port `%d` : %+v", s.Port, err) + } + } + + return nil +} + +func (tc *TestCluster) StopAllServers() error { + for _, s := range tc.Servers { + if err := s.Srv.Stop(); err != nil { + return fmt.Errorf( + "Failed to stop server listening on port `%d` : %+v", s.Port, err) + } + } + return nil } diff --git a/vendor/github.com/samuel/go-zookeeper/zk/structs.go b/vendor/github.com/samuel/go-zookeeper/zk/structs.go index 8fbc069ee1..d4af27deaa 100644 --- a/vendor/github.com/samuel/go-zookeeper/zk/structs.go +++ b/vendor/github.com/samuel/go-zookeeper/zk/structs.go @@ -270,6 +270,7 @@ type multiResponseOp struct { Header multiHeader String string Stat *Stat + Err ErrCode } type multiResponse struct { Ops []multiResponseOp @@ -327,6 +328,8 @@ func (r *multiRequest) Decode(buf []byte) (int, error) { } func (r *multiResponse) Decode(buf []byte) (int, error) { + var multiErr error + r.Ops = make([]multiResponseOp, 0) r.DoneHeader = multiHeader{-1, true, -1} total := 0 @@ -347,6 +350,8 @@ func (r *multiResponse) Decode(buf []byte) (int, error) { switch header.Type { default: return total, ErrAPIError + case opError: + w = reflect.ValueOf(&res.Err) case opCreate: w = reflect.ValueOf(&res.String) case opSetData: @@ -362,8 +367,12 @@ func (r *multiResponse) Decode(buf []byte) (int, error) { total += n } r.Ops = append(r.Ops, res) + if multiErr == nil && res.Err != errOk { + // Use the first error as the error returned from Multi(). + multiErr = res.Err.toError() + } } - return total, nil + return total, multiErr } type watcherEvent struct { @@ -598,43 +607,3 @@ func requestStructForOp(op int32) interface{} { } return nil } - -func responseStructForOp(op int32) interface{} { - switch op { - case opClose: - return &closeResponse{} - case opCreate: - return &createResponse{} - case opDelete: - return &deleteResponse{} - case opExists: - return &existsResponse{} - case opGetAcl: - return &getAclResponse{} - case opGetChildren: - return &getChildrenResponse{} - case opGetChildren2: - return &getChildren2Response{} - case opGetData: - return &getDataResponse{} - case opPing: - return &pingResponse{} - case opSetAcl: - return &setAclResponse{} - case opSetData: - return &setDataResponse{} - case opSetWatches: - return &setWatchesResponse{} - case opSync: - return &syncResponse{} - case opWatcherEvent: - return &watcherEvent{} - case opSetAuth: - return &setAuthResponse{} - // case opCheck: - // return &checkVersionResponse{} - case opMulti: - return &multiResponse{} - } - return nil -} diff --git a/vendor/github.com/samuel/go-zookeeper/zk/tracer.go b/vendor/github.com/samuel/go-zookeeper/zk/tracer.go deleted file mode 100644 index 7af2e96bbc..0000000000 --- a/vendor/github.com/samuel/go-zookeeper/zk/tracer.go +++ /dev/null @@ -1,148 +0,0 @@ -package zk - -import ( - "encoding/binary" - "fmt" - "io" - "net" - "sync" -) - -var ( - requests = make(map[int32]int32) // Map of Xid -> Opcode - requestsLock = &sync.Mutex{} -) - -func trace(conn1, conn2 net.Conn, client bool) { - defer conn1.Close() - defer conn2.Close() - buf := make([]byte, 10*1024) - init := true - for { - _, err := io.ReadFull(conn1, buf[:4]) - if err != nil { - fmt.Println("1>", client, err) - return - } - - blen := int(binary.BigEndian.Uint32(buf[:4])) - - _, err = io.ReadFull(conn1, buf[4:4+blen]) - if err != nil { - fmt.Println("2>", client, err) - return - } - - var cr interface{} - opcode := int32(-1) - readHeader := true - if client { - if init { - cr = &connectRequest{} - readHeader = false - } else { - xid := int32(binary.BigEndian.Uint32(buf[4:8])) - opcode = int32(binary.BigEndian.Uint32(buf[8:12])) - requestsLock.Lock() - requests[xid] = opcode - requestsLock.Unlock() - cr = requestStructForOp(opcode) - if cr == nil { - fmt.Printf("Unknown opcode %d\n", opcode) - } - } - } else { - if init { - cr = &connectResponse{} - readHeader = false - } else { - xid := int32(binary.BigEndian.Uint32(buf[4:8])) - zxid := int64(binary.BigEndian.Uint64(buf[8:16])) - errnum := int32(binary.BigEndian.Uint32(buf[16:20])) - if xid != -1 || zxid != -1 { - requestsLock.Lock() - found := false - opcode, found = requests[xid] - if !found { - opcode = 0 - } - delete(requests, xid) - requestsLock.Unlock() - } else { - opcode = opWatcherEvent - } - cr = responseStructForOp(opcode) - if cr == nil { - fmt.Printf("Unknown opcode %d\n", opcode) - } - if errnum != 0 { - cr = &struct{}{} - } - } - } - opname := "." - if opcode != -1 { - opname = opNames[opcode] - } - if cr == nil { - fmt.Printf("%+v %s %+v\n", client, opname, buf[4:4+blen]) - } else { - n := 4 - hdrStr := "" - if readHeader { - var hdr interface{} - if client { - hdr = &requestHeader{} - } else { - hdr = &responseHeader{} - } - if n2, err := decodePacket(buf[n:n+blen], hdr); err != nil { - fmt.Println(err) - } else { - n += n2 - } - hdrStr = fmt.Sprintf(" %+v", hdr) - } - if _, err := decodePacket(buf[n:n+blen], cr); err != nil { - fmt.Println(err) - } - fmt.Printf("%+v %s%s %+v\n", client, opname, hdrStr, cr) - } - - init = false - - written, err := conn2.Write(buf[:4+blen]) - if err != nil { - fmt.Println("3>", client, err) - return - } else if written != 4+blen { - fmt.Printf("Written != read: %d != %d\n", written, blen) - return - } - } -} - -func handleConnection(addr string, conn net.Conn) { - zkConn, err := net.Dial("tcp", addr) - if err != nil { - fmt.Println(err) - return - } - go trace(conn, zkConn, true) - trace(zkConn, conn, false) -} - -func StartTracer(listenAddr, serverAddr string) { - ln, err := net.Listen("tcp", listenAddr) - if err != nil { - panic(err) - } - for { - conn, err := ln.Accept() - if err != nil { - fmt.Println(err) - continue - } - go handleConnection(serverAddr, conn) - } -} diff --git a/vendor/github.com/stretchr/testify/suite/doc.go b/vendor/github.com/stretchr/testify/suite/doc.go new file mode 100644 index 0000000000..f91a245d3f --- /dev/null +++ b/vendor/github.com/stretchr/testify/suite/doc.go @@ -0,0 +1,65 @@ +// Package suite contains logic for creating testing suite structs +// and running the methods on those structs as tests. The most useful +// piece of this package is that you can create setup/teardown methods +// on your testing suites, which will run before/after the whole suite +// or individual tests (depending on which interface(s) you +// implement). +// +// A testing suite is usually built by first extending the built-in +// suite functionality from suite.Suite in testify. Alternatively, +// you could reproduce that logic on your own if you wanted (you +// just need to implement the TestingSuite interface from +// suite/interfaces.go). +// +// After that, you can implement any of the interfaces in +// suite/interfaces.go to add setup/teardown functionality to your +// suite, and add any methods that start with "Test" to add tests. +// Methods that do not match any suite interfaces and do not begin +// with "Test" will not be run by testify, and can safely be used as +// helper methods. +// +// Once you've built your testing suite, you need to run the suite +// (using suite.Run from testify) inside any function that matches the +// identity that "go test" is already looking for (i.e. +// func(*testing.T)). +// +// Regular expression to select test suites specified command-line +// argument "-run". Regular expression to select the methods +// of test suites specified command-line argument "-m". +// Suite object has assertion methods. +// +// A crude example: +// // Basic imports +// import ( +// "testing" +// "github.com/stretchr/testify/assert" +// "github.com/stretchr/testify/suite" +// ) +// +// // Define the suite, and absorb the built-in basic suite +// // functionality from testify - including a T() method which +// // returns the current testing context +// type ExampleTestSuite struct { +// suite.Suite +// VariableThatShouldStartAtFive int +// } +// +// // Make sure that VariableThatShouldStartAtFive is set to five +// // before each test +// func (suite *ExampleTestSuite) SetupTest() { +// suite.VariableThatShouldStartAtFive = 5 +// } +// +// // All methods that begin with "Test" are run as tests within a +// // suite. +// func (suite *ExampleTestSuite) TestExample() { +// assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive) +// suite.Equal(5, suite.VariableThatShouldStartAtFive) +// } +// +// // In order for 'go test' to run this suite, we need to create +// // a normal test function and pass our suite to suite.Run +// func TestExampleTestSuite(t *testing.T) { +// suite.Run(t, new(ExampleTestSuite)) +// } +package suite diff --git a/vendor/github.com/stretchr/testify/suite/interfaces.go b/vendor/github.com/stretchr/testify/suite/interfaces.go new file mode 100644 index 0000000000..b37cb04098 --- /dev/null +++ b/vendor/github.com/stretchr/testify/suite/interfaces.go @@ -0,0 +1,46 @@ +package suite + +import "testing" + +// TestingSuite can store and return the current *testing.T context +// generated by 'go test'. +type TestingSuite interface { + T() *testing.T + SetT(*testing.T) +} + +// SetupAllSuite has a SetupSuite method, which will run before the +// tests in the suite are run. +type SetupAllSuite interface { + SetupSuite() +} + +// SetupTestSuite has a SetupTest method, which will run before each +// test in the suite. +type SetupTestSuite interface { + SetupTest() +} + +// TearDownAllSuite has a TearDownSuite method, which will run after +// all the tests in the suite have been run. +type TearDownAllSuite interface { + TearDownSuite() +} + +// TearDownTestSuite has a TearDownTest method, which will run after +// each test in the suite. +type TearDownTestSuite interface { + TearDownTest() +} + +// BeforeTest has a function to be executed right before the test +// starts and receives the suite and test names as input +type BeforeTest interface { + BeforeTest(suiteName, testName string) +} + +// AfterTest has a function to be executed right after the test +// finishes and receives the suite and test names as input +type AfterTest interface { + AfterTest(suiteName, testName string) +} diff --git a/vendor/github.com/stretchr/testify/suite/suite.go b/vendor/github.com/stretchr/testify/suite/suite.go new file mode 100644 index 0000000000..991d3be80b --- /dev/null +++ b/vendor/github.com/stretchr/testify/suite/suite.go @@ -0,0 +1,121 @@ +package suite + +import ( + "flag" + "fmt" + "os" + "reflect" + "regexp" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var matchMethod = flag.String("testify.m", "", "regular expression to select tests of the testify suite to run") + +// Suite is a basic testing suite with methods for storing and +// retrieving the current *testing.T context. +type Suite struct { + *assert.Assertions + require *require.Assertions + t *testing.T +} + +// T retrieves the current *testing.T context. +func (suite *Suite) T() *testing.T { + return suite.t +} + +// SetT sets the current *testing.T context. +func (suite *Suite) SetT(t *testing.T) { + suite.t = t + suite.Assertions = assert.New(t) + suite.require = require.New(t) +} + +// Require returns a require context for suite. +func (suite *Suite) Require() *require.Assertions { + if suite.require == nil { + suite.require = require.New(suite.T()) + } + return suite.require +} + +// Assert returns an assert context for suite. Normally, you can call +// `suite.NoError(expected, actual)`, but for situations where the embedded +// methods are overridden (for example, you might want to override +// assert.Assertions with require.Assertions), this method is provided so you +// can call `suite.Assert().NoError()`. +func (suite *Suite) Assert() *assert.Assertions { + if suite.Assertions == nil { + suite.Assertions = assert.New(suite.T()) + } + return suite.Assertions +} + +// Run takes a testing suite and runs all of the tests attached +// to it. +func Run(t *testing.T, suite TestingSuite) { + suite.SetT(t) + + if setupAllSuite, ok := suite.(SetupAllSuite); ok { + setupAllSuite.SetupSuite() + } + defer func() { + if tearDownAllSuite, ok := suite.(TearDownAllSuite); ok { + tearDownAllSuite.TearDownSuite() + } + }() + + methodFinder := reflect.TypeOf(suite) + tests := []testing.InternalTest{} + for index := 0; index < methodFinder.NumMethod(); index++ { + method := methodFinder.Method(index) + ok, err := methodFilter(method.Name) + if err != nil { + fmt.Fprintf(os.Stderr, "testify: invalid regexp for -m: %s\n", err) + os.Exit(1) + } + if ok { + test := testing.InternalTest{ + Name: method.Name, + F: func(t *testing.T) { + parentT := suite.T() + suite.SetT(t) + if setupTestSuite, ok := suite.(SetupTestSuite); ok { + setupTestSuite.SetupTest() + } + if beforeTestSuite, ok := suite.(BeforeTest); ok { + beforeTestSuite.BeforeTest(methodFinder.Elem().Name(), method.Name) + } + defer func() { + if afterTestSuite, ok := suite.(AfterTest); ok { + afterTestSuite.AfterTest(methodFinder.Elem().Name(), method.Name) + } + if tearDownTestSuite, ok := suite.(TearDownTestSuite); ok { + tearDownTestSuite.TearDownTest() + } + suite.SetT(parentT) + }() + method.Func.Call([]reflect.Value{reflect.ValueOf(suite)}) + }, + } + tests = append(tests, test) + } + } + + if !testing.RunTests(func(_, _ string) (bool, error) { return true, nil }, + tests) { + t.Fail() + } +} + +// Filtering method according to set regular expression +// specified command-line argument -m +func methodFilter(name string) (bool, error) { + if ok, _ := regexp.MatchString("^Test", name); !ok { + return false, nil + } + return regexp.MatchString(*matchMethod, name) +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 0a58483ebc..646c122bde 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -465,6 +465,90 @@ "revision": "c589d0c9f0d81640c518354c7bcae77d99820aa3", "revisionTime": "2016-09-30T00:14:02Z" }, + { + "checksumSHA1": "cuTQkSEiIoMhDhB7xCbYDwDhrgw=", + "path": "github.com/gophercloud/gophercloud", + "revision": "0bf921da554eacc1552a70204be7a1201937c1e1", + "revisionTime": "2017-05-04T01:40:32Z" + }, + { + "checksumSHA1": "0KdIjTH5IO8hlIl8kdfI6313GiY=", + "path": "github.com/gophercloud/gophercloud/openstack", + "revision": "0bf921da554eacc1552a70204be7a1201937c1e1", + "revisionTime": "2017-05-04T01:40:32Z" + }, + { + "checksumSHA1": "4XWDCGMYqipwJymi9xJo9UffD7g=", + "path": "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions", + "revision": "caa61b3ca9f504196fd3b338f43cd99d830f7e2e", + "revisionTime": "2017-05-11T18:09:16Z" + }, + { + "checksumSHA1": "e7AW3YDVYJPKUjpqsB4AL9RRlTw=", + "path": "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips", + "revision": "caa61b3ca9f504196fd3b338f43cd99d830f7e2e", + "revisionTime": "2017-05-11T18:09:16Z" + }, + { + "checksumSHA1": "S1BV3o8Pa0aM5RaUuRYXY7LnPIc=", + "path": "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors", + "revision": "0bf921da554eacc1552a70204be7a1201937c1e1", + "revisionTime": "2017-05-04T01:40:32Z" + }, + { + "checksumSHA1": "Rnzx2YgOD41k8KoPA08tR992PxQ=", + "path": "github.com/gophercloud/gophercloud/openstack/compute/v2/images", + "revision": "0bf921da554eacc1552a70204be7a1201937c1e1", + "revisionTime": "2017-05-04T01:40:32Z" + }, + { + "checksumSHA1": "IjCvcaNnRW++hclt21WUkMYinaA=", + "path": "github.com/gophercloud/gophercloud/openstack/compute/v2/servers", + "revision": "0bf921da554eacc1552a70204be7a1201937c1e1", + "revisionTime": "2017-05-04T01:40:32Z" + }, + { + "checksumSHA1": "9z5GwbsfpB49gzkHu10pDH5roKA=", + "path": "github.com/gophercloud/gophercloud/openstack/identity/v2", + "revision": "0bf921da554eacc1552a70204be7a1201937c1e1", + "revisionTime": "2017-05-04T01:40:32Z" + }, + { + "checksumSHA1": "1sVqsZBZBNhDXLY9XzjMkcOkcbg=", + "path": "github.com/gophercloud/gophercloud/openstack/identity/v2/tenants", + "revision": "0bf921da554eacc1552a70204be7a1201937c1e1", + "revisionTime": "2017-05-04T01:40:32Z" + }, + { + "checksumSHA1": "AvUU5En9YpG25iLlcAPDgcQODjI=", + "path": "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens", + "revision": "0bf921da554eacc1552a70204be7a1201937c1e1", + "revisionTime": "2017-05-04T01:40:32Z" + }, + { + "checksumSHA1": "vsgZmEVQLtCmxuxf/q4u8XJGWpE=", + "path": "github.com/gophercloud/gophercloud/openstack/identity/v3", + "revision": "0bf921da554eacc1552a70204be7a1201937c1e1", + "revisionTime": "2017-05-04T01:40:32Z" + }, + { + "checksumSHA1": "ZKyEbJuIlvuZ9aUushINCXJHF4w=", + "path": "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens", + "revision": "0bf921da554eacc1552a70204be7a1201937c1e1", + "revisionTime": "2017-05-04T01:40:32Z" + }, + { + "checksumSHA1": "TDOZnaS0TO0NirpxV1QwPerAQTY=", + "path": "github.com/gophercloud/gophercloud/openstack/utils", + "revision": "0bf921da554eacc1552a70204be7a1201937c1e1", + "revisionTime": "2017-05-04T01:40:32Z" + }, + { + "checksumSHA1": "FNy075ydQZXvnL2bNNIOCmy/ghs=", + "path": "github.com/gophercloud/gophercloud/pagination", + "revision": "0bf921da554eacc1552a70204be7a1201937c1e1", + "revisionTime": "2017-05-04T01:40:32Z" + }, { "checksumSHA1": "LclVLJYrBi03PBjsVPpgoMbUDQ8=", "path": "github.com/hashicorp/consul/api", @@ -685,10 +769,10 @@ "revisionTime": "2017-05-22T06:49:09Z" }, { - "checksumSHA1": "+49Vr4Me28p3cR+gxX5SUQHbbas=", + "checksumSHA1": "5SYLEhADhdBVZAGPVHWggQl7H8k=", "path": "github.com/samuel/go-zookeeper/zk", - "revision": "177002e16a0061912f02377e2dd8951a8b3551bc", - "revisionTime": "2015-08-17T10:50:50-07:00" + "revision": "1d7be4effb13d2d908342d349d71a284a7542693", + "revisionTime": "2016-10-28T23:23:40Z" }, { "checksumSHA1": "YuPBOVkkE3uuBh4RcRUTF0n+frs=", diff --git a/web/ui/bindata.go b/web/ui/bindata.go index bf3e5c316d..7ea35f6d1c 100644 --- a/web/ui/bindata.go +++ b/web/ui/bindata.go @@ -141,7 +141,7 @@ func webUiTemplatesAlertsHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "web/ui/templates/alerts.html", size: 1832, mode: os.FileMode(420), modTime: time.Unix(1495632767, 0)} + info := bindataFileInfo{name: "web/ui/templates/alerts.html", size: 1832, mode: os.FileMode(420), modTime: time.Unix(1496733728, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -261,7 +261,7 @@ func webUiTemplatesTargetsHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "web/ui/templates/targets.html", size: 2275, mode: os.FileMode(420), modTime: time.Unix(1495632767, 0)} + info := bindataFileInfo{name: "web/ui/templates/targets.html", size: 2275, mode: os.FileMode(420), modTime: time.Unix(1496733728, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -406,7 +406,7 @@ func webUiStaticJsAlertsJs() (*asset, error) { return a, nil } -var _webUiStaticJsGraphJs = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xe4\x7d\x6b\x77\xdb\x38\xb2\xe0\x77\xff\x8a\x0a\x3b\x27\xa2\xda\x32\x65\xa7\x6f\xf7\xde\x91\x2d\xf7\xa6\xf3\x98\x64\x26\xaf\x71\xdc\xaf\xe3\x78\x7c\x20\x12\x12\x11\x53\x24\x07\x00\x6d\xab\x13\xfd\xf7\x3d\x28\x00\x24\x40\x52\x96\xba\x7b\x76\xce\xee\xb9\xfe\x20\x99\x78\x14\x0a\x55\x85\x42\xa1\xaa\x40\xdd\x10\x0e\xef\x79\xb1\xa4\x32\xa5\x95\x80\xa9\xfb\xf0\xe5\x0b\x7c\x5e\x1f\xef\xa9\x26\x0b\x4e\xca\xf4\x9c\x2e\xcb\x8c\x48\x7a\xbc\x87\x65\x1f\x9e\x3f\x7d\xf7\xf6\x19\x4c\xe1\xe8\xf0\xf0\xf0\x78\x6f\xaf\xe9\x19\xfd\x55\x35\x87\x29\xcc\xab\x3c\x96\xac\xc8\x43\x9a\xd1\x25\xcd\xe5\x08\x8a\x52\x3d\x8b\x11\xa4\x24\x4f\x32\xfa\x34\x25\xf9\x82\xda\xa7\x33\xba\x2c\x6e\xe8\x10\x3e\xef\x01\xc8\x94\x89\x88\x66\x30\x05\xd3\xf7\xd8\x16\x22\x2e\x2f\xcf\xdf\xbc\x86\x29\xe4\x55\x96\xd5\x15\x06\x36\x4c\xed\x28\x75\x8d\x3b\x18\x4c\xbd\xb1\x5b\x6d\x34\x0a\x2e\xea\x1a\x1d\xf0\x50\x0c\x55\x8f\xa1\xea\xba\xae\xfb\x73\x16\x5f\x8b\x94\xdc\xda\xb9\x7b\xa8\x25\x44\x12\x98\xc2\xc5\xe5\xf1\x9e\x2d\x62\x39\x93\x8c\x64\xec\x37\x1a\x0e\x8f\xf7\xd6\x3d\x04\x8c\x24\x5b\xd2\x17\x24\x96\x05\x57\x93\x52\x68\x04\xab\x60\x02\xdf\x1d\xc2\xd7\xfa\xe3\xf1\x7f\xc1\xd7\xf0\xcd\x77\xdf\x8e\x54\xd5\x6d\xb7\xea\x7f\x61\x45\xd2\xaa\xc0\xc2\xb4\x29\xc4\xe7\x25\x3e\xe3\xbf\x22\x98\xc0\x51\x3f\x46\x42\xd2\xf2\x27\x92\x55\x54\x21\x74\xa1\x1a\x1f\x89\x60\x04\xc1\xd1\xa1\xfe\x5a\xaa\xcf\x6f\xf1\xf3\x48\x7f\x7d\x73\xa8\x9f\x52\xf5\xf9\x18\x3f\xbf\xc3\xcf\x23\xfd\x70\x94\x60\x45\x12\xe0\xd0\x47\xb7\xf8\x84\x9f\xff\x85\x9f\xff\x8d\x9f\x47\x2b\x2c\x5f\x05\x7b\x97\x7d\x68\xe5\xd5\x12\xff\x51\x58\xf5\x89\x62\x54\xf2\x42\x16\x72\x55\x52\x87\xec\x5d\x26\x2b\xa9\x16\x34\x9b\xc3\x14\x59\xa4\xb8\xa7\x1e\x23\x96\x78\x0b\xa3\x3d\xe8\xfe\x3e\x72\x75\x3c\x86\x0f\x54\x42\x42\xe7\xa4\xca\xa4\x95\xc1\xc8\x02\xb1\xcf\x08\xcc\x80\x3d\x6e\x57\x72\x25\x92\x57\x2c\x2f\x2b\x69\x5b\xf5\x55\x7d\xf9\x82\x14\x55\xdd\xd9\x1c\x42\xaf\x9d\x24\x33\x98\x4e\xa7\x50\xe5\x09\x9d\xb3\x9c\x26\x56\x80\xbb\xad\xe0\x08\x45\xd8\x20\xff\x8c\x93\x5b\xbd\xd0\x21\x2e\x72\xc9\x8b\x4c\x00\xc9\x13\x7c\x20\x2c\xa7\x1c\xe6\xbc\x58\xc2\x4b\x5c\x07\x33\xc2\x05\x48\xa3\x10\xa2\x3d\x43\xbc\x66\x05\xea\x21\x07\x25\x91\xe9\x7b\x4e\xe7\xec\x6e\x30\x81\xf7\x4f\xce\x5f\x5e\xbd\x3f\x7b\xfe\xe2\xd5\x2f\x23\x5d\x3d\xab\x58\x96\xfc\x44\xb9\x60\x45\x3e\x98\xc0\x0f\x3f\xbe\x7a\xfd\xec\xea\xa7\xe7\x67\x1f\x5e\xbd\x7b\x6b\x17\xd7\xa7\x7f\x54\x94\xaf\x22\x7a\x27\x69\x9e\x84\xb5\xfe\x70\x67\x33\xac\xe9\xe8\xea\x86\x87\xe1\x9b\x4a\x48\x12\xa7\x34\xe2\x34\x4f\x28\x0f\x3d\x2d\x56\xeb\xa2\x61\xd3\x9d\x66\x11\x29\x4b\x35\x8e\x0f\x6d\x68\x19\xfc\x57\x2a\x81\xd3\x39\xe5\x34\x8f\xa9\x00\x59\x00\xc9\x32\x90\x29\x05\x96\x4b\xca\xa9\x90\x2c\x5f\x58\x8d\x25\x80\xe5\x58\xd7\x10\x55\xd3\x91\xe4\x89\x06\x37\x63\x79\x02\xf4\x86\xe6\xd2\xa8\x17\x8e\xf2\x52\x6b\xdc\x9f\xb9\x42\x87\x5b\x51\xa0\x59\x34\x67\x79\x12\x06\x5f\x61\xed\xd5\xad\xae\x0e\x60\xdf\x0a\x54\x33\x95\x7f\x29\xaa\xbd\x28\xf8\x12\xa6\x1e\x2c\x03\x41\xd7\x5f\xcd\x0b\xbe\x0c\xf4\xec\xf4\x08\x77\x25\xef\xef\x20\xe9\x9d\x24\x9c\x92\x8b\x9c\x2c\xe9\x54\xb5\xbb\x0c\x1c\xc2\xdd\x95\x3c\xba\xa6\xab\x92\x53\x21\xc2\x46\xed\x5b\xd9\x1b\x8f\xe1\xb9\x22\x10\xdc\x12\x01\xd8\x88\x26\x70\xcb\x64\x5a\x54\x12\x49\x24\x52\x36\x97\x70\x4d\x57\x11\xb6\x57\x52\x4d\xa3\xdb\x94\xc5\x29\x4c\xa7\x70\xf4\x0d\x3c\x7a\x04\x0f\x68\x84\xcd\xfe\x4e\x57\x16\x6e\x7b\xb2\x91\xa8\x66\x4b\x26\x43\xc4\x4c\xfd\xd1\xa8\xe4\x48\xe0\x67\x7a\x59\xda\x1a\x14\x7a\xc4\xeb\x49\x25\x8b\x03\x4e\x85\xd2\x08\x0a\x13\x35\x51\x50\x33\x85\x22\x07\x5c\x6e\x1a\x25\x94\xef\xf9\x5c\x50\x69\xd4\x43\xa4\x9f\x5e\x52\xb6\x48\x25\x1c\xe8\xb2\x38\x63\x34\x37\x65\xc7\x75\x3f\x0d\xfe\xdc\x90\xd0\xdf\x18\x9b\xa9\x00\x3c\x54\xcf\x51\x2c\x44\x38\x48\x11\xc4\x60\x04\x03\x52\xc9\x62\xd0\x2e\xa5\x59\x24\x62\x5e\x64\x99\x19\x7e\xdf\xe0\x66\xa7\xa7\xbf\x1e\xea\x8d\x2a\x2a\xf2\x70\x70\x4d\x57\x55\xa9\x27\x34\x18\x79\x9a\xaf\x85\x9e\xd9\xdc\x60\xad\x37\xb8\x16\x93\x63\xdc\x35\xf5\xfa\x70\xf7\x51\x47\x88\x50\x53\xbd\x72\x75\x58\xc3\x1f\x2d\x4c\x88\x85\x96\x24\x47\xad\xb9\x02\xa5\x16\xee\x35\x4d\x7e\x90\xf9\x26\x18\xb6\xc9\xd5\x4c\xe6\xdd\x8e\x3b\x8c\x6c\x5a\xba\xa3\xb2\x5c\x50\x2e\xdf\x50\xc9\x59\xbc\x09\x82\xa0\x19\x8d\x0d\x08\xdd\xfe\x6a\x89\x1d\x5c\x40\x9c\xce\x39\x15\xe9\x2b\x25\xf3\x37\x24\xdb\x05\x96\xe9\x72\xe9\x2e\xc7\xb8\xc8\x45\x91\xd1\x73\x54\xd6\x7d\xab\xd8\x34\x08\x5a\x1a\x50\x75\x80\x0d\x5d\xb4\xea\xa8\x95\x91\x3b\x9c\x24\x33\xd1\xdf\x8b\x5c\x28\x0b\xe6\x40\x16\x8b\x45\x46\xa7\x03\x49\x66\x03\x77\xba\xaa\x63\x44\xff\xd5\xd9\x88\x86\xea\x23\x0c\x44\x5a\xdc\xb6\x5b\x17\xb9\x2e\xcf\xa3\x19\x36\x0d\x1c\x99\xac\xd5\x86\x5a\x3b\x92\xf0\x05\xae\xb9\x87\x21\x8d\xf4\x83\x11\xf2\x9e\x0d\x4d\xd7\x47\x25\xe1\x34\x97\xe1\x30\x62\x79\x42\xef\x42\xb7\xbd\x2b\xb3\xb6\x42\x69\x9b\x87\x61\xf0\x95\x52\xa4\x06\x02\x91\x92\x87\x01\xe1\x8c\x1c\xd8\xcd\x30\x18\x0e\xa3\x94\x88\xa7\x19\x11\x22\x0c\x38\xcd\x0a\x92\x04\xc3\x96\x26\xd2\xfa\x07\xb7\xac\x46\xd5\xe8\x55\xa4\x55\xfe\x19\x95\x15\xcf\x41\x59\x91\x02\xe6\x45\x5c\x09\x98\x91\xf8\x5a\x6d\x25\xa8\x7c\x59\x2e\x24\x25\x09\x14\x73\xd0\xb0\xd4\x8e\x12\xf5\x09\x68\x34\x43\xd6\x5c\xd3\x55\x52\xdc\xe6\xca\x3e\xe2\x08\xbb\x97\x92\xcd\x02\xc6\x31\x3d\x92\x60\xf1\x0d\xc9\x42\xff\x69\x68\xda\x68\xa8\x1b\x34\xe9\x7a\xd8\xec\x1d\x9c\x17\x1b\x36\x0f\x5d\x17\x0c\xa3\x94\x25\x86\xea\x8d\xb0\x3e\xd1\x2a\x71\xb3\xac\x2a\xa5\xd4\x96\x70\xbb\xa2\x6a\x08\x5e\x17\xa7\xf5\xea\xc9\x1d\x13\x1b\x5b\xaf\xae\xc8\x1d\x13\x4e\xf3\x8c\x2e\x68\x9e\x6c\x40\x47\x57\xba\xca\xa6\x64\x79\x4e\x37\x4d\xda\xd4\xba\xdb\xe4\x0d\xc9\x3e\x48\x22\x37\xac\x32\xac\xbf\x12\xaa\x81\xb7\x29\xe7\xc9\x33\x22\x69\x7f\x1f\x47\xa1\xd1\x3c\xe9\x2a\x52\xd3\x59\x9d\x40\xa8\x3a\x4f\x94\x2c\xbe\xa6\x3c\xd4\x52\x91\x15\x31\xc9\xe8\x04\x06\x34\x1f\x68\x93\x4c\x19\x04\x44\x4e\x60\xf0\xeb\xaf\xbf\xfe\x7a\xf0\xe6\xcd\xc1\xb3\x67\xf0\xf2\xe5\x64\xb9\x34\xf5\xb2\x28\xb2\x19\xe1\xef\x33\x12\xa3\x8d\x33\x81\xc1\xac\x90\xb2\xb0\xf5\x82\x25\xf4\x87\xd5\x07\x96\xd0\x09\x48\x5e\x51\x53\x9a\x16\xb7\xe7\x45\x42\x56\x3f\x54\x52\x16\x79\xbb\xea\x69\x46\x09\xef\x16\x16\xc2\x01\xa2\xf7\xa1\x8e\xb5\x5b\xcf\xd9\x17\xf4\x66\xd2\x24\x1c\xa8\x7f\xcf\xd9\x92\xbe\xc7\xa9\x0f\x86\x48\x8b\x4d\x60\xb4\x45\xdc\x82\xa3\x94\x55\x52\x9a\xbd\x2f\x68\xed\x9e\x3d\xeb\xde\xdd\x35\x5b\x5b\x81\xdd\x40\xbb\x20\xaa\x52\xe1\x75\xa6\x9b\x5b\x20\xf5\xc2\x17\x1f\xea\x8d\xad\x73\x34\x35\x2b\xd4\xdd\xff\xf4\x0a\xc6\x83\xc0\xe0\x68\x60\x4e\xaa\xf6\x88\x23\x57\x19\x45\x70\x7a\x7b\xed\xc0\x53\x8d\x58\x5c\xd4\x5b\x6f\xb3\x19\x6b\xa1\x1b\x44\x8b\x6c\x55\xa6\xaa\xc9\xc0\x51\xa1\x3e\xa2\x61\x47\x35\x36\x50\x48\x92\x18\x35\x3a\x93\xf9\x41\xc9\xd9\x92\xf0\x55\x50\x1b\x6d\x0a\xb0\xd3\xa6\x1e\xec\x20\x4e\x69\x7c\xdd\x6a\xc7\xf1\x44\xde\x69\x5a\xe5\xd8\x98\x26\xb6\xf9\x1a\x68\x26\xe8\x46\x94\x3c\x30\xbf\x0f\xab\xce\x50\xf7\x63\xe6\x4d\x62\x6d\x8f\x39\x1e\x53\x42\x87\xf3\x0e\x8e\x71\xc6\xe2\xeb\xb0\xc3\xae\x3e\xda\x2b\x7b\xb9\x51\x79\x7f\xfb\xf0\xee\x6d\xc3\x8d\xf1\x18\x5e\xcd\x9d\x83\x89\xb2\xc9\xcd\x28\x23\x2c\x2e\x38\x5b\xb0\x9c\x64\x20\x28\x67\x54\x00\x7a\x2f\x16\x85\x84\x65\x25\x89\xa4\x49\x03\x27\x14\x4a\x81\x24\x43\x3c\x28\xde\x52\xc8\x29\x4d\xd4\x56\xc6\xa9\xb2\x4c\x24\xaf\x62\x09\x4c\xea\x83\xa3\x07\x59\x61\x84\x70\x23\x97\x1f\xc6\x4d\xa2\xad\x04\x4e\x72\xa1\xd4\xd1\x33\xb5\x88\x5b\x73\x69\x88\x07\x5d\xb1\xef\xd0\xe2\x7b\x18\x1c\x0e\x60\xa2\x56\x82\xdd\xf7\xda\xd4\xae\x01\xe9\x55\x88\x07\xfb\xb0\x36\x80\x3b\x87\x2a\x7b\xce\xe8\xf0\xa2\x65\xb6\x39\xf2\x62\x0d\x06\x67\x2c\x6b\xab\xdd\xdf\xaa\xc7\xa4\x30\x0b\x7e\x4e\x32\x41\x5b\x46\xba\xd9\x74\xea\x9d\xb6\x8b\xba\xde\x37\x66\xa8\x89\xad\x19\x1b\x5f\xa1\x1d\x7e\x19\x0c\x7b\x84\xcc\x9a\x1e\x31\xa7\x44\xd0\x33\x63\x39\xb9\x83\xde\x07\x3c\xa1\x3b\x00\x4f\x68\x0f\xf0\x5d\x51\xa7\x79\xb2\x0b\xe2\xcf\xf3\xe4\x77\xa2\xbd\x05\xb0\x45\xda\x01\xdc\x6b\xa7\xf5\x68\xfc\x96\xf1\xa5\xcf\x01\xaa\x2e\xe0\xb4\x54\x7b\x6b\x30\x82\xcf\xea\x24\x3a\xe9\x81\x87\xaa\x7d\x04\xcb\x42\x6d\xb2\xc1\x8c\xce\x0b\x4e\x83\x75\xc7\xa2\xb3\x86\x9e\x5a\xa7\x9c\xe2\x13\xcb\x17\x8d\x44\xeb\x83\xa9\x52\x51\x7a\x1b\xe8\x31\x2e\xec\xc9\x44\x35\x32\x46\x45\xdd\x63\x93\x36\x32\x9b\x1e\xba\x49\xef\x11\x57\x4b\xa9\xb2\x28\xab\x8c\x48\xfa\x0a\x67\x48\x66\x19\xd5\xb3\x14\x46\x78\x6b\xe5\xe6\xd8\xa5\xee\x48\x9d\xd5\xb1\xee\xf7\x5c\x36\x1e\xc0\x8d\x23\xee\xe4\x10\x7c\x18\x91\x4f\xe4\x2e\xb4\xba\x54\x0d\x52\x24\x13\x08\xfe\xfa\xfc\x3c\x18\x99\xc2\x8a\x67\x9e\xb7\x0b\xf6\x21\x18\x93\x92\x8d\x6f\x8e\xc6\x19\x99\xd1\x6c\x7c\x75\xa5\x28\x7b\x75\x35\xbe\x41\x67\x6a\xdd\x53\x29\xc0\xf3\x55\xa9\xf8\xfa\x49\x14\x79\x5d\x2e\xaa\x38\xa6\x42\x4c\x1a\x04\x55\xf5\x08\x9d\x15\xca\xa0\xac\x84\xeb\x46\x50\x34\x53\xf5\x4a\x2b\xca\x4a\xc0\x83\xe9\x14\x02\x03\x22\x70\x1b\x5a\x1a\xa6\xc5\xed\x73\x65\xa1\x87\x01\x7e\x81\xd2\x41\x2c\x5f\x00\xb9\x21\x2c\x53\x14\x02\x7d\xc4\x15\x0f\x9a\x2d\xae\x61\x6c\x53\xb2\xae\xff\x53\x94\x5b\xd6\x64\x45\x64\xd4\xdc\x9a\xa6\xf3\x82\x43\x88\x86\x06\xfa\x6c\x81\xc1\x89\xed\x10\x65\x34\x5f\xc8\xf4\x18\xd8\xfe\x7e\x0f\xb6\xee\x5a\xb8\x38\xbc\xac\x6d\x38\x92\x24\x61\x4e\x6f\xe1\x1d\x3e\x87\x06\xd8\x05\xbb\x1c\x41\xf3\xff\x70\xe8\x62\xbb\xe7\x01\x9e\x57\xbf\xfd\xb6\x3a\xa3\xa2\xca\x64\xed\xc1\xd4\x7f\xa8\x28\x26\xe8\xd2\x1f\x79\xd3\x57\x6d\xbb\xe5\x4b\x52\x4e\xe0\xf3\x7a\xe3\x40\x28\xca\x4a\x16\x49\x4a\x49\x12\x7a\x33\x2c\x2a\x1e\xd3\x89\xc5\xd8\x85\xca\x24\x5d\x8a\x09\x04\x24\xcb\x02\x7f\x34\x19\xa7\x94\x3b\xb2\xa1\x5a\xfa\x84\xb3\x9b\xfe\x2d\x85\x94\xdc\x50\x83\x39\x32\x21\xae\xb8\x3a\x2c\xeb\x39\x8e\x40\x5c\xb3\xd2\xeb\x58\x2f\x40\x87\x3c\x5a\x73\xa2\x5c\xa1\xd7\x0b\x1f\xdb\x23\x76\xa9\x6a\xba\xb9\x9d\x8e\xb7\x75\x59\x92\x52\x31\x63\xbd\xb5\x21\xb7\x8c\xc3\xc2\x68\xce\x32\x49\x79\xd8\x8c\x14\x19\xcd\x1a\x8e\x61\xbc\x18\xc1\x60\x30\xac\xe5\x62\xd4\xc1\x1c\xa0\xe4\xea\x5c\x74\x22\x24\x2f\xf2\xc5\xe9\x60\xd4\x6d\x50\x08\x75\xfa\x39\x19\xdb\x26\xad\x16\xeb\xe1\x8e\x28\x47\xf3\x82\x3f\x27\x71\xda\xa8\x52\xde\x25\x65\x3f\x65\x2e\x78\x64\x2d\xaa\x4b\x98\x02\x6f\x8f\xd8\xc6\xc1\x11\x44\x68\xf4\xb2\x12\x17\x60\x79\xef\x08\x6e\xff\xf5\x68\xcf\x93\x54\x2e\x3b\x52\x27\xda\x98\x63\x61\xa4\xda\x36\xd3\x23\xa3\x59\x77\x82\x56\x15\xf4\x4e\x73\x76\x19\x89\xb8\xe0\x14\x0e\xfa\xeb\x89\xa9\x6f\xcf\xdf\x4e\x10\xcf\x41\x87\xf0\x3d\x90\x48\x1f\x79\x9f\x16\xcb\x92\x70\x1a\xce\x86\x30\x01\xd6\x22\x52\x8b\x68\x0e\x95\xc4\x66\x72\xa4\x6c\x91\x66\x6c\x91\x7a\x34\x81\xde\xa5\x68\x00\x3e\x0c\x07\x27\x09\xbb\x39\x1d\x58\xf7\x7d\x7b\x56\xaa\xef\x65\x24\x24\x57\xaa\x78\x5f\x89\x1a\x36\x1f\xfa\x38\xf4\xa1\x3d\x1e\xc3\x79\xca\x04\x9a\xe3\x18\xa5\x48\x31\xac\x01\x64\x2e\x29\x07\x22\x25\x89\x53\x05\x14\xfd\xdd\x56\x0f\x41\x99\x55\x0b\x96\x8f\x80\x08\x60\xd2\x85\x55\xc8\x94\xf2\x5b\x26\x28\xcc\x38\x25\xd7\xa2\xd5\xcf\xce\x96\x64\x4c\xae\xa2\x1e\x55\xe7\xb9\x9c\x1c\xa4\xd1\x2b\x34\xe9\x9e\x3f\xe1\x4f\x6d\x4c\x6b\xeb\x2e\xd8\x62\x07\x2c\xa8\x7c\x57\xc7\xab\xb6\x6f\xfc\xad\xf8\x56\x73\x9c\xd6\x85\xe8\xef\xb6\x51\x51\x80\xc0\xf1\x6b\x1b\x6d\x1d\xd4\x4e\x06\x5b\x20\x24\x2d\xdb\x25\x78\x66\x09\xf6\x00\x2e\x37\x1b\xc0\xba\xcb\x30\xa2\x9e\xd6\x40\x5f\xe7\xc8\x06\x9f\xdc\xb3\xbc\xb2\x35\x9a\x40\x7a\xa4\x1e\x1d\xc7\x67\xc4\xf2\x27\x9c\x93\x55\xa8\xca\x47\xde\x74\x86\x70\x3a\x85\xc3\x86\x2d\x18\x96\x31\x50\xd0\x72\x31\x5b\x35\x9c\xba\xad\xc0\xd2\x09\xcd\xc7\x4b\x67\x64\xec\x53\xf3\xc9\xf3\x8e\xd6\x9d\x6c\x0c\xaa\x65\xf4\xb9\x2d\xb4\xaf\xb7\xed\xfe\xd5\xd6\x29\x2e\xad\x3a\xfe\xbf\xcd\x14\x24\x5c\xd0\x67\x15\x27\xb8\x58\x1d\x29\x40\xee\x9d\xd3\x3b\xd9\x88\x03\x16\x9d\x3d\x87\x29\x28\x23\xe3\x8c\x2e\x9e\xdf\x95\x61\xf0\xcf\xf0\xe2\xf0\xe0\x2f\x97\xfb\xc3\xf0\x62\x75\x9b\xa4\x4b\x71\xb9\x3f\x7c\xa8\x65\x11\x4d\x20\xdc\x9b\x95\x58\xd4\x10\x23\x2c\x0b\x0d\xb8\xda\xab\xf5\xc0\x34\xd5\xf1\x18\x34\xab\x90\x36\xaa\xce\x54\x59\x62\x3f\x98\xc2\x37\x2d\xd7\xcf\x77\x87\xd6\x6f\xa5\x46\x45\x32\xc3\x14\x70\x7a\xaf\x72\x69\x01\x5c\x1c\x5d\xd6\x98\x55\x39\x53\x9b\xa5\xad\x79\x7c\xe9\x90\x4f\xf7\xff\xba\x1b\xf2\x76\x12\x12\x2e\x14\x80\xcb\xad\x14\xf6\x4e\x8d\x3b\xaf\x33\x24\xce\x07\x1a\x17\x79\x52\xfb\x6e\x3d\x5e\x85\xad\x40\x93\xe3\xb0\xee\x33\x2c\xef\xc9\x63\xe8\x33\x36\x15\xcd\x3d\x14\x4e\xfa\x50\xb8\x07\x28\x1a\x9a\xbe\xab\xa9\x85\xeb\x96\xce\xc7\xce\x82\xdb\x70\xfa\x81\x7b\xfc\x03\x8d\x25\xee\x5a\xe8\xeb\x5d\x4e\x47\xde\x49\xfc\x3f\xcf\xb0\xed\x9c\x82\x03\x38\x52\x5c\x3d\xd5\xdc\x3d\x38\xd8\xc8\xb5\xd3\xff\x39\x5c\x5b\x50\xf9\xbc\x8e\x12\x6c\x67\x19\x2a\x1c\x2f\xb6\xf0\xe5\x0b\x78\x05\x3e\xd6\xdc\x06\xad\x96\x18\x56\xb3\xba\xc6\xf5\x3b\xef\xe2\x72\xdf\x6d\x4f\xe6\x1f\x7e\xdf\x64\x54\x51\xa2\x1b\x6b\xaf\x5a\xdd\xdd\x89\x34\x89\xa6\x50\xb5\x1d\x3a\xda\x2e\xc1\x94\xb6\x2d\x88\x89\x5e\x9c\x10\xd4\xbd\xa9\x43\xbb\x90\xc5\x20\xb4\xa3\x26\x7d\x9e\xf7\xc4\x00\x36\x90\x25\xa7\xb7\x06\x65\xc3\x3a\x4b\x20\x97\xc8\x66\x19\x9a\xb6\x78\x8c\xde\x79\xfd\xc2\x18\x1e\x8f\x60\x20\xf4\x8a\x1b\xf4\xd2\xdb\x00\x76\xea\x7c\xd1\xdf\x51\x21\xfd\xdf\x9e\xb7\xa8\x66\x92\x93\x58\xfe\x3f\x35\x79\xa7\xf5\xee\xe9\x6a\x71\x46\x09\xd7\x66\xf3\xb0\xb5\xda\x3b\xfa\xa8\xd1\x34\xeb\xbd\xb6\x0b\x59\x59\xdf\x61\x4f\xf0\x32\xa2\xcb\x52\xae\xc2\xa1\x13\x50\x22\x5c\x2a\xb9\x36\xc6\x91\xa6\xae\xa2\xb7\x2a\x0c\x87\xff\x8e\x5d\xc2\xa4\xd1\x14\x59\x65\x6c\xb5\xcd\x96\xb1\x4d\xef\xb0\xc6\xf5\x65\x30\x34\xe1\xb0\x2f\x5f\xe0\x0d\x91\x69\xb4\x24\x77\x21\xfe\x33\xcf\x8a\x82\xfb\xbb\xc6\x18\x1e\x7f\x7b\x38\x1c\xc1\x51\x3d\x6c\x13\x7f\xed\xe8\x17\x18\xdb\xec\x57\x47\xeb\x23\x52\xbf\xa4\xdc\xf3\x53\xda\xc2\x88\xcc\xd4\x61\x78\xe8\xda\x6b\x15\xcf\xec\x58\xc6\x4b\x67\x1f\x4b\xc2\xc9\xb2\xc9\xa7\x0b\x10\x4a\x30\x69\x1b\xc7\x36\x88\xb4\x31\x19\xb0\xb6\xce\x35\xc0\x08\x39\xa6\x0c\x73\x33\xb5\x03\x8f\x37\xc7\x6e\x53\x1d\x0e\x37\x0d\x8f\x7d\x20\xb4\x54\x96\x6d\xcd\x15\x5d\x5b\xf1\x4c\x6d\xe4\xfd\xee\x4f\x9d\x76\x86\x83\x05\xc6\x61\xad\x67\xec\x8a\x77\x8f\x6f\xd3\x4d\xde\xc0\x45\x72\x46\x45\x59\xe4\x82\x76\x1b\x1f\x6b\x5a\x78\xf1\x3e\x83\xb1\xd4\x32\xda\xc8\xab\x65\xdf\x6e\x78\xff\x61\x8c\x9f\xea\x80\xd0\x76\x9c\xfd\x23\xdf\x2f\xa9\x3a\x08\x6d\xf0\x38\xb7\xe4\x5f\x27\xac\xe8\xca\x60\xe8\x79\xa2\x2b\x9e\x6d\xf3\x2f\xab\xf2\x89\xa1\xd2\x7f\xda\xe7\x8c\xbd\xd0\x15\xb0\xa3\x6f\xd9\x40\x0d\x6b\xaf\xb2\x4f\xca\x6d\x5e\x86\xbb\x94\x8f\x94\xd0\x96\x6d\xf4\x55\x99\x3a\x5c\x05\xb8\x44\x5b\x48\xa3\x22\xe0\x9e\x87\x4d\xf5\xb9\x4b\x79\xc4\x0d\x5b\x31\xa6\xf9\xa0\x2f\xf5\xd6\xfe\x51\xae\x18\xda\xee\xa3\x27\xef\xb9\x96\xfc\x58\x75\xbb\xb3\x26\xb1\x3a\x4c\x7a\x9d\xb6\xba\xf5\xe9\x1d\x8d\x2b\xcc\x50\x35\x0e\xed\x00\xf6\x15\xd8\x61\x97\xca\x35\xf5\xe2\x62\x59\x66\x54\xd2\x9d\x09\x38\xdd\x40\xc0\xfb\x63\x05\x49\x73\x08\xef\xdb\x41\xe0\xa0\x59\xb4\xc7\x5e\x47\x59\x48\x92\xa9\xe2\x0f\x3a\x56\x8d\x09\xe0\xf7\x71\x48\x07\x99\xef\x61\xd3\xc6\x4e\xc6\x5f\xab\xd6\x0f\x2a\xd5\x40\xc4\x24\x23\x3c\x68\x73\xb9\x8b\xd2\xd1\x56\xe6\x76\xfb\xdc\x87\x82\x3d\xb4\xf6\x72\x7f\xdd\xf2\xc0\xd5\xdb\x76\x2a\x97\x59\x18\xbc\x2e\x48\x02\x4a\x11\x6a\xf6\xd7\x84\xdf\x87\x60\x29\xe0\x64\xc6\x61\x7c\x0a\x67\xb5\x4e\xd7\xad\x9c\x9d\x77\x1f\x02\xdb\x4c\xd5\x04\xe7\x0a\x73\x04\x68\xd2\x05\x74\x8f\xd6\x84\x1c\x11\xeb\x0d\x53\x37\xa8\xef\xe0\xb9\xab\x05\xdb\x55\xc1\x4b\xb1\xd8\x62\x8a\xab\x1e\x91\xd2\x14\xd8\xb6\x55\x6e\x8d\x9d\x2d\x43\x37\xb6\xd5\x1f\x1d\x7b\x30\x68\x0f\x6d\x69\xb0\x65\x68\x2f\x3f\x68\x07\x6b\xd0\xb5\x07\x14\x7b\x8a\x4a\xbe\x7a\x66\x65\xf5\x96\xe5\x49\x71\xab\xa7\x73\xae\x2b\xdb\x2d\xeb\x0d\x89\xb5\xb2\x58\xfb\x4c\xb6\x56\x92\x53\x63\xb7\xa1\xf1\x69\x21\xf8\xce\xad\x3a\x1f\xd4\x0e\x09\x53\x8b\x97\xd0\x0b\x5f\x61\xd5\x1f\x60\xee\x39\x3e\xf7\x26\x51\xa9\x39\x8c\x9a\x19\x7c\x6d\x6e\x2d\x6d\xa7\xb6\xbe\x32\xf0\x9a\xcc\x68\xe6\xed\xf4\x18\xbf\x15\x0d\xc9\xf1\xf9\x03\xfa\xe8\x85\xb9\xe1\xe3\xb8\x34\xb0\x16\x58\x0e\x6e\x37\x4d\x14\x5d\xa5\xb6\x1b\x1b\x0c\x76\x14\x89\x0b\x35\x2a\x2b\x91\x86\x81\x0d\x45\xa9\xc5\xa5\xfb\xee\x43\x50\x47\x9f\x8c\x2e\x17\x31\x29\xe9\xcb\xf3\x37\xaf\x0d\x9e\x17\xf8\x55\x47\x3d\xd7\xfe\xc1\x3d\xb3\xb3\x0b\x4e\x12\x76\x03\x71\x46\x84\x98\x7e\x0c\x74\xf1\xc7\xa0\x19\xca\x62\xf2\xa9\x60\x79\x18\x9c\xcc\xf8\x69\x30\xd4\xc3\x27\xec\xe6\x34\xd8\x4a\x4c\xed\xa4\x3f\x2f\xce\xc5\x5b\xed\x8a\xde\x48\x4e\x69\x5b\x98\x9a\xc8\x12\x47\xd9\xee\x83\x01\x8e\xfa\x39\x38\xbe\x8f\xf8\x5b\xa9\xbf\x9d\xfc\x3d\xf4\xaf\x49\x3e\xfd\x18\xd4\x74\xb1\xf4\x55\xe5\x1f\x83\x3a\x04\x81\x1a\x58\x7d\x98\xd9\xec\x4f\xfb\xc8\x38\xd2\x34\x5c\x07\x8e\x2f\x42\x77\xd8\xcd\x6f\xfd\x93\xf1\xf2\xd6\xb4\x44\xb7\x6d\x43\x4a\xbd\x62\xb1\xe9\x8b\xac\x20\xd2\xd4\xdb\x45\xc9\xc4\x5b\xf2\x56\x95\x0d\x9d\x4b\x1a\xc1\xfe\xab\x7c\x1e\x8c\x20\x38\x30\xdf\xf8\x0c\xb7\x2c\xcb\x60\x46\x35\xb0\x44\x2d\xa7\x02\xde\x92\xb7\x30\x5b\xb9\xf0\x87\x11\x9c\xa7\xd4\x82\x8a\x49\x3e\x90\xaa\x13\xe6\x95\xd0\x64\x04\xa2\xc0\xc4\x4e\x90\x29\x5d\x02\x11\xb0\x20\xa5\x80\x30\xaf\xb2\x6c\x18\xb9\x6e\x26\x7b\x73\x6e\xed\x79\xa4\xb7\x12\xc5\x4b\x18\x6b\x1b\xe7\xf7\xba\x0b\x4a\x92\x51\x29\xed\xe9\xf5\xcc\x5c\xe4\x8b\x9e\x16\x59\xc1\xa3\xf7\xba\xb2\x39\x4a\xa3\xd9\xe9\x98\x02\x4a\x86\x96\x44\x72\x76\x17\xf8\x2a\xaa\x31\xbf\x4c\x52\x01\x13\x90\x17\x12\x8a\x39\xe8\xf6\x18\x43\x7b\x00\xef\x33\x4a\x04\x05\x8a\x17\x64\x08\xc4\x05\xe7\x34\x96\x98\x0e\x4e\x85\x60\x45\x1e\x05\x7e\x22\x8d\x96\xf3\x75\xe3\xfb\x22\x36\xc7\x82\xd7\xd1\xc3\x46\x6f\x4a\xd1\x8e\x05\x1d\xd7\x4f\x5a\x8a\x9b\x60\x90\x14\x66\xad\xa2\x81\x83\xac\xa9\x17\x85\x89\x22\x59\xab\xe7\xd8\x55\x55\xc2\x89\xd1\xb7\xec\x1b\x1b\x7c\x6a\x54\x13\x52\xc7\x57\x09\xcd\xc0\x4d\x82\x46\x0d\xb8\xae\x73\xb3\xfe\x0c\x29\xdc\x51\x26\xf8\x39\xf2\xba\x4f\xcc\xb7\x7f\xd0\x91\x42\x87\xa2\x84\x4f\x29\x67\x01\xe9\xbf\xd6\x20\xea\xef\x6e\xa2\xc3\x23\x17\x87\x97\x6e\x4e\xc0\x6a\xe2\xec\x8d\xb8\x32\x35\xb4\x8b\xa3\xcb\x26\x5e\x5b\x27\x31\xac\x87\x8d\x79\x9d\xa9\xc3\x89\x91\xc0\x08\x1f\x43\xdd\x63\xdd\x64\xf6\xd5\x22\xa9\x8d\xa9\xe8\x37\xca\x8b\x17\x2c\xcb\x42\x35\x9d\x96\x2f\x93\xec\x68\x48\x74\xee\xe8\xde\xeb\x2d\xae\xd3\x33\xad\x17\xde\x7a\x15\xfc\xfd\x1c\x2f\x38\xe0\x0d\x5a\x92\xaf\x40\x72\x12\x53\xa1\xe4\x9d\xe4\x40\xef\x98\xbe\x1d\x87\xfa\x20\xf2\x13\xee\x1b\x97\x92\x33\x5c\x93\xad\x1f\xa7\x2c\x4b\x38\xcd\xc3\x61\x4f\xc0\xb1\x69\xdb\x4a\x3b\xc3\x0a\xcc\xff\xf7\x2a\xd6\xed\x8b\x04\x26\x10\x6f\xf6\xbf\x40\xdf\x20\x38\xb5\xd1\xf6\xe3\xf6\x4d\x82\x56\x73\x73\x85\xa0\xdb\xbe\x41\xbf\x73\xa7\x70\x5b\x23\x1c\xaa\xf1\xaf\xd1\x3c\x31\xde\xb5\x8d\x0e\x28\x45\xf9\xa7\x45\x7e\x43\xb9\x04\x59\xc0\x8f\x6f\x5f\xfd\x82\x36\xb9\x90\x64\x59\xda\x3b\x85\xce\x21\x6b\x77\x27\xe7\x97\x2f\xf0\xcd\x77\x66\x84\xa3\xd4\x5e\x6f\x8d\x7a\x5c\x7f\x16\xcd\x83\x7a\xa0\x7a\x9a\x28\x39\x9d\x3c\x17\xe1\xec\x3c\xef\x49\x82\x91\x7d\x93\x72\x7c\xcb\x64\x0a\x2c\xbf\x61\x82\xcd\x32\x0a\x81\x52\x45\x81\x5e\x79\x02\x88\xbe\x33\x18\x17\xf9\x9c\x2d\x2a\x4e\x13\xb8\x3b\x50\x4c\x80\x59\x51\xe5\x09\x41\x00\x34\x17\x15\xa7\xc2\x82\x97\x29\x91\x5a\xf2\x04\x10\x4e\x21\x61\xa2\xcc\xc8\xca\xdc\x42\x04\x02\x73\x76\xd7\xc0\x41\x2a\x78\x57\x71\x72\x52\x96\x98\x31\x51\xe0\xd0\x75\xfe\x41\x0d\x5f\x4d\xdc\x76\xc3\x26\x4d\x72\x33\x0a\x34\x92\xe0\xe2\xf0\x32\xba\x83\xd3\x86\x6a\x4e\xb8\x49\xd3\xa8\xca\xf1\x8a\x63\xf8\xf9\x6e\xd2\xb4\x1a\x81\x49\x3e\x5b\x7b\x89\xcf\x0e\x5c\xe1\xad\xcd\x03\x38\x52\xe3\x9c\x58\x8e\x74\x46\x41\x8b\x46\x0d\x61\x1a\xf4\x0e\xd0\xdc\x59\x7a\x5b\xdc\x42\xcc\x29\x91\xfa\x86\xa4\xda\x24\xfd\x45\xdc\xb9\xfb\xee\x6e\xa3\x3a\x97\x5a\x63\x60\x12\x01\x26\x8e\xf0\xd7\x8a\x54\xdf\x6d\x9c\x34\x1e\x5a\x67\x61\xe3\x61\x51\x5f\x75\x0c\x87\x23\x25\xf2\x46\x83\xde\xb2\x44\xa6\xf7\xf4\xf9\x59\xd5\xa3\xff\xe0\xbf\x0f\x47\xf0\xb8\xee\xa7\xcd\x7b\xca\x27\x3d\xa9\xf3\xdf\x9b\x3c\x8c\x00\x26\x10\x64\x2c\xa7\xd6\x9f\x86\xc7\x88\xb2\xc8\x88\x39\x18\xab\x3a\xc2\x8d\x13\xcd\x1e\x7e\x6b\x79\xd7\xc5\x4b\xa6\x5a\x92\x4a\x16\xc1\xc8\x23\xea\x0b\x96\x27\x98\x36\x2f\xa8\x91\xcc\x81\x80\x25\xb9\x1b\x2f\x59\xbe\xb7\x21\xa9\x5f\x29\x5d\xc9\x2b\xf7\x5a\xed\xcf\x29\xcd\x6d\xf6\xbe\x32\x30\xf4\x15\xbd\xa4\xde\xe1\x97\xe4\xae\xf1\x89\xdc\xb3\x16\x65\x73\x54\xaf\xa5\x45\xf5\x8f\x2b\xce\x75\xf9\x1b\x17\x12\x40\xd3\x61\x03\x44\x55\xfa\xbe\x60\x4d\x2a\x8a\x95\xd9\xba\x22\x5a\xc1\x69\x6b\x80\x47\x8f\xc0\xad\x7e\xd0\x36\x42\x70\xcf\x6c\xa1\xe4\x74\xe8\x71\x64\xd5\xf6\x84\xa2\xc4\xfe\xd4\xef\x6d\xa4\xdd\xdd\x30\x3c\x59\x8e\x34\xf9\x96\xe4\xee\xeb\xa3\xe8\xf0\xdb\xcd\xcd\x58\x6e\x69\xe3\x99\x3f\xc8\x01\xac\x7b\x95\xcf\x59\xce\xe4\xea\xb8\xc5\x99\x03\xbf\xe2\x77\x72\xe8\xdf\xc3\x84\x13\xc4\x71\x17\xd2\xeb\xb9\xdc\x4b\xf0\x3e\x1e\x2f\x77\xe4\xec\x72\x77\x7e\xae\x9d\x8b\x47\x88\xd5\x14\xd9\xd4\x8e\xdf\xf7\x33\x13\xf6\x1b\x97\xdc\x46\x6e\xaa\xcf\x03\xdb\xae\xef\xf6\xd0\x66\xe0\xe1\x61\x74\xf4\xb5\x8e\x30\x91\x99\x08\x55\xe1\x81\x82\x37\x6c\xac\xdb\x2d\xc3\x6e\x85\xb0\xb6\xde\x19\x25\x4a\x77\xc6\x34\xe9\xea\xdd\x08\xcd\x1f\x74\xa2\x7e\xd6\x5a\x66\xd2\xa7\xb2\x9d\x2b\x01\xab\x2d\xb0\x7e\x35\xaa\x7c\x23\x30\xad\xf7\x0a\xce\x68\x2e\x6b\x4d\x49\xe7\x36\xc7\x4d\xb2\xf8\xfa\x85\xb9\x65\x58\xc3\x7f\xc1\xee\xa4\xda\xae\xa3\xb7\xd5\x72\x46\x79\xa4\xaf\x21\xfe\xfd\xcd\x0f\xe7\xa3\x9e\x7d\x03\x51\x34\xfb\x86\x7b\x97\xc0\x27\xa7\x79\xe9\x43\x33\xb3\xb4\xb8\xa1\xfc\x19\x95\x84\x65\xfd\xf3\x7b\xd9\x34\xd8\x6d\x92\x1a\x4d\x3f\x0d\x56\xef\x03\x23\xb8\x1b\xc1\xca\x57\xa5\x26\x49\x61\x70\x22\x4a\x92\x5b\xf3\x51\x15\x06\x98\x03\x5a\xfb\xbd\xef\xe0\x6b\x34\xea\x86\x91\x2c\x7e\x3c\x7f\xaa\xbd\x06\xe1\x50\xa7\x80\xaa\xbe\xa7\x83\x63\x07\xac\xb8\x25\x32\x4e\xbb\x80\x71\x1e\x57\xba\x36\xd0\x37\x9e\xa6\xc1\x8c\xc4\xd7\x0b\xae\xcc\xa4\x03\x73\xf4\xd0\xe9\xa7\xa8\x42\xb0\x44\x0d\xa3\xac\xd9\xee\x40\x71\x91\x4b\x9a\xe3\xa5\x7e\x3d\xe4\x3e\x98\xd9\x46\x7d\xce\x1a\x34\xd6\xb4\xc7\x66\x02\xae\xf7\x6a\x65\x66\x62\xf2\xa6\xed\x10\x4e\x3a\x06\x36\x98\x71\x24\x8b\x1d\xd5\x29\x32\x2e\xc7\xc6\x41\xe7\xa3\xd1\xb5\x61\xf0\xa8\x6b\x6f\xf6\xf6\x30\xfe\x35\xd6\xf5\xda\x28\xba\x5b\x6d\xa4\xdc\x2b\x10\xce\x68\x4e\x3a\x70\xff\x90\x3f\xd0\x94\xdc\xb0\x82\xdb\x23\xdd\x4b\xdb\x21\x84\x9d\x44\x4f\xe3\x35\x31\xdf\xfe\xe0\x22\xa5\xd9\x8d\xb2\x56\x77\x1a\xf9\x1c\x2d\x86\xdd\x04\x7e\xd3\xa8\x6e\xac\xb3\xbe\x5a\xbf\xd5\xc3\x2a\xd8\x6f\x7f\xe4\x18\xea\xab\xae\x07\x2d\x47\x45\x8f\x26\xa8\x0f\x0a\x75\x10\xf5\x8f\x9a\x8d\xf7\x58\x0a\x8d\xba\xd9\x21\x5f\xab\x27\x90\xbd\x25\x9c\xdc\x4f\x13\x75\xde\x36\x58\x98\xcb\x99\x02\x4a\x82\x6f\x57\x71\xef\x6e\xce\x0b\x5e\xdb\x88\xfa\x10\x84\xde\x38\xe7\xc2\xa6\x20\x37\x74\xcf\x9c\x94\x9c\x6b\x9a\x4f\xfe\xf6\xe4\x17\xb0\x51\x28\x75\xb2\x29\x78\x42\xb9\xbe\xe1\x79\x50\x3b\xdc\x80\x49\xed\x13\x74\xc6\xd4\xc0\x6e\x95\x75\xaa\x20\x56\x82\x72\x75\xe8\x52\x67\x26\x9d\x3f\x8e\xf8\xb8\xef\x36\xa8\x6f\x77\x1a\x67\x96\x77\x78\xec\xbf\x15\x8a\x9e\xbd\xad\x2e\x8a\x5e\x97\xdc\xdb\x02\xd1\x2c\x95\x9d\x21\x60\xae\x34\x62\xcb\xcd\xd6\xf5\x15\x9c\x93\x99\x7f\xa9\xd7\xbd\xad\xe9\x84\x1f\xea\xdb\xa3\x3b\x49\x41\x2b\x39\xa0\x95\x5f\x46\x76\x92\x03\x9d\xf7\xd3\x5c\x3b\xbd\x1f\x4b\x97\xd2\xda\xd9\x6a\xbd\xef\x3f\x14\xc9\xca\x92\xda\x01\xe7\xbf\x6d\xe4\x0a\x2f\xcd\x81\x9c\x15\x89\xb9\x1e\x8d\xfd\xbc\xb4\x20\x71\xcb\x64\x9c\x86\xad\xb0\xa9\xc6\x3f\x26\x82\x42\x70\x43\x63\x59\xf0\x60\xb2\xe7\x9a\x8c\x7e\x7c\xd3\xe7\xa0\x1d\xc6\x38\x4a\x82\x13\xc9\x4f\x4f\x64\x02\x71\x91\xa9\xbd\x6a\x3a\x78\x3c\x38\x3d\x61\xa7\xb9\x66\xec\xc9\x98\x9d\x9e\x8c\x65\xa2\x3e\xf8\x69\x73\x3b\xa0\x9d\x5a\xd9\x9f\x30\xdc\x13\x6b\xf5\x6f\xa3\x21\x0f\x8c\xad\x6a\x1a\x5e\xb0\x4b\x77\xb7\xac\x23\x19\x7d\xee\xce\xda\xdb\x79\x7c\xdf\xd4\x4e\x5b\x31\x1d\x0d\xd2\x44\x5e\xd4\xd4\x4c\x13\xe3\xcd\xbc\x38\xba\x6c\xaa\xdc\x59\xeb\x79\xe2\xdd\x8d\xe3\x9a\xfe\xc6\x65\xfd\xff\x31\xfd\x6f\xfe\x38\xfd\x6f\xda\xf4\xaf\xd3\xe6\xcf\xe9\x9d\xb2\x70\x82\xda\xbf\x5d\xa3\xf7\x49\xa3\xf7\x09\x4e\xe0\xc6\xba\x8f\x2d\x6e\x9f\xfc\x9b\x8a\x0d\xa4\xfd\x69\xdd\xf8\xe2\xd3\xa5\xe1\x10\xfc\x6f\xc5\x35\xb7\xfc\x50\x73\x6e\xc6\xc7\xa7\x81\x1b\x0c\xff\xd3\xa2\xe1\x60\xb2\xb3\x64\x18\x07\xbf\x96\x8c\xfe\xd1\x75\x13\x6f\x24\x97\x13\x9b\x04\xb1\x3d\x10\x5a\xb6\xf7\x0f\x84\x4d\xbc\x81\x9c\x59\xfb\x63\x0e\xb7\x0c\x6a\x5c\x97\x93\xde\xfd\xe0\xc7\x5c\x54\x65\x59\x70\x49\x13\x73\xff\x01\x83\x33\x1d\x20\x5b\xb7\x76\xbe\xe1\x0d\x92\x7d\x77\x89\xdb\xaf\x99\xf3\xfc\xd4\x8e\x4d\x75\xd6\x5f\xec\x9b\x5a\xf5\xa5\x33\x37\xd4\x82\xe4\x6b\x10\xa0\xb9\x64\x72\xf5\x46\xdf\xa9\xc4\x89\x05\x8f\x82\x09\x04\x8f\xc8\xb2\x3c\xb6\x97\x90\x4e\xb0\x24\x93\x75\xc1\x29\x16\x2c\xea\x82\x41\x30\x98\xc0\xe0\xd1\xbf\xaa\x42\x1e\x9b\x9b\x91\xc1\x20\x50\x45\x5f\x7d\xf3\x97\xba\x64\xac\x4b\xee\x1e\xbf\x38\x1e\xd4\xef\x1f\x31\x46\xbe\x39\xd3\x18\xf4\x9a\xab\x99\x17\x8f\x4e\x4e\x83\xc1\xc7\xf1\xe5\x78\x31\x72\x6e\xd1\x89\x56\x22\x7a\x3d\x8d\x0b\x71\x69\xa3\x24\x6b\x8f\x2b\xef\x49\xdf\xed\x85\xe6\xfd\xa1\x36\x5c\xdc\x62\xa6\xea\xd6\x7a\x59\x64\x3f\x27\x11\x48\x73\x7d\x0c\x01\xa3\x3b\xfd\xc7\xb3\xd7\x4d\x18\xc3\x6d\xd5\xab\x53\xbd\x06\xda\x2b\xbb\x6e\x12\x2f\xbc\x5a\xeb\xda\xc1\xa1\x48\x92\x68\xab\x1c\xcc\x9b\x48\x51\x9a\x82\xaf\x48\x92\x5c\x99\x37\x20\x99\xfb\xf9\x5e\x73\xfd\xca\x28\x55\x34\x82\xcf\xeb\x61\xd7\x42\x69\xcd\xdf\xce\xa8\x4b\x03\x35\x3b\x93\xab\x91\x15\x31\x1e\xf3\x23\x41\x09\xd7\xef\xeb\x0b\x82\x16\xc3\x6c\xc4\xd2\x50\x0f\xd3\xcf\xde\xdb\xdc\xd6\x7e\x38\x91\xa8\x66\x5a\x3e\xc2\xa3\x61\x24\xca\x8c\xc9\x70\xf0\x68\x50\x67\xe5\x36\x30\x5e\xd2\xac\xac\x8f\x59\xed\xc9\xfc\xa3\xd5\x2c\x74\xc3\x65\x6d\x18\x7a\xc2\x4d\x17\x11\x3a\x98\x6e\xa5\x96\xa5\xb2\x4b\x2d\xfb\x8e\x49\x5f\x70\xba\xb8\x6a\x93\x11\x49\xf6\xb0\x7e\xbf\xa3\xf3\x92\x36\xe3\x54\x31\x6f\xbf\xd4\x0a\x53\x71\x56\x1b\x9c\x3f\x9e\xbd\x6e\x58\x3b\x74\xaa\xb5\x3e\x69\xf1\x7e\xb8\x07\x30\x6c\x5e\x44\xab\xd7\x83\x96\xbe\x26\x3a\xf5\xd0\xb0\x77\x68\xce\x69\xdd\x3c\x1c\x1b\x72\xab\x4f\x71\xcd\xfb\x52\x14\x9d\xc6\x63\x78\xfb\xee\xfc\xf9\xa4\x75\x13\x75\x46\xe1\x9a\x96\x12\xef\x1b\xaf\xf2\x58\x87\x5f\xc6\x95\x64\xd9\x58\x48\x6e\xbf\xe3\x22\xbf\x89\x16\xc5\x04\xe1\xbe\x66\xf9\xf5\x8b\x82\x3f\xaf\xe3\xe1\xf7\xf0\xa0\xa6\x47\xff\xb2\x45\x76\x6a\xe5\x63\x57\xad\x99\xbe\x17\x08\x5e\xe8\xb5\x85\x37\x2a\xdd\xe0\x79\x6b\xd5\x6b\x0a\x34\xf7\x48\x6d\xe0\xf1\x4f\x8b\xa7\x03\xe2\xdd\xec\x13\x8d\x95\x12\xea\xc8\xea\x82\xe6\x94\x13\xa9\xc5\x55\x37\xf3\x14\x8e\xc5\xdf\x4b\x1d\x78\x18\x61\x92\x6f\xe8\xc0\xb6\x49\x52\xfa\x55\x91\x3a\x37\xe5\x91\x79\xff\x58\xca\x84\x2c\xf8\x0a\x85\x43\x1d\x41\x68\xf8\x79\x3d\x82\x20\x18\x81\x0e\x93\x7e\xaf\x36\x64\x87\xa8\x5b\xd7\x88\x23\x90\x2e\x87\xb4\xdc\xf5\xe8\x68\x97\x45\xe6\x4a\x7f\xd3\x69\x08\x9f\xcd\xb4\x16\xe8\x06\xc0\x76\x3d\xf9\x83\xbd\x94\x6e\x09\xc8\x2e\x5d\xda\x9a\xf1\x1f\x9e\x1a\xab\xa1\xb9\x3a\xa3\x96\x3c\x3c\x38\xd3\xc4\xef\x82\xb3\xd3\xd3\x7a\x95\xdf\x90\x8c\x25\x3d\x6a\x47\xdf\x9e\x77\xd5\x96\xee\x46\x65\x6c\x59\xfd\x82\x17\xcb\x77\x7a\x00\x03\xa0\x3b\xdc\x08\x0e\x77\xa4\x4c\xd4\x8c\xae\x1d\xb5\x30\x85\xf1\x3f\x17\x1f\x93\xfd\x8f\x51\xb4\x3f\x8d\xf6\x1f\x8e\x7f\x1f\xb1\x7a\x66\xe8\xd2\x0b\x25\xf2\xbc\x2a\x33\x1b\xd9\x30\xd3\x74\xca\x3b\xbc\x6f\xea\x5a\x3b\xcd\xef\x9e\x5c\x24\xa9\x90\x2e\xbc\xe3\xfe\x24\xd4\xad\x93\xbc\x8f\x1f\x1b\xc4\x63\xa4\x45\xf6\x55\xa3\x67\xd4\xbe\xea\x34\x68\x8c\x86\xc6\x66\xe8\xdf\x52\x4b\x7c\xcb\xf2\xbb\xb9\xd2\xb6\x08\xcf\x7b\xcd\x06\x42\xd3\x2f\x62\x0e\x9d\x21\xed\x5e\x9a\xa3\xd7\xfd\xdd\x5c\x0f\xfa\xa2\xe0\x0a\x8a\x5d\xa4\x2e\x3a\x3b\xb3\xa1\xa9\xd0\x77\x4d\xc4\xcf\x4c\xa6\x61\x07\x49\x43\xec\x3a\x9f\xd9\x50\xe0\x3e\x7c\xb6\x53\x62\xdb\x24\x94\x2d\x11\xd3\xf0\x70\x74\xcf\xbc\xb5\xfa\xeb\x05\xd5\x2d\xf4\x37\x8f\x9d\x68\x52\xdb\x36\x1d\x92\x18\x5a\xb8\xef\x1e\xf3\x5f\x3d\xd0\xd8\x9a\xce\xea\x7e\x37\x7f\x97\x9b\x5d\xb8\x8b\x5f\xcd\x67\x0d\xe4\x49\x1c\x57\xcb\x2a\x23\x12\x93\x98\x77\x50\x26\x1b\x24\x16\xf6\xcd\x25\xa9\x0e\xd8\x3a\x8d\xa1\x79\x41\x77\xfb\x72\xbe\xd3\xfa\x77\x2f\xb5\xcd\x93\xdf\xae\x86\xbd\x37\x38\x80\x2f\xdc\x9d\x88\xab\xcb\xc4\xa6\xb7\x3a\x69\x3f\xc9\x13\x9b\x7f\x29\x35\x47\xb5\x81\x3a\x1d\x38\x1b\x78\xd3\xbc\xfe\x4d\x02\xb7\xef\xc5\xa1\x7e\xc7\x83\xdb\xd8\x02\x4d\x68\x5c\x24\xf4\xc7\xb3\x57\x4f\x8b\x65\x59\xe4\x34\xb7\xb4\xf4\x00\x1c\x5d\x36\x47\xa7\x8f\xfb\xea\xcc\x14\x40\x30\x1c\x1a\xa8\x6a\x25\xb9\x28\x4c\x21\x90\x64\xe6\xa4\xb9\xfa\x43\xd6\x6f\x0b\x70\x8a\xf5\xdb\xc3\x24\x99\x01\x13\x98\xfe\xb0\xa0\xdc\x38\x0e\x5c\x83\xf4\xa2\x19\xe6\xb2\x9e\xea\x4f\xf6\x65\x0f\xeb\x1e\xf6\x77\xdf\xcd\xb0\x8d\xe9\x6d\x3d\xe6\xb2\xda\x31\xd4\xcc\x28\xc1\x42\x59\x26\xcc\x88\x69\x10\x75\x73\x94\xb7\x8d\xd7\x63\x5e\x75\x2c\x96\x96\xa5\x55\x4b\x59\x69\x31\xec\xd7\xc0\xcc\x53\xbe\xbe\x99\xa7\xc5\x52\x3f\x46\xd7\x74\x25\xbc\x91\x86\x5d\x21\xbd\x6e\xde\x86\xee\x40\xba\x30\x28\xec\xc3\x35\x5d\x5d\x5a\x5b\xd5\x40\xb9\x50\x65\x4d\x76\xa1\x7b\x18\xd2\xbd\x5b\x0e\x05\x75\x0c\x36\x46\xb4\xbe\x8c\xf6\x81\xca\xaa\x34\xc1\x94\x98\xc4\x29\x9d\xe8\x97\xbb\x35\xcc\xf6\x2e\xad\xf5\xbe\x0f\x4d\x48\x22\x59\x3c\xfe\x24\xc6\xfa\xb0\x53\xff\x98\x40\x6a\x7f\x60\xe0\xfb\x9b\xa9\x62\xa2\xf7\xab\x00\x26\xd7\xa6\x73\x35\x0d\x13\x21\xe1\xb3\x7d\x23\x8f\xf7\xa6\x7f\xe3\x26\xb4\x7e\xb5\xfa\x57\x01\x50\xe0\x9b\x14\x4a\xbb\x64\x98\x78\x46\x4b\x4e\x63\x22\xa9\x3e\xcf\xe1\x91\xde\x4f\x0b\x4d\x18\xa7\xb1\x3c\x2f\xde\xb0\x85\x92\x91\xa4\x3e\xf5\x43\xdf\x05\x1f\xfc\x91\x15\xed\x90\xe8\x39\x03\x84\xce\xdd\x18\x14\x4a\x4d\x6e\xdf\x0d\xb8\x6e\xbc\x1c\x78\xb4\x3a\x4f\xa9\xa0\x20\x6f\x0b\x73\x1f\x50\xf4\xe3\x8d\x09\x46\xbd\xe8\x0e\x15\x14\xc2\x29\x90\x24\xa1\x09\x14\x79\xb6\x42\x57\xe7\x8c\xc4\xd7\xb7\x84\x27\x78\xf1\x8b\x48\x36\x63\x19\x93\x2b\x75\x72\x2b\xb2\x44\xcb\x88\x09\x7b\x47\x8e\x80\xf4\x92\x6c\xa3\xa3\x20\x25\x22\xbd\xc7\xb2\x69\xde\x13\x68\x37\x3f\xad\x0d\x93\x17\x9c\x2c\x96\x3a\x02\xdd\xa3\x1f\xfb\x46\xd1\xd1\x09\xbe\xaa\x99\x81\x37\xa9\x0c\xe3\x7d\xa0\x66\x4f\x0e\x8f\x86\x5a\xe9\x25\xbc\x28\x31\x50\xa5\xe0\xc0\x57\x98\xd9\x13\x63\xd8\x3b\x74\x12\xea\xba\x28\x37\x56\x3a\x57\xea\x6f\xed\xac\xa3\x0d\x72\x53\xab\x8d\x3f\x37\xcd\x9e\x03\xea\x9f\x99\x6d\xbf\x6a\x6a\x7b\xa5\x3c\xcb\xa7\xf0\xd5\x61\xb3\x6f\xd6\xfa\xb0\x47\x2d\xab\x36\xae\xba\x2b\x76\xd1\x74\xf7\xeb\xba\xa2\xa5\xe6\xc0\xfb\x2d\x83\x7a\x62\x78\xb7\xb6\xff\x38\xdc\x22\xb2\xc2\x7c\xdc\x3a\xf0\x22\x6b\x1f\x86\x6a\xb1\x0e\x8f\xf7\xfe\x4f\x00\x00\x00\xff\xff\x6b\x3d\x9f\x87\x48\x69\x00\x00") +var _webUiStaticJsGraphJs = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xe4\x7d\x6b\x77\xdb\x38\xb2\xe0\x77\xff\x8a\x0a\x3b\x27\xa2\xda\x32\x65\xa7\x6f\xf7\xde\x91\x2d\xf7\xa6\xf3\x98\x64\x26\xaf\x71\xdc\xaf\xe3\x78\x7c\x20\x12\x12\x19\x53\x24\x07\x00\x6d\xab\x13\xfd\xf7\x3d\x28\x3c\x08\x90\x94\xa5\xee\x9e\x9d\xb3\x7b\xae\x3f\xc8\x16\x1e\x85\x42\x55\xa1\x50\xa8\x2a\xc0\x37\x84\xc1\x7b\x56\x2e\xa9\x48\x69\xcd\x61\xea\x7e\xf9\xf2\x05\x3e\xaf\x8f\xf7\x64\x93\x05\x23\x55\x7a\x4e\x97\x55\x4e\x04\x3d\xde\xc3\xb2\x0f\xcf\x9f\xbe\x7b\xfb\x0c\xa6\x70\x74\x78\x78\x78\xbc\xb7\xd7\xf4\x8c\xfe\x2a\x9b\xc3\x14\xe6\x75\x11\x8b\xac\x2c\x42\x9a\xd3\x25\x2d\xc4\x08\xca\x4a\x7e\xe7\x23\x48\x49\x91\xe4\xf4\x69\x4a\x8a\x05\x35\xdf\xce\xe8\xb2\xbc\xa1\x43\xf8\xbc\x07\x20\xd2\x8c\x47\x34\x87\x29\xe8\xbe\xc7\xa6\x10\x71\x79\x79\xfe\xe6\x35\x4c\xa1\xa8\xf3\xdc\x56\x68\xd8\x30\x35\xa3\xd8\x1a\x77\x30\x98\x7a\x63\xb7\xda\x28\x14\x5c\xd4\x15\x3a\xe0\xa1\x18\xca\x1e\x43\xd9\x75\x6d\xfb\xb3\x2c\xbe\xe6\x29\xb9\x35\x73\xf7\x50\x4b\x88\x20\x30\x85\x8b\xcb\xe3\x3d\x53\x94\x15\x99\xc8\x48\x9e\xfd\x46\xc3\xe1\xf1\xde\xba\x87\x80\x91\xc8\x96\xf4\x05\x89\x45\xc9\xe4\xa4\x24\x1a\xc1\x2a\x98\xc0\x77\x87\xf0\xb5\xfa\x78\xfc\x5f\xf0\x35\x7c\xf3\xdd\xb7\x23\x59\x75\xdb\xad\xfa\x5f\x58\x91\xb4\x2a\xb0\x30\x6d\x0a\xf1\xfb\x12\xbf\xe3\x9f\x3c\x98\xc0\x51\x3f\x46\x5c\xd0\xea\x27\x92\xd7\x54\x22\x74\x21\x1b\x1f\xf1\x60\x04\xc1\xd1\xa1\xfa\xb5\x94\x9f\xdf\xe2\xe7\x91\xfa\xf5\xcd\xa1\xfa\x96\xca\xcf\xc7\xf8\xf9\x1d\x7e\x1e\xa9\x2f\x47\x09\x56\x24\x01\x0e\x7d\x74\x8b\xdf\xf0\xf3\xbf\xf0\xf3\xbf\xf1\xf3\x68\x85\xe5\xab\x60\xef\xb2\x0f\xad\xa2\x5e\xe2\x1f\x12\xab\x3e\x51\x8c\x2a\x56\x8a\x52\xac\x2a\xea\x90\xbd\xcb\x64\x29\xd5\x9c\xe6\x73\x98\x22\x8b\x24\xf7\xe4\xd7\x28\x4b\xbc\x85\xd1\x1e\x74\x7f\x1f\xb9\x3a\x1e\xc3\x07\x2a\x20\xa1\x73\x52\xe7\xc2\xc8\x60\x64\x80\x98\xef\x08\x4c\x83\x3d\x6e\x57\x32\x29\x92\x57\x59\x51\xd5\xc2\xb4\xea\xab\xfa\xf2\x05\x29\x2a\xbb\x67\x73\x08\xbd\x76\x82\xcc\x60\x3a\x9d\x42\x5d\x24\x74\x9e\x15\x34\x31\x02\xdc\x6d\x05\x47\x28\xc2\x1a\xf9\x67\x8c\xdc\xaa\x85\x0e\x71\x59\x08\x56\xe6\x1c\x48\x91\xe0\x17\x92\x15\x94\xc1\x9c\x95\x4b\x78\x89\xeb\x60\x46\x18\x07\xa1\x15\x42\xb4\xa7\x89\xd7\xac\x40\x35\xe4\xa0\x22\x22\x7d\xcf\xe8\x3c\xbb\x1b\x4c\xe0\xfd\x93\xf3\x97\x57\xef\xcf\x9e\xbf\x78\xf5\xcb\x48\x55\xcf\xea\x2c\x4f\x7e\xa2\x8c\x67\x65\x31\x98\xc0\x0f\x3f\xbe\x7a\xfd\xec\xea\xa7\xe7\x67\x1f\x5e\xbd\x7b\x6b\x16\xd7\xa7\x7f\xd4\x94\xad\x22\x7a\x27\x68\x91\x84\x56\x7f\xb8\xb3\x19\x5a\x3a\xba\xba\xe1\x61\xf8\xa6\xe6\x82\xc4\x29\x8d\x18\x2d\x12\xca\x42\x4f\x8b\x59\x5d\x34\x6c\xba\xd3\x3c\x22\x55\x25\xc7\xf1\xa1\x0d\x0d\x83\xff\x4a\x05\x30\x3a\xa7\x8c\x16\x31\xe5\x20\x4a\x20\x79\x0e\x22\xa5\x90\x15\x82\x32\xca\x45\x56\x2c\x8c\xc6\xe2\x90\x15\x58\xd7\x10\x55\xd1\x91\x14\x89\x02\x37\xcb\x8a\x04\xe8\x0d\x2d\x84\x56\x2f\x0c\xe5\xc5\x6a\xdc\x9f\x99\x44\x87\x19\x51\xa0\x79\x34\xcf\x8a\x24\x0c\xbe\xc2\xda\xab\x5b\x55\x1d\xc0\xbe\x11\xa8\x66\x2a\xff\x92\x54\x7b\x51\xb2\x25\x4c\x3d\x58\x1a\x82\xaa\xbf\x9a\x97\x6c\x19\xa8\xd9\xa9\x11\xee\x2a\xd6\xdf\x41\xd0\x3b\x41\x18\x25\x17\x05\x59\xd2\xa9\x6c\x77\x19\x38\x84\xbb\xab\x58\x74\x4d\x57\x15\xa3\x9c\x87\x8d\xda\x37\xb2\x37\x1e\xc3\x73\x49\x20\xb8\x25\x1c\xb0\x11\x4d\xe0\x36\x13\x69\x59\x0b\x24\x11\x4f\xb3\xb9\x80\x6b\xba\x8a\xb0\xbd\x94\x6a\x1a\xdd\xa6\x59\x9c\xc2\x74\x0a\x47\xdf\xc0\xa3\x47\xf0\x80\x46\xd8\xec\xef\x74\x65\xe0\xb6\x27\x1b\xf1\x7a\xb6\xcc\x44\x88\x98\xc9\x1f\x1a\x55\x0c\x09\xfc\x4c\x2d\x4b\x53\x83\x42\x8f\x78\x3d\xa9\x45\x79\xc0\x28\x97\x1a\x41\x62\x22\x27\x0a\x72\xa6\x50\x16\x80\xcb\x4d\xa1\x84\xf2\x3d\x9f\x73\x2a\xb4\x7a\x88\xd4\xb7\x97\x34\x5b\xa4\x02\x0e\x54\x59\x9c\x67\xb4\xd0\x65\xc7\xb6\x9f\x02\x7f\xae\x49\xe8\x6f\x8c\xcd\x54\x00\x1e\xca\xef\x51\xcc\x79\x38\x48\x11\xc4\x60\x04\x03\x52\x8b\x72\xd0\x2e\xa5\x79\xc4\x63\x56\xe6\xb9\x1e\x7e\x5f\xe3\x66\xa6\xa7\x7e\x3d\x54\x1b\x55\x54\x16\xe1\xe0\x9a\xae\xea\x4a\x4d\x68\x30\xf2\x34\x5f\x0b\x3d\xbd\xb9\xc1\x5a\x6d\x70\x2d\x26\xc7\xb8\x6b\xaa\xf5\xe1\xee\xa3\x8e\x10\xa1\xa6\x7a\xe5\xea\xb0\x86\x3f\x4a\x98\x10\x0b\x25\x49\x8e\x5a\x73\x05\x4a\x2e\xdc\x6b\x9a\xfc\x20\x8a\x4d\x30\x4c\x93\xab\x99\x28\xba\x1d\x77\x18\x59\xb7\x74\x47\xcd\x0a\x4e\x99\x78\x43\x05\xcb\xe2\x4d\x10\x38\xcd\x69\xac\x41\xa8\xf6\x57\x4b\xec\xe0\x02\x62\x74\xce\x28\x4f\x5f\x49\x99\xbf\x21\xf9\x2e\xb0\x74\x97\x4b\x77\x39\xc6\x65\xc1\xcb\x9c\x9e\xa3\xb2\xee\x5b\xc5\xba\x41\xd0\xd2\x80\xb2\x03\x6c\xe8\xa2\x54\x87\x55\x46\xee\x70\x82\xcc\x78\x7f\x2f\x72\x21\x2d\x98\x03\x51\x2e\x16\x39\x9d\x0e\x04\x99\x0d\xdc\xe9\xca\x8e\x11\xfd\x57\x67\x23\x1a\xca\x8f\x30\xe0\x69\x79\xdb\x6e\x5d\x16\xaa\xbc\x88\x66\xd8\x34\x70\x64\xd2\xaa\x0d\xb9\x76\x04\x61\x0b\x5c\x73\x0f\x43\x1a\xa9\x2f\x5a\xc8\x7b\x36\x34\x55\x1f\x55\x84\xd1\x42\x84\xc3\x28\x2b\x12\x7a\x17\xba\xed\x5d\x99\x35\x15\x52\xdb\x3c\x0c\x83\xaf\xa4\x22\xd5\x10\x88\x10\x2c\x0c\x08\xcb\xc8\x81\xd9\x0c\x83\xe1\x30\x4a\x09\x7f\x9a\x13\xce\xc3\x80\xd1\xbc\x24\x49\x30\x6c\x69\x22\xa5\x7f\x70\xcb\x6a\x54\x8d\x5a\x45\x4a\xe5\x9f\x51\x51\xb3\x02\xa4\x15\xc9\x61\x5e\xc6\x35\x87\x19\x89\xaf\xe5\x56\x82\xca\x37\x2b\xb8\xa0\x24\x81\x72\x0e\x0a\x96\xdc\x51\xa2\x3e\x01\x8d\x66\xc8\x9a\x6b\xba\x4a\xca\xdb\x42\xda\x47\x0c\x61\xf7\x52\xb2\x59\xc0\x38\xa6\x47\x12\x2c\xbe\x21\x79\xe8\x7f\x1b\xea\x36\x0a\xea\x06\x4d\xba\x1e\x36\x7b\x07\x63\xe5\x86\xcd\x43\xd5\x05\xc3\x28\xcd\x12\x4d\xf5\x46\x58\x9f\x28\x95\xb8\x59\x56\xa5\x52\x6a\x4b\xb8\x59\x51\x16\x82\xd7\xc5\x69\xbd\x7a\x72\x97\xf1\x8d\xad\x57\x57\xe4\x2e\xe3\x4e\xf3\x9c\x2e\x68\x91\x6c\x40\x47\x55\xba\xca\xa6\xca\x8a\x82\x6e\x9a\xb4\xae\x75\xb7\xc9\x1b\x92\x7f\x10\x44\x6c\x58\x65\x58\x7f\xc5\x65\x03\x6f\x53\x2e\x92\x67\x44\xd0\xfe\x3e\x8e\x42\xa3\x45\xd2\x55\xa4\xba\xb3\x3c\x81\x50\x79\x9e\xa8\xb2\xf8\x9a\xb2\x50\x49\x45\x5e\xc6\x24\xa7\x13\x18\xd0\x62\xa0\x4c\x32\x69\x10\x10\x31\x81\xc1\xaf\xbf\xfe\xfa\xeb\xc1\x9b\x37\x07\xcf\x9e\xc1\xcb\x97\x93\xe5\x52\xd7\x8b\xb2\xcc\x67\x84\xbd\xcf\x49\x8c\x36\xce\x04\x06\xb3\x52\x88\xd2\xd4\xf3\x2c\xa1\x3f\xac\x3e\x64\x09\x9d\x80\x60\x35\xd5\xa5\x69\x79\x7b\x5e\x26\x64\xf5\x43\x2d\x44\x59\xb4\xab\x9e\xe6\x94\xb0\x6e\x61\xc9\x1d\x20\x6a\x1f\xea\x58\xbb\x76\xce\xbe\xa0\x37\x93\x26\xe1\x40\xfe\x79\x9e\x2d\xe9\x7b\x9c\xfa\x60\x88\xb4\xd8\x04\x46\x59\xc4\x2d\x38\x52\x59\x25\x95\xde\xfb\x82\xd6\xee\xd9\xb3\xee\xdd\x5d\xb3\xb5\x15\x98\x0d\xb4\x0b\xa2\xae\x24\x5e\x67\xaa\xb9\x01\x62\x17\x3e\xff\x60\x37\xb6\xce\xd1\x54\xaf\x50\x77\xff\x53\x2b\x18\x0f\x02\x83\xa3\x81\x3e\xa9\x9a\x23\x8e\x58\xe5\x14\xc1\xa9\xed\xb5\x03\x4f\x36\xca\xe2\xd2\x6e\xbd\xcd\x66\xac\x84\x6e\x10\x2d\xf2\x55\x95\xca\x26\x03\x47\x85\xfa\x88\x86\x1d\xd5\xd8\x40\x21\x49\xa2\xd5\xe8\x4c\x14\x07\x15\xcb\x96\x84\xad\x02\x6b\xb4\x49\xc0\x4e\x1b\x3b\xd8\x41\x9c\xd2\xf8\xba\xd5\x8e\xe1\x89\xbc\xd3\xb4\x2e\xb0\x31\x4d\x4c\xf3\x35\xd0\x9c\xd3\x8d\x28\x79\x60\x7e\x1f\x56\x9d\xa1\xee\xc7\xcc\x9b\xc4\xda\x1c\x73\x3c\xa6\x84\x0e\xe7\x1d\x1c\xe3\x3c\x8b\xaf\xc3\x0e\xbb\xfa\x68\x2f\xed\xe5\x46\xe5\xfd\xed\xc3\xbb\xb7\x0d\x37\xc6\x63\x78\x35\x77\x0e\x26\xd2\x26\xd7\xa3\x8c\xb0\xb8\x64\xd9\x22\x2b\x48\x0e\x9c\xb2\x8c\x72\x40\xef\xc5\xa2\x14\xb0\xac\x05\x11\x34\x69\xe0\x84\x5c\x2a\x90\x64\x88\x07\xc5\x5b\x0a\x05\xa5\x89\xdc\xca\x18\x95\x96\x89\x60\x75\x2c\x20\x13\xea\xe0\xe8\x41\x96\x18\x21\xdc\xc8\xe5\x87\x76\x93\x28\x2b\x81\x91\x82\x4b\x75\xf4\x4c\x2e\xe2\xd6\x5c\x1a\xe2\x41\x57\xec\x3b\xb4\xf8\x1e\x06\x87\x03\x98\xc8\x95\x60\xf6\xbd\x36\xb5\x2d\x20\xb5\x0a\xf1\x60\x1f\x5a\x03\xb8\x73\xa8\x32\xe7\x8c\x0e\x2f\x5a\x66\x9b\x23\x2f\xc6\x60\x70\xc6\x32\xb6\xda\xfd\xad\x7a\x4c\x0a\xbd\xe0\xe7\x24\xe7\xb4\x65\xa4\xeb\x4d\xc7\xee\xb4\x5d\xd4\xd5\xbe\x31\x43\x4d\x6c\xcc\xd8\xf8\x0a\xed\xf0\xcb\x60\xd8\x23\x64\xc6\xf4\x88\x19\x25\x9c\x9e\x69\xcb\xc9\x1d\xf4\x3e\xe0\x09\xdd\x01\x78\x42\x7b\x80\xef\x8a\x3a\x2d\x92\x5d\x10\x7f\x5e\x24\xbf\x13\xed\x2d\x80\x0d\xd2\x0e\xe0\x5e\x3b\xad\x47\xe3\xb7\x8c\x2f\x75\x0e\x90\x75\x01\xa3\x95\xdc\x5b\x83\x11\x7c\x96\x27\xd1\x49\x0f\x3c\x54\xed\x23\x58\x96\x72\x93\x0d\x66\x74\x5e\x32\x1a\xac\x3b\x16\x9d\x31\xf4\xe4\x3a\x65\x14\xbf\x65\xc5\xa2\x91\x68\x75\x30\x95\x2a\x4a\x6d\x03\x3d\xc6\x85\x39\x99\xc8\x46\xda\xa8\xb0\x3d\x36\x69\x23\xbd\xe9\xa1\x9b\xf4\x1e\x71\x35\x94\xaa\xca\xaa\xce\x89\xa0\xaf\x70\x86\x64\x96\x53\x35\x4b\xae\x85\xd7\x2a\x37\xc7\x2e\x75\x47\xea\xac\x8e\x75\xbf\xe7\xb2\xf1\x00\x6e\x1c\x71\x27\x87\xe0\xc3\x88\x7c\x22\x77\xa1\xd1\xa5\x72\x90\x32\x99\x40\xf0\xd7\xe7\xe7\xc1\x48\x17\xd6\x2c\xf7\xbc\x5d\xb0\x0f\xc1\x98\x54\xd9\xf8\xe6\x68\x9c\x93\x19\xcd\xc7\x57\x57\x92\xb2\x57\x57\xe3\x1b\x74\xa6\xda\x9e\x52\x01\x9e\xaf\x2a\xc9\xd7\x4f\xbc\x2c\x6c\x39\xaf\xe3\x98\x72\x3e\x69\x10\x94\xd5\x23\x74\x56\x48\x83\xb2\xe6\xae\x1b\x41\xd2\x4c\xd6\x4b\xad\x28\x6a\x0e\x0f\xa6\x53\x08\x34\x88\xc0\x6d\x68\x68\x98\x96\xb7\xcf\xa5\x85\x1e\x06\xf8\x0b\xa4\x0e\xca\x8a\x05\x90\x1b\x92\xe5\x92\x42\xa0\x8e\xb8\xfc\x41\xb3\xc5\x35\x8c\x6d\x4a\xd6\xf6\x2f\x49\xb9\xa5\x25\x2b\x22\x23\xe7\xd6\x34\x9d\x97\x0c\x42\x34\x34\xd0\x67\x0b\x19\x9c\x98\x0e\x51\x4e\x8b\x85\x48\x8f\x21\xdb\xdf\xef\xc1\xd6\x5d\x0b\x17\x87\x97\xd6\x86\x23\x49\x12\x16\xf4\x16\xde\xe1\xf7\x50\x03\xbb\xc8\x2e\x47\xd0\xfc\x3d\x1c\xba\xd8\xee\x79\x80\xe7\xf5\x6f\xbf\xad\xce\x28\xaf\x73\x61\x3d\x98\xea\x07\x15\xc5\x04\x5d\xfa\x23\x6f\xfa\xb2\x6d\xb7\x7c\x49\xaa\x09\x7c\x5e\x6f\x1c\x08\x45\x59\xca\x22\x49\x29\x49\x42\x6f\x86\x65\xcd\x62\x3a\x31\x18\xbb\x50\x33\x41\x97\x7c\x02\x01\xc9\xf3\xc0\x1f\x4d\xc4\x29\x65\x8e\x6c\xc8\x96\x3e\xe1\xcc\xa6\x7f\x4b\x21\x25\x37\x54\x63\x8e\x4c\x88\x6b\x26\x0f\xcb\x6a\x8e\x23\xe0\xd7\x59\xe5\x75\xb4\x0b\xd0\x21\x8f\xd2\x9c\x28\x57\xe8\xf5\xc2\xaf\xed\x11\xbb\x54\xd5\xdd\xdc\x4e\xc7\xdb\xba\x2c\x49\x25\x99\xb1\xde\xda\x90\x19\xc6\x61\x61\x34\xcf\x72\x41\x59\xd8\x8c\x14\x69\xcd\x1a\x8e\x61\xbc\x18\xc1\x60\x30\xb4\x72\x31\xea\x60\x0e\x50\x31\x79\x2e\x3a\xe1\x82\x95\xc5\xe2\x74\x30\xea\x36\x28\xb9\x3c\xfd\x9c\x8c\x4d\x93\x56\x8b\xf5\x70\x47\x94\xa3\x79\xc9\x9e\x93\x38\x6d\x54\x29\xeb\x92\xb2\x9f\x32\x17\x2c\x32\x16\xd5\x25\x4c\x81\xb5\x47\x6c\xe3\xe0\x08\x22\x34\x7a\x59\x8a\x0b\x64\x45\xef\x08\x6e\xff\xf5\x68\xcf\x93\x54\x26\x3a\x52\xc7\xdb\x98\x63\x61\x24\xdb\x36\xd3\x23\xa3\x59\x77\x82\x46\x15\xf4\x4e\x73\x76\x19\xf1\xb8\x64\x14\x0e\xfa\xeb\x89\xae\x6f\xcf\xdf\x4c\x10\xcf\x41\x87\xf0\x3d\x90\x48\x1d\x79\x9f\x96\xcb\x8a\x30\x1a\xce\x86\x30\x81\xac\x45\xa4\x16\xd1\x1c\x2a\xf1\xcd\xe4\x48\xb3\x45\x9a\x67\x8b\xd4\xa3\x09\xf4\x2e\x45\x0d\xf0\x61\x38\x38\x49\xb2\x9b\xd3\x81\x71\xdf\xb7\x67\x25\xfb\x5e\x46\x5c\x30\xa9\x8a\xf7\xa5\xa8\x61\xf3\xa1\x8f\x43\x1f\xda\xe3\x31\x9c\xa7\x19\x47\x73\x1c\xa3\x14\x29\x86\x35\x80\xcc\x05\x65\x40\x84\x20\x71\x2a\x81\xa2\xbf\xdb\xe8\x21\xa8\xf2\x7a\x91\x15\x23\x20\x1c\x32\xe1\xc2\x2a\x45\x4a\xd9\x6d\xc6\x29\xcc\x18\x25\xd7\xbc\xd5\xcf\xcc\x96\xe4\x99\x58\x45\x3d\xaa\xce\x73\x39\x39\x48\xa3\x57\x68\xd2\x3d\x7f\xc2\x9f\xda\x98\xd6\xc6\x5d\xb0\xc5\x0e\x58\x50\xf1\xce\xc6\xab\xb6\x6f\xfc\xad\xf8\x56\x73\x9c\x56\x85\xe8\xef\x36\x51\x51\x80\xc0\xf1\x6b\x6b\x6d\x1d\x58\x27\x83\x29\xe0\x82\x56\xed\x12\x3c\xb3\x04\x7b\x00\x97\x9b\x0d\x60\xd5\x65\x18\x51\x4f\x6b\xa0\xaf\x73\x64\x82\x4f\xee\x59\x5e\xda\x1a\x4d\x20\x3d\x92\x5f\x1d\xc7\x67\x94\x15\x4f\x18\x23\xab\x50\x96\x8f\xbc\xe9\x0c\xe1\x74\x0a\x87\x0d\x5b\x30\x2c\xa3\xa1\xa0\xe5\xa2\xb7\x6a\x38\x75\x5b\x81\xa1\x13\x9a\x8f\x97\xce\xc8\xd8\xc7\xf2\xc9\xf3\x8e\xda\x4e\x26\x06\xd5\x32\xfa\xdc\x16\xca\xd7\xdb\x76\xff\x2a\xeb\x14\x97\x96\x8d\xff\x6f\x33\x05\x09\xe3\xf4\x59\xcd\x08\x2e\x56\x47\x0a\x90\x7b\xe7\xf4\x4e\x34\xe2\x80\x45\x67\xcf\x61\x0a\xd2\xc8\x38\xa3\x8b\xe7\x77\x55\x18\xfc\x33\xbc\x38\x3c\xf8\xcb\xe5\xfe\x30\xbc\x58\xdd\x26\xe9\x92\x5f\xee\x0f\x1f\x2a\x59\x44\x13\x08\xf7\x66\x29\x16\x16\x62\x84\x65\xa1\x06\x67\xbd\x5a\x0f\x74\x53\x15\x8f\x41\xb3\x0a\x69\x23\xeb\x74\x95\x21\xf6\x83\x29\x7c\xd3\x72\xfd\x7c\x77\x68\xfc\x56\x72\x54\x24\x33\x4c\x01\xa7\xf7\xaa\x10\x06\xc0\xc5\xd1\xa5\xc5\xac\x2e\x32\xb9\x59\x9a\x9a\xc7\x97\x0e\xf9\x54\xff\xaf\xbb\x21\x6f\x27\x21\xe1\x42\x02\xb8\xdc\x4a\x61\xef\xd4\xb8\xf3\x3a\x43\xe2\x7c\xa0\x71\x59\x24\xd6\x77\xeb\xf1\x2a\x6c\x05\x9a\x1c\x87\x75\x9f\x61\x79\x4f\x1e\x43\x9f\xb1\x29\x69\xee\xa1\x70\xd2\x87\xc2\x3d\x40\xd1\xd0\xf4\x5d\x4d\x2d\x5c\xb7\x74\x3e\x76\x16\xdc\x86\xd3\x0f\xdc\xe3\x1f\x68\x2c\x71\xd7\x42\x5f\xef\x72\x3a\xf2\x4e\xe2\xff\x79\x86\x6d\xe7\x14\x1c\xc0\x91\xe4\xea\xa9\xe2\xee\xc1\xc1\x46\xae\x9d\xfe\xcf\xe1\xda\x82\x8a\xe7\x36\x4a\xb0\x9d\x65\xa8\x70\xbc\xd8\xc2\x97\x2f\xe0\x15\xf8\x58\x33\x13\xb4\x5a\x62\x58\xcd\xe8\x1a\xd7\xef\xbc\x8b\xcb\x7d\xb7\x3d\x99\x7d\xf8\x7d\x93\x91\x45\x89\x6a\xac\xbc\x6a\xb6\xbb\x13\x69\xe2\x4d\xa1\x6c\x3b\x74\xb4\x5d\x82\x29\x6d\x5b\x10\xe3\xbd\x38\x21\xa8\x7b\x53\x87\x76\x21\x8b\x46\x68\x47\x4d\xfa\xbc\xe8\x89\x01\x6c\x20\x4b\x41\x6f\x35\xca\x9a\x75\x86\x40\x2e\x91\xf5\x32\xd4\x6d\xf1\x18\xbd\xf3\xfa\x85\x31\x3c\x1e\xc1\x80\xab\x15\x37\xe8\xa5\xb7\x06\xec\xd4\xf9\xa2\xbf\xa3\x42\xfa\xbf\x3d\x6f\x5e\xcf\x04\x23\xb1\xf8\x7f\x6a\xf2\x4e\xeb\xdd\xd3\xd5\xe2\x9c\x12\xa6\xcc\xe6\x61\x6b\xb5\x77\xf4\x51\xa3\x69\xd6\x7b\x6d\x17\xb2\xb4\xbe\xc3\x9e\xe0\x65\x44\x97\x95\x58\x85\x43\x27\xa0\x44\x98\x90\x72\xad\x8d\x23\x45\x5d\x49\x6f\x59\x18\x0e\xff\x1d\xbb\x84\x4e\xa3\x29\xf3\x5a\xdb\x6a\x9b\x2d\x63\x93\xde\x61\x8c\xeb\xcb\x60\xa8\xc3\x61\x5f\xbe\xc0\x1b\x22\xd2\x68\x49\xee\x42\xfc\x63\x9e\x97\x25\xf3\x77\x8d\x31\x3c\xfe\xf6\x70\x38\x82\x23\x3b\x6c\x13\x7f\xed\xe8\x17\x18\x9b\xec\x57\x47\xeb\x23\x52\xbf\xa4\xcc\xf3\x53\x9a\xc2\x88\xcc\xe4\x61\x78\xe8\xda\x6b\x35\xcb\xcd\x58\xda\x4b\x67\xbe\x56\x84\x91\x65\x93\x4f\x17\x20\x94\x60\xd2\x36\x8e\x4d\x10\x69\x63\x32\xa0\xb5\xce\x15\xc0\x08\x39\x26\x0d\x73\x3d\xb5\x03\x8f\x37\xc7\x6e\x53\x15\x0e\xd7\x0d\x8f\x7d\x20\xb4\x92\x96\xad\xe5\x8a\xaa\xad\x59\x2e\x37\xf2\x7e\xf7\xa7\x4a\x3b\xc3\xc1\x02\xed\xb0\x56\x33\x76\xc5\xbb\xc7\xb7\xe9\x26\x6f\xe0\x22\x39\xa3\xbc\x2a\x0b\x4e\xbb\x8d\x8f\x15\x2d\xbc\x78\x9f\xc6\x58\x28\x19\x6d\xe4\xd5\xb0\x6f\x37\xbc\xff\x30\xc6\x4f\x55\x40\x68\x3b\xce\xd6\x2b\x6e\xf8\xae\xfe\x68\x1d\x05\x7f\x49\xe5\x01\x69\x83\x27\xba\xb5\x2e\x54\x22\x8b\xaa\x0c\x86\x9e\x87\xba\x66\xf9\x36\xbf\xb3\x2c\x9f\x68\x24\xfe\xd3\xbe\x68\xec\x85\x2e\x82\x1d\x7d\xce\x1a\x6a\x68\xbd\xcd\x3e\x89\xb7\x79\x1f\xee\x52\x36\x92\xc2\x5c\xb5\xd1\x97\x65\xf2\xd0\x15\xe0\xd2\x6d\x21\x8d\x0a\x82\x79\x9e\x37\xd9\xe7\x2e\x65\x11\xd3\xec\xc6\x58\xe7\x83\xbe\x94\x5c\xf3\x43\x99\x64\x68\xbb\x8f\x9a\xbc\xe7\x72\xf2\x63\xd8\xed\xce\x8a\xc4\xf2\x90\xe9\x75\xda\xea\xee\xa7\x77\x34\xae\x31\x73\x55\x3b\xba\x03\xd8\x97\x60\x87\x5d\x2a\x5b\xea\xc5\xe5\xb2\xca\xa9\xa0\x3b\x13\x70\xba\x81\x80\xf7\xc7\x10\x92\xe6\x70\xde\xb7\xb3\xc0\x41\xb3\x98\x8f\xbd\x8e\xa2\x14\x24\x97\xc5\x1f\x54\x0c\x1b\x13\xc3\xef\xe3\x90\x0a\x3e\xdf\xc3\xa6\x8d\x9d\xb4\x1f\x57\xae\x1f\x54\xb6\x01\x8f\x49\x4e\x58\xd0\xe6\x72\x17\xa5\xa3\xad\xcc\xed\xf6\xb9\x0f\x05\x73\x98\xed\xe5\xfe\xba\xe5\x99\xb3\xdb\x79\x2a\x96\x79\x18\xbc\x2e\x49\x02\x52\x41\x2a\xf6\x5b\xc2\xef\x43\xb0\xe4\x70\x32\x63\x30\x3e\x85\x33\xab\xeb\x55\x2b\x67\x47\xde\x87\xc0\x34\x93\x35\xc1\xb9\xc4\x1c\x01\xea\x34\x02\xd5\xa3\x35\x21\x47\xc4\x7a\xc3\xd7\x0d\xea\x3b\x78\xf4\xac\x60\xbb\xaa\x79\xc9\x17\x5b\x4c\x74\xd9\x23\x92\x9a\x02\xdb\xb6\xca\x8d\x11\xb4\x65\xe8\xc6\xe6\xfa\xa3\x63\x0f\x06\xed\xa1\x0d\x0d\xb6\x0c\xed\xe5\x0d\xed\x60\x25\xba\x76\x82\x64\x4f\x59\x8b\x57\xcf\x8c\xac\xde\x66\x45\x52\xde\xaa\xe9\x9c\xab\xca\x76\x4b\x6b\x2c\x66\xad\xec\xd6\x3e\x53\xae\x95\xfc\xd4\xd8\x73\x68\x94\x1a\x08\xbe\xd3\xcb\xe6\x89\x9a\x21\x61\x6a\xf0\xe2\x6a\xe1\x4b\xac\xfa\x03\xcf\x3d\xc7\xea\xde\xe4\x2a\x39\x87\x51\x33\x83\xaf\xf5\x6d\xa6\xed\xd4\x56\x57\x09\x5e\x93\x19\xcd\x3d\x0b\x00\xe3\xba\xbc\x21\x39\x7e\xff\x80\xbe\x7b\xae\x6f\xfe\x38\xae\x0e\xac\x85\xac\x00\xb7\x9b\x22\x8a\xaa\x92\xdb\x8d\x09\x12\x3b\x8a\xc4\x85\x1a\x55\x35\x4f\xc3\xc0\x84\xa8\xe4\xe2\x52\x7d\xf7\x21\xb0\x51\x29\xad\xcb\x79\x4c\x2a\xfa\xf2\xfc\xcd\x6b\x8d\xe7\x05\xfe\xb2\xd1\xd0\xb5\x7f\xa0\xcf\xcd\xec\x82\x93\x24\xbb\x81\x38\x27\x9c\x4f\x3f\x06\xaa\xf8\x63\xd0\x0c\x65\x30\xf9\x54\x66\x45\x18\x9c\xcc\xd8\x69\x30\x54\xc3\x27\xd9\xcd\x69\xb0\x95\x98\xca\x79\x7f\x5e\x9e\xf3\xb7\xca\x45\xbd\x91\x9c\xc2\xb4\xd0\x35\x91\x21\x8e\xb4\xe9\x07\x03\x1c\xf5\x73\x70\x7c\x1f\xf1\xb7\x52\x7f\x3b\xf9\x7b\xe8\x6f\x49\x3e\xfd\x18\x58\xba\x18\xfa\xca\xf2\x8f\x81\x0d\x4d\xa0\x06\x96\x1f\x7a\x36\xfb\xd3\x3e\x32\x8e\x14\x0d\xd7\x81\xe3\xa3\x50\x1d\x76\xf3\x67\xff\xa4\xbd\xbf\x96\x96\xe8\xce\x6d\x48\xa9\x56\x2c\x36\x7d\x91\x97\x44\xe8\x7a\xb3\x28\x33\xfe\x96\xbc\x95\x65\x43\xe7\xf2\x46\xb0\xff\xaa\x98\x07\x23\x08\x0e\xf4\x6f\xfc\x0e\xb7\x59\x9e\xc3\x8c\x2a\x60\x89\x5c\x4e\x25\xbc\x25\x6f\x61\xb6\x72\xe1\x0f\x23\x38\x4f\xa9\x01\x15\x93\x62\x20\x64\x27\xcc\x37\xa1\xc9\x08\x78\x89\x09\x9f\x20\x52\xba\x04\xc2\x61\x41\x2a\x0e\x61\x51\xe7\xf9\x30\x72\xdd\x4f\xe6\x46\xdd\xda\xf3\x54\x6f\x25\x8a\x97\x48\xd6\x36\xda\xef\x75\x23\x54\x24\xa7\x42\x98\x53\xed\x99\xbe\xe0\x17\x3d\x2d\xf3\x92\x45\xef\x55\x65\x73\xc4\x46\xb3\xd3\x31\x05\xa4\x0c\x2d\x89\x60\xd9\x5d\xe0\xab\xa8\xc6\xfc\xd2\xc9\x06\x19\x87\xa2\x14\x50\xce\x41\xb5\xc7\xd8\xda\x03\x78\x9f\x53\xc2\x29\x50\xbc\x38\x43\x20\x2e\x19\xa3\xb1\xc0\x34\x71\xca\x79\x56\x16\x51\xe0\x27\xd8\x28\x39\x5f\x37\x3e\x31\x62\x72\x2f\x98\x8d\x2a\x36\x7a\x53\xf0\x76\x8c\xe8\xd8\x7e\x53\x52\xdc\x04\x89\x04\xd7\x6b\x15\x0d\x1c\x64\x8d\x5d\x14\x3a\xba\x64\xac\x9e\x63\x57\x55\x71\x27\x76\xdf\xb2\x6f\x4c\x50\xaa\x51\x4d\x48\x1d\x5f\x25\x34\x03\x37\x89\x1b\x16\xb0\xad\x73\xb3\x01\x35\x29\xdc\x51\x26\xf8\x39\xf2\xba\x4f\xf4\x6f\xff\xa0\x23\xb8\x0a\x51\x71\x9f\x52\xce\x02\x52\x3f\xad\x41\xe4\xcf\xdd\x44\x85\x4d\x2e\x0e\x2f\xdd\x5c\x81\xd5\xc4\xd9\x1b\x71\x65\x2a\x68\x17\x47\x97\x4d\x1c\xd7\x26\x37\xac\x87\x8d\x79\x9d\xcb\xc3\x89\x96\xc0\x08\xbf\x86\xaa\xc7\xba\xc9\xf8\x43\xd3\xaf\x93\x3e\xc0\x9d\x85\xab\x92\x9c\x90\x63\x1c\x15\x20\xc9\x73\x58\x66\x9c\x4b\x6b\x5f\x1e\xe0\x79\x73\xbb\xa9\xa0\xb7\xd6\xca\xd4\x2a\x53\x2d\x83\xd2\x31\x9f\xad\x12\x15\xce\xb6\x6f\x5d\x0a\xc7\x20\xe0\xc4\x2f\xa7\x45\x22\x4b\xf7\xdb\xad\x69\xe5\x65\xa4\x3e\xc9\xf3\xf2\x16\xa1\xcf\xa5\xd2\x90\xe8\x55\x65\x56\x08\xc8\x0a\x12\xc7\x35\x23\xb1\x0d\x2d\xa3\xf5\xa2\xcc\x5e\x1b\x7e\x94\x38\x3e\x7a\x04\xaa\xf8\xa2\x2a\xf9\x65\x74\x07\x27\x72\xdc\xce\xb0\xea\xd0\xef\xb2\xd3\x4e\x5c\xa9\x74\x07\x88\x63\x9e\x56\x25\x5e\xf4\xd4\x8c\x6a\xdb\xea\x2d\x10\x9f\xef\x26\x20\x46\xa0\x73\x86\xd6\xc3\x6e\xcc\x13\xc0\xde\x0a\xb6\x7d\x1b\xc6\x36\xae\x69\xb2\xa3\xfd\xd7\xb9\x72\x7d\xaf\xf3\xdf\x66\xdb\x1a\x0a\x1a\x27\x91\x6f\x86\xe1\x7d\x15\xbc\x10\x4d\x8a\x15\x08\x46\x62\xca\xa5\x9a\x22\x05\xd0\xbb\x4c\x5d\x76\x44\x35\x1e\xf9\xf7\x27\x1a\x0f\xa1\x33\x5c\x73\xf9\x22\x4e\xb3\x3c\x61\xb4\x08\x87\x3d\xf1\xe3\xa6\x6d\x2b\x8b\x10\x2b\xf0\x3a\x87\x57\xb1\x6e\xdf\x0b\xd1\x79\x15\xda\x6c\x09\xd4\x85\x90\x53\x93\x3c\x71\xdc\xbe\x18\xd2\x6a\xae\x6f\x84\x74\xdb\x37\xe8\x77\xae\x88\x6e\x6b\x84\x43\x35\xee\x52\x5a\x24\xda\x59\xba\xd1\x9f\x28\x29\xff\xb4\x2c\x6e\xe4\xda\x15\x25\xfc\xf8\xf6\xd5\x2f\x78\x94\xe2\x82\x2c\x2b\x73\x45\xd4\x39\x1b\xef\xee\xb3\xfe\xf2\x05\xbe\xf9\x4e\x8f\x70\x94\x9a\xdb\xca\x51\x8f\x27\xd7\xa0\x79\x60\x07\xb2\xd3\xdc\xae\x77\xde\x93\x04\x13\x35\x74\x06\xf9\x6d\x26\x52\xc8\x8a\x9b\x8c\x67\xb3\x9c\x42\x20\x57\x45\xa0\x14\x26\x07\xa2\xae\x80\xc6\x65\x31\xcf\x16\x35\xa3\x09\xdc\x1d\x48\x26\xc0\xac\xac\x8b\x84\x20\x00\x5a\xf0\x9a\x51\x6e\xc0\x8b\x94\x08\x25\x79\x1c\x08\xa3\x90\x64\xbc\xca\xc9\x4a\x5f\x2a\x05\x02\xf3\xec\xae\x81\x83\x54\xf0\x6e\x56\x15\xa4\xaa\x30\x01\xa6\xc4\xa1\x6d\x3a\x89\x85\x2f\x27\x6e\xba\x61\x93\x26\x57\xbd\x51\x3f\x17\x87\x52\xcb\x9c\x36\x54\x73\xa2\x87\x8a\x46\x75\x81\x37\x56\x51\x1f\xd8\x56\x1d\xbd\xb0\x6e\xc3\xf5\xb5\xdb\x01\x1c\x29\x6d\xa6\x39\xd2\x19\xc5\xaa\x1c\xdd\xa0\x77\x80\xe6\x0a\xda\xdb\xf2\x16\x62\x46\x89\x50\x17\x5e\xa5\x6d\xe3\x2f\xe2\xce\x53\x06\xae\xf5\xa3\x52\xe3\x15\x06\x3a\xaf\x63\xe2\x08\xbf\xdd\xff\xd4\x55\xd5\x49\xe3\x70\x77\x16\x36\x9e\xf1\xd5\xcd\xd5\x70\x38\x42\x75\x3c\xd2\xc7\xcf\x44\xa4\xf7\xf4\xf9\x59\xd6\xa3\xdb\xe7\xbf\x0f\x47\xf0\xd8\xf6\x53\xa7\x32\xca\x26\x3d\x37\x21\xbe\xd7\x69\x35\x01\x4c\x20\xc8\xb3\x82\x1a\x37\x28\x9e\xfe\xaa\x32\x27\xda\x9f\x21\xeb\x08\xd3\xbe\x4f\xe3\xb3\xb0\xf2\xae\x8a\x97\x99\x6c\x49\x6a\x51\x06\x23\x8f\xa8\x2f\xb2\x22\xc1\x5b\x10\x9c\x6a\xc9\x1c\x70\x58\x92\xbb\xf1\x32\x2b\xf6\x36\xdc\xd1\x90\x4a\x57\xb0\xda\xbd\x25\xfd\x73\x4a\x0b\x73\x19\x43\xda\x85\xea\xc6\x65\x62\xf7\xe2\x25\xb9\x6b\xf6\xe2\x7b\xd6\xa2\x68\x3c\x2c\x56\x5a\x64\xff\xb8\x66\x4c\x95\xbf\x71\x21\x01\x34\x1d\x36\x40\x94\xa5\xef\xe5\x8e\xdc\xf6\xee\xd9\x8a\x68\x05\xa7\xad\x01\x1e\x3d\x02\xb7\xfa\x41\xdb\x76\x44\x53\xa7\x85\x92\xd3\xa1\xc7\xff\x68\xb7\x52\x49\x89\xfd\xa9\xdf\x5b\x4b\xbb\xbb\x61\x78\xb2\x1c\x29\xf2\x2d\xc9\xdd\xd7\x47\xd1\xe1\xb7\x9b\x9b\x65\x85\xa1\x8d\xb7\xd3\x23\x07\xb0\xee\x55\x31\xcf\x8a\x4c\xac\x8e\x5b\x9c\x39\xf0\x2b\x7e\x27\x87\xfe\x3d\x4c\x38\x41\x1c\x77\x21\xbd\x9a\xcb\xbd\x04\xef\xe3\xf1\x72\x47\xce\x2e\x77\xe7\xe7\xda\xb9\x47\x86\x58\x4d\x91\x4d\xed\x74\x8c\x7e\x66\xc2\x7e\xe3\x49\xdd\xc8\x4d\xf9\x79\x60\xda\xf5\x5d\x06\xdb\x0c\x3c\x3c\x8c\x8e\xbe\x56\x01\x43\x32\xe3\xa1\x2c\x3c\x90\xf0\x86\xcd\xa1\x64\xcb\xb0\x5b\x21\xac\x8d\x53\x4d\x8a\xd2\x9d\x36\x4d\xba\x7a\x37\x42\xf3\x07\x7d\xdf\x9f\x95\x96\x99\xf4\xa9\x6c\xe7\x86\xc7\x6a\x0b\xac\x5f\xb5\x2a\xdf\x08\x4c\xe9\xbd\x92\x65\xb4\x10\x56\x53\xd2\xb9\x49\x59\x14\x59\x7c\xfd\x42\x5f\x1a\xb5\xf0\x5f\x64\x77\x42\x6e\xd7\xd1\xdb\x7a\x39\xa3\x2c\x52\xb7\x4a\xff\xfe\xe6\x87\xf3\x51\xcf\xbe\x81\x28\xea\x7d\xc3\xbd\x1a\xe2\x93\x53\xbf\xe1\xd1\xcc\x2c\x2d\x6f\x28\x7b\x46\x05\xc9\xf2\xfe\xf9\xbd\x6c\x1a\xec\x36\x49\x85\xa6\x9f\xd5\xac\xf6\x81\x11\xdc\x8d\x60\xe5\xab\x52\x9d\x73\x32\x38\xe1\x15\x29\x8c\xf9\x28\x0b\x03\x4c\xe9\xb5\xe1\x8a\x3b\xf8\x1a\x8d\xba\x61\x24\xca\x1f\xcf\x9f\x2a\x67\x4f\x38\x54\x19\xbd\xb2\xef\xe9\xe0\xd8\x01\xcb\x6f\x89\x88\xd3\x2e\x60\x9c\xc7\x95\xaa\x0d\xd4\x05\xb6\x69\x30\x23\xf1\xf5\x82\x49\x33\xe9\x40\x9f\x18\x55\x36\x31\xaa\x10\x2c\x91\xc3\x48\x6b\xb6\x3b\x50\x5c\x16\x82\x16\x78\x8c\x53\x43\xee\x83\x9e\x6d\xd4\xe7\x63\x43\x63\x4d\x39\xda\x26\xe0\x3a\x1d\x57\x7a\x26\x3a\x0d\xde\x0c\xe1\x64\xd7\x60\x83\x19\x43\xb2\x98\x51\x9d\x22\xed\x29\x6e\xfc\xaa\x3e\x1a\x5d\x1b\x06\x3d\x14\xe6\xa2\x76\x0f\xe3\x5f\x63\x5d\xaf\x8d\xa2\xba\x59\x23\xe5\x5e\x81\x70\x46\x73\xb2\xbb\xfb\x87\xfc\x81\xa6\xe4\x26\x2b\x59\xa4\xd5\xf7\x4b\xd3\x21\x84\x9d\x44\x4f\xe1\x35\xd1\xbf\xfd\xc1\x79\x4a\xf3\x1b\x69\xad\xee\x34\xf2\x39\x5a\x0c\xbb\x09\xfc\xa6\x51\xdd\xd0\xb5\x7d\x29\x61\xab\x63\x9c\x67\xbf\xfd\x91\x63\xa8\xaf\xba\x1e\xb4\xfc\x4b\x3d\x9a\xc0\x1e\x14\x6c\xec\xfb\x8f\x9a\x8d\xf7\x58\x0a\x8d\xba\xd9\x21\xfd\xae\x27\x2f\x61\x4b\x76\x40\x3f\x4d\xe4\x79\x5b\x63\xa1\xef\xda\x72\xa8\x08\x3e\x96\xe3\x5e\xc5\x9d\x97\xcc\xda\x88\xea\x10\x84\x4e\x54\xe7\xfe\x2d\x27\x37\x74\x4f\x9f\x94\x9c\x5b\xb7\x4f\xfe\xf6\xe4\x17\x30\xc1\x43\x79\xb2\x29\x59\x42\x99\xba\xb0\x7b\x60\xfd\xa4\x90\x09\xe5\xca\x75\xc6\x54\xc0\x6e\xa5\x75\x2a\x21\xd6\x9c\x32\x79\xe8\x92\x67\x26\x75\x1d\x00\xf1\x71\x9f\xaa\xb0\x97\x75\xb5\x0f\xd2\x3b\x3c\xf6\x5f\xf2\x45\x87\xec\x56\x17\x45\xaf\x27\xf5\x6d\x89\x68\xa2\xcb\x88\xc3\x5c\x6a\xc4\x96\x77\xb4\xeb\x2b\x38\x27\x33\xff\x8e\xb6\x7b\xf9\xd6\x89\x1a\xd9\xcb\xc0\x3b\x49\x41\x2b\xd7\xa3\x95\x2e\x48\x76\x92\x03\x95\xc6\xd5\xdc\x22\xbe\x1f\x4b\x97\xd2\xca\x47\x6e\x82\x26\x3f\x94\xc9\xca\x90\xda\x01\xe7\x3f\x1e\x73\x85\x77\x20\x41\xcc\xca\x44\xdf\x76\xc7\x7e\x5e\x96\x17\xbf\xcd\x44\x9c\x86\xad\x68\xb7\xc2\x3f\x26\x9c\x42\x70\x43\x63\x51\xb2\x60\xb2\xe7\x9a\x8c\x7e\x58\xda\xe7\xa0\x19\x46\x3b\x4a\x82\x13\xc1\x4e\x4f\x44\x02\x71\x99\xcb\xbd\x6a\x3a\x78\x3c\x38\x3d\xc9\x4e\x0b\xc5\xd8\x93\x71\x76\x7a\x32\x16\x89\xfc\x60\xa7\xcd\x65\x8f\x76\xa6\x6c\x7f\xfe\x77\x4f\x88\xdc\xbf\x5c\x88\x3c\xd0\xb6\xaa\x6e\x78\x91\x5d\xba\xbb\xa5\x0d\x40\xf5\x79\xa9\xad\x93\xfa\xf8\xbe\xa9\x9d\xb6\x42\x71\x0a\xa4\x0e\x98\xc9\xa9\xe9\x26\xda\x09\x7d\x71\x74\xd9\x54\xb9\xb3\x56\xf3\xc4\xab\x38\xc7\x96\xfe\x3a\xd2\xf0\xff\x31\xfd\x6f\xfe\x38\xfd\x6f\xda\xf4\xb7\xb7\x20\xce\xe9\x9d\xb4\x70\x02\x1b\x96\xb0\xe8\x7d\x52\xe8\x7d\x82\x13\xb8\x31\x5e\x7f\x83\xdb\x27\xff\xe2\x69\x03\x69\x7f\x6a\x1b\x5f\x7c\xba\xd4\x1c\x82\xff\x2d\xb9\xe6\x96\x1f\x2a\xce\xcd\xd8\xf8\x34\xf0\x5d\xbf\x7f\x52\x34\x1c\x4c\x76\x96\x0c\x1d\x97\x51\x92\xd1\x3f\xba\x6a\xe2\x8d\xe4\x72\x62\x93\x20\xb6\x07\x42\xcb\xf6\xfe\x81\xb0\x89\x37\x90\x33\x6b\x7f\xcc\xe1\x96\x41\xb5\xeb\x72\xd2\xbb\x1f\xfc\x58\xf0\xba\xaa\x4a\x26\x68\xa2\xaf\xb3\x60\x4c\xad\x03\x64\xeb\xd6\xce\x36\x3c\x08\xda\x77\x35\xbc\xfd\x6a\xa0\xe7\xa7\x76\x6c\xaa\xb3\xfe\x62\xdf\xd4\xb2\x77\x08\xdd\x08\x19\x92\xaf\x41\x80\x16\x22\x13\xab\x37\xea\x8a\x2c\x4e\x2c\x78\x14\x4c\x20\x78\x44\x96\xd5\xb1\xb9\x53\x76\x82\x25\xb9\xb0\x05\xa7\x58\xb0\xb0\x05\x83\x60\x30\x81\xc1\xa3\x7f\xd5\xa5\x38\xd6\x17\x5d\x83\x41\x20\x8b\xbe\xfa\xe6\x2f\xb6\x64\xac\x4a\xee\x1e\xbf\x38\x1e\xd8\xe7\x64\xb4\x91\xaf\xcf\x34\x1a\xbd\xe6\xa6\xed\xc5\xa3\x93\xd3\x60\xf0\x71\x7c\x39\x5e\x8c\x9c\x4b\x91\xbc\x75\xaf\xc0\x4e\xe3\x82\x5f\x9a\x18\xc8\xda\xe3\xca\x7b\xd2\x77\x19\xa5\x79\x0e\xd6\x84\xac\x5a\xcc\x94\xdd\x5a\x6f\x7f\xf6\x73\x12\x81\x34\xb7\x01\x11\x30\xba\xd3\x7f\x3c\x7b\xdd\x84\x31\xdc\x56\xbd\x3a\xd5\x6b\xa0\xbc\xb2\xeb\x26\x5f\xc6\xab\x35\xae\x1d\x1c\x8a\x24\x89\xb2\xca\x41\x3f\x2c\x8b\xd2\x14\x7c\x45\x92\xe4\x4a\x3f\x68\xa5\x9f\x5b\xf0\x9a\xab\x17\xc0\x64\xd1\x08\x3e\xaf\x87\x5d\x0b\xa5\x35\x7f\x33\xa3\x2e\x0d\xe4\xec\x74\x8a\x4d\x5e\xc6\x78\xcc\x8f\x38\x25\x4c\x3d\xbf\x18\x04\x2d\x86\x99\x40\xb3\xa6\x1e\x66\x0d\xbe\x37\x29\xab\xfd\x70\x22\x5e\xcf\x94\x7c\x84\x47\xc3\x88\x57\x79\x26\xc2\xc1\xa3\x81\x4d\xb2\x6e\x60\xbc\xa4\x79\x65\x8f\x59\xed\xc9\xfc\xa3\xd5\x2c\x74\xc3\x65\x6d\x18\x6a\xc2\x4d\x17\x1e\x3a\x98\x6e\xa5\x96\xa1\xb2\x4b\x2d\xf3\x64\xa8\x2f\x38\x5d\x5c\x95\xc9\x88\x24\x7b\x68\x9f\xeb\x74\xde\xdc\xd3\x4e\x15\xfd\x98\xa9\x52\x98\x92\xb3\xca\xe0\xfc\xf1\xec\x75\xc3\xda\xa1\x53\xad\xf4\x49\x8b\xf7\xc3\x3d\x80\x61\xf3\xae\xb0\x5a\x0f\x4a\xfa\x9a\xe8\xd4\x43\xcd\xde\xa1\x3e\xa7\x75\xd3\xa7\x4c\xc8\xcd\x9e\xe2\x9a\xe7\x6f\x24\x9d\xc6\x63\x78\xfb\xee\xfc\xf9\xa4\x75\xb1\x78\x46\xe1\x9a\x56\x02\xaf\x8f\xaf\x8a\x58\x85\x5f\xc6\xb5\xc8\xf2\x31\x17\xcc\xfc\x8e\xcb\xe2\x26\x5a\x94\x13\x84\xfb\x3a\x2b\xae\x5f\x94\xec\xb9\x4d\x63\xb8\x87\x07\x96\x1e\xfd\xcb\x16\xd9\xa9\x94\x8f\x59\xb5\x7a\xfa\x5e\xfc\x7e\xa1\xd6\x16\x5e\x90\x75\x73\x1e\x5a\xab\x5e\x51\xa0\xb9\x16\x6c\x02\x8f\x7f\x5a\x3c\x1d\x10\xef\x66\x9f\x68\x2c\x95\x50\x47\x56\x17\xb4\xa0\x8c\x08\x25\xae\xaa\x99\xa7\x70\x0c\xfe\x5e\xc6\xc7\x43\x15\xd8\x0e\x1d\xd8\x26\xb7\x4d\xbd\xfc\xa9\x52\x8a\x1e\xe9\xe7\xe4\xd2\x8c\x8b\x92\xad\x50\x38\xe4\x11\x84\x86\x9f\xd7\x23\x08\x82\x11\xa8\x30\xe9\xf7\x72\x43\x76\x88\xba\x75\x8d\x38\x02\xe9\x72\x48\xc9\x5d\x8f\x8e\x76\x59\xa4\x5f\x68\x68\x3a\x0d\xe1\xb3\x9e\xd6\x02\xdd\x00\xd8\xae\x27\xed\xb3\x97\xd2\x2d\x01\xd9\xa5\x4b\x5b\x33\xfe\xc3\x53\x63\x16\x9a\xab\x33\xac\xe4\xe1\xc1\x99\x26\x7e\x17\x9c\x9d\x9a\xd6\xab\xe2\x86\xe4\x59\xd2\xa3\x76\xd4\x63\x08\xae\xda\x52\xdd\xa8\x88\x0d\xab\x5f\xb0\x72\xf9\x4e\x0d\xa0\x01\x74\x87\x1b\xc1\xe1\x8e\x94\x89\x9a\xd1\x95\xa3\x16\xa6\x30\xfe\xe7\xe2\x63\xb2\xff\x31\x8a\xf6\xa7\xd1\xfe\xc3\xf1\xef\x23\x56\xcf\x0c\x5d\x7a\xa1\x44\x9e\xd7\x55\x6e\x22\x1b\x7a\x9a\x4e\x79\x87\xf7\x4d\x5d\x6b\xa7\xf9\xdd\x93\x8b\x04\xe5\xc2\x85\x77\xdc\x9f\x3b\xbc\x75\x92\xf7\xf1\x63\x83\x78\x8c\x94\xc8\xbe\x6a\xf4\x8c\xdc\x57\x9d\x06\x8d\xd1\xd0\xd8\x0c\xfd\x5b\x6a\x85\x8f\x66\xbf\x9b\x4b\x6d\x8b\xf0\xbc\x57\x53\x10\x9a\x7a\x57\x3b\x74\x86\x34\x7b\x69\x81\x5e\xf7\x77\x73\x35\xe8\x8b\x92\x49\x28\x66\x91\xba\xe8\xec\xcc\x86\xa6\x42\xe5\xf9\xf0\x9f\x33\x91\x86\x1d\x24\x35\xb1\x6d\x1a\xba\xa6\xc0\x7d\xf8\x6c\xa7\xc4\xb6\x49\x48\x5b\x22\xa6\xe1\xe1\xe8\x9e\x79\x2b\xf5\xd7\x0b\xaa\x5b\xe8\x6f\x1e\x3b\xd1\xc4\xda\x36\x1d\x92\x68\x5a\xb8\x4f\xc9\xf9\x2f\x49\x34\xb6\xa6\xb3\xba\xdf\xcd\xdf\x15\x7a\x17\xee\xe2\x67\xf9\xac\x80\x3c\x89\xe3\x7a\x59\xe7\x44\x60\xee\xf9\x0e\xca\x64\x83\xc4\xc2\xbe\xbe\xf3\xd6\x01\x6b\xd3\x18\x9a\xf7\xd6\xdb\x6f\x2d\x38\xad\x7f\xf7\x52\xdb\x3c\xf9\xed\x6a\xd8\x7b\x90\x03\x7c\xe1\xee\x44\x5c\x5d\x26\x36\xbd\xe5\x49\xfb\x49\x91\x98\xb4\x59\xa1\x38\xaa\x0c\xd4\xe9\xc0\xd9\xc0\x9b\xe6\xf6\x5f\x4c\xb8\x7d\x2f\x0e\xd5\x93\x1d\x6e\x63\x03\x34\xa1\x71\x99\xd0\x1f\xcf\x5e\x3d\x2d\x97\x55\x59\xd0\xc2\xd0\xd2\x03\x70\x74\xd9\x1c\x9d\x3e\xee\xcb\x33\x53\x00\xc1\x70\xa8\xa1\xca\x95\xe4\xa2\x30\x85\x40\x90\x99\x93\x9d\xec\x0f\x69\x1f\x7f\x70\x8a\xd5\x63\x70\x82\xcc\x20\xe3\x98\xfe\xb0\xa0\x4c\x3b\x0e\x5c\x83\xf4\xa2\x19\xe6\xd2\x4e\xf5\x27\xf3\x76\xc7\xba\x87\xfd\xdd\xa7\x36\xb6\x31\xbd\xad\xc7\x5c\x56\x3b\x86\x9a\x1e\x25\x58\x48\xcb\x24\xd3\x62\x1a\x44\xdd\xd4\xf2\x6d\xe3\xf5\x98\x57\x1d\x8b\xa5\x65\x69\x59\x29\xab\x0c\x86\xfd\x1a\x38\xf3\x94\xaf\x6f\xe6\x29\xb1\x54\x5f\xa3\x6b\xba\xe2\xde\x48\xc3\xae\x90\x5e\x37\x8f\xdb\x3b\x90\x2e\x34\x0a\xfb\x70\x4d\x57\x97\xc6\x56\xd5\x50\x2e\x64\x59\x27\x77\xd0\xe9\xdd\x72\x28\xc8\x63\xb0\x36\xa2\xd5\x1d\xc2\x0f\x54\xd4\x95\x0e\xa6\xc4\x24\x4e\xe9\x44\xbd\xd5\xd7\x30\xdb\xbb\x6b\xd8\xfb\xbc\x1d\x17\x44\x64\xf1\xf8\x13\x1f\xab\xc3\x8e\xfd\xdf\x10\xa9\xf9\x7f\x11\xdf\xdf\x4c\x25\x13\xbd\x7f\xf2\xa0\x73\x6d\x3a\x37\x0a\x13\x22\x88\xc4\x50\x4b\xb6\xf7\x8f\x1b\xb4\x9b\xd0\xf8\xd5\xec\x3f\x79\x40\x81\x57\x3d\x4d\x9d\xca\x67\x7f\x46\x2b\x46\x63\x22\xa8\x3a\xcf\xe1\x91\xde\xcf\xe6\x4d\x32\x46\x63\x71\x5e\xbe\xc9\x16\x52\x46\x12\x7b\xea\x87\xbe\x5c\x4f\xfc\x9f\x39\xca\x21\xd1\x73\x06\x08\x9d\x9c\x51\x14\x4a\x45\xee\x6e\x06\xa8\xf6\x72\xe0\xd1\xea\x3c\xa5\x9c\x82\xb8\x2d\xf5\x35\x4e\xde\x8f\x37\x26\x18\xf5\xa2\x3b\x94\x50\x08\xa3\x40\x92\x84\x26\x50\x16\xf9\x0a\x5d\x9d\x33\x12\x5f\xdf\x12\x96\xe0\x7d\x3d\x22\xb2\x59\x96\x67\x62\x25\x4f\x6e\x65\x9e\x28\x19\xd1\x61\xef\xc8\x11\x90\x5e\x92\x6d\x74\x14\xa4\x84\xa7\xf7\x58\x36\xcd\xb3\x8f\x66\xf3\x53\xda\x30\x79\xc1\xc8\x62\xa9\x22\xd0\x3d\xfa\xb1\x6f\x14\x15\x9d\x60\x2b\xcb\x0c\xbc\x00\xa7\x19\xef\x03\xd5\x7b\x72\x78\x34\x54\x4a\x2f\x61\x65\x85\x81\x2a\x09\x07\xbe\xc2\xcc\x9e\x18\xc3\xde\xa1\x93\x50\xd7\x45\xb9\xb1\xd2\x99\x54\x7f\x6b\x67\x1d\x6d\x90\x1b\xab\x36\xfe\xdc\x34\x7b\x0e\xa8\x7f\x66\xb6\xfd\xaa\xa9\xed\x95\xf2\x2c\x9f\xd2\x57\x87\xcd\xbe\x69\xf5\x61\x8f\x5a\x96\x6d\x5c\x75\x57\xee\xa2\xe9\xee\xd7\x75\x65\x4b\xcd\x81\xf7\xaf\x29\xec\xc4\xf0\x4a\x74\xff\x71\xb8\x45\x64\x89\xf9\xb8\x75\xe0\x45\xd6\x3e\x0c\xe5\x62\x1d\x1e\xef\xfd\x9f\x00\x00\x00\xff\xff\x83\x18\xa2\xe7\x17\x6b\x00\x00") func webUiStaticJsGraphJsBytes() ([]byte, error) { return bindataRead( @@ -421,7 +421,7 @@ func webUiStaticJsGraphJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "web/ui/static/js/graph.js", size: 26952, mode: os.FileMode(420), modTime: time.Unix(1495632771, 0)} + info := bindataFileInfo{name: "web/ui/static/js/graph.js", size: 27415, mode: os.FileMode(420), modTime: time.Unix(1496733731, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -446,7 +446,7 @@ func webUiStaticJsGraph_templateHandlebar() (*asset, error) { return a, nil } -var _webUiStaticJsProm_consoleJs = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xdc\x3c\x7f\x77\xdb\xb8\x91\xff\xe7\x53\x4c\xd8\x36\xa4\x2c\x99\x94\x93\xdd\xbd\xab\x1c\x65\x5f\x36\x71\x36\xb9\x4b\xbc\xb9\xc4\x69\xbb\x55\x74\x7e\xb0\x08\x49\x88\x29\x92\x05\x20\x5b\x6a\xe2\xef\x7e\x0f\x03\x80\x04\x28\x4a\x96\xdd\xeb\xde\xbb\xe6\x0f\xc6\x04\x06\x83\xc1\xfc\xc2\x60\x30\x54\x72\xf0\x00\x0e\xe0\xd5\x32\x9f\x48\x56\xe4\x02\x64\x01\x0b\x72\x49\x81\x49\xa0\x44\x30\xca\x55\xcb\x35\x67\x92\x42\xc9\x8b\x05\x95\x73\xba\x14\x30\x29\x72\x51\x64\x54\xf4\x40\x2c\x27\x73\x85\x81\x08\x98\x71\x52\xce\x45\xfc\x00\x14\xca\xe4\xc1\x83\xf7\xbc\x58\xbc\xd0\x80\x30\x84\xaf\x37\xc7\x5e\x53\x7c\xba\x5c\x5c\x50\xfe\xaa\xe0\x0b\x22\x25\xe5\x06\x64\x07\x44\x5c\x72\x3a\x65\x2b\x2a\x7e\x62\x33\x18\xc2\x28\xb8\x0c\x7a\x10\xbc\x53\x8f\x9f\xd5\xe3\x4c\x3d\xde\xab\xc7\x89\x7a\xfc\x55\x3d\x7e\x0d\xc6\x7b\xe3\x3c\xea\x3f\xfe\x4e\xe3\x65\x88\x18\x9f\x3f\xe3\xf3\x0c\x9f\xef\xf1\x79\x82\xcf\xbf\xe2\xf3\x57\xb6\x2f\xfe\x8f\x0b\x92\x65\x88\x7d\xa1\x06\x2e\xd5\x23\x57\x8f\x52\x3d\xa6\xea\x41\xd4\xe3\xef\xea\xb1\x56\x58\x3d\xb4\xe7\x42\x72\x56\x9e\x71\xc2\x32\x96\xcf\xfe\x4a\x79\x01\x43\x98\x1a\xa9\x45\xab\x0e\x7c\x7d\x00\x00\xc0\xa6\x10\xad\x62\x96\xa7\x74\xf5\xcb\x34\x0a\x68\xd0\x81\xe1\x10\x0e\x8f\x6c\x3f\x40\x92\xc0\x1b\x19\x0a\xc8\x0b\x09\x82\x4c\xa9\x12\x2f\xe2\x56\x63\x99\xea\x11\x13\x46\x73\xc9\xa6\x6c\xa2\x80\x88\x9a\x20\x36\x83\x39\x95\x4b\x9e\xc3\x2a\xe6\xb4\xcc\xc8\x84\x46\xc9\xe7\xf8\xc7\xfe\xc1\xef\x93\x1e\x84\x61\xe7\x18\xa1\x6e\x1e\xb8\x90\xc7\x0f\x94\xd8\x93\x04\x5e\x2f\x17\x24\x67\x7f\xa7\x40\x20\x47\x16\xc5\x3b\xd9\x36\xb7\xe0\x9b\xab\xbc\x22\x5c\xa1\x87\x21\xec\x42\x70\x6e\x31\x44\x48\xcf\xaa\xb7\x13\xda\xd1\x82\x1e\xc2\xef\x2d\xd2\x1e\x1c\xf5\xfb\x7d\x5c\xfb\x0a\x86\x8a\xb0\x51\x7f\x7c\x6c\xc8\xd4\x90\xa6\xf9\x08\x9b\x95\x84\xde\x11\x39\x8f\xc9\x85\x50\x2b\x7a\x0a\x95\x70\x2a\xee\xca\xe2\x64\x55\x16\xb9\x92\x02\xc9\xa2\x27\x1d\xe8\x1a\x4c\x0a\x81\xe2\xaf\x81\xdc\xad\x20\x91\x42\xf4\x8a\xad\x68\x1a\x3d\xe9\xb8\x38\xb6\x49\xa4\x07\x69\x91\x87\x12\x96\x82\xc2\x82\x65\x19\x4b\x16\x6c\xc2\x8b\x84\xca\x49\x0c\x76\xd1\xfb\x89\xed\xb4\x40\xe6\xbc\xb7\xeb\x6f\xca\xf0\x76\x2e\xec\xb1\xb6\xf7\x9c\x4e\x98\x50\x58\x9f\x74\x3a\x96\x35\xbf\x89\x76\x8c\xc6\x77\x15\xfb\x3f\x47\x62\x70\xcd\xe4\x1c\xd0\x6f\x11\x01\x72\x4e\xe1\x82\x08\xda\x03\x4e\xe4\x5c\x79\xee\x39\xc9\x91\xce\xfd\x84\x66\xfc\xdf\x6f\x6c\x6f\x6a\x56\x97\xab\x8f\xbf\xfb\x17\x31\x26\x8d\x97\xe5\x33\x20\x39\xd0\x15\x99\x48\xe0\xb4\xe4\x54\xd0\xdc\x7a\xd5\x7d\xa4\x72\x82\x23\xff\x1f\xb9\x41\xc3\x4f\x2d\x3c\xe8\x56\xe2\xba\x65\xff\xaf\x49\xf5\x16\xdb\x03\x97\x28\x68\x4c\x3a\x25\x13\x59\xf0\x9a\x21\x95\x8e\x04\x81\xd5\x8f\x15\x0c\x87\x43\xe8\x5b\xbd\x48\x0e\xe0\x65\xa1\x36\xb6\x39\xcb\x67\xb1\x8a\x53\x00\x6e\x80\x66\x82\x6e\x68\xd3\xb3\x61\xad\x4e\xd3\x82\x43\xa4\x66\x60\xc3\xfe\x31\x30\x78\xea\x92\x15\x67\x34\x9f\xc9\x39\x3c\x7a\x04\x8d\xf1\x9a\xbe\x63\xe8\x76\x59\xbd\x05\xaf\x20\xa9\x7a\x4c\x53\x45\xb7\x83\x75\xc4\xc6\xf5\x8e\x6a\x48\xbc\x8d\x18\x64\xcb\x16\x72\x9e\xc2\xd1\x06\x21\x07\xb7\x12\x82\x18\x7d\x52\x6a\x19\x8f\x2a\xf1\x18\xf9\x7a\x02\x3e\x63\x0b\xfa\xa2\xc8\x25\x2f\x32\x57\xa4\x7a\xfe\xb4\x98\x2c\x17\x34\x97\xf1\x8c\xca\x93\x8c\xaa\x3f\x7f\x5a\xbf\x49\xa3\x40\x45\x99\xe7\x18\x4a\x9e\xa7\x4b\x8e\x86\x72\x2e\xe6\x9c\xe5\x97\x41\x27\x2e\xf2\x49\xc6\x26\x97\x30\x04\x39\x67\x22\x4e\xe9\x84\x53\x22\xe8\x4b\x03\x18\x5f\xb0\x3c\x8d\x54\x17\x6a\xe2\x9d\xe6\x98\xf1\xe2\x7a\x73\x06\x96\xff\xc3\x33\x48\xb6\xa0\xe7\x17\x64\xb2\x83\xfe\x93\x3c\xbd\x2f\xe2\x69\xc1\xaf\x09\x4f\xb7\x53\x7e\x3f\xdc\x9c\x4e\x39\x15\xf3\xf3\x8b\xa5\x94\x45\xbe\x89\xdd\xf4\x37\x30\xeb\x45\x19\x56\x19\xd4\x30\xbc\x93\x1c\x82\x1a\x11\xcd\xd3\xbb\xe1\x40\x86\xd0\x3c\x0d\xb6\x12\x13\x17\x39\xcb\xcb\xa5\xac\x04\xc0\x44\x49\xe4\xa4\x75\x1d\xf5\xf4\xff\xfc\x51\x86\x9d\x7f\x22\xd9\x92\xde\x6d\xcd\xbe\xa0\xce\xaf\x14\x06\xb5\xfe\x6a\x87\xc0\xee\xb7\x4c\xdc\x15\x21\xcb\x25\xe5\x57\x24\x13\x9a\x9b\x0e\xb2\x37\xb6\x07\x8f\x33\xbf\x4c\xf1\xf0\x72\x84\xc7\x9a\xef\xf1\x79\x64\xfe\x9b\x07\xe8\x36\x36\xdd\x55\x13\x91\xf1\x58\x9e\x7b\x52\x03\x32\xe6\x12\xad\xf4\x59\x5a\xf6\x44\x41\xc6\x02\x73\xe4\xc8\x58\x53\x3d\x05\x95\x1f\x9a\x1a\xda\xdb\x98\x76\xc4\xc6\x35\x06\x49\x57\x52\x79\x2b\xcd\xfa\x16\xd0\x63\x13\x57\x54\x0c\x8d\x49\x59\xd2\x3c\x7d\x31\x67\x59\x1a\x65\xcc\x84\xa0\xdb\x34\x0f\x25\xd3\xd8\xad\x1d\x0f\x19\x97\xbc\x90\x85\x5c\x97\x54\x09\x07\xc3\x09\xeb\x73\xa2\x8d\xad\xd8\x1d\x77\xce\x72\xa6\x42\x1b\xd4\x9d\x7a\xda\x8e\xdd\xff\x1e\xee\x39\x90\xe6\xa9\xea\x3d\x2d\xae\xad\x04\x9a\xfa\x7c\x8f\x15\x10\x69\x02\x0f\x80\x9c\x5e\x03\xbe\xdf\x8d\x1e\x38\xd0\x91\x85\xe1\x6e\x33\x88\x70\x11\x28\xf3\x7f\x85\xbb\x99\xd2\x4c\xb5\x86\x60\x1d\x0c\xe0\x87\x3e\x1c\xe8\xc7\xe3\xef\xe0\x00\x9e\xfc\xf0\xbd\x8a\x6d\x82\xeb\xcd\xae\x7f\xc3\x8e\xb4\xd1\x81\x8d\xf3\xba\x11\xdf\x17\xf8\x8e\x7f\x8a\x60\x00\x47\x3b\x09\x13\x92\x96\x7a\x55\xca\x62\xd4\x98\xa3\xbe\xd8\x62\x34\x4f\xfa\xd6\x76\x7a\x10\x3c\xc6\xe7\x0f\xf8\x3c\xd2\x2f\x47\x29\x76\xa4\x01\xce\x7d\x74\x8d\x6f\xf8\xfc\x0e\x9f\xff\x8e\xcf\xa3\x35\xb6\xaf\x83\x07\xcd\x44\x42\xbb\xc0\xce\x05\x95\xaf\x89\x98\x6f\xee\xd3\xca\x0c\xad\x4e\x59\xeb\x2a\x09\xaf\x77\xc4\x68\xbb\xb6\x57\x8e\xc3\xca\xd2\x8c\x57\xde\x27\x4f\x51\x19\x3a\x90\xa0\x7c\x15\xe4\x35\xcb\xd3\xe2\x3a\xce\x8a\x89\xde\x6a\xe7\x9a\xa0\xe0\x77\xe5\x44\x4e\x02\xe8\x02\xcd\x27\x45\x4a\x3f\x7d\x78\xf3\xa2\x58\xe8\x90\x3e\xfa\x8f\x8f\xbf\x9c\xc6\x2a\x5a\xcf\x67\x6c\xba\xd6\xba\xf6\xd5\x12\x33\xa8\x28\xef\x59\x12\x06\xf6\x8f\x1b\xa5\x51\xbb\x84\xe6\xab\x63\x3b\x63\x0c\x85\x6d\x84\xdb\xa5\x23\x23\x84\x35\x46\xd5\x53\xa5\x67\x42\x5c\x58\xd8\xf1\x03\xd4\x2b\x3b\x1f\x2e\x0d\x59\x1d\xa5\x74\x63\xe1\x88\x49\x2c\x2f\xf4\xda\xa3\xef\x3b\xc6\x42\xdc\x38\xb1\x42\xe5\x70\xe4\xc9\x0f\xfd\xbe\xc3\x8d\xfe\x8d\x3d\x06\x29\xf2\xae\x7c\xd3\xdb\x24\xab\xee\xaa\xed\xb9\xa3\xe4\xa9\x1a\x3d\x61\x36\x47\x9c\x16\xd7\x4a\xfc\x7c\x49\x1b\xe7\x2e\xcb\xa0\x9b\xa8\xb3\x97\xaa\x7a\xca\xe7\x8a\xc5\xae\xf1\x8c\xae\xe4\xa6\xee\x7e\x38\x31\x34\x7f\xa0\xb3\x93\x55\x19\x05\xff\x1d\x8d\xfa\x87\x7f\x1c\x77\x3b\xd1\x68\x7d\x9d\xce\x17\x62\xfc\x63\xe7\xf7\xf5\x5e\xb7\x50\x5b\x35\x32\xcf\xc5\x1b\x63\x73\x54\x23\xad\xdd\xac\x19\xd0\x81\xaf\x76\x65\x8a\xd5\xc7\x55\x52\xc2\x7a\x4f\xa4\xff\x4d\x2e\x23\x33\x60\x74\x34\xae\x26\x5d\xe6\x4c\x6d\x3e\xb6\xe7\xf1\x18\xbe\x7d\x83\x50\x84\xc7\x0d\x76\xc1\xc1\x56\x27\xec\x38\xc1\x91\x42\xd7\x72\xf0\xda\x6f\xd7\x69\x63\x6d\xcd\x56\x85\x1a\xbd\x99\xbf\xc3\x5f\xd2\x35\xb0\x7c\x1f\xe2\xac\x5e\x21\xa2\xb8\x5c\x8a\x79\x34\xda\x67\x4d\x97\x74\x3d\xee\xa9\x79\xc6\x55\xca\x47\xa3\x10\x05\x97\x51\x45\x31\xe9\xc1\x85\x23\x8a\x0b\x75\x14\x3d\x04\x32\xea\x8f\x8f\xe1\xa6\xe3\x47\x25\x30\x04\x13\x97\x68\x4c\x2d\xc1\x88\x92\x70\xe5\x05\xff\xa0\xe1\x46\x6c\xac\xb0\x7a\xc6\x5b\x89\xa9\x86\x4e\x5c\xe8\x0e\x74\xeb\xd7\xa3\xf6\x43\x95\x1d\xb9\xaf\xe0\x9a\x47\x94\xdd\x3e\xfc\x23\x9d\x14\x79\x2a\xee\xe5\xca\xdb\x58\x76\xfb\x9e\x57\xf1\x93\x75\xbb\x6d\xfc\xb4\x14\x3d\x6d\xa3\xe8\x76\xf4\x2a\x88\xab\xd9\x6f\xa3\xbf\x3b\x22\x38\x76\x87\xdb\x40\x3d\xaa\x9a\xb5\x64\x5c\x69\xed\x27\x9a\xe6\xf9\xf4\x37\x12\xcd\xde\x32\x81\x43\x38\x52\x62\x7c\xa6\xc5\x79\x78\xb8\x4b\x3e\xcf\xfe\xf5\xe4\xe3\x10\xb2\xdd\xdd\xed\x8c\xe6\x6b\x63\x35\x80\x36\x92\x8a\x6e\x09\x2f\x3c\xc7\x6b\xa2\xa1\x4d\xf5\x50\x72\xd8\x12\x86\x0f\x87\x10\x86\x8d\x6c\x67\xbe\xcc\x32\x37\x11\x9e\x12\x49\xdf\x13\x2e\x2b\x9d\x6a\xa2\x89\x45\x99\x31\x19\x25\xa3\x43\x18\x8c\x93\x4e\xed\x84\x14\x39\xf1\xa7\xb3\x17\x51\x85\x62\xd4\x1f\xf7\x6a\x84\xa3\x23\xe5\x4f\x8f\xdc\x96\xc7\x5e\xff\x13\xef\xed\xbb\xf1\x5d\xd8\xf1\x0b\xff\xb8\x83\x27\x76\x61\x6d\xd1\xa4\xdd\x8d\x55\x7f\x83\x37\xaa\xc9\xf2\xc6\x0c\xaf\x43\x98\x4a\x7c\xa2\x46\x86\x38\x9c\x9d\x57\xbd\xd7\xa1\xce\x9d\xb7\xd5\xc6\x5a\x6a\x0a\x31\x90\x2c\x96\x78\x68\xb1\x73\x7c\x3a\x7b\xf1\x5a\x35\x45\x98\xb6\xeb\xc3\x8f\x10\xf6\x43\xe8\xb6\xf5\x0f\x5a\x1a\xab\x20\x86\xe5\x4b\x49\x1b\x88\xdf\xe9\xc6\x1d\xa8\x6b\x88\x41\x6b\x73\x0b\x53\x3e\x9d\xbd\x78\xb5\xcc\xb2\x5f\x29\xe1\x91\xda\xe4\x82\x43\x15\xb0\x47\xee\xe8\x22\x97\xf3\xa8\xd3\x3d\xaa\xbb\x9d\x5e\x73\x18\xe8\x42\x00\x01\x74\x8d\x59\x6b\xae\x74\x21\x18\x28\x68\xb3\x98\x7d\x19\x2f\x5a\x55\xa8\x66\x7b\x23\x4f\x10\x61\x22\xa3\x35\x8f\x63\x6d\xdd\x6a\x5b\x7d\xb4\xad\x54\xe4\x9e\xa6\xef\xe4\xe8\x36\xf5\x3c\x49\xe0\x8d\xe9\xaf\x4f\x60\x8f\xbf\xff\x03\x70\x92\xcf\x28\x3c\x82\x49\x91\x5f\x51\x2e\x61\x81\xb7\xf6\x22\x6e\xd1\xe1\x4a\xc3\x2d\xed\xae\x69\x21\xbf\xef\xb6\xd5\x98\xd3\x78\xf2\x1d\x74\x9c\x1c\x9b\xe3\x8f\xef\xb6\x2f\xb6\xae\xfb\xae\x6b\x38\xfc\xbf\x59\x83\x49\x04\x6d\xa1\xbf\x45\x7d\xc2\xb0\x55\x53\xee\xcf\x44\x33\xe2\x9f\x17\x54\x98\xa3\xfb\x2e\x47\x8b\xdd\xc3\x21\xee\x3c\xd6\xe1\xea\x21\x6d\xe7\x43\xeb\x7f\x37\x02\x49\x3f\x8e\xac\x32\x91\x33\x26\x24\x5f\xb7\x05\x90\x6a\x30\x42\x35\xc2\x9d\xc6\xd0\x2a\x6d\x87\xcd\x6a\xf5\x64\x51\xaf\xd8\xd9\xbf\x0d\x9b\x5a\x80\xeb\x13\xaf\x5a\x97\x7b\xc0\xd5\x60\x7e\x34\xb2\x6f\xe4\x71\xde\x48\x30\xc2\xd0\x6c\xde\xfb\x79\xb6\x0f\x9b\xca\xe7\x07\x2d\x55\xe0\xb0\x31\xd1\xc3\x86\xb4\x4c\xf2\x62\x92\x51\xc2\x2d\x50\xfb\x50\x13\x6e\xb5\xa3\x1d\x7a\xc1\x87\x77\x5e\x7a\x38\x04\xed\x5c\x9d\xe0\xb2\xdd\x48\x1e\x7a\x41\x8d\x9f\x1b\x8f\xbc\xfa\x92\xbd\x55\xdc\xcb\x87\x6e\x27\xde\x70\x41\x50\xe9\xf3\xa0\x25\x69\xdf\xdb\x98\xf8\xa0\xbe\xfc\xbc\xd9\x91\xd1\x6f\xa4\x98\xfd\x73\x5e\x92\x00\x26\xe9\x8b\x29\x90\x2c\x33\xb5\x54\x3d\x58\x0a\x9a\xc2\xc5\x1a\xd4\x11\x58\x39\x7c\xa5\x0a\x8d\x1a\x8c\x86\xca\x9b\x43\xb9\x07\x82\x10\x2f\xe9\x94\x2c\x33\x69\x73\xa3\x74\x55\xf2\x01\x0a\xad\x67\x0b\x83\x4e\x56\x25\xa7\x42\x28\x99\xc9\xc2\xa8\x37\xbc\x20\x39\x5c\x50\x20\x90\x19\xf2\x74\xc6\x09\xb7\x9b\xbc\x48\x69\x03\xc7\xcb\x5f\xde\x61\xb3\xc2\x80\x35\x42\xc6\x4c\x97\x79\x4a\xb9\xad\x23\x72\xff\x25\x09\xbc\x2e\xae\x21\x2b\xf2\x19\x56\x30\x68\x70\x26\xa0\xb8\xa2\xbc\x07\x2c\x07\xa1\xd9\xac\x06\xd7\x79\xac\x3b\xa6\xc3\x7b\xed\x33\x9f\xcd\xa9\x3a\x8f\xaf\x90\xbd\xf5\xec\x54\x49\x95\x48\x35\x63\x95\x29\xdb\x77\x46\x33\xa0\x87\x09\xcd\x54\xce\x1d\xfe\xa8\xa5\x52\x36\x9b\x23\x1b\xeb\xd9\x52\x76\xd5\x03\xba\x9a\x64\xcb\x94\x29\x26\x30\x99\x51\x01\x24\x4f\x21\xa3\x33\x6a\x56\xde\x42\x7c\x25\x50\x59\x00\x59\xca\xe2\x30\xa5\x92\x4e\x6c\xbd\xd6\x1c\x67\x1a\xc0\xe3\x7e\xff\x1f\x9e\x7c\xc1\xf2\x01\x04\x6a\x8e\xc0\xe2\x7a\xc7\x72\xb6\x58\x2e\xe0\xd7\x43\xb2\x62\x42\xa7\xa5\x7a\x90\x3a\x24\x65\xc5\x35\x15\x52\x05\x79\x44\x77\x23\x26\xb2\x1a\xa0\x2e\x4c\x59\x4e\xd3\x1e\x62\x22\xab\x5b\x30\xcd\xd9\x6c\xbe\x89\x8a\x53\xa5\x52\x94\x0f\x20\xcc\x58\x4e\xc3\x9e\x96\xe8\xba\xa4\x6a\x85\xd6\x80\x8a\x52\xd7\x35\x12\x4e\x0d\x1c\x2e\x2e\x24\x9c\x92\x10\x75\x98\x2c\x9a\x3a\xfc\xe7\x39\x91\x6a\xde\x89\xb2\xc4\x32\x2b\xa4\xf0\xe9\x91\x7c\x8d\xbc\x2a\x20\x2d\xda\x45\x23\xb0\x52\x52\x01\xa9\x38\xa7\xc8\xc9\x45\x46\xb7\x48\xf1\xcd\x14\x88\xb1\xa9\x1e\x30\x19\x66\x19\x16\x60\xc9\x39\x91\x31\x8c\x46\x90\x91\x0b\x9a\xc1\x78\x0c\xd7\x2c\xcb\x94\x25\x62\xd2\x97\xc9\xa5\xa4\xe9\x2e\x94\x76\x63\x30\x38\x2f\x28\x2e\x87\xa6\xba\x66\x88\xc0\x82\x94\x8a\x4f\x97\x74\x8d\x6b\xd2\x69\xd8\x2d\x66\xa2\x38\x26\xe6\xc5\x32\x4b\x6d\xdc\xaf\x14\x48\x71\x4e\x0d\x5d\x8a\x6d\x6b\xdb\xe6\x3b\x12\x4b\x9c\xe8\x01\x25\x93\x39\x50\xed\x21\xdb\xb1\xd8\x85\x93\xb2\xcc\x18\x4d\x51\x02\x73\xaa\x05\x03\x53\x5e\x2c\xf0\x75\x52\x70\x4e\x45\x59\xe4\x4a\x8f\xdb\x11\x99\x59\xac\x01\x28\x0f\x88\x94\x29\xea\x57\x67\x4a\xf3\x07\x10\x28\xe3\x0d\x7a\xae\x83\x40\x9b\xb0\x83\x56\xa0\xb4\x54\x8d\x58\x7f\xca\x99\x14\x03\x08\x7c\x68\x9d\x19\x35\xd0\xeb\x1a\xda\xe2\xdf\x81\xbb\x86\x4e\x12\xd0\xe5\x31\x2a\x54\x32\xf5\xb1\x2a\x68\x72\xf0\x3d\x5f\x31\x51\x15\xcf\x0c\x76\x56\xea\xd8\xc2\x9a\xde\x4e\xcc\xe6\xb6\x60\xae\xfc\x2e\xa4\x54\x12\x96\xe1\x44\xaf\x55\xc3\x1d\x67\xc2\xa2\xa5\xde\x46\x2c\xf4\xb3\x89\xd8\xaa\xa0\x45\xc7\x58\x7a\xc3\xaf\x93\xc8\xcd\x14\xb2\xb7\x7d\xb9\x51\xc4\xc3\x08\x61\x0d\x96\x3a\x6e\xd0\x0d\xa3\xcb\x71\x23\x3a\xf4\x10\x8d\x2e\x1b\xf9\x57\x8c\x4b\xd6\x25\x2d\xa6\x60\x63\x3f\xa5\x21\xc3\x21\x04\x5a\x6f\xab\x08\xc6\xeb\x86\x91\xf3\x3a\x3e\xde\x8a\x0c\xcd\xc5\x41\x06\xdf\xbe\xc1\x16\x08\xcb\x9f\xc0\x0d\x77\x75\xaf\xc9\xb8\xb7\x27\x63\x1d\x42\xda\x82\x66\xed\xec\x74\xb2\xdd\x99\xd2\x0b\xac\x3c\x52\x10\xde\xbf\xd6\xd6\xfd\xfa\x1e\x83\x2c\x84\x53\xc1\x80\xde\x38\x3d\x47\x27\x5d\x47\x83\x49\x02\xff\x49\x69\x09\x04\x38\x9d\x52\x4e\xf3\x09\x05\x51\xa0\x7b\x83\xe9\x92\x63\x9d\xe2\xb2\x54\x07\x69\x01\x11\x8d\x67\x31\x90\xdc\x96\x1d\x8b\x0e\x4c\xb4\x07\x59\x90\x94\x6a\x64\x2a\x16\x52\x56\x26\x28\x57\xa2\x97\x73\xca\x38\x48\xba\x28\x33\x85\xa2\x3a\x03\x73\x36\xb9\x14\x73\x72\x6d\x35\xce\x92\xb3\xeb\x98\x81\x7c\x31\x75\x19\x6a\xb2\x03\xc5\x90\x03\x38\x53\xce\x1b\x32\xb2\x2e\x96\x72\xa0\x9b\xbe\x19\x73\x86\x6f\xa0\x27\x80\x6f\xb6\xc3\xfc\xfb\x66\x1c\x4a\xdd\x91\xe8\xfd\xf6\x1b\xbc\xc5\x7d\xd5\x74\x24\xe6\x98\x25\x71\x92\xed\xc5\x0e\xd8\x6f\xd2\x13\xb8\x99\x4c\x32\x22\xc4\xa9\x96\x92\x57\x03\x83\x80\x0a\xce\x4a\xb2\x48\xa9\x57\xa9\x80\x10\xd5\xf9\x4e\xf2\x5d\xb3\x72\x77\x4a\x0f\x09\xaf\x30\x68\x56\x9c\xa5\xbb\xf0\xd8\x92\x1c\xee\x21\xb1\x23\x1b\xa8\x5e\xb2\xab\x3d\x70\xd9\xc1\x2d\x18\x5f\xb2\x2b\x07\xe4\x25\xbb\xda\xca\xad\x35\xfa\xdf\xc0\x07\xf6\xc3\x74\xc3\x46\x23\xf0\x2e\x58\xcb\xd1\xee\x1f\x7e\x84\x00\xa2\x00\xba\xe0\x35\xc7\x92\xb3\x85\xce\x67\x75\x02\x50\x5e\x5f\xeb\x94\x3e\x47\xab\xa9\xef\xc3\x2f\x77\x74\x9d\x39\x30\x0d\xfe\x1a\xad\xfe\xeb\x75\x06\x1b\xc0\x18\x9a\xd6\xab\xc3\xd7\x0d\x20\x1d\x42\xd6\x50\xfa\x5d\x2f\xe4\x1f\x52\x9b\xc6\xba\x76\xb1\xa1\xd2\x8d\xd5\xbd\xd5\x6c\xe5\xaa\x99\x7d\xd9\xaa\x12\xab\x5a\x25\x2a\xd8\x56\x8d\xd0\xbd\xff\x0b\xdc\xa8\x72\x19\x6f\x59\x7e\x79\x9f\x05\x3a\x83\x37\x11\x3e\xdf\x81\x8f\x68\x74\xce\xf8\x76\xbc\xcf\x7d\xb0\xe7\x5b\x99\x97\xb1\xfc\x32\x68\xc0\xfa\xcc\x0b\xba\xcd\xfe\x39\xa7\xd3\xd6\x2c\x8e\x38\x2b\x3e\x66\x44\xcc\xd1\xc5\x7e\xfa\xf0\x36\x72\xb6\xb7\x6a\x9d\xfa\x94\x72\x1f\xae\xd9\x91\xb5\x25\xe9\x96\xdd\xee\x27\x65\x57\x1a\x9b\x1d\xbe\xd5\x94\x2a\x80\x0d\x13\xae\xe6\xd1\x4e\xc1\xe4\x1e\x48\x9a\x9e\x5c\xd1\x5c\xbe\x65\x42\xd2\x9c\xf2\x28\xe4\x54\xb0\xbf\xab\x83\x4d\x23\xbf\xa7\xa2\x8b\xa8\x65\xd7\x6d\xa6\x76\xea\x74\x87\x82\x6a\x19\xe1\xec\xfc\x37\x4e\x86\xc3\xf1\x55\xbb\xd2\x92\x3f\xeb\x34\x59\x9d\xd3\xc2\xcc\xcb\x9f\x4c\xba\xb3\xa2\xd9\x64\x5d\xbf\xd6\x75\x10\xb6\x0a\xe2\x55\x56\x10\x19\xd5\xf9\x46\x15\x33\x31\x71\x4a\x4e\x55\x5b\x15\xce\x25\x09\x04\xdd\x37\x39\x56\x19\x1e\x9a\xff\xf1\xbd\x3a\x18\x20\xb2\x14\x58\x2e\x0b\x38\x25\xa7\x2a\x46\x70\xf0\x77\x62\x15\x69\x5b\x54\x13\x92\x87\x52\x0d\x42\x15\x53\x47\x50\x51\xa8\xb3\xcd\xb5\x0a\x25\x16\xf8\x19\x1b\x29\x05\x44\xc8\xc7\x78\xdb\xe5\x5a\x5d\x8c\xb1\x07\x5b\xa8\x98\x90\x92\xbe\x3e\x7b\xf7\xd6\x65\x8b\x8e\x02\x6b\xbe\xd0\x5c\x32\xb9\x7e\x47\x4a\x93\x9f\x01\x08\x1e\x05\x03\x08\x1e\x91\x45\x79\x1c\xe8\x83\x59\xf0\x14\x5b\x32\x59\x35\x3c\xc3\x86\x59\xd5\x10\x06\xe1\x00\xc2\x47\x7f\x5b\x16\xf2\x38\x34\x30\x61\xa0\x9a\x7e\xf7\xe4\x8f\x55\x4b\xa2\x5b\x56\x8f\x5f\x1d\x87\x6a\x45\x28\x6f\xb3\x26\x4d\x57\xfd\x85\xd7\xe8\xd1\xd3\x67\x41\xf8\x39\x19\x27\xb3\x5a\x11\x21\x12\x8d\xeb\xb5\x8a\xfc\x91\xd0\x31\xf0\x3e\x0a\xa3\x95\xb1\x71\x3f\x43\x6a\x9e\x08\x9a\x4d\x4d\x5a\xaf\xfa\x0a\x84\x64\x54\x56\xb7\x77\x1f\xcc\x36\x17\xbf\x28\xb2\x82\xc7\xef\x75\x67\x7d\x01\x26\x28\x67\xd4\xd6\xa9\x3c\x30\xa7\x2e\x26\x2a\xcd\xc1\xf4\x5a\x91\x83\xb6\xb4\x78\x5b\x30\xab\xfe\x3b\x7e\x60\x6b\x6a\x55\x50\xfc\x6a\x99\x4f\xea\xfa\x97\x2a\xa3\xe9\x07\xf2\xbe\x35\xaa\xa1\x93\x79\x51\x08\x5c\xb1\xe7\xee\x74\xf3\xa9\xc1\x5b\x33\x62\x7b\xa4\xef\xce\xb6\x3b\xdc\x47\x4a\x75\x6c\x6b\x66\xef\xdc\xfa\x59\x41\xfb\x3c\x78\x78\x68\x9d\xc7\x39\xec\x34\x07\x8c\xd8\xb8\xed\x04\xd5\x42\x5d\xa5\x03\xac\x07\x0b\x2a\x39\x9b\xb8\xc0\xed\xdf\xe9\x60\x91\x72\x59\xa8\xd8\x5f\x71\x6f\x43\x08\x23\x36\xae\x90\x1d\x57\xb8\x6e\xdc\xa2\x60\xd6\xa9\x7a\x3c\x6e\xb4\x50\xd8\x82\xbd\x1e\xeb\x1c\x25\xb5\xa2\xfd\x4c\x25\x1e\x55\x50\x87\xd0\x37\xa9\x37\x8e\x21\x95\x3e\x7b\xc7\x9e\x96\xbe\xa5\x39\xf2\xdd\x3d\x0a\x53\x2d\x09\x0a\x4f\x11\x4d\xc5\x7d\x5a\x73\xbf\x4d\x6a\x0a\x76\x44\xc7\x31\x8e\xe1\x54\x2c\x33\xd9\x2e\x38\x3d\xf3\xa8\x22\xa0\xdb\x1d\x57\xee\xc7\xfe\x53\x48\x06\x6d\x28\x47\x6c\x1c\x9b\x22\xbf\x05\x29\x6b\xf9\xb9\x35\x70\x5f\x57\x03\xd0\xa5\x03\xeb\x01\x9a\xb3\xbb\x4b\x44\x58\xfc\x76\x73\x0c\x37\x1d\x3f\xf1\x34\x51\xd6\x3c\xb0\xb6\x1e\xe3\x6b\xd4\x80\xd1\x79\x3b\x8d\xb2\xf6\xb0\x51\x25\xb2\x11\x1d\x47\x5b\x88\x36\x0a\x51\x21\xbc\xf1\xf3\x00\x7a\xd3\xc4\x0b\x11\xf4\x58\xf5\x6d\xd7\x43\xcd\x28\xc3\x4b\xd7\xb0\x29\xe7\x05\x3f\xa3\x2b\xb9\x4f\xe8\x00\x35\xb8\x17\x49\x85\x4e\x24\x85\x10\x61\x13\xda\x8f\xa5\xc2\xd3\x02\x2f\x06\x8c\x97\xd3\x2c\xa7\x69\xe8\x5c\x75\xd8\x50\xde\x8d\x41\x2a\x74\x9d\x63\xc7\x83\xdb\xad\x2d\x49\xe0\x03\xb5\xc9\x7a\xf7\x9a\xcd\xf3\xb7\x9a\x31\xae\xa2\xd4\xa6\x88\xf9\xf9\x20\x63\x39\x25\x3c\x70\x85\x66\xf2\xe1\x1e\x61\xc5\x74\x2a\xa8\xfc\xb3\xea\x71\x41\x6d\xf6\xda\x35\x39\xdd\xe6\x42\x99\x84\x9e\x8f\xb2\xe7\xf9\x0c\x9b\x23\x76\x11\xd9\x56\x17\x12\x13\xd3\x2e\xd0\x82\xac\xbc\x7e\x96\x37\xfa\x99\x77\xa9\xa0\x65\x30\x30\xff\x9b\x0d\xd0\x16\x77\x5c\x51\xfe\x12\x13\x6a\xad\x6c\x8c\x5f\xd7\x00\x15\x4b\x71\x31\x03\xfd\x9f\x9d\xa7\xc8\xb5\x64\x06\x9b\x01\xa1\xd5\xc3\xd5\x5b\x4c\x17\xdb\x72\x1f\x73\xf1\x54\x7f\xee\x21\x7e\x5a\xbf\xb0\x1a\x17\x05\xab\x73\xcc\x2e\x07\x1d\xf3\x99\x65\x8d\x87\x49\xba\xd8\x17\x8b\x82\x6d\xa0\xc0\x0f\xef\x90\x14\x57\xc0\xd0\x05\xaf\xf1\x2d\x9d\x4a\x5b\x82\x60\x27\x71\x7a\x9e\x99\x2b\x28\xbf\x4b\x63\xfa\xf6\xcd\x55\x3d\x49\x17\x8d\x79\x9c\xa6\xfb\xcf\xe2\xef\x3f\x86\x74\x34\x58\xfd\xf9\x47\xea\x7f\x39\x83\x72\x3e\x9f\x66\xac\x2c\x69\x1a\x38\xfb\x8d\xa1\xf0\x1e\x23\x37\x76\xa4\x16\x32\x38\x5d\x14\x57\xf4\x9e\x94\xdc\x65\xf0\x8d\x75\x97\x56\x1f\xd7\x4e\x56\xb8\xd2\xc8\xb5\xcb\xb5\xea\xb6\xa0\xb6\x9b\x46\x3e\x59\xc1\x77\x7d\x00\xcc\xa0\x54\xfb\xaa\x73\x2e\xf1\xac\x0a\x13\xe0\xed\xf6\xa4\x7a\xe2\x5f\x77\x9a\x92\x64\x93\x4b\x4d\x83\x6f\xd5\x7e\x5a\xdd\x9b\x70\x75\xcb\x84\x58\xe2\xb0\x6d\xce\x9b\xc6\x81\xb5\x1d\x8f\x4e\x0d\xee\x24\xdc\x77\x79\xd5\x49\xb2\x9a\x02\xa9\x34\x1e\x4e\x6f\x5c\xeb\x8d\x16\xad\xf0\x75\xcb\xb6\xa4\x29\xc2\xed\x11\xc6\xd7\x5b\xe5\x66\x2d\xca\xf5\x9c\x65\x14\xbc\xd4\x55\x9c\x11\x21\x71\x2b\xf2\xbe\x34\xb2\xbd\x5a\x25\x37\x33\x5e\xce\x30\xbb\x57\xb9\xc8\x2b\x56\x6c\x41\x5f\xf7\x6f\x4c\xd0\x36\xd4\xbf\xcc\x6f\xcd\x26\xdf\xca\x97\xd5\x9c\x8b\xb6\xeb\xf8\x26\xe0\xc5\x92\x65\xe9\x7f\x2d\x29\x5f\x7f\xe2\xde\xa7\xb2\x98\xe9\xa8\x3f\x6e\x76\x2a\x1c\x4c\xea\xdd\xc6\xc4\xcf\xcf\x5e\x9f\xbf\xff\x70\xf2\xea\xcd\x5f\xa0\x0b\x41\x42\x4a\x96\x5c\x1d\x25\x7f\x53\x28\xcf\xb1\x5c\xec\x47\xfc\x7b\x68\x6b\xeb\x5a\xbe\xa4\xd1\x73\x75\xcd\xc9\x53\x48\x5a\x0e\x31\xad\x19\x3b\x95\xec\xdb\x76\x6e\x67\x18\xe1\x12\xc7\x45\x65\x55\x39\x73\xe8\x20\xc1\x94\xe8\x23\x9a\xa7\x06\xb9\x81\xb9\x9d\x99\xdb\xab\x9d\xaa\x00\xf8\x8b\x0e\x80\xbf\xd8\x63\x0b\xf2\xbf\x8a\x7b\xbf\xd4\x71\x6f\xdd\x3b\xfa\x32\x8e\xc9\x45\xc1\x65\xe4\xfd\x80\x04\xc9\xb2\xea\x1e\x83\x5e\xc3\x73\xce\xc9\x3a\xda\x72\xe4\x72\xea\xff\x8c\xbc\xf7\x1b\x82\x12\xa5\x78\x5b\x79\xce\xe9\xdf\x96\x54\x48\xe1\x0b\xd8\x3b\xd8\x6d\x29\xbf\xdf\x7a\x0e\x6c\x7c\x49\xd9\xf8\x02\xcb\xaf\x6d\x3a\xae\xc0\x96\xbc\x8a\x1d\x3c\xa5\xdc\x58\x49\x7d\xf4\x41\xd7\x38\xe7\x66\xdd\x7f\x79\xf7\xf6\xb5\x94\xe5\x07\xbd\x20\x5b\xb3\xb3\x9a\xf3\xb8\x28\x69\x1e\x85\x33\x2a\xc3\x9e\x9a\xa6\x87\xdf\x02\x39\xfd\xfa\xf2\x56\x50\xbc\xc0\x1f\x42\xf8\x45\x14\x79\xe8\x0c\xcf\x31\x6c\xf5\x7e\x1b\x60\xce\xd5\xf1\xad\x91\xec\x6a\xc6\xed\x70\xbf\x08\xfd\xae\x31\xfa\x8e\x28\xfd\x04\x09\xcf\x0a\x82\x15\x16\x4a\xaf\x42\xaf\x9e\x7d\x9f\x18\x1d\xec\x4f\x2e\xc5\x59\x31\x8b\x5a\x50\xa2\x76\x84\x8d\xad\xd4\x97\x14\xb4\xe9\x5b\xdf\xf6\x25\x09\x14\x39\xda\x02\xcc\xa8\x14\x40\xf2\x35\xe0\x6b\x55\x7e\xd2\x66\x68\x4d\x8c\x9e\x9d\xed\xb4\xb5\x3a\xa2\xf0\x8e\xe3\x46\xa8\xae\xe0\xd5\x32\x77\xc9\x5d\x05\x9b\x9b\x2b\xf3\x3f\xcb\xd1\x27\xf2\x42\xe2\x6f\x71\x20\xf3\x2e\xe8\xb4\xe0\x14\xe9\x03\xb1\x9c\x4c\xa8\x70\xea\x6c\xdc\x0f\x0a\xea\xc0\xc7\x54\x9e\x2b\xb7\xe0\x6a\xec\xf1\x66\x16\xa4\x4a\xc7\x06\xc5\xc5\x17\x3a\x91\x5e\xde\xc3\xa0\x70\xbe\xef\xf3\xf4\xdf\x15\xfa\xcd\x36\xc1\x1d\x0e\xe1\xc8\x02\x59\x6f\x85\xa9\x16\x93\xab\xba\x0b\x67\x3c\xe7\x35\x72\xa2\x78\x2f\x7b\x6c\x67\xb9\x93\xf0\x84\x0a\x67\xbc\xd2\x3b\xd4\x04\x24\x74\x35\xe7\xd5\xd5\x32\x06\x46\x5a\x9b\xdf\x2c\x66\x3b\x2c\x94\x2d\x66\x26\xfd\x5e\x41\xc7\x82\x4f\x60\xd8\xd8\x03\xc3\x44\x48\x22\xd9\x24\x61\x8b\x59\x42\xbe\x90\xd5\xa1\x1a\x40\x79\x3c\x63\xd3\xb0\x31\x9e\x64\x68\xa6\x6f\x75\x4b\x1c\xc7\x4d\x80\x6d\xf6\x6f\x40\xc2\x8d\x8b\x33\xef\x9a\xa1\xc2\xd3\xa9\x2a\xfd\x3e\x56\x25\x3c\x98\x13\xd2\xc5\x8f\xc5\x14\x42\x3c\x8e\x85\x68\x69\x4e\xe9\x4f\xa3\xde\xaf\x91\xf4\x72\x4d\x23\x27\x0b\xea\x67\xcf\xf4\x77\xfb\x30\x84\x24\x8a\x0f\x7e\xec\x7c\x1e\x7d\x1e\x7d\x16\x07\xd1\xe7\xeb\x6e\xa7\xfb\x59\x1c\x7c\x1e\x7f\x1e\x63\x47\x32\x3b\xae\xa0\xc5\x52\x73\x04\x17\x66\x62\xac\x85\xd9\x7c\x39\x8d\xe9\x8a\x4e\x70\xa6\x4e\x9d\x03\x36\x43\xcc\x1f\x5d\xfd\x09\xe3\xe8\x68\xac\xfe\x44\x6a\x46\xba\xe5\xf1\x78\x5c\xf5\x3e\xf1\x8a\x25\x1e\xea\xb1\xcd\x2f\x5a\xaa\x02\x04\xe7\x87\x65\x14\x5c\xc5\xcb\x9f\xd9\x15\xcd\xeb\x44\x9b\x4d\x82\xd8\x42\x81\xe7\xef\xdf\xd8\x9f\xe2\x51\xb6\x4f\xca\x92\x17\x25\x67\x44\xd2\x8a\x6b\x0a\x8b\x2c\x2c\x50\x99\x15\x12\xa7\x6d\x96\x59\x6e\x26\x69\xdb\x13\xd7\x49\x02\x3f\xad\x6d\xbd\x58\xcf\x14\x73\xa9\xd9\xb2\xcc\xf0\x42\x97\x61\x34\xb2\xc9\x0e\x32\x88\xfc\xfc\xa7\x29\xc6\xd0\x8d\xf1\xf9\xb9\x7a\x3f\x3f\x57\x91\xd4\xd7\xa0\x91\x22\xd6\x2a\xc3\xf2\x8d\x0c\xaa\x62\x31\x76\x3a\x1f\x0d\xf7\x7b\x8f\xf1\xf7\xe1\x82\xf3\x73\xcf\x41\x4d\x8a\x5c\xb2\x7c\x49\x9b\x5e\x08\xe9\xe8\x0e\xcd\x24\x5d\x08\x86\x61\x50\x0b\x18\x5b\x95\x74\x83\xb0\x17\xb8\x25\x25\x8e\x28\x55\xef\x0d\x76\x62\xce\xed\x81\x2d\x57\x2b\xf2\x6c\x0d\x45\x4e\x0d\xea\x2b\xc2\x19\x15\xbd\xaa\x10\xae\x2e\xfa\xab\xd6\x58\x7d\x96\xfd\xf5\xe6\x37\xcd\x98\x6e\x72\x7a\x77\x92\xd1\x61\xab\xae\x58\xaa\xc6\x39\xcb\xe8\xf8\x99\x06\xa7\xc7\x72\xd5\xae\xd3\x17\x48\x1b\xe8\x68\x27\x3d\x06\x48\x21\x3c\x6a\x4b\x5d\xa3\x4c\xf0\xd3\xa1\x65\x26\x19\x22\xc6\x7c\x43\xcb\xb7\xbe\xad\x0b\x71\x8a\xb3\x7e\xc1\x9d\x2f\xbe\xa4\x6b\x11\x6d\x92\xd9\xb1\x9f\x20\x3e\x03\xe7\xd7\x09\x9b\xb3\xea\xbc\x3b\x8e\xe8\x6c\x16\x6b\x6d\x40\x1b\x94\x43\xe7\x87\x9b\x5a\xcc\x2b\x6a\x8a\xc6\x28\xa8\x75\x54\x0d\xac\xa3\xfe\xd8\x16\x8a\x35\x3c\x91\xc5\xdd\xf0\x45\x75\xb9\x23\xad\x0a\xaa\x45\x0f\x4a\x5e\xa4\xcb\x89\x76\x06\xa6\x18\x48\x85\xdb\x8a\x9f\x72\x4e\x17\x6d\x45\xdd\xcd\x1b\xf0\xe6\xd9\x50\x78\x5f\xc4\x91\x4d\x19\x39\x1a\x8e\xf0\x6d\xc7\x03\xd4\x13\xe4\xf3\xd7\x50\x01\x85\x03\x0d\x8b\x77\x27\xa1\x24\x17\xe1\x00\xfa\x37\x9d\xc6\xda\x9b\xdb\x2d\xd2\xfb\xbb\x70\xaf\x5f\x69\x40\x5f\xd9\x39\x7e\xa0\xd8\xf6\x3f\x01\x00\x00\xff\xff\xad\xde\xe8\x33\x80\x54\x00\x00") +var _webUiStaticJsProm_consoleJs = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xdc\x3c\x7f\x77\xdb\xb8\x91\x7f\xc7\x9f\x62\xc2\xb6\x21\x65\xc9\x94\x9d\xec\xee\x5d\xa5\x28\xfb\xb2\xf9\xb1\xc9\x5d\x92\xcd\x25\x4e\xdb\xad\xa2\xf3\x83\x49\x48\x42\x4c\x91\x2c\x00\xd9\x52\x13\x7f\xf7\x7b\x18\x00\x24\x40\x51\xb2\xec\x5e\xf7\xde\x35\x7f\x30\x26\x30\x98\x19\xcc\x0c\x06\x83\xc1\x50\xfd\xc3\x03\x38\x84\x97\xcb\x3c\x91\xac\xc8\x05\xc8\x02\x16\xe4\x82\x02\x93\x40\x89\x60\x94\xab\x96\x2b\xce\x24\x85\x92\x17\x0b\x2a\xe7\x74\x29\x20\x29\x72\x51\x64\x54\xf4\x40\x2c\x93\xb9\xc2\x40\x04\xcc\x38\x29\xe7\x22\x3e\x00\x85\xb2\x7f\x70\xf0\x9e\x17\x8b\x67\x1a\x10\x46\xf0\xf5\x7a\xe8\x35\xc5\xef\x96\x8b\x73\xca\x5f\x16\x7c\x41\xa4\xa4\xdc\x80\xec\x80\x88\x4b\x4e\xa7\x6c\x45\xc5\x4f\x6c\x06\x23\x18\x07\x17\x41\x0f\x82\xb7\xea\xf1\xb3\x7a\x9c\xaa\xc7\x7b\xf5\x78\xa1\x1e\x7f\x55\x8f\x5f\x83\xc9\xde\x38\x4f\x8e\x1f\x7e\xa7\xf1\x32\x44\x8c\xcf\x9f\xf1\x79\x8a\xcf\xf7\xf8\x7c\x81\xcf\xbf\xe2\xf3\x57\xb6\x2f\xfe\x8f\x0b\x92\x65\x88\x7d\xa1\x06\x2e\xd5\x23\x57\x8f\x52\x3d\xa6\xea\x41\xd4\xe3\xef\xea\xb1\x56\x58\x3d\xb4\x67\x42\x72\x56\x9e\x72\xc2\x32\x96\xcf\xfe\x4a\x79\x01\x23\x98\x1a\xad\x45\xab\x0e\x7c\x3d\x00\x00\x60\x53\x88\x56\x31\xcb\x53\xba\xfa\x65\x1a\x05\x34\xe8\xc0\x68\x04\x47\x27\xb6\x1f\xa0\xdf\x87\xd7\x32\x14\x90\x17\x12\x04\x99\x52\xa5\x5e\xc4\xad\xc6\x32\xd5\x23\x12\x46\x73\xc9\xa6\x2c\x51\x40\x44\x11\x88\xcd\x60\x4e\xe5\x92\xe7\xb0\x8a\x39\x2d\x33\x92\xd0\xa8\xff\x39\xfe\xf1\xf8\xf0\xf7\xfd\x1e\x84\x61\x67\x88\x50\xd7\x07\x2e\xe4\xf0\x40\xa9\xbd\xdf\x87\x57\xcb\x05\xc9\xd9\xdf\x29\x10\xc8\x51\x44\xf1\x4e\xb1\xcd\x2d\xf8\xe6\x2c\x2f\x09\x57\xe8\x61\x04\xbb\x10\x9c\x59\x0c\x11\xf2\xb3\xea\xed\x84\x76\xac\xa0\x87\xf0\x7b\xab\xb4\x07\x27\xc7\xc7\xc7\x38\xf7\x15\x8c\x14\x63\xe3\xe3\xc9\xd0\xb0\xa9\x21\x4d\xf3\x09\x36\x2b\x0d\xbd\x25\x72\x1e\x93\x73\xa1\x66\xf4\x18\x2a\xe5\x54\xd2\x95\xc5\x8b\x55\x59\xe4\x4a\x0b\x24\x8b\x1e\x75\xa0\x6b\x30\x29\x04\x4a\xbe\x06\x72\xb7\x81\x44\x0a\xd1\x4b\xb6\xa2\x69\xf4\xa8\xe3\xe2\xd8\xa6\x91\x1e\xa4\x45\x1e\x4a\x58\x0a\x0a\x0b\x96\x65\xac\xbf\x60\x09\x2f\xfa\x54\x26\x31\xd8\x49\xef\xa7\xb6\x77\x05\x0a\xe7\xbd\x9d\x7f\x53\x87\x37\x4b\x61\x8f\xb9\xbd\xe7\x34\x61\x42\x61\x7d\xd4\xe9\x58\xd1\xfc\x26\xd6\x31\x9e\xdc\x56\xed\xff\x1c\x8d\xc1\x15\x93\x73\x40\xbf\x45\x04\xc8\x39\x85\x73\x22\x68\x0f\x38\x91\x73\xe5\xb9\xe7\x24\x47\x3e\xf7\x53\x9a\xf1\x7f\xbf\xf1\x7a\x53\x54\x5d\xa9\x3e\xfc\xee\x5f\x64\x31\x69\xbc\x2c\x9f\x01\xc9\x81\xae\x48\x22\x81\xd3\x92\x53\x41\x73\xeb\x55\xf7\xd1\xca\x0b\x1c\xf9\xff\xc8\x0d\x1a\x79\x6a\xe5\x41\xb7\x52\xd7\x0d\xfb\x7f\xcd\xaa\x37\xd9\x1e\xb8\x4c\x41\x83\xe8\x94\x24\xb2\xe0\xb5\x40\x2a\x1b\x09\x02\x6b\x1f\x2b\x18\x8d\x46\x70\x6c\xed\xa2\x7f\x08\xcf\x0b\xb5\xb1\xcd\x59\x3e\x8b\x55\x9c\x02\x70\x0d\x34\x13\x74\xc3\x9a\x9e\x8c\x6a\x73\x9a\x16\x1c\x22\x45\x81\x8d\x8e\x87\xc0\xe0\xb1\xcb\x56\x9c\xd1\x7c\x26\xe7\xf0\xe0\x01\x34\xc6\x6b\xfe\x86\xd0\xed\xb2\x7a\x0b\x5e\x41\xbf\xea\x31\x4d\x15\xdf\x0e\xd6\x31\x9b\xd4\x3b\xaa\x61\xf1\x26\x66\x50\x2c\x5b\xd8\x79\x0c\x27\x1b\x8c\x1c\xde\xc8\x08\x62\xf4\x59\xa9\x75\x3c\xae\xd4\x63\xf4\xeb\x29\xf8\x94\x2d\xe8\xb3\x22\x97\xbc\xc8\x5c\x95\x6a\xfa\x69\x91\x2c\x17\x34\x97\xf1\x8c\xca\x17\x19\x55\x7f\xfe\xb4\x7e\x9d\x46\x81\x8a\x32\xcf\x30\x94\x3c\x4b\x97\x1c\x17\xca\x99\x98\x73\x96\x5f\x04\x9d\xb8\xc8\x93\x8c\x25\x17\x30\x02\x39\x67\x22\x4e\x69\xc2\x29\x11\xf4\xb9\x01\x8c\xcf\x59\x9e\x46\xaa\x0b\x2d\xf1\x56\x34\x66\xbc\xb8\xda\xa4\xc0\xf2\x7f\x98\x82\x64\x0b\x7a\x76\x4e\x92\x1d\xfc\xbf\xc8\xd3\xbb\x22\x9e\x16\xfc\x8a\xf0\x74\x3b\xe7\x77\xc3\xcd\xe9\x94\x53\x31\x3f\x3b\x5f\x4a\x59\xe4\x9b\xd8\x4d\x7f\x03\xb3\x9e\x94\x11\x95\x41\x0d\xa3\x5b\xe9\x21\xa8\x11\xd1\x3c\xbd\x1d\x0e\x14\x08\xcd\xd3\x60\x2b\x33\x71\x91\xb3\xbc\x5c\xca\x4a\x01\x4c\x94\x44\x26\xad\xf3\xa8\xc9\xff\xf3\x47\x19\x71\xfe\x89\x64\x4b\x7a\xbb\x39\xfb\x8a\x3a\xbb\x54\x18\xd4\xfc\xab\x1d\x02\xbb\xdf\x30\x71\x5b\x84\x2c\x97\x94\x5f\x92\x4c\x68\x69\x3a\xc8\x5e\xdb\x1e\x3c\xce\xfc\x32\xc5\xc3\xcb\x09\x1e\x6b\xbe\xc7\xe7\x89\xf9\x6f\x1e\xa0\xdb\xd8\x74\x57\x4d\x44\xc6\x63\x79\xee\x49\x0d\xc8\x98\xcb\xb4\xb2\x67\x69\xc5\x13\x05\x19\x0b\xcc\x91\x23\x63\x4d\xf3\x14\x54\x7e\x68\x5a\x68\x6f\x83\xec\x98\x4d\x6a\x0c\x92\xae\xa4\xf2\x56\x5a\xf4\x2d\xa0\x43\x13\x57\x54\x02\x8d\x49\x59\xd2\x3c\x7d\x36\x67\x59\x1a\x65\xcc\x84\xa0\xdb\x2c\x0f\x35\xd3\xd8\xad\x1d\x0f\x19\x97\xbc\x90\x85\x5c\x97\x54\x29\x07\xc3\x09\xeb\x73\xa2\x8d\xad\xd8\x1d\x77\xc6\x72\xa6\x42\x1b\xb4\x9d\x9a\x6c\xc7\xee\x7f\xf7\xf7\x1c\x48\xf3\x54\xf5\xbe\x2b\xae\xac\x06\x9a\xf6\x7c\x87\x19\x10\x69\x02\x0f\x80\x9c\x5e\x01\xbe\xdf\x8e\x1f\x38\xd4\x91\x85\x91\x6e\x33\x88\x70\x11\xa8\xe5\xff\x12\x77\x33\x65\x99\x6a\x0e\xc1\x3a\x18\xc0\x0f\xc7\x70\xa8\x1f\x0f\xbf\x83\x43\x78\xf4\xc3\xf7\x2a\xb6\x09\xae\x36\xbb\xfe\x0d\x3b\xd2\x46\x07\x36\xce\xeb\x46\x7c\x5f\xe0\x3b\xfe\x29\x82\x01\x9c\xec\x64\x4c\x48\x5a\xea\x59\xa9\x15\xa3\xc6\x9c\x1c\x8b\x2d\x8b\xe6\xd1\xb1\x5d\x3b\x3d\x08\x1e\xe2\xf3\x07\x7c\x9e\xe8\x97\x93\x14\x3b\xd2\x00\x69\x9f\x5c\xe1\x1b\x3e\xbf\xc3\xe7\xbf\xe3\xf3\x64\x8d\xed\xeb\xe0\xa0\x99\x48\x68\x57\xd8\x99\xa0\xf2\x15\x11\xf3\xcd\x7d\x5a\x2d\x43\x6b\x53\x76\x75\x95\x84\xd7\x3b\x62\xb4\xdd\xda\x2b\xc7\x61\x75\x69\xc6\x2b\xef\x93\xa7\x68\x0c\x1d\xe8\xa3\x7e\x15\xe4\x15\xcb\xd3\xe2\x2a\xce\x8a\x44\x6f\xb5\x73\xcd\x50\xf0\xbb\x32\x91\x49\x00\x5d\xa0\x79\x52\xa4\xf4\xd3\x87\xd7\xcf\x8a\x85\x0e\xe9\xa3\xff\xf8\xf8\xcb\xbb\x58\x45\xeb\xf9\x8c\x4d\xd7\xda\xd6\xbe\x5a\x66\x06\x15\xe7\x3d\xcb\xc2\xc0\xfe\x71\xad\x2c\x6a\x97\xd2\x7c\x73\x6c\x17\x8c\xe1\xb0\x8d\x71\x3b\x75\x14\x84\xb0\x8b\x51\xf5\x54\xe9\x99\x10\x27\x16\x76\xfc\x00\xf5\xd2\xd2\xc3\xa9\xa1\xa8\xa3\x94\x6e\x4c\x1c\x31\x89\xe5\xb9\x9e\x7b\xf4\x7d\xc7\xac\x10\x37\x4e\xac\x50\x39\x12\x79\xf4\xc3\xf1\xb1\x23\x8d\xe3\x6b\x7b\x0c\x52\xec\x5d\xfa\x4b\x6f\x93\xad\xba\xab\x5e\xcf\x1d\xa5\x4f\xd5\xe8\x29\xb3\x39\xe2\x5d\x71\xa5\xd4\xcf\x97\xb4\x71\xee\xb2\x02\xba\x8e\x3a\x7b\x99\xaa\x67\x7c\xae\x5a\xec\x1c\x4f\xe9\x4a\x6e\xda\xee\x87\x17\x86\xe7\x0f\x74\xf6\x62\x55\x46\xc1\x7f\x47\xe3\xe3\xa3\x3f\x4e\xba\x9d\x68\xbc\xbe\x4a\xe7\x0b\x31\xf9\xb1\xf3\xfb\x7a\xaf\x5b\xa8\xad\x1a\x85\xe7\xe2\x8d\xb1\x39\xaa\x91\xd6\x6e\xd6\x0c\xe8\xc0\x57\x3b\x33\x25\xea\x61\x95\x94\xb0\xde\x13\xf9\x7f\x9d\xcb\xc8\x0c\x18\x9f\x4c\x2a\xa2\xcb\x9c\xa9\xcd\xc7\xf6\x3c\x9c\xc0\xb7\x6f\x10\x8a\x70\xd8\x10\x17\x1c\x6e\x75\xc2\x8e\x13\x1c\x2b\x74\x2d\x07\xaf\xfd\x76\x9d\x36\xd1\xd6\x62\x55\xa8\xd1\x9b\xf9\x3b\xfc\x05\x5d\x03\xcb\xf7\x61\xce\xda\x15\x22\x8a\xcb\xa5\x98\x47\xe3\x7d\xe6\x74\x41\xd7\x93\x9e\xa2\x33\xa9\x52\x3e\x1a\x85\x28\xb8\x8c\x2a\x8e\x49\x0f\xce\x1d\x55\x9c\xab\xa3\xe8\x11\x90\xf1\xf1\x64\x08\xd7\x1d\x3f\x2a\x81\x11\x98\xb8\x44\x63\x6a\x09\x46\x94\x86\x2b\x2f\xf8\x07\x0d\x37\x66\x13\x85\xd5\x5b\xbc\x95\x9a\x6a\xe8\xbe\x0b\xdd\x81\x6e\xfd\x7a\xd2\x7e\xa8\xb2\x23\xf7\x55\x5c\xf3\x88\xb2\xdb\x87\x7f\xa4\x49\x91\xa7\xe2\x4e\xae\xbc\x4d\x64\x37\xef\x79\x95\x3c\x59\xb7\xdb\x26\x4f\xcb\xd1\xe3\x36\x8e\x6e\x46\xaf\x82\xb8\x5a\xfc\x36\xfa\xbb\x25\x82\xa1\x3b\xdc\x06\xea\x51\xd5\xac\x35\xe3\x6a\x6b\x3f\xd5\x34\xcf\xa7\xbf\x91\x6a\xf6\xd6\x09\x1c\xc1\x89\x52\xe3\x13\xad\xce\xa3\xa3\x5d\xfa\x79\xf2\xaf\xa7\x1f\x87\x91\xed\xee\x6e\x67\x34\x5f\x2f\x56\x03\x68\x23\xa9\xe8\x86\xf0\xc2\x73\xbc\x26\x1a\xda\x34\x0f\xa5\x87\x2d\x61\xf8\x68\x04\x61\xd8\xc8\x76\xe6\xcb\x2c\x73\x13\xe1\x29\x91\xf4\x3d\xe1\xb2\xb2\xa9\x26\x9a\x58\x94\x19\x93\x51\x7f\x7c\x04\x83\x49\xbf\x53\x3b\x21\xc5\x4e\xfc\xe9\xf4\x59\x54\xa1\x18\x1f\x4f\x7a\x35\xc2\xf1\x89\xf2\xa7\x27\x6e\xcb\x43\xaf\xff\x91\xf7\xf6\xdd\xe4\x36\xe2\xf8\x85\x7f\xdc\x21\x13\x3b\xb1\xb6\x68\xd2\xee\xc6\xaa\xbf\x21\x1b\xd5\x64\x65\x63\x86\xd7\x21\x4c\xa5\x3e\x51\x23\x43\x1c\xce\xce\xab\xde\xeb\x50\xe7\xd6\xdb\x6a\x63\x2e\x35\x87\x18\x48\x16\x4b\x3c\xb4\x58\x1a\x9f\x4e\x9f\xbd\x52\x4d\x11\xa6\xed\x8e\xe1\x47\x08\x8f\x43\xe8\xb6\xf5\x0f\x5a\x1a\xab\x20\x86\xe5\x4b\x49\x1b\x88\xdf\xea\xc6\x1d\xa8\x6b\x88\x41\x6b\x73\x8b\x50\x3e\x9d\x3e\x7b\xb9\xcc\xb2\x5f\x29\xe1\x91\xda\xe4\x82\x23\x15\xb0\x47\xee\xe8\x22\x97\xf3\xa8\xd3\x3d\xa9\xbb\x9d\x5e\x73\x18\xe8\x42\x00\x01\x74\xcd\xb2\xd6\x52\xe9\x42\x30\x50\xd0\x66\x32\xfb\x0a\x5e\xb4\x9a\x50\x2d\xf6\x46\x9e\x20\xc2\x44\x46\x6b\x1e\xc7\xae\x75\x6b\x6d\xf5\xd1\xb6\x32\x91\x3b\x2e\x7d\x27\x47\xb7\x69\xe7\xfd\x3e\xbc\x36\xfd\xf5\x09\xec\xe1\xf7\x7f\x00\x4e\xf2\x19\x85\x07\x90\x14\xf9\x25\xe5\x12\x16\x78\x6b\x2f\xe2\x16\x1b\xae\x2c\xdc\xf2\xee\x2e\x2d\x94\xf7\xed\xb6\x1a\x73\x1a\xef\x7f\x07\x1d\x27\xc7\xe6\xf8\xe3\xdb\xed\x8b\xad\xf3\xbe\xed\x1c\x8e\xfe\x6f\xe6\x60\x12\x41\x5b\xf8\x6f\x31\x9f\x30\x6c\xb5\x94\xbb\x0b\xd1\x8c\xf8\xe7\x05\x15\xe6\xe8\xbe\xcb\xd1\x62\xf7\x68\x84\x3b\x8f\x75\xb8\x7a\x48\xdb\xf9\xd0\xfa\xdf\x8d\x40\xd2\x8f\x23\xab\x4c\xe4\x8c\x09\xc9\xd7\x6d\x01\xa4\x1a\x8c\x50\x8d\x70\xa7\x31\xb4\x4a\xdb\x61\xb3\x9a\x3d\x59\xd4\x33\x76\xf6\x6f\x23\xa6\x16\xe0\xfa\xc4\xab\xe6\xe5\x1e\x70\x35\x98\x1f\x8d\xec\x1b\x79\x9c\x35\x12\x8c\x30\x32\x9b\xf7\x7e\x9e\xed\xc3\xa6\xf1\xf9\x41\x4b\x15\x38\x6c\x10\xba\xdf\xd0\x96\x49\x5e\x24\x19\x25\xdc\x02\xb5\x0f\x35\xe1\x56\x3b\xda\x91\x17\x7c\x78\xe7\xa5\xfb\x23\xd0\xce\xd5\x09\x2e\xdb\x17\xc9\x7d\x2f\xa8\xf1\x73\xe3\x91\x57\x5f\xb2\xb7\x89\x7b\xf9\xd0\xed\xcc\x1b\x29\x08\x2a\x7d\x19\xb4\x24\xed\x7b\x1b\x84\x0f\xeb\xcb\xcf\xeb\x1d\x19\xfd\x46\x8a\xd9\x3f\xe7\xf5\xfb\x80\x49\xfa\x62\x0a\x24\xcb\x4c\x2d\x55\x0f\x96\x82\xa6\x70\xbe\x06\x75\x04\x56\x0e\x5f\x99\x42\xa3\x06\xa3\x61\xf2\xe6\x50\xee\x81\x20\xc4\x73\x3a\x25\xcb\x4c\xda\xdc\x28\x5d\x95\x7c\x80\x4a\xeb\xd9\xc2\xa0\x17\xab\x92\x53\x21\x94\xce\x64\x61\xcc\x1b\x9e\x91\x1c\xce\x29\x10\xc8\x0c\x7b\x3a\xe3\x84\xdb\x4d\x5e\xa4\xb4\x81\xe3\xf9\x2f\x6f\xb1\x59\x61\xc0\x1a\x21\xb3\x4c\x97\x79\x4a\xb9\xad\x23\x72\xff\xf5\xfb\xf0\xaa\xb8\x82\xac\xc8\x67\x58\xc1\xa0\xc1\x99\x80\xe2\x92\xf2\x1e\xb0\x1c\x84\x16\xb3\x1a\x5c\xe7\xb1\x6e\x99\x0e\xef\xb5\x53\x3e\x9d\x53\x75\x1e\x5f\xa1\x78\x6b\xea\x54\x69\x95\x48\x45\xb1\xca\x94\xed\x4b\xd1\x0c\xe8\x61\x42\x33\x95\x73\x47\x3e\x6a\xaa\x94\xcd\xe6\x28\xc6\x9a\x5a\xca\x2e\x7b\x40\x57\x49\xb6\x4c\x99\x12\x02\x93\x19\x15\x40\xf2\x14\x32\x3a\xa3\x66\xe6\x2d\xcc\x57\x0a\x95\x05\x90\xa5\x2c\x8e\x52\x2a\x69\x62\xeb\xb5\xe6\x48\x69\x00\x0f\x8f\x8f\xff\x61\xe2\x0b\x96\x0f\x20\x50\x34\x02\x8b\xeb\x2d\xcb\xd9\x62\xb9\x80\x5f\x8f\xc8\x8a\x09\x9d\x96\xea\x41\xea\xb0\x94\x15\x57\x54\x48\x15\xe4\x11\xdd\x8d\x98\xc8\x6a\x80\xb6\x30\x65\x39\x4d\x7b\x88\x89\xac\x6e\xc0\x34\x67\xb3\xf9\x26\x2a\x4e\x95\x49\x51\x3e\x80\x30\x63\x39\x0d\x7b\x5a\xa3\xeb\x92\xaa\x19\xda\x05\x54\x94\xba\xae\x91\x70\x6a\xe0\x70\x72\x21\xe1\x94\x84\x68\xc3\x64\xd1\xb4\xe1\x3f\xcf\x89\x54\x74\x13\xb5\x12\xcb\xac\x90\xc2\xe7\x47\xf2\x35\xca\xaa\x80\xb4\x68\x57\x8d\xc0\x4a\x49\x05\xa4\xe2\x9c\x22\x27\xe7\x19\xdd\xa2\xc5\xd7\x53\x20\x66\x4d\xf5\x80\xc9\x30\xcb\xb0\x00\x4b\xce\x89\x8c\x61\x3c\x86\x8c\x9c\xd3\x0c\x26\x13\xb8\x62\x59\xa6\x56\x22\x26\x7d\x99\x5c\x4a\x9a\xee\x42\x69\x37\x06\x83\xf3\x9c\xe2\x74\x68\xaa\x6b\x86\x08\x2c\x48\xa9\xe4\x74\x41\xd7\x38\x27\x9d\x86\xdd\xb2\x4c\x94\xc4\xc4\xbc\x58\x66\xa9\x8d\xfb\x95\x01\x29\xc9\xa9\xa1\x4b\xb1\x6d\x6e\xdb\x7c\x47\xdf\x32\x27\x7a\x40\x49\x32\x07\xaa\x3d\x64\x3b\x16\x3b\x71\x52\x96\x19\xa3\x29\x6a\x60\x4e\xb5\x62\x60\xca\x8b\x05\xbe\x26\x05\xe7\x54\x94\x45\xae\xec\xb8\x1d\x91\xa1\x62\x17\x80\xf2\x80\xc8\x99\xe2\x7e\x75\xaa\x2c\x7f\x00\x81\x5a\xbc\x41\xcf\x75\x10\xb8\x26\xec\xa0\x15\x28\x2b\x55\x23\xd6\x9f\x72\x26\xc5\x00\x02\x1f\x5a\x67\x46\x0d\xf4\xba\x86\xb6\xf8\x77\xe0\xae\xa1\xfb\x7d\xd0\xe5\x31\x2a\x54\x32\xf5\xb1\x2a\x68\x72\xf0\x3d\x5d\x31\x51\x15\xcf\x0c\x76\x56\xea\xd8\xc2\x9a\xde\x4e\xcc\xe6\xb6\x60\xae\xfc\x2e\xa4\x54\x12\x96\x21\xa1\x57\xaa\xe1\x96\x94\xb0\x68\xa9\xb7\x11\x0b\xfd\x6c\x22\xb6\x2a\x68\xd1\x31\x96\xde\xf0\xeb\x24\x72\x33\x85\xec\x6d\x5f\x6e\x14\x71\x3f\x42\x58\x83\xa5\x8e\x1b\x74\xc3\xf8\x62\xd2\x88\x0e\x3d\x44\xe3\x8b\x46\xfe\x15\xe3\x92\x75\x49\x8b\x29\xd8\xd8\x4f\x59\xc8\x68\x04\x81\xb6\xdb\x2a\x82\xf1\xba\x61\xec\xbc\x4e\x86\x5b\x91\xe1\x72\x71\x90\xc1\xb7\x6f\xb0\x05\xc2\xca\x27\x70\xc3\x5d\xdd\x6b\x32\xee\xed\xc9\x58\x87\x91\xb6\xa0\x59\x3b\x3b\x9d\x6c\x77\x48\x7a\x81\x95\xc7\x0a\xc2\xfb\xd7\xda\xba\x5f\xdf\x63\x90\x85\x70\x2a\x18\xd0\x1b\xa7\x67\xe8\xa4\xeb\x68\xb0\xdf\x87\xff\xa4\xb4\x04\x02\x9c\x4e\x29\xa7\x79\x42\x41\x14\xe8\xde\x60\xba\xe4\x58\xa7\xb8\x2c\xd5\x41\x5a\x40\x44\xe3\x59\x0c\x24\xb7\x65\xc7\xa2\x03\x89\xf6\x20\x0b\x92\x52\x8d\x4c\xc5\x42\x6a\x95\x09\xca\x95\xea\xe5\x9c\x32\x0e\x92\x2e\xca\x4c\xa1\xa8\xce\xc0\x9c\x25\x17\x62\x4e\xae\xac\xc5\x59\x76\x76\x1d\x33\x50\x2e\xa6\x2e\x43\x11\x3b\x54\x02\x39\x84\x53\xe5\xbc\x21\x23\xeb\x62\x29\x07\xba\xe9\x9b\x59\xce\xf0\x0d\x34\x01\xf8\x66\x3b\xcc\xbf\x6f\xc6\xa1\xd4\x1d\x7d\xbd\xdf\x7e\x83\x37\xb8\xaf\x9a\x8e\xbe\x39\x66\x49\x24\xb2\xbd\xd8\x01\xfb\x4d\x7a\x02\x37\x93\x24\x23\x42\xbc\xd3\x5a\xf2\x6a\x60\x10\x50\xc1\x59\x4d\x16\x29\xf5\x2a\x15\x10\xa2\x3a\xdf\x49\xbe\x8b\x2a\x77\x49\x7a\x48\x78\x85\x41\x8b\xe2\x34\xdd\x85\xc7\x96\xe4\x70\x0f\x89\x1d\xd9\x40\xf5\x9c\x5d\xee\x81\xcb\x0e\x6e\xc1\xf8\x9c\x5d\x3a\x20\xcf\xd9\xe5\x56\x69\xad\xd1\xff\x06\x3e\xb0\x1f\xa6\x1b\x31\x1a\x85\x77\xc1\xae\x1c\xed\xfe\xe1\x47\x08\x20\x0a\xa0\x0b\x5e\x73\x2c\x39\x5b\xe8\x7c\x56\x27\x00\xe5\xf5\xb5\x4d\xe9\x73\xb4\x22\x7d\x17\x79\xb9\xa3\xeb\xcc\x81\x69\xf0\xe7\x68\xed\x5f\xcf\x33\xd8\x00\xc6\xd0\xb4\x9e\x1d\xbe\x6e\x00\xe9\x10\xb2\x86\xd2\xef\x7a\x22\xff\x90\xd9\x34\xe6\xb5\x4b\x0c\x95\x6d\xac\xee\x6c\x66\x2b\xd7\xcc\xec\xcb\x56\x93\x58\xd5\x26\x51\xc1\xb6\x5a\x84\xee\xfd\x5f\x90\x46\x95\xcb\x78\xc3\xf2\x8b\xbb\x4c\xd0\x19\xbc\x89\xf0\xe9\x0e\x7c\x44\xa3\x73\xc6\xb7\xe3\x7d\xea\x83\x3d\xdd\x2a\xbc\x8c\xe5\x17\x41\x03\xd6\x17\x5e\xd0\x6d\xf6\xcf\x39\x9d\xb6\x66\x71\xc4\x69\xf1\x31\x23\x62\x8e\x2e\xf6\xd3\x87\x37\x91\xb3\xbd\x55\xf3\xd4\xa7\x94\xbb\x48\xcd\x8e\xac\x57\x92\x6e\xd9\xed\x7e\x52\x76\xa9\xb1\xd9\xe1\x5b\x97\x52\x05\xb0\xb1\x84\x2b\x3a\xda\x29\x98\xdc\x03\x49\xd3\x17\x97\x34\x97\x6f\x98\x90\x34\xa7\x3c\x0a\x39\x15\xec\xef\xea\x60\xd3\xc8\xef\xa9\xe8\x22\x6a\xd9\x75\x9b\xa9\x9d\x3a\xdd\xa1\xa0\x5a\x46\x38\x3b\xff\xb5\x93\xe1\x70\x7c\xd5\xae\xb4\xe4\xcf\x3a\x4d\x56\xe7\xb4\x30\xf3\xf2\x27\x93\xee\xac\x78\x36\x59\xd7\xaf\x75\x1d\x84\xad\x82\x78\x99\x15\x44\x46\x75\xbe\x51\xc5\x4c\x4c\xbc\x23\xef\x54\x5b\x15\xce\xf5\xfb\x10\x74\x5f\xe7\x58\x65\x78\x64\xfe\xc7\xf7\xea\x60\x80\xc8\x52\x60\xb9\x2c\xe0\x1d\x79\xa7\x62\x04\x07\x7f\x27\x56\x91\xb6\x45\x95\x90\x3c\x94\x6a\x10\x9a\x98\x3a\x82\x8a\x42\x9d\x6d\xae\x54\x28\xb1\xc0\xcf\xd8\x48\x29\x20\x42\x39\xc6\xdb\x2e\xd7\xea\x62\x8c\x3d\xc4\x42\x45\x42\x4a\xfa\xea\xf4\xed\x1b\x57\x2c\x3a\x0a\xac\xe5\x42\x73\xc9\xe4\xfa\x2d\x29\x4d\x7e\x06\x20\x78\x10\x0c\x20\x78\x40\x16\xe5\x30\xd0\x07\xb3\xe0\x31\xb6\x64\xb2\x6a\x78\x82\x0d\xb3\xaa\x21\x0c\xc2\x01\x84\x0f\xfe\xb6\x2c\xe4\x30\x34\x30\x61\xa0\x9a\x7e\xf7\xe8\x8f\x55\x4b\x5f\xb7\xac\x1e\xbe\x1c\x86\x6a\x46\xa8\x6f\x33\x27\xcd\x57\xfd\x85\xd7\xf8\xc1\xe3\x27\x41\xf8\xb9\x3f\xe9\xcf\x6a\x43\x84\x48\x34\xae\xd7\x2a\xf6\xc7\x42\xc7\xc0\xfb\x18\x8c\x36\xc6\xc6\xfd\x0c\xa9\x65\x22\x68\x36\x35\x69\xbd\xea\x2b\x10\x92\x51\x59\xdd\xde\x7d\x30\xdb\x5c\xfc\xac\xc8\x0a\x1e\xbf\xd7\x9d\xf5\x05\x98\xa0\x9c\x51\x5b\xa7\x72\x60\x4e\x5d\x4c\x54\x96\x83\xe9\xb5\x22\x07\xbd\xd2\xe2\x6d\xc1\xac\xfa\x6f\x78\x60\x6b\x6a\x55\x50\xfc\x72\x99\x27\x75\xfd\x4b\x95\xd1\xf4\x03\x79\x7f\x35\xaa\xa1\xc9\xbc\x28\x04\xce\xd8\x73\x77\xba\xf9\x9d\xc1\x5b\x0b\x62\x7b\xa4\xef\x52\xdb\x1d\xee\x23\xa7\x3a\xb6\x35\xd4\x3b\x37\x7e\x56\xd0\x4e\x07\x0f\x0f\xad\x74\x9c\xc3\x4e\x73\xc0\x98\x4d\xda\x4e\x50\x2d\xdc\x55\x36\xc0\x7a\xb0\xa0\x92\xb3\xc4\x05\x6e\xff\x4e\x07\x8b\x94\xcb\x42\xc5\xfe\x4a\x7a\x1b\x4a\x18\xb3\x49\x85\x6c\x58\xe1\xba\x76\x8b\x82\x59\xa7\xea\xf1\xa4\xd1\xc2\x61\x0b\xf6\x7a\xac\x73\x94\xd4\x86\xf6\x33\x95\x78\x54\x41\x1b\x42\xdf\xa4\xde\x38\x86\x54\xfa\xec\x1d\x7b\x56\xfa\x86\xe6\x28\xf7\x03\xf7\x2c\x4c\xb5\x2a\x28\x3c\x46\x3c\x95\xf8\x69\x2d\xfe\x36\xb5\x29\xd8\x31\x9d\xc4\x38\x86\x53\xb1\xcc\x64\xbb\xe6\x34\xe9\x71\xc5\xc1\xa4\xf2\x3e\xf6\x9f\x42\x31\x68\x43\x38\x66\x93\xd8\xd4\xf8\x2d\x48\x59\xab\xcf\x2d\x81\xfb\xba\x1a\x80\xae\x1c\x58\x0f\x70\x35\xbb\x9b\x44\x84\xb5\x6f\xd7\x43\xb8\xee\xf8\x79\xa7\x44\x2d\xe6\x81\x5d\xea\x31\xbe\x46\x0d\x18\x9d\xb6\xd3\x28\x6b\x07\x1b\x55\x1a\x1b\xd3\x49\xb4\x85\x69\x63\x0f\x15\xc2\xeb\xe1\xc1\xbd\x7b\xf7\xf0\xba\x55\x50\x2e\x71\xd1\x0a\x14\x2b\xc9\x32\x58\x30\x21\x58\x3e\x03\x21\x69\x29\x62\x05\x89\x3e\x80\x5e\x7d\x74\x7d\x8b\x69\x2e\x0b\xa1\x75\x68\xde\x85\x24\x5c\x45\x3d\xc8\x67\xe3\x42\xe9\xc8\x6b\x75\x0b\x49\xac\xb7\x50\x24\x1b\x83\x9d\xb2\x32\x2f\x54\x2f\xa6\x53\x41\xe5\x9f\x75\xec\x71\xef\xde\xbd\xca\x28\x90\xba\xe2\x62\x08\x12\x1e\xb7\x72\xa2\x7a\xba\x23\xa4\xa6\x0c\xe3\x9e\x16\xc6\xd3\x2c\x2b\xae\x50\x0a\x53\xb5\x8d\x2a\x11\x94\x05\xcb\x25\xb0\x9c\x24\xc9\x92\x93\x64\x8d\xd2\xb8\xa7\x16\xff\x86\x19\xc5\x8e\xb5\xc2\x13\x94\xcb\x83\x07\x9b\xd6\x86\x60\xe3\xb2\x10\x93\x78\xa5\xbc\x0d\x74\xf5\xa4\xf1\x9a\xcd\x32\x73\xaf\x12\xb6\x5e\x86\x3b\xb0\x74\x86\x7a\x44\x59\x88\x6e\x57\xff\x5d\x2d\xea\x36\x54\xca\x3e\x25\x1a\xa7\x52\xfa\xb5\x19\x7e\x7d\x60\x1f\xed\xa4\xf4\xe6\xa3\xf1\x0c\xbd\x85\xf4\x86\xe6\x8a\xae\x9b\x55\xd2\x21\x18\x5e\xaf\xe1\xfe\x57\xdf\x9d\xde\xd7\x63\x8c\x94\xdc\x6d\x82\x72\x5e\xf0\x53\xba\x92\xfb\x04\xa2\x50\x83\x7b\x71\x79\xe8\xc4\xe5\x08\x11\x36\xa1\xfd\xc8\x3c\x7c\x57\xe0\x35\x93\xd9\x33\xf5\x0a\xa6\x69\xe8\x5c\x9c\x59\x6b\x73\x23\xda\x0a\x5d\x67\xe8\xc4\x03\x36\x50\xea\xf7\xe1\x03\xb5\x57\x3f\xee\xa5\xad\xb7\x7b\x6b\xc1\xb8\x7e\xa7\x76\xec\x78\xdb\x13\x64\x2c\xa7\x84\x07\xae\x0f\x30\xb7\x2b\xdb\x96\x81\x0b\x6a\xef\x42\x5c\x07\xae\xdb\x5c\x28\x93\x1e\xf6\x51\xf6\xbc\x1d\xc8\xde\x38\xb8\x88\x6c\xab\x0b\x89\xd7\x1c\x2e\xd0\x82\xac\xbc\x7e\x96\x37\xfa\x99\x77\x45\xa5\x75\x30\x30\xff\x9b\x70\xca\x96\x0a\x5d\x52\xfe\x1c\xd3\xb3\xad\x62\x8c\x5f\xd5\x00\x95\x48\x71\x32\x03\xfd\x9f\xa5\x53\xe4\x5a\x33\x83\xcd\xe3\x85\xb5\xc3\xd5\x1b\xbc\x7c\xb0\xc5\x63\xe6\x1a\xb3\xfe\x78\x48\xfc\xb4\x7e\x66\x2d\x2e\x0a\x56\x67\x78\x57\x11\x74\xcc\x47\xbb\x35\x1e\x26\xe9\x62\x5f\x2c\x0a\xb6\x81\x02\x3f\xe3\x44\x56\x5c\x05\x43\x17\xbc\xc6\x37\x74\x2a\x6d\x41\x8b\x25\xe2\xf4\x3c\x31\x17\x9a\x7e\x97\xc6\xf4\xed\x9b\x6b\x7a\x92\x2e\x1a\x74\x9c\xa6\xbb\x53\xf1\xa3\x19\xc3\x3a\x2e\x58\xfd\x31\x51\xea\x7f\x87\x85\x7a\x3e\x9b\x66\xac\x2c\x69\x1a\x38\xd1\x8b\xe1\xf0\x0e\x23\x37\xe2\x9b\x16\x36\x38\x5d\x14\x97\xf4\x8e\x9c\xdc\x66\xf0\xb5\xdd\x7d\xad\x3d\xae\x9d\x3b\x86\xca\x22\xd7\xae\xd4\x94\x1d\xac\x37\xe2\x6a\xbb\x36\x31\xe0\xd0\xdf\xfd\xfa\x14\xa0\xbe\xb4\xaa\x17\x5c\xe3\x5a\x43\x11\xea\xfa\x00\x98\xc8\xab\xc2\x3b\xe7\x78\xec\x2d\x47\xbc\x87\x69\x5f\x88\xaa\x27\xfe\x75\xe7\x1a\x94\x2c\xb9\xd0\x3c\xf8\xee\xc0\xbf\xdd\xf1\x08\xae\x6e\x20\x88\x95\x36\xdb\x68\x5e\x37\xf2\x26\xed\x78\x74\x86\x7a\x27\xe3\xbe\xaf\xac\x12\x1a\x15\x09\xe4\xd2\xb8\x46\xbd\xe3\xad\x37\x5a\xf4\x4a\xa9\x5b\xb6\xe5\xee\x11\x6e\x8f\xd3\x64\xbd\xc7\x6e\x96\x44\x5d\xcd\x59\x46\xc1\xcb\xa0\xc6\x19\x11\x12\xf7\x30\xef\x83\x37\xdb\xab\x6d\x79\x33\xf1\xea\x0c\xb3\x9b\x9c\x8b\xbc\x12\xc5\x16\xf4\x75\xff\x06\x81\xb6\xa1\x7e\x4d\x49\xeb\xa5\xc6\x8d\x72\x59\xcd\xb9\x68\xab\x0a\x69\x02\x9e\x2f\x59\x96\xfe\xd7\x92\xf2\xf5\x27\xee\x7d\xb1\x8d\x09\xb7\xfa\x1b\x7b\xa7\xd0\xc6\xdc\x00\xd9\xa3\xd9\xd3\xd3\x57\x67\xef\x3f\xbc\x78\xf9\xfa\x2f\xd0\x85\xa0\x4f\x4a\xd6\xbf\x3c\xe9\xff\x4d\xa1\x3c\xc3\xaa\xc5\x1f\xf1\xef\x91\x2d\xf1\x6c\xf9\xa0\x4b\xd3\xea\x9a\x04\x88\x0a\x06\x47\x98\x5d\xdf\x27\xf2\x75\x86\x11\x2e\x71\x5c\x54\x3a\xf1\x76\x8d\x04\x33\xf3\x0f\x68\x9e\x1a\xe4\x36\x12\xbe\x51\x98\xdb\x8b\xee\xaa\x88\xfb\x8b\x3e\x86\x7d\xb1\xa7\x67\x94\x7f\x75\xfa\xfa\x52\x9f\xbe\xea\xde\xf1\x97\x49\x4c\xce\x0b\x2e\x23\xef\x77\x4c\x48\x96\x9d\xd5\xf1\x26\x3c\xe5\x9c\xac\xa3\x2d\x27\x7f\xa7\x0c\xd5\xe8\x7b\xbf\x21\xa8\x51\x8a\x97\xe6\x67\x9c\xfe\x6d\x49\x85\x14\xbe\x82\xbd\xfc\xc2\x96\xaf\x40\xb6\xa6\x23\x1a\x1f\xf4\x36\x3e\x04\x6c\x9c\x43\x2a\xb0\x25\xaf\x82\x0e\xcf\x28\x37\x66\x52\x9f\xc0\xd1\x35\xce\xb9\x99\xf7\x5f\xde\xbe\x79\x25\x65\xf9\x41\x4f\xc8\x96\x8e\xad\xe6\x3c\x2e\x4a\x9a\x47\xe1\x8c\xca\xb0\xa7\xc8\xf4\xf0\x93\x34\xa7\x5f\xd7\x10\x08\x8a\x75\x24\x23\x08\xbf\x88\x22\x0f\x9d\xe1\x39\xc6\xbb\xde\x4f\x54\xcc\x79\x0f\x58\x33\xe7\xda\x0c\xf8\xe1\x6e\xa1\xfd\x6d\x83\xfb\x1d\xe1\xfd\x0b\x64\x3c\x2b\x08\x16\xfa\x28\xbb\x0a\xbd\xcf\x2a\xf6\x09\xee\xc1\xfe\xf2\x57\x9c\x15\xb3\xa8\x05\x25\x5a\x47\xd8\xd8\x4a\x7d\x4d\x41\x9b\xbd\x1d\xdb\xbe\x7e\x1f\x8a\x1c\xd7\x02\xcc\xa8\x14\x40\xf2\x35\xe0\x6b\x55\x05\xd5\xb6\xd0\x9a\x18\xbd\x75\xb6\x73\xad\xd5\x81\x82\x97\x15\x32\x4a\x75\x15\xaf\xa6\xb9\x4b\xef\x2a\x3a\xd9\x9c\x99\xff\x75\x98\x4e\x0c\x15\x12\x7f\x12\x06\x85\x77\x4e\xa7\x05\xa7\xc8\x1f\x88\x65\x92\x50\xe1\x94\x7b\xb9\xdf\xb5\xd4\xf1\x8c\xf9\x00\x42\xb9\x05\xd7\x62\x87\x9b\xc9\xb8\xea\x56\x20\x28\xce\xbf\xd0\x44\x7a\xe9\x37\x83\xc2\xf9\xcc\xd4\xb3\x7f\x57\xe9\xd7\xdb\x14\x77\x34\x82\x13\x0b\x64\xbd\x15\x66\xfc\x4c\xca\xf4\x36\x92\xf1\x9c\xd7\xd8\x09\xff\xbd\x4b\x0c\x4b\xe5\x56\xca\x13\x2a\x9c\xf1\x2a\x40\xd1\x12\x90\xd1\xd5\x9c\x57\x15\x0e\x18\x18\x69\x6b\x7e\xbd\x98\xed\x58\xa1\x6c\x31\x33\xb7\x40\x15\x74\x2c\x78\x02\xa3\xc6\x1e\x18\xf6\x85\x24\x92\x25\x7d\xb6\x98\xf5\xc9\x17\xb2\x3a\x52\x03\x28\x8f\x67\x6c\x1a\x36\xc6\x93\x0c\x97\xe9\x1b\xdd\x12\xc7\x71\x13\x60\xdb\xfa\x37\x20\xe1\xc6\xfd\xad\x77\xdb\x55\xe1\xe9\x54\x05\xa7\x1f\xab\x4a\x32\x4c\x4d\xea\x1a\xdc\x62\x0a\x21\x9e\xe3\x42\x5c\x69\x4e\x05\x5a\xa3\xec\xb4\x91\x7b\x75\x97\x46\x4e\x16\xd4\x4f\xe2\xea\x9f\x8f\x80\x11\xf4\xa3\xf8\xf0\xc7\xce\xe7\xf1\xe7\xf1\x67\x71\x18\x7d\xbe\xea\x76\xba\x9f\xc5\xe1\xe7\xc9\xe7\x09\x76\xf4\x67\xc3\x0a\x5a\x2c\xb5\x44\x70\x62\x26\xc6\x5a\x98\xcd\x97\xd3\x98\xae\x68\x82\x94\x3a\xf5\x55\x84\x19\x62\xfe\xe8\xea\x2f\x69\xc7\x27\x13\xf5\x27\x72\x33\xd6\x2d\x0f\x27\x93\xaa\xf7\x91\x57\xb3\x73\x5f\x8f\x6d\x7e\x58\x55\xd5\xc1\x38\xbf\x6f\xa4\xe0\x2a\x59\xfe\xcc\x2e\x69\x5e\xe7\x7b\x6d\xf6\xc4\xd6\xab\x3c\x7d\xff\xda\xfe\x22\x94\x5a\xfb\xa4\x2c\x79\x51\x72\x46\x24\xad\xa4\xa6\xb0\xc8\xc2\x02\x95\x59\x21\x91\x6c\xb3\xda\x77\xf3\xae\xa0\xfd\xfe\xa4\xdf\x87\x9f\xd6\xb6\x6c\xb1\x67\x6a\x0a\x15\xb5\x2c\x33\xb2\xd0\xd5\x40\x8d\x4b\x0d\x07\x19\x44\x7e\x1a\xde\xd4\x04\xe9\xc6\xf8\xec\x4c\xbd\x9f\x9d\xa9\x48\xea\x6b\xd0\xb8\xa9\xd0\x26\xc3\xf2\x8d\x44\xbe\x12\x31\x76\x3a\xdf\xae\x1f\xf7\x1e\xe2\xcf\x14\x06\x67\x67\x9e\x83\x4a\x8a\x5c\xb2\x7c\x49\x9b\x5e\x08\xf9\xe8\x8e\x0c\x91\x2e\x04\xa3\x30\xa8\x15\x8c\xad\x4a\xbb\x41\xd8\x0b\xdc\xca\x26\x47\x95\xaa\xf7\x1a\x3b\xf1\xf3\xf7\x03\x5b\x35\x59\xe4\xd9\x1a\x8a\x9c\x1a\xd4\x97\x84\x33\x2a\x7a\x55\x3d\x66\x5d\x7b\x5a\xcd\xb1\xfa\x75\x80\xaf\xd7\xc3\xdf\x32\x6f\xbf\x29\xe9\xdd\xc9\xee\xc6\x29\xfa\x7e\x54\x8d\x73\xa6\xd1\xf1\x8f\xd4\x4e\x8f\x95\xaa\x9d\xa7\xaf\x90\x36\xd0\xf1\x4e\x7e\x0c\x90\x42\x78\xd2\x76\x83\x82\x3a\xc1\x2f\xd8\x96\x99\x64\x88\x18\x13\x15\x2d\x9f\x9c\xb7\x4e\xc4\xa9\x11\xfc\x05\x77\xbe\xf8\x82\xae\x45\xb4\xc9\x66\xa7\xce\x51\x3b\x3f\x92\xd9\xa4\xaa\x93\xc5\x38\xa2\xb3\x59\x33\xb8\x01\x6d\x50\x8e\x9c\xdf\x0f\x6b\x59\x5e\x51\x53\x35\xc6\x40\xad\xa3\x6a\x60\x1d\x1f\x4f\x6c\xbd\x62\xc3\x13\x59\xdc\x0d\x5f\x54\x57\xdd\xd2\xaa\xae\x5f\xf4\xa0\xe4\x45\xba\x4c\xb4\x33\x30\x35\x69\x2a\xdc\x56\xf2\x94\x73\xba\x68\xfb\xb6\xa0\x59\x88\xd1\x3c\x1b\x0a\xef\xc3\x4c\xb2\xa9\x23\xc7\xc2\x11\xbe\xed\x78\x80\x76\xa2\x93\xf2\xa1\x02\x0a\x07\x1a\x16\xaf\xf0\x42\x49\xce\xc3\x01\x1c\x5f\x77\x1a\x73\x6f\x6e\xb7\xc8\xef\xef\xc2\xbd\x7e\x2c\x04\x7d\x65\x67\x78\xa0\xc4\xf6\x3f\x01\x00\x00\xff\xff\xe0\xde\x55\x28\x07\x57\x00\x00") func webUiStaticJsProm_consoleJsBytes() ([]byte, error) { return bindataRead( @@ -461,7 +461,7 @@ func webUiStaticJsProm_consoleJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "web/ui/static/js/prom_console.js", size: 21632, mode: os.FileMode(420), modTime: time.Unix(1495630073, 0)} + info := bindataFileInfo{name: "web/ui/static/js/prom_console.js", size: 22279, mode: os.FileMode(420), modTime: time.Unix(1496733731, 0)} a := &asset{bytes: bytes, info: info} return a, nil } diff --git a/web/ui/static/js/graph.js b/web/ui/static/js/graph.js index e3cf33d7f9..b28514e450 100644 --- a/web/ui/static/js/graph.js +++ b/web/ui/static/js/graph.js @@ -386,6 +386,7 @@ Prometheus.Graph.prototype.submitQuery = function() { url = PATH_PREFIX + "/api/v1/query"; success = function(json, textStatus) { self.handleConsoleResponse(json, textStatus); }; } + self.params = params; self.queryXhr = $.ajax({ method: self.queryForm.attr("method"), @@ -518,7 +519,21 @@ Prometheus.Graph.prototype.transformData = function(json) { color: palette.color() }; }); - Rickshaw.Series.zeroFill(data); + data.forEach(function(s) { + // Insert nulls for all missing steps. + var newSeries = []; + var pos = 0; + for (var t = self.params.start; t <= self.params.end; t += self.params.step) { + // Allow for floating point inaccuracy. + if (s.data.length > pos && s.data[pos].x < t + self.params.step / 100) { + newSeries.push(s.data[pos]); + pos++; + } else { + newSeries.push({x: t, y: null}); + } + } + s.data = newSeries; + }); return data; }; diff --git a/web/ui/static/js/prom_console.js b/web/ui/static/js/prom_console.js index c0f0b84647..505627b5da 100644 --- a/web/ui/static/js/prom_console.js +++ b/web/ui/static/js/prom_console.js @@ -447,13 +447,30 @@ PromConsole.Graph.prototype._render = function(data) { // Get the data into the right format. var seriesLen = 0; + for (var e = 0; e < data.length; e++) { for (var i = 0; i < data[e].data.result.length; i++) { - series[seriesLen++] = { + series[seriesLen] = { data: data[e].data.result[i].values.map(function(s) { return {x: s[0], y: self._parseValue(s[1])}; }), color: palette.color(), name: self._escapeHTML(nameFuncs[e](data[e].data.result[i].metric)), }; + // Insert nulls for all missing steps. + var newSeries = []; + var pos = 0; + var start = self.params.endTime - self.params.duration; + var step = self.params.duration / this.graphTd.offsetWidth; + for (var t = start; t <= self.params.endTime; t += step) { + // Allow for floating point inaccuracy. + if (series[seriesLen].data.length > pos && series[seriesLen].data[pos].x < t + step / 100) { + newSeries.push(series[seriesLen].data[pos]); + pos++; + } else { + newSeries.push({x: t, y: null}); + } + } + series[seriesLen].data = newSeries; + seriesLen++; } } this._clearGraph(); @@ -490,6 +507,9 @@ PromConsole.Graph.prototype._render = function(data) { } }, yFormatter: function(y) { + if (y === null) { + return ""; + } return this.params.yHoverFormatter(y) + this.params.yUnits; }.bind(this) });