// Sends a "new_server" or "confirm_new_server" message to target. // header: "new_server" or "confirm_new_server" // target: e.g. 1.2.3.4:20081 func Send_new_server(header string, target string) { keys := db.ServerKeys(target) if len(keys) == 0 { util.Log(0, "ERROR! Send_new_server: No key known for %v", target) return } msg := xml.NewHash("xml", "header", header) msg.Add(header) msg.Add("source", config.ServerSourceAddress) msg.Add("macaddress", config.MAC) msg.Add("loaded_modules", "gosaTriggered") msg.Add("loaded_modules", "siTriggered") msg.Add("loaded_modules", "logHandling") msg.Add("loaded_modules", "databases") msg.Add("loaded_modules", "server_server_com") msg.Add("loaded_modules", "clMessages") msg.Add("loaded_modules", "goSusi") msg.Add("key", keys[0]) msg.Add("target", target) serverpackageskey := config.ModuleKey["[ServerPackages]"] util.Log(2, "DEBUG! Sending %v to %v encrypted with key %v", header, target, serverpackageskey) Peer(target).Tell(msg.String(), serverpackageskey) }
// Updates the list of all releases func FAIReleasesListUpdate() { all_releases_mutex.Lock() defer all_releases_mutex.Unlock() all_releases = map[string]bool{} // NOTE: config.UnitTagFilter is not used here because unit tag filtering is done // in the FAIClasses() query. x, err := xml.LdifToHash("fai", true, ldapSearchBase(config.FAIBase, "objectClass=FAIbranch", "dn")) if err != nil { util.Log(0, "ERROR! LDAP error while trying to determine list of FAI releases: %v", err) return } for fai := x.First("fai"); fai != nil; fai = fai.Next() { dn := fai.Text("dn") release := extractReleaseFromFAIClassDN("ou=foo,ou=disk," + dn) if release == "" { continue } all_releases[release] = true } util.Log(1, "INFO! FAIReleasesListUpdate() found the following releases: %v", all_releases) }
// Tries to send a WOL to the given MAC. Returns true if one or more WOL packets were sent // or false if no subnet for the MAC is known or reachable. // NOTE: That this function returns true does not mean that the WOL has reached its target. func TriggerWake(macaddress string) bool { wake_target := []string{} if system := db.ServerWithMAC(macaddress); system != nil { wake_target = append(wake_target, strings.Split(system.Text("source"), ":")[0]) } if system := db.ClientWithMAC(macaddress); system != nil { wake_target = append(wake_target, strings.Split(system.Text("client"), ":")[0]) } if system := db.SystemFullyQualifiedNameForMAC(macaddress); system != "none" { wake_target = append(wake_target, system) } woken := false for i := range wake_target { if err := util.Wake(macaddress, wake_target[i]); err == nil { util.Log(1, "INFO! Sent Wake-On-LAN for MAC %v to %v", macaddress, wake_target[i]) woken = true // We do not break here, because the data in the serverDB or clientDB may // be stale and since we're sending UDP packets, there's no guarantee // that util.Wake() will fail even if the system is no longer there. // Since the WOL packets include the MAC address it can't hurt to // send more than necessary. } else { util.Log(0, "ERROR! Could not send Wake-On-LAN for MAC %v to %v: %v", macaddress, wake_target[i], err) } } return woken }
// Handles the message "CLMSG_PROGRESS". // xmlmsg: the decrypted and parsed message func clmsg_progress(xmlmsg *xml.Hash) { macaddress := xmlmsg.Text("macaddress") progress := xmlmsg.Text("CLMSG_PROGRESS") // ATTENTION! The CLMSG_PROGRESS message may be generated by clmsg_save_fai_log()! // In that case fields <header> and <source> are missing! util.Log(1, "INFO! Progress info from client %v with MAC %v: %v", xmlmsg.Text("source"), macaddress, progress) // Because we don't know what kind of job the progress is for, we update // all local jobs in status processing for the client's MAC. // In theory only one job should be in status processing for a single client at // any given time, but sometimes jobs get "lost", typically through manual // intervention. Progressing all jobs in lockstep has the nice side effect of // taking such old stuck jobs along. all_processing_jobs_for_mac := xml.FilterSimple("siserver", config.ServerSourceAddress, "status", "processing", "macaddress", macaddress) // the additional comparisons with "0" and "100" are there to allow overwriting // non-numerical progress values such as "hardware-detection". do_not_run_progress_backwards := xml.FilterOr([]xml.HashFilter{xml.FilterRel("progress", progress, -1, -1), xml.FilterRel("progress", "0", -1, -1), xml.FilterRel("progress", "100", 1, 1)}) filter := xml.FilterAnd([]xml.HashFilter{all_processing_jobs_for_mac, do_not_run_progress_backwards}) db.JobsModifyLocal(filter, xml.NewHash("job", "progress", progress)) if progress == "100" { util.Log(1, "INFO! Progress 100%% => Setting status \"done\" for client %v with MAC %v", xmlmsg.Text("source"), macaddress) db.JobsModifyLocal(all_processing_jobs_for_mac, xml.NewHash("job", "status", "done")) // Setting faistate => "localboot" is done in action/process_act.go in reaction // to the removal of the job. } }
// Initializes serverDB with data from the file config.ServerDBPath if it exists, // as well as the list of peer servers from DNS and [ServerPackages]/address. // Not an init() because main() needs to set up some things first. func ServersInit() { db_storer := &LoggingFileStorer{xml.FileStorer{config.ServerDBPath}} var delay time.Duration = config.DBPersistDelay serverDB = xml.NewDB("serverdb", db_storer, delay) if !config.FreshDatabase { xmldata, err := xml.FileToHash(config.ServerDBPath) if err != nil { if os.IsNotExist(err) { /* File does not exist is not an error that needs to be reported */ } else { util.Log(0, "ERROR! ServersInit reading '%v': %v", config.ServerDBPath, err) } } else { serverDB.Init(xmldata) } } if config.DNSLookup { addServersFromDNS() } else { util.Log(1, "INFO! DNS lookup disabled. Will not add peer servers from DNS.") } addServersFromConfig() util.Log(1, "INFO! All known peer addresses with duplicates removed: %v", ServerAddresses()) }
func FAIReboot(job *xml.Hash) { macaddress := job.Text("macaddress") util.Log(0, "INFO! Aborting all running install and softupdate jobs for %v", macaddress) delete_system := false faistate := "error:fiddledidoo:-1:crit:Job aborted by admin. System in unknown state." sys, err := db.SystemGetAllDataForMAC(macaddress, false) if err != nil { util.Log(0, "ERROR! FAIReboot(): %v", err) // do not abort. Killing jobs may still work. } else { // If the system is in incoming, delete it because faimond-ldap does not // cope well with incomplete LDAP objects and tries to boot them from local disk. dnparts := strings.SplitN(sys.Text("dn"), ",", 2) if len(dnparts) > 1 && strings.HasPrefix(dnparts[1], config.IncomingOU) { delete_system = true } } db.SystemForceFAIState(macaddress, faistate) if delete_system { util.Log(1, "INFO! System %v is in %v => Deleting LDAP entry", macaddress, config.IncomingOU) err = db.SystemReplace(sys, nil) if err != nil { util.Log(0, "ERROR! LDAP error while deleting %v: %v", macaddress, err) } } }
// Accepts TCP connections on listener and sends them on the channel tcp_connections. func acceptConnections(listener *net.TCPListener, tcp_connections chan<- *net.TCPConn) { for { message := true for { // if we've reached the maximum number of connections, wait if atomic.AddInt32(&ActiveConnections, 1) <= config.MaxConnections { break } atomic.AddInt32(&ActiveConnections, -1) if message { util.Log(0, "WARNING! Maximum number of %v active connections reached => Throttling", config.MaxConnections) message = false } time.Sleep(100 * time.Millisecond) } tcpConn, err := listener.AcceptTCP() if err != nil { if Shutdown { return } util.Log(0, "ERROR! AcceptTCP: %v", err) } else { tcp_connections <- tcpConn } } }
// Handles all messages of the form "new_*_config" by calling config.NewConfigHookPath. // xmlmsg: the decrypted and parsed message func new_foo_config(xmlmsg *xml.Hash) { target := xmlmsg.Text("target") if target != "" && target != config.ServerSourceAddress { // See https://code.google.com/p/go-susi/issues/detail?id=126 util.Log(0, "WARNING! Ignoring message with incorrect target: %v", xmlmsg) return } header := xmlmsg.Text("header") env := config.HookEnvironment() for _, tag := range xmlmsg.Subtags() { if tag == header { continue } env = append(env, tag+"="+strings.Join(xmlmsg.Get(tag), "\n")) } env = append(env, header+"=1") cmd := exec.Command(config.NewConfigHookPath) cmd.Env = append(env, os.Environ()...) util.Log(1, "INFO! Running %v with parameters %v", config.NewConfigHookPath, env) out, err := cmd.CombinedOutput() if err != nil { util.Log(0, "ERROR! Error executing %v: %v (%v)", config.NewConfigHookPath, err, string(out)) } }
// Sets the selected system's faistate and removes all running install and update // jobs affecting the system. // // ATTENTION! This function takes a while to complete because it tries multiple // times if necessary and verifies that the faistate has actually been set. func SystemForceFAIState(macaddress, faistate string) { util.Log(1, "INFO! Forcing faiState for %v to %v", macaddress, faistate) // retry for 30s endtime := time.Now().Add(30 * time.Second) for time.Now().Before(endtime) { SystemSetState(macaddress, "faiState", faistate) // remove softupdate and install jobs ... job_types_to_kill := xml.FilterOr( []xml.HashFilter{xml.FilterSimple("headertag", "trigger_action_reinstall"), xml.FilterSimple("headertag", "trigger_action_update")}) // ... that are already happening or scheduled within the next 5 minutes ... timeframe := xml.FilterRel("timestamp", util.MakeTimestamp(time.Now().Add(5*time.Minute)), -1, 0) // ... that affect the machine for which we force the faistate target := xml.FilterSimple("macaddress", macaddress) filter := xml.FilterAnd([]xml.HashFilter{job_types_to_kill, timeframe, target}) JobsRemove(filter) // Wait a little and see if the jobs are gone time.Sleep(3 * time.Second) if JobsQuery(filter).FirstChild() == nil { // if all jobs are gone // set state again just in case the job removal raced with something that set faistate SystemSetState(macaddress, "faiState", faistate) return // we're done } // else if some jobs remained util.Log(1, "INFO! ForceFAIState(%v, %v): Some install/softupdate jobs remain => Retrying", macaddress, faistate) } util.Log(0, "ERROR! ForceFAIState(%v, %v): Some install/softupdate jobs could not be removed.", macaddress, faistate) }
// Returns the IP address (IPv4 if possible) for the machine with the given name. // The name may or may not include a domain. // Returns "none" if the IP address could not be determined. // // ATTENTION! This function accesses a variety of external sources // and may therefore take a while. If possible you should use it asynchronously. func SystemIPAddressForName(host string) string { ip, err := util.Resolve(host, config.IP) if err != nil { // if host already contains a domain, give up if strings.Index(host, ".") >= 0 { util.Log(0, "ERROR! Resolve(\"%v\"): %v", host, err) return "none" } // if host does not contain a domain the DNS failure may simple be // caused by the machine being in a different subdomain. Try to // work around this by searching LDAP for the machine and use its // ipHostNumber if it is accurate. util.Log(1, "INFO! Could not resolve short name %v (error: %v). Trying LDAP.", host, err) var system *xml.Hash system, err = xml.LdifToHash("", true, ldapSearch(fmt.Sprintf("(&(objectClass=GOhard)(|(cn=%v)(cn=%v.*))%v)", LDAPFilterEscape(host), LDAPFilterEscape(host), config.UnitTagFilter), "ipHostNumber")) // the search may give multiple results. Use reverse lookup of ipHostNumber to // find the correct one (if there is one) for ihn := system.First("iphostnumber"); ihn != nil; ihn = ihn.Next() { ip := ihn.Text() fullname := SystemNameForIPAddress(ip) if strings.HasPrefix(fullname, host+".") { util.Log(1, "INFO! Found \"%v\" with IP %v in LDAP", fullname, ip) // use forward lookup for the full name to be sure we get the proper address return SystemIPAddressForName(fullname) } } util.Log(0, "ERROR! Could not get reliable IP address for %v from LDAP", host) return "none" } return ip }
// Handles the message "gosa_show_log_files_by_date_and_mac". // xmlmsg: the decrypted and parsed message // Returns: // unencrypted reply func gosa_show_log_files_by_date_and_mac(xmlmsg *xml.Hash) *xml.Hash { macaddress := xmlmsg.Text("mac") lmac := strings.ToLower(macaddress) subdir := xmlmsg.Text("date") if !macAddressRegexp.MatchString(macaddress) { emsg := fmt.Sprintf("Illegal or missing <mac> element in message: %v", xmlmsg) util.Log(0, "ERROR! %v", emsg) return ErrorReplyXML(emsg) } // As a precaution, make sure subdir contains no slashes. subdir = strings.Replace(subdir, "/", "_", -1) if subdir == "" { emsg := fmt.Sprintf("Missing or empty <date> element in message: %v", xmlmsg) util.Log(0, "ERROR! %v", emsg) return ErrorReplyXML(emsg) } header := "show_log_files_by_date_and_mac" x := xml.NewHash("xml", "header", header) x.Add(header) logdir := path.Join(config.FAILogPath, lmac, subdir) util.Log(2, "DEBUG! Listing log files from %v", logdir) names := []string{} dir, err := os.Open(logdir) if err == nil || !os.IsNotExist(err.(*os.PathError).Err) { if err != nil { util.Log(0, "ERROR! gosa_show_log_files_by_date_and_mac: %v", err) } else { defer dir.Close() fi, err := dir.Readdir(0) if err != nil { util.Log(0, "ERROR! gosa_show_log_files_by_date_and_mac: %v", err) } else { for _, info := range fi { // only list ordinary files if info.Mode()&^os.ModePerm == 0 { names = append(names, info.Name()) } } sort.Strings(names) for _, n := range names { x.Add(header, n) } } } } x.Add("source", config.ServerSourceAddress) x.Add("target", "GOSA") x.Add("session_id", "1") return x }
// Accepts UDP connections for TFTP requests on listen_address, serves read requests // for path P based on request_re and reply as follows: // // request_re and reply have to be lists of // equal length. Let request_re[i] be the first entry in request_re that // matches P, then reply[i] specifies the data to return for the request. // If reply[i] == "", then a file not found error is returned to the requestor. // If reply[i] starts with the character '|', the remainder is taken as the path // of a hook to execute and its stdout is returned to the requestor. // Otherwise reply[i] is taken as the path of the file whose contents to send to // the requestor. // // When executing a hook, an environment variable called "tftp_request" // is passed containing P. If request_re[i] has a capturing // group named "macaddress", the captured substring will be converted to // a MAC address by converting to lowercase, removing all characters // except 0-9a-f, left-padding to length 12 with 0s or truncating to length 12 // and inserting ":"s. The result will be added to // the hook environment in a variable named "macaddress" and if there // is an LDAP object for that macaddress, its attributes will be added // to the environment, too. // // Named subexpressions in request_re[i] other than "macaddress" will be // exported to the hook verbatim in like-named environment variables. func ListenAndServe(listen_address string, request_re []*regexp.Regexp, reply []string) { for i := range request_re { util.Log(1, "INFO! TFTP: %v -> %v", request_re[i], reply[i]) } udp_addr, err := net.ResolveUDPAddr("udp", listen_address) if err != nil { util.Log(0, "ERROR! Cannot start TFTP server: %v", err) return } udp_conn, err := net.ListenUDP("udp", udp_addr) if err != nil { util.Log(0, "ERROR! ListenUDP(): %v", err) return } defer udp_conn.Close() readbuf := make([]byte, 16384) for { n, return_addr, err := udp_conn.ReadFromUDP(readbuf) if err != nil { util.Log(0, "ERROR! ReadFromUDP(): %v", err) continue } // Make a copy of the buffer BEFORE starting the goroutine to prevent subsequent requests from // overwriting the buffer. payload := string(readbuf[:n]) go util.WithPanicHandler(func() { handleConnection(return_addr, payload, request_re, reply) }) } }
// Tries to re-establish communication with a client/server at the given IP, // by // 1) sending here_i_am to the server where we are registered. We do this // even if config.RunServer (i.e. we are registered at ourselves) because // this will trigger new_foreign_client messages sent to peers so that other // servers that may believe they own us correct their data. // 2) sending (if config.RunServer) new_server messages to all known servers // we find for the IP in our servers database. // 3) if config.RunServer and in 2) we did not find a server at that IP, // maybe it's a client that thinks we are its server. Send "deregistered" to // all ClientPorts in that case to cause re-registration. func tryToReestablishCommunicationWith(ip string) { // Wait a little to limit the rate of spam wars between // 2 machines that can't re-establish communication (e.g. because of changed // keys in server.conf). mapIP2ReestablishDelay_mutex.Lock() var delay time.Duration var ok bool if delay, ok = mapIP2ReestablishDelay[ip]; !ok { delay = 1 * time.Minute } mapIP2ReestablishDelay[ip] = 2 * delay mapIP2ReestablishDelay_mutex.Unlock() // if the delay exceeds 24h this means that we got multiple // reestablish requests while we're still waiting to begin one // in that case, bail out. if delay > 24*time.Hour { return } util.Log(0, "WARNING! Will try to re-establish communication with %v after waiting %v", ip, delay) time.Sleep(delay) // if we actually completed a 10h wait, reset the timer to 1 minute if delay >= 10*time.Hour { mapIP2ReestablishDelay_mutex.Lock() mapIP2ReestablishDelay[ip] = 1 * time.Minute mapIP2ReestablishDelay_mutex.Unlock() } util.Log(0, "WARNING! Will try to re-establish communication with %v", ip) ConfirmRegistration() // 1) ip, err := util.Resolve(ip, config.IP) if err != nil { util.Log(0, "ERROR! Resolve(): %v", err) } if config.RunServer { // 2) sendmuell := true for _, server := range db.ServerAddresses() { if strings.HasPrefix(server, ip) { sendmuell = false srv := server go util.WithPanicHandler(func() { Send_new_server("new_server", srv) }) } } if sendmuell { for _, port := range config.ClientPorts { addr := ip + ":" + port if addr != config.ServerSourceAddress { // never send "deregistered" to our own server dereg := "<xml><header>deregistered</header><source>" + config.ServerSourceAddress + "</source><target>" + addr + "</target></xml>" go security.SendLnTo(addr, dereg, "", false) } } } } }
func handle_tlsconn(conn *tls.Conn, context *Context) bool { conn.SetDeadline(time.Now().Add(config.TimeoutTLS)) err := conn.Handshake() if err != nil { util.Log(0, "ERROR! [SECURITY] TLS Handshake: %v", err) return false } var no_deadline time.Time conn.SetDeadline(no_deadline) state := conn.ConnectionState() if len(state.PeerCertificates) == 0 { util.Log(0, "ERROR! [SECURITY] TLS peer has no certificate") return false } cert := state.PeerCertificates[0] // docs are unclear about this but I think leaf certificate is the first entry because that's as it is in tls.Certificate if util.LogLevel >= 2 { // because creating the dump is expensive util.Log(2, "DEBUG! [SECURITY] Peer certificate presented by %v:\n%v", conn.RemoteAddr(), CertificateInfo(cert)) } for _, cacert := range config.CACert { err = cert.CheckSignatureFrom(cacert) if err == nil { if string(cacert.RawSubject) != string(cert.RawIssuer) { err = fmt.Errorf("Certificate was issued by wrong CA: \"%v\" instead of \"%v\"", cacert.Subject, cert.Issuer) } else { break // stop checking if we found a match for a CA. err == nil here! } } } if err != nil { util.Log(0, "ERROR! [SECURITY] TLS peer presented certificate not signed by trusted CA: %v", err) return false } for _, e := range cert.Extensions { if len(e.Id) == 4 && e.Id[0] == 2 && e.Id[1] == 5 && e.Id[2] == 29 && e.Id[3] == 17 { parseSANExtension(e.Value, context) } else if len(e.Id) == 9 && e.Id[0] == 1 && e.Id[1] == 3 && e.Id[2] == 6 && e.Id[3] == 1 && e.Id[4] == 4 && e.Id[5] == 1 && e.Id[6] == 45753 && e.Id[7] == 1 { switch e.Id[8] { case 5: err = parseConnectionLimits(e.Value, context) if err != nil { util.Log(0, "ERROR! [SECURITY] GosaConnectionLimits: %v", err) } case 6: //err = parseAccessControl(e.Value, context) //if err != nil { util.Log(0, "ERROR! [SECURITY] GosaAccessControl: %v", err) } } } } context.TLS = true return true }
// Tells this connection if its peer // advertises <loaded_modules>goSusi</loaded_modules>. func (conn *PeerConnection) SetGoSusi(is_gosusi bool) { if is_gosusi { util.Log(1, "INFO! Peer %v uses go-susi protocol", conn.addr) } else { util.Log(1, "INFO! Peer %v uses old gosa-si protocol", conn.addr) } conn.is_gosusi = is_gosusi }
// Returns a list of all known Debian software repositories as well as the // available releases and their sections. If none are found, the return // value is <faidb></faidb>. The general format of the return value is // <faidb> // <repository> // <timestamp>20130304093211</timestamp> // <fai_release>halut/2.4.0</fai_release> // <repopath>halut-security</repopath> // <tag>1154342234048479900</tag> // <server>http://vts-susi.example.de/repo</server> // <sections>main,contrib,non-free,lhm,ff</sections> // </repository> // <repository> // ... // </repository> // ... // </faidb> // // See operator's manual, description of message gosa_query_fai_server for // the meanings of the individual elements. func FAIServers() *xml.Hash { ldapSearchResults := []*xml.Hash{} // NOTE: We do NOT add config.UnitTagFilter here because the results are individually // tagged within the reply. x, err := xml.LdifToHash("repository", true, ldapSearch("(&(FAIrepository=*)(objectClass=FAIrepositoryServer))", "FAIrepository", "gosaUnitTag")) if err != nil { util.Log(0, "ERROR! LDAP error while looking for FAIrepositoryServer objects: %v", err) } else { ldapSearchResults = append(ldapSearchResults, x) } for _, ou := range config.LDAPServerOUs { x, err := xml.LdifToHash("repository", true, ldapSearchBaseScope(ou, "one", "(&(FAIrepository=*)(objectClass=FAIrepositoryServer))", "FAIrepository", "gosaUnitTag")) if err != nil { util.Log(0, "ERROR! LDAP error while looking for FAIrepositoryServer objects in %v: %v", ou, err) } else { ldapSearchResults = append(ldapSearchResults, x) } } result := xml.NewHash("faidb") timestamp := util.MakeTimestamp(time.Now()) mapRepoPath2FAIrelease_mutex.Lock() defer mapRepoPath2FAIrelease_mutex.Unlock() for _, x := range ldapSearchResults { for repo := x.First("repository"); repo != nil; repo = repo.Next() { tag := repo.Text("gosaunittag") // http://vts-susi.example.de/repo|parent-repo.example.de|plophos/4.1.0|main,restricted,universe,multiverse for _, fairepo := range repo.Get("fairepository") { repodat := strings.Split(fairepo, "|") if len(repodat) != 4 { util.Log(0, "ERROR! Cannot parse FAIrepository=%v", fairepo) continue } repository := xml.NewHash("repository", "timestamp", timestamp) repository.Add("repopath", repodat[2]) if fairelease, ok := mapRepoPath2FAIrelease[repodat[2]]; ok { repository.Add("fai_release", fairelease) } else { repository.Add("fai_release", repodat[2]) } if tag != "" { repository.Add("tag", tag) } repository.Add("server", repodat[0]) repository.Add("sections", repodat[3]) result.AddWithOwnership(repository) } } } return result }
// Reads the output from the program config.KernelListHookPath (LDIF) and // uses it to replace kerneldb. func KernelListHook() { start := time.Now() util.Log(1, "INFO! Running kernel-list-hook %v", config.KernelListHookPath) cmd := exec.Command(config.KernelListHookPath) cmd.Env = append(config.HookEnvironment(), os.Environ()...) cmd.Env = append(cmd.Env, "PackageListCacheDir="+config.PackageCacheDir) klist, err := xml.LdifToHash("kernel", true, cmd) if err != nil { util.Log(0, "ERROR! kernel-list-hook %v: %v", config.KernelListHookPath, err) return } if klist.First("kernel") == nil { util.Log(0, "ERROR! kernel-list-hook %v returned no data", config.KernelListHookPath) return } util.Log(1, "INFO! Finished kernel-list-hook. Running time: %v", time.Since(start)) kerneldata := xml.NewHash("kerneldb") accepted := 0 total := 0 for kernel := klist.First("kernel"); kernel != nil; kernel = kernel.Next() { total++ cn := kernel.Get("cn") if len(cn) == 0 { util.Log(0, "ERROR! kernel-list-hook %v returned entry without cn: %v", config.KernelListHookPath, kernel) continue } if len(cn) > 1 { util.Log(0, "ERROR! kernel-list-hook %v returned entry with multiple cn values: %v", config.KernelListHookPath, kernel) continue } release := kernel.Get("release") if len(release) == 0 { util.Log(0, "ERROR! kernel-list-hook %v returned entry without release: %v", config.KernelListHookPath, kernel) continue } if len(release) > 1 { util.Log(0, "ERROR! kernel-list-hook %v returned entry with multiple release values: %v", config.KernelListHookPath, kernel) continue } k := xml.NewHash("kernel", "fai_release", release[0]) k.Add("cn", cn[0]) kerneldata.AddWithOwnership(k) accepted++ } if kerneldata.First("kernel") == nil { util.Log(0, "ERROR! kernel-list-hook %v returned no valid entries", config.KernelListHookPath) } else { util.Log(1, "INFO! kernel-list-hook: %v/%v entries accepted into database", accepted, total) kerneldb.Init(kerneldata) } }
func main() { config.ReadArgs(os.Args[1:]) if config.PrintVersion { fmt.Printf(`go-susi %v (revision %v) Copyright (c) 2013 Matthias S. Benkmann This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. `, config.Version, config.Revision) } if config.PrintHelp { fmt.Println(`USAGE: tftp [args] --help print this text and exit --version print version and exit -v print operator debug messages (INFO) -vv print developer debug messages (DEBUG) ATTENTION! developer messages include keys! -c <file> read config from <file> instead of default location `) } if config.PrintVersion || config.PrintHelp { os.Exit(0) } config.ReadConfig() logdir, _ := path.Split(config.LogFilePath) logfile, err := os.OpenFile(logdir+"go-susi-tftp.log", os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644) if err != nil { util.Log(0, "ERROR! %v", err) // Do not exit. We can go on without logging to a file. } else { // Send log output to both stderr AND the log file logfile.Close() // will be re-opened on the first write util.Logger = log.New(io.MultiWriter(os.Stderr, util.LogFile(logfile.Name())), "", 0) } util.LogLevel = config.LogLevel config.ReadNetwork() // after config.ReadConfig() setConfigUnitTag() // after config.ReadNetwork() config.FAIBase = db.LDAPFAIBase() util.Log(1, "INFO! FAI base: %v", config.FAIBase) util.Log(1, "INFO! Accepting FAI monitoring messages on %v", config.FAIMonPort) go faimon(":" + config.FAIMonPort) util.Log(1, "INFO! Accepting TFTP requests on %v", config.TFTPPort) tftp.ListenAndServe(":"+config.TFTPPort, config.TFTPFiles, config.PXELinuxCfgHookPath) }
// Handles the message "gosa_set_activated_for_installation". // xmlmsg: the decrypted and parsed message func gosa_set_activated_for_installation(xmlmsg *xml.Hash) { if xmlmsg.Text("header")[0:4] == "gosa" { util.Log(2, "DEBUG! gosa_set_activated_for_installation -> gosa_trigger_action") gosa_trigger_action(xmlmsg) } else { util.Log(2, "DEBUG! job_set_activated_for_installation -> job_trigger_action") job_trigger_action(xmlmsg) } }
// Parses args and sets config variables accordingly. func ReadArgs(args []string) { LogLevel = 0 for i := 0; i < len(args); i++ { arg := args[i] if arg == "-v" || arg == "-vv" || arg == "-vvv" || arg == "-vvvv" || arg == "-vvvvv" || arg == "-vvvvvv" || arg == "-vvvvvvv" { LogLevel = len(arg) - 1 } else if arg == "-f" { FreshDatabase = true } else if strings.HasPrefix(arg, "--test=") { testdir := arg[7:] LogFilePath = testdir + "/go-susi.log" ServerConfigPath = testdir + "/server.conf" ServersOUConfigPath = testdir + "/ou=servers.conf" ClientConfigPath = testdir + "/client.conf" JobDBPath = testdir + "/jobdb.xml" ServerDBPath = testdir + "/serverdb.xml" ClientDBPath = testdir + "/clientdb.xml" CACertPath = []string{testdir + "/ca.cert"} CertPath = testdir + "/si.cert" CertKeyPath = testdir + "/si.key" PackageCacheDir = testdir FAILogPath = testdir } else if arg == "-c" { i++ if i >= len(args) { util.Log(0, "ERROR! ReadArgs: missing argument to -c") } else { ServerConfigPath = args[i] ClientConfigPath = "" } } else if arg == "--help" { PrintHelp = true } else if arg == "--version" { PrintVersion = true } else if arg == "--stats" { PrintStats = true } else { util.Log(0, "ERROR! ReadArgs: Unknown command line switch: %v", arg) } } }
// Opens a connection to target (e.g. "foo.example.com:20081"), // sends msg followed by \r\n. // If keep_open == false, the connection is closed, otherwise it is // returned together with the corresponding security.Context. // The connection will be secured according to // the config settings. If a certificate is configured, the connection // will use TLS (and the key argument will be ignored). Otherwise, key // will be used to GosaEncrypt() the message before sending it over // a non-TLS connection. // If an error occurs, it is logged and nil is returned even if keep_open. func SendLnTo(target, msg, key string, keep_open bool) (net.Conn, *Context) { conn, err := net.Dial("tcp", target) if err != nil { util.Log(0, "ERROR! Could not connect to %v: %v\n", target, err) return nil, nil } if !keep_open { defer conn.Close() } // enable keep alive to avoid connections hanging forever in case of routing issues etc. err = conn.(*net.TCPConn).SetKeepAlive(true) if err != nil { util.Log(0, "ERROR! SetKeepAlive: %v", err) // This is not fatal => Don't abort send attempt } if config.TLSClientConfig != nil { conn.SetDeadline(time.Now().Add(config.TimeoutTLS)) // don't allow stalling on STARTTLS _, err = util.WriteAll(conn, starttls) if err != nil { util.Log(0, "ERROR! [SECURITY] Could not send STARTTLS to %v: %v\n", target, err) conn.Close() // even if keep_open return nil, nil } var no_deadline time.Time conn.SetDeadline(no_deadline) conn = tls.Client(conn, config.TLSClientConfig) } else { msg = GosaEncrypt(msg, key) } context := ContextFor(conn) if context == nil { conn.Close() // even if keep_open return nil, nil } err = util.SendLn(conn, msg, config.Timeout) if err != nil { util.Log(0, "ERROR! [SECURITY] While sending message to %v: %v\n", target, err) conn.Close() // even if keep_open return nil, nil } if keep_open { return conn, context } return nil, nil }
func setConfigUnitTag() { util.Log(1, "INFO! Getting my own system's gosaUnitTag from LDAP") config.UnitTag = db.SystemGetState(config.MAC, "gosaUnitTag") if config.UnitTag == "" { util.Log(1, "INFO! No gosaUnitTag found for %v => gosaUnitTag support disabled", config.MAC) } else { config.UnitTagFilter = "(gosaUnitTag=" + config.UnitTag + ")" config.AdminBase, config.Department = db.LDAPAdminBase() util.Log(1, "INFO! gosaUnitTag: %v Admin base: %v Department: %v", config.UnitTag, config.AdminBase, config.Department) } }
func faiConnection(conn *net.TCPConn) { defer conn.Close() var err error err = conn.SetKeepAlive(true) if err != nil { util.Log(0, "ERROR! SetKeepAlive: %v", err) } var buf bytes.Buffer defer buf.Reset() readbuf := make([]byte, 4096) n := 1 for n != 0 { n, err = conn.Read(readbuf) if err != nil && err != io.EOF { util.Log(0, "ERROR! Read: %v", err) } if n == 0 && err == nil { util.Log(0, "ERROR! Read 0 bytes but no error reported") } // Find complete lines terminated by '\n' and process them. for start := 0; ; { eol := start for ; eol < n; eol++ { if readbuf[eol] == '\n' { break } } // no \n found, append to buf and continue reading if eol == n { buf.Write(readbuf[start:n]) break } // append to rest of line to buffered contents buf.Write(readbuf[start:eol]) start = eol + 1 buf.TrimSpace() util.Log(2, "DEBUG! FAI monitor message from %v: %v", conn.RemoteAddr(), buf.String()) buf.Reset() } } if buf.Len() != 0 { util.Log(2, "DEBUG! Incomplete FAI monitor message (i.e. not terminated by \"\\n\") from %v: %v", conn.RemoteAddr(), buf.String()) } }
// Returns the CN stored in LDAP for the system with the given MAC address. // It may or may not include a domain. // Use PlainnameForMAC() or FullyQualifiedNameForMAC() if you want // predictability. // // Returns "" (NOT "none" like the other functions!) // if the name could not be determined. // // ATTENTION! This function accesses LDAP and may therefore take a while. // If possible you should use it asynchronously. func SystemCommonNameForMAC(macaddress string) string { system, err := xml.LdifToHash("", true, ldapSearch(fmt.Sprintf("(&(objectClass=GOhard)(macAddress=%v)%v)", LDAPFilterEscape(macaddress), config.UnitTagFilter), "cn")) names := system.Get("cn") if len(names) == 0 { util.Log(0, "ERROR! Error getting cn for MAC %v: %v", macaddress, err) return "" } if len(names) != 1 { util.Log(0, "ERROR! Multiple LDAP objects with same MAC %v: %v", macaddress, names) return "" } return names[0] }
// Handles the message "gosa_show_log_by_mac". // xmlmsg: the decrypted and parsed message // Returns: // unencrypted reply func gosa_show_log_by_mac(xmlmsg *xml.Hash) *xml.Hash { macaddress := xmlmsg.Text("mac") if !macAddressRegexp.MatchString(macaddress) { emsg := fmt.Sprintf("Illegal or missing <mac> element in message: %v", xmlmsg) util.Log(0, "ERROR! %v", emsg) return ErrorReplyXML(emsg) } lmac := strings.ToLower(macaddress) logdir := path.Join(config.FAILogPath, lmac) names := []string{} dir, err := os.Open(logdir) if err == nil || !os.IsNotExist(err.(*os.PathError).Err) { if err != nil { util.Log(0, "ERROR! gosa_show_log_by_mac: %v", err) return ErrorReplyXML(err) } defer dir.Close() fi, err := dir.Readdir(0) if err != nil { util.Log(0, "ERROR! gosa_show_log_by_mac: %v", err) return ErrorReplyXML(err) } for _, info := range fi { if info.IsDir() { names = append(names, info.Name()) } } sort.Strings(names) } ele := "mac_" + strings.Replace(lmac, ":", "_", -1) x := xml.NewHash("xml", "header", "show_log_by_mac") x.Add("show_log_by_mac") x.Add("source", config.ServerSourceAddress) x.Add("target", "GOSA") x.Add("session_id", "1") for _, name := range names { x.Add(ele, name) } return x }
// Adds server (host:port) to the database if it does not exist yet (and if it // is not identical to this go-susi). func addServer(server string) { server, err := util.Resolve(server, config.IP) if err != nil { util.Log(0, "ERROR! util.Resolve(\"%v\"): %v", server, err) return } ip, port, err := net.SplitHostPort(server) if err != nil { util.Log(0, "ERROR! net.SplitHostPort(\"%v\"): %v", server, err) return } source := ip + ":" + port // do not add our own address if source == config.ServerSourceAddress { return } // if we don't have an entry for the server, generate a dummy entry. if len(ServerKeys(source)) == 0 { // There's no point in generating a random server key. // First of all, the server key is only as secure as the ServerPackages // module key (because whoever has that can decrypt the message that // contains the server key). // Secondly the whole gosa-si protocol is not really secure. For instance // there is lots of known plaintext and no salting of messages. And the // really important messages are all encrypted with fixed keys anyway. // So instead of pretending more security by generating a random key, // we make debugging a little easier by generating a unique key derived // from the ServerPackages module key. var key string if ip < config.IP { key = ip + config.IP } else { key = config.IP + ip } key = config.ModuleKey["[ServerPackages]"] + strings.Replace(key, ".", "", -1) server_xml := xml.NewHash("xml", "source", source) // If we have a TLS config, assume the peer does, too, and mark it as // such by storing an empty string as key. if config.TLSClientConfig != nil { key = "" } server_xml.Add("key", key) ServerUpdate(server_xml) } }
// Handles the message "gosa_query_fai_release". // xmlmsg: the decrypted and parsed message // Returns: // unencrypted reply func gosa_query_fai_release(xmlmsg *xml.Hash) *xml.Hash { where := xmlmsg.First("where") if where == nil { where = xml.NewHash("where") } filter, err := xml.WhereFilter(where) if err != nil { util.Log(0, "ERROR! gosa_query_fai_release: Error parsing <where>: %v", err) filter = xml.FilterNone } faiclassesdb := db.FAIClasses(filter) faiclasses := xml.NewHash("xml", "header", "query_fai_release") var count uint64 = 1 for child := faiclassesdb.FirstChild(); child != nil; child = child.Next() { answer := child.Remove() answer.Rename("answer" + strconv.FormatUint(count, 10)) faiclasses.AddWithOwnership(answer) count++ } faiclasses.Add("source", config.ServerSourceAddress) faiclasses.Add("target", xmlmsg.Text("source")) faiclasses.Add("session_id", "1") return faiclasses }
// Handles all messages of the form "gosa_trigger_action_*". // xmlmsg: the decrypted and parsed message // Returns: // unencrypted reply func gosa_trigger_action(xmlmsg *xml.Hash) *xml.Hash { util.Log(2, "DEBUG! gosa_trigger_action(%v) -> job_trigger_action", xmlmsg) // translate gosa_trigger_* to job_trigger_* header := "job_" + strings.SplitN(xmlmsg.Text("header"), "_", 2)[1] xmlmsg.First("header").SetText(header) return job_trigger_action(xmlmsg) }
// Reads from connection tcpConn, logs any data received as an error and signals // actual network errors by closing the connection and pinging the queue. // This function returns when the first error is encountered on tcpConn. func monitorConnection(tcpConn net.Conn, queue *deque.Deque) { buf := make([]byte, 65536) for { n, err := tcpConn.Read(buf) if n > 0 { util.Log(0, "ERROR! Received %v bytes of unexpected data on Tell() channel to %v", n, tcpConn.RemoteAddr()) } if err != nil { util.Log(2, "DEBUG! monitorConnection terminating: %v", err) tcpConn.Close() // make sure the connection is closed in case the error didn't queue.Push("") // ping to wake up handleConnection() if it's blocked return } } }
func Update(job *xml.Hash) { util.Log(1, "INFO! Changing faistate of %v to softupdate", job.Text("macaddress")) db.SystemSetState(job.Text("macaddress"), "faiState", "softupdate") // Wait before sending WOL to prevent the situation in issue #169. time.Sleep(config.ActionAnnouncementTTL) Wake(job) }