diff --git a/cmd/ipfs/init.go b/cmd/ipfs/init.go index 489e74a4d9f..20e15b27f27 100644 --- a/cmd/ipfs/init.go +++ b/cmd/ipfs/init.go @@ -160,7 +160,7 @@ func doInit(out io.Writer, repoRoot string, empty bool, nBitsForKeypair int, con } for _, profile := range confProfiles { - transformer, ok := config.ConfigProfiles[profile] + transformer, ok := config.Profiles[profile] if !ok { return fmt.Errorf("invalid configuration profile: %s", profile) } diff --git a/core/commands/config.go b/core/commands/config.go index 9bfcf035d51..75e64b564d1 100644 --- a/core/commands/config.go +++ b/core/commands/config.go @@ -142,6 +142,7 @@ Set the value of the 'Datastore.Path' key: "show": configShowCmd, "edit": configEditCmd, "replace": configReplaceCmd, + "profile": configProfileCmd, }, } @@ -293,6 +294,64 @@ can't be undone. }, } +var configProfileCmd = &cmds.Command{ + Helptext: cmdkit.HelpText{ + Tagline: "Apply profiles to config.", + }, + + Subcommands: map[string]*cmds.Command{ + "apply": configProfileApplyCmd, + }, +} + +var configProfileApplyCmd = &cmds.Command{ + Helptext: cmdkit.HelpText{ + Tagline: "Apply profile to config.", + }, + Arguments: []cmdkit.Argument{ + cmdkit.StringArg("profile", true, false, "The profile to apply to the config."), + }, + Run: func(req cmds.Request, res cmds.Response) { + profile, ok := config.Profiles[req.Arguments()[0]] + if !ok { + res.SetError(fmt.Errorf("%s is not a profile", req.Arguments()[0]), cmdkit.ErrNormal) + return + } + + err := transformConfig(req.InvocContext().ConfigRoot, req.Arguments()[0], profile) + if err != nil { + res.SetError(err, cmdkit.ErrNormal) + return + } + res.SetOutput(nil) + }, +} + +func transformConfig(configRoot string, configName string, transformer config.Transformer) error { + r, err := fsrepo.Open(configRoot) + if err != nil { + return err + } + defer r.Close() + + cfg, err := r.Config() + if err != nil { + return err + } + + err = transformer(cfg) + if err != nil { + return err + } + + _, err = r.BackupConfig("pre-" + configName + "-") + if err != nil { + return err + } + + return r.SetConfig(cfg) +} + func getConfig(r repo.Repo, key string) (*ConfigField, error) { value, err := r.GetConfigKey(key) if err != nil { diff --git a/docs/config.md b/docs/config.md index 6c129e44f23..33dc9d3aae4 100644 --- a/docs/config.md +++ b/docs/config.md @@ -4,6 +4,46 @@ The go-ipfs config file is a json document. It is read once at node instantiatio either for an offline command, or when starting the daemon. Commands that execute on a running daemon do not read the config file at runtime. +#### Profiles +Configuration profiles allow to tweak configuration quickly. Profiles can be +applied with `--profile` flag to `ipfs init` or with `ipfs config profile apply` +command. When a profile is applied a backup of the configuration file will +be created in $IPFS_PATH + +Available profiles: +- `server` + + Recommended for nodes with public IPv4 address (servers, VPSes, etc.), + disables host and content discovery in local networks. + +- `local-discovery` + + Sets default values to fields affected by `server` profile, enables + discovery in local networks. + +- `test` + + Reduces external interference, useful for running ipfs in test environments. + Note that with these settings node won't be able to talk to the rest of the + network without manual bootstrap. + +- `default-networking` + + Restores default network settings. Inverse profile of the `test` profile. + +- `badgerds` + + Replaces default datastore configuration with experimental badger datastore. + If you apply this profile after `ipfs init`, you will need to convert your + datastore to the new configuration. You can do this using [ipfs-ds-convert](https://github.com/ipfs/ipfs-ds-convert) + + WARNING: badger datastore is experimental. Make sure to backup your data + frequently. + +- `default-datastore` + + Restores default datastore configuration. + ## Table of Contents - [`Addresses`](#addresses) diff --git a/repo/config/init.go b/repo/config/init.go index 1c7874e3d90..2b1def8b522 100644 --- a/repo/config/init.go +++ b/repo/config/init.go @@ -28,17 +28,7 @@ func Init(out io.Writer, nBitsForKeypair int) (*Config, error) { // setup the node's default addresses. // NOTE: two swarm listen addrs, one tcp, one utp. - Addresses: Addresses{ - Swarm: []string{ - "/ip4/0.0.0.0/tcp/4001", - // "/ip4/0.0.0.0/udp/4002/utp", // disabled for now. - "/ip6/::/tcp/4001", - }, - Announce: []string{}, - NoAnnounce: []string{}, - API: "/ip4/127.0.0.1/tcp/5001", - Gateway: "/ip4/127.0.0.1/tcp/8080", - }, + Addresses: addressesConfig(), Datastore: datastore, Bootstrap: BootstrapPeerStrings(bootstrapPeers), @@ -97,6 +87,20 @@ const DefaultConnMgrLowWater = 600 // grace period const DefaultConnMgrGracePeriod = time.Second * 20 +func addressesConfig() Addresses { + return Addresses{ + Swarm: []string{ + "/ip4/0.0.0.0/tcp/4001", + // "/ip4/0.0.0.0/udp/4002/utp", // disabled for now. + "/ip6/::/tcp/4001", + }, + Announce: []string{}, + NoAnnounce: []string{}, + API: "/ip4/127.0.0.1/tcp/5001", + Gateway: "/ip4/127.0.0.1/tcp/8080", + } +} + // DefaultDatastoreConfig is an internal function exported to aid in testing. func DefaultDatastoreConfig() Datastore { return Datastore{ diff --git a/repo/config/profile.go b/repo/config/profile.go index 3a7bf3694f3..74383cda63f 100644 --- a/repo/config/profile.go +++ b/repo/config/profile.go @@ -1,46 +1,62 @@ package config -// ConfigProfiles is a map holding configuration transformers -var ConfigProfiles = map[string]func(*Config) error{ - "server": func(c *Config) error { +// Transformer is a function which takes configuration and applies some filter to it +type Transformer func(c *Config) error - // defaultServerFilters has a list of non-routable IPv4 prefixes - // according to http://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml - defaultServerFilters := []string{ - "/ip4/10.0.0.0/ipcidr/8", - "/ip4/100.64.0.0/ipcidr/10", - "/ip4/169.254.0.0/ipcidr/16", - "/ip4/172.16.0.0/ipcidr/12", - "/ip4/192.0.0.0/ipcidr/24", - "/ip4/192.0.0.0/ipcidr/29", - "/ip4/192.0.0.8/ipcidr/32", - "/ip4/192.0.0.170/ipcidr/32", - "/ip4/192.0.0.171/ipcidr/32", - "/ip4/192.0.2.0/ipcidr/24", - "/ip4/192.168.0.0/ipcidr/16", - "/ip4/198.18.0.0/ipcidr/15", - "/ip4/198.51.100.0/ipcidr/24", - "/ip4/203.0.113.0/ipcidr/24", - "/ip4/240.0.0.0/ipcidr/4", - } +// defaultServerFilters has a list of non-routable IPv4 prefixes +// according to http://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml +var defaultServerFilters = []string{ + "/ip4/10.0.0.0/ipcidr/8", + "/ip4/100.64.0.0/ipcidr/10", + "/ip4/169.254.0.0/ipcidr/16", + "/ip4/172.16.0.0/ipcidr/12", + "/ip4/192.0.0.0/ipcidr/24", + "/ip4/192.0.0.0/ipcidr/29", + "/ip4/192.0.0.8/ipcidr/32", + "/ip4/192.0.0.170/ipcidr/32", + "/ip4/192.0.0.171/ipcidr/32", + "/ip4/192.0.2.0/ipcidr/24", + "/ip4/192.168.0.0/ipcidr/16", + "/ip4/198.18.0.0/ipcidr/15", + "/ip4/198.51.100.0/ipcidr/24", + "/ip4/203.0.113.0/ipcidr/24", + "/ip4/240.0.0.0/ipcidr/4", +} - c.Swarm.AddrFilters = append(c.Swarm.AddrFilters, defaultServerFilters...) +// Profiles is a map holding configuration transformers. Docs are in docs/config.md +var Profiles = map[string]Transformer{ + "server": func(c *Config) error { + c.Addresses.NoAnnounce = appendSingle(c.Addresses.NoAnnounce, defaultServerFilters) + c.Swarm.AddrFilters = appendSingle(c.Swarm.AddrFilters, defaultServerFilters) c.Discovery.MDNS.Enabled = false return nil }, + "local-discovery": func(c *Config) error { + c.Addresses.NoAnnounce = deleteEntries(c.Addresses.NoAnnounce, defaultServerFilters) + c.Swarm.AddrFilters = deleteEntries(c.Swarm.AddrFilters, defaultServerFilters) + c.Discovery.MDNS.Enabled = true + return nil + }, "test": func(c *Config) error { c.Addresses.API = "/ip4/127.0.0.1/tcp/0" c.Addresses.Gateway = "/ip4/127.0.0.1/tcp/0" - - c.Swarm.DisableNatPortMap = true c.Addresses.Swarm = []string{ "/ip4/127.0.0.1/tcp/0", } + c.Swarm.DisableNatPortMap = true + c.Bootstrap = []string{} c.Discovery.MDNS.Enabled = false return nil }, + "default-networking": func(c *Config) error { + c.Addresses = addressesConfig() + + c.Swarm.DisableNatPortMap = false + c.Discovery.MDNS.Enabled = true + return nil + }, "badgerds": func(c *Config) error { c.Datastore.Spec = map[string]interface{}{ "type": "measure", @@ -53,4 +69,38 @@ var ConfigProfiles = map[string]func(*Config) error{ } return nil }, + "default-datastore": func(c *Config) error { + c.Datastore.Spec = DefaultDatastoreConfig().Spec + return nil + }, +} + +func appendSingle(a []string, b []string) []string { + m := map[string]struct{}{} + for _, f := range a { + m[f] = struct{}{} + } + for _, f := range b { + m[f] = struct{}{} + } + return mapKeys(m) +} + +func deleteEntries(arr []string, del []string) []string { + m := map[string]struct{}{} + for _, f := range arr { + m[f] = struct{}{} + } + for _, f := range del { + delete(m, f) + } + return mapKeys(m) +} + +func mapKeys(m map[string]struct{}) []string { + out := make([]string, 0, len(m)) + for f := range m { + out = append(out, f) + } + return out } diff --git a/repo/fsrepo/fsrepo.go b/repo/fsrepo/fsrepo.go index 757c36438ae..51823042a77 100644 --- a/repo/fsrepo/fsrepo.go +++ b/repo/fsrepo/fsrepo.go @@ -480,6 +480,32 @@ func (r *FSRepo) FileManager() *filestore.FileManager { return r.filemgr } +func (r *FSRepo) BackupConfig(prefix string) (string, error) { + temp, err := ioutil.TempFile(r.path, "config-"+prefix) + if err != nil { + return "", err + } + defer temp.Close() + + configFilename, err := config.Filename(r.path) + if err != nil { + return "", err + } + + orig, err := os.OpenFile(configFilename, os.O_RDONLY, 0600) + if err != nil { + return "", err + } + defer orig.Close() + + _, err = io.Copy(temp, orig) + if err != nil { + return "", err + } + + return orig.Name(), nil +} + // setConfigUnsynced is for private use. func (r *FSRepo) setConfigUnsynced(updated *config.Config) error { configFilename, err := config.Filename(r.path) diff --git a/repo/mock.go b/repo/mock.go index 030c2ff28d1..ad9ddcf6cb0 100644 --- a/repo/mock.go +++ b/repo/mock.go @@ -28,6 +28,10 @@ func (m *Mock) SetConfig(updated *config.Config) error { return nil } +func (m *Mock) BackupConfig(prefix string) (string, error) { + return "", errTODO +} + func (m *Mock) SetConfigKey(key string, value interface{}) error { return errTODO } diff --git a/repo/repo.go b/repo/repo.go index 0cbf8f53669..3403482c9ea 100644 --- a/repo/repo.go +++ b/repo/repo.go @@ -18,6 +18,7 @@ var ( type Repo interface { Config() (*config.Config, error) + BackupConfig(prefix string) (string, error) SetConfig(*config.Config) error SetConfigKey(key string, value interface{}) error diff --git a/test/sharness/t0021-config.sh b/test/sharness/t0021-config.sh index b7b985a8963..5ade07d0326 100755 --- a/test/sharness/t0021-config.sh +++ b/test/sharness/t0021-config.sh @@ -48,6 +48,33 @@ CONFIG_SET_JSON_TEST='{ } }' +test_profile_apply_revert() { + profile=$1 + inverse_profile=$2 + + test_expect_success "save expected config" ' + ipfs config show >expected + ' + + test_expect_success "'ipfs config profile apply ${profile}' works" ' + ipfs config profile apply '${profile}' + ' + + test_expect_success "profile ${profile} changed something" ' + ipfs config show >actual && + test_must_fail test_cmp expected actual + ' + + test_expect_success "'ipfs config profile apply ${inverse_profile}' works" ' + ipfs config profile apply '${inverse_profile}' + ' + + test_expect_success "config is back to previous state after ${inverse_profile} was applied" ' + ipfs config show >actual && + test_cmp expected actual + ' +} + test_config_cmd() { test_config_cmd_set "beep" "boop" test_config_cmd_set "beep1" "boop2" @@ -151,6 +178,50 @@ test_config_cmd() { echo "Error: setting private key with API is not supported" > replace_expected test_cmp replace_out replace_expected ' + + test_expect_success "'ipfs config Swarm.AddrFilters' looks good" ' + ipfs config Swarm.AddrFilters > actual_config && + test $(cat actual_config | wc -l) = 1 + ' + + test_expect_success "copy ipfs config" ' + cp "$IPFS_PATH/config" before_patch + ' + + test_expect_success "'ipfs config profile apply server' works" ' + ipfs config profile apply server + ' + + test_expect_success "backup was created and looks good" ' + test_cmp "$(find "$IPFS_PATH" -name "config-*")" before_patch + ' + + test_expect_success "'ipfs config Swarm.AddrFilters' looks good with server profile" ' + ipfs config Swarm.AddrFilters > actual_config && + test $(cat actual_config | wc -l) = 17 + ' + + test_expect_success "'ipfs config profile apply local-discovery' works" ' + ipfs config profile apply local-discovery + ' + + test_expect_success "'ipfs config Swarm.AddrFilters' looks good with applied local-discovery profile" ' + ipfs config Swarm.AddrFilters > actual_config && + test $(cat actual_config | wc -l) = 1 + ' + + test_profile_apply_revert server local-discovery + + # won't work as we already have this profile applied + # test_profile_apply_revert test + + # won't work as it changes datastore definition, which makes ipfs not launch + # without converting first + # test_profile_apply_revert badgerds + + test_expect_success "cleanup config backups" ' + find "$IPFS_PATH" -name "config-*" -exec rm {} \; + ' } test_init_ipfs