2016-12-22 16:14:15 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2017-01-24 15:55:37 +00:00
|
|
|
"bytes"
|
2016-12-22 16:14:15 +00:00
|
|
|
"context"
|
2017-07-21 21:06:10 +00:00
|
|
|
"crypto/tls"
|
2016-12-22 16:14:15 +00:00
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2017-01-30 12:12:25 +00:00
|
|
|
"io"
|
2016-12-22 16:14:15 +00:00
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
2017-11-30 19:58:35 +00:00
|
|
|
"net/url"
|
2016-12-22 16:14:15 +00:00
|
|
|
"os"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
cid "github.com/ipfs/go-cid"
|
|
|
|
logging "github.com/ipfs/go-log"
|
2017-01-30 12:12:25 +00:00
|
|
|
peer "github.com/libp2p/go-libp2p-peer"
|
|
|
|
ma "github.com/multiformats/go-multiaddr"
|
2017-01-24 14:06:17 +00:00
|
|
|
cli "github.com/urfave/cli"
|
2016-12-22 16:14:15 +00:00
|
|
|
)
|
|
|
|
|
2017-01-23 12:34:22 +00:00
|
|
|
const programName = `ipfs-cluster-ctl`
|
2016-12-22 16:14:15 +00:00
|
|
|
|
2017-01-25 17:10:18 +00:00
|
|
|
// Version is the cluster-ctl tool version. It should match
|
|
|
|
// the IPFS cluster's version
|
2017-11-15 22:39:50 +00:00
|
|
|
const Version = "0.3.0"
|
2017-01-24 14:06:17 +00:00
|
|
|
|
2016-12-22 16:14:15 +00:00
|
|
|
var (
|
2017-07-21 21:06:10 +00:00
|
|
|
defaultHost = fmt.Sprintf("127.0.0.1:%d", 9094)
|
|
|
|
defaultTimeout = 60
|
|
|
|
defaultProtocol = "http"
|
|
|
|
defaultTransport = http.DefaultTransport
|
2017-08-29 01:41:54 +00:00
|
|
|
defaultUsername = ""
|
|
|
|
defaultPassword = ""
|
2016-12-22 16:14:15 +00:00
|
|
|
)
|
|
|
|
|
2017-01-24 14:06:17 +00:00
|
|
|
var logger = logging.Logger("cluster-ctl")
|
2016-12-22 16:14:15 +00:00
|
|
|
|
|
|
|
// Description provides a short summary of the functionality of this tool
|
|
|
|
var Description = fmt.Sprintf(`
|
2017-01-24 14:06:17 +00:00
|
|
|
%s is a tool to manage IPFS Cluster nodes.
|
|
|
|
Use "%s help" to list all available commands and
|
|
|
|
"%s help <command>" to get usage information for a
|
|
|
|
specific one.
|
2016-12-22 16:14:15 +00:00
|
|
|
|
2017-01-23 13:21:26 +00:00
|
|
|
%s uses the IPFS Cluster API to perform requests and display
|
2016-12-22 16:14:15 +00:00
|
|
|
responses in a user-readable format. The location of the IPFS
|
|
|
|
Cluster server is assumed to be %s, but can be
|
2017-01-24 14:06:17 +00:00
|
|
|
configured with the --host option.
|
2016-12-22 16:14:15 +00:00
|
|
|
|
|
|
|
For feedback, bug reports or any additional information, visit
|
|
|
|
https://github.com/ipfs/ipfs-cluster.
|
|
|
|
`,
|
|
|
|
programName,
|
|
|
|
programName,
|
|
|
|
programName,
|
|
|
|
programName,
|
|
|
|
defaultHost)
|
|
|
|
|
2017-01-30 12:12:25 +00:00
|
|
|
type peerAddBody struct {
|
|
|
|
Addr string `json:"peer_multiaddress"`
|
|
|
|
}
|
|
|
|
|
2017-01-24 14:06:17 +00:00
|
|
|
func out(m string, a ...interface{}) {
|
|
|
|
fmt.Fprintf(os.Stderr, m, a...)
|
2016-12-22 16:14:15 +00:00
|
|
|
}
|
|
|
|
|
2017-01-24 14:06:17 +00:00
|
|
|
func checkErr(doing string, err error) {
|
|
|
|
if err != nil {
|
|
|
|
out("error %s: %s\n", doing, err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
}
|
2016-12-22 16:14:15 +00:00
|
|
|
|
2017-01-24 14:06:17 +00:00
|
|
|
func main() {
|
|
|
|
app := cli.NewApp()
|
|
|
|
app.Name = programName
|
|
|
|
app.Usage = "CLI for IPFS Cluster"
|
2017-07-03 16:36:00 +00:00
|
|
|
app.Description = Description
|
2017-01-24 14:06:17 +00:00
|
|
|
app.Version = Version
|
|
|
|
app.Flags = []cli.Flag{
|
|
|
|
cli.StringFlag{
|
|
|
|
Name: "host, l",
|
|
|
|
Value: defaultHost,
|
|
|
|
Usage: "host:port of the IPFS Cluster service API",
|
|
|
|
},
|
|
|
|
cli.BoolFlag{
|
|
|
|
Name: "https, s",
|
|
|
|
Usage: "use https to connect to the API",
|
|
|
|
},
|
2017-07-21 21:06:10 +00:00
|
|
|
cli.BoolFlag{
|
|
|
|
Name: "no-check-certificate",
|
2017-08-29 01:41:54 +00:00
|
|
|
Usage: "do not verify server TLS certificate. only valid with --https flag",
|
2017-07-21 21:06:10 +00:00
|
|
|
},
|
2017-02-08 18:52:34 +00:00
|
|
|
cli.StringFlag{
|
|
|
|
Name: "encoding, enc",
|
|
|
|
Value: "text",
|
|
|
|
Usage: "output format encoding [text, json]",
|
|
|
|
},
|
2017-01-24 14:06:17 +00:00
|
|
|
cli.IntFlag{
|
|
|
|
Name: "timeout, t",
|
|
|
|
Value: defaultTimeout,
|
|
|
|
Usage: "number of seconds to wait before timing out a request",
|
|
|
|
},
|
|
|
|
cli.BoolFlag{
|
|
|
|
Name: "debug, d",
|
|
|
|
Usage: "set debug log level",
|
|
|
|
},
|
2017-08-29 01:41:54 +00:00
|
|
|
cli.StringFlag{
|
|
|
|
Name: "basic-auth",
|
|
|
|
Usage: `<username>[:<password>] specify BasicAuth credentials for server that
|
|
|
|
requires authorization. implies --https, which you can disable with --force-http`,
|
|
|
|
EnvVar: "CLUSTER_CREDENTIALS",
|
|
|
|
},
|
|
|
|
cli.BoolFlag{
|
|
|
|
Name: "force-http, f",
|
|
|
|
Usage: "force HTTP. only valid when using BasicAuth",
|
|
|
|
},
|
2017-01-24 14:06:17 +00:00
|
|
|
}
|
2016-12-22 16:14:15 +00:00
|
|
|
|
2017-01-24 14:06:17 +00:00
|
|
|
app.Before = func(c *cli.Context) error {
|
|
|
|
defaultHost = c.String("host")
|
|
|
|
defaultTimeout = c.Int("timeout")
|
2017-08-29 01:41:54 +00:00
|
|
|
// check for BasicAuth credentials
|
|
|
|
if c.IsSet("basic-auth") {
|
|
|
|
defaultUsername, defaultPassword = parseCredentials(c.String("basic-auth"))
|
|
|
|
// turn on HTTPS unless flag says not to
|
|
|
|
if !c.Bool("force-http") {
|
|
|
|
err := c.Set("https", "true")
|
|
|
|
checkErr("setting HTTPS flag for BasicAuth (this should never fail)", err)
|
|
|
|
}
|
|
|
|
}
|
2017-01-24 14:06:17 +00:00
|
|
|
if c.Bool("https") {
|
|
|
|
defaultProtocol = "https"
|
2017-07-21 21:06:10 +00:00
|
|
|
defaultTransport = newTLSTransport(c.Bool("no-check-certificate"))
|
2017-01-24 14:06:17 +00:00
|
|
|
}
|
|
|
|
if c.Bool("debug") {
|
|
|
|
logging.SetLogLevel("cluster-ctl", "debug")
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2016-12-22 16:14:15 +00:00
|
|
|
|
2017-01-24 14:06:17 +00:00
|
|
|
app.Commands = []cli.Command{
|
2017-01-24 15:19:23 +00:00
|
|
|
{
|
|
|
|
Name: "id",
|
2017-07-03 16:36:00 +00:00
|
|
|
Usage: "retrieve peer information",
|
|
|
|
Description: `
|
|
|
|
This command displays information about the peer that the tool is contacting
|
|
|
|
(usually running in localhost).
|
2017-01-24 15:19:23 +00:00
|
|
|
`,
|
2017-02-08 18:52:34 +00:00
|
|
|
Flags: []cli.Flag{parseFlag(formatID)},
|
2017-01-24 18:55:06 +00:00
|
|
|
Action: func(c *cli.Context) error {
|
2017-01-30 12:12:25 +00:00
|
|
|
resp := request("GET", "/id", nil)
|
2017-02-08 18:52:34 +00:00
|
|
|
formatResponse(c, resp)
|
2017-01-24 18:55:06 +00:00
|
|
|
return nil
|
2017-01-24 15:19:23 +00:00
|
|
|
},
|
|
|
|
},
|
2017-01-24 14:06:17 +00:00
|
|
|
{
|
2017-07-03 16:36:00 +00:00
|
|
|
Name: "peers",
|
|
|
|
Description: "list and manage IPFS Cluster peers",
|
2017-01-24 14:06:17 +00:00
|
|
|
Subcommands: []cli.Command{
|
|
|
|
{
|
|
|
|
Name: "ls",
|
|
|
|
Usage: "list the nodes participating in the IPFS Cluster",
|
2017-07-03 16:36:00 +00:00
|
|
|
Description: `
|
|
|
|
This command provides a list of the ID information of all the peers in the Cluster.
|
2017-01-30 12:12:25 +00:00
|
|
|
`,
|
2017-07-03 16:36:00 +00:00
|
|
|
Flags: []cli.Flag{parseFlag(formatID)},
|
|
|
|
ArgsUsage: " ",
|
2017-01-30 12:12:25 +00:00
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
resp := request("GET", "/peers", nil)
|
2017-02-08 18:52:34 +00:00
|
|
|
formatResponse(c, resp)
|
2017-01-30 12:12:25 +00:00
|
|
|
return nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "add",
|
|
|
|
Usage: "add a peer to the Cluster",
|
2017-07-03 16:36:00 +00:00
|
|
|
Description: `
|
2017-01-30 12:12:25 +00:00
|
|
|
This command adds a new peer to the cluster. In order for the operation to
|
|
|
|
succeed, the new peer needs to be reachable and any other member of the cluster
|
|
|
|
should be online. The operation returns the ID information for the new peer.
|
|
|
|
`,
|
|
|
|
ArgsUsage: "<multiaddress>",
|
2017-02-08 18:52:34 +00:00
|
|
|
Flags: []cli.Flag{parseFlag(formatID)},
|
2017-01-30 12:12:25 +00:00
|
|
|
Action: func(c *cli.Context) error {
|
|
|
|
addr := c.Args().First()
|
|
|
|
if addr == "" {
|
|
|
|
return cli.NewExitError("Error: a multiaddress argument is needed", 1)
|
|
|
|
}
|
|
|
|
_, err := ma.NewMultiaddr(addr)
|
|
|
|
checkErr("parsing multiaddress", err)
|
|
|
|
addBody := peerAddBody{addr}
|
|
|
|
var buf bytes.Buffer
|
|
|
|
enc := json.NewEncoder(&buf)
|
|
|
|
enc.Encode(addBody)
|
|
|
|
resp := request("POST", "/peers", &buf)
|
2017-02-08 18:52:34 +00:00
|
|
|
formatResponse(c, resp)
|
2017-01-30 12:12:25 +00:00
|
|
|
return nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "rm",
|
|
|
|
Usage: "remove a peer from the Cluster",
|
2017-07-03 16:36:00 +00:00
|
|
|
Description: `
|
2017-01-30 12:12:25 +00:00
|
|
|
This command removes a peer from the cluster. If the peer is online, it will
|
|
|
|
automatically shut down. All other cluster peers should be online for the
|
|
|
|
operation to succeed, otherwise some nodes may be left with an outdated list of
|
|
|
|
cluster peers.
|
|
|
|
`,
|
|
|
|
ArgsUsage: "<peer ID>",
|
2017-02-08 18:52:34 +00:00
|
|
|
Flags: []cli.Flag{parseFlag(formatNone)},
|
2017-01-24 18:55:06 +00:00
|
|
|
Action: func(c *cli.Context) error {
|
2017-01-30 12:12:25 +00:00
|
|
|
pid := c.Args().First()
|
|
|
|
_, err := peer.IDB58Decode(pid)
|
|
|
|
checkErr("parsing peer ID", err)
|
|
|
|
resp := request("DELETE", "/peers/"+pid, nil)
|
2017-02-08 18:52:34 +00:00
|
|
|
formatResponse(c, resp)
|
2017-01-24 18:55:06 +00:00
|
|
|
return nil
|
2017-01-24 14:06:17 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
2017-07-03 16:36:00 +00:00
|
|
|
Name: "pin",
|
|
|
|
Description: "add, remove or list items managed by IPFS Cluster",
|
2017-01-24 14:06:17 +00:00
|
|
|
Subcommands: []cli.Command{
|
|
|
|
{
|
|
|
|
Name: "add",
|
|
|
|
Usage: "Track a CID (pin)",
|
2017-07-03 16:36:00 +00:00
|
|
|
Description: `
|
2016-12-22 16:14:15 +00:00
|
|
|
This command tells IPFS Cluster to start managing a CID. Depending on
|
|
|
|
the pinning strategy, this will trigger IPFS pin requests. The CID will
|
|
|
|
become part of the Cluster's state and will tracked from this point.
|
|
|
|
|
2017-01-24 14:06:17 +00:00
|
|
|
When the request has succeeded, the command returns the status of the CID
|
|
|
|
in the cluster and should be part of the list offered by "pin ls".
|
2017-03-08 17:51:03 +00:00
|
|
|
|
|
|
|
An optional replication factor can be provided: -1 means "pin everywhere"
|
|
|
|
and 0 means use cluster's default setting. Positive values indicate how many
|
|
|
|
peers should pin this content.
|
2017-01-24 14:06:17 +00:00
|
|
|
`,
|
2017-07-03 16:36:00 +00:00
|
|
|
ArgsUsage: "<CID>",
|
2017-03-08 17:51:03 +00:00
|
|
|
Flags: []cli.Flag{
|
|
|
|
parseFlag(formatGPInfo),
|
|
|
|
cli.IntFlag{
|
|
|
|
Name: "replication, r",
|
|
|
|
Value: 0,
|
|
|
|
Usage: "Sets a custom replication factor for this pin",
|
|
|
|
},
|
2017-11-28 20:06:12 +00:00
|
|
|
cli.StringFlag{
|
|
|
|
Name: "name, n",
|
|
|
|
Value: "",
|
|
|
|
Usage: "Sets a name for this pin",
|
|
|
|
},
|
2017-03-08 17:51:03 +00:00
|
|
|
},
|
2017-01-24 18:55:06 +00:00
|
|
|
Action: func(c *cli.Context) error {
|
2017-01-24 14:06:17 +00:00
|
|
|
cidStr := c.Args().First()
|
|
|
|
_, err := cid.Decode(cidStr)
|
|
|
|
checkErr("parsing cid", err)
|
2017-11-30 19:58:35 +00:00
|
|
|
escapedName := url.QueryEscape(c.String("name"))
|
|
|
|
query := fmt.Sprintf("?replication_factor=%d&name=%s", c.Int("replication"), escapedName)
|
2017-03-08 17:51:03 +00:00
|
|
|
resp := request("POST", "/pins/"+cidStr+query, nil)
|
2017-02-13 15:46:53 +00:00
|
|
|
formatResponse(c, resp)
|
2017-03-13 10:10:53 +00:00
|
|
|
if resp.StatusCode < 300 {
|
|
|
|
time.Sleep(1000 * time.Millisecond)
|
|
|
|
resp = request("GET", "/pins/"+cidStr, nil)
|
|
|
|
formatResponse(c, resp)
|
|
|
|
}
|
2017-01-24 18:55:06 +00:00
|
|
|
return nil
|
2017-01-24 14:06:17 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "rm",
|
|
|
|
Usage: "Stop tracking a CID (unpin)",
|
2017-07-03 16:36:00 +00:00
|
|
|
Description: `
|
2016-12-22 16:14:15 +00:00
|
|
|
This command tells IPFS Cluster to no longer manage a CID. This will
|
|
|
|
trigger unpinning operations in all the IPFS nodes holding the content.
|
|
|
|
|
2017-01-24 14:06:17 +00:00
|
|
|
When the request has succeeded, the command returns the status of the CID
|
2017-01-25 17:10:18 +00:00
|
|
|
in the cluster. The CID should disappear from the list offered by "pin ls",
|
2017-01-24 14:06:17 +00:00
|
|
|
although unpinning operations in the cluster may take longer or fail.
|
|
|
|
`,
|
2017-07-03 16:36:00 +00:00
|
|
|
ArgsUsage: "<CID>",
|
2017-02-08 18:52:34 +00:00
|
|
|
Flags: []cli.Flag{parseFlag(formatGPInfo)},
|
2017-01-24 18:55:06 +00:00
|
|
|
Action: func(c *cli.Context) error {
|
2017-01-24 14:06:17 +00:00
|
|
|
cidStr := c.Args().First()
|
|
|
|
_, err := cid.Decode(cidStr)
|
|
|
|
checkErr("parsing cid", err)
|
2017-03-13 10:10:53 +00:00
|
|
|
resp := request("DELETE", "/pins/"+cidStr, nil)
|
|
|
|
if resp.StatusCode < 300 {
|
|
|
|
time.Sleep(1000 * time.Millisecond)
|
|
|
|
resp := request("GET", "/pins/"+cidStr, nil)
|
|
|
|
formatResponse(c, resp)
|
|
|
|
}
|
2017-01-24 18:55:06 +00:00
|
|
|
return nil
|
2017-01-24 14:06:17 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "ls",
|
|
|
|
Usage: "List tracked CIDs",
|
2017-07-03 16:36:00 +00:00
|
|
|
Description: `
|
2017-02-13 15:46:53 +00:00
|
|
|
This command will list the CIDs which are tracked by IPFS Cluster and to
|
|
|
|
which peers they are currently allocated. This list does not include
|
2017-04-06 02:27:02 +00:00
|
|
|
any monitoring information about the IPFS status of the CIDs, it
|
|
|
|
merely represents the list of pins which are part of the shared state of
|
|
|
|
the cluster. For IPFS-status information about the pins, use "status".
|
2017-01-24 14:06:17 +00:00
|
|
|
`,
|
2017-07-03 16:36:00 +00:00
|
|
|
ArgsUsage: "[CID]",
|
2017-04-06 02:27:02 +00:00
|
|
|
Flags: []cli.Flag{parseFlag(formatPin)},
|
2017-01-24 18:55:06 +00:00
|
|
|
Action: func(c *cli.Context) error {
|
2017-04-06 02:27:02 +00:00
|
|
|
cidStr := c.Args().First()
|
|
|
|
if cidStr != "" {
|
|
|
|
_, err := cid.Decode(cidStr)
|
|
|
|
checkErr("parsing cid", err)
|
|
|
|
}
|
|
|
|
resp := request("GET", "/allocations/"+cidStr, nil)
|
2017-02-08 18:52:34 +00:00
|
|
|
formatResponse(c, resp)
|
2017-01-24 18:55:06 +00:00
|
|
|
return nil
|
2017-01-24 14:06:17 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "status",
|
|
|
|
Usage: "Retrieve the status of tracked items",
|
2017-07-03 16:36:00 +00:00
|
|
|
Description: `
|
2016-12-22 16:14:15 +00:00
|
|
|
This command retrieves the status of the CIDs tracked by IPFS
|
|
|
|
Cluster, including which member is pinning them and any errors.
|
|
|
|
If a CID is provided, the status will be only fetched for a single
|
|
|
|
item.
|
2017-01-25 18:38:23 +00:00
|
|
|
|
|
|
|
The status of a CID may not be accurate. A manual sync can be triggered
|
|
|
|
with "sync".
|
2017-12-01 11:56:26 +00:00
|
|
|
|
|
|
|
When the --local flag is passed, it will only fetch the status from the
|
|
|
|
contacted cluster peer. By default, status will be fetched from all peers.
|
2017-01-24 14:06:17 +00:00
|
|
|
`,
|
2017-07-03 16:36:00 +00:00
|
|
|
ArgsUsage: "[CID]",
|
2017-12-01 11:56:26 +00:00
|
|
|
Flags: []cli.Flag{
|
|
|
|
parseFlag(formatGPInfo),
|
|
|
|
localFlag(),
|
|
|
|
},
|
2017-01-24 18:55:06 +00:00
|
|
|
Action: func(c *cli.Context) error {
|
2017-12-01 11:56:26 +00:00
|
|
|
local := "false"
|
|
|
|
if c.Bool("local") {
|
|
|
|
local = "true"
|
|
|
|
}
|
|
|
|
|
2017-01-24 14:06:17 +00:00
|
|
|
cidStr := c.Args().First()
|
|
|
|
if cidStr != "" {
|
|
|
|
_, err := cid.Decode(cidStr)
|
|
|
|
checkErr("parsing cid", err)
|
|
|
|
}
|
2017-12-01 11:56:26 +00:00
|
|
|
resp := request("GET", "/pins/"+cidStr+"?local="+local, nil)
|
2017-02-08 18:52:34 +00:00
|
|
|
formatResponse(c, resp)
|
2017-01-24 18:55:06 +00:00
|
|
|
return nil
|
2017-01-24 14:06:17 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "sync",
|
2017-01-25 18:38:23 +00:00
|
|
|
Usage: "Sync status of tracked items",
|
2017-07-03 16:36:00 +00:00
|
|
|
Description: `
|
2017-01-25 18:38:23 +00:00
|
|
|
This command asks Cluster peers to verify that the current status of tracked
|
|
|
|
CIDs is accurate by triggering queries to the IPFS daemons that pin them.
|
2017-03-08 17:51:03 +00:00
|
|
|
If a CID is provided, the sync and recover operations will be limited to
|
2017-01-25 18:38:23 +00:00
|
|
|
that single item.
|
|
|
|
|
|
|
|
Unless providing a specific CID, the command will output only items which
|
|
|
|
have changed status because of the sync or are in error state in some node,
|
|
|
|
therefore, the output should be empty if no operations were performed.
|
|
|
|
|
|
|
|
CIDs in error state may be manually recovered with "recover".
|
2017-12-01 11:56:26 +00:00
|
|
|
|
|
|
|
When the --local flag is passed, it will only trigger sync
|
|
|
|
operations on the contacted peer. By default, all peers will sync.
|
2017-01-24 14:06:17 +00:00
|
|
|
`,
|
2017-07-03 16:36:00 +00:00
|
|
|
ArgsUsage: "[CID]",
|
2017-12-01 11:56:26 +00:00
|
|
|
Flags: []cli.Flag{
|
|
|
|
parseFlag(formatGPInfo),
|
|
|
|
localFlag(),
|
|
|
|
},
|
2017-01-24 18:55:06 +00:00
|
|
|
Action: func(c *cli.Context) error {
|
2017-12-01 11:56:26 +00:00
|
|
|
local := "false"
|
|
|
|
if c.Bool("local") {
|
|
|
|
local = "true"
|
|
|
|
}
|
2017-01-24 14:06:17 +00:00
|
|
|
cidStr := c.Args().First()
|
2017-01-25 18:38:23 +00:00
|
|
|
var resp *http.Response
|
2017-01-24 14:06:17 +00:00
|
|
|
if cidStr != "" {
|
|
|
|
_, err := cid.Decode(cidStr)
|
|
|
|
checkErr("parsing cid", err)
|
2017-12-01 11:56:26 +00:00
|
|
|
resp = request("POST", "/pins/"+cidStr+"/sync?local="+local, nil)
|
2017-01-25 18:38:23 +00:00
|
|
|
} else {
|
2017-12-01 11:56:26 +00:00
|
|
|
resp = request("POST", "/pins/sync?local="+local, nil)
|
2016-12-22 16:14:15 +00:00
|
|
|
}
|
2017-02-08 18:52:34 +00:00
|
|
|
formatResponse(c, resp)
|
2017-01-24 18:55:06 +00:00
|
|
|
return nil
|
2017-01-24 14:06:17 +00:00
|
|
|
},
|
|
|
|
},
|
2017-01-25 18:38:23 +00:00
|
|
|
{
|
|
|
|
Name: "recover",
|
|
|
|
Usage: "Recover tracked items in error state",
|
2017-07-03 16:36:00 +00:00
|
|
|
Description: `
|
2017-11-30 00:53:31 +00:00
|
|
|
This command asks Cluster peers to re-track or re-forget CIDs in
|
2017-01-25 18:38:23 +00:00
|
|
|
error state, usually because the IPFS pin or unpin operation has failed.
|
|
|
|
|
|
|
|
The command will wait for any operations to succeed and will return the status
|
2017-11-30 00:53:31 +00:00
|
|
|
of the item upon completion. Note that, when running on the full sets of tracked
|
2017-12-04 12:59:48 +00:00
|
|
|
CIDs (without argument), it may take a considerably long time.
|
2017-11-30 00:53:31 +00:00
|
|
|
|
2017-12-01 11:56:26 +00:00
|
|
|
When the --local flag is passed, it will only trigger recover
|
|
|
|
operations on the contacted peer (as opposed to on every peer).
|
2017-01-25 18:38:23 +00:00
|
|
|
`,
|
2017-11-30 00:53:31 +00:00
|
|
|
ArgsUsage: "[CID]",
|
|
|
|
Flags: []cli.Flag{
|
|
|
|
parseFlag(formatGPInfo),
|
2017-12-01 11:56:26 +00:00
|
|
|
localFlag(),
|
2017-11-30 00:53:31 +00:00
|
|
|
},
|
2017-01-25 18:38:23 +00:00
|
|
|
Action: func(c *cli.Context) error {
|
2017-11-30 00:53:31 +00:00
|
|
|
local := "false"
|
|
|
|
if c.Bool("local") {
|
|
|
|
local = "true"
|
|
|
|
}
|
2017-01-25 18:38:23 +00:00
|
|
|
cidStr := c.Args().First()
|
|
|
|
var resp *http.Response
|
|
|
|
if cidStr != "" {
|
|
|
|
_, err := cid.Decode(cidStr)
|
|
|
|
checkErr("parsing cid", err)
|
2017-11-30 00:53:31 +00:00
|
|
|
resp = request("POST", "/pins/"+cidStr+"/recover?local="+local, nil)
|
2017-02-08 18:52:34 +00:00
|
|
|
formatResponse(c, resp)
|
2017-01-25 18:38:23 +00:00
|
|
|
|
|
|
|
} else {
|
2017-11-30 00:53:31 +00:00
|
|
|
resp = request("POST", "/pins/recover?local="+local, nil)
|
|
|
|
formatResponse(c, resp)
|
2017-01-25 18:38:23 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
},
|
2017-01-24 15:24:46 +00:00
|
|
|
{
|
|
|
|
Name: "version",
|
|
|
|
Usage: "Retrieve cluster version",
|
2017-07-03 16:36:00 +00:00
|
|
|
Description: `
|
2017-01-24 15:24:46 +00:00
|
|
|
This command retrieves the IPFS Cluster version and can be used
|
|
|
|
to check that it matches the CLI version (shown by -v).
|
|
|
|
`,
|
2017-07-03 16:36:00 +00:00
|
|
|
ArgsUsage: " ",
|
|
|
|
Flags: []cli.Flag{parseFlag(formatVersion)},
|
2017-01-24 18:55:06 +00:00
|
|
|
Action: func(c *cli.Context) error {
|
2017-01-30 12:12:25 +00:00
|
|
|
resp := request("GET", "/version", nil)
|
2017-02-08 18:52:34 +00:00
|
|
|
formatResponse(c, resp)
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
2017-07-03 16:36:00 +00:00
|
|
|
Name: "commands",
|
|
|
|
Usage: "List all commands",
|
|
|
|
ArgsUsage: " ",
|
|
|
|
Hidden: true,
|
2017-02-08 18:52:34 +00:00
|
|
|
Action: func(c *cli.Context) error {
|
2017-07-03 16:36:00 +00:00
|
|
|
walkCommands(c.App.Commands, "ipfs-cluster-ctl")
|
2017-01-24 18:55:06 +00:00
|
|
|
return nil
|
2017-01-24 15:24:46 +00:00
|
|
|
},
|
|
|
|
},
|
2016-12-22 16:14:15 +00:00
|
|
|
}
|
|
|
|
|
2017-01-24 14:06:17 +00:00
|
|
|
app.Run(os.Args)
|
2016-12-22 16:14:15 +00:00
|
|
|
}
|
|
|
|
|
2017-02-08 18:52:34 +00:00
|
|
|
func parseFlag(t int) cli.IntFlag {
|
|
|
|
return cli.IntFlag{
|
|
|
|
Name: "parseAs",
|
|
|
|
Value: t,
|
|
|
|
Hidden: true,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-01 11:56:26 +00:00
|
|
|
func localFlag() cli.BoolFlag {
|
|
|
|
return cli.BoolFlag{
|
|
|
|
Name: "local",
|
|
|
|
Usage: "run operation only on the contacted peer",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-03 16:36:00 +00:00
|
|
|
func walkCommands(cmds []cli.Command, parentHelpName string) {
|
2017-02-08 18:52:34 +00:00
|
|
|
for _, c := range cmds {
|
2017-07-14 19:22:52 +00:00
|
|
|
h := c.HelpName
|
|
|
|
// Sometimes HelpName is empty
|
|
|
|
if h == "" {
|
2017-07-03 16:36:00 +00:00
|
|
|
h = fmt.Sprintf("%s %s", parentHelpName, c.FullName())
|
|
|
|
}
|
2017-07-14 19:22:52 +00:00
|
|
|
fmt.Println(h)
|
2017-07-03 16:36:00 +00:00
|
|
|
walkCommands(c.Subcommands, h)
|
2017-02-08 18:52:34 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-01-30 12:12:25 +00:00
|
|
|
func request(method, path string, body io.Reader, args ...string) *http.Response {
|
2016-12-22 16:14:15 +00:00
|
|
|
ctx, cancel := context.WithTimeout(context.Background(),
|
|
|
|
time.Duration(defaultTimeout)*time.Second)
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
u := defaultProtocol + "://" + defaultHost + path
|
|
|
|
// turn /a/{param0}/{param1} into /a/this/that
|
|
|
|
for i, a := range args {
|
|
|
|
p := fmt.Sprintf("{param%d}", i)
|
|
|
|
u = strings.Replace(u, p, a, 1)
|
|
|
|
}
|
2016-12-23 18:35:37 +00:00
|
|
|
u = strings.TrimSuffix(u, "/")
|
|
|
|
|
2016-12-22 16:14:15 +00:00
|
|
|
logger.Debugf("%s: %s", method, u)
|
|
|
|
|
2017-01-30 12:12:25 +00:00
|
|
|
r, err := http.NewRequest(method, u, body)
|
2016-12-22 16:14:15 +00:00
|
|
|
checkErr("creating request", err)
|
|
|
|
r.WithContext(ctx)
|
|
|
|
|
2017-08-29 01:41:54 +00:00
|
|
|
if len(defaultUsername) != 0 {
|
|
|
|
r.SetBasicAuth(defaultUsername, defaultPassword)
|
|
|
|
}
|
|
|
|
|
2017-07-21 21:06:10 +00:00
|
|
|
client := &http.Client{Transport: defaultTransport}
|
2016-12-22 16:14:15 +00:00
|
|
|
resp, err := client.Do(r)
|
2017-10-18 17:04:29 +00:00
|
|
|
checkErr(fmt.Sprintf("performing request to %s", defaultHost), err)
|
2016-12-22 16:14:15 +00:00
|
|
|
return resp
|
|
|
|
}
|
|
|
|
|
2017-02-08 18:52:34 +00:00
|
|
|
func formatResponse(c *cli.Context, r *http.Response) {
|
2016-12-22 16:14:15 +00:00
|
|
|
defer r.Body.Close()
|
|
|
|
body, err := ioutil.ReadAll(r.Body)
|
|
|
|
checkErr("reading body", err)
|
2017-03-28 15:48:26 +00:00
|
|
|
logger.Debugf("Response body: %s", body)
|
2016-12-22 16:14:15 +00:00
|
|
|
|
2017-02-08 18:52:34 +00:00
|
|
|
switch {
|
|
|
|
case r.StatusCode == http.StatusAccepted:
|
2017-03-28 15:48:26 +00:00
|
|
|
logger.Debug("Request accepted")
|
2017-02-08 18:52:34 +00:00
|
|
|
case r.StatusCode == http.StatusNoContent:
|
2017-12-06 14:00:01 +00:00
|
|
|
logger.Debug("Request succeeded. Response has no content")
|
2017-02-08 18:52:34 +00:00
|
|
|
default:
|
|
|
|
enc := c.GlobalString("encoding")
|
|
|
|
|
|
|
|
switch enc {
|
|
|
|
case "text":
|
2017-03-28 15:48:26 +00:00
|
|
|
if r.StatusCode > 399 {
|
|
|
|
textFormat(body, formatError)
|
2017-08-29 01:41:54 +00:00
|
|
|
os.Exit(2)
|
2017-03-28 15:48:26 +00:00
|
|
|
} else {
|
|
|
|
textFormat(body, c.Int("parseAs"))
|
|
|
|
}
|
2017-02-08 18:52:34 +00:00
|
|
|
default:
|
|
|
|
var resp interface{}
|
|
|
|
err = json.Unmarshal(body, &resp)
|
|
|
|
checkErr("decoding response", err)
|
|
|
|
prettyPrint(body)
|
|
|
|
}
|
2016-12-22 16:14:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-08-29 01:41:54 +00:00
|
|
|
func parseCredentials(userInput string) (string, string) {
|
|
|
|
credentials := strings.SplitN(userInput, ":", 2)
|
|
|
|
switch len(credentials) {
|
|
|
|
case 1:
|
|
|
|
// only username passed in (with no trailing `:`), return empty password
|
|
|
|
return credentials[0], ""
|
|
|
|
case 2:
|
|
|
|
return credentials[0], credentials[1]
|
|
|
|
default:
|
|
|
|
err := fmt.Errorf("invalid <username>[:<password>] input")
|
|
|
|
checkErr("parsing credentials", err)
|
|
|
|
return "", ""
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-01-24 15:55:37 +00:00
|
|
|
// JSON output is nice and allows users to build on top.
|
|
|
|
func prettyPrint(buf []byte) {
|
|
|
|
var dst bytes.Buffer
|
|
|
|
err := json.Indent(&dst, buf, "", " ")
|
|
|
|
checkErr("indenting json", err)
|
|
|
|
fmt.Printf("%s", dst.String())
|
|
|
|
}
|
|
|
|
|
2017-07-21 21:06:10 +00:00
|
|
|
func newTLSTransport(skipVerifyCert bool) *http.Transport {
|
|
|
|
// based on https://github.com/denji/golang-tls
|
|
|
|
tlsCfg := &tls.Config{
|
|
|
|
MinVersion: tls.VersionTLS12,
|
|
|
|
CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
|
|
|
|
PreferServerCipherSuites: true,
|
|
|
|
CipherSuites: []uint16{
|
|
|
|
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
|
|
|
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
|
|
|
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
|
|
|
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
|
|
|
},
|
|
|
|
InsecureSkipVerify: skipVerifyCert,
|
|
|
|
}
|
|
|
|
return &http.Transport{
|
|
|
|
TLSClientConfig: tlsCfg,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-01-24 15:55:37 +00:00
|
|
|
/*
|
|
|
|
// old ugly pretty print
|
2016-12-22 16:14:15 +00:00
|
|
|
func prettyPrint(obj interface{}, indent int) {
|
|
|
|
ind := func() string {
|
|
|
|
var str string
|
|
|
|
for i := 0; i < indent; i++ {
|
|
|
|
str += " "
|
|
|
|
}
|
|
|
|
return str
|
|
|
|
}
|
|
|
|
|
|
|
|
switch obj.(type) {
|
|
|
|
case []interface{}:
|
|
|
|
slice := obj.([]interface{})
|
|
|
|
for _, elem := range slice {
|
|
|
|
prettyPrint(elem, indent+2)
|
|
|
|
}
|
|
|
|
case map[string]interface{}:
|
|
|
|
m := obj.(map[string]interface{})
|
|
|
|
keys := make([]string, 0, len(m))
|
|
|
|
for k := range m {
|
|
|
|
keys = append(keys, k)
|
|
|
|
}
|
|
|
|
sort.Strings(keys)
|
|
|
|
for _, k := range keys {
|
|
|
|
v := m[k]
|
|
|
|
fmt.Printf(ind()+"%s: ", k)
|
|
|
|
switch v.(type) {
|
|
|
|
case []interface{}, map[string]interface{}:
|
|
|
|
fmt.Println()
|
|
|
|
prettyPrint(v, indent+4)
|
|
|
|
default:
|
|
|
|
prettyPrint(v, indent)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
fmt.Println(obj)
|
|
|
|
}
|
|
|
|
}
|
2017-01-24 15:55:37 +00:00
|
|
|
*/
|