From c83b7c0dc818ad1daca9f2b6987c33fd08b40064 Mon Sep 17 00:00:00 2001 From: candysmurf <77.ears@gmail.com> Date: Fri, 2 Jun 2017 18:01:27 -0700 Subject: [PATCH] Snap CLI for go-swagger generated RestAPI client --- .gitignore | 46 +++ LICENSE | 202 ++++++++++++ Makefile | 25 ++ README.md | 283 +++++++++++++++- glide.lock | 58 ++++ glide.yaml | 53 +++ main.go | 92 ++++++ scripts/build.sh | 63 ++++ scripts/common.sh | 130 ++++++++ scripts/deps.sh | 73 +++++ snaptel/commands.go | 199 ++++++++++++ snaptel/common.go | 111 +++++++ snaptel/config.go | 86 +++++ snaptel/flags.go | 168 ++++++++++ snaptel/metric.go | 194 +++++++++++ snaptel/plugin.go | 156 +++++++++ snaptel/task.go | 767 ++++++++++++++++++++++++++++++++++++++++++++ snaptel/watch.go | 142 ++++++++ 18 files changed, 2847 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 glide.lock create mode 100644 glide.yaml create mode 100644 main.go create mode 100755 scripts/build.sh create mode 100755 scripts/common.sh create mode 100755 scripts/deps.sh create mode 100644 snaptel/commands.go create mode 100644 snaptel/common.go create mode 100644 snaptel/config.go create mode 100644 snaptel/flags.go create mode 100644 snaptel/metric.go create mode 100644 snaptel/plugin.go create mode 100644 snaptel/task.go create mode 100644 snaptel/watch.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..42b3553 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +.idea +tmp/ +*.tmp +scratch/ +*.swp +profile.cov +gin-bin +tags +.vscode/ +vendor/ + +# we don't vendor godep _workspace +**/Godeps/_workspace/** + +# OSX stuff +.DS_Store + +*.cov + +# build and release artifacts +build/ +s3/ +release/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + 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 + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..58b20d0 --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +# File managed by pluginsync +# http://www.apache.org/licenses/LICENSE-2.0.txt +# +# +# Copyright 2017 Intel Corporation +# +# 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. + +default: + $(MAKE) deps + $(MAKE) all +deps: + bash -c "./scripts/deps.sh" +all: + bash -c "./scripts/build.sh" \ No newline at end of file diff --git a/README.md b/README.md index 70537f7..5593c3f 100644 --- a/README.md +++ b/README.md @@ -1 +1,282 @@ -# snap-cli + + +# snaptel +A Snap telemetry framework CLI + +## Usage +Either copy `snaptel` to `/usr/local/sbin` and ensure `/usr/local/sbin` is in your path, or use fully qualified filepath to the `snaptel` binary: + +```sh +$ snaptel +``` + +### Global Options + +```sh +NAME: + snaptel - The open telemetry framework + +USAGE: + snaptel [global options] command [command options] [arguments...] + +VERSION: + all-clis-f9fa285 + +COMMANDS: + help, h Shows a list of commands or help for one command + +GLOBAL OPTIONS: + --url value, -u value Sets the URL to use (default: "http://localhost:8181") [$SNAP_URL] + --insecure Ignore certificate errors when Snap API is running HTTPS [$SNAP_INSECURE] + --api-version value, -a value The Snap API version (default: "v2") [$SNAP_API_VERSION] + --password, -p Require password for REST API authentication [$SNAP_REST_PASSWORD] + --config value, -c value Path to a config file [$SNAPTEL_CONFIG_PATH, $SNAPCTL_CONFIG_PATH] + --timeout value, -t value Timeout to be set on HTTP request to the server (default: 10s) + --help, -h show help + --version, -v print the version +``` + +### Commands +``` +metric +plugin +task +help, h Shows a list of commands or help for one command +``` + +### Command Options + +#### plugin + +```sh +$ snaptel plugin + +NAME: + - + +USAGE: + command [command options] [arguments...] + +COMMANDS: + load load + unload unload + list list or list --running + :: or swap -t -n -v + config + +OPTIONS: + --help, -h show help + +``` + +#### metric + +```sh +$ snaptel metric + +NAME: + - + +USAGE: + command [command options] [arguments...] + +COMMANDS: + list list + get get -m -v + +OPTIONS: + --help, -h show help + +``` + +#### task + +```sh +$ snaptel task + +NAME: + - + +USAGE: + command [command options] [arguments...] + +COMMANDS: + create There are two ways to create a task. + 1) Use a task manifest with [--task-manifest] + 2) Provide a workflow manifest and schedule details. + + * Note: Start and stop date/time are optional. + + list list or list --verbose + start start + stop stop + remove remove + export export + watch watch or watch --verbose + enable enable + +OPTIONS: + --help, -h show help + +``` + +```sh +$ snaptel task create -h + +USAGE: + create [command options] [arguments...] + +DESCRIPTION: + Creates a new task in the snap scheduler + +OPTIONS: + --task-manifest value, -t value File path for task manifest to use for task creation. + --workflow-manifest value, -w value File path for workflow manifest to use for task creation + --interval value, -i value Interval for the task schedule [ex (simple schedule): 250ms, 1s, 30m (cron schedule): "0 * * * * *"] + --count value The count of runs for the task schedule [defaults to 0 what means no limit, e.g. set to 1 determines a single run task] + --start-date value Start date for the task schedule [defaults to today] + --start-time value Start time for the task schedule [defaults to now] + --stop-date value Stop date for the task schedule [defaults to today] + --stop-time value Start time for the task schedule [defaults to now] + --name value, -n value Optional requirement for giving task names + --duration value, -d value The amount of time to run the task [appends to start or creates a start time before a stop] + --no-start Do not start task on creation [normally started on creation] + --deadline value The deadline for the task to be killed after started if the task runs too long (All tasks default to 5s) + --max-failures value The number of consecutive failures before Snap disables the task + +``` + +Example Usage +------------- + +### Load and unload plugins, create and start a task + +In one terminal window, run snapteld (log level is set to 1 and signing is turned off for this example): +``` +$ snapteld --log-level 1 --log-path '' --plugin-trust 0 +``` + +prepare a task manifest file, for example, task.json with following content: + +```json +{ + "version": 1, + "name": "sample", + "schedule": { + "type": "simple", + "interval": "15s" + }, + "workflow": { + "collect": { + "metrics": { + "/intel/mock/foo": {}, + "/intel/mock/bar": {}, + "/intel/mock/*/baz": {} + }, + "config": { + "/intel/mock": { + "user": "root", + "password": "secret" + } + }, + "process": null, + "publish": [ + { + "plugin_name": "file", + "config": { + "file": "/tmp/collected_swagger" + } + } + ] + } + } +} +``` + +prepare a workflow manifest file, for example, workflow.json with the following content: +```json +{ + "collect": { + "metrics": { + "/intel/mock/foo": {} + }, + "config": { + "/intel/mock/foo": { + "password": "testval" + } + }, + "process": [], + "publish": [ + { + "plugin_name": "file", + "config": { + "file": "/tmp/rest.test" + } + } + ] + } +} +``` + +and then in another terminal: + +1. load a collector plugin +2. load a processing plugin +3. load a publishing plugin +4. list the plugins +5. list running plugins +6. swap plugins +7. list loaded metrics +8. list loaded metrics with details +9. list a specific metric including all versions +10. list a specific metric with its version +11. list a plugin config +12. unload the plugins +13. create a task using task manifest +14. create a task using workflow +15. create a single run task +16. list tasks +17. watch a task +18. export a task +19. stop a task + +```sh +$ snaptel plugin load /opt/snap/plugins/snap-plugin-collector-mock1 +$ snaptel plugin load /opt/snap/plugins/snap-plugin-processor-passthru +$ snaptel plugin load /opt/snap/plugins/snap-plugin-publisher-mock-file +$ snaptel plugin list +$ snaptel plugin list --running +$ snaptel plugin swap -t -n -v +$ snaptel metric list +$ snaptel metric list --verbose +$ snaptel metric get -m /intel/mock/foo +$ snaptel metric get -m /intel/mock/foo -v +$ snaptel plugin config get :: +$ snaptel plugin config get -t -n -v +$ snaptel plugin unload collector +$ snaptel task create -t mock-file.json +$ snaptel task create -w workflow.json -i 1s +$ snaptel task create -t mock-file.yml --count 1 +$ snaptel task list +$ snaptel task watch +$ snaptel task export +$ snaptel task stop +``` + diff --git a/glide.lock b/glide.lock new file mode 100644 index 0000000..fc33da4 --- /dev/null +++ b/glide.lock @@ -0,0 +1,58 @@ +hash: 650eee1c63aee5b96dd800130f9271cdc5de10b8c85948eb27e96ed97dd8175f +updated: 2017-06-02T13:54:57.77443829-07:00 +imports: +- name: github.com/appc/spec + version: db96f94ae6b227fe4d8288527ead8927181620f6 + subpackages: + - aci + - schema +- name: github.com/asaskevich/govalidator + version: 9699ab6b38bee2e02cd3fe8b99ecf67665395c96 +- name: github.com/ghodss/yaml + version: c3eb24aeea63668ebdac08d2e252f20df8b6b1ae +- name: github.com/golang/glog + version: 23def4e6c14b4da8ac2ed8007337bc5eb5007998 +- name: github.com/golang/protobuf + version: 888eb0692c857ec880338addf316bd662d5e630e + subpackages: + - proto +- name: github.com/hashicorp/go-msgpack + version: fa3f63826f7c23912c15263591e65d54d080b458 + subpackages: + - codec +- name: github.com/hashicorp/memberlist + version: a93fbd426dd831f5a66db3adc6a5ffa6f44cc60a +- name: github.com/intelsdi-x/gomit + version: db68f6fda248706a71980abc58e969fcd63f5ea6 +- name: github.com/julienschmidt/httprouter + version: 8c199fb6259ffc1af525cc3ad52ee60ba8359669 +- name: github.com/pborman/uuid + version: ca53cad383cad2479bbba7f7a1a05797ec1386e4 +- name: github.com/robfig/cron + version: 32d9c273155a0506d27cf73dd1246e86a470997e +- name: github.com/Sirupsen/logrus + version: be52937128b38f1d99787bb476c789e2af1147f1 +- name: github.com/urfave/cli + version: 0bdeddeeb0f650497d603c4ad7b20cfe685682f6 +- name: github.com/urfave/negroni + version: c7477ad8e330bef55bf1ebe300cf8aa67c492d1b +- name: github.com/vrischmann/jsonutil + version: 694784f9315ee9fc763c1d30f28753cba21307aa +- name: github.com/xeipuuv/gojsonschema + version: d3178baac32433047aa76f07317f84fbe2be6cda +- name: golang.org/x/crypto + version: aedad9a179ec1ea11b7064c57cbc6dc30d7724ec + subpackages: + - openpgp + - ssh/terminal +- name: golang.org/x/net + version: 04557861f124410b768b1ba5bb3a91b705afbfc6 + subpackages: + - context + - http2 + - trace +- name: google.golang.org/grpc + version: 0032a855ba5c8a3c8e0d71c2deef354b70af1584 +- name: gopkg.in/yaml.v2 + version: c1cd2254a6dd314c9d73c338c12688c9325d85c6 +testImports: [] diff --git a/glide.yaml b/glide.yaml new file mode 100644 index 0000000..9abfac8 --- /dev/null +++ b/glide.yaml @@ -0,0 +1,53 @@ +package: github.com/intelsdi-x/snap-cli +import: +- package: github.com/Sirupsen/logrus + version: be52937128b38f1d99787bb476c789e2af1147f1 +- package: github.com/appc/spec + version: db96f94ae6b227fe4d8288527ead8927181620f6 + subpackages: + - aci + - schema +- package: github.com/asaskevich/govalidator + version: 9699ab6b38bee2e02cd3fe8b99ecf67665395c96 +- package: github.com/urfave/cli + version: ^1.19.0 +- package: github.com/urfave/negroni + version: c7477ad8e330bef55bf1ebe300cf8aa67c492d1b +- package: github.com/ghodss/yaml + version: c3eb24aeea63668ebdac08d2e252f20df8b6b1ae +- package: github.com/golang/protobuf + version: 888eb0692c857ec880338addf316bd662d5e630e + subpackages: + - proto +- package: github.com/hashicorp/go-msgpack + version: fa3f63826f7c23912c15263591e65d54d080b458 + subpackages: + - codec +- package: github.com/hashicorp/memberlist + version: a93fbd426dd831f5a66db3adc6a5ffa6f44cc60a +- package: github.com/intelsdi-x/gomit +- package: github.com/julienschmidt/httprouter + version: 8c199fb6259ffc1af525cc3ad52ee60ba8359669 +- package: github.com/pborman/uuid + version: ca53cad383cad2479bbba7f7a1a05797ec1386e4 +- package: github.com/robfig/cron + version: 32d9c273155a0506d27cf73dd1246e86a470997e +- package: github.com/vrischmann/jsonutil + version: 694784f9315ee9fc763c1d30f28753cba21307aa +- package: github.com/xeipuuv/gojsonschema + version: d3178baac32433047aa76f07317f84fbe2be6cda +- package: golang.org/x/crypto + version: aedad9a179ec1ea11b7064c57cbc6dc30d7724ec + subpackages: + - openpgp + - ssh/terminal +- package: golang.org/x/net + version: 04557861f124410b768b1ba5bb3a91b705afbfc6 + subpackages: + - context + - trace + - http2 +- package: google.golang.org/grpc + version: 0032a855ba5c8a3c8e0d71c2deef354b70af1584 +- package: gopkg.in/yaml.v2 + version: c1cd2254a6dd314c9d73c338c12688c9325d85c6 diff --git a/main.go b/main.go new file mode 100644 index 0000000..9881ca2 --- /dev/null +++ b/main.go @@ -0,0 +1,92 @@ +/* +http://www.apache.org/licenses/LICENSE-2.0.txt + + +Copyright 2017 Intel Corporation + +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 main + +import ( + "fmt" + "net/url" + "os" + "sort" + + "github.com/golang/glog" + "github.com/intelsdi-x/snap-cli/snaptel" + "github.com/intelsdi-x/snap-client-go/client" + "github.com/urfave/cli" +) + +var ( + gitversion string +) + +func main() { + app := cli.NewApp() + app.Name = "snaptel" + app.Version = gitversion + app.Usage = "The open telemetry framework" + app.Flags = []cli.Flag{snaptel.FlURL, snaptel.FlSecure, snaptel.FlAPIVer, snaptel.FlPassword, snaptel.FlConfig, snaptel.FlTimeout} + app.Commands = snaptel.Commands + sort.Sort(ByCommand(app.Commands)) + app.Before = beforeAction + + app.Setup() + err := app.Run(os.Args) + if err != nil { + fmt.Println(err) + if ue, ok := err.(snaptel.UsageError); ok { + ue.Help() + } + os.Exit(1) + } +} + +// Run before every command +func beforeAction(ctx *cli.Context) error { + snaptel.FlURL.Value = ctx.String("url") + snaptel.FlAPIVer.Value = ctx.String("api-version") + + u, err := url.Parse(snaptel.FlURL.Value) + if err != nil { + glog.Fatal(err) + } + + c := client.NewHTTPClientWithConfig(nil, &client.TransportConfig{Host: u.Host, BasePath: snaptel.FlAPIVer.Value, Schemes: []string{u.Scheme}}) + snaptel.SetClient(c) + + return nil +} + +// ByCommand contains array of CLI commands. +type ByCommand []cli.Command + +func (s ByCommand) Len() int { + return len(s) +} +func (s ByCommand) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} +func (s ByCommand) Less(i, j int) bool { + if s[i].Name == "help" { + return false + } + if s[j].Name == "help" { + return true + } + return s[i].Name < s[j].Name +} diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..b56a95b --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash + +#http://www.apache.org/licenses/LICENSE-2.0.txt +# +# +#Copyright 2017 Intel Corporation +# +#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. + +set -e +set -u +set -o pipefail + +__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +__proj_dir="$(dirname "$__dir")" + +# shellcheck source=scripts/common.sh +. "${__dir}/common.sh" + +_info "project path: ${__proj_dir}" + +git_version=$(_git_version) +go_build=(go build -ldflags "-w -X main.gitversion=${git_version}") + +_info "snap build version: ${git_version}" +_info "git commit: $(git log --pretty=format:"%H" -1)" + +# rebuild binaries: +export GOOS=${GOOS:-$(go env GOOS)} +export GOARCH=${GOARCH:-$(go env GOARCH)} + +# Disable CGO for builds (except freebsd) +if [[ "${GOOS}" == "freebsd" ]]; then + _info "CGO enabled for freebsd" + export CGO_ENABLED=1 +else + export CGO_ENABLED=0 +fi + +if [[ "${GOARCH}" == "amd64" ]]; then + build_path="${__proj_dir}/build/${GOOS}/x86_64" +else + build_path="${__proj_dir}/build/${GOOS}/${GOARCH}" +fi + +snaptel="snaptel" +if [[ "${GOOS}" == "windows" ]]; then + snaptel="${snaptel}.exe" +fi + +mkdir -p "${build_path}" +_info "building snapteld/${snaptel} for ${GOOS}/${GOARCH}" +(cd "${__proj_dir}" && "${go_build[@]}" -o "${build_path}/${snaptel}" . || exit 1) diff --git a/scripts/common.sh b/scripts/common.sh new file mode 100755 index 0000000..98f4b7a --- /dev/null +++ b/scripts/common.sh @@ -0,0 +1,130 @@ +#!/bin/bash +# File managed by pluginsync + +# http://www.apache.org/licenses/LICENSE-2.0.txt +# +# +# Copyright 2017 Intel Corporation +# +# 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. + +set -e +set -u +set -o pipefail + +LOG_LEVEL="${LOG_LEVEL:-6}" +NO_COLOR="${NO_COLOR:-}" +NO_GO_TEST=${NO_GO_TEST:-'-not -path "./.*" -not -path "*/_*" -not -path "./Godeps/*" -not -path "./vendor/*"'} + +trap_exitcode() { + exit $? +} + +trap trap_exitcode SIGINT + +_fmt () { + local color_debug="\x1b[35m" + local color_info="\x1b[32m" + local color_notice="\x1b[34m" + local color_warning="\x1b[33m" + local color_error="\x1b[31m" + local colorvar=color_$1 + + local color="${!colorvar:-$color_error}" + local color_reset="\x1b[0m" + if [ "${NO_COLOR}" = "true" ] || [[ "${TERM:-}" != "xterm"* ]] || [ -t 1 ]; then + # Don't use colors on pipes or non-recognized terminals + color=""; color_reset="" + fi + echo -e "$(date -u +"%Y-%m-%d %H:%M:%S UTC") ${color}$(printf "[%9s]" "${1}")${color_reset}"; +} + +_debug () { [ "${LOG_LEVEL}" -ge 7 ] && echo "$(_fmt debug) ${*}" 1>&2 || true; } +_info () { [ "${LOG_LEVEL}" -ge 6 ] && echo "$(_fmt info) ${*}" 1>&2 || true; } +_notice () { [ "${LOG_LEVEL}" -ge 5 ] && echo "$(_fmt notice) ${*}" 1>&2 || true; } +_warning () { [ "${LOG_LEVEL}" -ge 4 ] && echo "$(_fmt warning) ${*}" 1>&2 || true; } +_error () { [ "${LOG_LEVEL}" -ge 3 ] && echo "$(_fmt error) ${*}" 1>&2 || true; exit 1; } + +_test_files() { + local test_files=$(sh -c "find . -type f -name '*.go' ${NO_GO_TEST} -print") + _debug "go source files ${test_files}" + echo "${test_files}" +} + +_test_dirs() { + local test_dirs=$(sh -c "find . -type f -name '*.go' ${NO_GO_TEST} -print0" | xargs -0 -n1 dirname | sort -u) + _debug "go code directories ${test_dirs}" + echo "${test_dirs}" +} + +_go_get() { + local _url=$1 + local _util + + _util=$(basename "${_url}") + + type -p "${_util}" > /dev/null || go get "${_url}" && _debug "go get ${_util} ${_url}" +} + +_gofmt() { + test -z "$(gofmt -l -d $(_test_files) | tee /dev/stderr)" +} + +_goimports() { + _go_get golang.org/x/tools/cmd/goimports + test -z "$(goimports -l -d $(_test_files) | tee /dev/stderr)" +} + +_golint() { + _go_get github.com/golang/lint/golint + golint ./... +} + +_go_vet() { + go vet $(_test_dirs) +} + +_go_race() { + go test -race ./... +} + +_go_test() { + _info "running test type: ${TEST_TYPE}" + # Standard go tooling behavior is to ignore dirs with leading underscors + for dir in $(_test_dirs); + do + if [[ -z ${go_cover+x} ]]; then + _debug "running go test with cover in ${dir}" + go test -v --tags="${TEST_TYPE}" -covermode=count -coverprofile="${dir}/profile.tmp" "${dir}" + if [ -f "${dir}/profile.tmp" ]; then + tail -n +2 "${dir}/profile.tmp" >> profile.cov + rm "${dir}/profile.tmp" + fi + else + _debug "running go test without cover in ${dir}" + go test -v --tags="${TEST_TYPE}" "${dir}" + fi + done +} + +_go_cover() { + go tool cover -func profile.cov +} + +_git_version() { + git_branch=$(git symbolic-ref HEAD 2> /dev/null | cut -b 12-) + git_branch="${git_branch:-test}" + git_sha=$(git log --pretty=format:"%h" -1) + git_version=$(git describe --always --exact-match 2> /dev/null || echo "${git_branch}-${git_sha}") + echo "${git_version}" +} diff --git a/scripts/deps.sh b/scripts/deps.sh new file mode 100755 index 0000000..75b1420 --- /dev/null +++ b/scripts/deps.sh @@ -0,0 +1,73 @@ +#!/bin/bash +# File managed by pluginsync + +# http://www.apache.org/licenses/LICENSE-2.0.txt +# +# +# Copyright 2017 Intel Corporation +# +# 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. + +set -e +set -u +set -o pipefail + +__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +__proj_dir="$(dirname "$__dir")" + +# shellcheck source=scripts/common.sh +. "${__dir}/common.sh" + +detect_go_dep() { + [[ -f "${__proj_dir}/Godeps/Godeps.json" ]] && _dep='godep' + [[ -f "${__proj_dir}/glide.yaml" ]] && _dep='glide' + [[ -f "${__proj_dir}/vendor/vendor.json" ]] && _dep='govendor' + _info "golang dependency tool: ${_dep}" + echo "${_dep}" +} + +install_go_dep() { + local _dep=${_dep:=$(_detect_dep)} + _info "ensuring ${_dep} is available" + case $_dep in + godep) + _go_get github.com/tools/godep + ;; + glide) + _go_get github.com/Masterminds/glide + ;; + govendor) + _go_get github.com/kardianos/govendor + ;; + esac +} + +restore_go_dep() { + local _dep=${_dep:=$(_detect_dep)} + _info "restoring dependency with ${_dep}" + case $_dep in + godep) + (cd "${__proj_dir}" && godep restore) + ;; + glide) + (cd "${__proj_dir}" && glide install) + ;; + govendor) + (cd "${__proj_dir}" && govendor sync) + ;; + esac +} + +_dep=$(detect_go_dep) +install_go_dep +restore_go_dep diff --git a/snaptel/commands.go b/snaptel/commands.go new file mode 100644 index 0000000..5239e3a --- /dev/null +++ b/snaptel/commands.go @@ -0,0 +1,199 @@ +/* +http://www.apache.org/licenses/LICENSE-2.0.txt + + +Copyright 2017 Intel Corporation + +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 snaptel + +import ( + "fmt" + "strings" + "text/tabwriter" + + "github.com/urfave/cli" +) + +var ( + // Commands defines a list of Snap CLI commands. + Commands = []cli.Command{ + { + Name: "task", + Subcommands: []cli.Command{ + { + Name: "create", + Description: "Creates a new task in the snap scheduler", + Usage: "There are two ways to create a task.\n\t1) Use a task manifest with [--task-manifest]\n\t2) Provide a workflow manifest and schedule details.\n\n\t* Note: Start, stop date/time, and count are optional.\n\t* Using `task create -h` to see options.\n", + Action: createTask, + Flags: []cli.Flag{ + flTaskManifest, + flWorkfowManifest, + flTaskSchedInterval, + flTaskSchedCount, + flTaskSchedStartDate, + flTaskSchedStartTime, + flTaskSchedStopDate, + flTaskSchedStopTime, + flTaskName, + flTaskSchedDuration, + flTaskSchedNoStart, + flTaskDeadline, + flTaskMaxFailures, + }, + }, + { + Name: "list", + Usage: "list or list --verbose", + Action: listTask, + Flags: []cli.Flag{ + flTaskManifest, + flWorkfowManifest, + flTaskSchedInterval, + flTaskSchedCount, + flTaskSchedStartDate, + flTaskSchedStartTime, + flTaskSchedStopDate, + flTaskSchedStopTime, + flTaskName, + flTaskSchedDuration, + flTaskSchedNoStart, + flTaskDeadline, + flTaskMaxFailures, + }, + }, + { + Name: "start", + Usage: "start ", + Action: startTask, + }, + { + Name: "stop", + Usage: "stop ", + Action: stopTask, + }, + { + Name: "remove", + Usage: "remove ", + Action: removeTask, + }, + { + Name: "export", + Usage: "export ", + Action: exportTask, + }, + { + Name: "watch", + Usage: "watch or watch --verbose", + Action: watchTask, + Flags: []cli.Flag{ + flVerbose, + }, + }, + { + Name: "enable", + Usage: "enable ", + Action: enableTask, + }, + }, + }, + { + Name: "plugin", + Subcommands: []cli.Command{ + { + Name: "load", + Usage: "load [--plugin-cert= --plugin-key= --plugin-ca-certs=]", + Action: loadPlugin, + Flags: []cli.Flag{ + flPluginAsc, + flPluginCert, + flPluginKey, + flPluginCACerts, + }, + }, + { + Name: "unload", + Usage: "unload ", + Action: unloadPlugin, + }, + { + Name: "list", + Usage: "list or list --running", + Action: listPlugins, + Flags: []cli.Flag{ + flRunning, + }, + }, + { + Name: "config", + Subcommands: []cli.Command{ + { + Name: "get", + Usage: "get :: or get -t -n -v ", + Action: getConfig, + Flags: []cli.Flag{ + flPluginName, + flPluginType, + flPluginVersion, + }, + }, + }, + }, + }, + }, + { + Name: "metric", + Subcommands: []cli.Command{ + { + Name: "list", + Usage: "list", + Action: listMetrics, + Flags: []cli.Flag{ + flMetricVersion, + flMetricNamespace, + flVerbose, + }, + }, + { + Name: "get", + Usage: `get -m or get -m -v `, + Action: getMetric, + Flags: []cli.Flag{ + flMetricVersion, + flMetricNamespace, + }, + }, + }, + }, + } +) + +func printFields(tw *tabwriter.Writer, indent bool, width int, fields ...interface{}) { + var argArray []interface{} + if indent { + argArray = append(argArray, strings.Repeat(" ", width)) + } + for i, field := range fields { + if field != nil { + argArray = append(argArray, field) + } else { + argArray = append(argArray, "") + } + if i < (len(fields) - 1) { + argArray = append(argArray, "\t") + } + } + fmt.Fprintln(tw, argArray...) +} diff --git a/snaptel/common.go b/snaptel/common.go new file mode 100644 index 0000000..9894656 --- /dev/null +++ b/snaptel/common.go @@ -0,0 +1,111 @@ +/* +http://www.apache.org/licenses/LICENSE-2.0.txt + + +Copyright 2017 Intel Corporation + +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 snaptel + +import ( + "fmt" + + snapClient "github.com/intelsdi-x/snap-client-go/client" + "github.com/intelsdi-x/snap-client-go/client/plugins" + "github.com/intelsdi-x/snap-client-go/client/tasks" + "github.com/urfave/cli" +) + +var client *snapClient.Snap + +// UsageError defines the error message and CLI context +type UsageError struct { + s string + ctx *cli.Context +} + +// Error prints the usage error +func (ue UsageError) Error() string { + return fmt.Sprintf("Error: %s \nUsage: %s", ue.s, ue.ctx.Command.Usage) +} + +// Help displays the command help +func (ue UsageError) Help() { + cli.ShowCommandHelp(ue.ctx, ue.ctx.Command.Name) +} + +func newUsageError(s string, ctx *cli.Context) UsageError { + return UsageError{s, ctx} +} + +// SetClient provides a way to set the private snapClient in this package. +func SetClient(cl *snapClient.Snap) { + client = cl +} + +// GetFirstChar gets the first character of a giving string. +func GetFirstChar(s string) string { + firstChar := "" + for _, r := range s { + firstChar = fmt.Sprintf("%c", r) + break + } + return firstChar +} + +func getErrorDetail(err error, ctx *cli.Context) error { + switch err.(type) { + case *plugins.GetMetricsNotFound: + return newUsageError(fmt.Sprintf("%v", err.(*plugins.GetMetricsNotFound).Payload.ErrorMessage), ctx) + case *plugins.GetMetricsInternalServerError: + return newUsageError(fmt.Sprintf("%v", err.(*plugins.GetMetricsInternalServerError).Payload.ErrorMessage), ctx) + case *plugins.LoadPluginBadRequest: + return newUsageError(fmt.Sprintf("%v", err.(*plugins.LoadPluginBadRequest).Payload.ErrorMessage), ctx) + case *plugins.LoadPluginConflict: + return newUsageError(fmt.Sprintf("%v", err.(*plugins.LoadPluginConflict).Payload.ErrorMessage), ctx) + case *plugins.LoadPluginInternalServerError: + return newUsageError(fmt.Sprintf("%v", err.(*plugins.LoadPluginInternalServerError).Payload.ErrorMessage), ctx) + case *plugins.LoadPluginUnsupportedMediaType: + return newUsageError(fmt.Sprintf("\n%v", err.(*plugins.LoadPluginUnsupportedMediaType).Payload.ErrorMessage), ctx) + case *plugins.UnloadPluginBadRequest: + return newUsageError(fmt.Sprintf("%v", err.(*plugins.UnloadPluginBadRequest).Payload.ErrorMessage), ctx) + case *plugins.UnloadPluginConflict: + return fmt.Errorf("%v", err.(*plugins.UnloadPluginConflict).Payload.ErrorMessage) + case *plugins.UnloadPluginInternalServerError: + return newUsageError(fmt.Sprintf("%v", err.(*plugins.UnloadPluginInternalServerError).Payload.ErrorMessage), ctx) + case *plugins.UnloadPluginNotFound: + return newUsageError(fmt.Sprintf("%v", err.(*plugins.UnloadPluginNotFound).Payload.ErrorMessage), ctx) + case *plugins.GetPluginBadRequest: + return newUsageError(fmt.Sprintf("%v", err.(*plugins.GetPluginBadRequest).Payload.ErrorMessage), ctx) + case *plugins.GetPluginInternalServerError: + return newUsageError(fmt.Sprintf("%v", err.(*plugins.GetPluginInternalServerError).Payload.ErrorMessage), ctx) + case *plugins.GetPluginNotFound: + return newUsageError(fmt.Sprintf("%v", err.(*plugins.GetPluginNotFound).Payload.ErrorMessage), ctx) + case *plugins.GetPluginConfigItemBadRequest: + return newUsageError(fmt.Sprintf("%v", err.(*plugins.GetPluginConfigItemBadRequest).Payload.ErrorMessage), ctx) + case *tasks.GetTaskNotFound: + return newUsageError(fmt.Sprintf("%v", err.(*tasks.GetTaskNotFound).Payload.ErrorMessage), ctx) + case *tasks.AddTaskInternalServerError: + return newUsageError(fmt.Sprintf("%v", err.(*tasks.AddTaskInternalServerError).Payload.ErrorMessage), ctx) + case *tasks.UpdateTaskStateBadRequest: + return newUsageError(fmt.Sprintf("%v", err.(*tasks.UpdateTaskStateBadRequest).Payload.ErrorMessage), ctx) + case *tasks.UpdateTaskStateConflict: + return newUsageError(fmt.Sprintf("%v", err.(*tasks.UpdateTaskStateConflict).Payload.ErrorMessage), ctx) + case *tasks.UpdateTaskStateInternalServerError: + return newUsageError(fmt.Sprintf("%v", err.(*tasks.UpdateTaskStateInternalServerError).Payload.ErrorMessage), ctx) + default: + return newUsageError(fmt.Sprintf("Error: %v", err), ctx) + } +} diff --git a/snaptel/config.go b/snaptel/config.go new file mode 100644 index 0000000..d360fa1 --- /dev/null +++ b/snaptel/config.go @@ -0,0 +1,86 @@ +/* +http://www.apache.org/licenses/LICENSE-2.0.txt + + +Copyright 2017 Intel Corporation + +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 snaptel + +import ( + "os" + "path/filepath" + "reflect" + "strconv" + "text/tabwriter" + + "github.com/intelsdi-x/snap-client-go/client/plugins" + "github.com/urfave/cli" +) + +func getConfig(ctx *cli.Context) error { + pDetails := filepath.SplitList(ctx.Args().First()) + var ptyp string + var pname string + var pver int + var err error + + if len(pDetails) == 3 { + ptyp = pDetails[0] + pname = pDetails[1] + pver, err = strconv.Atoi(pDetails[2]) + if err != nil { + return newUsageError("Can't convert version string to integer", ctx) + } + } else { + ptyp = ctx.String("plugin-type") + pname = ctx.String("plugin-name") + pver = ctx.Int("plugin-version") + } + + if ptyp == "" { + return newUsageError("Must provide plugin type", ctx) + } + if pname == "" { + return newUsageError("Must provide plugin name", ctx) + } + if pver < 1 { + return newUsageError("Plugin version must be greater than 0", ctx) + } + w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0) + defer w.Flush() + + params := plugins.NewGetPluginConfigItemParams() + params.SetPtype(ptyp) + params.SetPname(pname) + params.SetPversion(int64(pver)) + + resp, err := client.Plugins.GetPluginConfigItem(params) + if err != nil { + return getErrorDetail(err, ctx) + } + + printFields(w, false, 0, + "NAME", + "VALUE", + "TYPE", + ) + + cfg := resp.Payload.(map[string]interface{}) + for k, v := range cfg { + printFields(w, false, 0, k, v, reflect.TypeOf(v)) + } + return nil +} diff --git a/snaptel/flags.go b/snaptel/flags.go new file mode 100644 index 0000000..cdf9a5d --- /dev/null +++ b/snaptel/flags.go @@ -0,0 +1,168 @@ +/* +http://www.apache.org/licenses/LICENSE-2.0.txt + + +Copyright 2017 Intel Corporation + +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 snaptel + +import ( + "time" + + "github.com/urfave/cli" +) + +// FlURL to FlTimeout are Main flags +var ( + FlURL = cli.StringFlag{ + Name: "url, u", + Usage: "Sets the URL to use", + EnvVar: "SNAP_URL", + Value: "http://localhost:8181", + } + FlAPIVer = cli.StringFlag{ + Name: "api-version, a", + Usage: "The Snap API version", + EnvVar: "SNAP_API_VERSION", + Value: "v2", + } + FlSecure = cli.BoolFlag{ + Name: "insecure", + Usage: "Ignore certificate errors when Snap's API is running HTTPS", + EnvVar: "SNAP_INSECURE", + } + flRunning = cli.BoolFlag{ + Name: "running", + Usage: "Shows running plugins", + } + FlPassword = cli.BoolFlag{ + Name: "password, p", + Usage: "Require password for REST API authentication", + EnvVar: "SNAP_REST_PASSWORD", + } + FlConfig = cli.StringFlag{ + Name: "config, c", + EnvVar: "SNAPTEL_CONFIG_PATH,SNAPCTL_CONFIG_PATH", + Usage: "Path to a config file", + Value: "", + } + FlTimeout = cli.DurationFlag{ + Name: "timeout, t", + Usage: "Timeout to be set on HTTP request to the server", + Value: 10 * time.Second, + } + + // Plugin flags + flPluginAsc = cli.StringFlag{ + Name: "plugin-asc, a", + Usage: "The plugin asc", + } + flPluginCert = cli.StringFlag{ + Name: "plugin-cert, c", + Usage: "The path to plugin certificate file", + } + flPluginKey = cli.StringFlag{ + Name: "plugin-key, k", + Usage: "The path to plugin private key file", + } + flPluginCACerts = cli.StringFlag{ + Name: "plugin-ca-certs, r", + Usage: "List of CA cert paths (directory/file) for plugin to verify TLS clients", + } + flPluginType = cli.StringFlag{ + Name: "plugin-type, t", + Usage: "The plugin type", + } + flPluginName = cli.StringFlag{ + Name: "plugin-name, n", + Usage: "The plugin name", + } + flPluginVersion = cli.IntFlag{ + Name: "plugin-version, v", + Usage: "The plugin version", + } + + // Task flags + flTaskName = cli.StringFlag{ + Name: "name, n", + Usage: "Optional requirement for giving task names", + Value: "", + } + flTaskManifest = cli.StringFlag{ + Name: "task-manifest, t", + Usage: "File path for task manifest to use for task creation.", + } + flWorkfowManifest = cli.StringFlag{ + Name: "workflow-manifest, w", + Usage: "File path for workflow manifest to use for task creation", + } + flTaskSchedInterval = cli.StringFlag{ + Name: "interval, i", + Usage: "Interval for the task schedule [ex (simple schedule): 250ms, 1s, 30m (cron schedule): \"0 * * * * *\"]", + } + flTaskSchedStartTime = cli.StringFlag{ + Name: "start-time", + Usage: "Start time for the task schedule [defaults to now]", + } + flTaskSchedStopTime = cli.StringFlag{ + Name: "stop-time", + Usage: "Start time for the task schedule [defaults to now]", + } + flTaskSchedStartDate = cli.StringFlag{ + Name: "start-date", + Usage: "Start date for the task schedule [defaults to today]", + } + flTaskSchedStopDate = cli.StringFlag{ + Name: "stop-date", + Usage: "Stop date for the task schedule [defaults to today]", + } + flTaskSchedCount = cli.StringFlag{ + Name: "count", + Usage: "The count of runs for the task schedule [defaults to 0 what means no limit, e.g. set to 1 determines a single run task]", + } + flTaskSchedDuration = cli.StringFlag{ + Name: "duration, d", + Usage: "The amount of time to run the task [appends to start or creates a start time before a stop]", + } + flTaskSchedNoStart = cli.BoolFlag{ + Name: "no-start", + Usage: "Do not start task on creation [normally started on creation]", + } + flTaskDeadline = cli.StringFlag{ + Name: "deadline", + Usage: "The deadline for the task to be killed after started if the task runs too long (All tasks default to 5s)", + } + flTaskMaxFailures = cli.StringFlag{ + Name: "max-failures", + Usage: "The number of consecutive failures before Snap disables the task", + } + + // metric + flMetricVersion = cli.IntFlag{ + Name: "metric-version, v", + Usage: "The metric version. 0 (default) returns all. -1 returns latest.", + } + flMetricNamespace = cli.StringFlag{ + Name: "metric-namespace, m", + Usage: "A metric namespace", + } + + // general + flVerbose = cli.BoolFlag{ + Name: "verbose", + Usage: "Verbose output", + } +) diff --git a/snaptel/metric.go b/snaptel/metric.go new file mode 100644 index 0000000..db0b5e3 --- /dev/null +++ b/snaptel/metric.go @@ -0,0 +1,194 @@ +/* +http://www.apache.org/licenses/LICENSE-2.0.txt + + +Copyright 2017 Intel Corporation + +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 snaptel + +import ( + "fmt" + "os" + "sort" + "strconv" + "strings" + "text/tabwriter" + "time" + + "github.com/intelsdi-x/snap-client-go/client/plugins" + "github.com/intelsdi-x/snap-client-go/models" + "github.com/urfave/cli" +) + +func listMetrics(ctx *cli.Context) error { + verbose := ctx.Bool("verbose") + + metrics, err := queryMetrics(ctx) + if err != nil { + return err + } + + /* + NAMESPACE VERSION + /intel/mock/foo 1,2 + /intel/mock/bar 1 + */ + w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0) + + if verbose { + + // NAMESPACE VERSION UNIT DESCRIPTION + // /intel/mock/foo 1 + // /intel/mock/foo 2 mock unit mock description + // /intel/mock/[host]/baz 2 mock unit mock description + + printFields(w, false, 0, "NAMESPACE", "VERSION", "UNIT", "DESCRIPTION") + for _, mt := range metrics { + namespace := getNamespace(mt) + printFields(w, false, 0, namespace, mt.Version, mt.Unit, mt.Description) + } + w.Flush() + return nil + } + + // groups the same namespace of different versions. + metsByVer := make(map[string][]string) + for _, mt := range metrics { + metsByVer[*mt.Namespace] = append(metsByVer[*mt.Namespace], strconv.Itoa(int(mt.Version))) + } + //make list in alphabetical order + var key []string + for k := range metsByVer { + key = append(key, k) + } + sort.Strings(key) + + printFields(w, false, 0, "NAMESPACE", "VERSIONS") + for _, ns := range key { + printFields(w, false, 0, ns, strings.Join(metsByVer[ns], ",")) + } + w.Flush() + return nil +} + +func printMetric(metric *models.Metric, idx int) error { + /* + NAMESPACE VERSION LAST ADVERTISED TIME + /intel/mock/foo 2 Wed, 09 Sep 2015 10:01:04 PDT + + Rules for collecting /intel/mock/foo: + + NAME TYPE DEFAULT REQUIRED MINIMUM MAXIMUM + name string bob false + password string true + portRange int false 9000 10000 + */ + + namespace := getNamespace(metric) + + if idx > 0 { + fmt.Printf("\n") + } + w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0) + printFields(w, false, 0, "NAMESPACE", "VERSION", "UNIT", "LAST ADVERTISED TIME", "DESCRIPTION") + printFields(w, false, 0, namespace, metric.Version, metric.Unit, time.Unix(metric.LastAdvertisedTimestamp, 0).Format(time.RFC1123), metric.Description) + w.Flush() + if metric.Dynamic { + + // NAMESPACE VERSION UNIT LAST ADVERTISED TIME DESCRIPTION + // /intel/mock/[host]/baz 2 mock unit Wed, 09 Sep 2015 10:01:04 PDT mock description + // + // Dynamic elements of namespace: /intel/mock/[host]/baz + // + // NAME DESCRIPTION + // host name of the host + // + // Rules for collecting /intel/mock/[host]/baz: + // + // NAME TYPE DEFAULT REQUIRED MINIMUM MAXIMUM + + fmt.Printf("\n Dynamic elements of namespace: %s\n\n", namespace) + printFields(w, true, 6, "NAME", "DESCRIPTION") + for _, v := range metric.DynamicElements { + printFields(w, true, 6, v.Name, v.Description) + } + w.Flush() + } + fmt.Printf("\n Rules for collecting %s:\n\n", namespace) + printFields(w, true, 6, "NAME", "TYPE", "DEFAULT", "REQUIRED", "MINIMUM", "MAXIMUM") + for _, rule := range metric.Policy { + printFields(w, true, 6, rule.Name, rule.Type, rule.Default, rule.Required, rule.Minimum, rule.Maximum) + } + w.Flush() + return nil +} + +func getMetric(ctx *cli.Context) error { + if !ctx.IsSet("metric-namespace") { + return newUsageError("Error: Must provide metric namespace", ctx) + } + metrics, err := queryMetrics(ctx) + if err != nil { + return err + } + + for i, m := range metrics { + err := printMetric(m, i) + if err != nil { + return err + } + } + return nil +} + +func getNamespace(mt *models.Metric) string { + ns := mt.Namespace + if mt.Dynamic { + fc := GetFirstChar(*ns) + slice := strings.Split(*ns, fc) + for _, v := range mt.DynamicElements { + slice[v.Index+1] = "[" + *v.Name + "]" + } + *ns = strings.Join(slice, fc) + } + return *ns +} + +func queryMetrics(ctx *cli.Context) ([]*models.Metric, error) { + ns := ctx.String("metric-namespace") + ver := ctx.Int("metric-version") + params := plugins.NewGetMetricsParamsWithTimeout(FlTimeout.Value) + + if strings.Trim(ns, " ") != "" { + params.SetNs(&ns) + } + if ver > 0 { + ver64 := int64(ver) + params.SetVer(&ver64) + } + + resp, err := client.Plugins.GetMetrics(params) + if err != nil { + return nil, getErrorDetail(err, ctx) + } + + if (len(ns) > 0 || ver > 0) && len(resp.Payload.Metrics) == 0 { + return nil, fmt.Errorf("No metric found the giving namespace %s, version %d", ns, ver) + } else if len(resp.Payload.Metrics) == 0 { + return nil, fmt.Errorf("No metrics found. Have you loaded any collectors yet?") + } + return resp.Payload.Metrics, nil +} diff --git a/snaptel/plugin.go b/snaptel/plugin.go new file mode 100644 index 0000000..f4ae475 --- /dev/null +++ b/snaptel/plugin.go @@ -0,0 +1,156 @@ +/* +http://www.apache.org/licenses/LICENSE-2.0.txt + + +Copyright 2017 Intel Corporation + +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 snaptel + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "text/tabwriter" + "time" + + "github.com/intelsdi-x/snap-client-go/client/plugins" + "github.com/urfave/cli" +) + +func loadPlugin(ctx *cli.Context) error { + pAsc := ctx.String("plugin-asc") + var paths []string + if len(ctx.Args()) != 1 { + return newUsageError("Incorrect usage:", ctx) + } + paths = append(paths, ctx.Args().First()) + if pAsc != "" { + if !strings.Contains(pAsc, ".asc") { + return newUsageError("Must be a .asc file for the -a flag", ctx) + } + paths = append(paths, pAsc) + } + + params := plugins.NewLoadPluginParamsWithTimeout(FlTimeout.Value) + f, err := os.Open(filepath.Join(paths...)) + if err != nil { + return newUsageError("Cannot open the plugin", ctx) + } + defer f.Close() + params.SetPluginData(f) + + resp, err := client.Plugins.LoadPlugin(params) + if err != nil { + return getErrorDetail(err, ctx) + } + + p := resp.Payload + fmt.Println("Plugin loaded") + fmt.Printf("Name: %s\n", p.Name) + fmt.Printf("Version: %d\n", p.Version) + fmt.Printf("Type: %s\n", p.Type) + fmt.Printf("Signed: %v\n", p.Signed) + fmt.Printf("Loaded Time: %s\n\n", time.Unix(p.LoadedTimestamp, 0).Format(time.RFC1123)) + + return nil +} + +func unloadPlugin(ctx *cli.Context) error { + if len(ctx.Args()) < 2 { + return newUsageError("Incorrect usage:", ctx) + } + + pType := ctx.Args().Get(0) + pName := ctx.Args().Get(1) + pVerStr := ctx.Args().Get(2) + + if pType == "" { + return newUsageError("Must provide plugin type", ctx) + } + if pName == "" { + return newUsageError("Must provide plugin name", ctx) + } + if pVerStr == "" { + return newUsageError("Must provide plugin version", ctx) + } + + pVer, err := strconv.Atoi(pVerStr) + if err != nil { + return newUsageError("Can't convert version string to integer", ctx) + } + if pVer < 1 { + return newUsageError("Plugin version must be greater than zero", ctx) + } + + params := plugins.NewUnloadPluginParamsWithTimeout(FlTimeout.Value) + params.SetPname(pName) + params.SetPtype(pType) + params.SetPversion(int64(pVer)) + + _, err = client.Plugins.UnloadPlugin(params) + if err != nil { + return getErrorDetail(err, ctx) + } + + fmt.Println("Plugin unloaded") + fmt.Printf("Name: %s\n", pName) + fmt.Printf("Version: %d\n", pVer) + fmt.Printf("Type: %s\n", pType) + + return nil +} + +func listPlugins(ctx *cli.Context) error { + running := ctx.Bool("running") + params := plugins.NewGetPluginsParamsWithTimeout(FlTimeout.Value) + if running { + params.SetRunning(&running) + } + + resp, err := client.Plugins.GetPlugins(params) + if err != nil { + return getErrorDetail(err, ctx) + } + + lps := len(resp.Payload.Plugins) + + w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0) + if running { + if lps == 0 { + fmt.Println("No running plugins found. Have you started a task?") + return nil + } + + printFields(w, false, 0, "NAME", "HIT COUNT", "LAST HIT", "TYPE", "PPROF PORT") + for _, rp := range resp.Payload.Plugins { + printFields(w, false, 0, rp.Name, rp.HitCount, time.Unix(rp.LastHitTimestamp, 0).Format(time.RFC1123), rp.Type, rp.PprofPort) + } + } else { + if lps == 0 { + fmt.Println("No plugins found. Have you loaded a plugin?") + return nil + } + printFields(w, false, 0, "NAME", "VERSION", "TYPE", "SIGNED", "STATUS", "LOADED TIME") + for _, lp := range resp.Payload.Plugins { + printFields(w, false, 0, lp.Name, lp.Version, lp.Type, lp.Signed, lp.Status, time.Unix(lp.LoadedTimestamp, 0).Format(time.RFC1123)) + } + } + w.Flush() + + return nil +} diff --git a/snaptel/task.go b/snaptel/task.go new file mode 100644 index 0000000..2a837ff --- /dev/null +++ b/snaptel/task.go @@ -0,0 +1,767 @@ +/* +http://www.apache.org/licenses/LICENSE-2.0.txt + + +Copyright 2017 Intel Corporation + +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 snaptel + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "text/tabwriter" + "time" + + yaml "gopkg.in/yaml.v2" + + "golang.org/x/crypto/ssh/terminal" + + "github.com/intelsdi-x/snap-client-go/client/tasks" + "github.com/intelsdi-x/snap-client-go/models" + "github.com/robfig/cron" + "github.com/urfave/cli" +) + +var ( + // padding to picking a time to start a "NOW" task + createTaskNowPad = time.Second * 1 + timeParseFormat = "3:04PM" + dateParseFormat = "1-02-2006" + unionParseFormat = timeParseFormat + " " + dateParseFormat +) + +// Constants used to truncate task hit and miss counts +// e.g. 1K(10^3), 1M(10^6, 1G(10^9) etc (not 1024^#). We do not +// use units larger than Gb to support 32 bit compiles. +const ( + K = 1000 + M = 1000 * K + G = 1000 * M +) + +func trunc(n int) string { + var u string + + switch { + case n >= G: + u = "G" + n /= G + case n >= M: + u = "M" + n /= M + case n >= K: + u = "K" + n /= K + default: + return strconv.Itoa(n) + } + return strconv.Itoa(n) + u +} + +func createTask(ctx *cli.Context) error { + var err error + if ctx.IsSet("task-manifest") { + err = createTaskUsingTaskManifest(ctx) + } else if ctx.IsSet("workflow-manifest") { + err = createTaskUsingWFManifest(ctx) + } else { + return newUsageError("Must provide either --task-manifest or --workflow-manifest arguments", ctx) + } + return err +} + +func stringValToInt(val string) (int, error) { + // parse the input (string) as an integer value (and return that integer value + // to the caller or an error if the input value cannot be parsed as an integer) + parsedField, err := strconv.Atoi(val) + if err != nil { + splitErr := strings.Split(err.Error(), ": ") + errStr := splitErr[len(splitErr)-1] + // return a value of zero and the error encountered during string parsing + return 0, fmt.Errorf("Value '%v' cannot be parsed as an integer (%v)", val, errStr) + } + // return the integer equivalent of the input string and a nil error (indicating success) + return parsedField, nil +} + +// Parses the command-line parameters (if any) and uses them to override the underlying +// schedule for this task or to set a schedule for that task (if one is not already defined, +// as is the case when we're building a new task from a workflow manifest). +// +// Note: in this method, any of the following types of time windows can be specified: +// +// +---------------------------... (start with no stop and no duration; no end time for window) +// | +// start +// +// ...---------------------------+ (stop with no start and no duration; no start time for window) +// | +// stop +// +// +-----------------------------+ (start with a duration but no stop) +// | | +// |---------------------------->| +// start duration +// +// +-----------------------------+ (stop with a duration but no start) +// | | +// |<----------------------------| +// duration stop +// +// +-----------------------------+ (start and stop both specified) +// | | +// |<--------------------------->| +// start stop +// +// +-----------------------------+ (only duration specified, implies start is the current time) +// | | +// |---------------------------->| +// Now() duration +// +func setWindowedSchedule(start *time.Time, stop *time.Time, duration *time.Duration, t *models.Task) error { + // if there is an empty schedule already defined for this task, then set the + // type for that schedule to 'windowed' + if *t.Schedule.Type == "" { + *t.Schedule.Type = "windowed" + } else if *t.Schedule.Type != "windowed" { + // else if the task's existing schedule is not a 'windowed' schedule, + // then return an error + return fmt.Errorf("Usage error (schedule type mismatch); cannot replace existing schedule of type '%v' with a new, 'windowed' schedule", *t.Schedule.Type) + } + // if a duration was passed in, determine the start and stop times for our new + // 'windowed' schedule from the input parameters + if duration != nil { + // if start and stop were both defined, then return an error (since specifying the + // start, stop, *and* duration for a 'windowed' schedule is not allowed) + if start != nil && stop != nil { + return fmt.Errorf("Usage error (too many parameters); the window start, stop, and duration cannot all be specified for a 'windowed' schedule") + } + // if start is set and stop is not then use duration to create stop + if start != nil && stop == nil { + newStop := start.Add(*duration) + t.Schedule.StartTimestamp = start + t.Schedule.StopTimestamp = &newStop + return nil + } + // if stop is set and start is not then use duration to create start + if stop != nil && start == nil { + newStart := stop.Add(*duration * -1) + t.Schedule.StartTimestamp = &newStart + t.Schedule.StopTimestamp = stop + return nil + } + // otherwise, the start and stop are both undefined but a duration was passed in, + // so use the current date/time (plus the 'createTaskNowPad' value) as the + // start date/time and construct a stop date/time from that start date/time + // and the duration + newStart := time.Now().Add(createTaskNowPad) + newStop := newStart.Add(*duration) + t.Schedule.StartTimestamp = &newStart + t.Schedule.StopTimestamp = &newStop + return nil + } + // if a start date/time was specified, we will use it to replace + // the current schedule's start date/time + if start != nil { + t.Schedule.StartTimestamp = start + } + // if a stop date/time was specified, we will use it to replace the + // current schedule's stop date/time + if stop != nil { + t.Schedule.StopTimestamp = stop + } + // if we get this far, then just return a nil error (indicating success) + return nil +} + +// parse the command-line options and use them to setup a new schedule for this task +func setScheduleFromCliOptions(ctx *cli.Context, t *models.Task) error { + // check the start, stop, and duration values to see if we're looking at a windowed schedule (or not) + // first, get the parameters that define the windowed schedule + start := mergeDateTime( + strings.ToUpper(ctx.String("start-time")), + strings.ToUpper(ctx.String("start-date")), + ) + stop := mergeDateTime( + strings.ToUpper(ctx.String("stop-time")), + strings.ToUpper(ctx.String("stop-date")), + ) + // Grab the duration string (if one was passed in) and parse it + durationStr := ctx.String("duration") + var duration *time.Duration + if ctx.IsSet("duration") || durationStr != "" { + d, err := time.ParseDuration(durationStr) + if err != nil { + return fmt.Errorf("Usage error (bad duration format); %v", err) + } + duration = &d + } + // Grab the interval for the schedule (if one was provided). Note that if an + // interval value was not passed in and there is no interval defined for the + // schedule associated with this task, it's an error + interval := ctx.String("interval") + if t.Schedule == nil && !ctx.IsSet("interval") && interval == "" && *(t.Schedule.Interval) == "" { + return fmt.Errorf("Usage error (missing interval value); when constructing a new task schedule an interval must be provided") + } + // if a start, stop, or duration value was provided, or if the existing schedule for this task + // is 'windowed', then it's a 'windowed' schedule + isWindowed := (start != nil || stop != nil || duration != nil || (t.Schedule.Type != nil && *(t.Schedule.Type) == "windowed")) + // if an interval was passed in, then attempt to parse it (first as a duration, + // then as the definition of a cron job) + isCron := false + if interval != "" { + // first try to parse it as a duration + _, err := time.ParseDuration(interval) + if err != nil { + // if that didn't work, then try parsing the interval as cron job entry + _, e := cron.Parse(interval) + if e != nil { + return fmt.Errorf("Usage error (bad interval value): cannot parse interval value '%v' either as a duration or a cron entry", interval) + } + // if it's a 'windowed' schedule, then return an error (we can't use a + // cron entry interval with a 'windowed' schedule) + if isWindowed { + return fmt.Errorf("Usage error; cannot use a cron entry ('%v') as the interval for a 'windowed' schedule", interval) + } + isCron = true + } + t.Schedule.Interval = &interval + } + // if it's a 'windowed' schedule, then create a new 'windowed' schedule and add it to + // the current task; the existing schedule (if on exists) will be replaced by the new + // schedule in this method (note that it is an error to try to replace an existing + // schedule with a new schedule of a different type, so an error will be returned from + // this method call if that is the case) + if isWindowed { + return setWindowedSchedule(start, stop, duration, t) + } + // if it's not a 'windowed' schedule, then set the schedule type based on the 'isCron' flag, + // which was set above. + if isCron { + // make sure the current schedule type (if there is one) matches; if not it is an error + if t.Schedule.Type != nil && *(t.Schedule.Type) != "cron" { + return fmt.Errorf("Usage error; cannot replace existing schedule of type '%v' with a new, 'cron' schedule", t.Schedule.Type) + } + *t.Schedule.Type = "cron" + return nil + } + // if it wasn't a 'windowed' schedule and it's not a 'cron' schedule, then it must be a 'simple' + // schedule, so first make sure the current schedule type (if there is one) matches; if not + // then it's an error + if t.Schedule.Type != nil && *(t.Schedule.Type) != "simple" { + return fmt.Errorf("Usage error; cannot replace existing schedule of type '%v' with a new, 'simple' schedule", t.Schedule.Type) + } + + countValStr := ctx.String("count") + if ctx.IsSet("count") || countValStr != "" { + count, err := stringValToUint64(countValStr) + if err != nil { + return fmt.Errorf("Usage error (bad count format); %v", err) + } + t.Schedule.Count = count + + } + + // if we get this far set the schedule type and return a nil error (indicating success) + ty := "simple" + t.Schedule.Type = &ty + + return nil +} + +// stringValToUint64 parses the input (string) as an unsigned integer value (and returns that uint value +// to the caller or an error if the input value cannot be parsed as an unsigned integer) +func stringValToUint64(val string) (uint64, error) { + parsedField, err := strconv.ParseUint(val, 10, 64) + if err != nil { + splitErr := strings.Split(err.Error(), ": ") + errStr := splitErr[len(splitErr)-1] + // return a value of zero and the error encountered during string parsing + return 0, fmt.Errorf("Value '%v' cannot be parsed as an unsigned integer (%v)", val, errStr) + } + // return the unsigned integer equivalent of the input string and a nil error (indicating success) + return uint64(parsedField), nil +} + +// merge the command-line options into the current task +func mergeCliOptions(ctx *cli.Context, t *models.Task) error { + st := !ctx.IsSet("no-start") + t.Start = st + + // set the name of the task (if a 'name' was provided in the CLI options) + name := ctx.String("name") + if ctx.IsSet("name") || name != "" { + t.Name = name + } + + // set the deadline of the task (if a 'deadline' was provided in the CLI options) + deadline := ctx.String("deadline") + if ctx.IsSet("deadline") || deadline != "" { + t.Deadline = deadline + } + // set the MaxFailures for the task (if a 'max-failures' value was provided in the CLI options) + maxFailuresStrVal := ctx.String("max-failures") + if ctx.IsSet("max-failures") || maxFailuresStrVal != "" { + maxFailures, err := stringValToInt(maxFailuresStrVal) + if err != nil { + return err + } + t.MaxFailures = int64(maxFailures) + } + // set the schedule for the task from the CLI options (and return the results + // of that method call, indicating whether or not an error was encountered while + // setting up that schedule) + return setScheduleFromCliOptions(ctx, t) +} + +func convert(i interface{}) interface{} { + switch x := i.(type) { + case map[interface{}]interface{}: + m2i := map[string]interface{}{} + for k, v := range x { + m2i[k.(string)] = convert(v) + } + return m2i + case []interface{}: + for idx, v := range x { + x[idx] = convert(v) + } + } + return i +} + +func createTaskUsingTaskManifest(ctx *cli.Context) error { + // get the task manifest file to use + path := ctx.String("task-manifest") + ext := filepath.Ext(path) + file, e := ioutil.ReadFile(path) + if e != nil { + return fmt.Errorf("File error [%s] - %v", ext, e) + } + + bts := []byte(os.ExpandEnv(string(file))) + + var tsk *models.Task + switch ext { + case ".yaml", ".yml": + t, err := taskYamlToJSON(ctx, bts) + if err != nil { + return err + } + tsk = t + case ".json": + t, err := taskJSONToJSON(ctx, bts) + if err != nil { + return err + } + tsk = t + default: + return fmt.Errorf("Unsupported file type %s", ext) + } + + // Request parameters + params := tasks.NewAddTaskParamsWithTimeout(FlTimeout.Value) + params.SetTask(tsk) + + resp, err := client.Tasks.AddTask(params) + if err != nil { + return getErrorDetail(err, ctx) + } + + res := resp.Payload + fmt.Println("Task created") + fmt.Printf("ID: %s\n", res.ID) + fmt.Printf("Name: %s\n", res.Name) + fmt.Printf("State: %s\n", res.TaskState) + + return nil +} + +func createTaskUsingWFManifest(ctx *cli.Context) error { + // Get the workflow manifest filename from the command-line + path := ctx.String("workflow-manifest") + ext := filepath.Ext(path) + file, e := ioutil.ReadFile(path) + if e != nil { + return fmt.Errorf("File error [%s] - %v", ext, e) + } + + // check to make sure that an interval was specified using the appropriate command-line flag + interval := ctx.String("interval") + if !ctx.IsSet("interval") || interval == "" { + return fmt.Errorf("Workflow manifest requires that an interval be set via a command-line flag") + } + + var tsk *models.Task + switch ext { + case ".yaml", ".yml": + t, err := wfYamlToJSON(ctx, file) + if err != nil { + return err + } + tsk = t + case ".json": + t, err := wfJSONtoJSON(ctx, file) + if err != nil { + return err + } + tsk = t + default: + return fmt.Errorf("Unsupported file type %s", ext) + } + + params := tasks.NewAddTaskParamsWithTimeout(FlTimeout.Value) + params.SetTask(tsk) + + resp, err := client.Tasks.AddTask(params) + if err != nil { + return getErrorDetail(err, ctx) + } + res := resp.Payload + fmt.Println("Task created") + fmt.Printf("ID: %s\n", res.ID) + fmt.Printf("Name: %s\n", res.Name) + fmt.Printf("State: %s\n", res.TaskState) + + return nil +} + +func mergeDateTime(tm, dt string) *time.Time { + reTm := time.Now().Add(createTaskNowPad) + if dt == "" && tm == "" { + return nil + } + if dt != "" { + t, err := time.Parse(dateParseFormat, dt) + if err != nil { + fmt.Printf("Error creating task:\n%v\n", err) + os.Exit(1) + } + reTm = t + } + + if tm != "" { + _, err := time.ParseInLocation(timeParseFormat, tm, time.Local) + if err != nil { + fmt.Printf("Error creating task:\n%v\n", err) + os.Exit(1) + } + reTm, err = time.ParseInLocation(unionParseFormat, fmt.Sprintf("%s %s", tm, reTm.Format(dateParseFormat)), time.Local) + if err != nil { + fmt.Printf("Error creating task:\n%v\n", err) + os.Exit(1) + } + } + return &reTm +} + +func listTask(ctx *cli.Context) error { + params := tasks.NewGetTasksParamsWithTimeout(FlTimeout.Value) + resp, err := client.Tasks.GetTasks(params) + if err != nil { + return getErrorDetail(err, ctx) + } + + termWidth, _, _ := terminal.GetSize(int(os.Stdout.Fd())) + verbose := ctx.Bool("verbose") + + w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0) + if len(resp.Payload.Tasks) == 0 { + fmt.Println("No task found. Have you created a task?") + return nil + } + printFields(w, false, 0, + "ID", + "NAME", + "STATE", + "HIT", + "MISS", + "FAIL", + "CREATED", + "LAST FAILURE", + ) + for _, task := range resp.Payload.Tasks { + //165 is the width of the error message from ID - LAST FAILURE inclusive. + //If the header row wraps, then the error message will automatically wrap too + if termWidth < 165 { + verbose = true + } + printFields(w, false, 0, + task.ID, + fixSize(verbose, task.Name, 41), + task.TaskState, + trunc(int(task.HitCount)), + trunc(int(task.MissCount)), + trunc(int(task.FailedCount)), + time.Unix(task.CreationTimestamp, 0).Format(time.RFC1123), + /*153 is the width of the error message from ID up to LAST FAILURE*/ + fixSize(verbose, task.LastFailureMessage, termWidth-153), + ) + } + w.Flush() + + return nil +} + +func fixSize(verbose bool, msg string, width int) string { + if len(msg) < width { + for i := len(msg); i < width; i++ { + msg += " " + } + } else if len(msg) > width && !verbose { + return msg[:width-3] + "..." + } + return msg +} + +func startTask(ctx *cli.Context) error { + if len(ctx.Args()) != 1 { + return newUsageError("Incorrect usage", ctx) + } + + id := ctx.Args().First() + + params := tasks.NewUpdateTaskStateParamsWithTimeout(FlTimeout.Value) + params.SetID(id) + params.SetAction("start") + + _, err := client.Tasks.UpdateTaskState(params) + if err != nil { + return getErrorDetail(err, ctx) + } + + fmt.Println("Task started:") + fmt.Printf("ID: %s\n", id) + + return nil +} + +func stopTask(ctx *cli.Context) error { + if len(ctx.Args()) != 1 { + return newUsageError("Incorrect usage", ctx) + } + + id := ctx.Args().First() + + params := tasks.NewUpdateTaskStateParamsWithTimeout(FlTimeout.Value) + params.SetID(id) + params.SetAction("stop") + + _, err := client.Tasks.UpdateTaskState(params) + if err != nil { + return getErrorDetail(err, ctx) + } + + fmt.Println("Task stopped:") + fmt.Printf("ID: %s\n", id) + + return nil +} + +func removeTask(ctx *cli.Context) error { + if len(ctx.Args()) != 1 { + return newUsageError("Incorrect usage", ctx) + } + + id := ctx.Args().First() + + params := tasks.NewRemoveTaskParamsWithTimeout(FlTimeout.Value) + params.SetID(id) + + _, err := client.Tasks.RemoveTask(params) + if err != nil { + return getErrorDetail(err, ctx) + } + + fmt.Println("Task removed:") + fmt.Printf("ID: %s\n", id) + + return nil +} + +func enableTask(ctx *cli.Context) error { + if len(ctx.Args()) != 1 { + return newUsageError("Incorrect usage", ctx) + } + + id := ctx.Args().First() + + params := tasks.NewUpdateTaskStateParamsWithTimeout(FlTimeout.Value) + params.SetID(id) + params.SetAction("enable") + + _, err := client.Tasks.UpdateTaskState(params) + if err != nil { + return getErrorDetail(err, ctx) + } + + fmt.Println("Task enabled:") + fmt.Printf("ID: %s\n", id) + return nil +} + +func exportTask(ctx *cli.Context) error { + if len(ctx.Args()) != 1 { + return newUsageError("Incorrect usage", ctx) + } + id := ctx.Args().First() + params := tasks.NewGetTaskParamsWithTimeout(FlTimeout.Value) + params.SetID(id) + + resp, err := client.Tasks.GetTask(params) + if err != nil { + return getErrorDetail(err, ctx) + } + + tb, err := json.Marshal(resp.Payload) + if err != nil { + return fmt.Errorf("Error exporting task:\n%v", err) + } + fmt.Println(string(tb)) + return nil +} + +func sortTags(tags map[string]string) []string { + var tagSlice []string + var keys []string + for k := range tags { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + tagSlice = append(tagSlice, k+"="+tags[k]) + } + return tagSlice +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func validateTask(t *models.Task) error { + if err := validateScheduleExists(t.Schedule); err != nil { + return err + } + if t.Version != 1 { + return fmt.Errorf("Error: Invalid version provided for task manifest") + } + return nil +} + +func validateScheduleExists(schedule *models.Schedule) error { + if schedule == nil { + return fmt.Errorf("Error: Task manifest did not include a schedule") + } + if *schedule == (models.Schedule{}) { + return fmt.Errorf("Error: Task manifest included an empty schedule. Task manifests need to include a schedule") + } + return nil +} + +func wfYamlToJSON(ctx *cli.Context, bts []byte) (*models.Task, error) { + b, err := yamlToJSON(bts) + if err != nil { + return nil, err + } + + wf := models.WorkflowMap{} + err = json.Unmarshal(b, &wf) + if err != nil { + return nil, fmt.Errorf("Error parsing Workflow JSON: %v", err) + } + + t := models.Task{ + Version: 1, + Schedule: &models.Schedule{}, + } + t.Workflow = &wf + return toTaskJSON(ctx, t) +} + +func taskYamlToJSON(ctx *cli.Context, bts []byte) (*models.Task, error) { + b, err := yamlToJSON(bts) + if err != nil { + return nil, err + } + + t := models.Task{} + err = json.Unmarshal(b, &t) + if err != nil { + return nil, fmt.Errorf("Error parsing JSON file: %v", err) + } + return toTaskJSON(ctx, t) +} + +func yamlToJSON(bts []byte) ([]byte, error) { + var body interface{} + if err := yaml.Unmarshal(bts, &body); err != nil { + return nil, fmt.Errorf("Unmarshal YMAL file error: %v", err) + } + + body = convert(body) + b, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("Marshal interface to bytes error: %v", err) + } + return b, nil +} + +func toTaskJSON(ctx *cli.Context, t models.Task) (*models.Task, error) { + // merge any CLI options specified by the user (if any) into the current task; + // if an error is encountered, return it + if err := mergeCliOptions(ctx, &t); err != nil { + return nil, err + } + + // Validate task manifest includes schedule, workflow, and version + if err := validateTask(&t); err != nil { + return nil, err + } + return &t, nil +} + +func taskJSONToJSON(ctx *cli.Context, bts []byte) (*models.Task, error) { + t := models.Task{} + + if err := json.Unmarshal(bts, &t); err != nil { + return nil, fmt.Errorf("Error parsing JSON file: %v", err) + } + return toTaskJSON(ctx, t) +} + +func wfJSONtoJSON(ctx *cli.Context, bts []byte) (*models.Task, error) { + wf := models.WorkflowMap{} + err := json.Unmarshal(bts, &wf) + if err != nil { + return nil, fmt.Errorf("Error parsing Workflow JSON file: %v", err) + } + + t := models.Task{ + Version: 1, + Schedule: &models.Schedule{}, + } + t.Workflow = &wf + return toTaskJSON(ctx, t) +} diff --git a/snaptel/watch.go b/snaptel/watch.go new file mode 100644 index 0000000..50e1b3b --- /dev/null +++ b/snaptel/watch.go @@ -0,0 +1,142 @@ +/* +http://www.apache.org/licenses/LICENSE-2.0.txt + + +Copyright 2017 Intel Corporation + +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 snaptel + +import ( + "bufio" + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" + "text/tabwriter" + + "github.com/intelsdi-x/snap-client-go/models" + "github.com/urfave/cli" +) + +func watchTask(ctx *cli.Context) error { + if len(ctx.Args()) != 1 { + return newUsageError("Incorrect usage", ctx) + } + + verbose := ctx.Bool("verbose") + id := ctx.Args().First() + url := fmt.Sprintf("%s/%s/tasks/%s/watch", FlURL.Value, FlAPIVer.Value, id) + + // Currently, there is no way to implement a proper idel timeout for streaming. + // Therefore no timeout for this request. + resp, err := http.Get(url) + defer resp.Body.Close() + if err != nil { + return err + } + + var tskEvent models.StreamedTaskEvent + reader := bufio.NewReader(resp.Body) + + delim := []byte{':', ' '} + + fmt.Printf("Watching Task (%s):\n", id) + + w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0) + fields := []interface{}{"NAMESPACE", "DATA", "TIMESTAMP"} + if verbose { + fields = append(fields, "TAGS") + } + printFields(w, false, 0, fields...) + w.Flush() + + for { + bs, err := reader.ReadBytes('\n') + if err != nil && err != io.EOF { + return err + } + + if len(bs) < 2 { + continue + } + + spl := bytes.Split(bs, delim) + if len(spl) < 2 { + continue + } + + err = json.Unmarshal(bytes.TrimSpace(spl[1]), &tskEvent) + if err != nil { + return fmt.Errorf("Error unmarshal task stream: %v", err) + } + + var lines int + var extra int + for _, e := range tskEvent.Event { + fmt.Printf("\033[0J") + eventFields := []interface{}{ + e.Namespace, + e.Data, + e.Timestamp, + } + if !verbose { + printFields(w, false, 0, eventFields...) + continue + } + tags := sortTags(e.Tags) + if len(tags) <= 3 { + eventFields = append(eventFields, strings.Join(tags, ", ")) + printFields(w, false, 0, eventFields...) + continue + } + for i := 0; i < len(tags); i += 3 { + tagSlice := tags[i:min(i+3, len(tags))] + if i == 0 { + eventFields = append(eventFields, strings.Join(tagSlice, ", ")+",") + printFields(w, false, 0, eventFields...) + continue + } + extra++ + if i+3 > len(tags) { + printFields(w, false, 0, + "", + "", + "", + strings.Join(tagSlice, ", "), + ) + continue + } + printFields(w, false, 0, + "", + "", + "", + strings.Join(tagSlice, ", ")+",", + ) + } + } + lines = len(tskEvent.Event) + extra + fmt.Fprintf(w, "\033[%dA\n", lines+1) + + if err == io.EOF { + break + } + } + w.Flush() + return nil +}