Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
SUSE:SLE-15-SP1:Update
GeoIP
geoip-asn-csv-to-dat.cpp
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File geoip-asn-csv-to-dat.cpp of Package GeoIP
/* geoip-csv-to-dat - convert a country database from CSV to GeoIP binary format * * Copyright (c) 2009 Kalle Olavi Niemitalo. * Copyright (c) 2011 Patrick Matthäi * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #define _GNU_SOURCE 1 #include <algorithm> #include <arpa/inet.h> #include <cerrno> #include <cstdio> #include <cstdlib> #include <cstring> #include <error.h> #include <fstream> #include <getopt.h> #include <iostream> #include <queue> #include <set> #include <stack> #include <string> #include <sys/socket.h> #include <sys/types.h> #include <sysexits.h> #include <vector> #include <GeoIP.h> #include <map> // Format of GeoIP Country database files // ====================================== // // 1. Binary trie mapping IP addresses to countries. // 2. Optional unused data. // 3. Optional database-info block. // 4. Optional structure-info block. // // Binary trie // ----------- // // The trie treats IP addresses as bit sequences and maps them to // numbers. In the country database, each such number is 0xFFFF00 + // the the country ID that GeoIP_id_by_ipnum() would return. The // meanings of country IDs are hardcoded in libGeoIP and cannot be // overridden by the database. // // The root node of the trie is at the beginning of the file, and the // other nodes then follow it. Each node has the same size and // consists of two little-endian pointers that correspond to the two // possible values of a bit. In the country database, the pointers are // 24-bit, and each node is thus 6 bytes long. // // Each pointer is one of: // - The number that the whole lookup should return, i.e. 0xFFFF00 + id // in the country database. // - The number of the node that should be examined next, counting from // 0 at the beginning of the file. Pointing back to nodes with // smaller numbers is allowed, but loops are not allowed. // // Optional unused data // -------------------- // // The file format seems to permit extra data between the binary trie // and the optional blocks. // // Optional database-info block // ---------------------------- // // Near the end of the file, there may be a three-byte tag (0x00 0x00 // 0x00) followed by at most DATABASE_INFO_MAX_SIZE - 1 = 99 bytes of // text that describes the database. GeoIP_database_info() returns // this text and appends a terminating '\0'. // // The GeoLite Country IPv4 database downloadable from MaxMind // includes this database-info block. // // Optional structure-info block // ----------------------------- // // At the very end of the file, there may be a three-byte tag (0xFF // 0xFF 0xFF) followed by at most STRUCTURE_INFO_MAX_SIZE - 1 = 19 // bytes. The first byte is the database type, // e.g. GEOIP_COUNTRY_EDITION = 1 or GEOIP_COUNTRY_EDITION_V6 = 12, // possibly with 105 added to it. Type-specific information then // follows. There is no type-specific information for the country // editions. // // The GeoLite Country IPv4 database downloadable from MaxMind does // not include this structure-info block. namespace { class binary_trie { public: typedef uint_fast32_t edge_type; explicit binary_trie(edge_type leaf); void set_range( const uint8_t range_min[], const uint8_t range_max[], std::size_t bit_count, edge_type leaf); void reorder_depth_first(); void reorder_in_blocks(std::size_t bytes_per_block); void write_binary(std::ostream &dat_stream) const; void write_segment(std::ostream &dat_stream) const; void update_records(); private: struct node { edge_type edges[2]; }; std::vector<node> nodes; // This could be std::vector<bool> but that seems slower. typedef std::vector<uint8_t> bits_vector; void set_range_in_node( const bits_vector *min_bits, const bits_vector *max_bits, std::size_t bit_pos, edge_type edit_node, edge_type leaf); void set_range_in_edge( const bits_vector *min_bits, const bits_vector *max_bits, std::size_t bit_pos, edge_type edit_node, bool bit, edge_type leaf); void reorder( const std::vector<edge_type> &old_to_new, const std::vector<edge_type> &new_to_old); }; } /** Construct a binary trie and its root node. * * \param leaf * Both edges of the root node will initially point to this leaf. * The caller should provide a value that means nothing was found. */ binary_trie::binary_trie(edge_type leaf) { const node node = {{ leaf, leaf }}; nodes.push_back(node); } /** Edit the trie so it maps a range of bit sequences to the same * leaf. * * \param range_min * The first bit sequence in the range. Eight bits are packed in each * byte. The most significant bit of the whole sequence is in the * most significant bit of the first byte. * * \param range_max * The last bit sequence in the range. * * \param bit_count * The number of bits in both sequences. * * \param leaf * The leaf to which all the bit sequences in the range should be * mapped. */ void binary_trie::set_range( const uint8_t range_min[], const uint8_t range_max[], std::size_t bit_count, edge_type leaf) { bits_vector min_bits(bit_count); bits_vector max_bits(bit_count); for (std::size_t i = 0; i < bit_count; ++i) { std::size_t byte_pos = i / 8; uint8_t mask = 1 << ((~i) % 8); min_bits[i] = ((range_min[byte_pos] & mask) != 0); max_bits[i] = ((range_max[byte_pos] & mask) != 0); } set_range_in_node(&min_bits, &max_bits, 0, 0, leaf); } /** Edit a node in the trie so it maps a range of bit sequences to the * same leaf. * * \param min_bits * The first bit sequence in the range, or NULL if unbounded. * * \param max_bits * The last bit sequence in the range, or NULL if unbounded. * * \param bit_pos * Which bit in the sequences corresponds to \a edit_node. * * \param edit_node * The node to be modified. * * \param leaf * The leaf to which all the bit sequences in the range should be * mapped. */ void binary_trie::set_range_in_node( const bits_vector *min_bits, const bits_vector *max_bits, std::size_t bit_pos, edge_type edit_node, edge_type leaf) { if (!min_bits || (*min_bits)[bit_pos] == false) { set_range_in_edge(min_bits, (max_bits && (*max_bits)[bit_pos] == false) ? max_bits : NULL, bit_pos + 1, edit_node, false, leaf); } if (!max_bits || (*max_bits)[bit_pos] == true) { set_range_in_edge((min_bits && (*min_bits)[bit_pos] == true) ? min_bits : NULL, max_bits, bit_pos + 1, edit_node, true, leaf); } } /** Edit an edge in the trie so it maps a range of bit sequences to * the same leaf. * * \param min_bits * The first bit sequence in the range, or NULL if unbounded. * * \param max_bits * The last bit sequence in the range, or NULL if unbounded. * * \param bit_pos * Which bit in the sequences corresponds to \a bit. * * \param edit_node * The node in which the edge to be modified is located. * * \param bit * Which edge of \a edit_node should be modified. * * \param leaf * The leaf to which all the bit sequences in the range should be * mapped. */ void binary_trie::set_range_in_edge( const bits_vector *min_bits, const bits_vector *max_bits, std::size_t bit_pos, edge_type edit_node, bool bit, edge_type leaf) { // Check if the range fills this edge entirely. bool entire = true; if (min_bits && std::find(min_bits->begin() + bit_pos, min_bits->end(), true) != min_bits->end()) entire = false; if (max_bits && std::find(max_bits->begin() + bit_pos, max_bits->end(), false) != max_bits->end()) entire = false; if (entire) { nodes[edit_node].edges[bit] = leaf; } else { edge_type next = nodes[edit_node].edges[bit]; if (next >= nodes.size()) { const node new_node = {{ next, next }}; next = nodes.size(); nodes.push_back(new_node); nodes[edit_node].edges[bit] = next; } set_range_in_node(min_bits, max_bits, bit_pos, next, leaf); } } /** Renumber the nodes in depth-first order. */ void binary_trie::reorder_depth_first() { std::vector<edge_type> old_to_new, new_to_old; std::stack<edge_type> depth_first; old_to_new.resize(nodes.size(), -1); new_to_old.reserve(nodes.size()); depth_first.push(0); while (!depth_first.empty()) { const edge_type edge = depth_first.top(); depth_first.pop(); if (edge < nodes.size()) { old_to_new[edge] = new_to_old.size(); new_to_old.push_back(edge); depth_first.push(nodes[edge].edges[1]); depth_first.push(nodes[edge].edges[0]); } } reorder(old_to_new, new_to_old); } /** Renumber the nodes to make lookups use CPU and disk caches more * effectively. * * First group the nodes into blocks so that each block contains the * root of a subtrie and as many levels of its descendants as will * fit. This way, after the root is paged in, the next few lookup * steps need not page in anything else. Then, sort the nodes of each * block in depth-first order. That should give each lookup almost * 1/2 chance to find the next node immediately adjacent. * * With a block size of 1024 bytes, this renumbering reduces the time * required for random lookups by about 1.1%, compared to a plain * depth-first order. However, it's still 2.3% slower than the * database optimized by MaxMind. */ void binary_trie::reorder_in_blocks( std::size_t bytes_per_block) { const edge_type none = -1; std::vector<edge_type> old_to_new, new_to_old; ssize_t bytes_left = bytes_per_block; old_to_new.resize(nodes.size(), none); new_to_old.reserve(nodes.size()); for (edge_type subtrie = 0; subtrie < nodes.size(); ++subtrie) { // If subtrie has already been added to the output, // ignore it. if (old_to_new[subtrie] != none) continue; // Walk breadth-first from subtrie until we have a // block full of nodes or the subtrie runs out. Don't // add these nodes immediately to the output, however. // Instead just list them in nodes_in_block. std::set<edge_type> nodes_in_block; std::queue<edge_type> breadth_first; breadth_first.push(subtrie); if (bytes_left <= 0) bytes_left += bytes_per_block; while (bytes_left > 0 && !breadth_first.empty()) { edge_type edge = breadth_first.front(); breadth_first.pop(); if (edge >= nodes.size()) continue; // Let the last node of the block straddle the // block boundary. That's better than making // the hotter first node do so. bytes_left -= 6; nodes_in_block.insert(edge); breadth_first.push(nodes[edge].edges[0]); breadth_first.push(nodes[edge].edges[1]); } // Add the nodes from nodes_in_block to the output in // depth-first order. This assumes they are all // reachable from subtrie. std::stack<edge_type> depth_first; depth_first.push(subtrie); while (!depth_first.empty()) { edge_type edge = depth_first.top(); depth_first.pop(); if (nodes_in_block.find(edge) == nodes_in_block.end()) continue; old_to_new[edge] = new_to_old.size(); new_to_old.push_back(edge); depth_first.push(nodes[edge].edges[1]); depth_first.push(nodes[edge].edges[0]); } } reorder(old_to_new, new_to_old); } void binary_trie::reorder( const std::vector<edge_type> &old_to_new, const std::vector<edge_type> &new_to_old) { std::vector<node> new_nodes; new_nodes.reserve(new_to_old.size()); for (std::vector<edge_type>::const_iterator it = new_to_old.begin(); it != new_to_old.end(); ++it) { node new_node; for (int bit = 0; bit <= 1; ++bit) { edge_type old_edge = nodes[*it].edges[bit]; if (old_edge < nodes.size()) new_node.edges[bit] = old_to_new[old_edge]; else new_node.edges[bit] = old_edge; } new_nodes.push_back(new_node); } swap(new_nodes, nodes); } /** Add the size of the trie (number of nodes) to data records*/ void binary_trie::update_records() { for (std::vector<node>::iterator it = nodes.begin(); it != nodes.end(); ++it) { // previously, we commandeered the MSB in order to indicate which records // were data records, rather than pointers to other nodes in the trie. // Here, we remove that bit, and increment the record by the number of nodes, // because this is how libGeoIP determines whether a node points to an entry // inside the data section or another node. for (int i = 0;i<1;++i) { if (it->edges[i] & 0x80000000) // msb means data record pointer { it->edges[i] = (it->edges[i] & 0x7FFFFFFF) + nodes.size(); } } } } /** Write the 3 byte segment offset. **/ void binary_trie::write_segment(std::ostream &dat_stream) const { int len = nodes.size(); dat_stream << (char) (0xFF & len); dat_stream << (char) (0xFF & (len >> 8)); dat_stream << (char) (0xFF & (len >> 16)); } /** Write the trie to a stream in GeoIP binary format. */ void binary_trie::write_binary(std::ostream &dat_stream) const { for (std::vector<node>::const_iterator it = nodes.begin(); it != nodes.end(); ++it) { union { uint8_t bytes[6]; char chars[6]; } binary = {{ (it->edges[0] ) & 0xFF, (it->edges[0] >> 8) & 0xFF, (it->edges[0] >> 16) & 0xFF, (it->edges[1] ) & 0xFF, (it->edges[1] >> 8) & 0xFF, (it->edges[1] >> 16) & 0xFF }}; dat_stream.write(binary.chars, 6); if (dat_stream.bad()) return; } } namespace { void v4_csv_line_to_vector( const std::string line, std::vector<std::string> &fields) { std::string buf(line); std::string delim = ","; std::size_t fs; for(int i = 0; i<2;++i) { fs = buf.find(delim); fields.push_back(buf.substr(0,fs)); buf.erase(0,fs + 1); } fields.push_back(buf.substr(1, buf.length() - 2)); } void v6_csv_line_to_vector( const std::string line, std::vector<std::string> & fields) { std::string buf(line); std::string delim = ", "; std::size_t fs; for(int i = 0; i<3;++i) { fs = buf.rfind(delim); fields.push_back(buf.substr(fs+2, buf.length())); buf.erase(fs,buf.length()); } fields.push_back(buf.substr(0, buf.length())); } /** Load ranges of IP addresses from a CSV-formatted stream to * a trie. * * \param trie * Load the ranges to this trie, overwriting original values. * * \param csv_file_name * The name of the file that \a csv_stream is reading. * This string is used only for error messages. * * \param csv_stream * Load the ranges from this stream. * * \param address_family * The type of IP addresses in the CSV data: either AF_INET * for IPv4 or AF_INET6 for IPv6. */ void csv_stream_to_trie_db( binary_trie &trie, std::string &database_segment, const char *csv_file_name, std::istream &csv_stream, int address_family) { enum { V4_CSV_FIELD_MIN_DECIMAL, V4_CSV_FIELD_MAX_DECIMAL, V4_CSV_FIELD_ASNUM_DESCRIPTION, V4_CSV_FIELDS }; enum { V6_CSV_FIELD_NET_BITS, V6_CSV_FIELD_MAX_TEXT, V6_CSV_FIELD_MIN_TEXT, V6_CSV_FIELD_ASNUM_DESCRIPTION, V6_CSV_FIELDS }; std::string csv_line; std::map<std::string,int> segment_offset; std::vector<std::string> csv_fields; // create a map to track which as descriptions are added already int csv_line_number = 0; database_segment += '\0'; // padding so that record 0 is not at the start of the db. while (getline(csv_stream, csv_line)) { ++csv_line_number; std::string as; switch (address_family) { case AF_INET: v4_csv_line_to_vector(csv_line, csv_fields); if (csv_fields.size() != V4_CSV_FIELDS) { error_at_line(EX_DATAERR, 0, csv_file_name, csv_line_number, "Wrong number of fields"); } as = csv_fields[V4_CSV_FIELD_ASNUM_DESCRIPTION]; if (segment_offset.find(as) == segment_offset.end()) { // no key found segment_offset[as] = database_segment.length(); // start of record database_segment += csv_fields[V4_CSV_FIELD_ASNUM_DESCRIPTION] + '\x00'; } break; case AF_INET6: v6_csv_line_to_vector(csv_line, csv_fields); if (csv_fields.size() != V6_CSV_FIELDS) { error_at_line(EX_DATAERR, 0, csv_file_name, csv_line_number, "Wrong number of fields"); } as = csv_fields[V6_CSV_FIELD_ASNUM_DESCRIPTION]; if (segment_offset.find(as) == segment_offset.end()) { // no key found segment_offset[as] = database_segment.length(); // start of record database_segment += csv_fields[V6_CSV_FIELD_ASNUM_DESCRIPTION] + '\x00'; } break; default: abort(); } // use the MSB to indicate that this is a data record. Later, the field // is set to the segment offset + the nubmer of nodes in the trie. const binary_trie::edge_type leaf = 0x80000000 | segment_offset[as]; union { struct in_addr inet; uint8_t inetbytes[4]; struct in6_addr inet6; } minaddr, maxaddr; switch (address_family) { case AF_INET: minaddr.inet.s_addr = htonl(atoi(csv_fields[V4_CSV_FIELD_MIN_DECIMAL].c_str())); maxaddr.inet.s_addr = htonl(atoi(csv_fields[V4_CSV_FIELD_MAX_DECIMAL].c_str())); trie.set_range(minaddr.inetbytes, maxaddr.inetbytes, 32, leaf); break; case AF_INET6: if (inet_pton(address_family, csv_fields[V6_CSV_FIELD_MIN_TEXT].c_str(), &minaddr) <= 0) { error_at_line(EX_DATAERR, 0, csv_file_name, csv_line_number, "Cannot parse minimum address: %s", csv_fields[V6_CSV_FIELD_MIN_TEXT].c_str()); } if (inet_pton(address_family, csv_fields[V6_CSV_FIELD_MAX_TEXT].c_str(), &maxaddr) <= 0) { error_at_line(EX_DATAERR, 0, csv_file_name, csv_line_number, "Cannot parse maximum address: %s", csv_fields[V6_CSV_FIELD_MAX_TEXT].c_str()); } trie.set_range(minaddr.inetbytes, maxaddr.inetbytes, 128, leaf); break; default: abort(); } csv_fields.clear(); } if (csv_stream.bad()) { error(EX_IOERR, errno, "%s", csv_file_name); } } /** Load ranges of IP addresses from a CSV-formatted file or * standard input to a trie. * * \param trie * Load the ranges to this trie, overwriting original values. * * \param csv_file_name * The name of the CSV file that should be read, or "-" for * standard input. * * \param address_family * The type of IP addresses in the CSV data: either AF_INET * for IPv4 or AF_INET6 for IPv6. */ void csv_file_to_trie_db( binary_trie &trie, std::string &database_segment, const char *csv_file_name, int address_family) { if (std::strcmp(csv_file_name, "-") == 0) { csv_stream_to_trie_db(trie, database_segment, csv_file_name, std::cin, address_family); } else { std::ifstream csv_stream(csv_file_name, std::ios::in); if (!csv_stream) { error(EX_NOINPUT, errno, "%s", csv_file_name); } csv_stream_to_trie_db(trie, database_segment, csv_file_name, csv_stream, address_family); } } /** Write a GeoIP binary database to a stream. * * \param trie * Mapping from IP addresses to country codes or other values. * * \param dat_file_name * The name of the file that \a dat_stream is writing. * This string is used only for error messages. * * \param dat_stream * Write the database to this stream. * * \param database_info * Copyright or other information about the database, or NULL. * GeoIP_database_info() will return this string. * * \param address_family * The type of IP addresses in the database: either AF_INET * for IPv4 or AF_INET6 for IPv6. */ void write_dat_stream( const binary_trie &trie, const char *dat_file_name, std::ostream &dat_stream, const char *database_info, std::string database_segment, int address_family) { trie.write_binary(dat_stream); if (dat_stream.bad()) { error(EX_IOERR, errno, "%s", dat_file_name); } // or open the file and read the length if (database_segment.length() > 0) { dat_stream << database_segment; if (dat_stream.bad()) { error(EX_IOERR, errno, "%s", dat_file_name); } } // write the metadata section if (database_info) { const char tag[3] = { 0, 0, 0 }; dat_stream.write(tag, 3); dat_stream.write(database_info, std::strlen(database_info)); if (dat_stream.bad()) { error(EX_IOERR, errno, "%s", dat_file_name); } } switch (address_family) { case AF_INET: { const char structure_info[4] = { (char)0xFF, (char)0xFF, (char)0xFF, 9 }; dat_stream.write(structure_info, 4); break; } case AF_INET6: { const char structure_info[4] = { (char)0xFF, (char)0xFF, (char)0xFF, 21 }; dat_stream.write(structure_info, 4); break; } default: abort(); } trie.write_segment(dat_stream); if (dat_stream.bad()) { error(EX_IOERR, errno, "%s", dat_file_name); } } /** Write a GeoIP binary database to a file or standard output. * * \param trie * Mapping from IP addresses to country codes or other values. * * \param csv_file_name * The name of the file that should be written, or "-" for * standard output. * * \param database_info * Copyright or other information about the database, or NULL. * GeoIP_database_info() will return this string. * * \param address_family * The type of IP addresses in the database: either AF_INET * for IPv4 or AF_INET6 for IPv6. */ void write_dat_file( const binary_trie &trie, const char *dat_file_name, const char *database_info, std::string database_segment, int address_family) { if (std::strcmp(dat_file_name, "-") == 0) { write_dat_stream(trie, dat_file_name, std::cout, database_info, database_segment, address_family); } else { std::ofstream dat_stream( dat_file_name, std::ios::out | std::ios::binary); if (!dat_stream) { error(EX_CANTCREAT, errno, "%s", dat_file_name); } write_dat_stream(trie, dat_file_name, dat_stream, database_info, database_segment, address_family); } } struct cmdline { const char *csv_file_name; const char *dat_file_name; int address_family; const char *database_info; bool verbose; cmdline(int argc, char **argv); }; } cmdline::cmdline(int argc, char **argv): csv_file_name("-"), dat_file_name("-"), address_family(AF_INET), database_info(NULL), verbose(false) { enum { OPT_HELP = -2 }; static const struct option long_options[] = { { "info", required_argument, NULL, 'i' }, { "output", required_argument, NULL, 'o' }, { "verbose", no_argument, NULL, 'v' }, { "help", no_argument, NULL, OPT_HELP }, { NULL, 0, NULL, 0 } }; static const char *const usage = "\ Usage: %s [OPTION] [CSV-FILE]...\n\ Convert a country database from CSV to GeoIP binary format.\n\ \n\ -i, --info=TEXT add copyright or other info TEXT to output\n\ -o, --output=FILE write the binary data to FILE, not stdout\n\ -v, --verbose show what is going on\n\ --help display this help and exit\n"; for (;;) { int optret = getopt_long(argc, argv, "46i:o:v", long_options, NULL); if (optret == -1) break; switch (optret) { case '4': address_family = AF_INET; break; case '6': address_family = AF_INET6; break; case 'i': database_info = optarg; if (std::strlen(database_info) > 99) { error(EX_USAGE, 0, "Database info must not be longer than 99 bytes"); } break; case 'o': dat_file_name = optarg; break; case 'v': verbose = true; break; case OPT_HELP: std::printf(usage, program_invocation_name); std::exit(EX_OK); case '?': std::fprintf(stderr, "Try `%s --help' for more information.\n", program_invocation_name); std::exit(EX_USAGE); default: std::abort(); } } if (optind < argc) csv_file_name = argv[optind++]; if (optind < argc) { error(EX_USAGE, 0, "Only one non-option argument is allowed"); } } int main(int argc, char **argv) { cmdline cmdline(argc, argv); std::ostream *verbose_stream; if (!cmdline.verbose) verbose_stream = NULL; else if (strcmp(cmdline.dat_file_name, "-") == 0) verbose_stream = &std::cerr; else verbose_stream = &std::cout; if (verbose_stream) { *verbose_stream << program_invocation_name << ": Reading CSV and building the trie" << std::endl; } /* Initialize the trie with a value that will point to the start of the * data section, e.g. an empty record. See binary_trie::update_records() */ binary_trie trie(0x80000000); std::string database_segment; csv_file_to_trie_db(trie, database_segment, cmdline.csv_file_name, cmdline.address_family); if (verbose_stream) { *verbose_stream << program_invocation_name << ": Optimizing" << std::endl; } trie.reorder_depth_first(); trie.reorder_in_blocks(1024); trie.update_records(); if (verbose_stream) { *verbose_stream << program_invocation_name << ": Writing output" << std::endl; } write_dat_file(trie, cmdline.dat_file_name, cmdline.database_info, database_segment, cmdline.address_family); if (verbose_stream) { *verbose_stream << program_invocation_name << ": All done" << std::endl; } }
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor