Skip to content

Commit e3692ce

Browse files
authored
Merge pull request #23 from woodcoder/sdk-cli
Support calling Carbon Aware SDK CLI directly
2 parents c6e3d8d + 1dd72a2 commit e3692ce

File tree

3 files changed

+153
-30
lines changed

3 files changed

+153
-30
lines changed

README.md

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ Circa is a project for [Carbon Hack 22](https://taikai.network/en/gsf/hackathons
4949
<dt><b>-d</b> &lt;duration&gt;</dt>
5050
<dd>estimated window of runtime of command/task in minutes</dd>
5151
<dt><b>-u</b> &lt;api url&gt;</dt>
52-
<dd>url prefix of Carbon Aware API server to consult</dd>
52+
<dd>
53+
url prefix of Carbon Aware API server to consult OR<br>
54+
full path to Carbon Aware CLI executable
55+
</dd>
5356
</dl>
5457

5558
### Configuration
@@ -173,8 +176,60 @@ make
173176
### Building on Ubuntu
174177

175178
```
176-
sudo apt-get install -y build-essential libjansson-dev libcurl4-openssl-dev
179+
sudo apt-get install -y build-essential libcurl4-openssl-dev libjansson-dev
177180
autoreconf -fi
178181
./configure
179182
make
180183
```
184+
185+
### Building on Fedora
186+
187+
```
188+
sudo dnf -y install autoconf automake curl-devel jansson-devel
189+
autoreconf -fi
190+
./configure
191+
make
192+
```
193+
194+
### Carbon Aware CLI
195+
196+
To install the SDK CLI you will first need the .NET SDK
197+
198+
* macOS - download and install the (macOS .NET SDK Installer)[https://dotnet.microsoft.com/en-us/download/dotnet/6.0].
199+
* Ubuntu - `sudo apt-get install -y dotnet-sdk-6.0`
200+
* Fedora - `sudo yum -y install dotnet`
201+
202+
Then you will need the *new* CLI redesign
203+
(pull request)[https://github.com/Green-Software-Foundation/carbon-aware-sdk/pull/158] branch:
204+
```
205+
curl -LO https://github.com/microsoft/carbon-aware-sdk/archive/refs/heads/162/cli-redesign.tar.gz
206+
tar xf cli-redesign.tar.gz
207+
cd carbon-aware-sdk-162-cli-redesign/src/CarbonAware.CLI/src
208+
```
209+
210+
Update the `appsettings.json`, for example, with your WattTime credentials:
211+
```
212+
vi appsettings.json
213+
{
214+
"Logging": {
215+
"LogLevel": {
216+
"Default": "Information",
217+
"Microsoft.AspNetCore": "Warning"
218+
}
219+
},
220+
"AllowedHosts": "*",
221+
"carbonAwareVars": {
222+
"carbonIntensityDataSource": "WattTime"
223+
},
224+
"wattTimeClient": {
225+
"username": "<watttime username>",
226+
"password": "<watttime password>"
227+
}
228+
}
229+
```
230+
231+
Build a release and test it:
232+
```
233+
dotnet publish -c Release
234+
bin/Release/net6.0/publish/caw emissions -l eastus
235+
```

circa.c

Lines changed: 92 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <stdio.h>
66
#include <stdlib.h>
77
#include <string.h>
8+
#include <sys/stat.h>
89
#include <unistd.h>
910

1011
#include <curl/curl.h>
@@ -16,7 +17,13 @@
1617
"%s/emissions/forecasts/current?location=%s&dataEndAt=%s&windowSize=%d"
1718
#define URL_SIZE 256
1819

20+
#define ISO8601_CLI_FORMAT "%4d-%02d-%02dT%02d:%02d:%02dZ"
21+
#define ISO8601_CLI_SIZE 22
22+
#define CLI_FORMAT "%s emissions-forecasts -l %s -e %s -w %d"
23+
#define CLI_SIZE 256
24+
1925
typedef struct Params_t {
26+
bool cli;
2027
char *url;
2128
char *location;
2229
int window;
@@ -74,6 +81,7 @@ void home_path(char *file, char *path) {
7481

7582
void default_args(params_t *params) {
7683
// copy strings so they can be freed if overridden by config files
84+
params->cli = false;
7785
params->url = strdup("https://carbon-aware-api.azurewebsites.net");
7886
params->location = strdup("eastus");
7987
params->window = 30;
@@ -179,6 +187,14 @@ bool parse_args(int argc, char *argv[], params_t *params) {
179187
}
180188
i++;
181189

190+
// check to see if URL is path to carbon aware cli executable
191+
if (params->url[0] != 'h') {
192+
struct stat sb;
193+
if (stat(params->url, &sb) == 0 && sb.st_mode & S_IXUSR) {
194+
params->cli = true;
195+
}
196+
}
197+
182198
// anything else is the optional command and its params
183199
params->command = i;
184200
return true;
@@ -196,34 +212,36 @@ the program will just block until the best time.\n\n",
196212
printf("OPTIONS:\n\
197213
-l <location> specify location to check for carbon intensity\n\
198214
-d <duration> estimated window of runtime of command/task in minutes\n\
199-
-u <api url> url prefix of Carbon Aware API server to consult\n");
215+
-u <api url> url prefix of Carbon Aware API server to consult OR\n\
216+
full path to Carbon Aware CLI executable\n");
200217
}
201218

202-
void format_url(params_t *params, char *url) {
219+
void format_params(params_t *params, size_t iso8601_size, char *iso8601_format,
220+
size_t len, char *format, char *param_str) {
203221
time_t now;
204222
struct tm *time_tm;
205-
char end[ISO8601_URL_SIZE];
223+
char end[iso8601_size];
206224
int size;
207225

208226
time(&now);
209227
// add time this way isn't necessarily portable!
210228
now = now + params->hours * 60 * 60;
211229
time_tm = gmtime(&now);
212-
size = snprintf(end, ISO8601_URL_SIZE, ISO8601_URL_FORMAT,
213-
time_tm->tm_year + 1900, // years from 1900
214-
time_tm->tm_mon + 1, // 0-based
215-
time_tm->tm_mday, // 1-based
216-
time_tm->tm_hour, time_tm->tm_min, 0);
230+
size = snprintf(end, iso8601_size, iso8601_format,
231+
time_tm->tm_year + 1900, // years from 1900
232+
time_tm->tm_mon + 1, // 0-based
233+
time_tm->tm_mday, // 1-based
234+
time_tm->tm_hour, time_tm->tm_min, 0);
217235

218-
if (size > ISO8601_URL_SIZE) {
236+
if (size > iso8601_size) {
219237
// date string was truncated which should never happen
220238
fprintf(stderr, "Warning: end date corrupted (iso8601 date truncated)\n");
221239
}
222240

223241
printf("Requesting %d min duration window before %02d:%02d UTC in %s\n",
224242
params->window, time_tm->tm_hour, time_tm->tm_min, params->location);
225243

226-
snprintf(url, URL_SIZE, URL_FORMAT, params->url, params->location, end,
244+
snprintf(param_str, len, format, params->url, params->location, end,
227245
params->window);
228246
}
229247

@@ -240,7 +258,7 @@ static size_t write_response(void *contents, size_t size, size_t nmemb,
240258
char *ptr = realloc(mem->text, mem->size + realsize + 1);
241259
if (!ptr) {
242260
// out of memory!
243-
printf("not enough memory (realloc returned NULL)\n");
261+
fprintf(stderr, "not enough memory (realloc returned NULL)\n");
244262
return 0;
245263
}
246264

@@ -252,7 +270,8 @@ static size_t write_response(void *contents, size_t size, size_t nmemb,
252270
return realsize;
253271
}
254272

255-
void call_api(char *url, void (*extra_data)(response_t *, void *), void *data) {
273+
void call_api(char *url, void (*extract_data)(response_t *, void *),
274+
void *data) {
256275
CURL *curl;
257276
CURLcode res;
258277
response_t response;
@@ -273,7 +292,7 @@ void call_api(char *url, void (*extra_data)(response_t *, void *), void *data) {
273292
fprintf(stderr, "curl_easy_perform() failed: %s\n",
274293
curl_easy_strerror(res));
275294
} else {
276-
extra_data(&response, data);
295+
extract_data(&response, data);
277296
}
278297
curl_easy_cleanup(curl);
279298
}
@@ -283,12 +302,43 @@ void call_api(char *url, void (*extra_data)(response_t *, void *), void *data) {
283302
curl_global_cleanup();
284303
}
285304

305+
void call_cli(char *cmd, void (*extract_data)(response_t *, void *),
306+
void *data) {
307+
const size_t BUFSIZE = 255;
308+
response_t response;
309+
310+
response.text = malloc(1); // will be grown as needed by the realloc below
311+
response.size = 0; // no data at this point
312+
313+
char buf[BUFSIZE];
314+
FILE *fp;
315+
316+
if ((fp = popen(cmd, "r")) == NULL) {
317+
fprintf(stderr, "Error running CLI command\n");
318+
return;
319+
}
320+
321+
while (fgets(buf, BUFSIZE, fp) != NULL) {
322+
size_t read = strnlen(buf, BUFSIZE);
323+
write_response(buf, sizeof(char), read, &response);
324+
}
325+
326+
if (pclose(fp)) {
327+
fprintf(stderr, "Command not found or exited with error status\n");
328+
return;
329+
}
330+
331+
extract_data(&response, data);
332+
333+
free(response.text);
334+
}
335+
286336
void parse_response(response_t *response, void *wait_seconds) {
287337
size_t i;
288338
json_t *root;
289339
json_error_t error;
290340

291-
printf("Carbon Aware API returned %lu bytes\n",
341+
printf("Carbon Aware SDK returned %lu bytes\n",
292342
(unsigned long)response->size);
293343
root = json_loads(response->text, 0, &error);
294344

@@ -317,9 +367,13 @@ void parse_response(response_t *response, void *wait_seconds) {
317367

318368
optimals = json_object_get(data, "optimalDataPoints");
319369
if (!json_is_array(optimals)) {
320-
fprintf(stderr, "error: optimalDataPoints is not an array\n");
321-
json_decref(root);
322-
return;
370+
// might be cli and in capitals
371+
optimals = json_object_get(data, "OptimalDataPoints");
372+
if (!json_is_array(optimals)) {
373+
fprintf(stderr, "error: optimalDataPoints is not an array\n");
374+
json_decref(root);
375+
return;
376+
}
323377
}
324378

325379
// just look at the first for now
@@ -335,12 +389,16 @@ void parse_response(response_t *response, void *wait_seconds) {
335389

336390
timestamp = json_object_get(optimal, "timestamp");
337391
if (!json_is_string(timestamp)) {
338-
fprintf(stderr,
339-
"error: forecast %d optimalDataPoints %d timestamp is not a "
340-
"string\n",
341-
(int)i, (int)0);
342-
json_decref(root);
343-
return;
392+
// might be cli with different name
393+
timestamp = json_object_get(optimal, "Time");
394+
if (!json_is_string(timestamp)) {
395+
fprintf(stderr,
396+
"error: forecast %d optimalDataPoints %d timestamp is not a "
397+
"string\n",
398+
(int)i, (int)0);
399+
json_decref(root);
400+
return;
401+
}
344402
}
345403

346404
timestamp_text = json_string_value(timestamp);
@@ -380,11 +438,18 @@ int main(int argc, char *argv[]) {
380438
return 1;
381439
}
382440

383-
char url[URL_SIZE];
384-
format_url(&params, url);
385-
386441
double wait_seconds = -DBL_MAX;
387-
call_api(url, parse_response, &wait_seconds);
442+
if (params.cli) {
443+
char cli[CLI_SIZE];
444+
format_params(&params, ISO8601_CLI_SIZE, ISO8601_CLI_FORMAT, CLI_SIZE,
445+
CLI_FORMAT, cli);
446+
call_cli(cli, parse_response, &wait_seconds);
447+
} else {
448+
char url[URL_SIZE];
449+
format_params(&params, ISO8601_URL_SIZE, ISO8601_URL_FORMAT, URL_SIZE,
450+
URL_FORMAT, url);
451+
call_api(url, parse_response, &wait_seconds);
452+
}
388453

389454
if (wait_seconds > 5 * 60) {
390455
printf("Sleeping for %.2f minutes\n", wait_seconds / 60);

circa.conf

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
# `/etc/circa.conf` or `~/.circa/config`. The format is key value pairs
66
# separated by a single space.
77

8-
# Url to API endpoint, without trailing slash
8+
# Url to API endpoint, without trailing slash
99
url https://carbon-aware-api.azurewebsites.net
1010

11+
# OR full path to CLI executable
12+
# url /home/circa/carbon-aware-sdk-162-cli-redesign/src/CarbonAware.CLI/src/bin/Release/net6.0/publish/caw
13+
1114
# Location to request carbon intensity data for
1215
location uksouth

0 commit comments

Comments
 (0)