From e6bfb30ded2a924b7d996f5c8949050973dfe621 Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Thu, 13 Nov 2025 04:49:12 +1100 Subject: [PATCH] netbox -ASYNC update and API fixes --- src/modules/punk/args-999999.0a1.0.tm | 3 +- src/modules/punk/netbox-999999.0a1.0.tm | 1022 +++++++++++++++-- src/modules/punk/netbox/man-999999.0a1.0.tm | 305 ++++- src/modules/punk/ns-999999.0a1.0.tm | 33 +- .../bootsupport/modules/punk/args-0.2.1.tm | 3 +- .../src/bootsupport/modules/punk/ns-0.1.0.tm | 33 +- .../bootsupport/modules/punk/args-0.2.1.tm | 3 +- .../src/bootsupport/modules/punk/ns-0.1.0.tm | 33 +- .../_vfscommon.vfs/modules/punk/args-0.2.1.tm | 3 +- .../modules/punk/netbox-0.1.0.tm | 1022 +++++++++++++++-- .../modules/punk/netbox/man-0.1.0.tm | 305 ++++- .../_vfscommon.vfs/modules/punk/ns-0.1.0.tm | 33 +- 12 files changed, 2526 insertions(+), 272 deletions(-) diff --git a/src/modules/punk/args-999999.0a1.0.tm b/src/modules/punk/args-999999.0a1.0.tm index 2783d026..15471739 100644 --- a/src/modules/punk/args-999999.0a1.0.tm +++ b/src/modules/punk/args-999999.0a1.0.tm @@ -6481,7 +6481,7 @@ tcl::namespace::eval punk::args { } if {[dict exists $thisarg_checks -maxsize]} { set maxsize [dict get $thisarg_checks -maxsize] - if {$checkval ne "-1"} { + if {$maxsize ne "-1"} { if {[tcl::string::length $e_check] > $maxsize} { set msg "$argclass '$argname' for %caller% requires string with -maxsize $maxsize. Received len:[tcl::string::length $e_check] value:'$e_check'" #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg @@ -6679,6 +6679,7 @@ tcl::namespace::eval punk::args { upper - wordchar - xdigit { + #todo - combined types xdigit && lower ?? set-theoretic types? how? if {![tcl::string::is $type -strict $e_check]} { set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e'" lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] diff --git a/src/modules/punk/netbox-999999.0a1.0.tm b/src/modules/punk/netbox-999999.0a1.0.tm index f8a4becd..815e4216 100644 --- a/src/modules/punk/netbox-999999.0a1.0.tm +++ b/src/modules/punk/netbox-999999.0a1.0.tm @@ -161,6 +161,9 @@ tcl::namespace::eval punk::netbox::system { } return $sanconfig } + variable call_data + array unset call_data + proc make_rest_func {args} { set argd [punk::args::parse $args withid ::punk::netbox::system::make_rest_func] lassign [dict values $argd] leaders opts values received @@ -283,17 +286,16 @@ tcl::namespace::eval punk::netbox::system { dict set config headers content-type "application/json" } - #variable headerdict - #set config [dict create\ - # headers $headerdict\ - #] - set url [dict get $contexts $apicontextid url value] set endpoint "%endpoint%" if {[string first {{id}} $endpoint] != -1} { set id [dict get $values id] set endpoint [string map [list {{id}} $id] $endpoint] } + + #??? + dict set config error-body false + #todo - only show if debug (and obscure Authorization Token) set sanconfig [punk::netbox::system::obscured_config $config] puts stderr "url:${url}$endpoint query:'$query' verb:%verb% config:'$sanconfig'" @@ -305,6 +307,10 @@ tcl::namespace::eval punk::netbox::system { } else { set result [::rest::%verb% ${url}$endpoint $query $config] } + #puts "-----------" + #puts $result + #puts "-----------" + } else { puts stderr "%commandname% not called because -FORCE is false" set sanconfig [punk::netbox::system::obscured_config $config] @@ -353,7 +359,302 @@ tcl::namespace::eval punk::netbox::system { }] proc $commandname {args} $procbody } + #experiment + variable callid + set callid 0 + proc make_rest_func_async {args} { + set argd [punk::args::parse $args withid ::punk::netbox::system::make_rest_func] + lassign [dict values $argd] leaders opts values received + + set commandname [dict get $leaders commandname] + set endpoint [dict get $leaders endpoint] + set verb [dict get $opts -verb] + set bodycontrol [dict get $opts -body] + + set custom [dict create\ + %commandname% $commandname\ + %endpoint% $endpoint\ + %verb% $verb\ + %bodycontrol% $bodycontrol\ + %showpagedict% {!@@results @@results/@*/@*.@*}\ + %showpagedict2% {@@results/@*/@*.@* !@@results}\ + %showdict% {*}\ + %showdict2% {*/*}\ + %showlistofdicts% {@*/@*.@*}\ + ] + + + set procbody [string map $custom { + upvar ::punk::netbox::system::callid callid + incr callid + + set argd [punk::args::parse $args withid %commandname%] + lassign [dict values $argd] leaders opts values received solos multis + set apicontextid [dict get $leaders apicontextid] + if {[dict exists $received -RETURN]} { + set returntype [dict get $opts -RETURN] + } else { + if {[dict exists $opts -RETURN]} { + #not received - but has default + set returntype [dict get $opts -RETURN] + } else { + #fallback if -RETURN was defined without a default or was omitted + set returntype dict + } + } + + #-ASYNC is of -type none - solo flag + if {[dict exists $received -ASYNC]} { + set async true + } else { + set async false + } + + set FORCE 1 + if {[dict exists $opts -FORCE]} { + set FORCE [dict get $opts -FORCE] + } + + set query [list] ;#use list not dict - allows repeated params + dict for {k val} $opts { + switch -- $k { + -CUSTOM_PARAM { + #'-multiple true' + foreach kv $val { + lassign $kv paramname value + lappend query $paramname $value + } + } + -RETURN - -ASYNC - -FORCE { + #ignore - not options for endoint - already handled + } + default { + if {[string match *_FILTER $k]} { + #all _FILTER methods are '-multiple true' - so are present in $opts as a single key with a value that is a list + #e.g -X_Y_FILTER "blah" + set parts [split $k _] + set name1 [string range [string tolower [lindex $parts 0]] 1 end] ;#strip leading dash off first part + set nametail [string tolower [lrange $parts 1 end-1]] + set paramname [join [list $name1 {*}$nametail] _] + foreach fv $val { + lassign $fv filter value ;#filter is n,gte,lte etc + lappend query ${paramname}__$filter $value + } + } else { + set paramname [string range $k 1 end] + if {$k in $multis} { + foreach v $val { + lappend query $paramname $v + } + } else { + lappend query $paramname $val + } + } + } + } + + } + upvar ::punk::netbox::contexts contexts + if {![dict exists $contexts $apicontextid]} { + error "specified contextid '$apicontextid' not found" + } + + #rest api documentation is unclear on 'result' field + #note our default: result json + #this actually converts the json to a dict + + set config [dict create\ + format json\ + result json\ + ] + + dict set config headers [list Authorization [list Token [dict get $contexts $apicontextid token value]]] + set bodycontrol %bodycontrol% + switch -- $bodycontrol { + required { + set requestbody [dict get $values body] + #content type for the request data + dict set config headers content-type "application/json" + } + optional { + if {[dict exists $received body]} { + set requestbody [dict get $values body] + dict set config headers content-type "application/json" + } else { + set requestbody "" + } + } + default { + set requestbody "" + } + } + + + if {$returntype in "json jsondump"} { + #if we set result json - we get a dict instead of json :/ + dict set config result raw + } + + set url [dict get $contexts $apicontextid url value] + set endpoint "%endpoint%" + if {[string first {{id}} $endpoint] != -1} { + set id [dict get $values id] + set endpoint [string map [list {{id}} $id] $endpoint] + } + append url $endpoint + + #??? + dict set config error-body false + + #todo - only show if debug (and obscure Authorization Token) + set sanconfig [punk::netbox::system::obscured_config $config] + puts stderr "url:$url query:'$query' verb:%verb% config:'$sanconfig'" + if {$FORCE} { + set thisproc [dict get [info frame 0] proc] + #FORCE is true for most operations (and no -FORCE option even available) but the option exists and defaults to false for specifically unsafe + #e.g delete operations on entire endpoints + + #---------------------------------------- + #from rest make_interface generated func: + set headers {} + dict for {key val} [dict get $config headers] {lappend headers $key [::rest::substitute $val query]} + set query [::http::formatQuery {*}$query] + #---------------------------------------- + set error_body true + + dict set config method %verb% ;#rest::_call does an upvar on config array + dict set config body $bodycontrol ;#this is required|optional not the requestbody data itself + #dict set config timeout ??? ;#todo -TIMEOUT flag? + + #============================================================================ + #rest::_call is just a small wrapper over http:: methods + #if {$bodycontrol in {required optional}} { + # #set result [::rest::%verb% ${url}$endpoint $query $config $requestbody] + # set httptok [::rest::_call [list ::punk::netbox::system::rest_call_complete $thisproc,$callid] $headers $url $query $requestbody $error_body] + #} else { + # #set result [::rest::%verb% ${url}$endpoint $query $config] + # set httptok [::rest::_call [list ::punk::netbox::system::rest_call_complete $thisproc,$callid] $headers $url $query "" $error_body] + #} + set method [string toupper %verb%] + + if {[string match no* $bodycontrol]} { + #never put the query in the body if the user said no body + } else { + if {![dict exists $received body] && $bodycontrol eq "required"} { + #our make_rest_func_async args say we need a body - but our punk::args validation didn't require it + #review + set requestbody $query + set query {} + } + } + + if {$query != ""} {append url ?$query} + #configure options to the http::geturl command + set opts [list] + lappend opts -method $method + if {[dict exists $headers content-type]} { + lappend opts -type [dict get $headers content-type] + set headers [dict remove $headers content-type] + } + if {$requestbody != ""} { + #-query arg for http::geturl causes it to do a POST with the value as the request payload. + lappend opts -query $requestbody + } + lappend opts -command [list ::punk::netbox::system::http_call_complete $thisproc $callid] + #if {[dict exists $config timeout]} { + # lappend opts -timeout [dict get $config timeout] + #} + set httptok [http::geturl $url -headers $headers {*}$opts] + #============================================================================ + + + + + #puts "tok: $httptok" + #puts "-----------" + #puts $result + #puts "-----------" + + upvar ::punk::netbox::system::call_data call_data + set call_data($thisproc,$callid,httptoken) $httptok + set call_data($thisproc,$callid,command) $thisproc + set call_data($thisproc,$callid,returntype) $returntype + #todo - the actual async part - solo flag -ASYNC ? + if {$async} { + return $thisproc,$callid + } + #we need to then provide a $thisproc,$callid token to the caller and a retrieval/cleanup proc + http::wait $httptok + + return [punk::netbox::get_result $thisproc,$callid] + + } else { + puts stderr "%commandname% not called because -FORCE is false" + set sanconfig [punk::netbox::system::obscured_config $config] + puts "url:$url query:'$query' verb:%verb% config:'$sanconfig'" + return + } + }] + proc $commandname {args} $procbody + } + proc rest_call_complete {key func status args} { + variable call_data + if {[catch { + upvar 1 token token + #parray token + #This assumes there is only one call in flight unless key is made unique per call + set call_data($ns) [array get token] + } errMsg]} { + puts "rest_call_complete error: $errMsg" + } + puts $args + #puts "->$ns $func $status $args" + } + + proc http_call_complete {func callid tok} { + variable call_data + set call_data($func,$callid,http) [array get $tok] + set rcode [http::ncode $tok] + if {![string match 2* $rcode]} { + upvar 0 $tok status + if {$rcode ne ""} { + set data [list NETBOX ERROR HTTP $rcode] + if {$rcode eq "302"} { + #is location always capitalised this way by http? It can vary in raw server responses + #tcllib rest had it as Location - which definitely breaks in at least some cases. + #(perhaps http lib normalized it?) + set meta [dict get $status(meta)] + set keys [dict keys $meta] + set keyname [lsearch -inline -nocase $keys location] + if {$keyname ne ""} { + lappend data location [dict get $meta $keyname] + } else { + lappend data location "(unknown)" ;#can this even happen? + } + #For a rest api - we probably shouldn't follow redirects (review) + #It's probably generally better to return the error and get the user to update their URL + #there may be cases where redirect-following is needed - add an option for it? + } else { + if {$status(totalsize) > 0} { + lappend data errorbody $status(body) + } + } + } else { + #no HTTP response code - may be a socket error + set data [list NETBOX ERROR OTHER $status(status)] + if {[info exists status(error)]} { + lappend data errortext $status(error) + } + } + set call_data($func,$callid,result) [list ERROR $data] + http::cleanup $tok + return + } + set call_data($func,$callid,result) [list OK [http::data $tok]] + http::cleanup $tok + return + } } tcl::namespace::eval punk::netbox { @@ -374,16 +675,6 @@ tcl::namespace::eval punk::netbox { ::http::register https 443 ::tls::socket } - variable ipam - - #TEMP - todo - variable headerdict - set headerdict [dict create\ - Authorization "Token af65b993000874eaefeca0fa02b0d86014e48365"\ - ] - #temp - variable url https://www.netbox1.intx.com.au/ - variable contexts [dict create] variable context_id 0 @@ -632,6 +923,256 @@ tcl::namespace::eval punk::netbox { dict set contexts $contextid $allprops return $contextid } + + lappend PUNKARGS [list { + @id -id ::punk::netbox::get_status + @cmd -name punk::netbox::get_status\ + -summary\ + "Get status of previous -ASYNC call."\ + -help\ + "Retrieve the status from a previous punk::netbox::?ns::? + call which was run with -ASYNC. + + An error will be raised if the asynctoken is not valid or + if its result has already been retrieved with get_result. + + Returns PENDING, OK or ERROR + + It will not return the error text or result. + + It will not clear the error/result. + " + @values -min 1 -max 1 + asynctoken -type string -help\ + "A token as returned from a punk::netbox command when + it was given the -ASYNC flag" + }] + proc get_status {asynctoken} { + upvar ::punk::netbox::system::call_data call_data + if {![info exists call_data($asynctoken,httptoken)]} { + error "No such asynctoken: $asynctoken" + } + if {![info exists call_data($asynctoken,http)]} { + return PENDING + } else { + #even though we could return the raw result here, + #we don't - because the user should call get_result for cleanup. + return [lindex $call_data($asynctoken,result) 0] ;#OK|ERROR + } + } + lappend PUNKARGS [list { + @id -id ::punk::netbox::is_finished + @cmd -name punk::netbox::is_finished\ + -summary\ + "Test if result/error of previous -ASYNC call available."\ + -help\ + "Return an integer 0,1 or 2 indicating whether the result/error from a + previous punk::netbox::?ns::? call which was run with -ASYNC + is ready. + 0 indicates the request is still pending. + 1 indicates there is a result available. + 2 indicates there the command completed with an error. + + An error will be raised if the token is not valid or if its + result has already been retrieved with get_result." + @values -min 1 -max 1 + asynctoken -type string -help\ + "A token as returned from a punk::netbox command when + it was given the -ASYNC flag" + }] + proc is_finished {asynctoken} { + upvar ::punk::netbox::system::call_data call_data + if {![info exists call_data($asynctoken,httptoken)]} { + error "No such asynctoken: $asynctoken" + } + if {![info exists call_data($asynctoken,http)]} { + return 0 + } else { + if {[lindex $call_data($asynctoken,result) 0] eq "OK"} { + return 1 + } else { + return 2 + } + } + } + + lappend PUNKARGS [list { + @id -id ::punk::netbox::get_result_http + @cmd -name punk::netbox::get_result_http\ + -summary\ + "Get results of previous -ASYNC call."\ + -help\ + "Retrieve the http information from a previous punk::netbox::?ns::? + call which was run with -ASYNC. + + An error will be raised if the asynctoken is not valid or + if its result has already been retrieved with get_result. + + If the call has not yet returned or timed out, this call + will wait until the http token's status has been set. + + This call does not invalidate/clean-up the asynctoken and + if used, should be called prior to get_result. + " + @values -min 1 -max 1 + asynctoken -type string -help\ + "A token as returned from a punk::netbox command when + it was given the -ASYNC flag" + }] + proc get_result_http {asynctoken} { + upvar ::punk::netbox::system::call_data call_data + if {![info exists call_data($asynctoken,httptoken)]} { + error "No such asynctoken: $asynctoken" + } + set httptok $call_data($asynctoken,httptoken) + if {![info exists call_data($asynctoken,http)]} { + http::wait $httptok + } + return $call_data($asynctoken,http) + } + + lappend PUNKARGS [list { + @id -id ::punk::netbox::get_result + @cmd -name punk::netbox::get_result\ + -summary\ + "Get results of previous -ASYNC call."\ + -help\ + "Retrieve the result from a previous punk::netbox::?ns::? + call which was run with -ASYNC. + + An error will be raised for a call that was unsuccessful. + An error will be raised if the asynctoken is not valid or + if its result has already been retrieved with get_result. + + If the call has not yet returned or timed out, this call + will wait until the http token's status has been set." + @values -min 1 -max 1 + asynctoken -type string -help\ + "A token as returned from a punk::netbox command when + it was given the -ASYNC flag" + }] + proc get_result {asynctoken} { + upvar ::punk::netbox::system::call_data call_data + + set debug 0 ;#todo -DEBUG flag? + + if {![info exists call_data($asynctoken,httptoken)]} { + error "No such asynctoken: $asynctoken" + } + set httptok $call_data($asynctoken,httptoken) + if {![info exists call_data($asynctoken,http)]} { + http::wait $httptok + } + + #set command [lindex [split $asynctoken ,] 0] ;#review - assumes no comma in command name - not a great assumption! + set command $call_data($asynctoken,command) + + set resultlist $call_data($asynctoken,result) + set returntype $call_data($asynctoken,returntype) + #status is #OK|ERROR + lassign $resultlist status resulttext + #resulttext NETBOX ERROR ... + if {$status eq "ERROR"} { + set etype [lindex $resulttext 2] + #--------------------------------- + #this works - but the raw json seems more useful, and less prone to secondary errors + #--------------------------------- + #if {$etype eq "HTTP"} { + # #return -code error -errorcode [list NETBOX HTTP $ncode] $resulttext + # set errorbody [lindex $resulttext 5] + # if {$errorbody ne ""} { + # if {[catch { + # set result [list {*}[lrange $resulttext 0 4] [::rest::format_auto $errorbody]] ;#crude detection of xml/json - REVIEW + # #if xml - we don't get a dict - but netbox shouldn't output that anyway. + # #would be better just to output raw? + # } errM]} { + # set result $resulttext + # } + # } else { + # set result $resulttext + # } + #} else { + # set result $resulttext + #} + #--------------------------------- + #if we try to pass any of the error results to the requested returntypes below - it would only make sense for JSON anyway? + set result $resulttext + set errorcode [list NETBOX $etype [lindex $resulttext 3]] + if {!$debug} { + #clean up our copy of the http token data even on error + array unset call_data $asynctoken,* + } + return -code error -errorcode $errorcode $result + } + + #This is another main reason we couldn't just use ::rest lib as-is. + #Servers sometimes respond with empty body + #::rest lib tries to parse empty string as JSON and just emits an ugly error + #we may do the same if one of the returntype branches fails - but at least the user has a choice. + if {$resulttext ne ""} { + #review - could get nest structure from xml - but not relevant to netbox + #parsing json could fail here too + set result [::rest::format_auto $resulttext] + } else { + set result "" + } + + if {!$debug} { + #clean up our copy of the http token data + array unset call_data $asynctoken,* + } + + #try catch - return errorcode: NETBOX OUTPUTFORMATTING ? + switch -exact -- $returntype { + showpagedict { + return [punk::lib::showdict $result !@@results @@results/@*/@*.@*] + } + showpagedict2 { + return [punk::lib::showdict $result @@results/@*/@*.@* !@@results] + } + showdict { + if {$command eq "::punk::netbox::status"} { + #we get duplicate django-version for %showdict% - todo - something. + return [punk::lib::showdict $result @@django-version @@installed-apps/@*.@* !@@installed-apps] + } + return [punk::lib::showdict $result *] + } + showdict2 { + if {$command eq "::punk::netbox::status"} { + #we get duplicate django-version for %showdict% - todo - something. + return [punk::lib::showdict $result @@installed-apps/@*.@* !@@installed-apps] + } + return [punk::lib::showdict $result */*] + } + showlist { + return [punk::lib::showdict -roottype list $result] + } + showlistofdicts { + return [punk::lib::showdict $result @*/@*.@*] + } + jsondump { + package require huddle::json + #pretty-print via huddle (inefficient review) + set h [huddle::json::json2huddle parse $result] + return [huddle::jsondump $h] + } + linelist { + set ret "" + foreach r $result { + append ret $r \n + } + return $ret + } + default { + #plain result: (list or dict) or json - the counterintuitive 'result' field set to raw above sets the rest call's resulting format to json + return $result + } + } + + } + + + proc _homedir {} { if {[info exists ::env(HOME)]} { set home [file normalize $::env(HOME)] @@ -970,7 +1511,6 @@ tcl::namespace::eval punk::netbox { punk::args::define {*}[list\ { - @dynamic @id -id ::punk::netbox::status @cmd -name punk::netbox::status -help\ "status_list @@ -989,9 +1529,29 @@ tcl::namespace::eval punk::netbox { }\ [set ::punk::netbox::argdoc::_RETURN_STATUS]\ { + -ASYNC -type none @values -min 0 -max 0 }] - ::punk::netbox::system::make_rest_func ::punk::netbox::status api/status/ -verb get -body none + ::punk::netbox::system::make_rest_func_async ::punk::netbox::status api/status/ -verb get -body none + + #test - to see what the create_interface procs look like with a callback + #(they are not flexible enough argument-wise for this usecase) + #with a static callback baked into the function - it also gives no way to reliably determine + #which request matched which call if there are concurrent calls in flight for the same function/endpoint especially if args are the same + #(order of completion could presumably not always match call order) + #presumably an http::wait is required on each call, and they must be processed in order? + set RESTAPI(getstatus) { + url https://www.netbox1.intx.com.au/api/status/ + method get + headers {Authorization {Token bogus} content-type "application/json"} + callback {::punk::netbox::system::rest_call_complete RESTAPI} + error-body false + opt_args {test etc} + req_args {force} + } + #namespace eval ::punk::netbox::spud {} + ::rest::create_interface ::punk::netbox::RESTAPI + # #test function - todo use punk::netbox::system::make_rest_func @@ -1118,7 +1678,7 @@ tcl::namespace::eval punk::netbox::dcim { [set ::punk::netbox::argdoc::_create_update_options]\ { -q -type string - -tag -type string + -tag -type string -multiple true -tag__n -type string }\ [set ::punk::netbox::argdoc::_tenant_options]\ @@ -1169,7 +1729,7 @@ tcl::namespace::eval punk::netbox::dcim { { @values -min 0 -max 0 }] - ::punk::netbox::system::make_rest_func ::punk::netbox::dcim::devices_list api/dcim/devices/ -verb get -body none + ::punk::netbox::system::make_rest_func_async ::punk::netbox::dcim::devices_list api/dcim/devices/ -verb get -body none } tcl::namespace::eval punk::netbox::ipam { @@ -1204,7 +1764,7 @@ tcl::namespace::eval punk::netbox::ipam { [set ::punk::netbox::argdoc::_create_update_options]\ { -q - -tag + -tag -type string -multiple true }\ [set ::punk::netbox::argdoc::_tenant_options]\ [set ::punk::netbox::argdoc::_region_options]\ @@ -1223,7 +1783,7 @@ tcl::namespace::eval punk::netbox::ipam { @values -min 0 -max 0 }] } - ::punk::netbox::system::make_rest_func ::punk::netbox::ipam::vrfs_list api/ipam/vrfs/ -verb get -body none + ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::vrfs_list api/ipam/vrfs/ -verb get -body none namespace eval argdoc { lappend PUNKARGS [list\ @@ -1249,7 +1809,7 @@ tcl::namespace::eval punk::netbox::ipam { "A unique integer value identifying this VRF" }] } - ::punk::netbox::system::make_rest_func ::punk::netbox::ipam::vrfs_read api/ipam/vrfs/{id}/ -verb get -body none + ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::vrfs_read api/ipam/vrfs/{id}/ -verb get -body none namespace eval argdoc { punk::args::define {*}[list\ @@ -1278,7 +1838,7 @@ tcl::namespace::eval punk::netbox::ipam { { -q -type string -help\ "Query prefixes by substring" - -tag + -tag -type string -multiple true }\ [set ::punk::netbox::argdoc::_tenant_options]\ [set ::punk::netbox::argdoc::_region_options]\ @@ -1315,7 +1875,7 @@ tcl::namespace::eval punk::netbox::ipam { @values -min 0 -max 0 }] } - ::punk::netbox::system::make_rest_func ::punk::netbox::ipam::prefixes_list api/ipam/prefixes/ -verb get -body none + ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::prefixes_list api/ipam/prefixes/ -verb get -body none namespace eval argdoc { punk::args::define {*}[list\ @@ -1341,7 +1901,7 @@ tcl::namespace::eval punk::netbox::ipam { "JSON string" }] } - ::punk::netbox::system::make_rest_func ::punk::netbox::ipam::prefixes_create api/ipam/prefixes/{id}/ -verb post -body required + ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::prefixes_create api/ipam/prefixes/{id}/ -verb post -body required namespace eval argdoc { punk::args::define {*}[list\ @@ -1367,7 +1927,7 @@ tcl::namespace::eval punk::netbox::ipam { "A unique integer value identifying this prefix" }] } - ::punk::netbox::system::make_rest_func ::punk::netbox::ipam::prefixes_read api/ipam/prefixes/{id}/ -verb get -body none + ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::prefixes_read api/ipam/prefixes/{id}/ -verb get -body none namespace eval argdoc { punk::args::define {*}[list\ @@ -1396,7 +1956,7 @@ tcl::namespace::eval punk::netbox::ipam { }\ ] } - ::punk::netbox::system::make_rest_func ::punk::netbox::ipam::prefixes_available-ips_list api/ipam/prefixes/{id}/available-ips/ -verb get -body none + ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::prefixes_available-ips_list api/ipam/prefixes/{id}/available-ips/ -verb get -body none namespace eval argdoc { punk::args::define {*}[list\ @@ -1446,7 +2006,7 @@ tcl::namespace::eval punk::netbox::ipam { }\ ] } - ::punk::netbox::system::make_rest_func ::punk::netbox::ipam::prefixes_available-ips_create api/ipam/prefixes/{id}/available-ips/ -verb post -body required + ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::prefixes_available-ips_create api/ipam/prefixes/{id}/available-ips/ -verb post -body required namespace eval argdoc { punk::args::define {*}[list\ @@ -1475,7 +2035,7 @@ tcl::namespace::eval punk::netbox::ipam { }\ ] } - ::punk::netbox::system::make_rest_func ::punk::netbox::ipam::prefixes_available-prefixes_list api/ipam/prefixes/{id}/available-prefixes/ -verb get -body none + ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::prefixes_available-prefixes_list api/ipam/prefixes/{id}/available-prefixes/ -verb get -body none namespace eval argdoc { punk::args::define {*}[list\ @@ -1509,7 +2069,7 @@ tcl::namespace::eval punk::netbox::ipam { }\ ] } - ::punk::netbox::system::make_rest_func ::punk::netbox::ipam::prefixes_available-prefixes_create api/ipam/prefixes/{id}/available-prefixes/ -verb post -body required + ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::prefixes_available-prefixes_create api/ipam/prefixes/{id}/available-prefixes/ -verb post -body required namespace eval argdoc { punk::args::define {*}[list\ @@ -1537,7 +2097,9 @@ tcl::namespace::eval punk::netbox::ipam { [set ::punk::netbox::argdoc::_create_update_options]\ { -q - -tag + -tag -multiple true -help\ + "If multiple -tag options supplied, + they are ANDed (review)" }\ [set ::punk::netbox::argdoc::_tenant_options]\ [set ::punk::netbox::argdoc::_region_options]\ @@ -1575,32 +2137,8 @@ tcl::namespace::eval punk::netbox::ipam { @values -min 0 -max 0 }] } - ::punk::netbox::system::make_rest_func ::punk::netbox::ipam::ip-addresses_list api/ipam/ip-addresses/ -verb get -body none + ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::ip-addresses_list api/ipam/ip-addresses/ -verb get -body none - namespace eval argdoc { - punk::args::define {*}[list\ - { - @dynamic - @id -id ::punk::netbox::ipam::ip-addresses_read - @cmd -name punk::netbox::ipam::ip-addresses_read -help\ - "ipam_ip-addresses_read - GET request for endpoint /ipam/ip-addresses/{id}/" - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${$DYN_CONTEXTNAMES}} - @opts - }\ - [set ::punk::netbox::argdoc::_RETURN_DICT]\ - { - @values -min 1 -max 1 - id -type integer - }] - } - ::punk::netbox::system::make_rest_func ::punk::netbox::ipam::ip-addresses_read api/ipam/ip-addresses/{id}/ -verb get -body none namespace eval argdoc { punk::args::define {*}[list\ @@ -1649,7 +2187,7 @@ tcl::namespace::eval punk::netbox::ipam { } }] } - ::punk::netbox::system::make_rest_func ::punk::netbox::ipam::ip-addresses_create api/ipam/ip-addresses/ -verb post -body required + ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::ip-addresses_create api/ipam/ip-addresses/ -verb post -body required namespace eval argdoc { punk::args::define {*}[list\ @@ -1698,7 +2236,61 @@ tcl::namespace::eval punk::netbox::ipam { } }] } - ::punk::netbox::system::make_rest_func ::punk::netbox::ipam::ip-addresses_bulk_partial_update api/ipam/ip-addresses/ -verb patch -body required + ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::ip-addresses_bulk_partial_update api/ipam/ip-addresses/ -verb patch -body required + + namespace eval argdoc { + punk::args::define {*}[list\ + { + @dynamic + @id -id ::punk::netbox::ipam::ip-addresses_read + @cmd -name punk::netbox::ipam::ip-addresses_read -help\ + "ipam_ip-addresses_read + GET request for endpoint /ipam/ip-addresses/{id}/" + @leaders -min 1 -max 1 + apicontextid -help\ + "The name of the stored api context to use. + A contextid can be created in-memory using + api_context_create, or loaded from a .toml + file using api_context_load."\ + -choices {${$DYN_CONTEXTNAMES}} + @opts + }\ + [set ::punk::netbox::argdoc::_RETURN_DICT]\ + { + @values -min 1 -max 1 + id -type integer + }] + } + ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::ip-addresses_read api/ipam/ip-addresses/{id}/ -verb get -body none + + namespace eval argdoc { + punk::args::define {*}[list\ + { + @dynamic + @id -id ::punk::netbox::ipam::ip-addresses_delete + @cmd -name punk::netbox::ipam::ip-addresses_delete\ + -summary\ + "Delete an IP address by id."\ + -help\ + "ipam_ip-addresses_delete + DELETE request for endpoint /ipam/ip-addresses/{id}/" + @leaders -min 1 -max 1 + apicontextid -help\ + "The name of the stored api context to use. + A contextid can be created in-memory using + api_context_create, or loaded from a .toml + file using api_context_load."\ + -choices {${$DYN_CONTEXTNAMES}} + @opts + }\ + [set ::punk::netbox::argdoc::_RETURN_DICT]\ + { + @values -min 1 -max 1 + id -type integer + }] + } + ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::ip-addresses_delete api/ipam/ip-addresses/{id}/ -verb delete -body none + } @@ -1736,7 +2328,9 @@ tcl::namespace::eval punk::netbox::tenancy { [set ::punk::netbox::argdoc::_create_update_options]\ { -q -type string - -tag -type string + -tag -type string -multiple true -help\ + "If multiple -tag options supplied, + they are ANDed (review)" -tag__n -type string }\ [set ::punk::netbox::argdoc::_contact_options]\ @@ -1755,7 +2349,7 @@ tcl::namespace::eval punk::netbox::tenancy { }] } - ::punk::netbox::system::make_rest_func ::punk::netbox::tenancy::tenants_list api/tenancy/tenants/ -verb get -body none + ::punk::netbox::system::make_rest_func_async ::punk::netbox::tenancy::tenants_list api/tenancy/tenants/ -verb get -body none } tcl::namespace::eval punk::netbox::virtualization { @@ -1797,7 +2391,7 @@ tcl::namespace::eval punk::netbox::virtualization { [set ::punk::netbox::argdoc::_create_update_options]\ { -q - -tag + -tag -multiple true }\ [set ::punk::netbox::argdoc::_tenant_options]\ [set ::punk::netbox::argdoc::_contact_options]\ @@ -1837,7 +2431,7 @@ tcl::namespace::eval punk::netbox::virtualization { }] } - ::punk::netbox::system::make_rest_func ::punk::netbox::virtualization::virtual-machines_list api/virtualization/virtual-machines/ -verb get -body none + ::punk::netbox::system::make_rest_func_async ::punk::netbox::virtualization::virtual-machines_list api/virtualization/virtual-machines/ -verb get -body none namespace eval argdoc { lappend PUNKARGS [list\ @@ -1858,22 +2452,23 @@ tcl::namespace::eval punk::netbox::virtualization { }\ [set ::punk::netbox::argdoc::_RETURN_DICT]\ { - @values -min 2 -max 2 - id -type integer -help\ - "A unique integer value identifying this virtual machine" + @values -min 1 -max 1 body -type string -help\ "JSON string" }] } - ::punk::netbox::system::make_rest_func ::punk::netbox::virtualization::virtual-machines_create api/virtualization/virtual-machines/ -verb post -body required + ::punk::netbox::system::make_rest_func_async ::punk::netbox::virtualization::virtual-machines_create api/virtualization/virtual-machines/ -verb post -body required namespace eval argdoc { lappend PUNKARGS [list\ { @dynamic - @id -id ::punk::netbox::virtualization::virtual-machines_delete - @cmd -name punk::netbox::virtualization::virtual-machines_delete -help\ - "virtualization_virtual-machines_delete + @id -id ::punk::netbox::virtualization::virtual-machines_bulk_delete + @cmd -name punk::netbox::virtualization::virtual-machines_bulk_delete\ + -summary\ + "DELETE *ALL* virtual machines."\ + -help\ + "virtualization_virtual-machines_bulk_delete DELETE request for endpoint /virtualization/virtual-machines/ HTTP code: 204 " @@ -1886,13 +2481,13 @@ tcl::namespace::eval punk::netbox::virtualization { -choices {${$DYN_CONTEXTNAMES}} @opts -FORCE -default 0 -type boolean -help\ - "Set to true to BULK delete all items at this endpoint" + "Set to true to BULK delete *all* items at this endpoint" }\ { @values -min 0 -max 0 }] } - ::punk::netbox::system::make_rest_func ::punk::netbox::virtualization::virtual-machines_delete api/virtualization/virtual-machines/ -verb delete -body none + ::punk::netbox::system::make_rest_func_async ::punk::netbox::virtualization::virtual-machines_bulk_delete api/virtualization/virtual-machines/ -verb delete -body none namespace eval argdoc { lappend PUNKARGS [list\ @@ -1918,7 +2513,7 @@ tcl::namespace::eval punk::netbox::virtualization { "A unique integer value identifying this virtual machine" }] } - ::punk::netbox::system::make_rest_func ::punk::netbox::virtualization::virtual-machines_read api/virtualization/virtual-machines/{id}/ -verb get -body none + ::punk::netbox::system::make_rest_func_async ::punk::netbox::virtualization::virtual-machines_read api/virtualization/virtual-machines/{id}/ -verb get -body none namespace eval argdoc { lappend PUNKARGS [list\ @@ -1946,7 +2541,285 @@ tcl::namespace::eval punk::netbox::virtualization { "JSON string" }] } - ::punk::netbox::system::make_rest_func ::punk::netbox::virtualization::virtual-machines_update api/virtualization/virtual-machines/{id}/ -verb put -body required + ::punk::netbox::system::make_rest_func_async ::punk::netbox::virtualization::virtual-machines_update api/virtualization/virtual-machines/{id}/ -verb put -body required + + namespace eval argdoc { + lappend PUNKARGS [list\ + { + @dynamic + @id -id ::punk::netbox::virtualization::virtual-machines_delete + @cmd -name punk::netbox::virtualization::virtual-machines_delete\ + -summary\ + "Delete a single VM by id."\ + -help\ + "virtualization_virtual-machines_delete + DELETE request for endpoint /virtualization/virtual-machines/{id}" + @leaders -min 1 -max 1 + apicontextid -help\ + "The name of the stored api context to use. + A contextid can be created in-memory using + api_context_create, or loaded from a .toml + file using api_context_load."\ + -choices {${$DYN_CONTEXTNAMES}} + @opts + }\ + [set ::punk::netbox::argdoc::_RETURN_DICT]\ + { + @values -min 1 -max 1 + id -type integer -help\ + "A unique integer value identifying the virtual machine + to DELETE." + }] + } + ::punk::netbox::system::make_rest_func_async ::punk::netbox::virtualization::virtual-machines_delete api/virtualization/virtual-machines/{id}/ -verb delete -body none + +} + +tcl::namespace::eval punk::netbox::extras { + namespace export {[a-z]*} + + namespace eval argdoc { + variable PUNKARGS + variable DYN_CONTEXTNAMES + set DYN_CONTEXTNAMES {${[punk::netbox::api_context_names]}} + lappend PUNKARGS [list\ + { + @dynamic + @id -id ::punk::netbox::extras::tags_list + @cmd -name punk::netbox::extras::tags_list -help\ + "extras_tags_list + GET request for endpoint /extras/tags/" + @leaders -min 1 -max 1 + apicontextid -help\ + "The name of the stored api context to use. + A contextid can be created in-memory using + api_context_create, or loaded from a .toml + file using api_context_load."\ + -choices {${$DYN_CONTEXTNAMES}} + @opts + -id -type integer + -ID_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_number_filter_help}} + -name + -NAME_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_name_filter_help}} + -slug -type string + -color -type string + -description -type string + }\ + [set ::punk::netbox::argdoc::_create_update_options]\ + { + -q + }\ + [set ::punk::netbox::argdoc::_page_options]\ + [set ::punk::netbox::argdoc::_CUSTOM_PARAMS]\ + [set ::punk::netbox::argdoc::_RETURN_PAGEDICT]\ + { + -ASYNC -type none + @values -min 0 -max 0 + }] + } + + ::punk::netbox::system::make_rest_func_async ::punk::netbox::extras::tags_list api/extras/tags/ -verb get -body none + + namespace eval argdoc { + lappend PUNKARGS [list\ + { + @dynamic + @id -id ::punk::netbox::extras::tags_create + @cmd -name punk::netbox::extras::tags_create -help\ + "extras_tags_create + GET request for endpoint /extras/tags/" + @leaders -min 1 -max 1 + apicontextid -help\ + "The name of the stored api context to use. + A contextid can be created in-memory using + api_context_create, or loaded from a .toml + file using api_context_load."\ + -choices {${$DYN_CONTEXTNAMES}} + @opts + }\ + [set ::punk::netbox::argdoc::_RETURN_DICT]\ + { + -ASYNC -type none + @values -min 1 -max 1 + body -type string -help\ + "JSON string" + }] + } + ::punk::netbox::system::make_rest_func_async ::punk::netbox::extras::tags_create api/extras/tags/ -verb post -body required + + + set RESTAPI(create) { + url https://www.netbox1.intx.com.au/tags/create/ + method post + headers {Authorization {Token Bogus} content-type "application/json"} + callback {::punk::netbox::system::rest_call_complete RESTAPI} + body required + error-body false + } + ::rest::create_interface ::punk::netbox::extras::RESTAPI + + namespace eval argdoc { + lappend PUNKARGS [list\ + { + @dynamic + @id -id ::punk::netbox::extras::tags_bulk_delete + @cmd -name punk::netbox::extras::tags_bulk_delete -help\ + "extras_tags_bulk_delete + DELETE request for endpoint /extras/tags/ + HTTP code: 204 + " + @leaders -min 1 -max 1 + apicontextid -help\ + "The name of the stored api context to use. + A contextid can be created in-memory using + api_context_create, or loaded from a .toml + file using api_context_load."\ + -choices {${$DYN_CONTEXTNAMES}} + @opts + -FORCE -default 0 -type boolean -help\ + "Set to true to BULK delete all items at this endpoint" + }\ + { + -ASYNC -type none + @values -min 0 -max 0 + }] + } + ::punk::netbox::system::make_rest_func_async ::punk::netbox::extras::tags_bulk_delete api/extras/tags/ -verb delete -body none + + namespace eval argdoc { + lappend PUNKARGS [list\ + { + @dynamic + @id -id ::punk::netbox::extras::tags_read + @cmd -name punk::netbox::extras::tags_read -help\ + "extras_tags_read + GET request for endpoint /extras/tags/{id}" + @leaders -min 1 -max 1 + apicontextid -help\ + "The name of the stored api context to use. + A contextid can be created in-memory using + api_context_create, or loaded from a .toml + file using api_context_load."\ + -choices {${$DYN_CONTEXTNAMES}} + @opts + }\ + [set ::punk::netbox::argdoc::_RETURN_DICT]\ + { + @values -min 1 -max 1 + id -type integer -help\ + "A unique integer value identifying this tag" + }] + } + ::punk::netbox::system::make_rest_func_async ::punk::netbox::extras::tags_read api/extras/tags/{id}/ -verb get -body none + + namespace eval argdoc { + lappend PUNKARGS [list\ + { + @dynamic + @id -id ::punk::netbox::extras::tags_update + @cmd -name punk::netbox::extras::tags_update -help\ + "extras_tags_update + PUT request for endpoint /extras/tags/{id} + A PUT update is a complete update of the tag. + It will reset any non-supplied fields + in the body to their default" + @leaders -min 1 -max 1 + apicontextid -help\ + "The name of the stored api context to use. + A contextid can be created in-memory using + api_context_create, or loaded from a .toml + file using api_context_load."\ + -choices {${$DYN_CONTEXTNAMES}} + @opts + }\ + [set ::punk::netbox::argdoc::_RETURN_DICT]\ + { + @values -min 2 -max 2 + id -type integer -help\ + "A unique integer value identifying this tag" + body -type string -help\ + "JSON string" + }] + } + ::punk::netbox::system::make_rest_func_async ::punk::netbox::extras::tags_update api/extras/tags/{id}/ -verb put -body required + + + #namespace eval argdoc { + # lappend PUNKARGS [list\ + # { + # @dynamic + # @id -id ::punk::netbox::extras::tags_delete + # @cmd -name punk::netbox::extras::tags_delete -help\ + # "extras_tags_delete + # DELETE request for endpoint /extras/tags/{id} + # Deletes a specific tag identified by id." + # @leaders -min 1 -max 1 + # apicontextid -help\ + # "The name of the stored api context to use. + # A contextid can be created in-memory using + # api_context_create, or loaded from a .toml + # file using api_context_load."\ + # -choices {${$DYN_CONTEXTNAMES}} + # @opts + # }\ + # [set ::punk::netbox::argdoc::_RETURN_DICT]\ + # { + # @values -min 1 -max 1 + # id -type integer -help\ + # "A unique integer value identifying this tag" + # }] + #} + # 204 response with no JSON data on success + #The rest library will try to parse the empty string as json and fail + #it can parse "{}" but not "" + + + #404 for invalid id: + #allow: GET,PUT,PATCH,DELETE,HEAD,OPTIONS + #content-length: 23 + #content-type: application/json + #cross-origin-opener-policy: same-origin + #date: Tue,11 Nov 2025 17:46:04 GMT + #referrer-policy: same-origin + #server: nginx/1.26.2 + #vary: Accept,Cookie,Origin + #x-content-type-options: nosniff + #x-frame-options: SAMEORIGIN + + #{ + # "detail": "Not found." + #} + #::punk::netbox::system::make_rest_func ::punk::netbox::extras::tags_delete api/extras/tags/{id}/ -verb delete -body none + + + namespace eval argdoc { + lappend PUNKARGS [list\ + { + @dynamic + @id -id ::punk::netbox::extras::tags_delete + @cmd -name punk::netbox::extras::tags_delete -help\ + "extras_tags_delete + DELETE request for endpoint /extras/tags/{id} + Deletes a specific tag identified by id." + @leaders -min 1 -max 1 + apicontextid -help\ + "The name of the stored api context to use. + A contextid can be created in-memory using + api_context_create, or loaded from a .toml + file using api_context_load."\ + -choices {${$DYN_CONTEXTNAMES}} + @opts + }\ + [set ::punk::netbox::argdoc::_RETURN_DICT]\ + { + -ASYNC -type none + @values -min 1 -max 1 + id -type integer -help\ + "A unique integer value identifying this tag" + }] + } + ::punk::netbox::system::make_rest_func_async ::punk::netbox::extras::tags_delete api/extras/tags/{id}/ -verb delete -body none + } @@ -2085,7 +2958,8 @@ namespace eval ::punk::args::register { ::punk::netbox::dcim\ ::punk::netbox::ipam\ ::punk::netbox::tenancy\ - ::punk::netbox::virtualization + ::punk::netbox::virtualization\ + ::punk::netbox::extras\ } # ----------------------------------------------------------------------------- diff --git a/src/modules/punk/netbox/man-999999.0a1.0.tm b/src/modules/punk/netbox/man-999999.0a1.0.tm index 347b311b..a1851799 100644 --- a/src/modules/punk/netbox/man-999999.0a1.0.tm +++ b/src/modules/punk/netbox/man-999999.0a1.0.tm @@ -66,38 +66,6 @@ package require rest #*** !doctools #[section API] -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# oo::class namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#tcl::namespace::eval punk::netbox::man::class { - #*** !doctools - #[subsection {Namespace punk::netbox::man::class}] - #[para] class definitions - #if {[tcl::info::commands [tcl::namespace::current]::interface_sample1] eq ""} { - #*** !doctools - #[list_begin enumerated] - - # oo::class create interface_sample1 { - # #*** !doctools - # #[enum] CLASS [class interface_sample1] - # #[list_begin definitions] - - # method test {arg1} { - # #*** !doctools - # #[call class::interface_sample1 [method test] [arg arg1]] - # #[para] test method - # puts "test: $arg1" - # } - - # #*** !doctools - # #[list_end] [comment {-- end definitions interface_sample1}] - # } - - #*** !doctools - #[list_end] [comment {--- end class enumeration ---}] - #} -#} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ tcl::namespace::eval punk::netbox::man { variable PUNKARGS @@ -926,6 +894,277 @@ tcl::namespace::eval punk::netbox::man::virtualization { } +tcl::namespace::eval punk::netbox::man::extras { + namespace export {[a-z]*} + namespace ensemble create -parameters {apicontextid} + variable PUNKARGS + + tcl::namespace::eval tags { + namespace export {[a-z]*} + namespace ensemble create -parameters {apicontextid} + variable PUNKARGS + namespace eval argdoc { + variable PUNKARGS + set DYN_CONTEXTNAMES {${[punk::netbox::api_context_names]}} + } + + namespace eval argdoc { + lappend PUNKARGS [list\ + {@dynamic}\ + { + @id -id ::punk::netbox::man::extras::tags::create + @cmd -name punk::netbox::man::extras::tags::create -help\ + "extras_tags_create + POST request for endpoint /extras/tags/" + @leaders -min 1 -max 1 + apicontextid -help\ + "The name of the stored api context to use. + A contextid can be created in-memory using + api_context_create, or loaded from a .toml + file using api_context_load."\ + -choices {${$DYN_CONTEXTNAMES}} + @opts + -name -type string -minsize 1 -maxsize 100 -optional 0 + -slug -type string -minsize 1 -maxsize 100 -optional 1 + #todo - combined xdigit and lower?? + -color -type xdigit -maxsize 6 -optional 1 -help\ + "Default will be assigned by netbox. + e.g 9e9e9e" + -description -type string -maxsize 200 -default "" + }\ + {-RETURN -default table -choices {table tableobject list linelist}}\ + { + @values -min 0 -max 0 + } + ] + } + + #example body + # color must be a lower-cased hex string (6 digits) + # e.g a red tag + # { + # "name": "my_tag", + # "slug": "my_tag", + # "color": "ff0000", + # "description": "testing tag creation" + # } + + #example 201 response + #{ + # "id": 14, + # "url": "https://www.netbox1.intx.com.au/api/extras/tags/14/", + # "display": "jjj", + # "name": "jjj", + # "slug": "jjj", + # "color": "ff0000", + # "description": "j testing", + # "created": "2025-11-11T16:33:17.461484Z", + # "last_updated": "2025-11-11T16:33:17.461500Z" + #} + + #example 400 response + #{ + # "name": [ + # "tag with this name already exists." + # ], + # "slug": [ + # "tag with this slug already exists." + # ] + #} + + proc create {args} { + set argd [punk::args::parse $args withid "::punk::netbox::man::extras::tags::create"] + lassign [dict values $argd] leaders opts values received + set token [dict get $argd leaders apicontextid] + + set map [::list \" "\\\"" \\ \\\\ \r \\r \n \\n \t \\t \b \\b \f \\f] ;#review - unicode? tcllib? + set name [dict get $argd opts -name] + if {[dict exists $received -slug]} { + set slug [dict get $opts -slug] + } else { + set slug $name + } + set description [dict get $argd opts -description] + + #Escape for JSON + set name [string map $map $name] + set slug [string map $map $slug] + set description [string map $map $description] + + + set body "\{\n" + append body " \"name\": \"$name\"," \n + append body " \"slug\": \"$slug\"," \n + if {[dict exists $received -color]} { + append body " \"color\": \"[dict get $opts -color]\"," \n + } + append body " \"description\": \"$description\"" \n + append body "\}" \n + + puts "Post body JSON:" + puts $body + #todo RETURN + set resultd [punk::netbox::extras::tags_create $token -RETURN dict $body] + + + + } + + namespace eval argdoc { + lappend PUNKARGS [list\ + {@dynamic}\ + { + @id -id ::punk::netbox::man::extras::tags::delete + @cmd -name punk::netbox::man::extras::tags::delete\ + -summary\ + "Delete one tag by id."\ + -help\ + "extras_tags_delete + DELETE request for endpoint /extras/tags/{id} + Delete a single tag by id." + @leaders -min 1 -max 1 + apicontextid -help\ + "The name of the stored api context to use. + A contextid can be created in-memory using + api_context_create, or loaded from a .toml + file using api_context_load."\ + -choices {${$DYN_CONTEXTNAMES}} + @opts + @values -min 1 -max 1 + id -type integer -range {0 ""} + } + ] + } + proc delete {args} { + set argd [punk::args::parse $args withid "::punk::netbox::man::extras::tags::delete"] + lassign [dict values $argd] leaders opts values received + set token [dict get $argd leaders apicontextid] + set id [dict get $argd values id] + punk::netbox::extras::tags_delete $token -RETURN dict $id + return done + } + + + namespace eval argdoc { + + lappend PUNKARGS [::list\ + {@dynamic}\ + [punk::args::resolved_def\ + -antiglobs {@leaders @values -RETURN}\ + -override { + @id {-id "::punk::netbox::man::extras::tags::list" } + apicontextid {-choices {${$DYN_CONTEXTNAMES}} } + }\ + ::punk::netbox::extras::tags_list\ + ]\ + {-RETURN -default table -choices {table tableobject list linelist}}\ + {-MAXRESULTS -type integer -default -1}\ + {@values -min 0 -max 0}\ + ] + } + proc list {args} { + set argd [punk::args::parse $args withid "::punk::netbox::man::extras::tags::list"] + + set urlnext "" + set requests_allowed 1000 ;#Sanity check - consider making an option - review + set resultlist [::list] + set token [dict get $argd leaders apicontextid] + set opts [dict get $argd opts] + set vals [dict get $argd values] + set multis [dict get $argd multis] + set outer_return [dict get $opts -RETURN] + set opts [dict remove $opts -RETURN] ;#opts from punk::args::parse is a dict (no dup keys) - can use 'dict remove' safely + set maxresults [dict get $opts -MAXRESULTS] + set opts [dict remove $opts -MAXRESULTS] + set initial_pagelimit [dict get $opts -limit] + #we can't just pass through 'multi' opts even if only one was supplied - list level is wrong + set nextopts [::list] + dict for {opt val} $opts { + if {$opt ni $multis} { + lappend nextopts $opt $val + } else { + foreach v $val { + lappend nextopts $opt $v + } + } + } + #Now opts is a list with possible repeated options! (for flags that have -multiple true) + + if {$maxresults == -1} { + set maxresults $initial_pagelimit + } + if {$maxresults < $initial_pagelimit} { + punk::netbox::man::system::dupkeylist_setfirst nextopts -limit $maxresults + } + set to_go [expr {$maxresults - [llength $resultlist]}] + while {$urlnext ne "null"} { + if {$urlnext ne ""} { + set urlnext_params [punk::netbox::man::system::uri_get_querystring_as_keyval_list $urlnext] + if {[punk::netbox::man::system::dupkeylist_getfirst $nextopts -limit] > $to_go} { + punk::netbox::man::system::dupkeylist_setfirst urlnext_params limit $to_go + } + punk::netbox::man::system::optionlistvar_sync_from_urlparams nextopts $urlnext_params + } + puts "-->next:$urlnext nextopts:$nextopts vals:$vals" + set resultd [punk::netbox::extras::tags_list $token {*}$nextopts -RETURN dict {*}$vals] + set urlnext [dict get $resultd next] + set batch [dict get $resultd results] + lappend resultlist {*}$batch + + set to_go [expr {$maxresults - [llength $resultlist]}] + if {$to_go <= 0} {break} + incr requests_allowed -1 + if {$requests_allowed < 1} {break} + } + + if {$outer_return in {table tableobject}} { + package require textblock + set t [textblock::list_as_table -return tableobject -colheaders {id name slug color sample description}] + foreach tag $resultlist { + set name [dict get $tag name] + set rgb [dict get $tag color] + if {[string length $rgb] && [string length $rgb] == 6} { + set sample "[a+ Rgb#$rgb rgb#$rgb-contrasting] $name [a]" + } else { + set sample "" + } + + set r [::list\ + [dict get $tag id]\ + [dict get $tag name]\ + [dict get $tag slug]\ + $rgb\ + $sample\ + [dict get $tag description]\ + ] + $t add_row $r + } + } + switch -- $outer_return { + table { + set result [$t print] + $t destroy + return $result + } + tableobject { + return $t + } + linelist { + set ret "" + foreach r $resultlist { + append ret $r \n + } + return $ret + } + default { + return $resultlist + } + } + } + + } + +} tcl::namespace::eval punk::netbox::man::ip-addresses { namespace export {[a-z]*} @@ -1308,6 +1547,8 @@ namespace eval ::punk::args::register { ::punk::netbox::man::tenancy::tenants\ ::punk::netbox::man::virtualization\ ::punk::netbox::man::virtualization::virtual-machines\ + ::punk::netbox::man::extras\ + ::punk::netbox::man::extras::tags\ } # ----------------------------------------------------------------------------- diff --git a/src/modules/punk/ns-999999.0a1.0.tm b/src/modules/punk/ns-999999.0a1.0.tm index 13155d9d..a9066e8e 100644 --- a/src/modules/punk/ns-999999.0a1.0.tm +++ b/src/modules/punk/ns-999999.0a1.0.tm @@ -2581,9 +2581,10 @@ y" {return quirkykeyscript} #eval_base has been set by previous source or proc #It can also be set by previous eval - *if* a non default offset was returned by _cmdtrace_get_eval_offset + set eval_base [dict get $linedict $target eval_base] set eval_offset [dict get $linedict $target eval_offset] - set line [expr {$prevline + ($eval_offset-1) + ($traceline-1)}] - #puts "stack-- $callinfo" + set line [expr {$eval_base + ($eval_offset-1) + ($traceline-1)}] + puts "stack-- $callinfo" puts " step type: eval traceline: $traceline -- " if {[dict exists $callinfo cmd]} { #set cmd [string trim [dict get $callinfo cmd]] @@ -2653,9 +2654,11 @@ y" {return quirkykeyscript} } #puts "-- $callinfo" } else { - puts ">>step type: $type (nontargeted proc)>> $callinfo" + ::tcl::dict::incr tinfo($target) subcmds + #puts ">>step type: $type (nontargeted proc)>> $callinfo" } } else { + ::tcl::dict::incr tinfo($target) subcmds #todo - handle type 'source' and type 'eval' with keys 'method' 'class' (oo) #puts ------------------------- #puts ">[dict get $callinfo cmd]" @@ -2708,7 +2711,6 @@ y" {return quirkykeyscript} variable tinfo array unset tinfo variable _cmdtrace_disabled - set _cmdtrace_disabled false set argd [punk::args::parse $args -cache 1 withid ::punk::ns::cmdtrace] lassign [dict values $argd] leaders opts values received @@ -2718,6 +2720,8 @@ y" {return quirkykeyscript} set cinfo [uplevel 1 [list ::punk::ns::cmdinfo {*}$cmdargs]] set origin [dict get $cinfo origin] set arglist [dict get $cinfo args_remaining] + set origin_nscmd [nstail $origin] + set origin_ns [nsprefix $origin] if {[dict exists $received -target]} { set targets [dict get $opts -target] @@ -2740,15 +2744,16 @@ y" {return quirkykeyscript} } #don't raise the error when no -target supplied - as our launch command can contain extra arguments } - lappend resolved_targets $tgt_cmd + set nscmd [nstail $tgt_cmd] + set ns [nsprefix $tgt_cmd] + + lappend resolved_targets $tgt_cmd $ns $nscmd ::tcl::dict::set ::punk::ns::linedict $tgt_cmd [::tcl::dict::create eval_base 1 eval_offset 1 lines {} cmdtype $tgt_type successcalls 0 errorcalls 0] } #if the target command has a leading colon (e.g expr alternative :) we can't put a trace directly on a fully qualified name with a triple colon such as ::: #we will need to evaluate in the namespace - foreach tgt_cmd $resolved_targets { - set nscmd [nstail $tgt_cmd] - set ns [nsprefix $tgt_cmd] + foreach {tgt_cmd ns nscmd} $resolved_targets { puts "tracing target: $tgt_cmd whilst running: $origin $arglist" ::tcl::namespace::eval $ns [list ::trace add execution $nscmd enter [list ::punk::ns::_cmdtrace_enter ::punk::ns::linedict $tgt_cmd]] @@ -2758,17 +2763,17 @@ y" {return quirkykeyscript} try { - set origin_nscmd [nstail $origin] - set origin_ns [nsprefix $origin] #uplevel 1 [list $origin {*}$arglist] + set _cmdtrace_disabled false ::tcl::namespace::eval $origin_ns [list $origin_nscmd {*}$arglist] } trap {} {errMsg errOptions} { + set _cmdtrace_disabled true + #(even a puts can involve function calls - e.g in contexts where there are stacked channels) puts stderr "command error $errMsg" } finally { - foreach tgt_cmd $resolved_targets { - set nscmd [nstail $tgt_cmd] - set ns [nsprefix $tgt_cmd] + set _cmdtrace_disabled true + foreach {tgt_cmd ns nscmd} $resolved_targets { ::tcl::namespace::eval $ns [list ::trace remove execution $nscmd enterstep [list ::punk::ns::_cmdtrace_enterstep ::punk::ns::linedict $tgt_cmd]] ::tcl::namespace::eval $ns [list ::trace remove execution $nscmd enter [list ::punk::ns::_cmdtrace_enter ::punk::ns::linedict $tgt_cmd]] ::tcl::namespace::eval $ns [list ::trace remove execution $nscmd leave [list ::punk::ns::_cmdtrace_leave ::punk::ns::linedict $tgt_cmd]] @@ -2780,7 +2785,7 @@ y" {return quirkykeyscript} append final_display \n #todo - foreach tgt_cmd in resolved_targets? - foreach tgt_cmd $resolved_targets { + foreach {tgt_cmd _ _} $resolved_targets { set lines [dict get $linedict $tgt_cmd lines] if {[llength $lines]} { set procbody [punk::ns::corp -n $tgt_cmd] diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/args-0.2.1.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/args-0.2.1.tm index 0dd454e0..e2afc619 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/args-0.2.1.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/args-0.2.1.tm @@ -6481,7 +6481,7 @@ tcl::namespace::eval punk::args { } if {[dict exists $thisarg_checks -maxsize]} { set maxsize [dict get $thisarg_checks -maxsize] - if {$checkval ne "-1"} { + if {$maxsize ne "-1"} { if {[tcl::string::length $e_check] > $maxsize} { set msg "$argclass '$argname' for %caller% requires string with -maxsize $maxsize. Received len:[tcl::string::length $e_check] value:'$e_check'" #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg @@ -6679,6 +6679,7 @@ tcl::namespace::eval punk::args { upper - wordchar - xdigit { + #todo - combined types xdigit && lower ?? set-theoretic types? how? if {![tcl::string::is $type -strict $e_check]} { set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e'" lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm index 9a42ad0b..51350674 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm @@ -2581,9 +2581,10 @@ y" {return quirkykeyscript} #eval_base has been set by previous source or proc #It can also be set by previous eval - *if* a non default offset was returned by _cmdtrace_get_eval_offset + set eval_base [dict get $linedict $target eval_base] set eval_offset [dict get $linedict $target eval_offset] - set line [expr {$prevline + ($eval_offset-1) + ($traceline-1)}] - #puts "stack-- $callinfo" + set line [expr {$eval_base + ($eval_offset-1) + ($traceline-1)}] + puts "stack-- $callinfo" puts " step type: eval traceline: $traceline -- " if {[dict exists $callinfo cmd]} { #set cmd [string trim [dict get $callinfo cmd]] @@ -2653,9 +2654,11 @@ y" {return quirkykeyscript} } #puts "-- $callinfo" } else { - puts ">>step type: $type (nontargeted proc)>> $callinfo" + ::tcl::dict::incr tinfo($target) subcmds + #puts ">>step type: $type (nontargeted proc)>> $callinfo" } } else { + ::tcl::dict::incr tinfo($target) subcmds #todo - handle type 'source' and type 'eval' with keys 'method' 'class' (oo) #puts ------------------------- #puts ">[dict get $callinfo cmd]" @@ -2708,7 +2711,6 @@ y" {return quirkykeyscript} variable tinfo array unset tinfo variable _cmdtrace_disabled - set _cmdtrace_disabled false set argd [punk::args::parse $args -cache 1 withid ::punk::ns::cmdtrace] lassign [dict values $argd] leaders opts values received @@ -2718,6 +2720,8 @@ y" {return quirkykeyscript} set cinfo [uplevel 1 [list ::punk::ns::cmdinfo {*}$cmdargs]] set origin [dict get $cinfo origin] set arglist [dict get $cinfo args_remaining] + set origin_nscmd [nstail $origin] + set origin_ns [nsprefix $origin] if {[dict exists $received -target]} { set targets [dict get $opts -target] @@ -2740,15 +2744,16 @@ y" {return quirkykeyscript} } #don't raise the error when no -target supplied - as our launch command can contain extra arguments } - lappend resolved_targets $tgt_cmd + set nscmd [nstail $tgt_cmd] + set ns [nsprefix $tgt_cmd] + + lappend resolved_targets $tgt_cmd $ns $nscmd ::tcl::dict::set ::punk::ns::linedict $tgt_cmd [::tcl::dict::create eval_base 1 eval_offset 1 lines {} cmdtype $tgt_type successcalls 0 errorcalls 0] } #if the target command has a leading colon (e.g expr alternative :) we can't put a trace directly on a fully qualified name with a triple colon such as ::: #we will need to evaluate in the namespace - foreach tgt_cmd $resolved_targets { - set nscmd [nstail $tgt_cmd] - set ns [nsprefix $tgt_cmd] + foreach {tgt_cmd ns nscmd} $resolved_targets { puts "tracing target: $tgt_cmd whilst running: $origin $arglist" ::tcl::namespace::eval $ns [list ::trace add execution $nscmd enter [list ::punk::ns::_cmdtrace_enter ::punk::ns::linedict $tgt_cmd]] @@ -2758,17 +2763,17 @@ y" {return quirkykeyscript} try { - set origin_nscmd [nstail $origin] - set origin_ns [nsprefix $origin] #uplevel 1 [list $origin {*}$arglist] + set _cmdtrace_disabled false ::tcl::namespace::eval $origin_ns [list $origin_nscmd {*}$arglist] } trap {} {errMsg errOptions} { + set _cmdtrace_disabled true + #(even a puts can involve function calls - e.g in contexts where there are stacked channels) puts stderr "command error $errMsg" } finally { - foreach tgt_cmd $resolved_targets { - set nscmd [nstail $tgt_cmd] - set ns [nsprefix $tgt_cmd] + set _cmdtrace_disabled true + foreach {tgt_cmd ns nscmd} $resolved_targets { ::tcl::namespace::eval $ns [list ::trace remove execution $nscmd enterstep [list ::punk::ns::_cmdtrace_enterstep ::punk::ns::linedict $tgt_cmd]] ::tcl::namespace::eval $ns [list ::trace remove execution $nscmd enter [list ::punk::ns::_cmdtrace_enter ::punk::ns::linedict $tgt_cmd]] ::tcl::namespace::eval $ns [list ::trace remove execution $nscmd leave [list ::punk::ns::_cmdtrace_leave ::punk::ns::linedict $tgt_cmd]] @@ -2780,7 +2785,7 @@ y" {return quirkykeyscript} append final_display \n #todo - foreach tgt_cmd in resolved_targets? - foreach tgt_cmd $resolved_targets { + foreach {tgt_cmd _ _} $resolved_targets { set lines [dict get $linedict $tgt_cmd lines] if {[llength $lines]} { set procbody [punk::ns::corp -n $tgt_cmd] diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/args-0.2.1.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/args-0.2.1.tm index 0dd454e0..e2afc619 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/args-0.2.1.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/args-0.2.1.tm @@ -6481,7 +6481,7 @@ tcl::namespace::eval punk::args { } if {[dict exists $thisarg_checks -maxsize]} { set maxsize [dict get $thisarg_checks -maxsize] - if {$checkval ne "-1"} { + if {$maxsize ne "-1"} { if {[tcl::string::length $e_check] > $maxsize} { set msg "$argclass '$argname' for %caller% requires string with -maxsize $maxsize. Received len:[tcl::string::length $e_check] value:'$e_check'" #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg @@ -6679,6 +6679,7 @@ tcl::namespace::eval punk::args { upper - wordchar - xdigit { + #todo - combined types xdigit && lower ?? set-theoretic types? how? if {![tcl::string::is $type -strict $e_check]} { set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e'" lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm index 9a42ad0b..51350674 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.1.0.tm @@ -2581,9 +2581,10 @@ y" {return quirkykeyscript} #eval_base has been set by previous source or proc #It can also be set by previous eval - *if* a non default offset was returned by _cmdtrace_get_eval_offset + set eval_base [dict get $linedict $target eval_base] set eval_offset [dict get $linedict $target eval_offset] - set line [expr {$prevline + ($eval_offset-1) + ($traceline-1)}] - #puts "stack-- $callinfo" + set line [expr {$eval_base + ($eval_offset-1) + ($traceline-1)}] + puts "stack-- $callinfo" puts " step type: eval traceline: $traceline -- " if {[dict exists $callinfo cmd]} { #set cmd [string trim [dict get $callinfo cmd]] @@ -2653,9 +2654,11 @@ y" {return quirkykeyscript} } #puts "-- $callinfo" } else { - puts ">>step type: $type (nontargeted proc)>> $callinfo" + ::tcl::dict::incr tinfo($target) subcmds + #puts ">>step type: $type (nontargeted proc)>> $callinfo" } } else { + ::tcl::dict::incr tinfo($target) subcmds #todo - handle type 'source' and type 'eval' with keys 'method' 'class' (oo) #puts ------------------------- #puts ">[dict get $callinfo cmd]" @@ -2708,7 +2711,6 @@ y" {return quirkykeyscript} variable tinfo array unset tinfo variable _cmdtrace_disabled - set _cmdtrace_disabled false set argd [punk::args::parse $args -cache 1 withid ::punk::ns::cmdtrace] lassign [dict values $argd] leaders opts values received @@ -2718,6 +2720,8 @@ y" {return quirkykeyscript} set cinfo [uplevel 1 [list ::punk::ns::cmdinfo {*}$cmdargs]] set origin [dict get $cinfo origin] set arglist [dict get $cinfo args_remaining] + set origin_nscmd [nstail $origin] + set origin_ns [nsprefix $origin] if {[dict exists $received -target]} { set targets [dict get $opts -target] @@ -2740,15 +2744,16 @@ y" {return quirkykeyscript} } #don't raise the error when no -target supplied - as our launch command can contain extra arguments } - lappend resolved_targets $tgt_cmd + set nscmd [nstail $tgt_cmd] + set ns [nsprefix $tgt_cmd] + + lappend resolved_targets $tgt_cmd $ns $nscmd ::tcl::dict::set ::punk::ns::linedict $tgt_cmd [::tcl::dict::create eval_base 1 eval_offset 1 lines {} cmdtype $tgt_type successcalls 0 errorcalls 0] } #if the target command has a leading colon (e.g expr alternative :) we can't put a trace directly on a fully qualified name with a triple colon such as ::: #we will need to evaluate in the namespace - foreach tgt_cmd $resolved_targets { - set nscmd [nstail $tgt_cmd] - set ns [nsprefix $tgt_cmd] + foreach {tgt_cmd ns nscmd} $resolved_targets { puts "tracing target: $tgt_cmd whilst running: $origin $arglist" ::tcl::namespace::eval $ns [list ::trace add execution $nscmd enter [list ::punk::ns::_cmdtrace_enter ::punk::ns::linedict $tgt_cmd]] @@ -2758,17 +2763,17 @@ y" {return quirkykeyscript} try { - set origin_nscmd [nstail $origin] - set origin_ns [nsprefix $origin] #uplevel 1 [list $origin {*}$arglist] + set _cmdtrace_disabled false ::tcl::namespace::eval $origin_ns [list $origin_nscmd {*}$arglist] } trap {} {errMsg errOptions} { + set _cmdtrace_disabled true + #(even a puts can involve function calls - e.g in contexts where there are stacked channels) puts stderr "command error $errMsg" } finally { - foreach tgt_cmd $resolved_targets { - set nscmd [nstail $tgt_cmd] - set ns [nsprefix $tgt_cmd] + set _cmdtrace_disabled true + foreach {tgt_cmd ns nscmd} $resolved_targets { ::tcl::namespace::eval $ns [list ::trace remove execution $nscmd enterstep [list ::punk::ns::_cmdtrace_enterstep ::punk::ns::linedict $tgt_cmd]] ::tcl::namespace::eval $ns [list ::trace remove execution $nscmd enter [list ::punk::ns::_cmdtrace_enter ::punk::ns::linedict $tgt_cmd]] ::tcl::namespace::eval $ns [list ::trace remove execution $nscmd leave [list ::punk::ns::_cmdtrace_leave ::punk::ns::linedict $tgt_cmd]] @@ -2780,7 +2785,7 @@ y" {return quirkykeyscript} append final_display \n #todo - foreach tgt_cmd in resolved_targets? - foreach tgt_cmd $resolved_targets { + foreach {tgt_cmd _ _} $resolved_targets { set lines [dict get $linedict $tgt_cmd lines] if {[llength $lines]} { set procbody [punk::ns::corp -n $tgt_cmd] diff --git a/src/vfs/_vfscommon.vfs/modules/punk/args-0.2.1.tm b/src/vfs/_vfscommon.vfs/modules/punk/args-0.2.1.tm index 0dd454e0..e2afc619 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/args-0.2.1.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/args-0.2.1.tm @@ -6481,7 +6481,7 @@ tcl::namespace::eval punk::args { } if {[dict exists $thisarg_checks -maxsize]} { set maxsize [dict get $thisarg_checks -maxsize] - if {$checkval ne "-1"} { + if {$maxsize ne "-1"} { if {[tcl::string::length $e_check] > $maxsize} { set msg "$argclass '$argname' for %caller% requires string with -maxsize $maxsize. Received len:[tcl::string::length $e_check] value:'$e_check'" #return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list sizeviolation $type] -badarg $argname -argspecs $argspecs]] $msg @@ -6679,6 +6679,7 @@ tcl::namespace::eval punk::args { upper - wordchar - xdigit { + #todo - combined types xdigit && lower ?? set-theoretic types? how? if {![tcl::string::is $type -strict $e_check]} { set msg "$argclass $argname for %caller% requires type '$type'. Received: '$e'" lset clause_results $c_idx $a_idx [list err [list typemismatch $type] msg $msg] diff --git a/src/vfs/_vfscommon.vfs/modules/punk/netbox-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/netbox-0.1.0.tm index 9700ff64..7c5212ac 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/netbox-0.1.0.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/netbox-0.1.0.tm @@ -161,6 +161,9 @@ tcl::namespace::eval punk::netbox::system { } return $sanconfig } + variable call_data + array unset call_data + proc make_rest_func {args} { set argd [punk::args::parse $args withid ::punk::netbox::system::make_rest_func] lassign [dict values $argd] leaders opts values received @@ -283,17 +286,16 @@ tcl::namespace::eval punk::netbox::system { dict set config headers content-type "application/json" } - #variable headerdict - #set config [dict create\ - # headers $headerdict\ - #] - set url [dict get $contexts $apicontextid url value] set endpoint "%endpoint%" if {[string first {{id}} $endpoint] != -1} { set id [dict get $values id] set endpoint [string map [list {{id}} $id] $endpoint] } + + #??? + dict set config error-body false + #todo - only show if debug (and obscure Authorization Token) set sanconfig [punk::netbox::system::obscured_config $config] puts stderr "url:${url}$endpoint query:'$query' verb:%verb% config:'$sanconfig'" @@ -305,6 +307,10 @@ tcl::namespace::eval punk::netbox::system { } else { set result [::rest::%verb% ${url}$endpoint $query $config] } + #puts "-----------" + #puts $result + #puts "-----------" + } else { puts stderr "%commandname% not called because -FORCE is false" set sanconfig [punk::netbox::system::obscured_config $config] @@ -353,7 +359,302 @@ tcl::namespace::eval punk::netbox::system { }] proc $commandname {args} $procbody } + #experiment + variable callid + set callid 0 + proc make_rest_func_async {args} { + set argd [punk::args::parse $args withid ::punk::netbox::system::make_rest_func] + lassign [dict values $argd] leaders opts values received + + set commandname [dict get $leaders commandname] + set endpoint [dict get $leaders endpoint] + set verb [dict get $opts -verb] + set bodycontrol [dict get $opts -body] + + set custom [dict create\ + %commandname% $commandname\ + %endpoint% $endpoint\ + %verb% $verb\ + %bodycontrol% $bodycontrol\ + %showpagedict% {!@@results @@results/@*/@*.@*}\ + %showpagedict2% {@@results/@*/@*.@* !@@results}\ + %showdict% {*}\ + %showdict2% {*/*}\ + %showlistofdicts% {@*/@*.@*}\ + ] + + + set procbody [string map $custom { + upvar ::punk::netbox::system::callid callid + incr callid + + set argd [punk::args::parse $args withid %commandname%] + lassign [dict values $argd] leaders opts values received solos multis + set apicontextid [dict get $leaders apicontextid] + if {[dict exists $received -RETURN]} { + set returntype [dict get $opts -RETURN] + } else { + if {[dict exists $opts -RETURN]} { + #not received - but has default + set returntype [dict get $opts -RETURN] + } else { + #fallback if -RETURN was defined without a default or was omitted + set returntype dict + } + } + + #-ASYNC is of -type none - solo flag + if {[dict exists $received -ASYNC]} { + set async true + } else { + set async false + } + + set FORCE 1 + if {[dict exists $opts -FORCE]} { + set FORCE [dict get $opts -FORCE] + } + + set query [list] ;#use list not dict - allows repeated params + dict for {k val} $opts { + switch -- $k { + -CUSTOM_PARAM { + #'-multiple true' + foreach kv $val { + lassign $kv paramname value + lappend query $paramname $value + } + } + -RETURN - -ASYNC - -FORCE { + #ignore - not options for endoint - already handled + } + default { + if {[string match *_FILTER $k]} { + #all _FILTER methods are '-multiple true' - so are present in $opts as a single key with a value that is a list + #e.g -X_Y_FILTER "blah" + set parts [split $k _] + set name1 [string range [string tolower [lindex $parts 0]] 1 end] ;#strip leading dash off first part + set nametail [string tolower [lrange $parts 1 end-1]] + set paramname [join [list $name1 {*}$nametail] _] + foreach fv $val { + lassign $fv filter value ;#filter is n,gte,lte etc + lappend query ${paramname}__$filter $value + } + } else { + set paramname [string range $k 1 end] + if {$k in $multis} { + foreach v $val { + lappend query $paramname $v + } + } else { + lappend query $paramname $val + } + } + } + } + + } + upvar ::punk::netbox::contexts contexts + if {![dict exists $contexts $apicontextid]} { + error "specified contextid '$apicontextid' not found" + } + + #rest api documentation is unclear on 'result' field + #note our default: result json + #this actually converts the json to a dict + + set config [dict create\ + format json\ + result json\ + ] + + dict set config headers [list Authorization [list Token [dict get $contexts $apicontextid token value]]] + set bodycontrol %bodycontrol% + switch -- $bodycontrol { + required { + set requestbody [dict get $values body] + #content type for the request data + dict set config headers content-type "application/json" + } + optional { + if {[dict exists $received body]} { + set requestbody [dict get $values body] + dict set config headers content-type "application/json" + } else { + set requestbody "" + } + } + default { + set requestbody "" + } + } + + + if {$returntype in "json jsondump"} { + #if we set result json - we get a dict instead of json :/ + dict set config result raw + } + + set url [dict get $contexts $apicontextid url value] + set endpoint "%endpoint%" + if {[string first {{id}} $endpoint] != -1} { + set id [dict get $values id] + set endpoint [string map [list {{id}} $id] $endpoint] + } + append url $endpoint + + #??? + dict set config error-body false + + #todo - only show if debug (and obscure Authorization Token) + set sanconfig [punk::netbox::system::obscured_config $config] + puts stderr "url:$url query:'$query' verb:%verb% config:'$sanconfig'" + if {$FORCE} { + set thisproc [dict get [info frame 0] proc] + #FORCE is true for most operations (and no -FORCE option even available) but the option exists and defaults to false for specifically unsafe + #e.g delete operations on entire endpoints + + #---------------------------------------- + #from rest make_interface generated func: + set headers {} + dict for {key val} [dict get $config headers] {lappend headers $key [::rest::substitute $val query]} + set query [::http::formatQuery {*}$query] + #---------------------------------------- + set error_body true + + dict set config method %verb% ;#rest::_call does an upvar on config array + dict set config body $bodycontrol ;#this is required|optional not the requestbody data itself + #dict set config timeout ??? ;#todo -TIMEOUT flag? + + #============================================================================ + #rest::_call is just a small wrapper over http:: methods + #if {$bodycontrol in {required optional}} { + # #set result [::rest::%verb% ${url}$endpoint $query $config $requestbody] + # set httptok [::rest::_call [list ::punk::netbox::system::rest_call_complete $thisproc,$callid] $headers $url $query $requestbody $error_body] + #} else { + # #set result [::rest::%verb% ${url}$endpoint $query $config] + # set httptok [::rest::_call [list ::punk::netbox::system::rest_call_complete $thisproc,$callid] $headers $url $query "" $error_body] + #} + set method [string toupper %verb%] + + if {[string match no* $bodycontrol]} { + #never put the query in the body if the user said no body + } else { + if {![dict exists $received body] && $bodycontrol eq "required"} { + #our make_rest_func_async args say we need a body - but our punk::args validation didn't require it + #review + set requestbody $query + set query {} + } + } + + if {$query != ""} {append url ?$query} + #configure options to the http::geturl command + set opts [list] + lappend opts -method $method + if {[dict exists $headers content-type]} { + lappend opts -type [dict get $headers content-type] + set headers [dict remove $headers content-type] + } + if {$requestbody != ""} { + #-query arg for http::geturl causes it to do a POST with the value as the request payload. + lappend opts -query $requestbody + } + lappend opts -command [list ::punk::netbox::system::http_call_complete $thisproc $callid] + #if {[dict exists $config timeout]} { + # lappend opts -timeout [dict get $config timeout] + #} + set httptok [http::geturl $url -headers $headers {*}$opts] + #============================================================================ + + + + + #puts "tok: $httptok" + #puts "-----------" + #puts $result + #puts "-----------" + + upvar ::punk::netbox::system::call_data call_data + set call_data($thisproc,$callid,httptoken) $httptok + set call_data($thisproc,$callid,command) $thisproc + set call_data($thisproc,$callid,returntype) $returntype + #todo - the actual async part - solo flag -ASYNC ? + if {$async} { + return $thisproc,$callid + } + #we need to then provide a $thisproc,$callid token to the caller and a retrieval/cleanup proc + http::wait $httptok + + return [punk::netbox::get_result $thisproc,$callid] + + } else { + puts stderr "%commandname% not called because -FORCE is false" + set sanconfig [punk::netbox::system::obscured_config $config] + puts "url:$url query:'$query' verb:%verb% config:'$sanconfig'" + return + } + }] + proc $commandname {args} $procbody + } + proc rest_call_complete {key func status args} { + variable call_data + if {[catch { + upvar 1 token token + #parray token + #This assumes there is only one call in flight unless key is made unique per call + set call_data($ns) [array get token] + } errMsg]} { + puts "rest_call_complete error: $errMsg" + } + puts $args + #puts "->$ns $func $status $args" + } + + proc http_call_complete {func callid tok} { + variable call_data + set call_data($func,$callid,http) [array get $tok] + set rcode [http::ncode $tok] + if {![string match 2* $rcode]} { + upvar 0 $tok status + if {$rcode ne ""} { + set data [list NETBOX ERROR HTTP $rcode] + if {$rcode eq "302"} { + #is location always capitalised this way by http? It can vary in raw server responses + #tcllib rest had it as Location - which definitely breaks in at least some cases. + #(perhaps http lib normalized it?) + set meta [dict get $status(meta)] + set keys [dict keys $meta] + set keyname [lsearch -inline -nocase $keys location] + if {$keyname ne ""} { + lappend data location [dict get $meta $keyname] + } else { + lappend data location "(unknown)" ;#can this even happen? + } + #For a rest api - we probably shouldn't follow redirects (review) + #It's probably generally better to return the error and get the user to update their URL + #there may be cases where redirect-following is needed - add an option for it? + } else { + if {$status(totalsize) > 0} { + lappend data errorbody $status(body) + } + } + } else { + #no HTTP response code - may be a socket error + set data [list NETBOX ERROR OTHER $status(status)] + if {[info exists status(error)]} { + lappend data errortext $status(error) + } + } + set call_data($func,$callid,result) [list ERROR $data] + http::cleanup $tok + return + } + set call_data($func,$callid,result) [list OK [http::data $tok]] + http::cleanup $tok + return + } } tcl::namespace::eval punk::netbox { @@ -374,16 +675,6 @@ tcl::namespace::eval punk::netbox { ::http::register https 443 ::tls::socket } - variable ipam - - #TEMP - todo - variable headerdict - set headerdict [dict create\ - Authorization "Token af65b993000874eaefeca0fa02b0d86014e48365"\ - ] - #temp - variable url https://www.netbox1.intx.com.au/ - variable contexts [dict create] variable context_id 0 @@ -632,6 +923,256 @@ tcl::namespace::eval punk::netbox { dict set contexts $contextid $allprops return $contextid } + + lappend PUNKARGS [list { + @id -id ::punk::netbox::get_status + @cmd -name punk::netbox::get_status\ + -summary\ + "Get status of previous -ASYNC call."\ + -help\ + "Retrieve the status from a previous punk::netbox::?ns::? + call which was run with -ASYNC. + + An error will be raised if the asynctoken is not valid or + if its result has already been retrieved with get_result. + + Returns PENDING, OK or ERROR + + It will not return the error text or result. + + It will not clear the error/result. + " + @values -min 1 -max 1 + asynctoken -type string -help\ + "A token as returned from a punk::netbox command when + it was given the -ASYNC flag" + }] + proc get_status {asynctoken} { + upvar ::punk::netbox::system::call_data call_data + if {![info exists call_data($asynctoken,httptoken)]} { + error "No such asynctoken: $asynctoken" + } + if {![info exists call_data($asynctoken,http)]} { + return PENDING + } else { + #even though we could return the raw result here, + #we don't - because the user should call get_result for cleanup. + return [lindex $call_data($asynctoken,result) 0] ;#OK|ERROR + } + } + lappend PUNKARGS [list { + @id -id ::punk::netbox::is_finished + @cmd -name punk::netbox::is_finished\ + -summary\ + "Test if result/error of previous -ASYNC call available."\ + -help\ + "Return an integer 0,1 or 2 indicating whether the result/error from a + previous punk::netbox::?ns::? call which was run with -ASYNC + is ready. + 0 indicates the request is still pending. + 1 indicates there is a result available. + 2 indicates there the command completed with an error. + + An error will be raised if the token is not valid or if its + result has already been retrieved with get_result." + @values -min 1 -max 1 + asynctoken -type string -help\ + "A token as returned from a punk::netbox command when + it was given the -ASYNC flag" + }] + proc is_finished {asynctoken} { + upvar ::punk::netbox::system::call_data call_data + if {![info exists call_data($asynctoken,httptoken)]} { + error "No such asynctoken: $asynctoken" + } + if {![info exists call_data($asynctoken,http)]} { + return 0 + } else { + if {[lindex $call_data($asynctoken,result) 0] eq "OK"} { + return 1 + } else { + return 2 + } + } + } + + lappend PUNKARGS [list { + @id -id ::punk::netbox::get_result_http + @cmd -name punk::netbox::get_result_http\ + -summary\ + "Get results of previous -ASYNC call."\ + -help\ + "Retrieve the http information from a previous punk::netbox::?ns::? + call which was run with -ASYNC. + + An error will be raised if the asynctoken is not valid or + if its result has already been retrieved with get_result. + + If the call has not yet returned or timed out, this call + will wait until the http token's status has been set. + + This call does not invalidate/clean-up the asynctoken and + if used, should be called prior to get_result. + " + @values -min 1 -max 1 + asynctoken -type string -help\ + "A token as returned from a punk::netbox command when + it was given the -ASYNC flag" + }] + proc get_result_http {asynctoken} { + upvar ::punk::netbox::system::call_data call_data + if {![info exists call_data($asynctoken,httptoken)]} { + error "No such asynctoken: $asynctoken" + } + set httptok $call_data($asynctoken,httptoken) + if {![info exists call_data($asynctoken,http)]} { + http::wait $httptok + } + return $call_data($asynctoken,http) + } + + lappend PUNKARGS [list { + @id -id ::punk::netbox::get_result + @cmd -name punk::netbox::get_result\ + -summary\ + "Get results of previous -ASYNC call."\ + -help\ + "Retrieve the result from a previous punk::netbox::?ns::? + call which was run with -ASYNC. + + An error will be raised for a call that was unsuccessful. + An error will be raised if the asynctoken is not valid or + if its result has already been retrieved with get_result. + + If the call has not yet returned or timed out, this call + will wait until the http token's status has been set." + @values -min 1 -max 1 + asynctoken -type string -help\ + "A token as returned from a punk::netbox command when + it was given the -ASYNC flag" + }] + proc get_result {asynctoken} { + upvar ::punk::netbox::system::call_data call_data + + set debug 0 ;#todo -DEBUG flag? + + if {![info exists call_data($asynctoken,httptoken)]} { + error "No such asynctoken: $asynctoken" + } + set httptok $call_data($asynctoken,httptoken) + if {![info exists call_data($asynctoken,http)]} { + http::wait $httptok + } + + #set command [lindex [split $asynctoken ,] 0] ;#review - assumes no comma in command name - not a great assumption! + set command $call_data($asynctoken,command) + + set resultlist $call_data($asynctoken,result) + set returntype $call_data($asynctoken,returntype) + #status is #OK|ERROR + lassign $resultlist status resulttext + #resulttext NETBOX ERROR ... + if {$status eq "ERROR"} { + set etype [lindex $resulttext 2] + #--------------------------------- + #this works - but the raw json seems more useful, and less prone to secondary errors + #--------------------------------- + #if {$etype eq "HTTP"} { + # #return -code error -errorcode [list NETBOX HTTP $ncode] $resulttext + # set errorbody [lindex $resulttext 5] + # if {$errorbody ne ""} { + # if {[catch { + # set result [list {*}[lrange $resulttext 0 4] [::rest::format_auto $errorbody]] ;#crude detection of xml/json - REVIEW + # #if xml - we don't get a dict - but netbox shouldn't output that anyway. + # #would be better just to output raw? + # } errM]} { + # set result $resulttext + # } + # } else { + # set result $resulttext + # } + #} else { + # set result $resulttext + #} + #--------------------------------- + #if we try to pass any of the error results to the requested returntypes below - it would only make sense for JSON anyway? + set result $resulttext + set errorcode [list NETBOX $etype [lindex $resulttext 3]] + if {!$debug} { + #clean up our copy of the http token data even on error + array unset call_data $asynctoken,* + } + return -code error -errorcode $errorcode $result + } + + #This is another main reason we couldn't just use ::rest lib as-is. + #Servers sometimes respond with empty body + #::rest lib tries to parse empty string as JSON and just emits an ugly error + #we may do the same if one of the returntype branches fails - but at least the user has a choice. + if {$resulttext ne ""} { + #review - could get nest structure from xml - but not relevant to netbox + #parsing json could fail here too + set result [::rest::format_auto $resulttext] + } else { + set result "" + } + + if {!$debug} { + #clean up our copy of the http token data + array unset call_data $asynctoken,* + } + + #try catch - return errorcode: NETBOX OUTPUTFORMATTING ? + switch -exact -- $returntype { + showpagedict { + return [punk::lib::showdict $result !@@results @@results/@*/@*.@*] + } + showpagedict2 { + return [punk::lib::showdict $result @@results/@*/@*.@* !@@results] + } + showdict { + if {$command eq "::punk::netbox::status"} { + #we get duplicate django-version for %showdict% - todo - something. + return [punk::lib::showdict $result @@django-version @@installed-apps/@*.@* !@@installed-apps] + } + return [punk::lib::showdict $result *] + } + showdict2 { + if {$command eq "::punk::netbox::status"} { + #we get duplicate django-version for %showdict% - todo - something. + return [punk::lib::showdict $result @@installed-apps/@*.@* !@@installed-apps] + } + return [punk::lib::showdict $result */*] + } + showlist { + return [punk::lib::showdict -roottype list $result] + } + showlistofdicts { + return [punk::lib::showdict $result @*/@*.@*] + } + jsondump { + package require huddle::json + #pretty-print via huddle (inefficient review) + set h [huddle::json::json2huddle parse $result] + return [huddle::jsondump $h] + } + linelist { + set ret "" + foreach r $result { + append ret $r \n + } + return $ret + } + default { + #plain result: (list or dict) or json - the counterintuitive 'result' field set to raw above sets the rest call's resulting format to json + return $result + } + } + + } + + + proc _homedir {} { if {[info exists ::env(HOME)]} { set home [file normalize $::env(HOME)] @@ -970,7 +1511,6 @@ tcl::namespace::eval punk::netbox { punk::args::define {*}[list\ { - @dynamic @id -id ::punk::netbox::status @cmd -name punk::netbox::status -help\ "status_list @@ -989,9 +1529,29 @@ tcl::namespace::eval punk::netbox { }\ [set ::punk::netbox::argdoc::_RETURN_STATUS]\ { + -ASYNC -type none @values -min 0 -max 0 }] - ::punk::netbox::system::make_rest_func ::punk::netbox::status api/status/ -verb get -body none + ::punk::netbox::system::make_rest_func_async ::punk::netbox::status api/status/ -verb get -body none + + #test - to see what the create_interface procs look like with a callback + #(they are not flexible enough argument-wise for this usecase) + #with a static callback baked into the function - it also gives no way to reliably determine + #which request matched which call if there are concurrent calls in flight for the same function/endpoint especially if args are the same + #(order of completion could presumably not always match call order) + #presumably an http::wait is required on each call, and they must be processed in order? + set RESTAPI(getstatus) { + url https://www.netbox1.intx.com.au/api/status/ + method get + headers {Authorization {Token bogus} content-type "application/json"} + callback {::punk::netbox::system::rest_call_complete RESTAPI} + error-body false + opt_args {test etc} + req_args {force} + } + #namespace eval ::punk::netbox::spud {} + ::rest::create_interface ::punk::netbox::RESTAPI + # #test function - todo use punk::netbox::system::make_rest_func @@ -1118,7 +1678,7 @@ tcl::namespace::eval punk::netbox::dcim { [set ::punk::netbox::argdoc::_create_update_options]\ { -q -type string - -tag -type string + -tag -type string -multiple true -tag__n -type string }\ [set ::punk::netbox::argdoc::_tenant_options]\ @@ -1169,7 +1729,7 @@ tcl::namespace::eval punk::netbox::dcim { { @values -min 0 -max 0 }] - ::punk::netbox::system::make_rest_func ::punk::netbox::dcim::devices_list api/dcim/devices/ -verb get -body none + ::punk::netbox::system::make_rest_func_async ::punk::netbox::dcim::devices_list api/dcim/devices/ -verb get -body none } tcl::namespace::eval punk::netbox::ipam { @@ -1204,7 +1764,7 @@ tcl::namespace::eval punk::netbox::ipam { [set ::punk::netbox::argdoc::_create_update_options]\ { -q - -tag + -tag -type string -multiple true }\ [set ::punk::netbox::argdoc::_tenant_options]\ [set ::punk::netbox::argdoc::_region_options]\ @@ -1223,7 +1783,7 @@ tcl::namespace::eval punk::netbox::ipam { @values -min 0 -max 0 }] } - ::punk::netbox::system::make_rest_func ::punk::netbox::ipam::vrfs_list api/ipam/vrfs/ -verb get -body none + ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::vrfs_list api/ipam/vrfs/ -verb get -body none namespace eval argdoc { lappend PUNKARGS [list\ @@ -1249,7 +1809,7 @@ tcl::namespace::eval punk::netbox::ipam { "A unique integer value identifying this VRF" }] } - ::punk::netbox::system::make_rest_func ::punk::netbox::ipam::vrfs_read api/ipam/vrfs/{id}/ -verb get -body none + ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::vrfs_read api/ipam/vrfs/{id}/ -verb get -body none namespace eval argdoc { punk::args::define {*}[list\ @@ -1278,7 +1838,7 @@ tcl::namespace::eval punk::netbox::ipam { { -q -type string -help\ "Query prefixes by substring" - -tag + -tag -type string -multiple true }\ [set ::punk::netbox::argdoc::_tenant_options]\ [set ::punk::netbox::argdoc::_region_options]\ @@ -1315,7 +1875,7 @@ tcl::namespace::eval punk::netbox::ipam { @values -min 0 -max 0 }] } - ::punk::netbox::system::make_rest_func ::punk::netbox::ipam::prefixes_list api/ipam/prefixes/ -verb get -body none + ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::prefixes_list api/ipam/prefixes/ -verb get -body none namespace eval argdoc { punk::args::define {*}[list\ @@ -1341,7 +1901,7 @@ tcl::namespace::eval punk::netbox::ipam { "JSON string" }] } - ::punk::netbox::system::make_rest_func ::punk::netbox::ipam::prefixes_create api/ipam/prefixes/{id}/ -verb post -body required + ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::prefixes_create api/ipam/prefixes/{id}/ -verb post -body required namespace eval argdoc { punk::args::define {*}[list\ @@ -1367,7 +1927,7 @@ tcl::namespace::eval punk::netbox::ipam { "A unique integer value identifying this prefix" }] } - ::punk::netbox::system::make_rest_func ::punk::netbox::ipam::prefixes_read api/ipam/prefixes/{id}/ -verb get -body none + ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::prefixes_read api/ipam/prefixes/{id}/ -verb get -body none namespace eval argdoc { punk::args::define {*}[list\ @@ -1396,7 +1956,7 @@ tcl::namespace::eval punk::netbox::ipam { }\ ] } - ::punk::netbox::system::make_rest_func ::punk::netbox::ipam::prefixes_available-ips_list api/ipam/prefixes/{id}/available-ips/ -verb get -body none + ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::prefixes_available-ips_list api/ipam/prefixes/{id}/available-ips/ -verb get -body none namespace eval argdoc { punk::args::define {*}[list\ @@ -1446,7 +2006,7 @@ tcl::namespace::eval punk::netbox::ipam { }\ ] } - ::punk::netbox::system::make_rest_func ::punk::netbox::ipam::prefixes_available-ips_create api/ipam/prefixes/{id}/available-ips/ -verb post -body required + ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::prefixes_available-ips_create api/ipam/prefixes/{id}/available-ips/ -verb post -body required namespace eval argdoc { punk::args::define {*}[list\ @@ -1475,7 +2035,7 @@ tcl::namespace::eval punk::netbox::ipam { }\ ] } - ::punk::netbox::system::make_rest_func ::punk::netbox::ipam::prefixes_available-prefixes_list api/ipam/prefixes/{id}/available-prefixes/ -verb get -body none + ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::prefixes_available-prefixes_list api/ipam/prefixes/{id}/available-prefixes/ -verb get -body none namespace eval argdoc { punk::args::define {*}[list\ @@ -1509,7 +2069,7 @@ tcl::namespace::eval punk::netbox::ipam { }\ ] } - ::punk::netbox::system::make_rest_func ::punk::netbox::ipam::prefixes_available-prefixes_create api/ipam/prefixes/{id}/available-prefixes/ -verb post -body required + ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::prefixes_available-prefixes_create api/ipam/prefixes/{id}/available-prefixes/ -verb post -body required namespace eval argdoc { punk::args::define {*}[list\ @@ -1537,7 +2097,9 @@ tcl::namespace::eval punk::netbox::ipam { [set ::punk::netbox::argdoc::_create_update_options]\ { -q - -tag + -tag -multiple true -help\ + "If multiple -tag options supplied, + they are ANDed (review)" }\ [set ::punk::netbox::argdoc::_tenant_options]\ [set ::punk::netbox::argdoc::_region_options]\ @@ -1575,32 +2137,8 @@ tcl::namespace::eval punk::netbox::ipam { @values -min 0 -max 0 }] } - ::punk::netbox::system::make_rest_func ::punk::netbox::ipam::ip-addresses_list api/ipam/ip-addresses/ -verb get -body none + ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::ip-addresses_list api/ipam/ip-addresses/ -verb get -body none - namespace eval argdoc { - punk::args::define {*}[list\ - { - @dynamic - @id -id ::punk::netbox::ipam::ip-addresses_read - @cmd -name punk::netbox::ipam::ip-addresses_read -help\ - "ipam_ip-addresses_read - GET request for endpoint /ipam/ip-addresses/{id}/" - @leaders -min 1 -max 1 - apicontextid -help\ - "The name of the stored api context to use. - A contextid can be created in-memory using - api_context_create, or loaded from a .toml - file using api_context_load."\ - -choices {${$DYN_CONTEXTNAMES}} - @opts - }\ - [set ::punk::netbox::argdoc::_RETURN_DICT]\ - { - @values -min 1 -max 1 - id -type integer - }] - } - ::punk::netbox::system::make_rest_func ::punk::netbox::ipam::ip-addresses_read api/ipam/ip-addresses/{id}/ -verb get -body none namespace eval argdoc { punk::args::define {*}[list\ @@ -1649,7 +2187,7 @@ tcl::namespace::eval punk::netbox::ipam { } }] } - ::punk::netbox::system::make_rest_func ::punk::netbox::ipam::ip-addresses_create api/ipam/ip-addresses/ -verb post -body required + ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::ip-addresses_create api/ipam/ip-addresses/ -verb post -body required namespace eval argdoc { punk::args::define {*}[list\ @@ -1698,7 +2236,61 @@ tcl::namespace::eval punk::netbox::ipam { } }] } - ::punk::netbox::system::make_rest_func ::punk::netbox::ipam::ip-addresses_bulk_partial_update api/ipam/ip-addresses/ -verb patch -body required + ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::ip-addresses_bulk_partial_update api/ipam/ip-addresses/ -verb patch -body required + + namespace eval argdoc { + punk::args::define {*}[list\ + { + @dynamic + @id -id ::punk::netbox::ipam::ip-addresses_read + @cmd -name punk::netbox::ipam::ip-addresses_read -help\ + "ipam_ip-addresses_read + GET request for endpoint /ipam/ip-addresses/{id}/" + @leaders -min 1 -max 1 + apicontextid -help\ + "The name of the stored api context to use. + A contextid can be created in-memory using + api_context_create, or loaded from a .toml + file using api_context_load."\ + -choices {${$DYN_CONTEXTNAMES}} + @opts + }\ + [set ::punk::netbox::argdoc::_RETURN_DICT]\ + { + @values -min 1 -max 1 + id -type integer + }] + } + ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::ip-addresses_read api/ipam/ip-addresses/{id}/ -verb get -body none + + namespace eval argdoc { + punk::args::define {*}[list\ + { + @dynamic + @id -id ::punk::netbox::ipam::ip-addresses_delete + @cmd -name punk::netbox::ipam::ip-addresses_delete\ + -summary\ + "Delete an IP address by id."\ + -help\ + "ipam_ip-addresses_delete + DELETE request for endpoint /ipam/ip-addresses/{id}/" + @leaders -min 1 -max 1 + apicontextid -help\ + "The name of the stored api context to use. + A contextid can be created in-memory using + api_context_create, or loaded from a .toml + file using api_context_load."\ + -choices {${$DYN_CONTEXTNAMES}} + @opts + }\ + [set ::punk::netbox::argdoc::_RETURN_DICT]\ + { + @values -min 1 -max 1 + id -type integer + }] + } + ::punk::netbox::system::make_rest_func_async ::punk::netbox::ipam::ip-addresses_delete api/ipam/ip-addresses/{id}/ -verb delete -body none + } @@ -1736,7 +2328,9 @@ tcl::namespace::eval punk::netbox::tenancy { [set ::punk::netbox::argdoc::_create_update_options]\ { -q -type string - -tag -type string + -tag -type string -multiple true -help\ + "If multiple -tag options supplied, + they are ANDed (review)" -tag__n -type string }\ [set ::punk::netbox::argdoc::_contact_options]\ @@ -1755,7 +2349,7 @@ tcl::namespace::eval punk::netbox::tenancy { }] } - ::punk::netbox::system::make_rest_func ::punk::netbox::tenancy::tenants_list api/tenancy/tenants/ -verb get -body none + ::punk::netbox::system::make_rest_func_async ::punk::netbox::tenancy::tenants_list api/tenancy/tenants/ -verb get -body none } tcl::namespace::eval punk::netbox::virtualization { @@ -1797,7 +2391,7 @@ tcl::namespace::eval punk::netbox::virtualization { [set ::punk::netbox::argdoc::_create_update_options]\ { -q - -tag + -tag -multiple true }\ [set ::punk::netbox::argdoc::_tenant_options]\ [set ::punk::netbox::argdoc::_contact_options]\ @@ -1837,7 +2431,7 @@ tcl::namespace::eval punk::netbox::virtualization { }] } - ::punk::netbox::system::make_rest_func ::punk::netbox::virtualization::virtual-machines_list api/virtualization/virtual-machines/ -verb get -body none + ::punk::netbox::system::make_rest_func_async ::punk::netbox::virtualization::virtual-machines_list api/virtualization/virtual-machines/ -verb get -body none namespace eval argdoc { lappend PUNKARGS [list\ @@ -1858,22 +2452,23 @@ tcl::namespace::eval punk::netbox::virtualization { }\ [set ::punk::netbox::argdoc::_RETURN_DICT]\ { - @values -min 2 -max 2 - id -type integer -help\ - "A unique integer value identifying this virtual machine" + @values -min 1 -max 1 body -type string -help\ "JSON string" }] } - ::punk::netbox::system::make_rest_func ::punk::netbox::virtualization::virtual-machines_create api/virtualization/virtual-machines/ -verb post -body required + ::punk::netbox::system::make_rest_func_async ::punk::netbox::virtualization::virtual-machines_create api/virtualization/virtual-machines/ -verb post -body required namespace eval argdoc { lappend PUNKARGS [list\ { @dynamic - @id -id ::punk::netbox::virtualization::virtual-machines_delete - @cmd -name punk::netbox::virtualization::virtual-machines_delete -help\ - "virtualization_virtual-machines_delete + @id -id ::punk::netbox::virtualization::virtual-machines_bulk_delete + @cmd -name punk::netbox::virtualization::virtual-machines_bulk_delete\ + -summary\ + "DELETE *ALL* virtual machines."\ + -help\ + "virtualization_virtual-machines_bulk_delete DELETE request for endpoint /virtualization/virtual-machines/ HTTP code: 204 " @@ -1886,13 +2481,13 @@ tcl::namespace::eval punk::netbox::virtualization { -choices {${$DYN_CONTEXTNAMES}} @opts -FORCE -default 0 -type boolean -help\ - "Set to true to BULK delete all items at this endpoint" + "Set to true to BULK delete *all* items at this endpoint" }\ { @values -min 0 -max 0 }] } - ::punk::netbox::system::make_rest_func ::punk::netbox::virtualization::virtual-machines_delete api/virtualization/virtual-machines/ -verb delete -body none + ::punk::netbox::system::make_rest_func_async ::punk::netbox::virtualization::virtual-machines_bulk_delete api/virtualization/virtual-machines/ -verb delete -body none namespace eval argdoc { lappend PUNKARGS [list\ @@ -1918,7 +2513,7 @@ tcl::namespace::eval punk::netbox::virtualization { "A unique integer value identifying this virtual machine" }] } - ::punk::netbox::system::make_rest_func ::punk::netbox::virtualization::virtual-machines_read api/virtualization/virtual-machines/{id}/ -verb get -body none + ::punk::netbox::system::make_rest_func_async ::punk::netbox::virtualization::virtual-machines_read api/virtualization/virtual-machines/{id}/ -verb get -body none namespace eval argdoc { lappend PUNKARGS [list\ @@ -1946,7 +2541,285 @@ tcl::namespace::eval punk::netbox::virtualization { "JSON string" }] } - ::punk::netbox::system::make_rest_func ::punk::netbox::virtualization::virtual-machines_update api/virtualization/virtual-machines/{id}/ -verb put -body required + ::punk::netbox::system::make_rest_func_async ::punk::netbox::virtualization::virtual-machines_update api/virtualization/virtual-machines/{id}/ -verb put -body required + + namespace eval argdoc { + lappend PUNKARGS [list\ + { + @dynamic + @id -id ::punk::netbox::virtualization::virtual-machines_delete + @cmd -name punk::netbox::virtualization::virtual-machines_delete\ + -summary\ + "Delete a single VM by id."\ + -help\ + "virtualization_virtual-machines_delete + DELETE request for endpoint /virtualization/virtual-machines/{id}" + @leaders -min 1 -max 1 + apicontextid -help\ + "The name of the stored api context to use. + A contextid can be created in-memory using + api_context_create, or loaded from a .toml + file using api_context_load."\ + -choices {${$DYN_CONTEXTNAMES}} + @opts + }\ + [set ::punk::netbox::argdoc::_RETURN_DICT]\ + { + @values -min 1 -max 1 + id -type integer -help\ + "A unique integer value identifying the virtual machine + to DELETE." + }] + } + ::punk::netbox::system::make_rest_func_async ::punk::netbox::virtualization::virtual-machines_delete api/virtualization/virtual-machines/{id}/ -verb delete -body none + +} + +tcl::namespace::eval punk::netbox::extras { + namespace export {[a-z]*} + + namespace eval argdoc { + variable PUNKARGS + variable DYN_CONTEXTNAMES + set DYN_CONTEXTNAMES {${[punk::netbox::api_context_names]}} + lappend PUNKARGS [list\ + { + @dynamic + @id -id ::punk::netbox::extras::tags_list + @cmd -name punk::netbox::extras::tags_list -help\ + "extras_tags_list + GET request for endpoint /extras/tags/" + @leaders -min 1 -max 1 + apicontextid -help\ + "The name of the stored api context to use. + A contextid can be created in-memory using + api_context_create, or loaded from a .toml + file using api_context_load."\ + -choices {${$DYN_CONTEXTNAMES}} + @opts + -id -type integer + -ID_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_number_filter_help}} + -name + -NAME_FILTER -type list -minsize 2 -maxsize 2 -multiple 1 -unindentedfields {-help} -help {${$::punk::netbox::argdoc::_name_filter_help}} + -slug -type string + -color -type string + -description -type string + }\ + [set ::punk::netbox::argdoc::_create_update_options]\ + { + -q + }\ + [set ::punk::netbox::argdoc::_page_options]\ + [set ::punk::netbox::argdoc::_CUSTOM_PARAMS]\ + [set ::punk::netbox::argdoc::_RETURN_PAGEDICT]\ + { + -ASYNC -type none + @values -min 0 -max 0 + }] + } + + ::punk::netbox::system::make_rest_func_async ::punk::netbox::extras::tags_list api/extras/tags/ -verb get -body none + + namespace eval argdoc { + lappend PUNKARGS [list\ + { + @dynamic + @id -id ::punk::netbox::extras::tags_create + @cmd -name punk::netbox::extras::tags_create -help\ + "extras_tags_create + GET request for endpoint /extras/tags/" + @leaders -min 1 -max 1 + apicontextid -help\ + "The name of the stored api context to use. + A contextid can be created in-memory using + api_context_create, or loaded from a .toml + file using api_context_load."\ + -choices {${$DYN_CONTEXTNAMES}} + @opts + }\ + [set ::punk::netbox::argdoc::_RETURN_DICT]\ + { + -ASYNC -type none + @values -min 1 -max 1 + body -type string -help\ + "JSON string" + }] + } + ::punk::netbox::system::make_rest_func_async ::punk::netbox::extras::tags_create api/extras/tags/ -verb post -body required + + + set RESTAPI(create) { + url https://www.netbox1.intx.com.au/tags/create/ + method post + headers {Authorization {Token Bogus} content-type "application/json"} + callback {::punk::netbox::system::rest_call_complete RESTAPI} + body required + error-body false + } + ::rest::create_interface ::punk::netbox::extras::RESTAPI + + namespace eval argdoc { + lappend PUNKARGS [list\ + { + @dynamic + @id -id ::punk::netbox::extras::tags_bulk_delete + @cmd -name punk::netbox::extras::tags_bulk_delete -help\ + "extras_tags_bulk_delete + DELETE request for endpoint /extras/tags/ + HTTP code: 204 + " + @leaders -min 1 -max 1 + apicontextid -help\ + "The name of the stored api context to use. + A contextid can be created in-memory using + api_context_create, or loaded from a .toml + file using api_context_load."\ + -choices {${$DYN_CONTEXTNAMES}} + @opts + -FORCE -default 0 -type boolean -help\ + "Set to true to BULK delete all items at this endpoint" + }\ + { + -ASYNC -type none + @values -min 0 -max 0 + }] + } + ::punk::netbox::system::make_rest_func_async ::punk::netbox::extras::tags_bulk_delete api/extras/tags/ -verb delete -body none + + namespace eval argdoc { + lappend PUNKARGS [list\ + { + @dynamic + @id -id ::punk::netbox::extras::tags_read + @cmd -name punk::netbox::extras::tags_read -help\ + "extras_tags_read + GET request for endpoint /extras/tags/{id}" + @leaders -min 1 -max 1 + apicontextid -help\ + "The name of the stored api context to use. + A contextid can be created in-memory using + api_context_create, or loaded from a .toml + file using api_context_load."\ + -choices {${$DYN_CONTEXTNAMES}} + @opts + }\ + [set ::punk::netbox::argdoc::_RETURN_DICT]\ + { + @values -min 1 -max 1 + id -type integer -help\ + "A unique integer value identifying this tag" + }] + } + ::punk::netbox::system::make_rest_func_async ::punk::netbox::extras::tags_read api/extras/tags/{id}/ -verb get -body none + + namespace eval argdoc { + lappend PUNKARGS [list\ + { + @dynamic + @id -id ::punk::netbox::extras::tags_update + @cmd -name punk::netbox::extras::tags_update -help\ + "extras_tags_update + PUT request for endpoint /extras/tags/{id} + A PUT update is a complete update of the tag. + It will reset any non-supplied fields + in the body to their default" + @leaders -min 1 -max 1 + apicontextid -help\ + "The name of the stored api context to use. + A contextid can be created in-memory using + api_context_create, or loaded from a .toml + file using api_context_load."\ + -choices {${$DYN_CONTEXTNAMES}} + @opts + }\ + [set ::punk::netbox::argdoc::_RETURN_DICT]\ + { + @values -min 2 -max 2 + id -type integer -help\ + "A unique integer value identifying this tag" + body -type string -help\ + "JSON string" + }] + } + ::punk::netbox::system::make_rest_func_async ::punk::netbox::extras::tags_update api/extras/tags/{id}/ -verb put -body required + + + #namespace eval argdoc { + # lappend PUNKARGS [list\ + # { + # @dynamic + # @id -id ::punk::netbox::extras::tags_delete + # @cmd -name punk::netbox::extras::tags_delete -help\ + # "extras_tags_delete + # DELETE request for endpoint /extras/tags/{id} + # Deletes a specific tag identified by id." + # @leaders -min 1 -max 1 + # apicontextid -help\ + # "The name of the stored api context to use. + # A contextid can be created in-memory using + # api_context_create, or loaded from a .toml + # file using api_context_load."\ + # -choices {${$DYN_CONTEXTNAMES}} + # @opts + # }\ + # [set ::punk::netbox::argdoc::_RETURN_DICT]\ + # { + # @values -min 1 -max 1 + # id -type integer -help\ + # "A unique integer value identifying this tag" + # }] + #} + # 204 response with no JSON data on success + #The rest library will try to parse the empty string as json and fail + #it can parse "{}" but not "" + + + #404 for invalid id: + #allow: GET,PUT,PATCH,DELETE,HEAD,OPTIONS + #content-length: 23 + #content-type: application/json + #cross-origin-opener-policy: same-origin + #date: Tue,11 Nov 2025 17:46:04 GMT + #referrer-policy: same-origin + #server: nginx/1.26.2 + #vary: Accept,Cookie,Origin + #x-content-type-options: nosniff + #x-frame-options: SAMEORIGIN + + #{ + # "detail": "Not found." + #} + #::punk::netbox::system::make_rest_func ::punk::netbox::extras::tags_delete api/extras/tags/{id}/ -verb delete -body none + + + namespace eval argdoc { + lappend PUNKARGS [list\ + { + @dynamic + @id -id ::punk::netbox::extras::tags_delete + @cmd -name punk::netbox::extras::tags_delete -help\ + "extras_tags_delete + DELETE request for endpoint /extras/tags/{id} + Deletes a specific tag identified by id." + @leaders -min 1 -max 1 + apicontextid -help\ + "The name of the stored api context to use. + A contextid can be created in-memory using + api_context_create, or loaded from a .toml + file using api_context_load."\ + -choices {${$DYN_CONTEXTNAMES}} + @opts + }\ + [set ::punk::netbox::argdoc::_RETURN_DICT]\ + { + -ASYNC -type none + @values -min 1 -max 1 + id -type integer -help\ + "A unique integer value identifying this tag" + }] + } + ::punk::netbox::system::make_rest_func_async ::punk::netbox::extras::tags_delete api/extras/tags/{id}/ -verb delete -body none + } @@ -2085,7 +2958,8 @@ namespace eval ::punk::args::register { ::punk::netbox::dcim\ ::punk::netbox::ipam\ ::punk::netbox::tenancy\ - ::punk::netbox::virtualization + ::punk::netbox::virtualization\ + ::punk::netbox::extras\ } # ----------------------------------------------------------------------------- diff --git a/src/vfs/_vfscommon.vfs/modules/punk/netbox/man-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/netbox/man-0.1.0.tm index fc1095c8..b44a9a33 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/netbox/man-0.1.0.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/netbox/man-0.1.0.tm @@ -66,38 +66,6 @@ package require rest #*** !doctools #[section API] -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -# oo::class namespace -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ -#tcl::namespace::eval punk::netbox::man::class { - #*** !doctools - #[subsection {Namespace punk::netbox::man::class}] - #[para] class definitions - #if {[tcl::info::commands [tcl::namespace::current]::interface_sample1] eq ""} { - #*** !doctools - #[list_begin enumerated] - - # oo::class create interface_sample1 { - # #*** !doctools - # #[enum] CLASS [class interface_sample1] - # #[list_begin definitions] - - # method test {arg1} { - # #*** !doctools - # #[call class::interface_sample1 [method test] [arg arg1]] - # #[para] test method - # puts "test: $arg1" - # } - - # #*** !doctools - # #[list_end] [comment {-- end definitions interface_sample1}] - # } - - #*** !doctools - #[list_end] [comment {--- end class enumeration ---}] - #} -#} -# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ tcl::namespace::eval punk::netbox::man { variable PUNKARGS @@ -926,6 +894,277 @@ tcl::namespace::eval punk::netbox::man::virtualization { } +tcl::namespace::eval punk::netbox::man::extras { + namespace export {[a-z]*} + namespace ensemble create -parameters {apicontextid} + variable PUNKARGS + + tcl::namespace::eval tags { + namespace export {[a-z]*} + namespace ensemble create -parameters {apicontextid} + variable PUNKARGS + namespace eval argdoc { + variable PUNKARGS + set DYN_CONTEXTNAMES {${[punk::netbox::api_context_names]}} + } + + namespace eval argdoc { + lappend PUNKARGS [list\ + {@dynamic}\ + { + @id -id ::punk::netbox::man::extras::tags::create + @cmd -name punk::netbox::man::extras::tags::create -help\ + "extras_tags_create + POST request for endpoint /extras/tags/" + @leaders -min 1 -max 1 + apicontextid -help\ + "The name of the stored api context to use. + A contextid can be created in-memory using + api_context_create, or loaded from a .toml + file using api_context_load."\ + -choices {${$DYN_CONTEXTNAMES}} + @opts + -name -type string -minsize 1 -maxsize 100 -optional 0 + -slug -type string -minsize 1 -maxsize 100 -optional 1 + #todo - combined xdigit and lower?? + -color -type xdigit -maxsize 6 -optional 1 -help\ + "Default will be assigned by netbox. + e.g 9e9e9e" + -description -type string -maxsize 200 -default "" + }\ + {-RETURN -default table -choices {table tableobject list linelist}}\ + { + @values -min 0 -max 0 + } + ] + } + + #example body + # color must be a lower-cased hex string (6 digits) + # e.g a red tag + # { + # "name": "my_tag", + # "slug": "my_tag", + # "color": "ff0000", + # "description": "testing tag creation" + # } + + #example 201 response + #{ + # "id": 14, + # "url": "https://www.netbox1.intx.com.au/api/extras/tags/14/", + # "display": "jjj", + # "name": "jjj", + # "slug": "jjj", + # "color": "ff0000", + # "description": "j testing", + # "created": "2025-11-11T16:33:17.461484Z", + # "last_updated": "2025-11-11T16:33:17.461500Z" + #} + + #example 400 response + #{ + # "name": [ + # "tag with this name already exists." + # ], + # "slug": [ + # "tag with this slug already exists." + # ] + #} + + proc create {args} { + set argd [punk::args::parse $args withid "::punk::netbox::man::extras::tags::create"] + lassign [dict values $argd] leaders opts values received + set token [dict get $argd leaders apicontextid] + + set map [::list \" "\\\"" \\ \\\\ \r \\r \n \\n \t \\t \b \\b \f \\f] ;#review - unicode? tcllib? + set name [dict get $argd opts -name] + if {[dict exists $received -slug]} { + set slug [dict get $opts -slug] + } else { + set slug $name + } + set description [dict get $argd opts -description] + + #Escape for JSON + set name [string map $map $name] + set slug [string map $map $slug] + set description [string map $map $description] + + + set body "\{\n" + append body " \"name\": \"$name\"," \n + append body " \"slug\": \"$slug\"," \n + if {[dict exists $received -color]} { + append body " \"color\": \"[dict get $opts -color]\"," \n + } + append body " \"description\": \"$description\"" \n + append body "\}" \n + + puts "Post body JSON:" + puts $body + #todo RETURN + set resultd [punk::netbox::extras::tags_create $token -RETURN dict $body] + + + + } + + namespace eval argdoc { + lappend PUNKARGS [list\ + {@dynamic}\ + { + @id -id ::punk::netbox::man::extras::tags::delete + @cmd -name punk::netbox::man::extras::tags::delete\ + -summary\ + "Delete one tag by id."\ + -help\ + "extras_tags_delete + DELETE request for endpoint /extras/tags/{id} + Delete a single tag by id." + @leaders -min 1 -max 1 + apicontextid -help\ + "The name of the stored api context to use. + A contextid can be created in-memory using + api_context_create, or loaded from a .toml + file using api_context_load."\ + -choices {${$DYN_CONTEXTNAMES}} + @opts + @values -min 1 -max 1 + id -type integer -range {0 ""} + } + ] + } + proc delete {args} { + set argd [punk::args::parse $args withid "::punk::netbox::man::extras::tags::delete"] + lassign [dict values $argd] leaders opts values received + set token [dict get $argd leaders apicontextid] + set id [dict get $argd values id] + punk::netbox::extras::tags_delete $token -RETURN dict $id + return done + } + + + namespace eval argdoc { + + lappend PUNKARGS [::list\ + {@dynamic}\ + [punk::args::resolved_def\ + -antiglobs {@leaders @values -RETURN}\ + -override { + @id {-id "::punk::netbox::man::extras::tags::list" } + apicontextid {-choices {${$DYN_CONTEXTNAMES}} } + }\ + ::punk::netbox::extras::tags_list\ + ]\ + {-RETURN -default table -choices {table tableobject list linelist}}\ + {-MAXRESULTS -type integer -default -1}\ + {@values -min 0 -max 0}\ + ] + } + proc list {args} { + set argd [punk::args::parse $args withid "::punk::netbox::man::extras::tags::list"] + + set urlnext "" + set requests_allowed 1000 ;#Sanity check - consider making an option - review + set resultlist [::list] + set token [dict get $argd leaders apicontextid] + set opts [dict get $argd opts] + set vals [dict get $argd values] + set multis [dict get $argd multis] + set outer_return [dict get $opts -RETURN] + set opts [dict remove $opts -RETURN] ;#opts from punk::args::parse is a dict (no dup keys) - can use 'dict remove' safely + set maxresults [dict get $opts -MAXRESULTS] + set opts [dict remove $opts -MAXRESULTS] + set initial_pagelimit [dict get $opts -limit] + #we can't just pass through 'multi' opts even if only one was supplied - list level is wrong + set nextopts [::list] + dict for {opt val} $opts { + if {$opt ni $multis} { + lappend nextopts $opt $val + } else { + foreach v $val { + lappend nextopts $opt $v + } + } + } + #Now opts is a list with possible repeated options! (for flags that have -multiple true) + + if {$maxresults == -1} { + set maxresults $initial_pagelimit + } + if {$maxresults < $initial_pagelimit} { + punk::netbox::man::system::dupkeylist_setfirst nextopts -limit $maxresults + } + set to_go [expr {$maxresults - [llength $resultlist]}] + while {$urlnext ne "null"} { + if {$urlnext ne ""} { + set urlnext_params [punk::netbox::man::system::uri_get_querystring_as_keyval_list $urlnext] + if {[punk::netbox::man::system::dupkeylist_getfirst $nextopts -limit] > $to_go} { + punk::netbox::man::system::dupkeylist_setfirst urlnext_params limit $to_go + } + punk::netbox::man::system::optionlistvar_sync_from_urlparams nextopts $urlnext_params + } + puts "-->next:$urlnext nextopts:$nextopts vals:$vals" + set resultd [punk::netbox::extras::tags_list $token {*}$nextopts -RETURN dict {*}$vals] + set urlnext [dict get $resultd next] + set batch [dict get $resultd results] + lappend resultlist {*}$batch + + set to_go [expr {$maxresults - [llength $resultlist]}] + if {$to_go <= 0} {break} + incr requests_allowed -1 + if {$requests_allowed < 1} {break} + } + + if {$outer_return in {table tableobject}} { + package require textblock + set t [textblock::list_as_table -return tableobject -colheaders {id name slug color sample description}] + foreach tag $resultlist { + set name [dict get $tag name] + set rgb [dict get $tag color] + if {[string length $rgb] && [string length $rgb] == 6} { + set sample "[a+ Rgb#$rgb rgb#$rgb-contrasting] $name [a]" + } else { + set sample "" + } + + set r [::list\ + [dict get $tag id]\ + [dict get $tag name]\ + [dict get $tag slug]\ + $rgb\ + $sample\ + [dict get $tag description]\ + ] + $t add_row $r + } + } + switch -- $outer_return { + table { + set result [$t print] + $t destroy + return $result + } + tableobject { + return $t + } + linelist { + set ret "" + foreach r $resultlist { + append ret $r \n + } + return $ret + } + default { + return $resultlist + } + } + } + + } + +} tcl::namespace::eval punk::netbox::man::ip-addresses { namespace export {[a-z]*} @@ -1308,6 +1547,8 @@ namespace eval ::punk::args::register { ::punk::netbox::man::tenancy::tenants\ ::punk::netbox::man::virtualization\ ::punk::netbox::man::virtualization::virtual-machines\ + ::punk::netbox::man::extras\ + ::punk::netbox::man::extras::tags\ } # ----------------------------------------------------------------------------- diff --git a/src/vfs/_vfscommon.vfs/modules/punk/ns-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/ns-0.1.0.tm index 9a42ad0b..51350674 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/ns-0.1.0.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/ns-0.1.0.tm @@ -2581,9 +2581,10 @@ y" {return quirkykeyscript} #eval_base has been set by previous source or proc #It can also be set by previous eval - *if* a non default offset was returned by _cmdtrace_get_eval_offset + set eval_base [dict get $linedict $target eval_base] set eval_offset [dict get $linedict $target eval_offset] - set line [expr {$prevline + ($eval_offset-1) + ($traceline-1)}] - #puts "stack-- $callinfo" + set line [expr {$eval_base + ($eval_offset-1) + ($traceline-1)}] + puts "stack-- $callinfo" puts " step type: eval traceline: $traceline -- " if {[dict exists $callinfo cmd]} { #set cmd [string trim [dict get $callinfo cmd]] @@ -2653,9 +2654,11 @@ y" {return quirkykeyscript} } #puts "-- $callinfo" } else { - puts ">>step type: $type (nontargeted proc)>> $callinfo" + ::tcl::dict::incr tinfo($target) subcmds + #puts ">>step type: $type (nontargeted proc)>> $callinfo" } } else { + ::tcl::dict::incr tinfo($target) subcmds #todo - handle type 'source' and type 'eval' with keys 'method' 'class' (oo) #puts ------------------------- #puts ">[dict get $callinfo cmd]" @@ -2708,7 +2711,6 @@ y" {return quirkykeyscript} variable tinfo array unset tinfo variable _cmdtrace_disabled - set _cmdtrace_disabled false set argd [punk::args::parse $args -cache 1 withid ::punk::ns::cmdtrace] lassign [dict values $argd] leaders opts values received @@ -2718,6 +2720,8 @@ y" {return quirkykeyscript} set cinfo [uplevel 1 [list ::punk::ns::cmdinfo {*}$cmdargs]] set origin [dict get $cinfo origin] set arglist [dict get $cinfo args_remaining] + set origin_nscmd [nstail $origin] + set origin_ns [nsprefix $origin] if {[dict exists $received -target]} { set targets [dict get $opts -target] @@ -2740,15 +2744,16 @@ y" {return quirkykeyscript} } #don't raise the error when no -target supplied - as our launch command can contain extra arguments } - lappend resolved_targets $tgt_cmd + set nscmd [nstail $tgt_cmd] + set ns [nsprefix $tgt_cmd] + + lappend resolved_targets $tgt_cmd $ns $nscmd ::tcl::dict::set ::punk::ns::linedict $tgt_cmd [::tcl::dict::create eval_base 1 eval_offset 1 lines {} cmdtype $tgt_type successcalls 0 errorcalls 0] } #if the target command has a leading colon (e.g expr alternative :) we can't put a trace directly on a fully qualified name with a triple colon such as ::: #we will need to evaluate in the namespace - foreach tgt_cmd $resolved_targets { - set nscmd [nstail $tgt_cmd] - set ns [nsprefix $tgt_cmd] + foreach {tgt_cmd ns nscmd} $resolved_targets { puts "tracing target: $tgt_cmd whilst running: $origin $arglist" ::tcl::namespace::eval $ns [list ::trace add execution $nscmd enter [list ::punk::ns::_cmdtrace_enter ::punk::ns::linedict $tgt_cmd]] @@ -2758,17 +2763,17 @@ y" {return quirkykeyscript} try { - set origin_nscmd [nstail $origin] - set origin_ns [nsprefix $origin] #uplevel 1 [list $origin {*}$arglist] + set _cmdtrace_disabled false ::tcl::namespace::eval $origin_ns [list $origin_nscmd {*}$arglist] } trap {} {errMsg errOptions} { + set _cmdtrace_disabled true + #(even a puts can involve function calls - e.g in contexts where there are stacked channels) puts stderr "command error $errMsg" } finally { - foreach tgt_cmd $resolved_targets { - set nscmd [nstail $tgt_cmd] - set ns [nsprefix $tgt_cmd] + set _cmdtrace_disabled true + foreach {tgt_cmd ns nscmd} $resolved_targets { ::tcl::namespace::eval $ns [list ::trace remove execution $nscmd enterstep [list ::punk::ns::_cmdtrace_enterstep ::punk::ns::linedict $tgt_cmd]] ::tcl::namespace::eval $ns [list ::trace remove execution $nscmd enter [list ::punk::ns::_cmdtrace_enter ::punk::ns::linedict $tgt_cmd]] ::tcl::namespace::eval $ns [list ::trace remove execution $nscmd leave [list ::punk::ns::_cmdtrace_leave ::punk::ns::linedict $tgt_cmd]] @@ -2780,7 +2785,7 @@ y" {return quirkykeyscript} append final_display \n #todo - foreach tgt_cmd in resolved_targets? - foreach tgt_cmd $resolved_targets { + foreach {tgt_cmd _ _} $resolved_targets { set lines [dict get $linedict $tgt_cmd lines] if {[llength $lines]} { set procbody [punk::ns::corp -n $tgt_cmd]