<commit_before>// go-rst - A reStructuredText parser for Go
// 2014 (c) The go-rst Authors
// MIT Licensed. See LICENSE for details.

package parse

import (
	"code.google.com/p/go.text/unicode/norm"
	"fmt"
	"github.com/demizer/go-elog"
	"github.com/demizer/go-spew/spew"
	"reflect"
)

var spd = spew.ConfigState{Indent: "\t", DisableMethods: true}

type systemMessageLevel int

const (
	levelInfo systemMessageLevel = iota
	levelWarning
	levelError
	levelSevere
)

var systemMessageLevels = [...]string{
	"INFO",
	"WARNING",
	"ERROR",
	"SEVERE",
}

func (s systemMessageLevel) String() string {
	return systemMessageLevels[s]
}

type parserMessage int

const (
	warningShortUnderline parserMessage = iota
	severeUnexpectedSectionTitle
	severeUnexpectedSectionTitleOrTransition
	severeIncompleteSectionTitle
	severeMissingMatchingUnderlineForOverline
)

var parserErrors = [...]string{
	"warningShortUnderline",
	"severeUnexpectedSectionTitle",
	"severeUnexpectedSectionTitleOrTransition",
	"severeIncompleteSectionTitle",
	"severeMissingMatchingUnderlineForOverline",
}

func (p parserMessage) String() string {
	return parserErrors[p]
}

func (p parserMessage) Message() (s string) {
	switch p {
	case warningShortUnderline:
		s = "Title underline too short."
	case severeUnexpectedSectionTitle:
		s = "Unexpected section title."
	case severeUnexpectedSectionTitleOrTransition:
		s = "Unexpected section title or transition."
	case severeIncompleteSectionTitle:
		s = "Incomplete section title."
	case severeMissingMatchingUnderlineForOverline:
		s = "Missing matching underline for section title overline."
	}
	return
}

func (p parserMessage) Level() (s systemMessageLevel) {
	switch p {
	case warningShortUnderline:
		s = levelWarning
	case severeUnexpectedSectionTitle:
		s = levelSevere
	case severeUnexpectedSectionTitleOrTransition:
		s = levelSevere
	case severeIncompleteSectionTitle:
		s = levelSevere
	case severeMissingMatchingUnderlineForOverline:
		s = levelSevere
	}
	return
}

type sectionLevels []*SectionNode

func (s *sectionLevels) String() string {
	var out string
	for _, sec := range *s {
		out += fmt.Sprintf("level: %d, rune: %q, overline: %t, length: %d\n",
			sec.Level, sec.UnderLine.Rune, sec.OverLine != nil, sec.Length)
	}
	return out
}

// Returns nil if not found
func (s *sectionLevels) FindByRune(adornChar rune) *SectionNode {
	for _, sec := range *s {
		if sec.UnderLine.Rune == adornChar {
			return sec
		}
	}
	return nil
}

// If exists == true, a section node with the same text and underline has been found in
// sectionLevels, sec is the matching SectionNode. If exists == false, then the sec return value is
// the similarly leveled SectionNode. If exists == false and sec == nil, then the SectionNode added
// to sectionLevels is a new Node.
func (s *sectionLevels) Add(section *SectionNode) (exists bool, sec *SectionNode) {
	sec = s.FindByRune(section.UnderLine.Rune)
	if sec != nil {
		if sec.Text != section.Text {
			section.Level = sec.Level
		}
	} else {
		section.Level = len(*s) + 1
	}
	exists = false
	*s = append(*s, section)
	return
}

func (s *sectionLevels) Level() int {
	return len(*s)
}

// Parse is the entry point for the reStructuredText parser.
func Parse(name, text string) (t *Tree, errors []error) {
	t = New(name)
	if !norm.NFC.IsNormalString(text) {
		text = norm.NFC.String(text)
	}
	t.text = text
	_, errors = t.Parse(text, t)
	return
}

func New(name string) *Tree {
	return &Tree{
		Name:          name,
		Nodes:         newList(),
		nodeTarget:    newList(),
		sectionLevels: new(sectionLevels),
		indentWidth:   indentWidth,
	}
}

const (
	zed         = 3
	indentWidth = 4 // Default indent width
)

type Tree struct {
	Name             string
	Nodes            *NodeList // The root node list
	nodeTarget       *NodeList // Used by the parser to add nodes to a target NodeList
	Errors           []error
	text             string
	lex              *lexer
	tokenBackupCount int
	peekCount        int
	token            [7]*item
	sectionLevels    *sectionLevels // Encountered section levels
	id               int            // The unique id of the node in the tree
	indentWidth      int
	indentLevel      int
}

// startParse initializes the parser, using the lexer.
func (t *Tree) startParse(lex *lexer) {
	t.lex = lex
}

func (t *Tree) Parse(text string, treeSet *Tree) (tree *Tree, errors []error) {
	log.Debugln("Start")
	t.startParse(lex(t.Name, text))
	t.text = text
	t.parse(treeSet)
	log.Debugln("End")
	return t, t.Errors
}

func (t *Tree) parse(tree *Tree) {
	log.Debugln("Start")

	t.nodeTarget = t.Nodes

	for t.peek(1).Type != itemEOF {
		var n Node

		token := t.next()
		log.Infof("\nParser got token: %#+v\n\n", token)

		switch token.Type {
		case itemSectionAdornment:
			n = t.section(token)
		case itemParagraph:
			n = newParagraph(token, &t.id)
		case itemSpace:
			n = t.indent(token)
			if n == nil {
				continue
			}
		case itemTitle, itemBlankLine:
			// itemTitle is consumed when evaluating itemSectionAdornment
			continue
		}

		t.nodeTarget.append(n)
		switch n.NodeType() {
		case NodeSection, NodeBlockQuote:
			// Set the loop to append items to the NodeList of the new section
			t.nodeTarget = reflect.ValueOf(n).Elem().FieldByName("NodeList").Addr().Interface().(*NodeList)
		}
	}

	log.Debugln("End")
}

func (t *Tree) peekBack(pos int) *item {
	return t.token[zed-pos]
}

func (t *Tree) peek(pos int) *item {
	// log.Debugln("\n", "Pos:", pos)
	// log.Debugf("##### peek() before #####\n")
	// spd.Dump(t.token)
	nItem := t.token[zed]
	for i := 1; i <= pos; i++ {
		if t.token[zed+i] != nil {
			nItem = t.token[zed+i]
			log.Debugf("Using %#+v\n", nItem)
			continue
		} else {
			log.Debugln("Getting next item")
			t.token[zed+i] = t.lex.nextItem()
			nItem = t.token[zed+i]
		}
	}
	// log.Debugf("\n##### peek() aftermath #####\n")
	// spd.Dump(t.token)
	// log.Debugf("Returning: %#+v\n", nItem)
	return nItem
}

func (t *Tree) peekSkip(iSkip itemElement) *item {
	var nItem *item
	var count int = 1
	for {
		nItem = t.peek(count)
		if nItem.Type != iSkip {
			 break
		}
		count++
	}
	return nItem
}

func (t *Tree) next() *item {
	// log.Debugf("\n##### next() before #####\n")
	// spd.Dump(t.token)
	for x := 0; x < len(t.token)-1; x++ {
		t.token[x] = t.token[x+1]
		t.token[x+1] = nil
	}
	if t.token[zed] == nil {
		t.token[zed] = t.lex.nextItem()
	}
	// log.Debugf("\n##### next() aftermath #####\n\n")
	// spd.Dump(t.token)
	return t.token[zed]
}

func (t *Tree) section(i *item) Node {
	log.Debugln("Start")
	var overAdorn, indent, title, underAdorn *item

	if pBack := t.peekBack(1); pBack != nil && pBack.Type == itemTitle {
		title = t.peekBack(1)
		underAdorn = i
	} else if pBack := t.peekBack(1); pBack != nil && pBack.Type == itemSpace {
		if t.peekBack(2).Type == itemTitle {
			return t.systemMessage(severeUnexpectedSectionTitle)
		}
		return t.systemMessage(severeUnexpectedSectionTitleOrTransition)
	} else if pFor := t.peekSkip(itemSpace); pFor != nil && pFor.Type == itemTitle {
		overAdorn = i
		t.next()
	loop:
		for {
			switch tTok := t.token[zed]; tTok.Type {
			case itemTitle:
				title = tTok
				t.next()
			case itemSpace:
				indent = tTok
				t.next()
			case itemSectionAdornment:
				underAdorn = tTok
				break loop
			}
		}
	} else if pFor := t.peekSkip(itemSpace); pFor != nil && pFor.Type == itemParagraph {
		t.next()
		t.next()
		if p := t.peek(1); p != nil && p.Type == itemBlankLine {
			return t.systemMessage(severeMissingMatchingUnderlineForOverline)
		}
		return t.systemMessage(severeIncompleteSectionTitle)
	}

	sec := newSection(title, overAdorn, underAdorn, indent, &t.id)
	exists, eSec := t.sectionLevels.Add(sec)
	if !exists && eSec != nil {
		// There is a matching level in sectionLevels
		t.nodeTarget = &(*t.sectionLevels)[sec.Level-2].NodeList
	}

	if indent == nil {
		if title.Length != underAdorn.Length {
			sec.NodeList = append(sec.NodeList, t.systemMessage(warningShortUnderline))
		}
	}

	log.Debugln("End")
	return sec
}

func (t *Tree) systemMessage(err parserMessage) Node {
	var lbText string
	var lbTextLen int
	var backToken int

	s := newSystemMessage(&item{
		Type: itemSystemMessage,
		Line: t.token[zed].Line,
	},
		err.Level(), &t.id)

	msg := newParagraph(&item{
		Text:   err.Message(),
		Length: len(err.Message()),
	}, &t.id)

	log.Debugln("FOUND", err)

	switch err {
	case severeIncompleteSectionTitle, severeMissingMatchingUnderlineForOverline:
		lbText = t.token[zed-2].Text.(string) + "\n" +t.token[zed-1].Text.(string)+
			t.token[zed].Text.(string)
		s.Line = t.token[zed-2].Line
		lbTextLen = len(lbText) + 1
	case warningShortUnderline, severeUnexpectedSectionTitle:
		backToken = zed - 1
		if t.peekBack(1).Type == itemSpace {
			backToken = zed - 2
		}
		lbText = t.token[backToken].Text.(string) + "\n" + t.token[zed].Text.(string)
		lbTextLen = len(lbText) + 1
	case severeUnexpectedSectionTitleOrTransition:
		lbText = t.token[zed].Text.(string)
		lbTextLen = len(lbText)
	}

	lb := newLiteralBlock(&item{
		Type:   itemLiteralBlock,
		Text:   lbText,
		Length: lbTextLen, // Add one to account for the backslash
	}, &t.id)

	s.NodeList = append(s.NodeList, msg, lb)
	return s
}

func (t *Tree) indent(i *item) Node {
	level := i.Length / t.indentWidth
	if t.peekBack(1).Type == itemBlankLine {
		if t.indentLevel == level {
			// Append to the current blockquote NodeList
			return nil
		}
		t.indentLevel = level
		return newBlockQuote(&item{Type: itemBlockquote, Line: i.Line}, level, &t.id)
	}
	return nil
}
<commit_msg>parse.go: Add warningShortOverline<commit_after>// go-rst - A reStructuredText parser for Go
// 2014 (c) The go-rst Authors
// MIT Licensed. See LICENSE for details.

package parse

import (
	"code.google.com/p/go.text/unicode/norm"
	"fmt"
	"github.com/demizer/go-elog"
	"github.com/demizer/go-spew/spew"
	"reflect"
)

var spd = spew.ConfigState{Indent: "\t", DisableMethods: true}

type systemMessageLevel int

const (
	levelInfo systemMessageLevel = iota
	levelWarning
	levelError
	levelSevere
)

