<commit_before>//go:build linux && cgo && !agent

package db

import (
	"database/sql"
	"fmt"
	"net/http"

	"github.com/lxc/lxd/lxd/db/cluster"
	"github.com/lxc/lxd/lxd/db/query"
	"github.com/lxc/lxd/shared/api"
	"github.com/lxc/lxd/shared/logger"
	"github.com/lxc/lxd/shared/version"
)

// ClusterGroup is a value object holding db-related details about a cluster group.
type ClusterGroup struct {
	ID          int
	Name        string
	Description string
	Nodes       []string
}

// ClusterGroupFilter specifies potential query parameter fields.
type ClusterGroupFilter struct {
	ID   *int
	Name *string
}

var clusterGroupObjects = cluster.RegisterStmt(`
SELECT cluster_groups.id, cluster_groups.name, coalesce(cluster_groups.description, '')
  FROM cluster_groups
  ORDER BY cluster_groups.name
`)

var clusterGroupObjectsByName = cluster.RegisterStmt(`
SELECT cluster_groups.id, cluster_groups.name, coalesce(cluster_groups.description, '')
  FROM cluster_groups
  WHERE cluster_groups.name = ? ORDER BY cluster_groups.name
`)

var clusterGroupCreate = cluster.RegisterStmt(`
INSERT INTO cluster_groups (name, description)
  VALUES (?, ?)
`)

var clusterGroupID = cluster.RegisterStmt(`
SELECT cluster_groups.id FROM cluster_groups
  WHERE cluster_groups.name = ?
`)

var clusterGroupRename = cluster.RegisterStmt(`
UPDATE cluster_groups SET name = ? WHERE name = ?
`)

var clusterGroupDeleteByName = cluster.RegisterStmt(`
DELETE FROM cluster_groups WHERE name = ?
`)

var clusterGroupUpdate = cluster.RegisterStmt(`
UPDATE cluster_groups
  SET name = ?, description = ?
 WHERE id = ?
`)

var clusterGroupDeleteNodesRef = cluster.RegisterStmt(`
DELETE FROM nodes_cluster_groups WHERE group_id = ?
`)

// GetClusterGroups returns all available ClusterGroups.
// generator: ClusterGroup GetMany
func (c *ClusterTx) GetClusterGroups(filter ClusterGroupFilter) ([]ClusterGroup, error) {
	// Result slice.
	objects := make([]ClusterGroup, 0)

	// Pick the prepared statement and arguments to use based on active criteria.
	var stmt *sql.Stmt
	var args []any

	if filter.Name != nil && filter.ID == nil {
		stmt = cluster.Stmt(c.tx, clusterGroupObjectsByName)
		args = []any{
			filter.Name,
		}
	} else if filter.ID == nil && filter.Name == nil {
		stmt = cluster.Stmt(c.tx, clusterGroupObjects)
		args = []any{}
	} else {
		return nil, fmt.Errorf("No statement exists for the given Filter")
	}

	// Dest function for scanning a row.
	dest := func(i int) []any {
		objects = append(objects, ClusterGroup{})
		return []any{
			&objects[i].ID,
			&objects[i].Name,
			&objects[i].Description,
		}
	}

	// Select.
	err := query.SelectObjects(stmt, dest, args...)
	if err != nil {
		return nil, fmt.Errorf("Failed to fetch cluster groups: %w", err)
	}

	// Get nodes in cluster group.
	for i := 0; i < len(objects); i++ {
		objects[i].Nodes, err = c.GetClusterGroupNodes(objects[i].Name)
		if err != nil {
			return nil, err
		}
	}

	return objects, nil
}

// GetClusterGroup returns the ClusterGroup with the given key.
// generator: ClusterGroup GetOne
func (c *ClusterTx) GetClusterGroup(name string) (*ClusterGroup, error) {
	filter := ClusterGroupFilter{}
	filter.Name = &name

	objects, err := c.GetClusterGroups(filter)
	if err != nil {
		return nil, fmt.Errorf("Failed to fetch cluster group: %w", err)
	}

	switch len(objects) {
	case 0:
		return nil, api.StatusErrorf(http.StatusNotFound, "Cluster group not found")
	case 1:
		return &objects[0], nil
	default:
		return nil, fmt.Errorf("More than one cluster group matches")
	}
}

// GetClusterGroupID return the ID of the ClusterGroup with the given key.
// generator: ClusterGroup ID
func (c *ClusterTx) GetClusterGroupID(name string) (int64, error) {
	stmt := cluster.Stmt(c.tx, clusterGroupID)
	rows, err := stmt.Query(name)
	if err != nil {
		return -1, fmt.Errorf("Failed to get cluster group ID: %w", err)
	}

	defer func() { _ = rows.Close() }()

	// Ensure we read one and only one row.
	if !rows.Next() {
		return -1, api.StatusErrorf(http.StatusNotFound, "Cluster group not found")
	}

	var id int64
	err = rows.Scan(&id)
	if err != nil {
		return -1, fmt.Errorf("Failed to scan ID: %w", err)
	}

	if rows.Next() {
		return -1, fmt.Errorf("More than one row returned")
	}

	err = rows.Err()
	if err != nil {
		return -1, fmt.Errorf("Result set failure: %w", err)
	}

	return id, nil
}

// ClusterGroupExists checks if a ClusterGroup with the given key exists.
// generator: ClusterGroup Exists
func (c *ClusterTx) ClusterGroupExists(name string) (bool, error) {
	_, err := c.GetClusterGroupID(name)
	if err != nil {
		if api.StatusErrorCheck(err, http.StatusNotFound) {
			return false, nil
		}

		return false, err
	}

	return true, nil
}

// CreateClusterGroup adds a new ClusterGroup to the database.
// generator: ClusterGroup Create
func (c *ClusterTx) CreateClusterGroup(object ClusterGroup) (int64, error) {
	// Check if a ClusterGroup with the same key exists.
	exists, err := c.ClusterGroupExists(object.Name)
	if err != nil {
		return -1, fmt.Errorf("Failed to check for duplicates: %w", err)
	}

	if exists {
		return -1, fmt.Errorf("This cluster group already exists")
	}

	args := make([]any, 2)

	// Populate the statement arguments.
	args[0] = object.Name
	args[1] = object.Description

	// Prepared statement to use.
	stmt := cluster.Stmt(c.tx, clusterGroupCreate)

	// Execute the statement.
	result, err := stmt.Exec(args...)
	if err != nil {
		return -1, fmt.Errorf("Failed to create cluster group: %w", err)
	}

	id, err := result.LastInsertId()
	if err != nil {
		return -1, fmt.Errorf("Failed to fetch cluster group ID: %w", err)
	}

	// Insert nodes reference.
	err = addNodesToClusterGroup(c.tx, int(id), object.Nodes)
	if err != nil {
		return -1, fmt.Errorf("Insert nodes for cluster group: %w", err)
	}

	return id, nil
}

// RenameClusterGroup renames the ClusterGroup matching the given key parameters.
// generator: ClusterGroup Rename
func (c *ClusterTx) RenameClusterGroup(name string, to string) error {
	stmt := cluster.Stmt(c.tx, clusterGroupRename)
	result, err := stmt.Exec(to, name)
	if err != nil {
		return fmt.Errorf("Failed to rename cluster group: %w", err)
	}

	n, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("Fetch affected rows: %w", err)
	}

	if n != 1 {
		return fmt.Errorf("Query affected %d rows instead of 1", n)
	}

	return nil
}

// DeleteClusterGroup deletes the ClusterGroup matching the given key parameters.
// generator: ClusterGroup DeleteOne-by-Name
func (c *ClusterTx) DeleteClusterGroup(name string) error {
	stmt := cluster.Stmt(c.tx, clusterGroupDeleteByName)
	result, err := stmt.Exec(name)
	if err != nil {
		return fmt.Errorf("Failed to delete cluster group: %w", err)
	}

	n, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("Fetch affected rows: %w", err)
	}

	if n != 1 {
		return fmt.Errorf("Query deleted %d rows instead of 1", n)
	}

	return nil
}

// UpdateClusterGroup updates the ClusterGroup matching the given key parameters.
// generator: ClusterGroup Update
func (c *ClusterTx) UpdateClusterGroup(name string, object ClusterGroup) error {
	id, err := c.GetClusterGroupID(name)
	if err != nil {
		return fmt.Errorf("Failed to get cluster group: %w", err)
	}

	stmt := cluster.Stmt(c.tx, clusterGroupUpdate)
	result, err := stmt.Exec(object.Name, object.Description, id)
	if err != nil {
		return fmt.Errorf("Failed to update cluster group: %w", err)
	}

	n, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("Fetch affected rows: %w", err)
	}

	if n != 1 {
		return fmt.Errorf("Query updated %d rows instead of 1", n)
	}

	// Delete current nodes.
	stmt = cluster.Stmt(c.tx, clusterGroupDeleteNodesRef)
	_, err = stmt.Exec(id)
	if err != nil {
		return fmt.Errorf("Failed to delete current nodes: %w", err)
	}

	// Insert nodes reference.
	err = addNodesToClusterGroup(c.tx, int(id), object.Nodes)
	if err != nil {
		return fmt.Errorf("Failed to insert nodes for cluster group: %w", err)
	}

	return nil
}

// ClusterGroupToAPI is a convenience to convert a ClusterGroup db struct into
// an API cluster group struct.
func ClusterGroupToAPI(clusterGroup *ClusterGroup, nodes []string) *api.ClusterGroup {
	c := &api.ClusterGroup{
		ClusterGroupPut: api.ClusterGroupPut{
			Description: clusterGroup.Description,
			Members:     nodes,
		},
		ClusterGroupPost: api.ClusterGroupPost{
			Name: clusterGroup.Name,
		},
	}

	return c
}

// GetClusterGroupNodes returns a list of nodes of the given cluster group.
func (c *ClusterTx) GetClusterGroupNodes(groupName string) ([]string, error) {
	q := `SELECT nodes.name FROM nodes_cluster_groups
JOIN nodes ON nodes.id = nodes_cluster_groups.node_id
JOIN cluster_groups ON cluster_groups.id = nodes_cluster_groups.group_id
WHERE cluster_groups.name = ?`

	return query.SelectStrings(c.tx, q, groupName)
}

// GetClusterGroupURIs returns all available ClusterGroup URIs.
// generator: ClusterGroup URIs
func (c *ClusterTx) GetClusterGroupURIs(filter ClusterGroupFilter) ([]string, error) {
	var args []any
	var sql string
	if filter.Name != nil && filter.ID == nil {
		sql = `SELECT cluster_groups.name FROM cluster_groups
WHERE cluster_groups.name = ? ORDER BY cluster_groups.name
`
		args = []any{
			filter.Name,
		}
	} else if filter.ID == nil && filter.Name == nil {
		sql = `SELECT cluster_groups.name FROM cluster_groups ORDER BY cluster_groups.name`
		args = []any{}
	} else {
		return nil, fmt.Errorf("No statement exists for the given Filter")
	}

	names, err := query.SelectStrings(c.tx, sql, args...)
	if err != nil {
		return nil, err
	}

	uris := make([]string, len(names))
	for i, name := range names {
		uris[i] = api.NewURL().Path(version.APIVersion, "cluster/groups", name).String()
	}

	return uris, nil
}

//AddNodeToClusterGroup adds a given node to the given cluster group.
func (c *ClusterTx) AddNodeToClusterGroup(groupName string, nodeName string) error {
	groupID, err := c.GetClusterGroupID(groupName)
	if err != nil {
		return fmt.Errorf("Failed to get cluster group ID: %w", err)
	}

	nodeInfo, err := c.GetNodeByName(nodeName)
	if err != nil {
		return fmt.Errorf("Failed to get node info: %w", err)
	}

	_, err = c.tx.Exec(`INSERT INTO nodes_cluster_groups (node_id, group_id) VALUES(?, ?);`, nodeInfo.ID, groupID)
	if err != nil {
		return err
	}

	return nil
}

