github.com/tommi2day/tnscli@v0.0.0-20240401211958-338fc0647b73/cmd/service.go (about)

     1  // Package cmd commands
     2  package cmd
     3  
     4  import (
     5  	"fmt"
     6  	"net"
     7  	"path"
     8  	"regexp"
     9  	"sort"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/spf13/viper"
    14  
    15  	goora "github.com/sijms/go-ora/v2"
    16  
    17  	"github.com/tommi2day/gomodules/common"
    18  	"github.com/tommi2day/gomodules/dblib"
    19  
    20  	log "github.com/sirupsen/logrus"
    21  
    22  	"github.com/spf13/cobra"
    23  )
    24  
    25  var (
    26  	serviceCmd = &cobra.Command{
    27  		Use:   "service",
    28  		Short: "Service sub command",
    29  		Long:  ``,
    30  	}
    31  
    32  	checkCmd = &cobra.Command{
    33  		Use:          "check",
    34  		Short:        "Check TNS Entries",
    35  		Long:         `Check all TNS Entries or one with real connect to database`,
    36  		RunE:         checkTns,
    37  		SilenceUsage: true,
    38  	}
    39  	portcheckCmd = &cobra.Command{
    40  		Use:          "portcheck",
    41  		Short:        "try to connect each service and report if it is open or not",
    42  		Long:         `list defined host:port and checks if requested. If racinfo.ini or SRV info given,  addresses will be checked as well`,
    43  		RunE:         doPortcheck,
    44  		SilenceUsage: true,
    45  	}
    46  	infoCmd = &cobra.Command{
    47  		Use:   "info",
    48  		Short: "give details for the given service",
    49  		Long:  `printout more details for the service`,
    50  	}
    51  	portInfoCmd = &cobra.Command{
    52  		Use:          "ports",
    53  		Short:        "list service addresses and ports",
    54  		Long:         `list defined host:port and checks if requested. If racinfo.ini given, it will be listed as well`,
    55  		RunE:         portInfo,
    56  		SilenceUsage: true,
    57  	}
    58  	jdbcInfoCmd = &cobra.Command{
    59  		Use:          "jdbc",
    60  		Short:        "print tns entry as jdbc string",
    61  		Long:         `printout jdbc string for the service`,
    62  		RunE:         getJdbcInfo,
    63  		SilenceUsage: true,
    64  	}
    65  	tnsInfoCmd = &cobra.Command{
    66  		Use:          "tns",
    67  		Short:        "print tns entry for the given service",
    68  		Long:         `printout tns entry for the service`,
    69  		RunE:         getTnsInfo,
    70  		SilenceUsage: true,
    71  	}
    72  )
    73  
    74  const defaultUser = "C##TCHECK"
    75  const defaultPassword = "<MyCheckPassword>"
    76  const racinfoFile = "racinfo.ini"
    77  
    78  var dbUser = ""
    79  var dbPass = ""
    80  var tnsKey = ""
    81  var timeout = 15
    82  var dbhostFlag = false
    83  var all = false
    84  var racinfo = ""
    85  var nodns = false
    86  var ipv4 = false
    87  var nameserver = ""
    88  var pingTimeout = 5
    89  var tcpcheck = false
    90  var dnstcp = false
    91  var noModifyTransportConnectTimeout = false
    92  
    93  func init() {
    94  	serviceCmd.PersistentFlags().StringVarP(&tnsKey, "service", "s", "", "service name to check")
    95  	RootCmd.AddCommand(serviceCmd)
    96  
    97  	checkCmd.PersistentFlags().StringVarP(&dbUser, "user", "u", dbUser, "User for real connect or set TNSCLI_USER")
    98  	checkCmd.PersistentFlags().StringVarP(&dbPass, "password", "p", dbPass, "Password for real connect or set TNSCLI_PASSWORD")
    99  	checkCmd.PersistentFlags().BoolVarP(&all, "all", "a", false, "check all entries")
   100  	checkCmd.PersistentFlags().IntVarP(&timeout, "timeout", "t", timeout, "timeout in sec")
   101  	checkCmd.Flags().BoolVarP(&dbhostFlag, "dbhost", "H", false, "print actual connected host:cdb:pdb")
   102  
   103  	portInfoCmd.Flags().StringVarP(&racinfo, "racinfo", "r", "", "path to racinfo.ini to resolve all RAC TCP Adresses, default $TNS_ADMIN/racinfo.ini")
   104  	portInfoCmd.Flags().StringVarP(&nameserver, "nameserver", "n", "", "alternative nameserver to use for DNS lookup (IP:PORT)")
   105  	portInfoCmd.Flags().BoolVar(&nodns, "nodns", false, "do not use DNS to resolve hostnames")
   106  	portInfoCmd.Flags().BoolVar(&ipv4, "ipv4", false, "resolve only IPv4 addresses")
   107  	portInfoCmd.Flags().BoolVar(&dnstcp, "dnstcp", false, "Use TCP to resolve DNS names")
   108  
   109  	portcheckCmd.Flags().StringVarP(&racinfo, "racinfo", "r", "", "path to racinfo.ini to resolve all RAC TCP Adresses, default $TNS_ADMIN/racinfo.ini")
   110  	portcheckCmd.Flags().StringVarP(&nameserver, "nameserver", "n", "", "alternative nameserver to use for DNS lookup (IP:PORT)")
   111  	portcheckCmd.Flags().BoolVar(&nodns, "nodns", false, "do not use DNS to resolve hostnames")
   112  	portcheckCmd.Flags().BoolVar(&ipv4, "ipv4", false, "resolve only IPv4 addresses")
   113  	portcheckCmd.Flags().IntVarP(&pingTimeout, "timeout", "t", pingTimeout, "timeout for tcp ping")
   114  	portcheckCmd.Flags().BoolVar(&dnstcp, "dnstcp", false, "Use TCP to resolve DNS names")
   115  
   116  	jdbcInfoCmd.Flags().BoolVar(&noModifyTransportConnectTimeout, "noModifyTransportConnectTimeout", false, "Do not modify TRANSPORT_CONNECT_TIMEOUT in ms")
   117  	infoCmd.AddCommand(portInfoCmd)
   118  	infoCmd.AddCommand(jdbcInfoCmd)
   119  	infoCmd.AddCommand(tnsInfoCmd)
   120  
   121  	serviceCmd.AddCommand(checkCmd)
   122  	serviceCmd.AddCommand(infoCmd)
   123  	serviceCmd.AddCommand(portcheckCmd)
   124  }
   125  func getEntry(tnsKey string) (entry dblib.TNSEntry, err error) {
   126  	// load available tns entries
   127  	tnsEntries, domain, err := dblib.GetTnsnames(filename, true)
   128  	l := len(tnsEntries)
   129  	if err != nil {
   130  		return
   131  	}
   132  	if l == 0 {
   133  		err = fmt.Errorf("cannot proceed without tns entries")
   134  		log.Error(err)
   135  		return
   136  	}
   137  	if tnsKey == "" {
   138  		err = fmt.Errorf("dont have a service to check, use --service to provide")
   139  		return
   140  	}
   141  	log.Debugf("get info for service %s ", tnsKey)
   142  
   143  	entry, found := dblib.GetEntry(tnsKey, tnsEntries, domain)
   144  	if !found {
   145  		err = fmt.Errorf("xealias %s not found", tnsKey)
   146  		return
   147  	}
   148  	return
   149  }
   150  
   151  func portInfo(_ *cobra.Command, args []string) (err error) {
   152  	if len(args) > 0 {
   153  		tnsKey = args[0]
   154  	}
   155  	if tnsKey == "" {
   156  		err = fmt.Errorf("dont have a service to check, use --service to provide")
   157  		return
   158  	}
   159  	if racinfo == "" {
   160  		racinfo = path.Join(viper.GetString("tns_admin"), racinfoFile)
   161  	}
   162  	entry, err := getEntry(tnsKey)
   163  	if err != nil {
   164  		return
   165  	}
   166  	servers := entry.Servers
   167  	l := len(servers)
   168  	if l == 0 {
   169  		err = fmt.Errorf("xealias %s: No hosts found", tnsKey)
   170  		return
   171  	}
   172  	log.Infof("Alias %s uses %d hosts", tnsKey, l)
   173  	dblib.IgnoreDNSLookup = nodns
   174  	dblib.IPv4Only = ipv4
   175  	ns, p, e := common.GetHostPort(nameserver)
   176  	if e != nil {
   177  		ns = nameserver
   178  		p = 0
   179  	}
   180  	dblib.Resolver = dblib.SetResolver(ns, p, dnstcp)
   181  	allservices := getServices(servers)
   182  	log.Infof("Alias %s uses %d addresses", tnsKey, len(allservices))
   183  	for _, s := range allservices {
   184  		if tcpcheck {
   185  			doTCPPing(s.Host, s.Address)
   186  		} else {
   187  			fmt.Printf("%s (%s)\n", s.Host, s.Address)
   188  		}
   189  	}
   190  	return
   191  }
   192  
   193  func getServices(servers []dblib.TNSAddress) (allservices dblib.ServiceEntries) {
   194  	for _, s := range servers {
   195  		host := s.Host
   196  		port := s.Port
   197  		services := dblib.GetRacAdresses(host, racinfo)
   198  		if len(services) == 0 {
   199  			log.Debugf("no racinfo found, will use original entry %s:%s", host, port)
   200  			services = append(services, dblib.ServiceEntryType{Host: host, Port: port, Address: host + ":" + port})
   201  		}
   202  		allservices = append(allservices, services...)
   203  	}
   204  	return
   205  }
   206  
   207  func doPortcheck(c *cobra.Command, args []string) (err error) {
   208  	tcpcheck = true
   209  	err = portInfo(c, args)
   210  	return
   211  }
   212  
   213  func doTCPPing(host string, address string) {
   214  	d := net.Dialer{Timeout: time.Duration(pingTimeout) * time.Second}
   215  	_, err := d.Dial("tcp", address)
   216  	if err != nil {
   217  		match, _ := regexp.MatchString("refused", err.Error())
   218  		if match {
   219  			// Closed
   220  			log.Infof("%s, %s is CLOSED/REFUSED (no service)", host, address)
   221  			fmt.Printf("%s (%s) is CLOSED/REFUSED (no service)\n", host, address)
   222  			return
   223  		}
   224  		match, _ = regexp.MatchString("timeout", err.Error())
   225  		if match {
   226  			// Timeout
   227  			log.Infof("%s (%s) TIMEOUT (blocked)", host, address)
   228  			fmt.Printf("%s (%s) TIMEOUT (blocked)\n", host, address)
   229  			return
   230  		}
   231  	} else {
   232  		// Open
   233  		log.Infof("%s(%s) is OPEN", host, address)
   234  		fmt.Printf("%s (%s) is OPEN\n", host, address)
   235  		return
   236  	}
   237  	// Default
   238  	e := err.Error()
   239  	e = strings.ReplaceAll(e, "dial tcp:", "")
   240  	log.Infof("%s(%s) port status PROBLEM: %s ", host, address, e)
   241  	fmt.Printf("%s (%s) port status PROBLEM: %s\n", host, address, e)
   242  }
   243  
   244  func getTnsInfo(_ *cobra.Command, args []string) (err error) {
   245  	if tnsKey == "" {
   246  		tnsKey = args[0]
   247  	}
   248  	entry, err := getEntry(tnsKey)
   249  	if err == nil {
   250  		desc := entry.Desc
   251  		tnsAlias := entry.Name
   252  		loc := entry.Location
   253  		desc = strings.ReplaceAll(desc, "\r", " ")
   254  		desc = strings.ReplaceAll(desc, "\n", "\n  ")
   255  		desc = strings.ReplaceAll(desc, "(ADDRESS_LIST", "  (ADDRESS_LIST")
   256  		desc = strings.ReplaceAll(desc, "(CONNECT_DATA", "  (CONNECT_DATA")
   257  		out := fmt.Sprintf("# Location: %s \n%s=  %s", loc, tnsAlias, desc)
   258  		log.Infof(out)
   259  		fmt.Println(out)
   260  	}
   261  	return
   262  }
   263  
   264  // getJdbcInfo prints the jdbc url for the given service
   265  func getJdbcInfo(_ *cobra.Command, args []string) (err error) {
   266  	out := ""
   267  	if tnsKey == "" {
   268  		tnsKey = args[0]
   269  	}
   270  	entry, err := getEntry(tnsKey)
   271  	if err != nil {
   272  		return err
   273  	}
   274  	desc := entry.Desc
   275  	// disable TRANSPORT_CONNECT_TIMEOUT modification
   276  	if noModifyTransportConnectTimeout {
   277  		dblib.ModifyJDBCTransportConnectTimeout = false
   278  	}
   279  	out, err = dblib.GetJDBCUrl(desc)
   280  	log.Infof(out)
   281  	fmt.Println(out)
   282  
   283  	return
   284  }
   285  
   286  func checkTns(_ *cobra.Command, args []string) (err error) {
   287  	// load available tns entries
   288  	tnsEntries, domain, err := dblib.GetTnsnames(filename, true)
   289  	l := len(tnsEntries)
   290  	if err != nil {
   291  		return
   292  	}
   293  	if l == 0 {
   294  		err = fmt.Errorf("cannot proceed without tns entries")
   295  		log.Error(err)
   296  		return
   297  	}
   298  	log.Debugf("have %d tns entries", l)
   299  	if dbUser == "" {
   300  		dbUser = common.GetEnv("TNSCLI_USER", "")
   301  		log.Debugf("use default db user %s", dbUser)
   302  	}
   303  	if dbPass == "" {
   304  		dbPass = common.GetEnv("TNSCLI_PASSWORD", "")
   305  		log.Debug("use default db pass")
   306  	}
   307  
   308  	// do checks depending on mode
   309  	if all {
   310  		// all flag given, check every entry
   311  		return allCheck(tnsEntries)
   312  	}
   313  	// check specific entries from arg
   314  	return singleCheck(args, tnsEntries, domain)
   315  }
   316  
   317  func allCheck(tnsEntries dblib.TNSEntries) (err error) {
   318  	var failed []string
   319  	l := len(tnsEntries)
   320  	log.Debugf("check all %d entries", l)
   321  	keys := make([]string, 0, l)
   322  	for k := range tnsEntries {
   323  		keys = append(keys, k)
   324  	}
   325  	sort.Strings(keys)
   326  	o := 0
   327  	e := 0
   328  	i := 0
   329  	for _, k := range keys {
   330  		entry := tnsEntries[k]
   331  		desc := entry.Desc
   332  		tnsAlias := entry.Name
   333  		fmt.Printf("%s: ", tnsAlias)
   334  		ok, elapsed, hostval, errmsg := CheckWithOracle(dbUser, dbPass, desc, timeout)
   335  		if ok {
   336  			o++
   337  			if dbhostFlag {
   338  				fmt.Printf(" OK-> %s, %s\n", hostval, elapsed.Round(time.Millisecond))
   339  			} else {
   340  				fmt.Printf(" OK-> %s\n", elapsed.Round(time.Millisecond))
   341  			}
   342  		} else {
   343  			e++
   344  			fmt.Printf(" ERROR: %s\n", errmsg)
   345  			failed = append(failed, fmt.Sprintf("%s: %v", tnsAlias, errmsg))
   346  		}
   347  		i++
   348  	}
   349  	log.Info("Checks finished ...")
   350  	log.Infof(" %d entries checked, %d ok, %d failed\n", i, o, e)
   351  	if len(failed) > 0 {
   352  		for _, s := range failed {
   353  			fmt.Println(s)
   354  		}
   355  		err = fmt.Errorf("some checks failed")
   356  	}
   357  	return
   358  }
   359  
   360  func singleCheck(args []string, tnsEntries dblib.TNSEntries, domain string) (err error) {
   361  	// not all modus, we have to  check one single entry
   362  	// use first argument as service if is nothing given
   363  	la := len(args)
   364  	log.Debugf("service %s, domain '%s', args: %d -> %v", tnsKey, domain, la, args)
   365  	if tnsKey == "" && la > 0 {
   366  		tnsKey = args[0]
   367  	}
   368  	if tnsKey == "" {
   369  		err = fmt.Errorf("dont have a service to check, use --service to provide")
   370  		return
   371  	}
   372  	log.Debugf("get Entry for service %s ", tnsKey)
   373  	if entry, found := dblib.GetEntry(tnsKey, tnsEntries, domain); found {
   374  		err = testService(entry)
   375  		return
   376  	}
   377  	err = fmt.Errorf("alias %s not found", tnsKey)
   378  	return
   379  }
   380  func testService(entry dblib.TNSEntry) (err error) {
   381  	desc := entry.Desc
   382  	location := entry.Location
   383  	tnsAlias := entry.Name
   384  	log.Debugf("connect service %s from %s, timeout: %d s", tnsAlias, location, timeout)
   385  	log.Infof("use entry \n%s=%s\n", tnsAlias, strings.ReplaceAll(desc, "\r", " "))
   386  	con := ""
   387  	if len(dbUser) > 0 {
   388  		con = fmt.Sprintf("using user '%s'", dbUser)
   389  	}
   390  	ok, elapsed, hostval, errmsg := CheckWithOracle(dbUser, dbPass, desc, timeout)
   391  	if ok {
   392  		hv := ""
   393  		if hostval != "" {
   394  			hv = "(" + hostval + ") "
   395  		}
   396  		log.Infof("service %s connected %s%s in %s\n", tnsKey, hv, con, elapsed.Round(time.Millisecond))
   397  		if dbhostFlag {
   398  			fmt.Printf("%s -> %s\n", tnsKey, hostval)
   399  		} else {
   400  			fmt.Printf("OK, service %s reachable\n", tnsAlias)
   401  		}
   402  	} else {
   403  		err = fmt.Errorf("service %s %s NOT reached:%s", tnsAlias, con, errmsg)
   404  	}
   405  	return
   406  }
   407  
   408  // CheckWithOracle try connecting to oracle with dummy creds to get an ORA error.
   409  // If this happens, the connection is working
   410  func CheckWithOracle(dbuser string, dbpass string, tnsDesc string, timeout int) (ok bool, elapsed time.Duration, hostval string, err error) {
   411  	urlOptions := map[string]string{
   412  		// "CONNECTION TIMEOUT": "3",
   413  	}
   414  	ok = false
   415  	if dbuser == "" {
   416  		dbuser = defaultUser
   417  	}
   418  	if dbpass == "" {
   419  		dbpass = defaultPassword
   420  	}
   421  	// jdbc url needs spaces stripped
   422  	tnsDesc = strings.Join(strings.Fields(tnsDesc), "")
   423  	url := goora.BuildJDBC(dbuser, dbpass, tnsDesc, urlOptions)
   424  	log.Debugf("Try to connect %s@%s", dbuser, tnsDesc)
   425  	start := time.Now()
   426  	db, err := dblib.DBConnect("oracle", url, timeout)
   427  	elapsed = time.Since(start)
   428  
   429  	// check results
   430  	if err != nil {
   431  		// check error code, we expect 1017
   432  		isOerr, code, _ := dblib.HaveOerr(err)
   433  		if isOerr && code == 1017 {
   434  			ok = true
   435  			log.Warnf("Connect OK, but Login error, maybe expected")
   436  		}
   437  	} else {
   438  		log.Debugf("Connection OK, test if db is open using select")
   439  		sql := "select 'DB is open, sysdate:'||to_char(sysdate,'YYYY-MM-DD HH24:MI:SS') from dual"
   440  		if dbhostFlag {
   441  			// extract host,cdb and pdb from database
   442  			sql = "select sys_context('USERENV','SERVER_HOST')||':'||sys_context('USERENV','INSTANCE_NAME')||':'||nvl(sys_context('USERENV','CON_NAME'),'') as dbhostFlag from dual"
   443  		}
   444  		hostval, err = dblib.SelectOneStringValue(db, sql)
   445  		log.Infof("Query returned:  %s", hostval)
   446  		if err == nil {
   447  			ok = true
   448  		}
   449  	}
   450  	return
   451  }