var systemMessageLevels = [...]string{
	"INFO",
	"WARNING",
	"ERROR",
	"SEVERE",
}

func (s systemMessageLevel) String() string {
	return systemMessageLevels[s]
}

type parserMessage int

const (
	warningShortOverline parserMessage = iota
	warningShortUnderline
	severeUnexpectedSectionTitle
	severeUnexpectedSectionTitleOrTransition
	severeIncompleteSectionTitle
	severeMissingMatchingUnderlineForOverline
)

var parserErrors = [...]string{
	"warningShortOverline",
	"warningShortUnderline",
	"severeUnexpectedSectionTitle",
	"severeUnexpectedSectionTitleOrTransition",
	"severeIncompleteSectionTitle",
	"severeMissingMatchingUnderlineForOverline",
}

func (p parserMessage) String() string {
	return parserErrors[p]
}

func (p parserMessage) Message() (s string) {
	switch p {
	case warningShortOverline:
		s = "Title overline too short."
	case warningShortUnderline:
		s = "Title underline too short."
	case severeUnexpectedSectionTitle:
		s = "Unexpected section title."
	case severeUnexpectedSectionTitleOrTransition:
		s = "Unexpected section title or transition."
	case severeIncompleteSectionTitle:
		s = "Incomplete section title."
	case severeMissingMatchingUnderlineForOverline:
		s = "Missing matching underline for section title overline."
	}
	return
}

func (p parserMessage) Level() (s systemMessageLevel) {
	switch p {
	case warningShortOverline, warningShortUnderline:
		s = levelWarning
	case severeUnexpectedSectionTitle:
		s = levelSevere
	case severeUnexpectedSectionTitleOrTransition:
		s = levelSevere
	case severeIncompleteSectionTitle:
		s = levelSevere
	case severeMissingMatchingUnderlineForOverline:
		s = levelSevere
	}
	return
}

type sectionLevels []*SectionNode

func (s *sectionLevels) String() string {
	var out string
	for _, sec := range *s {
		out += fmt.Sprintf("level: %d, rune: %q, overline: %t, length: %d\n",
			sec.Level, sec.UnderLine.Rune, sec.OverLine != nil, sec.Length)
	}
	return out
}

// Returns nil if not found
func (s *sectionLevels) FindByRune(adornChar rune) *SectionNode {
	for _, sec := range *s {
		if sec.UnderLine.Rune == adornChar {
			return sec
		}
	}
	return nil
}

// If exists == true, a section node with the same text and underline has been found in
// sectionLevels, sec is the matching SectionNode. If exists == false, then the sec return value is
// the similarly leveled SectionNode. If exists == false and sec == nil, then the SectionNode added
// to sectionLevels is a new Node.
func (s *sectionLevels) Add(section *SectionNode) (exists bool, sec *SectionNode) {
	sec = s.FindByRune(section.UnderLine.Rune)
	if sec != nil {
		if sec.Text != section.Text {
			section.Level = sec.Level
		}
	} else {
		section.Level = len(*s) + 1
	}
	exists = false
	*s = append(*s, section)
	return
}

func (s *sectionLevels) Level() int {
	return len(*s)
}

// Parse is the entry point for the reStructuredText parser.
func Parse(name, text string) (t *Tree, errors []error) {
	t = New(name)
	if !norm.NFC.IsNormalString(text) {
		text = norm.NFC.String(text)
	}
	t.text = text
	_, errors = t.Parse(text, t)
	return
}

func New(name string) *Tree {
	return &Tree{
		Name:          name,
		Nodes:         newList(),
		nodeTarget:    newList(),
		sectionLevels: new(sectionLevels),
		indentWidth:   indentWidth,
	}
}

const (
	zed         = 3
	indentWidth = 4 // Default indent width
)

type Tree struct {
	Name             string
	Nodes            *NodeList // The root node list
	nodeTarget       *NodeList // Used by the parser to add nodes to a target NodeList
	Errors           []error
	text             string
	lex              *lexer
	tokenBackupCount int
	peekCount        int
	token            [7]*item
	sectionLevels    *sectionLevels // Encountered section levels
	id               int            // The unique id of the node in the tree
	indentWidth      int
	indentLevel      int
}

// startParse initializes the parser, using the lexer.
func (t *Tree) startParse(lex *lexer) {
	t.lex = lex
}

func (t *Tree) Parse(text string, treeSet *Tree) (tree *Tree, errors []error) {
	log.Debugln("Start")
	t.startParse(lex(t.Name, text))
	t.text = text
	t.parse(treeSet)
	log.Debugln("End")
	return t, t.Errors
}

func (t *Tree) parse(tree *Tree) {
	log.Debugln("Start")

	t.nodeTarget = t.Nodes

	for t.peek(1).Type != itemEOF {
		var n Node

		token := t.next()
		log.Infof("\nParser got token: %#+v\n\n", token)

		switch token.Type {
		case itemSectionAdornment:
			n = t.section(token)
		case itemParagraph:
			n = newParagraph(token, &t.id)
		case itemSpace:
			n = t.indent(token)
			if n == nil {
				continue
			}
		case itemTitle, itemBlankLine:
			// itemTitle is consumed when evaluating itemSectionAdornment
			continue
		}

		t.nodeTarget.append(n)
		switch n.NodeType() {
		case NodeSection, NodeBlockQuote:
			// Set the loop to append items to the NodeList of the new section
			t.nodeTarget = reflect.ValueOf(n).Elem().FieldByName("NodeList").Addr().Interface().(*NodeList)
		}
	}

	log.Debugln("End")
}

func (t *Tree) peekBack(pos int) *item {
	return t.token[zed-pos]
}

func (t *Tree) peek(pos int) *item {
	// log.Debugln("\n", "Pos:", pos)
	// log.Debugf("##### peek() before #####\n")
	// spd.Dump(t.token)
	nItem := t.token[zed]
	for i := 1; i <= pos; i++ {
		if t.token[zed+i] != nil {
			nItem = t.token[zed+i]
			log.Debugf("Using %#+v\n", nItem)
			continue
		} else {
			log.Debugln("Getting next item")
			t.token[zed+i] = t.lex.nextItem()
			nItem = t.token[zed+i]
		}
	}
	// log.Debugf("\n##### peek() aftermath #####\n")
	// spd.Dump(t.token)
	// log.Debugf("Returning: %#+v\n", nItem)
	return nItem
}

func (t *Tree) peekSkip(iSkip itemElement) *item {
	var nItem *item
	var count int = 1
	for {
		nItem = t.peek(count)
		if nItem.Type != iSkip {
			 break
		}
		count++
	}
	return nItem
}

func (t *Tree) next() *item {
	// log.Debugf("\n##### next() before #####\n")
	// spd.Dump(t.token)
	for x := 0; x < len(t.token)-1; x++ {
		t.token[x] = t.token[x+1]
		t.token[x+1] = nil
	}
	if t.token[zed] == nil {
		t.token[zed] = t.lex.nextItem()
	}
	// log.Debugf("\n##### next() aftermath #####\n\n")
	// spd.Dump(t.token)
	return t.token[zed]
}

func (t *Tree) section(i *item) Node {
	log.Debugln("Start")
	var overAdorn, indent, title, underAdorn *item

	if pBack := t.peekBack(1); pBack != nil && pBack.Type == itemTitle {
		title = t.peekBack(1)
		underAdorn = i
	} else if pBack := t.peekBack(1); pBack != nil && pBack.Type == itemSpace {
		if t.peekBack(2).Type == itemTitle {
			return t.systemMessage(severeUnexpectedSectionTitle)
		}
		return t.systemMessage(severeUnexpectedSectionTitleOrTransition)
	} else if pFor := t.peekSkip(itemSpace); pFor != nil && pFor.Type == itemTitle {
		overAdorn = i
		t.next()
	loop:
		for {
			switch tTok := t.token[zed]; tTok.Type {
			case itemTitle:
				title = tTok
				t.next()
			case itemSpace:
				indent = tTok
				t.next()
			case itemSectionAdornment:
				underAdorn = tTok
				break loop
			}
		}
	} else if pFor := t.peekSkip(itemSpace); pFor != nil && pFor.Type == itemParagraph {
		t.next()
		t.next()
		if p := t.peek(1); p != nil && p.Type == itemBlankLine {
			return t.systemMessage(severeMissingMatchingUnderlineForOverline)
		}
		return t.systemMessage(severeIncompleteSectionTitle)
	}

	sec := newSection(title, overAdorn, underAdorn, indent, &t.id)
	exists, eSec := t.sectionLevels.Add(sec)
	if !exists && eSec != nil {
		// There is a matching level in sectionLevels
		t.nodeTarget = &(*t.sectionLevels)[sec.Level-2].NodeList
	}

	if indent == nil {
		if title.Length != underAdorn.Length {
			sec.NodeList = append(sec.NodeList, t.systemMessage(warningShortUnderline))
		}
	}

	log.Debugln("End")
	return sec
}

func (t *Tree) systemMessage(err parserMessage) Node {
	var lbText string
	var lbTextLen int
	var backToken int

	s := newSystemMessage(&item{
		Type: itemSystemMessage,
		Line: t.token[zed].Line,
	},
		err.Level(), &t.id)

	msg := newParagraph(&item{
		Text:   err.Message(),
		Length: len(err.Message()),
	}, &t.id)

	log.Debugln("FOUND", err)
	var overLine, indent, title, underLine, newLine string

	switch err {
	case severeIncompleteSectionTitle, severeMissingMatchingUnderlineForOverline:
		lbText = t.token[zed-2].Text.(string) + "\n" +t.token[zed-1].Text.(string)+
			t.token[zed].Text.(string)
		s.Line = t.token[zed-2].Line
		lbTextLen = len(lbText) + 1
	case warningShortOverline:
		backToken = zed - 2
		if t.peekBack(2).Type == itemSpace {
			backToken = zed - 3
			indent = t.token[zed-2].Text.(string)
		}
		overLine = t.token[backToken].Text.(string)
		title = t.token[zed-1].Text.(string)
		underLine = t.token[zed].Text.(string)
		newLine = "\n"
		lbText =  overLine + newLine + indent + title + newLine + underLine
		s.Line = t.token[backToken].Line
		lbTextLen = len(lbText) + 2
	case warningShortUnderline, severeUnexpectedSectionTitle:
		backToken = zed - 1
		if t.peekBack(1).Type == itemSpace {
			backToken = zed - 2
		}
		lbText = t.token[backToken].Text.(string) + "\n" + t.token[zed].Text.(string)
		lbTextLen = len(lbText) + 1
	case severeUnexpectedSectionTitleOrTransition:
		lbText = t.token[zed].Text.(string)
		lbTextLen = len(lbText)
	}

	lb := newLiteralBlock(&item{
		Type:   itemLiteralBlock,
		Text:   lbText,
		Length: lbTextLen, // Add one to account for the backslash
	}, &t.id)

	s.NodeList = append(s.NodeList, msg, lb)
	return s
}

func (t *Tree) indent(i *item) Node {
	level := i.Length / t.indentWidth
	if t.peekBack(1).Type == itemBlankLine {
		if t.indentLevel == level {
			// Append to the current blockquote NodeList
			return nil
		}
		t.indentLevel = level
		return newBlockQuote(&item{Type: itemBlockquote, Line: i.Line}, level, &t.id)
	}
	return nil
}
<|endoftext|>


<commit_before>package conflag

import (
	"reflect"
	"regexp"
	"strings"
	"testing"
)

type ParseTest struct {
	configString string
	expected     map[string]interface{}
}

type ParseErrorTest struct {
	configString string
	expected     *regexp.Regexp
}

// helper
func (c conf) asMap() map[string]interface{} {
	return c
}

// JSON parser tests

