From 6cbce88a337aefe3cf2089fcbe98e006f766822d Mon Sep 17 00:00:00 2001 From: Nate Koenig Date: Tue, 9 Apr 2024 15:02:51 -0700 Subject: [PATCH 1/4] Use config.yaml file from default cache location, if it exists (#410) * Use config.yaml file from default cache location, if it exists Signed-off-by: Nate Koenig * support config.yml Signed-off-by: Nate Koenig --------- Signed-off-by: Nate Koenig --- src/ClientConfig.cc | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/ClientConfig.cc b/src/ClientConfig.cc index 1b11c76e..db7b77e6 100644 --- a/src/ClientConfig.cc +++ b/src/ClientConfig.cc @@ -218,18 +218,24 @@ ClientConfig::ClientConfig() : dataPtr(new ClientConfigPrivate) << "to set cache path. Please use [GZ_FUEL_CACHE_PATH] instead." << std::endl; } - else - { - return; - } } - if (!gz::common::isDirectory(gzFuelPath)) + if (!gzFuelPath.empty()) { - gzerr << "[" << gzFuelPath << "] is not a directory" << std::endl; - return; + if (!gz::common::isDirectory(gzFuelPath)) + gzerr << "[" << gzFuelPath << "] is not a directory" << std::endl; + else + this->SetCacheLocation(gzFuelPath); } - this->SetCacheLocation(gzFuelPath); + + std::string configYamlFile = common::joinPaths(this->CacheLocation(), + "config.yaml"); + std::string configYmlFile = common::joinPaths(this->CacheLocation(), + "config.yml"); + if (gz::common::exists(configYamlFile)) + this->LoadConfig(configYamlFile); + else if (gz::common::exists(configYmlFile)) + this->LoadConfig(configYmlFile); } ////////////////////////////////////////////////// From 6880aecc292fae82dbd00bd23cce5286e63ed001 Mon Sep 17 00:00:00 2001 From: Nate Koenig Date: Tue, 16 Apr 2024 05:20:27 -0700 Subject: [PATCH 2/4] Add Private function to world identifier (#414) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Private function to world identifier Signed-off-by: Nate Koenig * Update include/gz/fuel_tools/WorldIdentifier.hh Co-authored-by: Alejandro Hernández Cordero Signed-off-by: Nate Koenig * Wrap line Signed-off-by: Nate Koenig --------- Signed-off-by: Nate Koenig Co-authored-by: Alejandro Hernández Cordero --- include/gz/fuel_tools/WorldIdentifier.hh | 10 ++++++++++ src/WorldIdentifier.cc | 17 ++++++++++++++++- src/WorldIdentifier_TEST.cc | 6 ++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/include/gz/fuel_tools/WorldIdentifier.hh b/include/gz/fuel_tools/WorldIdentifier.hh index d4579248..0cc99faa 100644 --- a/include/gz/fuel_tools/WorldIdentifier.hh +++ b/include/gz/fuel_tools/WorldIdentifier.hh @@ -150,6 +150,16 @@ namespace gz /// \return World information string public: std::string AsPrettyString(const std::string &_prefix = "") const; + /// \brief Returns the privacy setting of the world. + /// \return True if the world is private, false if the world is + /// public. + public: bool Private() const; + + /// \brief Set the privacy setting of the world. + /// \param[in] _private True indicates the world is private, + /// false indicates the world is public. + public: void SetPrivate(bool _private); + /// \brief PIMPL private: std::unique_ptr dataPtr; }; diff --git a/src/WorldIdentifier.cc b/src/WorldIdentifier.cc index 9c9296a0..26b22244 100644 --- a/src/WorldIdentifier.cc +++ b/src/WorldIdentifier.cc @@ -43,8 +43,12 @@ class gz::fuel_tools::WorldIdentifierPrivate /// \brief World version. Valid versions start from 1, 0 means the tip. public: unsigned int version{0}; - /// \brief Path of this model in the local cache + /// \brief Path of this world in the local cache public: std::string localPath; + + /// \brief True indicates the world is private, false indicates the + /// world is public. + public: bool privacy{false}; }; ////////////////////////////////////////////////// @@ -229,3 +233,14 @@ std::string WorldIdentifier::AsPrettyString(const std::string &_prefix) const return out.str(); } +////////////////////////////////////////////////// +bool WorldIdentifier::Private() const +{ + return this->dataPtr->privacy; +} + +////////////////////////////////////////////////// +void WorldIdentifier::SetPrivate(bool _private) +{ + this->dataPtr->privacy = _private; +} diff --git a/src/WorldIdentifier_TEST.cc b/src/WorldIdentifier_TEST.cc index 947d8fd1..7ad7566e 100644 --- a/src/WorldIdentifier_TEST.cc +++ b/src/WorldIdentifier_TEST.cc @@ -37,6 +37,12 @@ TEST(WorldIdentifier, SetFields) EXPECT_EQ(std::string("hello"), id.Name()); EXPECT_EQ(std::string("acai"), id.Owner()); EXPECT_EQ(6u, id.Version()); + + EXPECT_FALSE(id.Private()); + id.SetPrivate(true); + EXPECT_TRUE(id.Private()); + id.SetPrivate(false); + EXPECT_FALSE(id.Private()); } ///////////////////////////////////////////////// From d14ffcd7aba27b920639ac014050621340e784cc Mon Sep 17 00:00:00 2001 From: Nate Koenig Date: Thu, 25 Apr 2024 05:58:31 -0700 Subject: [PATCH 3/4] CLI for creating config.yaml (#413) Signed-off-by: Nate Koenig --- README.md | 19 +++++ conf/CMakeLists.txt | 4 - conf/config.yaml | 14 ---- src/cmd/cmdfuel.rb.in | 150 +++++++++++++++++++++++++++++++++- src/gz_TEST.cc | 19 ++++- src/gz_src_TEST.cc | 3 +- tutorials/02_configuration.md | 11 +++ 7 files changed, 199 insertions(+), 21 deletions(-) delete mode 100644 conf/config.yaml diff --git a/README.md b/README.md index 34ecbebf..12778747 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,25 @@ Downloading model: Download succeeded. ``` +**Private access tokens** +Private models and worlds can be downloaded using access tokens. +Access tokens are generated on `app.gazebosim.org`. After logging in, +go to `Settings->Access Tokens`. + +An access token can be used with CLI commands via the `--header` option: + +```bash +$ gz fuel download -u https://fuel.gazebosim.org/1.0/openrobotics/models/ambulance --header 'Private-Token: ' +``` + +Or, an access token can be stored in a `~/.gz/fuel/config.yaml` file. The token is then +automatically used by the command line tool and API calls. Use the `configure` helper +tool create your `~/.gz/fuel/config.yaml` file. + +```bash +$ gz fuel configure +``` + **C++ Get List models** ``` // Create a client (uses https://fuel.gazebosim.org by default) diff --git a/conf/CMakeLists.txt b/conf/CMakeLists.txt index a37a5804..e25320e9 100644 --- a/conf/CMakeLists.txt +++ b/conf/CMakeLists.txt @@ -18,7 +18,3 @@ configure_file( # Install the yaml configuration files in an unversioned location. install(FILES ${CMAKE_CURRENT_BINARY_DIR}/fuel${PROJECT_VERSION_MAJOR}.yaml DESTINATION ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/gz/) - -# Install config.yaml -install (FILES config.yaml DESTINATION - ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/gz/${GZ_DESIGNATION}${PROJECT_VERSION_MAJOR}/) diff --git a/conf/config.yaml b/conf/config.yaml deleted file mode 100644 index bf70a5dd..00000000 --- a/conf/config.yaml +++ /dev/null @@ -1,14 +0,0 @@ ---- -# The list of servers. -servers: - - - name: osrf - url: https://fuel.gazebosim.org - - # - - # name: another_server - # url: https://myserver - -# Where are the assets stored in disk. -# cache: -# path: /tmp/gz/fuel diff --git a/src/cmd/cmdfuel.rb.in b/src/cmd/cmdfuel.rb.in index 632d9443..23e89a67 100755 --- a/src/cmd/cmdfuel.rb.in +++ b/src/cmd/cmdfuel.rb.in @@ -25,7 +25,10 @@ else include Fiddle end +require 'fileutils' require 'optparse' +require 'uri' +require 'yaml' # Constants. LIBRARY_NAME = '@library_location@' @@ -61,6 +64,7 @@ COMMANDS = { 'fuel' => " gz fuel [action] [options] \n"\ " \n"\ "Available Actions: \n"\ + " configure Create config.yaml configuration file \n"\ " delete Delete resources \n"\ " download Download resources \n"\ " edit Edit a resource \n"\ @@ -80,6 +84,20 @@ COMMANDS = { 'fuel' => } SUBCOMMANDS = { + 'configure' => + "Create `~/.gz/fuel/config.yaml` to hold Fuel server configurations. \n"\ + " \n"\ + " gz fuel configure [options] \n"\ + " \n"\ + " --defaults Use all the defaults and save. \n"\ + " This will overwrite ~/.gz/fuel/config.yaml. \n"\ + " --console Output to the console instead of to a file. \n"\ + " -h [--help] Print this help message. \n"\ + " \n"\ + " --force-version Use a specific library version. \n"\ + " \n"\ + " --versions Show the available versions. \n", + 'delete' => "Delete simulation resources \n"\ " \n"\ @@ -208,7 +226,9 @@ class Cmd 'pbtxt2config' => '', 'private' => '', 'onlymodels' => '0', - 'onlyworlds' => '0' + 'onlyworlds' => '0', + 'defaults' => false, + 'console' => false } usage = COMMANDS[args[0]] @@ -276,6 +296,12 @@ class Cmd opts.on('--onlyworlds', 'Only update worlds') do options['onlyworlds'] = '1' end + opts.on('--defaults', 'Use default values') do + options['defaults'] = true + end + opts.on('--console', 'Output to console') do + options['console'] = true + end end # opt_parser do opt_parser.parse!(args) @@ -347,6 +373,12 @@ class Cmd end # parse() def execute(args) + # Graceful exit on ctrl-c + Signal.trap("SIGINT") do + puts "\nExiting" + exit + end + options = parse(args) # Read the plugin that handles the command. @@ -394,6 +426,8 @@ class Cmd end case options['subcommand'] + when 'configure' + configure(options['defaults'], options['console']) when 'delete' Importer.extern 'int deleteUrl(const char *, const char *)' if not Importer.deleteUrl(options['url'], options['header']) @@ -463,4 +497,118 @@ class Cmd "from #{plugin}." end # begin end # execute + + # Runs `gz fuel configure` + def configure(defaults, console) + # Default fuel directory and configuration path + local_fuel_dir = File.join(Dir.home, '.gz', 'fuel') + config_path = File.join(local_fuel_dir, 'config.yaml') + + # The default Fuel server URL + default_url = 'https://fuel.gazebosim.org' + + # A lambda function that prompts the user to enter Fuel server information + get_server_info = lambda { + server_url = default_url + default_name = 'Fuel' + + # Prompt the user for a server name with a default value + # Repeat until the URL is valid, or the user hits ctrl-c + until defaults + print "Fuel server URL [#{default_url}]: " + server_url = STDIN.gets.chomp + server_url = default_url if server_url.empty? + begin + uri = URI.parse(server_url) + default_name = !uri.host || uri.host.empty? ? uri.to_s : uri.host + break + rescue URI::InvalidURIError + puts 'Invalid URL.\n' + end + end + + # Prompt the user for an access token with a default value of an + # empty string + access_token = '' + unless defaults + print 'Optional access token [None]: ' + access_token = STDIN.gets.chomp + end + + # Get the cache location + cache = local_fuel_dir + unless defaults + print "Local cache path [#{local_fuel_dir}]: " + cache = STDIN.gets.chomp + cache = local_fuel_dir if cache.empty? + end + + # Get a name to associate with this server + server_name = default_name + unless defaults + print "Name this server [#{default_name}]: " + server_name = STDIN.gets.chomp + server_name = default_name if server_name.empty? + end + + [server_name, server_url, access_token, cache] + } + + unless console + puts '# Set Fuel server configurations.' + puts "# This will create or replace `#{config_path}`.\n\n" + end + + servers = [] + confirmation = 'n' + # Allow the user to enter multiple Fuel servers + begin + server_name, server_url, access_token, cache = get_server_info.call + servers << { 'name' => server_name, + 'url' => server_url, + 'private-token' => access_token, + 'cache' => { 'path' => cache } } + unless defaults + print "\nAdd another Fuel server? [y/N]:" + confirmation = STDIN.gets.chomp.downcase + confirmation = 'n' if confirmation.empty? + end + end while confirmation == 'y' + + unless defaults || console + puts "\nReview:\n" + servers.each do |server| + puts " Name: #{server['name']}" + puts " URL: #{server['url']}" + puts " Cache: #{server['cache']['path']}" + puts " Access token: #{server['private-token']}" + puts "\n" if servers.size > 1 + end + end + + confirmation = 'y' + unless defaults || console + print 'Save? [Y/n]:' + confirmation = STDIN.gets.chomp.downcase + confirmation = 'y' if confirmation.empty? + end + + # Save settings + if confirmation == 'y' + config = {'servers' => servers} + + if console + puts config.to_yaml + else + # Make sure the ~/.gz/fuel directory exists. + FileUtils.mkdir_p(local_fuel_dir) + + # Write to the config.yaml file + File.open(config_path, 'w') { |file| file.write(config.to_yaml) } + puts 'Settings saved to ~/.gz/fuel/config.yaml.' + end + else + puts 'Settings not saved.' + end + end end # class diff --git a/src/gz_TEST.cc b/src/gz_TEST.cc index 815fa7f6..12056e62 100644 --- a/src/gz_TEST.cc +++ b/src/gz_TEST.cc @@ -100,7 +100,8 @@ TEST(CmdLine, GZ_UTILS_TEST_ENABLED_ONLY_ON_LINUX(ListFail)) TEST(CmdLine, GZ_UTILS_TEST_ENABLED_ONLY_ON_LINUX(ModelListConfigServerUgly)) { - auto output = custom_exec_str(g_listCmd + " -t model --raw"); + auto output = custom_exec_str(g_listCmd + + " -t model --raw -u 'https://fuel.gazebosim.org/1.0' -o openrobotics"); EXPECT_NE(output.find("https://fuel.gazebosim.org/1.0/"), std::string::npos) << output; EXPECT_EQ(output.find("owners"), std::string::npos) << output; @@ -151,3 +152,19 @@ TEST(CmdLine, EXPECT_NE(output.find("owners"), std::string::npos) << output; EXPECT_NE(output.find("worlds"), std::string::npos) << output; } + +TEST(CmdLine, + GZ_UTILS_TEST_ENABLED_ONLY_ON_LINUX(ConfigureDefaultsConsole)) +{ + std::string output = custom_exec_str( + g_exec + " fuel configure --console --defaults"); + + std::string expected = + "---\n" + "servers:\n" + "- name: Fuel\n" + " url: https://fuel.gazebosim.org\n" + " private-token: ''\n"; + + EXPECT_EQ(output.find(expected), 0); +} diff --git a/src/gz_src_TEST.cc b/src/gz_src_TEST.cc index 37b28960..44883187 100644 --- a/src/gz_src_TEST.cc +++ b/src/gz_src_TEST.cc @@ -97,7 +97,8 @@ TEST_F(CmdLine, ModelListFail) // https://github.com/gazebosim/gz-fuel-tools/issues/105 TEST_F(CmdLine, ModelListConfigServerUgly) { - EXPECT_TRUE(listModels("", "openroboticstest", "true")); + EXPECT_TRUE(listModels("https://fuel.gazebosim.org", + "openroboticstest", "true")); EXPECT_NE(this->stdOutBuffer.str().find("https://fuel.gazebosim.org"), std::string::npos) << this->stdOutBuffer.str(); diff --git a/tutorials/02_configuration.md b/tutorials/02_configuration.md index c8e9038f..940eeeef 100644 --- a/tutorials/02_configuration.md +++ b/tutorials/02_configuration.md @@ -35,6 +35,17 @@ The `cache` section captures options related with the local storage of the assets. `path` specifies the local directory where all assets will be downloaded. If not used, all assets are stored under `$HOME/.gz/fuel`. +## Guided Configuration + +The `gz fuel configure` CLI will walk you through the process of creating a +`~/.gz/fuel/config.yaml` file. Just run the following command, and answer +the prompts. Note that this command will replace your existing `~/.gz/fuel/config.yaml` +if you choose to save on the last step. + +```bash +gz fuel configure +``` + ## Custom configuration file path Gazebo Fuel's default configuration file is stored under From 52c86bcef9b4d3e0d588a30f73d36cb8a4f06a80 Mon Sep 17 00:00:00 2001 From: Nate Koenig Date: Mon, 29 Apr 2024 11:13:43 -0700 Subject: [PATCH 4/4] Fix test Signed-off-by: Nate Koenig --- src/gz_TEST.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gz_TEST.cc b/src/gz_TEST.cc index edc6df92..eb6ab82b 100644 --- a/src/gz_TEST.cc +++ b/src/gz_TEST.cc @@ -101,8 +101,8 @@ TEST(CmdLine, GZ_UTILS_TEST_ENABLED_ONLY_ON_LINUX(ModelListConfigServerUgly)) { auto output = custom_exec_str(g_listCmd + - " -t model --raw -u 'https://fuel.gazebosim.org/1.0' -o openrobotics"); - EXPECT_NE(output.find("https://fuel.gazebosim.org/1.0/"), + " -t model --raw -u 'https://fuel.gazebosim.org' -o openrobotics"); + EXPECT_NE(output.find("https://fuel.gazebosim.org"), std::string::npos) << output; EXPECT_EQ(output.find("owners"), std::string::npos) << output; }