diff --git a/container.go b/container.go new file mode 100644 index 0000000..9758977 --- /dev/null +++ b/container.go @@ -0,0 +1,263 @@ +package main + +import ( + "database/sql" + "encoding/json" + "fmt" + + "dim/query" + "dim/types" + "github.com/lib/pq" +) + +// ContainerCreate will create a new container. +func containerCreate(c *Context, req Request, res *Response) error { + subnet := types.Subnet{} + options := struct { + Attributes types.FieldMap `json:"attributes"` + DisallowChildren bool `json:"disallow_children"` + Layer3Domain string `json:"layer3domain"` + AllowOverlap bool `json:"allow_overlap"` + }{ + Attributes: types.FieldMap{}, + } + if err := req.ParseAtLeast(1, &subnet, &options); err != nil { + res.AddMessage(LevelError, "could not parse options: %s", err) + return nil + } + if options.Layer3Domain == "" { + res.AddMessage(LevelError, "layer3domain name is empty") + return nil + } + attrs, err := options.Attributes.MarshalJSON() + if err != nil { + res.AddMessage(LevelError, "could not encode attributes to json: %s", err) + return nil + } + + l3id := 0 + err = c.tx.QueryRow(`select id from layer3domains where name = $1`, options.Layer3Domain).Scan(&l3id) + if err != nil { + res.AddMessage(LevelError, "could not resolve layer3domain") + return fmt.Errorf("could not resolve layer3domain '%s': %#v", options.Layer3Domain, err) + } + + _, err = c.tx.Exec(`insert into containers(layer3domain_id, subnet, created_by, modified_by, attributes) + values ($1, $2, $3, $3, $4)`, l3id, subnet.String(), c.username, attrs) + if err != nil { + res.AddMessage(LevelError, "could not create new container") + return fmt.Errorf("could not create new container '%s': %#v", subnet.String(), err) + } + res.AddMessage(LevelInfo, "created container '%s'", subnet.String()) + return nil +} + +// ContainerDelete deletes a container. +func containerDelete(c *Context, req Request, res *Response) error { + subnet := types.Subnet{} + options := struct { + Layer3Domain string `json:"layer3domain"` + Recursive bool `json:"recursive"` + }{} + if err := req.ParseAtLeast(1, &subnet, &options); err != nil { + res.AddMessage(LevelError, "could not parse options: %s", err) + return nil + } + + l3id := 0 + err := c.tx.QueryRow(`select id from layer3domains where name = $1 for update`, options.Layer3Domain). + Scan(&l3id) + if err != nil { + if err == sql.ErrNoRows { + res.AddMessage(LevelError, "layer3domain '%s' does not exist", options.Layer3Domain) + return nil + } + res.AddMessage(LevelError, "could not fetch layer3domain '%s'", options.Layer3Domain) + return fmt.Errorf("could not fetch layer3domain '%s': %#v", options.Layer3Domain, err) + } + // TODO implement Recursive (delete all containers and IPs in this subnet) + _, err = c.tx.Exec(`delete from containers where subnet = $1 and layer3domain_id = $2`, subnet.String(), l3id) + if err != nil { + res.AddMessage(LevelError, "could not delete container '%s' in layer3domain '%s'", subnet.String(), options.Layer3Domain) + return fmt.Errorf("could not delete subnets for container '%s': %#v", subnet.String(), err) + } + res.AddMessage(LevelInfo, "deleted container '%s' in layer3domain '%s'", subnet.String(), options.Layer3Domain) + return nil +} + +// ContainerList lists all containers with the requested fields. +func containerList(c *Context, req Request, res *Response) error { + options := struct { + Attributes types.FieldList `json:"attributes"` + Layer3Domain string `json:"layer3domain"` + IPBlock types.Subnet `json:"ipblock"` + IPVersion types.IPVersion `json:"version"` + Depth int `json:"depth"` + Status string `json:"status"` // TODO not supported yet + }{ + Attributes: types.NewFieldList("subnet", "", "subnets"), + } + if err := req.ParseAtLeast(0, &options); err != nil { + res.AddMessage(LevelError, "could not parse options") + return nil + } + + rawQuery := `select cfl.layer3domain_id, cfl.subnet, cfl.parents, c.attributes, cfl.state + from containers_free_list cfl + left join containers c on cfl.layer3domain_id = c.layer3domain_id and cfl.subnet = c.subnet` + args := []interface{}{} + if options.Layer3Domain != "" { + l3id := 0 + err := c.tx.QueryRow(`select id from layer3domains where name = $1`, options.Layer3Domain).Scan(&l3id) + if err != nil { + res.AddMessage(LevelError, "could not resolve layer3domain") + return fmt.Errorf("could not resolve layer3domain '%s': %#v", options.Layer3Domain, err) + } + args = append(args, l3id) + rawQuery += " where layer3domain_id = $1" + } + + rows, err := c.tx.Query(rawQuery, args...) + if err != nil { + res.AddMessage(LevelError, "could not fetch containers") + return fmt.Errorf("could not fetch container tree: %#v", err) + } + defer rows.Close() + type ( + Container struct { + Status string `json:"status"` + Attributes json.RawMessage `json:"attributes"` + Containers map[string]Container `json:"containers"` + } + ) + result := map[int]Container{} + for rows.Next() { + cont := Container{Containers: map[string]Container{}} + parents := &[]string{} + l3id := 0 + subnet := "" + attr := sql.NullString{} + if err := rows.Scan(&l3id, &subnet, pq.Array(parents), &attr, &cont.Status); err != nil { + res.AddMessage(LevelError, "could not scan containers") + return fmt.Errorf("could not scan containers: %#v", err) + } + if _, found := result[l3id]; !found { + result[l3id] = Container{Status: "layer3domain", Containers: map[string]Container{}} + } + current := result[l3id] + for _, parent := range *parents { + if _, found := current.Containers[parent]; !found { + current.Containers[parent] = Container{Status: "container", Containers: map[string]Container{}} + } + current = current.Containers[parent] + } + if attr.Valid { + cont.Attributes = json.RawMessage(attr.String) + } + current.Containers[subnet] = cont + } + res.Result = result + return nil +} + +func containerGetAttr(c *Context, req Request, res *Response) error { + subnet := types.Subnet{} + options := struct { + Layer3Domain string + }{} + if err := req.ParseAtLeast(2, &subnet, &options); err != nil { + res.AddMessage(LevelError, "could not parse options: %s", err) + return nil + } + + result := json.RawMessage{} + selClause := query.FieldsToJSON("c", map[string]string{ + "subnet": "c.subnet", + "modified_by": "c.modified_by", + "modified_at": "c.modified_at", + "created_by": "c.created_by", + "created_at": "c.created_at", + "layer3domain": "l.name", + }) + queryStr := fmt.Sprintf(`select %s from containers c join layer3domains l + on c.layer3domain_id = l.id + where c.subnet = $1 and l.name = $2`, selClause) + err := c.tx.QueryRow(queryStr, subnet, options.Layer3Domain).Scan(&result) + if err != nil { + res.AddMessage(LevelError, "could not return result") + return fmt.Errorf("could not get container '%s': %s - query: %s", subnet, err, queryStr) + } + res.Result = result + return nil +} + +func containerSetAttr(c *Context, req Request, res *Response) error { + subnet := types.Subnet{} + attrs := types.FieldMap{} + options := struct { + Layer3Domain string + }{} + if err := req.ParseAtLeast(3, &subnet, &attrs, &options); err != nil { + res.AddMessage(LevelError, "could not parse options: %s", err) + return nil + } + if attrs.Size() == 0 { + res.AddMessage(LevelError, "no key/value pairs provided to update") + return nil + } + if options.Layer3Domain == "" { + res.AddMessage(LevelError, "layer3domain is required") + return nil + } + if attrs.Contains("layer3domain") { + res.AddMessage(LevelError, "can't change the layer3domain of a subnet") + return nil + } + + // TODO this is ugly. Can we have better API somehow? + fieldMap := map[string]string{ + "layer3domain_id": "", + "subnet": "", + "modified_by": "", + "modified_at": "", + "created_by": "", + "created_at": "", + } + if attrs.Contains("subnets") { + res.AddMessage(LevelError, "can not set subnets as attributes") + return nil + } + + l3name := options.Layer3Domain + l3id := 0 + err := c.tx.QueryRow(`select id from layer3domains where name = $1`, l3name).Scan(&l3id) + if err != nil { + if err == sql.ErrNoRows { + res.AddMessage(LevelError, "layer3domain '%s' does not exist", l3name) + return nil + } + res.AddMessage(LevelError, "could not get layer3domain") + return fmt.Errorf("could not fetch layer3domain id for name '%s': %#v", l3name, err) + } + + setClause, args, err := query.FieldMapToUpdate(attrs, fieldMap) + if err != nil { + res.AddMessage(LevelError, "could not encode requested attributes: %s", err) + return nil + } + queryStr := fmt.Sprintf("update containers p set %s where subnet = $%d::cidr and layer3domain_id = $%d returning subnet", setClause, len(args)+1, len(args)+2) + args = append(args, subnet) // don't forget to add the where clause parameter + args = append(args, l3id) // don't forget to add the layer3domain to the where clause + c.Debugf(LevelInfo, "query: %s - args: %#v", queryStr, args) + changed := "" + if err := c.tx.QueryRow(queryStr, args...).Scan(&changed); err != nil { + if err == sql.ErrNoRows { + res.AddMessage(LevelError, "subnet '%s' in layer3domain '%s' does not exist", subnet.String(), l3name) + return nil + } + res.AddMessage(LevelError, "could not set attributes") + c.Logf(LevelError, "could not set attributes on subnet '%s': %s - query: `%s` - args: `%#v`", subnet, err, queryStr, args) + return nil + } + return nil +}