// RemoveNodeFromClusterGroup removes a given node from the given group name.
func (c *ClusterTx) RemoveNodeFromClusterGroup(groupName string, nodeName string) error {
	groupID, err := c.GetClusterGroupID(groupName)
	if err != nil {
		return fmt.Errorf("Failed to get cluster group ID: %w", err)
	}

	nodeInfo, err := c.GetNodeByName(nodeName)
	if err != nil {
		return fmt.Errorf("Failed to get node info: %w", err)
	}

	_, err = c.tx.Exec(`DELETE FROM nodes_cluster_groups WHERE node_id = ? AND group_id = ?`, nodeInfo.ID, groupID)
	if err != nil {
		return err
	}

	return nil
}

//GetClusterGroupsWithNode returns a list of cluster group names the given node belongs to.
func (c *ClusterTx) GetClusterGroupsWithNode(nodeName string) ([]string, error) {
	q := `SELECT cluster_groups.name FROM nodes_cluster_groups
JOIN cluster_groups ON cluster_groups.id = nodes_cluster_groups.group_id
JOIN nodes ON nodes.id = nodes_cluster_groups.node_id
WHERE nodes.name = ?`

	return query.SelectStrings(c.tx, q, nodeName)
}

// ToAPI returns a LXD API entry.
func (c *ClusterGroup) ToAPI() (*api.ClusterGroup, error) {
	result := api.ClusterGroup{
		ClusterGroupPut: api.ClusterGroupPut{
			Description: c.Description,
			Members:     c.Nodes,
		},
		ClusterGroupPost: api.ClusterGroupPost{
			Name: c.Name,
		},
	}

	return &result, nil
}

// addNodesToClusterGroup adds the given nodes the the cluster group with the given ID.
func addNodesToClusterGroup(tx *sql.Tx, id int, nodes []string) error {
	str := `
INSERT INTO nodes_cluster_groups (group_id, node_id)
  VALUES (
    ?,
    (SELECT nodes.id
     FROM nodes
     WHERE nodes.name = ?)
  )`
	stmt, err := tx.Prepare(str)
	if err != nil {
		return err
	}

	defer func() { _ = stmt.Close() }()

	for _, node := range nodes {
		_, err = stmt.Exec(id, node)
		if err != nil {
			logger.Debugf("Error adding node %q to cluster group: %s", node, err)
			return err
		}
	}

	return nil
}
<commit_msg>lxd/db/cluster: Improve URL generation.<commit_after>//go:build linux && cgo && !agent

package db

import (
	"database/sql"
	"fmt"
	"net/http"

	"github.com/lxc/lxd/lxd/db/cluster"
	"github.com/lxc/lxd/lxd/db/query"
	"github.com/lxc/lxd/shared/api"
	"github.com/lxc/lxd/shared/logger"
	"github.com/lxc/lxd/shared/version"
)

// ClusterGroup is a value object holding db-related details about a cluster group.
type ClusterGroup struct {
	ID          int
	Name        string
	Description string
	Nodes       []string
}

// ClusterGroupFilter specifies potential query parameter fields.
type ClusterGroupFilter struct {
	ID   *int
	Name *string
}

var clusterGroupObjects = cluster.RegisterStmt(`
SELECT cluster_groups.id, cluster_groups.name, coalesce(cluster_groups.description, '')
  FROM cluster_groups
  ORDER BY cluster_groups.name
`)

var clusterGroupObjectsByName = cluster.RegisterStmt(`
SELECT cluster_groups.id, cluster_groups.name, coalesce(cluster_groups.description, '')
  FROM cluster_groups
  WHERE cluster_groups.name = ? ORDER BY cluster_groups.name
`)

var clusterGroupCreate = cluster.RegisterStmt(`
INSERT INTO cluster_groups (name, description)
  VALUES (?, ?)
`)

var clusterGroupID = cluster.RegisterStmt(`
SELECT cluster_groups.id FROM cluster_groups
  WHERE cluster_groups.name = ?
`)

var clusterGroupRename = cluster.RegisterStmt(`
UPDATE cluster_groups SET name = ? WHERE name = ?
`)

var clusterGroupDeleteByName = cluster.RegisterStmt(`
DELETE FROM cluster_groups WHERE name = ?
`)

var clusterGroupUpdate = cluster.RegisterStmt(`
UPDATE cluster_groups
  SET name = ?, description = ?
 WHERE id = ?
`)

var clusterGroupDeleteNodesRef = cluster.RegisterStmt(`
DELETE FROM nodes_cluster_groups WHERE group_id = ?
`)

// GetClusterGroups returns all available ClusterGroups.
// generator: ClusterGroup GetMany
func (c *ClusterTx) GetClusterGroups(filter ClusterGroupFilter) ([]ClusterGroup, error) {
	// Result slice.
	objects := make([]ClusterGroup, 0)

	// Pick the prepared statement and arguments to use based on active criteria.
	var stmt *sql.Stmt
	var args []any

	if filter.Name != nil && filter.ID == nil {
		stmt = cluster.Stmt(c.tx, clusterGroupObjectsByName)
		args = []any{
			filter.Name,
		}
	} else if filter.ID == nil && filter.Name == nil {
		stmt = cluster.Stmt(c.tx, clusterGroupObjects)
		args = []any{}
	} else {
		return nil, fmt.Errorf("No statement exists for the given Filter")
	}

	// Dest function for scanning a row.
	dest := func(i int) []any {
		objects = append(objects, ClusterGroup{})
		return []any{
			&objects[i].ID,
			&objects[i].Name,
			&objects[i].Description,
		}
	}

	// Select.
	err := query.SelectObjects(stmt, dest, args...)
	if err != nil {
		return nil, fmt.Errorf("Failed to fetch cluster groups: %w", err)
	}

	// Get nodes in cluster group.
	for i := 0; i < len(objects); i++ {
		objects[i].Nodes, err = c.GetClusterGroupNodes(objects[i].Name)
		if err != nil {
			return nil, err
		}
	}

	return objects, nil
}

// GetClusterGroup returns the ClusterGroup with the given key.
// generator: ClusterGroup GetOne
func (c *ClusterTx) GetClusterGroup(name string) (*ClusterGroup, error) {
	filter := ClusterGroupFilter{}
	filter.Name = &name

	objects, err := c.GetClusterGroups(filter)
	if err != nil {
		return nil, fmt.Errorf("Failed to fetch cluster group: %w", err)
	}

	switch len(objects) {
	case 0:
		return nil, api.StatusErrorf(http.StatusNotFound, "Cluster group not found")
	case 1:
		return &objects[0], nil
	default:
		return nil, fmt.Errorf("More than one cluster group matches")
	}
}

// GetClusterGroupID return the ID of the ClusterGroup with the given key.
// generator: ClusterGroup ID
func (c *ClusterTx) GetClusterGroupID(name string) (int64, error) {
	stmt := cluster.Stmt(c.tx, clusterGroupID)
	rows, err := stmt.Query(name)
	if err != nil {
		return -1, fmt.Errorf("Failed to get cluster group ID: %w", err)
	}

	defer func() { _ = rows.Close() }()

	// Ensure we read one and only one row.
	if !rows.Next() {
		return -1, api.StatusErrorf(http.StatusNotFound, "Cluster group not found")
	}

	var id int64
	err = rows.Scan(&id)
	if err != nil {
		return -1, fmt.Errorf("Failed to scan ID: %w", err)
	}

	if rows.Next() {
		return -1, fmt.Errorf("More than one row returned")
	}

	err = rows.Err()
	if err != nil {
		return -1, fmt.Errorf("Result set failure: %w", err)
	}

	return id, nil
}

// ClusterGroupExists checks if a ClusterGroup with the given key exists.
// generator: ClusterGroup Exists
func (c *ClusterTx) ClusterGroupExists(name string) (bool, error) {
	_, err := c.GetClusterGroupID(name)
	if err != nil {
		if api.StatusErrorCheck(err, http.StatusNotFound) {
			return false, nil
		}

		return false, err
	}

	return true, nil
}

// CreateClusterGroup adds a new ClusterGroup to the database.
// generator: ClusterGroup Create
func (c *ClusterTx) CreateClusterGroup(object ClusterGroup) (int64, error) {
	// Check if a ClusterGroup with the same key exists.
	exists, err := c.ClusterGroupExists(object.Name)
	if err != nil {
		return -1, fmt.Errorf("Failed to check for duplicates: %w", err)
	}

	if exists {
		return -1, fmt.Errorf("This cluster group already exists")
	}

	args := make([]any, 2)

	// Populate the statement arguments.
	args[0] = object.Name
	args[1] = object.Description

	// Prepared statement to use.
	stmt := cluster.Stmt(c.tx, clusterGroupCreate)

	// Execute the statement.
	result, err := stmt.Exec(args...)
	if err != nil {
		return -1, fmt.Errorf("Failed to create cluster group: %w", err)
	}

	id, err := result.LastInsertId()
	if err != nil {
		return -1, fmt.Errorf("Failed to fetch cluster group ID: %w", err)
	}

	// Insert nodes reference.
	err = addNodesToClusterGroup(c.tx, int(id), object.Nodes)
	if err != nil {
		return -1, fmt.Errorf("Insert nodes for cluster group: %w", err)
	}

	return id, nil
}

// RenameClusterGroup renames the ClusterGroup matching the given key parameters.
// generator: ClusterGroup Rename
func (c *ClusterTx) RenameClusterGroup(name string, to string) error {
	stmt := cluster.Stmt(c.tx, clusterGroupRename)
	result, err := stmt.Exec(to, name)
	if err != nil {
		return fmt.Errorf("Failed to rename cluster group: %w", err)
	}

	n, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("Fetch affected rows: %w", err)
	}

	if n != 1 {
		return fmt.Errorf("Query affected %d rows instead of 1", n)
	}

	return nil
}

// DeleteClusterGroup deletes the ClusterGroup matching the given key parameters.
// generator: ClusterGroup DeleteOne-by-Name
func (c *ClusterTx) DeleteClusterGroup(name string) error {
	stmt := cluster.Stmt(c.tx, clusterGroupDeleteByName)
	result, err := stmt.Exec(name)
	if err != nil {
		return fmt.Errorf("Failed to delete cluster group: %w", err)
	}

	n, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("Fetch affected rows: %w", err)
	}

	if n != 1 {
		return fmt.Errorf("Query deleted %d rows instead of 1", n)
	}

	return nil
}

// UpdateClusterGroup updates the ClusterGroup matching the given key parameters.
// generator: ClusterGroup Update
func (c *ClusterTx) UpdateClusterGroup(name string, object ClusterGroup) error {
	id, err := c.GetClusterGroupID(name)
	if err != nil {
		return fmt.Errorf("Failed to get cluster group: %w", err)
	}

	stmt := cluster.Stmt(c.tx, clusterGroupUpdate)
	result, err := stmt.Exec(object.Name, object.Description, id)
	if err != nil {
		return fmt.Errorf("Failed to update cluster group: %w", err)
	}

	n, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("Fetch affected rows: %w", err)
	}

	if n != 1 {
		return fmt.Errorf("Query updated %d rows instead of 1", n)
	}

	// Delete current nodes.
	stmt = cluster.Stmt(c.tx, clusterGroupDeleteNodesRef)
	_, err = stmt.Exec(id)
	if err != nil {
		return fmt.Errorf("Failed to delete current nodes: %w", err)
	}

	// Insert nodes reference.
	err = addNodesToClusterGroup(c.tx, int(id), object.Nodes)
	if err != nil {
		return fmt.Errorf("Failed to insert nodes for cluster group: %w", err)
	}

	return nil
}

