<commit_before>// Copyright (c) 2015-2022 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package logger

import (
	"sync"

	"github.com/minio/minio/internal/logger/target/http"
	"github.com/minio/minio/internal/logger/target/kafka"
	"github.com/minio/minio/internal/logger/target/types"
)

// Target is the entity that we will receive
// a single log entry and Send it to the log target
//   e.g. Send the log to a http server
type Target interface {
	String() string
	Endpoint() string
	Init() error
	Cancel()
	Send(entry interface{}, errKind string) error
	Type() types.TargetType
}

var (
	swapAuditMuRW  sync.RWMutex
	swapSystemMuRW sync.RWMutex

	// systemTargets is the set of enabled loggers.
	// Must be immutable at all times.
	// Can be swapped to another while holding swapMu
	systemTargets = []Target{}

	// This is always set represent /dev/console target
	consoleTgt Target
)

// SystemTargets returns active targets.
// Returned slice may not be modified in any way.
func SystemTargets() []Target {
	swapSystemMuRW.RLock()
	defer swapSystemMuRW.RUnlock()

	res := systemTargets
	return res
}

// AuditTargets returns active audit targets.
// Returned slice may not be modified in any way.
func AuditTargets() []Target {
	swapAuditMuRW.RLock()
	defer swapAuditMuRW.RUnlock()

	res := auditTargets
	return res
}

// auditTargets is the list of enabled audit loggers
// Must be immutable at all times.
// Can be swapped to another while holding swapMu
var (
	auditTargets = []Target{}
)

// AddSystemTarget adds a new logger target to the
// list of enabled loggers
func AddSystemTarget(t Target) error {
	if err := t.Init(); err != nil {
		return err
	}

	swapSystemMuRW.Lock()
	defer swapSystemMuRW.Unlock()

	if consoleTgt == nil {
		if t.Type() == types.TargetConsole {
			consoleTgt = t
		}
	}
	updated := append(make([]Target, 0, len(systemTargets)+1), systemTargets...)
	updated = append(updated, t)
	systemTargets = updated

	return nil
}

func cancelAllSystemTargets() {
	for _, tgt := range systemTargets {
		tgt.Cancel()
	}
}

func initSystemTargets(cfgMap map[string]http.Config) (tgts []Target, err error) {
	for _, l := range cfgMap {
		if l.Enabled {
			t := http.New(l)
			if err = t.Init(); err != nil {
				return tgts, err
			}
			tgts = append(tgts, t)
		}
	}
	return tgts, err
}

func initKafkaTargets(cfgMap map[string]kafka.Config) (tgts []Target, err error) {
	for _, l := range cfgMap {
		if l.Enabled {
			t := kafka.New(l)
			if err = t.Init(); err != nil {
				return tgts, err
			}
			tgts = append(tgts, t)
		}
	}
	return tgts, err
}

// UpdateSystemTargets swaps targets with newly loaded ones from the cfg
func UpdateSystemTargets(cfg Config) error {
	newTgts, err := initSystemTargets(cfg.HTTP)
	if err != nil {
		return err
	}

	swapSystemMuRW.Lock()
	defer swapSystemMuRW.Unlock()

	for _, tgt := range systemTargets {
		// Preserve console target when dynamically updating
		// other HTTP targets, console target is always present.
		if tgt.Type() == types.TargetConsole {
			newTgts = append(newTgts, tgt)
			break
		}
	}

	cancelAllSystemTargets() // cancel running targets
	systemTargets = newTgts

	return nil
}

func cancelAuditTargetType(t types.TargetType) {
	for _, tgt := range auditTargets {
		if tgt.Type() == t {
			tgt.Cancel()
		}
	}
}

func existingAuditTargets(t types.TargetType) []Target {
	tgts := make([]Target, 0, len(auditTargets))
	for _, tgt := range auditTargets {
		if tgt.Type() == t {
			tgts = append(tgts, tgt)
		}
	}
	return tgts
}

// UpdateAuditWebhookTargets swaps audit webhook targets with newly loaded ones from the cfg
func UpdateAuditWebhookTargets(cfg Config) error {
	newTgts, err := initSystemTargets(cfg.AuditWebhook)
	if err != nil {
		return err
	}

	// retain kafka targets
	swapAuditMuRW.Lock()
	defer swapAuditMuRW.Unlock()

	newTgts = append(existingAuditTargets(types.TargetKafka), newTgts...)
	cancelAuditTargetType(types.TargetHTTP) // cancel running targets
	auditTargets = newTgts

	return nil
}

