diff options
-rw-r--r-- | CHANGES | 15 | ||||
-rw-r--r-- | UPGRADE.txt | 12 | ||||
-rw-r--r-- | apps/app_fax.c | 3 | ||||
-rwxr-xr-x | configure | 2 | ||||
-rw-r--r-- | configure.ac | 2 | ||||
-rwxr-xr-x | contrib/scripts/sip_to_pjsip/sip_to_pjsip.py | 2 | ||||
-rwxr-xr-x | contrib/scripts/sip_to_pjsip/sip_to_pjsql.py | 81 | ||||
-rw-r--r-- | contrib/scripts/sip_to_pjsip/sqlconfigparser.py | 69 | ||||
-rw-r--r-- | main/bridge_roles.c | 8 | ||||
-rw-r--r-- | main/loader.c | 140 | ||||
-rw-r--r-- | res/res_config_sqlite.c | 2 | ||||
-rw-r--r-- | res/res_monitor.c | 3 | ||||
-rw-r--r-- | res/res_pjsip.c | 12 | ||||
-rw-r--r-- | res/res_pjsip/pjsip_configuration.c | 1 | ||||
-rw-r--r-- | res/res_pjsip_header_funcs.c | 27 |
15 files changed, 328 insertions, 51 deletions
@@ -12,6 +12,11 @@ --- Functionality changes from Asterisk 15 to Asterisk 16 -------------------- ------------------------------------------------------------------------------ +app_fax +------------------ + * The app_fax module is now deprecated, users should migrate to the + replacement module res_fax. + app_macro ------------------ * The app_macro module is now deprecated and by default it is no longer @@ -43,6 +48,16 @@ app_queue When set the wrapuptime on the member is used instead of the wrapuptime defined for the queue itself. +res_config_sqlite +------------------ + * The res_config_sqlite module is now deprecated, users should migrate to the + replacement module res_config_sqlite3. + +res_monitor +------------------ + * The res_monitor module is now deprecated, users should migrate to the + replacement module app_mixmonitor. + res_pjsip ------------------ * A new AMI action, PJSIPShowAors, has been added which displays information diff --git a/UPGRADE.txt b/UPGRADE.txt index d398e5fea..366825863 100644 --- a/UPGRADE.txt +++ b/UPGRADE.txt @@ -26,6 +26,10 @@ New in 16.0.0: +app_fax: + - The app_fax module is now deprecated, users should migrate to the + replacement module res_fax. + app_macro: - The app_macro module is now deprecated and by default it is no longer built. Users should migrate to app_stack (Gosub). A warning is logged @@ -44,6 +48,14 @@ cdr_syslog: - The cdr_syslog module is now deprecated and by default it is no longer built. +res_config_sqlite: + - The res_config_sqlite module is now deprecated, users should migrate to the + replacement module res_config_sqlite3. + +res_monitor: + - The res_monitor module is now deprecated, users should migrate to the + replacement module app_mixmonitor. + Core: - libedit is no longer available as an embedded library and must be provided by the system. diff --git a/apps/app_fax.c b/apps/app_fax.c index 540e8e35c..293925ac1 100644 --- a/apps/app_fax.c +++ b/apps/app_fax.c @@ -16,7 +16,8 @@ <defaultenabled>no</defaultenabled> <depend>spandsp</depend> <conflict>res_fax</conflict> - <support_level>extended</support_level> + <support_level>deprecated</support_level> + <replacement>res_fax</replacement> ***/ #include "asterisk.h" @@ -21858,7 +21858,7 @@ rm -f core conftest.err conftest.$ac_objext \ conftest$ac_exeext conftest.$ac_ext else #looking in imap directory didn't work, try c-client imap_ldflags="" - imap_libs="-lc-client" + imap_libs="-lcrypto -lssl -lc-client" imap_include="-DUSE_SYSTEM_CCLIENT" CPPFLAGS="${saved_cppflags}" LIBS="${saved_libs}" diff --git a/configure.ac b/configure.ac index 96210f203..596a167f0 100644 --- a/configure.ac +++ b/configure.ac @@ -1980,7 +1980,7 @@ if test "${USE_IMAP_TK}" != "no"; then ) else #looking in imap directory didn't work, try c-client imap_ldflags="" - imap_libs="-lc-client" + imap_libs="-lcrypto -lssl -lc-client" imap_include="-DUSE_SYSTEM_CCLIENT" CPPFLAGS="${saved_cppflags}" LIBS="${saved_libs}" diff --git a/contrib/scripts/sip_to_pjsip/sip_to_pjsip.py b/contrib/scripts/sip_to_pjsip/sip_to_pjsip.py index 533e4baec..9f7d99104 100755 --- a/contrib/scripts/sip_to_pjsip/sip_to_pjsip.py +++ b/contrib/scripts/sip_to_pjsip/sip_to_pjsip.py @@ -1203,7 +1203,7 @@ def convert(sip, filename, non_mappings, include): map specific sections from sip.conf into it. Returns the new pjsip.conf object once completed """ - pjsip = astconfigparser.MultiOrderedConfigParser() + pjsip = sip.__class__() non_mappings[filename] = astdicts.MultiOrderedDict() nmapped = non_mapped(non_mappings[filename]) if not include: diff --git a/contrib/scripts/sip_to_pjsip/sip_to_pjsql.py b/contrib/scripts/sip_to_pjsip/sip_to_pjsql.py new file mode 100755 index 000000000..d93bca500 --- /dev/null +++ b/contrib/scripts/sip_to_pjsip/sip_to_pjsql.py @@ -0,0 +1,81 @@ +#!/usr/bin/python + +from sip_to_pjsip import cli_options +from sip_to_pjsip import convert +import sip_to_pjsip +import optparse + + +import sqlconfigparser + + +def write_pjsip(filename, pjsip, non_mappings): + """ + Write pjsip.sql file to disk + """ + try: + with open(filename, 'wt') as fp: + pjsip.write(fp) + + except IOError: + print "Could not open file ", filename, " for writing" + +def cli_options(): + """ + Parse command line options and apply them. If invalid input is given, + print usage information + + """ + global user + global password + global host + global port + global database + global table + + usage = "usage: %prog [options] [input-file [output-file]]\n\n" \ + "Converts the chan_sip configuration input-file to mysql output-file.\n" \ + "The input-file defaults to 'sip.conf'.\n" \ + "The output-file defaults to 'pjsip.sql'." + parser = optparse.OptionParser(usage=usage) + parser.add_option('-u', '--user', dest='user', default="root", + help='mysql username') + parser.add_option('-p', '--password', dest='password', default="root", + help='mysql password') + parser.add_option('-H', '--host', dest='host', default="127.0.0.1", + help='mysql host ip') + parser.add_option('-P', '--port', dest='port', default="3306", + help='mysql port number') + parser.add_option('-D', '--database', dest='database', default="asterisk", + help='mysql port number') + parser.add_option('-t', '--table', dest='table', default="sippeers", + help='name of sip realtime peers table') + + options, args = parser.parse_args() + + user = options.user + password = options.password + host = options.host + port = options.port + database = options.database + table = options.table + + sip_filename = args[0] if len(args) else 'sip.conf' + pjsip_filename = args[1] if len(args) == 2 else 'pjsip.sql' + + return sip_filename, pjsip_filename + +if __name__ == "__main__": + sip_filename, pjsip_filename = cli_options() + sip = sqlconfigparser.SqlConfigParser(table) + sip_to_pjsip.sip = sip + sip.connect(user,password,host,port,database) + print 'Please, report any issue at:' + print ' https://issues.asterisk.org/' + print 'Reading', sip_filename + sip.read(sip_filename) + print 'Converting to PJSIP realtime sql...' + pjsip, non_mappings = convert(sip, pjsip_filename, dict(), False) + print 'Writing', pjsip_filename + write_pjsip(pjsip_filename, pjsip, non_mappings) + diff --git a/contrib/scripts/sip_to_pjsip/sqlconfigparser.py b/contrib/scripts/sip_to_pjsip/sqlconfigparser.py new file mode 100644 index 000000000..e87224ff1 --- /dev/null +++ b/contrib/scripts/sip_to_pjsip/sqlconfigparser.py @@ -0,0 +1,69 @@ +from astconfigparser import MultiOrderedConfigParser + +import MySQLdb +import traceback + +class SqlConfigParser(MultiOrderedConfigParser): + + _tablename = "sippeers" + + def __init__(self,tablename="sippeers"): + self._tablename=tablename + MultiOrderedConfigParser.__init__(self) + + def connect(self, user, password, host, port, database): + self.cnx = MySQLdb.connect(user=user,passwd=password,host=host,port=int(port),db=database) + + def read(self, filename, sect=None): + MultiOrderedConfigParser.read(self, filename, sect) + # cursor = self.cnx.cursor(dictionary=True) + cursor = self.cnx.cursor(cursorclass=MySQLdb.cursors.DictCursor) + cursor.execute("SELECT * from `" + MySQLdb.escape_string(self._tablename) + "`") + rows = cursor.fetchall() + + for row in rows: + sect = self.add_section(row['name']) + for key in row: + if (row[key] != None): + for elem in str(row[key]).split(";"): + sect[key] = elem + #sect[key] = str(row[key]).split(";") + + def write_dicts(self, config_file, mdicts): + """Write the contents of the mdicts to the specified config file""" + for section, sect_list in mdicts.iteritems(): + # every section contains a list of dictionaries + for sect in sect_list: + sql = "INSERT INTO " + if (sect.get('type')[0] == "endpoint"): + sql += "ps_endpoints " + elif (sect.get('type')[0] == "aor" and section != "sbc"): + sql += "ps_aors " + elif (sect.get('type')[0] == "identify"): + sql += "ps_endpoint_id_ips" + else: + continue + + sql += " SET `id` = " + "\"" + MySQLdb.escape_string(section) + "\"" + for key, val_list in sect.iteritems(): + if key == "type": + continue + # every value is also a list + + key_val = " `" + key + "`" + key_val += " = " + "\"" + MySQLdb.escape_string(";".join(val_list)) + "\"" + sql += "," + sql += key_val + + config_file.write("%s;\n" % (sql)) + + def write(self, config_file): + """Write configuration information out to a file""" + try: + self.write_dicts(config_file, self._sections) + except Exception,e: + print "Could not open file ", config_file, " for writing" + traceback.print_exc() + + + diff --git a/main/bridge_roles.c b/main/bridge_roles.c index a9b95a352..6dbae6fa7 100644 --- a/main/bridge_roles.c +++ b/main/bridge_roles.c @@ -51,12 +51,12 @@ struct bridge_role_option { struct bridge_role { AST_LIST_ENTRY(bridge_role) list; - AST_LIST_HEAD(, bridge_role_option) options; + AST_LIST_HEAD_NOLOCK(, bridge_role_option) options; char role[AST_ROLE_LEN]; }; struct bridge_roles_datastore { - AST_LIST_HEAD(, bridge_role) role_list; + AST_LIST_HEAD_NOLOCK(, bridge_role) role_list; }; /*! @@ -128,6 +128,8 @@ static struct bridge_roles_datastore *setup_bridge_roles_datastore(struct ast_ch return NULL; } + AST_LIST_HEAD_INIT_NOLOCK(&roles_datastore->role_list); + datastore->data = roles_datastore; ast_channel_datastore_add(chan, datastore); return roles_datastore; @@ -264,6 +266,8 @@ static int setup_bridge_role(struct bridge_roles_datastore *roles_datastore, con return -1; } + AST_LIST_HEAD_INIT_NOLOCK(&role->options); + ast_copy_string(role->role, role_name, sizeof(role->role)); AST_LIST_INSERT_TAIL(&roles_datastore->role_list, role, list); diff --git a/main/loader.c b/main/loader.c index 6b29f0e96..08d9552ff 100644 --- a/main/loader.c +++ b/main/loader.c @@ -87,6 +87,40 @@ </syntax> </managerEventInstance> </managerEvent> + <managerEvent language="en_US" name="Load"> + <managerEventInstance class="EVENT_FLAG_SYSTEM"> + <synopsis>Raised when a module has been loaded in Asterisk.</synopsis> + <syntax> + <parameter name="Module"> + <para>The name of the module that was loaded</para> + </parameter> + <parameter name="Status"> + <para>The result of the load request.</para> + <enumlist> + <enum name="Failure"><para>Module could not be loaded properly</para></enum> + <enum name="Success"><para>Module loaded and configured</para></enum> + <enum name="Decline"><para>Module is not configured</para></enum> + </enumlist> + </parameter> + </syntax> + </managerEventInstance> + </managerEvent> + <managerEvent language="en_US" name="Unload"> + <managerEventInstance class="EVENT_FLAG_SYSTEM"> + <synopsis>Raised when a module has been unloaded in Asterisk.</synopsis> + <syntax> + <parameter name="Module"> + <para>The name of the module that was unloaded</para> + </parameter> + <parameter name="Status"> + <para>The result of the unload request.</para> + <enumlist> + <enum name="Success"><para>Module unloaded successfully</para></enum> + </enumlist> + </parameter> + </syntax> + </managerEventInstance> + </managerEvent> ***/ #ifndef RTLD_NOW @@ -161,6 +195,27 @@ struct ast_module { static AST_DLLIST_HEAD_STATIC(module_list, ast_module); + +struct load_results_map { + int result; + const char *name; +}; + +static const struct load_results_map load_results[] = { + { AST_MODULE_LOAD_SUCCESS, "Success" }, + { AST_MODULE_LOAD_DECLINE, "Decline" }, + { AST_MODULE_LOAD_SKIP, "Skip" }, + { AST_MODULE_LOAD_PRIORITY, "Priority" }, + { AST_MODULE_LOAD_FAILURE, "Failure" }, +}; +#define AST_MODULE_LOAD_UNKNOWN_STRING "Unknown" /* Status string for unknown load status */ + +static void publish_load_message_type(const char* type, const char *name, const char *status); +static void publish_reload_message(const char *name, enum ast_module_reload_result result); +static void publish_load_message(const char *name, enum ast_module_load_result result); +static void publish_unload_message(const char *name, const char* status); + + /* * module_list is cleared by its constructor possibly after * we start accumulating built-in modules, so we need to @@ -1007,6 +1062,7 @@ int ast_unload_resource(const char *resource_name, enum ast_module_unload_mode f unload_dynamic_module(mod); ast_test_suite_event_notify("MODULE_UNLOAD", "Message: %s", resource_name); ast_update_use_count(); + publish_unload_message(resource_name, "Success"); } return res; @@ -1196,29 +1252,30 @@ static void queue_reload_request(const char *module) /*! * \since 12 * \internal - * \brief Publish a \ref stasis message regarding the reload result + * \brief Publish a \ref stasis message regarding the type. */ -static void publish_reload_message(const char *name, enum ast_module_reload_result result) +static void publish_load_message_type(const char* type, const char *name, const char *status) { RAII_VAR(struct stasis_message *, message, NULL, ao2_cleanup); RAII_VAR(struct ast_json_payload *, payload, NULL, ao2_cleanup); RAII_VAR(struct ast_json *, json_object, NULL, ast_json_unref); RAII_VAR(struct ast_json *, event_object, NULL, ast_json_unref); - char res_buffer[8]; + + ast_assert(type != NULL); + ast_assert(!ast_strlen_zero(name)); + ast_assert(!ast_strlen_zero(status)); if (!ast_manager_get_generic_type()) { return; } - snprintf(res_buffer, sizeof(res_buffer), "%u", result); - event_object = ast_json_pack("{s: s, s: s}", - "Module", S_OR(name, "All"), - "Status", res_buffer); - json_object = ast_json_pack("{s: s, s: i, s: o}", - "type", "Reload", + event_object = ast_json_pack("{s:s, s:s}", + "Module", name, + "Status", status); + json_object = ast_json_pack("{s:s, s:i, s:o}", + "type", type, "class_type", EVENT_FLAG_SYSTEM, "event", ast_json_ref(event_object)); - if (!json_object) { return; } @@ -1236,6 +1293,54 @@ static void publish_reload_message(const char *name, enum ast_module_reload_resu stasis_publish(ast_manager_get_topic(), message); } +static const char* loadresult2str(enum ast_module_load_result result) +{ + int i; + for (i = 0; i < ARRAY_LEN(load_results); i++) { + if (load_results[i].result == result) { + return load_results[i].name; + } + } + + ast_log(LOG_WARNING, "Failed to find correct load result status. result %d\n", result); + return AST_MODULE_LOAD_UNKNOWN_STRING; +} + +/*! + * \internal + * \brief Publish a \ref stasis message regarding the load result + */ +static void publish_load_message(const char *name, enum ast_module_load_result result) +{ + const char *status; + + status = loadresult2str(result); + + publish_load_message_type("Load", name, status); +} + +/*! + * \internal + * \brief Publish a \ref stasis message regarding the unload result + */ +static void publish_unload_message(const char *name, const char* status) +{ + publish_load_message_type("Unload", name, status); +} + +/*! + * \since 12 + * \internal + * \brief Publish a \ref stasis message regarding the reload result + */ +static void publish_reload_message(const char *name, enum ast_module_reload_result result) +{ + char res_buffer[8]; + + snprintf(res_buffer, sizeof(res_buffer), "%u", result); + publish_load_message_type("Reload", S_OR(name, "All"), res_buffer); +} + enum ast_module_reload_result ast_module_reload(const char *name) { struct ast_module *cur; @@ -1462,10 +1567,7 @@ static enum ast_module_load_result load_resource(const char *resource_name, unsi res |= ast_vector_string_split(&mod->optional_modules, mod->info->optional_modules, ",", 0, strcasecmp); res |= ast_vector_string_split(&mod->enhances, mod->info->enhances, ",", 0, strcasecmp); if (res) { - ast_log(LOG_WARNING, "Failed to initialize dependency structures for module '%s'.\n", resource_name); - unload_dynamic_module(mod); - - return required ? AST_MODULE_LOAD_FAILURE : AST_MODULE_LOAD_DECLINE; + goto prestart_error; } } @@ -1484,12 +1586,20 @@ static enum ast_module_load_result load_resource(const char *resource_name, unsi res = start_resource(mod); } + if (ast_fully_booted && !ast_shutdown_final()) { + publish_load_message(resource_name, res); + } + return res; prestart_error: ast_log(LOG_WARNING, "Module '%s' could not be loaded.\n", resource_name); unload_dynamic_module(mod); - return required ? AST_MODULE_LOAD_FAILURE : AST_MODULE_LOAD_DECLINE; + res = required ? AST_MODULE_LOAD_FAILURE : AST_MODULE_LOAD_DECLINE; + if (ast_fully_booted && !ast_shutdown_final()) { + publish_load_message(resource_name, res); + } + return res; } int ast_load_resource(const char *resource_name) diff --git a/res/res_config_sqlite.c b/res/res_config_sqlite.c index 5ca623ccc..83d2dca72 100644 --- a/res/res_config_sqlite.c +++ b/res/res_config_sqlite.c @@ -82,7 +82,7 @@ /*** MODULEINFO <depend>sqlite</depend> - <support_level>extended</support_level> + <support_level>deprecated</support_level> ***/ #include "asterisk.h" diff --git a/res/res_monitor.c b/res/res_monitor.c index a8631dcb7..95acf554d 100644 --- a/res/res_monitor.c +++ b/res/res_monitor.c @@ -25,7 +25,8 @@ /*** MODULEINFO <use type="module">func_periodic_hook</use> - <support_level>core</support_level> + <support_level>deprecated</support_level> + <replacement>app_mixmonitor</replacement> ***/ #include "asterisk.h" diff --git a/res/res_pjsip.c b/res/res_pjsip.c index 310ff20bc..7b59035c2 100644 --- a/res/res_pjsip.c +++ b/res/res_pjsip.c @@ -3350,8 +3350,6 @@ void ast_sip_add_usereqphone(const struct ast_sip_endpoint *endpoint, pj_pool_t { pjsip_sip_uri *sip_uri; int i = 0; - pjsip_param *param; - static const pj_str_t STR_USER = { "user", 4 }; static const pj_str_t STR_PHONE = { "phone", 5 }; if (!endpoint || !endpoint->usereqphone || (!PJSIP_URI_SCHEME_IS_SIP(uri) && !PJSIP_URI_SCHEME_IS_SIPS(uri))) { @@ -3379,15 +3377,7 @@ void ast_sip_add_usereqphone(const struct ast_sip_endpoint *endpoint, pj_pool_t return; } - if (pjsip_param_find(&sip_uri->other_param, &STR_USER)) { - /* Don't add it if it's already there */ - return; - } - - param = PJ_POOL_ALLOC_T(pool, pjsip_param); - param->name = STR_USER; - param->value = STR_PHONE; - pj_list_insert_before(&sip_uri->other_param, param); + sip_uri->user_param = STR_PHONE; } pjsip_dialog *ast_sip_create_dialog_uac(const struct ast_sip_endpoint *endpoint, diff --git a/res/res_pjsip/pjsip_configuration.c b/res/res_pjsip/pjsip_configuration.c index d1bfdfe01..3094f248e 100644 --- a/res/res_pjsip/pjsip_configuration.c +++ b/res/res_pjsip/pjsip_configuration.c @@ -2193,6 +2193,7 @@ static void info_configuration_destroy(struct ast_sip_endpoint_info_configuratio static void media_configuration_destroy(struct ast_sip_endpoint_media_configuration *media) { + ast_rtp_dtls_cfg_free(&media->rtp.dtls_cfg); ast_string_field_free_memory(&media->rtp); ast_string_field_free_memory(media); } diff --git a/res/res_pjsip_header_funcs.c b/res/res_pjsip_header_funcs.c index 79302632d..6c0f9151d 100644 --- a/res/res_pjsip_header_funcs.c +++ b/res/res_pjsip_header_funcs.c @@ -146,18 +146,11 @@ struct hdr_list_entry { pjsip_hdr *hdr; AST_LIST_ENTRY(hdr_list_entry) nextptr; }; -AST_LIST_HEAD(hdr_list, hdr_list_entry); - -/*! \brief Destructor for hdr_list */ -static void hdr_list_destroy(void *obj) -{ - AST_LIST_HEAD_DESTROY((struct hdr_list *) obj); -} +AST_LIST_HEAD_NOLOCK(hdr_list, hdr_list_entry); /*! \brief Datastore for saving headers */ static const struct ast_datastore_info header_datastore = { .type = "header_datastore", - .destroy = hdr_list_destroy, }; /*! \brief Data structure used for ast_sip_push_task_synchronous */ @@ -215,7 +208,7 @@ static int incoming_request(struct ast_sip_session *session, pjsip_rx_data * rda ast_log(AST_LOG_ERROR, "Unable to create datastore for header functions.\n"); return 0; } - AST_LIST_HEAD_INIT((struct hdr_list *) datastore->data); + AST_LIST_HEAD_INIT_NOLOCK((struct hdr_list *) datastore->data); } insert_headers(pool, (struct hdr_list *) datastore->data, rdata->msg_info.msg); @@ -340,7 +333,7 @@ static int add_header(void *obj) ast_log(AST_LOG_ERROR, "Unable to create datastore for header functions.\n"); return -1; } - AST_LIST_HEAD_INIT((struct hdr_list *) datastore->data); + AST_LIST_HEAD_INIT_NOLOCK((struct hdr_list *) datastore->data); } ast_debug(1, "Adding header %s with value %s\n", data->header_name, @@ -486,15 +479,15 @@ static int func_read_header(struct ast_channel *chan, const char *function, char header_data.buf = buf; header_data.len = len; - if (stricmp(args.action, "read") == 0) { + if (!strcasecmp(args.action, "read")) { return ast_sip_push_task_synchronous(channel->session->serializer, read_header, &header_data); - } else if (stricmp(args.action, "remove") == 0) { + } else if (!strcasecmp(args.action, "remove")) { return ast_sip_push_task_synchronous(channel->session->serializer, remove_header, &header_data); } else { ast_log(AST_LOG_ERROR, - "Unknown action \'%s\' is not valid, Must be \'read\' or \'remove\'.\n", + "Unknown action '%s' is not valid, must be 'read' or 'remove'.\n", args.action); return -1; } @@ -545,18 +538,18 @@ static int func_write_header(struct ast_channel *chan, const char *cmd, char *da header_data.buf = NULL; header_data.len = 0; - if (stricmp(args.action, "add") == 0) { + if (!strcasecmp(args.action, "add")) { return ast_sip_push_task_synchronous(channel->session->serializer, add_header, &header_data); - } else if (stricmp(args.action, "update") == 0) { + } else if (!strcasecmp(args.action, "update")) { return ast_sip_push_task_synchronous(channel->session->serializer, update_header, &header_data); - } else if (stricmp(args.action, "remove") == 0) { + } else if (!strcasecmp(args.action, "remove")) { return ast_sip_push_task_synchronous(channel->session->serializer, remove_header, &header_data); } else { ast_log(AST_LOG_ERROR, - "Unknown action \'%s\' is not valid, Must be \'add\', \'update\', or \'remove\'.\n", + "Unknown action '%s' is not valid, must be 'add', 'update', or 'remove'.\n", args.action); return -1; } |