// ClusterGroupToAPI is a convenience to convert a ClusterGroup db struct into
// an API cluster group struct.
func ClusterGroupToAPI(clusterGroup *ClusterGroup, nodes []string) *api.ClusterGroup {
	c := &api.ClusterGroup{
		ClusterGroupPut: api.ClusterGroupPut{
			Description: clusterGroup.Description,
			Members:     nodes,
		},
		ClusterGroupPost: api.ClusterGroupPost{
			Name: clusterGroup.Name,
		},
	}

	return c
}

// GetClusterGroupNodes returns a list of nodes of the given cluster group.
func (c *ClusterTx) GetClusterGroupNodes(groupName string) ([]string, error) {
	q := `SELECT nodes.name FROM nodes_cluster_groups
JOIN nodes ON nodes.id = nodes_cluster_groups.node_id
JOIN cluster_groups ON cluster_groups.id = nodes_cluster_groups.group_id
WHERE cluster_groups.name = ?`

	return query.SelectStrings(c.tx, q, groupName)
}

// GetClusterGroupURIs returns all available ClusterGroup URIs.
// generator: ClusterGroup URIs
func (c *ClusterTx) GetClusterGroupURIs(filter ClusterGroupFilter) ([]string, error) {
	var args []any
	var sql string
	if filter.Name != nil && filter.ID == nil {
		sql = `SELECT cluster_groups.name FROM cluster_groups
WHERE cluster_groups.name = ? ORDER BY cluster_groups.name
`
		args = []any{
			filter.Name,
		}
	} else if filter.ID == nil && filter.Name == nil {
		sql = `SELECT cluster_groups.name FROM cluster_groups ORDER BY cluster_groups.name`
		args = []any{}
	} else {
		return nil, fmt.Errorf("No statement exists for the given Filter")
	}

	names, err := query.SelectStrings(c.tx, sql, args...)
	if err != nil {
		return nil, err
	}

	uris := make([]string, len(names))
	for i, name := range names {
		uris[i] = api.NewURL().Path(version.APIVersion, "cluster", "groups", name).String()
	}

	return uris, nil
}

//AddNodeToClusterGroup adds a given node to the given cluster group.
func (c *ClusterTx) AddNodeToClusterGroup(groupName string, nodeName string) error {
	groupID, err := c.GetClusterGroupID(groupName)
	if err != nil {
		return fmt.Errorf("Failed to get cluster group ID: %w", err)
	}

	nodeInfo, err := c.GetNodeByName(nodeName)
	if err != nil {
		return fmt.Errorf("Failed to get node info: %w", err)
	}

	_, err = c.tx.Exec(`INSERT INTO nodes_cluster_groups (node_id, group_id) VALUES(?, ?);`, nodeInfo.ID, groupID)
	if err != nil {
		return err
	}

	return nil
}

// RemoveNodeFromClusterGroup removes a given node from the given group name.
func (c *ClusterTx) RemoveNodeFromClusterGroup(groupName string, nodeName string) error {
	groupID, err := c.GetClusterGroupID(groupName)
	if err != nil {
		return fmt.Errorf("Failed to get cluster group ID: %w", err)
	}

	nodeInfo, err := c.GetNodeByName(nodeName)
	if err != nil {
		return fmt.Errorf("Failed to get node info: %w", err)
	}

	_, err = c.tx.Exec(`DELETE FROM nodes_cluster_groups WHERE node_id = ? AND group_id = ?`, nodeInfo.ID, groupID)
	if err != nil {
		return err
	}

	return nil
}

//GetClusterGroupsWithNode returns a list of cluster group names the given node belongs to.
func (c *ClusterTx) GetClusterGroupsWithNode(nodeName string) ([]string, error) {
	q := `SELECT cluster_groups.name FROM nodes_cluster_groups
JOIN cluster_groups ON cluster_groups.id = nodes_cluster_groups.group_id
JOIN nodes ON nodes.id = nodes_cluster_groups.node_id
WHERE nodes.name = ?`

	return query.SelectStrings(c.tx, q, nodeName)
}

// ToAPI returns a LXD API entry.
func (c *ClusterGroup) ToAPI() (*api.ClusterGroup, error) {
	result := api.ClusterGroup{
		ClusterGroupPut: api.ClusterGroupPut{
			Description: c.Description,
			Members:     c.Nodes,
		},
		ClusterGroupPost: api.ClusterGroupPost{
			Name: c.Name,
		},
	}

	return &result, nil
}

// addNodesToClusterGroup adds the given nodes the the cluster group with the given ID.
func addNodesToClusterGroup(tx *sql.Tx, id int, nodes []string) error {
	str := `
INSERT INTO nodes_cluster_groups (group_id, node_id)
  VALUES (
    ?,
    (SELECT nodes.id
     FROM nodes
     WHERE nodes.name = ?)
  )`
	stmt, err := tx.Prepare(str)
	if err != nil {
		return err
	}

	defer func() { _ = stmt.Close() }()

	for _, node := range nodes {
		_, err = stmt.Exec(id, node)
		if err != nil {
			logger.Debugf("Error adding node %q to cluster group: %s", node, err)
			return err
		}
	}

	return nil
}
<|endoftext|>


<commit_before>package edward

import (
	"sort"

	"github.com/pkg/errors"
	"github.com/yext/edward/runner"
	"github.com/yext/edward/services"
)

func (c *Client) Log(names []string) error {
	if len(names) == 0 {
		return errors.New("At least one service or group must be specified")
	}
	sgs, err := c.getServicesOrGroups(names)
	if err != nil {
		return errors.WithStack(err)
	}

	var logChannel = make(chan runner.LogLine)
	var lines []runner.LogLine
	for _, sg := range sgs {
		switch v := sg.(type) {
		case *services.ServiceConfig:
			newLines, err := followServiceLog(v, logChannel)
			if err != nil {
				return err
			}
			lines = append(lines, newLines...)
		case *services.ServiceGroupConfig:
			newLines, err := followGroupLog(v, logChannel)
			if err != nil {
				return err
			}
			lines = append(lines, newLines...)
		}
	}

	// Sort initial lines
	sort.Sort(byTime(lines))
	for _, line := range lines {
		printMessage(line, services.CountServices(sgs) > 1)
	}

	for logMessage := range logChannel {
		printMessage(logMessage, services.CountServices(sgs) > 1)
	}

	return nil
}
<commit_msg>Check service status while tailing and exit when all services stopped. #89<commit_after>package edward

import (
	"sort"
	"time"

	"github.com/pkg/errors"
	"github.com/yext/edward/runner"
	"github.com/yext/edward/services"
)

func (c *Client) Log(names []string) error {
	if len(names) == 0 {
		return errors.New("At least one service or group must be specified")
	}
	sgs, err := c.getServicesOrGroups(names)
	if err != nil {
		return errors.WithStack(err)
	}

	var logChannel = make(chan runner.LogLine)
	var lines []runner.LogLine
	for _, sg := range sgs {
		switch v := sg.(type) {
		case *services.ServiceConfig:
			newLines, err := followServiceLog(v, logChannel)
			if err != nil {
				return err
			}
			lines = append(lines, newLines...)
		case *services.ServiceGroupConfig:
			newLines, err := followGroupLog(v, logChannel)
			if err != nil {
				return err
			}
			lines = append(lines, newLines...)
		}
	}

	var stopChannel = make(chan runner.LogLine)
	statusTicker := time.NewTicker(time.Second * 5)
	go func() {
		for _ = range statusTicker.C {
			running, err := checkAllRunning(sgs)
			if err != nil {
				c.Logger.Printf("Error checking service state for tailing: %v", err)
				continue
			}
			// All services stopped, notify the log process
			if !running {
				statusTicker.Stop()
				close(stopChannel)
			}
		}
	}()

	// Sort initial lines
	sort.Sort(byTime(lines))
	for _, line := range lines {
		printMessage(line, services.CountServices(sgs) > 1)
	}

	var running = true
	for running {
		select {
		case logMessage := <-logChannel:
			printMessage(logMessage, services.CountServices(sgs) > 1)
		case <-stopChannel:
			running = false
		}
	}

	return nil
}

func checkAllRunning(sgs []services.ServiceOrGroup) (bool, error) {
	for _, sg := range sgs {
		stats, err := sg.Status()
		if err != nil {
			return false, errors.WithStack(err)
		}
		for _, status := range stats {
			if status.Status != services.StatusStopped {
				return true, nil
			}
		}
	}
	return false, nil
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"time"

	metrics "github.com/rcrowley/go-metrics"
)

// A channel of points and related sampling
type Destination struct {
	Name       string
	points     chan Point
	depthGauge metrics.Gauge
}

func NewDestination(name string, chanCap int) *Destination {
	destination := &Destination{Name: name}
	destination.points = make(chan Point, chanCap)
	destination.depthGauge = metrics.NewRegisteredGauge(
		fmt.Sprintf("lumbermill.points.pending.", name),
		metrics.DefaultRegistry,
	)

	go destination.Sample(10 * time.Second)

	return destination
}

// Update depth guages every so often
func (d *Destination) Sample(every time.Duration) {
	for {
		time.Sleep(every)
		d.depthGauge.Update(int64(len(d.points)))
	}
}

// Post the point, or increment a counter if channel is full
func (d *Destination) PostPoint(point Point) {
	select {
	case d.points <- point:
	default:
		droppedErrorCounter.Inc(1)
	}
}
<commit_msg>no need to fmt<commit_after>package main

import (
	"time"

	metrics "github.com/rcrowley/go-metrics"
)

// A channel of points and related sampling
type Destination struct {
	Name       string
	points     chan Point
	depthGauge metrics.Gauge
}

func NewDestination(name string, chanCap int) *Destination {
	destination := &Destination{Name: name}
	destination.points = make(chan Point, chanCap)
	destination.depthGauge = metrics.NewRegisteredGauge(
		"lumbermill.points.pending."+name,
		metrics.DefaultRegistry,
	)

	go destination.Sample(10 * time.Second)

	return destination
}

// Update depth guages every so often
func (d *Destination) Sample(every time.Duration) {
	for {
		time.Sleep(every)
		d.depthGauge.Update(int64(len(d.points)))
	}
}

// Post the point, or increment a counter if channel is full
func (d *Destination) PostPoint(point Point) {
	select {
	case d.points <- point:
	default:
		droppedErrorCounter.Inc(1)
	}
}
<|endoftext|>


<commit_before>package play

import (
	"container/list"
	"strings"
	"sync"
	"time"

	"github.com/pkg/errors"
)

var (
	// DefaultPlaybackStartTimeout is the default amount of time to wait for a playback to start before declaring that the playback has failed.
	DefaultPlaybackStartTimeout = 2 * time.Second

	// DefaultMaxPlaybackTime is the default maximum amount of time any playback is allowed to run.  If this time is exeeded, the playback will be cancelled.
	DefaultMaxPlaybackTime = 10 * time.Minute

	// DefaultFirstDigitTimeout is the default amount of time to wait, after the playback for all audio completes, for the first digit to be received.
	DefaultFirstDigitTimeout = 4 * time.Second

	// DefaultInterDigitTimeout is the maximum time to wait for additional
	// digits after the first is received.
	DefaultInterDigitTimeout = 3 * time.Second

	// DefaultOverallDigitTimeout is the default maximum time to wait for a
	// response, after the playback for all audio is complete, regardless of the
	// number of received digits or pattern matching.
	DefaultOverallDigitTimeout = 3 * time.Minute

	// DigitBufferSize is the number of digits stored in the received-digit
	// event buffer before further digit events are ignored.  NOTE that digits
	// overflowing this buffer are still stored in the digits received buffer.
	// This only affects the digit _signaling_ buffer.
	DigitBufferSize = 20
)