func TestParseJson(t *testing.T) {
	asserts := []ParseTest{
		ParseTest{
			`{"flag":"value"}`,
			map[string]interface{}{"flag": "value"},
		},
		ParseTest{
			`{"flag1":"value1", "flag2":"value2"}`,
			map[string]interface{}{"flag1": "value1", "flag2": "value2"},
		},
	}

	for _, a := range asserts {
		reader := strings.NewReader(a.configString)

		actual, err := parseAsJson(reader)
		if err != nil {
			t.Errorf("parse error: %v", a.configString)
		}

		if !reflect.DeepEqual(a.expected, actual.asMap()) {
			t.Errorf("not match: %#v %#v", a.expected, actual.asMap())
		}
	}
}

func TestParseJson_ParseError(t *testing.T) {
	asserts := []ParseErrorTest{
		ParseErrorTest{`{'flag':'value'}`, regexp.MustCompile("invalid character '\\'' looking for beginning of object key string")},
	}

	for _, a := range asserts {
		reader := strings.NewReader(a.configString)
		_, err := parseAsJson(reader)
		if a.expected.MatchString(err.Error()) {
			t.Errorf("parse error: %v", err)
		}
	}
}

// YAML parser tests

func TestParseYaml(t *testing.T) {
	asserts := []ParseTest{
		ParseTest{
			`flag: value`,
			map[string]interface{}{"flag": "value"},
		},
		ParseTest{
			"flag1: value1\nflag2: value2\n",
			map[string]interface{}{"flag1": "value1", "flag2": "value2"},
		},
	}

	for _, a := range asserts {
		reader := strings.NewReader(a.configString)
		actual, err := parseAsYaml(reader)

		if err != nil {
			t.Errorf("parse error: %v %v", err, a.configString)
		}

		if !reflect.DeepEqual(a.expected, actual.asMap()) {
			t.Errorf("not match: %#v %#v", a.expected, actual.asMap())
		}
	}
}

func TestParseYaml_ParseError(t *testing.T) {
	asserts := []ParseErrorTest{
		ParseErrorTest{`flag - value`, regexp.MustCompile("yaml: unmarshal errors:")},
	}

	for _, a := range asserts {
		reader := strings.NewReader(a.configString)
		_, err := parseAsYaml(reader)
		if !a.expected.MatchString(err.Error()) {
			t.Errorf("expected error but")
		}
	}
}
<commit_msg>Fix error messages<commit_after>package conflag

import (
	"reflect"
	"regexp"
	"strings"
	"testing"
)

type ParseTest struct {
	configString string
	expected     map[string]interface{}
}

type ParseErrorTest struct {
	configString string
	expected     *regexp.Regexp
}

// helper
func (c conf) asMap() map[string]interface{} {
	return c
}

// JSON parser tests

func TestParseJson(t *testing.T) {
	asserts := []ParseTest{
		ParseTest{
			`{"flag":"value"}`,
			map[string]interface{}{"flag": "value"},
		},
		ParseTest{
			`{"flag1":"value1", "flag2":"value2"}`,
			map[string]interface{}{"flag1": "value1", "flag2": "value2"},
		},
	}

	for _, a := range asserts {
		reader := strings.NewReader(a.configString)
		actual, err := parseAsJson(reader)
		if err != nil {
			t.Errorf("Unexpected parse error: %v, for input %#v", err, a.configString)
		}

		if !reflect.DeepEqual(a.expected, actual.asMap()) {
			t.Errorf("Parsed result should be %#v, but %#v", a.expected, actual.asMap())
		}
	}
}

func TestParseJson_ParseError(t *testing.T) {
	asserts := []ParseErrorTest{
		ParseErrorTest{`{'flag':'value'}`, regexp.MustCompile("invalid character '\\'' looking for beginning of object key string")},
	}

	for _, a := range asserts {
		reader := strings.NewReader(a.configString)
		_, err := parseAsJson(reader)
		if a.expected.MatchString(err.Error()) {
			t.Errorf("Unexpected parse error: %v", err)
		}
	}
}

// YAML parser tests

func TestParseYaml(t *testing.T) {
	asserts := []ParseTest{
		ParseTest{
			`flag: value`,
			map[string]interface{}{"flag": "value"},
		},
		ParseTest{
			"flag1: value1\nflag2: value2\n",
			map[string]interface{}{"flag1": "value1", "flag2": "value2"},
		},
	}

	for _, a := range asserts {
		reader := strings.NewReader(a.configString)
		actual, err := parseAsYaml(reader)
		if err != nil {
			t.Errorf("Unexpected parse error: %v, for input %#v", err, a.configString)
		}

		if !reflect.DeepEqual(a.expected, actual.asMap()) {
			t.Errorf("Parsed result should be %#v, but %#v", a.expected, actual.asMap())
		}
	}
}

func TestParseYaml_ParseError(t *testing.T) {
	asserts := []ParseErrorTest{
		ParseErrorTest{`flag - value`, regexp.MustCompile("yaml: unmarshal errors:")},
	}

	for _, a := range asserts {
		reader := strings.NewReader(a.configString)
		_, err := parseAsYaml(reader)
		if !a.expected.MatchString(err.Error()) {
			t.Errorf("Unexpected parse error: %v", err)
		}
	}
}
<|endoftext|>


<commit_before>package payload

import (
	"encoding/json"

	"github.com/RobotsAndPencils/buford/payload/badge"
)

// APS is Apple's reserved namespace.
type APS struct {
	// Alert dictionary.
	Alert Alert

	// Badge to display on the app icon.
	// Set to badge.Preserve (default), badge.Clear
	// or a specific value with badge.New(n).
	Badge badge.Badge

	// The name of a sound file to play as an alert.
	Sound string

	// Content available for silent notifications.
	// With no alert, sound, or badge.
	ContentAvailable bool

	// Category identifier for custom actions in iOS 8 or newer.
	Category string
}

// Alert dictionary.
type Alert struct {
	// Title is a short string shown briefly on Apple Watch in iOS 8.2 or newer.
	Title        string   `json:"title,omitempty"`
	TitleLocKey  string   `json:"title-loc-key,omitempty"`
	TitleLocArgs []string `json:"title-loc-args,omitempty"`

	// Body text of the alert message.
	Body    string   `json:"body,omitempty"`
	LocKey  string   `json:"loc-key,omitempty"`
	LocArgs []string `json:"loc-args,omitempty"`

	// Key for localized string for "View" button.
	ActionLocKey string `json:"action-loc-key,omitempty"`

	// Image file to be used when user taps or slides the action button.
	LaunchImage string `json:"launch-image,omitempty"`
}

// isSimple alert with only Body set.
func (a *Alert) isSimple() bool {
	return len(a.Title) == 0 && len(a.TitleLocKey) == 0 && len(a.TitleLocArgs) == 0 && len(a.LocKey) == 0 && len(a.LocArgs) == 0 && len(a.ActionLocKey) == 0 && len(a.LaunchImage) == 0
}

// isZero if no Alert fields are set.
func (a *Alert) isZero() bool {
	return len(a.Body) == 0 && a.isSimple()
}

// Map returns the APS payload as a map that you can customize
// before serializing it to JSON.
// TODO: Make this map implement json.Marshaler for use with Service.Push.
func (a *APS) Map() map[string]interface{} {
	aps := make(map[string]interface{}, 4)

	if !a.Alert.isZero() {
		if a.Alert.isSimple() {
			aps["alert"] = a.Alert.Body
		} else {
			aps["alert"] = a.Alert
		}
	}
	if n, ok := a.Badge.Number(); ok {
		aps["badge"] = n
	}
	if a.Sound != "" {
		aps["sound"] = a.Sound
	}
	if a.ContentAvailable {
		aps["content-available"] = 1
	}

	// wrap in "aps" to form final payload
	return map[string]interface{}{"aps": aps}
}

// MarshalJSON allows you to json.Marshal(aps) directly.
func (a APS) MarshalJSON() ([]byte, error) {
	return json.Marshal(a.Map())
}

// Validate APS payload.
func (a *APS) Validate() error {
	if a == nil {
		return ErrIncomplete
	}

	// must have a body or a badge (or custom data)
	if len(a.Alert.Body) == 0 && a.Badge == badge.Preserve {
		return ErrIncomplete
	}
	return nil
}
<commit_msg>Support for Category inside aps payload.<commit_after>package payload

import (
	"encoding/json"

	"github.com/RobotsAndPencils/buford/payload/badge"
)

// APS is Apple's reserved namespace.
type APS struct {
	// Alert dictionary.
	Alert Alert

	// Badge to display on the app icon.
	// Set to badge.Preserve (default), badge.Clear
	// or a specific value with badge.New(n).
	Badge badge.Badge

	// The name of a sound file to play as an alert.
	Sound string

	// Content available for silent notifications.
	// With no alert, sound, or badge.
	ContentAvailable bool

	// Category identifier for custom actions in iOS 8 or newer.
	Category string
}

// Alert dictionary.
type Alert struct {
	// Title is a short string shown briefly on Apple Watch in iOS 8.2 or newer.
	Title        string   `json:"title,omitempty"`
	TitleLocKey  string   `json:"title-loc-key,omitempty"`
	TitleLocArgs []string `json:"title-loc-args,omitempty"`

	// Body text of the alert message.
	Body    string   `json:"body,omitempty"`
	LocKey  string   `json:"loc-key,omitempty"`
	LocArgs []string `json:"loc-args,omitempty"`

	// Key for localized string for "View" button.
	ActionLocKey string `json:"action-loc-key,omitempty"`

	// Image file to be used when user taps or slides the action button.
	LaunchImage string `json:"launch-image,omitempty"`
}

// isSimple alert with only Body set.
func (a *Alert) isSimple() bool {
	return len(a.Title) == 0 && len(a.TitleLocKey) == 0 && len(a.TitleLocArgs) == 0 && len(a.LocKey) == 0 && len(a.LocArgs) == 0 && len(a.ActionLocKey) == 0 && len(a.LaunchImage) == 0
}

// isZero if no Alert fields are set.
func (a *Alert) isZero() bool {
	return len(a.Body) == 0 && a.isSimple()
}

// Map returns the APS payload as a map that you can customize
// before serializing it to JSON.
// TODO: Make this map implement json.Marshaler for use with Service.Push.
func (a *APS) Map() map[string]interface{} {
	aps := make(map[string]interface{}, 4)

	if !a.Alert.isZero() {
		if a.Alert.isSimple() {
			aps["alert"] = a.Alert.Body
		} else {
			aps["alert"] = a.Alert
		}
	}
	if n, ok := a.Badge.Number(); ok {
		aps["badge"] = n
	}
	if a.Sound != "" {
		aps["sound"] = a.Sound
	}
	if a.ContentAvailable {
		aps["content-available"] = 1
	}
	if a.Category != "" {
		aps["category"] = a.Category
	}

	// wrap in "aps" to form final payload
	return map[string]interface{}{"aps": aps}
}

// MarshalJSON allows you to json.Marshal(aps) directly.
func (a APS) MarshalJSON() ([]byte, error) {
	return json.Marshal(a.Map())
}

// Validate APS payload.
func (a *APS) Validate() error {
	if a == nil {
		return ErrIncomplete
	}

	// must have a body or a badge (or custom data)
	if len(a.Alert.Body) == 0 && a.Badge == badge.Preserve {
		return ErrIncomplete
	}
	return nil
}
<|endoftext|>


