@@ -11,8 +11,128 @@ import (
1111 v1 "cloudstackctl/apis/v1"
1212
1313 cs "github.com/apache/cloudstack-go/v2/cloudstack"
14+ "golang.org/x/term"
1415)
1516
17+ const (
18+ ansiReset = "\033 [0m"
19+ ansiBold = "\033 [1m"
20+ ansiCyan = "\033 [36m"
21+ ansiBlue = "\033 [34m"
22+ ansiGreen = "\033 [32m"
23+ ansiYellow = "\033 [33m"
24+ ansiMagenta = "\033 [35m"
25+ ansiRed = "\033 [31m"
26+ )
27+
28+ // colorMode controls color output:
29+ //
30+ // "auto" – colors only when stdout is a TTY (default)
31+ // "always" – force colors on
32+ // "never" – force colors off
33+ var colorMode = "auto"
34+
35+ // SetColorMode sets the color output mode. Valid values: "auto", "always", "never".
36+ func SetColorMode (mode string ) {
37+ colorMode = mode
38+ }
39+
40+ func shellColorEnabled () bool {
41+ switch colorMode {
42+ case "always" :
43+ return true
44+ case "never" :
45+ return false
46+ }
47+ // "auto": respect env vars and TTY detection
48+ if os .Getenv ("NO_COLOR" ) != "" {
49+ return false
50+ }
51+ if os .Getenv ("TERM" ) == "" || os .Getenv ("TERM" ) == "dumb" {
52+ return false
53+ }
54+ return term .IsTerminal (int (os .Stdout .Fd ()))
55+ }
56+
57+ // tabwriterEsc is the escape byte used by tabwriter to mark invisible sequences.
58+ // Content wrapped in \xff pairs is excluded from column-width calculations.
59+ const tabwriterEsc = "\xff "
60+
61+ func colorize (s , color string ) string {
62+ if ! shellColorEnabled () {
63+ return s
64+ }
65+ // Wrap ANSI codes in tabwriter escape markers so they are not counted
66+ // as visible characters when computing column widths.
67+ return tabwriterEsc + color + tabwriterEsc + s + tabwriterEsc + ansiReset + tabwriterEsc
68+ }
69+
70+ func newTabWriter () * tabwriter.Writer {
71+ return tabwriter .NewWriter (os .Stdout , 0 , 0 , 2 , ' ' , tabwriter .StripEscape )
72+ }
73+
74+ func colorHeader (s string ) string {
75+ return colorize (s , ansiBold + ansiBlue )
76+ }
77+
78+ func colorResourceName (kind , s string ) string {
79+ switch kind {
80+ case "Volume" :
81+ return colorize (s , ansiCyan )
82+ case "Network" :
83+ return colorize (s , ansiYellow )
84+ case "VirtualMachine" :
85+ return colorize (s , ansiGreen )
86+ case "Template" :
87+ return colorize (s , ansiMagenta )
88+ case "SSHKey" :
89+ return colorize (s , ansiBlue )
90+ case "SecurityGroup" :
91+ return colorize (s , ansiRed )
92+ case "AffinityGroup" :
93+ return colorize (s , ansiMagenta )
94+ case "UserData" :
95+ return colorize (s , ansiYellow )
96+ case "Project" :
97+ return colorize (s , ansiCyan )
98+ case "Component" :
99+ return colorize (s , ansiYellow )
100+ case "VMSpec" :
101+ return colorize (s , ansiMagenta )
102+ case "Application" :
103+ return colorize (s , ansiCyan )
104+ default :
105+ return s
106+ }
107+ }
108+
109+ func colorStatus (s string ) string {
110+ switch s {
111+ case "Running" , "Healthy" , "Implemented" , "Setup" , "Ready" :
112+ return colorize (s , ansiGreen )
113+ case "Allocated" , "Starting" , "Started" , "Creating" , "Stopping" :
114+ return colorize (s , ansiYellow )
115+ case "Error" , "Failed" , "Destroyed" , "VMNotFound" , "Unhealthy" :
116+ return colorize (s , ansiRed )
117+ default :
118+ return s
119+ }
120+ }
121+
122+ func colorBool (v bool ) string {
123+ if v {
124+ return colorize ("true" , ansiGreen )
125+ }
126+ return colorize ("false" , ansiRed )
127+ }
128+
129+ func colorDrift (v bool ) string {
130+ if v {
131+ return colorize ("true" , ansiRed )
132+ }
133+ return colorize ("false" , ansiGreen )
134+ }
135+
16136// PrintCloudStackResource renders a short tabular view for common CloudStack
17137// list response objects. If the kind is not recognized or the provided
18138// object doesn't match expected SDK types, the function falls back to
@@ -31,70 +151,70 @@ func PrintCloudStackResource(kind string, obj any) error {
31151 }
32152 case "VirtualMachine" :
33153 if resp , ok := obj .(* cs.ListVirtualMachinesResponse ); ok {
34- w := tabwriter . NewWriter ( os . Stdout , 0 , 0 , 2 , ' ' , 0 )
35- fmt .Fprintln (w , "VIRTUAL MACHINE\t ID \t IP ADDRESS\t PUBLIC IP\t SERVICE OFFERING\t STATUS" )
154+ w := newTabWriter ( )
155+ fmt .Fprintln (w , colorHeader ( "VIRTUAL MACHINE" ) + " \t " + colorHeader ( "ID" ) + " \t " + colorHeader ( "IP ADDRESS" ) + " \t " + colorHeader ( "PUBLIC IP" ) + " \t " + colorHeader ( "SERVICE OFFERING" ) + " \t " + colorHeader ( "STATUS" ) )
36156 for _ , v := range resp .VirtualMachines {
37- fmt .Fprintf (w , "%s\t %s\t %s\t %s\t %s\t %s\n " , v .Name , v .Id , v .Ipaddress , v .Publicip , v .Serviceofferingname , v .State )
157+ fmt .Fprintf (w , "%s\t %s\t %s\t %s\t %s\t %s\n " , colorResourceName ( "VirtualMachine" , v .Name ) , v .Id , v .Ipaddress , v .Publicip , v .Serviceofferingname , colorStatus ( v .State ) )
38158 }
39159 w .Flush ()
40160 return nil
41161 }
42162 case "Template" :
43163 if resp , ok := obj .(* cs.ListTemplatesResponse ); ok {
44- w := tabwriter . NewWriter ( os . Stdout , 0 , 0 , 2 , ' ' , 0 )
45- fmt .Fprintln (w , "TEMPLATE\t ID \t OS \t FEATURED" )
164+ w := newTabWriter ( )
165+ fmt .Fprintln (w , colorHeader ( "TEMPLATE" ) + " \t " + colorHeader ( "ID" ) + " \t " + colorHeader ( "OS" ) + " \t " + colorHeader ( "FEATURED" ) )
46166 for _ , t := range resp .Templates {
47- fmt .Fprintf (w , "%s\t %s\t %s\t %t \n " , t .Name , t .Id , t .Ostypename , t .Isfeatured )
167+ fmt .Fprintf (w , "%s\t %s\t %s\t %s \n " , colorResourceName ( "Template" , t .Name ) , t .Id , t .Ostypename , colorBool ( t .Isfeatured ) )
48168 }
49169 w .Flush ()
50170 return nil
51171 }
52172 case "SSHKey" :
53173 if resp , ok := obj .(* cs.ListSSHKeyPairsResponse ); ok {
54- w := tabwriter . NewWriter ( os . Stdout , 0 , 0 , 2 , ' ' , 0 )
55- fmt .Fprintln (w , "SSH KEY\t FINGERPRINT" )
174+ w := newTabWriter ( )
175+ fmt .Fprintln (w , colorHeader ( "SSH KEY" ) + " \t " + colorHeader ( "FINGERPRINT" ) )
56176 for _ , k := range resp .SSHKeyPairs {
57- fmt .Fprintf (w , "%s\t %s\n " , k .Name , k .Fingerprint )
177+ fmt .Fprintf (w , "%s\t %s\n " , colorResourceName ( "SSHKey" , k .Name ) , k .Fingerprint )
58178 }
59179 w .Flush ()
60180 return nil
61181 }
62182 case "SecurityGroup" :
63183 if resp , ok := obj .(* cs.ListSecurityGroupsResponse ); ok {
64- w := tabwriter . NewWriter ( os . Stdout , 0 , 0 , 2 , ' ' , 0 )
65- fmt .Fprintln (w , "SECURITY GROUP\t ID \t DESCRIPTION" )
184+ w := newTabWriter ( )
185+ fmt .Fprintln (w , colorHeader ( "SECURITY GROUP" ) + " \t " + colorHeader ( "ID" ) + " \t " + colorHeader ( "DESCRIPTION" ) )
66186 for _ , sg := range resp .SecurityGroups {
67- fmt .Fprintf (w , "%s\t %s\t %s\n " , sg .Name , sg .Id , sg .Description )
187+ fmt .Fprintf (w , "%s\t %s\t %s\n " , colorResourceName ( "SecurityGroup" , sg .Name ) , sg .Id , sg .Description )
68188 }
69189 w .Flush ()
70190 return nil
71191 }
72192 case "AffinityGroup" :
73193 if resp , ok := obj .(* cs.ListAffinityGroupsResponse ); ok {
74- w := tabwriter . NewWriter ( os . Stdout , 0 , 0 , 2 , ' ' , 0 )
75- fmt .Fprintln (w , "AFFINITY GROUP\t ID \t DESCRIPTION" )
194+ w := newTabWriter ( )
195+ fmt .Fprintln (w , colorHeader ( "AFFINITY GROUP" ) + " \t " + colorHeader ( "ID" ) + " \t " + colorHeader ( "DESCRIPTION" ) )
76196 for _ , a := range resp .AffinityGroups {
77- fmt .Fprintf (w , "%s\t %s\t %s\n " , a .Name , a .Id , a .Description )
197+ fmt .Fprintf (w , "%s\t %s\t %s\n " , colorResourceName ( "AffinityGroup" , a .Name ) , a .Id , a .Description )
78198 }
79199 w .Flush ()
80200 return nil
81201 }
82202 case "UserData" :
83203 if resp , ok := obj .(* cs.ListUserDataResponse ); ok {
84- w := tabwriter . NewWriter ( os . Stdout , 0 , 0 , 2 , ' ' , 0 )
85- fmt .Fprintln (w , "USERDATA\t ID \t ACCOUNT" )
204+ w := newTabWriter ( )
205+ fmt .Fprintln (w , colorHeader ( "USERDATA" ) + " \t " + colorHeader ( "ID" ) + " \t " + colorHeader ( "ACCOUNT" ) )
86206 for _ , u := range resp .UserData {
87- fmt .Fprintf (w , "%s\t %s\t %s\n " , u .Name , u .Id , u .Account )
207+ fmt .Fprintf (w , "%s\t %s\t %s\n " , colorResourceName ( "UserData" , u .Name ) , u .Id , u .Account )
88208 }
89209 w .Flush ()
90210 return nil
91211 }
92212 case "Project" :
93213 if resp , ok := obj .(* cs.ListProjectsResponse ); ok {
94- w := tabwriter . NewWriter ( os . Stdout , 0 , 0 , 2 , ' ' , 0 )
95- fmt .Fprintln (w , "PROJECT\t ID \t STATE \t DISPLAY TEXT" )
214+ w := newTabWriter ( )
215+ fmt .Fprintln (w , colorHeader ( "PROJECT" ) + " \t " + colorHeader ( "ID" ) + " \t " + colorHeader ( "STATE" ) + " \t " + colorHeader ( "DISPLAY TEXT") )
96216 for _ , p := range resp .Projects {
97- fmt .Fprintf (w , "%s\t %s\t %s\t %s\n " , p .Name , p .Id , p .State , p .Displaytext )
217+ fmt .Fprintf (w , "%s\t %s\t %s\t %s\n " , colorResourceName ( "Project" , p .Name ) , p .Id , colorStatus ( p .State ) , p .Displaytext )
98218 }
99219 w .Flush ()
100220 return nil
@@ -109,36 +229,25 @@ func PrintCloudStackResource(kind string, obj any) error {
109229
110230// PrintVolumes prints a table of volumes.
111231func PrintVolumes (vols []* cs.Volume ) {
112- w := tabwriter . NewWriter ( os . Stdout , 0 , 0 , 2 , ' ' , 0 )
113- fmt .Fprintln (w , "VOLUME\t ID \t VIRTUAL MACHINE\t TYPE \t STATUS" )
232+ w := newTabWriter ( )
233+ fmt .Fprintln (w , colorHeader ( "VOLUME" ) + " \t " + colorHeader ( "ID" ) + " \t " + colorHeader ( "VIRTUAL MACHINE" ) + " \t " + colorHeader ( "TYPE" ) + " \t " + colorHeader ( "STATUS" ) )
114234 for _ , v := range vols {
115- fmt .Fprintf (w , "%s\t %s\t %s\t %s\t %s\n " , v .Name , v .Id , v .Vmname , v .Type , v .State )
235+ fmt .Fprintf (w , "%s\t %s\t %s\t %s\t %s\n " , colorResourceName ( "Volume" , v .Name ) , v .Id , v .Vmname , v .Type , colorStatus ( v .State ) )
116236 }
117237 w .Flush ()
118238}
119239
120240// PrintNetworks prints a table of networks.
121241func PrintNetworks (nets []* cs.Network ) {
122- w := tabwriter .NewWriter (os .Stdout , 0 , 0 , 2 , ' ' , 0 )
123- fmt .Fprintln (w , "NETWORK\t ID\t ZONE\t VLAN\t DISPLAY TEXT\t TYPE\t STATE" )
124- client , _ := cloudstack .NewClient ()
242+ w := newTabWriter ()
243+ fmt .Fprintln (w , colorHeader ("NETWORK" )+ "\t " + colorHeader ("ID" )+ "\t " + colorHeader ("VLAN" )+ "\t " + colorHeader ("DISPLAY TEXT" )+ "\t " + colorHeader ("TYPE" )+ "\t " + colorHeader ("STATE" ))
125244
126245 for _ , n := range nets {
127246 display := n .Displaytext
128247 if display == "" {
129248 display = n .Name
130249 }
131250
132- zoneName := n .Zoneid
133- if n .Zoneid != "" && client != nil {
134- zp := client .Zone .NewListZonesParams ()
135- zp .SetId (n .Zoneid )
136- zr , zerr := client .Zone .ListZones (zp )
137- if zerr == nil && zr != nil && len (zr .Zones ) > 0 {
138- zoneName = zr .Zones [0 ].Name
139- }
140- }
141-
142251 vlan := ""
143252 if b , merr := json .Marshal (n ); merr == nil {
144253 var m map [string ]interface {}
@@ -151,15 +260,15 @@ func PrintNetworks(nets []*cs.Network) {
151260 }
152261 }
153262
154- fmt .Fprintf (w , "%s\t %s\t %s\t %s\t %s\t %s\t %s \ n " , n .Name , n .Id , zoneName , vlan , display , n .Type , n .State )
263+ fmt .Fprintf (w , "%s\t %s\t %s\t %s\t %s\t %s\n " , colorResourceName ( "Network" , n .Name ) , n .Id , vlan , display , n .Type , colorStatus ( n .State ) )
155264 }
156265 w .Flush ()
157266}
158267
159268// PrintVMsFromController prints VMs returned by the controller query.
160269func PrintVMsFromController (vms []v1.VirtualMachine ) {
161- w := tabwriter . NewWriter ( os . Stdout , 0 , 0 , 2 , ' ' , 0 )
162- fmt .Fprintln (w , "VIRTUAL MACHINE\t APPLICATION \t COMPONENT \t ID \t IP ADDRESS\t PUBLIC IP\t SERVICE OFFERING\t STATUS \t READY \t DRIFT" )
270+ w := newTabWriter ( )
271+ fmt .Fprintln (w , colorHeader ( "VIRTUAL MACHINE" ) + " \t " + colorHeader ( "APPLICATION" ) + " \t " + colorHeader ( "COMPONENT" ) + " \t " + colorHeader ( "ID" ) + " \t " + colorHeader ( "IP ADDRESS" ) + " \t " + colorHeader ( "PUBLIC IP" ) + " \t " + colorHeader ( "SERVICE OFFERING" ) + " \t " + colorHeader ( "STATUS" ) + " \t " + colorHeader ( "READY" ) + " \t " + colorHeader ( "DRIFT" ) )
163272 client , _ := cloudstack .NewClient ()
164273 for _ , vm := range vms {
165274 id := vm .CloudStackID
@@ -189,26 +298,26 @@ func PrintVMsFromController(vms []v1.VirtualMachine) {
189298 if so == "" && vm .ObservedSpec .ServiceOffering != "" {
190299 so = vm .ObservedSpec .ServiceOffering
191300 }
192- fmt .Fprintf (w , "%s\t %s\t %s\t %s\t %s\t %s\t %s\t %s\t %t \t %t \n " ,
193- vm .Metadata .Name ,
301+ fmt .Fprintf (w , "%s\t %s\t %s\t %s\t %s\t %s\t %s\t %s\t %s \t %s \n " ,
302+ colorResourceName ( "VirtualMachine" , vm .Metadata .Name ) ,
194303 vm .Application ,
195304 vm .Component ,
196305 id ,
197306 ipAddress ,
198307 publicIP ,
199308 so ,
200- vm .Status .ObservedState ,
201- vm .Status .Ready ,
202- vm .Status .Drift ,
309+ colorStatus ( vm .Status .ObservedState ) ,
310+ colorBool ( vm .Status .Ready ) ,
311+ colorDrift ( vm .Status .Drift ) ,
203312 )
204313 }
205314 w .Flush ()
206315}
207316
208317// PrintComponents prints components returned by the controller DB query.
209318func PrintComponents (comps []v1.Component ) {
210- w := tabwriter . NewWriter ( os . Stdout , 0 , 0 , 2 , ' ' , 0 )
211- fmt .Fprintln (w , "COMPONENT\t APPLICATION \t REPLICAS \t VM SPEC\t STATE \t OBSERVED REPLICAS\t LAST CHECKED\t CREATED" )
319+ w := newTabWriter ( )
320+ fmt .Fprintln (w , colorHeader ( "COMPONENT" ) + " \t " + colorHeader ( "APPLICATION" ) + " \t " + colorHeader ( "REPLICAS" ) + " \t " + colorHeader ( "VM SPEC" ) + " \t " + colorHeader ( "STATE" ) + " \t " + colorHeader ( "OBSERVED REPLICAS" ) + " \t " + colorHeader ( "LAST CHECKED" ) + " \t " + colorHeader ( "CREATED" ) )
212321
213322 for _ , c := range comps {
214323 replicas := c .Spec .Replicas
@@ -224,15 +333,15 @@ func PrintComponents(comps []v1.Component) {
224333 if ! c .CreatedAt .IsZero () {
225334 created = c .CreatedAt .Format (time .RFC3339 )
226335 }
227- fmt .Fprintf (w , "%s\t %s\t %d\t %s\t %s\t %d\t %s\t %s\n " , c .Metadata .Name , appNames , replicas , vmSpec , state , observed , last , created )
336+ fmt .Fprintf (w , "%s\t %s\t %d\t %s\t %s\t %d\t %s\t %s\n " , colorResourceName ( "Component" , c .Metadata .Name ) , appNames , replicas , vmSpec , colorStatus ( state ) , observed , last , created )
228337 }
229338 w .Flush ()
230339}
231340
232341// PrintVMSpecs prints VirtualMachineSpecResource entries in a compact table.
233342func PrintVMSpecs (specs []v1.VirtualMachineSpecResource ) {
234- w := tabwriter . NewWriter ( os . Stdout , 0 , 0 , 2 , ' ' , 0 )
235- fmt .Fprintln (w , "VM SPEC\t TEMPLATE \t SERVICE OFFERING\t NETWORKS \t VOLUMES \t CREATED" )
343+ w := newTabWriter ( )
344+ fmt .Fprintln (w , colorHeader ( "VM SPEC" ) + " \t " + colorHeader ( "TEMPLATE" ) + " \t " + colorHeader ( "SERVICE OFFERING" ) + " \t " + colorHeader ( "NETWORKS" ) + " \t " + colorHeader ( "VOLUMES" ) + " \t " + colorHeader ( "CREATED" ) )
236345 for _ , s := range specs {
237346 tmpl := s .Spec .Template
238347 so := s .Spec .ServiceOffering
@@ -255,15 +364,15 @@ func PrintVMSpecs(specs []v1.VirtualMachineSpecResource) {
255364 if ! s .CreatedAt .IsZero () {
256365 created = s .CreatedAt .Format (time .RFC3339 )
257366 }
258- fmt .Fprintf (w , "%s\t %s\t %s\t %s\t %d\t %s\n " , s .Metadata .Name , tmpl , so , nets , volCount , created )
367+ fmt .Fprintf (w , "%s\t %s\t %s\t %s\t %d\t %s\n " , colorResourceName ( "VMSpec" , s .Metadata .Name ) , tmpl , so , nets , volCount , created )
259368 }
260369 w .Flush ()
261370}
262371
263372// PrintApplications prints applications returned by the controller DB query.
264373func PrintApplications (apps []v1.Application ) {
265- w := tabwriter . NewWriter ( os . Stdout , 0 , 0 , 2 , ' ' , 0 )
266- fmt .Fprintln (w , "APPLICATION\t COMPONENTS \t STATE \t READY \t LAST CHECKED\t CREATED" )
374+ w := newTabWriter ( )
375+ fmt .Fprintln (w , colorHeader ( "APPLICATION" ) + " \t " + colorHeader ( "COMPONENTS" ) + " \t " + colorHeader ( "STATE" ) + " \t " + colorHeader ( "READY" ) + " \t " + colorHeader ( "LAST CHECKED" ) + " \t " + colorHeader ( "CREATED" ) )
267376 for _ , a := range apps {
268377 compNames := ""
269378 if len (a .Spec .Components ) > 0 {
@@ -280,7 +389,7 @@ func PrintApplications(apps []v1.Application) {
280389 if ! a .CreatedAt .IsZero () {
281390 created = a .CreatedAt .Format (time .RFC3339 )
282391 }
283- fmt .Fprintf (w , "%s\t %s\t %s\t %t \t %s\t %s\n " , a .Metadata .Name , compNames , a .Status .ObservedState , a .Status .Ready , last , created )
392+ fmt .Fprintf (w , "%s\t %s\t %s\t %s \t %s\t %s\n " , colorResourceName ( "Application" , a .Metadata .Name ) , compNames , colorStatus ( a .Status .ObservedState ), colorBool ( a .Status .Ready ) , last , created )
284393 }
285394 w .Flush ()
286395}
0 commit comments