// Result describes the result of a playback operation
type Result struct {
	mu sync.Mutex

	// Duration indicates how long the playback execution took, from start to finish
	Duration time.Duration

	// DTMF records any DTMF which was received by the playback, as modified by any match functions
	DTMF string

	// Error indicates any error encountered which caused the termination of the playback
	Error error

	// MatchResult indicates the final result of any applied match function for DTMF digits which were received
	MatchResult MatchResult

	// Status indicates the resulting status of the playback, why it was stopped
	Status Status
}

// Status indicates the final status of a playback, be it individual of an entire sequence.  This Status indicates the reason the playback stopped.
type Status int

const (
	// InProgress indicates that the audio is currently playing or is staged to play
	InProgress Status = iota

	// Cancelled indicates that the audio was cancelled.  This cancellation could be due
	// to anything from the control context being closed or a DTMF Match being found
	Cancelled

	// Failed indicates that the audio playback failed.  This indicates that one
	// or more of the audio playbacks failed to be played.  This could be due to
	// a system, network, or Asterisk error, but it could also be due to an
	// invalid audio URI.  Check the returned error for more details.
	Failed

	// Finished indicates that the playback completed playing all bound audio
	// URIs in full.  Note that for a prompt-style execution, this also means
	// that no DTMF was matched to the match function.
	Finished

	// Hangup indicates that the audio playback was interrupted due to a hangup.
	Hangup

	// Timeout indicates that audio playback timed out.  It is not known whether this was due to a failure in the playback, a network loss, or some other problem.
	Timeout
)

// MatchResult indicates the status of a match for the received DTMF of a playback
type MatchResult int

const (
	// Incomplete indicates that there are not enough digits to determine a match
	Incomplete MatchResult = iota

	// Complete indicates that a match was found and the current DTMF pattern is complete
	Complete

	// Invalid indicates that a match cannot befound from the current DTMF received set
	Invalid
)

type uriList struct {
	list    *list.List
	current *list.Element
	mu      sync.Mutex
}

func (u *uriList) Empty() bool {
	if u == nil || u.list == nil || u.list.Len() == 0 {
		return true
	}
	return false
}

func (u *uriList) Add(uri string) {
	u.mu.Lock()
	defer u.mu.Unlock()

	if u.list == nil {
		u.list = list.New()
	}

	u.list.PushBack(uri)

	if u.current == nil {
		u.current = u.list.Front()
	}
}

func (u *uriList) First() string {
	if u.list == nil {
		return ""
	}

	u.mu.Lock()
	defer u.mu.Unlock()

	u.current = u.list.Front()
	return u.val()
}

func (u *uriList) Next() string {
	if u.list == nil {
		return ""
	}

	u.mu.Lock()
	defer u.mu.Unlock()

	if u.current == nil {
		return ""
	}

	u.current = u.current.Next()
	return u.val()
}

func (u *uriList) val() string {
	if u.current == nil {
		return ""
	}

	ret, ok := u.current.Value.(string)
	if !ok {
		return ""
	}
	return ret
}

// Options represent the various playback options which can modify the operation of a Playback.
type Options struct {
	// uriList is the list of audio URIs to play
	uriList *uriList

	// playbackStartTimeout defines the amount of time to wait for a playback to
	// start before declaring it failed.
	//
	// This value is important because ARI does NOT report playback failures in
	// any usable way.
	//
	// If not specified, the default is DefaultPlaybackStartTimeout
	playbackStartTimeout time.Duration

	// maxPlaybackTime is the maximum amount of time to wait for a playback
	// session to complete, everything included.  The playback will be
	// terminated if this time is exceeded.
	maxPlaybackTime time.Duration

	// firstDigitTimeout is the maximum length of time to wait
	// after the prompt sequence ends for the user to enter
	// a response.
	//
	// If not specified, the default is DefaultFirstDigitTimeout.
	firstDigitTimeout time.Duration

	// interDigitTimeout is the maximum length of time to wait
	// for an additional digit after a digit is received.
	//
	// If not specified, the default is DefaultInterDigitTimeout.
	interDigitTimeout time.Duration

	// overallDigitTimeout is the maximum length of time to wait
	// for a response regardless of digits received after the completion
	// of all audio playbacks.
	// If not specified, the default is DefaultOverallTimeout.
	overallDigitTimeout time.Duration

	// matchFunc is an optional function which, if supplied, returns
	// a string and an int.
	//
	// The string is allows the MatchFunc to return a different number
	// to be used as `result.Data`.  This is commonly used for prompts
	// which look for a terminator.  In such a practice, the terminator
	// would be stripped from the match and this argument would be populated
	// with the result.  Otherwise, the original string should be returned.
	// NOTE: Whatever is returned here will become `result.Data`.
	//
	// The int parameter indicates the result of the match, and it should
	// be one of:
	//  Incomplete (0) : insufficient digits to determine match.
	//  Complete (1) : A match was found.
	//  Invalid (2) : A match could not be found, given the digits received.
	// If this function returns a non-zero int, then the prompt will be stopped.
	// If not specified MatchAny will be used.
	matchFunc func(string) (string, MatchResult)

	// maxReplays is the maximum number of times the audio sequence will be
	// replayed if there is no response.  By default, the audio sequence is
	// played only once.
	maxReplays int
}

// NewDefaultOptions returns a set of options which represent reasonable defaults for most simple playbacks.
func NewDefaultOptions() *Options {
	opts := &Options{
		playbackStartTimeout: DefaultPlaybackStartTimeout,
		maxPlaybackTime:      DefaultMaxPlaybackTime,
		uriList:              new(uriList),
	}

	MatchAny()(opts) // nolint  No error is possible with MatchAny

	return opts
}

// ApplyOptions applies a set of OptionFuncs to the Playback
func (o *Options) ApplyOptions(opts ...OptionFunc) (err error) {
	for _, f := range opts {
		err = f(o)
		if err != nil {
			return errors.Wrap(err, "failed to apply option")
		}
	}
	return nil
}

// NewPromptOptions returns a set of options which represent reasonable defaults for most prompt playbacks.  It will terminate when any single DTMF digit is received.
func NewPromptOptions() *Options {
	opts := NewDefaultOptions()

	opts.firstDigitTimeout = DefaultFirstDigitTimeout
	opts.interDigitTimeout = DefaultInterDigitTimeout
	opts.overallDigitTimeout = DefaultOverallDigitTimeout

	return opts
}

// OptionFunc defines an interface for functions which can modify a play session's Options
type OptionFunc func(*Options) error

// NoExitOnDTMF disables exiting the playback when DTMF is received.  Note that
// this is just a wrapper for MatchFunc(nil), so it is mutually exclusive with
// MatchFunc; whichever comes later will win.
func NoExitOnDTMF() OptionFunc {
	return func(o *Options) error {
		o.matchFunc = nil
		return nil
	}
}

// URI adds a set of audio URIs to a playback
func URI(uri ...string) OptionFunc {
	return func(o *Options) error {
		if o.uriList == nil {
			o.uriList = new(uriList)
		}

		for _, u := range uri {
			if u != "" {
				o.uriList.Add(u)
			}
		}
		return nil
	}
}

// PlaybackStartTimeout overrides the default playback start timeout
func PlaybackStartTimeout(timeout time.Duration) OptionFunc {
	return func(o *Options) error {
		o.playbackStartTimeout = timeout
		return nil
	}
}

// DigitTimeouts sets the digit timeouts.  Passing a negative value to any of these indicates that the default value (shown in parentheses below) should be used.
//
//  - First digit timeout (4 sec):  The time (after the stop of the audio) to wait for the first digit to be received
//
//  - Inter digit timeout (3 sec):  The time (after receiving a digit) to wait for the _next_ digit to be received
//
//  - Overall digit timeout (3 min):  The maximum amount of time to wait (after the stop of the audio) for digits to be received, regardless of the digit frequency
//
func DigitTimeouts(first, inter, overall time.Duration) OptionFunc {
	return func(o *Options) error {
		if first >= 0 {
			o.firstDigitTimeout = first
		}
		if inter >= 0 {
			o.interDigitTimeout = inter
		}
		if overall >= 0 {
			o.overallDigitTimeout = overall
		}
		return nil
	}
}

// Replays sets the number of replays of the audio sequence before exiting
func Replays(count int) OptionFunc {
	return func(o *Options) error {
		o.maxReplays = count
		return nil
	}
}

// MatchAny indicates that the playback should be considered Matched and terminated if
// any DTMF digit is received during the playback or post-playback time.
func MatchAny() OptionFunc {
	return func(o *Options) error {
		o.matchFunc = func(pat string) (string, MatchResult) {
			if len(pat) > 0 {
				return pat, Complete
			}
			return pat, Incomplete
		}
		return nil
	}
}

// MatchHash indicates that the playback should be considered Matched and terminated if it contains a hash (#).  The hash (and any subsequent digits) is removed from the final result.
func MatchHash() OptionFunc {
	return func(o *Options) error {
		o.matchFunc = func(pat string) (string, MatchResult) {
			if strings.Contains(pat, "#") {
				return strings.Split(pat, "#")[0], Complete
			}
			return pat, Incomplete
		}
		return nil
	}
}

// MatchTerminator indicates that the playback shoiuld be considered Matched and terminated if it contains the provided Terminator string.  The terminator (and any subsequent digits) is removed from the final result.
func MatchTerminator(terminator string) OptionFunc {
	return func(o *Options) error {
		o.matchFunc = func(pat string) (string, MatchResult) {
			if strings.Contains(pat, terminator) {
				return strings.Split(pat, terminator)[0], Complete
			}
			return pat, Incomplete
		}
		return nil
	}
}

// MatchLen indicates that the playback should be considered Matched and terminated if the given number of DTMF digits are receieved.
func MatchLen(length int) OptionFunc {
	return func(o *Options) error {
		o.matchFunc = func(pat string) (string, MatchResult) {
			if len(pat) >= length {
				return pat, Complete
			}
			return pat, Incomplete
		}
		return nil
	}
}

// MatchLenOrTerminator indicates that the playback should be considered Matched and terminated if the given number of DTMF digits are receieved or if the given terminator is received.  If the terminator is present, it and any subsequent digits will be removed from the final result.
func MatchLenOrTerminator(length int, terminator string) OptionFunc {
	return func(o *Options) error {
		o.matchFunc = func(pat string) (string, MatchResult) {
			if len(pat) >= length {
				return pat, Complete
			}
			if strings.Contains(pat, terminator) {
				return strings.Split(pat, terminator)[0], Complete
			}
			return pat, Incomplete
		}
		return nil
	}
}

// MatchFunc uses the provided match function to determine when the playback should be terminated based on DTMF input.
func MatchFunc(f func(string) (string, MatchResult)) OptionFunc {
	return func(o *Options) error {
		o.matchFunc = f
		return nil
	}
}
<commit_msg>add MatchDiscrete<commit_after>package play

import (
	"container/list"
	"strings"
	"sync"
	"time"

	"github.com/pkg/errors"
)

var (
	// DefaultPlaybackStartTimeout is the default amount of time to wait for a playback to start before declaring that the playback has failed.
	DefaultPlaybackStartTimeout = 2 * time.Second

	// DefaultMaxPlaybackTime is the default maximum amount of time any playback is allowed to run.  If this time is exeeded, the playback will be cancelled.
	DefaultMaxPlaybackTime = 10 * time.Minute

	// DefaultFirstDigitTimeout is the default amount of time to wait, after the playback for all audio completes, for the first digit to be received.
	DefaultFirstDigitTimeout = 4 * time.Second

	// DefaultInterDigitTimeout is the maximum time to wait for additional
	// digits after the first is received.
	DefaultInterDigitTimeout = 3 * time.Second

	// DefaultOverallDigitTimeout is the default maximum time to wait for a
	// response, after the playback for all audio is complete, regardless of the
	// number of received digits or pattern matching.
	DefaultOverallDigitTimeout = 3 * time.Minute

	// DigitBufferSize is the number of digits stored in the received-digit
	// event buffer before further digit events are ignored.  NOTE that digits
	// overflowing this buffer are still stored in the digits received buffer.
	// This only affects the digit _signaling_ buffer.
	DigitBufferSize = 20
)