// UpdateAuditKafkaTargets swaps audit kafka targets with newly loaded ones from the cfg
func UpdateAuditKafkaTargets(cfg Config) error {
	updated, err := initKafkaTargets(cfg.AuditKafka)
	if err != nil {
		return err
	}

	swapAuditMuRW.Lock()
	defer swapAuditMuRW.Unlock()

	// retain HTTP targets
	updated = append(existingAuditTargets(types.TargetHTTP), updated...)
	cancelAuditTargetType(types.TargetKafka) // cancel running targets
	auditTargets = updated

	return nil
}
<commit_msg>Cancel old logger/audit targets outside lock (#14927)<commit_after>// Copyright (c) 2015-2022 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package logger

import (
	"sync"

	"github.com/minio/minio/internal/logger/target/http"
	"github.com/minio/minio/internal/logger/target/kafka"
	"github.com/minio/minio/internal/logger/target/types"
)

// Target is the entity that we will receive
// a single log entry and Send it to the log target
//   e.g. Send the log to a http server
type Target interface {
	String() string
	Endpoint() string
	Init() error
	Cancel()
	Send(entry interface{}, errKind string) error
	Type() types.TargetType
}

var (
	swapAuditMuRW  sync.RWMutex
	swapSystemMuRW sync.RWMutex

	// systemTargets is the set of enabled loggers.
	// Must be immutable at all times.
	// Can be swapped to another while holding swapMu
	systemTargets = []Target{}

	// This is always set represent /dev/console target
	consoleTgt Target
)

// SystemTargets returns active targets.
// Returned slice may not be modified in any way.
func SystemTargets() []Target {
	swapSystemMuRW.RLock()
	defer swapSystemMuRW.RUnlock()

	res := systemTargets
	return res
}

// AuditTargets returns active audit targets.
// Returned slice may not be modified in any way.
func AuditTargets() []Target {
	swapAuditMuRW.RLock()
	defer swapAuditMuRW.RUnlock()

	res := auditTargets
	return res
}

// auditTargets is the list of enabled audit loggers
// Must be immutable at all times.
// Can be swapped to another while holding swapMu
var (
	auditTargets = []Target{}
)

// AddSystemTarget adds a new logger target to the
// list of enabled loggers
func AddSystemTarget(t Target) error {
	if err := t.Init(); err != nil {
		return err
	}

	swapSystemMuRW.Lock()
	defer swapSystemMuRW.Unlock()

	if consoleTgt == nil {
		if t.Type() == types.TargetConsole {
			consoleTgt = t
		}
	}
	updated := append(make([]Target, 0, len(systemTargets)+1), systemTargets...)
	updated = append(updated, t)
	systemTargets = updated

	return nil
}

func initSystemTargets(cfgMap map[string]http.Config) (tgts []Target, err error) {
	for _, l := range cfgMap {
		if l.Enabled {
			t := http.New(l)
			if err = t.Init(); err != nil {
				return tgts, err
			}
			tgts = append(tgts, t)
		}
	}
	return tgts, err
}

func initKafkaTargets(cfgMap map[string]kafka.Config) (tgts []Target, err error) {
	for _, l := range cfgMap {
		if l.Enabled {
			t := kafka.New(l)
			if err = t.Init(); err != nil {
				return tgts, err
			}
			tgts = append(tgts, t)
		}
	}
	return tgts, err
}

// Split targets into two groups:
//  group1 contains all targets of type t
//  group2 contains the remaining targets
func splitTargets(targets []Target, t types.TargetType) (group1 []Target, group2 []Target) {
	for _, target := range targets {
		if target.Type() == t {
			group1 = append(group1, target)
		} else {
			group2 = append(group2, target)
		}
	}
	return
}

func cancelTargets(targets []Target) {
	for _, target := range targets {
		target.Cancel()
	}
}

// UpdateSystemTargets swaps targets with newly loaded ones from the cfg
func UpdateSystemTargets(cfg Config) error {
	newTgts, err := initSystemTargets(cfg.HTTP)
	if err != nil {
		return err
	}

	swapSystemMuRW.Lock()
	consoleTargets, otherTargets := splitTargets(systemTargets, types.TargetConsole)
	newTgts = append(newTgts, consoleTargets...)
	systemTargets = newTgts
	swapSystemMuRW.Unlock()

	cancelTargets(otherTargets) // cancel running targets
	return nil
}

// UpdateAuditWebhookTargets swaps audit webhook targets with newly loaded ones from the cfg
func UpdateAuditWebhookTargets(cfg Config) error {
	newWebhookTgts, err := initSystemTargets(cfg.AuditWebhook)
	if err != nil {
		return err
	}

	swapAuditMuRW.Lock()
	// Retain kafka targets
	oldWebhookTgts, otherTgts := splitTargets(auditTargets, types.TargetHTTP)
	newWebhookTgts = append(newWebhookTgts, otherTgts...)
	auditTargets = newWebhookTgts
	swapAuditMuRW.Unlock()

	cancelTargets(oldWebhookTgts) // cancel running targets
	return nil
}

// UpdateAuditKafkaTargets swaps audit kafka targets with newly loaded ones from the cfg
func UpdateAuditKafkaTargets(cfg Config) error {
	newKafkaTgts, err := initKafkaTargets(cfg.AuditKafka)
	if err != nil {
		return err
	}

	swapAuditMuRW.Lock()
	// Retain webhook targets
	oldKafkaTgts, otherTgts := splitTargets(auditTargets, types.TargetKafka)
	newKafkaTgts = append(newKafkaTgts, otherTgts...)
	auditTargets = newKafkaTgts
	swapAuditMuRW.Unlock()

	cancelTargets(oldKafkaTgts) // cancel running targets
	return nil
}
<|endoftext|>


<commit_before>/*
Copyright 2018 The Ceph-CSI Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package rbd

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/ceph/ceph-csi/internal/util"

	"k8s.io/apimachinery/pkg/util/wait"
)

const (
	rbdTonbd  = "rbd-nbd"
	moduleNbd = "nbd"

	accessTypeKRbd = "krbd"
	accessTypeNbd  = "nbd"

	rbd = "rbd"

	// Output strings returned during invocation of "rbd unmap --device-type... <imageSpec>" when
	// image is not found to be mapped. Used to ignore errors when attempting to unmap such images.
	// The %s format specifier should contain the <imageSpec> string
	// NOTE: When using devicePath instead of imageSpec, the error strings are different
	rbdUnmapCmdkRbdMissingMap = "rbd: %s: not a mapped image or snapshot"
	rbdUnmapCmdNbdMissingMap  = "rbd-nbd: %s is not mapped"
	rbdMapConnectionTimeout   = "Connection timed out"
)

var hasNBD = false

func init() {
	hasNBD = checkRbdNbdTools()
}

// rbdDeviceInfo strongly typed JSON spec for rbd device list output (of type krbd).
type rbdDeviceInfo struct {
	ID             string `json:"id"`
	Pool           string `json:"pool"`
	RadosNamespace string `json:"namespace"`
	Name           string `json:"name"`
	Device         string `json:"device"`
}

// nbdDeviceInfo strongly typed JSON spec for rbd-nbd device list output (of type nbd)
// NOTE: There is a bug in rbd output that returns id as number for nbd, and string for krbd, thus
// requiring 2 different JSON structures to unmarshal the output.
// NOTE: image key is "name" in krbd output and "image" in nbd output, which is another difference.
type nbdDeviceInfo struct {
	ID             int64  `json:"id"`
	Pool           string `json:"pool"`
	RadosNamespace string `json:"namespace"`
	Name           string `json:"image"`
	Device         string `json:"device"`
}

// rbdGetDeviceList queries rbd about mapped devices and returns a list of rbdDeviceInfo
// It will selectively list devices mapped using krbd or nbd as specified by accessType.
func rbdGetDeviceList(ctx context.Context, accessType string) ([]rbdDeviceInfo, error) {
	// rbd device list --format json --device-type [krbd|nbd]
	var (
		rbdDeviceList []rbdDeviceInfo
		nbdDeviceList []nbdDeviceInfo
	)

	stdout, _, err := util.ExecCommand(ctx, rbd, "device", "list", "--format="+"json", "--device-type", accessType)
	if err != nil {
		return nil, fmt.Errorf("error getting device list from rbd for devices of type (%s): %w", accessType, err)
	}

	if accessType == accessTypeKRbd {
		err = json.Unmarshal([]byte(stdout), &rbdDeviceList)
	} else {
		err = json.Unmarshal([]byte(stdout), &nbdDeviceList)
	}
	if err != nil {
		return nil, fmt.Errorf("error to parse JSON output of device list for devices of type (%s): %w", accessType, err)
	}

	// convert output to a rbdDeviceInfo list for consumers
	if accessType == accessTypeNbd {
		for _, device := range nbdDeviceList {
			rbdDeviceList = append(
				rbdDeviceList,
				rbdDeviceInfo{
					ID:             strconv.FormatInt(device.ID, 10),
					Pool:           device.Pool,
					RadosNamespace: device.RadosNamespace,
					Name:           device.Name,
					Device:         device.Device,
				})
		}
	}

	return rbdDeviceList, nil
}

// findDeviceMappingImage finds a devicePath, if available, based on image spec (pool/{namespace/}image) on the node.
func findDeviceMappingImage(ctx context.Context, pool, namespace, image string, useNbdDriver bool) (string, bool) {
	accessType := accessTypeKRbd
	if useNbdDriver {
		accessType = accessTypeNbd
	}

	imageSpec := fmt.Sprintf("%s/%s", pool, image)
	if namespace != "" {
		imageSpec = fmt.Sprintf("%s/%s/%s", pool, namespace, image)
	}

	rbdDeviceList, err := rbdGetDeviceList(ctx, accessType)
	if err != nil {
		util.WarningLog(ctx, "failed to determine if image (%s) is mapped to a device (%v)", imageSpec, err)
		return "", false
	}

	for _, device := range rbdDeviceList {
		if device.Name == image && device.Pool == pool && device.RadosNamespace == namespace {
			return device.Device, true
		}
	}

	return "", false
}

// Stat a path, if it doesn't exist, retry maxRetries times.
func waitForPath(ctx context.Context, pool, namespace, image string, maxRetries int, useNbdDriver bool) (string, bool) {
	for i := 0; i < maxRetries; i++ {
		if i != 0 {
			time.Sleep(time.Second)
		}

		device, found := findDeviceMappingImage(ctx, pool, namespace, image, useNbdDriver)
		if found {
			return device, found
		}
	}

	return "", false
}

// Check if rbd-nbd tools are installed.
func checkRbdNbdTools() bool {
	// check if the module is loaded or compiled in
	_, err := os.Stat(fmt.Sprintf("/sys/module/%s", moduleNbd))
	if os.IsNotExist(err) {
		// try to load the module
		_, _, err = util.ExecCommand(context.TODO(), "modprobe", moduleNbd)
		if err != nil {
			util.ExtendedLogMsg("rbd-nbd: nbd modprobe failed with error %v", err)
			return false
		}
	}
	if _, _, err := util.ExecCommand(context.TODO(), rbdTonbd, "--version"); err != nil {
		util.ExtendedLogMsg("rbd-nbd: running rbd-nbd --version failed with error %v", err)
		return false
	}
	util.ExtendedLogMsg("rbd-nbd tools were found.")
	return true
}

func attachRBDImage(ctx context.Context, volOptions *rbdVolume, cr *util.Credentials) (string, error) {
	var err error

	image := volOptions.RbdImageName
	useNBD := false
	if volOptions.Mounter == rbdTonbd && hasNBD {
		useNBD = true
	}

	devicePath, found := waitForPath(ctx, volOptions.Pool, volOptions.RadosNamespace, image, 1, useNBD)
	if !found {
		backoff := wait.Backoff{
			Duration: rbdImageWatcherInitDelay,
			Factor:   rbdImageWatcherFactor,
			Steps:    rbdImageWatcherSteps,
		}

		err = waitForrbdImage(ctx, backoff, volOptions)

		if err != nil {
			return "", err
		}
		devicePath, err = createPath(ctx, volOptions, cr)
	}

	return devicePath, err
}

func appendDeviceTypeAndOptions(cmdArgs []string, isNbd bool, userOptions string) []string {
	accessType := accessTypeKRbd
	if isNbd {
		accessType = accessTypeNbd
	}

	cmdArgs = append(cmdArgs, "--device-type", accessType)
	if userOptions != "" {
		cmdArgs = append(cmdArgs, "--options", userOptions)
	}

	return cmdArgs
}

func createPath(ctx context.Context, volOpt *rbdVolume, cr *util.Credentials) (string, error) {
	isNbd := false
	imagePath := volOpt.String()

	util.TraceLog(ctx, "rbd: map mon %s", volOpt.Monitors)

	mapArgs := []string{
		"--id", cr.ID,
		"-m", volOpt.Monitors,
		"--keyfile=" + cr.KeyFile,
		"map", imagePath,
	}

	// Choose access protocol
	if volOpt.Mounter == rbdTonbd && hasNBD {
		isNbd = true
	}

	mapArgs = appendDeviceTypeAndOptions(mapArgs, isNbd, volOpt.MapOptions)
	if volOpt.readOnly {
		mapArgs = append(mapArgs, "--read-only")
	}

	// Execute map
	stdout, stderr, err := util.ExecCommand(ctx, rbd, mapArgs...)
	if err != nil {
		util.WarningLog(ctx, "rbd: map error %v, rbd output: %s", err, stderr)
		// unmap rbd image if connection timeout
		if strings.Contains(err.Error(), rbdMapConnectionTimeout) {
			detErr := detachRBDImageOrDeviceSpec(ctx, imagePath, true, isNbd, volOpt.Encrypted, volOpt.VolID, volOpt.UnmapOptions)
			if detErr != nil {
				util.WarningLog(ctx, "rbd: %s unmap error %v", imagePath, detErr)
			}
		}
		return "", fmt.Errorf("rbd: map failed with error %v, rbd error output: %s", err, stderr)
	}
	devicePath := strings.TrimSuffix(stdout, "\n")

	return devicePath, nil
}

func waitForrbdImage(ctx context.Context, backoff wait.Backoff, volOptions *rbdVolume) error {
	imagePath := volOptions.String()

	err := wait.ExponentialBackoff(backoff, func() (bool, error) {
		used, err := volOptions.isInUse()
		if err != nil {
			return false, fmt.Errorf("fail to check rbd image status: (%w)", err)
		}
		if (volOptions.DisableInUseChecks) && (used) {
			util.UsefulLog(ctx, "valid multi-node attach requested, ignoring watcher in-use result")
			return used, nil
		}
		return !used, nil
	})
	// return error if rbd image has not become available for the specified timeout
	if errors.Is(err, wait.ErrWaitTimeout) {
		return fmt.Errorf("rbd image %s is still being used", imagePath)
	}
	// return error if any other errors were encountered during waiting for the image to become available
	return err
}

func detachRBDDevice(ctx context.Context, devicePath, volumeID, unmapOptions string, encrypted bool) error {
	nbdType := false
	if strings.HasPrefix(devicePath, "/dev/nbd") {
		nbdType = true
	}

	return detachRBDImageOrDeviceSpec(ctx, devicePath, false, nbdType, encrypted, volumeID, unmapOptions)
}

// detachRBDImageOrDeviceSpec detaches an rbd imageSpec or devicePath, with additional checking
// when imageSpec is used to decide if image is already unmapped.
func detachRBDImageOrDeviceSpec(ctx context.Context, imageOrDeviceSpec string, isImageSpec, isNbd, encrypted bool, volumeID, unmapOptions string) error {
	if encrypted {
		mapperFile, mapperPath := util.VolumeMapper(volumeID)
		mappedDevice, mapper, err := util.DeviceEncryptionStatus(ctx, mapperPath)
		if err != nil {
			util.ErrorLog(ctx, "error determining LUKS device on %s, %s: %s",
				mapperPath, imageOrDeviceSpec, err)
			return err
		}
		if len(mapper) > 0 {
			// mapper found, so it is open Luks device
			err = util.CloseEncryptedVolume(ctx, mapperFile)
			if err != nil {
				util.ErrorLog(ctx, "error closing LUKS device on %s, %s: %s",
					mapperPath, imageOrDeviceSpec, err)
				return err
			}
			imageOrDeviceSpec = mappedDevice
		}
	}

	unmapArgs := []string{"unmap", imageOrDeviceSpec}
	unmapArgs = appendDeviceTypeAndOptions(unmapArgs, isNbd, unmapOptions)

	_, stderr, err := util.ExecCommand(ctx, rbd, unmapArgs...)
	if err != nil {
		// Messages for krbd and nbd differ, hence checking either of them for missing mapping
		// This is not applicable when a device path is passed in
		if isImageSpec &&
			(strings.Contains(stderr, fmt.Sprintf(rbdUnmapCmdkRbdMissingMap, imageOrDeviceSpec)) ||
				strings.Contains(stderr, fmt.Sprintf(rbdUnmapCmdNbdMissingMap, imageOrDeviceSpec))) {
			// Devices found not to be mapped are treated as a successful detach
			util.TraceLog(ctx, "image or device spec (%s) not mapped", imageOrDeviceSpec)
			return nil
		}
		return fmt.Errorf("rbd: unmap for spec (%s) failed (%w): (%s)", imageOrDeviceSpec, err, stderr)
	}

	return nil
}
<commit_msg>rbd: enable mapping and unmapping from a network namespace<commit_after>/*
Copyright 2018 The Ceph-CSI Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package rbd

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/ceph/ceph-csi/internal/util"

	"k8s.io/apimachinery/pkg/util/wait"
)

const (
	rbdTonbd  = "rbd-nbd"
	moduleNbd = "nbd"

	accessTypeKRbd = "krbd"
	accessTypeNbd  = "nbd"

	rbd = "rbd"

	// Output strings returned during invocation of "rbd unmap --device-type... <imageSpec>" when
	// image is not found to be mapped. Used to ignore errors when attempting to unmap such images.
	// The %s format specifier should contain the <imageSpec> string
	// NOTE: When using devicePath instead of imageSpec, the error strings are different
	rbdUnmapCmdkRbdMissingMap = "rbd: %s: not a mapped image or snapshot"
	rbdUnmapCmdNbdMissingMap  = "rbd-nbd: %s is not mapped"
	rbdMapConnectionTimeout   = "Connection timed out"
)

var hasNBD = false

func init() {
	hasNBD = checkRbdNbdTools()
}

// rbdDeviceInfo strongly typed JSON spec for rbd device list output (of type krbd).
type rbdDeviceInfo struct {
	ID             string `json:"id"`
	Pool           string `json:"pool"`
	RadosNamespace string `json:"namespace"`
	Name           string `json:"name"`
	Device         string `json:"device"`
}

// nbdDeviceInfo strongly typed JSON spec for rbd-nbd device list output (of type nbd)
// NOTE: There is a bug in rbd output that returns id as number for nbd, and string for krbd, thus
// requiring 2 different JSON structures to unmarshal the output.
// NOTE: image key is "name" in krbd output and "image" in nbd output, which is another difference.
type nbdDeviceInfo struct {
	ID             int64  `json:"id"`
	Pool           string `json:"pool"`
	RadosNamespace string `json:"namespace"`
	Name           string `json:"image"`
	Device         string `json:"device"`
}

// rbdGetDeviceList queries rbd about mapped devices and returns a list of rbdDeviceInfo
// It will selectively list devices mapped using krbd or nbd as specified by accessType.
func rbdGetDeviceList(ctx context.Context, accessType string) ([]rbdDeviceInfo, error) {
	// rbd device list --format json --device-type [krbd|nbd]
	var (
		rbdDeviceList []rbdDeviceInfo
		nbdDeviceList []nbdDeviceInfo
	)

	stdout, _, err := util.ExecCommand(ctx, rbd, "device", "list", "--format="+"json", "--device-type", accessType)
	if err != nil {
		return nil, fmt.Errorf("error getting device list from rbd for devices of type (%s): %w", accessType, err)
	}

	if accessType == accessTypeKRbd {
		err = json.Unmarshal([]byte(stdout), &rbdDeviceList)
	} else {
		err = json.Unmarshal([]byte(stdout), &nbdDeviceList)
	}
	if err != nil {
		return nil, fmt.Errorf("error to parse JSON output of device list for devices of type (%s): %w", accessType, err)
	}

	// convert output to a rbdDeviceInfo list for consumers
	if accessType == accessTypeNbd {
		for _, device := range nbdDeviceList {
			rbdDeviceList = append(
				rbdDeviceList,
				rbdDeviceInfo{
					ID:             strconv.FormatInt(device.ID, 10),
					Pool:           device.Pool,
					RadosNamespace: device.RadosNamespace,
					Name:           device.Name,
					Device:         device.Device,
				})
		}
	}

	return rbdDeviceList, nil
}

// findDeviceMappingImage finds a devicePath, if available, based on image spec (pool/{namespace/}image) on the node.
func findDeviceMappingImage(ctx context.Context, pool, namespace, image string, useNbdDriver bool) (string, bool) {
	accessType := accessTypeKRbd
	if useNbdDriver {
		accessType = accessTypeNbd
	}

	imageSpec := fmt.Sprintf("%s/%s", pool, image)
	if namespace != "" {
		imageSpec = fmt.Sprintf("%s/%s/%s", pool, namespace, image)
	}

	rbdDeviceList, err := rbdGetDeviceList(ctx, accessType)
	if err != nil {
		util.WarningLog(ctx, "failed to determine if image (%s) is mapped to a device (%v)", imageSpec, err)
		return "", false
	}

	for _, device := range rbdDeviceList {
		if device.Name == image && device.Pool == pool && device.RadosNamespace == namespace {
			return device.Device, true
		}
	}

	return "", false
}

// Stat a path, if it doesn't exist, retry maxRetries times.
func waitForPath(ctx context.Context, pool, namespace, image string, maxRetries int, useNbdDriver bool) (string, bool) {
	for i := 0; i < maxRetries; i++ {
		if i != 0 {
			time.Sleep(time.Second)
		}

		device, found := findDeviceMappingImage(ctx, pool, namespace, image, useNbdDriver)
		if found {
			return device, found
		}
	}

	return "", false
}

// Check if rbd-nbd tools are installed.
func checkRbdNbdTools() bool {
	// check if the module is loaded or compiled in
	_, err := os.Stat(fmt.Sprintf("/sys/module/%s", moduleNbd))
	if os.IsNotExist(err) {
		// try to load the module
		_, _, err = util.ExecCommand(context.TODO(), "modprobe", moduleNbd)
		if err != nil {
			util.ExtendedLogMsg("rbd-nbd: nbd modprobe failed with error %v", err)
			return false
		}
	}
	if _, _, err := util.ExecCommand(context.TODO(), rbdTonbd, "--version"); err != nil {
		util.ExtendedLogMsg("rbd-nbd: running rbd-nbd --version failed with error %v", err)
		return false
	}
	util.ExtendedLogMsg("rbd-nbd tools were found.")
	return true
}

func attachRBDImage(ctx context.Context, volOptions *rbdVolume, cr *util.Credentials) (string, error) {
	var err error

	image := volOptions.RbdImageName
	useNBD := false
	if volOptions.Mounter == rbdTonbd && hasNBD {
		useNBD = true
	}

	devicePath, found := waitForPath(ctx, volOptions.Pool, volOptions.RadosNamespace, image, 1, useNBD)
	if !found {
		backoff := wait.Backoff{
			Duration: rbdImageWatcherInitDelay,
			Factor:   rbdImageWatcherFactor,
			Steps:    rbdImageWatcherSteps,
		}

		err = waitForrbdImage(ctx, backoff, volOptions)

		if err != nil {
			return "", err
		}
		devicePath, err = createPath(ctx, volOptions, cr)
	}

	return devicePath, err
}

func appendDeviceTypeAndOptions(cmdArgs []string, isNbd bool, userOptions string) []string {
	accessType := accessTypeKRbd
	if isNbd {
		accessType = accessTypeNbd
	}

	cmdArgs = append(cmdArgs, "--device-type", accessType)
	if !isNbd {
		// Enable mapping and unmapping images from a non-initial network
		// namespace (e.g. for Multus CNI).  The network namespace must be
		// owned by the initial user namespace.
		cmdArgs = append(cmdArgs, "--options", "noudev")
	}
	if userOptions != "" {
		// userOptions is appended after, possibly overriding the above
		// default options.
		cmdArgs = append(cmdArgs, "--options", userOptions)
	}

	return cmdArgs
}

func createPath(ctx context.Context, volOpt *rbdVolume, cr *util.Credentials) (string, error) {
	isNbd := false
	imagePath := volOpt.String()

	util.TraceLog(ctx, "rbd: map mon %s", volOpt.Monitors)

	mapArgs := []string{
		"--id", cr.ID,
		"-m", volOpt.Monitors,
		"--keyfile=" + cr.KeyFile,
		"map", imagePath,
	}

	// Choose access protocol
	if volOpt.Mounter == rbdTonbd && hasNBD {
		isNbd = true
	}

	mapArgs = appendDeviceTypeAndOptions(mapArgs, isNbd, volOpt.MapOptions)
	if volOpt.readOnly {
		mapArgs = append(mapArgs, "--read-only")
	}

	// Execute map
	stdout, stderr, err := util.ExecCommand(ctx, rbd, mapArgs...)
	if err != nil {
		util.WarningLog(ctx, "rbd: map error %v, rbd output: %s", err, stderr)
		// unmap rbd image if connection timeout
		if strings.Contains(err.Error(), rbdMapConnectionTimeout) {
			detErr := detachRBDImageOrDeviceSpec(ctx, imagePath, true, isNbd, volOpt.Encrypted, volOpt.VolID, volOpt.UnmapOptions)
			if detErr != nil {
				util.WarningLog(ctx, "rbd: %s unmap error %v", imagePath, detErr)
			}
		}
		return "", fmt.Errorf("rbd: map failed with error %v, rbd error output: %s", err, stderr)
	}
	devicePath := strings.TrimSuffix(stdout, "\n")

	return devicePath, nil
}

func waitForrbdImage(ctx context.Context, backoff wait.Backoff, volOptions *rbdVolume) error {
	imagePath := volOptions.String()

	err := wait.ExponentialBackoff(backoff, func() (bool, error) {
		used, err := volOptions.isInUse()
		if err != nil {
			return false, fmt.Errorf("fail to check rbd image status: (%w)", err)
		}
		if (volOptions.DisableInUseChecks) && (used) {
			util.UsefulLog(ctx, "valid multi-node attach requested, ignoring watcher in-use result")
			return used, nil
		}
		return !used, nil
	})
	// return error if rbd image has not become available for the specified timeout
	if errors.Is(err, wait.ErrWaitTimeout) {
		return fmt.Errorf("rbd image %s is still being used", imagePath)
	}
	// return error if any other errors were encountered during waiting for the image to become available
	return err
}

func detachRBDDevice(ctx context.Context, devicePath, volumeID, unmapOptions string, encrypted bool) error {
	nbdType := false
	if strings.HasPrefix(devicePath, "/dev/nbd") {
		nbdType = true
	}

	return detachRBDImageOrDeviceSpec(ctx, devicePath, false, nbdType, encrypted, volumeID, unmapOptions)
}

// detachRBDImageOrDeviceSpec detaches an rbd imageSpec or devicePath, with additional checking
// when imageSpec is used to decide if image is already unmapped.
func detachRBDImageOrDeviceSpec(ctx context.Context, imageOrDeviceSpec string, isImageSpec, isNbd, encrypted bool, volumeID, unmapOptions string) error {
	if encrypted {
		mapperFile, mapperPath := util.VolumeMapper(volumeID)
		mappedDevice, mapper, err := util.DeviceEncryptionStatus(ctx, mapperPath)
		if err != nil {
			util.ErrorLog(ctx, "error determining LUKS device on %s, %s: %s",
				mapperPath, imageOrDeviceSpec, err)
			return err
		}
		if len(mapper) > 0 {
			// mapper found, so it is open Luks device
			err = util.CloseEncryptedVolume(ctx, mapperFile)
			if err != nil {
				util.ErrorLog(ctx, "error closing LUKS device on %s, %s: %s",
					mapperPath, imageOrDeviceSpec, err)
				return err
			}
			imageOrDeviceSpec = mappedDevice
		}
	}

	unmapArgs := []string{"unmap", imageOrDeviceSpec}
	unmapArgs = appendDeviceTypeAndOptions(unmapArgs, isNbd, unmapOptions)

	_, stderr, err := util.ExecCommand(ctx, rbd, unmapArgs...)
	if err != nil {
		// Messages for krbd and nbd differ, hence checking either of them for missing mapping
		// This is not applicable when a device path is passed in
		if isImageSpec &&
			(strings.Contains(stderr, fmt.Sprintf(rbdUnmapCmdkRbdMissingMap, imageOrDeviceSpec)) ||
				strings.Contains(stderr, fmt.Sprintf(rbdUnmapCmdNbdMissingMap, imageOrDeviceSpec))) {
			// Devices found not to be mapped are treated as a successful detach
			util.TraceLog(ctx, "image or device spec (%s) not mapped", imageOrDeviceSpec)
			return nil
		}
		return fmt.Errorf("rbd: unmap for spec (%s) failed (%w): (%s)", imageOrDeviceSpec, err, stderr)
	}

	return nil
}
<|endoftext|>


<commit_before>package rest

import (
	"database/sql"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"regexp"
	"strconv"
	"strings"

	"github.com/gorilla/mux"
	"gopkg.in/inconshreveable/log15.v2"

	"github.com/nsec/askgod/api"
)

func (r *rest) getTeam(writer http.ResponseWriter, request *http.Request, logger log15.Logger) {
	// Extract the client IP
	ip, err := r.getIP(request)
	if err != nil {
		logger.Error("Failed to get the client's IP", log15.Ctx{"error": err})
		r.errorResponse(500, "Internal Server Error", writer, request)
		return
	}

	// Look for a matching team
	record, err := r.db.GetTeamForIP(*ip)
	if err == sql.ErrNoRows {
		logger.Warn("No team found for IP", log15.Ctx{"ip": ip.String()})
		r.errorResponse(404, "No team found for IP", writer, request)
		return
	} else if err != nil {
		logger.Error("Failed to get the team", log15.Ctx{"error": err})
		r.errorResponse(500, "Internal Server Error", writer, request)
		return
	}

	// Convert to the team view of a team
	team := api.Team{}
	team.ID = record.ID
	team.Name = record.Name
	team.Country = record.Country
	team.Website = record.Website

	r.jsonResponse(team, writer, request)
}

func (r *rest) updateTeam(writer http.ResponseWriter, request *http.Request, logger log15.Logger) {
	// Configuration validation
	if !r.config.Teams.SelfRegister {
		logger.Warn("Unauthorized attempt to self-register")
		r.errorResponse(403, "Team self-registration disabled", writer, request)
		return
	}

	// Decode the provided JSON input
	newTeam := api.TeamPut{}
	err := json.NewDecoder(request.Body).Decode(&newTeam)
	if err != nil {
		logger.Warn("Malformed JSON provided", log15.Ctx{"error": err})
		r.errorResponse(400, "Malformed JSON provided", writer, request)
		return
	}

	// Sanity checks
	validName := func(name string) bool {
		// Validate the length
		if len(name) < 1 || len(name) > 30 {
			return false
		}

		// Validate the character set
		match, _ := regexp.MatchString("^[a-zA-Z0-9 /\\\\~!@#$%&*()\\-_+={}\\[\\];:',.?]*$", name)
		if !match {
			return false
		}

		return true
	}

	if !validName(newTeam.Name) {
		logger.Warn("Bad team name", log15.Ctx{"name": newTeam.Name})
		r.errorResponse(400, "Bad team name", writer, request)
		return
	}

	if len(newTeam.Country) > 2 {
		logger.Warn("Bad team country code", log15.Ctx{"country": newTeam.Country})
		r.errorResponse(400, "Bad team country code", writer, request)
		return
	}

	if newTeam.Website != "" {
		u, err := url.ParseRequestURI(newTeam.Website)
		if err != nil {
			logger.Warn("Bad team URL", log15.Ctx{"url": newTeam.Website})
			r.errorResponse(400, "Bad team URL", writer, request)
			return
		}

		newTeam.Website = u.String()
	}

	// Extract the client IP
	ip, err := r.getIP(request)
	if err != nil {
		logger.Error("Failed to get the client's IP", log15.Ctx{"error": err})
		r.errorResponse(500, "Internal Server Error", writer, request)
		return
	}

	// Look for a matching team
	team, err := r.db.GetTeamForIP(*ip)
	if err == sql.ErrNoRows {
		logger.Warn("No team found for IP", log15.Ctx{"ip": ip.String()})
		r.errorResponse(404, "No team found for IP", writer, request)
		return
	} else if err != nil {
		logger.Error("Failed to get the team", log15.Ctx{"error": err})
		r.errorResponse(500, "Internal Server Error", writer, request)
		return
	}

	// Validate the request
	if !r.config.Teams.SelfUpdate {
		if team.Name != "" && team.Name != newTeam.Name {
			logger.Debug("Unauthorized attempt to change already set team property", log15.Ctx{"property": "name"})
			r.errorResponse(400, "Team name is already set", writer, request)
			return
		}

		if team.Country != "" && team.Country != newTeam.Country {
			logger.Debug("Unauthorized attempt to change already set team property", log15.Ctx{"property": "country"})
			r.errorResponse(400, "Team country is already set", writer, request)
			return
		}

		if team.Website != "" && team.Website != newTeam.Website {
			logger.Debug("Unauthorized attempt to change already set team property", log15.Ctx{"property": "website"})
			r.errorResponse(400, "Team website is already set", writer, request)
			return
		}
	}

	// Setup the new record
	newRecord := api.AdminTeamPut{}
	newRecord.Name = newTeam.Name
	newRecord.Country = strings.ToUpper(newTeam.Country)
	newRecord.Website = newTeam.Website
	newRecord.Notes = team.Notes
	newRecord.Subnets = team.Subnets

	// Attempt to update the database
	err = r.db.UpdateTeam(team.ID, newRecord)
	if err != nil {
		logger.Error("Failed to update the team", log15.Ctx{"error": err})
		r.errorResponse(500, "Internal Server Error", writer, request)
		return
	}

	eventSend("timeline", api.EventTimeline{TeamID: team.ID, Team: &newTeam, Type: "team-updated"})
	logger.Info("Team updated", log15.Ctx{"id": team.ID, "name": newTeam.Name, "country": newTeam.Country, "website": newTeam.Website})
}

func (r *rest) adminGetTeams(writer http.ResponseWriter, request *http.Request, logger log15.Logger) {
	// Get all the teams from the database
	teams, err := r.db.GetTeams()
	if err != nil {
		logger.Error("Failed to query the team list", log15.Ctx{"error": err})
		r.errorResponse(500, fmt.Sprintf("%v", err), writer, request)
		return
	}

	r.jsonResponse(teams, writer, request)
}

func (r *rest) adminCreateTeam(writer http.ResponseWriter, request *http.Request, logger log15.Logger) {
	// Decode the provided JSON input
	newTeam := api.AdminTeamPost{}
	err := json.NewDecoder(request.Body).Decode(&newTeam)
	if err != nil {
		logger.Warn("Malformed JSON provided", log15.Ctx{"error": err})
		r.errorResponse(400, "Malformed JSON provided", writer, request)
		return
	}

	// Attempt to create the database record
	id, err := r.db.CreateTeam(newTeam)
	if err != nil {
		logger.Error("Failed to create the team", log15.Ctx{"error": err})
		r.errorResponse(500, fmt.Sprintf("%v", err), writer, request)
		return
	}

	eventSend("timeline", api.EventTimeline{TeamID: id, Team: &newTeam.AdminTeamPut.TeamPut, Type: "team-added"})
	logger.Info("New team defined", log15.Ctx{"id": id, "subnets": newTeam.Subnets})
}

func (r *rest) adminGetTeam(writer http.ResponseWriter, request *http.Request, logger log15.Logger) {
	idVar := mux.Vars(request)["id"]

	// Convert the provided id to int
	id, err := strconv.ParseInt(idVar, 10, 64)
	if err != nil {
		logger.Warn("Invalid team ID provided", log15.Ctx{"id": idVar})
		r.errorResponse(400, "Invalid team ID provided", writer, request)
		return
	}

	// Attempt to get the DB record
	team, err := r.db.GetTeam(id)
	if err == sql.ErrNoRows {
		logger.Warn("Invalid team ID provided", log15.Ctx{"id": idVar})
		r.errorResponse(404, "Invalid team ID provided", writer, request)
		return
	} else if err != nil {
		logger.Error("Failed to get the team", log15.Ctx{"error": err})
		r.errorResponse(500, fmt.Sprintf("%v", err), writer, request)
		return
	}

	r.jsonResponse(team, writer, request)
}

func (r *rest) adminUpdateTeam(writer http.ResponseWriter, request *http.Request, logger log15.Logger) {
	idVar := mux.Vars(request)["id"]

	// Convert the provided id to int
	id, err := strconv.ParseInt(idVar, 10, 64)
	if err != nil {
		logger.Warn("Invalid team ID provided", log15.Ctx{"id": idVar})
		r.errorResponse(400, "Invalid team ID provided", writer, request)
		return
	}

	// Decode the provided JSON input
	newTeam := api.AdminTeamPut{}
	err = json.NewDecoder(request.Body).Decode(&newTeam)
	if err != nil {
		logger.Warn("Malformed JSON provided", log15.Ctx{"error": err})
		r.errorResponse(400, "Malformed JSON provided", writer, request)
		return
	}

	// Attempt to update the database
	err = r.db.UpdateTeam(id, newTeam)
	if err == sql.ErrNoRows {
		logger.Warn("Invalid team ID provided", log15.Ctx{"id": idVar})
		r.errorResponse(404, "Invalid team ID provided", writer, request)
		return
	} else if err != nil {
		logger.Error("Failed to update the team", log15.Ctx{"error": err})
		r.errorResponse(500, fmt.Sprintf("%v", err), writer, request)
		return
	}

	eventSend("timeline", api.EventTimeline{TeamID: id, Team: &newTeam.TeamPut, Type: "team-updated"})
	logger.Info("Team updated", log15.Ctx{"id": id, "name": newTeam.Name, "country": newTeam.Country, "website": newTeam.Website})
}

func (r *rest) adminDeleteTeam(writer http.ResponseWriter, request *http.Request, logger log15.Logger) {
	idVar := mux.Vars(request)["id"]

	// Convert the provided id to int
	id, err := strconv.ParseInt(idVar, 10, 64)
	if err != nil {
		logger.Warn("Invalid team ID provided", log15.Ctx{"id": idVar})
		r.errorResponse(400, "Invalid team ID provided", writer, request)
		return
	}

	// Attempt to get the DB record
	err = r.db.DeleteTeam(id)
	if err == sql.ErrNoRows {
		logger.Warn("Invalid team ID provided", log15.Ctx{"id": idVar})
		r.errorResponse(404, "Invalid team ID provided", writer, request)
		return
	} else if err != nil {
		logger.Error("Failed to delete the team", log15.Ctx{"error": err})
		r.errorResponse(500, fmt.Sprintf("%v", err), writer, request)
		return
	}

	eventSend("timeline", api.EventTimeline{TeamID: id, Type: "team-removed"})
	logger.Info("Team deleted", log15.Ctx{"id": id})
}
<commit_msg>askgod-server: Use DB values for team-updated event<commit_after>package rest

import (
	"database/sql"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"regexp"
	"strconv"
	"strings"

	"github.com/gorilla/mux"
	"gopkg.in/inconshreveable/log15.v2"

	"github.com/nsec/askgod/api"
)

func (r *rest) getTeam(writer http.ResponseWriter, request *http.Request, logger log15.Logger) {
	// Extract the client IP
	ip, err := r.getIP(request)
	if err != nil {
		logger.Error("Failed to get the client's IP", log15.Ctx{"error": err})
		r.errorResponse(500, "Internal Server Error", writer, request)
		return
	}

	// Look for a matching team
	record, err := r.db.GetTeamForIP(*ip)
	if err == sql.ErrNoRows {
		logger.Warn("No team found for IP", log15.Ctx{"ip": ip.String()})
		r.errorResponse(404, "No team found for IP", writer, request)
		return
	} else if err != nil {
		logger.Error("Failed to get the team", log15.Ctx{"error": err})
		r.errorResponse(500, "Internal Server Error", writer, request)
		return
	}

	// Convert to the team view of a team
	team := api.Team{}
	team.ID = record.ID
	team.Name = record.Name
	team.Country = record.Country
	team.Website = record.Website

	r.jsonResponse(team, writer, request)
}

func (r *rest) updateTeam(writer http.ResponseWriter, request *http.Request, logger log15.Logger) {
	// Configuration validation
	if !r.config.Teams.SelfRegister {
		logger.Warn("Unauthorized attempt to self-register")
		r.errorResponse(403, "Team self-registration disabled", writer, request)
		return
	}

	// Decode the provided JSON input
	newTeam := api.TeamPut{}
	err := json.NewDecoder(request.Body).Decode(&newTeam)
	if err != nil {
		logger.Warn("Malformed JSON provided", log15.Ctx{"error": err})
		r.errorResponse(400, "Malformed JSON provided", writer, request)
		return
	}

	// Sanity checks
	validName := func(name string) bool {
		// Validate the length
		if len(name) < 1 || len(name) > 30 {
			return false
		}

		// Validate the character set
		match, _ := regexp.MatchString("^[a-zA-Z0-9 /\\\\~!@#$%&*()\\-_+={}\\[\\];:',.?]*$", name)
		if !match {
			return false
		}

		return true
	}

	if !validName(newTeam.Name) {
		logger.Warn("Bad team name", log15.Ctx{"name": newTeam.Name})
		r.errorResponse(400, "Bad team name", writer, request)
		return
	}

	if len(newTeam.Country) > 2 {
		logger.Warn("Bad team country code", log15.Ctx{"country": newTeam.Country})
		r.errorResponse(400, "Bad team country code", writer, request)
		return
	}

	if newTeam.Website != "" {
		u, err := url.ParseRequestURI(newTeam.Website)
		if err != nil {
			logger.Warn("Bad team URL", log15.Ctx{"url": newTeam.Website})
			r.errorResponse(400, "Bad team URL", writer, request)
			return
		}

		newTeam.Website = u.String()
	}

	// Extract the client IP
	ip, err := r.getIP(request)
	if err != nil {
		logger.Error("Failed to get the client's IP", log15.Ctx{"error": err})
		r.errorResponse(500, "Internal Server Error", writer, request)
		return
	}

	// Look for a matching team
	team, err := r.db.GetTeamForIP(*ip)
	if err == sql.ErrNoRows {
		logger.Warn("No team found for IP", log15.Ctx{"ip": ip.String()})
		r.errorResponse(404, "No team found for IP", writer, request)
		return
	} else if err != nil {
		logger.Error("Failed to get the team", log15.Ctx{"error": err})
		r.errorResponse(500, "Internal Server Error", writer, request)
		return
	}

	// Validate the request
	if !r.config.Teams.SelfUpdate {
		if team.Name != "" && team.Name != newTeam.Name {
			logger.Debug("Unauthorized attempt to change already set team property", log15.Ctx{"property": "name"})
			r.errorResponse(400, "Team name is already set", writer, request)
			return
		}

		if team.Country != "" && team.Country != newTeam.Country {
			logger.Debug("Unauthorized attempt to change already set team property", log15.Ctx{"property": "country"})
			r.errorResponse(400, "Team country is already set", writer, request)
			return
		}

		if team.Website != "" && team.Website != newTeam.Website {
			logger.Debug("Unauthorized attempt to change already set team property", log15.Ctx{"property": "website"})
			r.errorResponse(400, "Team website is already set", writer, request)
			return
		}
	}

	// Setup the new record
	newRecord := api.AdminTeamPut{}
	newRecord.Name = newTeam.Name
	newRecord.Country = strings.ToUpper(newTeam.Country)
	newRecord.Website = newTeam.Website
	newRecord.Notes = team.Notes
	newRecord.Subnets = team.Subnets

	// Attempt to update the database
	err = r.db.UpdateTeam(team.ID, newRecord)
	if err != nil {
		logger.Error("Failed to update the team", log15.Ctx{"error": err})
		r.errorResponse(500, "Internal Server Error", writer, request)
		return
	}

	eventSend("timeline", api.EventTimeline{TeamID: team.ID, Team: &newRecord.TeamPut, Type: "team-updated"})
	logger.Info("Team updated", log15.Ctx{"id": team.ID, "name": newRecord.Name, "country": newRecord.Country, "website": newRecord.Website})
}

func (r *rest) adminGetTeams(writer http.ResponseWriter, request *http.Request, logger log15.Logger) {
	// Get all the teams from the database
	teams, err := r.db.GetTeams()
	if err != nil {
		logger.Error("Failed to query the team list", log15.Ctx{"error": err})
		r.errorResponse(500, fmt.Sprintf("%v", err), writer, request)
		return
	}

	r.jsonResponse(teams, writer, request)
}

func (r *rest) adminCreateTeam(writer http.ResponseWriter, request *http.Request, logger log15.Logger) {
	// Decode the provided JSON input
	newTeam := api.AdminTeamPost{}
	err := json.NewDecoder(request.Body).Decode(&newTeam)
	if err != nil {
		logger.Warn("Malformed JSON provided", log15.Ctx{"error": err})
		r.errorResponse(400, "Malformed JSON provided", writer, request)
		return
	}

	// Attempt to create the database record
	id, err := r.db.CreateTeam(newTeam)
	if err != nil {
		logger.Error("Failed to create the team", log15.Ctx{"error": err})
		r.errorResponse(500, fmt.Sprintf("%v", err), writer, request)
		return
	}

	eventSend("timeline", api.EventTimeline{TeamID: id, Team: &newTeam.AdminTeamPut.TeamPut, Type: "team-added"})
	logger.Info("New team defined", log15.Ctx{"id": id, "subnets": newTeam.Subnets})
}

func (r *rest) adminGetTeam(writer http.ResponseWriter, request *http.Request, logger log15.Logger) {
	idVar := mux.Vars(request)["id"]

	// Convert the provided id to int
	id, err := strconv.ParseInt(idVar, 10, 64)
	if err != nil {
		logger.Warn("Invalid team ID provided", log15.Ctx{"id": idVar})
		r.errorResponse(400, "Invalid team ID provided", writer, request)
		return
	}

	// Attempt to get the DB record
	team, err := r.db.GetTeam(id)
	if err == sql.ErrNoRows {
		logger.Warn("Invalid team ID provided", log15.Ctx{"id": idVar})
		r.errorResponse(404, "Invalid team ID provided", writer, request)
		return
	} else if err != nil {
		logger.Error("Failed to get the team", log15.Ctx{"error": err})
		r.errorResponse(500, fmt.Sprintf("%v", err), writer, request)
		return
	}

	r.jsonResponse(team, writer, request)
}

func (r *rest) adminUpdateTeam(writer http.ResponseWriter, request *http.Request, logger log15.Logger) {
	idVar := mux.Vars(request)["id"]

	// Convert the provided id to int
	id, err := strconv.ParseInt(idVar, 10, 64)
	if err != nil {
		logger.Warn("Invalid team ID provided", log15.Ctx{"id": idVar})
		r.errorResponse(400, "Invalid team ID provided", writer, request)
		return
	}

	// Decode the provided JSON input
	newTeam := api.AdminTeamPut{}
	err = json.NewDecoder(request.Body).Decode(&newTeam)
	if err != nil {
		logger.Warn("Malformed JSON provided", log15.Ctx{"error": err})
		r.errorResponse(400, "Malformed JSON provided", writer, request)
		return
	}

	// Attempt to update the database
	err = r.db.UpdateTeam(id, newTeam)
	if err == sql.ErrNoRows {
		logger.Warn("Invalid team ID provided", log15.Ctx{"id": idVar})
		r.errorResponse(404, "Invalid team ID provided", writer, request)
		return
	} else if err != nil {
		logger.Error("Failed to update the team", log15.Ctx{"error": err})
		r.errorResponse(500, fmt.Sprintf("%v", err), writer, request)
		return
	}

	eventSend("timeline", api.EventTimeline{TeamID: id, Team: &newTeam.TeamPut, Type: "team-updated"})
	logger.Info("Team updated", log15.Ctx{"id": id, "name": newTeam.Name, "country": newTeam.Country, "website": newTeam.Website})
}

func (r *rest) adminDeleteTeam(writer http.ResponseWriter, request *http.Request, logger log15.Logger) {
	idVar := mux.Vars(request)["id"]

	// Convert the provided id to int
	id, err := strconv.ParseInt(idVar, 10, 64)
	if err != nil {
		logger.Warn("Invalid team ID provided", log15.Ctx{"id": idVar})
		r.errorResponse(400, "Invalid team ID provided", writer, request)
		return
	}

	// Attempt to get the DB record
	err = r.db.DeleteTeam(id)
	if err == sql.ErrNoRows {
		logger.Warn("Invalid team ID provided", log15.Ctx{"id": idVar})
		r.errorResponse(404, "Invalid team ID provided", writer, request)
		return
	} else if err != nil {
		logger.Error("Failed to delete the team", log15.Ctx{"error": err})
		r.errorResponse(500, fmt.Sprintf("%v", err), writer, request)
		return
	}

	eventSend("timeline", api.EventTimeline{TeamID: id, Type: "team-removed"})
	logger.Info("Team deleted", log15.Ctx{"id": id})
}
<|endoftext|>


<commit_before>package interpreter

import (
	"fmt"
	"io"
	"os"
	"os/exec"
	"regexp"
)

type CommandNotFound struct {
	Name string
	Err  error
}

func (this CommandNotFound) Stringer() string {
	return fmt.Sprintf("'%s' is not recognized as an internal or external command,\noperable program or batch file", this.Name)
}

func (this CommandNotFound) Error() string {
	return this.Stringer()
}

type NextT int

const (
	THROUGH  NextT = 0
	CONTINUE NextT = 1
	SHUTDOWN NextT = 2
)

type Interpreter struct {
	exec.Cmd
	Stdio        [3]*os.File
	HookCount    int
	IsBackGround bool
	Closer       io.Closer
}

func New() *Interpreter {
	this := Interpreter{
		Stdio: [3]*os.File{os.Stdin, os.Stdout, os.Stderr},
	}
	this.Stdin = os.Stdin
	this.Stdout = os.Stdout
	this.Stderr = os.Stderr
	return &this
}

func (this *Interpreter) SetStdin(f *os.File) {
	this.Stdio[0] = f
	this.Stdin = f
}
func (this *Interpreter) SetStdout(f *os.File) {
	this.Stdio[1] = f
	this.Stdout = f
}
func (this *Interpreter) SetStderr(f *os.File) {
	this.Stdio[2] = f
	this.Stderr = f
}

func (this *Interpreter) Clone() *Interpreter {
	rv := new(Interpreter)
	rv.Stdio[0] = this.Stdio[0]
	rv.Stdio[1] = this.Stdio[1]
	rv.Stdio[2] = this.Stdio[2]
	rv.Stdin = this.Stdin
	rv.Stdout = this.Stdout
	rv.Stderr = this.Stderr
	rv.HookCount = this.HookCount
	// Dont Copy 'Closer' and 'IsBackGround'
	return rv
}

type ArgsHookT func(args []string) []string

var argsHook = func(args []string) []string {
	return args
}

func SetArgsHook(argsHook_ ArgsHookT) (rv ArgsHookT) {
	rv, argsHook = argsHook, argsHook_
	return
}

type HookT func(*Interpreter) (NextT, error)

var hook = func(*Interpreter) (NextT, error) {
	return THROUGH, nil
}

func SetHook(hook_ HookT) (rv HookT) {
	rv, hook = hook, hook_
	return
}

var errorStatusPattern = regexp.MustCompile("^exit status ([0-9]+)")
var ErrorLevel string

func nvl(a *os.File, b *os.File) *os.File {
	if a != nil {
		return a
	} else {
		return b
	}
}

func (this *Interpreter) Spawnvp() (NextT, error) {
	var whatToDo NextT = CONTINUE
	var err error = nil

	if argsHook != nil {
		this.Args = argsHook(this.Args)
	}
	if len(this.Args) > 0 {
		whatToDo, err = hook(this)
		if whatToDo == THROUGH {
			this.Path, err = exec.LookPath(this.Args[0])
			if err == nil {
				if this.IsBackGround {
					go func() {
						this.Run()
						if this.Closer != nil {
							this.Closer.Close()
							this.Closer = nil
						}
					}()
				} else {
					err = this.Run()
				}
			} else {
				err = &CommandNotFound{this.Args[0], err}
			}
		}
	}
	if err != nil {
		m := errorStatusPattern.FindStringSubmatch(err.Error())
		if m != nil {
			ErrorLevel = m[1]
			err = nil
		} else {
			ErrorLevel = "-1"
		}
	} else {
		ErrorLevel = "0"
	}
	return whatToDo, err
}

func (this *Interpreter) Interpret(text string) (NextT, error) {
	statements, statementsErr := Parse(text)
	if statementsErr != nil {
		return CONTINUE, statementsErr
	}
	for _, pipeline := range statements {
		var pipeIn *os.File = nil
		for _, state := range pipeline {
			cmd := new(Interpreter)
			cmd.HookCount = this.HookCount
			cmd.SetStdin(nvl(this.Stdio[0], os.Stdin))
			cmd.SetStdout(nvl(this.Stdio[1], os.Stdout))
			cmd.SetStderr(nvl(this.Stdio[2], os.Stderr))
			if pipeIn != nil {
				cmd.SetStdin(pipeIn)
				pipeIn = nil
			}
			var err error = nil
			var pipeOut *os.File = nil
			isBackGround := false

			switch state.Term {
			case "|", "|&":
				isBackGround = true
				pipeIn, pipeOut, err = os.Pipe()
				if err != nil {
					return CONTINUE, err
				}
				// defer pipeIn.Close()
				cmd.SetStdout(pipeOut)
				if state.Term == "|&" {
					cmd.SetStderr(pipeOut)
				}
			case "&":
				isBackGround = true
			}

			for _, red := range state.Redirect {
				err = red.OpenOn(cmd)
				if err != nil {
					return CONTINUE, err
				}
			}
			cmd.Args = state.Argv
			cmd.IsBackGround = isBackGround
			cmd.Closer = pipeOut
			whatToDo, err := cmd.Spawnvp()
			if whatToDo == SHUTDOWN || err != nil {
				return whatToDo, err
			}
		}
	}
	return CONTINUE, nil
}
<commit_msg>(Made the hook on command-not-found on the golang layer)<commit_after>package interpreter

import (
	"fmt"
	"io"
	"os"
	"os/exec"
	"regexp"
)

type CommandNotFound struct {
	Name string
	Err  error
}

func (this CommandNotFound) Stringer() string {
	return fmt.Sprintf("'%s' is not recognized as an internal or external command,\noperable program or batch file", this.Name)
}

func (this CommandNotFound) Error() string {
	return this.Stringer()
}

type NextT int

const (
	THROUGH  NextT = 0
	CONTINUE NextT = 1
	SHUTDOWN NextT = 2
)

type Interpreter struct {
	exec.Cmd
	Stdio        [3]*os.File
	HookCount    int
	IsBackGround bool
	Closer       io.Closer
}

func New() *Interpreter {
	this := Interpreter{
		Stdio: [3]*os.File{os.Stdin, os.Stdout, os.Stderr},
	}
	this.Stdin = os.Stdin
	this.Stdout = os.Stdout
	this.Stderr = os.Stderr
	return &this
}

func (this *Interpreter) SetStdin(f *os.File) {
	this.Stdio[0] = f
	this.Stdin = f
}
func (this *Interpreter) SetStdout(f *os.File) {
	this.Stdio[1] = f
	this.Stdout = f
}
func (this *Interpreter) SetStderr(f *os.File) {
	this.Stdio[2] = f
	this.Stderr = f
}

func (this *Interpreter) Clone() *Interpreter {
	rv := new(Interpreter)
	rv.Stdio[0] = this.Stdio[0]
	rv.Stdio[1] = this.Stdio[1]
	rv.Stdio[2] = this.Stdio[2]
	rv.Stdin = this.Stdin
	rv.Stdout = this.Stdout
	rv.Stderr = this.Stderr
	rv.HookCount = this.HookCount
	// Dont Copy 'Closer' and 'IsBackGround'
	return rv
}

type ArgsHookT func(args []string) []string

var argsHook = func(args []string) []string {
	return args
}

func SetArgsHook(argsHook_ ArgsHookT) (rv ArgsHookT) {
	rv, argsHook = argsHook, argsHook_
	return
}

type HookT func(*Interpreter) (NextT, error)

var hook = func(*Interpreter) (NextT, error) {
	return THROUGH, nil
}

func SetHook(hook_ HookT) (rv HookT) {
	rv, hook = hook, hook_
	return
}

var OnCommandNotFound = func(this *Interpreter, err error) error {
	err = &CommandNotFound{this.Args[0], err}
	return err
}

var errorStatusPattern = regexp.MustCompile("^exit status ([0-9]+)")
var ErrorLevel string

func nvl(a *os.File, b *os.File) *os.File {
	if a != nil {
		return a
	} else {
		return b
	}
}

func (this *Interpreter) Spawnvp() (NextT, error) {
	var whatToDo NextT = CONTINUE
	var err error = nil

	if argsHook != nil {
		this.Args = argsHook(this.Args)
	}
	if len(this.Args) > 0 {
		whatToDo, err = hook(this)
		if whatToDo == THROUGH {
			this.Path, err = exec.LookPath(this.Args[0])
			if err == nil {
				if this.IsBackGround {
					go func() {
						this.Run()
						if this.Closer != nil {
							this.Closer.Close()
							this.Closer = nil
						}
					}()
				} else {
					err = this.Run()
				}
			} else {
				err = OnCommandNotFound(this, err)
			}
		}
	}
	if err != nil {
		m := errorStatusPattern.FindStringSubmatch(err.Error())
		if m != nil {
			ErrorLevel = m[1]
			err = nil
		} else {
			ErrorLevel = "-1"
		}
	} else {
		ErrorLevel = "0"
	}
	return whatToDo, err
}

func (this *Interpreter) Interpret(text string) (NextT, error) {
	statements, statementsErr := Parse(text)
	if statementsErr != nil {
		return CONTINUE, statementsErr
	}
	for _, pipeline := range statements {
		var pipeIn *os.File = nil
		for _, state := range pipeline {
			cmd := new(Interpreter)
			cmd.HookCount = this.HookCount
			cmd.SetStdin(nvl(this.Stdio[0], os.Stdin))
			cmd.SetStdout(nvl(this.Stdio[1], os.Stdout))
			cmd.SetStderr(nvl(this.Stdio[2], os.Stderr))
			if pipeIn != nil {
				cmd.SetStdin(pipeIn)
				pipeIn = nil
			}
			var err error = nil
			var pipeOut *os.File = nil
			isBackGround := false

			switch state.Term {
			case "|", "|&":
				isBackGround = true
				pipeIn, pipeOut, err = os.Pipe()
				if err != nil {
					return CONTINUE, err
				}
				// defer pipeIn.Close()
				cmd.SetStdout(pipeOut)
				if state.Term == "|&" {
					cmd.SetStderr(pipeOut)
				}
			case "&":
				isBackGround = true
			}

			for _, red := range state.Redirect {
				err = red.OpenOn(cmd)
				if err != nil {
					return CONTINUE, err
				}
			}
			cmd.Args = state.Argv
			cmd.IsBackGround = isBackGround
			cmd.Closer = pipeOut
			whatToDo, err := cmd.Spawnvp()
			if whatToDo == SHUTDOWN || err != nil {
				return whatToDo, err
			}
		}
	}
	return CONTINUE, nil
}
<|endoftext|>


<commit_before>package main

import (
	"context"
	"encoding/binary"
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"math/rand"
	"net/http"
	"regexp"
	"runtime"
	"time"

	"github.com/lomik/graphite-clickhouse/autocomplete"
	"github.com/lomik/graphite-clickhouse/config"
	"github.com/lomik/graphite-clickhouse/find"
	"github.com/lomik/graphite-clickhouse/helper/version"
	"github.com/lomik/graphite-clickhouse/index"
	"github.com/lomik/graphite-clickhouse/prometheus"
	"github.com/lomik/graphite-clickhouse/render"
	"github.com/lomik/graphite-clickhouse/tagger"
	"github.com/lomik/zapwriter"
	"go.uber.org/zap"

	_ "net/http/pprof"
)

// Version of graphite-clickhouse
const Version = "0.11.0"

func init() {
	version.Version = Version
}

type LogResponseWriter struct {
	http.ResponseWriter
	status int
}

func (w *LogResponseWriter) WriteHeader(status int) {
	w.status = status
	w.ResponseWriter.WriteHeader(status)
}

func (w *LogResponseWriter) Status() int {
	if w.status == 0 {
		return http.StatusOK
	}
	return w.status
}

func WrapResponseWriter(w http.ResponseWriter) *LogResponseWriter {
	if wrapped, ok := w.(*LogResponseWriter); ok {
		return wrapped
	}
	return &LogResponseWriter{ResponseWriter: w}
}

var requestIdRegexp *regexp.Regexp = regexp.MustCompile("^[a-zA-Z0-9_.-]+$")

func Handler(logger *zap.Logger, handler http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		writer := WrapResponseWriter(w)

		requestID := r.Header.Get("X-Request-Id")
		if requestID == "" || !requestIdRegexp.MatchString(requestID) {
			var b [16]byte
			binary.LittleEndian.PutUint64(b[:], rand.Uint64())
			binary.LittleEndian.PutUint64(b[8:], rand.Uint64())
			requestID = fmt.Sprintf("%x", b)
		}

		logger := logger.With(zap.String("request_id", requestID))

		r = r.WithContext(
			context.WithValue(
				context.WithValue(
					r.Context(),
					"logger",
					logger,
				),
				"requestID",
				requestID,
			),
		)

		start := time.Now()
		handler.ServeHTTP(writer, r)
		d := time.Since(start)
		logger.Info("access",
			zap.Duration("time", d),
			zap.String("method", r.Method),
			zap.String("url", r.URL.String()),
			zap.String("peer", r.RemoteAddr),
			zap.Int("status", writer.Status()),
		)
	})
}

func main() {
	rand.Seed(time.Now().UnixNano())

	var err error

	/* CONFIG start */

	configFile := flag.String("config", "/etc/graphite-clickhouse/graphite-clickhouse.conf", "Filename of config")
	printDefaultConfig := flag.Bool("config-print-default", false, "Print default config")
	checkConfig := flag.Bool("check-config", false, "Check config and exit")
	buildTags := flag.Bool("tags", false, "Build tags table")
	pprof := flag.String("pprof", "", "Additional pprof listen addr for non-server modes (tagger, etc..)")

	printVersion := flag.Bool("version", false, "Print version")

	flag.Parse()

	if *printVersion {
		fmt.Print(Version)
		return
	}

	if *printDefaultConfig {
		if err = config.PrintDefaultConfig(); err != nil {
			log.Fatal(err)
		}
		return
	}

	cfg, err := config.ReadConfig(*configFile)
	if err != nil {
		log.Fatal(err)
	}

	// config parsed successfully. Exit in check-only mode
	if *checkConfig {
		return
	}

	if err = zapwriter.ApplyConfig(cfg.Logging); err != nil {
		log.Fatal(err)
	}

	runtime.GOMAXPROCS(cfg.Common.MaxCPU)

	/* CONFIG end */

	if pprof != nil && *pprof != "" {
		go func() { log.Fatal(http.ListenAndServe(*pprof, nil)) }()
	}

	/* CONSOLE COMMANDS start */
	if *buildTags {
		if err := tagger.Make(cfg); err != nil {
			log.Fatal(err)
		}
		return
	}

	/* CONSOLE COMMANDS end */

	http.Handle("/metrics/find/", Handler(zapwriter.Default(), find.NewHandler(cfg)))
	http.Handle("/metrics/index.json", Handler(zapwriter.Default(), index.NewHandler(cfg)))
	http.Handle("/render/", Handler(zapwriter.Default(), render.NewHandler(cfg)))
	http.Handle("/tags/autoComplete/tags", Handler(zapwriter.Default(), autocomplete.NewTags(cfg)))
	http.Handle("/tags/autoComplete/values", Handler(zapwriter.Default(), autocomplete.NewValues(cfg)))
	http.HandleFunc("/debug/config", func(w http.ResponseWriter, r *http.Request) {
		b, err := json.MarshalIndent(cfg, "", "  ")
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		w.Write(b)
	})

	http.Handle("/", Handler(zapwriter.Default(), prometheus.NewHandler(cfg)))

	log.Fatal(http.ListenAndServe(cfg.Common.Listen, nil))
}
<commit_msg>version 0.11.1<commit_after>package main

