1 /*
2   Copyright (c) DataStax, Inc.
3 
4   Licensed under the Apache License, Version 2.0 (the "License");
5   you may not use this file except in compliance with the License.
6   You may obtain a copy of the License at
7 
8   http://www.apache.org/licenses/LICENSE-2.0
9 
10   Unless required by applicable law or agreed to in writing, software
11   distributed under the License is distributed on an "AS IS" BASIS,
12   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   See the License for the specific language governing permissions and
14   limitations under the License.
15 */
16 
17 #ifndef __TEST_EMBEDDED_ADS_HPP__
18 #define __TEST_EMBEDDED_ADS_HPP__
19 #include "exception.hpp"
20 #include "options.hpp"
21 #include "test_utils.hpp"
22 #include "tlog.hpp"
23 
24 #include "scoped_lock.hpp"
25 
26 #include <string>
27 #ifdef _WIN32
28 #define putenv _putenv
29 #endif
30 
31 #include <uv.h>
32 
33 // TODO: This should be broken out in the future if required by more than one test (currently
34 // Authentication tests)
35 
36 // Defines for ADS configuration
37 #define EMBEDDED_ADS_JAR_FILENAME "embedded-ads.jar"
38 #define EMBEDDED_ADS_CONFIGURATION_DIRECTORY "ads_config"
39 #define EMBEDDED_ADS_CONFIGURATION_FILE "krb5.conf"
40 #define CASSANDRA_KEYTAB_ADS_CONFIGURATION_FILE "cassandra.keytab"
41 #define DSE_KEYTAB_ADS_CONFIGURATION_FILE "dse.keytab"
42 #define DSE_USER_KEYTAB_ADS_CONFIGURATION_FILE "dseuser.keytab"
43 #define UNKNOWN_KEYTAB_ADS_CONFIGURATION_FILE "unknown.keytab"
44 #define BILL_KEYTAB_ADS_CONFIGURATION_FILE "bill.keytab"
45 #define BOB_KEYTAB_ADS_CONFIGURATION_FILE "bob.keytab"
46 #define CHARLIE_KEYTAB_ADS_CONFIGURATION_FILE "charlie.keytab"
47 #define STEVE_KEYTAB_ADS_CONFIGURATION_FILE "steve.keytab"
48 #define REALM "DATASTAX.COM"
49 #define DSE_SERVICE_PRINCIPAL "dse/_HOST@DATASTAX.COM"
50 #define CASSANDRA_USER "cassandra"
51 #define CASSANDRA_PASSWORD "cassandra"
52 #define CASSANDRA_USER_PRINCIPAL "cassandra@DATASTAX.COM"
53 #define DSE_USER "dseuser"
54 #define DSE_USER_PRINCIPAL "dseuser@DATASTAX.COM"
55 #define UNKNOWN "unknown"
56 #define UNKNOWN_PRINCIPAL "unknown@DATASTAX.COM"
57 #define BILL_PRINCIPAL "bill@DATASTAX.COM"
58 #define BOB_PRINCIPAL "bob@DATASTAX.COM"
59 #define CHARLIE_PRINCIPAL "charlie@DATASTAX.COM"
60 #define STEVE_PRINCIPAL "steve@DATASTAX.COM"
61 
62 // Output buffer size for spawn pipe(s)
63 #define OUTPUT_BUFFER_SIZE 10240
64 
65 namespace test {
66 
67 /**
68  * Embedded ADS for easily authenticating with DSE using Kerberos
69  */
70 class EmbeddedADS {
71   /**
72    * Result for command execution
73    */
74   typedef struct CommandResult_ {
75     /**
76      * Error code (e.g. exit status)
77      */
78     int error_code;
79     /**
80      * Standard output from executing command
81      */
82     std::string standard_output;
83     /**
84      * Standard error from executing command
85      */
86     std::string standard_error;
87 
CommandResult_test::EmbeddedADS::CommandResult_88     CommandResult_()
89         : error_code(-1) {}
90   } CommandResult;
91 
92 public:
93   /**
94    * @throws EmbeddedADS::Exception If applications are not available to operate the ADS
95    *                        properly
96    */
EmbeddedADS()97   EmbeddedADS() {
98     // TODO: Update test to work with remote deployments
99 #ifdef _WIN32
100     // Unable to execute ADS locally and use remote DSE cluster
101     throw Exception("ADS Server will not be Created: Must run locally with DSE cluster");
102 #endif
103 #ifdef CASS_USE_LIBSSH2
104     if (Options::deployment_type() == CCM::DeploymentType::REMOTE) {
105       throw Exception("ADS Server will not be Created: Must run locally with DSE cluster");
106     }
107 #endif
108 
109     // Initialize the mutex
110     uv_mutex_init(&mutex_);
111 
112     // Check to see if all applications and files are available for ADS
113     bool is_useable = true;
114     std::string message;
115     if (!is_java_available()) {
116       is_useable = false;
117       message += "Java";
118     }
119     if (!is_kerberos_client_available()) {
120       is_useable = false;
121       if (!message.empty()) {
122         message += " and ";
123       }
124       message += "Kerberos clients (kinit/kdestroy)";
125     }
126     if (!Utils::file_exists(EMBEDDED_ADS_JAR_FILENAME)) {
127       is_useable = false;
128       if (!message.empty()) {
129         message += " and ";
130       }
131       message += "embedded ADS JAR file";
132     }
133 
134     if (!is_useable) {
135       message = "Unable to Create ADS Server: Missing " + message;
136       throw Exception(message);
137     }
138   }
139 
~EmbeddedADS()140   ~EmbeddedADS() {
141     terminate_process();
142     uv_mutex_destroy(&mutex_);
143   }
144 
145   /**
146    * Start the ADS process
147    */
start_process()148   void start_process() { uv_thread_create(&thread_, EmbeddedADS::process_start, NULL); }
149 
150   /**
151    * Terminate the ADS process
152    */
terminate_process()153   void terminate_process() {
154     uv_process_kill(&process_, SIGTERM);
155     uv_thread_join(&thread_);
156 
157     // Reset the static variables
158     configuration_directory_ = "";
159     configuration_file_ = "";
160     cassandra_keytab_file_ = "";
161     dse_keytab_file_ = "";
162     dseuser_keytab_file_ = "";
163     unknown_keytab_file_ = "";
164     bill_keytab_file_ = "";
165     bob_keytab_file_ = "";
166     charlie_keytab_file_ = "";
167     steve_keytab_file_ = "";
168     is_initialized_ = false;
169   }
170 
171   /**
172    * Flag to determine if the ADS process is fully initialized
173    *
174    * @return True is ADS is initialized; false otherwise
175    */
is_initialized()176   static bool is_initialized() {
177     datastax::internal::ScopedMutex lock(&mutex_);
178     return is_initialized_;
179   }
180 
181   /**
182    * Get the configuration director being used by the ADS process
183    *
184    * @return Absolute path to the ADS configuration directory; empty string
185    *         indicates ADS was not started properly
186    */
get_configuration_directory()187   static std::string get_configuration_directory() { return configuration_directory_; }
188 
189   /**
190    * Get the configuration file being used by the ADS process
191    *
192    * @return Absolute path to the ADS configuration file; empty string indicates
193    *         ADS was not started properly
194    */
get_configuration_file()195   static std::string get_configuration_file() { return configuration_file_; }
196 
197   /**
198    * Get the Cassandra keytab configuration file being used by the ADS process
199    *
200    * @return Absolute path to the Cassandra keytab configuration file; empty
201    *         string indicates ADS was not started properly
202    */
get_cassandra_keytab_file()203   static std::string get_cassandra_keytab_file() { return cassandra_keytab_file_; }
204 
205   /**
206    * Get the DSE keytab configuration file being used by the ADS process
207    *
208    * @return Absolute path to the DSE keytab configuration file; empty
209    *         string indicates ADS was not started properly
210    */
get_dse_keytab_file()211   static std::string get_dse_keytab_file() { return dse_keytab_file_; }
212 
213   /**
214    * Get the DSE user keytab configuration file being used by the ADS process
215    *
216    * @return Absolute path to the DSE user keytab configuration file; empty
217    *         string indicates ADS was not started properly
218    */
get_dseuser_keytab_file()219   static std::string get_dseuser_keytab_file() { return dseuser_keytab_file_; }
220 
221   /**
222    * Get the unknown keytab configuration file being used by the ADS process
223    *
224    * @return Absolute path to the unknown keytab configuration file; empty
225    *         string indicates ADS was not started properly
226    */
get_unknown_keytab_file()227   static std::string get_unknown_keytab_file() { return unknown_keytab_file_; }
228 
229   /**
230    * Get the Bill keytab configuration file being used by the ADS process
231    *
232    * @return Absolute path to the Bill keytab configuration file; empty string
233    *         indicates ADS was not started properly
234    */
get_bill_keytab_file()235   static std::string get_bill_keytab_file() { return bill_keytab_file_; }
236 
237   /**
238    * Get the Bob keytab configuration file being used by the ADS process
239    *
240    * @return Absolute path to the Bob keytab configuration file; empty string
241    *         indicates ADS was not started properly
242    */
get_bob_keytab_file()243   static std::string get_bob_keytab_file() { return bob_keytab_file_; }
244 
245   /**
246    * Get the Charlie keytab configuration file being used by the ADS process
247    *
248    * @return Absolute path to the Charlie keytab configuration file; empty
249    *         string indicates ADS was not started properly
250    */
get_charlie_keytab_file()251   static std::string get_charlie_keytab_file() { return charlie_keytab_file_; }
252 
253   /**
254    * Get the Steve keytab configuration file being used by the ADS process
255    *
256    * @return Absolute path to the Steve keytab configuration file; empty string
257    *         string indicates ADS was not started properly
258    */
get_steve_keytab_file()259   static std::string get_steve_keytab_file() { return steve_keytab_file_; }
260 
261   /**
262    * Check to see if the Kerberos client binaries are Heimdal
263    *
264    * @return True if Kerberos implementation is Heimdal; false otherwise
265    */
is_kerberos_client_heimdal()266   static bool is_kerberos_client_heimdal() {
267     if (is_kerberos_client_available()) {
268       // kinit
269       char* kinit_args[3];
270       kinit_args[0] = const_cast<char*>("kinit");
271       kinit_args[1] = const_cast<char*>("--version");
272       kinit_args[2] = NULL;
273 
274       // Check the output of the kinit command for Heimdal
275       CommandResult result = execute_command(kinit_args);
276       if (result.error_code == 0) {
277         // Check both outputs
278         bool is_in_standard_output = Utils::contains(result.standard_output, "Heimdal");
279         bool is_in_standard_error = Utils::contains(result.standard_error, "Heimdal");
280         return (is_in_standard_output || is_in_standard_error);
281       }
282     }
283 
284     return false;
285   }
286 
287   /**
288    * Acquire a ticket into the cache of the ADS for a given principal and keytab
289    * file
290    *
291    * @param principal Principal identity
292    * @param keytab_file Filename of keytab to use
293    */
acquire_ticket(const std::string & principal,const std::string & keytab_file)294   void acquire_ticket(const std::string& principal, const std::string& keytab_file) {
295     char* args[6];
296     args[0] = const_cast<char*>("kinit");
297     args[1] = const_cast<char*>("-k");
298     args[2] = const_cast<char*>("-t");
299     args[3] = const_cast<char*>(keytab_file.c_str());
300     args[4] = const_cast<char*>(principal.c_str());
301     args[5] = NULL;
302     execute_command(args);
303   }
304 
305   /**
306    * Destroy all tickets in the cache
307    */
destroy_tickets()308   void destroy_tickets() {
309     char* args[3];
310     args[0] = const_cast<char*>("kdestroy");
311     args[1] = const_cast<char*>("-A");
312     args[2] = NULL;
313     execute_command(args);
314   }
315 
316   /**
317    * Assign the Kerberos environment for keytab use
318    *
319    * @param keytab_file Filename of keytab to use
320    */
use_keytab(const std::string & keytab_file)321   void use_keytab(const std::string& keytab_file) {
322     // MIT Kerberos
323     setenv("KRB5_CLIENT_KTNAME", keytab_file);
324     // Heimdal
325     setenv("KRB5_KTNAME", keytab_file);
326   }
327 
328   /**
329    * Clear/Unassign the Kerberos environment for keytab use
330    */
clear_keytab()331   void clear_keytab() {
332     // MIT Kerberos
333     setenv("KRB5_CLIENT_KTNAME", "");
334     // Heimdal
335     setenv("KRB5_KTNAME", "");
336   }
337 
338 private:
339   /**
340    * Thread for the ADS process to execute in
341    */
342   uv_thread_t thread_;
343   /**
344    * Mutex for process piped buffer allocation and reads
345    */
346   static uv_mutex_t mutex_;
347   /**
348    * Information regarding spawned process
349    */
350   static uv_process_t process_;
351   /**
352    * ADS configuration directory
353    */
354   static std::string configuration_directory_;
355   /**
356    * KRB5_CONFIG configuration file
357    */
358   static std::string configuration_file_;
359   /**
360    * Cassandra keytab configuration file
361    */
362   static std::string cassandra_keytab_file_;
363   /**
364    * DSE keytab configuration file
365    */
366   static std::string dse_keytab_file_;
367   /**
368    * DSE user keytab configuration file
369    */
370   static std::string dseuser_keytab_file_;
371   /**
372    * Unknown keytab configuration file
373    */
374   static std::string unknown_keytab_file_;
375   /**
376    * Bill keytab configuration file
377    */
378   static std::string bill_keytab_file_;
379   /**
380    * Bob keytab configuration file
381    */
382   static std::string bob_keytab_file_;
383   /**
384    * Charlie keytab configuration file
385    */
386   static std::string charlie_keytab_file_;
387   /**
388    * Steve keytab configuration file
389    */
390   static std::string steve_keytab_file_;
391   /**
392    * Flag to determine if the ADS process is initialized
393    */
394   static bool is_initialized_;
395 
396   /**
397    * Execute a command while supplying the KRB5_CONFIG to the ADS server
398    * configuration file
399    *
400    * @param Process and arguments to execute
401    * @return Error code returned from executing command
402    */
execute_command(char * args[])403   static CommandResult execute_command(char* args[]) {
404     // Create the loop
405     uv_loop_t loop;
406     uv_loop_init(&loop);
407     uv_process_options_t options;
408     memset(&options, 0, sizeof(uv_process_options_t));
409 
410     // Create the options for reading information from the spawn pipes
411     uv_pipe_t standard_output;
412     uv_pipe_t error_output;
413     uv_pipe_init(&loop, &standard_output, 0);
414     uv_pipe_init(&loop, &error_output, 0);
415     uv_stdio_container_t stdio[3];
416     options.stdio_count = 3;
417     options.stdio = stdio;
418     options.stdio[0].flags = UV_IGNORE;
419     options.stdio[1].flags = static_cast<uv_stdio_flags>(UV_CREATE_PIPE | UV_WRITABLE_PIPE);
420     options.stdio[1].data.stream = (uv_stream_t*)&standard_output;
421     options.stdio[2].flags = static_cast<uv_stdio_flags>(UV_CREATE_PIPE | UV_WRITABLE_PIPE);
422     options.stdio[2].data.stream = (uv_stream_t*)&error_output;
423 
424     // Create the options for the process
425     options.args = args;
426     options.exit_cb = EmbeddedADS::process_exit;
427     options.file = args[0];
428 
429     // Start the process and process loop (if spawned)
430     CommandResult result;
431     uv_process_t process;
432     result.error_code = uv_spawn(&loop, &process, &options);
433     if (result.error_code == 0) {
434       TEST_LOG("Launched " << args[0] << " with ID " << process_.pid);
435 
436       // Configure the storage for the output pipes
437       std::string stdout_message;
438       std::string stderr_message;
439       standard_output.data = &result.standard_output;
440       error_output.data = &result.standard_error;
441 
442       // Start the output thread loops
443       uv_read_start(reinterpret_cast<uv_stream_t*>(&standard_output),
444                     EmbeddedADS::output_allocation, EmbeddedADS::process_read);
445       uv_read_start(reinterpret_cast<uv_stream_t*>(&error_output), EmbeddedADS::output_allocation,
446                     EmbeddedADS::process_read);
447 
448       // Start the process loop
449       uv_run(&loop, UV_RUN_DEFAULT);
450       uv_loop_close(&loop);
451     }
452     return result;
453   }
454 
455   /**
456    * Check to see if Java is available in order to execute the ADS process
457    *
458    * @return True if Java is available; false otherwise
459    */
is_java_available()460   static bool is_java_available() {
461     char* args[3];
462     args[0] = const_cast<char*>("java");
463     args[1] = const_cast<char*>("-help");
464     args[2] = NULL;
465     return (execute_command(args).error_code == 0);
466   }
467 
468   /**
469    * Check to see if the Kerberos client binaries are available in order to
470    * properly execute request for the ADS
471    *
472    * @return True if kinit and kdestroy are available; false otherwise
473    */
is_kerberos_client_available()474   static bool is_kerberos_client_available() {
475     // kinit
476     char* kinit_args[3];
477     kinit_args[0] = const_cast<char*>("kinit");
478     kinit_args[1] = const_cast<char*>("--help");
479     kinit_args[2] = NULL;
480     bool is_kinit_available = (execute_command(kinit_args).error_code == 0);
481 
482     // kdestroy
483     char* kdestroy_args[3];
484     kdestroy_args[0] = const_cast<char*>("kdestroy");
485     kdestroy_args[1] = const_cast<char*>("--help");
486     kdestroy_args[2] = NULL;
487     bool is_kdestroy_available = (execute_command(kdestroy_args).error_code == 0);
488 
489     return (is_kinit_available && is_kdestroy_available);
490   }
491 
492   /**
493    * uv_thread_create callback for executing the ADS process
494    *
495    * @param arg UNUSED
496    */
process_start(void * arg)497   static void process_start(void* arg) {
498     // Create the configuration directory for the ADS
499     Utils::mkdir(EMBEDDED_ADS_CONFIGURATION_DIRECTORY);
500 
501     // Initialize the loop and process arguments
502     uv_loop_t loop;
503     uv_loop_init(&loop);
504     uv_process_options_t options;
505     memset(&options, 0, sizeof(uv_process_options_t));
506 
507     char* args[7];
508     args[0] = const_cast<char*>("java");
509     args[1] = const_cast<char*>("-jar");
510     args[2] = const_cast<char*>(EMBEDDED_ADS_JAR_FILENAME);
511     args[3] = const_cast<char*>("-k");
512     args[4] = const_cast<char*>("--confdir");
513     args[5] = const_cast<char*>(EMBEDDED_ADS_CONFIGURATION_DIRECTORY);
514     args[6] = NULL;
515 
516     // Create the options for reading information from the spawn pipes
517     uv_pipe_t standard_output;
518     uv_pipe_t error_output;
519     uv_pipe_init(&loop, &standard_output, 0);
520     uv_pipe_init(&loop, &error_output, 0);
521     uv_stdio_container_t stdio[3];
522     options.stdio_count = 3;
523     options.stdio = stdio;
524     options.stdio[0].flags = UV_IGNORE;
525     options.stdio[1].flags = static_cast<uv_stdio_flags>(UV_CREATE_PIPE | UV_WRITABLE_PIPE);
526     options.stdio[1].data.stream = (uv_stream_t*)&standard_output;
527     options.stdio[2].flags = static_cast<uv_stdio_flags>(UV_CREATE_PIPE | UV_WRITABLE_PIPE);
528     options.stdio[2].data.stream = (uv_stream_t*)&error_output;
529 
530     // Create the options for the process
531     options.args = args;
532     options.exit_cb = EmbeddedADS::process_exit;
533     options.file = args[0];
534 
535     // Start the process
536     int error_code = uv_spawn(&loop, &process_, &options);
537     if (error_code == 0) {
538       TEST_LOG("Launched " << args[0] << " with ID " << process_.pid);
539 
540       // Configure the storage for the output pipes
541       std::string stdout_message;
542       std::string stderr_message;
543       standard_output.data = &stdout_message;
544       error_output.data = &stderr_message;
545 
546       // Start the output thread loops
547       uv_read_start(reinterpret_cast<uv_stream_t*>(&standard_output),
548                     EmbeddedADS::output_allocation, EmbeddedADS::process_read);
549       uv_read_start(reinterpret_cast<uv_stream_t*>(&error_output), EmbeddedADS::output_allocation,
550                     EmbeddedADS::process_read);
551 
552       // Indicate the ADS configurations
553       configuration_directory_ = Utils::cwd() + Utils::PATH_SEPARATOR +
554                                  EMBEDDED_ADS_CONFIGURATION_DIRECTORY + Utils::PATH_SEPARATOR;
555       configuration_file_ = configuration_directory_ + EMBEDDED_ADS_CONFIGURATION_FILE;
556       cassandra_keytab_file_ = configuration_directory_ + CASSANDRA_KEYTAB_ADS_CONFIGURATION_FILE;
557       dse_keytab_file_ = configuration_directory_ + DSE_KEYTAB_ADS_CONFIGURATION_FILE;
558       dseuser_keytab_file_ = configuration_directory_ + DSE_USER_KEYTAB_ADS_CONFIGURATION_FILE;
559       unknown_keytab_file_ = configuration_directory_ + UNKNOWN_KEYTAB_ADS_CONFIGURATION_FILE;
560       bill_keytab_file_ = configuration_directory_ + BILL_KEYTAB_ADS_CONFIGURATION_FILE;
561       bob_keytab_file_ = configuration_directory_ + BOB_KEYTAB_ADS_CONFIGURATION_FILE;
562       charlie_keytab_file_ = configuration_directory_ + CHARLIE_KEYTAB_ADS_CONFIGURATION_FILE;
563       steve_keytab_file_ = configuration_directory_ + STEVE_KEYTAB_ADS_CONFIGURATION_FILE;
564 
565       // Inject the configuration environment variable
566       setenv("KRB5_CONFIG", configuration_file_);
567 
568       // Start the process loop
569       uv_run(&loop, UV_RUN_DEFAULT);
570       uv_loop_close(&loop);
571     } else {
572       TEST_LOG_ERROR(uv_strerror(error_code));
573     }
574   }
575 
576   /**
577    * uv_spawn callback for handling the completion of the process
578    *
579    * @param process Process
580    * @param error_code Error/Exit code
581    * @param term_signal Terminating signal
582    */
process_exit(uv_process_t * process,int64_t error_code,int term_signal)583   static void process_exit(uv_process_t* process, int64_t error_code, int term_signal) {
584     datastax::internal::ScopedMutex lock(&mutex_);
585     TEST_LOG("Process " << process->pid << " Terminated: " << error_code);
586     uv_close(reinterpret_cast<uv_handle_t*>(process), NULL);
587   }
588 
589   /**
590    * uv_read_start callback for allocating memory for the buffer in the pipe
591    *
592    * @param handle Handle information for the pipe being read
593    * @param suggested_size Suggested size for the buffer
594    * @param buffer Buffer to allocate bytes for
595    */
output_allocation(uv_handle_t * handle,size_t suggested_size,uv_buf_t * buffer)596   static void output_allocation(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buffer) {
597     datastax::internal::ScopedMutex lock(&mutex_);
598     buffer->base = new char[OUTPUT_BUFFER_SIZE];
599     buffer->len = OUTPUT_BUFFER_SIZE;
600   }
601 
602   /**
603    * uv_read_start callback for processing the buffer in the pipe
604    *
605    * @param stream Stream to process (stdout/stderr)
606    * @param buffer_length Length of the buffer
607    * @param buffer Buffer to process
608    */
process_read(uv_stream_t * stream,ssize_t buffer_length,const uv_buf_t * buffer)609   static void process_read(uv_stream_t* stream, ssize_t buffer_length, const uv_buf_t* buffer) {
610     datastax::internal::ScopedMutex lock(&mutex_);
611 
612     // Get the pipe message contents
613     std::string* message = reinterpret_cast<std::string*>(stream->data);
614 
615     if (buffer_length > 0) {
616       // Process the buffer and determine if the ADS is finished initializing
617       std::string output(buffer->base, buffer_length);
618       message->append(output);
619 
620       if (!is_initialized_ &&
621           message->find("Principal Initialization Complete") != std::string::npos) {
622         Utils::msleep(10000); // TODO: Not 100% ready; need to add a better check mechanism
623         is_initialized_ = true;
624       }
625       TEST_LOG(Utils::trim(output));
626     } else if (buffer_length < 0) {
627       uv_close(reinterpret_cast<uv_handle_t*>(stream), NULL);
628     }
629 
630     // Clean up the memory allocated
631     delete[] buffer->base;
632   }
633 
setenv(const std::string & name,const std::string & value)634   static void setenv(const std::string& name, const std::string& value) {
635 #ifdef _WIN32
636     putenv(const_cast<char*>(std::string(name + "=" + value).c_str()));
637 #else
638     ::setenv(name.c_str(), value.c_str(), 1);
639 #endif
640   }
641 };
642 
643 } // namespace test
644 
645 #endif // __TEST_EMBEDDED_ADS_HPP__
646