<commit_before>// Copyright 2015-2016, Cyrill @ Schumacher.fm and the CoreStore contributors
//
// 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 payment defines a payment processing library which can connect to
// payment gateways.
//
// It contains an abstraction level for all payment methods.
//
// Consider API from https://github.com/thephpleague/omnipay
// and Magento\Payment\Model
package payment
<commit_msg>payment: Add todo for https://www.w3.org/TR/#tr_Web_Payments<commit_after>// Copyright 2015-2016, Cyrill @ Schumacher.fm and the CoreStore contributors
//
// 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 payment defines a payment processing library which can connect to
// payment gateways.
//
// It contains an abstraction level for all payment methods.
//
// Consider API from https://github.com/thephpleague/omnipay
// and Magento\Payment\Model
//
// W3C Draft https://www.w3.org/TR/#tr_Web_Payments
// https://news.ycombinator.com/item?id=15046601
package payment
<|endoftext|>


<commit_before>package pddl

import (
	"fmt"
	"log"
	. "goplan/lifted"
)

type Parser struct {
	lex    *Lexer
	peeks  [2]token
	npeeks int
}

func (p *Parser) next() token {
	if p.npeeks == 0 {
		return p.lex.token()
	}
	t := p.peeks[0]
	for i := 1; i < p.npeeks; i++ {
		p.peeks[i-1] = p.peeks[i]
	}
	p.npeeks--
	return t
}

func Parse(lex *Lexer) *Parser {
	return &Parser{
		lex: lex,
	}
}

func (p *Parser) loc() Loc {
	return Loc{p.lex.name, p.lex.lineno}
}

func (p *Parser) errorf(format string, args ...interface{}) {
	log.Panicf("%s: %s", p.loc(), fmt.Sprintf(format, args...))
}

// peek at the nth token
func (p *Parser) peekn(n int) token {
	if n > len(p.peeks) {
		panic("Too much peeking in the Parser")
	}
	for ; p.npeeks < n; p.npeeks++ {
		p.peeks[p.npeeks] = p.lex.token()
	}
	return p.peeks[n-1]
}

func (p *Parser) peek() token {
	return p.peekn(1)
}

func (p *Parser) junk(n int) {
	for i := 0; i < n; i++ {
		p.next()
	}
}

func (p *Parser) accept(typ tokenType) (t token, ok bool) {
	if p.peek().typ == typ {
		t = p.next()
		ok = true
	}
	return
}

func (p *Parser) acceptNamedList(name string) bool {
	if p.peek().typ != tokOpen || p.peekn(2).txt != name {
		return false
	}
	p.junk(2)
	return true
}

func (p *Parser) expect(typ tokenType) token {
	t := p.peek()
	if t.typ != typ {
		p.errorf("expected %v, got %v", typ, t)
	}
	return p.next()
}

func (p *Parser) expectId(s string) token {
	t := p.peek()
	typ := tokId
	if s[0] == ':' {
		typ = tokCid
	} else if s[0] == '?' {
		typ = tokQid
	}
	if t.typ != typ || t.txt != s {
		p.errorf("expected identifier [\"%s\"], got %v", s, t)
	}
	return p.next()
}
<commit_msg>Use copy() instead of doing it with a for loop.<commit_after>package pddl

import (
	"fmt"
	"log"
	. "goplan/lifted"
)

type Parser struct {
	lex    *Lexer
	peeks  [2]token
	npeeks int
}

func (p *Parser) next() token {
	if p.npeeks == 0 {
		return p.lex.token()
	}
	t := p.peeks[0]
	copy(p.peeks[:], p.peeks[1:])
	p.npeeks--
	return t
}

func Parse(lex *Lexer) *Parser {
	return &Parser{
		lex: lex,
	}
}

func (p *Parser) loc() Loc {
	return Loc{p.lex.name, p.lex.lineno}
}

func (p *Parser) errorf(format string, args ...interface{}) {
	log.Panicf("%s: %s", p.loc(), fmt.Sprintf(format, args...))
}

// peek at the nth token
func (p *Parser) peekn(n int) token {
	if n > len(p.peeks) {
		panic("Too much peeking in the Parser")
	}
	for ; p.npeeks < n; p.npeeks++ {
		p.peeks[p.npeeks] = p.lex.token()
	}
	return p.peeks[n-1]
}

func (p *Parser) peek() token {
	return p.peekn(1)
}

func (p *Parser) junk(n int) {
	for i := 0; i < n; i++ {
		p.next()
	}
}

func (p *Parser) accept(typ tokenType) (t token, ok bool) {
	if p.peek().typ == typ {
		t = p.next()
		ok = true
	}
	return
}

func (p *Parser) acceptNamedList(name string) bool {
	if p.peek().typ != tokOpen || p.peekn(2).txt != name {
		return false
	}
	p.junk(2)
	return true
}

func (p *Parser) expect(typ tokenType) token {
	t := p.peek()
	if t.typ != typ {
		p.errorf("expected %v, got %v", typ, t)
	}
	return p.next()
}

func (p *Parser) expectId(s string) token {
	t := p.peek()
	typ := tokId
	if s[0] == ':' {
		typ = tokCid
	} else if s[0] == '?' {
		typ = tokQid
	}
	if t.typ != typ || t.txt != s {
		p.errorf("expected identifier [\"%s\"], got %v", s, t)
	}
	return p.next()
}
<|endoftext|>


<commit_before>// Copyright (c) 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package peer

import (
	"context"
	"fmt"

	"go.uber.org/yarpc/api/peer"
	"go.uber.org/yarpc/api/transport"
	"go.uber.org/yarpc/internal/introspection"
)

// Single implements the Chooser interface for a single peer
type Single struct {
	p   peer.Peer
	err error
}

// NewSingle creates a static Chooser with a single Peer
func NewSingle(pid peer.Identifier, transport peer.Transport) *Single {
	s := &Single{}
	p, err := transport.RetainPeer(pid, s)
	s.p = p
	s.err = err
	return s
}

// Choose returns the single peer
func (s *Single) Choose(context.Context, *transport.Request) (peer.Peer, func(error), error) {
	s.p.StartRequest()
	return s.p, s.onFinish, s.err
}

func (s *Single) onFinish(_ error) {
	s.p.EndRequest()
}

// NotifyStatusChanged receives notifications from the transport when the peer
// connects, disconnects, accepts a request, and so on.
func (s *Single) NotifyStatusChanged(_ peer.Identifier) {
}

// Start is a noop
func (s *Single) Start() error {
	return nil
}

// Stop is a noop
func (s *Single) Stop() error {
	return nil
}

// IsRunning is a noop
func (s *Single) IsRunning() bool {
	return true
}

// Introspect returns a ChooserStatus with a single PeerStatus.
func (s *Single) Introspect() introspection.ChooserStatus {
	peerStatus := s.p.Status()
	peer := introspection.PeerStatus{
		Identifier: s.p.Identifier(),
		State: fmt.Sprintf("%s, %d pending request(s)",
			peerStatus.ConnectionStatus.String(),
			peerStatus.PendingRequestCount),
	}

	return introspection.ChooserStatus{
		Name:  "Single",
		Peers: []introspection.PeerStatus{peer},
	}
}
<commit_msg>Single peer chooser start:retain stop:release (#799)<commit_after>// Copyright (c) 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package peer

import (
	"context"
	"fmt"

	"go.uber.org/yarpc/api/peer"
	"go.uber.org/yarpc/api/transport"
	"go.uber.org/yarpc/internal/introspection"
	intsync "go.uber.org/yarpc/internal/sync"
)

// Single implements the Chooser interface for a single peer
type Single struct {
	once          intsync.LifecycleOnce
	t             peer.Transport
	pid           peer.Identifier
	p             peer.Peer
	err           error
	boundOnFinish func(error)
}

// NewSingle creates a static Chooser with a single Peer
func NewSingle(pid peer.Identifier, transport peer.Transport) *Single {
	s := &Single{
		once: intsync.Once(),
		pid:  pid,
		t:    transport,
	}
	s.boundOnFinish = s.onFinish
	return s
}

// Choose returns the single peer
func (s *Single) Choose(ctx context.Context, _ *transport.Request) (peer.Peer, func(error), error) {
	if err := s.once.WhenRunning(ctx); err != nil {
		return nil, nil, err
	}
	s.p.StartRequest()
	return s.p, s.boundOnFinish, s.err
}

func (s *Single) onFinish(_ error) {
	s.p.EndRequest()
}

// NotifyStatusChanged receives notifications from the transport when the peer
// connects, disconnects, accepts a request, and so on.
func (s *Single) NotifyStatusChanged(_ peer.Identifier) {
}

// Start is a noop
func (s *Single) Start() error {
	return s.once.Start(s.start)
}

func (s *Single) start() error {
	p, err := s.t.RetainPeer(s.pid, s)
	s.p = p
	s.err = err
	return err
}

// Stop is a noop
func (s *Single) Stop() error {
	return s.once.Stop(s.stop)
}

func (s *Single) stop() error {
	return s.t.ReleasePeer(s.pid, s)
}

// IsRunning is a noop
func (s *Single) IsRunning() bool {
	return true
}

// Introspect returns a ChooserStatus with a single PeerStatus.
func (s *Single) Introspect() introspection.ChooserStatus {
	peerStatus := s.p.Status()
	peer := introspection.PeerStatus{
		Identifier: s.p.Identifier(),
		State: fmt.Sprintf("%s, %d pending request(s)",
			peerStatus.ConnectionStatus.String(),
			peerStatus.PendingRequestCount),
	}

	return introspection.ChooserStatus{
		Name:  "Single",
		Peers: []introspection.PeerStatus{peer},
	}
}
<|endoftext|>


<commit_before>package softlayer

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"regexp"
	"time"

	log "github.com/Sirupsen/logrus"
	"github.com/codegangsta/cli"
	"github.com/docker/machine/drivers"
	"github.com/docker/machine/provider"
	"github.com/docker/machine/ssh"
	"github.com/docker/machine/state"
)

const (
	ApiEndpoint = "https://api.softlayer.com/rest/v3"
)

type Driver struct {
	storePath      string
	IPAddress      string
	deviceConfig   *deviceConfig
	Id             int
	Client         *Client
	SSHUser        string
	SSHPort        int
	MachineName    string
	CaCertPath     string
	PrivateKeyPath string
	SwarmMaster    bool
	SwarmHost      string
	SwarmDiscovery string
}

type deviceConfig struct {
	DiskSize      int
	Cpu           int
	Hostname      string
	Domain        string
	Region        string
	Memory        int
	Image         string
	HourlyBilling bool
	LocalDisk     bool
	PrivateNet    bool
}

func init() {
	drivers.Register("softlayer", &drivers.RegisteredDriver{
		New:            NewDriver,
		GetCreateFlags: GetCreateFlags,
	})
}

func NewDriver(machineName string, storePath string, caCert string, privateKey string) (drivers.Driver, error) {
	return &Driver{MachineName: machineName, storePath: storePath, CaCertPath: caCert, PrivateKeyPath: privateKey}, nil
}

func (d *Driver) AuthorizePort(ports []*drivers.Port) error {
	return nil
}

func (d *Driver) DeauthorizePort(ports []*drivers.Port) error {
	return nil
}

func (d *Driver) GetMachineName() string {
	return d.MachineName
}

func (d *Driver) GetSSHHostname() (string, error) {
	return d.GetIP()
}

func (d *Driver) GetSSHKeyPath() string {
	return filepath.Join(d.storePath, "id_rsa")
}

func (d *Driver) GetSSHPort() (int, error) {
	if d.SSHPort == 0 {
		d.SSHPort = 22
	}

	return d.SSHPort, nil
}

func (d *Driver) GetSSHUsername() string {
	if d.SSHUser == "" {
		d.SSHUser = "root"
	}

	return d.SSHUser
}

func (d *Driver) GetProviderType() provider.ProviderType {
	return provider.Remote
}