import (
	"context"
	"encoding/binary"
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"math/rand"
	"net/http"
	"regexp"
	"runtime"
	"time"

	"github.com/lomik/graphite-clickhouse/autocomplete"
	"github.com/lomik/graphite-clickhouse/config"
	"github.com/lomik/graphite-clickhouse/find"
	"github.com/lomik/graphite-clickhouse/helper/version"
	"github.com/lomik/graphite-clickhouse/index"
	"github.com/lomik/graphite-clickhouse/prometheus"
	"github.com/lomik/graphite-clickhouse/render"
	"github.com/lomik/graphite-clickhouse/tagger"
	"github.com/lomik/zapwriter"
	"go.uber.org/zap"

	_ "net/http/pprof"
)

// Version of graphite-clickhouse
const Version = "0.11.1"

func init() {
	version.Version = Version
}

type LogResponseWriter struct {
	http.ResponseWriter
	status int
}

func (w *LogResponseWriter) WriteHeader(status int) {
	w.status = status
	w.ResponseWriter.WriteHeader(status)
}

func (w *LogResponseWriter) Status() int {
	if w.status == 0 {
		return http.StatusOK
	}
	return w.status
}

func WrapResponseWriter(w http.ResponseWriter) *LogResponseWriter {
	if wrapped, ok := w.(*LogResponseWriter); ok {
		return wrapped
	}
	return &LogResponseWriter{ResponseWriter: w}
}