// Result describes the result of a playback operation
type Result struct {
	mu sync.Mutex

	// Duration indicates how long the playback execution took, from start to finish
	Duration time.Duration

	// DTMF records any DTMF which was received by the playback, as modified by any match functions
	DTMF string

	// Error indicates any error encountered which caused the termination of the playback
	Error error

	// MatchResult indicates the final result of any applied match function for DTMF digits which were received
	MatchResult MatchResult

	// Status indicates the resulting status of the playback, why it was stopped
	Status Status
}

// Status indicates the final status of a playback, be it individual of an entire sequence.  This Status indicates the reason the playback stopped.
type Status int

const (
	// InProgress indicates that the audio is currently playing or is staged to play
	InProgress Status = iota

	// Cancelled indicates that the audio was cancelled.  This cancellation could be due
	// to anything from the control context being closed or a DTMF Match being found
	Cancelled

	// Failed indicates that the audio playback failed.  This indicates that one
	// or more of the audio playbacks failed to be played.  This could be due to
	// a system, network, or Asterisk error, but it could also be due to an
	// invalid audio URI.  Check the returned error for more details.
	Failed

	// Finished indicates that the playback completed playing all bound audio
	// URIs in full.  Note that for a prompt-style execution, this also means
	// that no DTMF was matched to the match function.
	Finished

	// Hangup indicates that the audio playback was interrupted due to a hangup.
	Hangup

	// Timeout indicates that audio playback timed out.  It is not known whether this was due to a failure in the playback, a network loss, or some other problem.
	Timeout
)

// MatchResult indicates the status of a match for the received DTMF of a playback
type MatchResult int

const (
	// Incomplete indicates that there are not enough digits to determine a match
	Incomplete MatchResult = iota

	// Complete indicates that a match was found and the current DTMF pattern is complete
	Complete

	// Invalid indicates that a match cannot befound from the current DTMF received set
	Invalid
)

type uriList struct {
	list    *list.List
	current *list.Element
	mu      sync.Mutex
}

func (u *uriList) Empty() bool {
	if u == nil || u.list == nil || u.list.Len() == 0 {
		return true
	}
	return false
}

func (u *uriList) Add(uri string) {
	u.mu.Lock()
	defer u.mu.Unlock()

	if u.list == nil {
		u.list = list.New()
	}

	u.list.PushBack(uri)

	if u.current == nil {
		u.current = u.list.Front()
	}
}

func (u *uriList) First() string {
	if u.list == nil {
		return ""
	}

	u.mu.Lock()
	defer u.mu.Unlock()

	u.current = u.list.Front()
	return u.val()
}

func (u *uriList) Next() string {
	if u.list == nil {
		return ""
	}

	u.mu.Lock()
	defer u.mu.Unlock()

	if u.current == nil {
		return ""
	}

	u.current = u.current.Next()
	return u.val()
}

func (u *uriList) val() string {
	if u.current == nil {
		return ""
	}

	ret, ok := u.current.Value.(string)
	if !ok {
		return ""
	}
	return ret
}

// Options represent the various playback options which can modify the operation of a Playback.
type Options struct {
	// uriList is the list of audio URIs to play
	uriList *uriList

	// playbackStartTimeout defines the amount of time to wait for a playback to
	// start before declaring it failed.
	//
	// This value is important because ARI does NOT report playback failures in
	// any usable way.
	//
	// If not specified, the default is DefaultPlaybackStartTimeout
	playbackStartTimeout time.Duration

	// maxPlaybackTime is the maximum amount of time to wait for a playback
	// session to complete, everything included.  The playback will be
	// terminated if this time is exceeded.
	maxPlaybackTime time.Duration

	// firstDigitTimeout is the maximum length of time to wait
	// after the prompt sequence ends for the user to enter
	// a response.
	//
	// If not specified, the default is DefaultFirstDigitTimeout.
	firstDigitTimeout time.Duration

	// interDigitTimeout is the maximum length of time to wait
	// for an additional digit after a digit is received.
	//
	// If not specified, the default is DefaultInterDigitTimeout.
	interDigitTimeout time.Duration

	// overallDigitTimeout is the maximum length of time to wait
	// for a response regardless of digits received after the completion
	// of all audio playbacks.
	// If not specified, the default is DefaultOverallTimeout.
	overallDigitTimeout time.Duration

	// matchFunc is an optional function which, if supplied, returns
	// a string and an int.
	//
	// The string is allows the MatchFunc to return a different number
	// to be used as `result.Data`.  This is commonly used for prompts
	// which look for a terminator.  In such a practice, the terminator
	// would be stripped from the match and this argument would be populated
	// with the result.  Otherwise, the original string should be returned.
	// NOTE: Whatever is returned here will become `result.Data`.
	//
	// The int parameter indicates the result of the match, and it should
	// be one of:
	//  Incomplete (0) : insufficient digits to determine match.
	//  Complete (1) : A match was found.
	//  Invalid (2) : A match could not be found, given the digits received.
	// If this function returns a non-zero int, then the prompt will be stopped.
	// If not specified MatchAny will be used.
	matchFunc func(string) (string, MatchResult)

	// maxReplays is the maximum number of times the audio sequence will be
	// replayed if there is no response.  By default, the audio sequence is
	// played only once.
	maxReplays int
}

// NewDefaultOptions returns a set of options which represent reasonable defaults for most simple playbacks.
func NewDefaultOptions() *Options {
	opts := &Options{
		playbackStartTimeout: DefaultPlaybackStartTimeout,
		maxPlaybackTime:      DefaultMaxPlaybackTime,
		uriList:              new(uriList),
	}

	MatchAny()(opts) // nolint  No error is possible with MatchAny

	return opts
}

// ApplyOptions applies a set of OptionFuncs to the Playback
func (o *Options) ApplyOptions(opts ...OptionFunc) (err error) {
	for _, f := range opts {
		err = f(o)
		if err != nil {
			return errors.Wrap(err, "failed to apply option")
		}
	}
	return nil
}

// NewPromptOptions returns a set of options which represent reasonable defaults for most prompt playbacks.  It will terminate when any single DTMF digit is received.
func NewPromptOptions() *Options {
	opts := NewDefaultOptions()

	opts.firstDigitTimeout = DefaultFirstDigitTimeout
	opts.interDigitTimeout = DefaultInterDigitTimeout
	opts.overallDigitTimeout = DefaultOverallDigitTimeout

	return opts
}

// OptionFunc defines an interface for functions which can modify a play session's Options
type OptionFunc func(*Options) error

// NoExitOnDTMF disables exiting the playback when DTMF is received.  Note that
// this is just a wrapper for MatchFunc(nil), so it is mutually exclusive with
// MatchFunc; whichever comes later will win.
func NoExitOnDTMF() OptionFunc {
	return func(o *Options) error {
		o.matchFunc = nil
		return nil
	}
}

// URI adds a set of audio URIs to a playback
func URI(uri ...string) OptionFunc {
	return func(o *Options) error {
		if o.uriList == nil {
			o.uriList = new(uriList)
		}

		for _, u := range uri {
			if u != "" {
				o.uriList.Add(u)
			}
		}
		return nil
	}
}

// PlaybackStartTimeout overrides the default playback start timeout
func PlaybackStartTimeout(timeout time.Duration) OptionFunc {
	return func(o *Options) error {
		o.playbackStartTimeout = timeout
		return nil
	}
}

// DigitTimeouts sets the digit timeouts.  Passing a negative value to any of these indicates that the default value (shown in parentheses below) should be used.
//
//  - First digit timeout (4 sec):  The time (after the stop of the audio) to wait for the first digit to be received
//
//  - Inter digit timeout (3 sec):  The time (after receiving a digit) to wait for the _next_ digit to be received
//
//  - Overall digit timeout (3 min):  The maximum amount of time to wait (after the stop of the audio) for digits to be received, regardless of the digit frequency
//
func DigitTimeouts(first, inter, overall time.Duration) OptionFunc {
	return func(o *Options) error {
		if first >= 0 {
			o.firstDigitTimeout = first
		}
		if inter >= 0 {
			o.interDigitTimeout = inter
		}
		if overall >= 0 {
			o.overallDigitTimeout = overall
		}
		return nil
	}
}

// Replays sets the number of replays of the audio sequence before exiting
func Replays(count int) OptionFunc {
	return func(o *Options) error {
		o.maxReplays = count
		return nil
	}
}

// MatchAny indicates that the playback should be considered Matched and terminated if
// any DTMF digit is received during the playback or post-playback time.
func MatchAny() OptionFunc {
	return func(o *Options) error {
		o.matchFunc = func(pat string) (string, MatchResult) {
			if len(pat) > 0 {
				return pat, Complete
			}
			return pat, Incomplete
		}
		return nil
	}
}

// MatchDiscrete indicates that the playback should be considered Matched and terminated if
// the received DTMF digits match any of the discrete list of strings.
func MatchDiscrete(list []string) OptionFunc {
	return func(o *Options) error {
		o.matchFunc = func(pat string) (string, MatchResult) {
			var maxLen int
			for _, t := range list {
				if t == pat {
					return pat, Complete
				}
				if len(t) > maxLen {
					maxLen = len(t)
				}
			}
			if len(pat) > maxLen {
				return pat, Invalid
			}
			return pat, Incomplete
		}
		return nil
	}
}

// MatchHash indicates that the playback should be considered Matched and terminated if it contains a hash (#).  The hash (and any subsequent digits) is removed from the final result.
func MatchHash() OptionFunc {
	return func(o *Options) error {
		o.matchFunc = func(pat string) (string, MatchResult) {
			if strings.Contains(pat, "#") {
				return strings.Split(pat, "#")[0], Complete
			}
			return pat, Incomplete
		}
		return nil
	}
}

// MatchTerminator indicates that the playback shoiuld be considered Matched and terminated if it contains the provided Terminator string.  The terminator (and any subsequent digits) is removed from the final result.
func MatchTerminator(terminator string) OptionFunc {
	return func(o *Options) error {
		o.matchFunc = func(pat string) (string, MatchResult) {
			if strings.Contains(pat, terminator) {
				return strings.Split(pat, terminator)[0], Complete
			}
			return pat, Incomplete
		}
		return nil
	}
}

// MatchLen indicates that the playback should be considered Matched and terminated if the given number of DTMF digits are receieved.
func MatchLen(length int) OptionFunc {
	return func(o *Options) error {
		o.matchFunc = func(pat string) (string, MatchResult) {
			if len(pat) >= length {
				return pat, Complete
			}
			return pat, Incomplete
		}
		return nil
	}
}

// MatchLenOrTerminator indicates that the playback should be considered Matched and terminated if the given number of DTMF digits are receieved or if the given terminator is received.  If the terminator is present, it and any subsequent digits will be removed from the final result.
func MatchLenOrTerminator(length int, terminator string) OptionFunc {
	return func(o *Options) error {
		o.matchFunc = func(pat string) (string, MatchResult) {
			if len(pat) >= length {
				return pat, Complete
			}
			if strings.Contains(pat, terminator) {
				return strings.Split(pat, terminator)[0], Complete
			}
			return pat, Incomplete
		}
		return nil
	}
}