func GetCreateFlags() []cli.Flag {
	// Set hourly billing to true by default since codegangsta cli doesn't take default bool values
	if os.Getenv("SOFTLAYER_HOURLY_BILLING") == "" {
		os.Setenv("SOFTLAYER_HOURLY_BILLING", "true")
	}
	return []cli.Flag{
		cli.IntFlag{
			EnvVar: "SOFTLAYER_MEMORY",
			Name:   "softlayer-memory",
			Usage:  "Memory in MB for machine",
			Value:  1024,
		},
		cli.IntFlag{
			EnvVar: "SOFTLAYER_DISK_SIZE",
			Name:   "softlayer-disk-size",
			Usage:  "Disk size for machine, a value of 0 uses the default size on softlayer",
			Value:  0,
		},
		cli.StringFlag{
			EnvVar: "SOFTLAYER_USER",
			Name:   "softlayer-user",
			Usage:  "softlayer user account name",
			Value:  "",
		},
		cli.StringFlag{
			EnvVar: "SOFTLAYER_API_KEY",
			Name:   "softlayer-api-key",
			Usage:  "softlayer user API key",
			Value:  "",
		},
		cli.StringFlag{
			EnvVar: "SOFTLAYER_REGION",
			Name:   "softlayer-region",
			Usage:  "softlayer region for machine",
			Value:  "dal01",
		},
		cli.IntFlag{
			EnvVar: "SOFTLAYER_CPU",
			Name:   "softlayer-cpu",
			Usage:  "number of CPU's for the machine",
			Value:  1,
		},
		cli.StringFlag{
			EnvVar: "SOFTLAYER_HOSTNAME",
			Name:   "softlayer-hostname",
			Usage:  "hostname for the machine",
			Value:  "docker",
		},
		cli.StringFlag{
			EnvVar: "SOFTLAYER_DOMAIN",
			Name:   "softlayer-domain",
			Usage:  "domain name for machine",
			Value:  "",
		},
		cli.StringFlag{
			EnvVar: "SOFTLAYER_API_ENDPOINT",
			Name:   "softlayer-api-endpoint",
			Usage:  "softlayer api endpoint to use",
			Value:  ApiEndpoint,
		},
		cli.BoolFlag{
			EnvVar: "SOFTLAYER_HOURLY_BILLING",
			Name:   "softlayer-hourly-billing",
			Usage:  "set hourly billing for machine - on by default",
		},
		cli.BoolFlag{
			EnvVar: "SOFTLAYER_LOCAL_DISK",
			Name:   "softlayer-local-disk",
			Usage:  "use machine local disk instead of softlayer SAN",
		},
		cli.BoolFlag{
			EnvVar: "SOFTLAYER_PRIVATE_NET",
			Name:   "softlayer-private-net-only",
			Usage:  "Use only private networking",
		},
		cli.StringFlag{
			EnvVar: "SOFTLAYER_IMAGE",
			Name:   "softlayer-image",
			Usage:  "OS image for machine",
			Value:  "UBUNTU_LATEST",
		},
	}
}

func validateDeviceConfig(c *deviceConfig) error {
	if c.Hostname == "" {
		return fmt.Errorf("Missing required setting - --softlayer-hostname")
	}
	if c.Domain == "" {
		return fmt.Errorf("Missing required setting - --softlayer-domain")
	}

	if c.Region == "" {
		return fmt.Errorf("Missing required setting - --softlayer-region")
	}
	if c.Cpu < 1 {
		return fmt.Errorf("Missing required setting - --softlayer-cpu")
	}

	return nil
}

func validateClientConfig(c *Client) error {
	if c.ApiKey == "" {
		return fmt.Errorf("Missing required setting - --softlayer-api-key")
	}

	if c.User == "" {
		return fmt.Errorf("Missing required setting - --softlayer-user")
	}

	if c.Endpoint == "" {
		return fmt.Errorf("Missing required setting - --softlayer-api-endpoint")
	}

	return nil
}

func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error {

	d.Client = &Client{
		Endpoint: flags.String("softlayer-api-endpoint"),
		User:     flags.String("softlayer-user"),
		ApiKey:   flags.String("softlayer-api-key"),
	}

	d.SwarmMaster = flags.Bool("swarm-master")
	d.SwarmHost = flags.String("swarm-host")
	d.SwarmDiscovery = flags.String("swarm-discovery")
	d.SSHUser = "root"
	d.SSHPort = 22

	if err := validateClientConfig(d.Client); err != nil {
		return err
	}

	d.deviceConfig = &deviceConfig{
		Hostname:      flags.String("softlayer-hostname"),
		DiskSize:      flags.Int("softlayer-disk-size"),
		Cpu:           flags.Int("softlayer-cpu"),
		Domain:        flags.String("softlayer-domain"),
		Memory:        flags.Int("softlayer-memory"),
		PrivateNet:    flags.Bool("softlayer-private-net-only"),
		LocalDisk:     flags.Bool("softlayer-local-disk"),
		HourlyBilling: flags.Bool("softlayer-hourly-billing"),
		Image:         flags.String("softlayer-image"),
		Region:        flags.String("softlayer-region"),
	}
	return validateDeviceConfig(d.deviceConfig)
}

func (d *Driver) getClient() *Client {
	return d.Client
}

func (d *Driver) DriverName() string {
	return "softlayer"
}

func (d *Driver) GetURL() (string, error) {
	ip, err := d.GetIP()
	if err != nil {
		return "", err
	}
	if ip == "" {
		return "", nil
	}
	return "tcp://" + ip + ":2376", nil
}

func (d *Driver) GetIP() (string, error) {
	if d.IPAddress != "" {
		return d.IPAddress, nil
	}
	return d.getClient().VirtualGuest().GetPublicIp(d.Id)
}

func (d *Driver) GetState() (state.State, error) {
	s, err := d.getClient().VirtualGuest().PowerState(d.Id)
	if err != nil {
		return state.None, err
	}
	var vmState state.State
	switch s {
	case "Running":
		vmState = state.Running
	case "Halted":
		vmState = state.Stopped
	default:
		vmState = state.None
	}
	return vmState, nil
}

func (d *Driver) GetActiveTransaction() (string, error) {
	t, err := d.getClient().VirtualGuest().ActiveTransaction(d.Id)
	if err != nil {
		return "", err
	}
	return t, nil
}

func (d *Driver) PreCreateCheck() error {
	return nil
}

func (d *Driver) waitForStart() {
	log.Infof("Waiting for host to become available")
	for {
		s, err := d.GetState()
		if err != nil {
			log.Debugf("Failed to GetState - %+v", err)
			continue
		}

		if s == state.Running {
			break
		} else {
			log.Debugf("Still waiting - state is %s...", s)
		}
		time.Sleep(2 * time.Second)
	}
}

func (d *Driver) getIp() (string, error) {
	log.Infof("Getting Host IP")
	for {
		var (
			ip  string
			err error
		)
		if d.deviceConfig.PrivateNet {
			ip, err = d.getClient().VirtualGuest().GetPrivateIp(d.Id)
		} else {
			ip, err = d.getClient().VirtualGuest().GetPublicIp(d.Id)
		}
		if err != nil {
			time.Sleep(2 * time.Second)
			continue
		}
		// not a perfect regex, but should be just fine for our needs
		exp := regexp.MustCompile(`\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}`)
		if exp.MatchString(ip) {
			return ip, nil
		}
		time.Sleep(2 * time.Second)
	}
}

func (d *Driver) waitForSetupTransactions() {
	log.Infof("Waiting for host setup transactions to complete")
	// sometimes we'll hit a case where there's no active transaction, but if
	// we check again in a few seconds, it moves to the next transaction. We
	// don't want to get false-positives, so we check a few times in a row to make sure!
	noActiveCount, maxNoActiveCount := 0, 3
	for {
		t, err := d.GetActiveTransaction()
		if err != nil {
			noActiveCount = 0
			log.Debugf("Failed to GetActiveTransaction - %+v", err)
			continue
		}

		if t == "" {
			if noActiveCount == maxNoActiveCount {
				break
			}
			noActiveCount++
		} else {
			noActiveCount = 0
			log.Debugf("Still waiting - active transaction is %s...", t)
		}
		time.Sleep(2 * time.Second)
	}
}

func (d *Driver) Create() error {
	log.Infof("Creating SSH key...")
	key, err := d.createSSHKey()
	if err != nil {
		return err
	}

	spec := d.buildHostSpec()
	spec.SshKeys = []*SshKey{key}

	id, err := d.getClient().VirtualGuest().Create(spec)
	if err != nil {
		return fmt.Errorf("Error creating host: %q", err)
	}
	d.Id = id
	d.getIp()
	d.waitForStart()
	d.waitForSetupTransactions()
	ssh.WaitForTCP(d.IPAddress + ":22")

	cmd, err := drivers.GetSSHCommandFromDriver(d, "sudo apt-get update && DEBIAN_FRONTEND=noninteractive sudo apt-get install -yq curl")
	if err != nil {
		return err

	}
	if err := cmd.Run(); err != nil {
		return err

	}

	return nil
}

func (d *Driver) buildHostSpec() *HostSpec {
	spec := &HostSpec{
		Hostname:       d.deviceConfig.Hostname,
		Domain:         d.deviceConfig.Domain,
		Cpu:            d.deviceConfig.Cpu,
		Memory:         d.deviceConfig.Memory,
		Datacenter:     Datacenter{Name: d.deviceConfig.Region},
		Os:             d.deviceConfig.Image,
		HourlyBilling:  d.deviceConfig.HourlyBilling,
		PrivateNetOnly: d.deviceConfig.PrivateNet,
	}
	if d.deviceConfig.DiskSize > 0 {
		spec.BlockDevices = []BlockDevice{{Device: "0", DiskImage: DiskImage{Capacity: d.deviceConfig.DiskSize}}}
	}
	return spec
}

func (d *Driver) createSSHKey() (*SshKey, error) {
	if err := ssh.GenerateSSHKey(d.GetSSHKeyPath()); err != nil {
		return nil, err
	}

	publicKey, err := ioutil.ReadFile(d.publicSSHKeyPath())
	if err != nil {
		return nil, err
	}

	key, err := d.getClient().SshKey().Create(d.deviceConfig.Hostname, string(publicKey))
	if err != nil {
		return nil, err
	}

	return key, nil
}

func (d *Driver) publicSSHKeyPath() string {
	return d.GetSSHKeyPath() + ".pub"
}

func (d *Driver) Kill() error {
	return d.getClient().VirtualGuest().PowerOff(d.Id)
}
func (d *Driver) Remove() error {
	var err error
	for i := 0; i < 5; i++ {
		if err = d.getClient().VirtualGuest().Cancel(d.Id); err != nil {
			time.Sleep(2 * time.Second)
			continue
		}
		break
	}
	return err
}
func (d *Driver) Restart() error {
	return d.getClient().VirtualGuest().Reboot(d.Id)
}
func (d *Driver) Start() error {
	return d.getClient().VirtualGuest().PowerOn(d.Id)
}
func (d *Driver) Stop() error {
	return d.getClient().VirtualGuest().PowerOff(d.Id)
}
<commit_msg>softlayer: enable local disk in hostspec<commit_after>package softlayer

import (
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"regexp"
	"time"

	log "github.com/Sirupsen/logrus"
	"github.com/codegangsta/cli"
	"github.com/docker/machine/drivers"
	"github.com/docker/machine/provider"
	"github.com/docker/machine/ssh"
	"github.com/docker/machine/state"
)

const (
	ApiEndpoint = "https://api.softlayer.com/rest/v3"
)

type Driver struct {
	storePath      string
	IPAddress      string
	deviceConfig   *deviceConfig
	Id             int
	Client         *Client
	SSHUser        string
	SSHPort        int
	MachineName    string
	CaCertPath     string
	PrivateKeyPath string
	SwarmMaster    bool
	SwarmHost      string
	SwarmDiscovery string
}