var requestIdRegexp *regexp.Regexp = regexp.MustCompile("^[a-zA-Z0-9_.-]+$")

func Handler(logger *zap.Logger, handler http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		writer := WrapResponseWriter(w)

		requestID := r.Header.Get("X-Request-Id")
		if requestID == "" || !requestIdRegexp.MatchString(requestID) {
			var b [16]byte
			binary.LittleEndian.PutUint64(b[:], rand.Uint64())
			binary.LittleEndian.PutUint64(b[8:], rand.Uint64())
			requestID = fmt.Sprintf("%x", b)
		}

		logger := logger.With(zap.String("request_id", requestID))

		r = r.WithContext(
			context.WithValue(
				context.WithValue(
					r.Context(),
					"logger",
					logger,
				),
				"requestID",
				requestID,
			),
		)

		start := time.Now()
		handler.ServeHTTP(writer, r)
		d := time.Since(start)
		logger.Info("access",
			zap.Duration("time", d),
			zap.String("method", r.Method),
			zap.String("url", r.URL.String()),
			zap.String("peer", r.RemoteAddr),
			zap.Int("status", writer.Status()),
		)
	})
}

func main() {
	rand.Seed(time.Now().UnixNano())

	var err error

	/* CONFIG start */

	configFile := flag.String("config", "/etc/graphite-clickhouse/graphite-clickhouse.conf", "Filename of config")
	printDefaultConfig := flag.Bool("config-print-default", false, "Print default config")
	checkConfig := flag.Bool("check-config", false, "Check config and exit")
	buildTags := flag.Bool("tags", false, "Build tags table")
	pprof := flag.String("pprof", "", "Additional pprof listen addr for non-server modes (tagger, etc..)")

	printVersion := flag.Bool("version", false, "Print version")

	flag.Parse()

	if *printVersion {
		fmt.Print(Version)
		return
	}

	if *printDefaultConfig {
		if err = config.PrintDefaultConfig(); err != nil {
			log.Fatal(err)
		}
		return
	}

	cfg, err := config.ReadConfig(*configFile)
	if err != nil {
		log.Fatal(err)
	}

	// config parsed successfully. Exit in check-only mode
	if *checkConfig {
		return
	}

	if err = zapwriter.ApplyConfig(cfg.Logging); err != nil {
		log.Fatal(err)
	}

	runtime.GOMAXPROCS(cfg.Common.MaxCPU)

	/* CONFIG end */

	if pprof != nil && *pprof != "" {
		go func() { log.Fatal(http.ListenAndServe(*pprof, nil)) }()
	}

	/* CONSOLE COMMANDS start */
	if *buildTags {
		if err := tagger.Make(cfg); err != nil {
			log.Fatal(err)
		}
		return
	}

	/* CONSOLE COMMANDS end */

	http.Handle("/metrics/find/", Handler(zapwriter.Default(), find.NewHandler(cfg)))
	http.Handle("/metrics/index.json", Handler(zapwriter.Default(), index.NewHandler(cfg)))
	http.Handle("/render/", Handler(zapwriter.Default(), render.NewHandler(cfg)))
	http.Handle("/tags/autoComplete/tags", Handler(zapwriter.Default(), autocomplete.NewTags(cfg)))
	http.Handle("/tags/autoComplete/values", Handler(zapwriter.Default(), autocomplete.NewValues(cfg)))
	http.HandleFunc("/debug/config", func(w http.ResponseWriter, r *http.Request) {
		b, err := json.MarshalIndent(cfg, "", "  ")
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		w.Write(b)
	})

	http.Handle("/", Handler(zapwriter.Default(), prometheus.NewHandler(cfg)))

	log.Fatal(http.ListenAndServe(cfg.Common.Listen, nil))
}
<|endoftext|>