// MatchFunc uses the provided match function to determine when the playback should be terminated based on DTMF input.
func MatchFunc(f func(string) (string, MatchResult)) OptionFunc {
	return func(o *Options) error {
		o.matchFunc = f
		return nil
	}
}
<|endoftext|>


<commit_before>package hdhomerun

import (
	"bytes"
	"encoding/hex"
	"fmt"
	"net"
	"reflect"
	"testing"
	"time"
)

type testConnection struct {
	*IOConnection
}

func newTestConnection() *testConnection {
	return &testConnection{
		IOConnection: NewIOConnection(&bytes.Buffer{}),
	}
}

func newTestDevice() *Device {
	return &Device{
		id:         []byte{0x01, 0x02, 0x03, 0x04},
		Connection: newTestConnection(),
	}

}

func TestDefaultAddr(t *testing.T) {
	d := newTestDevice()
	if d.Addr() != nil {
		t.Errorf("Expected nil addr but got %v", d.Addr())
	}
}

func TestGetSet(t *testing.T) {
	tests := []struct {
		name          string
		value         string
		reply         *Packet
		expectedValue TagValue
		expectedErr   reflect.Type
	}{
		{
			name:          "help",
			reply:         getRpy.p,
			expectedValue: getRpy.p.Tags[TagGetSetValue].Value,
		}, {
			name:          "/tuner0/channel",
			value:         "auto:849000000",
			reply:         setRpy.p,
			expectedValue: setRpy.p.Tags[TagGetSetValue].Value,
		}, {
			name:          "help",
			reply:         discoverRpy.p,
			expectedValue: setRpy.p.Tags[TagGetSetValue].Value,
			expectedErr:   reflect.TypeOf(ErrWrongPacketType("")),
		}, {
			name:        "help",
			reply:       getRpyErr.p,
			expectedErr: reflect.TypeOf(ErrRemoteError("")),
		},
	}

	for _, test := range tests {
		d := newTestDevice()
		d.Send(test.reply)

		var value TagValue
		var err error
		if test.value == "" {
			value, err = d.Get(test.name)
		} else {
			value, err = d.Set(test.name, test.value)
		}

		if reflect.TypeOf(err) != test.expectedErr {
			t.Errorf("Expected error %v but got %v", test.expectedErr, reflect.TypeOf(err))
		}

		if err != nil {
			continue
		}

		if !reflect.DeepEqual(value, test.expectedValue) {
			t.Errorf("Expected return value of %s but got %s", test.expectedValue, value)
		}

		err = d.Close()
		if err != nil {
			t.Errorf("Expected no error but got %v", err)
		}
	}
}

func TestDiscover(t *testing.T) {
	tests := []struct {
		reply   testPacket
		devices []string
		err     reflect.Type
	}{
		{
			reply:   discoverRpy,
			devices: []string{hex.EncodeToString(discoverRpy.p.Tags[TagDeviceId].Value)},
		}, {
			reply:   discoverReq,
			devices: []string{},
		}, {
			reply: testPacket{
				p: nil,
				b: []byte{
					0x00,
				},
			},
			devices: []string{},
			err:     reflect.TypeOf(fmt.Errorf("")),
		},
	}

	for _, test := range tests {
		listener, _ := net.ListenUDP("udp", &net.UDPAddr{IP: net.IP{127, 0, 0, 1}, Port: 65001})
		go func() {
			listener.SetReadDeadline(time.Now().Add(time.Second))
			_, addr, _ := listener.ReadFromUDP(make([]byte, 1024))
			listener.WriteTo(test.reply.b, addr)
			listener.Close()
		}()

		devices := make([]string, 0)
		for result := range Discover(net.IP{127, 0, 0, 1}, time.Second) {
			if reflect.TypeOf(result.Err) != test.err {
				t.Errorf("Expected error type %v but got %v(%v)", test.err, reflect.TypeOf(result.Err), result.Err)
			}

			if result.Err != nil {
				continue
			}
			devices = append(devices, result.Device.ID())

			if result.Device.Addr().String() != "127.0.0.1:65001" {
				t.Errorf("Expected adress 127.0.0.1:65001 but got %s", result.Device.Addr().String())
			}
		}

		if !reflect.DeepEqual(devices, test.devices) {
			t.Errorf("Expected devices %v but got %v", test.devices, devices)
		}
	}
}
<commit_msg>Device no longer stores ID<commit_after>package hdhomerun

import (
	"bytes"
	"encoding/hex"
	"fmt"
	"net"
	"reflect"
	"testing"
	"time"
)

type testConnection struct {
	*IOConnection
}

func newTestConnection() *testConnection {
	return &testConnection{
		IOConnection: NewIOConnection(&bytes.Buffer{}),
	}
}

func newTestDevice() *Device {
	return &Device{
		Connection: newTestConnection(),
	}

}

func TestDefaultAddr(t *testing.T) {
	d := newTestDevice()
	if d.Addr() != nil {
		t.Errorf("Expected nil addr but got %v", d.Addr())
	}
}

func TestGetSet(t *testing.T) {
	tests := []struct {
		name          string
		value         string
		reply         *Packet
		expectedValue TagValue
		expectedErr   reflect.Type
	}{
		{
			name:          "help",
			reply:         getRpy.p,
			expectedValue: getRpy.p.Tags[TagGetSetValue].Value,
		}, {
			name:          "/tuner0/channel",
			value:         "auto:849000000",
			reply:         setRpy.p,
			expectedValue: setRpy.p.Tags[TagGetSetValue].Value,
		}, {
			name:          "help",
			reply:         discoverRpy.p,
			expectedValue: setRpy.p.Tags[TagGetSetValue].Value,
			expectedErr:   reflect.TypeOf(ErrWrongPacketType("")),
		}, {
			name:        "help",
			reply:       getRpyErr.p,
			expectedErr: reflect.TypeOf(ErrRemoteError("")),
		},
	}

	for _, test := range tests {
		d := newTestDevice()
		d.Send(test.reply)

		var value TagValue
		var err error
		if test.value == "" {
			value, err = d.Get(test.name)
		} else {
			value, err = d.Set(test.name, test.value)
		}

		if reflect.TypeOf(err) != test.expectedErr {
			t.Errorf("Expected error %v but got %v", test.expectedErr, reflect.TypeOf(err))
		}

		if err != nil {
			continue
		}

		if !reflect.DeepEqual(value, test.expectedValue) {
			t.Errorf("Expected return value of %s but got %s", test.expectedValue, value)
		}

		err = d.Close()
		if err != nil {
			t.Errorf("Expected no error but got %v", err)
		}
	}
}

func TestDiscover(t *testing.T) {
	tests := []struct {
		reply   testPacket
		devices []string
		err     reflect.Type
	}{
		{
			reply:   discoverRpy,
			devices: []string{hex.EncodeToString(discoverRpy.p.Tags[TagDeviceId].Value)},
		}, {
			reply:   discoverReq,
			devices: []string{},
		}, {
			reply: testPacket{
				p: nil,
				b: []byte{
					0x00,
				},
			},
			devices: []string{},
			err:     reflect.TypeOf(fmt.Errorf("")),
		},
	}

	for _, test := range tests {
		listener, _ := net.ListenUDP("udp", &net.UDPAddr{IP: net.IP{127, 0, 0, 1}, Port: 65001})
		go func() {
			listener.SetReadDeadline(time.Now().Add(time.Second))
			_, addr, _ := listener.ReadFromUDP(make([]byte, 1024))
			listener.WriteTo(test.reply.b, addr)
			listener.Close()
		}()

		devices := make([]string, 0)
		for result := range Discover(net.IP{127, 0, 0, 1}, time.Second) {
			if reflect.TypeOf(result.Err) != test.err {
				t.Errorf("Expected error type %v but got %v(%v)", test.err, reflect.TypeOf(result.Err), result.Err)
			}

			if result.Err != nil {
				continue
			}
			devices = append(devices, result.ID.String())

			if result.Device.Addr().String() != "127.0.0.1:65001" {
				t.Errorf("Expected adress 127.0.0.1:65001 but got %s", result.Device.Addr().String())
			}
		}

		if !reflect.DeepEqual(devices, test.devices) {
			t.Errorf("Expected devices %v but got %v", test.devices, devices)
		}
	}
}
<|endoftext|>


<commit_before>package main

import (
	"bufio"
	"bytes"
	"fmt"
	"log"
	"os"
	"os/exec"
	"strings"

	"github.com/dynport/dgtk/cli"
)

type Commits struct {
}

func (c *Commits) Run() error {
	theUrl, e := githubUrl()
	if e != nil {
		return e
	}
	return openUrl(theUrl + "/commits/master")
}

func githubRepo() (string, error) {
	out, e := exec.Command("git", "remote", "-v").CombinedOutput()
	if e != nil {
		if strings.Contains(string(out), "Not a git repository") {
			return "", nil
		}
		return "", fmt.Errorf("%s: %s:", e, string(out))
	}
	for scanner := bufio.NewScanner(bytes.NewReader(out)); ; scanner.Scan() {
		fields := strings.Fields(scanner.Text())
		if len(fields) > 1 && strings.HasPrefix(fields[1], "git@github.com:") {
			repo := fields[1]
			parts := strings.Split(repo, ":")
			return strings.TrimSuffix(parts[1], ".git"), nil
		}
	}
	return "", e

}

func githubUrl() (string, error) {
	out, e := exec.Command("git", "remote", "-v").CombinedOutput()
	if e != nil {
		return "", e
	}
	for scanner := bufio.NewScanner(bytes.NewReader(out)); ; scanner.Scan() {
		fields := strings.Fields(scanner.Text())
		if len(fields) > 1 && strings.HasPrefix(fields[1], "git@github.com:") {
			repo := fields[1]
			parts := strings.Split(repo, ":")
			if len(parts) > 1 {
				return "https://github.com/" + strings.TrimSuffix(parts[1], ".git"), nil
			}
		}
	}
	return "", fmt.Errorf("error getting github url from %s (I only know about 'git@github.com/' remotes for now", string(out))
}

type Browse struct {
}

func (o *Browse) Run() error {
	theUrl, e := githubUrl()
	if e != nil {
		return e
	}
	return openUrl(theUrl)
}

func openGithubUrl(suffix string) error {
	u, e := githubUrl()
	if e != nil {
		return e
	}
	u += "/" + strings.TrimPrefix(suffix, "/")
	return openUrl(u)
}

func openUrl(theUrl string) error {
	logger.Printf("opening %q", theUrl)
	c := exec.Command("open", theUrl)
	c.Stdout = os.Stdout
	c.Stderr = os.Stderr
	c.Stdin = nil
	return c.Run()
}

var router = cli.NewRouter()

type GithubNotifications struct {
}

func (g *GithubNotifications) Run() error {
	return openUrl("https://github.com/notifications")
}

type GithubPulls struct {
}

func (g *GithubPulls) Run() error {
	u, e := githubUrl()
	if e != nil {
		return e
	}
	return openUrl(u + "/pulls")
}

func main() {
	log.SetFlags(0)
	router.Register("browse", &Browse{}, "Browse github repository")
	router.Register("commits", &Commits{}, "List github commits")
	router.Register("gists/browse", &BrowseGists{}, "Browse Gists")
	router.Register("gists/create", &CreateGist{}, "Create a new")
	router.Register("gists/delete", &DeleteGist{}, "Create a new")
	router.Register("gists/list", &ListGists{}, "List Gists")
	router.Register("gists/open", &OpenGist{}, "Open a Gist")
	router.Register("issues/list", &issuesList{}, "List github issues")
	router.Register("issues/browse", &issuesBrowse{}, "List github issues")
	router.Register("issues/create", &issuesCreate{}, "List github issues")
	router.Register("issues/open", &issueOpen{}, "Open github issues")
	router.Register("issues/tag", &issueTag{}, "Tag issue")
	router.Register("issues/close", &issueClose{}, "Close github issues")
	router.Register("issues/assign", &issueAssign{}, "Assign gitbub issue")
	router.Register("notifications", &GithubNotifications{}, "Browse github notifications")
	router.Register("pulls", &GithubPulls{}, "List github pull requests")
	e := router.RunWithArgs()
	switch e {
	case nil, cli.ErrorHelpRequested, cli.ErrorNoRoute:
		// ignore
	default:
		log.Fatal(e.Error())
	}
}
<commit_msg>fix getting github repo<commit_after>package main