type deviceConfig struct {
	DiskSize      int
	Cpu           int
	Hostname      string
	Domain        string
	Region        string
	Memory        int
	Image         string
	HourlyBilling bool
	LocalDisk     bool
	PrivateNet    bool
}

func init() {
	drivers.Register("softlayer", &drivers.RegisteredDriver{
		New:            NewDriver,
		GetCreateFlags: GetCreateFlags,
	})
}

func NewDriver(machineName string, storePath string, caCert string, privateKey string) (drivers.Driver, error) {
	return &Driver{MachineName: machineName, storePath: storePath, CaCertPath: caCert, PrivateKeyPath: privateKey}, nil
}

func (d *Driver) AuthorizePort(ports []*drivers.Port) error {
	return nil
}

func (d *Driver) DeauthorizePort(ports []*drivers.Port) error {
	return nil
}

func (d *Driver) GetMachineName() string {
	return d.MachineName
}

func (d *Driver) GetSSHHostname() (string, error) {
	return d.GetIP()
}

func (d *Driver) GetSSHKeyPath() string {
	return filepath.Join(d.storePath, "id_rsa")
}

func (d *Driver) GetSSHPort() (int, error) {
	if d.SSHPort == 0 {
		d.SSHPort = 22
	}

	return d.SSHPort, nil
}

func (d *Driver) GetSSHUsername() string {
	if d.SSHUser == "" {
		d.SSHUser = "root"
	}

	return d.SSHUser
}

func (d *Driver) GetProviderType() provider.ProviderType {
	return provider.Remote
}

func GetCreateFlags() []cli.Flag {
	// Set hourly billing to true by default since codegangsta cli doesn't take default bool values
	if os.Getenv("SOFTLAYER_HOURLY_BILLING") == "" {
		os.Setenv("SOFTLAYER_HOURLY_BILLING", "true")
	}
	return []cli.Flag{
		cli.IntFlag{
			EnvVar: "SOFTLAYER_MEMORY",
			Name:   "softlayer-memory",
			Usage:  "Memory in MB for machine",
			Value:  1024,
		},
		cli.IntFlag{
			EnvVar: "SOFTLAYER_DISK_SIZE",
			Name:   "softlayer-disk-size",
			Usage:  "Disk size for machine, a value of 0 uses the default size on softlayer",
			Value:  0,
		},
		cli.StringFlag{
			EnvVar: "SOFTLAYER_USER",
			Name:   "softlayer-user",
			Usage:  "softlayer user account name",
			Value:  "",
		},
		cli.StringFlag{
			EnvVar: "SOFTLAYER_API_KEY",
			Name:   "softlayer-api-key",
			Usage:  "softlayer user API key",
			Value:  "",
		},
		cli.StringFlag{
			EnvVar: "SOFTLAYER_REGION",
			Name:   "softlayer-region",
			Usage:  "softlayer region for machine",
			Value:  "dal01",
		},
		cli.IntFlag{
			EnvVar: "SOFTLAYER_CPU",
			Name:   "softlayer-cpu",
			Usage:  "number of CPU's for the machine",
			Value:  1,
		},
		cli.StringFlag{
			EnvVar: "SOFTLAYER_HOSTNAME",
			Name:   "softlayer-hostname",
			Usage:  "hostname for the machine",
			Value:  "docker",
		},
		cli.StringFlag{
			EnvVar: "SOFTLAYER_DOMAIN",
			Name:   "softlayer-domain",
			Usage:  "domain name for machine",
			Value:  "",
		},
		cli.StringFlag{
			EnvVar: "SOFTLAYER_API_ENDPOINT",
			Name:   "softlayer-api-endpoint",
			Usage:  "softlayer api endpoint to use",
			Value:  ApiEndpoint,
		},
		cli.BoolFlag{
			EnvVar: "SOFTLAYER_HOURLY_BILLING",
			Name:   "softlayer-hourly-billing",
			Usage:  "set hourly billing for machine - on by default",
		},
		cli.BoolFlag{
			EnvVar: "SOFTLAYER_LOCAL_DISK",
			Name:   "softlayer-local-disk",
			Usage:  "use machine local disk instead of softlayer SAN",
		},
		cli.BoolFlag{
			EnvVar: "SOFTLAYER_PRIVATE_NET",
			Name:   "softlayer-private-net-only",
			Usage:  "Use only private networking",
		},
		cli.StringFlag{
			EnvVar: "SOFTLAYER_IMAGE",
			Name:   "softlayer-image",
			Usage:  "OS image for machine",
			Value:  "UBUNTU_LATEST",
		},
	}
}

func validateDeviceConfig(c *deviceConfig) error {
	if c.Hostname == "" {
		return fmt.Errorf("Missing required setting - --softlayer-hostname")
	}
	if c.Domain == "" {
		return fmt.Errorf("Missing required setting - --softlayer-domain")
	}

	if c.Region == "" {
		return fmt.Errorf("Missing required setting - --softlayer-region")
	}
	if c.Cpu < 1 {
		return fmt.Errorf("Missing required setting - --softlayer-cpu")
	}

	return nil
}

func validateClientConfig(c *Client) error {
	if c.ApiKey == "" {
		return fmt.Errorf("Missing required setting - --softlayer-api-key")
	}

	if c.User == "" {
		return fmt.Errorf("Missing required setting - --softlayer-user")
	}

	if c.Endpoint == "" {
		return fmt.Errorf("Missing required setting - --softlayer-api-endpoint")
	}

	return nil
}

func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error {

	d.Client = &Client{
		Endpoint: flags.String("softlayer-api-endpoint"),
		User:     flags.String("softlayer-user"),
		ApiKey:   flags.String("softlayer-api-key"),
	}

	d.SwarmMaster = flags.Bool("swarm-master")
	d.SwarmHost = flags.String("swarm-host")
	d.SwarmDiscovery = flags.String("swarm-discovery")
	d.SSHUser = "root"
	d.SSHPort = 22

	if err := validateClientConfig(d.Client); err != nil {
		return err
	}

	d.deviceConfig = &deviceConfig{
		Hostname:      flags.String("softlayer-hostname"),
		DiskSize:      flags.Int("softlayer-disk-size"),
		Cpu:           flags.Int("softlayer-cpu"),
		Domain:        flags.String("softlayer-domain"),
		Memory:        flags.Int("softlayer-memory"),
		PrivateNet:    flags.Bool("softlayer-private-net-only"),
		LocalDisk:     flags.Bool("softlayer-local-disk"),
		HourlyBilling: flags.Bool("softlayer-hourly-billing"),
		Image:         flags.String("softlayer-image"),
		Region:        flags.String("softlayer-region"),
	}
	return validateDeviceConfig(d.deviceConfig)
}

func (d *Driver) getClient() *Client {
	return d.Client
}

func (d *Driver) DriverName() string {
	return "softlayer"
}

func (d *Driver) GetURL() (string, error) {
	ip, err := d.GetIP()
	if err != nil {
		return "", err
	}
	if ip == "" {
		return "", nil
	}
	return "tcp://" + ip + ":2376", nil
}

func (d *Driver) GetIP() (string, error) {
	if d.IPAddress != "" {
		return d.IPAddress, nil
	}
	return d.getClient().VirtualGuest().GetPublicIp(d.Id)
}

func (d *Driver) GetState() (state.State, error) {
	s, err := d.getClient().VirtualGuest().PowerState(d.Id)
	if err != nil {
		return state.None, err
	}
	var vmState state.State
	switch s {
	case "Running":
		vmState = state.Running
	case "Halted":
		vmState = state.Stopped
	default:
		vmState = state.None
	}
	return vmState, nil
}

func (d *Driver) GetActiveTransaction() (string, error) {
	t, err := d.getClient().VirtualGuest().ActiveTransaction(d.Id)
	if err != nil {
		return "", err
	}
	return t, nil
}

func (d *Driver) PreCreateCheck() error {
	return nil
}

func (d *Driver) waitForStart() {
	log.Infof("Waiting for host to become available")
	for {
		s, err := d.GetState()
		if err != nil {
			log.Debugf("Failed to GetState - %+v", err)
			continue
		}

		if s == state.Running {
			break
		} else {
			log.Debugf("Still waiting - state is %s...", s)
		}
		time.Sleep(2 * time.Second)
	}
}

func (d *Driver) getIp() (string, error) {
	log.Infof("Getting Host IP")
	for {
		var (
			ip  string
			err error
		)
		if d.deviceConfig.PrivateNet {
			ip, err = d.getClient().VirtualGuest().GetPrivateIp(d.Id)
		} else {
			ip, err = d.getClient().VirtualGuest().GetPublicIp(d.Id)
		}
		if err != nil {
			time.Sleep(2 * time.Second)
			continue
		}
		// not a perfect regex, but should be just fine for our needs
		exp := regexp.MustCompile(`\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}`)
		if exp.MatchString(ip) {
			return ip, nil
		}
		time.Sleep(2 * time.Second)
	}
}

func (d *Driver) waitForSetupTransactions() {
	log.Infof("Waiting for host setup transactions to complete")
	// sometimes we'll hit a case where there's no active transaction, but if
	// we check again in a few seconds, it moves to the next transaction. We
	// don't want to get false-positives, so we check a few times in a row to make sure!
	noActiveCount, maxNoActiveCount := 0, 3
	for {
		t, err := d.GetActiveTransaction()
		if err != nil {
			noActiveCount = 0
			log.Debugf("Failed to GetActiveTransaction - %+v", err)
			continue
		}

		if t == "" {
			if noActiveCount == maxNoActiveCount {
				break
			}
			noActiveCount++
		} else {
			noActiveCount = 0
			log.Debugf("Still waiting - active transaction is %s...", t)
		}
		time.Sleep(2 * time.Second)
	}
}

func (d *Driver) Create() error {
	log.Infof("Creating SSH key...")
	key, err := d.createSSHKey()
	if err != nil {
		return err
	}

	spec := d.buildHostSpec()
	spec.SshKeys = []*SshKey{key}

	id, err := d.getClient().VirtualGuest().Create(spec)
	if err != nil {
		return fmt.Errorf("Error creating host: %q", err)
	}
	d.Id = id
	d.getIp()
	d.waitForStart()
	d.waitForSetupTransactions()
	ssh.WaitForTCP(d.IPAddress + ":22")

	cmd, err := drivers.GetSSHCommandFromDriver(d, "sudo apt-get update && DEBIAN_FRONTEND=noninteractive sudo apt-get install -yq curl")
	if err != nil {
		return err

	}
	if err := cmd.Run(); err != nil {
		return err

	}

	return nil
}

func (d *Driver) buildHostSpec() *HostSpec {
	spec := &HostSpec{
		Hostname:       d.deviceConfig.Hostname,
		Domain:         d.deviceConfig.Domain,
		Cpu:            d.deviceConfig.Cpu,
		Memory:         d.deviceConfig.Memory,
		Datacenter:     Datacenter{Name: d.deviceConfig.Region},
		Os:             d.deviceConfig.Image,
		HourlyBilling:  d.deviceConfig.HourlyBilling,
		PrivateNetOnly: d.deviceConfig.PrivateNet,
		LocalDisk:      d.deviceConfig.LocalDisk,
	}
	if d.deviceConfig.DiskSize > 0 {
		spec.BlockDevices = []BlockDevice{{Device: "0", DiskImage: DiskImage{Capacity: d.deviceConfig.DiskSize}}}
	}
	return spec
}

func (d *Driver) createSSHKey() (*SshKey, error) {
	if err := ssh.GenerateSSHKey(d.GetSSHKeyPath()); err != nil {
		return nil, err
	}

	publicKey, err := ioutil.ReadFile(d.publicSSHKeyPath())
	if err != nil {
		return nil, err
	}

	key, err := d.getClient().SshKey().Create(d.deviceConfig.Hostname, string(publicKey))
	if err != nil {
		return nil, err
	}

	return key, nil
}