<commit_before>// Copyright 2015 by caixw, All rights reserved.
// Use of this source code is governed by a MIT
// license that can be found in the LICENSE file.

package orm

import (
	"bytes"
	"errors"

	"github.com/issue9/orm/fetch"
)

const (
	and = iota
	or
)

type Where struct {
	e     engine
	table string
	cond  *bytes.Buffer
	args  []interface{}
}

func newWhere(engine engine) *Where {
	return &Where{
		e:    engine,
		cond: new(bytes.Buffer),
		args: []interface{}{},
	}
}

func (w *Where) where(op int, cond string, args ...interface{}) *Where {
	switch {
	case w.cond.Len() == 0:
		w.cond.WriteString(" WHERE(")
	case op == and:
		w.cond.WriteString(" AND(")
	case op == or:
		w.cond.WriteString(" OR(")
	default:
		panic("Where:where:错误的参数op")
	}
	w.cond.WriteString(cond)
	w.cond.WriteByte(')')
	return w
}

func (w *Where) And(cond string, args ...interface{}) *Where {
	return w.where(and, cond, args...)
}

func (w *Where) Or(cond string, args ...interface{}) *Where {
	return w.where(or, cond, args...)
}

func (w *Where) Table(tableName string) *Where {
	w.table = tableName
	return w
}