import (
	"bufio"
	"bytes"
	"fmt"
	"log"
	"os"
	"os/exec"
	"strings"

	"github.com/dynport/dgtk/cli"
)

type Commits struct {
}

func (c *Commits) Run() error {
	theUrl, e := githubUrl()
	if e != nil {
		return e
	}
	return openUrl(theUrl + "/commits/master")
}

func githubRepo() (string, error) {
	out, e := exec.Command("git", "remote", "-v").CombinedOutput()
	if e != nil {
		if strings.Contains(string(out), "Not a git repository") {
			return "", nil
		}
		return "", fmt.Errorf("%s: %s:", e, string(out))
	}
	scanner := bufio.NewScanner(bytes.NewReader(out))
	for scanner.Scan() {
		fields := strings.Fields(scanner.Text())
		if len(fields) > 1 && strings.HasPrefix(fields[1], "git@github.com:") {
			repo := fields[1]
			parts := strings.Split(repo, ":")
			return strings.TrimSuffix(parts[1], ".git"), nil
		}
	}
	return "", e

}

func githubUrl() (string, error) {
	out, e := exec.Command("git", "remote", "-v").CombinedOutput()
	if e != nil {
		return "", e
	}
	for scanner := bufio.NewScanner(bytes.NewReader(out)); ; scanner.Scan() {
		fields := strings.Fields(scanner.Text())
		if len(fields) > 1 && strings.HasPrefix(fields[1], "git@github.com:") {
			repo := fields[1]
			parts := strings.Split(repo, ":")
			if len(parts) > 1 {
				return "https://github.com/" + strings.TrimSuffix(parts[1], ".git"), nil
			}
		}
	}
	return "", fmt.Errorf("error getting github url from %s (I only know about 'git@github.com/' remotes for now", string(out))
}

type Browse struct {
}

func (o *Browse) Run() error {
	theUrl, e := githubUrl()
	if e != nil {
		return e
	}
	return openUrl(theUrl)
}

func openGithubUrl(suffix string) error {
	u, e := githubUrl()
	if e != nil {
		return e
	}
	u += "/" + strings.TrimPrefix(suffix, "/")
	return openUrl(u)
}

func openUrl(theUrl string) error {
	logger.Printf("opening %q", theUrl)
	c := exec.Command("open", theUrl)
	c.Stdout = os.Stdout
	c.Stderr = os.Stderr
	c.Stdin = nil
	return c.Run()
}

var router = cli.NewRouter()

type GithubNotifications struct {
}

func (g *GithubNotifications) Run() error {
	return openUrl("https://github.com/notifications")
}

type GithubPulls struct {
}

func (g *GithubPulls) Run() error {
	u, e := githubUrl()
	if e != nil {
		return e
	}
	return openUrl(u + "/pulls")
}

func main() {
	log.SetFlags(0)
	router.Register("browse", &Browse{}, "Browse github repository")
	router.Register("commits", &Commits{}, "List github commits")
	router.Register("gists/browse", &BrowseGists{}, "Browse Gists")
	router.Register("gists/create", &CreateGist{}, "Create a new")
	router.Register("gists/delete", &DeleteGist{}, "Create a new")
	router.Register("gists/list", &ListGists{}, "List Gists")
	router.Register("gists/open", &OpenGist{}, "Open a Gist")
	router.Register("issues/list", &issuesList{}, "List github issues")
	router.Register("issues/browse", &issuesBrowse{}, "List github issues")
	router.Register("issues/create", &issuesCreate{}, "List github issues")
	router.Register("issues/open", &issueOpen{}, "Open github issues")
	router.Register("issues/tag", &issueTag{}, "Tag issue")
	router.Register("issues/close", &issueClose{}, "Close github issues")
	router.Register("issues/assign", &issueAssign{}, "Assign gitbub issue")
	router.Register("notifications", &GithubNotifications{}, "Browse github notifications")
	router.Register("pulls", &GithubPulls{}, "List github pull requests")
	e := router.RunWithArgs()
	switch e {
	case nil, cli.ErrorHelpRequested, cli.ErrorNoRoute:
		// ignore
	default:
		log.Fatal(e.Error())
	}
}
<|endoftext|>


<commit_before>// Copyright 2017 Pilosa Corp.
//
// 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 pilosa

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"strconv"
	"strings"
	"sync"
	"time"
)

// Default version check URL.
const (
	defaultVersionCheckURL = "https://diagnostics.pilosa.com/v0/version"
)

type versionResponse struct {
	Version string `json:"version"`
	Message string `json:"message"`
}

// DiagnosticsCollector represents a collector/sender of diagnostics data.
type DiagnosticsCollector struct {
	mu          sync.Mutex
	host        string
	VersionURL  string
	version     string
	lastVersion string
	startTime   int64
	start       time.Time

	metrics map[string]interface{}

	client *http.Client

	logOutput io.Writer

	server *Server
}

// NewDiagnosticsCollector returns a new DiagnosticsCollector given an addr in the format "hostname:port".
func NewDiagnosticsCollector(host string) *DiagnosticsCollector {
	return &DiagnosticsCollector{
		host:       host,
		VersionURL: defaultVersionCheckURL,
		startTime:  time.Now().Unix(),
		start:      time.Now(),
		client:     &http.Client{Timeout: 10 * time.Second},
		metrics:    make(map[string]interface{}),
		logOutput:  ioutil.Discard,
	}
}

// SetVersion of locally running Pilosa Cluster to check against master.
func (d *DiagnosticsCollector) SetVersion(v string) {
	d.version = v
	d.Set("Version", v)
}

// Flush sends the current metrics.
func (d *DiagnosticsCollector) Flush() error {
	d.mu.Lock()
	defer d.mu.Unlock()
	d.metrics["Uptime"] = (time.Now().Unix() - d.startTime)
	buf, err := d.encode()
	if err != nil {
		return err
	}
	req, err := http.NewRequest("POST", d.host, bytes.NewReader(buf))
	req.Header.Set("Content-Type", "application/json")
	resp, err := d.client.Do(req)
	if err != nil {
		return err
	}
	// Intentionally ignoring response body, as user does not need to be notified of error.
	defer resp.Body.Close()
	return nil
}

// CheckVersion of the local build against Pilosa master.
func (d *DiagnosticsCollector) CheckVersion() error {
	var rsp versionResponse
	req, err := http.NewRequest("GET", d.VersionURL, nil)
	resp, err := d.client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("http: status=%d", resp.StatusCode)
	} else if err := json.NewDecoder(resp.Body).Decode(&rsp); err != nil {
		return fmt.Errorf("json decode: %s", err)
	}

	// If version has not changed since the last check, return
	if rsp.Version == d.lastVersion {
		return nil
	}

	d.lastVersion = rsp.Version
	if err := d.compareVersion(rsp.Version); err != nil {
		d.logger().Printf("%s\n", err.Error())
	}

	return nil
}

// compareVersion check version strings.
func (d *DiagnosticsCollector) compareVersion(value string) error {
	currentVersion := versionSegments(value)
	localVersion := versionSegments(d.version)

	if localVersion[0] < currentVersion[0] { //Major
		return fmt.Errorf("Warning: You are running Pilosa %s. A newer version (%s) is available: https://github.com/pilosa/pilosa/releases", d.version, value)
	} else if localVersion[1] < currentVersion[1] && localVersion[0] == currentVersion[0] { // Minor
		return fmt.Errorf("Warning: You are running Pilosa %s. The latest Minor release is %s: https://github.com/pilosa/pilosa/releases", d.version, value)
	} else if localVersion[2] < currentVersion[2] && localVersion[0] == currentVersion[0] && localVersion[1] == currentVersion[1] { // Patch
		return fmt.Errorf("There is a new patch release of Pilosa available: %s: https://github.com/pilosa/pilosa/releases", value)
	}

	return nil
}

// Encode metrics maps into the json message format.
func (d *DiagnosticsCollector) encode() ([]byte, error) {
	return json.Marshal(d.metrics)
}

// Set adds a key value metric.
func (d *DiagnosticsCollector) Set(name string, value interface{}) {
	d.mu.Lock()
	defer d.mu.Unlock()
	d.metrics[name] = value
}

// SetLogger Set the logger output type.
func (d *DiagnosticsCollector) SetLogger(logger io.Writer) {
	d.logOutput = logger
}

// logger returns a logger that writes to LogOutput.
func (d *DiagnosticsCollector) logger() *log.Logger {
	return log.New(d.logOutput, "", log.LstdFlags)
}

// logErr logs the error and returns true if an error exists
func (d *DiagnosticsCollector) logErr(err error) bool {
	if err != nil {
		d.logOutput.Write([]byte(err.Error()))
		return true
	}
	return false
}

// EnrichWithOSInfo adds OS information to the diagnostics payload.
func (d *DiagnosticsCollector) EnrichWithOSInfo() {
	uptime, err := d.server.SystemInfo.Uptime()
	if !d.logErr(err) {
		d.Set("HostUptime", uptime)
	}
	platform, err := d.server.SystemInfo.Platform()
	if !d.logErr(err) {
		d.Set("OSPlatform", platform)
	}
	family, err := d.server.SystemInfo.Family()
	if !d.logErr(err) {
		d.Set("OSFamily", family)
	}
	version, err := d.server.SystemInfo.OSVersion()
	if !d.logErr(err) {
		d.Set("OSVersion", version)
	}
	kernelVersion, err := d.server.SystemInfo.KernelVersion()
	if !d.logErr(err) {
		d.Set("OSKernelVersion", kernelVersion)
	}
}

// EnrichWithMemoryInfo adds memory information to the diagnostics payload.
func (d *DiagnosticsCollector) EnrichWithMemoryInfo() {
	memFree, err := d.server.SystemInfo.MemFree()
	if !d.logErr(err) {
		d.Set("MemFree", memFree)
	}
	memTotal, err := d.server.SystemInfo.MemTotal()
	if !d.logErr(err) {
		d.Set("MemTotal", memTotal)
	}
	memUsed, err := d.server.SystemInfo.MemUsed()
	if !d.logErr(err) {
		d.Set("MemUsed", memUsed)
	}
}

// EnrichWithSchemaProperties adds schema info to the diagnostics payload.
func (d *DiagnosticsCollector) EnrichWithSchemaProperties() {
	var numSlices uint64
	numFrames := 0
	numIndexes := 0
	bsiFieldCount := 0
	timeQuantumEnabled := false

	for _, index := range d.server.Holder.Indexes() {
		numSlices += index.MaxSlice() + 1
		numIndexes += 1
		for _, frame := range index.Frames() {
			numFrames += 1
			if frame.rangeEnabled {
				if fields, err := frame.GetFields(); err == nil {
					bsiFieldCount += len(fields)
				}
			}
			if frame.TimeQuantum() != "" {
				timeQuantumEnabled = true
			}
		}
	}

	d.Set("NumIndexes", numIndexes)
	d.Set("NumFrames", numFrames)
	d.Set("NumSlices", numSlices)
	d.Set("BSIFieldCount", bsiFieldCount)
	d.Set("TimeQuantumEnabled", timeQuantumEnabled)
}