func (d *Driver) publicSSHKeyPath() string {
	return d.GetSSHKeyPath() + ".pub"
}

func (d *Driver) Kill() error {
	return d.getClient().VirtualGuest().PowerOff(d.Id)
}
func (d *Driver) Remove() error {
	var err error
	for i := 0; i < 5; i++ {
		if err = d.getClient().VirtualGuest().Cancel(d.Id); err != nil {
			time.Sleep(2 * time.Second)
			continue
		}
		break
	}
	return err
}
func (d *Driver) Restart() error {
	return d.getClient().VirtualGuest().Reboot(d.Id)
}
func (d *Driver) Start() error {
	return d.getClient().VirtualGuest().PowerOn(d.Id)
}
func (d *Driver) Stop() error {
	return d.getClient().VirtualGuest().PowerOff(d.Id)
}
<|endoftext|>


<commit_before>// Copyright 2014 PagerDuty, Inc.
// All rights reserved - Do not redistribute!

package main

import (
	"bytes"
	"fmt"
	"os"
	"os/exec"
	"path"
	"regexp"
	"strings"
	"syscall"
	"time"

	"github.com/PagerDuty/godspeed"
	"github.com/codeskyblue/go-uuid"
	"github.com/jessevdk/go-flags"
	"github.com/nightlyone/lockfile"
	"github.com/tideland/goas/v3/logger"
)

// MaxBody is the maximum length of a event body
const MaxBody = 4096
const intErrCode = 200

// args is for argument parsing
type args struct {
	Label     string `short:"l" long:"label" default:"" description:"name for cron job to be used in statsd emissions and DogStatsd events. alphanumeric only; cronner will lowercase it"`
	Cmd       string `short:"c" long:"command" default:"" description:"command to run (please use full path) and its args; executed as user running cronner"`
	AllEvents bool   `short:"e" long:"event" default:"false" description:"emit a start and end datadog event"`
	FailEvent bool   `short:"E" long:"event-fail" default:"false" description:"only emit an event on failure"`
	LogOnFail bool   `short:"F" long:"log-on-fail" default:"false" description:"when a command fails, log its full output (stdout/stderr) to the log directory using the UUID as the filename"`
	LogPath   string `long:"log-path" default:"/var/log/cronner/" description:"where to place the log files for command output (path for -l/--log-on-fail output)"`
	LogLevel  string `short:"L" long:"log-level" default:"error" description:"set the level at which to log at [none|error|info|debug]"`
	Sensitive bool   `short:"s" long:"sensitive" default:"false" description:"specify whether command output may contain sensitive details, this only avoids it being printed to stderr"`
	Lock      bool   `short:"k" long:"lock" default:"false" description:"lock based on label so that multiple commands with the same label can not run concurrently"`
	LockDir   string `short:"d" long:"lock-dir" default:"/var/lock" description:"the directory where lock files will be places"`
}

// parse function configures the go-flags parser and runs it
// it also does some light input validation
func (a *args) parse() error {
	p := flags.NewParser(a, flags.HelpFlag|flags.PassDoubleDash)

	_, err := p.Parse()

	// determine if there was a parsing error
	// unfortunately, help message is returned as an error
	if err != nil {
		if !strings.Contains(err.Error(), "Usage") {
			logger.Errorf("error: %v\n", err)
			os.Exit(1)
		} else {
			fmt.Printf("%v\n", err.Error())
			os.Exit(0)
		}
	}

	r := regexp.MustCompile("^[a-zA-Z0-9_\\.]+$")

	if !r.MatchString(a.Label) {
		return fmt.Errorf("cron label '%v' is invalid, it can only be alphanumeric with underscores and periods", a.Label)
	}

	if a.Cmd == "" {
		return fmt.Errorf("you must specify a command to run")
	}

	// lowercase the metric to try and encourage sanity
	a.Label = strings.ToLower(a.Label)

	var logLevel logger.LogLevel

	switch strings.ToLower(a.LogLevel) {
	case "none":
		logLevel = logger.LevelFatal
	case "error":
		logLevel = logger.LevelError
	case "info":
		logLevel = logger.LevelInfo
	case "debug":
		logLevel = logger.LevelDebug
	default:
		return fmt.Errorf("%v is not a known log level, try none, debug, info, or error", a.LogLevel)
	}
	logger.SetLevel(logLevel)

	return nil
}

func withLock(cmd *exec.Cmd, label string, gs *godspeed.Godspeed, lock bool, lockDir string) (int, float64, error) {
	var lf lockfile.Lockfile
	if lock {
		lockPath := path.Join(lockDir, fmt.Sprintf("cronner-%v.lock", label))

		lf, err := lockfile.New(lockPath)
		if err != nil {
			logger.Criticalf("Cannot init lock. reason: %v", err)
			return intErrCode, 0, err
		}

		err = lf.TryLock()
		if err != nil {
			logger.Criticalf("Cannot lock. reason: %v", err)
			return intErrCode, 0, err
		}
	}

	// log start time
	s := time.Now().UTC()

	cmdErr := cmd.Run()

	// This next section computes the wallclock run time in ms.
	// However, there is the unfortunate limitation in that
	// it uses the clock that gets adjusted by ntpd. Within pure
	// Go, I don't have access to CLOCK_MONOTONIC_RAW.
	//
	// However, based on our usage I don't think we care about it
	// being off by a few milliseconds.
	t := time.Since(s).Seconds() * 1000

	if lock {
		if err := lf.Unlock(); err != nil {
			logger.Criticalf("Cannot unlock. reason: %v", err)
			return intErrCode, t, err
		}
	}

	var ret int

	if cmdErr != nil {
		if ee, ok := cmdErr.(*exec.ExitError); ok {
			status := ee.Sys().(syscall.WaitStatus)
			ret = status.ExitStatus()
		} else {
			ret = intErrCode
		}
	}

	return ret, t, cmdErr
}

func runCommand(cmd *exec.Cmd, label string, gs *godspeed.Godspeed, lock bool, lockDir string) (int, []byte, float64, error) {
	var b bytes.Buffer

	// comnbine stdout and stderr to the same buffer
	cmd.Stdout = &b
	cmd.Stderr = &b

	ret, t, err := withLock(cmd, label, gs, lock, lockDir)

	// emit the metric for how long it took us and return code
	gs.Timing(fmt.Sprintf("cron.%v.time", label), t, nil)
	gs.Gauge(fmt.Sprintf("cron.%v.exit_code", label), float64(ret), nil)

	return ret, b.Bytes(), t, err
}

// emit a godspeed (dogstatsd) event
func emitEvent(title, body, label, alertType, uuidStr string, g *godspeed.Godspeed) {
	var buf bytes.Buffer

	// if the event's body is bigger than MaxBody
	if len(body) > MaxBody {
		// push the first MaxBody/2 bytes in to the buffer
		buf.WriteString(body[0 : MaxBody/2])

		// add indication of truncated output to the buffer
		buf.WriteString("...\n=== OUTPUT TRUNCATED ===\n")

		// add the last 1024 bytes to the buffer
		buf.WriteString(body[len(body)-((MaxBody/2)+1) : len(body)-1])

		body = string(buf.Bytes())
	}

	fields := make(map[string]string)
	fields["source_type_name"] = "cron"

	if len(alertType) > 0 {
		fields["alert_type"] = alertType
	}

	if len(uuidStr) > 0 {
		fields["aggregation_key"] = uuidStr
	}

	tags := []string{"source_type:cron", fmt.Sprintf("label_name:%v", label)}

	g.Event(title, body, fields, tags)
}

func main() {
	logger.SetLogger(logger.NewStandardLogger(os.Stderr))

	// get and parse the command line options
	opts := &args{}
	err := opts.parse()

	// make sure parsing didn't bomb
	if err != nil {
		logger.Errorf("error: %v\n", err)
		os.Exit(1)
	}

	// build a Godspeed client
	gs, err := godspeed.NewDefault()

	// make sure nothing went wrong with Godspeed
	if err != nil {
		logger.Errorf("error: %v\n", err)
		os.Exit(1)
	}

	gs.SetNamespace("pagerduty")

	// get the hostname and validate nothing happened
	hostname, err := os.Hostname()

	if err != nil {
		logger.Errorf("error: %v\n", err)
		os.Exit(1)
	}

	// split the command in to its binary and arguments
	cmdParts := strings.Split(opts.Cmd, " ")

	// build the args slice
	var args []string
	if len(cmdParts) > 1 {
		args = cmdParts[1:]
	}

	// get the *exec.Cmd instance
	cmd := exec.Command(cmdParts[0], args...)

	uuidStr := uuid.New()

	if opts.AllEvents {
		// emit a DD event to indicate we are starting the job
		emitEvent(fmt.Sprintf("Cron %v starting on %v", opts.Label, hostname), fmt.Sprintf("UUID:%v\n", uuidStr), opts.Label, "info", uuidStr, gs)
	}

	// run the command and return the output as well as the return status
	ret, out, wallRtMs, err := runCommand(cmd, opts.Label, gs, opts.Lock, opts.LockDir)

	// default variables are for success
	// we change them later if there was a failure
	msg := "succeeded"
	alertType := "success"

	// if the command failed change the state variables to their failure values
	if err != nil {
		msg = "failed"
		alertType = "error"
	}

	if opts.AllEvents || (opts.FailEvent && alertType == "error") {
		// build the pieces of the completion event
		title := fmt.Sprintf("Cron %v %v in %.5f seconds on %v", opts.Label, msg, wallRtMs/1000, hostname)

		body := fmt.Sprintf("UUID: %v\nexit code: %d\n", uuidStr, ret)
		if err != nil {
			er := regexp.MustCompile("^exit status ([-]?\\d)")

			// do not show the 'more:' line, if the line is just telling us
			// what the exit code is
			if !er.MatchString(err.Error()) {
				body = fmt.Sprintf("%vmore: %v\n", body, err.Error())
			}
		}

		var cmdOutput string

		if len(out) > 0 {
			cmdOutput = string(out)
		} else {
			cmdOutput = "(none)"
		}

		body = fmt.Sprintf("%voutput: %v", body, cmdOutput)

		emitEvent(title, body, opts.Label, alertType, uuidStr, gs)
	}

	// this code block is meant to be ran last
	if alertType == "error" && opts.LogOnFail {
		filename := path.Join(opts.LogPath, fmt.Sprintf("%v-%v.out", opts.Label, uuidStr))
		if !saveOutput(filename, out, opts.Sensitive) {
			os.Exit(1)
		}
	}
}

// bailOut is for failures during logfile writing
func bailOut(out []byte, sensitive bool) bool {
	if !sensitive {
		fmt.Fprintf(os.Stderr, "here is the output in hopes you are looking here:\n\n%v", string(out))
		os.Exit(1)
	}
	return false
}

// saveOutput saves the output (out) to the file specified
func saveOutput(filename string, out []byte, sensitive bool) bool {
	// check to see whehter or not the output file already exists
	// this should really never happen, but just in case it does...
	if _, err := os.Stat(filename); !os.IsNotExist(err) {
		fmt.Fprintf(os.Stderr, "flagrant error: output file '%v' already exists\n", filename)
		return bailOut(out, sensitive)
	}

	outFile, err := os.Create(filename)

	if err != nil {
		fmt.Fprintf(os.Stderr, "error opening file to save command output: %v\n", err.Error())
		return bailOut(out, sensitive)
	}

	defer outFile.Close()

	if err = outFile.Chmod(0400); err != nil {
		fmt.Fprintf(os.Stderr, "error setting permissions (0400) on file '%v': %v\n", filename, err.Error())
		return bailOut(out, sensitive)
	}

	nwrt, err := outFile.Write(out)

	if err != nil {
		fmt.Fprintf(os.Stderr, "error writing to file '%v': %v\n", filename, err.Error())
		return bailOut(out, sensitive)
	}

	if nwrt != len(out) {
		fmt.Fprintf(os.Stderr, "error writing to file '%v': number of bytes written not equal to output (total: %d, written: %d)\n", filename, len(out), nwrt)
		return bailOut(out, sensitive)
	}

	return true
}
<commit_msg>add ability to accept command as positional arguments<commit_after>// Copyright 2014 PagerDuty, Inc.
// All rights reserved - Do not redistribute!