func (w *Where) Delete() error {
	if len(w.table) == 0 {
		return errors.New("Where:Delete:未指定表名")
	}

	sql := bytes.NewBufferString("DELETE FROM ")
	w.e.Dialect().Quote(sql, w.table)
	sql.WriteString(w.cond.String())
	_, err := w.e.Exec(false, sql.String(), w.args...)
	return err
}

func (w *Where) Update(data map[string]interface{}) error {
	if len(w.table) == 0 {
		return errors.New("Where:Update:未指定表名")
	}

	if len(data) == 0 {
		return errors.New("Where:Update:未指定需要更新的数据")
	}

	sql := bytes.NewBufferString("UPDATE ")
	w.e.Dialect().Quote(sql, w.table)
	sql.WriteString(" SET ")
	for k, v := range data {
		w.e.Dialect().Quote(sql, k)
		sql.WriteByte('=')
		WriteString(sql, v)
		sql.WriteByte(',')
	}
	sql.Truncate(sql.Len() - 1) // 去掉最后一个逗号

	sql.WriteString(w.cond.String())
	_, err := w.e.Exec(false, sql.String(), w.args...)
	return err
}

func (w *Where) Select(cols ...string) ([]map[string]interface{}, error) {
	if len(w.table) == 0 {
		return nil, errors.New("Where:Select:未指定表名")
	}

	if len(cols) == 0 {
		cols = append(cols, "*")
	}

	sql := bytes.NewBufferString("SELECT ")
	for _, v := range cols {
		w.e.Dialect().Quote(sql, v)
		sql.WriteByte(',')
	}
	sql.Truncate(sql.Len() - 1)

	sql.WriteString(" FROM ")
	w.e.Dialect().Quote(sql, w.table)
	sql.WriteByte(' ')
	sql.WriteString(w.cond.String())

	rows, err := w.e.Query(false, sql.String(), w.args...)
	if err != nil {
		return nil, err
	}
	return fetch.Map(false, rows)
}
<commit_msg>为Where添加SelectMap方法<commit_after>// Copyright 2015 by caixw, All rights reserved.
// Use of this source code is governed by a MIT
// license that can be found in the LICENSE file.