// versionSegments returns the numeric segments of the version as a slice of ints.
func versionSegments(segments string) []int {
	segments = strings.Trim(segments, "v")
	segments = strings.Split(segments, "-")[0]
	s := strings.Split(segments, ".")
	segmentSlice := make([]int, len(s))
	for i, v := range s {
		segmentSlice[i], _ = strconv.Atoi(v)
	}
	return segmentSlice
}

// SystemInfo collects information about the host OS.
type SystemInfo interface {
	Uptime() (uint64, error)
	Platform() (string, error)
	Family() (string, error)
	OSVersion() (string, error)
	KernelVersion() (string, error)
	MemFree() (uint64, error)
	MemTotal() (uint64, error)
	MemUsed() (uint64, error)
}

// NewNopSystemInfo creates a no-op implementation of SystemInfo.
func NewNopSystemInfo() *NopSystemInfo {
	return &NopSystemInfo{}
}

// NopSystemInfo is a no-op implementation of SystemInfo.
type NopSystemInfo struct {
}

// Uptime is a no-op implementation of SystemInfo.Uptime.
func (n *NopSystemInfo) Uptime() (uint64, error) {
	return 0, nil
}

// Platform is a no-op implementation of SystemInfo.Platform.
func (n *NopSystemInfo) Platform() (string, error) {
	return "", nil
}

// Family is a no-op implementation of SystemInfo.Family.
func (n *NopSystemInfo) Family() (string, error) {
	return "", nil
}

// OSVersion is a no-op implementation of SystemInfo.OSVersion.
func (n *NopSystemInfo) OSVersion() (string, error) {
	return "", nil
}

// KernelVersion is a no-op implementation of SystemInfo.KernelVersion.
func (n *NopSystemInfo) KernelVersion() (string, error) {
	return "", nil
}

// MemFree is a no-op implementation of SystemInfo.MemFree.
func (n *NopSystemInfo) MemFree() (uint64, error) {
	return 0, nil
}

// MemTotal is a no-op implementation of SystemInfo.MemTotal.
func (n *NopSystemInfo) MemTotal() (uint64, error) {
	return 0, nil
}

// MemUsed is a no-op implementation of SystemInfo.MemUsed.
func (n *NopSystemInfo) MemUsed() (uint64, error) {
	return 0, nil
}
<commit_msg>Fix bug: backend won't store empty strings.<commit_after>// Copyright 2017 Pilosa Corp.
//
// 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 pilosa

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"strconv"
	"strings"
	"sync"
	"time"
)

// Default version check URL.
const (
	defaultVersionCheckURL = "https://diagnostics.pilosa.com/v0/version"
)

type versionResponse struct {
	Version string `json:"version"`
	Message string `json:"message"`
}

// DiagnosticsCollector represents a collector/sender of diagnostics data.
type DiagnosticsCollector struct {
	mu          sync.Mutex
	host        string
	VersionURL  string
	version     string
	lastVersion string
	startTime   int64
	start       time.Time

	metrics map[string]interface{}

	client *http.Client

	logOutput io.Writer

	server *Server
}

// NewDiagnosticsCollector returns a new DiagnosticsCollector given an addr in the format "hostname:port".
func NewDiagnosticsCollector(host string) *DiagnosticsCollector {
	return &DiagnosticsCollector{
		host:       host,
		VersionURL: defaultVersionCheckURL,
		startTime:  time.Now().Unix(),
		start:      time.Now(),
		client:     &http.Client{Timeout: 10 * time.Second},
		metrics:    make(map[string]interface{}),
		logOutput:  ioutil.Discard,
	}
}

// SetVersion of locally running Pilosa Cluster to check against master.
func (d *DiagnosticsCollector) SetVersion(v string) {
	d.version = v
	d.Set("Version", v)
}

// Flush sends the current metrics.
func (d *DiagnosticsCollector) Flush() error {
	d.mu.Lock()
	defer d.mu.Unlock()
	d.metrics["Uptime"] = (time.Now().Unix() - d.startTime)
	buf, err := d.encode()
	if err != nil {
		return err
	}
	req, err := http.NewRequest("POST", d.host, bytes.NewReader(buf))
	req.Header.Set("Content-Type", "application/json")
	resp, err := d.client.Do(req)
	if err != nil {
		return err
	}
	// Intentionally ignoring response body, as user does not need to be notified of error.
	defer resp.Body.Close()
	return nil
}

// CheckVersion of the local build against Pilosa master.
func (d *DiagnosticsCollector) CheckVersion() error {
	var rsp versionResponse
	req, err := http.NewRequest("GET", d.VersionURL, nil)
	resp, err := d.client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("http: status=%d", resp.StatusCode)
	} else if err := json.NewDecoder(resp.Body).Decode(&rsp); err != nil {
		return fmt.Errorf("json decode: %s", err)
	}

	// If version has not changed since the last check, return
	if rsp.Version == d.lastVersion {
		return nil
	}

	d.lastVersion = rsp.Version
	if err := d.compareVersion(rsp.Version); err != nil {
		d.logger().Printf("%s\n", err.Error())
	}

	return nil
}

// compareVersion check version strings.
func (d *DiagnosticsCollector) compareVersion(value string) error {
	currentVersion := versionSegments(value)
	localVersion := versionSegments(d.version)

	if localVersion[0] < currentVersion[0] { //Major
		return fmt.Errorf("Warning: You are running Pilosa %s. A newer version (%s) is available: https://github.com/pilosa/pilosa/releases", d.version, value)
	} else if localVersion[1] < currentVersion[1] && localVersion[0] == currentVersion[0] { // Minor
		return fmt.Errorf("Warning: You are running Pilosa %s. The latest Minor release is %s: https://github.com/pilosa/pilosa/releases", d.version, value)
	} else if localVersion[2] < currentVersion[2] && localVersion[0] == currentVersion[0] && localVersion[1] == currentVersion[1] { // Patch
		return fmt.Errorf("There is a new patch release of Pilosa available: %s: https://github.com/pilosa/pilosa/releases", value)
	}

	return nil
}

// Encode metrics maps into the json message format.
func (d *DiagnosticsCollector) encode() ([]byte, error) {
	return json.Marshal(d.metrics)
}

// Set adds a key value metric.
func (d *DiagnosticsCollector) Set(name string, value interface{}) {
	switch v := value.(type) {
	case string:
		if v == "" {
			// Do not set empty string
			return
		}
	}
	d.mu.Lock()
	defer d.mu.Unlock()
	d.metrics[name] = value
}

// SetLogger Set the logger output type.
func (d *DiagnosticsCollector) SetLogger(logger io.Writer) {
	d.logOutput = logger
}

// logger returns a logger that writes to LogOutput.
func (d *DiagnosticsCollector) logger() *log.Logger {
	return log.New(d.logOutput, "", log.LstdFlags)
}

// logErr logs the error and returns true if an error exists
func (d *DiagnosticsCollector) logErr(err error) bool {
	if err != nil {
		d.logOutput.Write([]byte(err.Error()))
		return true
	}
	return false
}

// EnrichWithOSInfo adds OS information to the diagnostics payload.
func (d *DiagnosticsCollector) EnrichWithOSInfo() {
	uptime, err := d.server.SystemInfo.Uptime()
	if !d.logErr(err) {
		d.Set("HostUptime", uptime)
	}
	platform, err := d.server.SystemInfo.Platform()
	if !d.logErr(err) {
		d.Set("OSPlatform", platform)
	}
	family, err := d.server.SystemInfo.Family()
	if !d.logErr(err) {
		d.Set("OSFamily", family)
	}
	version, err := d.server.SystemInfo.OSVersion()
	if !d.logErr(err) {
		d.Set("OSVersion", version)
	}
	kernelVersion, err := d.server.SystemInfo.KernelVersion()
	if !d.logErr(err) {
		d.Set("OSKernelVersion", kernelVersion)
	}
}

// EnrichWithMemoryInfo adds memory information to the diagnostics payload.
func (d *DiagnosticsCollector) EnrichWithMemoryInfo() {
	memFree, err := d.server.SystemInfo.MemFree()
	if !d.logErr(err) {
		d.Set("MemFree", memFree)
	}
	memTotal, err := d.server.SystemInfo.MemTotal()
	if !d.logErr(err) {
		d.Set("MemTotal", memTotal)
	}
	memUsed, err := d.server.SystemInfo.MemUsed()
	if !d.logErr(err) {
		d.Set("MemUsed", memUsed)
	}
}

// EnrichWithSchemaProperties adds schema info to the diagnostics payload.
func (d *DiagnosticsCollector) EnrichWithSchemaProperties() {
	var numSlices uint64
	numFrames := 0
	numIndexes := 0
	bsiFieldCount := 0
	timeQuantumEnabled := false

	for _, index := range d.server.Holder.Indexes() {
		numSlices += index.MaxSlice() + 1
		numIndexes += 1
		for _, frame := range index.Frames() {
			numFrames += 1
			if frame.rangeEnabled {
				if fields, err := frame.GetFields(); err == nil {
					bsiFieldCount += len(fields)
				}
			}
			if frame.TimeQuantum() != "" {
				timeQuantumEnabled = true
			}
		}
	}

	d.Set("NumIndexes", numIndexes)
	d.Set("NumFrames", numFrames)
	d.Set("NumSlices", numSlices)
	d.Set("BSIFieldCount", bsiFieldCount)
	d.Set("TimeQuantumEnabled", timeQuantumEnabled)
}

// versionSegments returns the numeric segments of the version as a slice of ints.
func versionSegments(segments string) []int {
	segments = strings.Trim(segments, "v")
	segments = strings.Split(segments, "-")[0]
	s := strings.Split(segments, ".")
	segmentSlice := make([]int, len(s))
	for i, v := range s {
		segmentSlice[i], _ = strconv.Atoi(v)
	}
	return segmentSlice
}

// SystemInfo collects information about the host OS.
type SystemInfo interface {
	Uptime() (uint64, error)
	Platform() (string, error)
	Family() (string, error)
	OSVersion() (string, error)
	KernelVersion() (string, error)
	MemFree() (uint64, error)
	MemTotal() (uint64, error)
	MemUsed() (uint64, error)
}

// NewNopSystemInfo creates a no-op implementation of SystemInfo.
func NewNopSystemInfo() *NopSystemInfo {
	return &NopSystemInfo{}
}

// NopSystemInfo is a no-op implementation of SystemInfo.
type NopSystemInfo struct {
}

// Uptime is a no-op implementation of SystemInfo.Uptime.
func (n *NopSystemInfo) Uptime() (uint64, error) {
	return 0, nil
}

// Platform is a no-op implementation of SystemInfo.Platform.
func (n *NopSystemInfo) Platform() (string, error) {
	return "", nil
}

// Family is a no-op implementation of SystemInfo.Family.
func (n *NopSystemInfo) Family() (string, error) {
	return "", nil
}

// OSVersion is a no-op implementation of SystemInfo.OSVersion.
func (n *NopSystemInfo) OSVersion() (string, error) {
	return "", nil
}

// KernelVersion is a no-op implementation of SystemInfo.KernelVersion.
func (n *NopSystemInfo) KernelVersion() (string, error) {
	return "", nil
}

// MemFree is a no-op implementation of SystemInfo.MemFree.
func (n *NopSystemInfo) MemFree() (uint64, error) {
	return 0, nil
}

// MemTotal is a no-op implementation of SystemInfo.MemTotal.
func (n *NopSystemInfo) MemTotal() (uint64, error) {
	return 0, nil
}

// MemUsed is a no-op implementation of SystemInfo.MemUsed.
func (n *NopSystemInfo) MemUsed() (uint64, error) {
	return 0, nil
}
<|endoftext|>