diff --git a/Makefile b/Makefile index f167dce..163ff38 100644 --- a/Makefile +++ b/Makefile @@ -4,8 +4,7 @@ install: security scripts/build/install security: - #go get github.com/securego/gosec/cmd/gosec - @gosec ./... + @gosec --exclude=G204 ./... clean: rm -rf _exe/ diff --git a/README.md b/README.md index 789d039..7beb881 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,15 @@ Usage: lw [command] Available Commands: + asset All things assets auth authentication actions - cloud Interact with LiquidWeb's Cloud platform. + cloud Interact with LiquidWeb's Cloud platform + completion Generate completion script + dedicated All things dedicated server help Help about any command network network actions plan Process YAML plan file + ssh SSH to a Server version show build information Flags: @@ -72,6 +76,7 @@ Current commands supported in a `plan` file: - cloud server create - cloud server resize - cloud template restore +- ssh Example: @@ -93,6 +98,8 @@ cloud: bandwidth: "SS.5000" ``` +You can find more examples of plans in `examples/plans`. + ### Plan Variables Plan yaml can make use of golang's template variables. Allows variables to be passed on the diff --git a/cmd/asset.go b/cmd/asset.go new file mode 100644 index 0000000..bf4f744 --- /dev/null +++ b/cmd/asset.go @@ -0,0 +1,40 @@ +/* +Copyright © LiquidWeb + +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 cmd + +import ( + "os" + + "github.com/spf13/cobra" +) + +var assetCmd = &cobra.Command{ + Use: "asset", + Short: "All things assets", + Long: `An asset is an individual component on an account. + +For a full list of capabilities, please refer to the "Available Commands" section.`, + Run: func(cmd *cobra.Command, args []string) { + if err := cmd.Help(); err != nil { + lwCliInst.Die(err) + } + os.Exit(1) + }, +} + +func init() { + rootCmd.AddCommand(assetCmd) +} diff --git a/cmd/assetDetails.go b/cmd/assetDetails.go new file mode 100644 index 0000000..652dc6b --- /dev/null +++ b/cmd/assetDetails.go @@ -0,0 +1,81 @@ +/* +Copyright © LiquidWeb + +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 cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/liquidweb/liquidweb-cli/types/api" + "github.com/liquidweb/liquidweb-cli/validate" +) + +var assetDetailsCmdUniqIdFlag []string + +var assetDetailsCmd = &cobra.Command{ + Use: "details", + Short: "Get details of a specific asset", + Long: `Get details of a specific asset. + +An asset is an individual component on an account. Assets have categories. +`, + Run: func(cmd *cobra.Command, args []string) { + jsonFlag, _ := cmd.Flags().GetBool("json") + + for _, uniqId := range assetDetailsCmdUniqIdFlag { + validateFields := map[interface{}]interface{}{ + uniqId: "UniqId", + } + if err := validate.Validate(validateFields); err != nil { + fmt.Printf("%s ... skipping\n", err) + continue + } + + var details apiTypes.Subaccnt + apiArgs := map[string]interface{}{ + "uniq_id": uniqId, + "alsowith": []string{"categories"}, + } + + if err := lwCliInst.CallLwApiInto("bleed/asset/details", apiArgs, &details); err != nil { + lwCliInst.Die(err) + } + + if jsonFlag { + pretty, err := lwCliInst.JsonEncodeAndPrettyPrint(details) + if err != nil { + lwCliInst.Die(err) + } + fmt.Print(pretty) + } else { + fmt.Print(details) + } + } + }, +} + +func init() { + assetCmd.AddCommand(assetDetailsCmd) + + assetDetailsCmd.Flags().Bool("json", false, "output in json format") + assetDetailsCmd.Flags().StringSliceVar(&assetDetailsCmdUniqIdFlag, "uniq-id", []string{}, + "uniq-id of the asset. For multiple, must be ',' separated") + + if err := assetDetailsCmd.MarkFlagRequired("uniq-id"); err != nil { + lwCliInst.Die(err) + } +} diff --git a/cmd/assetList.go b/cmd/assetList.go new file mode 100644 index 0000000..4db9ec4 --- /dev/null +++ b/cmd/assetList.go @@ -0,0 +1,96 @@ +/* +Copyright © LiquidWeb + +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 cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/liquidweb/liquidweb-cli/instance" + "github.com/liquidweb/liquidweb-cli/types/api" +) + +var assetListCmdCategoriesFlag []string + +var assetListCmd = &cobra.Command{ + Use: "list", + Short: "List assets on your account", + Long: `List assets on your account. + +An asset is an individual component on an account. Assets have categories. + +Examples: + +* List all assets in the Provisioned and DNS categories: +- lw asset list --categories Provisioned,DNS + +* List all dedicated servers: +- lw asset list --categories StrictDedicated +`, + Run: func(cmd *cobra.Command, args []string) { + jsonFlag, _ := cmd.Flags().GetBool("json") + + apiArgs := map[string]interface{}{ + "alsowith": []string{"categories"}, + } + + if len(assetListCmdCategoriesFlag) > 0 { + apiArgs["category"] = assetListCmdCategoriesFlag + } + + methodArgs := instance.AllPaginatedResultsArgs{ + Method: "bleed/asset/list", + ResultsPerPage: 100, + MethodArgs: apiArgs, + } + results, err := lwCliInst.AllPaginatedResults(&methodArgs) + if err != nil { + lwCliInst.Die(err) + } + + if jsonFlag { + pretty, err := lwCliInst.JsonEncodeAndPrettyPrint(results) + if err != nil { + lwCliInst.Die(err) + } + fmt.Print(pretty) + } else { + cnt := 1 + for _, item := range results.Items { + + var details apiTypes.Subaccnt + if err := instance.CastFieldTypes(item, &details); err != nil { + lwCliInst.Die(err) + } + + fmt.Printf("%d.) ", cnt) + fmt.Print(details) + cnt++ + } + } + }, +} + +func init() { + assetCmd.AddCommand(assetListCmd) + + assetListCmd.Flags().Bool("json", false, "output in json format") + + assetListCmd.Flags().StringSliceVar(&assetListCmdCategoriesFlag, "categories", + []string{}, "categories to include separated by ','") + +} diff --git a/cmd/cloud.go b/cmd/cloud.go index a0901b8..18e8ae8 100644 --- a/cmd/cloud.go +++ b/cmd/cloud.go @@ -23,7 +23,7 @@ import ( var cloudCmd = &cobra.Command{ Use: "cloud", - Short: "Interact with LiquidWeb's Cloud platform.", + Short: "Interact with LiquidWeb's Cloud platform", Long: `Command line interface to LiquidWeb's Cloud platform. For a full list of capabilities, please refer to the "Available Commands" section.`, diff --git a/cmd/cloudServerCreate.go b/cmd/cloudServerCreate.go index 891127c..de32890 100644 --- a/cmd/cloudServerCreate.go +++ b/cmd/cloudServerCreate.go @@ -39,7 +39,7 @@ Requires various flags. Please see the flag section of help. Examples: # Create a Cloud Server on a Private Parent named "private" -'cloud server create --private-parent private --memory 1024 --diskspace 40 --vcpu 2 --zone 40460 --template DEBIAN_10_UNMANAGED' +'cloud server create --private-parent private --memory 1024 --diskspace 40 --vcpu 2 --template DEBIAN_10_UNMANAGED' # Create a Cloud Server on config-id 1 'cloud server create --config-id 1 --template DEBIAN_10_UNMANAGED --zone 40460' @@ -55,6 +55,36 @@ These examples use default values for various flags, such as password, type, ssh For a list of Templates, Configs, and Region/Zones, see 'cloud server options --configs --templates --zones' For a list of images, see 'cloud images list' For a list of backups, see 'cloud backups list' + +Plan Example: + +--- +cloud: + server: + create: + - type: "SS.VPS.WIN" + password: "1fk4ds$jktl43u90dsa" + template: "WINDOWS_2019_UNMANAGED" + zone: 40460 + hostname: "db1.dev.addictmud.org" + ips: 1 + public-ssh-key: "" + config-id: 88 + backup-days: 5 + bandwidth: "SS.5000" + backup-id: -1 + image-id: -1 + pool-ips: + - "10.111.12.13" + - "10.12.13.14" + private-parent: "my pp" + memory: 0 + diskspace: 0 + vcpu: 0 + winav: "" + ms-sql: "" + +lw plan --file /tmp/cloud.server.create.yaml `, Run: func(cmd *cobra.Command, args []string) { params := &instance.CloudServerCreateParams{} diff --git a/cmd/cloudServerDetails.go b/cmd/cloudServerDetails.go index ac76576..8bc7a30 100644 --- a/cmd/cloudServerDetails.go +++ b/cmd/cloudServerDetails.go @@ -17,7 +17,6 @@ package cmd import ( "fmt" - "os" "github.com/spf13/cobra" @@ -28,6 +27,7 @@ import ( var blockStorageVolumeList apiTypes.MergedPaginatedList var fetchedBlockStorageVolumes bool +var cloudServerDetailsCmdUniqIdFlag []string var cloudServerDetailsCmd = &cobra.Command{ Use: "details", @@ -39,32 +39,33 @@ You can check this methods API documentation for what the returned fields mean: https://cart.liquidweb.com/storm/api/docs/bleed/Storm/Server.html#method_details `, Run: func(cmd *cobra.Command, args []string) { - uniqIdFlag, _ := cmd.Flags().GetString("uniq-id") jsonFlag, _ := cmd.Flags().GetBool("json") - validateFields := map[interface{}]interface{}{ - uniqIdFlag: "UniqId", - } - if err := validate.Validate(validateFields); err != nil { - lwCliInst.Die(err) - } - - var details apiTypes.CloudServerDetails - if err := lwCliInst.CallLwApiInto("bleed/storm/server/details", - map[string]interface{}{"uniq_id": uniqIdFlag}, &details); err != nil { - lwCliInst.Die(err) - } + for _, uniqId := range cloudServerDetailsCmdUniqIdFlag { + validateFields := map[interface{}]interface{}{ + uniqId: "UniqId", + } + if err := validate.Validate(validateFields); err != nil { + fmt.Printf("%s ... skipping\n", err) + continue + } - if jsonFlag { - pretty, err := lwCliInst.JsonEncodeAndPrettyPrint(details) - if err != nil { + var details apiTypes.CloudServerDetails + if err := lwCliInst.CallLwApiInto("bleed/storm/server/details", + map[string]interface{}{"uniq_id": uniqId}, &details); err != nil { lwCliInst.Die(err) } - fmt.Printf(pretty) - os.Exit(0) - } - _printExtendedCloudServerDetails(&details) + if jsonFlag { + pretty, err := lwCliInst.JsonEncodeAndPrettyPrint(details) + if err != nil { + lwCliInst.Die(err) + } + fmt.Print(pretty) + } else { + _printExtendedCloudServerDetails(&details) + } + } }, } @@ -122,7 +123,8 @@ func init() { cloudServerCmd.AddCommand(cloudServerDetailsCmd) cloudServerDetailsCmd.Flags().Bool("json", false, "output in json format") - cloudServerDetailsCmd.Flags().String("uniq-id", "", "get details of this uniq-id") + cloudServerDetailsCmd.Flags().StringSliceVar(&cloudServerDetailsCmdUniqIdFlag, "uniq-id", []string{}, + "uniq-id of the cloud server. For multiple, must be ',' separated") if err := cloudServerDetailsCmd.MarkFlagRequired("uniq-id"); err != nil { lwCliInst.Die(err) diff --git a/cmd/cloudServerResize.go b/cmd/cloudServerResize.go index 8895264..c0dc709 100644 --- a/cmd/cloudServerResize.go +++ b/cmd/cloudServerResize.go @@ -56,15 +56,23 @@ are required: Downtime Expectations: -When resizing a Cloud Server on a private parent, you can add memory or vcpu(s) -without downtime. If you change the diskspace however, then a reboot will be -required. - -When resizing a Cloud Server that isn't on a private parent, there will be one -reboot during the resize. The only case there will be two reboots is when -going to a config with more diskspace, and --skip-fs-resize wasn't passed. - -During all resizes, the Cloud Server is online as the disk synchronizes. +Please see 'lw help cloud server resize-expection' to determine if a specific +resize would require a reboot or not. + +Plan Example: + +--- +cloud: + server: + resize: + - config-id: 0 + private-parent: "nvme-pp" + uniq-id: "{{- .Var.uniq_id -}}" + diskspace: 1800 + vcpu: 16 + memory: 124000 + +lw plan --file /tmp/cloud.server.resize.yaml --var uniq_id=ABC123 `, Run: func(cmd *cobra.Command, args []string) { params := &instance.CloudServerResizeParams{} diff --git a/cmd/cloudTemplateRestore.go b/cmd/cloudTemplateRestore.go index 7bc3b00..d3e2b1c 100644 --- a/cmd/cloudTemplateRestore.go +++ b/cmd/cloudTemplateRestore.go @@ -26,7 +26,19 @@ import ( var cloudTemplateRestoreCmd = &cobra.Command{ Use: "restore", Short: "Restore a Cloud Template on a Cloud Server", - Long: `Restore a Cloud Template on a Cloud Server.`, + Long: `Restore a Cloud Template on a Cloud Server. + +Plan Example: + +--- +cloud: + template: + restore: + - uniq-id: ABC123 + template: DEBIAN_10_UNMANAGED + +lw plan --file cloud.template.restore.yaml +`, Run: func(cmd *cobra.Command, args []string) { params := &instance.CloudTemplateRestoreParams{} diff --git a/cmd/dedicated.go b/cmd/dedicated.go new file mode 100644 index 0000000..32a5ffe --- /dev/null +++ b/cmd/dedicated.go @@ -0,0 +1,40 @@ +/* +Copyright © LiquidWeb + +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 cmd + +import ( + "os" + + "github.com/spf13/cobra" +) + +var dedicatedCmd = &cobra.Command{ + Use: "dedicated", + Short: "All things dedicated server", + Long: `Command line interface for all things specific to running dedicated servers. + +For a full list of capabilities, please refer to the "Available Commands" section.`, + Run: func(cmd *cobra.Command, args []string) { + if err := cmd.Help(); err != nil { + lwCliInst.Die(err) + } + os.Exit(1) + }, +} + +func init() { + rootCmd.AddCommand(dedicatedCmd) +} diff --git a/cmd/dedicatedServer.go b/cmd/dedicatedServer.go new file mode 100644 index 0000000..7e63164 --- /dev/null +++ b/cmd/dedicatedServer.go @@ -0,0 +1,40 @@ +/* +Copyright © LiquidWeb + +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 cmd + +import ( + "os" + + "github.com/spf13/cobra" +) + +var dedicatedServerCmd = &cobra.Command{ + Use: "server", + Short: "Traditional dedicated servers.", + Long: `Command line interface for traditional dedicated servers. + +For a full list of capabilities, please refer to the "Available Commands" section.`, + Run: func(cmd *cobra.Command, args []string) { + if err := cmd.Help(); err != nil { + lwCliInst.Die(err) + } + os.Exit(1) + }, +} + +func init() { + dedicatedCmd.AddCommand(dedicatedServerCmd) +} diff --git a/cmd/dedicatedServerDetails.go b/cmd/dedicatedServerDetails.go new file mode 100644 index 0000000..ce439f7 --- /dev/null +++ b/cmd/dedicatedServerDetails.go @@ -0,0 +1,90 @@ +/* +Copyright © LiquidWeb + +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 cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/liquidweb/liquidweb-cli/types/api" + "github.com/liquidweb/liquidweb-cli/validate" +) + +var dedicatedServerDetailsCmdUniqIdFlag []string + +var dedicatedServerDetailsCmd = &cobra.Command{ + Use: "details", + Short: "Get details of a dedicated server", + Long: `Get details of a dedicated server`, + Run: func(cmd *cobra.Command, args []string) { + jsonFlag, _ := cmd.Flags().GetBool("json") + + for _, uniqId := range dedicatedServerDetailsCmdUniqIdFlag { + validateFields := map[interface{}]interface{}{ + uniqId: "UniqId", + } + if err := validate.Validate(validateFields); err != nil { + fmt.Printf("%s ... skipping\n", err) + continue + } + + var details apiTypes.Subaccnt + apiArgs := map[string]interface{}{ + "uniq_id": uniqId, + "alsowith": []string{"categories"}, + } + + if err := lwCliInst.CallLwApiInto("bleed/asset/details", apiArgs, &details); err != nil { + lwCliInst.Die(err) + } + + var found bool + for _, category := range details.Categories { + if category == "StrictDedicated" { + found = true + break + } + } + + if !found { + lwCliInst.Die(fmt.Errorf("UniqId [%s] is not a dedicated server", uniqId)) + } + + if jsonFlag { + pretty, err := lwCliInst.JsonEncodeAndPrettyPrint(details) + if err != nil { + panic(err) + } + fmt.Print(pretty) + } else { + fmt.Print(details) + } + } + }, +} + +func init() { + dedicatedServerCmd.AddCommand(dedicatedServerDetailsCmd) + + dedicatedServerDetailsCmd.Flags().Bool("json", false, "output in json format") + dedicatedServerDetailsCmd.Flags().StringSliceVar(&dedicatedServerDetailsCmdUniqIdFlag, "uniq-id", []string{}, + "uniq-id of the dedicated server. For multiple, must be ',' separated") + + if err := dedicatedServerDetailsCmd.MarkFlagRequired("uniq-id"); err != nil { + lwCliInst.Die(err) + } +} diff --git a/cmd/dedicatedServerList.go b/cmd/dedicatedServerList.go new file mode 100644 index 0000000..53ac91b --- /dev/null +++ b/cmd/dedicatedServerList.go @@ -0,0 +1,73 @@ +/* +Copyright © LiquidWeb + +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 cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/liquidweb/liquidweb-cli/instance" + "github.com/liquidweb/liquidweb-cli/types/api" +) + +var dedicatedServerListCmd = &cobra.Command{ + Use: "list", + Short: "List Dedicated Servers on your account", + Long: `List Dedicated Servers on your account`, + Run: func(cmd *cobra.Command, args []string) { + jsonFlag, _ := cmd.Flags().GetBool("json") + + methodArgs := instance.AllPaginatedResultsArgs{ + Method: "bleed/asset/list", + ResultsPerPage: 100, + MethodArgs: map[string]interface{}{ + "category": []string{"StrictDedicated"}, + }, + } + results, err := lwCliInst.AllPaginatedResults(&methodArgs) + if err != nil { + lwCliInst.Die(err) + } + + if jsonFlag { + pretty, err := lwCliInst.JsonEncodeAndPrettyPrint(results) + if err != nil { + lwCliInst.Die(err) + } + fmt.Printf(pretty) + } else { + serverCnt := 1 + for _, item := range results.Items { + + var details apiTypes.Subaccnt + if err := instance.CastFieldTypes(item, &details); err != nil { + lwCliInst.Die(err) + } + + fmt.Printf("%d.) ", serverCnt) + fmt.Print(details) + serverCnt++ + } + } + }, +} + +func init() { + dedicatedServerCmd.AddCommand(dedicatedServerListCmd) + + dedicatedServerListCmd.Flags().Bool("json", false, "output in json format") +} diff --git a/cmd/ssh.go b/cmd/ssh.go new file mode 100644 index 0000000..1957e35 --- /dev/null +++ b/cmd/ssh.go @@ -0,0 +1,88 @@ +/* +Copyright © LiquidWeb + +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 cmd + +import ( + "github.com/spf13/cobra" + + "github.com/liquidweb/liquidweb-cli/instance" +) + +var sshCmd = &cobra.Command{ + Use: "ssh", + Short: "SSH to a Server", + Long: `SSH to a Server. + +Starts an interactive SSH session to your server. If --command is passed, a non interactive +session is started running only your passed command. + +Examples: + +* SSH by hostname accepting all defaults: +- lw ssh --host dexg.ulxy5e656r.io + +* SSH by uniq-id accepting all defaults: +- lw ssh --host ABC123 + +* SSH by uniq-id making use of all flags: +- lw ssh --host ABC123 --agent-forwarding --port 2222 --private-key-file /home/myself/.ssh-alt/id_rsa \ + --user amanda --command "ps faux && free -m" + +Plan Examples: + +--- +ssh: + - host: nd00.ltv1wv76kc.io + command: "free -m" + user: "root" + private-key-file: "/home/myself/.ssh/id_rsa" + agent-forwarding: true + port: 22 + - host: PPB4NZ + command: "hostname && free -m" + +lw plan --file /tmp/ssh.yaml +`, + Run: func(cmd *cobra.Command, args []string) { + params := &instance.SshParams{} + + params.Host, _ = cmd.Flags().GetString("host") + params.PrivateKeyFile, _ = cmd.Flags().GetString("private-key-file") + params.User, _ = cmd.Flags().GetString("user") + params.AgentForwarding, _ = cmd.Flags().GetBool("agent-forwarding") + params.Port, _ = cmd.Flags().GetInt("port") + params.Command, _ = cmd.Flags().GetString("command") + + if err := lwCliInst.Ssh(params); err != nil { + lwCliInst.Die(err) + } + }, +} + +func init() { + rootCmd.AddCommand(sshCmd) + + sshCmd.Flags().String("host", "", "uniq-id or hostname for the Server") + sshCmd.Flags().Int("port", 22, "ssh port to use") + sshCmd.Flags().String("private-key-file", "", "path to a specific/non default ssh private key to use") + sshCmd.Flags().Bool("agent-forwarding", false, "whether or not to enable ssh agent forwarding") + sshCmd.Flags().String("user", "root", "username to use for the ssh connection") + sshCmd.Flags().String("command", "", "run this command and exit rather than start an interactive shell") + + if err := sshCmd.MarkFlagRequired("host"); err != nil { + lwCliInst.Die(err) + } +} diff --git a/plan.yaml.example b/examples/plans/cloud.server.create.yaml similarity index 100% rename from plan.yaml.example rename to examples/plans/cloud.server.create.yaml diff --git a/examples/plans/cloud.server.resize.yaml b/examples/plans/cloud.server.resize.yaml new file mode 100644 index 0000000..e5787ce --- /dev/null +++ b/examples/plans/cloud.server.resize.yaml @@ -0,0 +1,10 @@ +--- +cloud: + server: + resize: + - config-id: 0 + private-parent: "nvme-pp" + uniq-id: "{{- .Var.uniq_id -}}" + diskspace: 1800 + vcpu: 16 + memory: 124000 diff --git a/examples/plans/cloud.template.restore.yaml b/examples/plans/cloud.template.restore.yaml new file mode 100644 index 0000000..cb250df --- /dev/null +++ b/examples/plans/cloud.template.restore.yaml @@ -0,0 +1,6 @@ +--- +cloud: + template: + restore: + - uniq-id: ABC123 + template: DEBIAN_10_UNMANAGED diff --git a/examples/plans/ssh.yaml b/examples/plans/ssh.yaml new file mode 100644 index 0000000..66e2d51 --- /dev/null +++ b/examples/plans/ssh.yaml @@ -0,0 +1,10 @@ +--- +ssh: + - host: nd00.ltv1wv76kc.io + command: "free -m" + user: "root" + private-key-file: "/home/myself/.ssh/id_rsa" + agent-forwarding: true + port: 22 + - host: PPB4NZ + command: "hostname && free -m" diff --git a/instance/plan.go b/instance/plan.go index 225962c..08cc46a 100644 --- a/instance/plan.go +++ b/instance/plan.go @@ -21,6 +21,7 @@ import ( type Plan struct { Cloud *PlanCloud + Ssh []SshParams } type PlanCloud struct { @@ -45,6 +46,12 @@ func (ci *Client) ProcessPlan(plan *Plan) error { } } + for _, x := range plan.Ssh { + if err := ci.processPlanSsh(&x); err != nil { + return err + } + } + return nil } @@ -65,6 +72,12 @@ func (ci *Client) processPlanCloud(cloud *PlanCloud) error { return nil } +func (ci *Client) processPlanSsh(params *SshParams) (err error) { + err = ci.Ssh(params) + + return +} + func (ci *Client) processPlanCloudServer(server *PlanCloudServer) error { if server.Create != nil { diff --git a/instance/ssh.go b/instance/ssh.go new file mode 100644 index 0000000..9b86af8 --- /dev/null +++ b/instance/ssh.go @@ -0,0 +1,135 @@ +/* +Copyright © LiquidWeb + +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 instance + +import ( + "fmt" + "os" + "os/exec" + + "github.com/liquidweb/liquidweb-cli/types/api" + "github.com/liquidweb/liquidweb-cli/validate" +) + +type SshParams struct { + Host string `yaml:"host"` + Port int `yaml:"port"` + PrivateKeyFile string `yaml:"private-key-file"` + User string `yaml:"user"` + AgentForwarding bool `yaml:"agent-forwarding"` + Command string `yaml:"command"` +} + +func (self *SshParams) UnmarshalYAML(unmarshal func(interface{}) error) error { + type rawType SshParams + raw := rawType{ + Port: 22, + User: "root", + } + if err := unmarshal(&raw); err != nil { + return err + } + *self = SshParams(raw) + + return nil +} + +func (self *SshParams) TranslateHost(ci *Client) (ip string, err error) { + validateFields := map[interface{}]interface{}{ + self.Host: "UniqId", + } + + if err = validate.Validate(validateFields); err == nil { + var subaccnt apiTypes.Subaccnt + apiArgs := map[string]interface{}{ + "uniq_id": self.Host, + } + if aErr := ci.CallLwApiInto("bleed/asset/details", apiArgs, &subaccnt); aErr != nil { + err = aErr + return + } + + ip = subaccnt.Ip + } else { + methodArgs := AllPaginatedResultsArgs{ + Method: "bleed/asset/list", + ResultsPerPage: 100, + } + results, aErr := ci.AllPaginatedResults(&methodArgs) + if aErr != nil { + err = aErr + return + } + for _, item := range results.Items { + var subaccnt apiTypes.Subaccnt + if err = CastFieldTypes(item, &subaccnt); err != nil { + return + } + + if subaccnt.Domain == self.Host { + ip = subaccnt.Ip + break + } + } + } + + if ip == "" { + err = fmt.Errorf("unable to determine ip for Host [%s]", self.Host) + return + } + + return +} + +func (self *Client) Ssh(params *SshParams) (err error) { + validateFields := map[interface{}]interface{}{ + params.Port: "PositiveInt", + } + if err = validate.Validate(validateFields); err != nil { + return + } + + ip, err := params.TranslateHost(self) + if err != nil { + return + } + + sshArgs := []string{} + if params.PrivateKeyFile != "" { + sshArgs = append(sshArgs, "-i", params.PrivateKeyFile) + } + + if params.AgentForwarding { + sshArgs = append(sshArgs, "-A") + } + + sshArgs = append(sshArgs, fmt.Sprintf("%s@%s", params.User, ip)) + sshArgs = append(sshArgs, fmt.Sprintf("-p %d", params.Port)) + + if params.Command != "" { + sshArgs = append(sshArgs, params.Command) + } + + cmd := exec.Command("ssh", sshArgs...) + + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + cmd.Stdin = os.Stdin + + err = cmd.Run() + + return +} diff --git a/types/api/subaccnt.go b/types/api/subaccnt.go new file mode 100644 index 0000000..b1926b8 --- /dev/null +++ b/types/api/subaccnt.go @@ -0,0 +1,46 @@ +package apiTypes + +import ( + "fmt" + "strings" +) + +type Subaccnt struct { + Active bool `json:"active" mapstructure:"active"` + Domain string `json:"domain" mapstructure:"domain"` + Ip string `json:"ip" mapstructure:"ip"` + ProjectId int64 `json:"project_id" mapstructure:"project_id"` + ProjectName string `json:"project_name" mapstructure:"project_name"` + RegionId int `json:"region_id" mapstructure:"region_id"` + Status string `json:"status" mapstructure:"status"` + Type string `json:"type" mapstructure:"type"` + UniqId string `json:"uniq_id" mapstructure:"uniq_id"` + Username string `json:"username" mapstructure:"username"` + Categories []string `json:"categories" mapstructure:"categories"` +} + +func (x Subaccnt) String() string { + var slice []string + + slice = append(slice, fmt.Sprintf("Domain: %s UniqId: %s\n", x.Domain, x.UniqId)) + + if len(x.Categories) > 0 { + slice = append(slice, fmt.Sprintln("\tCategories:")) + for _, category := range x.Categories { + slice = append(slice, fmt.Sprintf("\t\t* %s\n", category)) + } + } + + if x.Ip != "" && x.Ip != "127.0.0.1" { + slice = append(slice, fmt.Sprintf("\tIp: %s\n", x.Ip)) + } + + if x.ProjectName != "" && x.ProjectId != 0 { + slice = append(slice, fmt.Sprintf("\tProjectName: %s (id %d)\n", x.ProjectName, x.ProjectId)) + } + slice = append(slice, fmt.Sprintf("\tRegionId: %d\n", x.RegionId)) + slice = append(slice, fmt.Sprintf("\tStatus: %s\n", x.Status)) + slice = append(slice, fmt.Sprintf("\tType: %s\n", x.Type)) + + return strings.Join(slice[:], "") +}