diff --git a/README.md b/README.md index ea65c36962..c58c1ad78a 100644 --- a/README.md +++ b/README.md @@ -1134,18 +1134,19 @@ $ scw inspect myserver | jq '.[0].public_ip.address' * Support of `scw _userdata name VAR=@/path/to/file` ([#183](https://github.com/scaleway/scaleway-cli/issues/183)) * Support of `scw restart -w` ([#185](https://github.com/scaleway/scaleway-cli/issues/185)) * Restarting multiple servers in parallel ([#185](https://github.com/scaleway/scaleway-cli/issues/185)) +* Added _security-groups ([#179](https://github.com/scaleway/scaleway-cli/issues/179)) View full [commits list](https://github.com/scaleway/scaleway-cli/compare/v1.5.0...master) ### v1.5.0 (2015-09-11) -* Support of `scw tag --bootscript=""` option ([#149](https://github.com/scaleway/scaleway-cli/issues/149) -* `scw info` now prints user/organization info from the API ([#130](https://github.com/scaleway/scaleway-cli/issues/130) +* Support of `scw tag --bootscript=""` option ([#149](https://github.com/scaleway/scaleway-cli/issues/149)) +* `scw info` now prints user/organization info from the API ([#130](https://github.com/scaleway/scaleway-cli/issues/130)) * Added helpers to manipulate new `user_data` API ([#150](https://github.com/scaleway/scaleway-cli/issues/150)) * Renamed `create-image-from-s3.sh` example and now auto-filling image metadata (title and bootscript) based on the Makefile configuration * Support of `scw rm -f/--force` option ([#158](https://github.com/scaleway/scaleway-cli/issues/158)) * Added `scw _userdata local ...` option which interacts with the Metadata API without authentication ([#166](https://github.com/scaleway/scaleway-cli/issues/166)) -* Initial version of `scw _billing` (price estimation tool) ([#118](https://github.com/scaleway/scaleway-cli/issues/118) +* Initial version of `scw _billing` (price estimation tool) ([#118](https://github.com/scaleway/scaleway-cli/issues/118)) * Fix: debian-package installation * Fix: nil pointer dereference ([#155](https://github.com/scaleway/scaleway-cli/pull/155)) ([@ebfe](https://github.com/ebfe)) * Fix: regression on scw create ([#142](https://github.com/scaleway/scaleway-cli/issues/142)) diff --git a/pkg/api/api.go b/pkg/api/api.go index 9f8aba3901..77ce07be87 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -30,9 +30,9 @@ import ( // Default values var ( - ComputeAPI string = "https://api.scaleway.com/" - AccountAPI string = "https://account.scaleway.com/" - MetadataAPI string = "http://169.254.42.42/" + ComputeAPI = "https://api.scaleway.com/" + AccountAPI = "https://account.scaleway.com/" + MetadataAPI = "http://169.254.42.42/" ) // ScalewayAPI is the interface used to communicate with the Scaleway API @@ -381,6 +381,59 @@ type ScalewayTasks struct { Tasks []ScalewayTask `json:"tasks,omitempty"` } +// ScalewaySecurityGroupRule definition +type ScalewaySecurityGroupRule struct { + Direction string `json:"direction"` + Protocol string `json:"protocol"` + IPRange string `json:"ip_range"` + DestPortFrom int `json:"dest_port_from,omitempty"` + Action string `json:"action"` + Postion int `json:"position"` + DestPortTo string `json:"dest_port_to"` + Editable bool `json:"editable"` + ID string `json:"id"` +} + +// ScalewayGetSecurityGroupRules represents the response of a GET /security_group/{groupID}/rules +type ScalewayGetSecurityGroupRules struct { + Rules []ScalewaySecurityGroupRule `json:"rules"` +} + +// ScalewayGetSecurityGroupRule represents the response of a GET /security_group/{groupID}/rules/{ruleID} +type ScalewayGetSecurityGroupRule struct { + Rules ScalewaySecurityGroupRule `json:"rule"` +} + +// ScalewayNewSecurityGroupRule definition POST/PUT request /security_group/{groupID} +type ScalewayNewSecurityGroupRule struct { + Action string `json:"action"` + Direction string `json:"direction"` + IPRange string `json:"ip_range"` + Protocol string `json:"protocol"` + DestPortFrom int `json:"dest_port_from,omitempty"` +} + +// ScalewaySecurityGroups definition +type ScalewaySecurityGroups struct { + Description string `json:"description"` + EnableDefaultSecurity bool `json:"enable_default_security"` + ID string `json:"id"` + Organization string `json:"organization"` + Name string `json:"name"` + OrganizationDefault bool `json:"organization_default"` + Servers []ScalewaySecurityGroup `json:"servers"` +} + +// ScalewayGetSecurityGroups represents the response of a GET /security_groups/ +type ScalewayGetSecurityGroups struct { + SecurityGroups []ScalewaySecurityGroups `json:"security_groups"` +} + +// ScalewayGetSecurityGroup represents the response of a GET /security_groups/{groupID} +type ScalewayGetSecurityGroup struct { + SecurityGroups ScalewaySecurityGroups `json:"security_group"` +} + // ScalewaySecurityGroup represents a Scaleway security group type ScalewaySecurityGroup struct { // Identifier is a unique identifier for the security group @@ -390,6 +443,13 @@ type ScalewaySecurityGroup struct { Name string `json:"name,omitempty"` } +// ScalewayNewSecurityGroup definition POST/PUT request /security_groups +type ScalewayNewSecurityGroup struct { + Organization string `json:"organization"` + Name string `json:"name"` + Description string `json:"description"` +} + // ScalewayServer represents a Scaleway C1 server type ScalewayServer struct { // Identifier is a unique identifier for the server @@ -575,6 +635,7 @@ type ScalewayUserDefinition struct { SSHPublicKeys []ScalewayKeyDefinition `json:"ssh_public_keys"` } +// ScalewayUsersDefinition represents the response of a GET /user type ScalewayUsersDefinition struct { User ScalewayUserDefinition `json:"user"` } @@ -1359,6 +1420,7 @@ func (s *ScalewayAPI) GetBootscript(bootscriptID string) (*ScalewayBootscript, e return &oneBootscript.Bootscript, nil } +// ScalewayUserdatas represents the response of a GET /user_data type ScalewayUserdatas struct { UserData []string `json:"user_data"` } @@ -1678,6 +1740,74 @@ func (s *ScalewayAPI) GetImageID(needle string, exitIfMissing bool) string { return "" } +// GetSecurityGroups returns a ScalewaySecurityGroups +func (s *ScalewayAPI) GetSecurityGroups() (*ScalewayGetSecurityGroups, error) { + resp, err := s.GetResponse("security_groups") + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var securityGroups ScalewayGetSecurityGroups + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(&securityGroups) + if err != nil { + return nil, err + } + return &securityGroups, nil +} + +// GetSecurityGroupRules returns a ScalewaySecurityGroupRules +func (s *ScalewayAPI) GetSecurityGroupRules(groupID string) (*ScalewayGetSecurityGroupRules, error) { + resp, err := s.GetResponse(fmt.Sprintf("security_groups/%s/rules", groupID)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var securityGroupRules ScalewayGetSecurityGroupRules + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(&securityGroupRules) + if err != nil { + return nil, err + } + return &securityGroupRules, nil +} + +// GetASecurityGroupRule returns a ScalewaySecurityGroupRule +func (s *ScalewayAPI) GetASecurityGroupRule(groupID string, rulesID string) (*ScalewayGetSecurityGroupRule, error) { + resp, err := s.GetResponse(fmt.Sprintf("security_groups/%s/rules/%s", groupID, rulesID)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var securityGroupRules ScalewayGetSecurityGroupRule + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(&securityGroupRules) + if err != nil { + return nil, err + } + return &securityGroupRules, nil +} + +// GetASecurityGroup returns a ScalewaySecurityGroup +func (s *ScalewayAPI) GetASecurityGroup(groupsID string) (*ScalewayGetSecurityGroup, error) { + resp, err := s.GetResponse(fmt.Sprintf("security_groups/%s", groupsID)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var securityGroups ScalewayGetSecurityGroup + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(&securityGroups) + if err != nil { + return nil, err + } + return &securityGroups, nil +} + // GetBootscriptID returns exactly one bootscript matching or dies func (s *ScalewayAPI) GetBootscriptID(needle string) string { // Parses optional type prefix, i.e: "bootscript:name" -> "name" diff --git a/pkg/cli/commands.go b/pkg/cli/commands.go index e291084f46..66c8af1573 100644 --- a/pkg/cli/commands.go +++ b/pkg/cli/commands.go @@ -45,4 +45,5 @@ var Commands = []*Command{ cmdCompletion, cmdFlushCache, cmdPatch, + cmdSecurityGroups, } diff --git a/pkg/cli/x_patch.go b/pkg/cli/x_patch.go index 6b535226da..927d2382de 100644 --- a/pkg/cli/x_patch.go +++ b/pkg/cli/x_patch.go @@ -43,7 +43,7 @@ func runPatch(cmd *Command, args []string) error { // Parsing FIELD=VALUE updateParts := strings.SplitN(args[1], "=", 2) if len(updateParts) != 2 { - cmd.PrintShortUsage() + return cmd.PrintShortUsage() } fieldName := updateParts[0] newValue := updateParts[1] diff --git a/pkg/cli/x_security-groups.go b/pkg/cli/x_security-groups.go new file mode 100644 index 0000000000..0a715572c3 --- /dev/null +++ b/pkg/cli/x_security-groups.go @@ -0,0 +1,296 @@ +// Copyright (C) 2015 Scaleway. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE.md file. + +package cli + +import ( + "encoding/json" + "fmt" + "io" + "strconv" + "strings" + + "github.com/scaleway/scaleway-cli/pkg/api" +) + +var cmdSecurityGroups = &Command{ + Exec: runSecurityGroups, + UsageLine: "_security-groups [OPTIONS] [ARGS]", + Description: "Interacts with security-groups", + Hidden: true, + Help: "Interacts with security-groups", + Examples: ` + $ scw _security-groups + $ scw _security-groups "SecurityGroupID" + $ scw _security-groups --new "NAME:DESCRIPTION" + $ scw _security-groups --update "NAME:DESCRIPTION" "SecurityGroupID" + $ scw _security-groups --delete "SecurityGroupID" + $ scw _security-groups --rules "SecurityGroupID" + $ scw _security-groups --rule-id "SecurityGroupID:RuleID" + $ scw _security-groups --rule-delete "SecurityGroupID:RuleID" + $ scw _security-groups --rule-new "SecurityGroupID:ACTION:DIRECTION:IP_RANGE:PROTOCOL[:DEST_PORT_FROM]" + $ scw _security-groups --rule-update "SecurityGroupID:RuleID:ACTION:DIRECTION:IP_RANGE:PROTOCOL[:DEST_PORT_FROM]" +`, +} + +func init() { + cmdSecurityGroups.Flag.BoolVar(&securityGroupsHelp, []string{"h", "-help"}, false, "Print usage") + // cmdSecurityGroups.Flag.BoolVar(&securityGroupsRaw, []string{"r", "-raw"}, false, "Displays the output in raw mode") + cmdSecurityGroups.Flag.StringVar(&securityGroupsNew, []string{"n", "-new"}, "", "Adds a new security group") + cmdSecurityGroups.Flag.StringVar(&securityGroupsUpdate, []string{"u", "-update"}, "", "Updates a security group") + cmdSecurityGroups.Flag.StringVar(&securityGroupsDelete, []string{"d", "-delete"}, "", "Deteles a security group") + cmdSecurityGroups.Flag.StringVar(&securityGroupsRules, []string{"r", "-rules"}, "", "Displays the rules") + cmdSecurityGroups.Flag.StringVar(&securityGroupsRuleID, []string{"ri", "-rule-id"}, "", "Displays one rule") + cmdSecurityGroups.Flag.StringVar(&securityGroupsRuleDelete, []string{"rd", "-rule-delete"}, "", "Deletes one rule") + cmdSecurityGroups.Flag.StringVar(&securityGroupsRuleNew, []string{"rn", "-rule-new"}, "", "Adds a new rule") + cmdSecurityGroups.Flag.StringVar(&securityGroupsRuleUpdate, []string{"ru", "-rule-update"}, "", "Updates one rule") +} + +// Flags +// var securityGroupsRaw bool // -r, --raw flag +var securityGroupsHelp bool // -h, --help flag +var securityGroupsNew string // -n, --new flag +var securityGroupsUpdate string // -u, --update flag +var securityGroupsDelete string // -d, --delete flag +var securityGroupsRules string // -r, --rules flag +var securityGroupsRuleID string // -ri, --rule-id flag +var securityGroupsRuleDelete string // -rd, --rule-delete flag +var securityGroupsRuleNew string // -rn, --rule-new flag +var securityGroupsRuleUpdate string // -ru, --rule-update flag + +func printRawMode(out io.Writer, data interface{}) error { + js, err := json.Marshal(data) + if err != nil { + return fmt.Errorf("Unable to parse the data: %v", err) + } + fmt.Fprintf(out, string(js)) + return nil +} + +func runSecurityGroups(cmd *Command, args []string) error { + if securityGroupsHelp { + return cmd.PrintUsage() + } + + if securityGroupsNew != "" { + var newGroups api.ScalewayNewSecurityGroup + + newParts := strings.SplitN(securityGroupsNew, ":", 2) + if len(newParts) != 2 { + return cmd.PrintShortUsage() + } + newGroups.Organization = cmd.API.Organization + newGroups.Name = newParts[0] + newGroups.Description = newParts[1] + resp, err := cmd.API.PostResponse("security_groups", newGroups) + if err != nil { + return err + } + defer resp.Body.Close() + + // Succeed POST code + if resp.StatusCode == 201 { + return nil + } + + var error api.ScalewayAPIError + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(&error) + if err != nil { + return err + } + error.StatusCode = resp.StatusCode + error.Debug() + return error + } else if securityGroupsUpdate != "" { + var newGroups api.ScalewayNewSecurityGroup + + if len(args) != 1 { + return cmd.PrintShortUsage() + } + newParts := strings.SplitN(securityGroupsUpdate, ":", 2) + if len(newParts) != 2 { + return cmd.PrintShortUsage() + } + newGroups.Organization = cmd.API.Organization + newGroups.Name = newParts[0] + newGroups.Description = newParts[1] + resp, err := cmd.API.PutResponse(fmt.Sprintf("security_groups/%s", args[0]), newGroups) + if err != nil { + return err + } + defer resp.Body.Close() + + // Succeed PUT code + if resp.StatusCode == 200 { + return nil + } + + var error api.ScalewayAPIError + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(&error) + if err != nil { + return err + } + error.StatusCode = resp.StatusCode + error.Debug() + return error + } else if securityGroupsDelete != "" { + resp, err := cmd.API.DeleteResponse(fmt.Sprintf("security_groups/%s", securityGroupsDelete)) + if err != nil { + return err + } + defer resp.Body.Close() + + // Succeed PUT code + if resp.StatusCode == 204 { + return nil + } + + var error api.ScalewayAPIError + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(&error) + if err != nil { + return err + } + error.StatusCode = resp.StatusCode + error.Debug() + return error + } else if securityGroupsRules != "" { + securityGroupRules, err := cmd.API.GetSecurityGroupRules(securityGroupsRules) + if err != nil { + return err + } + printRawMode(cmd.Streams().Stdout, *securityGroupRules) + return nil + } else if securityGroupsRuleID != "" { + newParts := strings.SplitN(securityGroupsRuleID, ":", 2) + if len(newParts) != 2 { + return cmd.PrintShortUsage() + } + GroupRuleID, err := cmd.API.GetASecurityGroupRule(newParts[0], newParts[1]) + if err != nil { + return err + } + printRawMode(cmd.Streams().Stdout, *GroupRuleID) + return nil + } else if securityGroupsRuleDelete != "" { + newParts := strings.SplitN(securityGroupsRuleDelete, ":", 2) + if len(newParts) != 2 { + return cmd.PrintShortUsage() + } + resp, err := cmd.API.DeleteResponse(fmt.Sprintf("security_groups/%s/rules/%s", newParts[0], newParts[1])) + if err != nil { + return err + } + defer resp.Body.Close() + + // Succeed PUT code + if resp.StatusCode == 204 { + return nil + } + + var error api.ScalewayAPIError + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(&error) + if err != nil { + return err + } + error.StatusCode = resp.StatusCode + error.Debug() + return error + } else if securityGroupsRuleNew != "" { + var newRule api.ScalewayNewSecurityGroupRule + + newParts := strings.Split(securityGroupsRuleNew, ":") + if len(newParts) != 5 && len(newParts) != 6 { + return cmd.PrintShortUsage() + } + newRule.Action = newParts[1] + newRule.Direction = newParts[2] + newRule.IPRange = newParts[3] + newRule.Protocol = newParts[4] + if len(newParts) == 6 { + var err error + + newRule.DestPortFrom, err = strconv.Atoi(newParts[5]) + if err != nil { + return err + } + } + resp, err := cmd.API.PostResponse(fmt.Sprintf("security_groups/%s/rules", newParts[0]), newRule) + if err != nil { + return err + } + defer resp.Body.Close() + + // Succeed POST code + if resp.StatusCode == 201 { + return nil + } + + var error api.ScalewayAPIError + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(&error) + if err != nil { + return err + } + error.StatusCode = resp.StatusCode + error.Debug() + return error + } else if securityGroupsRuleUpdate != "" { + var newRule api.ScalewayNewSecurityGroupRule + + newParts := strings.Split(securityGroupsRuleUpdate, ":") + if len(newParts) != 6 && len(newParts) != 7 { + return cmd.PrintShortUsage() + } + newRule.Action = newParts[2] + newRule.Direction = newParts[3] + newRule.IPRange = newParts[4] + newRule.Protocol = newParts[5] + if len(newParts) == 7 { + var err error + + newRule.DestPortFrom, err = strconv.Atoi(newParts[6]) + if err != nil { + return err + } + } + resp, err := cmd.API.PutResponse(fmt.Sprintf("security_groups/%s/rules/%s", newParts[0], newParts[1]), newRule) + if err != nil { + return err + } + defer resp.Body.Close() + + // Succeed PUT code + if resp.StatusCode == 200 { + return nil + } + + var error api.ScalewayAPIError + decoder := json.NewDecoder(resp.Body) + err = decoder.Decode(&error) + if err != nil { + return err + } + error.StatusCode = resp.StatusCode + error.Debug() + return error + } + if len(args) == 1 { + securityGroups, err := cmd.API.GetASecurityGroup(args[0]) + if err != nil { + return err + } + printRawMode(cmd.Streams().Stdout, *securityGroups) + return nil + } + securityGroups, err := cmd.API.GetSecurityGroups() + if err != nil { + return err + } + printRawMode(cmd.Streams().Stdout, *securityGroups) + return nil +}