package orm

import (
	"bytes"
	"database/sql"
	"errors"

	"github.com/issue9/orm/fetch"
)

const (
	and = iota
	or
)

type Where struct {
	e     engine
	table string
	cond  *bytes.Buffer
	args  []interface{}
}

func newWhere(engine engine) *Where {
	return &Where{
		e:    engine,
		cond: new(bytes.Buffer),
		args: []interface{}{},
	}
}

func (w *Where) where(op int, cond string, args ...interface{}) *Where {
	switch {
	case w.cond.Len() == 0:
		w.cond.WriteString(" WHERE(")
	case op == and:
		w.cond.WriteString(" AND(")
	case op == or:
		w.cond.WriteString(" OR(")
	default:
		panic("Where:where:错误的参数op")
	}
	w.cond.WriteString(cond)
	w.cond.WriteByte(')')
	return w
}

func (w *Where) And(cond string, args ...interface{}) *Where {
	return w.where(and, cond, args...)
}

func (w *Where) Or(cond string, args ...interface{}) *Where {
	return w.where(or, cond, args...)
}

func (w *Where) Table(tableName string) *Where {
	w.table = tableName
	return w
}

func (w *Where) Delete(replace bool) error {
	if len(w.table) == 0 {
		return errors.New("Where:Delete:未指定表名")
	}

	sql := bytes.NewBufferString("DELETE FROM ")
	w.e.Dialect().Quote(sql, w.table)
	sql.WriteString(w.cond.String())
	_, err := w.e.Exec(replace, sql.String(), w.args...)
	return err
}

func (w *Where) Update(replace bool, data map[string]interface{}) error {
	if len(w.table) == 0 {
		return errors.New("Where:Update:未指定表名")
	}

	if len(data) == 0 {
		return errors.New("Where:Update:未指定需要更新的数据")
	}

	sql := bytes.NewBufferString("UPDATE ")
	w.e.Dialect().Quote(sql, w.table)
	sql.WriteString(" SET ")
	for k, v := range data {
		w.e.Dialect().Quote(sql, k)
		sql.WriteByte('=')
		WriteString(sql, v)
		sql.WriteByte(',')
	}
	sql.Truncate(sql.Len() - 1) // 去掉最后一个逗号

	sql.WriteString(w.cond.String())
	_, err := w.e.Exec(replace, sql.String(), w.args...)
	return err
}

func (w *Where) Select(replace bool, objs interface{}) error {
	rows, err := w.buildSelectSQL(replace)
	if err != nil {
		return err
	}
	return fetch.Obj(objs, rows)
}

func (w *Where) SelectMap(replace bool, cols ...string) ([]map[string]interface{}, error) {
	rows, err := w.buildSelectSQL(replace, cols...)
	if err != nil {
		return nil, err
	}
	return fetch.Map(false, rows)
}

func (w *Where) buildSelectSQL(replace bool, cols ...string) (*sql.Rows, error) {
	if len(w.table) == 0 {
		return nil, errors.New("Where:Select:未指定表名")
	}

	if len(cols) == 0 {
		cols = append(cols, "*")
	}

	sql := bytes.NewBufferString("SELECT ")
	for _, v := range cols {
		w.e.Dialect().Quote(sql, v)
		sql.WriteByte(',')
	}
	sql.Truncate(sql.Len() - 1)

	sql.WriteString(" FROM ")
	w.e.Dialect().Quote(sql, w.table)
	sql.WriteByte(' ')
	sql.WriteString(w.cond.String())

	return w.e.Query(replace, sql.String(), w.args...)
}
<|endoftext|>


<commit_before>package gowikia

const (
	API_URL = "https://api.tictail.com"
	API_VER = "v1"

	API_GET = "GET"

	API_STORES = "stores"
)

type WikiaApi struct {
	url string
}

func NewWikiaApi(wikiaUrl string) (*WikiaApi, error) {
	valid, err := isValidUrl(wikiaUrl)
	if !valid {
		return nil, err
	}
	return &WikiaApi{wikiaUrl}, nil
}
<commit_msg>Started documentation<commit_after>/*
Package gowikia provides a way to accessing Wikia.com's API from go

Example usage:

  package main

  import "github.com/aquilax/go-wikia"

  func main() {
      wikiaApi = NewWikiaApi("http://muppet.wikia.com")
      latestActivity, err := wikiaApi.LatestActivity()
	  ...
  }
*/
package gowikia

type WikiaApi struct {
	url string
}

// NewWikiaApi function creates new wikiaApi or throws an error on invalid url
func NewWikiaApi(wikiaUrl string) (*WikiaApi, error) {
	valid, err := isValidUrl(wikiaUrl)
	if !valid {
		return nil, err
	}
	return &WikiaApi{wikiaUrl}, nil
}
<|endoftext|>


<commit_before>/*
Copyright 2013 Brice Figureau

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
	"flag"
	"fmt"
	"github.com/masterzen/winrm/winrm"
	"os"
)

func main() {
	var (
		hostname string
		user     string
		pass     string
		cmd      string
		port     int
	)

	flag.StringVar(&hostname, "hostname", "localhost", "winrm host")
	flag.StringVar(&user, "username", "vagrant", "winrm admin username")
	flag.StringVar(&pass, "password", "vagrant", "winrm admin password")
	flag.IntVar(&port, "port", 5985, "winrm port")
	flag.Parse()

	cmd = flag.Arg(0)
	client := winrm.NewClient(&winrm.Endpoint{hostname, port}, user, pass)
	exitCode, err := client.RunWithInput(cmd, os.Stdout, os.Stderr, os.Stdin)

	if err != nil {
		fmt.Println(err)
	}

	os.Exit(exitCode)
}
<commit_msg>Added exit codes as client API no longer returns them.<commit_after>/*
Copyright 2013 Brice Figureau

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
	"flag"
	"fmt"
	"github.com/masterzen/winrm/winrm"
	"os"
)

func main() {
	var (
		hostname string
		user     string
		pass     string
		cmd      string
		port     int
	)

	flag.StringVar(&hostname, "hostname", "localhost", "winrm host")
	flag.StringVar(&user, "username", "vagrant", "winrm admin username")
	flag.StringVar(&pass, "password", "vagrant", "winrm admin password")
	flag.IntVar(&port, "port", 5985, "winrm port")
	flag.Parse()

	cmd = flag.Arg(0)
	client := winrm.NewClient(&winrm.Endpoint{hostname, port}, user, pass)
	err := client.RunWithInput(cmd, os.Stdout, os.Stderr, os.Stdin)

	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	os.Exit(0)
}
<|endoftext|>


<commit_before>package xattr

// XAttrError records an error and the operation, file path and attribute that caused it.
type XAttrError struct {
	Op   string
	Path string
	Name string
	Err  error
}

func (e *XAttrError) Error() string {
	return e.Op + " " + e.Path + " " + e.Name + ": " + e.Err.Error()
}

// nullTermToStrings converts an array of NULL terminated UTF-8 strings to a []string.
func nullTermToStrings(buf []byte) (result []string) {
	offset := 0
	for index, b := range buf {
		if b == 0 {
			result = append(result, string(buf[offset:index]))
			offset = index + 1
		}
	}
	return
}
<commit_msg>Add doc comment<commit_after>/*
Package xattr provides support for extended attributes on linux, darwin and freebsd.
Extended attributes are name:value pairs associated permanently with files and directories,
similar to the environment strings associated with a process.
An attribute may be defined or undefined. If it is defined, its value may be empty or non-empty.
More details you can find here: https://en.wikipedia.org/wiki/Extended_file_attributes
*/
package xattr

// XAttrError records an error and the operation, file path and attribute that caused it.
type XAttrError struct {
	Op   string
	Path string
	Name string
	Err  error
}

func (e *XAttrError) Error() string {
	return e.Op + " " + e.Path + " " + e.Name + ": " + e.Err.Error()
}

// nullTermToStrings converts an array of NULL terminated UTF-8 strings to a []string.
func nullTermToStrings(buf []byte) (result []string) {
	offset := 0
	for index, b := range buf {
		if b == 0 {
			result = append(result, string(buf[offset:index]))
			offset = index + 1
		}
	}
	return
}
<|endoftext|>


<commit_before>package libxml2

/*
#cgo pkg-config: libxml-2.0
#include <stdbool.h>
#include "libxml/globals.h"
#include "libxml/xpath.h"
#include <libxml/xpathInternals.h>

// Macro wrapper function
static inline void MY_xmlFree(void *p) {
	xmlFree(p);
}

// Macro wrapper function
static inline bool MY_xmlXPathNodeSetIsEmpty(xmlNodeSetPtr ptr) {
	return xmlXPathNodeSetIsEmpty(ptr);
}

// Because Go can't do pointer airthmetics...
static inline xmlNodePtr MY_xmlNodeSetTabAt(xmlNodePtr *nodes, int i) {
	return nodes[i];
}

*/
import "C"
import (
	"errors"
	"fmt"
	"log"
)

type XPathObjectType int

const (
	XPathUndefined XPathObjectType = iota
	XPathNodeSet
	XPathBoolean
	XPathNumber
	XPathString
	XPathPoint
	XPathRange
	XPathLocationSet
	XPathUSers
	XPathXsltTree
)

const _XPathObjectType_name = "XPathUndefinedXPathNodeSetXPathBooleanXPathNumberXPathStringXPathPointXPathRangeXPathLocationSetXPathUSersXPathXsltTree"

var _XPathObjectType_index = [...]uint8{0, 14, 26, 38, 49, 60, 70, 80, 96, 106, 119}

func (i XPathObjectType) String() string {
	if i < 0 || i+1 >= XPathObjectType(len(_XPathObjectType_index)) {
		return fmt.Sprintf("XPathObjectType(%d)", i)
	}
	return _XPathObjectType_name[_XPathObjectType_index[i]:_XPathObjectType_index[i+1]]
}

type XPathObject struct {
	ptr *C.xmlXPathObject
	// This flag controls if the StringValue should use the *contents* (literal value)
	// of the nodeset instead of stringifying the node
	ForceLiteral bool
}