package main

import (
	"bytes"
	"fmt"
	"os"
	"os/exec"
	"path"
	"regexp"
	"strings"
	"syscall"
	"time"

	"github.com/PagerDuty/godspeed"
	"github.com/codeskyblue/go-uuid"
	"github.com/jessevdk/go-flags"
	"github.com/nightlyone/lockfile"
	"github.com/tideland/goas/v3/logger"
)

// MaxBody is the maximum length of a event body
const MaxBody = 4096
const intErrCode = 200

// args is for argument parsing
type args struct {
	Label     string `short:"l" long:"label" default:"" description:"name for cron job to be used in statsd emissions and DogStatsd events. alphanumeric only; cronner will lowercase it"`
	Cmd       string `short:"c" long:"command" default:"" description:"command to run (please use full path) and its args; executed as user running cronner"`
	AllEvents bool   `short:"e" long:"event" default:"false" description:"emit a start and end datadog event"`
	FailEvent bool   `short:"E" long:"event-fail" default:"false" description:"only emit an event on failure"`
	LogOnFail bool   `short:"F" long:"log-on-fail" default:"false" description:"when a command fails, log its full output (stdout/stderr) to the log directory using the UUID as the filename"`
	LogPath   string `long:"log-path" default:"/var/log/cronner/" description:"where to place the log files for command output (path for -l/--log-on-fail output)"`
	LogLevel  string `short:"L" long:"log-level" default:"error" description:"set the level at which to log at [none|error|info|debug]"`
	Sensitive bool   `short:"s" long:"sensitive" default:"false" description:"specify whether command output may contain sensitive details, this only avoids it being printed to stderr"`
	Lock      bool   `short:"k" long:"lock" default:"false" description:"lock based on label so that multiple commands with the same label can not run concurrently"`
	LockDir   string `short:"d" long:"lock-dir" default:"/var/lock" description:"the directory where lock files will be places"`
}

// parse function configures the go-flags parser and runs it
// it also does some light input validation
func (a *args) parse() error {
	p := flags.NewParser(a, flags.HelpFlag|flags.PassDoubleDash)

	leftOvers, err := p.Parse()

	// determine if there was a parsing error
	// unfortunately, help message is returned as an error
	if err != nil {
		if !strings.Contains(err.Error(), "Usage") {
			logger.Errorf("error: %v\n", err)
			os.Exit(1)
		} else {
			fmt.Printf("%v\n", err.Error())
			os.Exit(0)
		}
	}

	r := regexp.MustCompile("^[a-zA-Z0-9_\\.]+$")

	if !r.MatchString(a.Label) {
		return fmt.Errorf("cron label '%v' is invalid, it can only be alphanumeric with underscores and periods", a.Label)
	}

	if a.Cmd == "" {
		if len(leftOvers) == 0 {
			return fmt.Errorf("you must specify a command to run either using by adding it to the end, or using the command flag")
		}
		a.Cmd = strings.Join(leftOvers, " ")
	}

	// lowercase the metric to try and encourage sanity
	a.Label = strings.ToLower(a.Label)

	var logLevel logger.LogLevel

	switch strings.ToLower(a.LogLevel) {
	case "none":
		logLevel = logger.LevelFatal
	case "error":
		logLevel = logger.LevelError
	case "info":
		logLevel = logger.LevelInfo
	case "debug":
		logLevel = logger.LevelDebug
	default:
		return fmt.Errorf("%v is not a known log level, try none, debug, info, or error", a.LogLevel)
	}
	logger.SetLevel(logLevel)

	return nil
}

func withLock(cmd *exec.Cmd, label string, gs *godspeed.Godspeed, lock bool, lockDir string) (int, float64, error) {
	var lf lockfile.Lockfile
	if lock {
		lockPath := path.Join(lockDir, fmt.Sprintf("cronner-%v.lock", label))

		lf, err := lockfile.New(lockPath)
		if err != nil {
			logger.Criticalf("Cannot init lock. reason: %v", err)
			return intErrCode, 0, err
		}

		err = lf.TryLock()
		if err != nil {
			logger.Criticalf("Cannot lock. reason: %v", err)
			return intErrCode, 0, err
		}
	}

	// log start time
	s := time.Now().UTC()

	cmdErr := cmd.Run()

	// This next section computes the wallclock run time in ms.
	// However, there is the unfortunate limitation in that
	// it uses the clock that gets adjusted by ntpd. Within pure
	// Go, I don't have access to CLOCK_MONOTONIC_RAW.
	//
	// However, based on our usage I don't think we care about it
	// being off by a few milliseconds.
	t := time.Since(s).Seconds() * 1000

	if lock {
		if err := lf.Unlock(); err != nil {
			logger.Criticalf("Cannot unlock. reason: %v", err)
			return intErrCode, t, err
		}
	}

	var ret int

	if cmdErr != nil {
		if ee, ok := cmdErr.(*exec.ExitError); ok {
			status := ee.Sys().(syscall.WaitStatus)
			ret = status.ExitStatus()
		} else {
			ret = intErrCode
		}
	}

	return ret, t, cmdErr
}

func runCommand(cmd *exec.Cmd, label string, gs *godspeed.Godspeed, lock bool, lockDir string) (int, []byte, float64, error) {
	var b bytes.Buffer

	// comnbine stdout and stderr to the same buffer
	cmd.Stdout = &b
	cmd.Stderr = &b

	ret, t, err := withLock(cmd, label, gs, lock, lockDir)

	// emit the metric for how long it took us and return code
	gs.Timing(fmt.Sprintf("cron.%v.time", label), t, nil)
	gs.Gauge(fmt.Sprintf("cron.%v.exit_code", label), float64(ret), nil)

	return ret, b.Bytes(), t, err
}

// emit a godspeed (dogstatsd) event
func emitEvent(title, body, label, alertType, uuidStr string, g *godspeed.Godspeed) {
	var buf bytes.Buffer

	// if the event's body is bigger than MaxBody
	if len(body) > MaxBody {
		// push the first MaxBody/2 bytes in to the buffer
		buf.WriteString(body[0 : MaxBody/2])

		// add indication of truncated output to the buffer
		buf.WriteString("...\n=== OUTPUT TRUNCATED ===\n")

		// add the last 1024 bytes to the buffer
		buf.WriteString(body[len(body)-((MaxBody/2)+1) : len(body)-1])

		body = string(buf.Bytes())
	}

	fields := make(map[string]string)
	fields["source_type_name"] = "cron"

	if len(alertType) > 0 {
		fields["alert_type"] = alertType
	}

	if len(uuidStr) > 0 {
		fields["aggregation_key"] = uuidStr
	}

	tags := []string{"source_type:cron", fmt.Sprintf("label_name:%v", label)}

	g.Event(title, body, fields, tags)
}

func main() {
	logger.SetLogger(logger.NewStandardLogger(os.Stderr))

	// get and parse the command line options
	opts := &args{}
	err := opts.parse()

	// make sure parsing didn't bomb
	if err != nil {
		logger.Errorf("error: %v\n", err)
		os.Exit(1)
	}

	// build a Godspeed client
	gs, err := godspeed.NewDefault()

	// make sure nothing went wrong with Godspeed
	if err != nil {
		logger.Errorf("error: %v\n", err)
		os.Exit(1)
	}

	gs.SetNamespace("pagerduty")

	// get the hostname and validate nothing happened
	hostname, err := os.Hostname()

	if err != nil {
		logger.Errorf("error: %v\n", err)
		os.Exit(1)
	}

	// split the command in to its binary and arguments
	cmdParts := strings.Split(opts.Cmd, " ")

	// build the args slice
	var args []string
	if len(cmdParts) > 1 {
		args = cmdParts[1:]
	}

	// get the *exec.Cmd instance
	cmd := exec.Command(cmdParts[0], args...)

	uuidStr := uuid.New()

	if opts.AllEvents {
		// emit a DD event to indicate we are starting the job
		emitEvent(fmt.Sprintf("Cron %v starting on %v", opts.Label, hostname), fmt.Sprintf("UUID:%v\n", uuidStr), opts.Label, "info", uuidStr, gs)
	}

	// run the command and return the output as well as the return status
	ret, out, wallRtMs, err := runCommand(cmd, opts.Label, gs, opts.Lock, opts.LockDir)

	// default variables are for success
	// we change them later if there was a failure
	msg := "succeeded"
	alertType := "success"

	// if the command failed change the state variables to their failure values
	if err != nil {
		msg = "failed"
		alertType = "error"
	}

	if opts.AllEvents || (opts.FailEvent && alertType == "error") {
		// build the pieces of the completion event
		title := fmt.Sprintf("Cron %v %v in %.5f seconds on %v", opts.Label, msg, wallRtMs/1000, hostname)

		body := fmt.Sprintf("UUID: %v\nexit code: %d\n", uuidStr, ret)
		if err != nil {
			er := regexp.MustCompile("^exit status ([-]?\\d)")

			// do not show the 'more:' line, if the line is just telling us
			// what the exit code is
			if !er.MatchString(err.Error()) {
				body = fmt.Sprintf("%vmore: %v\n", body, err.Error())
			}
		}

		var cmdOutput string

		if len(out) > 0 {
			cmdOutput = string(out)
		} else {
			cmdOutput = "(none)"
		}

		body = fmt.Sprintf("%voutput: %v", body, cmdOutput)

		emitEvent(title, body, opts.Label, alertType, uuidStr, gs)
	}

	// this code block is meant to be ran last
	if alertType == "error" && opts.LogOnFail {
		filename := path.Join(opts.LogPath, fmt.Sprintf("%v-%v.out", opts.Label, uuidStr))
		if !saveOutput(filename, out, opts.Sensitive) {
			os.Exit(1)
		}
	}
}

// bailOut is for failures during logfile writing
func bailOut(out []byte, sensitive bool) bool {
	if !sensitive {
		fmt.Fprintf(os.Stderr, "here is the output in hopes you are looking here:\n\n%v", string(out))
		os.Exit(1)
	}
	return false
}

// saveOutput saves the output (out) to the file specified
func saveOutput(filename string, out []byte, sensitive bool) bool {
	// check to see whehter or not the output file already exists
	// this should really never happen, but just in case it does...
	if _, err := os.Stat(filename); !os.IsNotExist(err) {
		fmt.Fprintf(os.Stderr, "flagrant error: output file '%v' already exists\n", filename)
		return bailOut(out, sensitive)
	}

	outFile, err := os.Create(filename)

	if err != nil {
		fmt.Fprintf(os.Stderr, "error opening file to save command output: %v\n", err.Error())
		return bailOut(out, sensitive)
	}

	defer outFile.Close()

	if err = outFile.Chmod(0400); err != nil {
		fmt.Fprintf(os.Stderr, "error setting permissions (0400) on file '%v': %v\n", filename, err.Error())
		return bailOut(out, sensitive)
	}

	nwrt, err := outFile.Write(out)

	if err != nil {
		fmt.Fprintf(os.Stderr, "error writing to file '%v': %v\n", filename, err.Error())
		return bailOut(out, sensitive)
	}

	if nwrt != len(out) {
		fmt.Fprintf(os.Stderr, "error writing to file '%v': number of bytes written not equal to output (total: %d, written: %d)\n", filename, len(out), nwrt)
		return bailOut(out, sensitive)
	}

	return true
}
<|endoftext|>