func (x XPathObject) Type() XPathObjectType {
	return XPathObjectType(x.ptr._type)
}

func (x XPathObject) Float64Value() float64 {
	return float64(x.ptr.floatval)
}

func (x XPathObject) BoolValue() bool {
	return C.int(x.ptr.boolval) == 1
}

func (x XPathObject) NodeList() NodeList {
	nodeset := x.ptr.nodesetval
	if nodeset == nil {
		return NodeList(nil)
	}

	if nodeset.nodeNr == 0 {
		return NodeList(nil)
	}

	ret := make(NodeList, nodeset.nodeNr)
	for i := 0; i < int(nodeset.nodeNr); i++ {
		ret[i] = wrapToNode(C.MY_xmlNodeSetTabAt(nodeset.nodeTab, C.int(i)))
	}

	return ret
}

func (x XPathObject) StringValue() string {
	switch x.Type() {
	case XPathNodeSet:
		if x.ForceLiteral {
			log.Printf("Returning literal value: %v", x.NodeList().Literal())
			return x.NodeList().Literal()
		}
		return x.NodeList().String()
	default:
		return fmt.Sprintf("%v", x)
	}
}

func (x *XPathObject) Free() {
	//	if x.ptr.nodesetval != nil {
	//		C.xmlXPathFreeNodeSet(x.ptr.nodesetval)
	//	}
	C.xmlXPathFreeObject(x.ptr)
}

type XPathContext struct {
	ptr *C.xmlXPathContext
}

// XPathExpression is a compiled XPath.
type XPathExpression struct {
	ptr *C.xmlXPathCompExpr
	// This exists mainly for debugging purposes
	expr string
}

func NewXPathExpression(s string) (*XPathExpression, error) {
	p := C.xmlXPathCompile(stringToXmlChar(s))
	if p == nil {
		return nil, errors.New("xpath compilation failed")
	}

	return &XPathExpression{ptr: p, expr: s}, nil
}

func (x *XPathExpression) Free() {
	if x.ptr == nil {
		return
	}
	C.xmlXPathFreeCompExpr(x.ptr)
}

// Note that although we are specifying `n... Node` for the argument,
// only the first, node is considered for the context node
func NewXPathContext(n ...Node) (*XPathContext, error) {
	ctx := C.xmlXPathNewContext(nil)
	ctx.namespaces = nil

	if len(n) > 0 && n[0] != nil {
		ctx.node = (*C.xmlNode)(n[0].pointer())
	}
	return &XPathContext{ptr: ctx}, nil
}

func (x *XPathContext) Exists(xpath string) bool {
	res, err := x.FindValue(xpath)
	if err != nil {
		return false
	}
	defer res.Free()

	switch res.Type() {
	case XPathNodeSet:
		return res.ptr.nodesetval.nodeNr > 0
	default:
		panic("unimplemented")
	}
	return false
}

func (x *XPathContext) Free() {
	if x.ptr == nil {
		return
	}

	C.xmlXPathFreeContext(x.ptr)
}

func (x *XPathContext) FindNodes(s string) (NodeList, error) {
	expr, err := NewXPathExpression(s)
	if err != nil {
		return nil, err
	}
	defer expr.Free()

	return x.FindNodesExpr(expr)
}

func (x *XPathContext) evalXPath(expr *XPathExpression) (*XPathObject, error) {
	if expr == nil {
		return nil, errors.New("empty XPathExpression")
	}

	// If there is no document associated with this context,
	// then xmlXPathCompiledEval() just fails to match
	ctx := x.ptr

	if ctx.node != nil && ctx.node.doc != nil {
		ctx.doc = ctx.node.doc
	}

	if ctx.doc == nil {
		ctx.doc = C.xmlNewDoc(stringToXmlChar("1.0"))
		defer C.xmlFreeDoc(ctx.doc)
	}

	res := C.xmlXPathCompiledEval(expr.ptr, ctx)
	if res == nil {
		return nil, errors.New("empty result")
	}

	return &XPathObject{ptr: res}, nil
}

func (x *XPathContext) FindNodesExpr(expr *XPathExpression) (NodeList, error) {
	res, err := x.evalXPath(expr)
	if err != nil {
		return nil, err
	}
	defer res.Free()

	return res.NodeList(), nil
}

func (x *XPathContext) FindValue(s string) (*XPathObject, error) {
	expr, err := NewXPathExpression(s)
	if err != nil {
		return nil, err
	}
	defer expr.Free()

	return x.FindValueExpr(expr)
}

// FindValueExpr evaluates the given XPath expression and returns an XPathObject.
// You must call `Free()` on this returned object
func (x *XPathContext) FindValueExpr(expr *XPathExpression) (*XPathObject, error) {
	res, err := x.evalXPath(expr)
	if err != nil {
		return nil, err
	}

	res.ForceLiteral = true
	return res, nil
}

func (x *XPathContext) LookupNamespaceURI(name string) (string, error) {
	s := C.xmlXPathNsLookup(x.ptr, stringToXmlChar(name))
	if s == nil {
		return "", errors.New("not found")
	}
	return xmlCharToString(s), nil
}

func (x *XPathContext) RegisterNs(name, nsuri string) error {
	res := C.xmlXPathRegisterNs(x.ptr, stringToXmlChar(name), stringToXmlChar(nsuri))
	if res == -1 {
		return errors.New("cannot register namespace")
	}
	return nil
}
<commit_msg>Add SetContextNode<commit_after>package libxml2

/*
#cgo pkg-config: libxml-2.0
#include <stdbool.h>
#include "libxml/globals.h"
#include "libxml/xpath.h"
#include <libxml/xpathInternals.h>

// Macro wrapper function
static inline void MY_xmlFree(void *p) {
	xmlFree(p);
}

// Macro wrapper function
static inline bool MY_xmlXPathNodeSetIsEmpty(xmlNodeSetPtr ptr) {
	return xmlXPathNodeSetIsEmpty(ptr);
}

// Because Go can't do pointer airthmetics...
static inline xmlNodePtr MY_xmlNodeSetTabAt(xmlNodePtr *nodes, int i) {
	return nodes[i];
}

*/
import "C"
import (
	"errors"
	"fmt"
	"log"
)

type XPathObjectType int

const (
	XPathUndefined XPathObjectType = iota
	XPathNodeSet
	XPathBoolean
	XPathNumber
	XPathString
	XPathPoint
	XPathRange
	XPathLocationSet
	XPathUSers
	XPathXsltTree
)

const _XPathObjectType_name = "XPathUndefinedXPathNodeSetXPathBooleanXPathNumberXPathStringXPathPointXPathRangeXPathLocationSetXPathUSersXPathXsltTree"

var _XPathObjectType_index = [...]uint8{0, 14, 26, 38, 49, 60, 70, 80, 96, 106, 119}

func (i XPathObjectType) String() string {
	if i < 0 || i+1 >= XPathObjectType(len(_XPathObjectType_index)) {
		return fmt.Sprintf("XPathObjectType(%d)", i)
	}
	return _XPathObjectType_name[_XPathObjectType_index[i]:_XPathObjectType_index[i+1]]
}

type XPathObject struct {
	ptr *C.xmlXPathObject
	// This flag controls if the StringValue should use the *contents* (literal value)
	// of the nodeset instead of stringifying the node
	ForceLiteral bool
}

func (x XPathObject) Type() XPathObjectType {
	return XPathObjectType(x.ptr._type)
}

func (x XPathObject) Float64Value() float64 {
	return float64(x.ptr.floatval)
}

func (x XPathObject) BoolValue() bool {
	return C.int(x.ptr.boolval) == 1
}

func (x XPathObject) NodeList() NodeList {
	nodeset := x.ptr.nodesetval
	if nodeset == nil {
		return NodeList(nil)
	}

	if nodeset.nodeNr == 0 {
		return NodeList(nil)
	}

	ret := make(NodeList, nodeset.nodeNr)
	for i := 0; i < int(nodeset.nodeNr); i++ {
		ret[i] = wrapToNode(C.MY_xmlNodeSetTabAt(nodeset.nodeTab, C.int(i)))
	}

	return ret
}

func (x XPathObject) StringValue() string {
	switch x.Type() {
	case XPathNodeSet:
		if x.ForceLiteral {
			log.Printf("Returning literal value: %v", x.NodeList().Literal())
			return x.NodeList().Literal()
		}
		return x.NodeList().String()
	default:
		return fmt.Sprintf("%v", x)
	}
}

func (x *XPathObject) Free() {
	//	if x.ptr.nodesetval != nil {
	//		C.xmlXPathFreeNodeSet(x.ptr.nodesetval)
	//	}
	C.xmlXPathFreeObject(x.ptr)
}

type XPathContext struct {
	ptr *C.xmlXPathContext
}

// XPathExpression is a compiled XPath.
type XPathExpression struct {
	ptr *C.xmlXPathCompExpr
	// This exists mainly for debugging purposes
	expr string
}

func NewXPathExpression(s string) (*XPathExpression, error) {
	p := C.xmlXPathCompile(stringToXmlChar(s))
	if p == nil {
		return nil, errors.New("xpath compilation failed")
	}

	return &XPathExpression{ptr: p, expr: s}, nil
}

func (x *XPathExpression) Free() {
	if x.ptr == nil {
		return
	}
	C.xmlXPathFreeCompExpr(x.ptr)
}

// Note that although we are specifying `n... Node` for the argument,
// only the first, node is considered for the context node
func NewXPathContext(n ...Node) (*XPathContext, error) {
	ctx := C.xmlXPathNewContext(nil)
	ctx.namespaces = nil

	obj := &XPathContext{ptr: ctx}
	if len(n) > 0 {
		obj.SetContextNode(n[0])
	}
	return obj, nil
}

func (x *XPathContext) SetContextNode(n Node) {
	if n == nil {
		return
	}
	x.ptr.node = (*C.xmlNode)(n.pointer())
}

func (x *XPathContext) Exists(xpath string) bool {
	res, err := x.FindValue(xpath)
	if err != nil {
		return false
	}
	defer res.Free()

	switch res.Type() {
	case XPathNodeSet:
		return res.ptr.nodesetval.nodeNr > 0
	default:
		panic("unimplemented")
	}
	return false
}

func (x *XPathContext) Free() {
	if x.ptr == nil {
		return
	}

	C.xmlXPathFreeContext(x.ptr)
}

func (x *XPathContext) FindNodes(s string) (NodeList, error) {
	expr, err := NewXPathExpression(s)
	if err != nil {
		return nil, err
	}
	defer expr.Free()

	return x.FindNodesExpr(expr)
}

func (x *XPathContext) evalXPath(expr *XPathExpression) (*XPathObject, error) {
	if expr == nil {
		return nil, errors.New("empty XPathExpression")
	}

	// If there is no document associated with this context,
	// then xmlXPathCompiledEval() just fails to match
	ctx := x.ptr

	if ctx.node != nil && ctx.node.doc != nil {
		ctx.doc = ctx.node.doc
	}

	if ctx.doc == nil {
		ctx.doc = C.xmlNewDoc(stringToXmlChar("1.0"))
		defer C.xmlFreeDoc(ctx.doc)
	}

	res := C.xmlXPathCompiledEval(expr.ptr, ctx)
	if res == nil {
		return nil, errors.New("empty result")
	}

	return &XPathObject{ptr: res}, nil
}

func (x *XPathContext) FindNodesExpr(expr *XPathExpression) (NodeList, error) {
	res, err := x.evalXPath(expr)
	if err != nil {
		return nil, err
	}
	defer res.Free()

	return res.NodeList(), nil
}

func (x *XPathContext) FindValue(s string) (*XPathObject, error) {
	expr, err := NewXPathExpression(s)
	if err != nil {
		return nil, err
	}
	defer expr.Free()

	return x.FindValueExpr(expr)
}

// FindValueExpr evaluates the given XPath expression and returns an XPathObject.
// You must call `Free()` on this returned object
func (x *XPathContext) FindValueExpr(expr *XPathExpression) (*XPathObject, error) {
	res, err := x.evalXPath(expr)
	if err != nil {
		return nil, err
	}

	res.ForceLiteral = true
	return res, nil
}

func (x *XPathContext) LookupNamespaceURI(name string) (string, error) {
	s := C.xmlXPathNsLookup(x.ptr, stringToXmlChar(name))
	if s == nil {
		return "", errors.New("not found")
	}
	return xmlCharToString(s), nil
}

func (x *XPathContext) RegisterNs(name, nsuri string) error {
	res := C.xmlXPathRegisterNs(x.ptr, stringToXmlChar(name), stringToXmlChar(nsuri))
	if res == -1 {
		return errors.New("cannot register namespace")
	}
	return nil
}
<|endoftext|>