From febad01af908895457b9864980fce64e3116d456 Mon Sep 17 00:00:00 2001 From: Chris Prather Date: Fri, 16 Oct 2020 12:41:27 -0400 Subject: [PATCH] add a logger for outputting information to the user --- api.go | 304 --- builds.go | 614 ------ builds_integration_test.go | 34 - builds_test.go | 377 ---- cli/builds.go | 304 +++ cli/cli.go | 110 +- cli/config.go | 131 +- cli/config_test.go | 237 +++ cli/datacenters.go | 152 ++ cli/device_reports.go | 27 + cli/devices.go | 154 +- cli/hardware.go | 127 ++ cli/organizations.go | 143 ++ cli/racks.go | 239 +++ cli/relays.go | 76 + cli/roles.go | 96 + cli/rooms.go | 129 ++ cli/schema.go | 36 + cli/{user.go => users.go} | 52 +- cli/validations.go | 43 + cmd_api.go | 93 - conch/builds.go | 76 +- conch/builds_test.go | 53 +- conch/client.go | 101 +- conch/client_test.go | 7 +- conch/conch.go | 12 +- conch/conch_test.go | 21 +- conch/datacenters.go | 25 +- conch/datacenters_test.go | 23 +- conch/deviceReports.go | 4 +- conch/deviceReports_test.go | 2 +- conch/devices.go | 42 +- conch/devices_test.go | 5 +- conch/hardwareProducts.go | 8 +- conch/hardwareProducts_test.go | 6 +- conch/hardwareVendors.go | 17 +- conch/hardwareVendors_test.go | 7 +- conch/organizations.go | 24 +- conch/organizations_test.go | 28 +- conch/rackRoles.go | 39 + conch/rackRoles_test.go | 68 + conch/racks.go | 118 ++ conch/racks_test.go | 142 ++ conch/relays.go | 13 +- conch/relays_test.go | 12 +- conch/rooms.go | 138 ++ conch/rooms_test.go | 211 ++ conch/schema.go | 10 + conch/types/DetailedDevice.go | 17 - conch/types/DeviceSettings.go | 13 - conch/types/Devices.go | 69 - conch/types/Links.go | 9 - conch/types/UserDetailed.go | 44 - conch/types/UserDetailed_test.go | 1 - conch/types/UserSettings.go | 52 - conch/types/ValidationStateWithResults.go | 9 - conch/types/{types.go => common.go} | 16 +- conch/types/{Requests.go => requests.go} | 122 +- conch/types/{Responses.go => responses.go} | 193 +- conch/types/templates.go | 758 +++++++ conch/users.go | 17 +- conch/users_test.go | 2 +- conch/validations.go | 35 +- conch/validations_test.go | 27 +- datacenter.go | 392 ---- datacenter_integration_test.go | 34 - devices.go | 766 ------- devices_integration_test.go | 101 - errors.go | 104 - fixtures/conch-v3/builds.yaml | 128 -- fixtures/conch-v3/datacenter.yaml | 122 -- fixtures/conch-v3/device.yaml | 1783 ----------------- fixtures/conch-v3/hardware.yaml | 669 ------- fixtures/conch-v3/organizations.yaml | 152 -- fixtures/conch-v3/racks-roles.yaml | 158 -- fixtures/conch-v3/racks.yaml | 1301 ------------ fixtures/conch-v3/relays.yaml | 146 -- fixtures/conch-v3/rooms.yaml | 271 --- fixtures/conch-v3/user.yaml | 64 - fixtures/conch-v3/workspace.yaml | 101 - .../request/BuildAddOrganization.yaml | 34 - .../json-schema/request/BuildAddUser.yaml | 35 - fixtures/json-schema/request/BuildCreate.yaml | 35 - .../request/BuildCreateDevice.yaml | 34 - .../request/DatacenterRoomCreate.yaml | 33 - .../request/HardwareProductCreate.yaml | 33 - .../request/OrganizationAddUser.yaml | 35 - .../request/OrganizationCreate.yaml | 35 - .../request/RackAssignmentUpdates.yaml | 34 - .../json-schema/request/RackLayoutCreate.yaml | 33 - .../json-schema/request/RackRoleCreate.yaml | 34 - fixtures_test.go | 386 ---- go.mod | 4 +- go.sum | 10 +- hardware.go | 413 ---- hardware_integration_test.go | 129 -- hardware_test.go | 114 -- integration_test.go | 99 - logger/logger.go | 55 + main.go | 184 +- organizations.go | 380 ---- organizations_integration_test.go | 34 - organizations_test.go | 258 --- racks.go | 888 -------- racks_integration_test.go | 97 - racks_test.go | 80 - relays.go | 310 --- relays_integration_test.go | 44 - roles.go | 320 --- roles_integration_test.go | 36 - roles_test.go | 37 - rooms.go | 382 ---- rooms_integration_test.go | 52 - rooms_test.go | 37 - schema.go | 26 - schema_test.go | 95 - tables/tables.go | 25 +- template/template.go | 35 +- templates.go | 267 --- user.go | 354 ---- user_integration_test.go | 19 - validations.go | 292 --- workspaces.go | 1172 ----------- workspaces_integration_test.go | 25 - 124 files changed, 4128 insertions(+), 15272 deletions(-) delete mode 100644 api.go delete mode 100644 builds.go delete mode 100644 builds_integration_test.go delete mode 100644 builds_test.go create mode 100644 cli/builds.go create mode 100644 cli/config_test.go create mode 100644 cli/datacenters.go create mode 100644 cli/device_reports.go create mode 100644 cli/hardware.go create mode 100644 cli/organizations.go create mode 100644 cli/racks.go create mode 100644 cli/relays.go create mode 100644 cli/roles.go create mode 100644 cli/rooms.go create mode 100644 cli/schema.go rename cli/{user.go => users.go} (69%) create mode 100644 cli/validations.go delete mode 100644 cmd_api.go create mode 100644 conch/rackRoles.go create mode 100644 conch/rackRoles_test.go create mode 100644 conch/racks.go create mode 100644 conch/racks_test.go create mode 100644 conch/rooms.go create mode 100644 conch/rooms_test.go create mode 100644 conch/schema.go delete mode 100644 conch/types/DetailedDevice.go delete mode 100644 conch/types/DeviceSettings.go delete mode 100644 conch/types/Devices.go delete mode 100644 conch/types/Links.go delete mode 100644 conch/types/UserDetailed.go delete mode 100644 conch/types/UserDetailed_test.go delete mode 100644 conch/types/UserSettings.go delete mode 100644 conch/types/ValidationStateWithResults.go rename conch/types/{types.go => common.go} (54%) rename conch/types/{Requests.go => requests.go} (68%) rename conch/types/{Responses.go => responses.go} (75%) create mode 100644 conch/types/templates.go delete mode 100644 datacenter.go delete mode 100644 datacenter_integration_test.go delete mode 100644 devices.go delete mode 100644 devices_integration_test.go delete mode 100644 errors.go delete mode 100644 fixtures/conch-v3/builds.yaml delete mode 100644 fixtures/conch-v3/datacenter.yaml delete mode 100644 fixtures/conch-v3/device.yaml delete mode 100644 fixtures/conch-v3/hardware.yaml delete mode 100644 fixtures/conch-v3/organizations.yaml delete mode 100644 fixtures/conch-v3/racks-roles.yaml delete mode 100644 fixtures/conch-v3/racks.yaml delete mode 100644 fixtures/conch-v3/relays.yaml delete mode 100644 fixtures/conch-v3/rooms.yaml delete mode 100644 fixtures/conch-v3/user.yaml delete mode 100644 fixtures/conch-v3/workspace.yaml delete mode 100644 fixtures/json-schema/request/BuildAddOrganization.yaml delete mode 100644 fixtures/json-schema/request/BuildAddUser.yaml delete mode 100644 fixtures/json-schema/request/BuildCreate.yaml delete mode 100644 fixtures/json-schema/request/BuildCreateDevice.yaml delete mode 100644 fixtures/json-schema/request/DatacenterRoomCreate.yaml delete mode 100644 fixtures/json-schema/request/HardwareProductCreate.yaml delete mode 100644 fixtures/json-schema/request/OrganizationAddUser.yaml delete mode 100644 fixtures/json-schema/request/OrganizationCreate.yaml delete mode 100644 fixtures/json-schema/request/RackAssignmentUpdates.yaml delete mode 100644 fixtures/json-schema/request/RackLayoutCreate.yaml delete mode 100644 fixtures/json-schema/request/RackRoleCreate.yaml delete mode 100644 fixtures_test.go delete mode 100644 hardware.go delete mode 100644 hardware_integration_test.go delete mode 100644 hardware_test.go delete mode 100644 integration_test.go create mode 100644 logger/logger.go delete mode 100644 organizations.go delete mode 100644 organizations_integration_test.go delete mode 100644 organizations_test.go delete mode 100644 racks.go delete mode 100644 racks_integration_test.go delete mode 100644 racks_test.go delete mode 100644 relays.go delete mode 100644 relays_integration_test.go delete mode 100644 roles.go delete mode 100644 roles_integration_test.go delete mode 100644 roles_test.go delete mode 100644 rooms.go delete mode 100644 rooms_integration_test.go delete mode 100644 rooms_test.go delete mode 100644 schema.go delete mode 100644 schema_test.go delete mode 100644 templates.go delete mode 100644 user.go delete mode 100644 user_integration_test.go delete mode 100644 validations.go delete mode 100644 workspaces.go delete mode 100644 workspaces_integration_test.go diff --git a/api.go b/api.go deleted file mode 100644 index eea4aaf..0000000 --- a/api.go +++ /dev/null @@ -1,304 +0,0 @@ -// Copyright Joyent, Inc. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package main - -import ( - "encoding/json" - "errors" - "fmt" - "io/ioutil" - "net" - "net/http" - "strings" - "time" - - "github.com/dghubble/sling" -) - -type Conch struct { - URL string - Token string - - UserAgent map[string]string - - Debug bool - Trace bool - JsonOnly bool - StrictParsing bool - DevelMode bool - - HTTP *http.Client -} - -var defaultTransport = &http.Transport{ - Proxy: http.ProxyFromEnvironment, - Dial: (&net.Dialer{ - Timeout: 5 * time.Second, - KeepAlive: 5 * time.Second, - DualStack: true, - }).Dial, - TLSHandshakeTimeout: 5 * time.Second, -} - -func (c *Conch) PrintJSON(d interface{}) { - fmt.Println(c.AsJSON(d)) -} - -func (c *Conch) AsJSON(d interface{}) string { - if j, err := json.MarshalIndent(d, "", " "); err != nil { - panic(err) - } else { - return string(j) - } -} - -func (c *Conch) Sling() *sling.Sling { - userAgent := fmt.Sprintf("Conch/%s", Version) - if len(c.UserAgent) > 0 { - for k, v := range c.UserAgent { - userAgent = fmt.Sprintf("%s %s/%s", userAgent, k, v) - } - } - - if c.HTTP == nil { - c.HTTP = &http.Client{ - Transport: defaultTransport, - } - } - - s := sling.New(). - Client(c.HTTP). - Set("User-Agent", userAgent) - - if c.URL != "" { - s = s.Base(c.URL) - } - - if c.Token != "" { - s = s.Set("Authorization", "Bearer "+c.Token) - } - - return s.New() -} - -func (c *Conch) DoBadly(s *sling.Sling) *ConchResponse { - response := ConchResponse{Strict: c.StrictParsing} - - req, err := s.Request() - if err != nil { - response.Error = err - panic(&response) - } - - response.Request = req - - // BUG(sungo) Logging - - res, err := c.HTTP.Do(req) - response.Response = res - - if (res == nil) || (err != nil) { - response.Error = err - panic(&response) - } - - defer res.Body.Close() - bodyBytes, err := ioutil.ReadAll(res.Body) - if err != nil { - response.Error = err - panic(&response) - } - - response.Body = string(bodyBytes) - return &response -} - -func (c *Conch) Do(s *sling.Sling) *ConchResponse { - response := ConchResponse{Strict: c.StrictParsing} - - req, err := s.Request() - if err != nil { - response.Error = err - panic(&response) - } - - response.Request = req - - // BUG(sungo) Logging - - res, err := c.HTTP.Do(req) - response.Response = res - - if (res == nil) || (err != nil) { - response.Error = err - panic(&response) - } - - defer res.Body.Close() - bodyBytes, err := ioutil.ReadAll(res.Body) - if err != nil { - response.Error = err - panic(&response) - } - - response.Body = string(bodyBytes) - - if response.IsError() { - e := struct { - Error string `json:"error"` - }{} - - if ok := response.Parse(&e); ok { - response.Error = fmt.Errorf( - "HTTP Error %d: %s", - response.StatusCode(), - e.Error, - ) - } else { - response.Error = fmt.Errorf( - "HTTP Error %d: %s", - response.StatusCode(), - response.Status(), - ) - } - panic(&response) - } - - switch response.StatusCode() { - case 201: - fallthrough - case 204: - if location, err := response.Response.Location(); err == nil { - if location != nil { - return c.Do(c.Sling().Get(location.String())) - } - } - } - - return &response -} - -func (c *Conch) Version() string { - res := c.Do(c.Sling().Get("/version")) - - v := struct { - Version string `json:"version"` - }{} - - if ok := res.Parse(&v); !ok { - panic(res) - } - - return v.Version -} - -/*****************/ - -// ErrNoResponse indicates an operation was attempted on the structure when no -// HTTP response is present -var ErrNoResponse = errors.New("no HTTP response found") - -// ConchResponse holds the notions of what happened to an HTTP request and -// convenience functions around payload parsing and the like -type ConchResponse struct { - Request *http.Request - Response *http.Response - - Strict bool - Body string - Error error -} - -/***/ - -// StatusCode provides the HTTP response status code. If we don't have a -// response, -1 is returned. -func (r *ConchResponse) StatusCode() int { - if r.Response == nil { - return -1 - } - return r.Response.StatusCode -} - -// Status provides the string version of the HTTP status code. If we don't have -// a response, "" is returned. -func (r *ConchResponse) Status() string { - if r.Response == nil { - return "" - } - return r.Response.Status -} - -// IsError provides a really simplistic notion of when an HTTP response has -// gone awry. Specifically, if the status code is between 400 and 600, it is -// considered in error. The response is also considered to be ok/successful if -// it hasn't happened yet. -func (r *ConchResponse) IsError() bool { - if r.Response == nil { - return false - } - - if (r.StatusCode() >= 400) && (r.StatusCode() < 600) { - return true - } - - return false -} - -// IsErrorOurFault is a convenience function to spot when an HTTP error code is -// in the 400s -func (r *ConchResponse) IsErrorOurFault() bool { - if !r.IsError() { - return false - } - - if r.StatusCode() >= 400 && r.StatusCode() < 500 { - return true - } - - return false -} - -// IsErrorTheirFault is a convenience function to spot when an HTTP error code -// is in the 500s -func (r *ConchResponse) IsErrorTheirFault() bool { - if !r.IsError() { - return false - } - - if r.StatusCode() >= 500 && r.StatusCode() < 600 { - return true - } - - return false -} - -// Parse, well, parses the JSON payload in the HTTP response and tries to shove -// it into the provided structure. If Strict is true, the parser disallows -// any unknown fields. To quote the go docs, an error is returned when "the -// input contains object keys which do not match any non-ignored, exported -// fields in the destination." -// -// So, if the API sends us data we aren't expecting, you'll get an error. -// However, you will *not* get an error if the API fails to send data you're -// expecting. -func (r *ConchResponse) Parse(data interface{}) bool { - r.Error = nil - if r.Response == nil { - r.Error = ErrNoResponse - return false - } - - dec := json.NewDecoder(strings.NewReader(r.Body)) - if r.Strict { - dec.DisallowUnknownFields() - } - - err := dec.Decode(data) - r.Error = err - return (err == nil) -} diff --git a/builds.go b/builds.go deleted file mode 100644 index 336d21c..0000000 --- a/builds.go +++ /dev/null @@ -1,614 +0,0 @@ -package main - -import ( - "bytes" - "fmt" - "net/url" - "sort" - "strings" - "time" - - "github.com/gofrs/uuid" - cli "github.com/jawher/mow.cli" - "github.com/joyent/kosh/tables" - "github.com/joyent/kosh/template" -) - -type Builds struct { - *Conch -} - -type Build struct { - ID uuid.UUID `json:"id" faker:"uuid"` - Name string `json:"name"` - Description string `json:"description"` - Admins UserAndRoles `json:"admins"` - Created time.Time `json:"created" faker:"-"` - Started time.Time `json:"started" faker:"-"` - Completed time.Time `json:"completed" faker:"-"` - CompletedUser UserAndRole `json:"completed_user" faker:"-"` -} - -func (b Build) String() string { - if API.JsonOnly { - return API.AsJSON(b) - } - - t, err := template.NewTemplate().Parse(buildTemplate) - if err != nil { - panic(err) - } - - buf := new(bytes.Buffer) - - if err := t.Execute(buf, b); err != nil { - panic(err) - } - - return buf.String() -} - -type BuildList []Build - -func (bl BuildList) Len() int { - return len(bl) -} - -func (bl BuildList) Swap(i, j int) { - bl[i], bl[j] = bl[j], bl[i] -} - -func (bl BuildList) Less(i, j int) bool { - return bl[i].Name < bl[j].Name -} - -func (bl BuildList) String() string { - sort.Sort(bl) - if API.JsonOnly { - return API.AsJSON(bl) - } - - tableString := &strings.Builder{} - table := tables.NewTable(tableString) - tables.TableToMarkdown(table) - - table.SetHeader([]string{ - "Name", - "Description", - "Started", - "Completed", - "Completed By", - }) - - for _, b := range bl { - table.Append([]string{ - b.Name, - b.Description, - b.Started.String(), - b.Completed.String(), - b.CompletedUser.Email, - }) - } - - table.Render() - return tableString.String() -} - -func (c *Conch) Builds() *Builds { - return &Builds{c} -} - -var BuildRoleList = []string{"admin", "rw", "ro"} - -func prettyBuildRoleList() string { - return strings.Join(BuildRoleList, ", ") -} - -func okBuildRole(role string) bool { - for _, b := range BuildRoleList { - if role == b { - return true - } - } - return false -} - - -func (b *Builds) GetAll() (list BuildList) { - res := b.Do(b.Sling().Get("/build")) - if ok := res.Parse(&list); !ok { - panic(res) - } - return -} - -func (b *Builds) Get(ID uuid.UUID) (build Build) { - uri := fmt.Sprintf("/build/%s", url.PathEscape(ID.String())) - res := b.Do(b.Sling().Get(uri)) - if ok := res.Parse(&build); !ok { - panic(res) - } - return -} - -func (b *Builds) GetByName(name string) (build Build) { - uri := fmt.Sprintf("/build/%s", url.PathEscape(name)) - res := b.Do(b.Sling().Get(uri)) - if ok := res.Parse(&build); !ok { - panic(res) - } - return -} - -func (b *Builds) Create(name, description string, admins []map[string]string) (build Build) { - payload := make(map[string]interface{}) - payload["name"] = name - payload["admins"] = admins - if description != "" { - payload["description"] = description - } - - res := b.Do(b.Sling().New().Post("/build"). - Set("Content-Type", "application/json"). - BodyJSON(payload), - ) - - if ok := res.Parse(&build); !ok { - panic(res) - } - - return -} - -func (b *Builds) GetUsers(ID uuid.UUID) (list UserAndRoles) { - res := b.Do(b.Sling().Get(fmt.Sprintf("/build/%s/user", ID.String()))) - if ok := res.Parse(&list); !ok { - panic(res) - } - return -} - -func (b *Builds) AddUser(ID uuid.UUID, email, role string, sendEmail bool) { - uri := fmt.Sprintf("/build/%s/user", url.PathEscape(ID.String())) - - payload := make(map[string]string) - payload["email"] = email - payload["role"] = role - - send := 0 - if sendEmail { - send = 1 - } - q := struct { - SendEmail int `url:"send_mail"` - }{send} - - _ = b.Do( - b.Sling().Post(uri). - Set("Content-Type", "application/json"). - QueryStruct(q). - BodyJSON(payload), - ) -} - -// userID is a string because it may be a UUID or an Email, the API accepts both -func (b *Builds) RemoveUser(ID uuid.UUID, userID string, sendEmail bool) bool { - uri := fmt.Sprintf( - "/build/%s/user/%s", - url.PathEscape(ID.String()), - url.PathEscape(userID), - ) - - send := 0 - if sendEmail { - send = 1 - } - q := struct { - SendEmail int `url:"send_mail"` - }{send} - - res := b.Do(b.Sling().Delete(uri).QueryStruct(q)) - - return res.StatusCode() == 204 -} - -func (b *Builds) GetOrgs(ID uuid.UUID) (list OrgAndRoles) { - res := b.Do(b.Sling().Get(fmt.Sprintf("/build/%s/organization", ID.String()))) - if ok := res.Parse(&list); !ok { - panic(res) - } - return -} - -func (b *Builds) AddOrg(ID uuid.UUID, orgID, role string, sendEmail bool) { - uri := fmt.Sprintf("/build/%s/organization", url.PathEscape(ID.String())) - - payload := make(map[string]string) - payload["organization_id"] = orgID - payload["role"] = role - - send := 0 - if sendEmail { - send = 1 - } - q := struct { - SendEmail int `url:"send_mail"` - }{send} - - _ = b.Do( - b.Sling().Post(uri). - Set("Content-Type", "application/json"). - QueryStruct(q). - BodyJSON(payload), - ) -} - -// userID is a string because it may be a UUID or an Email, the API accepts both -func (b *Builds) RemoveOrg(ID uuid.UUID, orgID string, sendEmail bool) bool { - uri := fmt.Sprintf( - "/build/%s/organization/%s", - url.PathEscape(ID.String()), - url.PathEscape(orgID), - ) - - send := 0 - if sendEmail { - send = 1 - } - q := struct { - SendEmail int `url:"send_mail"` - }{send} - - res := b.Do(b.Sling().Delete(uri).QueryStruct(q)) - - return res.StatusCode() == 204 -} - -func (b *Builds) GetDevices(ID uuid.UUID) (list DeviceList) { - res := b.Do(b.Sling().Get(fmt.Sprintf("/build/%s/device", ID.String()))) - if ok := res.Parse(&list); !ok { - panic(res) - } - return -} - -func (b *Builds) CreateDevice(ID uuid.UUID, deviceID, sku string) { - type BDC struct { - Serial string `json:"serial_number"` - SKU string `json:"sku"` - } - - type BDCList []BDC - - list := BDCList{{deviceID, sku}} - - uri := fmt.Sprintf("/build/%s/device", url.PathEscape(ID.String())) - - _ = b.Do( - b.Sling().Post(uri). - Set("Content-Type", "application/json"). - BodyJSON(list), - ) -} - -func (b *Builds) AddDevice(ID uuid.UUID, deviceID string) { - uri := fmt.Sprintf("/build/%s/device/%s", url.PathEscape(ID.String()), url.PathEscape(deviceID)) - - _ = b.Do(b.Sling().Post(uri)) -} - -func (b *Builds) RemoveDevice(ID uuid.UUID, deviceID string) bool { - uri := fmt.Sprintf( - "/build/%s/device/%s", - url.PathEscape(ID.String()), - url.PathEscape(deviceID), - ) - - res := b.Do(b.Sling().Delete(uri)) - - return res.StatusCode() == 204 -} - -func (b *Builds) GetRacks(ID uuid.UUID) (list RackList) { - res := b.Do(b.Sling().Get(fmt.Sprintf("/build/%s/rack", ID.String()))) - if ok := res.Parse(&list); !ok { - panic(res) - } - return -} - -func (b *Builds) AddRack(ID uuid.UUID, rackID string) { - uri := fmt.Sprintf("/build/%s/rack/%s", url.PathEscape(ID.String()), url.PathEscape(rackID)) - - _ = b.Do(b.Sling().Post(uri)) -} - -func (b *Builds) RemoveRack(ID uuid.UUID, rackID string) bool { - uri := fmt.Sprintf( - "/build/%s/rack/%s", - url.PathEscape(ID.String()), - url.PathEscape(rackID), - ) - - res := b.Do(b.Sling().Delete(uri)) - - return res.StatusCode() == 204 -} - -func init() { - - App.Command("builds", "Work with builds", func(cmd *cli.Cmd) { - cmd.Command("get ls", "Get a list of all builds", func(cmd *cli.Cmd) { - cmd.Action = func() { - fmt.Println(API.Builds().GetAll()) - } - }) - - cmd.Command("create", "Create a new build", func(cmd *cli.Cmd) { - nameArg := cmd.StringArg("NAME", "", "Name of the new build") - - descOpt := cmd.StringOpt("description", "", "A description of the build") - adminEmailArg := cmd.StringOpt( - "admin", - "", - "Email address for the (initial) admin user for the build. This does *not* create the user.", - ) - - cmd.Spec = "NAME [OPTIONS]" - cmd.Action = func() { - API.Builds().Create( - *nameArg, - *descOpt, - []map[string]string{{"email": *adminEmailArg}}, - ) - } - }) - - }) - - App.Command("build", "Work with a specific build", func(cmd *cli.Cmd) { - var b Build - buildNameArg := cmd.StringArg("NAME", "", "Name or ID of the build") - - cmd.Spec = "NAME" - cmd.Before = func() { - b = API.Builds().GetByName(*buildNameArg) - // TODO(sungo): should we verify that the build exists? - } - - cmd.Command("get", "Get information about a single build by its name", func(cmd *cli.Cmd) { - - cmd.Action = func() { - fmt.Println(b) - } - }) - - cmd.Command("users", "Manage users in a specific build", func(cmd *cli.Cmd) { - - cmd.Command("get ls", "Get a list of users in an build", func(cmd *cli.Cmd) { - cmd.Action = func() { - fmt.Println(API.Builds().GetUsers(b.ID)) - } - }) - - cmd.Command("add", "Add a user to an build", func(cmd *cli.Cmd) { - userEmailArg := cmd.StringArg( - "EMAIL", - "", - "The email of the user to add to the build. Does *not* create the user", - ) - - roleOpt := cmd.StringOpt( - "role", - "ro", - "The role for the user. One of: "+prettyBuildRoleList(), - ) - - sendEmailOpt := cmd.BoolOpt( - "send-email", - true, - "Send email to the target user, notifying them of the change", - ) - - cmd.Spec = "EMAIL [OPTIONS]" - cmd.Action = func() { - if !okBuildRole(*roleOpt) { - panic(fmt.Errorf( - "'role' value must be one of: %s", - prettyBuildRoleList(), - )) - } - API.Builds().AddUser( - b.ID, - *userEmailArg, - *roleOpt, - *sendEmailOpt, - ) - fmt.Println(API.Builds().GetUsers(b.ID)) - } - - }) - - cmd.Command("remove rm", "remove a user from an build", func(cmd *cli.Cmd) { - userEmailArg := cmd.StringArg( - "EMAIL", - "", - "The email or ID of the user to modify", - ) - - sendEmailOpt := cmd.BoolOpt( - "send-email", - true, - "Send email to the target user, notifying them of the change", - ) - cmd.Spec = "EMAIL [OPTIONS]" - cmd.Action = func() { - API.Builds().RemoveUser( - b.ID, - *userEmailArg, - *sendEmailOpt, - ) - fmt.Println(API.Builds().GetUsers(b.ID)) - } - }) - }) - - cmd.Command("organizations orgs", "Manage organizations in a specific build", func(cmd *cli.Cmd) { - - cmd.Command("get ls", "Get a list of organizations in an build", func(cmd *cli.Cmd) { - cmd.Action = func() { - fmt.Println(API.Builds().GetOrgs(b.ID)) - } - }) - - cmd.Command("add", "Add a organization to an build", func(cmd *cli.Cmd) { - orgNameArg := cmd.StringArg( - "NAME", - "", - "The name of the organization to add to the build. Does *not* create the organization", - ) - - roleOpt := cmd.StringOpt( - "role", - "ro", - "The role for the organization. One of: "+prettyBuildRoleList(), - ) - - sendEmailOpt := cmd.BoolOpt( - "send-email", - true, - "Send email to the organization admins, notifying them of the change", - ) - - cmd.Spec = "NAME [OPTIONS]" - cmd.Action = func() { - if !okBuildRole(*roleOpt) { - panic(fmt.Errorf( - "'role' value must be one of: %s", - prettyBuildRoleList(), - )) - } - API.Builds().AddOrg( - b.ID, - *orgNameArg, - *roleOpt, - *sendEmailOpt, - ) - fmt.Println(API.Builds().GetOrgs(b.ID)) - } - - }) - - cmd.Command("remove rm", "remove an organization from a build", func(cmd *cli.Cmd) { - orgNameArg := cmd.StringArg( - "NAME", - "", - "The name or ID of the organization to modify", - ) - - sendEmailOpt := cmd.BoolOpt( - "send-email", - true, - "Send email to the target organization admins, notifying them of the change", - ) - cmd.Spec = "EMAIL [OPTIONS]" - cmd.Action = func() { - API.Builds().RemoveOrg( - b.ID, - *orgNameArg, - *sendEmailOpt, - ) - fmt.Println(API.Builds().GetOrgs(b.ID)) - } - }) - }) - - cmd.Command("devices ds", "Manage devices in a specific build", func(cmd *cli.Cmd) { - - cmd.Command("get ls", "Get a list of devices in an build", func(cmd *cli.Cmd) { - cmd.Action = func() { - fmt.Println(API.Builds().GetDevices(b.ID)) - } - }) - - cmd.Command("add", "Add a device to an build", func(cmd *cli.Cmd) { - deviceIDArg := cmd.StringArg( - "ID", - "", - "The ID or serial number of the device to add to the build. Does *not* create the device", - ) - - cmd.Spec = "ID [OPTIONS]" - cmd.Action = func() { - API.Builds().AddDevice( - b.ID, - *deviceIDArg, - ) - fmt.Println(API.Builds().GetDevices(b.ID)) - } - - }) - - cmd.Command("remove rm", "remove a device from a build", func(cmd *cli.Cmd) { - deviceIDArg := cmd.StringArg( - "ID", - "", - "The ID or serial number of the device to add to the build. Does *not* create the device", - ) - - cmd.Spec = "ID [OPTIONS]" - cmd.Action = func() { - API.Builds().RemoveDevice( - b.ID, - *deviceIDArg, - ) - fmt.Println(API.Builds().GetDevices(b.ID)) - } - }) - }) - - cmd.Command("racks", "Manage racks in a specific build", func(cmd *cli.Cmd) { - - cmd.Command("get ls", "Get a list of racks in an build", func(cmd *cli.Cmd) { - cmd.Action = func() { - fmt.Println(API.Builds().GetRacks(b.ID)) - } - }) - - cmd.Command("add", "Add a rack to an build", func(cmd *cli.Cmd) { - rackIDArg := cmd.StringArg( - "ID", - "", - "The ID of the rack to add to the build. Does *not* create the rack", - ) - - cmd.Spec = "ID [OPTIONS]" - cmd.Action = func() { - API.Builds().AddRack( - b.ID, - *rackIDArg, - ) - fmt.Println(API.Builds().GetRacks(b.ID)) - } - - }) - - cmd.Command("remove rm", "remove a rack from a build", func(cmd *cli.Cmd) { - rackIDArg := cmd.StringArg( - "ID", - "", - "The ID of the rack to add to the build. Does *not* create the device", - ) - - cmd.Spec = "ID [OPTIONS]" - cmd.Action = func() { - API.Builds().RemoveRack( - b.ID, - *rackIDArg, - ) - fmt.Println(API.Builds().GetRacks(b.ID)) - } - }) - }) - }) -} diff --git a/builds_integration_test.go b/builds_integration_test.go deleted file mode 100644 index 0955190..0000000 --- a/builds_integration_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package main - -import ( - "testing" -) - -func TestBuildAPIintegration(t *testing.T) { - defer errorHandler() - - setupAPIClient() - r := setupRecorder("fixtures/conch-v3/builds") - defer r() // Make sure recorder is stopped once done with it - - var build Build - t.Run("create a build", func(t *testing.T) { - defer errorHandler() - fake := newTestBuild() - build = API.Builds().Create( - fake.Name, - fake.Description, - []map[string]string{{"email": "conch@example.com"}}, - ) - }) - - t.Run("get all builds", func(t *testing.T) { - defer errorHandler() - _ = API.Builds().GetAll() - }) - - t.Run("get a specific build", func(t *testing.T) { - defer errorHandler() - _ = API.Builds().Get(build.ID) - }) -} diff --git a/builds_test.go b/builds_test.go deleted file mode 100644 index 305f1fb..0000000 --- a/builds_test.go +++ /dev/null @@ -1,377 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestBuildsGetAll(t *testing.T) { - spy := requestSpy{} - buildList := newTestBuildList() - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - json.NewEncoder(w).Encode(buildList) - })) - defer server.Close() - - API.URL = server.URL - - b := API.Builds() - - got := b.GetAll() - - assertRequestCount(t, spy.requestCount, 1) - assertRequestPath(t, spy.requestPath, "/build") - assert.Equal(t, got, buildList) -} - -func TestBuildsGet(t *testing.T) { - spy := requestSpy{} - build := newTestBuild() - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - json.NewEncoder(w).Encode(build) - })) - defer server.Close() - - API.URL = server.URL - b := API.Builds() - - got := b.Get(build.ID) - - assertRequestMethod(t, spy.requestMethod, "GET") - assertRequestCount(t, spy.requestCount, 1) - assertRequestPath(t, spy.requestPath, fmt.Sprintf("/build/%s", build.ID)) - assert.Equal(t, got, build) -} - -func TestBuildsGetByName(t *testing.T) { - spy := requestSpy{} - build := newTestBuild() - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - json.NewEncoder(w).Encode(build) - })) - defer server.Close() - - API.URL = server.URL - b := API.Builds() - - got := b.GetByName(build.Name) - - assertRequestMethod(t, spy.requestMethod, "GET") - assertRequestCount(t, spy.requestCount, 1) - assertRequestPath(t, spy.requestPath, fmt.Sprintf("/build/%s", build.Name)) - assert.Equal(t, got, build) -} - -func TestBuildsCreate(t *testing.T) { - spy := requestSpy{} - var got Build - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - body, _ := ioutil.ReadAll(r.Body) - assertJSONSchema(t, body, "request/BuildCreate") - json.NewDecoder(bytes.NewBuffer(body)).Decode(&got) - json.NewEncoder(w).Encode(got) - })) - defer server.Close() - - API.URL = server.URL - - b := API.Builds() - - want := b.Create("Z", "Z Build", []map[string]string{{"email": "admin@example.com"}}) - - assertRequestCount(t, spy.requestCount, 1) - assertRequestPath(t, spy.requestPath, "/build") - assertRequestMethod(t, spy.requestMethod, "POST") - if got.Name != "Z" { - t.Errorf("Invalid requestBody. Got %v expected something with name Z", got) - } - - // now let's check what we made in the server is what we got in the client - assert.Equal(t, got, want) -} - -func TestBuildsGetUsers(t *testing.T) { - spy := requestSpy{} - build := newTestBuild() - list := newTestUserAndRoles() - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - json.NewEncoder(w).Encode(list) - })) - defer server.Close() - - API.URL = server.URL - - b := API.Builds() - - got := b.GetUsers(build.ID) - - assertRequestMethod(t, spy.requestMethod, "GET") - assertRequestCount(t, spy.requestCount, 1) - assertRequestPath(t, spy.requestPath, fmt.Sprintf("/build/%s/user", build.ID)) - assert.Equal(t, got, list) -} - -func TestBuildsAddUser(t *testing.T) { - spy := requestSpy{} - build := newTestBuild() - user := newTestUser() - var got UserAndRole - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - body, _ := ioutil.ReadAll(r.Body) - assertJSONSchema(t, body, "request/BuildAddUser") - json.NewDecoder(bytes.NewBuffer(body)).Decode(&got) - })) - defer server.Close() - - API.URL = server.URL - - b := API.Builds() - - b.AddUser(build.ID, user.Email, "admin", false) - - assertRequestMethod(t, spy.requestMethod, "POST") - assertRequestCount(t, spy.requestCount, 1) - assertRequestPath(t, spy.requestPath, fmt.Sprintf("/build/%s/user", build.ID)) -} - -func TestBuildsRemoveUser(t *testing.T) { - spy := requestSpy{} - build := newTestBuild() - user := newTestUser() - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - })) - defer server.Close() - - API.URL = server.URL - - b := API.Builds() - b.RemoveUser(build.ID, user.Email, false) - - assertRequestMethod(t, spy.requestMethod, "DELETE") - assertRequestCount(t, spy.requestCount, 1) - assertRequestPath(t, spy.requestPath, fmt.Sprintf("/build/%s/user/%s", build.ID, user.Email)) -} - -func TestBuildsGetOrgs(t *testing.T) { - spy := requestSpy{} - build := newTestBuild() - list := newTestOrgAndRoles() - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - json.NewEncoder(w).Encode(list) - })) - defer server.Close() - - API.URL = server.URL - - b := API.Builds() - - got := b.GetOrgs(build.ID) - - assertRequestMethod(t, spy.requestMethod, "GET") - assertRequestCount(t, spy.requestCount, 1) - assertRequestPath(t, spy.requestPath, fmt.Sprintf("/build/%s/organization", build.ID)) - assert.Equal(t, got, list) -} - -func TestBuildsAddOrg(t *testing.T) { - spy := requestSpy{} - build := newTestBuild() - org := newTestOrganization() - var got OrgAndRole - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - body, _ := ioutil.ReadAll(r.Body) - assertJSONSchema(t, body, "request/BuildAddOrganization") - json.NewDecoder(bytes.NewBuffer(body)).Decode(&got) - })) - defer server.Close() - - API.URL = server.URL - - b := API.Builds() - - b.AddOrg(build.ID, org.ID.String(), "admin", false) - - assertRequestMethod(t, spy.requestMethod, "POST") - assertRequestCount(t, spy.requestCount, 1) - assertRequestPath(t, spy.requestPath, fmt.Sprintf("/build/%s/organization", build.ID)) -} - -func TestBuildsRemoveOrg(t *testing.T) { - spy := requestSpy{} - build := newTestBuild() - org := newTestOrganization() - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - })) - defer server.Close() - - API.URL = server.URL - - b := API.Builds() - b.RemoveOrg(build.ID, org.ID.String(), false) - - assertRequestMethod(t, spy.requestMethod, "DELETE") - assertRequestCount(t, spy.requestCount, 1) - assertRequestPath(t, spy.requestPath, fmt.Sprintf("/build/%s/organization/%s", build.ID, org.ID)) -} - -func TestBuildsGetDevices(t *testing.T) { - spy := requestSpy{} - build := newTestBuild() - list := newTestDeviceList() - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - json.NewEncoder(w).Encode(list) - })) - defer server.Close() - - API.URL = server.URL - - b := API.Builds() - - got := b.GetDevices(build.ID) - - assertRequestMethod(t, spy.requestMethod, "GET") - assertRequestCount(t, spy.requestCount, 1) - assertRequestPath(t, spy.requestPath, fmt.Sprintf("/build/%s/device", build.ID)) - assert.Equal(t, got, list) -} - -func TestBuildsAddDevice(t *testing.T) { - spy := requestSpy{} - build := newTestBuild() - device := newTestDevice() - var got Device - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - body, _ := ioutil.ReadAll(r.Body) - assertJSONSchema(t, body, "request/BuildCreateDevice") - json.NewDecoder(bytes.NewBuffer(body)).Decode(&got) - })) - defer server.Close() - - API.URL = server.URL - - b := API.Builds() - - b.AddDevice(build.ID, device.ID.String()) - - assertRequestMethod(t, spy.requestMethod, "POST") - assertRequestCount(t, spy.requestCount, 1) - assertRequestPath(t, spy.requestPath, fmt.Sprintf("/build/%s/device/%s", build.ID, device.ID)) -} - -func TestBuildsRemoveDevice(t *testing.T) { - spy := requestSpy{} - build := newTestBuild() - device := newTestDevice() - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - })) - defer server.Close() - - API.URL = server.URL - - b := API.Builds() - b.RemoveDevice(build.ID, device.ID.String()) - - assertRequestMethod(t, spy.requestMethod, "DELETE") - assertRequestCount(t, spy.requestCount, 1) - assertRequestPath(t, spy.requestPath, fmt.Sprintf("/build/%s/device/%s", build.ID, device.ID)) -} - -func TestBuildsGetRacks(t *testing.T) { - spy := requestSpy{} - build := newTestBuild() - list := newTestRackList() - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - json.NewEncoder(w).Encode(list) - })) - defer server.Close() - - API.URL = server.URL - - b := API.Builds() - - got := b.GetRacks(build.ID) - - assertRequestMethod(t, spy.requestMethod, "GET") - assertRequestCount(t, spy.requestCount, 1) - assertRequestPath(t, spy.requestPath, fmt.Sprintf("/build/%s/rack", build.ID)) - assert.Equal(t, got, list) -} - -func TestBuildsAddRack(t *testing.T) { - spy := requestSpy{} - build := newTestBuild() - rack := newTestRack() - var got Rack - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - body, _ := ioutil.ReadAll(r.Body) - json.NewDecoder(bytes.NewBuffer(body)).Decode(&got) - })) - defer server.Close() - - API.URL = server.URL - - b := API.Builds() - - b.AddRack(build.ID, rack.ID.String()) - - assertRequestMethod(t, spy.requestMethod, "POST") - assertRequestCount(t, spy.requestCount, 1) - assertRequestPath(t, spy.requestPath, fmt.Sprintf("/build/%s/rack/%s", build.ID, rack.ID)) -} - -func TestBuildsRemoveRack(t *testing.T) { - spy := requestSpy{} - build := newTestBuild() - rack := newTestRack() - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - })) - defer server.Close() - - API.URL = server.URL - - b := API.Builds() - b.RemoveRack(build.ID, rack.ID.String()) - - assertRequestMethod(t, spy.requestMethod, "DELETE") - assertRequestCount(t, spy.requestCount, 1) - assertRequestPath(t, spy.requestPath, fmt.Sprintf("/build/%s/rack/%s", build.ID, rack.ID)) -} diff --git a/cli/builds.go b/cli/builds.go new file mode 100644 index 0000000..6984292 --- /dev/null +++ b/cli/builds.go @@ -0,0 +1,304 @@ +package cli + +import ( + "fmt" + "strings" + + cli "github.com/jawher/mow.cli" + "github.com/joyent/kosh/conch/types" +) + +var buildRoleList = []string{"admin", "rw", "ro"} + +func prettyBuildRoleList() string { + return strings.Join(buildRoleList, ", ") +} + +func okBuildRole(role string) bool { + for _, b := range buildRoleList { + if role == b { + return true + } + } + return false +} + +func buildsCmd(cfg Config) func(cmd *cli.Cmd) { + conch := cfg.ConchClient() + display := cfg.Renderer() + log := cfg.GetLogger() + return func(cmd *cli.Cmd) { + cmd.Before = func() { + conch = cfg.ConchClient() + } + cmd.Action = func() { + log.Debug("display(conch.GetAllBuilds())") + display(conch.GetAllBuilds()) + } + cmd.Command("get ls", "Get a list of all builds", func(cmd *cli.Cmd) { + cmd.Action = func() { + log.Debug("display(conch.GetAllBuilds())") + display(conch.GetAllBuilds()) + } + }) + + cmd.Command("create", "Create a new build", func(cmd *cli.Cmd) { + nameArg := cmd.StringArg("NAME", "", "Name of the new build") + + descOpt := cmd.StringOpt("description", "", "A description of the build") + adminEmailArg := cmd.StringOpt( + "admin", + "", + "Email address for the (initial) admin user for the build. This does *not* create the user.", + ) + + cmd.Spec = "NAME [OPTIONS]" + cmd.Action = func() { + conch.CreateBuild( + types.BuildCreate{ + Name: types.MojoStandardPlaceholder(*nameArg), + Description: types.NonEmptyString(*descOpt), + Admins: []types.Admin{types.Admin{Email: types.EmailAddress(*adminEmailArg)}}, + }, + ) + display(conch.GetAllBuilds()) + } + }) + } +} + +func buildCmd(cfg Config) func(*cli.Cmd) { + conch := cfg.ConchClient() + display := cfg.Renderer() + + return func(cmd *cli.Cmd) { + var b types.Build + buildNameArg := cmd.StringArg("NAME", "", "Name or ID of the build") + + cmd.Spec = "NAME" + cmd.Before = func() { + conch = cfg.ConchClient() + b = conch.GetBuildByName(*buildNameArg) + } + + cmd.Action = func() { display(b) } + + cmd.Command("get", "Get information about a single build by its name", func(cmd *cli.Cmd) { + cmd.Action = func() { + display(b) + } + }) + + cmd.Command("users", "Manage users in a specific build", func(cmd *cli.Cmd) { + cmd.Action = func() { + display(conch.GetBuildUsers(*buildNameArg)) + } + cmd.Command("get ls", "Get a list of users in an build", func(cmd *cli.Cmd) { + cmd.Action = func() { + display(conch.GetBuildUsers(*buildNameArg)) + } + }) + + cmd.Command("add", "Add a user to an build", func(cmd *cli.Cmd) { + userEmailArg := cmd.StringArg( + "EMAIL", + "", + "The email of the user to add to the build. Does *not* create the user", + ) + + roleOpt := cmd.StringOpt( + "role", + "ro", + "The role for the user. One of: "+prettyBuildRoleList(), + ) + + sendEmailOpt := cmd.BoolOpt( + "send-email", + true, + "Send email to the target user, notifying them of the change", + ) + + cmd.Spec = "EMAIL [OPTIONS]" + cmd.Action = func() { + if !okBuildRole(*roleOpt) { + fatal(fmt.Errorf( + "'role' value must be one of: %s", + prettyBuildRoleList(), + )) + } + conch.AddBuildUser( + *buildNameArg, + types.BuildAddUser{ + Email: types.EmailAddress(*userEmailArg), + Role: types.Role(*roleOpt), + }, + *sendEmailOpt, + ) + display(conch.GetBuildUsers(*buildNameArg)) + } + }) + + cmd.Command("remove rm", "remove a user from an build", func(cmd *cli.Cmd) { + userEmailArg := cmd.StringArg( + "EMAIL", + "", + "The email or ID of the user to modify", + ) + + sendEmailOpt := cmd.BoolOpt( + "send-email", + true, + "Send email to the target user, notifying them of the change", + ) + cmd.Spec = "EMAIL [OPTIONS]" + cmd.Action = func() { + conch.DeleteBuildUser(*buildNameArg, *userEmailArg, *sendEmailOpt) + display(conch.GetBuildUsers(*buildNameArg)) + } + }) + }) + + cmd.Command("organizations orgs", "Manage organizations in a specific build", func(cmd *cli.Cmd) { + cmd.Command("get ls", "Get a list of organizations in an build", func(cmd *cli.Cmd) { + cmd.Action = func() { + display(conch.GetAllBuildOrganizations(*buildNameArg)) + } + }) + + cmd.Command("add", "Add a organization to an build", func(cmd *cli.Cmd) { + orgNameArg := cmd.StringArg( + "NAME", + "", + "The name of the organization to add to the build. Does *not* create the organization", + ) + + roleOpt := cmd.StringOpt( + "role", + "ro", + "The role for the organization. One of: "+prettyBuildRoleList(), + ) + + sendEmailOpt := cmd.BoolOpt( + "send-email", + true, + "Send email to the organization admins, notifying them of the change", + ) + + cmd.Spec = "NAME [OPTIONS]" + cmd.Action = func() { + if !okBuildRole(*roleOpt) { + fatal(fmt.Errorf( + "'role' value must be one of: %s", + prettyBuildRoleList(), + )) + } + org := conch.GetOrganizationByName(*orgNameArg) + + conch.AddBuildOrganization(*buildNameArg, types.BuildAddOrganization{ + org.ID, + types.Role(*roleOpt), + }, + *sendEmailOpt, + ) + display(conch.GetAllBuildOrganizations(*buildNameArg)) + } + }) + + cmd.Command("remove rm", "remove an organization from a build", func(cmd *cli.Cmd) { + orgNameArg := cmd.StringArg( + "NAME", + "", + "The name or ID of the organization to modify", + ) + + sendEmailOpt := cmd.BoolOpt( + "send-email", + true, + "Send email to the target organization admins, notifying them of the change", + ) + cmd.Spec = "EMAIL [OPTIONS]" + cmd.Action = func() { + conch.DeleteBuildOrganization(*buildNameArg, + *orgNameArg, + *sendEmailOpt, + ) + display(conch.GetAllBuildOrganizations(*buildNameArg)) + } + }) + }) + + cmd.Command("devices ds", "Manage devices in a specific build", func(cmd *cli.Cmd) { + cmd.Command("get ls", "Get a list of devices in an build", func(cmd *cli.Cmd) { + cmd.Action = func() { + display(conch.GetAllBuildDevices(*buildNameArg)) + } + }) + + cmd.Command("add", "Add a device to an build", func(cmd *cli.Cmd) { + deviceIDArg := cmd.StringArg( + "ID", + "", + "The ID or serial number of the device to add to the build. Does *not* create the device", + ) + + cmd.Spec = "ID [OPTIONS]" + cmd.Action = func() { + conch.AddBuildDeviceByName(*buildNameArg, *deviceIDArg) + display(conch.GetAllBuildDevices(*buildNameArg)) + } + }) + + cmd.Command("remove rm", "remove a device from a build", func(cmd *cli.Cmd) { + deviceIDArg := cmd.StringArg( + "ID", + "", + "The ID or serial number of the device to add to the build. Does *not* create the device", + ) + + cmd.Spec = "ID [OPTIONS]" + cmd.Action = func() { + build := conch.GetBuildByName(*buildNameArg) + device := conch.GetDeviceBySerial(*deviceIDArg) + conch.DeleteBuildDeviceByID(build.ID, device.ID) + display(conch.GetAllBuildDevices(*buildNameArg)) + } + }) + }) + + cmd.Command("racks", "Manage racks in a specific build", func(cmd *cli.Cmd) { + cmd.Command("get ls", "Get a list of racks in an build", func(cmd *cli.Cmd) { + cmd.Action = func() { + display(conch.GetBuildRacks(*buildNameArg)) + } + }) + + cmd.Command("add", "Add a rack to an build", func(cmd *cli.Cmd) { + rackIDArg := cmd.StringArg( + "ID", + "", + "The ID of the rack to add to the build. Does *not* create the rack", + ) + + cmd.Spec = "ID [OPTIONS]" + cmd.Action = func() { + conch.AddBuildRackByID(*buildNameArg, *rackIDArg) + display(conch.GetBuildRacks(*buildNameArg)) + } + }) + + cmd.Command("remove rm", "remove a rack from a build", func(cmd *cli.Cmd) { + rackIDArg := cmd.StringArg( + "ID", + "", + "The ID of the rack to add to the build. Does *not* create the device", + ) + + cmd.Spec = "ID [OPTIONS]" + cmd.Action = func() { + conch.DeleteBuildRackByID(*buildNameArg, *rackIDArg) + display(conch.GetBuildRacks(*buildNameArg)) + } + }) + }) + } +} diff --git a/cli/cli.go b/cli/cli.go index e567918..00a4957 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -1,66 +1,122 @@ package cli import ( + "fmt" + "io" + "os" + cli "github.com/jawher/mow.cli" + "github.com/joyent/kosh/logger" ) const ( - ProductionURL = "https://conch.joyent.us" - StagingURL = "https://staging.conch.joyent.us" + productionURL = "https://conch.joyent.us" + stagingURL = "https://staging.conch.joyent.us" + edgeURL = "https://edge.conch.joyent.us" ) -func NewApp(config Config) *cli.Cli { +func fatal(e error) { + fmt.Println(e) + cli.Exit(1) +} + +func getInputReader(filePathArg string) (io.Reader, error) { + if filePathArg == "-" { + return os.Stdin, nil + } + return os.Open(filePathArg) +} +func requireSysAdmin(c Config) func() { + return func() { + if !c.ConchClient().IsSysAdmin() { + fmt.Println("This action requires Conch systems administrator privileges") + cli.Exit(1) + } + } +} + +// NewApp creates a new kosh app, takes a cli.Config and returns an instance of cli.Cli +func NewApp(config Config) *cli.Cli { app := cli.App("kosh", "Command line interface for Conch") + app.Spec = "[-vVdj]" - app.Version("V version", config.Version) + app.Version("V version", config.GetVersion()) - config.ConchToken = *app.String(cli.StringOpt{ + conchToken := app.String(cli.StringOpt{ Name: "t token", Value: "", Desc: "API token", EnvVar: "KOSH_TOKEN", }) - config.ConchEnvironment = *app.String(cli.StringOpt{ - Name: "environment env", - Value: "production", - Desc: "Specify the environment to be used: production, staging, development (provide URL in the --url parameter)", - EnvVar: "KOSH_ENV", - }) - - config.ConchURL = *app.String(cli.StringOpt{ + conchURL := app.String(cli.StringOpt{ Name: "u url", - Value: "", - Desc: "If the environment is 'development', this specifies the API URL. Ignored if --environment is 'production' or 'staging'", + Value: productionURL, + Desc: "This specifies the API URL.", EnvVar: "KOSH_URL", }) - config.OutputJSON = *app.Bool(cli.BoolOpt{ + outputJSON := app.Bool(cli.BoolOpt{ Name: "j json", Value: false, Desc: "Output JSON only", EnvVar: "KOSH_JSON_ONLY", }) - config.StrictParse = *app.Bool(cli.BoolOpt{ - Name: "strict", + levelDebug := app.Bool(cli.BoolOpt{ + Name: "d debug", Value: false, - Desc: "Intended for developers. Parse API responses strictly, not allowing new fields", - EnvVar: "KOSH_DEVEL_STRICT", + Desc: "Enable Debugging output (for debugging purposes *very* noisy). ", + EnvVar: "KOSH_DEBUG_MODE", }) - config.DevMode = *app.Bool(cli.BoolOpt{ - Name: "developer", + levelInfo := app.Bool(cli.BoolOpt{ + Name: "v verbose", Value: false, - Desc: "Activate developer mode. This disables most user-friendly protections, is noisy, and switches to developer-friendly output where appropriate", - EnvVar: "KOSH_DEVEL_MODE", + Desc: "Enable Verbose Output", + EnvVar: "KOSH_VERBOSE_MODE", }) - app.Command("whoami", "Display details of the current user", whoamiCmd(config)) - app.Command("user", "Commands for dealing with the current user (you)", userCmd(config)) - app.Command("devices ds", "Commands for dealing with multiple devices", devicesCmd(config)) + app.Command("build b", "Work with a specific build", buildCmd(config)) + app.Command("builds bs", "Work with builds", buildsCmd(config)) + app.Command("datacenter dc", "Deal with a single datacenter", datacenterCmd(config)) + app.Command("datacenters dcs", "Work with the datacenters you have access to", datacentersCmd(config)) app.Command("device d", "Perform actions against a single device", deviceCmd(config)) + app.Command("device-report dr", "Deal with device reports", deviceReportCmd(config)) + app.Command("devices ds", "Commands for dealing with multiple devices", devicesCmd(config)) + app.Command("hardware h", "Work with hardware profiles and vendors", hardwareCmd(config)) + app.Command("organization org", "Work with a specific organization", organizationCmd(config)) + app.Command("organizations orgs", "Work with organizations", organizationsCmd(config)) + app.Command("rack r", "Work with a single rack", rackCmd(config)) + app.Command("racks rs", "Work with datacenter racks", racksCmd(config)) + app.Command("relay", "Perform actions against a single relay", relayCmd(config)) + app.Command("relays", "Perform actions against the whole list of relays", relaysCmd(config)) + app.Command("roles", "Work with datacenter rack roles", rolesCmd(config)) + app.Command("role", "Work with a single rack role", roleCmd(config)) + app.Command("room", "Deal with a single datacenter room", roomCmd(config)) + app.Command("rooms", "Work with datacenter rooms", roomsCmd(config)) + app.Command("schema", "Get the server JSON Schema for a given request or response", schemaCmd(config)) + app.Command("user u", "Commands for dealing with the current user (you)", userCmd(config)) + app.Command("validation v", "Work with validations", validationCmd(config)) + app.Command("whoami", "Display details of the current user", whoamiCmd(config)) + + app.Before = func() { + if *conchToken == "" { + fmt.Println("Need to provide --token or set KOSH_TOKEN") + cli.Exit(1) + } + + config.SetURL(*conchURL) + config.SetToken(*conchToken) + config.SetOutputJSON(*outputJSON) + config.SetLogger(logger.Logger{ + LevelDebug: *levelDebug, + LevelInfo: *levelInfo, + }) + config.GetLogger().Debug("Starting App") + config.GetLogger().Info(config) + } return app } diff --git a/cli/config.go b/cli/config.go index 66d817c..db6e3f8 100644 --- a/cli/config.go +++ b/cli/config.go @@ -1,19 +1,39 @@ package cli import ( + "encoding/json" "fmt" - "os/user" - "runtime" + "io" + "log" + "os" + "strings" "github.com/joyent/kosh/conch" + "github.com/joyent/kosh/logger" + "github.com/joyent/kosh/tables" + "github.com/joyent/kosh/template" ) -type Logger interface { - Debug(...interface{}) - Info(...interface{}) +type Config interface { + GetVersion() string + + SetURL(string) + + SetToken(string) + + SetLogger(logger.Logger) + + GetOutputJSON() bool + SetOutputJSON(bool) + + ConchClient() *conch.Client + Renderer() func(interface{}) + + conch.Config } -type Config struct { +// Config struct +type DefaultConfig struct { Version string GitRev string @@ -21,41 +41,92 @@ type Config struct { ConchToken string ConchEnvironment string - OutputJSON bool - StrictParse bool - DevMode bool + OutputJSON bool - Logger + logger.Logger } -func NewConfig(Version, GitRev string) Config { - return Config{ +func (c *DefaultConfig) GetVersion() string { return c.Version } +func (c *DefaultConfig) GetURL() string { return c.ConchURL } +func (c *DefaultConfig) GetToken() string { return c.ConchToken } +func (c *DefaultConfig) GetLogger() logger.Logger { return c.Logger } + +func (c *DefaultConfig) SetURL(URL string) { c.ConchURL = URL } +func (c *DefaultConfig) SetToken(token string) { c.ConchToken = token } +func (c *DefaultConfig) GetOutputJSON() bool { return c.OutputJSON } +func (c *DefaultConfig) SetOutputJSON(p bool) { c.OutputJSON = p } +func (c *DefaultConfig) SetLogger(l logger.Logger) { c.Logger = l } + +// NewConfig takes a Version and a GitRev and returns a Config object +func NewConfig(Version, GitRev string) *DefaultConfig { + return &DefaultConfig{ Version: Version, GitRev: GitRev, + Logger: logger.New(), } } -func BuildUserAgent(GitRev string) map[string]string { - var isRoot bool - if current, err := user.Current(); err == nil { - if current.Uid == "0" { - isRoot = true - } +const configTemplate = ` +--- +# Config + +* Version: {{ .Version }} +* GitRev: {{ .GitRev }} + +* ConchURL: {{ .ConchURL }} +* ConchToken: {{ .ConchToken }} +* ConchEnvironment: {{ .ConchEnvironment }} + +* OutputJSON: {{ .OutputJSON }} +--- +` + +func (c *DefaultConfig) String() string { + t, err := template.NewTemplate().Parse(configTemplate) + if err != nil { + log.Fatal(err) + } + + buf := &strings.Builder{} + if e := t.Execute(buf, c); e != nil { + log.Fatal(e) } + return buf.String() +} - agentBits := make(map[string]string) - agent := fmt.Sprintf( - "%s (%s; %s; r=%v)", - GitRev, - runtime.GOOS, - runtime.GOARCH, - isRoot, - ) +// ConchClient returns a configured client for the Conch API +func (c *DefaultConfig) ConchClient() *conch.Client { + return conch.New(c) +} - agentBits["Kosh"] = agent - return agentBits +func (c *DefaultConfig) Renderer() func(interface{}) { + return c.RenderTo(os.Stdout) } -func (c Config) ConchClient() *conch.Client { - return conch.New(c.ConchURL, c.ConchToken, c.Logger) +func renderJSON(i interface{}) string { + b, e := json.Marshal(i) + if e != nil { + fatal(e) + } + return string(b) +} + +func (c *DefaultConfig) RenderTo(w io.Writer) func(interface{}) { + return func(i interface{}) { + if c.OutputJSON { + fmt.Fprintln(w, renderJSON(i)) + } + switch t := i.(type) { + case template.Templated: + s, e := template.Render(t) + if e != nil { + fatal(e) + } + fmt.Fprintln(w, s) + case tables.Tabulable: + fmt.Fprintln(w, tables.Render(t)) + default: + fmt.Fprintln(w, renderJSON(t)) + } + } } diff --git a/cli/config_test.go b/cli/config_test.go new file mode 100644 index 0000000..970e8cf --- /dev/null +++ b/cli/config_test.go @@ -0,0 +1,237 @@ +package cli_test + +import ( + "bytes" + "net/http" + "net/http/httptest" + "testing" + + "github.com/joyent/kosh/cli" + "github.com/joyent/kosh/conch" + "github.com/joyent/kosh/conch/types" + "github.com/joyent/kosh/logger" + + "github.com/stretchr/testify/assert" +) + +type config struct { + url string + token string + logger logger.Logger +} + +func (c config) GetURL() string { return c.url } +func (c config) GetToken() string { return c.token } +func (c config) GetLogger() logger.Logger { return c.logger } + +func newConfig(URL string) config { + return config{ + URL, + "token", + logger.New(), + } +} + +func TestRender(t *testing.T) { + buffer := bytes.NewBufferString("") + display := cli.NewConfig("test", "test").RenderTo(buffer) + + // TODO replace with fixtures + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + conch := conch.New(newConfig(ts.URL)) + + tests := []struct { + Name string + Do func() + }{ + { + Name: "GetCurrentUser()", + Do: func() { display(conch.GetCurrentUser()) }, + }, + + { + Name: "display(conch.FindDevicesByField(\"hostname\", hostname))", + Do: func() { display(conch.FindDevicesByField("hostname", "foo")) }, + }, + + { + Name: "display(conch.FindDevicesBySetting(key, value))", + Do: func() { display(conch.FindDevicesBySetting("foo", "bar")) }, + }, + + { + Name: "display(conch.FindDevicesByTag(key, value))", + Do: func() { display(conch.FindDevicesByTag("foo", "bar")) }, + }, + + { + Name: " display(conch.GetAllBuildDevices(*buildNameArg))", + Do: func() { display(conch.GetAllBuildDevices("build-000")) }, + }, + + { + Name: " display(conch.GetAllBuildOrganizations(*buildNameArg))", + Do: func() { display(conch.GetAllBuildOrganizations("build-000")) }, + }, + + { + Name: " display(conch.GetAllBuilds())", + Do: func() { display(conch.GetAllBuilds()) }, + }, + + { + Name: " display(conch.GetAllDatacenterRooms(dc.ID))", + Do: func() { display(conch.GetAllDatacenterRooms(types.UUID{})) }, + }, + + { + Name: " display(conch.GetAllDatacenters())", + Do: func() { display(conch.GetAllDatacenters()) }, + }, + + { + Name: " display(conch.GetAllRackRoles())", + Do: func() { display(conch.GetAllRackRoles()) }, + }, + + { + Name: " display(conch.GetAllRelays())", + Do: func() { display(conch.GetAllRelays()) }, + }, + + { + Name: " display(conch.GetAllRoomRacks(room.ID))", + Do: func() { display(conch.GetAllRoomRacks(types.UUID{})) }, + }, + + { + Name: " display(conch.GetAllRooms())", + Do: func() { display(conch.GetAllRooms()) }, + }, + + { + Name: " display(conch.GetAllValidationPlans())", + Do: func() { display(conch.GetAllValidationPlans()) }, + }, + + { + Name: " display(conch.GetBuildRacks(*buildNameArg))", + Do: func() { display(conch.GetBuildRacks("build-001")) }, + }, + + { + Name: " display(conch.GetBuildUsers(*buildNameArg))", + Do: func() { display(conch.GetBuildUsers("build-001")) }, + }, + + { + Name: " display(conch.GetCurrentUser())", + Do: func() { display(conch.GetCurrentUser()) }, + }, + + { + Name: " display(conch.GetCurrentUserSettingByName(setting))", + Do: func() { display(conch.GetCurrentUserSettingByName("foo")) }, + }, + + { + Name: " display(conch.GetCurrentUserSettings())", + Do: func() { display(conch.GetCurrentUserSettings()) }, + }, + + { + Name: " display(conch.GetDeviceBySerial(*id))", + Do: func() { display(conch.GetDeviceBySerial("serial")) }, + }, + + { + Name: " display(conch.GetDeviceInterfaceByName(*id, name))", + Do: func() { display(conch.GetDeviceInterfaceByName("foo", "device")) }, + }, + + { + Name: " display(conch.GetDeviceLocation(*id))", + Do: func() { display(conch.GetDeviceLocation("foo")) }, + }, + + { + Name: " display(conch.GetDevicePhase(*id))", + Do: func() { display(conch.GetDevicePhase("foo")) }, + }, + + { + Name: " display(conch.GetDeviceSettingByName(*id, key))", + Do: func() { display(conch.GetDeviceSettingByName("foo", "bar")) }, + }, + + { + Name: " display(conch.GetDeviceSettings(*id))", + Do: func() { display(conch.GetDeviceSettings("foo")) }, + }, + + { + Name: " display(conch.GetDeviceTagByName(*id, name))", + Do: func() { display(conch.GetDeviceTagByName("foo", "bar")) }, + }, + + { + Name: " display(conch.GetDeviceTags(*id))", + Do: func() { display(conch.GetDeviceTags("foo")) }, + }, + + { + Name: " display(conch.GetDeviceValidationStates(*id))", + Do: func() { display(conch.GetDeviceValidationStates("foo")) }, + }, + + { + Name: " display(conch.GetHardwareProductByID(*name))", + Do: func() { display(conch.GetHardwareProductByID("foo")) }, + }, + + { + Name: " display(conch.GetHardwareProducts())", + Do: func() { display(conch.GetHardwareProducts()) }, + }, + + { + Name: " display(conch.GetHardwareVendors())", + Do: func() { display(conch.GetHardwareVendors()) }, + }, + + { + Name: " display(conch.GetOrganizationByID(o.ID))", + Do: func() { display(conch.GetOrganizationByID(types.UUID{})) }, + }, + + { + Name: " display(conch.GetOrganizations())", + Do: func() { display(conch.GetOrganizations()) }, + }, + + { + Name: " display(conch.GetRackAssignments(rack.ID))", + Do: func() { display(conch.GetRackAssignments(types.UUID{})) }, + }, + + { + Name: " display(conch.GetRackLayout(rack.ID))", + Do: func() { display(conch.GetRackLayout(types.UUID{})) }, + }, + + { + Name: " display(conch.GetRoomByID(room.ID))", + Do: func() { display(conch.GetRoomByID(types.UUID{})) }, + }, + } + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + defer ts.Close() + test.Do() + assert.NotEmpty(t, buffer.String()) + buffer.Reset() + }) + } +} diff --git a/cli/datacenters.go b/cli/datacenters.go new file mode 100644 index 0000000..a96e177 --- /dev/null +++ b/cli/datacenters.go @@ -0,0 +1,152 @@ +package cli + +import ( + "errors" + "fmt" + + cli "github.com/jawher/mow.cli" + "github.com/joyent/kosh/conch/types" +) + +func datacentersCmd(cfg Config) func(*cli.Cmd) { + conch := cfg.ConchClient() + display := cfg.Renderer() + + return func(cmd *cli.Cmd) { + cmd.Before = func() { + conch = cfg.ConchClient() + } + cmd.Command("get", "Get a list of all datacenters", func(cmd *cli.Cmd) { + cmd.Action = func() { + display(conch.GetAllDatacenters()) + } + }) + + cmd.Command("create", "Create a single datacenter", func(cmd *cli.Cmd) { + var ( + vendorOpt = cmd.StringOpt("vendor", "", "Vendor") + regionOpt = cmd.StringOpt("region", "", "Region") + locationOpt = cmd.StringOpt("location", "", "Location") + vendorNameOpt = cmd.StringOpt("vendor-name", "", "Vendor Name") + ) + + cmd.Spec = "--vendor --region --location [OPTIONS]" + cmd.Action = func() { + // The user can be very silly and supply something like + // '--vendor ""' which will pass the cli lib's requirement + // check but is still crap + if *vendorOpt == "" { + fmt.Println("--vendor is required") + cli.Exit(1) + } + if *regionOpt == "" { + fmt.Println("--region is required") + cli.Exit(1) + } + if *locationOpt == "" { + fmt.Println("--location is required") + cli.Exit(1) + } + + conch.CreateDatacenter(types.DatacenterCreate{ + Location: types.NonEmptyString(*locationOpt), + Region: types.NonEmptyString(*regionOpt), + Vendor: types.NonEmptyString(*vendorOpt), + VendorName: types.NonEmptyString(*vendorNameOpt), + }) + } + }) + } +} + +func datacenterCmd(cfg Config) func(*cli.Cmd) { + conch := cfg.ConchClient() + display := cfg.Renderer() + + return func(cmd *cli.Cmd) { + var dc types.Datacenter + + idArg := cmd.StringArg( + "UUID", + "", + "The UUID of the datacenter. Short UUIDs (first segment) accepted", + ) + + cmd.Spec = "UUID" + + cmd.Before = func() { + conch = cfg.ConchClient() + dc = conch.GetDatacenterByName(*idArg) + if (dc == types.Datacenter{}) { + fatal(errors.New("couldn't find datacenter")) + } + } + cmd.Command("get", "Information about a single datacenter", func(cmd *cli.Cmd) { + cmd.Action = func() { + display(dc) + } + }) + + cmd.Command("delete", "Delete a single datacenter", func(cmd *cli.Cmd) { + cmd.Action = func() { + conch.DeleteDatacenter(dc.ID) + display(conch.GetAllDatacenters()) + } + }) + + cmd.Command("update", "Update a single datacenter", func(cmd *cli.Cmd) { + regionOpt := cmd.StringOpt( + "region", + "", + "Region identifier", + ) + vendorOpt := cmd.StringOpt( + "vendor", + "", + "Vendor", + ) + vendorNameOpt := cmd.StringOpt( + "vendor-name", + "", + "Vendor Name", + ) + locationOpt := cmd.StringOpt( + "location", + "", + "Location", + ) + + cmd.Action = func() { + var count int + if *regionOpt != "" { + count++ + } + if *vendorOpt != "" { + count++ + } + if *vendorNameOpt != "" { + count++ + } + if *locationOpt != "" { + count++ + } + + if count == 0 { + fatal(errors.New("one option must be provided")) + } + conch.UpdateDatacenter(dc.ID, types.DatacenterUpdate{ + Location: types.NonEmptyString(*locationOpt), + Region: types.NonEmptyString(*regionOpt), + Vendor: types.NonEmptyString(*vendorOpt), + VendorName: types.NonEmptyString(*vendorNameOpt), + }) + } + }) + + cmd.Command("rooms", "Get the room list for a single datacenter", func(cmd *cli.Cmd) { + cmd.Action = func() { + display(conch.GetAllDatacenterRooms(dc.ID)) + } + }) + } +} diff --git a/cli/device_reports.go b/cli/device_reports.go new file mode 100644 index 0000000..4008a76 --- /dev/null +++ b/cli/device_reports.go @@ -0,0 +1,27 @@ +package cli + +import ( + cli "github.com/jawher/mow.cli" +) + +func deviceReportCmd(cfg Config) func(cmd *cli.Cmd) { + return func(cmd *cli.Cmd) { + cmd.Command("post", "Post a new device report", deviceReportPostCmd(cfg)) + } +} + +func deviceReportPostCmd(cfg Config) func(*cli.Cmd) { + conch := cfg.ConchClient() + + return func(cmd *cli.Cmd) { + filePathArg := cmd.StringArg("FILE", "-", "Path to a JSON file that defines the layout. '-' indicates STDIN") + + input, err := getInputReader(*filePathArg) + if err != nil { + fatal(err) + } + + cmd.Before = func() { conch = cfg.ConchClient() } + cmd.Action = func() { conch.SendDeviceReport(input) } + } +} diff --git a/cli/devices.go b/cli/devices.go index 272cf05..4e8da02 100644 --- a/cli/devices.go +++ b/cli/devices.go @@ -9,19 +9,6 @@ import ( cli "github.com/jawher/mow.cli" ) -type Renderable interface { - JSON() ([]byte, error) - String() string -} - -func display(cfg Config, item Renderable) { - if cfg.OutputJSON { - fmt.Println(item.JSON()) - } else { - fmt.Println(item.String()) - } -} - func devicesCmd(cfg Config) func(cmd *cli.Cmd) { return func(cmd *cli.Cmd) { cmd.Command("search s", "Search for devices", deviceSearchCmd(cfg)) @@ -37,39 +24,42 @@ func deviceSearchCmd(cfg Config) func(cmd *cli.Cmd) { } func searchBySettingCmd(cfg Config) func(cmd *cli.Cmd) { - conch := cfg.ConchClient() + display := cfg.Renderer() return func(cmd *cli.Cmd) { key := *cmd.StringArg("KEY", "", "Setting name") value := *cmd.StringArg("VALUE", "", "Setting Value") cmd.Spec = "KEY VALUE" cmd.Action = func() { - display(cfg, conch.FindDevicesBySetting(key, value)) + conch := cfg.ConchClient() + display(conch.FindDevicesBySetting(key, value)) } } } func searchByTagCmd(cfg Config) func(cmd *cli.Cmd) { - conch := cfg.ConchClient() + display := cfg.Renderer() return func(cmd *cli.Cmd) { key := *cmd.StringArg("KEY", "", "Tag name") value := *cmd.StringArg("VALUE", "", "Tag Value") cmd.Spec = "KEY VALUE" cmd.Action = func() { - display(cfg, conch.FindDevicesByTag(key, value)) + conch := cfg.ConchClient() + display(conch.FindDevicesByTag(key, value)) } } } func searchByHostnameCmd(cfg Config) func(cmd *cli.Cmd) { - conch := cfg.ConchClient() + display := cfg.Renderer() return func(cmd *cli.Cmd) { hostname := *cmd.StringArg("HOSTNAME", "", "hostname") cmd.Spec = "HOSTNAME" cmd.Action = func() { - display(cfg, conch.FindDevicesByField("hostname", hostname)) + conch := cfg.ConchClient() + display(conch.FindDevicesByField("hostname", hostname)) } } } @@ -77,12 +67,11 @@ func searchByHostnameCmd(cfg Config) func(cmd *cli.Cmd) { // Single Device Commands func deviceCmd(cfg Config) func(cmd *cli.Cmd) { return func(cmd *cli.Cmd) { - id := *cmd.StringArg( + id := cmd.StringArg( "DEVICE", "", "UUID or serial number of the device. Short UUIDs are *not* accepted", ) - cmd.Spec = "DEVICE" cmd.Command("get", "Get information about a single device", deviceGetCmd(cfg, id)) @@ -95,33 +84,43 @@ func deviceCmd(cfg Config) func(cmd *cli.Cmd) { cmd.Command("preflight", "Data that is only accurate inside preflight", devicePreflightCmd(cfg, id)) cmd.Command("phase", "Actions on the lifecycle phase of the device", devicePhaseCmd(cfg, id)) - cmd.Command("report", "Get the most recently recorded report for this device", deviceReportCmd(cfg, id)) + cmd.Command("report", "Get the most recently recorded report for this device", deviceDeviceReportCmd(cfg, id)) } } -func deviceGetCmd(cfg Config, id string) func(cmd *cli.Cmd) { - conch := cfg.ConchClient() +func deviceGetCmd(cfg Config, id *string) func(cmd *cli.Cmd) { + display := cfg.Renderer() return func(cmd *cli.Cmd) { - cmd.Action = func() { display(cfg, conch.GetDeviceById(id)) } + cmd.Action = func() { + conch := cfg.ConchClient() + display(conch.GetDeviceBySerial(*id)) + } } } -func deviceValidationsCmd(cfg Config, id string) func(cmd *cli.Cmd) { - conch := cfg.ConchClient() +func deviceValidationsCmd(cfg Config, id *string) func(cmd *cli.Cmd) { + display := cfg.Renderer() return func(cmd *cli.Cmd) { - cmd.Action = func() { display(cfg, conch.GetDeviceValidationStates(id)) } + cmd.Action = func() { + conch := cfg.ConchClient() + display(conch.GetDeviceValidationStates(*id)) + } } } -func deviceSettingsCmd(cfg Config, id string) func(cmd *cli.Cmd) { - conch := cfg.ConchClient() +func deviceSettingsCmd(cfg Config, id *string) func(cmd *cli.Cmd) { + display := cfg.Renderer() return func(cmd *cli.Cmd) { - cmd.Action = func() { display(cfg, conch.GetDeviceSettings(id)) } + cmd.Action = func() { + conch := cfg.ConchClient() + display(conch.GetDeviceSettings(*id)) + } } } -func deviceSettingCmd(cfg Config, id string) func(cmd *cli.Cmd) { +func deviceSettingCmd(cfg Config, id *string) func(cmd *cli.Cmd) { conch := cfg.ConchClient() + display := cfg.Renderer() return func(cmd *cli.Cmd) { key := *cmd.StringArg( "NAME", @@ -130,13 +129,15 @@ func deviceSettingCmd(cfg Config, id string) func(cmd *cli.Cmd) { ) cmd.Spec = "NAME" - + cmd.Before = func() { + conch = cfg.ConchClient() + } cmd.Action = func() { - display(cfg, conch.GetDeviceSettingByName(id, key)) + display(conch.GetDeviceSettingByName(*id, key)) } cmd.Command("get", "Get a particular device setting", func(cmd *cli.Cmd) { - cmd.Action = func() { display(cfg, conch.GetDeviceSettingByName(id, key)) } + cmd.Action = func() { display(conch.GetDeviceSettingByName(*id, key)) } }) cmd.Command("set", "Set a particular device setting", func(cmd *cli.Cmd) { @@ -144,38 +145,46 @@ func deviceSettingCmd(cfg Config, id string) func(cmd *cli.Cmd) { cmd.Spec = "VALUE" cmd.Action = func() { - conch.SetDeviceSetting(id, key, value) - display(cfg, conch.GetDeviceSettings(id)) + conch.SetDeviceSetting(*id, key, value) + display(conch.GetDeviceSettings(*id)) } }) cmd.Command("delete rm", "Delete a particular device setting", func(cmd *cli.Cmd) { cmd.Action = func() { - conch.DeleteDeviceSetting(id, key) - display(cfg, conch.GetDeviceSettings(id)) + conch.DeleteDeviceSetting(*id, key) + display(conch.GetDeviceSettings(*id)) } }) } } -func deviceTagsCmd(cfg Config, id string) func(cmd *cli.Cmd) { - conch := cfg.ConchClient() +func deviceTagsCmd(cfg Config, id *string) func(cmd *cli.Cmd) { + display := cfg.Renderer() return func(cmd *cli.Cmd) { - cmd.Action = func() { display(cfg, conch.GetDeviceTags(id)) } + cmd.Action = func() { + conch := cfg.ConchClient() + display(conch.GetDeviceTags(*id)) + } } } -func deviceTagCmd(cfg Config, id string) func(cmd *cli.Cmd) { +func deviceTagCmd(cfg Config, id *string) func(cmd *cli.Cmd) { conch := cfg.ConchClient() + display := cfg.Renderer() return func(cmd *cli.Cmd) { name := *cmd.StringArg("NAME", "", "Name of the tag") cmd.Spec = "NAME" - cmd.Action = func() { display(cfg, conch.GetDeviceTagByName(id, name)) } + cmd.Before = func() { + conch = cfg.ConchClient() + } + + cmd.Action = func() { display(conch.GetDeviceTagByName(*id, name)) } cmd.Command("get", "Get a particular device tag", func(cmd *cli.Cmd) { - cmd.Action = func() { display(cfg, conch.GetDeviceTagByName(id, name)) } + cmd.Action = func() { display(conch.GetDeviceTagByName(*id, name)) } }) cmd.Command("set", "Set a particular device tag", func(cmd *cli.Cmd) { @@ -183,45 +192,50 @@ func deviceTagCmd(cfg Config, id string) func(cmd *cli.Cmd) { cmd.Spec = "VALUE" cmd.Action = func() { - conch.SetDeviceTag(id, name, value) - display(cfg, conch.GetDeviceTags(id)) + conch.SetDeviceTag(*id, name, value) + display(conch.GetDeviceTags(*id)) } }) cmd.Command("delete rm", "Delete a particular device tag", func(cmd *cli.Cmd) { cmd.Action = func() { - conch.DeleteDeviceTag(id, name) - display(cfg, conch.GetDeviceTags(id)) + conch.DeleteDeviceTag(*id, name) + display(conch.GetDeviceTags(*id)) } }) } } -func deviceInterfaceCmd(cfg Config, id string) func(cmd *cli.Cmd) { - conch := cfg.ConchClient() +func deviceInterfaceCmd(cfg Config, id *string) func(cmd *cli.Cmd) { + display := cfg.Renderer() return func(cmd *cli.Cmd) { name := *cmd.StringArg("NAME", "", "Name of the interface") cmd.Spec = "NAME" - cmd.Action = func() { display(cfg, conch.GetDeviceInterfaceByName(id, name)) } + cmd.Action = func() { + conch := cfg.ConchClient() + display(conch.GetDeviceInterfaceByName(*id, name)) + } } } -func devicePreflightCmd(cfg Config, id string) func(cmd *cli.Cmd) { +func devicePreflightCmd(cfg Config, id *string) func(cmd *cli.Cmd) { conch := cfg.ConchClient() + display := cfg.Renderer() return func(cmd *cli.Cmd) { cmd.Before = func() { - if conch.GetDevicePhase(id) != "integration" { + conch = cfg.ConchClient() + if conch.GetDevicePhase(*id) != "integration" { os.Stderr.WriteString("Warning: This device is no longer in the 'integration' phase. This data is likely to be inaccurate\n") } } cmd.Command("location", "The location of a device in preflight", func(cmd *cli.Cmd) { - cmd.Action = func() { display(cfg, conch.GetDeviceLocation(id)) } + cmd.Action = func() { display(conch.GetDeviceLocation(*id)) } }) cmd.Command("ipmi", "IPMI address for a device in preflight", func(cmd *cli.Cmd) { cmd.Action = func() { - iface := conch.GetDeviceInterfaceByName(id, "ipmi1") + iface := conch.GetDeviceInterfaceByName(*id, "ipmi1") fmt.Println(iface.Ipaddr) } }) @@ -229,7 +243,7 @@ func devicePreflightCmd(cfg Config, id string) func(cmd *cli.Cmd) { } /***/ -var PhasesList = []string{ +var phasesList = []string{ "integration", "installation", "production", @@ -238,11 +252,11 @@ var PhasesList = []string{ } func prettyPhasesList() string { - return strings.Join(PhasesList, ", ") + return strings.Join(phasesList, ", ") } func okPhase(phase string) bool { - for _, b := range PhasesList { + for _, b := range phasesList { if phase == b { return true } @@ -250,11 +264,16 @@ func okPhase(phase string) bool { return false } -func devicePhaseCmd(cfg Config, id string) func(cmd *cli.Cmd) { +func devicePhaseCmd(cfg Config, id *string) func(cmd *cli.Cmd) { conch := cfg.ConchClient() + display := cfg.Renderer() return func(cmd *cli.Cmd) { + cmd.Before = func() { + conch = cfg.ConchClient() + } + cmd.Command("get", "Get the phase of the device", func(cmd *cli.Cmd) { - cmd.Action = func() { display(cfg, conch.GetDevicePhase(id)) } + cmd.Action = func() { display(conch.GetDevicePhase(*id)) } }) cmd.Command("set", "Set the phase of the device [one of: "+prettyPhasesList()+"]", func(cmd *cli.Cmd) { @@ -264,16 +283,19 @@ func devicePhaseCmd(cfg Config, id string) func(cmd *cli.Cmd) { if !okPhase(phase) { log.Fatal("Phase must be one of: " + prettyPhasesList()) } - conch.SetDevicePhase(id, phase) - display(cfg, conch.GetDevicePhase(id)) + conch.SetDevicePhase(*id, phase) + display(conch.GetDevicePhase(*id)) } }) } } -func deviceReportCmd(cfg Config, id string) func(cmd *cli.Cmd) { - conch := cfg.ConchClient() +func deviceDeviceReportCmd(cfg Config, id *string) func(cmd *cli.Cmd) { + display := cfg.Renderer() return func(cmd *cli.Cmd) { - cmd.Action = func() { display(cfg, conch.GetDeviceById(id).LatestReport) } + cmd.Action = func() { + conch := cfg.ConchClient() + display(conch.GetDeviceBySerial(*id).LatestReport) + } } } diff --git a/cli/hardware.go b/cli/hardware.go new file mode 100644 index 0000000..1297e3f --- /dev/null +++ b/cli/hardware.go @@ -0,0 +1,127 @@ +package cli + +import ( + "fmt" + + cli "github.com/jawher/mow.cli" + "github.com/joyent/kosh/conch/types" +) + +func cmdCreateProduct(cfg Config) func(*cli.Cmd) { + display := cfg.Renderer() + return func(cmd *cli.Cmd) { + var ( + name = cmd.StringOpt("name", "", "Name of the hardware product") + alias = cmd.StringOpt("alias", "", "Alias for the hardware product") + vendor = cmd.StringOpt("vendor", "", "Vendor of the hardware product") + SKU = cmd.StringOpt("sku", "", "SKU for the hardware product") + rackUnitSize = cmd.IntOpt("rack-unit-size", 2, "RU size of the hardware product") + validationPlanOpt = cmd.StringOpt("validation-plan", "", "Name of the plan to validate the product against") + purpose = cmd.StringOpt("purpose", "", "Purpose of the product") + biosFirmware = cmd.StringOpt("bios-firmware", "", "BIOS firmware version for the product") + cpuType = cmd.StringOpt("cpu-type", "", "CPU type for the product") + ) + + cmd.Spec = "--sku --name --alias --vendor --validation-plan --purpose --bios-firmware --cpu-type [OPTIONS]" + cmd.Action = func() { + conch := cfg.ConchClient() + validationPlan := conch.GetValidationPlanByName(*validationPlanOpt) + vendor := conch.GetHardwareVendorByID(*vendor) + create := types.HardwareProductCreate{ + Name: types.MojoStandardPlaceholder(*name), + Alias: types.MojoStandardPlaceholder(*alias), + HardwareVendorID: vendor.ID, + Sku: types.MojoStandardPlaceholder(*SKU), + RackUnitSize: types.PositiveInteger(*rackUnitSize), + ValidationPlanID: validationPlan.ID, + Purpose: *purpose, + BiosFirmware: *biosFirmware, + CPUType: *cpuType, + } + conch.CreateHardwareProduct(create) + display(conch.GetHardwareProductByID(*name)) + } + } +} + +func cmdListProducts(cfg Config) func(cmd *cli.Cmd) { + display := cfg.Renderer() + return func(cmd *cli.Cmd) { + cmd.Action = func() { + conch := cfg.ConchClient() + display(conch.GetHardwareProducts()) + } + } +} + +func hardwareCmd(cfg Config) func(*cli.Cmd) { + conch := cfg.ConchClient() + display := cfg.Renderer() + + return func(cmd *cli.Cmd) { + cmd.Before = func() { + conch = cfg.ConchClient() + } + + cmd.Command("products", "Work with hardware products", func(cmd *cli.Cmd) { + cmd.Command("create", "Create a hardware product", cmdCreateProduct(cfg)) + cmd.Command("get ls", "Get a list of all hardware products", cmdListProducts(cfg)) + }) + + cmd.Command("product", "Work with a hardware product", func(cmd *cli.Cmd) { + var hp types.HardwareProduct + idArg := cmd.StringArg("PRODUCT", "", "The SKU, UUID, alias, or name of the hardware product.") + cmd.Before = func() { + hp = conch.GetHardwareProductByID(*idArg) + if (hp == types.HardwareProduct{}) { + fmt.Println("Hardware Product not found for " + *idArg) + cli.Exit(1) + } + } + cmd.Command("get", "Show a hardware vendor's details", func(cmd *cli.Cmd) { + cmd.Action = func() { fmt.Println(hp) } + }) + cmd.Command("delete rm", "Remove a hardware product", func(cmd *cli.Cmd) { + cmd.Action = func() { + conch.DeleteHardwareProduct(hp.ID) + display(conch.GetHardwareProducts()) + } + }) + }) + + cmd.Command("vendors", "Work with hardware vendors", func(cmd *cli.Cmd) { + cmd.Command("get ls", "Get a list of all hardware vendors", func(cmd *cli.Cmd) { + display(conch.GetHardwareVendors()) + }) + cmd.Command("create", "Create a hardware vendor", func(cmd *cli.Cmd) { + name := cmd.StringArg("NAME", "", "The name of the hardware vendor.") + cmd.Action = func() { + conch.FindOrCreateHardwareVendor(*name) + } + }) + }) + + cmd.Command("vendor", "Work a specific hardware vendor", func(cmd *cli.Cmd) { + var hv types.HardwareVendor + idArg := cmd.StringArg("NAME", "", "The name, or UUID of the hardware vendor.") + + // grab the Vendor for the given ID + cmd.Before = func() { + hv = conch.GetHardwareVendorByID(*idArg) + if (hv == types.HardwareVendor{}) { + fmt.Println("Hardware Vendor not found for " + *idArg) + cli.Exit(1) + } + } + + cmd.Command("get", "Show a hardware vendor's details", func(cmd *cli.Cmd) { + cmd.Action = func() { fmt.Println(hv) } + }) + cmd.Command("delete rm", "Remove a hardware vendor", func(cmd *cli.Cmd) { + cmd.Action = func() { + conch.DeleteHardwareVendor(hv.ID) + } + }) + }) + } +} diff --git a/cli/organizations.go b/cli/organizations.go new file mode 100644 index 0000000..fe9a842 --- /dev/null +++ b/cli/organizations.go @@ -0,0 +1,143 @@ +package cli + +import ( + "fmt" + + cli "github.com/jawher/mow.cli" + "github.com/joyent/kosh/conch/types" +) + +func organizationsCmd(cfg Config) func(cmd *cli.Cmd) { + conch := cfg.ConchClient() + display := cfg.Renderer() + return func(cmd *cli.Cmd) { + cmd.Before = func() { + conch = cfg.ConchClient() + } + cmd.Command("get ls", "Get a list of all organizations", func(cmd *cli.Cmd) { + cmd.Action = func() { + display(conch.GetOrganizations()) + } + }) + + cmd.Command("create", "Create a new organization", func(cmd *cli.Cmd) { + nameArg := cmd.StringArg("NAME", "", "Name of the new organization") + + descOpt := cmd.StringOpt("description", "", "A description of the organization") + adminEmailArg := cmd.StringOpt( + "admin", + "", + "Email address for the (initial) admin user for the organization. This does *not* create the user.", + ) + + cmd.Spec = "NAME [OPTIONS]" + cmd.Action = func() { + conch.CreateOrganization(types.OrganizationCreate{ + Name: types.MojoStandardPlaceholder(*nameArg), + Description: types.NonEmptyString(*descOpt), + Admins: []types.Admin{ + types.Admin{Email: types.EmailAddress(*adminEmailArg)}, + }, + }) + } + }) + } +} + +func organizationCmd(cfg Config) func(cmd *cli.Cmd) { + conch := cfg.ConchClient() + display := cfg.Renderer() + + return func(cmd *cli.Cmd) { + var o types.Organization + organizationNameArg := cmd.StringArg("NAME", "", "Name or ID of the Organization") + + cmd.Spec = "NAME" + cmd.Before = func() { + conch = cfg.ConchClient() + o = conch.GetOrganizationByName(*organizationNameArg) + } + + cmd.Command("get", "Get information about a single organization by its name", func(cmd *cli.Cmd) { + cmd.Action = func() { + fmt.Println(o) + } + }) + + cmd.Command("delete rm", "Remove a specific organization", func(cmd *cli.Cmd) { + cmd.Action = func() { + conch.DeleteOrganization(o.ID) + } + }) + + cmd.Command("users", "Manage users in a specific organization", func(cmd *cli.Cmd) { + cmd.Command("get ls", "Get a list of users in an organization", func(cmd *cli.Cmd) { + cmd.Action = func() { + display(conch.GetOrganizationByID(o.ID)) + } + }) + + cmd.Command("add", "Add a user to an organization", func(cmd *cli.Cmd) { + userEmailArg := cmd.StringArg( + "EMAIL", + "", + "The email of the user to add to the organization. Does *not* create the user", + ) + + roleOpt := cmd.StringOpt( + "role", + "ro", + "The role for the user. One of: "+prettyBuildRoleList(), + ) + + sendEmailOpt := cmd.BoolOpt( + "send-email", + true, + "Send email to the target user, notifying them of the change", + ) + + cmd.Spec = "EMAIL [OPTIONS]" + cmd.Action = func() { + if !okBuildRole(*roleOpt) { + fatal(fmt.Errorf( + "'role' value must be one of: %s", + prettyBuildRoleList(), + )) + } + conch.AddOrganizationUser( + o.ID, + types.OrganizationAddUser{ + Email: types.EmailAddress(*userEmailArg), + Role: types.Role(*roleOpt), + }, + *sendEmailOpt, + ) + display(conch.GetOrganizationByID(o.ID)) + } + }) + + cmd.Command("remove rm", "remove a user from an organization", func(cmd *cli.Cmd) { + userEmailArg := cmd.StringArg( + "EMAIL", + "", + "The email or ID of the user to modify", + ) + + sendEmailOpt := cmd.BoolOpt( + "send-email", + true, + "Send email to the target user, notifying them of the change", + ) + cmd.Spec = "EMAIL [OPTIONS]" + cmd.Action = func() { + conch.DeleteOrganizationUser( + o.ID, + *userEmailArg, + *sendEmailOpt, + ) + display(conch.GetOrganizationByID(o.ID)) + } + }) + }) + } +} diff --git a/cli/racks.go b/cli/racks.go new file mode 100644 index 0000000..6cda0ce --- /dev/null +++ b/cli/racks.go @@ -0,0 +1,239 @@ +package cli + +import ( + "errors" + "fmt" + + cli "github.com/jawher/mow.cli" + "github.com/joyent/kosh/conch/types" +) + +func racksCmd(cfg Config) func(*cli.Cmd) { + conch := cfg.ConchClient() + // display := cfg.Renderer() + + return func(cmd *cli.Cmd) { + cmd.Before = func() { + requireSysAdmin(cfg)() + conch = cfg.ConchClient() + } + + cmd.Command("create", "Create a new rack", func(cmd *cli.Cmd) { + var ( + nameOpt = cmd.StringOpt("name", "", "Name of the rack") + roomAliasOpt = cmd.StringOpt("room", "", "Alias of the datacenter room") + roleNameOpt = cmd.StringOpt("role", "", "Name of the role") + buildNameOpt = cmd.StringOpt("build", "", "Build for the rack") + phaseOpt = cmd.StringOpt("phase", "", "Optional phase for the rack") + ) + + cmd.Spec = "--name --room --role [OPTIONS]" + cmd.Action = func() { + var ( + roomID types.UUID + roleID types.UUID + buildID types.UUID + ) + + // The user can be very silly and supply something like + // `--name ""` which will pass the cli lib's requirement + // check but is still crap + if *nameOpt == "" { + fatal(errors.New("--name is required")) + } + + if *roomAliasOpt == "" { + fatal(errors.New("--room is required")) + } else { + room := conch.GetRoomByAlias(*roomAliasOpt) + if (room == types.DatacenterRoomDetailed{}) { + fatal(errors.New("could not find room")) + } + roomID = room.ID + } + + if *roleNameOpt == "" { + fatal(errors.New("--role is required")) + } else { + role := conch.GetRackRoleByName(*roleNameOpt) + if (role == types.RackRole{}) { + fatal(errors.New("could not find rack role")) + } + roleID = role.ID + } + + if *buildNameOpt == "" { + fatal(errors.New("--build is required")) + } else { + build := conch.GetBuildByName(*buildNameOpt) + buildID = build.ID + } + conch.CreateRack(types.RackCreate{ + Name: types.MojoRelaxedPlaceholder(*nameOpt), + DatacenterRoomID: roomID, + RackRoleID: roleID, + Phase: types.DevicePhase(*phaseOpt), + BuildID: buildID, + }) + } + }) + } +} + +func rackCmd(cfg Config) func(*cli.Cmd) { + conch := cfg.ConchClient() + display := cfg.Renderer() + return func(cmd *cli.Cmd) { + var rack types.Rack + + idArg := cmd.StringArg( + "UUID", + "", + "The UUID of the rack. Short UUIDs are *not* accepted, unless you are a Conch sysadmin", + ) + + cmd.Spec = "UUID" + + cmd.Before = func() { + conch = cfg.ConchClient() + rack = conch.GetRackByName(*idArg) + if (rack == types.Rack{}) { + fatal(errors.New("could not find the rack")) + } + } + + cmd.Command("get", "Get a single rack", func(cmd *cli.Cmd) { + cmd.Action = func() { display(rack) } + }) + + cmd.Command("update", "Update information about a single rack", func(cmd *cli.Cmd) { + var ( + nameOpt = cmd.StringOpt("name", "", "Name of the rack") + roomAliasOpt = cmd.StringOpt("room", "", "Alias of the datacenter room") + roleNameOpt = cmd.StringOpt("role", "", "Name of the role") + phaseOpt = cmd.StringOpt("phase", "", "Phase for the rack") + + serialNumberOpt = cmd.StringOpt("serial-number", "", "Serial number of the rack") + clearSerialOpt = cmd.BoolOpt("clear-serial-number", false, "Delete the serial number. Overrides --serial-number") + + assetTagOpt = cmd.StringOpt("asset-tag", "", "Asset Tag of the rack") + clearAssetTagOpt = cmd.BoolOpt("clear-asset-tag", false, "Delete the asset tag. Overrides --asset-tag") + ) + + cmd.Action = func() { + var ( + roomID types.UUID + roleID types.UUID + serial *string + assetTag *string + ) + + if *roomAliasOpt != "" { + room := conch.GetRoomByAlias(*roomAliasOpt) + if (room == types.DatacenterRoomDetailed{}) { + fatal(errors.New("could not find room")) + } + roomID = room.ID + } + if *roleNameOpt != "" { + role := conch.GetRackRoleByName(*roomAliasOpt) + if (role == types.RackRole{}) { + fatal(errors.New("could not find rack role")) + } + roleID = role.ID + } + + empty := "" + + if *clearSerialOpt { + serial = nil + } else if *serialNumberOpt != "" { + serial = serialNumberOpt + } else { + serial = &empty + } + + if *clearAssetTagOpt { + assetTag = nil + } else if *assetTagOpt != "" { + assetTag = assetTagOpt + } else { + assetTag = &empty + } + + conch.UpdateRack(rack.ID, types.RackUpdate{ + Name: types.MojoRelaxedPlaceholder(*nameOpt), + DatacenterRoomID: roomID, + RackRoleID: roleID, + Phase: types.DevicePhase(*phaseOpt), + SerialNumber: serial, + AssetTag: assetTag, + }) + } + }) + + cmd.Command("delete rm", "Delete a rack", func(cmd *cli.Cmd) { + cmd.Before = requireSysAdmin(cfg) + cmd.Action = func() { + conch.DeleteRack(rack.ID) + fmt.Println("OK") + } + }) + + cmd.Command("layout", "The layout of the rack", func(cmd *cli.Cmd) { + cmd.Command("get", "Get the layout of a rack", func(cmd *cli.Cmd) { + cmd.Action = func() { + display(conch.GetRackLayout(rack.ID)) + } + }) + + cmd.Command("export", "Export the layout of the rack as JSON", func(cmd *cli.Cmd) { + cmd.Action = func() { + fmt.Println(renderJSON(conch.GetRackLayout(rack.ID))) + } + }) + + cmd.Command("import", "Import the layout of this rack (using the same format as 'export')", func(cmd *cli.Cmd) { + var ( + filePathArg = cmd.StringArg("FILE", "-", "Path to a JSON file that defines the layout. '-' indicates STDIN") + overwriteOpt = cmd.BoolOpt("overwrite", false, "If the rack has an existing layout, *overwrite* it. This is a destructive action") + ) + cmd.Action = func() { + layout := conch.GetRackLayout(rack.ID) + if len(layout) > 0 { + if !*overwriteOpt { + fatal(errors.New("rack already has a layout. use --overwrite to force")) + } + } + + input, err := getInputReader(*filePathArg) + if err != nil { + fatal(err) + } + + update := conch.ReadRackLayoutUpdate(input) + conch.UpdateRackLayout(rack.ID, update) + fmt.Println("OK") + } + }) + }) + + cmd.Command("assign", "Assign devices to rack slots, using the `--json` output from 'assignments'", func(cmd *cli.Cmd) { + filePathArg := cmd.StringArg("FILE", "-", "Path to a JSON file to use as the data source. '-' indicates STDIN") + cmd.Action = func() { + input, err := getInputReader(*filePathArg) + if err != nil { + fatal(err) + } + update := conch.ReadRackAssignmentUpdate(input) + conch.UpdateRackAssignments(rack.ID, update) + } + }) + + cmd.Command("assignments", "The devices assigned to the rack", func(cmd *cli.Cmd) { + cmd.Action = func() { + display(conch.GetRackAssignments(rack.ID)) + } + }) + } +} diff --git a/cli/relays.go b/cli/relays.go new file mode 100644 index 0000000..77cd41b --- /dev/null +++ b/cli/relays.go @@ -0,0 +1,76 @@ +package cli + +import ( + "errors" + "fmt" + + cli "github.com/jawher/mow.cli" + "github.com/joyent/kosh/conch/types" +) + +func relaysCmd(cfg Config) func(cmd *cli.Cmd) { + display := cfg.Renderer() + + return func(cmd *cli.Cmd) { + cmd.Command("get", "Get a list of relays", func(cmd *cli.Cmd) { + cmd.Action = func() { + conch := cfg.ConchClient() + display(conch.GetAllRelays()) + } + }) + } +} + +func relayCmd(cfg Config) func(cmd *cli.Cmd) { + conch := cfg.ConchClient() + display := cfg.Renderer() + + return func(cmd *cli.Cmd) { + var relay types.Relay + relayArg := cmd.StringArg( + "RELAY", + "", + "ID of the relay", + ) + + cmd.Spec = "RELAY" + + cmd.Before = func() { + conch = cfg.ConchClient() + } + cmd.Command("get", "Get data about a single relay", func(cmd *cli.Cmd) { + cmd.Action = func() { + relay = conch.GetRelayBySerial(*relayArg) + if (relay == types.Relay{}) { + fatal(errors.New("relay not found")) + } + fmt.Println(relay) + } + }) + + cmd.Command("register", "Register a relay with the API", func(cmd *cli.Cmd) { + var ( + versionOpt = cmd.StringOpt("version", "", "The version of the relay") + sshPortOpt = cmd.IntOpt("ssh_port port", 22, "The SSH port for the relay") + ipAddrOpt = cmd.StringOpt("ipaddr ip", "", "The IP address for the relay") + nameOpt = cmd.StringOpt("name", "", "The name of the relay") + ) + + cmd.Action = func() { + conch.RegisterRelay(*relayArg, types.RegisterRelay{ + Version: *versionOpt, + Ipaddr: *ipAddrOpt, + Name: types.NonEmptyString(*nameOpt), + SSHPort: types.NonNegativeInteger(*sshPortOpt), + }) + } + }) + + cmd.Command("delete rm", "Delete a relay", func(cmd *cli.Cmd) { + cmd.Action = func() { + conch.DeleteRelay(*relayArg) + display(conch.GetAllRelays()) + } + }) + } +} diff --git a/cli/roles.go b/cli/roles.go new file mode 100644 index 0000000..39f3fd5 --- /dev/null +++ b/cli/roles.go @@ -0,0 +1,96 @@ +package cli + +import ( + "errors" + + cli "github.com/jawher/mow.cli" + "github.com/joyent/kosh/conch/types" +) + +func rolesCmd(cfg Config) func(cmd *cli.Cmd) { + conch := cfg.ConchClient() + display := cfg.Renderer() + return func(cmd *cli.Cmd) { + cmd.Before = func() { + requireSysAdmin(cfg)() + conch = cfg.ConchClient() + } + cmd.Command("get", "Get a list of all rack roles", func(cmd *cli.Cmd) { + cmd.Action = func() { display(conch.GetAllRackRoles()) } + }) + + cmd.Command("create", "Create a new rack role", func(cmd *cli.Cmd) { + var ( + nameOpt = cmd.StringOpt("name", "", "The name of the role") + rackSizeOpt = cmd.IntOpt("rack-size", 0, "Size of the rack necessary for this role") + ) + + cmd.Spec = "--name --rack-size" + cmd.Action = func() { + if *nameOpt == "" { + fatal(errors.New("--name is required")) + } + + if *rackSizeOpt == 0 { + fatal(errors.New("--rack-size is required and cannot be 0")) + } + conch.CreateRackRole(types.RackRoleCreate{ + Name: types.MojoStandardPlaceholder(*nameOpt), + RackSize: types.PositiveInteger(*rackSizeOpt), + }) + } + }) + } +} + +func roleCmd(cfg Config) func(cmd *cli.Cmd) { + conch := cfg.ConchClient() + display := cfg.Renderer() + return func(cmd *cli.Cmd) { + var role types.RackRole + + nameArg := cmd.StringArg( + "NAME", + "", + "The name of the rack role", + ) + + cmd.Spec = "NAME" + + cmd.Before = func() { + requireSysAdmin(cfg)() + conch := cfg.ConchClient() + + role = conch.GetRackRoleByName(*nameArg) + if (role == types.RackRole{}) { + fatal(errors.New("couldn't find the role")) + } + } + + cmd.Command("get", "Get information about a single rack role", func(cmd *cli.Cmd) { + cmd.Action = func() { display(role) } + }) + + cmd.Command("update", "Update information about a single rack role", func(cmd *cli.Cmd) { + var ( + nameOpt = cmd.StringOpt("name", "", "The name of the role") + rackSizeOpt = cmd.IntOpt("rack-size", 0, "Size of the rack necessary for this role") + ) + + cmd.Action = func() { + conch.UpdateRackRole(role.ID, types.RackRoleUpdate{ + Name: types.MojoStandardPlaceholder(*nameOpt), + RackSize: types.PositiveInteger(*rackSizeOpt), + }) + display(conch.GetAllRackRoles()) + } + }) + + cmd.Command("delete", "Delete a single rack role", func(cmd *cli.Cmd) { + cmd.Action = func() { + conch.DeleteRackRole(role.ID) + display(conch.GetAllRackRoles()) + } + }) + } +} diff --git a/cli/rooms.go b/cli/rooms.go new file mode 100644 index 0000000..2cd677d --- /dev/null +++ b/cli/rooms.go @@ -0,0 +1,129 @@ +package cli + +import ( + "errors" + + cli "github.com/jawher/mow.cli" + "github.com/joyent/kosh/conch/types" +) + +func roomsCmd(cfg Config) func(cmd *cli.Cmd) { + conch := cfg.ConchClient() + display := cfg.Renderer() + return func(cmd *cli.Cmd) { + cmd.Before = func() { + requireSysAdmin(cfg)() + conch = cfg.ConchClient() + } + + cmd.Command("get", "Get a list of all rooms", func(cmd *cli.Cmd) { + cmd.Action = func() { + display(conch.GetAllRooms()) + } + }) + + cmd.Command("create", "Create a single room", func(cmd *cli.Cmd) { + var ( + aliasOpt = cmd.StringOpt("alias", "", "Alias") + azOpt = cmd.StringOpt("az", "", "AZ") + datacenterIDOpt = cmd.StringOpt("datacenter-id", "", "Datacenter UUID (first segment of UUID accepted)") + vendorNameOpt = cmd.StringOpt("vendor-name", "", "Vendor Name") + ) + + cmd.Spec = "--datacenter-id --alias --az [OPTIONS]" + cmd.Action = func() { + // The user can be very silly and supply something like + // '--alias ""' which will pass the cli lib's requirement + // check but is still crap + if *aliasOpt == "" { + fatal(errors.New("--alias is required")) + } + if *azOpt == "" { + fatal(errors.New("--az is required")) + } + if *datacenterIDOpt == "" { + fatal(errors.New("--datacenter-id is required")) + } + + datacenter := conch.GetDatacenterByName(*datacenterIDOpt) + if (datacenter == types.Datacenter{}) { + fatal(errors.New("could not find the datacenter")) + } + + conch.CreateRoom(types.DatacenterRoomCreate{ + DatacenterID: datacenter.ID, + Az: types.NonEmptyString(*azOpt), + Alias: types.MojoStandardPlaceholder(*aliasOpt), + VendorName: types.MojoRelaxedPlaceholder(*vendorNameOpt), + }) + } + }) + } +} + +func roomCmd(cfg Config) func(cmd *cli.Cmd) { + conch := cfg.ConchClient() + display := cfg.Renderer() + + return func(cmd *cli.Cmd) { + var room types.DatacenterRoomDetailed + + aliasArg := cmd.StringArg( + "ALIAS", + "", + "The unique alias of the datacenter room", + ) + + cmd.Spec = "ALIAS" + + cmd.Before = func() { + requireSysAdmin(cfg)() + + room = conch.GetRoomByAlias(*aliasArg) + if (room == types.DatacenterRoomDetailed{}) { + fatal(errors.New("could not find the room")) + } + } + + cmd.Command("get", "Information about a single room", func(cmd *cli.Cmd) { + cmd.Action = func() { display(room) } + }) + + cmd.Command("update", "Update information about a single room", func(cmd *cli.Cmd) { + var ( + aliasOpt = cmd.StringOpt("alias", "", "Alias") + azOpt = cmd.StringOpt("az", "", "AZ") + datacenterIDOpt = cmd.StringOpt("datacenter-id", "", "Datacenter UUID (first segment of UUID accepted)") + vendorNameOpt = cmd.StringOpt("vendor-name", "", "Vendor Name") + ) + + cmd.Action = func() { + dc := conch.GetDatacenterByName(*datacenterIDOpt) + if (dc == types.Datacenter{}) { + fatal(errors.New("could not find the datacenter")) + } + + conch.UpdateRoom(room.ID, types.DatacenterRoomUpdate{ + DatacenterID: dc.ID, + Az: types.NonEmptyString(*azOpt), + Alias: types.MojoStandardPlaceholder(*aliasOpt), + VendorName: types.MojoRelaxedPlaceholder(*vendorNameOpt), + }) + display(conch.GetRoomByID(room.ID)) + } + }) + + cmd.Command("delete", "Delete a single room", func(cmd *cli.Cmd) { + cmd.Action = func() { + conch.DeleteRoom(room.ID) + display(conch.GetAllRooms()) + } + }) + + cmd.Command("racks", "View the racks assigned to a single room", func(cmd *cli.Cmd) { + cmd.Action = func() { + display(conch.GetAllRoomRacks(room.ID)) + } + }) + } +} diff --git a/cli/schema.go b/cli/schema.go new file mode 100644 index 0000000..276288e --- /dev/null +++ b/cli/schema.go @@ -0,0 +1,36 @@ +package cli + +import ( + "fmt" + + cli "github.com/jawher/mow.cli" +) + +func schemaCmd(cfg Config) func(*cli.Cmd) { + conch := cfg.ConchClient() + display := cfg.Renderer() + + return func(cmd *cli.Cmd) { + cmd.Before = func() { + conch = cfg.ConchClient() + } + + cmd.Command("request", "View your Conch profile", func(cmd *cli.Cmd) { + name := cmd.StringArg("NAME", "", "The string name of a request schema") + cmd.Spec = "NAME" + + cmd.Action = func() { + display(conch.GetSchema(fmt.Sprintf("request/%s", *name))) + } + }) + + cmd.Command("response", "Get the settings for the current user", func(cmd *cli.Cmd) { + name := cmd.StringArg("NAME", "", "The string name of a response schema") + cmd.Spec = "NAME" + + cmd.Action = func() { + display(conch.GetSchema(fmt.Sprintf("response/%s", *name))) + } + }) + } +} diff --git a/cli/user.go b/cli/users.go similarity index 69% rename from cli/user.go rename to cli/users.go index 3d228a1..7c9f9cb 100644 --- a/cli/user.go +++ b/cli/users.go @@ -2,7 +2,6 @@ package cli import ( "fmt" - "log" cli "github.com/jawher/mow.cli" "github.com/joyent/kosh/conch/types" @@ -19,29 +18,23 @@ func userCmd(cfg Config) func(*cli.Cmd) { } func profileCmd(cfg Config) func(cmd *cli.Cmd) { - conch := cfg.ConchClient() + display := cfg.Renderer() return func(cmd *cli.Cmd) { cmd.Action = func() { - user := conch.GetCurrentUser() - if cfg.OutputJSON { - fmt.Println(user.JSON()) - } else { - fmt.Println(user.String()) - } + conch := cfg.ConchClient() + log := cfg.GetLogger() + log.Debug("display(conch.GetCurrentUser())") + display(conch.GetCurrentUser()) } } } func settings(cfg Config) func(cmd *cli.Cmd) { - conch := cfg.ConchClient() + display := cfg.Renderer() return func(cmd *cli.Cmd) { cmd.Action = func() { - settings := conch.GetCurrentUserSettings() - if cfg.OutputJSON { - fmt.Println(settings.JSON()) - } else { - fmt.Println(settings.String()) - } + conch := cfg.ConchClient() + display(conch.GetCurrentUserSettings()) } } } @@ -57,22 +50,18 @@ func userSetting(cfg Config) func(cmd *cli.Cmd) { } func userSettingGet(cfg Config, setting string) func(cmd *cli.Cmd) { - conch := cfg.ConchClient() + display := cfg.Renderer() return func(cmd *cli.Cmd) { cmd.Action = func() { - setting := conch.GetCurrentUserSettingByName(setting) - if cfg.OutputJSON { - fmt.Println(setting.JSON()) - } else { - fmt.Println(setting.String()) - } + conch := cfg.ConchClient() + display(conch.GetCurrentUserSettingByName(setting)) } } } func userSettingSet(cfg Config, setting string) func(cmd *cli.Cmd) { - conch := cfg.ConchClient() + display := cfg.Renderer() return func(cmd *cli.Cmd) { value := *cmd.StringArg("VALUE", "", "The new value of the setting") @@ -80,28 +69,25 @@ func userSettingSet(cfg Config, setting string) func(cmd *cli.Cmd) { cmd.Spec = "VALUE" cmd.Action = func() { + conch := cfg.ConchClient() if e := conch.SetCurrentUserSettingByName(setting, types.UserSetting(value)); e != nil { - log.Fatal(e) - } - setting := conch.GetCurrentUserSettingByName(setting) - if cfg.OutputJSON { - fmt.Println(setting.JSON()) - } else { - fmt.Println(setting.String()) + fatal(e) } + display(conch.GetCurrentUserSettingByName(setting)) } } } func userSettingDelete(cfg Config, setting string) func(cmd *cli.Cmd) { - conch := cfg.ConchClient() + // display := cfg.Renderer() return func(cmd *cli.Cmd) { cmd.Action = func() { + conch := cfg.ConchClient() if e := conch.DeleteCurrentUserSetting(setting); e != nil { - log.Fatal(e) + fatal(e) } - if !cfg.OutputJSON { + if !cfg.GetOutputJSON() { fmt.Println("OK") } } diff --git a/cli/validations.go b/cli/validations.go new file mode 100644 index 0000000..41c0f68 --- /dev/null +++ b/cli/validations.go @@ -0,0 +1,43 @@ +package cli + +import ( + "errors" + + cli "github.com/jawher/mow.cli" + "github.com/joyent/kosh/conch/types" +) + +func validationCmd(cfg Config) func(cmd *cli.Cmd) { + conch := cfg.ConchClient() + display := cfg.Renderer() + + return func(cmd *cli.Cmd) { + cmd.Before = func() { + conch = cfg.ConchClient() + } + cmd.Command("plans", "Work with validation plans", func(cmd *cli.Cmd) { + cmd.Command("get ls", "Get a list of all plans", func(cmd *cli.Cmd) { + cmd.Action = func() { + display(conch.GetAllValidationPlans()) + } + }) + }) + cmd.Command("plan", "Work with a specific validation plan", func(cmd *cli.Cmd) { + var plan types.ValidationPlan + + idArg := cmd.StringArg("UUID", "", "UUID of the Validation Plan, Short IDs accepted") + cmd.Spec = "UUID" + + cmd.Before = func() { + plan = conch.GetValidationPlanByName(*idArg) + if (plan == types.ValidationPlan{}) { + fatal(errors.New("could not find the validation plan")) + } + } + + cmd.Command("get", "Get information about a single build by its name", func(cmd *cli.Cmd) { + cmd.Action = func() { display(plan) } + }) + }) + } +} diff --git a/cmd_api.go b/cmd_api.go deleted file mode 100644 index 4fa1fde..0000000 --- a/cmd_api.go +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright Joyent, Inc. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package main - -import ( - "bytes" - "fmt" - "io/ioutil" - "os" - - "github.com/jawher/mow.cli" -) - -func init() { - App.Command( - "api", - "", - func(cmd *cli.Cmd) { - cmd.Command( - "get", - "Perform an HTTP GET against the provided URL", - func(cmd *cli.Cmd) { - var urlArg = cmd.StringArg("URL", "", "The API path to GET. Must *not* include the hostname or port") - cmd.Spec = "URL" - cmd.Action = func() { - fmt.Println(API.DoBadly(API.Sling().Get(*urlArg)).Body) - } - }, - ) - - cmd.Command( - "delete", - "Perform an HTTP DELETE against the provided URL", - func(cmd *cli.Cmd) { - var urlArg = cmd.StringArg("URL", "", "The API path to DELETE. Must *not* include the hostname or port") - cmd.Spec = "URL" - cmd.Action = func() { - fmt.Println(API.DoBadly(API.Sling().New().Delete(*urlArg)).Body) - } - }, - ) - - cmd.Command( - "post", - "Perform an HTTP POST against the provided URL", - func(cmd *cli.Cmd) { - var ( - urlArg = cmd.StringArg("URL", "", "The API path to POST. Must *not* include the hostname or port") - filePathArg = cmd.StringArg("FILE", "-", "Path to a JSON file to use as the request body. '-' indicates STDIN") - ) - cmd.Spec = "URL [FILE]" - - cmd.Action = func() { - var b []byte - var err error - if *filePathArg == "-" { - b, err = ioutil.ReadAll(os.Stdin) - } else { - b, err = ioutil.ReadFile(*filePathArg) - } - if err != nil { - panic(err) - } - - fmt.Println(API.DoBadly( - API.Sling().New().Post(*urlArg). - Set("Content-Type", "application/json"). - Body(bytes.NewReader(b)), - ).Body) - } - }, - ) - cmd.Command( - "version", - "Get the version of the API we are talking to", - func(cmd *cli.Cmd) { - cmd.Action = func() { - if API.JsonOnly { - fmt.Printf("{\"version\":\"%s\"}\n", API.Version()) - } else { - fmt.Println(API.Version()) - } - } - }, - ) - - }, - ) -} diff --git a/conch/builds.go b/conch/builds.go index 762f41f..25fc087 100644 --- a/conch/builds.go +++ b/conch/builds.go @@ -2,104 +2,116 @@ package conch import "github.com/joyent/kosh/conch/types" -// GET /builds -func (c *Client) GetBuilds() (builds types.Builds) { - c.Build("").Receive(builds) +// GetAllBuilds - GET /builds +func (c *Client) GetAllBuilds(options ...types.BuildQueryOptions) (builds types.Builds) { + c.Build("").Receive(&builds) return } -// POST /build +// CreateBuild - POST /build func (c *Client) CreateBuild(create types.BuildCreate) error { _, e := c.Build("").Post(create).Send() return e } -// GET /build/:build_id_or_name +// GetBuildByName - GET /build/:build_id_or_name func (c *Client) GetBuildByName(name string) (build types.Build) { - c.Build(name).Receive(build) + c.Build(name).Receive(&build) return } -// POST /build/:build_id_or_name +// UpdateBuild - POST /build/:build_id_or_name func (c *Client) UpdateBuild(name string, update types.BuildUpdate) error { _, e := c.Build(name).Post(update).Send() return e } -// GET /build/:build_id_or_name/user +// GetBuildUsers - GET /build/:build_id_or_name/user func (c *Client) GetBuildUsers(name string) (build types.BuildUsers) { - c.Build(name).User("").Receive(build) + c.Build(name).User("").Receive(&build) return } -// POST /build/:build_id_or_name/user -func (c *Client) AddBuildUser(name string, update types.BuildAddUser) error { +// AddBuildUser - POST /build/:build_id_or_name/user +func (c *Client) AddBuildUser(name string, update types.BuildAddUser, sendEmail bool) error { _, e := c.Build(name).User("").Post(update).Send() return e } -// DELETE /build/:build_id_or_name/user/#target_user_id_or_email -func (c *Client) DeleteBuildUser(name, user string) error { +// DeleteBuildUser - DELETE /build/:build_id_or_name/user/#target_user_id_or_email +func (c *Client) DeleteBuildUser(name, user string, sendEmail bool) error { _, e := c.Build(name).User(user).Delete().Send() return e } -// GET /build/:build_id_or_name/user -func (c *Client) GetBuildOrganizations(name string) (build types.BuildOrganizations) { - c.Build(name).Organization("").Receive(build) +// GetAllBuildOrganizations - GET /build/:build_id_or_name/user +func (c *Client) GetAllBuildOrganizations(name string) (build types.BuildOrganizations) { + c.Build(name).Organization("").Receive(&build) return } -// POST /build/:build_id_or_name/user -func (c *Client) AddBuildOrganization(name string, update types.BuildAddOrganization) error { +// AddBuildOrganization - POST /build/:build_id_or_name/user +func (c *Client) AddBuildOrganization(name string, update types.BuildAddOrganization, sendEmail bool) error { _, e := c.Build(name).Organization("").Post(update).Send() return e } -// DELETE /build/:build_id_or_name/user/#target_user_id_or_email -func (c *Client) DeleteBuildOrganization(build, org string) error { +// DeleteBuildOrganization - DELETE /build/:build_id_or_name/user/#target_user_id_or_email +func (c *Client) DeleteBuildOrganization(build, org string, sendEmail bool) error { _, e := c.Build(build).Organization(org).Delete().Send() return e } -// GET /build/:build_id_or_name/device -func (c *Client) GetBuildDevices(name string) (build types.Build) { - c.Build(name).Device("").Receive(build) +// GetAllBuildDevices - GET /build/:build_id_or_name/device +func (c *Client) GetAllBuildDevices(name string) (build types.Build) { + c.Build(name).Device("").Receive(&build) return } -// GET /build/:build_id_or_name/device/pxe +// GetBuildDevicesPXE - GET /build/:build_id_or_name/device/pxe func (c *Client) GetBuildDevicesPXE(name string) (build types.Build) { - c.Build(name).Device("").PXE().Receive(build) + c.Build(name).Device("").PXE().Receive(&build) return } -// POST /build/:build_id_or_name/device +// AddNewBuildDevice - POST /build/:build_id_or_name/device func (c *Client) AddNewBuildDevice(name string, device types.BuildCreateDevices) error { _, e := c.Build(name).Device("").Post(device).Send() return e } -// POST /build/:build_id_or_name/device/:device_id_or_serial_number -func (c *Client) AddBuildDeviceByID(name, device string) error { +// AddBuildDevbiceByName - POST /build/:build_id_or_name/device/:device_id_or_serial_number +func (c *Client) AddBuildDeviceByName(name, device string) error { _, e := c.Build(name).Device(device).Post("").Send() return e } +// AddBuildDevbiceByID - POST /build/:build_id_or_name/device/:device_id_or_serial_number +func (c *Client) AddBuildDeviceByID(buildID, deviceID types.UUID) error { + _, e := c.Build(buildID.String()).Device(deviceID.String()).Post("").Send() + return e +} + // DELETE /build/:build_id_or_name/device/:device_id_or_serial_number -func (c *Client) DeleteBuildDeviceByID(name, device string) error { - _, e := c.Build(name).Device(device).Delete().Send() +func (c *Client) DeleteBuildDeviceByID(buildID, deviceID types.UUID) error { + _, e := c.Build(buildID.String()).Device(deviceID.String()).Delete().Send() return e } // GET /build/:build_id_or_name/rack func (c *Client) GetBuildRacks(name string) (racks types.Racks) { - c.Build(name).Rack("").Receive(racks) + c.Build(name).Rack("").Receive(&racks) return } -// GET /build/:build_id_or_name/rack/:rack_id_or_name +// POST /build/:build_id_or_name/rack/:rack_id_or_name func (c *Client) AddBuildRackByID(name, rack string) error { _, e := c.Build(name).Rack(rack).Post("").Send() return e } + +// DELETE /build/:build_id_or_name/rack/:rack_id_or_name +func (c *Client) DeleteBuildRackByID(name, rack string) error { + _, e := c.Build(name).Rack(rack).Delete().Send() + return e +} diff --git a/conch/builds_test.go b/conch/builds_test.go index f2aff63..f4c0722 100644 --- a/conch/builds_test.go +++ b/conch/builds_test.go @@ -20,7 +20,7 @@ func TestBuilds(t *testing.T) { { URL: "/build/", Method: "GET", - Do: func(c *conch.Client) { _ = c.GetBuilds() }, + Do: func(c *conch.Client) { _ = c.GetAllBuilds() }, }, { URL: "/build/", @@ -51,34 +51,59 @@ func TestBuilds(t *testing.T) { { URL: "/build/foo/user/", Method: "POST", - Do: func(c *conch.Client) { _ = c.AddBuildUser("foo", types.BuildAddUser{}) }, + Do: func(c *conch.Client) { _ = c.AddBuildUser("foo", types.BuildAddUser{}, false) }, }, + { + URL: "/build/foo/user/", + Method: "POST", + Do: func(c *conch.Client) { _ = c.AddBuildUser("foo", types.BuildAddUser{}, true) }, + }, + { URL: "/build/foo/user/alice/", Method: "DELETE", - Do: func(c *conch.Client) { _ = c.DeleteBuildUser("foo", "alice") }, + Do: func(c *conch.Client) { _ = c.DeleteBuildUser("foo", "alice", false) }, }, + { + URL: "/build/foo/user/alice/", + Method: "DELETE", + Do: func(c *conch.Client) { _ = c.DeleteBuildUser("foo", "alice", true) }, + }, + { URL: "/build/foo/organization/", Method: "GET", - Do: func(c *conch.Client) { _ = c.GetBuildOrganizations("foo") }, + Do: func(c *conch.Client) { _ = c.GetAllBuildOrganizations("foo") }, }, { URL: "/build/foo/organization/", Method: "POST", Do: func(c *conch.Client) { - _ = c.AddBuildOrganization("foo", types.BuildAddOrganization{}) + _ = c.AddBuildOrganization("foo", types.BuildAddOrganization{}, false) + }, + }, + { + URL: "/build/foo/organization/", + Method: "POST", + Do: func(c *conch.Client) { + _ = c.AddBuildOrganization("foo", types.BuildAddOrganization{}, true) }, }, { URL: "/build/foo/organization/lemmings/", Method: "DELETE", - Do: func(c *conch.Client) { _ = c.DeleteBuildOrganization("foo", "lemmings") }, + Do: func(c *conch.Client) { _ = c.DeleteBuildOrganization("foo", "lemmings", false) }, }, + { + URL: "/build/foo/organization/lemmings/", + Method: "DELETE", + Do: func(c *conch.Client) { _ = c.DeleteBuildOrganization("foo", "lemmings", true) }, + }, + { URL: "/build/foo/device/", Method: "GET", - Do: func(c *conch.Client) { _ = c.GetBuildDevices("foo") }, + Do: func(c *conch.Client) { _ = c.GetAllBuildDevices("foo") }, }, { URL: "/build/foo/device/pxe/", @@ -93,24 +118,24 @@ func TestBuilds(t *testing.T) { }, }, { - URL: "/build/foo/device/DEADBEEF/", + URL: "/build/00000000-0000-0000-0000-000000000000/device/00000000-0000-0000-0000-000000000000/", Method: "POST", Do: func(c *conch.Client) { - _ = c.AddBuildDeviceByID("foo", "DEADBEEF") + _ = c.AddBuildDeviceByID(types.UUID{}, types.UUID{}) }, }, { - URL: "/build/foo/device/DEADBEEF/", + URL: "/build/00000000-0000-0000-0000-000000000000/device/00000000-0000-0000-0000-000000000000/", Method: "POST", Do: func(c *conch.Client) { - _ = c.AddBuildDeviceByID("foo", "DEADBEEF") + _ = c.AddBuildDeviceByID(types.UUID{}, types.UUID{}) }, }, { - URL: "/build/foo/device/DEADBEEF/", + URL: "/build/00000000-0000-0000-0000-000000000000/device/00000000-0000-0000-0000-000000000000/", Method: "DELETE", Do: func(c *conch.Client) { - _ = c.DeleteBuildDeviceByID("foo", "DEADBEEF") + _ = c.DeleteBuildDeviceByID(types.UUID{}, types.UUID{}) }, }, { @@ -141,7 +166,7 @@ func TestBuilds(t *testing.T) { w.WriteHeader(http.StatusOK) })) defer ts.Close() - test.Do(conch.New(ts.URL, "token", &logger{})) + test.Do(conch.New(newConfig(ts.URL))) assert.True(t, seen, "saw the correct post to conch") }) } diff --git a/conch/client.go b/conch/client.go index 6a8c72e..22642ad 100644 --- a/conch/client.go +++ b/conch/client.go @@ -7,21 +7,48 @@ import ( "strings" "github.com/dghubble/sling" + "github.com/joyent/kosh/logger" ) -type Logger interface { - Debug(...interface{}) -} - type Client struct { Sling *sling.Sling - Logger + logger.Logger } -func New(api, token string, l Logger) *Client { - s := sling.New().Base(api).Set("Authorization", "Bearer "+token) - return &Client{s, l} +type Config interface { + GetURL() string + GetToken() string + GetLogger() logger.Logger +} + +func New(c Config) *Client { + c.GetLogger().Debug(c) + s := sling.New().Base(c.GetURL()).Set("Authorization", "Bearer "+c.GetToken()) + return &Client{s, c.GetLogger()} +} + +/* +func buildUserAgent(GitRev string) map[string]string { + var isRoot bool + if current, err := user.Current(); err == nil { + if current.Uid == "0" { + isRoot = true + } + } + + agentBits := make(map[string]string) + agent := fmt.Sprintf( + "%s (%s; %s; r=%v)", + GitRev, + runtime.GOOS, + runtime.GOARCH, + isRoot, + ) + + agentBits["Kosh"] = agent + return agentBits } +*/ func (c *Client) New() *Client { s := c.Sling.New() @@ -98,6 +125,18 @@ func (c *Client) Rack(id ...string) *Client { return c.Path("rack", id...) } +func (c *Client) RackRole(id ...string) *Client { + return c.Path("rack_role", id...) +} + +func (c *Client) Room(id ...string) *Client { + return c.Path("room", id...) +} + +func (c *Client) Layout(id ...string) *Client { + return c.Path("layout", id...) +} + func (c *Client) Validation(id ...string) *Client { return c.Path("validation", id...) } @@ -114,6 +153,10 @@ func (c *Client) Interface(id ...string) *Client { return c.Path("interface", id...) } +func (c *Client) Schema(name ...string) *Client { + return c.Path("schema", name...) +} + func (c *Client) Field(ids ...string) *Client { c = c.New() for _, id := range ids { @@ -151,6 +194,10 @@ func (c *Client) SKU() *Client { return c.Path("sku") } +func (c *Client) Assignment() *Client { + return c.Path("assignment") +} + func (c *Client) Links() *Client { return c.Path("links") } @@ -199,39 +246,37 @@ func (c *Client) Put(data interface{}) *Client { return c } -func (c *Client) Delete() *Client { +func (c *Client) Delete(data ...interface{}) *Client { c = c.New() c.Sling.Delete("") + for _, d := range data { + c.Sling.BodyJSON(d) + } return c } // when you don't expect a response func (c *Client) Send() (*http.Response, error) { + c.Debug("Send") req, err := c.Sling.Request() - if err != nil { - return nil, err - } + c.Info("URL: ", req.URL) + c.Debug(req, err) - c.Debug("Sending request: ", req.URL) res, err := c.Sling.Do(req, nil, nil) - if err != nil { - c.Debug("Error: %+v", err) - return nil, err - } - return res, nil + c.Debug(res, err) + + return res, err } // when you do expect a response -func (c *Client) Receive(data interface{}) (interface{}, error) { +func (c *Client) Receive(data interface{}) (*http.Response, error) { + c.Debug("Receive") req, err := c.Sling.Request() - if err != nil { - return nil, err - } + c.Info("URL: ", req.URL) + c.Debug(req, err) - c.Debug("Sending request: ", req.URL) - _, err = c.Sling.ReceiveSuccess(data) - if err != nil { - return nil, err - } - return data, nil + res, err := c.Sling.ReceiveSuccess(data) + c.Debug(res, err) + + return res, err } diff --git a/conch/client_test.go b/conch/client_test.go index c0d508c..36a281b 100644 --- a/conch/client_test.go +++ b/conch/client_test.go @@ -9,12 +9,9 @@ import ( "github.com/dnaeon/go-vcr/cassette" "github.com/dnaeon/go-vcr/recorder" "github.com/joyent/kosh/conch" + "github.com/joyent/kosh/logger" ) -type logger struct{} - -func (*logger) Debug(...interface{}) {} - func NewTestClient(fixture string) *conch.Client { api := os.Getenv("KOSH_URL") token := os.Getenv("KOSH_TOKEN") @@ -39,5 +36,5 @@ func NewTestClient(fixture string) *conch.Client { s := sling.New().Base(api).Client(&http.Client{Transport: r}).Set("Authorization", "Bearer "+token) - return &conch.Client{Sling: s, Logger: &logger{}} + return &conch.Client{Sling: s, Logger: logger.New()} } diff --git a/conch/conch.go b/conch/conch.go index fb405fd..dfa2440 100644 --- a/conch/conch.go +++ b/conch/conch.go @@ -4,19 +4,19 @@ import "github.com/joyent/kosh/conch/types" // GET /ping func (c *Client) Ping() (ping types.Ping) { - c.Path("ping").Receive(ping) + c.Path("ping").Receive(&ping) return } // GET /version func (c *Client) Version() (version types.Version) { - c.Path("version").Receive(version) + c.Path("version").Receive(&version) return } // POST /login func (c *Client) Login(user, pass string) (token types.LoginToken) { - c.Path("login").Post(types.Login{Email: types.EmailAddress(user), Password: types.NonEmptyString(pass)}).Receive(token) + c.Path("login").Post(types.Login{Email: types.EmailAddress(user), Password: types.NonEmptyString(pass)}).Receive(&token) return } @@ -28,6 +28,10 @@ func (c *Client) Logout() error { // POST /refresh_token func (c *Client) RefreshToken() (token types.LoginToken) { - c.Path("refresh_token").Post("").Receive(token) + c.Path("refresh_token").Post("").Receive(&token) return } + +func (c *Client) IsSysAdmin() bool { + return c.GetCurrentUser().IsAdmin +} diff --git a/conch/conch_test.go b/conch/conch_test.go index 993e318..ae37d8a 100644 --- a/conch/conch_test.go +++ b/conch/conch_test.go @@ -7,9 +7,28 @@ import ( "testing" "github.com/joyent/kosh/conch" + "github.com/joyent/kosh/logger" "github.com/stretchr/testify/assert" ) +type config struct { + url string + token string + logger logger.Logger +} + +func (c config) GetURL() string { return c.url } +func (c config) GetToken() string { return c.token } +func (c config) GetLogger() logger.Logger { return c.logger } + +func newConfig(URL string) config { + return config{ + URL, + "token", + logger.New(), + } +} + func TestDefaultRoutes(t *testing.T) { tests := []struct { URL string @@ -55,7 +74,7 @@ func TestDefaultRoutes(t *testing.T) { w.WriteHeader(http.StatusOK) })) defer ts.Close() - test.Do(conch.New(ts.URL, "token", &logger{})) + test.Do(conch.New(newConfig(ts.URL))) assert.True(t, seen, "saw the correct post to conch") }) } diff --git a/conch/datacenters.go b/conch/datacenters.go index d63eec9..739cf75 100644 --- a/conch/datacenters.go +++ b/conch/datacenters.go @@ -3,8 +3,8 @@ package conch import "github.com/joyent/kosh/conch/types" // GET /dc -func (c *Client) GetDatacenters() (dc types.Datacenters) { - c.DC("").Receive(dc) +func (c *Client) GetAllDatacenters() (dc types.Datacenters) { + c.DC("").Receive(&dc) return } @@ -15,25 +15,30 @@ func (c *Client) CreateDatacenter(dc types.DatacenterCreate) error { } // GET /dc/:datacenter_id -func (c *Client) GetDatacenterByID(id string) (dc types.Datacenter) { - c.DC(id).Receive(dc) +func (c *Client) GetDatacenterByName(name string) (dc types.Datacenter) { + c.DC(name).Receive(&dc) + return +} + +func (c *Client) GetDatacenterByID(id types.UUID) (dc types.Datacenter) { + c.DC(id.String()).Receive(&dc) return } // POST /dc/:datacenter_id -func (c *Client) UpdateDatacenter(id string, update types.DatacenterUpdate) error { - _, e := c.DC(id).Post(update).Send() +func (c *Client) UpdateDatacenter(id types.UUID, update types.DatacenterUpdate) error { + _, e := c.DC(id.String()).Post(update).Send() return e } // DELETE /dc/:datacenter_id -func (c *Client) DeleteDatacenter(id string) error { - _, e := c.DC(id).Delete().Send() +func (c *Client) DeleteDatacenter(id types.UUID) error { + _, e := c.DC(id.String()).Delete().Send() return e } // GET /dc/:datacenter_id/rooms -func (c *Client) GetDatacenterRooms(id string) (rooms types.DatacenterRoomsDetailed) { - c.DC(id).Rooms().Receive(rooms) +func (c *Client) GetAllDatacenterRooms(id types.UUID) (rooms types.DatacenterRoomsDetailed) { + c.DC(id.String()).Rooms().Receive(&rooms) return } diff --git a/conch/datacenters_test.go b/conch/datacenters_test.go index 9ee02d7..065176c 100644 --- a/conch/datacenters_test.go +++ b/conch/datacenters_test.go @@ -20,7 +20,7 @@ func TestDatacenterRoutes(t *testing.T) { { URL: "/dc/", Method: "GET", - Do: func(c *conch.Client) { _ = c.GetDatacenters() }, + Do: func(c *conch.Client) { _ = c.GetAllDatacenters() }, }, { URL: "/dc/", @@ -30,22 +30,27 @@ func TestDatacenterRoutes(t *testing.T) { { URL: "/dc/foo/", Method: "GET", - Do: func(c *conch.Client) { _ = c.GetDatacenterByID("foo") }, + Do: func(c *conch.Client) { _ = c.GetDatacenterByName("foo") }, }, { - URL: "/dc/foo/", + URL: "/dc/00000000-0000-0000-0000-000000000000/", + Method: "GET", + Do: func(c *conch.Client) { _ = c.GetDatacenterByID(types.UUID{}) }, + }, + { + URL: "/dc/00000000-0000-0000-0000-000000000000/", Method: "POST", - Do: func(c *conch.Client) { _ = c.UpdateDatacenter("foo", types.DatacenterUpdate{}) }, + Do: func(c *conch.Client) { _ = c.UpdateDatacenter(types.UUID{}, types.DatacenterUpdate{}) }, }, { - URL: "/dc/foo/", + URL: "/dc/00000000-0000-0000-0000-000000000000/", Method: "DELETE", - Do: func(c *conch.Client) { _ = c.DeleteDatacenter("foo") }, + Do: func(c *conch.Client) { _ = c.DeleteDatacenter(types.UUID{}) }, }, { - URL: "/dc/foo/rooms/", + URL: "/dc/00000000-0000-0000-0000-000000000000/rooms/", Method: "GET", - Do: func(c *conch.Client) { _ = c.GetDatacenterRooms("foo") }, + Do: func(c *conch.Client) { _ = c.GetAllDatacenterRooms(types.UUID{}) }, }, } @@ -61,7 +66,7 @@ func TestDatacenterRoutes(t *testing.T) { w.WriteHeader(http.StatusOK) })) defer ts.Close() - test.Do(conch.New(ts.URL, "token", &logger{})) + test.Do(conch.New(newConfig(ts.URL))) assert.True(t, seen, "saw the correct post to conch") }) } diff --git a/conch/deviceReports.go b/conch/deviceReports.go index 1c7bdd1..2b2580d 100644 --- a/conch/deviceReports.go +++ b/conch/deviceReports.go @@ -19,12 +19,12 @@ func (c *Client) SendDeviceReport(r io.Reader) error { func (c *Client) ValidateDeviceReport(r io.Reader) (results types.ReportValidationResults) { report := &types.DeviceReport{} json.NewDecoder(r).Decode(report) - c.DeviceReport().Post(report).Receive(results) + c.DeviceReport().Post(report).Receive(&results) return } // GET /device_report/:device_report_id func (c *Client) GetDeviceReport(id string) (report types.DeviceReportRow) { - c.DeviceReport(id).Receive(report) + c.DeviceReport(id).Receive(&report) return } diff --git a/conch/deviceReports_test.go b/conch/deviceReports_test.go index 7a8090f..01ca387 100644 --- a/conch/deviceReports_test.go +++ b/conch/deviceReports_test.go @@ -53,7 +53,7 @@ func TestDeviceReportRoutes(t *testing.T) { w.WriteHeader(http.StatusOK) })) defer ts.Close() - test.Do(conch.New(ts.URL, "token", &logger{})) + test.Do(conch.New(newConfig(ts.URL))) assert.True(t, seen, "saw the correct post to conch") }) } diff --git a/conch/devices.go b/conch/devices.go index 9a660a4..68ad040 100644 --- a/conch/devices.go +++ b/conch/devices.go @@ -13,42 +13,48 @@ import ( // GET /device?:key=:value func (c *Client) FindDevicesBySetting(key, value string) (device types.Devices) { - c.Device("").WithParams(map[string]string{key: value}).Receive(device) + c.Device("").WithParams(map[string]string{key: value}).Receive(&device) return } func (c *Client) FindDevicesByTag(key, value string) (device types.Devices) { key = fmt.Sprintf("tag_%s", key) - c.Device("").WithParams(map[string]string{key: value}).Receive(device) + c.Device("").WithParams(map[string]string{key: value}).Receive(&device) return } func (c *Client) FindDevicesByField(key, value string) (device types.Device) { - c.Device("").WithParams(map[string]string{key: value}).Receive(device) + c.Device("").WithParams(map[string]string{key: value}).Receive(&device) return } // GET /device/:device_id_or_serial_number -func (c *Client) GetDeviceById(id string) (device types.DetailedDevice) { - c.Device(id).Receive(device) +func (c *Client) GetDeviceBySerial(serial string) (device types.DetailedDevice) { + c.Device(serial).Receive(&device) + return +} + +// GET /device/:device_id_or_serial_number +func (c *Client) GetDeviceByID(id types.UUID) (device types.DetailedDevice) { + c.Device(id.String()).Receive(&device) return } // GET /device/:device_id_or_serial_number/pxe func (c *Client) GetDevicePXE(id string) (pxe types.DevicePXE) { - c.Device(id).PXE().Receive(pxe) + c.Device(id).PXE().Receive(&pxe) return } // GET /device/:device_id_or_serial_number/phase func (c *Client) GetDevicePhase(id string) (phase types.DevicePhase) { - c.Device(id).Phase().Receive(phase) + c.Device(id).Phase().Receive(&phase) return } // GET /device/:device_id_or_serial_number/sku func (c *Client) GetDeviceSKU(id string) (sku types.DeviceSku) { - c.Device(id).SKU().Receive(sku) + c.Device(id).SKU().Receive(&sku) return } @@ -97,7 +103,7 @@ func (c *Client) SetDeviceBuild(id string, build types.DeviceBuild) error { // GET /device/:device_id_or_serial_number/location func (c *Client) GetDeviceLocation(id string) (location types.DeviceLocation) { - c.Device(id).Location().Receive(location) + c.Device(id).Location().Receive(&location) return } @@ -115,7 +121,7 @@ func (c *Client) DeleteDeviceLocation(id string) error { // GET /device/:device_id_or_serial_number/settings func (c *Client) GetDeviceSettings(id string) (settings types.DeviceSettings) { - c.Device(id).Settings("").Receive(settings) + c.Device(id).Settings("").Receive(&settings) return } @@ -127,18 +133,18 @@ func (c *Client) SetDeviceSettings(id string, settings types.DeviceSettings) err // GET /device/:device_id_or_serial_number/settings/:key func (c *Client) GetDeviceSettingByName(id, name string) (setting types.DeviceSetting) { - c.Device(id).Settings(name).Receive(setting) + c.Device(id).Settings(name).Receive(&setting) return } func (c *Client) GetDeviceTags(id string) (tags types.DeviceSettings) { - c.Device(id).Settings("").Receive(tags) + c.Device(id).Settings("").Receive(&tags) return } func (c *Client) GetDeviceTagByName(id, name string) (tag types.DeviceSetting) { name = fmt.Sprintf("tag_%s", name) - c.Device(id).Settings(name).Receive(tag) + c.Device(id).Settings(name).Receive(&tag) return } @@ -168,30 +174,30 @@ func (c *Client) DeleteDeviceSetting(id, name string) error { // POST /device/:device_id_or_serial_number/validation/:validation_id func (c *Client) RunValidationForDevice(device, validation string, report types.DeviceReport) (results types.ValidationResults) { - c.Device(device).Validation(validation).Post(report).Receive(results) + c.Device(device).Validation(validation).Post(report).Receive(&results) return } // GET /device/:device_id_or_serial_number/validation_state?status=&status=... func (c *Client) GetDeviceValidationStates(id string, states ...string) (validations types.ValidationStateWithResults) { - c.Device(id).ValidationStates(states...).Receive(validations) + c.Device(id).ValidationStates(states...).Receive(&validations) return } // GET /device/:device_id_or_serial_number/interface func (c *Client) GetDeviceInterfaces(id string) (nics types.DeviceNics) { - c.Device(id).Interface("").Receive(nics) + c.Device(id).Interface("").Receive(&nics) return } // GET /device/:device_id_or_serial_number/interface func (c *Client) GetDeviceInterfaceByName(id, name string) (nic types.DeviceNic) { - c.Device(id).Interface(name).Receive(nic) + c.Device(id).Interface(name).Receive(&nic) return } // GET /device/:device_id_or_serial_number/interface func (c *Client) GetDeviceInterfaceField(id, name, field string) (nicField types.DeviceNicField) { - c.Device(id).Interface(name).Field(field).Receive(nicField) + c.Device(id).Interface(name).Field(field).Receive(&nicField) return } diff --git a/conch/devices_test.go b/conch/devices_test.go index dacdf2f..0fca41b 100644 --- a/conch/devices_test.go +++ b/conch/devices_test.go @@ -12,7 +12,6 @@ import ( ) func TestDevices(t *testing.T) { - tests := []struct { URL string Method string @@ -36,7 +35,7 @@ func TestDevices(t *testing.T) { { URL: "/device/DEADBEEF/", Method: "GET", - Do: func(c *conch.Client) { _ = c.GetDeviceById("DEADBEEF") }, + Do: func(c *conch.Client) { _ = c.GetDeviceBySerial("DEADBEEF") }, }, { URL: "/device/DEADBEEF/pxe/", @@ -186,7 +185,7 @@ func TestDevices(t *testing.T) { w.WriteHeader(http.StatusOK) })) defer ts.Close() - test.Do(conch.New(ts.URL, "token", &logger{})) + test.Do(conch.New(newConfig(ts.URL))) assert.True(t, seen, "saw the correct post to conch") }) } diff --git a/conch/hardwareProducts.go b/conch/hardwareProducts.go index f5a909d..73201c1 100644 --- a/conch/hardwareProducts.go +++ b/conch/hardwareProducts.go @@ -4,7 +4,7 @@ import "github.com/joyent/kosh/conch/types" // GET /hardware_product func (c *Client) GetHardwareProducts() (products types.HardwareProducts) { - c.HardwareProduct().Receive(products) + c.HardwareProduct().Receive(&products) return } @@ -16,7 +16,7 @@ func (c *Client) CreateHardwareProduct(product types.HardwareProductCreate) erro // GET /hardware_product/:hardware_product_id_or_other func (c *Client) GetHardwareProductByID(id string) (products types.HardwareProduct) { - c.HardwareProduct(id).Receive(products) + c.HardwareProduct(id).Receive(&products) return } @@ -27,8 +27,8 @@ func (c *Client) UpdateHardwareProduct(id string, update types.HardwareProductUp } // DELETE /hardware_product/:hardware_product_id_or_other -func (c *Client) DeleteHardwareProduct(id string) error { - _, e := c.HardwareProduct(id).Delete().Send() +func (c *Client) DeleteHardwareProduct(id types.UUID) error { + _, e := c.HardwareProduct(id.String()).Delete().Send() return e } diff --git a/conch/hardwareProducts_test.go b/conch/hardwareProducts_test.go index 6eab8d6..68b13df 100644 --- a/conch/hardwareProducts_test.go +++ b/conch/hardwareProducts_test.go @@ -42,9 +42,9 @@ func TestHardwareProductRoutes(t *testing.T) { }, }, { - URL: "/hardware_product/foo/", + URL: "/hardware_product/00000000-0000-0000-0000-000000000000/", Method: "DELETE", - Do: func(c *conch.Client) { _ = c.DeleteHardwareProduct("foo") }, + Do: func(c *conch.Client) { _ = c.DeleteHardwareProduct(types.UUID{}) }, }, { URL: "/hardware_product/foo/specification?path=%5B%2Fbar%5D%2F", @@ -74,7 +74,7 @@ func TestHardwareProductRoutes(t *testing.T) { w.WriteHeader(http.StatusOK) })) defer ts.Close() - test.Do(conch.New(ts.URL, "token", &logger{})) + test.Do(conch.New(newConfig(ts.URL))) assert.True(t, seen, "saw the correct post to conch") }) } diff --git a/conch/hardwareVendors.go b/conch/hardwareVendors.go index 9bd7a7c..0301e73 100644 --- a/conch/hardwareVendors.go +++ b/conch/hardwareVendors.go @@ -4,22 +4,31 @@ import "github.com/joyent/kosh/conch/types" // GET /hardware_vendor func (c *Client) GetHardwareVendors() (vendors types.HardwareVendors) { - c.HardwareVendor().Receive(vendors) + c.HardwareVendor().Receive(&vendors) return } // GET /hardware_vendor/:hardware_vendor_id_or_name func (c *Client) GetHardwareVendorByID(id string) (vendor types.HardwareVendor) { - c.HardwareVendor(id).Receive(vendor) + c.HardwareVendor(id).Receive(&vendor) return } // DELETE /hardware_vendor/:hardware_vendor_id_or_name -func (c *Client) DeleteHardwareVendor(id string) error { - _, e := c.HardwareVendor(id).Delete().Send() +func (c *Client) DeleteHardwareVendor(id types.UUID) error { + _, e := c.HardwareVendor(id.String()).Delete().Send() return e } +func (c *Client) FindOrCreateHardwareVendor(id string) (vendor types.HardwareVendor) { + vendor = c.GetHardwareVendorByID(id) + if (vendor == types.HardwareVendor{}) { + c.CreateHardwareVendor(id) + vendor = c.GetHardwareVendorByID(id) + } + return +} + // POST /hardware_vendor/:hardware_vendor_id_or_name func (c *Client) CreateHardwareVendor(id string) error { _, e := c.HardwareVendor(id).Post("").Send() diff --git a/conch/hardwareVendors_test.go b/conch/hardwareVendors_test.go index f043591..c8d1f48 100644 --- a/conch/hardwareVendors_test.go +++ b/conch/hardwareVendors_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/joyent/kosh/conch" + "github.com/joyent/kosh/conch/types" "github.com/stretchr/testify/assert" ) @@ -27,9 +28,9 @@ func TestHardwareVendorRoutes(t *testing.T) { Do: func(c *conch.Client) { _ = c.GetHardwareVendorByID("foo") }, }, { - URL: "/hardware_vendor/foo/", + URL: "/hardware_vendor/00000000-0000-0000-0000-000000000000/", Method: "DELETE", - Do: func(c *conch.Client) { _ = c.DeleteHardwareVendor("foo") }, + Do: func(c *conch.Client) { _ = c.DeleteHardwareVendor(types.UUID{}) }, }, { URL: "/hardware_vendor/foo/", @@ -50,7 +51,7 @@ func TestHardwareVendorRoutes(t *testing.T) { w.WriteHeader(http.StatusOK) })) defer ts.Close() - test.Do(conch.New(ts.URL, "token", &logger{})) + test.Do(conch.New(newConfig(ts.URL))) assert.True(t, seen, "saw the correct post to conch") }) } diff --git a/conch/organizations.go b/conch/organizations.go index 3122b1a..297b1b9 100644 --- a/conch/organizations.go +++ b/conch/organizations.go @@ -4,7 +4,7 @@ import "github.com/joyent/kosh/conch/types" // GET /organization func (c *Client) GetOrganizations() (orgs types.Organizations) { - c.Organization().Receive(orgs) + c.Organization().Receive(&orgs) return } @@ -15,8 +15,14 @@ func (c *Client) CreateOrganization(org types.OrganizationCreate) error { } // GET /organization/:organization_id_or_name -func (c *Client) GetOrganizationByID(id string) (org types.Organization) { - c.Organization(id).Receive(org) +func (c *Client) GetOrganizationByName(name string) (org types.Organization) { + c.Organization(name).Receive(&org) + return +} + +// GET /organization/:organization_id_or_name +func (c *Client) GetOrganizationByID(id types.UUID) (org types.Organization) { + c.Organization(id.String()).Receive(&org) return } @@ -27,19 +33,19 @@ func (c *Client) UpdateOrganization(id string, update types.OrganizationUpdate) } // DELETE /organization/:organization_id_or_name -func (c *Client) DeleteOrganization(id string) error { - _, e := c.Organization(id).Delete().Send() +func (c *Client) DeleteOrganization(id types.UUID) error { + _, e := c.Organization(id.String()).Delete().Send() return e } // POST /organization/:organization_id_or_name/user?send_mail=<1|0> -func (c *Client) AddOrganizationUser(id string, user types.OrganizationAddUser) error { - _, e := c.Organization(id).Post(user).Send() +func (c *Client) AddOrganizationUser(id types.UUID, user types.OrganizationAddUser, sendEmail bool) error { + _, e := c.Organization(id.String()).User().Post(user).Send() return e } // DELETE /organization/:organization_id_or_name/user/#target_user_id_or_email?send_mail=<1|0> -func (c *Client) DeleteOrganizationUser(id, user string) error { - _, e := c.Organization(id).User(user).Delete().Send() +func (c *Client) DeleteOrganizationUser(id types.UUID, user string, sendEmail bool) error { + _, e := c.Organization(id.String()).User(user).Delete().Send() return e } diff --git a/conch/organizations_test.go b/conch/organizations_test.go index 8c76df0..238cf4c 100644 --- a/conch/organizations_test.go +++ b/conch/organizations_test.go @@ -30,7 +30,7 @@ func TestOrganizationRoutes(t *testing.T) { { URL: "/organization/foo/", Method: "GET", - Do: func(c *conch.Client) { _ = c.GetOrganizationByID("foo") }, + Do: func(c *conch.Client) { _ = c.GetOrganizationByName("foo") }, }, { URL: "/organization/foo/", @@ -40,21 +40,33 @@ func TestOrganizationRoutes(t *testing.T) { }, }, { - URL: "/organization/foo/", + URL: "/organization/00000000-0000-0000-0000-000000000000/", Method: "DELETE", - Do: func(c *conch.Client) { _ = c.DeleteOrganization("foo") }, + Do: func(c *conch.Client) { _ = c.DeleteOrganization(types.UUID{}) }, }, { - URL: "/organization/foo/", + URL: "/organization/00000000-0000-0000-0000-000000000000/user/", + Method: "POST", + Do: func(c *conch.Client) { + _ = c.AddOrganizationUser(types.UUID{}, types.OrganizationAddUser{}, false) + }, + }, + { + URL: "/organization/00000000-0000-0000-0000-000000000000/user/", Method: "POST", Do: func(c *conch.Client) { - _ = c.AddOrganizationUser("foo", types.OrganizationAddUser{}) + _ = c.AddOrganizationUser(types.UUID{}, types.OrganizationAddUser{}, true) }, }, { - URL: "/organization/foo/user/bar/", + URL: "/organization/00000000-0000-0000-0000-000000000000/user/bar/", + Method: "DELETE", + Do: func(c *conch.Client) { _ = c.DeleteOrganizationUser(types.UUID{}, "bar", false) }, + }, + { + URL: "/organization/00000000-0000-0000-0000-000000000000/user/bar/", Method: "DELETE", - Do: func(c *conch.Client) { _ = c.DeleteOrganizationUser("foo", "bar") }, + Do: func(c *conch.Client) { _ = c.DeleteOrganizationUser(types.UUID{}, "bar", true) }, }, } @@ -70,7 +82,7 @@ func TestOrganizationRoutes(t *testing.T) { w.WriteHeader(http.StatusOK) })) defer ts.Close() - test.Do(conch.New(ts.URL, "token", &logger{})) + test.Do(conch.New(newConfig(ts.URL))) assert.True(t, seen, "saw the correct post to conch") }) } diff --git a/conch/rackRoles.go b/conch/rackRoles.go new file mode 100644 index 0000000..67665f6 --- /dev/null +++ b/conch/rackRoles.go @@ -0,0 +1,39 @@ +package conch + +import "github.com/joyent/kosh/conch/types" + +// GET /rack_role +func (c *Client) GetAllRackRoles() (roles types.RackRoles) { + c.RackRole().Receive(roles) + return +} + +// POST /rack_role +func (c *Client) CreateRackRole(role types.RackRoleCreate) error { + _, e := c.RackRole().Post(role).Send() + return e +} + +// GET /rack_role/:rack_role_id_or_name +func (c *Client) GetRackRoleByName(name string) (role types.RackRole) { + c.RackRole(name).Receive(role) + return +} + +// GET /rack_role/:rack_role_id_or_name +func (c *Client) GetRackRoleByID(id types.UUID) (role types.RackRole) { + c.RackRole(id.String()).Receive(role) + return +} + +// POST /rack_role/:rack_role_id_or_name +func (c *Client) UpdateRackRole(id types.UUID, update types.RackRoleUpdate) error { + _, e := c.RackRole(id.String()).Post(update).Send() + return e +} + +// DELETE /rack_role/:rack_role_id_or_name +func (c *Client) DeleteRackRole(id types.UUID) error { + _, e := c.RackRole(id.String()).Delete().Send() + return e +} diff --git a/conch/rackRoles_test.go b/conch/rackRoles_test.go new file mode 100644 index 0000000..5124663 --- /dev/null +++ b/conch/rackRoles_test.go @@ -0,0 +1,68 @@ +package conch_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/joyent/kosh/conch" + "github.com/joyent/kosh/conch/types" + "github.com/stretchr/testify/assert" +) + +func TestRackRole(t *testing.T) { + tests := []struct { + URL string + Method string + Do func(c *conch.Client) + }{ + { + URL: "/rack_role/", + Method: "GET", + Do: func(c *conch.Client) { _ = c.GetAllRackRoles() }, + }, + { + URL: "/rack_role/", + Method: "POST", + Do: func(c *conch.Client) { _ = c.CreateRackRole(types.RackRoleCreate{}) }, + }, + { + URL: "/rack_role/foo/", + Method: "GET", + Do: func(c *conch.Client) { _ = c.GetRackRoleByName("foo") }, + }, + { + URL: "/rack_role/00000000-0000-0000-0000-000000000000/", + Method: "GET", + Do: func(c *conch.Client) { _ = c.GetRackRoleByID(types.UUID{}) }, + }, + { + URL: "/rack_role/00000000-0000-0000-0000-000000000000/", + Method: "POST", + Do: func(c *conch.Client) { _ = c.UpdateRackRole(types.UUID{}, types.RackRoleUpdate{}) }, + }, + { + URL: "/rack_role/00000000-0000-0000-0000-000000000000/", + Method: "DELETE", + Do: func(c *conch.Client) { _ = c.DeleteRackRole(types.UUID{}) }, + }, + } + + for _, test := range tests { + name := fmt.Sprintf("%s %s", test.Method, test.URL) + t.Run(name, func(t *testing.T) { + seen := false + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.URL, r.URL.String()) + assert.Equal(t, test.Method, r.Method) + seen = true + + w.WriteHeader(http.StatusOK) + })) + defer ts.Close() + test.Do(conch.New(newConfig(ts.URL))) + assert.True(t, seen, "saw the correct post to conch") + }) + } +} diff --git a/conch/racks.go b/conch/racks.go new file mode 100644 index 0000000..1526631 --- /dev/null +++ b/conch/racks.go @@ -0,0 +1,118 @@ +package conch + +import ( + "encoding/json" + "io" + + "github.com/joyent/kosh/conch/types" +) + +// POST /rack +func (c *Client) CreateRack(rack types.RackCreate) error { + _, e := c.Rack().Post(rack).Send() + return e +} + +// GET /rack/:rack_id_or_name +func (c *Client) GetRackByName(name string) (rack types.Rack) { + c.Rack(name).Receive(&rack) + return +} + +func (c *Client) GetRackByID(id types.UUID) (rack types.Rack) { + c.Rack(id.String()).Receive(&rack) + return +} + +// POST /rack/:rack_id_or_name +func (c *Client) UpdateRack(id types.UUID, rack types.RackUpdate) error { + _, e := c.Rack(id.String()).Post(rack).Send() + return e +} + +// DELETE /rack/:rack_id_or_name +func (c *Client) DeleteRack(id types.UUID) error { + _, e := c.Rack(id.String()).Delete().Send() + return e +} + +// GET /rack/:rack_id_or_name/layout +func (c *Client) GetRackLayout(id types.UUID) (rack types.RackLayouts) { + c.Rack(id.String()).Layout().Receive(&rack) + return +} + +// POST /rack/:rack_id_or_name/layout +func (c *Client) UpdateRackLayout(id types.UUID, layout types.RackLayoutUpdate) error { + _, e := c.Rack(id.String()).Layout().Post(layout).Send() + return e +} + +func (c *Client) ReadRackLayoutUpdate(r io.Reader) (update types.RackLayoutUpdate) { + json.NewDecoder(r).Decode(&update) + return +} + +// GET /rack/:rack_id_or_name/assignment +func (c *Client) GetRackAssignments(id types.UUID) (rack types.RackAssignments) { + c.Rack(id.String()).Assignment().Receive(&rack) + return +} + +// POST /rack/:rack_id_or_name/assignment +func (c *Client) UpdateRackAssignments(id types.UUID, rack types.RackAssignmentUpdates) error { + _, e := c.Rack(id.String()).Assignment().Post(rack).Send() + return e +} + +func (c *Client) ReadRackAssignmentUpdate(r io.Reader) (update types.RackAssignmentUpdates) { + json.NewDecoder(r).Decode(&update) + return +} + +// DELETE /rack/:rack_id_or_name/assignment +func (c *Client) DeleteRackAssignments(id types.UUID, deletes types.RackAssignmentDeletes) error { + _, e := c.Rack(id.String()).Assignment().Delete(deletes).Send() + return e +} + +// POST /rack/:rack_id_or_name/phase?rack_only=<0|1> +func (c *Client) UpdateRackPhase(id types.UUID, phase types.RackPhase, rackOnly bool) error { + _, e := c.Rack(id.String()).Phase().Post(phase).Send() + return e +} + +// POST /rack/:rack_id_or_name/links +func (c *Client) UpdateRackLinks(id types.UUID, links types.RackLinks) error { + _, e := c.Rack(id.String()).Links().Post(links).Send() + return e +} + +// DELETE /rack/:rack_id_or_name/links +func (c *Client) DeleteRackLinks(id types.UUID, phase types.RackLinks) error { + _, e := c.Rack(id.String()).Links().Delete().Send() + return e +} + +// GET /rack/:rack_id_or_name/layout/:layout_id_or_rack_unit_start +func (c *Client) GetSingleRackLayoutByRU(id types.UUID, ru string) (rack types.RackLayout) { + c.Rack(id.String()).Layout(ru).Receive(&rack) + return +} + +func (c *Client) GetSingleRackLayoutByID(rackID, layoutID types.UUID) (rack types.RackLayout) { + c.Rack(rackID.String()).Layout(layoutID.String()).Receive(&rack) + return +} + +// POST /rack/:rack_id_or_name/layout/:layout_id_or_rack_unit_start +func (c *Client) UpdateSingleRackLayout(rackID, layoutID types.UUID, update types.RackLayoutUpdate) error { + _, e := c.Rack(rackID.String()).Layout(layoutID.String()).Post(update).Send() + return e +} + +// DELETE /rack/:rack_id_or_name/layout/:layout_id_or_rack_unit_start +func (c *Client) DeleteSingleRackLayout(rackID types.UUID, layoutID types.UUID) error { + _, e := c.Rack(rackID.String()).Layout(layoutID.String()).Delete().Send() + return e +} diff --git a/conch/racks_test.go b/conch/racks_test.go new file mode 100644 index 0000000..f63c652 --- /dev/null +++ b/conch/racks_test.go @@ -0,0 +1,142 @@ +package conch_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/joyent/kosh/conch" + "github.com/joyent/kosh/conch/types" + "github.com/stretchr/testify/assert" +) + +func TestRacks(t *testing.T) { + tests := []struct { + URL string + Method string + Do func(c *conch.Client) + }{ + { + URL: "/rack/", + Method: "POST", + Do: func(c *conch.Client) { _ = c.CreateRack(types.RackCreate{}) }, + }, + { + URL: "/rack/foo/", + Method: "GET", + Do: func(c *conch.Client) { _ = c.GetRackByName("foo") }, + }, + { + URL: "/rack/00000000-0000-0000-0000-000000000000/", + Method: "GET", + Do: func(c *conch.Client) { _ = c.GetRackByID(types.UUID{}) }, + }, + { + URL: "/rack/00000000-0000-0000-0000-000000000000/", + Method: "POST", + Do: func(c *conch.Client) { _ = c.UpdateRack(types.UUID{}, types.RackUpdate{}) }, + }, + { + URL: "/rack/00000000-0000-0000-0000-000000000000/", + Method: "DELETE", + Do: func(c *conch.Client) { _ = c.DeleteRack(types.UUID{}) }, + }, + { + URL: "/rack/00000000-0000-0000-0000-000000000000/layout/", + Method: "GET", + Do: func(c *conch.Client) { _ = c.GetRackLayout(types.UUID{}) }, + }, + { + URL: "/rack/00000000-0000-0000-0000-000000000000/layout/", + Method: "POST", + Do: func(c *conch.Client) { _ = c.UpdateRackLayout(types.UUID{}, types.RackLayoutUpdate{}) }, + }, + { + URL: "/rack/00000000-0000-0000-0000-000000000000/assignment/", + Method: "GET", + Do: func(c *conch.Client) { _ = c.GetRackAssignments(types.UUID{}) }, + }, + { + URL: "/rack/00000000-0000-0000-0000-000000000000/assignment/", + Method: "POST", + Do: func(c *conch.Client) { _ = c.UpdateRackAssignments(types.UUID{}, types.RackAssignmentUpdates{}) }, + }, + { + URL: "/rack/00000000-0000-0000-0000-000000000000/assignment/", + Method: "DELETE", + Do: func(c *conch.Client) { _ = c.DeleteRackAssignments(types.UUID{}, types.RackAssignmentDeletes{}) }, + }, + { + URL: "/rack/00000000-0000-0000-0000-000000000000/phase/", + Method: "POST", + Do: func(c *conch.Client) { + _ = c.UpdateRackPhase(types.UUID{}, types.RackPhase{}, false) + }, + }, + { + URL: "/rack/00000000-0000-0000-0000-000000000000/phase/", + Method: "POST", + Do: func(c *conch.Client) { + _ = c.UpdateRackPhase(types.UUID{}, types.RackPhase{}, true) + }, + }, + { + URL: "/rack/00000000-0000-0000-0000-000000000000/links/", + Method: "POST", + Do: func(c *conch.Client) { + _ = c.UpdateRackLinks(types.UUID{}, types.RackLinks{}) + }, + }, + { + URL: "/rack/00000000-0000-0000-0000-000000000000/links/", + Method: "DELETE", + Do: func(c *conch.Client) { + _ = c.DeleteRackLinks(types.UUID{}, types.RackLinks{}) + }, + }, + { + URL: "/rack/00000000-0000-0000-0000-000000000000/layout/01/", + Method: "GET", + Do: func(c *conch.Client) { _ = c.GetSingleRackLayoutByRU(types.UUID{}, "01") }, + }, + { + URL: "/rack/00000000-0000-0000-0000-000000000000/layout/00000000-0000-0000-0000-000000000000/", + Method: "GET", + Do: func(c *conch.Client) { _ = c.GetSingleRackLayoutByID(types.UUID{}, types.UUID{}) }, + }, + { + URL: "/rack/00000000-0000-0000-0000-000000000000/layout/00000000-0000-0000-0000-000000000000/", + Method: "POST", + Do: func(c *conch.Client) { + _ = c.UpdateSingleRackLayout( + types.UUID{}, + types.UUID{}, + types.RackLayoutUpdate{}, + ) + }, + }, + { + URL: "/rack/00000000-0000-0000-0000-000000000000/layout/00000000-0000-0000-0000-000000000000/", + Method: "DELETE", + Do: func(c *conch.Client) { _ = c.DeleteSingleRackLayout(types.UUID{}, types.UUID{}) }, + }, + } + + for _, test := range tests { + name := fmt.Sprintf("%s %s", test.Method, test.URL) + t.Run(name, func(t *testing.T) { + seen := false + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.URL, r.URL.String()) + assert.Equal(t, test.Method, r.Method) + seen = true + + w.WriteHeader(http.StatusOK) + })) + defer ts.Close() + test.Do(conch.New(newConfig(ts.URL))) + assert.True(t, seen, "saw the correct post to conch") + }) + } +} diff --git a/conch/relays.go b/conch/relays.go index c19fc86..99a64f0 100644 --- a/conch/relays.go +++ b/conch/relays.go @@ -9,14 +9,19 @@ func (c *Client) RegisterRelay(id string, relay types.RegisterRelay) error { } // GET /relay -func (c *Client) GetRelays() (relays types.Relays) { - c.Relay().Receive(relays) +func (c *Client) GetAllRelays() (relays types.Relays) { + c.Relay().Receive(&relays) return } // GET /relay/:relay_id_or_serial_number -func (c *Client) GetRelayByID(id string) (relay types.Relay) { - c.Relay(id).Receive(relay) +func (c *Client) GetRelayBySerial(serial string) (relay types.Relay) { + c.Relay(serial).Receive(&relay) + return +} + +func (c *Client) GetRelayByID(id types.UUID) (relay types.Relay) { + c.Relay(id.String()).Receive(&relay) return } diff --git a/conch/relays_test.go b/conch/relays_test.go index 33ccf6c..3de61bf 100644 --- a/conch/relays_test.go +++ b/conch/relays_test.go @@ -25,13 +25,19 @@ func TestRelayRoutes(t *testing.T) { { URL: "/relay/", Method: "GET", - Do: func(c *conch.Client) { _ = c.GetRelays() }, + Do: func(c *conch.Client) { _ = c.GetAllRelays() }, }, { URL: "/relay/foo/", Method: "GET", - Do: func(c *conch.Client) { _ = c.GetRelayByID("foo") }, + Do: func(c *conch.Client) { _ = c.GetRelayBySerial("foo") }, }, + { + URL: "/relay/00000000-0000-0000-0000-000000000000/", + Method: "GET", + Do: func(c *conch.Client) { _ = c.GetRelayByID(types.UUID{}) }, + }, + { URL: "/relay/foo/", Method: "DELETE", @@ -51,7 +57,7 @@ func TestRelayRoutes(t *testing.T) { w.WriteHeader(http.StatusOK) })) defer ts.Close() - test.Do(conch.New(ts.URL, "token", &logger{})) + test.Do(conch.New(newConfig(ts.URL))) assert.True(t, seen, "saw the correct post to conch") }) } diff --git a/conch/rooms.go b/conch/rooms.go new file mode 100644 index 0000000..07037e8 --- /dev/null +++ b/conch/rooms.go @@ -0,0 +1,138 @@ +package conch + +import "github.com/joyent/kosh/conch/types" + +// GET /room +func (c *Client) GetAllRooms() (rooms types.DatacenterRoomsDetailed) { + c.Room().Receive(rooms) + return +} + +// POST /room +func (c *Client) CreateRoom(room types.DatacenterRoomCreate) error { + _, e := c.Room().Post(room).Send() + return e +} + +// GET /room/:datacenter_room_id_or_alias +func (c *Client) GetRoomByAlias(alias string) (room types.DatacenterRoomDetailed) { + c.Room(alias).Receive(room) + return +} + +func (c *Client) GetRoomByID(id types.UUID) (room types.DatacenterRoomDetailed) { + c.Room(id.String()).Receive(room) + return +} + +// POST /room/:datacenter_room_id_or_alias +func (c *Client) UpdateRoom(id types.UUID, room types.DatacenterRoomUpdate) error { + _, e := c.Room(id.String()).Post(room).Send() + return e +} + +// DELETE /room/:datacenter_room_id_or_alias +func (c *Client) DeleteRoom(id types.UUID) error { + _, e := c.Room(id.String()).Delete().Send() + return e +} + +// GET /room/:datacenter_room_id_or_alias/rack +func (c *Client) GetAllRoomRacks(id types.UUID) (racks types.Racks) { + c.Room(id.String()).Rack().Receive(racks) + return +} + +// GET /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name +func (c *Client) GetRoomRackByName(id types.UUID, name string) (rack types.Rack) { + c.Room(id.String()).Rack(name).Receive(rack) + return +} + +func (c *Client) GetRoomRackByID(roomID, rackID types.UUID) (rack types.Rack) { + c.Room(roomID.String()).Rack(rackID.String()).Receive(rack) + return +} + +// POST /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name +func (c *Client) UpdateRoomRack(roomID, rackID types.UUID, rack types.RackUpdate) error { + _, e := c.Room(roomID.String()).Rack(rackID.String()).Post(rack).Send() + return e +} + +// DELETE /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name +func (c *Client) DeleteRoomRack(roomID, rackID types.UUID) error { + _, e := c.Room(roomID.String()).Rack(rackID.String()).Delete().Send() + return e +} + +// GET /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/layout +func (c *Client) GetRoomRackLayout(roomID, rackID types.UUID) (rack types.RackLayouts) { + c.Room(roomID.String()).Rack(roomID.String()).Layout().Receive(rack) + return +} + +// POST /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/layout +func (c *Client) UpdateRoomRackLayout(roomID, rackID types.UUID, layout types.RackLayoutUpdate) error { + _, e := c.Room(roomID.String()).Rack(rackID.String()).Layout().Post(layout).Send() + return e +} + +// GET /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/assignment +func (c *Client) GetRoomRackAssignments(roomID, rackID types.UUID) (rack types.RackAssignments) { + c.Room(roomID.String()).Rack(rackID.String()).Assignment().Receive(rack) + return +} + +// POST /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/assignment +func (c *Client) UpdateRoomRackAssignments(roomID, rackID types.UUID, rack types.RackAssignmentUpdates) error { + _, e := c.Room(roomID.String()).Rack(rackID.String()).Assignment().Post(rack).Send() + return e +} + +// DELETE /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/assignment +func (c *Client) DeleteRoomRackAssignments(roomID, rackID types.UUID, deletes types.RackAssignmentDeletes) error { + _, e := c.Room(roomID.String()).Rack(rackID.String()).Assignment().Delete(deletes).Send() + return e +} + +// POST /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/phase?rack_only=<0|1> +func (c *Client) UpdateRoomRackPhase(roomID, rackID types.UUID, phase types.RackPhase, rackOnly bool) error { + _, e := c.Room(roomID.String()).Rack(rackID.String()).Phase().Post(phase).Send() + return e +} + +// POST /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/links +func (c *Client) UpdateRoomRackLinks(roomID, rackID types.UUID, links types.RackLinks) error { + _, e := c.Room(roomID.String()).Rack(rackID.String()).Links().Post(links).Send() + return e +} + +// DELETE /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/links +func (c *Client) DeleteRoomRackLinks(roomID, rackID types.UUID, phase types.RackLinks) error { + _, e := c.Room(roomID.String()).Rack(rackID.String()).Links().Delete().Send() + return e +} + +// GET /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/layout/:layout_id_or_rack_unit_start +func (c *Client) GetSingleRoomRackLayoutByRU(roomID, rackID types.UUID, ru string) (rack types.RackLayout) { + c.Room(roomID.String()).Rack(rackID.String()).Layout(ru).Receive(rack) + return +} + +func (c *Client) GetSingleRoomRackLayoutByID(roomID, rackID, layoutID types.UUID) (rack types.RackLayout) { + c.Room(rackID.String()).Rack(rackID.String()).Layout(layoutID.String()).Receive(rack) + return +} + +// POST /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/layout/:layout_id_or_rack_unit_start +func (c *Client) UpdateSingleRoomRackLayout(roomID, rackID, layoutID types.UUID, update types.RackLayoutUpdate) error { + _, e := c.Room(roomID.String()).Rack(rackID.String()).Layout(layoutID.String()).Post(update).Send() + return e +} + +// DELETE /room/:datacenter_room_id_or_alias/rack/:rack_id_or_name/layout/:layout_id_or_rack_unit_start +func (c *Client) DeleteSingleRoomRackLayout(roomID, rackID, layoutID types.UUID) error { + _, e := c.Room(roomID.String()).Rack(rackID.String()).Layout(layoutID.String()).Delete().Send() + return e +} diff --git a/conch/rooms_test.go b/conch/rooms_test.go new file mode 100644 index 0000000..b9a9658 --- /dev/null +++ b/conch/rooms_test.go @@ -0,0 +1,211 @@ +package conch_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/joyent/kosh/conch" + "github.com/joyent/kosh/conch/types" + "github.com/stretchr/testify/assert" +) + +func TestRooms(t *testing.T) { + tests := []struct { + URL string + Method string + Do func(c *conch.Client) + }{ + { + URL: "/room/", + Method: "GET", + Do: func(c *conch.Client) { _ = c.GetAllRooms() }, + }, + { + URL: "/room/", + Method: "POST", + Do: func(c *conch.Client) { _ = c.CreateRoom(types.DatacenterRoomCreate{}) }, + }, + { + URL: "/room/foo/", + Method: "GET", + Do: func(c *conch.Client) { _ = c.GetRoomByAlias("foo") }, + }, + { + URL: "/room/00000000-0000-0000-0000-000000000000/", + Method: "GET", + Do: func(c *conch.Client) { _ = c.GetRoomByID(types.UUID{}) }, + }, + { + URL: "/room/00000000-0000-0000-0000-000000000000/", + Method: "POST", + Do: func(c *conch.Client) { + _ = c.UpdateRoom(types.UUID{}, types.DatacenterRoomUpdate{}) + }, + }, + { + URL: "/room/00000000-0000-0000-0000-000000000000/", + Method: "DELETE", + Do: func(c *conch.Client) { _ = c.DeleteRoom(types.UUID{}) }, + }, + { + URL: "/room/00000000-0000-0000-0000-000000000000/rack/", + Method: "GET", + Do: func(c *conch.Client) { _ = c.GetAllRoomRacks(types.UUID{}) }, + }, + { + URL: "/room/00000000-0000-0000-0000-000000000000/rack/foo/", + Method: "GET", + Do: func(c *conch.Client) { _ = c.GetRoomRackByName(types.UUID{}, "foo") }, + }, + { + URL: "/room/00000000-0000-0000-0000-000000000000/rack/00000000-0000-0000-0000-000000000000/", + Method: "GET", + Do: func(c *conch.Client) { _ = c.GetRoomRackByID(types.UUID{}, types.UUID{}) }, + }, + { + URL: "/room/00000000-0000-0000-0000-000000000000/rack/00000000-0000-0000-0000-000000000000/", + Method: "POST", + Do: func(c *conch.Client) { + _ = c.UpdateRoomRack(types.UUID{}, types.UUID{}, types.RackUpdate{}) + }, + }, + { + URL: "/room/00000000-0000-0000-0000-000000000000/rack/00000000-0000-0000-0000-000000000000/", + Method: "DELETE", + Do: func(c *conch.Client) { _ = c.DeleteRoomRack(types.UUID{}, types.UUID{}) }, + }, + { + URL: "/room/00000000-0000-0000-0000-000000000000/rack/00000000-0000-0000-0000-000000000000/layout/", + Method: "GET", + Do: func(c *conch.Client) { _ = c.GetRoomRackLayout(types.UUID{}, types.UUID{}) }, + }, + { + URL: "/room/00000000-0000-0000-0000-000000000000/rack/00000000-0000-0000-0000-000000000000/layout/", + Method: "POST", + Do: func(c *conch.Client) { + _ = c.UpdateRoomRackLayout(types.UUID{}, types.UUID{}, types.RackLayoutUpdate{}) + }, + }, + { + URL: "/room/00000000-0000-0000-0000-000000000000/rack/00000000-0000-0000-0000-000000000000/assignment/", + Method: "GET", + Do: func(c *conch.Client) { + _ = c.GetRoomRackAssignments(types.UUID{}, types.UUID{}) + }, + }, + { + URL: "/room/00000000-0000-0000-0000-000000000000/rack/00000000-0000-0000-0000-000000000000/assignment/", + Method: "POST", + Do: func(c *conch.Client) { + _ = c.UpdateRoomRackAssignments( + types.UUID{}, + types.UUID{}, + types.RackAssignmentUpdates{}, + ) + }, + }, + { + URL: "/room/00000000-0000-0000-0000-000000000000/rack/00000000-0000-0000-0000-000000000000/assignment/", + Method: "DELETE", + Do: func(c *conch.Client) { + _ = c.DeleteRoomRackAssignments( + types.UUID{}, + types.UUID{}, + types.RackAssignmentDeletes{}, + ) + }, + }, + { + URL: "/room/00000000-0000-0000-0000-000000000000/rack/00000000-0000-0000-0000-000000000000/phase/", + Method: "POST", + Do: func(c *conch.Client) { + _ = c.UpdateRoomRackPhase(types.UUID{}, types.UUID{}, types.RackPhase{}, false) + }, + }, + { + URL: "/room/00000000-0000-0000-0000-000000000000/rack/00000000-0000-0000-0000-000000000000/phase/", + Method: "POST", + Do: func(c *conch.Client) { + _ = c.UpdateRoomRackPhase(types.UUID{}, types.UUID{}, types.RackPhase{}, true) + }, + }, + { + URL: "/room/00000000-0000-0000-0000-000000000000/rack/00000000-0000-0000-0000-000000000000/links/", + Method: "POST", + Do: func(c *conch.Client) { + _ = c.UpdateRoomRackLinks(types.UUID{}, types.UUID{}, types.RackLinks{}) + }, + }, + { + URL: "/room/00000000-0000-0000-0000-000000000000/rack/00000000-0000-0000-0000-000000000000/links/", + Method: "DELETE", + Do: func(c *conch.Client) { + _ = c.DeleteRoomRackLinks(types.UUID{}, types.UUID{}, types.RackLinks{}) + }, + }, + { + URL: "/room/00000000-0000-0000-0000-000000000000/rack/00000000-0000-0000-0000-000000000000/layout/01/", + Method: "GET", + Do: func(c *conch.Client) { + _ = c.GetSingleRoomRackLayoutByRU( + types.UUID{}, + types.UUID{}, + "01", + ) + }, + }, + { + URL: "/room/00000000-0000-0000-0000-000000000000/rack/00000000-0000-0000-0000-000000000000/layout/00000000-0000-0000-0000-000000000000/", + Method: "GET", + Do: func(c *conch.Client) { + _ = c.GetSingleRoomRackLayoutByID( + types.UUID{}, + types.UUID{}, + types.UUID{}, + ) + }, + }, + { + URL: "/room/00000000-0000-0000-0000-000000000000/rack/00000000-0000-0000-0000-000000000000/layout/00000000-0000-0000-0000-000000000000/", + Method: "POST", + Do: func(c *conch.Client) { + _ = c.UpdateSingleRoomRackLayout( + types.UUID{}, + types.UUID{}, + types.UUID{}, + types.RackLayoutUpdate{}, + ) + }, + }, + { + URL: "/room/00000000-0000-0000-0000-000000000000/rack/00000000-0000-0000-0000-000000000000/layout/00000000-0000-0000-0000-000000000000/", + Method: "DELETE", + Do: func(c *conch.Client) { + _ = c.DeleteSingleRoomRackLayout( + types.UUID{}, + types.UUID{}, + types.UUID{}, + ) + }, + }, + } + + for _, test := range tests { + name := fmt.Sprintf("%s %s", test.Method, test.URL) + t.Run(name, func(t *testing.T) { + seen := false + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, test.URL, r.URL.String()) + assert.Equal(t, test.Method, r.Method) + seen = true + + w.WriteHeader(http.StatusOK) + })) + defer ts.Close() + test.Do(conch.New(newConfig(ts.URL))) + assert.True(t, seen, "saw the correct post to conch") + }) + } +} diff --git a/conch/schema.go b/conch/schema.go new file mode 100644 index 0000000..debeeb7 --- /dev/null +++ b/conch/schema.go @@ -0,0 +1,10 @@ +package conch + +import ( + "github.com/qri-io/jsonschema" +) + +func (c *Client) GetSchema(name string) (schema jsonschema.Schema) { + c.Schema(name).Receive(&schema) + return +} diff --git a/conch/types/DetailedDevice.go b/conch/types/DetailedDevice.go deleted file mode 100644 index 93c76bf..0000000 --- a/conch/types/DetailedDevice.go +++ /dev/null @@ -1,17 +0,0 @@ -package types - -func (d DetailedDevice) JSON() ([]byte, error) { - return AsJSON(d) -} - -func (d DetailedDevice) String() string { - return "[ comming soon ]" -} - -func (d DeviceReport) JSON() ([]byte, error) { - return AsJSON(d) -} - -func (d DeviceReport) String() string { - return "[ comming soon ]" -} diff --git a/conch/types/DeviceSettings.go b/conch/types/DeviceSettings.go deleted file mode 100644 index 06bc5f3..0000000 --- a/conch/types/DeviceSettings.go +++ /dev/null @@ -1,13 +0,0 @@ -package types - -func (d DeviceSetting) JSON() ([]byte, error) { return AsJSON(d) } -func (d DeviceSetting) String() string { panic("TODO") } - -func (d DeviceSettings) JSON() ([]byte, error) { return AsJSON(d) } -func (d DeviceSettings) String() string { panic("TODO") } - -func (d DeviceNic) JSON() ([]byte, error) { return AsJSON(d) } -func (d DeviceNic) String() string { panic("TODO") } - -func (d DeviceLocation) JSON() ([]byte, error) { return AsJSON(d) } -func (d DeviceLocation) String() string { panic("TODO") } diff --git a/conch/types/Devices.go b/conch/types/Devices.go deleted file mode 100644 index 10b5e1b..0000000 --- a/conch/types/Devices.go +++ /dev/null @@ -1,69 +0,0 @@ -package types - -import ( - "sort" - "strings" - - "github.com/joyent/kosh/tables" - "github.com/joyent/kosh/template" -) - -func (d Device) JSON() ([]byte, error) { - return AsJSON(d) -} - -func (d Device) String() string { - return "[ comming soon ]" -} - -func (d Devices) JSON() ([]byte, error) { - return AsJSON(d) -} - -func (d Devices) Len() int { - return len(d) -} - -func (d Devices) Swap(i, j int) { - d[i], d[j] = d[j], d[i] -} - -func (d Devices) Less(i, j int) bool { - return d[i].SerialNumber < d[j].SerialNumber -} - -func (d Devices) String() string { - sort.Sort(d) - - tableString := &strings.Builder{} - table := tables.NewTable(tableString) - tables.TableToMarkdown(table) - - table.SetHeader([]string{ - "Serial", - "Hostname", - "Asset Tag", - "Hardware", - "Phase", - "Updated", - "Validated", - }) - - for _, device := range d { - table.Append([]string{ - string(device.SerialNumber), - device.Hostname, - device.AssetTag, - device.HardwareProductID.String(), - string(device.Phase), - template.TimeStr(device.Updated), - device.Validated, - }) - } - - table.Render() - return tableString.String() -} - -func (d DevicePhase) JSON() ([]byte, error) { return AsJSON(d) } -func (d DevicePhase) String() string { return string(d) } diff --git a/conch/types/Links.go b/conch/types/Links.go deleted file mode 100644 index 009dbc3..0000000 --- a/conch/types/Links.go +++ /dev/null @@ -1,9 +0,0 @@ -package types - -func NewDeviceLinks(uris ...string) DeviceLinks { - links := []Link{} - for _, u := range uris { - links = append(links, Link(u)) - } - return DeviceLinks{links} -} diff --git a/conch/types/UserDetailed.go b/conch/types/UserDetailed.go deleted file mode 100644 index 2fbcd3e..0000000 --- a/conch/types/UserDetailed.go +++ /dev/null @@ -1,44 +0,0 @@ -package types - -import ( - "strings" - - "github.com/joyent/kosh/template" -) - -func (u UserDetailed) JSON() ([]byte, error) { - return AsJSON(u) -} - -func (u UserDetailed) String() (string, error) { - - t, err := template.NewTemplate().Parse(detailedUserTemplate) - if err != nil { - return "", err - } - - buf := &strings.Builder{} - - if err := t.Execute(buf, u); err != nil { - return "", err - } - - return buf.String(), nil -} - -const detailedUserTemplate = ` -ID: {{ .ID }} -Name: {{ .Name }} -Email: {{ .Email }} -System Admin: {{ if $.IsAdmin }}Yes{{ else }}No{{ end }} - -Created: {{ TimeStr .Created }} -Last Login: {{ if $.LastLogin.IsZero }}Never/Unknown{{ else }}{{ TimeStr .LastLogin }}{{ end }} - - -Workspaces: -{{ .Workspaces }} - -Organizations: -{{ .Organizations }} -` diff --git a/conch/types/UserDetailed_test.go b/conch/types/UserDetailed_test.go deleted file mode 100644 index d932b46..0000000 --- a/conch/types/UserDetailed_test.go +++ /dev/null @@ -1 +0,0 @@ -package types_test diff --git a/conch/types/UserSettings.go b/conch/types/UserSettings.go deleted file mode 100644 index 46fd6b2..0000000 --- a/conch/types/UserSettings.go +++ /dev/null @@ -1,52 +0,0 @@ -package types - -import ( - "fmt" - "io" - "sort" - "strings" - - "github.com/joyent/kosh/tables" -) - -func (u UserSettings) JSON() ([]byte, error) { - return AsJSON(u) -} - -func (u UserSettings) String() string { - builder := &strings.Builder{} - u.RenderTable(builder) - return builder.String() -} - -func (u UserSettings) RenderTable(writer io.Writer) { - keys := make([]string, 0) - for setting := range u { - keys = append(keys, setting) - } - sort.Strings(keys) - - table := tables.NewTable(writer) - tables.TableToMarkdown(table) - table.SetHeader([]string{ - "Key", - "Value", - }) - - for _, key := range keys { - table.Append([]string{ - key, - fmt.Sprintf("%v", u[key]), - }) - } - - table.Render() -} - -func (u UserSetting) JSON() ([]byte, error) { - return AsJSON(u) -} - -func (u UserSetting) String() string { - return string(u) -} diff --git a/conch/types/ValidationStateWithResults.go b/conch/types/ValidationStateWithResults.go deleted file mode 100644 index 83b5060..0000000 --- a/conch/types/ValidationStateWithResults.go +++ /dev/null @@ -1,9 +0,0 @@ -package types - -func (v ValidationStateWithResults) JSON() ([]byte, error) { - return AsJSON(v) -} - -func (v ValidationStateWithResults) String() string { - return "" -} diff --git a/conch/types/types.go b/conch/types/common.go similarity index 54% rename from conch/types/types.go rename to conch/types/common.go index cb108cb..a13123f 100644 --- a/conch/types/types.go +++ b/conch/types/common.go @@ -1,23 +1,15 @@ package types import ( - "encoding/json" - "github.com/gofrs/uuid" ) type UUID struct{ uuid.UUID } -// see https://metacpan.org/pod/Mojolicious::Guides::Routing#Relaxed-placeholders type MojoRelaxedPlaceholder string -// see https://metacpan.org/pod/Mojolicious::Guides::Routing#Standard-placeholders type MojoStandardPlaceholder string -func AsJSON(d interface{}) ([]byte, error) { - return json.MarshalIndent(d, "", " ") -} - type Link string type DeviceSerialNumber string @@ -29,3 +21,11 @@ type Macaddr string type NonEmptyString string type EmailAddress string + +func NewDeviceLinks(uris ...string) DeviceLinks { + links := []Link{} + for _, u := range uris { + links = append(links, Link(u)) + } + return DeviceLinks{links} +} diff --git a/conch/types/Requests.go b/conch/types/requests.go similarity index 68% rename from conch/types/Requests.go rename to conch/types/requests.go index a551a1e..3e37231 100644 --- a/conch/types/Requests.go +++ b/conch/types/requests.go @@ -2,6 +2,12 @@ package types import "time" +type BuildQueryOptions struct { + WithDeviceHealth bool + WithDevicePhases bool + WithRackPhases bool +} + // generated by "schematyper -o types/RequestType_BuildAddOrganization.go --package=types --ptr-for-omit BuildAddOrganization.json" -- DO NOT EDIT type BuildAddOrganization struct { @@ -44,10 +50,6 @@ type BuildCreate struct { Started time.Time `json:"started,omitempty"` } -// generated by "schematyper -o types/RequestType_BuildOrganizations.go --package=types --ptr-for-omit BuildOrganizations.json" -- DO NOT EDIT - -type BuildOrganizations interface{} - // generated by "schematyper -o types/RequestType_BuildUpdate.go --package=types --ptr-for-omit BuildUpdate.json" -- DO NOT EDIT type BuildUpdate struct { @@ -158,8 +160,42 @@ type Temp struct { // generated by "schematyper -o types/RequestType_HardwareProductCreate.go --package=types --ptr-for-omit HardwareProductCreate.json" -- DO NOT EDIT type HardwareProductCreate struct { - HardwareProductUpdate - HardwareProductCreateEmbedded1 + Alias MojoStandardPlaceholder `json:"alias,omitempty"` + BiosFirmware string `json:"bios_firmware,omitempty"` + CPUNum int `json:"cpu_num,omitempty"` + CPUType string `json:"cpu_type,omitempty"` + DimmsNum int `json:"dimms_num,omitempty"` + GenerationName NonEmptyString `json:"generation_name,omitempty"` + HardwareVendorID UUID `json:"hardware_vendor_id,omitempty"` + HbaFirmware interface{} `json:"hba_firmware,omitempty"` + LegacyProductName interface{} `json:"legacy_product_name,omitempty"` + Name MojoStandardPlaceholder `json:"name,omitempty"` + NicsNum int `json:"nics_num,omitempty"` + NvmeSsdNum int `json:"nvme_ssd_num,omitempty"` + NvmeSsdSize interface{} `json:"nvme_ssd_size,omitempty"` + NvmeSsdSlots interface{} `json:"nvme_ssd_slots,omitempty"` + Prefix interface{} `json:"prefix,omitempty"` + PsuTotal int `json:"psu_total,omitempty"` + Purpose string `json:"purpose,omitempty"` + RAMTotal int `json:"ram_total,omitempty"` + RackUnitSize PositiveInteger `json:"rack_unit_size,omitempty"` + RaidLunNum int `json:"raid_lun_num,omitempty"` + SasHddNum int `json:"sas_hdd_num,omitempty"` + SasHddSize interface{} `json:"sas_hdd_size,omitempty"` + SasHddSlots interface{} `json:"sas_hdd_slots,omitempty"` + SasSsdNum int `json:"sas_ssd_num,omitempty"` + SasSsdSize interface{} `json:"sas_ssd_size,omitempty"` + SasSsdSlots interface{} `json:"sas_ssd_slots,omitempty"` + SataHddNum int `json:"sata_hdd_num,omitempty"` + SataHddSize interface{} `json:"sata_hdd_size,omitempty"` + SataHddSlots interface{} `json:"sata_hdd_slots,omitempty"` + SataSsdNum int `json:"sata_ssd_num,omitempty"` + SataSsdSize interface{} `json:"sata_ssd_size,omitempty"` + SataSsdSlots interface{} `json:"sata_ssd_slots,omitempty"` + Sku MojoStandardPlaceholder `json:"sku,omitempty"` + Specification interface{} `json:"specification,omitempty"` + UsbNum int `json:"usb_num,omitempty"` + ValidationPlanID UUID `json:"validation_plan_id,omitempty"` } type HardwareProductCreateEmbedded1 interface{} @@ -287,3 +323,77 @@ type UpdateUser struct { IsAdmin bool `json:"is_admin,omitempty"` Name NonEmptyString `json:"name,omitempty"` } + +type RackRoleCreate struct { + Name MojoStandardPlaceholder `json:"name"` + RackSize PositiveInteger `json:"rack_size"` +} + +type RackRoleUpdate struct { + Name MojoStandardPlaceholder `json:"name,omitempty"` + RackSize PositiveInteger `json:"rack_size,omitempty"` +} + +type RackUpdate struct { + AssetTag interface{} `json:"asset_tag,omitempty"` + BuildID UUID `json:"build_id,omitempty"` + DatacenterRoomID UUID `json:"datacenter_room_id,omitempty"` + Name MojoRelaxedPlaceholder `json:"name,omitempty"` + Phase DevicePhase `json:"phase,omitempty"` + RackRoleID UUID `json:"rack_role_id,omitempty"` + SerialNumber interface{} `json:"serial_number,omitempty"` +} + +type RackCreate struct { + AssetTag NonEmptyString `json:"asset_tag,omitempty"` + BuildID UUID `json:"build_id"` + DatacenterRoomID UUID `json:"datacenter_room_id"` + Name MojoRelaxedPlaceholder `json:"name"` + Phase DevicePhase `json:"phase,omitempty"` + RackRoleID UUID `json:"rack_role_id"` + SerialNumber NonEmptyString `json:"serial_number,omitempty"` +} + +type RackLayoutUpdate struct { + HardwareProductID UUID `json:"hardware_product_id,omitempty"` + RackID interface{} `json:"rack_id,omitempty"` + RackUnitStart PositiveInteger `json:"rack_unit_start,omitempty"` +} + +type RackAssignmentDelete struct { + DeviceID UUID `json:"device_id"` + RackUnitStart PositiveInteger `json:"rack_unit_start"` +} + +type RackAssignmentDeletes []RackAssignmentDelete + +type RackAssignmentUpdate struct { + DeviceAssetTag interface{} `json:"device_asset_tag,omitempty"` + DeviceID UUID `json:"device_id,omitempty"` + DeviceSerialNumber DeviceSerialNumber `json:"device_serial_number,omitempty"` + RackUnitStart PositiveInteger `json:"rack_unit_start"` +} + +type RackAssignmentUpdates []RackAssignmentUpdate + +type RackPhase struct { + Phase DevicePhase `json:"phase"` +} + +type RackLinks struct { + Links []Link `json:"links,omitempty"` +} + +type DatacenterRoomCreate struct { + Alias MojoStandardPlaceholder `json:"alias"` + Az NonEmptyString `json:"az"` + DatacenterID UUID `json:"datacenter_id"` + VendorName MojoRelaxedPlaceholder `json:"vendor_name"` +} + +type DatacenterRoomUpdate struct { + Alias MojoStandardPlaceholder `json:"alias,omitempty"` + Az NonEmptyString `json:"az,omitempty"` + DatacenterID UUID `json:"datacenter_id,omitempty"` + VendorName MojoRelaxedPlaceholder `json:"vendor_name,omitempty"` +} diff --git a/conch/types/Responses.go b/conch/types/responses.go similarity index 75% rename from conch/types/Responses.go rename to conch/types/responses.go index aeb687f..1d0da73 100644 --- a/conch/types/Responses.go +++ b/conch/types/responses.go @@ -6,16 +6,16 @@ import "time" type Build struct { Admins []UserTerse `json:"admins"` - Completed interface{} `json:"completed"` - CompletedUser interface{} `json:"completed_user"` + Completed time.Time `json:"completed"` + CompletedUser UserTerse `json:"completed_user"` Created time.Time `json:"created"` - Description interface{} `json:"description"` + Description string `json:"description"` DeviceHealth map[string]NonNegativeInteger `json:"device_health,omitempty"` DevicePhases map[string]NonNegativeInteger `json:"device_phases,omitempty"` ID UUID `json:"id"` Name MojoStandardPlaceholder `json:"name"` RackPhases map[string]NonNegativeInteger `json:"rack_phases,omitempty"` - Started interface{} `json:"started"` + Started time.Time `json:"started"` RoleViaOrganizationID UUID `json:"role_via_organization_id,omitempty"` } @@ -33,7 +33,15 @@ type UserTerse struct { // generated by "schematyper -o types/ResponseType_BuildOrgganizations.go --package=types --ptr-for-omit BuildOrgganizations.json" -- DO NOT EDIT -type BuildOrgganizations interface{} +type BuildOrganizations []BuildOrganization + +type BuildOrganization struct { + ID UUID `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Admins []UserTerse `json: "admins"` + Role Role `json:"role"` +} // generated by "schematyper -o types/ResponseType_Builds.go --package=types --ptr-for-omit Builds.json" -- DO NOT EDIT @@ -54,7 +62,7 @@ type BuildUsers []BuildUser type DatacenterRoomDetailed struct { Alias MojoStandardPlaceholder `json:"alias"` - Az string `json:"az"` + AZ string `json:"az"` Created time.Time `json:"created"` DatacenterID UUID `json:"datacenter_id"` ID UUID `json:"id"` @@ -67,13 +75,13 @@ type DatacenterRoomsDetailed []DatacenterRoomDetailed // generated by "schematyper -o types/ResponseType_Datacenters.go --package=types --ptr-for-omit Datacenters.json" -- DO NOT EDIT type Datacenter struct { - Created time.Time `json:"created"` - ID UUID `json:"id"` - Location string `json:"location"` - Region string `json:"region"` - Updated time.Time `json:"updated"` - Vendor string `json:"vendor"` - VendorName interface{} `json:"vendor_name"` + Created time.Time `json:"created"` + ID UUID `json:"id"` + Location string `json:"location"` + Region string `json:"region"` + Updated time.Time `json:"updated"` + Vendor string `json:"vendor"` + VendorName string `json:"vendor_name"` } type Datacenters []Datacenter @@ -81,33 +89,33 @@ type Datacenters []Datacenter // generated by "schematyper -o types/ResponseType_DetailedDevice.go --package=types --ptr-for-omit DetailedDevice.json" -- DO NOT EDIT type DetailedDevice struct { - AssetTag interface{} `json:"asset_tag"` - BuildID interface{} `json:"build_id"` - BuildName interface{} `json:"build_name"` + AssetTag DeviceAssetTag `json:"asset_tag"` + BuildID UUID `json:"build_id"` + BuildName string `json:"build_name"` Created time.Time `json:"created"` Disks []DetailedDeviceDisk `json:"disks,omitempty"` HardwareProductID UUID `json:"hardware_product_id"` Health DeviceHealth `json:"health"` - Hostname interface{} `json:"hostname"` + Hostname string `json:"hostname"` ID UUID `json:"id"` - LastSeen interface{} `json:"last_seen"` + LastSeen time.Time `json:"last_seen"` LatestReport DeviceReport `json:"latest_report"` Links []DetailedDeviceLink `json:"links"` - Location interface{} `json:"location,omitempty"` + Location DeviceLocation `json:"location,omitempty"` Nics []Nic `json:"nics,omitempty"` Phase DevicePhase `json:"phase"` SerialNumber DeviceSerialNumber `json:"serial_number"` Sku MojoStandardPlaceholder `json:"sku"` - SystemUUID interface{} `json:"system_uuid"` + SystemUUID UUID `json:"system_uuid"` Updated time.Time `json:"updated"` - UptimeSince interface{} `json:"uptime_since"` - Validated interface{} `json:"validated"` + UptimeSince time.Time `json:"uptime_since"` + Validated time.Time `json:"validated"` } type DetailedDeviceDisk struct { Created time.Time `json:"created"` DriveType interface{} `json:"drive_type"` - Enclosure interface{} `json:"enclosure"` + Enclosure int `json:"enclosure"` Firmware interface{} `json:"firmware"` Hba interface{} `json:"hba,omitempty"` Health interface{} `json:"health"` @@ -115,7 +123,7 @@ type DetailedDeviceDisk struct { Model interface{} `json:"model"` SerialNumber DiskSerialNumber `json:"serial_number"` Size interface{} `json:"size"` - Slot interface{} `json:"slot"` + Slot int `json:"slot"` Transport interface{} `json:"transport"` Updated time.Time `json:"updated"` Vendor interface{} `json:"vendor"` @@ -207,13 +215,13 @@ type DeviceNicFields struct { IfaceName DeviceInterfaceName `json:"iface_name,omitempty"` IfaceType string `json:"iface_type,omitempty"` IfaceVendor string `json:"iface_vendor,omitempty"` - Ipaddr string `json:"ipaddr,omitempty"` - Mac Macaddr `json:"mac,omitempty"` - Mtu string `json:"mtu,omitempty"` + Ipaddr Ipaddr `json:"ipaddr,omitempty"` + MAC Macaddr `json:"mac,omitempty"` + MTU string `json:"mtu,omitempty"` State string `json:"state,omitempty"` } -type Ipaddr interface{} +type Ipaddr string // generated by "schematyper -o types/ResponseType_DeviceNics.go --package=types --ptr-for-omit DeviceNics.json" -- DO NOT EDIT @@ -240,7 +248,7 @@ type DevicePXE struct { type TargetHardwareProduct struct { Alias string `json:"alias"` HardwareVendorID UUID `json:"hardware_vendor_id"` - ID interface{} `json:"id"` + ID UUID `json:"id"` Name string `json:"name"` Sku MojoStandardPlaceholder `json:"sku"` } @@ -268,10 +276,10 @@ type DevicePxes []DevicePxe // generated by "schematyper -o types/ResponseType_DeviceReportRow.go --package=types --ptr-for-omit DeviceReportRow.json" -- DO NOT EDIT type DeviceReportRow struct { - Created time.Time `json:"created"` - DeviceID UUID `json:"device_id"` - ID UUID `json:"id"` - Report interface{} `json:"report"` + Created time.Time `json:"created"` + DeviceID UUID `json:"device_id"` + ID UUID `json:"id"` + Report DeviceReportV300 `json:"report"` } // generated by "schematyper -o types/ResponseType_DeviceSerials.go --package=types --ptr-for-omit DeviceSerials.json" -- DO NOT EDIT @@ -287,26 +295,26 @@ type DeviceSettings map[string]DeviceSetting // generated by "schematyper -o types/ResponseType_Devices.go --package=types --ptr-for-omit Devices.json" -- DO NOT EDIT type Device struct { - AssetTag string `json:"asset_tag"` - BuildID string `json:"build_id"` + AssetTag DeviceAssetTag `json:"asset_tag"` + BuildID UUID `json:"build_id"` BuildName string `json:"build_name"` Created time.Time `json:"created"` HardwareProductID UUID `json:"hardware_product_id"` Health DeviceHealth `json:"health"` Hostname string `json:"hostname"` ID UUID `json:"id"` - LastSeen string `json:"last_seen"` + LastSeen time.Time `json:"last_seen"` Links []Link `json:"links"` Phase DevicePhase `json:"phase"` - RackID string `json:"rack_id,omitempty"` + RackID UUID `json:"rack_id,omitempty"` RackName string `json:"rack_name,omitempty"` RackUnitStart string `json:"rack_unit_start,omitempty"` SerialNumber DeviceSerialNumber `json:"serial_number"` Sku MojoStandardPlaceholder `json:"sku"` - SystemUUID string `json:"system_uuid"` + SystemUUID UUID `json:"system_uuid"` Updated time.Time `json:"updated"` - UptimeSince string `json:"uptime_since"` - Validated string `json:"validated"` + UptimeSince time.Time `json:"uptime_since"` + Validated time.Time `json:"validated"` } type Devices []Device @@ -326,10 +334,10 @@ type DeviceSku struct { type HardwareProduct struct { Alias MojoStandardPlaceholder `json:"alias"` Created time.Time `json:"created"` - GenerationName interface{} `json:"generation_name"` + GenerationName string `json:"generation_name"` ID UUID `json:"id"` Name MojoStandardPlaceholder `json:"name"` - Sku MojoStandardPlaceholder `json:"sku"` + SKU MojoStandardPlaceholder `json:"sku"` Updated time.Time `json:"updated"` } @@ -357,7 +365,7 @@ type LoginToken struct { type Organization struct { Builds []Build `json:"builds"` Created time.Time `json:"created"` - Description interface{} `json:"description"` + Description string `json:"description"` ID UUID `json:"id"` Name MojoStandardPlaceholder `json:"name"` Users []UserTerse `json:"users"` @@ -377,8 +385,8 @@ type Ping struct { type DeviceAssetTag string type Rack struct { - AssetTag interface{} `json:"asset_tag"` - BuildID interface{} `json:"build_id"` + AssetTag DeviceAssetTag `json:"asset_tag"` + BuildID UUID `json:"build_id"` BuildName interface{} `json:"build_name"` Created time.Time `json:"created"` DatacenterRoomAlias MojoStandardPlaceholder `json:"datacenter_room_alias"` @@ -389,7 +397,7 @@ type Rack struct { Phase DevicePhase `json:"phase"` RackRoleID UUID `json:"rack_role_id"` RackRoleName MojoStandardPlaceholder `json:"rack_role_name"` - SerialNumber interface{} `json:"serial_number"` + SerialNumber DeviceSerialNumber `json:"serial_number"` Updated time.Time `json:"updated"` } @@ -400,14 +408,14 @@ type Racks []Rack type Relay struct { Created time.Time `json:"created,omitempty"` ID UUID `json:"id,omitempty"` - Ipaddr interface{} `json:"ipaddr,omitempty"` + Ipaddr Ipaddr `json:"ipaddr,omitempty"` LastSeen time.Time `json:"last_seen,omitempty"` - Name interface{} `json:"name,omitempty"` - SSHPort interface{} `json:"ssh_port,omitempty"` + Name string `json:"name,omitempty"` + SSHPort PositiveInteger `json:"ssh_port,omitempty"` SerialNumber RelaySerialNumber `json:"serial_number"` Updated time.Time `json:"updated,omitempty"` UserID UUID `json:"user_id,omitempty"` - Version interface{} `json:"version,omitempty"` + Version string `json:"version,omitempty"` } type Relays []Relay @@ -431,18 +439,18 @@ type ReportValidationResults struct { type Role string type UserDetailed struct { - Builds []Build `json:"builds"` - Created time.Time `json:"created"` - Email EmailAddress `json:"email"` - ForcePasswordChange bool `json:"force_password_change"` - ID UUID `json:"id"` - IsAdmin bool `json:"is_admin"` - LastLogin interface{} `json:"last_login"` - LastSeen interface{} `json:"last_seen"` - Name string `json:"name"` - Organizations []Organization `json:"organizations"` - RefuseSessionAuth bool `json:"refuse_session_auth"` - Workspaces interface{} `json:"workspaces"` + Builds Builds `json:"builds"` + Created time.Time `json:"created"` + Email EmailAddress `json:"email"` + ForcePasswordChange bool `json:"force_password_change"` + ID UUID `json:"id"` + IsAdmin bool `json:"is_admin"` + LastLogin time.Time `json:"last_login"` + LastSeen time.Time `json:"last_seen"` + Name string `json:"name"` + Organizations Organizations `json:"organizations"` + RefuseSessionAuth bool `json:"refuse_session_auth"` + Workspaces interface{} `json:"workspaces"` } type UsersDetailed []UserDetailed @@ -451,7 +459,7 @@ type WorkspaceAndRole struct { Description interface{} `json:"description"` ID UUID `json:"id"` Name MojoStandardPlaceholder `json:"name"` - ParentWorkspaceID interface{} `json:"parent_workspace_id"` + ParentWorkspaceID UUID `json:"parent_workspace_id"` Role Role `json:"role"` RoleViaWorkspaceID UUID `json:"role_via_workspace_id,omitempty"` } @@ -469,7 +477,7 @@ type UserSettings map[string]UserSetting type UserToken struct { Created time.Time `json:"created"` Expires time.Time `json:"expires"` - LastIpaddr interface{} `json:"last_ipaddr"` + LastIpaddr Ipaddr `json:"last_ipaddr"` LastUsed interface{} `json:"last_used"` Name string `json:"name"` } @@ -491,9 +499,9 @@ type ValidationPlans []ValidationPlan type ValidationResult struct { Category string `json:"category"` - Component interface{} `json:"component"` + Component string `json:"component"` Hint interface{} `json:"hint"` - ID interface{} `json:"id"` + ID UUID `json:"id"` Message string `json:"message"` Status ValidationStatus `json:"status"` ValidationID UUID `json:"validation_id"` @@ -521,14 +529,14 @@ type Validations []Validation // generated by "schematyper -o types/ResponseType_ValidationStateWithResults.go --package=types --ptr-for-omit ValidationStateWithResults.json" -- DO NOT EDIT type ValidationStateWithResults struct { - Created time.Time `json:"created"` - DeviceID UUID `json:"device_id"` - DeviceReportID UUID `json:"device_report_id"` - HardwareProductID UUID `json:"hardware_product_id"` - ID UUID `json:"id"` - Results []ValidationResult `json:"results"` - Status ValidationStatus `json:"status"` - ValidationPlanID UUID `json:"validation_plan_id"` + Created time.Time `json:"created"` + DeviceID UUID `json:"device_id"` + DeviceReportID UUID `json:"device_report_id"` + HardwareProductID UUID `json:"hardware_product_id"` + ID UUID `json:"id"` + Status ValidationStatus `json:"status"` + ValidationPlanID UUID `json:"validation_plan_id"` + Results ValidationResults `json:"results"` } // generated by "schematyper -o types/ResponseType_Version.go --package=types --ptr-for-omit Version.json" -- DO NOT EDIT @@ -536,3 +544,40 @@ type ValidationStateWithResults struct { type Version struct { Version string `json:"version"` } + +// generated by "schematyper -o types/ResponseType_RackRoles.go --package=types --ptr-for-omit RackRoles.json" -- DO NOT EDIT +type RackRole struct { + Created time.Time `json:"created"` + ID UUID `json:"id"` + Name MojoStandardPlaceholder `json:"name"` + RackSize PositiveInteger `json:"rack_size"` + Updated time.Time `json:"updated"` +} + +type RackRoles []RackRole + +type RackLayout struct { + Created time.Time `json:"created"` + HardwareProductID UUID `json:"hardware_product_id"` + ID UUID `json:"id"` + RackID UUID `json:"rack_id"` + RackName MojoRelaxedPlaceholder `json:"rack_name"` + RackUnitSize PositiveInteger `json:"rack_unit_size"` + RackUnitStart PositiveInteger `json:"rack_unit_start"` + Sku MojoStandardPlaceholder `json:"sku"` + Updated time.Time `json:"updated"` +} + +type RackLayouts []RackLayout + +type RackAssignment struct { + DeviceAssetTag DeviceAssetTag `json:"device_asset_tag"` + DeviceID UUID `json:"device_id"` + DeviceSerialNumber DeviceSerialNumber `json:"device_serial_number"` + HardwareProductName string `json:"hardware_product_name"` + RackUnitSize PositiveInteger `json:"rack_unit_size"` + RackUnitStart PositiveInteger `json:"rack_unit_start"` + Sku MojoStandardPlaceholder `json:"sku"` +} + +type RackAssignments []RackAssignment diff --git a/conch/types/templates.go b/conch/types/templates.go new file mode 100644 index 0000000..7878afc --- /dev/null +++ b/conch/types/templates.go @@ -0,0 +1,758 @@ +package types + +import ( + "fmt" + "sort" + "strconv" + "strings" + + "github.com/joyent/kosh/tables" + "github.com/joyent/kosh/template" +) + +func (bl Builds) Len() int { return len(bl) } +func (bl Builds) Swap(i, j int) { bl[i], bl[j] = bl[j], bl[i] } +func (bl Builds) Less(i, j int) bool { return bl[i].Name < bl[j].Name } +func (bl Builds) Headers() []string { + return []string{ + "Name", + "Description", + "Started", + "Completed", + "Completed By", + } +} + +func (bl Builds) ForEach(do func([]string)) { + for _, b := range bl { + do([]string{ + string(b.Name), + b.Description, + b.Started.String(), + b.Completed.String(), + string(b.CompletedUser.Email), + }) + } +} + +const buildTemplate = ` +# Build {{ .Name }} + +{{ .Description }} + +## Admins ## +{{ range .Admins }} +* {{ .Name }} - {{ .Email }} +{{ end }} + +--- +* Created: {{ .Created }} +* Started: {{ .Started }} +* Completed: {{ .Completed }} by {{ .CompletedUser.Name }}({{ .CompletedUser.Email }}) +` + +func (b Build) Template() string { return buildTemplate } + +func (bu BuildUsers) Len() int { return len(bu) } +func (bu BuildUsers) Swap(i, j int) { bu[i], bu[j] = bu[j], bu[i] } +func (bu BuildUsers) Less(i, j int) bool { return bu[i].Name < bu[j].Name } +func (bu BuildUsers) Headers() []string { + return []string{ + "ID", + "Name", + "Email", + "Role", + } +} + +func (bu BuildUsers) ForEach(do func([]string)) { + for _, u := range bu { + do([]string{ + template.CutUUID(u.ID.String()), + u.Name, + string(u.Email), + string(u.Role), + }) + } +} + +func (bo BuildOrganizations) Len() int { return len(bo) } +func (bo BuildOrganizations) Swap(i, j int) { bo[i], bo[j] = bo[j], bo[i] } +func (bo BuildOrganizations) Less(i, j int) bool { return bo[i].Name < bo[j].Name } +func (bo BuildOrganizations) Headers() []string { + return []string{ + "ID", + "Name", + "Description", + "Role", + } +} + +func (bo BuildOrganizations) ForEach(do func([]string)) { + for _, o := range bo { + do([]string{ + template.CutUUID(o.ID.String()), + o.Name, + o.Description, + string(o.Role), + }) + } +} + +func (dl Datacenters) Len() int { return len(dl) } +func (dl Datacenters) Swap(i, j int) { dl[i], dl[j] = dl[j], dl[i] } +func (dl Datacenters) Less(i, j int) bool { return dl[i].VendorName < dl[j].VendorName } +func (dl Datacenters) Headers() []string { + return []string{ + "ID", + "Vendor", + "Vendor Name", + "Region", + "Location", + } +} + +func (dl Datacenters) ForEach(do func([]string)) { + for _, d := range dl { + do([]string{ + template.CutUUID(d.ID.String()), + d.Vendor, + d.VendorName, + d.Region, + d.Location, + }) + } +} + +const datacenterTemplate = ` +ID: {{ .ID }} +Vendor: {{ .Vendor }} +Vendor Name: {{ .VendorName }} +Region: {{ .Region }} +Location: {{ .Location }} + +Created: {{ TimeStr .Created }} +Updated: {{ TimeStr .Updated }} +` + +func (d Datacenter) Template() string { return datacenterTemplate } + +func (ds DeviceSettings) String() string { + tableString := &strings.Builder{} + table := tables.NewTable(tableString) + + var keys []string + for key := range ds { + keys = append(keys, key) + } + sort.Strings(keys) + + for _, key := range keys { + value := ds[key] + table.Append([]string{key, string(value)}) + } + + table.Render() + return tableString.String() +} + +const detailedDeviceTemplate = ` +ID: {{ .ID }} +Serial: {{ .SerialNumber }} +Asset Tag: {{ .AssetTag }} +Hostname: {{ .Hostname }} +System UUID: {{ .SystemUUID }} + +Phase: {{ .Phase }} +Health: {{ .Health }} +Validated: {{ if not $.Validated.IsZero }}{{ .Validated.Local }}{{ end }} + +Created: {{ TimeStr .Created }} +Updated: {{ TimeStr .Updated }} +Last Seen: {{ TimeStr .LastSeen }}{{ if .Links }} + +Links: {{ range .Links }} + - {{ $ }} +{{ end }}{{ end }} + +Location: {{- if ne .Phase "integration" }} ** Device has left integration. This data is historic and likely not accurate. **{{ end }} + AZ: {{ .Location.Az }} + Datacenter: + Datacenter: {{ .Location.DatacenterRoom }} + Rack: {{ .Location.Rack }} + RU: {{ .Location.RackUnitStart }} + + +Network Interfaces: {{ range .Nics }} + - {{ .InterfaceName }} - {{ .Mac }} + Type: {{ .InterfaceType }} + Vendor: {{ .InterfaceVendor }}{{ if ne .PeerMac "" }} + Peer: {{ .PeerMac }}{{ end }}{{ if ne .PeerSwitch "" }} - {{ .PeerSwitch }}{{ end }} +{{ end }} + +Disks:{{range $name, $slots := .Disks}} + Enclosure: {{ $name }}{{ range $slots }} + Slot: {{ .Slot }} + SN: {{ .SerialNumber }} + Type: {{ .DriveType }} + Vendor: {{ .Vendor }} + Model: {{ .Model }} + Size: {{ .Size }} + Health: {{ .Health }} + Firmware: {{ .Firmware }} + Transport: {{ .Transport }} +{{ end }}{{ end }} +` + +func (d DetailedDevice) Template() string { return detailedDeviceTemplate } + +const deviceTemplate = ` +ID: {{ .ID }} +Serial: {{ .SerialNumber }} +Asset Tag: {{ .AssetTag }} +Hostname: {{ .Hostname }} +System UUID: {{ .SystemUUID }} + +Phase: {{ .Phase }} +Health: {{ .Health }} +Validated: {{ if not $.Validated.IsZero }}{{ .Validated.Local }}{{ end }} + +Created: {{ TimeStr .Created }} +Updated: {{ TimeStr .Updated }} +Last Seen: {{ TimeStr .LastSeen }}{{ if .Links }} + +Rack: + ID: {{ CutUUID .RackID }} + Name: {{ .RackName }} + RU: {{ .RackUnitStart }} + +Links: {{ range .Links }} + - {{ $ }} +{{ end }}{{ end }} + +` + +func (d Device) Template() string { return deviceTemplate } + +func (d Devices) Len() int { return len(d) } +func (d Devices) Swap(i, j int) { d[i], d[j] = d[j], d[i] } +func (d Devices) Less(i, j int) bool { return d[i].SerialNumber < d[j].SerialNumber } +func (d Devices) Headers() []string { + return []string{ + "Serial", + "Hostname", + "Asset Tag", + "Hardware", + "Phase", + "Updated", + "Validated", + } +} + +func (d Devices) ForEach(do func([]string)) { + for _, device := range d { + do([]string{ + string(device.SerialNumber), + device.Hostname, + string(device.AssetTag), + device.HardwareProductID.String(), + string(device.Phase), + template.TimeStr(device.Updated), + template.TimeStr(device.Validated), + }) + } +} + +const deviceReportTemplate = `` + +func (d DeviceReport) Template() string { return deviceReportTemplate } + +const hardwareProductTemplate = ` +ID: {{ .ID }} +Name: {{ .Name }} +SKU: {{ .SKU }} + +Alias: {{ .Alias }} +GenerationName: {{ .GenerationName }} + +Created: {{ TimeStr .Created }} +Updated: {{ TimeStr .Updated }} +` + +func (hp HardwareProduct) Template() string { return hardwareProductTemplate } + +// TODO sort interface, tabulable interface +func (h HardwareProducts) String() string { + tableString := &strings.Builder{} + table := tables.NewTable(tableString) + + table.SetHeader([]string{ + "ID", + "SKU", + "Name", + "Alias", + "GenerationName", + "Created", + "Updated", + }) + + for _, hp := range h { + table.Append([]string{ + template.CutUUID(hp.ID.String()), + string(hp.SKU), + string(hp.Name), + string(hp.Alias), + hp.GenerationName, + hp.Created.String(), + hp.Updated.String(), + }) + } + table.Render() + return tableString.String() +} + +const hardwareVendorTemplate = ` +Name: {{ .Name }} +ID: {{ .ID }} +Created: {{ TimeStr .Created }} +Updated: {{ TimeStr .Updated }} +` + +func (h HardwareVendor) Template() string { return hardwareVendorTemplate } + +func (h HardwareVendors) Len() int { return len(h) } +func (h HardwareVendors) Swap(i, j int) { h[i], h[j] = h[j], h[i] } +func (h HardwareVendors) Less(i, j int) bool { return h[i].Name < h[j].Name } +func (h HardwareVendors) Headers() []string { + return []string{ + "Name", + "ID", + "Created", + "Updated", + } +} + +func (h HardwareVendors) ForEach(do func([]string)) { + for _, v := range h { + do([]string{ + string(v.Name), + template.CutUUID(v.ID.String()), + template.TimeStr(v.Created), + template.TimeStr(v.Updated), + }) + } +} + +const organizationTemplate = ` +Name: {{ .Name }} +ID: {{ .ID }} +Description: {{ .Description }} +` + +func (o Organization) Template() string { return organizationTemplate } + +func (o Organizations) Len() int { return len(o) } +func (o Organizations) Swap(i, j int) { o[i], o[j] = o[j], o[i] } +func (o Organizations) Less(i, j int) bool { return o[i].Name < o[j].Name } +func (o Organizations) Headers() []string { + return []string{ + "Name", + "Role", + "Description", + } +} + +func (o Organizations) ForEach(do func([]string)) { + for _, org := range o { + do([]string{ + string(org.Name), + string(org.Role), + org.Description, + }) + } +} +func (o Organizations) String() { tables.Render(o) } + +const rackTemplate = ` +ID: {{ .ID }} +Name: {{ .Name }} +Serial Number: {{ .SerialNumber }} +Asset Tag: {{ .AssetTag }} +Phase: {{ .Phase }} +Role: {{ .RackRoleName }} +Room: {{ .DatacenterRoomAlias }} + +Created: {{ TimeStr .Created }} +Updated: {{ TimeStr .Updated }} +` + +func (r Rack) Template() string { return rackTemplate } + +func (r Racks) Len() int { return len(r) } +func (r Racks) Swap(i, j int) { r[i], r[j] = r[j], r[i] } +func (r Racks) Less(i, j int) bool { return r[i].SerialNumber > r[j].SerialNumber } +func (rl Racks) Headers() []string { + return []string{ + "ID", + "Name", + "Room", + "Role", + "Serial Number", + "Asset Tag", + "Phase", + "Created", + "Updated", + } +} + +func (rl Racks) ForEach(do func([]string)) { + for _, r := range rl { + do([]string{ + r.ID.String(), + string(r.Name), + template.CutUUID(r.DatacenterRoomID.String()), + string(r.RackRoleName), + string(r.SerialNumber), + string(r.AssetTag), + string(r.Phase), + template.TimeStr(r.Created), + template.TimeStr(r.Updated), + }) + } +} + +func (r RackLayouts) Len() int { return len(r) } +func (r RackLayouts) Swap(i, j int) { r[i], r[j] = r[j], r[i] } +func (r RackLayouts) Less(i, j int) bool { return r[i].RackUnitStart > r[j].RackUnitStart } + +func (rl RackLayouts) Headers() []string { + return []string{ + "Rack Unit Start", + "Rack Unit Size", + "ID", + "Hardware Product", + "Created", + "Updated", + } +} + +func (rl RackLayouts) ForEach(do func([]string)) { + for _, r := range rl { + do([]string{ + strconv.Itoa(int(r.RackUnitStart)), + strconv.Itoa(int(r.RackUnitSize)), + template.CutUUID(r.ID.String()), + r.HardwareProductID.String(), + template.TimeStr(r.Created), + template.TimeStr(r.Updated), + }) + } +} + +func (ra RackAssignments) Len() int { return len(ra) } +func (ra RackAssignments) Swap(i, j int) { ra[i], ra[j] = ra[j], ra[i] } +func (ra RackAssignments) Less(i, j int) bool { return ra[i].RackUnitStart > ra[j].RackUnitStart } + +func (ra RackAssignments) Headers() []string { + return []string{ + "Device Serial", + "Device Asset Tag", + "Hardware Product", + "Rack Unit Start", + "Rack Unit Size", + } +} + +func (ra RackAssignments) ForEach(do func([]string)) { + for _, r := range ra { + do([]string{ + string(r.DeviceSerialNumber), + string(r.DeviceAssetTag), + string(r.HardwareProductName), + strconv.Itoa(int(r.RackUnitStart)), + strconv.Itoa(int(r.RackUnitSize)), + }) + } +} + +func (r RackRoles) Len() int { return len(r) } +func (r RackRoles) Swap(i, j int) { r[i], r[j] = r[j], r[i] } +func (r RackRoles) Less(i, j int) bool { return r[i].Name < r[j].Name } +func (rl RackRoles) String() string { + sort.Sort(rl) + + tableString := &strings.Builder{} + table := tables.NewTable(tableString) + + table.SetHeader([]string{ + "Name", + "RackSize", + "Created", + "Updated", + }) + + for _, r := range rl { + table.Append([]string{ + string(r.Name), + strconv.Itoa(int(r.RackSize)), + template.TimeStr(r.Created), + template.TimeStr(r.Updated), + }) + } + + table.Render() + return tableString.String() +} + +const rackRoleTemplate = ` +Name: {{ .Name }} +Rack Size: {{ .RackSize }} + +Created: {{ TimeStr .Created }} +Updated: {{ TimeStr .Updated }} +` + +func (r RackRole) Template() string { return rackRoleTemplate } + +const relayTemplate = ` +ID: {{ .ID }} +Serial Number: {{ .SerialNumber }} +Name: {{ .Name }} +Version: {{ .Version }} +Created: {{ TimeStr .Created }} +Updated: {{ TimeStr .Updated }} + +IP Address: {{ .IpAddr }} +SSH Port: {{ .SshPort }} +` + +func (r Relay) Template() string { return relayTemplate } + +func (r Relays) Len() int { return len(r) } +func (r Relays) Swap(i, j int) { r[i], r[j] = r[j], r[i] } +func (r Relays) Less(i, j int) bool { return r[i].Updated.Before(r[j].Updated) } + +func (rl Relays) String() string { + sort.Sort(rl) + + tableString := &strings.Builder{} + table := tables.NewTable(tableString) + + table.SetHeader([]string{ + "Serial", + "Name", + "Version", + "IP", + "Updated", + }) + + for _, r := range rl { + table.Append([]string{ + string(r.SerialNumber), + string(r.Name), + string(r.Version), + string(r.Ipaddr), + template.TimeStr(r.Updated), + }) + } + + table.Render() + return tableString.String() +} + +const roomTemplate = ` +ID: {{ .ID }} +Alias: {{ .Alias }} +AZ: {{ .AZ }} +Vendor Name: {{ .VendorName }} +Datacenter ID: {{ .DatacenterID }} + +Created: {{ TimeStr .Created }} +Updated: {{ TimeStr .Updated }} +` + +func (r DatacenterRoomDetailed) Template() string { return roomTemplate } + +func (dr DatacenterRoomsDetailed) Len() int { return len(dr) } +func (dr DatacenterRoomsDetailed) Swap(i, j int) { dr[i], dr[j] = dr[j], dr[i] } +func (dr DatacenterRoomsDetailed) Less(i, j int) bool { return dr[i].Alias < dr[j].Alias } +func (dr DatacenterRoomsDetailed) String() string { + sort.Sort(dr) + + tableString := &strings.Builder{} + table := tables.NewTable(tableString) + + table.SetHeader([]string{ + "ID", + "Alias", + "AZ", + "Vendor Name", + "Datacenter ID", + "Created", + "Updated", + }) + + for _, r := range dr { + table.Append([]string{ + template.CutUUID(r.ID.String()), + string(r.Alias), + string(r.AZ), + string(r.VendorName), + template.CutUUID(r.DatacenterID.String()), + template.TimeStr(r.Created), + template.TimeStr(r.Updated), + }) + } + + table.Render() + return tableString.String() +} + +func (u UserSettings) Headers() []string { + return []string{ + "Key", + "Value", + } +} + +func (u UserSettings) ForEach(do func([]string)) { + keys := make([]string, 0) + for setting := range u { + keys = append(keys, setting) + } + sort.Strings(keys) + + for _, key := range keys { + do([]string{ + key, + fmt.Sprintf("%v", u[key]), + }) + } +} + +const detailedUserTemplate = ` +# {{ .Name }} + +* ID: {{ .ID }} +* Email: {{ .Email }} +* System Admin: {{ if $.IsAdmin }}Yes{{ else }}No{{ end }} + +Created: {{ TimeStr .Created }} + +Last Login: {{ if $.LastLogin.IsZero }}Never/Unknown{{ else }}{{ TimeStr .LastLogin }}{{ end }} + +## Organizations + +{{ Table .Organizations }} +` + +func (u UserDetailed) Template() string { return detailedUserTemplate } + +const validationPlanTemplate = ` +ID: {{ .ID }} +Name: {{ .Name }} +Description: {{ .Description }} +Created: {{ .Created }} +` + +func (v ValidationPlan) Template() string { return validationPlanTemplate } + +func (v ValidationPlans) Len() int { return len(v) } +func (v ValidationPlans) Swap(i, j int) { v[i], v[j] = v[j], v[i] } +func (v ValidationPlans) Less(i, j int) bool { return v[i].Name < v[j].Name } + +func (v ValidationPlans) String() string { + sort.Sort(v) + + tableString := &strings.Builder{} + table := tables.NewTable(tableString) + table.SetRowLine(true) + + table.SetHeader([]string{ + "ID", + "Name", + "Description", + "Created", + }) + + for _, p := range v { + table.Append([]string{ + template.CutUUID(p.ID.String()), + string(p.Name), + p.Description, + p.Created.String(), + }) + } + + table.Render() + return tableString.String() +} + +const validationStateWithResultsTemplate = ` +ID: {{ .ID }} +Device: {{ CutUUID .DeviceID.String }} +Hardware Product: {{ CutUUID .HardwareProductID.String }} +Validation Plan: {{ CutUUID .ValidationPlanID.String }} +Created: {{ TimeStr .Created }} +Status: {{ .Status }} + +Results: +{{ .Results }} +` + +func (v ValidationStateWithResults) Template() string { return validationStateWithResultsTemplate } + +func (v ValidationResults) Len() int { return len(v) } +func (v ValidationResults) Swap(i, j int) { v[i], v[j] = v[j], v[i] } +func (v ValidationResults) Less(i, j int) bool { return v[i].Category < v[j].Category } + +func (v ValidationResults) String() string { + sort.Sort(v) + + tableString := &strings.Builder{} + table := tables.NewTable(tableString) + table.SetRowLine(true) + + table.SetHeader([]string{ + "Status", + "Category", + "Component", + "Message", + }) + + for _, r := range v { + table.Append([]string{ + string(r.Status), + r.Category, + r.Component, + r.Message, + }) + } + + table.Render() + return tableString.String() +} + +const deviceNicTemplate = ` +Name: {{ .IfaceName }} +Vendor: {{ .IfaceVendor }} +Type: {{ .IfaceType }} + +IP Address: {{ .Ipaddr }} +MAC: {{ .MAC }} +MTU: {{ .MTU }} +State: {{ .State }} + +Device ID: {{ .DeviceID }} +` + +func (dn DeviceNic) Template() string { return deviceNicTemplate } + +const deviceLocationTemplate = ` +Rack {{ .Rack }} +Rack Unit Start: {{ .RackUnitStart }} +DatacenterRoom: {{ .DatacenterRoom }} +AZ: {{ .Az }} +` + +func (dl DeviceLocation) Template() string { return deviceLocationTemplate } diff --git a/conch/users.go b/conch/users.go index a586339..5d751cd 100644 --- a/conch/users.go +++ b/conch/users.go @@ -4,6 +4,7 @@ import "github.com/joyent/kosh/conch/types" // GET /user/me func (c *Client) GetCurrentUser() (me types.UserDetailed) { + c.Debug("GetCurrentUser()") me = c.GetUserByID("me") return } @@ -20,7 +21,7 @@ func (c *Client) ChangeCurrentUserPassword(setting types.UserSetting) error { // GET /user/me/settings func (c *Client) GetCurrentUserSettings() (settings types.UserSettings) { - c.User("me").Settings("").Receive(settings) + c.User("me").Settings("").Receive(&settings) return } @@ -32,7 +33,7 @@ func (c *Client) SetCurrentUserSettings(settings types.UserSettings) error { // GET /user/me/setting/:name func (c *Client) GetCurrentUserSettingByName(name string) (setting types.UserSetting) { - c.User("me").Settings(name).Receive(setting) + c.User("me").Settings(name).Receive(&setting) return } @@ -56,7 +57,7 @@ func (c *Client) GetCurrentUserTokens() (tokens types.UserTokens) { // POST /user/me/token func (c *Client) CreateCurrentUserToken(newToken types.NewUserToken) (token types.NewUserToken) { - c.User("me").Token().Post(newToken).Receive(token) + c.User("me").Token().Post(newToken).Receive(&token) return } @@ -73,7 +74,7 @@ func (c *Client) DeleteCurrentUserToken(name string) error { // GET /user/:target_user_id_or_email func (c *Client) GetUserByID(id string) (user types.UserDetailed) { - c.User(id).Receive(user) + c.User(id).Receive(&user) return } @@ -103,25 +104,25 @@ func (c *Client) ChangeUserPassword(id string, setting types.UserSetting) error // GET /user func (c *Client) GetUsers() (me types.UsersDetailed) { - c.User("").Receive(me) + c.User("").Receive(&me) return } // POST /user?send_mail=<1|0> func (c *Client) CreateUser(newUser types.NewUser) (user types.NewUser) { - c.User().Post(newUser).Receive(user) + c.User().Post(newUser).Receive(&user) return } // GET /user/:target_user_id_or_email/token func (c *Client) GetUserToken(id string) (tokens types.UserTokens) { - c.User(id).Token().Receive(tokens) + c.User(id).Token().Receive(&tokens) return } // GET /user/:target_user_id_or_email/token/:token_name func (c *Client) GetUserTokenByName(id, name string) (token types.UserToken) { - c.User(id).Token(name).Receive(token) + c.User(id).Token(name).Receive(&token) return } diff --git a/conch/users_test.go b/conch/users_test.go index 5a50620..3f1cc20 100644 --- a/conch/users_test.go +++ b/conch/users_test.go @@ -140,7 +140,7 @@ func TestUser(t *testing.T) { w.WriteHeader(http.StatusOK) })) defer ts.Close() - test.Do(conch.New(ts.URL, "token", &logger{})) + test.Do(conch.New(newConfig(ts.URL))) assert.True(t, seen, "saw the correct post to conch") }) } diff --git a/conch/validations.go b/conch/validations.go index 8b7b827..04dce2e 100644 --- a/conch/validations.go +++ b/conch/validations.go @@ -4,36 +4,51 @@ import "github.com/joyent/kosh/conch/types" // GET /validation func (c *Client) GetValidations() (validations types.Validations) { - c.Validation().Receive(validations) + c.Validation().Receive(&validations) return } // GET /validation/:validation_id_or_name -func (c *Client) GetValidationByID(id string) (validation types.Validation) { - c.Validation(id).Receive(validation) +func (c *Client) GetValidationByName(name string) (validation types.Validation) { + c.Validation(name).Receive(&validation) + return +} + +func (c *Client) GetValidationByID(id types.UUID) (validation types.Validation) { + c.Validation(id.String()).Receive(&validation) return } // GET /validation_plan -func (c *Client) GetValidationPlans() (plans types.ValidationPlans) { - c.ValidationPlan().Receive(plans) +func (c *Client) GetAllValidationPlans() (plans types.ValidationPlans) { + c.ValidationPlan().Receive(&plans) return } // GET /validation_plan/:validation_plan_id_or_name -func (c *Client) GetValidationPlanByID(id string) (plan types.ValidationPlan) { - c.ValidationPlan(id).Receive(plan) +func (c *Client) GetValidationPlanByName(name string) (plan types.ValidationPlan) { + c.ValidationPlan(name).Receive(&plan) + return +} + +func (c *Client) GetValidationPlanByID(id types.UUID) (plan types.ValidationPlan) { + c.ValidationPlan(id.String()).Receive(&plan) return } // GET /validation_plan/:validation_plan_id_or_name/validation func (c *Client) GetValidationPlanValidations(id string) (validations types.Validations) { - c.ValidationPlan(id).Validation().Receive(validations) + c.ValidationPlan(id).Validation().Receive(&validations) return } // GET /validation_state/:validation_state_id_or_name -func (c *Client) GetValidationStateByID(id string) (state types.ValidationStateWithResults) { - c.ValidationState(id).Receive(state) +func (c *Client) GetValidationStateByName(name string) (state types.ValidationStateWithResults) { + c.ValidationState(name).Receive(&state) + return +} + +func (c *Client) GetValidationStateByID(id types.UUID) (state types.ValidationStateWithResults) { + c.ValidationState(id.String()).Receive(&state) return } diff --git a/conch/validations_test.go b/conch/validations_test.go index 0918fbb..f77a30d 100644 --- a/conch/validations_test.go +++ b/conch/validations_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/joyent/kosh/conch" + "github.com/joyent/kosh/conch/types" "github.com/stretchr/testify/assert" ) @@ -24,18 +25,29 @@ func TestValidationRoutes(t *testing.T) { { URL: "/validation/foo/", Method: "GET", - Do: func(c *conch.Client) { _ = c.GetValidationByID("foo") }, + Do: func(c *conch.Client) { _ = c.GetValidationByName("foo") }, + }, + { + URL: "/validation/00000000-0000-0000-0000-000000000000/", + Method: "GET", + Do: func(c *conch.Client) { _ = c.GetValidationByID(types.UUID{}) }, }, { URL: "/validation_plan/", Method: "GET", - Do: func(c *conch.Client) { _ = c.GetValidationPlans() }, + Do: func(c *conch.Client) { _ = c.GetAllValidationPlans() }, }, { URL: "/validation_plan/foo/", Method: "GET", - Do: func(c *conch.Client) { _ = c.GetValidationPlanByID("foo") }, + Do: func(c *conch.Client) { _ = c.GetValidationPlanByName("foo") }, + }, + { + URL: "/validation_plan/00000000-0000-0000-0000-000000000000/", + Method: "GET", + Do: func(c *conch.Client) { _ = c.GetValidationPlanByID(types.UUID{}) }, }, + { URL: "/validation_plan/foo/validation/", Method: "GET", @@ -44,7 +56,12 @@ func TestValidationRoutes(t *testing.T) { { URL: "/validation_state/foo/", Method: "GET", - Do: func(c *conch.Client) { _ = c.GetValidationStateByID("foo") }, + Do: func(c *conch.Client) { _ = c.GetValidationStateByName("foo") }, + }, + { + URL: "/validation_state/00000000-0000-0000-0000-000000000000/", + Method: "GET", + Do: func(c *conch.Client) { _ = c.GetValidationStateByID(types.UUID{}) }, }, } @@ -60,7 +77,7 @@ func TestValidationRoutes(t *testing.T) { w.WriteHeader(http.StatusOK) })) defer ts.Close() - test.Do(conch.New(ts.URL, "token", &logger{})) + test.Do(conch.New(newConfig(ts.URL))) assert.True(t, seen, "saw the correct post to conch") }) } diff --git a/datacenter.go b/datacenter.go deleted file mode 100644 index f5df8a9..0000000 --- a/datacenter.go +++ /dev/null @@ -1,392 +0,0 @@ -// Copyright Joyent, Inc. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package main - -//lint:file-ignore U1000 WIP - -import ( - "bytes" - "errors" - "fmt" - "net/url" - "strings" - "time" - - "github.com/gofrs/uuid" - cli "github.com/jawher/mow.cli" - "github.com/joyent/kosh/tables" - "github.com/joyent/kosh/template" -) - -type Datacenters struct { - *Conch -} - -func (c *Conch) Datacenters() *Datacenters { - return &Datacenters{c} -} - -type Datacenter struct { - ID uuid.UUID `json:"id" faker:"uuid"` - Vendor string `json:"vendor"` - VendorName string `json:"vendor_name,omitempty"` - Region string `json:"region"` - Location string `json:"location"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` -} - -func (d Datacenter) String() string { - if API.JsonOnly { - return API.AsJSON(d) - } - - t, err := template.NewTemplate().Parse(datacenterTemplate) - if err != nil { - panic(err) - } - - buf := new(bytes.Buffer) - if err := t.Execute(buf, d); err != nil { - panic(err) - } - - return buf.String() -} - -type DatacenterList []Datacenter - -func (dl DatacenterList) String() string { - if API.JsonOnly { - return API.AsJSON(dl) - } - - tableString := &strings.Builder{} - table := tables.NewTable(tableString) - tables.TableToMarkdown(table) - - table.SetHeader([]string{ - "ID", - "Vendor", - "Vendor Name", - "Region", - "Location", - }) - - for _, d := range dl { - table.Append([]string{ - template.CutUUID(d.ID.String()), - d.Vendor, - d.VendorName, - d.Region, - d.Location, - }) - } - - table.Render() - return tableString.String() -} - -/****/ - -func (d *Datacenters) GetAll() DatacenterList { - dl := make(DatacenterList, 0) - res := d.Do(d.Sling().Get("/dc")) - if ok := res.Parse(&dl); !ok { - panic(res) - } - return dl -} - -func (d *Datacenters) FindDatacenterID(id string) (bool, uuid.UUID) { - ids := make([]uuid.UUID, 0) - for _, datacenter := range d.GetAll() { - ids = append(ids, datacenter.ID) - } - - return FindUUID(id, ids) -} - -func (d *Datacenters) Get(id uuid.UUID) Datacenter { - var dc Datacenter - uri := fmt.Sprintf( - "/dc/%s", - url.PathEscape(id.String()), - ) - - res := d.Do(d.Sling().Get(uri)) - if ok := res.Parse(&dc); !ok { - panic(res) - } - return dc -} - -func (d *Datacenters) Update(id uuid.UUID, region string, vendor string, location string, vendorName string) Datacenter { - - var dc Datacenter - - uri := fmt.Sprintf( - "/dc/%s", - url.PathEscape(id.String()), - ) - - payload := make(map[string]string) - - if region != "" { - payload["region"] = region - } - - if vendor != "" { - payload["vendor"] = vendor - } - - if vendorName != "" { - payload["vendor_name"] = vendorName - } - - if location != "" { - payload["location"] = location - } - - if len(payload) == 0 { - panic(errors.New("at least one property must be defined: region, vendor, vendor name, location")) - } - - res := d.Do( - d.Sling().New().Post(uri). - Set("Content-Type", "application/json"). - BodyJSON(payload), - ) - - if ok := res.Parse(&dc); !ok { - panic(res) - } - - return dc -} - -func (d *Datacenters) Create(region string, vendor string, location string, vendorName string) Datacenter { - payload := make(map[string]string) - if vendor == "" { - panic(errors.New("'vendor' cannot be empty")) - } - payload["vendor"] = vendor - - if region == "" { - panic(errors.New("'region' cannot be empty")) - } - payload["region"] = region - - if location == "" { - panic(errors.New("'location' cannot be empty")) - } - payload["location"] = location - - if vendorName != "" { - payload["vendor_name"] = vendorName - } - - /**/ - - var dc Datacenter - - // We get a 303 on success - res := d.Do( - d.Sling().New().Post("/dc"). - Set("Content-Type", "application/json"). - BodyJSON(payload), - ) - if ok := res.Parse(&dc); !ok { - panic(res) - } - - return dc -} - -func (d *Datacenters) CreateFromStruct(newDC Datacenter) Datacenter { - return d.Create( - newDC.Region, - newDC.Vendor, - newDC.Location, - newDC.VendorName, - ) -} - -func (d *Datacenters) Delete(id uuid.UUID) { - uri := fmt.Sprintf( - "/dc/%s", - url.PathEscape(id.String()), - ) - - res := d.Do(d.Sling().New().Delete(uri)) - - if res.StatusCode() != 204 { - // I know this is weird. Like in other places, it should be impossible - // to reach here unless the status code is 204. The API returns 204 - // (which gets us here) or 409 (which will explode before it gets here). - // If we got here via some other code, then there's some new behavior - // that we need to know about. - panic(res) - } -} - -/****/ - -func (d *Datacenters) GetRooms(id uuid.UUID) RoomList { - rooms := make(RoomList, 0) - uri := fmt.Sprintf( - "/dc/%s/rooms", - url.PathEscape(id.String()), - ) - - res := d.Do(d.Sling().Get(uri)) - if ok := res.Parse(&rooms); !ok { - panic(res) - } - - return rooms -} - -/****/ - -func init() { - - App.Command("datacenters", "Work with all the datacenters you have access to", func(cmd *cli.Cmd) { - cmd.Before = RequireSysAdmin - cmd.Command("get", "Get a list of all datacenters", func(cmd *cli.Cmd) { - cmd.Action = func() { - fmt.Println(API.Datacenters().GetAll()) - } - }) - - cmd.Command("create", "Create a single datacenter", func(cmd *cli.Cmd) { - var ( - vendorOpt = cmd.StringOpt("vendor", "", "Vendor") - regionOpt = cmd.StringOpt("region", "", "Region") - locationOpt = cmd.StringOpt("location", "", "Location") - vendorNameOpt = cmd.StringOpt("vendor-name", "", "Vendor Name") - ) - - cmd.Spec = "--vendor --region --location [OPTIONS]" - cmd.Action = func() { - // The user can be very silly and supply something like - // '--vendor ""' which will pass the cli lib's requirement - // check but is still crap - if *vendorOpt == "" { - panic(errors.New("--vendor is required")) - } - if *regionOpt == "" { - panic(errors.New("--region is required")) - } - if *locationOpt == "" { - panic(errors.New("--location is required")) - } - - fmt.Println(API.Datacenters().Create( - *regionOpt, - *vendorOpt, - *locationOpt, - *vendorNameOpt, - )) - } - }) - - }) - - App.Command("datacenter", "Deal with a single datacenter", func(cmd *cli.Cmd) { - var datacenterID uuid.UUID - - idArg := cmd.StringArg( - "UUID", - "", - "The UUID of the datacenter. Short UUIDs (first segment) accepted", - ) - - cmd.Spec = "UUID" - - cmd.Before = func() { - RequireSysAdmin() - var ok bool - - if ok, datacenterID = API.Datacenters().FindDatacenterID(*idArg); !ok { - panic(errors.New("could not find the datacenter")) - } - } - - cmd.Command("get", "Information about a single datacenter", func(cmd *cli.Cmd) { - cmd.Action = func() { - fmt.Println(API.Datacenters().Get(datacenterID)) - } - }) - - cmd.Command("delete", "Delete a single datacenter", func(cmd *cli.Cmd) { - cmd.Action = func() { - // Lower layers panic if there's a problem - API.Datacenters().Delete(datacenterID) - - fmt.Println(API.Datacenters().GetAll()) - } - }) - - cmd.Command("update", "Update a single datacenter", func(cmd *cli.Cmd) { - regionOpt := cmd.StringOpt( - "region", - "", - "Region identifier", - ) - vendorOpt := cmd.StringOpt( - "vendor", - "", - "Vendor", - ) - vendorNameOpt := cmd.StringOpt( - "vendor-name", - "", - "Vendor Name", - ) - locationOpt := cmd.StringOpt( - "location", - "", - "Location", - ) - - cmd.Action = func() { - var count int - if *regionOpt != "" { - count++ - } - if *vendorOpt != "" { - count++ - } - if *vendorNameOpt != "" { - count++ - } - if *locationOpt != "" { - count++ - } - - if count == 0 { - panic(errors.New("one option must be provided")) - } - - fmt.Println(API.Datacenters().Update( - datacenterID, - *regionOpt, - *vendorOpt, - *locationOpt, - *vendorNameOpt, - )) - } - }) - - cmd.Command("rooms", "Get the room list for a single datacenter", func(cmd *cli.Cmd) { - cmd.Action = func() { - fmt.Println(API.Datacenters().GetRooms(datacenterID)) - } - }) - }) -} diff --git a/datacenter_integration_test.go b/datacenter_integration_test.go deleted file mode 100644 index f14ef1f..0000000 --- a/datacenter_integration_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package main - -import ( - "testing" -) - -var dc Datacenter - -func TestDatacenterAPIIntegration(t *testing.T) { - setupAPIClient() - r := setupRecorder("fixtures/conch-v3/datacenter") - defer r() // Make sure recorder is stopped once done with it - - t.Run("create", func(t *testing.T) { - - dc = API.Datacenters().Create( - "Atlantis", - "Asguardians", - "Hala", - "", - ) - t.Logf("created %v", dc) - }) - - t.Run("get-all", func(t *testing.T) { - h := API.Datacenters().GetAll() - t.Logf("got %v", h) - }) - - t.Run("get-one", func(t *testing.T) { - h := API.Datacenters().Get(dc.ID) - t.Logf("got %v", h) - }) -} diff --git a/devices.go b/devices.go deleted file mode 100644 index 9aa9c9b..0000000 --- a/devices.go +++ /dev/null @@ -1,766 +0,0 @@ -// Copyright Joyent, Inc. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package main - -import ( - "bytes" - "fmt" - "net/url" - "os" - "regexp" - "sort" - "strings" - "time" - - "github.com/gofrs/uuid" - cli "github.com/jawher/mow.cli" - "github.com/joyent/kosh/tables" - "github.com/joyent/kosh/template" -) - -type Devices struct { - *Conch -} - -func (c *Conch) Devices() *Devices { - return &Devices{c} -} - -/***/ - -type DeviceSettings map[string]interface{} - -func (ds DeviceSettings) String() string { - if API.JsonOnly { - return API.AsJSON(ds) - } - - tableString := &strings.Builder{} - table := tables.NewTable(tableString) - tables.TableToMarkdown(table) - - var keys []string - for key := range ds { - keys = append(keys, key) - } - sort.Strings(keys) - - for _, key := range keys { - value := ds[key] - table.Append([]string{key, value.(string)}) - } - - table.Render() - return tableString.String() -} - -func (d Devices) Setting(id, key string) interface{} { - uri := fmt.Sprintf( - "/device/%s/settings/%s", - url.PathEscape(id), - url.PathEscape(key), - ) - - // The json schema for a DeviceSetting is basically "A DeviceSettings but with only one key" - var settings DeviceSettings - - res := d.Do(d.Sling().New().Get(uri)) - if ok := res.Parse(&settings); !ok { - panic(res) - } - return settings[key] -} - -func (d Devices) Settings(id string) DeviceSettings { - uri := fmt.Sprintf("/device/%s/settings", url.PathEscape(id)) - res := d.Do(d.Sling().New().Get(uri)) - - settings := make(DeviceSettings) - if ok := res.Parse(&settings); !ok { - panic(res) - } - - out := make(DeviceSettings) - re := regexp.MustCompile(`^tag\.`) - for key, value := range settings { - if !re.MatchString(key) { - out[key] = value - } - } - - return out -} - -func (ds *Devices) SetSetting(id, key, value string) { - uri := fmt.Sprintf( - "/device/%s/settings/%s", - url.PathEscape(id), - url.PathEscape(key), - ) - - settings := make(DeviceSettings) - settings[key] = value - - ds.Do( - ds.Sling().New().Post(uri). - Set("Content-Type", "application/json"). - BodyJSON(settings), - ) -} - -func (ds *Devices) DeleteSetting(id, key string) { - uri := fmt.Sprintf( - "/device/%s/settings/%s", - url.PathEscape(id), - url.PathEscape(key), - ) - ds.Do(ds.Sling().New().Delete(uri)) -} - -func (d Devices) Tags(id string) DeviceSettings { - uri := fmt.Sprintf("/device/%s/settings", url.PathEscape(id)) - res := d.Do(d.Sling().New().Get(uri)) - - settings := make(DeviceSettings) - if ok := res.Parse(&settings); !ok { - panic(res) - } - - tags := make(DeviceSettings) - - re := regexp.MustCompile(`^tag\.`) - for key, value := range settings { - if re.MatchString(key) { - tag := strings.TrimPrefix(key, "tag.") - tags[tag] = value - } - } - - return tags -} - -func (d Devices) Tag(id, key string) interface{} { - re := regexp.MustCompile(`^tag\.`) - if !re.MatchString(key) { - key = "tag." + key - } - return d.Setting(id, key) -} - -func (d Devices) SetTag(id, key, value string) { - re := regexp.MustCompile(`^tag\.`) - if !re.MatchString(key) { - key = "tag." + key - } - d.SetSetting(id, key, value) -} - -func (d Devices) DeleteTag(id, key string) { - re := regexp.MustCompile(`^tag\.`) - if !re.MatchString(key) { - key = "tag." + key - } - d.DeleteSetting(id, key) -} - -/***/ - -type DeviceReport map[string]interface{} - -/***/ - -type DeviceLocation struct { - Datacenter Datacenter `json:"datacenter"` - AZ string `json:"az"` - RoomName string `json:"datacenter_room"` - Rack Rack - RackName string `json:"rack"` - RackUnitStart int `json:"rack_unit_start" faker:"rack_unit_start"` - TargetHardwareProduct struct { - ID uuid.UUID `json:"id" faker:"uuid"` - Name string `json:"name"` - Alias string `json:"alias"` - Vendor string `json:"hardware_vendor_id"` - SKU string `json:"sku,omitempty"` - } `json:"target_hardware_product"` -} - -func (dl DeviceLocation) String() string { - if API.JsonOnly { - return API.AsJSON(dl) - } - - t, err := template.NewTemplate().Parse(deviceLocationTemplate) - if err != nil { - panic(err) - } - - buf := new(bytes.Buffer) - - if err := t.Execute(buf, dl); err != nil { - panic(err) - } - - return buf.String() -} - -func (ds *Devices) GetLocation(id string) (l DeviceLocation) { - uri := fmt.Sprintf("/device/%s/location", url.PathEscape(id)) - res := ds.Do(ds.Sling().New().Get(uri)) - if ok := res.Parse(&l); !ok { - panic(res) - } - return l -} - -func (ds *Devices) DeleteLocation(id string) { - uri := fmt.Sprintf("/device/%s/location", url.PathEscape(id)) - - res := ds.Do(ds.Sling().New().Delete(uri)) - if res.IsError() { - panic(res) - } -} - -/***/ - -type DeviceNic struct { - DeviceID uuid.UUID `json:"device_id" faker:"uuid"` - MAC string `json:"mac"` - InterfaceName string `json:"iface_name"` - InterfaceVendor string `json:"iface_vendor"` - State string `json:"state,omitempty"` - IpAddress string `json:"ipaddr,omitempty"` - MTU int `json:"mtu,omitempty"` - - InterfaceType string `json:"iface_type"` -} - -func (dn DeviceNic) String() string { - if API.JsonOnly { - return API.AsJSON(dn) - } - - t, err := template.NewTemplate().Parse(deviceNicTemplate) - if err != nil { - panic(err) - } - - buf := new(bytes.Buffer) - - if err := t.Execute(buf, dn); err != nil { - panic(err) - } - - return buf.String() -} - -// type DeviceNics []DeviceNic - -func (ds *Devices) GetInterface(id, name string) (n DeviceNic) { - uri := fmt.Sprintf( - "/device/%s/interface/%s", - url.PathEscape(id), - url.PathEscape(name), - ) - - res := ds.Do(ds.Sling().New().Get(uri)) - if ok := res.Parse(&n); !ok { - panic(res) - } - return n -} - -func (ds *Devices) GetIPMI(id string) string { - return ds.GetInterface(id, "ipmi1").IpAddress -} - -/***/ - -type deviceCore struct { - ID uuid.UUID `json:"id" faker:"uuid"` - Serial string `json:"serial_number"` - AssetTag string `json:"asset_tag,omitempty" faker:"-"` - Created time.Time `json:"created" faker:"-"` - Updated time.Time `json:"updated" faker:"-"` - LastSeen time.Time `json:"last_seen" faker:"-"` - - HardwareProductID uuid.UUID `json:"hardware_product_id" faker:"uuid"` - Health string `json:"health"` - Hostname string `json:"hostname,omitempty" faker:"-"` - SystemUUID uuid.UUID `json:"system_uuid" faker:"uuid"` - UptimeSince time.Time `json:"uptime_since,omitempty" faker:"-"` - Validated time.Time `json:"validated,omitempty" faker:"-"` - Phase string `json:"phase"` - - BuildID uuid.UUID `json:"build_id" faker:"-"` - BuildName string `json:"build_name"` - SKU string `json:"sku"` -} - -type Disk struct { - ID uuid.UUID `json:"id" faker:"uuid"` - SerialNumber string `json:"serial_number"` - Slot int `json:"slot,omitempty" faker:"-"` - Size int `json:"size,omitempty" faker:"-"` - Vendor string `json:"vendor,omitempty" faker:"-"` - Model string `json:"model,omitempty" faker:"-"` - Firmware string `json:"firmware,omitempty" faker:"-"` - Transport string `json:"transport,omitempty" faker:"-"` - Health string `json:"health,omitempty" faker:"-"` - DriveType string `json:"drive_type,omitempty" faker:"-"` - Enclosure int `json:"enclosure,omitempty" faker:"-"` - Created time.Time `json:"created" faker:"-"` - Updated time.Time `json:"updated" faker:"-"` - HBA interface{} `json:"hba" faker:"-"` // TODO figure out where this belongs -} -type Disks []Disk - -type DetailedDevice struct { - deviceCore - Links []string `json:"links"` - Location DeviceLocation `json:"location,omitempty" faker:"-"` - Nics []struct { - Mac string `json:"mac"` - InterfaceName string `json:"iface_name"` - InterfaceVendor string `json:"iface_vendor"` - InterfaceType string `json:"iface_type"` - PeerMac string `json:"peer_mac,omitempty" faker:"-"` - PeerSwitch string `json:"peer_switch,omitempty" faker:"-"` - PeerPort string `json:"peer_port,omitempty" faker:"-"` - } `json:"nics"` - Disks Disks `json:"disks"` - LatestReport DeviceReport `json:"latest_report,omitempty" faker:"-"` -} - -func (d DetailedDevice) String() string { - if API.JsonOnly { - return API.AsJSON(d) - } - - enclosures := make(map[int]map[int]Disk) - for _, disk := range d.Disks { - enclosure, ok := enclosures[disk.Enclosure] - if !ok { - enclosure = make(map[int]Disk) - } - - if _, ok := enclosure[disk.Slot]; !ok { - enclosure[disk.Slot] = disk - } - - enclosures[disk.Enclosure] = enclosure - } - - var rackRole RackRole - if (d.Location.Rack == Rack{}) { - d.Location.Rack = API.Racks().GetByName(d.Location.RackName) - } - if (d.Location.Rack.RoleID != uuid.UUID{}) { - rackRole = API.RackRoles().Get(d.Location.Rack.RoleID) - } - - var hp HardwareProduct - if (d.HardwareProductID != uuid.UUID{}) { - hp = API.Hardware().GetProduct(d.HardwareProductID) - } - - validations := API.Devices().ValidationState(d.ID.String()) - - extended := struct { - DetailedDevice - RackRole RackRole - HardwareProduct HardwareProduct - Enclosures map[int]map[int]Disk - Validations ValidationStateWithResults - }{d, rackRole, hp, enclosures, validations} - - t, err := template.NewTemplate().Parse(deviceTemplate) - if err != nil { - panic(err) - } - - buf := new(bytes.Buffer) - - if err := t.Execute(buf, extended); err != nil { - panic(err) - } - - return buf.String() -} - -/***/ - -type Device struct { - deviceCore - RackID uuid.UUID `json:"rack_id,omitempty" faker:"-"` - RackUnitStart int `json:"rack_unit_start,omitempty" faker:"rack_unit_start"` -} - -type DeviceList []Device - -func (d DeviceList) Len() int { - return len(d) -} - -func (d DeviceList) Swap(i, j int) { - d[i], d[j] = d[j], d[i] -} - -func (d DeviceList) Less(i, j int) bool { - return d[i].Serial < d[j].Serial -} - -func (d DeviceList) String() string { - sort.Sort(d) - if API.JsonOnly { - return API.AsJSON(d) - } - - tableString := &strings.Builder{} - table := tables.NewTable(tableString) - tables.TableToMarkdown(table) - - table.SetHeader([]string{ - "Serial", - "Hostname", - "Asset Tag", - "Hardware", - "Phase", - "Updated", - "Validated", - }) - - hpCache := make(map[uuid.UUID]HardwareProduct) - - for _, device := range d { - if _, ok := hpCache[device.HardwareProductID]; !ok { - hpCache[device.HardwareProductID] = API.Hardware().GetProduct(device.HardwareProductID) - } - - table.Append([]string{ - device.Serial, - device.Hostname, - device.AssetTag, - hpCache[device.HardwareProductID].Name, - device.Phase, - template.TimeStr(device.Updated), - template.TimeStr(device.Validated), - }) - } - - table.Render() - return tableString.String() -} - -// id is a string because the API accepts both a UUID and a serial number -func (ds *Devices) Get(id string) (d DetailedDevice) { - uri := fmt.Sprintf("/device/%s", url.PathEscape(id)) - res := ds.Do(ds.Sling().New().Get(uri)) - if ok := res.Parse(&d); !ok { - panic(res) - } - return d -} - -func (ds *Devices) FindByField(key, value string) DeviceList { - uri := fmt.Sprintf( - "/device?%s=%s", - url.PathEscape(key), - url.PathEscape(value), - ) - d := make(DeviceList, 0) - - res := ds.Do(ds.Sling().New().Get(uri)) - if ok := res.Parse(&d); !ok { - panic(res) - } - return d -} - -func (ds *Devices) FindBySetting(key, value string) DeviceList { - return ds.FindByField(key, value) -} - -func (ds *Devices) FindByTag(key, value string) DeviceList { - return ds.FindByField("tag."+key, value) -} - -/***/ - -// id is a string because the API accepts both a UUID and a serial number -func (ds *Devices) ValidationState(id string) (v ValidationStateWithResults) { - uri := fmt.Sprintf("/device/%s/validation_state", url.PathEscape(id)) - res := ds.DoBadly(ds.Sling().New().Get(uri)) - if res.StatusCode() == 404 { - return v - } - if ok := res.Parse(&v); !ok { - panic(res) - } - return v -} - -/***/ - -func (ds *Devices) GetPhase(id string) string { - data := struct { - ID uuid.UUID `json:"id"` - Phase string `json:"phase"` - }{} - - uri := fmt.Sprintf("/device/%s/phase", url.PathEscape(id)) - res := ds.Do(ds.Sling().New().Get(uri)) - if ok := res.Parse(&data); !ok { - panic(res) - } - return data.Phase -} - -func (ds *Devices) SetPhase(id, phase string) string { - uri := fmt.Sprintf("/device/%s/phase", url.PathEscape(id)) - - payload := make(map[string]string) - payload["id"] = id - payload["phase"] = phase - - ds.Do( - ds.Sling().New().Post(uri). - Set("Content-Type", "application/json"). - BodyJSON(payload), - ) - - return ds.GetPhase(id) -} - -/***/ - -var HealthList = []string{"error", "fail", "unknown", "pass"} - -func prettyDeviceHealthList() string { - return strings.Join(HealthList, ", ") -} - -func okHealth(health string) bool { - for _, b := range HealthList { - if health == b { - return true - } - } - return false -} - -/***/ -var PhasesList = []string{"integration", "installation", "production", "diagnostics", "decommissioned"} - -func prettyPhasesList() string { - return strings.Join(PhasesList, ", ") -} - -func okPhase(phase string) bool { - for _, b := range PhasesList { - if phase == b { - return true - } - } - return false -} - -/***/ - -func init() { - App.Command("devices ds", "Commands for dealing with multiple devices", func(cmd *cli.Cmd) { - cmd.Command("search s", "Search for devices", func(cmd *cli.Cmd) { - - cmd.Command("setting", "Search for devices by exact setting value", func(cmd *cli.Cmd) { - var ( - keyArg = cmd.StringArg("KEY", "", "Setting name") - valueArg = cmd.StringArg("VALUE", "", "Setting Value") - ) - cmd.Spec = "KEY VALUE" - - cmd.Action = func() { - fmt.Println(API.Devices().FindBySetting(*keyArg, *valueArg)) - } - }) - - cmd.Command("tag", "Search for devices by exact tag value", func(cmd *cli.Cmd) { - var ( - keyArg = cmd.StringArg("KEY", "", "Tag name") - valueArg = cmd.StringArg("VALUE", "", "Tag Value") - ) - cmd.Spec = "KEY VALUE" - - cmd.Action = func() { - fmt.Println(API.Devices().FindByTag(*keyArg, *valueArg)) - } - }) - - cmd.Command("hostname", "Search for devices by exact hostname", func(cmd *cli.Cmd) { - var ( - hostnameArg = cmd.StringArg("HOSTNAME", "", "hostname") - ) - cmd.Spec = "HOSTNAME" - - cmd.Action = func() { - fmt.Println(API.Devices().FindByField("hostname", *hostnameArg)) - } - }) - }) - }, - ) - - App.Command("device", "Perform actions against a single device", func(cmd *cli.Cmd) { - idArg := cmd.StringArg( - "DEVICE", - "", - "UUID or serial number of the device. Short UUIDs are *not* accepted", - ) - - cmd.Spec = "DEVICE" - - cmd.Command("get", "Get information about a single device", func(cmd *cli.Cmd) { - cmd.Action = func() { fmt.Println(API.Devices().Get(*idArg)) } - }) - - cmd.Command("validations", "Get the most recent validation results for a single device", func(cmd *cli.Cmd) { - cmd.Action = func() { fmt.Println(API.Devices().ValidationState(*idArg)) } - }) - - cmd.Command("settings", "See all settings for a device", func(cmd *cli.Cmd) { - cmd.Action = func() { fmt.Println(API.Devices().Settings(*idArg)) } - }) - - cmd.Command("setting", "See a single setting for a device", func(cmd *cli.Cmd) { - keyArg := cmd.StringArg( - "NAME", - "", - "Name of the setting", - ) - - cmd.Spec = "NAME" - - cmd.Action = func() { - fmt.Println(API.Devices().Setting(*idArg, *keyArg)) - } - - cmd.Command("get", "Get a particular device setting", func(cmd *cli.Cmd) { - cmd.Action = func() { - fmt.Println(API.Devices().Setting(*idArg, *keyArg)) - } - }) - - cmd.Command("set", "Set a particular device setting", func(cmd *cli.Cmd) { - valueArg := cmd.StringArg("VALUE", "", "Value of the setting") - cmd.Spec = "VALUE" - - cmd.Action = func() { - API.Devices().SetSetting(*idArg, *keyArg, *valueArg) - fmt.Println(API.Devices().Settings(*idArg)) - } - }) - - cmd.Command("delete rm", "Delete a particular device setting", func(cmd *cli.Cmd) { - cmd.Action = func() { - API.Devices().DeleteSetting(*idArg, *keyArg) - fmt.Println(API.Devices().Settings(*idArg)) - } - }) - }) - - cmd.Command("tags", "See all tags for a device", func(cmd *cli.Cmd) { - cmd.Action = func() { fmt.Println(API.Devices().Tags(*idArg)) } - }) - cmd.Command("tag", "See a single tag for a device", func(cmd *cli.Cmd) { - nameArg := cmd.StringArg("NAME", "", "Name of the tag") - - cmd.Spec = "NAME" - - cmd.Action = func() { - fmt.Println(API.Devices().Tag(*idArg, *nameArg)) - } - - cmd.Command("get", "Get a particular device tag", func(cmd *cli.Cmd) { - cmd.Action = func() { - fmt.Println(API.Devices().Tag(*idArg, *nameArg)) - } - }) - - cmd.Command("set", "Set a particular device tag", func(cmd *cli.Cmd) { - valueArg := cmd.StringArg("VALUE", "", "Value of the tag") - cmd.Spec = "VALUE" - - cmd.Action = func() { - API.Devices().SetTag(*idArg, *nameArg, *valueArg) - fmt.Println(API.Devices().Tags(*idArg)) - } - }) - - cmd.Command("delete rm", "Delete a particular device tag", func(cmd *cli.Cmd) { - cmd.Action = func() { - API.Devices().DeleteTag(*idArg, *nameArg) - fmt.Println(API.Devices().Tags(*idArg)) - } - }) - }) - - cmd.Command("interface", "Information about a single interface", func(cmd *cli.Cmd) { - nameArg := cmd.StringArg("NAME", "", "Name of the interface") - cmd.Spec = "NAME" - cmd.Action = func() { fmt.Println(API.Devices().GetInterface(*idArg, *nameArg)) } - }) - - cmd.Command("preflight", "Data that is only accurate inside preflight", func(cmd *cli.Cmd) { - cmd.Before = func() { - if API.Devices().GetPhase(*idArg) != "integration" { - os.Stderr.WriteString("Warning: This device is no longer in the 'integration' phase. This data is likely to be inaccurate\n") - } - } - - cmd.Command("location", "The location of a device in preflight", func(cmd *cli.Cmd) { - cmd.Action = func() { fmt.Println(API.Devices().GetLocation(*idArg)) } - }) - - cmd.Command("ipmi", "IPMI address for a device in preflight", func(cmd *cli.Cmd) { - cmd.Action = func() { fmt.Println(API.Devices().GetIPMI(*idArg)) } - }) - }) - - cmd.Command("phase", "Actions on the lifecycle phase of the device", func(cmd *cli.Cmd) { - cmd.Command("get", "Get the phase of the device", func(cmd *cli.Cmd) { - cmd.Action = func() { fmt.Println(API.Devices().GetPhase(*idArg)) } - }) - - cmd.Command("set", "Set the phase of the device [one of: "+prettyPhasesList()+"]", func(cmd *cli.Cmd) { - phaseArg := cmd.StringArg("PHASE", "", "Name of the phase [one of: "+prettyPhasesList()+"]") - cmd.Spec = "PHASE" - cmd.Action = func() { - if !okPhase(*phaseArg) { - panic("Phase must be one of: " + prettyPhasesList()) - } - - fmt.Println(API.Devices().SetPhase(*idArg, *phaseArg)) - } - }) - }) - - cmd.Command("validations", "Information about the latest validation runs", func(cmd *cli.Cmd) { - cmd.Action = func() { fmt.Println(API.Devices().ValidationState(*idArg)) } - }) - - cmd.Command("report", "Get the most recently recorded report for this device", func(cmd *cli.Cmd) { - cmd.Action = func() { - d := API.Devices().Get(*idArg) - if d.LatestReport == nil { - fmt.Println("{}") - return - } - API.PrintJSON(d.LatestReport) - } - }) - }) -} diff --git a/devices_integration_test.go b/devices_integration_test.go deleted file mode 100644 index 8709376..0000000 --- a/devices_integration_test.go +++ /dev/null @@ -1,101 +0,0 @@ -package main - -import ( - "testing" -) - -const TestSetting = "test_setting" - -func TestDeviceAPIIntegration(t *testing.T) { - defer errorHandler() - - setupAPIClient() - - r := setupRecorder("fixtures/conch-v3/device") - defer r() // Make sure recorder is stopped once done with it - - f := newFixture() - f.setupRackLayout() - defer f.reset() - - var testDevice DetailedDevice - - t.Run("create a device by adding it to a build with a sku", func(t *testing.T) { - defer errorHandler() - - mock := newTestDevice() - mock.Serial = "AAAAAA" - API.Builds().CreateDevice(f.build.ID, mock.Serial, f.switchProduct.SKU) - testDevice = API.Devices().Get(mock.Serial) - }) - - t.Run("set and fetching a device setting", func(t *testing.T) { - defer errorHandler() - - API.Devices().SetSetting(testDevice.ID.String(), TestSetting, "foo") - s := API.Devices().Setting(testDevice.ID.String(), TestSetting) - t.Logf("got %v", s) - }) - - t.Run("fetching all device settings", func(t *testing.T) { - defer errorHandler() - - s := API.Devices().Settings(testDevice.ID.String()) - t.Logf("got %v", s) - }) - - t.Run("fetch device tags", func(t *testing.T) { - - s := API.Devices().Tags(testDevice.ID.String()) - t.Logf("got %v", s) - }) - - t.Run("set and fetch device location", func(t *testing.T) { - defer errorHandler() - - _ = API.Racks().UpdateAssignments( - f.rack.ID, - RackAssignmentUpdates{{testDevice.ID, "", 1}}, - ) - s := API.Devices().GetLocation(testDevice.ID.String()) - t.Logf("got %v", s) - }) - - t.Run("fetch getting a device", func(t *testing.T) { - defer errorHandler() - - s := API.Devices().Get(testDevice.ID.String()) - t.Logf("got %v", s) - }) - - t.Run("fetch device validation state", func(t *testing.T) { - defer errorHandler() - - s := API.Devices().ValidationState(testDevice.ID.String()) - t.Logf("got %v", s) - }) - - t.Run("fetch device phase", func(t *testing.T) { - - s := API.Devices().GetPhase(testDevice.ID.String()) - t.Logf("got %v", s) - }) - - // GetInterface - t.Run("fetch device interface", func(t *testing.T) { - // TODO: sort out device report submission - /* - defer errorHandler() - - s := API.Devices().GetIPMI(testDevice.ID.String()) - t.Logf("got %v", s) - */ - }) - - t.Run("remove device", func(t *testing.T) { - defer errorHandler() - - // API.Builds().RemoveDevice(f.build.ID, testDevice.ID.String()) - API.Devices().DeleteLocation(testDevice.ID.String()) - }) -} diff --git a/errors.go b/errors.go deleted file mode 100644 index 89c9f56..0000000 --- a/errors.go +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright Joyent, Inc. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package main - -//lint:file-ignore U1000 WIP - -import ( - "encoding/json" - "fmt" - "net/http/httputil" - "os" - "reflect" - "runtime/debug" - - "github.com/davecgh/go-spew/spew" -) - -func errorHandler() { - if r := recover(); r != nil { - if API.DevelMode { - - if reflect.TypeOf(r).String() == "*main.ConchResponse" { - res := r.(*ConchResponse) - - reqDump, err := httputil.DumpRequest(res.Request, true) - if err != nil { - fmt.Println(err) - } - fmt.Fprintf(os.Stderr, - "HTTP Request: %s\n\n", - reqDump, - ) - - fmt.Fprintf(os.Stderr, - "HTTP Status Code: %d\nHTTP Status: %s\nString Error: %s\n\n", - res.StatusCode(), - res.Status(), - res.Error.Error(), - ) - - fmt.Fprintf( - os.Stderr, - "RAW HTTP RESPONSE:\n%s\n\n", - res.Body, - ) - - var s interface{} - if err := json.Unmarshal([]byte(res.Body), &s); err == nil { - fmt.Fprintln(os.Stderr, "MARSHALLED RESPONSE:") - spew.Fdump(os.Stderr, s) - } - - } else { - fmt.Fprintf(os.Stderr, "ERROR: %v\n\n", r) - fmt.Fprintf(os.Stderr, "RAW ERROR: ") - spew.Fdump(os.Stderr, r) - } - - fmt.Fprintln(os.Stderr) - debug.PrintStack() - os.Exit(1) - } - - var msg string - if reflect.TypeOf(r).String() == "*main.ConchResponse" { - res := r.(*ConchResponse) - if res.Error != nil { - msg = res.Error.Error() - } else { - msg = fmt.Sprintf("An HTTP error occured: %s", res.Status()) - } - } else { - msg = fmt.Sprintf("An error occurred: %s", r) - } - - if API.JsonOnly { - fmt.Fprintf(os.Stderr, "{\"error\":\"%s\"}\n", msg) - } else { - fmt.Fprintln(os.Stderr, msg) - } - - os.Exit(1) - } -} - -func Spew(d interface{}) { - spew.Fdump(os.Stderr, d) -} - -func init() { - spew.Config = spew.ConfigState{ - Indent: " ", - SortKeys: true, - DisablePointerAddresses: true, - DisableMethods: true, - DisableCapacities: true, - DisablePointerMethods: true, - SpewKeys: true, - } -} diff --git a/fixtures/conch-v3/builds.yaml b/fixtures/conch-v3/builds.yaml deleted file mode 100644 index 617b0a2..0000000 --- a/fixtures/conch-v3/builds.yaml +++ /dev/null @@ -1,128 +0,0 @@ ---- -version: 1 -interactions: -- request: - body: | - {"admins":[{"email":"conch@example.com"}],"description":"rbiZdTbPxWgYZIdzBlSOWhQHu","name":"hcGWnWqXTHvSdRUatQgYixRob"} - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/build - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Mon, 13 Jan 2020 23:16:19 GMT - Location: - - /build/80b13c12-be16-47e3-a058-61b445ed65f1 - Request-Id: - - QXNsOP8yGRXc - Server: - - Mojolicious (Perl) - X-Request-Id: - - QXNsOP8yGRXc - status: 303 See Other - code: 303 - duration: "" -- request: - body: "" - form: {} - headers: - Content-Type: - - application/json - Referer: - - http://10.51.54.42:5000/build - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/build/80b13c12-be16-47e3-a058-61b445ed65f1 - method: GET - response: - body: '{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T23:16:19.472652Z","description":"rbiZdTbPxWgYZIdzBlSOWhQHu","id":"80b13c12-be16-47e3-a058-61b445ed65f1","name":"hcGWnWqXTHvSdRUatQgYixRob","started":null}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "315" - Content-Type: - - application/json - Date: - - Mon, 13 Jan 2020 23:16:19 GMT - Request-Id: - - M1EmEesZ8HxZ - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - M1EmEesZ8HxZ - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/build - method: GET - response: - body: '[{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:36:53.702266Z","description":"DQZvExFeJFTsgakhAMzkRCBZG","id":"231af6d2-d3c1-419d-a7f5-12a423e10f10","name":"AEWjxjVdLaxqKXxwCryfmEiHU","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T23:10:57.843687Z","description":"oSrpSWYLDvXrRpgyiMxsJemZs","id":"53cf4f03-231f-4b89-865e-96c7f4bf2daa","name":"AJZVijpgbpeFlGMaemgMVshTZ","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T04:44:50.575787Z","description":"DimdNbTKQpsdYzhKHqJLusJBQ","id":"31256189-b9c6-460e-a611-97026dac3e69","name":"AJtNQrTykxScaLuXGZhHpvreG","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T04:49:52.537501Z","description":"xhzrIXrFWvTLKePuaLjCDsNBB","id":"f64f8f99-54e2-4e19-8a9e-06e9712942cb","name":"ANJyGMIhrVKmVKeGDAFXsxjtz","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T02:54:25.064251Z","description":"mZCpmMHhEvlfbdMZUPHZNmxwF","id":"06d0abc8-2c76-436a-9f06-aaddfee00dfd","name":"AcuCGafgLYhWYJVUUBlRORrFi","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-17T17:37:08.862189Z","description":"NDjEelNOTKwNzQzPhZdcpKXox","id":"a00452c7-eb88-4a56-bbec-17c13eca7be8","name":"AiFlXsNrVvbUNcBVNNvNRGovb","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-30T18:10:33.039321Z","description":"LzqAkdUEEeEJcNfCnGorEBSPW","id":"9b55f8bb-3059-41cd-8aeb-ef429c5301c5","name":"AtlIooxIBczMrOOjeyCaMmtAl","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T04:29:58.488541Z","description":"ZxDULbhoUJPKCKWAixMtStFqF","id":"3c462bb3-e997-4dcd-9873-e70149783359","name":"AxlVlHcWXXlgvpZirjEtaPbYo","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T21:26:06.370390Z","description":"EhNuwYYbFoFrCKFDvIWliWgKn","id":"ad5b66ac-deec-44de-a6a3-b61b09b6baa7","name":"BMnCrQLLJiBlkRmGCiYPuvUSw","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:27:46.731358Z","description":"NwTFckEMVdAjOEgiopqxEZnBe","id":"fe6c1cd6-0a7d-4113-acb5-8d63cfe0bf52","name":"BsEnJCvFoBVQecwfcbwnyZiUg","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T04:52:33.491547Z","description":"LZTGpfyiStoYqJJMRspShnzFr","id":"8697d4aa-0da3-46be-9f05-b6b5f169fdec","name":"BtsaNauzNNUNpqtcmBaWqPwDn","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T04:47:52.833392Z","description":"fuUreMDFkaLUClVtRfXeAPmAD","id":"906d6be9-14d7-4576-8789-67822c6abf0a","name":"CBSYGJqtREpncOZkGrmHYknnM","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-17T16:23:35.821243Z","description":"uiXWmSqOWqsMcSdgfpgJRXTyB","id":"e00ba54a-e5e9-49d0-a884-c46460c474bc","name":"CGqELNgPkGeLyMoAbEoESSlFi","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-17T17:38:37.680083Z","description":"bVNCVccPemEKLIRauWJiXbMfi","id":"76e8b039-7382-4b54-9d1b-b6537a9f44bf","name":"CKkaFTMfVySTBhxxtKgfyvZae","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T23:01:04.219927Z","description":"NMHjtHzWKytoVDxziaSSmRqnw","id":"28b49a29-2fb9-411d-b765-bb7b7f203780","name":"CNopqErVAsoQyYURoBioTInbw","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-30T18:08:48.376205Z","description":"iMvxReofXJHlrKKvgobXpJCZq","id":"9d8b52be-7388-482b-86be-34837ea6ae9d","name":"CdcwsXpLBWnIAjuauvsxGUjeu","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-11T04:41:23.748495Z","description":"FPwLHQalhWamqUKfAigKBvSDy","id":"2bbb536e-3b8c-437e-9609-07dfa10a3218","name":"CvNrOSiVYNrDdBhaisDWWSETQ","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:28:26.113035Z","description":"JDUUHJPTfHDgmBuvrFOsSkZIs","id":"a7bb48e9-6416-49a6-a62d-b5e9c0b92f7f","name":"CwtQKmSnxjDERqphbArkpAlFx","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-17T15:20:08.885358Z","description":"EDCDJpmsjKuZDuxUwlmMeIOkk","id":"9892a54b-7959-45b8-b92c-3578962be9f6","name":"CygKljBITFxApkIyEWqcAYXjj","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:38:35.925355Z","description":"yfsibEbSgEwRcJpLXdFpqILDn","id":"49be08c9-1180-467e-8b57-361637dd2f2d","name":"DFPmZSrsIcOkvFsTCdClkKHdi","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T21:26:05.118644Z","description":"qaRjwStXmVGGxxVtIHPnRTAzV","id":"d6517f60-990d-44ba-9d19-f395fab251ed","name":"DJwGlYWkodiWeuDoBJtzGAXeJ","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:26:12.054636Z","description":"QgXkXZrizMmTMTpNHzDqfcjXC","id":"942bcbd3-3ba5-413d-86f4-46d3743c5377","name":"DREqDqWLYvwghksSfwBuiiUrZ","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:42:53.154625Z","description":"jTqhNPdADtwTpqAUdSxStwSJU","id":"6ded614f-09e8-4dfd-900d-15e7b926a74c","name":"DRrJGaJxdYAzVKfoKrfBffbON","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-30T18:07:45.766191Z","description":"ymVleiieNYRCioGLtQULVYvwR","id":"2ada5980-edaf-4a09-a50f-165c30974e66","name":"DSyxubaCWzEflFZcoEsdLSTba","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-09T20:05:04.125065Z","description":"TGrvGYyfDuSZvLKSJwbahBUrG","id":"8dedd685-ded8-428b-8873-baf09ef57339","name":"DpVhJDBsbRgIXxzkWwovVMwwW","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T16:27:40.851657Z","description":"QpFUtSxSjTzFjSplfJVMYsNwX","id":"bc54e340-56a8-4a9a-9cfd-ee6cbb3a7047","name":"DqjCeAeKUEqeBJBEMQdClmkdn","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:34:17.040660Z","description":"EeSxKgkCMqNIFhCyWBsGxOSLm","id":"ec2f3456-895b-4255-a77e-9b8624e22e97","name":"DtMcbCRGRpfhGtDodUgwpxIcQ","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T15:11:27.867576Z","description":"ngxBPihwujmGltOeyJgnZfwHY","id":"0638dcf6-f7c7-41e7-8f20-70601c47b6f9","name":"DuhzDuzfjwDovEYNxGlsROwSq","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T14:21:42.571821Z","description":"iRziNNCroLZrTRcRvtkAPJVuS","id":"d40cc497-4f85-44fc-8a42-ad3cd5bc5b55","name":"DylzfidisJYAFRiPFjjyPjLhm","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T16:45:09.442170Z","description":"jGEyERivMptYfKAqQjMZFjKik","id":"9625a62a-480e-4afb-b8e8-d517503ae275","name":"EBeEBcVipnaDqWCPkoyVWpMZM","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:35:03.409213Z","description":"vlvkKqaUHnHkyoDelScUnFair","id":"54f9c064-00d2-473f-82f6-665b9d59b31a","name":"EMJguaMEPkqXQINKCEkAealFS","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-30T20:09:47.071282Z","description":"PFCJFXRjjYPsUrBicuTboCTYg","id":"6966f93d-11c8-427a-ad09-367dd9cd8574","name":"EPpjzWvcjhuRUbccRKCEtEVxR","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-17T15:20:21.496796Z","description":"aeqVJSFTpaVgBBBcotbzqzIAz","id":"5ada8092-5e69-4e31-8b12-1c9b6b7a3f61","name":"EWWwhgCECTlMUUFNcnziOqEca","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T04:51:30.288907Z","description":"sLsUGoIsiRJYnaXPOUfSDJtcU","id":"e0b1079e-7256-4d69-9ee6-c888128a9564","name":"EaJUImfFQVinWELSEMVgFFjOn","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T02:10:09.023823Z","description":"TjTdTXPioOPDUXcpnyYTcsKGI","id":"72f0ed96-7386-4985-a77d-3316791841dc","name":"EalxkSBtIxABVREcEqckoxvbo","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:38:45.335153Z","description":"KWLuzZwGLCmqFDDCgEsNUmBZE","id":"cfcb4227-fbc6-4d86-8190-8ac8cf7fb33b","name":"EqoDwKJzjRLyLyVkkrdtWDtQH","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:15:51.267029Z","description":"dlBEOrLfilmdFlBWcFDZDLUDL","id":"150fb4c5-7fee-4e5e-b021-2ef5cf93ac26","name":"FHunfLWnaIudGfBGjAuSYXKLP","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:32:38.207811Z","description":"vVaiOffZKYNPKnMAtrLplXIJO","id":"4ca1a0cb-be08-4f27-ae28-f11558d4e262","name":"FRXWnxvbJIxVNcLxxIJnQZlqv","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T21:29:19.711897Z","description":"qAuNGnEwtjBkSYcvkdJftpPnO","id":"a219dbeb-722c-4907-8515-278cf1ff18b6","name":"FUCZWooWZwogtBsFoEadSFqfr","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T18:58:52.178664Z","description":"VlnvxfsMcGYLEFLjmiqzXMges","id":"3e50a331-4a06-42f4-8ce1-babee7e6141e","name":"FbpZsGsLfBZqoFjDBwWCuLlxC","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-30T20:12:58.520968Z","description":"HTClDxEkbrxFUDprcyKVuBcEP","id":"0095aba2-3db2-433c-a3d1-3b9bd987229d","name":"FcPphMPNxbSyAiWZnaoJVyZtH","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-09T22:52:05.760222Z","description":"vtyYuINtlhpWHguvRJlMMVMlQ","id":"e3d1102d-0450-43ec-a4f7-993ce768b4b3","name":"FhmWsqvSEWkpGXqahkyiLYpKl","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T04:51:29.619267Z","description":"XcdaHwZitxdzcMwbUerrgJiBa","id":"e76c6f68-4b79-43f1-a2fe-cebd0ce67fea","name":"FkDdQtmRnjzEFKCYAwhCmLdBt","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T17:21:19.407996Z","description":"AVWRKPUzohSNfWEPjwPBvNZDp","id":"d51aed6a-2a9e-4ab4-b1bf-1faabb3e1840","name":"FmMnNEWyULmtYcwGiprXNVetx","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:42:51.924249Z","description":"XsjlROujVLoghdHMwrkBmGnor","id":"b63d0bf2-970b-4c94-9301-4c6530cc537b","name":"FrHXJIdHiAwAJQtrhworxaVgp","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T16:41:54.412774Z","description":"HQlBlxUjnWBQLWZLfSQrvWXlK","id":"fa315b12-e272-4ed0-89a5-c0892ba78c54","name":"FvPfbOpTCXjMeDkuvaOIwfwbA","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T02:57:48.435844Z","description":"qOOsrmRtEwsqrbvpcHTcMoiSV","id":"264063d9-b49d-4406-a19b-6eedfdba3965","name":"FxivjhdyzWbnhmgHqXnZzqwDF","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T21:08:23.055867Z","description":"WUCWRowFemqIROcigOgaGMxSW","id":"b5eebe32-3358-4b52-82ea-e4bad0c0e2c1","name":"GEhwPBGNROededOrFNHrpbPSS","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:29:44.039630Z","description":"hGWgMriZnfKrODREDqWBmQNFE","id":"8cdda014-442c-4d30-83b7-b0f4a7e2fe14","name":"GEpzeVIARdFCdwCzUFQOJzODs","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T14:06:30.676223Z","description":"YjSunvwRyvWIEvMTcLLfXsmtO","id":"0cba0e75-d0ad-4847-a5fe-4b1bc9b37f4c","name":"GNTRYAAzgXDZquEPDClbcOXRa","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:32:28.730885Z","description":"bWLvkiqfEySAyhqVSCmxTrHUF","id":"fb5977a2-250d-4c9e-8fea-c77088c5f5d6","name":"GPlgEedPBUHLFWiLGcrNGjzoF","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T04:55:10.645371Z","description":"uhlxkVKVsuYedEXYzoLFFtUrA","id":"722eaadf-b175-4c63-bae0-ff7373b51740","name":"GRykVOUsFncIuLUaqQkGddqgT","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T17:25:11.823491Z","description":"MMbdMxJyNDVTbMMTdgYDuYQFy","id":"cb74d3d0-dce2-42de-ab5c-d323398c9cf3","name":"GSmZwcRbuRpWzOsXxvfXQdOhJ","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:02:05.894024Z","description":"MQJwsWMDPgFXYYyhZkQqwdUnr","id":"cbcac233-feef-425b-9d97-f55f749b7411","name":"GTEQutifouGcrOmPMNOAmDsVK","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:40:27.751781Z","description":"OmmfXNrBIJGaANgCtyjPaAJNA","id":"c68fe269-4e8d-451e-b9a8-ef0d12b95951","name":"GZVHsDdEmSiYLPaCiBsfpWoRg","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T04:46:23.319803Z","description":"kYHNOZVbJMupgXMVerLeBGqwq","id":"e5e841de-f5ff-4748-96fb-2cd9994a15ed","name":"GZaxesBbJfQYWugBwgzUOwTvk","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:03:24.220843Z","description":"rZLVzpJfYUCvoVICvswEuQhtc","id":"57031909-4eb1-4208-93c6-a4d4ef6a214c","name":"GrOdjQzIgAzlQSkslLSTEJtts","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-30T18:13:42.832872Z","description":"ZhJdcRxHJIBUFGYtJgvhOlwkZ","id":"4bd8722a-29ec-4908-b263-851cdc537faa","name":"GvQJATlPFwXWhnvpxpTySmado","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:23:27.997800Z","description":"QwszojkWqwjoywZaxIgoruSyB","id":"f6d3a7cc-4ac7-47d3-bfe9-47d4b01d7791","name":"GwvCMQjqieLpyiolJWypzvXWG","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:32:11.729241Z","description":"GJLBHZjhkWBECwCkssGKPKEQe","id":"87c83e79-e82a-4b7d-ae21-b0e6175ab3f6","name":"GyLtILvAqirDorBlGRVPfmvtr","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T16:43:28.217653Z","description":"HuPRNtfCsHVOHwgFJNvqKtvVh","id":"87a0ac01-da8d-48ab-a963-4573fea9b0ac","name":"HAEDYUHRPFUCrTOZyymXJjBPl","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:36:55.042694Z","description":"ovwuKGfUKYxKoEBGzLhgMvnmh","id":"3e48e780-2051-4511-8cb8-92bd267a0bca","name":"HEMgoQgHtKSxEsxdiJzcyxgSm","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T16:37:06.051616Z","description":"PoCRwLPLxxxplBybaCBqpXLfA","id":"9cb39deb-7d0f-49d9-9c2b-03cee2815e1e","name":"HGXIvDWGIRVXvceeXlBhmjgox","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T14:20:15.465662Z","description":"NVivTcNrmonXXuIuMbHAqgeXs","id":"35488e11-e925-4fa4-b670-21b7556404d5","name":"HHfENaGQVDxynunTNRlWplUNu","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:55:03.510769Z","description":"IDvNNLwhBQiOdxqZcKbXtanrT","id":"52d22862-92a7-47ae-8180-ae8488ef1475","name":"HHtDFpkAeIhbzzXCjFkOtZImm","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:32:36.861042Z","description":"TVGXqOJtFxkmQpmutGFVjUqmu","id":"a519f6e3-6807-4450-9f08-fdac81c148c9","name":"HISYoIuBhBHFoGpgtMLdCvOxZ","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T16:41:55.288924Z","description":"mjCHopguLtVxrhiHHXfAHWPno","id":"d56bba52-45bb-4706-a2f7-cb10c4fc7384","name":"HLmUbMndPmfutVKJzzaHUmLYn","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:39:05.283429Z","description":"KHfEzKkUXzeVogvwYfQMSZxex","id":"324f60a1-7f43-401e-8092-9c652444cd17","name":"HRgECNKoHCqgeFHAXZUxbCYPf","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T23:13:03.994699Z","description":"CqRJtUhBahDkCOmCWDRPQABKk","id":"a6445d9f-f903-46fb-bcee-6578d2920421","name":"HYuGosPpTjrSsNepDEOVdQVjX","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T16:49:06.890589Z","description":"NcgGukFaXkittOTOxCEHJfLRn","id":"362f796e-11d0-4a86-a5c7-7d4a489f0dbf","name":"HcINnJsDzOycRKybaSkHpUdWM","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T16:27:41.785859Z","description":"ElxlWoZyFbxrbAOxycopXTbZA","id":"42272ed1-6792-441f-bfde-dda14f64e263","name":"HeCwAcJeihlazbMOlqzkOKPXN","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T16:37:05.274264Z","description":"gTkisociTHbIjvJezIWWqbMld","id":"12f7d798-4ac9-46f2-ab3b-46d39b2cf896","name":"HoBgqyrDoKzCdgbdalYNyfJfZ","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:05:57.412484Z","description":"hvWUSeBCAKqRwNjPuQxJQUcqw","id":"8aeb460d-010c-44b3-8589-3a1d2166b815","name":"HpHqimBvuwVJHmqKAkHQaoncI","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:32:10.481831Z","description":"MaZyeGAdotwSBdewHAJVsaWkp","id":"b7dedec5-8a69-41f5-ad6b-a8faedaa7547","name":"HpxufxGebJvJKurDiuWcLlhZz","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T23:01:00.826243Z","description":"UbtlRDEmkUcIIPdCEtTEVqoSS","id":"cf46bc7a-ce7b-43ca-beff-0cce6c7ad680","name":"IBJtNvlZrzabsSGXoqjBfleIh","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T15:11:40.208832Z","description":"ZgIepWqxKUUZrrRVpsEciDelr","id":"af8a143c-efbd-4954-b012-4164328b0a83","name":"IJNwXiUKfhBPdCLylryZRZlDj","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T04:50:04.471842Z","description":"hWBwMiWBXhHBnlkhADRbGPulq","id":"f957051e-f997-45ee-a93a-c8a040304e13","name":"IYOAGAiYtlFPhFnOVTjvsyXBm","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:19:31.920354Z","description":"DzcsCVaROYtsQImWgkXCjvYEd","id":"49fc7888-d7c2-4f9e-a3f4-397cfa54669c","name":"IalPnrLvVnjmMYBHwpXgxrcMz","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T02:45:44.666394Z","description":"gZIhVsPRbVhYFVBXbLPCohLgW","id":"8ccf3485-3104-4820-a844-593939f8c05e","name":"IcbRCjoZAUreUfaWllAPNVYHF","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:01:55.706476Z","description":"WenAFuVJMGTGkhwVdYJlSlAjj","id":"e74d1655-d97e-4094-a2b0-a395476474c2","name":"IlhgyRMmbfkWAdlIFjsbjZCvb","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T23:11:49.179223Z","description":"umvBvmzCoQaOZNLDSpVWxMqEJ","id":"a4fa58ba-8c14-4f6d-8382-2c6736871493","name":"IoKFkRXOWGnUcxpzhALoOzmzh","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T14:14:14.153750Z","description":"udPlUQTiTTlYGPbKysepIgNDr","id":"9e77a7e3-e31a-4a62-bb9d-08b03a761a75","name":"ItorYlffourZdQuGrfpVOlxfO","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-30T20:21:29.178998Z","description":"EfrlKMsoWLTWqAyGEteVZRnca","id":"e3b95a13-0e8c-451b-a341-a346218ffe57","name":"IuhIEeaFXsKfVZihogBfpyFIe","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T04:36:52.365234Z","description":"PqbWFIyqIDHtsxIEvXxIBNkIX","id":"3f6494d5-d749-4707-a47b-5914f468c127","name":"JTqmJVUcwlRsSPldzqCCwdTvH","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T17:25:40.783240Z","description":"IWaDcaOmgMZMIamGwminuvYYj","id":"dd642cfc-5264-4f3c-9085-e8940c5505be","name":"JUIILtSGUWDWfRxKorUPaoXCy","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-11T15:50:43.388439Z","description":"zsNGbNzmNAEhbGabbqhIfiFmN","id":"5e9b96af-a063-4103-ac47-5bed50bd660d","name":"JVpIMxDNmqozteYZKpvpkcMRt","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:46:00.356257Z","description":"NGKxToAoubpywJGLjGVBPzwBs","id":"1f3b1051-eda5-4d65-aa4e-86e8b4508dbf","name":"JWZImzptWOyUTDeRvyxIohhZr","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-11-01T18:58:52.923695Z","description":"BbkSeoizXbKyfNhJMwfKUDMfE","id":"349d2d85-e72f-4e6f-acb5-fdf8d2a1ba44","name":"JXAcJZUTPfraWPQDsCTeXchRK","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T04:29:59.150545Z","description":"XwawedihgmVfUyBycynjeGEWr","id":"eef09635-e209-4a27-a183-0932c018e02f","name":"JYrzrOFWhinQLHKpscZPvTDRz","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T16:45:22.478700Z","description":"yZwXYSmPGsWyahJMOWDJdqdgL","id":"8d720d6d-0733-4a59-a16b-ceb0e83b3b0c","name":"JdgNtTxEWUhfNydgBHRnuVJxy","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:49:26.389094Z","description":"xhNNTNYmfneUgLvNhDLGiZPmE","id":"f9abe467-4379-4eb7-91f1-fce7bbcf1c59","name":"JmQOhcWOSOdNSrssuaGsBOWsx","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-16T18:25:15.308519Z","description":"BKKyjpoLBoHltUuAHQHgFrphP","id":"6d127edf-3b83-47ba-8bca-281cca0e69b1","name":"KGSYHYjBgAVkiHQJGtJzghiyS","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T23:02:52.218374Z","description":"SoTUMdRcGvHWEoRVASKvUhruv","id":"564d3365-4d45-4b83-bd36-af047fe2ee99","name":"KgnRYLYwYFkqNLyzfYfsRczMu","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:40:41.858838Z","description":"WJbpHdpapkEyfvivFQoWvYFsH","id":"04dfb70a-6c77-4158-a50c-91db36758882","name":"KgpIcGxZGSLiinjijSoGBUcjQ","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-28T20:44:38.634529Z","description":"RekRRuXazFUoexMitTlsLDCUm","id":"33bb0595-8715-4296-96f7-3710591336b8","name":"KrXwfvzLlmfrngNIqkwmsiTGj","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T16:49:05.988540Z","description":"SvMqgakwPEYMgOzQRFcpgSiCG","id":"b94a28d2-2659-4d8c-a2d0-92968673c5ea","name":"KrciywQGhfZEwnhvIadeGUoPw","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:24:43.824679Z","description":"JSNUUupbCaatPvYdhlWhmLgYL","id":"624bd31e-4074-492f-bffb-653d38619873","name":"LBaJEokwEpuRETtEoNiEXHWKk","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-30T18:09:13.711396Z","description":"DyREdplzyAWGRLNQSPuAhBjbD","id":"8427bdda-4cee-44df-8a9f-b2fdd35175dd","name":"LWHgzoAWQzArdCuNzGYbtONlS","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-16T16:28:09.904998Z","description":"zdVAbnodQjgGEzvIUXHsdhsTu","id":"b6ca5387-5aba-4036-a5c4-f8fe0249d7be","name":"LaSRNZGmqBwaVyIoatvyJLZCJ","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:46:51.673571Z","description":"bOHKuSdWsXzyDbnFxJjKtamvN","id":"7c961ee0-2186-4738-bab0-b967424ac373","name":"LbcbhhdCaBIoTOhHvYBHKgqjD","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T02:51:10.018073Z","description":"EEXmGiJhstIifLvFrKNOBgVen","id":"850d5c3c-d5dc-4e7c-9dbb-f352fc2e5a85","name":"LemzhIjQBqsTdgRqznWADcqVD","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-11T15:53:47.585533Z","description":"KdteEPBTfzwElbeBfvvXXngjM","id":"cb2a3bda-16c7-4b11-8d39-f0fbb5e83c8d","name":"LowkqeDCcMmwYllxsoFORBuYo","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T23:02:54.280669Z","description":"vSVWPxbMGBiJLmvBFJobhSPWJ","id":"93355971-8f7d-4e00-8296-b8cbacb5ffda","name":"MHWpDFxOskQUqYWplSAMqGEgI","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-09T22:49:02.654947Z","description":"CBTQSIuGEcyzlUaYfbYSEOYAS","id":"32edaf4a-70b9-4726-bde9-c06cb1f1f328","name":"MTokKzutMUMgGyeflswoxJFGg","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T17:21:18.510690Z","description":"kSoMZmJhiQmvSiBlAbRqcvvNk","id":"3d41fa0d-84f8-47f0-9b89-53fc259f5823","name":"MdexnvDCJWASpVEbLGfQbQFJN","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:34:15.848099Z","description":"wygoTKoClNTjAlaMHzyeHBBnQ","id":"8901299c-19c6-4a36-aed5-7e27572b8430","name":"MxQUbTFeTUwCfNouICymPaPlX","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T18:58:52.868597Z","description":"xMCCuiMCMeLeItYjdgMfCUgGp","id":"a9fe3380-e23d-4ad3-8945-570b397c5a57","name":"NFWWNINcUnLMruDozjAxvAoHL","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T17:27:42.818459Z","description":"JFIKCalIUtAkHJSTgGUfKEpRQ","id":"ae1eb150-dbfb-4b32-9a7c-6ed15eecaede","name":"NIinMpGQJPOzLeUgJHqyMqNSj","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T21:28:19.851848Z","description":"BZzrooNcLUWBrmNcUvBhsooIZ","id":"a040c3e6-6ee6-46bb-ab3b-ff3c5386017f","name":"NTxspNMqFicDSoXFJphkAksdO","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:32:55.150636Z","description":"CPVGDAmWToJNEZZzwHvQiyCFa","id":"323bfd0b-eef3-483d-8dac-44106751272a","name":"NUUUVuZiNfGRKwvbZMESpuMoN","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T02:47:30.638660Z","description":"YzJTgGUZBOnMwhLzBjFURJraW","id":"7fb2b805-69a0-4fc5-a1ad-37f5ddab7cd1","name":"NWOujDtGPnayKQwWMoeiOlKcK","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-30T20:44:39.392502Z","description":"ERdboUlXgbttZTfpTkaSYIVBK","id":"b3d6051d-7c35-44fb-9018-0460eec14a0a","name":"NYnKksKkAeGiuPJdLnHgIXwGF","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T23:15:20.202361Z","description":"EVjHnMsHMLewRDwIiCdjygnuD","id":"c7f97d66-9cba-4fae-acfd-28bdd625618e","name":"NYpzcaytiduKWJbUziZAHtnEV","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-17T15:18:14.449018Z","description":"JPcTqBMmDUKqEdYbJgaujxtql","id":"42dce0ff-8bfb-4304-a636-8bc77df3f6d6","name":"NZxcWvyJoTYIdzHMRTjBafcNi","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T18:57:38.178984Z","description":"RlTXVUilgwhuLxGnbGlMJNYys","id":"6c244c72-ffa7-4a1c-bfcb-bce034003b0e","name":"NajfqFBoAXudsjGyotXvJtbPK","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T21:28:21.144993Z","description":"BntNmltuzLPeLrTgQsirIxZqd","id":"b3360a22-b3d8-43e2-ab65-0ffa10c2eddd","name":"NiYhRJuLGnKCknexmdkyhLEkR","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T21:08:24.300521Z","description":"TwxWtSFnOGZMbQAtkaKZbNAio","id":"0c6ce6f3-470a-4adf-8c19-fbb9098a1d57","name":"NizEyasQicqgyLRRTwYJXyakv","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T17:23:05.359373Z","description":"vGgUaccPUplggIqKoFMiVKDJN","id":"1d65dccd-e7ed-409a-aee2-725ab3412eee","name":"NuVTXeSIjUtAUDwfHzumOTNwc","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-30T20:43:59.976825Z","description":"twievGfMxVXaYESuoEPrChNvk","id":"2c7e6cc1-c19d-468e-9b6d-90c324899c82","name":"NzQXBvFJFqGpvzsxprzKFCQaa","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-14T21:01:25.286455Z","description":"RTUmVWjkgWpjlyzoNihTKCGrr","id":"81495510-c1bf-4097-b384-3e5181882198","name":"OASHoJBzslunGbjfSPbTaaFcQ","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-16T18:32:20.602343Z","description":"GrfEwcGmuQBHfVfdcParQTJiC","id":"1edb1599-b020-4d9a-ba00-3cdb06156865","name":"OCiSaNnwFsMtXvMjtSKchYMoY","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:42:46.634854Z","description":"vtUHyPnsnfaDTNpOZEWEYGMAq","id":"565b030e-e0fd-4b0d-b099-66ab59f7a349","name":"OEiLdRRZIUIlXVNBWAfddXlPB","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-30T18:14:05.228229Z","description":"WpoRknRdseGoUrxsJLBCRfcMg","id":"6237872b-b6cd-4153-b905-04eb1748f0b5","name":"OJFZmwTxdGtlZaNEstVOPchBE","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T04:47:53.490564Z","description":"cgfFIDKsVABZSzQpMkAdYvySy","id":"891d0dae-ca34-4f83-85e4-67d8579d1845","name":"OPzSDYdIGOBBVzPqEAwUcGizT","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:45:18.052886Z","description":"dhAgVCSAuaAJQXXDyMxuJjJwl","id":"045c2aa5-be2a-4f95-a0ad-1e157b11bd7e","name":"OgjimnbREoPsxpxiIFTKQXLWA","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-17T16:23:01.438611Z","description":"lQotHFBqxczNytyaPGrKetVVT","id":"070396a8-829d-4a3a-a8ca-a21893913e7b","name":"OhvWaorUJybSiMjLItlfacQBI","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-30T20:22:06.472445Z","description":"LqvmvBLyCwWIoshPKgHIXuPcy","id":"38098773-176b-4cae-a58d-f7fc54393f4b","name":"OlOmoCCpXAvexxnjVouiFQwxV","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T15:11:28.601502Z","description":"CdpMnAePaKgxwaykzTWNUgOEp","id":"04abfc0b-bdb7-4710-bf54-4b330ccde86a","name":"OlVwPkjanAdVUJfxiZoCMxMBM","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:03:02.760449Z","description":"sshnYbjhJVfiXJvLmNIdBwofG","id":"451ffb18-2a77-4d45-9d8f-410ae27115ea","name":"OmqVSrwQFFgiSWghntlgFoffS","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:28:34.884261Z","description":"UIfqwXawwdnfynPyspeRJrRFM","id":"8cf07384-2bd8-4847-8ecb-88b9166861a8","name":"OohiXuwTOCSVCiBjcIGModPPC","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:45:19.345121Z","description":"xBFEyiwEXjgtXvtGcIKQgYvGl","id":"6958f737-aab4-4d20-bbec-4c5d0e798a7f","name":"OooGRnZrDsriUkrHKjYTkuXPz","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T15:18:15.261729Z","description":"kVOBdiaBveLfJAVLxCIvpVQQS","id":"32265a3f-ffce-4446-aaf2-3d9c4ef6cfda","name":"PTrcdzWtMpfuLfwdAiKcXmYuI","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-28T19:34:58.144097Z","description":"SBJeFrivlrIXzZgJLCHKELKhE","id":"616ddc6e-079c-4475-b60c-1c05b751b7ca","name":"PXfqGUMISsNuOmhqIUQhjeLoh","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:43:28.167054Z","description":"lEisjEDPLqqUuijfveYGFwAwF","id":"f21681b0-01ec-4da0-ac34-fa120333e26f","name":"PZcrdRqZLxCIICFSBBhnwGzzn","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T21:26:39.628811Z","description":"NowGrAJXFubsQkcKxMQoLKGds","id":"ffe9cb4e-e2e4-45d2-9e14-e4d27d8bbf41","name":"PlhTIzDcUuPpCvwtDhuazcsNs","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:45:21.413742Z","description":"AahQbIMEYcyxDuHaXIzcsXjwT","id":"416daa06-baff-4738-81eb-88cc52f05e1a","name":"QDPGXjVLjJpjjDlMIgjNwMUSd","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-11-01T18:55:59.649441Z","description":"QfqWVYWtMIFzLcNYPCfvPFOIB","id":"5a841119-00f2-49c5-948d-2469f2f42a22","name":"QHPyUNUQUZyFncvpzjgYULslJ","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T02:41:58.859442Z","description":"WvsGliaVgjKRtkBvLVJdHrcEv","id":"d5c2cc29-3049-4849-9cbe-8cbc41cb8328","name":"QJqPHnzFNSIxBFQKhujZENnXD","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T04:47:30.173466Z","description":"JheFIhWclnltzvlaJuPNvJvlV","id":"22a3c525-be4a-459a-bc57-d4f8b97868c5","name":"QQCZfhojpQBJYprfSoFsqAfPi","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-30T18:15:28.155598Z","description":"UiTbwnrZufOcbLZELlSsUbyZz","id":"969ce220-7e3c-43ec-ad30-0488f2fa10b3","name":"QRlDPmAAPukTIjRiXAZviIVDk","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T02:54:26.156077Z","description":"TZUzrWvadaguAQDNfqJlcaXDZ","id":"84bfcbcb-5e75-4bf7-8277-4b17c277a9fe","name":"QURHFTJturoidgVcjZCkTTCwK","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T04:30:14.918904Z","description":"OpcUdGFOyTnoFzjUZobPbjvEh","id":"1347a251-4929-4c9a-8d85-a9124c1acd7d","name":"QbihQaEnGMWyXHLgtzoiqwVYL","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:31:58.171158Z","description":"MFuwTBBTvjZXEUDWrnLNugveC","id":"1d5e625b-72bb-4ed9-bf02-1a162711c15e","name":"QrrlgpnSLKJIuJfVfVoGEGUVz","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T23:01:24.784249Z","description":"UNUbfMpKrgmBCxjCuBNkCodZa","id":"719a60fb-1bc8-40dd-a796-abb06924abd7","name":"QviZJnljCDjGkJtisXVwTyZpK","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T14:06:29.963235Z","description":"FYzHfTkXHBoboABQEglanppbW","id":"57c8d964-1ca4-42c6-a3e0-5a4411e1ad0c","name":"QwaMbgaUtjGfOfKZtpikmqMCz","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T23:10:54.343389Z","description":"eXImwGeSAuZXranEmRXTcSBMT","id":"1504e5d2-35d9-4e73-ab59-9edefc24b71d","name":"RFcEhJrjJTWZQoyrmmZockTtn","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-11-01T21:34:29.649304Z","description":"bxsAaayVYEOVPpZfAHGWBoaCx","id":"11a8f2e7-1ce3-4fa4-8f03-258323007226","name":"RFtFxZHEDlIFlOIdXOjnilZLP","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-15T19:56:11.430003Z","description":"RSpsUzyYhgrjzBmfBVOTDqirw","id":"0eaca419-fe76-486c-ab8b-e2fe6e36c7e4","name":"RReCFOErusaSCKwAXUKtOqDTI","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T04:30:13.976343Z","description":"JqkJZMUQggfzvTSpldMepulei","id":"15a0cb3b-413b-4139-8ebb-7813df2e2c05","name":"RlgsdbtFtXfxJqpgqHUkuRKMP","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-28T20:06:44.997528Z","description":"pyJmktWjKwlJNuVizvjPqpEOc","id":"16926c74-0445-4611-82fa-2fab0375db17","name":"RpgMcwvCoEMnYgFwMJjNlZmGa","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-11-01T18:55:51.295042Z","description":"YyadujHKPfutHUZZspHDiWjhl","id":"50c58e1e-da03-46f3-b7b3-a07e921ba464","name":"RtZaDeKbTKeVvgCrYLluQwkRJ","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T14:24:31.011562Z","description":"xiTKGSjusUUFRxSeCbtyIlkdm","id":"9d57070d-42d6-41f5-b791-af18f68a8f0a","name":"RvLTAOelURolxURNJldIfLSZA","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T14:24:08.545284Z","description":"JsIsTWvIBbbRFAayecTFFtlSo","id":"bb0419e9-a575-42bc-b162-0086f69644a2","name":"RvZdGnfhQhmOUcGtIxgKclFPn","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-17T15:39:48.921155Z","description":"EfVNnnzxvxnmGPsAGdRzCGLgn","id":"6fddfea6-672d-4bfe-8a96-01e8a0c69e92","name":"SDnFDhkPFoSnytEJDVMBQrJrI","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:44:00.541189Z","description":"aBWMGOJiufFQUskhqLNgRnjWD","id":"1ae79c49-3ffc-4fd8-a382-669da4c1a8a3","name":"SZJxfwVehrQjrKyOrxQKItJWW","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T14:05:13.374446Z","description":"tmKJxqqqaXTJwQQyAIbvepLDI","id":"b1528b0b-85f3-44a9-abcd-cbc696887266","name":"SsuwfMkVXxxEAUGqEgcnDVAnO","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:08:39.919191Z","description":"YBBqUdulfNUjscdMQgyxmLJdO","id":"d387ab71-011e-4147-aeae-86eff76114aa","name":"TJxgcqdMeNhSpdCqjMbCYCrcw","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-11-01T18:03:12.683790Z","description":"xWuCgNITLkVpqvjzAkipaprSU","id":"849d9cdc-e782-43da-9125-a703c460f324","name":"TestBuild","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:19:30.793604Z","description":"dWZJsxDegAYePUjPBqoQnOvVi","id":"2094ee17-8cb9-498f-afdc-6807db0327e1","name":"TvWKmgMxhlsGuLqseGdRZhWuU","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:43:31.106762Z","description":"PUFltsVJCNtLxFQaHiHGcbAOn","id":"06dc564e-0907-41c6-aa5f-1ac0d5827f41","name":"TxEjGZCVhJGwdscfYeJhUHlih","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T23:10:55.700956Z","description":"GFjLijAEkljAHmvbsUgcKKDeh","id":"bf29b643-61f8-4e45-a194-3427950b39eb","name":"UBjKCuaplJfcIudERTJTmyyld","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:04:50.941461Z","description":"RQTpQvzLtHowmeEGFGBDgBGoj","id":"b5ddc690-e1ae-4310-918c-a924afe38100","name":"UDnRzCnhIahCprrTsVIrIIqgD","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:43:17.538585Z","description":"HeIKpqlKEKVbLoHtzvqvdHzfO","id":"eda88e48-8d6d-4ac9-aaeb-809ceb0ec9db","name":"UEeUvLmsxDWqvICormuxOgvHV","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-14T21:53:47.017441Z","description":"azpfirGmzAfdpcuFfPqinNOUG","id":"1ad917b2-effa-4efc-94fc-b6cdc3c35592","name":"UVASFYkLNsUebXXpDTBLNZKWQ","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-15T16:08:34.825931Z","description":"ABfrfhrtedtDAObVKMXbvATQF","id":"4dfa5ba4-a3de-4f45-95ea-fa970c1b26e2","name":"UZtxTtOZVqNvicLOYRVAvCbYf","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T02:57:49.549059Z","description":"AVABDsIyvZBpwDoqvwizHoOag","id":"cbaca6dc-8d57-4049-9f7e-080d657949ce","name":"UduOZFHgJeLRmyUdNvpoNxntF","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-28T20:45:05.138911Z","description":"NdmPALkpuKWCYzxzBvqyOZQlm","id":"c1c3965b-66b7-4c22-8f72-c5185f4167b7","name":"UmMrSnTXOdqYaHAKLirGizAdl","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-14T16:08:59.182631Z","description":"uTwhZsjlrjrleZFSQVCDWGSXO","id":"a064a6a0-79e2-4ae3-9686-11a7db14b5b8","name":"UqYRIjeoAVswNfZqOmEEAXoVz","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:46:48.270864Z","description":"VHzYmfQjtYhuIXlmHoSurvHkF","id":"93f71ba5-d128-49c6-b419-0f929d474136","name":"VAblCTYvIApVmmYxYUZDcoOfB","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-11T04:41:24.956145Z","description":"XVFOZKzNuuNzVqTzcuNoacuaM","id":"5d0ac02e-de8e-41e6-9aa4-20da1360d33f","name":"VBKMEMSkmXLaeUdoLYAFWllLs","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-28T20:07:12.558801Z","description":"qJnAuJDocaOxwLMTUIbAIxGrr","id":"06cad2fd-d244-4147-b37c-619590b2c52c","name":"VCNTWvPnUgvSHrySuKBigyhEF","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:29:04.184419Z","description":"uOCzEhyMVWdRXQrrexBkBcSNU","id":"22847c61-24f0-4b6e-844b-df68f106fc87","name":"VCOwIEQQSXkTsFOFknptFehzH","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-14T22:10:08.439993Z","description":"kmOGUwaOPGQpRgEjLGVutDbOJ","id":"ecb41feb-95ce-4eb7-890c-b5429e77284f","name":"VUKuRaPyGoSeSwIcSbnLlAlBI","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:36:51.529660Z","description":"UcmEFexpzWGvCxlBDPScPvjMA","id":"e73b71e9-809e-440f-a249-208f739a6897","name":"VYnnSWylpurAKWKykxZaXGfsu","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T17:19:07.368848Z","description":"jCwIkkVgbFktXlBZeAoFcQVjJ","id":"36779133-8770-49e4-9b34-fe05c085d380","name":"VbJHSsTvlhjJkwYzqSCpLbGJI","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:33:44.676844Z","description":"RhaBdxeiDLnVyHOLFFDMiViii","id":"5e34d3fa-dc4e-422b-97ce-ad9bdace7a01","name":"VpUdpGzEjUKNbGbQenioEjFng","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-16T18:31:52.816385Z","description":"VpQbEIOUWXMUqTchdhTvgAclt","id":"693d61e2-d1b9-4f23-b7ac-4345d1dc4385","name":"VruVkaEepTxOgkDLGIJEgoNUF","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-11T04:45:18.631845Z","description":"MimVHiTdWdORPECNyxFkdNcii","id":"235ed49a-c268-4b50-86fb-5356a752f560","name":"VsGJxXDphXyqltzOHiptFPymA","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T02:52:41.417328Z","description":"bSHgzakwqqrHUjJjSsgQdfCBv","id":"21ee2789-0eed-4dfc-ac1d-5fc592fc2b40","name":"WDDWYJJDizWFTjYyLbnKEYIhh","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:03:01.966513Z","description":"ThNUOvuuIMKRSddVRqBKUQqzx","id":"01f08433-4f1b-4898-a7c3-dc61daece06d","name":"WGgvfRQcsKGoVYsOfzxzwGQYf","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:42:45.816274Z","description":"SgbqeUVwTyBInZKNbKmGBgTjb","id":"7a0eba94-27fd-4e5c-affc-e07596302211","name":"WIzjhLTNAgxLPUNbNjTTCWjkx","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T23:04:20.598238Z","description":"ifgQpFKYWaQrGczANqinYgcON","id":"ca8004c5-930f-4055-8fad-8a606c600b40","name":"WNUZySpJRqBbijxDLbcaGDVcd","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:02:06.614654Z","description":"sGsJsdxskcUTqBwmhqUBiQUcQ","id":"6ac3d813-fa12-414e-a75b-bde9a6625261","name":"WspjLsMhDyRoHnsYGVAianNSc","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:43:32.363916Z","description":"heVmXgpXpLTKzNQkMXSosdVYY","id":"14eac75d-ab48-47b1-a3b5-364f58aa710b","name":"WuHezezvCETCoqUpVWMSEpbVH","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T14:14:59.460985Z","description":"sETuWHpcKsOXsxucczKAktvkP","id":"7138ceac-650d-45a8-a708-e61018f1c974","name":"WuNaFCIVOBfyyESckIwTBioBW","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:30:53.606103Z","description":"JFTxyEqeQpoEmgywfXvrYrFDW","id":"31db083c-1690-40b4-9948-d5340b051578","name":"XGvBSyIAQyhzoCaQollMlFJbB","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T21:06:44.797702Z","description":"KAYALaENrIxWArePVTbKYjMhj","id":"4bee057f-b4c3-42a3-aaf3-6f8df9d15f59","name":"XHZUjZRsucvRvGZKGOSHVuUgb","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-30T18:12:54.203351Z","description":"PZrKpeLyMxyWDmEeiILFqaYpI","id":"60be9c61-199a-4f42-a218-7949239de226","name":"XOkrdqTsqJueYVLXIhCaljogw","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-17T20:06:59.698324Z","description":"atZcNhaynwwzxIMtLdreYAgLR","id":"711cb3a8-8ab0-4b5c-843c-3bbef98b93f6","name":"XXnEEBKVFCHZFRdFCNxvgoYmQ","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T21:30:43.060460Z","description":"ZMZBUnQAyZYiQXrmvwFBXZCew","id":"78106126-661d-4b69-96d2-dc919a65b924","name":"XdywEmuMvcmTdLbxqfSOZxmBg","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T21:27:55.380601Z","description":"QFzGeypDDxihfdVnfHJmapMhi","id":"d55b6ee4-18be-467f-9352-3d6c15461699","name":"XgQJeROyoRcJwKfmGYjdZuaje","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T02:59:59.101647Z","description":"QnXujSTSsBxYeBlCQOvcLWtZy","id":"eaddc1ba-6225-4182-b015-cdd1d0690ef9","name":"XxLHLTiDpQPwatfbziMTjJEBo","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-11-01T21:34:30.485338Z","description":"YaCCaakZBtAfmxlRXVxmOTizf","id":"7fa5470d-a6d3-40c7-9705-8f00cd6650f9","name":"XzSwtyDofENhydzseKixubVse","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T04:54:48.912402Z","description":"SyezsHkAsqcIsvqeMAGTPqdhw","id":"75336404-9c5e-4662-95a5-52c5c8e1f86f","name":"YFDaedtvNgHURwQYVTZfddXwK","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-15T20:21:42.609753Z","description":"OOWGbQxkbPKyIFXHxINXzrCsu","id":"8b49c8f6-f644-413f-97cc-2ed3a05cabad","name":"YIeUKjyDBgvveopqExUKikEAU","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:37:54.716187Z","description":"FkzQhbkcKCdUknXRDjDmYwfsp","id":"5d6070a0-2a0c-4621-8955-e9f782fb8079","name":"YJTxGGrccSNijmTwRVWKCWscs","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T16:45:23.254531Z","description":"cuvdXjXizEKIZxMucmVfklhWn","id":"f2b6847e-ea41-4efc-ab75-271faafc201d","name":"YLjtTgWjxNpePhnXhHLbrhbWf","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T02:45:21.393307Z","description":"yucKaXOrqVaxcVucktRVoyeYr","id":"7c4ba968-c221-426b-8dec-3e179538d818","name":"YRoYPFRGUlMRDqYGLusuHlkTD","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T21:05:17.428525Z","description":"AlunnFFekeajrzDyVHbXdTntx","id":"5480ef13-0f9b-4a4a-bdfe-0612a8a5966a","name":"YTNoiQKvXBGWesiZEuQeyZltc","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T14:24:07.850557Z","description":"smfxBjpnUxVljyMfViDBbozrY","id":"301c55b9-33ab-405a-874a-9806ca3414e0","name":"YfCzsPZSlKuUsWukbqMBtbfiW","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T02:45:13.013165Z","description":"fGttsopuEBpUmSfuRZLmtEEHv","id":"fe3be2f7-52b8-471d-9c9e-be6269e308fd","name":"YmwtgtNvstPDHjmSFRGTgJScT","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-16T18:10:48.932251Z","description":"GbbzmYPuwJhkQWvoaUdHIKNwZ","id":"44abd60c-1842-45da-b1b6-3418c8f94a73","name":"YoHDVAjeCFXgADfSqanDtYvdx","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-28T14:54:39.817118Z","description":"qgUNjujPRIbCDiFPkYicVOtFM","id":"2d2c754b-fa24-491f-837d-c4b5f78d1629","name":"YrCzxfTWMrVzLXpsjTrNwmTQw","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-11-01T19:00:05.767563Z","description":"qZQGQStUlxnrrETuBwqEDWJcl","id":"c501e11b-fb14-4bd8-956a-168c95563bc0","name":"ZIYUWDniRHfWEBjAuZkNxtGlx","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:35:35.131672Z","description":"LQjlSLyfmGTJQjovMQrtCLxKI","id":"ab40f51e-35f1-4673-bd07-cd9aa44b1634","name":"ZSslgPiuLNXMztBAmttQNNrob","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-11-01T18:58:53.749753Z","description":"JALCJmzaNbyrdgLxqexGfPOnN","id":"fe39553f-4b1b-4568-8536-450c025a1e50","name":"ZaxaljjEUNFQJlVVvaBpFYvRN","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-11-01T20:19:23.953671Z","description":"XzFfthFSnXTUbMxURqDRQGGne","id":"1fa2d9de-1f22-4255-9f54-ca5a544b668e","name":"ZlZzDBRKKhLkOsUpqEjZHwFFM","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:36:50.309020Z","description":"GLYyDEXRNEYtBZeMwPknYRzgF","id":"d0ecda36-2876-48b4-9781-73892aa0601c","name":"ZlqoQKxnpybIZVORVKToZLgzL","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-28T20:42:18.740042Z","description":"WMSROcVAeinmMiClMxUdFzoBE","id":"e6204aa7-7eff-4af2-bba7-07b29199ed57","name":"ZrhkjVAAvzqcjZliDcYNjzlET","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T21:32:31.910999Z","description":"pXzHWaejmLRmlOULUndQVOdsN","id":"daffc500-9c8a-43d6-bcb8-5fe1b3f97e5f","name":"ZyKXRkIlLhRPmMfDqmouQtMpv","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-16T15:18:34.686065Z","description":"NKFdtKWpLKHpjfsYlsVRdrOWY","id":"5625bbb1-bc4c-454a-ac18-6340356f302b","name":"ZyeHjTTKkeNrAcTErTAQyvKie","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T23:04:24.002392Z","description":"ogUwFEAmrpMAgVPLwJCzSdmlO","id":"70c1ded9-08f3-4570-a243-92efa46af5f4","name":"aQTHuIMDsNsBcLsIqoSsKnqud","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-30T18:08:16.729564Z","description":"JLwkDhRZhuhrHNWXHqOmyLksx","id":"0b6efa2e-9d60-4ff5-8b39-fe9b6434a417","name":"aQocuMSPtBhydlhawylwdtkkF","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T04:55:11.299022Z","description":"PjPVfuztgipwoZUzXcwFfatLv","id":"52d3c82a-5142-43b4-b4a7-dca51c60d1f1","name":"aXwUFAxONKNcyzbIDDNvGQvCB","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-11T18:15:05.482878Z","description":"fOCCzXjsewxSwRhIgwnGwuUwJ","id":"446e2622-c3c9-479a-a185-93c2886f4d1f","name":"advVJBNVkDfYzjJathnxsvkPR","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-30T18:11:22.785394Z","description":"kcIYPZCQHdkPKMuOskiLqrsxR","id":"b67f5b01-8cbe-4e4a-95e2-d8dec0655bdb","name":"asALOnnybeIedSNYOJAAvyVhQ","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-15T16:18:43.887833Z","description":"AyhIMoXomnXZhDXrjFhhATHfu","id":"95e5793e-f1b4-4018-8724-3048419b81f0","name":"atXlKPkcXqIettYyzxdDKEXzc","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:39:45.193725Z","description":"TTFmXNyLEPlUAKHXRyxjCBVHC","id":"885dcfb6-18a2-4f56-ae70-11a6b6b3c9dc","name":"bBjWyPpXFHiwVmijcZsfInuEX","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T23:02:50.819514Z","description":"kpsIRXbROzKWawgcgcrdwoiKR","id":"171b1d8c-0534-4e1b-8121-9e4c5c29073a","name":"bEjVxOfEDuPLYqPpgcGdEgmgO","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T14:14:51.847602Z","description":"AbKiNOUlnrmJiqtkgHTpHYgOP","id":"cc4bfc9b-ad51-4adb-b6db-6a0355dad027","name":"bHGNbmLesKjWrvIvzzrSFZMWx","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-17T21:05:26.060205Z","description":"RTQTTucQprQsTTdfExPPzETwc","id":"4846bb7f-79e6-4cac-88b7-0c8e001a1c22","name":"bHZRMpeaCXEggeelybthIUceC","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:29:02.863199Z","description":"HQDdFfKOIQAeJrWHOVlTjywyE","id":"87fb22d7-2c55-47e8-83ed-9c2742c17c5d","name":"bNlEViTKIBQhUwuVujYRQzjSd","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-11-01T19:03:23.261764Z","description":"nWbEtefLJbPzpiNbeBqyvdTku","id":"fde41673-6cb7-4152-9131-5ea8410073ac","name":"bOlIxxoJAOtdWyjcWYNYcIsLj","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T02:55:27.235172Z","description":"ANjUPUWaWVfjZekUvnlGZcLfM","id":"528f06e4-5971-4617-a7d4-ac447a6c5667","name":"bPgChbmnAmXSVggEHWrgngcZw","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:44:38.540636Z","description":"MWrMAXUhuLeXLuJaAQONDjiFN","id":"b0778379-9b3b-4388-8073-238de08626f4","name":"bTrHfjZRlWQGmcSuUhbYbXpVT","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-15T16:42:09.193544Z","description":"pUuntSPHYXKdiduvlXRPrtRvG","id":"346c62b1-bc8e-423e-9f8a-8566b508bbce","name":"bVShwXRXwjNicMJRDkhXaxaJv","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:44:01.802174Z","description":"HQgUlPCehTaNrXnLVPOtarXjC","id":"a9ee590d-aef4-4caa-902d-7bd8a3f5048b","name":"bchLEJKPTOXzlREwoOOVlaMzg","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T18:57:36.252540Z","description":"raSMAPKwgJiyStGXfdOyZmUKm","id":"ee5c6d21-ad00-44ec-ba07-d5113024cfe3","name":"benXVkqJNjvhDOJGTgEllwzkV","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:30:01.650298Z","description":"AWUzhEyxwymkxGsCPNmCiWEFa","id":"5f24c7a9-194a-4570-b4b8-ef426267c365","name":"beqKjPTmuZWUztIJINnmtMyUl","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:00:12.456588Z","description":"dvLYAcrfWZvbIXKEQaPpgZaSu","id":"a729d73c-c93a-4aaf-81e5-0d339ce37f61","name":"bhxSolZtydJiJoPIGFbpizGZI","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-15T19:59:22.850995Z","description":"QsqoMvCcYnrGvpYzpbBXOLAyE","id":"29e787bb-61c6-447f-b705-995d4ccd342a","name":"bpvCUpiMjrWFyqbzsIeQqBTQZ","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:00:00.273238Z","description":"hVWxjjzJHLvSiczATfIgWaZgU","id":"614bbd1d-23e3-4093-8f38-030807df1ca2","name":"cAQgOdNLBBTSCWPFNTTngIcMX","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:27:55.822983Z","description":"uNtgCdeJDlIhNsnIXaWaHnCRH","id":"5850efcb-cc66-40b0-8128-19f0fea01a56","name":"cEQwdSHoaKecNDNysyeAKbNni","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:08:41.192100Z","description":"GpbamOIBTUNdbiDJeUyxqrWrn","id":"576aa43a-6de5-4b91-a7e1-05422466ea7b","name":"cNRoKjZbgBfsvmCATQGehHZxo","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-15T18:13:34.764141Z","description":"gvGQaDgUnmNFkXIjDuhvbCINw","id":"664d91de-bc02-4e7a-814a-b02951d44d99","name":"cRPvtofCvCujfCMjAeEkJIGsR","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:30:52.336661Z","description":"odDowGhgiSDPmYheRlrJtwyAN","id":"834e1d3a-6210-4338-830a-2a0890c5013d","name":"ciLhFNKXwDYPvXseYVRwzYGDt","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:32:55.868348Z","description":"RPRzdQacZoTdBXmExeqDQhEyg","id":"c1a19085-67ea-4614-b8f3-227cef338321","name":"cjIOvRoWnuLRfiKmmkeUYNjIy","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:30:27.590198Z","description":"qXxbRwlVUdZyndDDLhPCYKluO","id":"387b5ede-44d0-4f1d-9745-7f2182f5f844","name":"cokbDqlMxgVBbZrLmIaAxiRAs","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T14:21:43.281811Z","description":"TPyknBnuiMlJwsCwlOdNykYUk","id":"ea5613da-deb0-4ff5-b6d6-5741a55318a0","name":"cpZADgagFgBXruWInTwVSlFIL","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-09T22:49:45.086137Z","description":"NlBJClbfUAZShigatWSUfEZKz","id":"e1c9dbde-97d8-4e03-8c42-9c9c35dc0bc4","name":"crOeBkaZmWNRXnoYcRgnUIFuS","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T16:50:25.100583Z","description":"hNAbdLHZLfVlQKpAdjyOifBqs","id":"ef91a873-a359-488d-b8c7-21c33a197861","name":"cwciYauWPdrPgNmJFRZwCNBHh","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T18:59:58.691228Z","description":"THdbyVTLbHUubDrfyVCWbKVyQ","id":"543afb97-fca6-46c1-8039-a70f89c0d3d9","name":"cwyBmbcpocXdRDyKmTPVaFOVc","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:43:30.343792Z","description":"OcCLErHrQDsdoTalLpXqBMKEJ","id":"010bb09b-e166-44eb-9953-a7e00430b406","name":"cxSzthXuljRaUeADMwuutnewV","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-30T18:16:46.584502Z","description":"KTVKcJLimuxYBFsazoTPBNFOJ","id":"814aac5e-dcf7-4cb1-a81e-9afff8700228","name":"dLSjYmsvlSmXxXiroFMxglTAe","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-28T20:20:52.952945Z","description":"vTUUqQNwmPCyluZrxHEqLvans","id":"26d46755-62be-4f67-aa9c-e5675a7fd549","name":"dbUopISUIkTjAhXuMGTiSGWpu","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:39:06.008643Z","description":"FATrpJIOWmnIoFRTEkpPkEnsG","id":"5aad2442-538a-478f-bd00-ab4c5056047e","name":"djGDnBVULDLgzuTMybdOnFOSX","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:29:45.190262Z","description":"WIWJrqJvZWAJLKvnNvBDDwXOM","id":"86509c35-6a51-483a-ad86-9ad96af872e8","name":"dvAxmIDwgthAyQnZwBWdiQmGm","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:35:04.645554Z","description":"PLdrpzTsneYFlOqRCmyNnihut","id":"3998c48a-b07f-4fc3-a391-7560d91ccf0d","name":"eBrVCaNKXsnQVZpYrUtcVDVPM","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-21T16:18:51.390372Z","description":"yvxXbSUQVlFxyrTQHdTHjRCuN","id":"ae409271-3218-4fb4-921d-cbfe1819da89","name":"eNFFCzLgQkwQQGZYEvtMUbpCD","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T14:05:14.090576Z","description":"NnecfQqmKtKwelSdDLUHDsRkq","id":"17957ed3-44b9-42e9-a468-f56b980c7ad9","name":"eXMWEOxfyaStaNLyCwDKzvefv","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:31:14.289634Z","description":"wIXsOrYITgHfKTKpOwaTwAUOq","id":"37913957-aaaa-41bc-9a04-24d7dca1ec94","name":"eXpGRnqvaDZRlXxibdeaOZuuc","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-11T21:26:40.391054Z","description":"OIodShFucjUmGRuUwQrWKOjDQ","id":"03c7c99c-61c0-4e88-9df9-b7c5e4612a65","name":"ecdFdFTiurepYWblkbPvJDiMO","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:23:29.131583Z","description":"dGRzybxAbwNPChiEVIZxGHkKs","id":"b2cfc16a-efc7-401d-92c8-0cf7bda05393","name":"edjgObXuZkOJqlydbqfBoIulR","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:36:57.158954Z","description":"AejjwFEkfcWmoTYHzXkGNNCdz","id":"e5786ac5-d351-476f-ab75-616cce5b8105","name":"eeCgQHocnFFUPFCoTfLQiSPeq","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:28:59.897519Z","description":"KixkeOBpAQKvolzEAkFSKDWDO","id":"5ac98be9-3865-41f4-8f65-6f413a6fb96a","name":"efWpEgAGSpyEPAJKyAzvGaFgN","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T23:12:36.759286Z","description":"ORtETsAgyQcbjxhjHRLzlIvJk","id":"64b5b6d5-f05f-432b-92d4-f9d32a302e6c","name":"efkkuyvHEZomfLQrBKgEJqTDy","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T15:11:13.381542Z","description":"KcoqhchaNEdjvbxCytkaaDgae","id":"5523dc55-49d2-41c2-b5db-4169ad67aa12","name":"ejUPgYhIRlpGYiWbzVOHQmFAl","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T04:54:29.006868Z","description":"xiVeQINSFppVuVYaunCMRhbhU","id":"ca0c79e4-9835-46c3-8f87-2f695f2a334f","name":"elZlrgkYFCzqciYEtmcSIfJqz","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-11-01T19:02:34.818704Z","description":"YSlIMYXwiSTtPpDOKqibdpoqL","id":"18c344b3-0a44-432a-84fb-836b0a87757b","name":"fKPJCzoQoPLtmjsxoKhEWIudF","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-17T15:18:56.912872Z","description":"WFEjITzKHrMWOJYJUXlTFYwgu","id":"278628e9-7d7a-4a92-937a-c3e45069b5ea","name":"fbMsbNnncfWmAHhcmvHZkYLUm","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:27:29.578495Z","description":"NjAztWFtTHYBHWntjnkfsJpnZ","id":"a174675f-09c8-4f28-a42e-94fc18cc96b2","name":"flcjkVYTxCYiindSRIrlLOKbU","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:19:50.841484Z","description":"aeynXkPOGgANPpSgVRKQAdQDF","id":"b06b367c-86a1-41d6-926c-7ff2f66540ae","name":"fwNrGRuNycvbOeixHhOjsBxvP","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-14T21:48:10.194104Z","description":"TzgSdzSKqeQRUimuCZvXgGNbT","id":"da7e8dc5-dd13-4327-9eaa-0f361e6abd0f","name":"fzoVEPcIBYChDfloqirLkEhFQ","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-11-01T19:20:59.543565Z","description":"cFwUkDfbukunzZiuKrPLVXGbU","id":"63f85e21-2dee-4818-93fe-2c29ab4e9f02","name":"gBIIxowWvWYIhwzloFJlkQbCJ","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-17T15:19:55.048086Z","description":"XrwMZwhAysjIpKnMNesykkUUB","id":"2f5644fe-3f2c-41b4-ba3a-de551dd0e45e","name":"gBIPlVnSpuhaHQzjpMSPBUrbR","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T04:46:22.729533Z","description":"QBhrLqkTptyrsgdqgnExREvHP","id":"4f2dcc2c-cb83-4efb-9e0b-47365dce777c","name":"gBJAxPlhlXHvUXbFMPTgdBSNz","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-11-01T20:27:49.737883Z","description":"KUrVMSHsjNCyMHPTVDtiKhxcm","id":"66eb3d90-0d8c-443a-a82a-6f63d1d2968a","name":"gFCwgZDkGrJKVkZNmVYFrHynB","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T02:44:21.044731Z","description":"mCeJLlIlNgRaJyQiznDrNQwoq","id":"c226bf89-c289-4081-9cfb-e9d0d0353821","name":"gJnxBSugJnRxQmvIBEXiAJJwz","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:32:30.006990Z","description":"dNhuhQaWzvzWQBUnOmRmGNSRg","id":"fe6ac059-3ab4-49cb-9f6a-2eb5b2adda0e","name":"gQJELLLhWvPaHDCAHssLewqXu","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-17T15:29:25.873100Z","description":"KkYIGYbTTuKYiPXigWYDlfqRQ","id":"2ec791b3-e268-4cb9-93f9-d0dba7ab71cd","name":"gSwrziGDNNAucLqbRwgprchlS","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T23:01:26.842599Z","description":"GnMzKhuAFwaVyonmuojgiQukF","id":"48a1546a-e7c4-4f72-b81b-7698f463659a","name":"gUdqpmUeTuCyzstpblRZZneKP","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T16:45:08.713189Z","description":"ScRfBDnJXcBECdEdmOGSUAZIl","id":"b7c46510-f807-43ab-827e-aa96e4fd1cba","name":"gUfcrEGeVAozgCMEVPQhrtPzJ","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T04:37:00.030493Z","description":"HAGChSByYygNyIIdLsPtOJMKd","id":"b1014a59-1ad4-4285-8426-dc2061c733ee","name":"gVYsMYESUnXYHqrqNQJnewTFx","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-17T15:36:35.321085Z","description":"utQnaZrBmnPjsVJtcoDsWoVNA","id":"0e794065-f60a-4d82-a81d-761a9d68da6e","name":"gasfqfCTOuPTGIUzIDTJxKijK","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:20:05.544326Z","description":"RXJEITBnYgQLLZFheBxEUfpyB","id":"5f7b0f52-51df-4f90-ac87-4a2566d766e9","name":"gdNRTFiGKyYItgaNJinFvvFQH","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T23:01:02.151404Z","description":"HSdFdjGpIMDhEzuBqhsrJNKOK","id":"b3fe2dfc-04d3-4217-9034-78d843cc08a1","name":"ggPmGtPTeZdkViGDoXezFlEeO","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-14T21:49:19.516642Z","description":"ZkogmduoulaKXWxSTRqmLJVzH","id":"b8b122a4-c76c-4006-9846-381e5d24a841","name":"ggjDNSjvmQfxYkeBvVTSfLjMp","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-30T18:07:02.647785Z","description":"ILmMinvfQxyojHSyAMlwNxjtG","id":"5c84c485-9e11-42b6-9421-7cbae3be94e9","name":"gmqwbLaPVkAeJEwMxDrjIBoJE","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:54:35.769003Z","description":"FqpKqxsteTakzGYDeePDoIgNg","id":"e3d442fc-a8d6-4708-9ff6-76518d1be5ec","name":"hNVHtQrxjPlpOgrpnFUSzbIOg","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T14:28:49.791038Z","description":"PdGiudSUbANmDNmPVKJXIziiA","id":"fc5b51aa-2be2-4d83-8bad-434d8b5c41c7","name":"hZbaRNureLcgHWerPolVbvvpQ","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T23:16:19.472652Z","description":"rbiZdTbPxWgYZIdzBlSOWhQHu","id":"80b13c12-be16-47e3-a058-61b445ed65f1","name":"hcGWnWqXTHvSdRUatQgYixRob","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-11T04:47:58.440894Z","description":"YjVWrGQDbPvlLJbUMucGIjsQd","id":"50b260de-b23f-4228-8eb1-e69c8a7adbd5","name":"hdJWqWmmyqlbSFHMtlCLrEwCN","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T17:24:40.322156Z","description":"UCnDdTQJuTIlWoTVvtCndiywi","id":"90a00c2c-b7bb-498c-8374-d31ba0611c36","name":"hlmTHGjriIdFBKaMylAvUvOle","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-17T15:24:18.061680Z","description":"qUSTJUPyROiKmALeHKSBXjhmU","id":"8ea2e4f7-ca6f-4f9b-9a73-018eadb27188","name":"hpIaBWIunxbUMbcjTjdbDszwf","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-11-01T19:00:04.985703Z","description":"RIsPuHsWTLssjpQxwgNkEJVBk","id":"30fc8510-cc9b-4d22-8de9-0bea50946a82","name":"iCsdIazEwkETiDAfuxRLZoewO","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:46:49.585456Z","description":"OtEtBmDlhAQeYgsrMCczpBUgX","id":"9ce5cc68-b9e9-4b59-9f2f-03f3d1cebd89","name":"iwOEcoTadFFXxRmBnAVvMUeyI","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-11T15:51:49.338378Z","description":"GyCCsDrJuqRxFEvksSVbXFclO","id":"ca9adf11-1b72-463e-9317-2b43217e1468","name":"izcuUKkDLTzumjlFGNFDWSoAM","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T14:28:49.028654Z","description":"fLHkNfufpNQsteZJxlXtMIntL","id":"b44b4bfd-7716-4e6a-8e26-ebedec211708","name":"jBaxPzsSkVWczBHbDFOVGhsbw","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T23:13:00.584156Z","description":"wZlkJAKVYjjMNNsPJMYpEPoBv","id":"19bf5068-f20b-430d-bd7e-fbd9b25fb997","name":"jEGaCVcNzxzcDkYelUVtRTCAb","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T04:37:00.713460Z","description":"RddNYbGvpSaMbtCGSKnwwSEek","id":"a9c25d6d-7e88-4479-96bd-f0ea4b6bd478","name":"jKnjrvVYCshtmdinvsOCzZfRB","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T04:36:51.645124Z","description":"gkvkaLklRMyleNMpjGICUmBoR","id":"3572bd88-182e-4aec-8925-7a75dcb81b76","name":"jSluXhjYSOexbPwEEmoeReYfa","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T17:22:03.601247Z","description":"gTVyvzUWwjvncsAtqOHuqBFZu","id":"3b77324a-7d18-4336-85af-c8dc6fab734e","name":"jjDwksLLQkQgMssbaLWpOcUUE","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:04:50.232749Z","description":"GkLqtywFgONSVYmwTapoPZykD","id":"7f1bb033-492c-481e-a023-f5f141a500c1","name":"jptNJxFIaeRXnFziqZMOBgkJa","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T14:24:30.315251Z","description":"OoATbCHpaBzLDXFlkCrZQYALF","id":"4f33a231-9fed-4c13-99bb-c56655cd1011","name":"jqaeRzPEeBzOCwUDgWdWTKihl","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T02:53:35.177124Z","description":"qIchqTSwqvJFyiTSZyQQJFJzQ","id":"3517979c-e8ee-451d-aa37-82b8ac370874","name":"jzFvJbvvVKjELlQtzHoXmtQJX","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T18:59:59.468305Z","description":"NwIGhuoQlOTYTObZLpnqKMkqB","id":"4c8aefff-ea78-4123-9e99-dcfe91b4b5b7","name":"kDZGLyQXwhWNbHYBGpKtROama","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T21:30:44.326640Z","description":"jDogPcUPPYbsqxNkaNHSysNTM","id":"698a0266-fe54-48d0-b10e-afa525bc36b9","name":"kDvvPDhKhLYxJQYUDBtgouqwA","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:33:41.943028Z","description":"vgACnKABwcATpOxtznXTjfsSE","id":"5b3fef14-dadb-4530-a118-55cbf130c669","name":"kFGNJwnRLEswwtCkGmAYyFlun","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-28T20:32:52.956826Z","description":"PIRpJPhZKAtjAfyrJHJdbGqEU","id":"9b3f3a89-6a7c-4fff-ae34-e56037d16c11","name":"kGaNsJRgHFOhoDXSkIYqmuEDy","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T23:12:35.447051Z","description":"oWfkiuFpBekjjORgJVpXHJTYj","id":"b61a6a74-c5ee-43cb-9974-aff31ffb5118","name":"kHxJneFtwBAxRCUAQqyZNntgJ","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:28:58.765207Z","description":"VIbuwDKszZsFzTZzUHjqIejAN","id":"870ba7f4-6107-4be3-8802-f66c078a50fc","name":"kQGBxakhGoDwgspLXHqGHGJuI","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-16T16:31:15.295773Z","description":"nHnfUAmkAISsInlCYQwrgiDVX","id":"c9c9a0c7-4eab-4ee5-8ded-212807e52ad2","name":"kUlvegadLQJPUTdOspRYSArET","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-30T20:17:36.688589Z","description":"LzLQesMpNjhczNYwuWpHnJQGs","id":"8bd983e2-5d09-4dbc-b413-da0293c1ea46","name":"krrHqJCrSpSPImEujRDxygXFf","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T04:50:03.860685Z","description":"HsdGoZZFuQIOvUkFYWcPzkXFu","id":"613538bc-a3c5-440f-8fe4-b998e35c0cc1","name":"kzHOtKUCSewnTSnQZUYHCrCDJ","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T17:27:41.860392Z","description":"MMAvdEOnrusfyMughMgNFxAdi","id":"51fb9976-8a15-4a20-8a6a-842e9179ba72","name":"lERjGMGfRBzTPPlcJIhwYhgJB","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T04:47:31.329445Z","description":"jEQocuPoeBXIfkpdbPwJoULPH","id":"c486314a-2b1a-4da9-97e3-db910ff5189d","name":"lNLAgjgosHxIodSDluCIiumdP","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:33:08.202899Z","description":"sdibsGpThZGdoKnEUqzWaApIA","id":"36b90332-c6a1-46a3-9b9c-d73dfa56ed96","name":"lRxQaaVgigGrvKERRIqGSTzUc","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:40:39.728247Z","description":"lNcrmXjueatacQIDXiGuBlXhh","id":"3046188b-a679-4584-a666-ef623f9e2c10","name":"lUyORXOCsKVhsYuKHoVRozEpb","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T14:20:57.866821Z","description":"cOEFQlQYWOqwTgjRbvgJkAPiw","id":"6020fb0d-ad77-4bda-8ba8-2cdd8e0e7ebf","name":"lVZGOsIMBNWDbNueJXYkhIaca","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-16T14:38:03.420463Z","description":"AfuMEESuZPBNxkwbNItEBUmtg","id":"ff3a0fdc-02f8-49fa-af9b-1da4ee1dad73","name":"ltPcvqQCczaudbZKpBDLeOOCH","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-28T20:07:29.439192Z","description":"ulDGZKuQVLNOTcFRuYyuYberx","id":"6eb7f8e7-25c6-422c-af95-c61de26b52cf","name":"mApsajYgFbphoQEJNtzdPlBnf","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-30T20:23:39.704406Z","description":"JxlhJIvgIJKMLZhDffxtMDlmi","id":"45f6f505-d27c-4b20-ba2e-772586ef44a6","name":"mIADmQRclPXqVsRrUXTWnzTpy","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T14:20:58.586401Z","description":"cRwqrgFAvWLLPaWxtTFuzuehy","id":"fdc03bcb-359d-4ad8-bf61-3b15399883d6","name":"mNxcWqNKquuxnJDqlQVmgDBcM","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-16T18:19:37.477807Z","description":"PrVxqdFZMdcmCOsqxypbcGRtD","id":"34119656-226f-4c1d-8242-698bb8f7ffb9","name":"mctqngaMmAvdNBKoRqoYSauSe","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-30T20:25:07.455017Z","description":"vMDxdBeRKxvXQZjfRvrFqxpNW","id":"bb646bd4-deb8-4c62-b37c-41f0acaa66d5","name":"mjfkYHnXmccmKnOuShYTPvehx","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:55:02.895648Z","description":"bINmLjzkuJJTknPtzDqUIsfvy","id":"149e8c91-1ebf-4be3-8724-b3800df6ce31","name":"mvZQZgofRHrhNIWimaRfsGddy","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:27:54.578541Z","description":"bKubuklVtVEHwHtCDxiFVmBgT","id":"76bf039d-8dd0-4218-a29f-7a7636e0ca9b","name":"nFGxtNsRTVPlmFquDRVAtCKuZ","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:42:17.316656Z","description":"BvTktnuBEqBTRiPIwfiuaJJaZ","id":"6f5960a9-9a9f-4c3f-a432-771063ce9143","name":"nLzfmTQvkucSfpTQjjAuJMCpf","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-17T16:29:15.936408Z","description":"ASVgFHujiQqIFfkrZmOchCHyV","id":"e81a2ea6-c5c5-4e4d-bfa6-e06085ac094b","name":"nNWkndPbQczsmHaUPyErrNOkV","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-11-01T19:25:15.646082Z","description":"AguKWtDNBeEYvItrBexDndOpV","id":"9777ca4b-1a64-4359-930a-0db6ae469090","name":"nQjjnSbKiHxwpBBOsjheIcIbb","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-30T18:09:40.519098Z","description":"dSGrQDOUEsiBSUYpOiJzcrNBJ","id":"b0c3022b-1ddd-4f6a-9d33-9de12b88b55e","name":"nSBzfOYjCBAXFrxQykBpXmhsy","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T16:50:24.122423Z","description":"GMbnBxbqqocVAWIITwkzdXxmp","id":"f9e8f184-ce37-4861-8ee3-0da1d7369efd","name":"ndQcRkRIHGswNQsiwYEkTvWft","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-11-01T19:25:16.481314Z","description":"MOWUhxeXkmwxZDpWtBchWrRfn","id":"9ab0becc-0300-47b3-ad88-c923f9505446","name":"ndeDLlznMAeLZPKTEVjqfDncs","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-30T18:17:14.725608Z","description":"HgsKUeHrRBQDcJivmfJWhtzYs","id":"971c7139-b5ef-41e1-837d-8da0cdbb0af6","name":"neUaOEngVIFJpTVCOlkAqDdgk","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T04:54:48.200011Z","description":"ZpLptKaMhEigQiqovCTGhjvoV","id":"6ffc7f7c-ebb4-44dd-9757-a8bb6c8d1341","name":"nuBJXlPuHzTPYltpyXnfshAuU","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T21:29:20.970886Z","description":"gpMQeVHbkHWYYvsUOMYGlJuPh","id":"2ab7fa27-4643-4a1f-ab1e-eaf60a66f00b","name":"nuydsrOZpiiyUcjYirpuJYZRN","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T04:52:32.851238Z","description":"IOPfrIsiuVAuPQdMhdrGxrimb","id":"d5615359-f49d-4c6e-901c-229cb7536000","name":"nvLkKEwSzHkerXOLyfmghZDzE","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-30T20:16:34.674869Z","description":"sulMNssTzQmNchfLNfQRcUubv","id":"de25c052-8c6f-42db-986c-a0461fcd1f49","name":"nwGpQjkwikkVQSFTtPzJeWFuE","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T21:26:40.915186Z","description":"KLeBPhicyTVGyZrwmUaxaIWwX","id":"787cfb25-d3cb-459b-893d-aace6d557c9b","name":"nwJuENzQkBRfDroRSazihutpN","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T14:20:14.821282Z","description":"IwEzkmWgiDUQDtbfumsyPVgjX","id":"98b88bf6-b207-4f0e-b939-38dcced75d10","name":"nxExcBzgiDcCkNWrBMCWuuyjm","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-11-01T19:21:01.833729Z","description":"oXlagJkBldfSovcCpPAilZugx","id":"18b456c7-d5b9-45b5-adb3-41d0ee30b0b0","name":"oKJoILOpDtCwNrZoKprzxUkPZ","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:27:48.059592Z","description":"oiNSYDGGeQjFPduUFlveRahun","id":"1c0f30e7-b289-4d98-8046-adf39917b9fb","name":"oSTHAJIeNQDTMgBwprNWDugCh","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T14:14:14.856475Z","description":"hvUBgcoFkfjXwnlQYtqFabaQq","id":"6895a3bc-9db8-40b6-977d-dc1ca4e3b928","name":"oTdcLDejcpWiugOcsQbiovyCo","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-21T16:15:13.608677Z","description":"lFIcniZplovsaTrLXNbEdGMos","id":"a079ad30-8eaf-468c-91cd-d1f4eb886e8f","name":"oUTBMTWANxAyanngexdzTjNJQ","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-11T05:48:31.236725Z","description":"rTHEUYFIuTALkaGqKIufBYooR","id":"235ddb56-6d2a-4694-a6a9-a81cc251fa7d","name":"obmkkUKzviSXXkRxsUxDBVgSF","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-11T04:45:17.785386Z","description":"zcbTmnwOeVuLHooFRnHZXBLEd","id":"b4ff47c4-b2d9-4705-8d49-8aea5fe0d81d","name":"ocaCnWOwxPmpijLJwtiIzaecf","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-28T16:22:12.228947Z","description":"UxGYLtNkUwujahrCREjJhUuvW","id":"6a15516f-3fe7-4287-b811-b805a8c3bc88","name":"ohZzqiVKculSsXmkipZkRLNfS","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:05:58.530470Z","description":"BCfSdfkjMZlvUQVPVqIzWjHFY","id":"e0c72bba-bb07-4eaa-bf81-2301e06ebfca","name":"oqQGMAHzzFwRjqRpADYQBaMfV","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-28T16:30:41.041393Z","description":"gAUKaWIwoSQojAGpfqFWycGwP","id":"d6d2b4e1-faef-4d09-a9fd-7ed99f68f970","name":"pNbUnfLfxYyAYezEdQdKPJyzF","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:15:52.406300Z","description":"jPyAUYQEEEhhQPXIRkaBAKUCO","id":"39be4ea2-85e2-40b1-b146-ebf6ca71c14e","name":"pPiirqwqpXJjEuUcZnlugZkUy","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-14T21:50:06.694801Z","description":"oBzIXspSIeWsOruPPstgbAKpb","id":"7da0faac-16c8-484c-8e0b-d11c689a4656","name":"pbHLXtmbmqByhWLZBMTJVojRT","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T23:12:38.900770Z","description":"BrNsQZLhvQLMpFbSkdfsjtDLD","id":"cc5a398d-1c93-4335-94ae-c15725d8740a","name":"pbrSzrZOwndxHBoQioenDAGlC","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-15T16:17:48.976713Z","description":"vxwLXSIFMgixPOUiUGSNSbrvk","id":"76cff27a-3cd4-4203-98f9-b8897fd0b756","name":"pebsNjkedgGzijhcqAnHydJJm","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:41:31.758727Z","description":"zYUbjzKHdtRTivVgfSBfyEgfQ","id":"a42d89e5-2a73-41f6-8898-d25679fe7a57","name":"pginhIiRigIJqbqogeQMPXamK","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-15T19:59:06.847048Z","description":"mhzvZeqtVjZHhGcYJTZzQCgeD","id":"7b3a23e0-71dd-4657-859a-c861eb576b02","name":"pxueAsEPuRRmyXpjKAwlOBRED","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T17:24:39.351378Z","description":"LLCdVxrydSFhlEsIVDdFGxjqO","id":"3098dc39-40d0-4f6e-899e-e10813cc61d6","name":"qSEWSuWAjhAQktfjUcoJNSWqT","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:20:04.277869Z","description":"EzIKnrstJgQGAfggwRBfowOwk","id":"1322dc41-18e6-4b7d-9e3f-f38d3d60b49b","name":"qUanmzfpkPWhtWZSnOheSMsRe","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T17:22:04.514973Z","description":"OqpdEZLMRCKOQZXaaIZErQbQy","id":"61aa7c71-b591-4009-959e-fc545c5bbca3","name":"qWBvDPGeenMQLUGkVZUdEDDHp","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-15T17:18:04.527433Z","description":"RzuSHGsRrdqlxPaXVQhXihzUw","id":"d5aa8eb2-667d-4755-9e63-c179c9f81307","name":"qYviVFMxOsPsJYGckMitpfVSl","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:33:42.710120Z","description":"nNqHOBjMvgXQbYACjKVZhaHMF","id":"574d69be-7e5f-49a4-bfba-f60390816f08","name":"qlaqUFbytSKNfSFJqbXWpIQtX","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T21:06:46.048383Z","description":"eBMlDJvmEnrRXiGPnsoSTFapq","id":"38aecb45-6cca-4812-9292-90074992b0b0","name":"qryDQEamFIAqRaqyBbzwYkKSG","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-30T20:26:16.837827Z","description":"RhpWjoRyIRoWdhigqaCYHWPfR","id":"d7fb0391-3084-418f-8f4c-1b60c28f2078","name":"rRByMalfYoZIZUTvqdrKuZSdf","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-14T16:09:26.302879Z","description":"IxaXIBoFjGONrdqOYiiPaRXLz","id":"1e9d5501-0725-45a0-8eb7-348b490b5ba1","name":"rRvWiUoGzSYNpeJyXRMXrdVFK","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T23:11:47.826607Z","description":"LzxXLDdrswbrdVMlXyBaPesvO","id":"4c6d33b0-231c-4d38-af52-b13f0d5d2eaa","name":"roEtiruJUTkDfPeeodyzfsBdQ","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-11T04:47:59.240441Z","description":"OfnfYmunawXstdXRMOrDnPSLn","id":"fc07e191-3826-4e7c-bf48-b2f8077c1235","name":"rzaDzWzqHfWPzWWQGySipeTCC","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T16:45:12.720485Z","description":"UzXKURZLsavCvXxcCjNjdaAKa","id":"c6c5a917-d074-4841-b844-7201a3c48765","name":"sISYknuZXvinpMXsynuUaiOxj","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T21:05:16.439106Z","description":"wmQcRQUghWQKleecjSWTHhlVZ","id":"1bf1db1d-96c1-4200-9b5d-ec88fd87a4c5","name":"sPSnkAQtdwcXZLDsWKswCzTlW","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-30T20:13:46.046081Z","description":"uOiBUeqjPmurjUBwQLeXYxQpB","id":"dd2a808f-232b-43c0-88cd-bdb43d2395ad","name":"sTqLXVZpBvWnhzobwetBklwbq","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T21:27:53.709780Z","description":"mZAWeUKVdMrrYzbXzOqovbPvQ","id":"5f7a34d0-4d44-4450-b65c-82c5fd4cad48","name":"sgtGUczNRVvoOUSyfhwfDQSTi","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T02:44:11.952918Z","description":"acpbXWLowqTpNcoInISMtIhVH","id":"b5926c68-a671-4c61-abcf-29f996785ad8","name":"soBlVFMbESpgEeOgqVRDTfyng","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T14:15:00.190467Z","description":"lAlpkBAwlLmYzeysFkqqaihfM","id":"9a257560-f223-45d2-82b4-f2287454025b","name":"sozflUfObWFpRAoaTFTBMhxyv","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-11T05:11:14.990777Z","description":"DlOztuYUqUvtOBNFHibgnwHwY","id":"c981bc22-b028-4a8b-a32e-2b2eb2d22239","name":"sxPHkOVmxOFFyDqdfYyjdTmEn","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:14:16.313954Z","description":"NvbzDOpUradmxXvVZxiglGNgK","id":"9c040c99-14ca-4248-bb42-2fbd5464014c","name":"tBfPXuQXgsMqVWOUOJPTKNVVC","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T02:51:29.909997Z","description":"MXXqobZGAqvEEXKvbyplEfJxz","id":"0f9a80e8-6404-411d-a8be-6a8ca487bdfe","name":"tCrLFbdkVGhriteOSxRKlVhJT","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:42:09.964319Z","description":"QRQQbtEgtnedvQJcDbWeRCeeZ","id":"fb2b7f90-9f34-4667-9907-dca0d6707c46","name":"tIVOmZNYFmhSXziQDungzZPPJ","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:43:16.295254Z","description":"ffKCGKRkwINPHNoWXNCqWNxMm","id":"848dc517-ead4-4068-bccd-67f7d9cc7ba8","name":"tVZTXdPDfRWgbOJkCYWnhbkQD","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T14:14:49.539154Z","description":"jxqJsUNbgPCDlpSmqdPKzTlcU","id":"6cc8c148-fb01-431b-b562-5569ff32a52f","name":"thKXQMEvaXWCByvmmUYUTzuCg","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T02:55:24.739663Z","description":"RYzfNrdzvUpmSobRAcqnCamzc","id":"add8a31d-cc5b-487d-b6f9-defb2907d9cd","name":"tlwIdLwwUqLsCcRdckraDIvFG","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:46:02.467605Z","description":"CSVSNLaPJAkXLReiKGcQELFIy","id":"54767f82-0eb3-475b-af08-42db302acac7","name":"tqgiCfXlEdhWIrQzssRvVMFcr","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:44:39.852368Z","description":"NrhxInmflFtRAuIkTMLJOCBZD","id":"195b3970-490e-4d17-b1b4-c83a87b82142","name":"ttZYlKZIicEwLsdeixbHtuTlu","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:40:53.201507Z","description":"NAJaDVfQKSnSWGwDeqShWRaip","id":"fb119be6-43ac-4e94-8d74-90095a03e172","name":"twEkNSUGtTkrYDTqUsIAFuyli","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-30T20:23:08.432364Z","description":"wNAlxHlOePbTHlSuLHTvYMDxf","id":"c6be21e6-1dca-4e1c-b8c8-204d5be67e62","name":"uBwcFSTwsmdXScrpYFSvvGHxh","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T16:45:54.396164Z","description":"MjjuKvEXOezOpARMYznIUECjp","id":"d79c3a6d-52d0-4a38-b00f-62d5db835768","name":"uDYXtfWOzktFMwnkPprIHMsUW","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:27:42.490533Z","description":"TVthvoNEsRCBTGncZMbTLLIbv","id":"340de44b-1300-4730-a6b9-bd004e3be5eb","name":"uHBIZJxmhrAUGPFHetlbbvUqr","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:26:10.933027Z","description":"GDmTfrojBAIbmGtVdHdCHoLTv","id":"bce402ec-315e-4797-9618-26357231c422","name":"uHjLtwWrImVfIWuZxQiieYYkZ","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T16:45:14.057840Z","description":"colyWkwjjLmcVrBIxDplkZmka","id":"1e72d499-aef1-4d8c-962f-204398f32e2b","name":"uaTKePqrNmrmbmtFZCrpfUdZi","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T23:01:23.428885Z","description":"BkYnERmbthGWyYXGRJdUHZyxU","id":"e304cbb7-454e-4539-9a01-280f468835ff","name":"uecunYKLQvDNPiyQmQjqsIqAj","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T16:45:55.212264Z","description":"ijVZKsowKNkJTLuIzKzmVRaeB","id":"f14920dc-b4b6-4dc6-947f-194f0b5431f8","name":"ujtWPzKyhSEgnFqFaTvxSVyEB","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T23:11:51.257097Z","description":"APykbDwsQILbOUYpUkjhakzZw","id":"6e78554e-5f8e-4ab9-8abc-bf78470268f7","name":"uqCIMOisUYSoREWLhvXQnzxPv","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T02:43:42.875250Z","description":"RaSheBHRaFJgysIlzixcGnXqX","id":"5920b968-a2bc-42b5-9804-6aa3cf9885ae","name":"uquCWBhDvipKiMcTdOflWKnss","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T23:04:21.887533Z","description":"aopEEzwstAiiPCrsLFllMhPUB","id":"b5da4938-1332-4b03-ba32-4aef8690179f","name":"uvIihyksqdlNocaVhAdSMAghC","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-17T21:06:21.855420Z","description":"pymKiyfSKwVOYkkSeVjPISRwW","id":"dcf39708-1c45-4ece-9958-8c63f1527b87","name":"uygRKTCkxqvEBiRBHcpcoQnHF","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-11-01T19:08:05.481025Z","description":"MYhjLzSRidaFwnhGZMjldQcCF","id":"1c253df5-9d11-4274-9e50-80c345e3d28d","name":"uzEFUcGVAZauiUCUACXwTiSSr","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-15T16:37:56.475522Z","description":"tsztdHUdslGxcbDpQuEzrChWb","id":"ae195a10-b4e9-49d0-bfc6-b8fb4bb0b752","name":"vDCUvCBFEuiJbZbSdKBmYFVpz","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:40:40.559016Z","description":"laetpTWVgfiQeRUyJbpPGVMcM","id":"e3da6443-c96c-45e1-9c42-29d733f579c3","name":"vIpmhkYhDKODxFrhbyEtQCoAb","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-11T05:11:13.737858Z","description":"kwqcXYuJJRcGNtLbgyDxboQql","id":"1767b605-64b6-4e99-bb6d-b82d36a97a02","name":"vUUbEqXkQqevOQvWDbSOcFoYp","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:33:45.941448Z","description":"RoyKoNIzDaqIscUtkpmqHYYxf","id":"2ab1ef3a-27fa-426e-9e59-4b819ad6acb9","name":"vUzTyPbWBABJOjNkbMKYyspVt","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:43:26.912334Z","description":"WcgGtQApRErVtUnHqdUVltQGQ","id":"abbbb867-ac89-497e-b24c-a87fada84d91","name":"vWQOGWvmjGeCeHbRPRPmhCWbT","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-09T20:30:56.272261Z","description":"iIWrpPpDOrcgFItWcBAXqxycj","id":"16481cad-b591-414b-9dcc-fdea0e8af3f4","name":"vWaFoFNrpMwsFfrySfbCoVeAm","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-28T20:47:27.441114Z","description":"KmbvJkFNgfwAgckXXpWcAsMeb","id":"e18fa935-8569-40a5-aeb6-cb0fa4f22de9","name":"vdOHtBECjhqmRehEfMBkHXbaI","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T15:11:39.438384Z","description":"HyYwLycUmAyisWYfYGMmTbgwF","id":"3698dad4-cddf-417f-9356-773f75a34a22","name":"vsIaHrfUcBgxkkCqKwbVbtHUt","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:33:08.952586Z","description":"ldXMPTIhlsNIPvbzyXlYhvXwI","id":"53f7593f-1b70-44b0-b40a-7587e8157e9f","name":"wJqdajEjdpvMwfHOSYBiiNLIk","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:45:59.071636Z","description":"wAoTSTzjxjCrUjvyeFDYtACQr","id":"b2121dfa-61bc-4d4e-ab1f-9e1fc3ce5481","name":"wUCgQhuWDebSUaZxoFFlsRZnH","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T15:11:12.599341Z","description":"WDsjIGBvkGDTTLXmUxQLJOFPd","id":"15306dcc-7bac-45f2-921e-81834af03a9d","name":"wUWxinQtcihyIhxmYTobEZHBn","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-15T20:24:00.744909Z","description":"QPunHbArshSHDbKKggsYjQPCA","id":"65b5bc16-0fad-477a-bdb9-302b8268031d","name":"wYNjEmpYwGmDlEHABtKqZyjYE","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T04:54:28.322329Z","description":"pTPCRILxGxXylAFtyyXhzlTLm","id":"f25150d9-a314-41d8-8693-652c0fb00a26","name":"wkVJGUtJDsJkDTUGOZmhBFqrN","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:40:43.129640Z","description":"ktpVXJNCrcDNsiZvoDepkFUkN","id":"5d833040-91e4-4546-8bfd-3a2c31b3953e","name":"wrzvHDjiDvTscTaoDaMumJRwM","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:24:42.663153Z","description":"RxMuJuEJSEFVltWlLkLhTAAqY","id":"05e4655f-fa45-46fd-9328-189bdcc1f780","name":"xBkgnBypefKIiyGwjVQgHlycB","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-30T20:26:07.310756Z","description":"HasZSDoLcsoaVYKVFIZsJOzUB","id":"9f0bc93c-9684-44bc-bb0a-3c55ab99b3cd","name":"xFdIluUeAtpRwEgbhxkqFyhyz","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:37:51.240977Z","description":"nHibTFModOTooMpAbxQlnGLQV","id":"f621a5d9-6e80-4617-8c6c-a65af3f6ce86","name":"xLIiXnFxjdVtLuncpufVeYumn","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T17:19:06.486582Z","description":"kcAQFyBzINCRfcPctRzkhwzEI","id":"8995a330-c52c-48e5-a9c2-2ac42e2941cc","name":"xLkFgaxjyLZzeDKEfspIXJvVJ","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T04:49:56.452780Z","description":"CxdWQcwacJaiJPRbFGZUXdSty","id":"69298c2e-c272-49ef-827f-7cb369a95c44","name":"xLmDlcdIwWSvZztzKJeXAdwtc","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:31:59.502326Z","description":"VryPSMsKTxscpBaudTiqrLKce","id":"b8dbf679-5164-442b-a0ea-8273872bb071","name":"xQwuoULVbLeTLutHBHEOUCyNH","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:36:35.396030Z","description":"PGeJiQChINYpVGMzJGqchsTxY","id":"1a1a9cae-1577-4b2a-afdf-e09ba0ff35a7","name":"xUheBAWjqNigOftcsfbMkrgeU","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-11-01T20:19:28.256763Z","description":"oMxsyfwGLkoCQgdQQfSICjPXp","id":"5738df74-527b-4ea2-9318-e4068111a165","name":"xVSemsMzdHxYDkJcdMgyALMzU","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:00:24.772788Z","description":"dNCRKZAxisyuVnfNvDdCjAJJX","id":"7d2c5579-205e-44ff-864c-b0b99d120c19","name":"xXCxieKGxXkjWgdoOFnpcfhCT","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T23:13:01.937117Z","description":"JEJcYtZqqWjkOQQHHVQnzPOPS","id":"5ebddf3f-64e1-4cd7-9cf7-c9e3bcd74807","name":"xgxfXsmpXWBvysfcUpEgWXJKN","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-28T20:46:43.810733Z","description":"HSmxOxuNgOCuXeCQahjEVooZP","id":"8e565343-0fe8-4c86-a1b6-a504464bd426","name":"xoxWivbFqNKraBAurMYJBDgKt","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:38:44.559552Z","description":"eSQtKiqAgeupjvvNICEAbAzuc","id":"fb5a093a-f75f-4463-99a6-ddca1287c813","name":"xrlbMQiBQfciHFTJJoiYpMZvx","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:41:32.987397Z","description":"UCMMGfRLWQzPgGIfxJoJaufQC","id":"921ab6f0-4357-4e1f-8d9c-05e0857ef238","name":"xsIqJsaTvpYQQNjTKBTGYouXH","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T19:42:18.065017Z","description":"UrojkDCoUpQyrUSYdieuZZYxv","id":"9347e3e7-c52a-4015-8618-bed49924362c","name":"xvHALEYckyHDDsPICRXHVnqDu","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-28T18:24:24.091184Z","description":"CbjCNcEgHcQxhilVShBPAIGvt","id":"5a7363fd-f5a9-4444-a08c-8ecab696e7df","name":"yBCivSdtrcABvULXsEtMPVUlu","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-17T16:23:45.115848Z","description":"SctuXrzuEftRwpkfOZRAfMuzH","id":"329f6e48-4873-4b03-85ca-fb84237142d7","name":"yEieqAdHrMnEdgUXXSQoRuFqv","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-09T22:47:08.362392Z","description":"KhavHAzMNoIVrcUnmQimMCWQX","id":"f471b560-0b86-4c6f-afa5-6cb6d9c37d7a","name":"yEqKQitjYkNpuhylVfDjQxVer","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-30T20:12:05.121446Z","description":"GMsmkqrDxafaihbZYUdesvhKi","id":"4c96fe67-8c7f-459d-a28a-d479a9d43bb0","name":"yFahdguAgVecpWStxTNciAPSh","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T17:25:41.647537Z","description":"ONdwdKNeLbQePTHhFNFchyYSI","id":"373f76a3-ff89-458c-ac8c-fb3209873359","name":"yHzOlnqiVMwpjapVZoVZHDpsM","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T17:25:10.497296Z","description":"fCkyAxFuEUrlkSWNeWmPTCLos","id":"ec44b744-1cc1-4c96-8215-d531c7b22b85","name":"yMmcFnMgRzfcxbetFljIajzPP","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T17:23:04.456281Z","description":"EJqBsESfdgErXVWgyimDWiKix","id":"f61d2d86-2d69-465f-aaa8-ce20fef11d64","name":"yNwNAshGuruqibKbpdmGeVNRf","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:14:15.088575Z","description":"NobgZaFQnGAkimtpTCqpSMHMM","id":"51d277fb-ce70-497d-8bd5-fe4f17535207","name":"yRVhEgCYmffkxTWbeTtmllDmy","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T02:48:30.088939Z","description":"eFJnyTZsTqYIytiJOvGVSXOeq","id":"aa450e36-535d-4550-8440-4c062212ca1c","name":"yVTtGhCYsBGaBnoEhPqWgTKVx","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-17T16:29:28.717165Z","description":"kOfdWeXOmBKTtuSWWCjFzoJyZ","id":"839bb018-a3ed-4028-a164-4b675724095d","name":"yXwgMYbBkfWTPmiZzuyUzeVzy","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T04:44:49.905199Z","description":"dAdUZNVctinvPeYISqBKydDQJ","id":"215c8eab-56bb-4aef-aff5-3efc7aaa9ba8","name":"yctgSsoiAGmuPuUeOkboKwemA","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-28T20:04:10.300012Z","description":"UWuRQmnHlepxojeZjsuWXvOzD","id":"3286fc40-e0c2-49ce-a94c-c5ad0027025b","name":"yeMzBVkXTTTjCaLbfxREfaLwp","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:49:25.119029Z","description":"KMZeXybgLcAcfqKXhEQpuWlpM","id":"403d53aa-5b6f-4431-b403-91df109f2b29","name":"yobPoCATdQAkiqPgqmLJMPpbE","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-11T05:48:29.939776Z","description":"wWXrxvVcqIZyqgIPsJAtkQfKw","id":"c32cfc4b-9f7e-4526-aa62-401fb8fd758c","name":"ytDVZIqFiDKqjbvFXotdROEXy","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:30:06.374652Z","description":"pTtsExfgPIlmXsUEUvIgNSYcU","id":"5e7a655a-c169-47b5-bece-94f382ac5bd4","name":"yteIGwDeLJiQpCruSobRhHFya","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T03:35:36.418491Z","description":"DvAQOdobDeHhigpjEDONApDGv","id":"1ef7662c-ee71-4253-8486-b528a1d41852","name":"yzDlVnGQeqkHHFAiWkAVEQUiM","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T21:32:30.650334Z","description":"qPQJOGtZpFhtTjGXRkOUqiRCR","id":"0571fcd3-63e7-431b-ba57-feba13c234c0","name":"zJWXEKaIMWuGlHHjfPyKXoZWA","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:37:52.607287Z","description":"KAXOXrZyOELdIZxXQONhsripV","id":"6af9bfb5-9fec-4753-a034-a5be9b3a9b0d","name":"zRWekaiVFhvPeZpSNBoXTkJPT","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T16:43:27.288956Z","description":"pyqLlGaWOkGxuqrOZNeVFQnlF","id":"8d520d98-aaa1-49d9-a3c2-e6609e13abdd","name":"zWUbFNbGjiImDlPmoCLzdQHVO","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-31T15:18:14.571489Z","description":"DJKcIkfyrnmyrqmEknmVtIxWn","id":"ad20a677-7a6a-4277-a98f-9a63fc24c06a","name":"zZgubkuNOmyBKzjkzeBKrrLDy","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-15T16:42:00.568094Z","description":"DnYJOOZmjpGcPMVoLdcPnSIVS","id":"3ac842c5-d623-4e49-bbf2-66f54d3f362f","name":"zpYyzwtNKKuGZmfloFgFmUvdj","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T22:36:43.060585Z","description":"eeQAAewekxXHjkeyZzNdrzZDH","id":"02fef6ea-d2e5-4376-b38b-aee7e88389d5","name":"zyWomWJMWpryEOtQAHSYPrqks","started":null},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2019-10-16T18:24:47.105725Z","description":"cuYFaZjOXCNqHbqqQMEEAHnTV","id":"9661e074-cb9c-40c8-a9be-350230819919","name":"zzVtxvumERqoJEpSadKFcpnGm","started":null}]' - headers: - Cache-Control: - - no-cache - Content-Length: - - "137129" - Content-Type: - - application/json - Date: - - Mon, 13 Jan 2020 23:16:20 GMT - Request-Id: - - apRjjLbi3+yK - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - apRjjLbi3+yK - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/build/80b13c12-be16-47e3-a058-61b445ed65f1 - method: GET - response: - body: '{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-13T23:16:19.472652Z","description":"rbiZdTbPxWgYZIdzBlSOWhQHu","id":"80b13c12-be16-47e3-a058-61b445ed65f1","name":"hcGWnWqXTHvSdRUatQgYixRob","started":null}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "315" - Content-Type: - - application/json - Date: - - Mon, 13 Jan 2020 23:16:20 GMT - Request-Id: - - M3KMMsMKldoe - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - M3KMMsMKldoe - status: 200 OK - code: 200 - duration: "" diff --git a/fixtures/conch-v3/datacenter.yaml b/fixtures/conch-v3/datacenter.yaml deleted file mode 100644 index 8fd12c0..0000000 --- a/fixtures/conch-v3/datacenter.yaml +++ /dev/null @@ -1,122 +0,0 @@ ---- -version: 1 -interactions: -- request: - body: | - {"location":"Hala","region":"Atlantis","vendor":"Asguardians"} - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/dc - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Mon, 13 Jan 2020 23:16:20 GMT - Location: - - /dc/88089480-2f31-4cc6-8e3e-afb41c91423c - Request-Id: - - wDQFqNm48BA4 - Server: - - Mojolicious (Perl) - X-Request-Id: - - wDQFqNm48BA4 - status: 204 No Content - code: 204 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/dc/88089480-2f31-4cc6-8e3e-afb41c91423c - method: GET - response: - body: '{"created":"2019-10-10T21:39:38.188875Z","id":"88089480-2f31-4cc6-8e3e-afb41c91423c","location":"Hala","region":"Atlantis","updated":"2019-10-10T21:39:38.188875Z","vendor":"Asguardians","vendor_name":null}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "205" - Content-Type: - - application/json - Date: - - Mon, 13 Jan 2020 23:16:20 GMT - Request-Id: - - 5jbkHZxlwZqP - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - 5jbkHZxlwZqP - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/dc - method: GET - response: - body: '[{"created":"2019-10-17T17:37:09.540656Z","id":"a48f646f-a4bd-4873-b28c-af107cda26f8","location":"gKDqIpIeJgoQDIkZgoOyKzpjt","region":"LUcrXRYSDVYdEbKidrNtQVBAf","updated":"2019-10-17T17:37:09.540656Z","vendor":"ADeRzFuzPofzCNiBZDeFjJTaF","vendor_name":"WJdKvvPjlsJyvBuZuVrwIUCoA"},{"created":"2019-10-28T20:46:44.472962Z","id":"fc9d5a7a-a64e-4ff3-a9dc-f27b945c488d","location":"GdQINFxhjTCcMHybUBVCMBbaq","region":"eAuGplghsSxmXIpbWjoiSZTAQ","updated":"2019-10-28T20:46:44.472962Z","vendor":"AECgGovBhfDdRDtEcnUGUpKLk","vendor_name":"gjFHYPRUcbVDTHnhFVfuJRiQw"},{"created":"2019-10-15T16:36:27.897696Z","id":"27f4f869-e3e6-4413-96fa-70ec9267b66d","location":"aIYbTfEgmkOhsdLbbzYTRuZIw","region":"XPKcaVTlhQZueRoOGGqcjpcFx","updated":"2019-10-15T16:36:27.897696Z","vendor":"AIVfGcIDtBTZKcdEeOtuEQoNt","vendor_name":"ybiakkVxRQepnQcfoKgDFukTB"},{"created":"2019-10-31T14:20:15.717658Z","id":"ade89ff5-a037-4295-baa7-b1a3902251c1","location":"nzycLQrJIqjZVddGFSWmSSlPp","region":"JOqmdJwbSTvoVcMOScbNMAfrH","updated":"2019-10-31T14:20:15.717658Z","vendor":"AVgZQHmJVeibXOEhJnKdVkbba","vendor_name":"RNtvVKUpbvImpnpjdblvOzHjg"},{"created":"2019-10-28T20:04:10.941773Z","id":"91bd392a-6f0c-4e6e-b5e8-f5729b309b08","location":"ZHiZIJjOMzwrYrWoHgqkFHlPF","region":"NdHrFMsTODipyahZKEseLXoPK","updated":"2019-10-28T20:04:10.941773Z","vendor":"AYmRLMziNRvdcJGrQVtQAoAMU","vendor_name":"fCmrkGJVHGZDUYDjailAhLsna"},{"created":"2020-01-11T05:48:31.572715Z","id":"cfc0ca58-8a1f-4b98-8db6-c1e1588ab7c5","location":"feIOQBqzdulTbUTNfoYCUVahw","region":"HDEFkdZIFHlBVDwCqWOGGHRqR","updated":"2020-01-11T05:48:31.572715Z","vendor":"ArBrpthTPQVHzFuaXdxeeCmKY","vendor_name":"MYpzHNAcEZLejfnEBePLmnqTp"},{"created":"2019-10-28T15:43:28.863482Z","id":"279025dd-fb09-4d70-9554-654a6783d458","location":"xFaHAOEiDuaWcffSdEeegEXNR","region":"KUAipdjXcLebXQlowpPsPUNjw","updated":"2019-10-28T15:43:28.863482Z","vendor":"AsWswygvzqEQBynvyCdvXLVnG","vendor_name":"TNROpIkGhPtdeZblVTEEgAhPr"},{"created":"2020-01-13T17:23:05.614483Z","id":"5c56e1b7-174d-4b97-97c5-8ab1cbab6390","location":"vAZkNccFgcnZmorJkeFZhwEub","region":"TzBBGTROzHckQOgGDBrzpGClA","updated":"2020-01-13T17:23:05.614483Z","vendor":"AsXYzsApVaUgeeaUmBCCEKrHP","vendor_name":"FPNxoUUMtDiUoIXYeTErrBEpv"},{"created":"2019-10-10T21:39:38.188875Z","id":"88089480-2f31-4cc6-8e3e-afb41c91423c","location":"Hala","region":"Atlantis","updated":"2019-10-10T21:39:38.188875Z","vendor":"Asguardians","vendor_name":null},{"created":"2019-10-30T18:08:49.172973Z","id":"15677d9d-efce-42e9-96dc-16ed1748a269","location":"tzyGMpalkZrYCtXxUNCiBHXaD","region":"pGTSIYmCxBWdMNsSmJMiCsYDC","updated":"2019-10-30T18:08:49.172973Z","vendor":"BQTfRkBKoctPbXJatbWehfLsh","vendor_name":"HGLlocUlduHIHLVtaiyiwowKs"},{"created":"2019-10-28T20:45:05.806504Z","id":"54733021-9a3e-4f1c-86ee-ad642a5aaeca","location":"HuKvcapcjDlRtsLwjTaeFmLez","region":"DdTwjeixPCqLzViyDEXZjjQuY","updated":"2019-10-28T20:45:05.806504Z","vendor":"BdYWmYVudAmhHFvShMuQDFofW","vendor_name":"UGsdWxGjqiIjQGStRnQuWSwKv"},{"created":"2020-01-13T17:25:12.083939Z","id":"45c81470-b77b-47f3-9237-244e6023b343","location":"WgPiCYaLNzVFtCknYTClGmPeN","region":"VTomJdhCFaYKbVLrPQLcCHhLM","updated":"2020-01-13T17:25:12.083939Z","vendor":"CuljsFRYyiqISwxweQFwukWBg","vendor_name":"xfWZsaybKkPzbRCcaWSaUELvJ"},{"created":"2019-10-31T03:35:04.012012Z","id":"a3847fbe-0062-42a5-b7b4-45b27a29bb0a","location":"hvDWbWfHZIzBONNvbxypSklVA","region":"yeTXjpOuyfbGVKUaNLDyXsStn","updated":"2019-10-31T03:35:04.012012Z","vendor":"DhGRHcWbhgcpSBBIvgwggufKS","vendor_name":"fItvUiEeAmnRjkrosCjqpLVdx"},{"created":"2019-11-01T20:27:50.851972Z","id":"94c5f2a2-a91e-47aa-acb2-8dcbbacb4889","location":"zJKMsBfnDGRLTGCOsvqDpggpE","region":"fmjBUeIgpalvDECNCtXgmsLts","updated":"2019-11-01T20:27:50.851972Z","vendor":"DiqlEJixsJtiuSZMNsfuhzKHs","vendor_name":"wkauGWxWWgZhduGgjkfUdtRrM"},{"created":"2019-10-31T03:35:35.742778Z","id":"46d617fd-2f0c-41fb-8b11-f83e7e963190","location":"hFypCoQJggBcaOIuPcvuoiLJw","region":"dadUcsOtCdJjyxJaIkwjpenEM","updated":"2019-10-31T03:35:35.742778Z","vendor":"DlgsiYrolRaugLRxkWIrRYCeQ","vendor_name":"CHrfiVIqmxeWwbAAIWlmbDEzA"},{"created":"2019-10-30T18:13:43.601451Z","id":"d7edfd70-6f3e-4df1-8f74-aa76f58d5119","location":"dfaYgChPlNQSMYZMXiOQBlVbk","region":"RYiUHkWpFPeMfNwgVlPWhxIFJ","updated":"2019-10-30T18:13:43.601451Z","vendor":"DsLzzRizTJNPOXPXvpvTHAKGv","vendor_name":"vouyNQXaCMsTnsNXqotmNcvgg"},{"created":"2020-01-13T16:50:25.385193Z","id":"5862c0a5-5888-4e9f-86a2-f25743e5e4d4","location":"dNirNlaBpXXUjYxVGGiSIjeTh","region":"NqhsgUoMsElPStffxOXQSJTcr","updated":"2020-01-13T16:50:25.385193Z","vendor":"ErgOytqjjveLFkIXyupjFlfln","vendor_name":"NqfxZoIkxHDhUduOmFCebvJGO"},{"created":"2019-10-30T20:17:37.491516Z","id":"611840bb-b0f4-40a1-95be-8753959f4fb4","location":"GdaIokuPEpkendAzJngNiTcYC","region":"wTXycDiyvvDvUwJLhhDXxRcZc","updated":"2019-10-30T20:17:37.491516Z","vendor":"EyhJpumEnSEcizcSzhdQZSJEp","vendor_name":"kCXFBlmRRekqAzoTerQEIJRUF"},{"created":"2019-10-31T15:18:15.520578Z","id":"f5a5fc3e-9011-4425-a127-b42e70a4bf2b","location":"mrNDuGMsbOYBMBkyMYUmofMBd","region":"pYGYbKrGKarUDFnuzgMQOyUQI","updated":"2019-10-31T15:18:15.520578Z","vendor":"FBlRnVZdLoZGkLdwlSUETtoKL","vendor_name":"vwhBseIUiHawRYZbiqUbeMjsz"},{"created":"2020-01-13T22:37:54.425643Z","id":"f33fc43f-6571-4c58-840e-7b07c97659e2","location":"LMLsynyOxzIHRjWGTYhXAdbRR","region":"NGBTBfxXIVljMXzZAbZqqWyCP","updated":"2020-01-13T22:37:54.425643Z","vendor":"FCXewcHsKrNNhZlvCgOHQMUPd","vendor_name":"OqRYKpuYexJlIhGiPWablFRUO"},{"created":"2019-10-16T14:37:50.503526Z","id":"1e3a55c2-2487-42be-89a4-a53c19620cdd","location":"nYOEShhillewLsaWqgFsGigNm","region":"MzwqWfOBYhpUjxFMDLqLvouFl","updated":"2019-10-16T14:37:50.503526Z","vendor":"FKNBiBCbLRiuMzfuNjSaVLmoc","vendor_name":"VRzXxpGJCwRbaJKfXBEqhIeFt"},{"created":"2019-10-30T18:11:23.577446Z","id":"43f8d806-5631-434c-8842-00e9aa8581b3","location":"EiXuzMPgXeVhGDBnvpdyiTdbB","region":"yOQBeGvWTZRxzvEhRMIPAJMJD","updated":"2019-10-30T18:11:23.577446Z","vendor":"FaOmdcbTFOUiXehdcxbdgmEFe","vendor_name":"zzyVMJZMBBLaouGUpzlFAWmzn"},{"created":"2019-10-28T19:28:16.373344Z","id":"c8b82b98-9f4f-40d7-815c-76166e453dcf","location":"jyeBiJcEmZEpwfSWHlrpccrCU","region":"piEQLEZADCjIjBFtkOhGYNeOL","updated":"2019-10-28T19:28:16.373344Z","vendor":"GMqKAiZhMEcPXtBucGuCAMykv","vendor_name":"OyZpVZTavwReOJVcHUylKpJhr"},{"created":"2019-10-31T03:49:25.795023Z","id":"6d5b7786-0acd-4d8f-95d7-c833820ef3a1","location":"KFzZolNSzrOrZLNRsJAuDWZsV","region":"LjnQOLOkocgXDshlmgLvFuWfl","updated":"2019-10-31T03:49:25.795023Z","vendor":"GTnGNLuzABRdePiwYsJbzSDqW","vendor_name":"pjpaHOYtADlYbvIhEdFhqEZvU"},{"created":"2019-10-15T19:23:38.928259Z","id":"acff059d-b38d-40df-a062-2d5eea18c496","location":"HjDYvFdFyOMohvAWuzKOJwBIO","region":"YzzYyNysSaIenFLfKZWkUTtGL","updated":"2019-10-15T19:23:38.928259Z","vendor":"HMuwuRdONOgkqPtpdLJSxTnKG","vendor_name":"WAPhhZhdKjJaJuBRcXVNJGBqn"},{"created":"2019-10-31T15:11:13.642689Z","id":"4456c008-97ba-4f0a-977e-dc767a02e3c5","location":"DXJqfFtJHvJsCBrRnFFFAKPiR","region":"qdtqEmKeHVsIQozEWUGndXqfv","updated":"2019-10-31T15:11:13.642689Z","vendor":"HQQARFHDwSixTrJUvJsYnUgzs","vendor_name":"wTQkglYYCFCXctbNmLJhXoOBU"},{"created":"2019-10-31T03:36:50.916247Z","id":"3ad418c3-11df-48a6-b0fb-bbd4c68f6125","location":"MLflqWMEfHQDizFLkvdNECFem","region":"NqHjOgCpIsGHEyYyPlnnfxULa","updated":"2019-10-31T03:36:50.916247Z","vendor":"HTuUKkBzcVrjPEFcmgSjZHsMc","vendor_name":"sGEEfjLuzsmckkXkCKzuNiQuV"},{"created":"2019-10-15T19:41:04.703692Z","id":"ceb364c5-7b7e-4a59-9ae6-7921a5329cbf","location":"usRteWqnnKtyhtOKudRcCNCNo","region":"LvFCHkMwKddkoqPnveOIhRMkN","updated":"2019-10-15T19:41:04.703692Z","vendor":"HdpBoIeapOcXmkCvWdmuhwALl","vendor_name":"koZXARXEKpcSWwiiAqKlpNkox"},{"created":"2019-10-30T18:14:06.073882Z","id":"bcaf23d1-0728-41a9-8035-07e2049ae94a","location":"tFecUzGrMWHWWqTYUEbiaXiso","region":"tTxTvcJRwFyPnTAQheifbqVUJ","updated":"2019-10-30T18:14:06.073882Z","vendor":"HgLkNDPKXuywpZVoQdgCLUrCq","vendor_name":"GmktCjWPljKMLHRNGaxNQEbFW"},{"created":"2019-10-16T14:38:03.721407Z","id":"d7650902-3351-4dd7-86bb-e10739351c7c","location":"VagLXyRiNJWuFMFDmGFlfAiwJ","region":"McrREtSbZpjsHRPSsHiFyGwXp","updated":"2019-10-16T14:38:03.721407Z","vendor":"HkdRzRtFexXHqOZIWALnlQCHM","vendor_name":"vQbabNpWjLzNLVpVhMVlaJsHO"},{"created":"2020-01-13T22:28:26.021239Z","id":"f6cf781c-c800-4b40-842d-fd7addc0424c","location":"cnLwEfidzJFXXeTQvcPnkTYGO","region":"awhuywAepazOaGcjtdogoqClv","updated":"2020-01-13T22:28:26.021239Z","vendor":"HpRFYmFKBlmQlAZXBHiqKKjrB","vendor_name":"iEBDouZBHuIrodFxKZbjSkLLl"},{"created":"2019-10-28T20:44:39.273658Z","id":"ef35d596-5c85-40b4-bbc3-5e897453236a","location":"ghzAeSqqrPGbZUZElvIUqhtSH","region":"CnOnwaKNLnXUVbsUOeousUFlH","updated":"2019-10-28T20:44:39.273658Z","vendor":"HthuBXweihPwkXqGkrYipnJBd","vendor_name":"TWKxRYjwaWFWLkxnhnHOqnoLU"},{"created":"2020-01-13T22:27:42.406502Z","id":"8294c9bd-d1df-4cfb-babd-b20031f4073a","location":"tGrdZooaTPYEqQthzkackUNJD","region":"gWqaaLEihimKLfYjHmNVhZgtg","updated":"2020-01-13T22:27:42.406502Z","vendor":"HuzvipzbhLyoLIMbSqTKIdKGX","vendor_name":"CPxMzjBRAoflMVhJgdlzgieOb"},{"created":"2019-10-31T14:06:30.938989Z","id":"3c982d79-dcfb-42d4-ba56-c65ed7381375","location":"XGAqGIpMLBdHNquyDMwXofmtd","region":"WpsCqDtYfzWVCQaTkkAWoNmTn","updated":"2019-10-31T14:06:30.938989Z","vendor":"IHJDKuPRYMlpmMANKNGrCDbQc","vendor_name":"QPHSyYGyhhfDvkQBPCMBkYyjp"},{"created":"2019-10-31T03:40:42.478595Z","id":"c1cea351-cbef-4759-ba7d-0292ced6bdce","location":"bjDhJRLqxANmwKIMKExXqKAuU","region":"DvbyfOIqheaOFDnXtxeHkNNKV","updated":"2019-10-31T03:40:42.478595Z","vendor":"IHZRcvUVhdBMXJHKcIsqzGETf","vendor_name":"JhhdFwhPZNmgjTaMKCseZcxcK"},{"created":"2020-01-13T22:31:14.201253Z","id":"bbddb004-11c2-40d6-8c8f-6ab191216140","location":"nEYWXAJSvelGzxyKIIXKOsrtz","region":"hGEYfgtkkvRHNigOwbTfvfYsh","updated":"2020-01-13T22:31:14.201253Z","vendor":"JLcXuvGjRfiFsiYASHvpFROrm","vendor_name":"VuJtOVXhsehdSZknYimPBCqeb"},{"created":"2019-10-31T03:30:52.968267Z","id":"b8336463-a25f-4ee1-bee5-434c2bf2c893","location":"lKRVllZwFXRmowfYUToXfrpFE","region":"zMrkTQdEWSDKIvnOVJLzsdjNd","updated":"2019-10-31T03:30:52.968267Z","vendor":"JSTDtlqElmuIUCKXsMHYVxFUD","vendor_name":"EbrYmHqUqHwGoPqIfUyiCKsvM"},{"created":"2019-10-28T15:44:10.142864Z","id":"0457bb04-64e7-44ab-a0d3-d745bcc719e9","location":"SgmuuHIfxYWKsRpuagHmgbLIl","region":"ARiZaiiPSFuXZLrhYVCCxWnrX","updated":"2019-10-28T15:44:10.142864Z","vendor":"JczpnjFDmALilLfFsJWaAaPeM","vendor_name":"XWvmCjXuPXzKOoASQccVgTAiN"},{"created":"2019-10-28T19:35:00.252669Z","id":"6c51c1aa-fa05-45da-86c7-65c5346775b2","location":"DEUrPCIHeUXqZJJAcmJHFeiPi","region":"BmngYHOyJrgBCZmEozjWTAXaJ","updated":"2019-10-28T19:35:00.252669Z","vendor":"JjHjeSemuQQTovUhtVHxTMyor","vendor_name":"ZVhqwZnzoVnmCMGraRdEpfrta"},{"created":"2019-10-28T20:20:53.605405Z","id":"5e77c072-4028-44f1-b669-f3ab1b63c1b9","location":"eqtJrxBxdQuOgPlHVyGlqeJNI","region":"YtzCmEVJGgZZJtEahSFhQBqax","updated":"2019-10-28T20:20:53.605405Z","vendor":"JmzpURsbKCrlcwCBWgTbsoqjq","vendor_name":"FJhZUiPWFvKAJmZSKiOVXlUXt"},{"created":"2019-10-31T03:29:44.554836Z","id":"7df897b1-4952-4def-afc9-0d21d90e775e","location":"LSydsqrxtlgFeicPqADjVOdQs","region":"TyzZMsfPHsJfhzAMUOCULIsdE","updated":"2019-10-31T03:29:44.554836Z","vendor":"JubztQnEmQIpwjNMYTjeZdtOg","vendor_name":"ufIUkufAlUjXejYilMZoskHIy"},{"created":"2019-10-30T18:17:15.524667Z","id":"c087446f-6eb7-4a74-82d7-174d54dc2fa4","location":"fkCRjqjALpBTfjVKxcpsUISdI","region":"LfMFMWNuboLNJVErxlxVVFdkS","updated":"2019-10-30T18:17:15.524667Z","vendor":"KVEtYQzvHKWdqogYnuyaftZxb","vendor_name":"gDsFIUsKLfclaVCdQoclPSRVL"},{"created":"2020-01-13T22:32:29.922597Z","id":"483a493b-7973-4426-93a5-9ecbbdf3155e","location":"xyhRbOPYgHvmmGkSzQolRtPaL","region":"UdBBTkngNuEUYyDGwCMNtqzzj","updated":"2020-01-13T22:32:29.922597Z","vendor":"KVIHEpnWGKOBuODZabSqWJudi","vendor_name":"GrnjWuYBbIsTBQSIwsAEtejNh"},{"created":"2019-10-31T02:51:10.492286Z","id":"bdc48d4a-9daf-4302-a880-7657ef899182","location":"txhlgGSrKQaWeaTbGOSgXIomO","region":"gOrCwUKnUdhMdYHUavjJBUGcn","updated":"2019-10-31T02:51:10.492286Z","vendor":"KVksMxvuiUCtCTCVbNGYqTGPV","vendor_name":"IAdKAfEpgStvjKPhPnloNyvMq"},{"created":"2019-10-31T02:52:41.943555Z","id":"b5703b38-e251-4ded-a8ca-7c959244ef90","location":"dIChTMfXIeVjQdRClAZXchBUu","region":"VBdVQSbqAgKGGLJhdDrUIZVCC","updated":"2019-10-31T02:52:41.943555Z","vendor":"KiGkzdLdTzliHBeQolfgCOsLq","vendor_name":"lTfHIIqEbmqnZOMGTKKjgjWHg"},{"created":"2019-10-30T20:12:05.834309Z","id":"0857cd2e-f3a0-43b5-8729-a7a0443b9304","location":"TmJLKRcuEodabZVctWLwgmFUP","region":"XyIcIfstLCQNWWtNgLaxFiOZS","updated":"2019-10-30T20:12:05.834309Z","vendor":"KiVUEcJYjqxiDOevYiUgaQmqN","vendor_name":"HExNSnQpjJhYFoXcUVwSDNImG"},{"created":"2020-01-13T22:19:50.751769Z","id":"2878c084-9607-499f-a0ef-7d6277bf4833","location":"UoSkziQzZNwQsCneRrvruQktC","region":"sjxwgVPUQSmaxMxYynxXGfZXG","updated":"2020-01-13T22:19:50.751769Z","vendor":"KvZpgwmyJjeeHSEtDTCISneTa","vendor_name":"BpUGjDSDbaxqjkIeJmovVFfVs"},{"created":"2019-10-21T16:19:18.019076Z","id":"48140a9e-f76b-4d4f-ae39-49551d4ed5ee","location":"OQldZRxtGUCQjdSpOQDQpBDUf","region":"mcCDoxgSBsJUtfiYUiIwqogQD","updated":"2019-10-21T16:19:18.019076Z","vendor":"KwccpAuPrwbHLGVlYigdnhEsJ","vendor_name":"ANgkxqnSJXKkCXNBtqlRjPvDn"},{"created":"2020-01-13T17:21:19.647422Z","id":"3d14d83f-df07-4c57-8d37-b4df6445df33","location":"XZkfftdScIezVJdsfyskzfSuM","region":"qfsMWJAaXiRTCMpYXmGwCzFBm","updated":"2020-01-13T17:21:19.647422Z","vendor":"KzJusKQbwqaXSGzfUmmEmGZpV","vendor_name":"SWkNoMxbignEOsijWdxtrhetT"},{"created":"2019-10-17T17:38:38.331033Z","id":"c24093a0-da30-4bee-afb6-bba98ef60230","location":"qrFXZFUdYCUZqCYwifjSkuZGF","region":"JcCgrPazCKqsKbPCJIbPzyYXM","updated":"2019-10-17T17:38:38.331033Z","vendor":"LPoMtCWECBNkDOWXsMgqorSNu","vendor_name":"jFlxeugRmqABvPwSZnrnaHzKg"},{"created":"2020-01-13T21:06:45.960001Z","id":"a9e7cd35-4548-475d-9fda-575481d83c33","location":"omnpBUIMXVzvBbDNxEExWPcVB","region":"NJInKZzZdxbmnMhHzxxSXQqUe","updated":"2020-01-13T21:06:45.960001Z","vendor":"LSUzESpuzlBrworjzFtmlyJcE","vendor_name":"GOQJlzLQIXieZbfAkPaAYdjWp"},{"created":"2020-01-13T21:08:24.210103Z","id":"42ab22c3-b403-41bb-b0a6-7a31298a135b","location":"JZOYtqABmcaDLxMejldmAKRyR","region":"UhOHfFVnPLPRwacrjQMwDfwBN","updated":"2020-01-13T21:08:24.210103Z","vendor":"LTPnlVukSZrUkTEJwPGGClhuK","vendor_name":"JPhDNlJColrxgEvnGZomDlNgl"},{"created":"2019-10-31T03:44:01.148790Z","id":"f25336e4-de60-47cd-82f6-620f99a96a83","location":"ZXVaMVYmEfrDdVLYVBVooXvCC","region":"aZGOARvpwyGDRIFduqOZVlfZV","updated":"2019-10-31T03:44:01.148790Z","vendor":"LWQNxYOKgwIGXANThJcJPwCVD","vendor_name":"jlpWoMOmHqUfvTQBZdJDdTnzC"},{"created":"2019-10-30T18:12:54.939391Z","id":"49646a5e-5743-4f0d-ac77-9d9a5ab413e3","location":"CWUyzQDkrcAMFywLnTbgHlWUG","region":"LTJRIwUiarRvmprRzwbmbIcZP","updated":"2019-10-30T18:12:54.939391Z","vendor":"LjrwIeFRjYptRqphhWLGgfSRG","vendor_name":"FxUQdBWgDWYzpaQBmepoMrsMf"},{"created":"2019-10-17T21:05:27.529469Z","id":"76238328-3f68-4315-82d6-816ec07aaafe","location":"lwqFdLFzFSvcoUfvSgYMtsuVv","region":"awSWiyXTxvronnbYjsgpzgRvV","updated":"2019-10-17T21:05:27.529469Z","vendor":"LmSGFBiuqinAozOFRaQGkmhRz","vendor_name":"bNRFfWFoBUCuyUceWLDbdGayr"},{"created":"2020-01-13T16:45:14.410976Z","id":"9360bd08-8d08-4d17-960a-c2235571507b","location":"iwHryyPJNFdnNewCrplEWMSwe","region":"lqqnOJpmLTyXYvhgSJeDCdbfy","updated":"2020-01-13T16:45:14.410976Z","vendor":"MGzPHCzukBSktKodhlOERhTHe","vendor_name":"xkHAfGdxmtjioXAXJyAIurzkd"},{"created":"2019-10-31T19:42:18.327178Z","id":"016e3f71-81b9-4273-8fa7-d642d979b2c7","location":"efQfyCxBivyJbbtWRcOGRyIqm","region":"mdXfBKTpQLtJjbWlvQRwyByYP","updated":"2019-10-31T19:42:18.327178Z","vendor":"MqnEKvRXkYBvsvrzCQHQPwVvs","vendor_name":"GDJjeVADDXFwvjgRiEHvfQnyk"},{"created":"2019-10-31T02:59:59.633298Z","id":"c1d80d58-c955-4371-ae59-eb30b6e4bd74","location":"SlzATTXSHKSifKzMFpGhYiVUQ","region":"KFUydVeXxAJmCFhXNMiUrxQQn","updated":"2019-10-31T02:59:59.633298Z","vendor":"MrJcVCkITlCcEyFLxtJiZWJYK","vendor_name":"jGjsKItegrSIyTJQjTQjhTizb"},{"created":"2019-10-31T19:40:53.469696Z","id":"c9d6f9cb-b57f-4b0a-9945-a70fea57b0ae","location":"GdRYqpTRBXeDLLAmBBFpwradt","region":"bqbTMbZgeXNupSUcZXlQeMZTD","updated":"2019-10-31T19:40:53.469696Z","vendor":"MzntmylMqCbFgQhhyCUzbhWBf","vendor_name":"rYZOSkzAUzFFSuUzfEHpaIeUA"},{"created":"2019-11-01T19:08:04.811001Z","id":"efc6bcd5-4c56-417d-a390-cba128f80b18","location":"uCGQEFlEpoCjtxIncyIlNPULj","region":"YxcturGRmzHLqNlXNbHSaQlky","updated":"2019-11-01T19:08:04.811001Z","vendor":"NOyxieeHtlJYZgVXUNQsqAPfa","vendor_name":"fEVuVnPUWPuoxWxDHznGmoeGG"},{"created":"2019-10-30T20:21:29.877551Z","id":"f7a17500-ca1e-416c-a5dc-649374850ecf","location":"MbGWJDBAETaqaOTKiOfxCNVKf","region":"ZkkFyBGXaWoXdckfqUqILWMeX","updated":"2019-10-30T20:21:29.877551Z","vendor":"NPuZmluAKXlEjlKUJQMVsTkgR","vendor_name":"VjkBgPvzjtjkfHIqXLzCgXMWj"},{"created":"2019-10-30T20:12:59.279799Z","id":"d2e27073-4ae2-4c3a-87ce-14311c895a83","location":"QudZNTafPUzgCyBlXfIaBAXjF","region":"qYorPOcbnkKNsExfzqZjNTCkt","updated":"2019-10-30T20:12:59.279799Z","vendor":"NSCKGeSoSnRwaNAkSbRCVlRIJ","vendor_name":"nWALtyllFkmUBxtiellfkZxiv"},{"created":"2019-10-30T20:09:47.835848Z","id":"8d912aea-0f4f-4cd5-8d5e-af666d8e119f","location":"xQbzBLPtCbPCPebYyZUVplMcV","region":"hsetAmlvlIWGwCSSRtcicqVay","updated":"2019-10-30T20:09:47.835848Z","vendor":"NUUtrvWeXnVMafToQYKMcGFBM","vendor_name":"lTYjalMevgFqnSBMgzDOVVjHm"},{"created":"2019-10-28T16:22:40.519748Z","id":"a3908aad-237d-48c4-b63f-a87afa04d990","location":"YMZbSrBzquoXRuMvSRsUyYmYz","region":"BjcpEHrMmugOJnDYLtwVsXGQW","updated":"2019-10-28T16:22:40.519748Z","vendor":"NjVxHPJyzvvpOyGOcVbOQAMVT","vendor_name":"KzdOdvGapAwMEiDEDjnHQqhGv"},{"created":"2019-10-31T03:14:15.682517Z","id":"ea8fd82a-fdf7-4245-aea9-4e665c00d161","location":"WzbqbTMRfNuvFemrszvZkGrpv","region":"GnkJDgEYYrVFqWxGXhCeNjdMg","updated":"2019-10-31T03:14:15.682517Z","vendor":"ODoSZDpfFAnPKMoKdxFKRStvm","vendor_name":"IpvATStoRLysgyxzHyNAoOGod"},{"created":"2020-01-13T21:32:31.820221Z","id":"d972266b-6b1b-468c-a760-7cb16db48722","location":"MtOGUldLHoabLSLumhGDJdHqM","region":"rioZwtERZFsXlJLuBOeTVvtFM","updated":"2020-01-13T21:32:31.820221Z","vendor":"OHRPWTHDcPyEvwueGOiVwbPPE","vendor_name":"XjyKWHfTWIeZILzIJouGjWsDk"},{"created":"2019-10-28T19:26:28.197659Z","id":"518f3cc7-ada4-4eab-bb54-cd3a2aabaa43","location":"BCszTcTsEfoGksXYYKMuaWdar","region":"wbkVjasDLXjMyLOZiCGBCmtHb","updated":"2019-10-28T19:26:28.197659Z","vendor":"OyXVAoIQTeLznDSOSZpxdsPFg","vendor_name":"EluEHMQoUTdTGxVimwXJRmPKW"},{"created":"2019-10-28T19:33:41.716931Z","id":"323d96de-cd31-4a24-b6cd-c4910cd7381d","location":"NryBZBJYGUDfiTgQypXARoxzs","region":"lBAFdJjNBofavepJiLSyOZJms","updated":"2019-10-28T19:33:41.716931Z","vendor":"PDKaGJbQTYJLjZYntxCUAuiSt","vendor_name":"zQesLxvmKtbYkOHlyKwxxJhxT"},{"created":"2019-10-31T03:44:39.196708Z","id":"c5728856-4cb3-49bc-b3ae-7b07ebda0027","location":"BuxfQnkzVyaXldlgqbvxofvvR","region":"PmIWKKQDjOEfCLwFjmeFixLiJ","updated":"2019-10-31T03:44:39.196708Z","vendor":"PEwuIKcKzhmVQjCVeMNFKUFuS","vendor_name":"yhBFtvDezEODrQCITzWeOEalo"},{"created":"2019-10-31T15:11:40.466998Z","id":"776d17a5-fc8b-4d75-acf1-297ea35b7d35","location":"WwInihUVuctHRcjnnsqZlqOKy","region":"XlXyRZjrAdaePZtwGOhrybplW","updated":"2019-10-31T15:11:40.466998Z","vendor":"PLDolmiVmRxbkVNBqgSFPMTWk","vendor_name":"DCphIaXgBseilRrOHcBgMcHPK"},{"created":"2019-10-28T14:53:19.046233Z","id":"9689425a-520d-4bd0-95a1-961bb92a9067","location":"tjqDRIhUPlRoRPcOiRIkJnYRu","region":"GTCOzagtnKDVpXbnTQHkvAJEH","updated":"2019-10-28T14:53:19.046233Z","vendor":"PVyNoldbSXoAYEubfKYXGaeRr","vendor_name":"ndlIKgJtyqTuyavCnTPjBKcUP"},{"created":"2019-10-31T15:11:28.852462Z","id":"2db20edb-8728-4c34-a88d-0529fd760f32","location":"wyZeMRVZFTxlXDJHMKAelpilL","region":"rZzwkQdoNAliwXJFKhAQOmQde","updated":"2019-10-31T15:11:28.852462Z","vendor":"QDRSnlFdleAtVRyTaQtnxJpak","vendor_name":"drdLOvWUdChTlwKuOyEcAryTC"},{"created":"2019-10-31T03:34:16.397343Z","id":"a61804fd-0067-46f1-9113-4f0665bfde70","location":"AbQqMlDeBnnUrrahBCuyaDzAg","region":"qRfXEwLHiRVlOfzOSYYhzaxYd","updated":"2019-10-31T03:34:16.397343Z","vendor":"QvHRXjqdRanElcIWvLunjYrEv","vendor_name":"KdXfgRZKgvuQcwAqgxovxIqMh"},{"created":"2020-01-13T22:45:21.132025Z","id":"72956036-54a5-47f2-8db3-c4a17a2238e0","location":"obHgFCqFOUnkGjhKyeJNRLjWR","region":"gsapdZRHholmCneyftiWptfoA","updated":"2020-01-13T22:45:21.132025Z","vendor":"RELyenwUQbOXDlSzGGJxgeJIQ","vendor_name":"YcFUflkOuRwNnwJKuhlsRnCCs"},{"created":"2019-10-30T20:23:40.516840Z","id":"f0cad974-835b-4c2d-8761-8b560cf92831","location":"kwBifNmBUZQYoubRBeznpxFva","region":"uUCjqtOAvocYkUXmpSyntLKXV","updated":"2019-10-30T20:23:40.516840Z","vendor":"SEtgXvChNMolDktrLBaJfUXcq","vendor_name":"fxGzaEWcbMZoayAYYAPHxEWLY"},{"created":"2019-10-28T14:54:40.455586Z","id":"fe0eb14b-ed2a-4377-b492-49dd6d0abbe2","location":"dnoyqfhiZUdMWQpsTqGmuxttw","region":"jLnBNDVkHbWAxEjfaROmbDUpY","updated":"2019-10-28T14:54:40.455586Z","vendor":"SFkSgQVoDZkYHvzVISvCbBkcG","vendor_name":"sfccxaYYxKbvXxDcrCJwMaLPO"},{"created":"2019-10-31T02:51:30.382251Z","id":"db2239a3-b2c9-453e-b099-e8008c99c362","location":"lUcutiHyGVXqzHZtBCAhscAwY","region":"gwCncdULTTAwMaUkjSlGJYEsg","updated":"2019-10-31T02:51:30.382251Z","vendor":"SHgmOeDTBVnfKvWRVTlWUWEsE","vendor_name":"wClLtkKMZFBlsZnxbmseJuLmM"},{"created":"2019-10-28T16:54:39.596783Z","id":"88450bde-05a7-4f32-bc20-172062f40610","location":"WkWItunCSgRQlhIupEjUeGYEQ","region":"GSysrokcFJwIJREpgVKnbqSvh","updated":"2019-10-28T16:54:39.596783Z","vendor":"SXaLnPkBEZoQTBplKLcuzdwkI","vendor_name":"AOvGQfVailxEauCBVdtfOVOPo"},{"created":"2019-10-15T19:41:25.728721Z","id":"3ea9a214-8f76-4da5-a3d2-09044b690593","location":"fdXwrrwWDVMFDhfOZBMCrfLif","region":"amPUQSmqLVvObhDJhZQwcsMRt","updated":"2019-10-15T19:41:25.728721Z","vendor":"SuwiueQfRlHstKcnlzNWTDncd","vendor_name":"CuXvRvAfPNDnjoLuGFQuBucXp"},{"created":"2019-10-31T19:00:12.721079Z","id":"0a7401aa-bce3-4b61-8d53-c03c02fc3b10","location":"qIqMmHKKgYzrNrYthkEeagZFx","region":"gjdjZSrNWZztIWlsJaIEnqCmS","updated":"2019-10-31T19:00:12.721079Z","vendor":"THmeFPUfKYFyCReaSZHQkrSTQ","vendor_name":"EUdShbxxLkHrbpUmUbsdZlHiz"},{"created":"2020-01-13T21:27:55.301030Z","id":"2fa781fb-84e6-4d5b-a786-ca8bdc27284c","location":"dyCUfnFMkAZZenXrPqohnjPnV","region":"JdtkCxbCYxJSLDGfuIuogsoqv","updated":"2020-01-13T21:27:55.301030Z","vendor":"TUKkgsySsWCvuCmRJpjKOtXqu","vendor_name":"EqtKdOZmfJsLAugoIOZqHBtEH"},{"created":"2019-10-31T03:05:57.907632Z","id":"1384355d-5cb8-4bdc-9ea7-772db8fc90dd","location":"LKZNaTUhuXtzIygSqSgUJHRdO","region":"LVVrKyVOfVPnDMayngejiHvor","updated":"2019-10-31T03:05:57.907632Z","vendor":"TXAjabKbMnCFoteHtRFxaYWvZ","vendor_name":"xNrcxIRfkVFJgFyXSBTsnrECv"},{"created":"2019-11-01T18:03:12.963988Z","id":"383a1e41-b923-4ab6-b1b6-1dc15afc5f52","location":"LByzWUAoMrTTIvjigVyWgvtzQ","region":"gfRVVidGvDeFSaBRBNiqypayV","updated":"2019-11-01T18:03:12.963988Z","vendor":"TestDatacenter","vendor_name":"ssshRHccKiiHHHaKHdXWsiScY"},{"created":"2019-10-28T16:31:24.040114Z","id":"23095256-a460-481c-b836-7f6bd4b83f33","location":"EovbtFhxAujqyEOQnqAnaJuzM","region":"VpyBUvOdlEZySErgmqfhbrlLQ","updated":"2019-10-28T16:31:24.040114Z","vendor":"ThfyMpiwxSfxAbURAkiyYntYJ","vendor_name":"SRfrtkGkoBEaEfCDYwjMbpzUO"},{"created":"2020-01-13T16:43:28.499039Z","id":"ade745df-7a01-43a2-a503-64035f1416f8","location":"GhaNJLPVoVQLiucLBNcPoSgYt","region":"zfNJcziOpGXthVlpdVrpHzmSq","updated":"2020-01-13T16:43:28.499039Z","vendor":"UFHUjzllOzHqwmhhkTBMGPUHc","vendor_name":"ZyYYNHYfJKIfbLfThkNFRyAPC"},{"created":"2019-10-31T19:03:24.479666Z","id":"c1890040-6ce5-402e-ab7c-f7b1e7cd9850","location":"QKHUZjVuwTIeZbndgNzRyQPny","region":"BuGSkvHGVycsWNhbnRLgwSQHu","updated":"2019-10-31T19:03:24.479666Z","vendor":"UdbbGpFuUZAVVaMlxnOamqBgX","vendor_name":"DNeJEDCEHkWnkIvgivviYCGjY"},{"created":"2020-01-13T22:30:05.418293Z","id":"52e9f8e7-d8fc-4217-8d62-99055a06fbe0","location":"SISGnBfevtOweEGPRLclcBmLF","region":"dcfiCyrGBziEpGuaqkvkBRxAw","updated":"2020-01-13T22:30:05.418293Z","vendor":"UvDTuYCDHrTKLCYvfeMMNIudO","vendor_name":"hQInTLCNElVOwkYLKnUWYRzve"},{"created":"2019-10-17T15:18:15.045595Z","id":"5942ed14-0028-47b0-9296-28b8b23e6416","location":"pQHzJGxqtaFaBIyDIMVPjpzAm","region":"FSjCDHxiDAyjdsIBZPPFFlmdj","updated":"2019-10-17T15:18:15.045595Z","vendor":"VChTscdPsPznrutWWbWvDdSkn","vendor_name":"ENRRrrabilpBGZlVtysFKzApO"},{"created":"2019-10-15T16:35:11.040993Z","id":"f0660ecc-8bce-4f0c-8217-daa895e38691","location":"EzSkHYbEagyrhJLBOaRxaoRXk","region":"WAevCDtPNYtGCaiDzWXsnELii","updated":"2019-10-15T16:35:11.040993Z","vendor":"VFdovLZFpadZOAAZwwtLTchQi","vendor_name":"DmYwvcXZcyKtLdNQFUmUoqpbb"},{"created":"2019-10-28T20:32:53.697950Z","id":"0ddf080a-416a-45c4-bdf1-edc796c985cf","location":"jDtrxBNXVEiqIevPndUjfznaF","region":"UkWRhsYZpkjYUjaQjvWODHcHC","updated":"2019-10-28T20:32:53.697950Z","vendor":"VGtOPeKwnuAaHDeNtmsEqHgeH","vendor_name":"drHCefyKoRWKWNDgJTDJhgjqZ"},{"created":"2019-10-28T16:31:45.690878Z","id":"b2323218-b496-4c94-868e-b45c9a1cf2f3","location":"iuHdNSprBpisylhxAkefZBnWM","region":"WFMfrFUyYAxiWNQhtKXWOFjGv","updated":"2019-10-28T16:31:45.690878Z","vendor":"WGmJWrKxzCGKtVvUkcSiOdZfH","vendor_name":"nZrPjsFLUIeIXeKVLjvvFyHsT"},{"created":"2019-10-31T14:14:15.104436Z","id":"8a9fc809-3d0f-4eaf-bacc-be66ccfcdd2a","location":"ZOZelcVINcGhTWdBLsSISVbEE","region":"nEICmADNlQtCdoaQkArAiAtwX","updated":"2019-10-31T14:14:15.104436Z","vendor":"WHpcLEBJFuGOLCMsPONRCvubh","vendor_name":"umMabEEEZOUNWtuztnecpBmoA"},{"created":"2019-11-01T19:00:06.025395Z","id":"886ed1f2-cb8b-42f3-b3d1-4b58f8e1994f","location":"LrmeWfpDxdVPOVvqsBSxoxHCA","region":"dgnIKVNpImVIyBfgoInZmYdhj","updated":"2019-11-01T19:00:06.025395Z","vendor":"WeTMUtTGoyBGBtlWldaBPxXcA","vendor_name":"bgfsLafYMwRPSorUNSSTmMEan"},{"created":"2019-10-31T14:21:43.558361Z","id":"7bbf78aa-362f-4db7-ab8c-88b16594f1db","location":"cgBLTtYBkFXrsviYlYSoBHprM","region":"ZPAlfnLckXColBulhPUzhsAVx","updated":"2019-10-31T14:21:43.558361Z","vendor":"XLCiPiiWhMkVaveaKZeNxAFRY","vendor_name":"gaDOVDlZnCLZrwkuGeMeoOtJv"},{"created":"2020-01-13T21:26:40.814891Z","id":"df433403-d279-4619-8433-4c559d0325bb","location":"FhIWndjZrjdomFzfLjybnoSUP","region":"QOhGHewKkmUFOEQcCJPwndAxg","updated":"2020-01-13T21:26:40.814891Z","vendor":"XLtdZiJMjEmzxRTgrxBjtWnyT","vendor_name":"tfkRQYDzyomLJGUtjFqSDUjRJ"},{"created":"2019-10-15T19:42:46.833981Z","id":"f5af60b2-c45a-4714-b81b-e0baae39f24d","location":"fKfbUQqjRzIvENXukiZgxPoqZ","region":"EMJvAoAGrOZdJmqQrPgeQsrzi","updated":"2019-10-15T19:42:46.833981Z","vendor":"XspKtCcXinehlTFkNJjSoJySE","vendor_name":"dcEQMtbwKdiBNHDPQhXWYERDA"},{"created":"2019-10-31T03:41:32.354564Z","id":"5b072996-c518-4b70-ae3b-4fbe911f7585","location":"JutvGjRuFyaUqlFNVtFniUcst","region":"FTxniUUPJbJOwJgvdtUaKaeeH","updated":"2019-10-31T03:41:32.354564Z","vendor":"XvsQBfYAJZzRWwYYkVZXbOGUc","vendor_name":"pCekarvoVcLNQHtHCifdRQkLW"},{"created":"2019-10-31T14:24:08.814642Z","id":"038fb0f4-8775-449a-b08a-89a9b8b0cf58","location":"wEKdZGgOxkRHDOJOiFqlyXZPq","region":"RbtwZPQGOFCmfruLbQpbiNlYZ","updated":"2019-10-31T14:24:08.814642Z","vendor":"YCAJOECWShpcXPyQCxJLGMbwd","vendor_name":"EywCERJDgyEsqVpzxVeweThiu"},{"created":"2019-10-28T20:07:30.179811Z","id":"d4e1e71c-becf-461e-a081-380cf2019f4a","location":"IdjxQqvGDcoiyvMzkicfiFqhc","region":"nOFJjVLJsIsFywcrHunjEfXEw","updated":"2019-10-28T20:07:30.179811Z","vendor":"YCvmNSZgBLctJwpjioAXckOGo","vendor_name":"oizwqzrsnYNlXEwJkeuhUdXEt"},{"created":"2020-01-13T16:41:55.529203Z","id":"4dd8899f-67b3-4784-b19e-e541b0d6f1fc","location":"GRyXAGbNGlCLyBNUDjeHkNZpP","region":"OHKtIqesCNiIYMDledVJpPxnA","updated":"2020-01-13T16:41:55.529203Z","vendor":"YDcIcUWjeTDXdMOjjnUNyRHbs","vendor_name":"NOySpeBZbkmFSYnvigbufFMSQ"},{"created":"2019-10-31T03:24:43.183491Z","id":"c78ce907-6ded-4293-bf73-a8756461f676","location":"ePMrooAEvKoVuvfBBAWhaFRPK","region":"sJTPdsgZlmSYrWogElPIqdjxL","updated":"2019-10-31T03:24:43.183491Z","vendor":"YTBwlRCvFNLteNGsqhcdzuYLl","vendor_name":"goHpuRjSgEjmBKLahwifzQzVB"},{"created":"2020-01-13T22:31:59.415002Z","id":"9f53d480-5ba9-4a28-a29b-026cc3ffe780","location":"ODKqTPclYVIYEjnhkXuWgCsSK","region":"tpPkahzkzOouhlFBnzEwhBlbO","updated":"2020-01-13T22:31:59.415002Z","vendor":"YWLwFQIRXRsOxVkVdANRtnZXc","vendor_name":"GvegBKxrJahKfAfJYydnmzAaW"},{"created":"2019-10-31T14:15:00.441752Z","id":"56aa74c7-5ff7-4c5d-9ec9-1fb2f0495e03","location":"aanTDXBjcfjipVWRQbpsiLlVu","region":"IAWDQpEnwSaaWGuOsTSsAZAIk","updated":"2019-10-31T14:15:00.441752Z","vendor":"ZCZeNmEQZEBkDhbRYBrGtdBQb","vendor_name":"fcMlwsMeABzTEUysoEvcWffYF"},{"created":"2019-10-31T19:38:45.585565Z","id":"81f00a54-9461-4571-877a-8c62fc7ac3f2","location":"WjDOzVXZsIGpStkmnjeCOphcd","region":"OzGrREhtMmFvFXRZgkGHBlfpx","updated":"2019-10-31T19:38:45.585565Z","vendor":"ZGxYpNhKtffXVgvWrpuiEHDZK","vendor_name":"EBPPLSbXKStvNgcNlTBUBEVxe"},{"created":"2019-10-31T19:00:25.036492Z","id":"2ebd682b-892d-4867-b869-c978c927d942","location":"gdmduXdlSaWVsEOIUsnAsgsSo","region":"UgBPVvvWfFSnGfXqHFqKBbeLU","updated":"2019-10-31T19:00:25.036492Z","vendor":"ZHsIvVdTnohkGcEOnFPBmDTsI","vendor_name":"gtxsDAggsigedzuaooiZDehLt"},{"created":"2019-10-31T19:40:28.023405Z","id":"069415c6-b4ff-4572-b6e7-e6d609f3c834","location":"VGsjPdBkbUwISfFYCBwhIvLvk","region":"KAzOeqxfBmUEEVaOxWhChHhWO","updated":"2019-10-31T19:40:28.023405Z","vendor":"ZOAIqVELnIaqzeKhfZTxMeCva","vendor_name":"UhPKdgPpobpyFMXHdosvetkfI"},{"created":"2019-10-17T21:06:22.473863Z","id":"be0da3f1-0156-43a1-91a1-909585f55c4a","location":"NXSMvyuuXcJRDHhiNBIOwxGin","region":"kjbsbjCXxqTcKdycEeWIrfPGU","updated":"2019-10-17T21:06:22.473863Z","vendor":"ZdJDlAZHFBYrHlELmxRXzCLcz","vendor_name":"TpgXYtTssDKttweRuyZZDSdHs"},{"created":"2019-10-31T02:54:25.544391Z","id":"5ec0eaa2-fb58-48de-931e-99216390572c","location":"hbsJliXbifqgwwlQuzGILwspx","region":"CMPYzVKtIBQsDMlAmYRRCFUXj","updated":"2019-10-31T02:54:25.544391Z","vendor":"ZkhpbBksKHwWIFTlnsYjgPGkD","vendor_name":"wCypTdLvSFbOSIheWzqOFuvnD"},{"created":"2019-10-28T18:24:24.762910Z","id":"143cfb5b-5028-4b0c-87cd-307fc9878233","location":"EtRkEsNfgSddUeJPgxsCXPMus","region":"jFzLcqMGJAYaDUBbrxUtVIkZm","updated":"2019-10-28T18:24:24.762910Z","vendor":"ZpQeXCqclNXbEoznptzLhBsEb","vendor_name":"qOwhsCPIIjQiRCtEUXLfPwmWU"},{"created":"2019-10-31T03:19:31.272229Z","id":"195b167e-258a-43a1-adfc-10b27d3b6ff3","location":"fepVDPYVTeTkHBrumyNibKuyO","region":"JhWplEoXDXQkwRJkTPKjtYQHN","updated":"2019-10-31T03:19:31.272229Z","vendor":"ZwuKzbpMTYECWlwRwahfBPxKf","vendor_name":"LrQIzGgvTXZrSBiBzKHnbslnC"},{"created":"2019-10-30T18:09:14.454852Z","id":"0ed554ec-8758-4be6-8d63-8e640f1ea0fc","location":"jCCFllxaYIvlroIorDuzQeQqh","region":"GKDuargNUkYnnlENxunchwqZi","updated":"2019-10-30T18:09:14.454852Z","vendor":"aIojsBNKQVJgXRrQVpPEynxDk","vendor_name":"DjutWFqmUCpkuFGPERmFJEMOb"},{"created":"2020-01-13T17:22:04.770411Z","id":"b074ee84-e98d-445f-8b15-494d760bf113","location":"uTeUYAOxvUABmIjPKBsicLVDI","region":"SIaIulGKjWZhsrvnsWiOQhYaX","updated":"2020-01-13T17:22:04.770411Z","vendor":"aSvKsQxovdSTdPmBZzPaBioST","vendor_name":"yDbeodylzFIPUukGefbSeOcmk"},{"created":"2019-10-30T20:16:35.444113Z","id":"0c119af8-d6c1-45ab-8845-8da4bd8e052a","location":"uSsfIaIovexGiqzXSBufxECYN","region":"sFyBYFfKGCwfJjsdMzGaAIxIh","updated":"2019-10-30T20:16:35.444113Z","vendor":"bPrnkqEzMNxribjYAOhfLvlbd","vendor_name":"cCdRTCGnHTRDZMzccPCXGTmkG"},{"created":"2019-10-31T14:24:31.264527Z","id":"a6aa3b24-ccab-42b2-bb35-077757cfb44b","location":"PHCkVBtVXmmhVuLnrxzqrlYXt","region":"vYvAjmmvvvxPcWfaIUoHSPsrC","updated":"2019-10-31T14:24:31.264527Z","vendor":"bpZISHscMeWEKUkndkKIJgGej","vendor_name":"clhlbPEDZrirrMxIdZaDTOHFf"},{"created":"2020-01-13T21:30:44.239979Z","id":"507e69e3-2f76-49ac-ab4f-cb23e80c2450","location":"FxhtymhbCmbQyEIElkQKYDjoY","region":"DYiDxiJkEHXqnYMXwjFYjnMil","updated":"2020-01-13T21:30:44.239979Z","vendor":"byAEoYmTNVxUnhLsMAChcUIyi","vendor_name":"bdFUVqMuLymtnQkoePpBYyrQr"},{"created":"2020-01-13T17:27:42.872550Z","id":"272e71dc-79ae-4c34-82fc-1c94227bcf11","location":"KKvRFuKtECJpPfOJgZFycreMh","region":"HEMPkgtKIYmbOHENnrnnhysAT","updated":"2020-01-13T17:27:42.872550Z","vendor":"cjsiEIZfmguaSOqGOCnqiAHaW","vendor_name":"xzmOqfiqRoQCmrNJxzXaJqDqd"},{"created":"2019-10-28T14:45:10.503632Z","id":"86c74065-5a7e-455c-beec-800b4e5df1ab","location":"JJcOUSYGcgibhhAgJcfkUNsGA","region":"SyotcBAbMJBavDGeHeNBbvEPk","updated":"2019-10-28T14:45:10.503632Z","vendor":"ctYmwsEhFmnpcsYFENEMaJIEX","vendor_name":"UrHVsGZZFgzeJSheNJZrHXOgN"},{"created":"2019-10-31T02:55:25.295911Z","id":"3016b422-4790-4416-b066-eaf9baab2b45","location":"mgYDMTYFhUNVugusXDNTBZkWa","region":"TYqHEkqxvHjzqIwrDhgQxJHTu","updated":"2019-10-31T02:55:25.295911Z","vendor":"cyTiqRURbDpmhBEUjtoyOlFHP","vendor_name":"fQSpqilyviCsSTzCYbCPKbGmt"},{"created":"2019-10-28T20:07:14.666890Z","id":"aacdd36e-16ea-4e56-8bb2-cd70cf42c893","location":"YSKxjzPhwUNeqjHGdbokgdEcv","region":"ylkPwnATJkYNqcdBRtrZAbAFD","updated":"2019-10-28T20:07:14.666890Z","vendor":"dEBEBmDOylCPeneHDNCWauCRh","vendor_name":"oZGgHfresqMLBDzArcdJCPqqs"},{"created":"2019-10-28T15:43:08.142071Z","id":"dc5bd5e0-ed2b-4389-a9e6-ca02afbca357","location":"LwLAbOIItDxWfwtmXeFsatXkf","region":"txMtvHrvTQqHmjRDIKNuIivVv","updated":"2019-10-28T15:43:08.142071Z","vendor":"dWKZVTJoReyZODLGVKOCvXjsa","vendor_name":"vYCuLYPXgaeKGcOLLXGlveNBe"},{"created":"2019-10-30T18:09:41.332944Z","id":"5e4f1a4e-65a5-4e17-803d-84a7543bc15f","location":"MvhvisYeZmxhecTMeOZBmNuOk","region":"GbtWGRxjNqiVfUoxzslCylVMl","updated":"2019-10-30T18:09:41.332944Z","vendor":"dfRyouUanGbJvSfANRJlPbVTu","vendor_name":"nYklBdedFLhevDtNGWhUhchVH"},{"created":"2019-10-15T16:39:23.507876Z","id":"3eeabbdb-998a-48fd-83c4-9d6553b23290","location":"jjcojYIVkLsObbDNJAtVbMqrO","region":"dZgTqPYywTqusGeVwHdhbMqad","updated":"2019-10-15T16:39:23.507876Z","vendor":"dkRYHaZdBgNGhhSxaPfETSbnc","vendor_name":"ZmlbojSyolcpzwuvAroPCInIs"},{"created":"2019-10-30T18:07:46.461159Z","id":"e3dd52a7-cc5c-47b2-bf95-3ab1ec967566","location":"xHjZzwlUopCkLyxnYXqUvYHRk","region":"TctVtTFbuhQGfXsSaWJtsCysF","updated":"2019-10-30T18:07:46.461159Z","vendor":"eWPzxVVUNaCSkyAzZCoUXUBxl","vendor_name":"fuzRqcftRKUemQhPZajSAqWEV"},{"created":"2019-10-30T20:23:11.596337Z","id":"54800858-4fbf-4f09-8421-824260a49be3","location":"JDmOZtTAqMMOdzhVFVjEQWWBn","region":"KgNNxihYQbNEitkVYYmFUmKlN","updated":"2019-10-30T20:23:11.596337Z","vendor":"eWrneIpMwrXscdkAZBKnQLMkB","vendor_name":"YfKOovFtIMNVpWvAdxPxDQEWl"},{"created":"2019-10-21T16:15:14.248947Z","id":"6b1e0c52-6681-451f-ac87-5e61affe9f00","location":"RGOIFseiSXCZDSXSHNQPnkxTk","region":"LdyXnrHKqrOgoErGAGdtyHZEN","updated":"2019-10-21T16:15:14.248947Z","vendor":"eYUiXhqXiAbuHFWiqLevkUWAh","vendor_name":"siidxicobeubJcUbXeDtxdgsO"},{"created":"2019-10-15T16:37:26.470806Z","id":"d22740ca-f2b5-415a-ae8f-94c6dc23b89d","location":"bQRpONXOxJeaCFjnBnrcKxTKt","region":"YcpRxDSkaIVeyNGVXKYFhvWAZ","updated":"2019-10-15T16:37:26.470806Z","vendor":"ejHxJwZShtWRhIjjDSbgXVKto","vendor_name":"IGgWGGpgYPoFVxqREovAIwRoV"},{"created":"2020-01-13T22:08:41.106814Z","id":"a5960645-d999-43ad-b82b-602e6a856210","location":"HkUJFoChKuHyjDYCUcrcWQQoY","region":"MObtySNCMlLVrcrJRxRRhEAER","updated":"2020-01-13T22:08:41.106814Z","vendor":"ekTRbCyZpXRHWaLoXBMDLaiEN","vendor_name":"FCYGczLDYkHHzIueyttSYmsTK"},{"created":"2019-10-28T14:52:19.729929Z","id":"a5c0c517-6747-4e97-8c99-8c7a5667435b","location":"WxguuIasooQBwHIPGfMkDUClx","region":"ZbKIqPfhUefdGwgzhUvcQpfbv","updated":"2019-10-28T14:52:19.729929Z","vendor":"fAKdQGMsJUChpiZXCcmBuBCSd","vendor_name":"OlIyUfRiwSgUYyHjMMhVbCMHK"},{"created":"2019-10-31T03:33:45.310825Z","id":"220a1163-656a-4674-987d-0b94487c4299","location":"cuGMnRyJEAaTohSjQWtjsVCId","region":"KEVYHDUFDhfHaLycBxELrvunq","updated":"2019-10-31T03:33:45.310825Z","vendor":"fTTHIxllEZjygOKmnxoeFZmNi","vendor_name":"KfzBpOzRzwkojYySZsDpuoUua"},{"created":"2019-10-28T20:42:19.414060Z","id":"1e648300-33ac-44aa-abc5-4e0a94426217","location":"iLfaMWhMNBsEfMaaZQTDQzqLB","region":"DuijwHcIWUWkUNPgJjZThIXAl","updated":"2019-10-28T20:42:19.414060Z","vendor":"fhIUZQjyeteueMUnIrpwLPuDd","vendor_name":"PYdNdfQfRekwBeHlRdwEUzWHz"},{"created":"2019-10-24T18:31:45.173959Z","id":"bb241ed2-50c6-493a-9339-f7ca879f3774","location":"zYZlMALDwCGlCQsfwznIajTOh","region":"mtdljgexgEVQmcizyVRIzDsEO","updated":"2019-10-24T18:31:45.173959Z","vendor":"fpkupZSmhFVZJszayyYAyTeoH","vendor_name":"toyMrcVBjolRPMgpulxdQeyQI"},{"created":"2019-10-31T03:32:37.572739Z","id":"06015839-c137-415c-a004-8c4780c21584","location":"KxyUDqWWJTnKNjKLRxvvXoQlN","region":"mTjTvYoERoaNUBjNMJlJHGdTM","updated":"2019-10-31T03:32:37.572739Z","vendor":"gmmUMZjCzvtRVDPMsAwBFyEMG","vendor_name":"RiFHpgeXFnAXgbfzvLdUqZRLK"},{"created":"2020-01-13T16:49:07.136312Z","id":"294826b2-3d8b-41ce-84bb-69295557d9b2","location":"BejWITIaiPAlBLjixsLxJvXOu","region":"ZWIrBakvoYPXOxyQDBdanyStP","updated":"2020-01-13T16:49:07.136312Z","vendor":"hDfdquaxFSOxhpkxqCyekyMNI","vendor_name":"yIhSVNLoUtFgzTsMwjlvrRWCJ"},{"created":"2020-01-13T22:27:29.488025Z","id":"82a52efc-419c-4a6f-aaf4-2f67db6e0072","location":"WMqbFwCBShSJxcVpmXwQbuTTS","region":"FtbzapkcnOCcMDbGNxOhXDZEA","updated":"2020-01-13T22:27:29.488025Z","vendor":"hJZeBhcNepxyukHGWmgBepkFG","vendor_name":"BGgEqMxqzqDxTRjSmIrVEuZGK"},{"created":"2019-10-31T03:23:28.496247Z","id":"4dd8e387-fd74-44e7-815e-e1ab81138d50","location":"mNdmeRfkOUZRdPiaTyEojufEB","region":"BqePInKSOIcXLiLlRTCfzPPRB","updated":"2019-10-31T03:23:28.496247Z","vendor":"hknEaTjlFJJAVysyWfPnmpVbV","vendor_name":"iFNuyNQSodozZAUzmtTxaJnfT"},{"created":"2019-10-31T03:15:51.784746Z","id":"39211320-03b3-4d69-9bc6-2fee6a12e20f","location":"DlmWgokXdqINsRfvLMufhmeTO","region":"obtpzlFEYCwXhKLmstOglkWFI","updated":"2019-10-31T03:15:51.784746Z","vendor":"hqBMXjgczNyLFrWYCPteofArx","vendor_name":"CCOyIfGDwBuxkYeOoAZjyXkgl"},{"created":"2019-10-31T02:57:48.943432Z","id":"302467ee-c7bd-4a2b-a92a-8688710695ec","location":"TTpHtdawKuHIOPDWRRDomzBdA","region":"hhkbjKIsgkKNrbCtmmskGgBKs","updated":"2019-10-31T02:57:48.943432Z","vendor":"hsdNWINdpAZLvlcCCsqMWANqR","vendor_name":"DuNTChjHSujfhJSaupqFgdQUp"},{"created":"2020-01-13T17:25:41.894174Z","id":"23c598e6-98b7-4974-b68a-440390900eba","location":"YQkLTjYOjsNNwDVjHWySMKwgT","region":"ysnFKsWnlYTYFXOFrfEDkqIuB","updated":"2020-01-13T17:25:41.894174Z","vendor":"hyrQIpZBRQBEXuxpjIJtvhrkZ","vendor_name":"FHEASTcoXkjQEldlHjQgZctzT"},{"created":"2019-10-31T14:20:58.842209Z","id":"229e95a0-c439-46c2-a725-3cad31f28d1f","location":"EyEyYLYFPxPkDykcXOLZQfrPf","region":"hlylRjDJSUZzJJeAUEAjKqoQZ","updated":"2019-10-31T14:20:58.842209Z","vendor":"iFPaVPmMokRFBEhyUANkhxVvz","vendor_name":"aAuyDxIJrTiyJnPlDZnHVDqSm"},{"created":"2019-10-26T18:36:29.419925Z","id":"2680e58b-4b51-46b9-9c9f-d0c49f31403e","location":"wFqIzFWmqYiraMJDSPcPxwgsz","region":"HezUiUeINIsBVkNOxvYaDcBSn","updated":"2019-10-26T18:36:29.419925Z","vendor":"iTSOGvthDntBbjKiAJWclfqsU","vendor_name":"MncztNfSCvBqGnmmBHvcQFyxL"},{"created":"2019-10-31T14:28:50.046707Z","id":"4cd6ac2f-8c7a-4fe8-bde0-e2032125b478","location":"FcCoQRFMzUMgRDbeoxhhjXvWE","region":"owtDPoRIEfNCkLRYXMILQPOgU","updated":"2019-10-31T14:28:50.046707Z","vendor":"ibvngLMlbyULtBQuQJuBXqePs","vendor_name":"ElGGigemOTuWZFAUBwefjoGmt"},{"created":"2020-01-13T21:29:20.889658Z","id":"6ca1b8c5-567c-47fb-a831-162450966d66","location":"LZeFQQOORhcsYxfGvJCagNnlO","region":"HzWRMkAVCqTclvctMdsCNnDRM","updated":"2020-01-13T21:29:20.889658Z","vendor":"igpbhvVbujcUkiHBUCzxvsiyA","vendor_name":"ISojeHYhvLIWRIUkWyVnzAMyf"},{"created":"2019-10-17T16:23:45.759412Z","id":"df17d213-f8a3-4ed2-aca2-061d5f3b123a","location":"NFhvcDxlzHfcGVjjxzAOJojvH","region":"aOoGnRIfSICAuCVRnfebRZQQn","updated":"2019-10-17T16:23:45.759412Z","vendor":"kOmTgyciJmJIUUfNSvVwIvzVN","vendor_name":"wtmazgtgySkrdvvJqtLvODkBL"},{"created":"2019-10-28T16:30:41.742803Z","id":"811c087e-62ed-4a38-9cd8-10322f987759","location":"blUlkEmHRHIRAzqezXAVMeIFi","region":"YKQSkkTfIkEaCwTMJOcRooKGa","updated":"2019-10-28T16:30:41.742803Z","vendor":"kUvIAIirxvsRloIFpbaUhuAsz","vendor_name":"zIMcjNkXzaSEBSlajymrzKxqz"},{"created":"2020-01-13T21:28:21.058427Z","id":"9f0ff7bd-8821-4f24-aac6-b1854a0cb82d","location":"btCPWUSzZcBuBmmZmhRKSGTLm","region":"oMbcpOyluiToqZdYcQlmWBhlQ","updated":"2020-01-13T21:28:21.058427Z","vendor":"kheiORELzDCnUNrNJciAhJrLg","vendor_name":"YbTxmjantxANlUzyCbLLIZzpJ"},{"created":"2020-01-13T23:10:58.910729Z","id":"01740fca-cfc5-499e-bec0-09b61d5dbde9","location":"CLbHNppSswlydyExUWSFXzoUJ","region":"htcuPoXVxODircNayeQNNqczL","updated":"2020-01-13T23:10:58.910729Z","vendor":"kjbKKcKrPDcovxOitVZboPuQY","vendor_name":"mcYdfywQlkjUBSWDwPDYpMseC"},{"created":"2020-01-13T22:43:30.057498Z","id":"47912904-d398-4151-9a3b-8431d33a0512","location":"vsMKhJWRMXziuZRwrIoksSrMc","region":"yJydAFpoBpOcVchfHUOmwBKPt","updated":"2020-01-13T22:43:30.057498Z","vendor":"lKDquYGtgWaBhlnSyhboWfsJJ","vendor_name":"BFwRwLFboQrbpKLmpZXAMeghL"},{"created":"2019-10-31T02:53:35.704697Z","id":"cc94689e-276f-4717-904d-64abdbaa583c","location":"YNBPFroAnKfuZzVZgHNqgcNrQ","region":"iMmlPwHlmlITZFRYXcJeJYMdI","updated":"2019-10-31T02:53:35.704697Z","vendor":"ldSxERCvETXZEiHWRywXIuNLx","vendor_name":"RsqgivAuXmNKoHftUALCgjtbH"},{"created":"2019-10-28T14:58:11.860413Z","id":"1b188fdb-04e0-4a16-add0-5047e82ebfe5","location":"fxPdnLIZVqeLrkBWhmhKoyJjg","region":"RpgtXeeAUccFhQQXpGqYoSvOn","updated":"2019-10-28T14:58:11.860413Z","vendor":"mIWnCODybEhJaWTDvnukDsmKW","vendor_name":"xjqECeTVFGiPrqDiUluMeLGRZ"},{"created":"2019-10-21T16:18:52.017004Z","id":"fbb97020-9ea8-4aee-b0f6-afa3a684fa47","location":"WnQJzQAjDXzOMlFxAoyNdcSqa","region":"bFGyrJXdXwXgNgTYNoqPQCgOE","updated":"2019-10-21T16:18:52.017004Z","vendor":"mKdaheNEXTlMCTNDJdnaiaGHG","vendor_name":"hcHnrqDBSFXprUVmLlXoTDCNr"},{"created":"2019-10-31T19:42:12.072409Z","id":"8f1ffcb7-8c82-484d-949f-fd0f64e1490c","location":"yvnhFuTXJHHTKkyVsHabVJJmW","region":"wZFYNLXzyutcPrBiWtfeQxoty","updated":"2019-10-31T19:42:12.072409Z","vendor":"mWzYowkvmCqBTdsrQVDdmuRXg","vendor_name":"QLknPcHcTdqjHGKHqXrrFypDi"},{"created":"2019-10-30T20:25:08.277708Z","id":"e5a07436-5868-48c2-887b-e916d19732e4","location":"kAfWGPZsuZiiKaSJVRnIxDIeJ","region":"BZjGZImJVuGSBlHFAmouhEkVo","updated":"2019-10-30T20:25:08.277708Z","vendor":"mqPJTLpvOXpYEuGwnhUGXqQpi","vendor_name":"RemAAwCayoTLdHudHXJafsoFo"},{"created":"2019-10-31T03:32:11.087015Z","id":"46352f08-f912-4526-a779-afcd29260596","location":"dBXtfWoVhkarPcjwIcbMWHgvA","region":"FubMqFmmTujSfFnmsCStQLJQh","updated":"2019-10-31T03:32:11.087015Z","vendor":"nBeAUIWtJKnfrFCSYhqedcmZZ","vendor_name":"iMEYURZMhHxYxZwwHocbevvFL"},{"created":"2019-10-28T19:27:01.921980Z","id":"e78356fe-7b79-41d8-86cb-6d2cac2ec31c","location":"snmtgVMjxNyUqMUXskVQkxXlM","region":"cZeyahnZqaXQUhWfvKudDUdmt","updated":"2019-10-28T19:27:01.921980Z","vendor":"nQjNhCHAYigfMKNMtYhBeOPMD","vendor_name":"nlhySToPMWXijPrTYMgooTYuH"},{"created":"2019-10-28T16:22:12.859786Z","id":"2e9afd4a-f336-494f-8df6-5bb768bec901","location":"GCNGYCXwvYlmfZBTXwdNicTxT","region":"qSOuXlPMaNTIhraLoocSmsxpy","updated":"2019-10-28T16:22:12.859786Z","vendor":"nTgrPwdFhAXoPZXdKuUCLAuoq","vendor_name":"zMKrWUIZyQStzuJyJnojPJHiK"},{"created":"2019-10-31T03:43:16.906170Z","id":"dff66693-6abb-4668-a2de-8b433721b83a","location":"uQCFYdhwXVjfakywxHzVYxEsG","region":"HsMBkikKowtNGKUAAZTovrtOr","updated":"2019-10-31T03:43:16.906170Z","vendor":"nfXSVlUAWFGuSijZmWHSZYrYh","vendor_name":"UtIyEIMTBDrxTXGmcNybmLAhV"},{"created":"2019-11-01T19:02:35.077242Z","id":"a98a4657-6269-4608-9aa6-946f1f67d07b","location":"MUxqCJnTSZsfYrVtCAaBqaqaf","region":"ZbIBFfFHiGxVpOTDFnAjzKhSR","updated":"2019-11-01T19:02:35.077242Z","vendor":"nhUlcwGappdqjyFPHCaWLYtcW","vendor_name":"lALMaUsLXyvVRFgsLoFBHdYwj"},{"created":"2019-10-28T20:47:28.096818Z","id":"2b42e8c0-5827-4191-a972-841869581b9c","location":"lwNZWZTjxFrNKQodasmkZQBYu","region":"awrvsjGOgQvBOJXfvkwrVWxGG","updated":"2019-10-28T20:47:28.096818Z","vendor":"nxAVHeaHmPmgGqTUOhHHPXRWX","vendor_name":"BFCcLHsKooAmtzghBjBPwAECT"},{"created":"2019-10-17T16:23:02.083978Z","id":"80a68b60-8d26-4d5f-8123-63b827f01b13","location":"ZyqmQTKSxnnDVbtoBeMidilVo","region":"xIGoebYNtJWciYCEdkNfwqlzj","updated":"2019-10-17T16:23:02.083978Z","vendor":"oLxoTaytGOnMaxSAIumZqtATv","vendor_name":"pkvGRgfpRwiJsHLPaWssiHCoN"},{"created":"2020-01-13T16:27:42.045353Z","id":"dc2422a9-893f-4bc0-b000-d3a5d91c237a","location":"iTiWjCvrBNeWJvPrjlHTIRLNN","region":"TwWQVgMITkcNsNivtHocleIwc","updated":"2020-01-13T16:27:42.045353Z","vendor":"oTEgswVkhcvPjhJaukDwYZAVD","vendor_name":"cIUAFOZalkHJeKdETSqMfqkSc"},{"created":"2019-10-30T18:08:17.499713Z","id":"c98207fe-e386-417e-9c35-54c9a2e7fbd5","location":"zxoWJwkCUitcXYYxFzxKiHEwy","region":"uUMANTiYFRHVtuemBDVXQTDlw","updated":"2019-10-30T18:08:17.499713Z","vendor":"oXMpOfYQWSuIjcVdHJBASbKIm","vendor_name":"tsvtBNwaoApjBnOpUklCYcGsV"},{"created":"2020-01-13T23:11:52.322928Z","id":"dbec06e0-2df0-4c00-b148-dc9340b986b0","location":"LDqPoFMlANxXgjJpqqxQlZAqL","region":"nuiSFODFwYmvQLcoASJuUIweK","updated":"2020-01-13T23:11:52.322928Z","vendor":"oraOQgbUaMoEtXGXYHtgdzbEF","vendor_name":"wbEJwliUfoIhLUtmTBjKAMVkG"},{"created":"2019-10-31T03:26:11.441311Z","id":"678e2e1d-e193-44cd-805d-fd3282054dd7","location":"faxTqDmWhyDGstDTgRfBWYvTa","region":"fQXTowTofMdSyMOTRikAQTTqR","updated":"2019-10-31T03:26:11.441311Z","vendor":"osZwOTfHqCQwekuqModjGPtGD","vendor_name":"WRgBKyGFSQZfPuLtXENUobKnw"},{"created":"2019-10-17T16:23:36.471290Z","id":"a9cc15aa-7625-444b-bb14-62e5daff61e9","location":"YLaTgEcIWJQPGegTisgRdTTzn","region":"LWRknqxYdQSsPTGMpOcKnktCj","updated":"2019-10-17T16:23:36.471290Z","vendor":"pFqFYmPEOXWFrnFzhSGghsvXf","vendor_name":"qFGjtbXohAteudZMiotdNktRv"},{"created":"2019-10-31T19:33:42.972277Z","id":"024d89b1-c09c-4b49-9e0b-358ee60ff366","location":"WTfNWlmnmWsdAKsICwdeFqjaE","region":"SXoPAENUHBzEIXnPlpzqadgyL","updated":"2019-10-31T19:33:42.972277Z","vendor":"pGCDtyIMbfplcGgfQrxgREyfg","vendor_name":"umdAZiMiDdjvEiGOwzakoHqGo"},{"created":"2019-10-31T14:14:52.669120Z","id":"812bd02f-42b2-49bb-aaa4-639fb659646d","location":"QeiPruyhmtIMazvSmVfLIOSYM","region":"YcHINjuZFDrSuZwTbzSnSLZsA","updated":"2019-10-31T14:14:52.669120Z","vendor":"pIelpcPQHlXExzyJFobvamPbz","vendor_name":"KUIopsEQKTVRrjXKGOsQhjdjO"},{"created":"2019-10-28T14:49:54.652040Z","id":"afaba904-226e-4436-8609-f4aefb496185","location":"bnTIBdKEGhWFUhOGvoMPMydqV","region":"ugSlndLJdbgWIwhocIRxcDKNQ","updated":"2019-10-28T14:49:54.652040Z","vendor":"pZegqnMajKSilNkTWwLdYbXpx","vendor_name":"WPyOfgMsPebXJIpPaIEdGukVY"},{"created":"2020-01-13T21:26:06.285250Z","id":"cfcd368d-f153-4e3c-9273-279279371998","location":"HuLCcCHPVoNVRhWtbOGzfbBRA","region":"NTEKHqsakggAWvrUsKUukrisp","updated":"2020-01-13T21:26:06.285250Z","vendor":"palEqfYyDuPhuUFqoRwHGUgnD","vendor_name":"KsQIwCgTkTjpDfJBhYaNcUHOF"},{"created":"2019-10-31T19:33:09.210956Z","id":"f6bbf00e-667b-476a-9d74-37564594556b","location":"dgtarLiaYhECHffClcYbRUPKx","region":"jdtvkBAMTlfYAXrqxdaZjKMBb","updated":"2019-10-31T19:33:09.210956Z","vendor":"pneceoaHqgSoVNTypOZhirVFU","vendor_name":"NAHlcKVMbyVjiRZhJdxSDTnba"},{"created":"2019-10-17T20:07:00.392143Z","id":"237035d5-8477-446d-bc90-a81eb51c198b","location":"cOOCyyKCgiHfNXmfAIJqIaFna","region":"eVYfFUBGwSklEffokzGDEGXGC","updated":"2019-10-17T20:07:00.392143Z","vendor":"pwgWBqPosGxaDuQvrLnVGXVEu","vendor_name":"LQTbZGcppLfbAfmCfhNQivFwQ"},{"created":"2020-01-13T22:36:42.778432Z","id":"0cba9936-0ed3-4fad-aed7-c7993ead130e","location":"EtUWIymLzCmtOviaAlRNyifvV","region":"gYCkkDardmrPrptJvqklvfKtq","updated":"2020-01-13T22:36:42.778432Z","vendor":"qICIvWWprsfANnBIgCPEHyCVN","vendor_name":"GPYQatSLtPVTYDgsXaYvqdkHB"},{"created":"2020-01-13T21:05:17.486221Z","id":"391a63c9-79db-4805-8ffc-c480bad968cc","location":"rCrNzALqlEoGEGJjdDhtQWpEN","region":"ASutnUgIeyPogGZRXAPMPrtou","updated":"2020-01-13T21:05:17.486221Z","vendor":"qaaLXBpCAictVufYFKCjZBHWb","vendor_name":"OtHedcggVmsFTpPJSevdngDME"},{"created":"2019-10-30T18:15:28.864036Z","id":"3e9a56ad-a4e0-4d42-b438-47d56ae66f2e","location":"vkHWyuRxMxcXPJCFlXQyymbPn","region":"tyZjaUkdheYPPChxEcSmhGGCz","updated":"2019-10-30T18:15:28.864036Z","vendor":"qvnkwfgzxVhsSFeZQeItLqFCS","vendor_name":"bhInmEhwhqdeyIooVuokYfsDI"},{"created":"2020-01-13T22:36:56.861980Z","id":"cb9aea27-2870-44e6-bdf1-95e329dfb197","location":"OjxqnElXJMybMWzpzMUQmXfAi","region":"JLjEIjVKXPiSueAsgdlqYSxKk","updated":"2020-01-13T22:36:56.861980Z","vendor":"sFsvwuklHalvATWNkxkeUKcQp","vendor_name":"HmKkPVEOyTKUQnKMATOAELkEL"},{"created":"2019-10-31T03:28:59.271246Z","id":"a6962fdc-97d5-4d99-96d6-11e1435ec441","location":"AADrZythIbyxvkJqLuAWXJtat","region":"RCrJfEXYmnAGQUeMQXmPdtSFs","updated":"2019-10-31T03:28:59.271246Z","vendor":"sIJtFFJgiqzNlORQVgNsGkVIE","vendor_name":"wfFRUFphRbIyhniCsMCeOrkQr"},{"created":"2019-10-15T16:37:57.062325Z","id":"98ffd143-dfb4-4793-b50e-ebf8fe5527e4","location":"STBrgQvFzUDHZLQMhURJtlzIQ","region":"KvcCKyJjdWOXPZuxNUkLArBXz","updated":"2019-10-15T16:37:57.062325Z","vendor":"sVZJRLGLfADkUmzgNAAGuXIhn","vendor_name":"GgvsdLWEInjRymWTWmbdgkbft"},{"created":"2019-10-31T03:43:31.706624Z","id":"4008bbad-6636-478f-a1b1-b1202429eb2e","location":"nCzrkVrMVQuWZRMaGPCxrfOyb","region":"gWbpMWydtPzqdSuiFyycSUjiM","updated":"2019-10-31T03:43:31.706624Z","vendor":"sjVTRJytfhuDmBxPYFBZYOAlg","vendor_name":"CSWJRiuLsejwsVvhoaokzhlcE"},{"created":"2020-01-13T22:29:04.097881Z","id":"9060b1bd-c368-4247-ad96-248b75783f25","location":"ppINpzuGAKtUOZcZQPbVWlEMR","region":"yYLnNYSKIyAhxSOGTzwINYuRF","updated":"2020-01-13T22:29:04.097881Z","vendor":"tCYRLGHopBQuzwXaKZTCSwdeP","vendor_name":"uPMUSoGIOoPzJXAuKjqoPGLwR"},{"created":"2019-10-30T18:10:33.801945Z","id":"f913c28c-93bb-46e3-99e5-71efad574b7a","location":"pMIgDBNRJwlNCaezXilheuyPg","region":"crYuajuNzgNspfHPogVeRMEfR","updated":"2019-10-30T18:10:33.801945Z","vendor":"uIZDXZKYLbdxMkFJMWdMxqYsr","vendor_name":"JGKxzjzpYIxCEurNhHElBaOSg"},{"created":"2019-10-18T14:45:34.304642Z","id":"d99e06ab-e324-407f-9254-4d81ce89bc3d","location":"AljvajJPOoulOuPalWTZqUCBH","region":"fovOavpiJzQQBdJPcCTrNSAFg","updated":"2019-10-18T14:45:34.304642Z","vendor":"uuhFSIbwKDBEmJiOLEfQWPunH","vendor_name":"gJRHsZxdYmaOKCBIkpTScZhMP"},{"created":"2019-10-28T16:54:01.896134Z","id":"aad28ee8-69d4-4507-8740-8e132cc20f55","location":"PXFvAKjDpHSVciWviaDZkixWq","region":"rwhtPvbfzdAOloUExarMKASHt","updated":"2019-10-28T16:54:01.896134Z","vendor":"vLrqJQMkWUjaTbFlZNAGkbnSs","vendor_name":"MIayHyYBnjpTkuWZRpgvmRube"},{"created":"2020-01-13T17:24:40.577540Z","id":"31003771-f038-47b6-88e7-67203cb3fba4","location":"WGaMQWUAFIFpRvrLrUPnrggeJ","region":"kFjVoxXlBOMUmSvpQkQEcOcUn","updated":"2020-01-13T17:24:40.577540Z","vendor":"vdSITVedOQGiQBQbwfmdWJzBU","vendor_name":"pEoFrRxtLKEOfKHmnVXFNaxZz"},{"created":"2019-10-28T18:24:03.252621Z","id":"ebf736c9-992a-4cd8-b058-7fb52854bf0d","location":"mQbMMuloOPivIWhXEUAZgYulx","region":"cFTbPsYZZwVYFhaqMPCtllKsN","updated":"2019-10-28T18:24:03.252621Z","vendor":"vzkSUYodwKDuTJHKhedWEnWTB","vendor_name":"NeaiVjIjZoJBRUHXXIXERFSVf"},{"created":"2019-10-30T18:16:47.344096Z","id":"c822c946-fbc5-4a51-a954-5584630b322e","location":"cUsLkacTNZHFAJpJxeMxBjlOS","region":"roarIMandVNxCBxUCGnudJYOJ","updated":"2019-10-30T18:16:47.344096Z","vendor":"wNDbYvYfBEIolcQQtznYEzmPh","vendor_name":"kLyzBLTMNWRAyoFWPMXdMwLDJ"},{"created":"2019-10-28T15:38:59.065370Z","id":"80c7e272-e96a-4a2c-85c9-c23db2971820","location":"ddXuqzSnHfhguEMdrVgkMbney","region":"nBhlrKgPJwEAemSRmmQcHIqht","updated":"2019-10-28T15:38:59.065370Z","vendor":"wUKfqCZmgXithkmajhIdLvCrF","vendor_name":"pnNvMXqmahXGfviRxtKNTNUND"},{"created":"2019-10-15T19:23:57.264008Z","id":"361079f0-3568-4b16-ab8b-f908b8ead823","location":"toiWMWFNMqSxnkBMHgdMcjeiR","region":"ilIogDuopmopDEjjVTYEIMCHt","updated":"2019-10-15T19:23:57.264008Z","vendor":"wXroyvGWhItnTIAWpBJSmNFLB","vendor_name":"mxKZqDSSbEXlogRWfhPpncGLq"},{"created":"2019-10-31T19:39:45.446616Z","id":"49579549-ad23-4480-ae39-a578616f3147","location":"pMSJCvtjuELEXuqnZoamTdHiv","region":"kKYciCOYeSAwJpEHkjjBtiaEh","updated":"2019-10-31T19:39:45.446616Z","vendor":"whxVbFGHKukcDiYpUinIWXMuy","vendor_name":"JqMCfDekTArzusasxmNhbtRzA"},{"created":"2019-10-31T16:37:06.320309Z","id":"ad583845-8845-4d0e-beb6-2e561017e7ed","location":"pPzhIwLVxpcuMDbJrfEgDHtBb","region":"kGAhTmwYnJNlETurQQQAzbyjZ","updated":"2019-10-31T16:37:06.320309Z","vendor":"xFdUYJlAsHXCQwAuOSSdNXhNt","vendor_name":"oAzSrZvMscPyDdSGQjfcqNrCl"},{"created":"2019-10-31T03:42:52.531845Z","id":"cf6e4687-5786-443b-bc70-ff8445fcc283","location":"txumQjRgYKSXPZEEJeYfjlBOn","region":"ynjwNshueaslNzOvhexAJKVcF","updated":"2019-10-31T03:42:52.531845Z","vendor":"xZvAkeHkjvDfLkyleaakNOvMh","vendor_name":"EusnQLKDtBiIUlHtjDDALbkRp"},{"created":"2020-01-13T22:30:27.505196Z","id":"ac601b3b-04ed-4009-aaa4-c19ad5ffce52","location":"ipxtGSEOpEofmRLZgASLIgBuc","region":"jfSITqJZsFxtdfECYuBtwdQud","updated":"2020-01-13T22:30:27.505196Z","vendor":"xbvWcZJcFyBiXDzlJFnnsrJMo","vendor_name":"BjPxBHhIckawpncLWaMZKLWDX"},{"created":"2020-01-13T22:27:47.974304Z","id":"fe87bbf4-028f-44f1-ac7f-284df563b3d6","location":"IauWkFEfGesYkkrtJFTGGiuGz","region":"DBoKfMzAqCRHKJZZtvopAIwLg","updated":"2020-01-13T22:27:47.974304Z","vendor":"xcLlbpjtrHPXsqNgeveRZPwNk","vendor_name":"JAiAQiwfbQKnMSdTuBWIdivoI"},{"created":"2020-01-13T22:20:05.458350Z","id":"2e7bd285-629c-41a4-b6aa-6f478ccd8d97","location":"ojfmXRWGViQXNVVfFnVTumDwO","region":"nYHcfCPKemilqWBVJmjNWaYma","updated":"2020-01-13T22:20:05.458350Z","vendor":"xfqmlBBnuZBGPDRWxPxXltaQv","vendor_name":"PGbCYEuPzyyvmqThksWcbRtDz"},{"created":"2019-10-17T16:29:16.510637Z","id":"9d32856d-0a10-4c20-86f3-f527b4330492","location":"BpxvNmIiIYOcUVgUhpXlyMqKo","region":"TSsPNiibfBzVtiWhUiNPnJAFh","updated":"2019-10-17T16:29:16.510637Z","vendor":"yBurMHIGqFSkNuZmOyhdNKSCU","vendor_name":"aBTRYAgXuGhoaEUCbnnAxpNxm"},{"created":"2019-10-31T19:38:36.193137Z","id":"6ce4a3cc-ff7d-4d6f-ae00-e3225cef413c","location":"JtrXWdaydSrrfaqVPWfxbUFWt","region":"XvRqJBIBtOfnPRvFXhbwoYeFg","updated":"2019-10-31T19:38:36.193137Z","vendor":"yIbxSaJxiKffcjFKMWyJVAXYj","vendor_name":"eoZwienlMEPljJuWmJKbTDfki"},{"created":"2019-10-16T15:18:34.961851Z","id":"d87054e3-967c-4880-9c75-3cf3f97aa9c5","location":"YAndnKmBvPJOxcqfKUEEfSCnx","region":"HQXCIOBDrIwMlBoFuQAakTegy","updated":"2019-10-16T15:18:34.961851Z","vendor":"yPUhetETArLifYsTcvSJqbECA","vendor_name":"MdtqWQIdhIteLtJBAkhBoNhyJ"},{"created":"2019-10-28T14:46:04.187553Z","id":"32a14586-548e-4018-b65a-9e1278ea8644","location":"fODHvSzOYIiBnCwsunElVrISa","region":"hVBXObIliufFHHWDqRjSthLco","updated":"2019-10-28T14:46:04.187553Z","vendor":"yqPKwfMctgMaoCeUsUmvSsFMB","vendor_name":"nBByzkUvsJmICJQwQqyfSILbN"},{"created":"2019-10-31T03:27:55.167723Z","id":"bd1613cb-8a32-49df-a6bf-f49da41a25e6","location":"dLrPalCqyFSopspUEYTXYCaYO","region":"SCdXLNSZiLCqklijLQIPYXsbQ","updated":"2019-10-31T03:27:55.167723Z","vendor":"yqYVzHddifmAhKeKkqDjOZBga","vendor_name":"cKXaYKGCevrBVYUekxAaOaoJg"},{"created":"2019-10-17T15:19:03.109208Z","id":"62b4115e-d137-4695-a959-a1090968ae57","location":"JLSTvuxLCyjleLyLybdqLjpYA","region":"TSeBFfTgYURbJhwZOPuKWCcFE","updated":"2019-10-17T15:19:03.109208Z","vendor":"zLWibSYWrASJWneBwuuaKKfUZ","vendor_name":"voWtoCluKQVZLgGRHbKxJkyPf"},{"created":"2019-10-15T19:42:54.912529Z","id":"d8859539-13db-44e2-9e00-ee1fbffbc9eb","location":"FJqBByqwTVopjMAlFOHFWuOdK","region":"HTAZXdpWhMAUlODNkMYOvolPK","updated":"2019-10-15T19:42:54.912529Z","vendor":"zTvCluDlQQstGmEYEXTOwEiMT","vendor_name":"hxXdGFIxlAxFUJXcQOtOYuHSd"},{"created":"2020-01-13T22:28:34.799135Z","id":"bb4328b4-253f-4685-aa3a-f8f0169f3c5c","location":"xEDieWVEMuJmoWoryHNKhetaT","region":"yTlzErqzzrlnkKOQMweHLENmF","updated":"2020-01-13T22:28:34.799135Z","vendor":"zYgtBqzjUmfzlncnRmBdehvfq","vendor_name":"IesoiDsiQlqRIHANcjYodvyRs"},{"created":"2019-10-30T20:22:07.220202Z","id":"29b23bc6-cca2-4029-b910-210dc68a3baa","location":"AhGnnEjBvthAJVtlflrwYkvkO","region":"cSeQjAcdUJYYhzQrLQlmNbpil","updated":"2019-10-30T20:22:07.220202Z","vendor":"ztweaGanpICHOLovbMjtxZIKb","vendor_name":"rMUHBJNrceaTQUoGYwqaEbLjc"},{"created":"2019-10-30T20:13:46.792474Z","id":"8e58a6c8-4e93-444a-97ad-ec4130708f1d","location":"KslZXzahskDPGcsASkFkampqx","region":"ntaInqUXYnChlcIVqdsjbhCVP","updated":"2019-10-30T20:13:46.792474Z","vendor":"zxBTAWRtpojKeXyAIDiXqDpGw","vendor_name":"sSzimLXFdQhTOPFzgUwtBYsLO"},{"created":"2020-01-13T17:19:07.611627Z","id":"41f71b06-6242-4201-9760-3e53c35704c5","location":"VCFwTNYcgIpkcMDWwQKIVdLiv","region":"FeQDZvuLZfrZJLiGlJYyZWyBU","updated":"2020-01-13T17:19:07.611627Z","vendor":"zyMSLunUuppdUWqsiAlgRsOUa","vendor_name":"EPSbNAaIODOmOHNLIGPhgEocA"}]' - headers: - Cache-Control: - - no-cache - Content-Length: - - "56958" - Content-Type: - - application/json - Date: - - Mon, 13 Jan 2020 23:16:20 GMT - Request-Id: - - GNC6vNod5gX+ - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - GNC6vNod5gX+ - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/dc/88089480-2f31-4cc6-8e3e-afb41c91423c - method: GET - response: - body: '{"created":"2019-10-10T21:39:38.188875Z","id":"88089480-2f31-4cc6-8e3e-afb41c91423c","location":"Hala","region":"Atlantis","updated":"2019-10-10T21:39:38.188875Z","vendor":"Asguardians","vendor_name":null}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "205" - Content-Type: - - application/json - Date: - - Mon, 13 Jan 2020 23:16:20 GMT - Request-Id: - - e3hHEeR2/2I3 - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - e3hHEeR2/2I3 - status: 200 OK - code: 200 - duration: "" diff --git a/fixtures/conch-v3/device.yaml b/fixtures/conch-v3/device.yaml deleted file mode 100644 index 4490f5d..0000000 --- a/fixtures/conch-v3/device.yaml +++ /dev/null @@ -1,1783 +0,0 @@ ---- -version: 1 -interactions: -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_vendor/MyBigVendor - method: GET - response: - body: '{"error":"Entity Not Found"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "28" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:25 GMT - Request-Id: - - qWH9gOZPxm+0 - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - qWH9gOZPxm+0 - status: 404 Not Found - code: 404 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_vendor/MyBigVendor - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Sun, 26 Jan 2020 19:04:25 GMT - Location: - - /hardware_vendor/MyBigVendor - Request-Id: - - fonfGbizHibf - Server: - - Mojolicious (Perl) - X-Request-Id: - - fonfGbizHibf - status: 303 See Other - code: 303 - duration: "" -- request: - body: "" - form: {} - headers: - Referer: - - http://10.51.54.42:5000/hardware_vendor/MyBigVendor - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_vendor/MyBigVendor - method: GET - response: - body: '{"created":"2020-01-26T19:04:25.777925Z","id":"713e2342-ed15-409d-a07c-bbca41941d92","name":"MyBigVendor","updated":"2020-01-26T19:04:25.777925Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "146" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:25 GMT - Request-Id: - - FAJrBEVlyK3I - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - FAJrBEVlyK3I - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_vendor/MyBigVendor - method: GET - response: - body: '{"created":"2020-01-26T19:04:25.777925Z","id":"713e2342-ed15-409d-a07c-bbca41941d92","name":"MyBigVendor","updated":"2020-01-26T19:04:25.777925Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "146" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:25 GMT - Request-Id: - - i831ckqZQrd4 - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - i831ckqZQrd4 - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/validation_plan/a30ab8b2-8a9e-4e51-8bb0-92862abd8b54 - method: GET - response: - body: '{"created":"2019-10-09T17:21:37.861483Z","description":"Validation plan - containing all validations run in Conch v1 on servers","id":"a30ab8b2-8a9e-4e51-8bb0-92862abd8b54","name":"Conch - v1 Legacy Plan: Server"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "209" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:25 GMT - Request-Id: - - eG/SeT99BegJ - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - eG/SeT99BegJ - status: 200 OK - code: 200 - duration: "" -- request: - body: | - {"alias":"VgvjJOOnrRwUYxWQFOtemUQuQ","bios_firmware":"XqkAkCZYLvgRLvYqIrtHPFhVN","cpu_type":"AjbeLNHApsBhqcpiSwSZtssQx","hardware_vendor_id":"713e2342-ed15-409d-a07c-bbca41941d92","name":"KPHacrRjdKTXHRQkYFizhiXiJ","purpose":"uNggImrxtZCFRXriVRahxKFvd","rack_unit_size":2,"sku":"xYxLsvSQYDsWVAxNTrUcxHIgL","validation_plan_id":"a30ab8b2-8a9e-4e51-8bb0-92862abd8b54"} - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_product - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Sun, 26 Jan 2020 19:04:25 GMT - Location: - - /hardware_product/2eca6269-a424-45f2-a3a9-a9913b8976ba - Request-Id: - - jhGk7EV8f4r3 - Server: - - Mojolicious (Perl) - X-Request-Id: - - jhGk7EV8f4r3 - status: 303 See Other - code: 303 - duration: "" -- request: - body: "" - form: {} - headers: - Content-Type: - - application/json - Referer: - - http://10.51.54.42:5000/hardware_product - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_product/2eca6269-a424-45f2-a3a9-a9913b8976ba - method: GET - response: - body: '{"alias":"VgvjJOOnrRwUYxWQFOtemUQuQ","bios_firmware":"XqkAkCZYLvgRLvYqIrtHPFhVN","cpu_num":0,"cpu_type":"AjbeLNHApsBhqcpiSwSZtssQx","created":"2020-01-26T19:04:25.859308Z","dimms_num":0,"generation_name":null,"hardware_vendor_id":"713e2342-ed15-409d-a07c-bbca41941d92","hba_firmware":null,"id":"2eca6269-a424-45f2-a3a9-a9913b8976ba","legacy_product_name":null,"name":"KPHacrRjdKTXHRQkYFizhiXiJ","nics_num":0,"nvme_ssd_num":0,"nvme_ssd_size":null,"nvme_ssd_slots":null,"prefix":null,"psu_total":0,"purpose":"uNggImrxtZCFRXriVRahxKFvd","rack_unit_size":2,"raid_lun_num":0,"ram_total":0,"sas_hdd_num":0,"sas_hdd_size":null,"sas_hdd_slots":null,"sas_ssd_num":0,"sas_ssd_size":null,"sas_ssd_slots":null,"sata_hdd_num":0,"sata_hdd_size":null,"sata_hdd_slots":null,"sata_ssd_num":0,"sata_ssd_size":null,"sata_ssd_slots":null,"sku":"xYxLsvSQYDsWVAxNTrUcxHIgL","specification":null,"updated":"2020-01-26T19:04:25.859308Z","usb_num":0,"validation_plan_id":"a30ab8b2-8a9e-4e51-8bb0-92862abd8b54"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "985" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:25 GMT - Location: - - /hardware_product/2eca6269-a424-45f2-a3a9-a9913b8976ba - Request-Id: - - mKxPAnvfFv6f - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - mKxPAnvfFv6f - status: 200 OK - code: 200 - duration: "" -- request: - body: | - {"alias":"fZUdZGfPTpNuoaETCoAXHzHdh","bios_firmware":"KDvLhxavVmQzskmemwXPdBUrb","cpu_type":"NhgfDfTpxIeGdtBbqqFVpflLH","hardware_vendor_id":"713e2342-ed15-409d-a07c-bbca41941d92","name":"ZLepZJcnMPSuCGgjCKATuzdbm","purpose":"QrtEnSloWHelCmwPRFIzWToNy","rack_unit_size":1,"sku":"FxCjKnuMmPTawRhYMJmKDWwrN","validation_plan_id":"a30ab8b2-8a9e-4e51-8bb0-92862abd8b54"} - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_product - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Sun, 26 Jan 2020 19:04:25 GMT - Location: - - /hardware_product/6621614d-ad3d-42f7-8a6d-3cb94d8bd76e - Request-Id: - - PAJPT2jJuxYC - Server: - - Mojolicious (Perl) - X-Request-Id: - - PAJPT2jJuxYC - status: 303 See Other - code: 303 - duration: "" -- request: - body: "" - form: {} - headers: - Content-Type: - - application/json - Referer: - - http://10.51.54.42:5000/hardware_product - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_product/6621614d-ad3d-42f7-8a6d-3cb94d8bd76e - method: GET - response: - body: '{"alias":"fZUdZGfPTpNuoaETCoAXHzHdh","bios_firmware":"KDvLhxavVmQzskmemwXPdBUrb","cpu_num":0,"cpu_type":"NhgfDfTpxIeGdtBbqqFVpflLH","created":"2020-01-26T19:04:25.923019Z","dimms_num":0,"generation_name":null,"hardware_vendor_id":"713e2342-ed15-409d-a07c-bbca41941d92","hba_firmware":null,"id":"6621614d-ad3d-42f7-8a6d-3cb94d8bd76e","legacy_product_name":null,"name":"ZLepZJcnMPSuCGgjCKATuzdbm","nics_num":0,"nvme_ssd_num":0,"nvme_ssd_size":null,"nvme_ssd_slots":null,"prefix":null,"psu_total":0,"purpose":"QrtEnSloWHelCmwPRFIzWToNy","rack_unit_size":1,"raid_lun_num":0,"ram_total":0,"sas_hdd_num":0,"sas_hdd_size":null,"sas_hdd_slots":null,"sas_ssd_num":0,"sas_ssd_size":null,"sas_ssd_slots":null,"sata_hdd_num":0,"sata_hdd_size":null,"sata_hdd_slots":null,"sata_ssd_num":0,"sata_ssd_size":null,"sata_ssd_slots":null,"sku":"FxCjKnuMmPTawRhYMJmKDWwrN","specification":null,"updated":"2020-01-26T19:04:25.923019Z","usb_num":0,"validation_plan_id":"a30ab8b2-8a9e-4e51-8bb0-92862abd8b54"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "985" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:25 GMT - Location: - - /hardware_product/6621614d-ad3d-42f7-8a6d-3cb94d8bd76e - Request-Id: - - UzaWSuNSqe1y - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - UzaWSuNSqe1y - status: 200 OK - code: 200 - duration: "" -- request: - body: | - {"location":"wVUkuFBhOkeuJjPIMOuKFFCtq","region":"mVQptcFUoeCvPUAmWRFQWAAtL","vendor":"raufanYXyJBEiiKSSHtQcmMfO","vendor_name":"DdVxUofQpudwRdHoXPtjYHVri"} - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/dc - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Sun, 26 Jan 2020 19:04:25 GMT - Location: - - /dc/bc85ceef-2d58-45ae-a659-1b666f794795 - Request-Id: - - 16IfPczqe5tG - Server: - - Mojolicious (Perl) - X-Request-Id: - - 16IfPczqe5tG - status: 201 Created - code: 201 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/dc/bc85ceef-2d58-45ae-a659-1b666f794795 - method: GET - response: - body: '{"created":"2020-01-26T19:04:25.970655Z","id":"bc85ceef-2d58-45ae-a659-1b666f794795","location":"wVUkuFBhOkeuJjPIMOuKFFCtq","region":"mVQptcFUoeCvPUAmWRFQWAAtL","updated":"2020-01-26T19:04:25.970655Z","vendor":"raufanYXyJBEiiKSSHtQcmMfO","vendor_name":"DdVxUofQpudwRdHoXPtjYHVri"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "280" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:25 GMT - Request-Id: - - djkc/WI3xJyV - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - djkc/WI3xJyV - status: 200 OK - code: 200 - duration: "" -- request: - body: | - {"alias":"YZScFfGvMadGNUUBxvuztEhmc","az":"rSHTRbXlBtZUUPZowzIXvGTtU","datacenter_id":"bc85ceef-2d58-45ae-a659-1b666f794795","vendor_name":"zNHMvLDMgbkfUIPtAhglCOwTg"} - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/room - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Location: - - /room/89ce0266-20fe-4136-a772-e2d9ace8d956 - Request-Id: - - oEoP5M4Z5O/c - Server: - - Mojolicious (Perl) - X-Request-Id: - - oEoP5M4Z5O/c - status: 303 See Other - code: 303 - duration: "" -- request: - body: "" - form: {} - headers: - Content-Type: - - application/json - Referer: - - http://10.51.54.42:5000/room - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/room/89ce0266-20fe-4136-a772-e2d9ace8d956 - method: GET - response: - body: '{"alias":"YZScFfGvMadGNUUBxvuztEhmc","az":"rSHTRbXlBtZUUPZowzIXvGTtU","created":"2020-01-26T19:04:26.008325Z","datacenter_id":"bc85ceef-2d58-45ae-a659-1b666f794795","id":"89ce0266-20fe-4136-a772-e2d9ace8d956","updated":"2020-01-26T19:04:26.008325Z","vendor_name":"zNHMvLDMgbkfUIPtAhglCOwTg"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "291" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Location: - - /room/89ce0266-20fe-4136-a772-e2d9ace8d956 - Request-Id: - - SIn+zxbhU4hQ - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - SIn+zxbhU4hQ - status: 200 OK - code: 200 - duration: "" -- request: - body: | - {"admins":[{"email":"conch@example.com"}],"description":"XZPonJXYDzKogdPMyeIjmkYpX","name":"JIrULNPxKPKZncyExKMfLfGcY"} - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/build - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Location: - - /build/7d2b96d4-3b2a-4a24-9fb9-fbfddf35dcc9 - Request-Id: - - K5WUjtWhPPs0 - Server: - - Mojolicious (Perl) - X-Request-Id: - - K5WUjtWhPPs0 - status: 303 See Other - code: 303 - duration: "" -- request: - body: "" - form: {} - headers: - Content-Type: - - application/json - Referer: - - http://10.51.54.42:5000/build - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/build/7d2b96d4-3b2a-4a24-9fb9-fbfddf35dcc9 - method: GET - response: - body: '{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-26T19:04:26.049247Z","description":"XZPonJXYDzKogdPMyeIjmkYpX","id":"7d2b96d4-3b2a-4a24-9fb9-fbfddf35dcc9","name":"JIrULNPxKPKZncyExKMfLfGcY","started":null}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "315" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Request-Id: - - /uABG1jTI8i6 - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - /uABG1jTI8i6 - status: 200 OK - code: 200 - duration: "" -- request: - body: | - {"name":"PsSRIniTNebDKOEpDnzQrZASj","rack_size":60} - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack_role - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Location: - - /rack_role/b233a04d-d453-4730-8f4d-c945948c8d3b - Request-Id: - - jWSWJb/hD0m1 - Server: - - Mojolicious (Perl) - X-Request-Id: - - jWSWJb/hD0m1 - status: 303 See Other - code: 303 - duration: "" -- request: - body: "" - form: {} - headers: - Content-Type: - - application/json - Referer: - - http://10.51.54.42:5000/rack_role - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack_role/b233a04d-d453-4730-8f4d-c945948c8d3b - method: GET - response: - body: '{"created":"2020-01-26T19:04:26.096897Z","id":"b233a04d-d453-4730-8f4d-c945948c8d3b","name":"PsSRIniTNebDKOEpDnzQrZASj","rack_size":60,"updated":"2020-01-26T19:04:26.096897Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "175" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Request-Id: - - eR+HhloicFFn - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - eR+HhloicFFn - status: 200 OK - code: 200 - duration: "" -- request: - body: | - {"build_id":"7d2b96d4-3b2a-4a24-9fb9-fbfddf35dcc9","datacenter_room_id":"89ce0266-20fe-4136-a772-e2d9ace8d956","name":"RjWeunugnyPEEqdAfhxXBwAJK","phase":"integration","rack_role_id":"b233a04d-d453-4730-8f4d-c945948c8d3b"} - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Location: - - /rack/7aadeb89-bd00-4b44-8dba-79409603b38f - Request-Id: - - Nv5/woVeg9zq - Server: - - Mojolicious (Perl) - X-Request-Id: - - Nv5/woVeg9zq - status: 303 See Other - code: 303 - duration: "" -- request: - body: "" - form: {} - headers: - Content-Type: - - application/json - Referer: - - http://10.51.54.42:5000/rack - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack/7aadeb89-bd00-4b44-8dba-79409603b38f - method: GET - response: - body: '{"asset_tag":null,"build_id":"7d2b96d4-3b2a-4a24-9fb9-fbfddf35dcc9","build_name":"JIrULNPxKPKZncyExKMfLfGcY","created":"2020-01-26T19:04:26.135631Z","datacenter_room_alias":"YZScFfGvMadGNUUBxvuztEhmc","datacenter_room_id":"89ce0266-20fe-4136-a772-e2d9ace8d956","full_rack_name":"zNHMvLDMgbkfUIPtAhglCOwTg:RjWeunugnyPEEqdAfhxXBwAJK","id":"7aadeb89-bd00-4b44-8dba-79409603b38f","name":"RjWeunugnyPEEqdAfhxXBwAJK","phase":"integration","rack_role_id":"b233a04d-d453-4730-8f4d-c945948c8d3b","rack_role_name":"PsSRIniTNebDKOEpDnzQrZASj","serial_number":null,"updated":"2020-01-26T19:04:26.135631Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "593" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Location: - - /rack/7aadeb89-bd00-4b44-8dba-79409603b38f - Request-Id: - - FTeBZkTY7ffr - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - FTeBZkTY7ffr - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack_role/b233a04d-d453-4730-8f4d-c945948c8d3b - method: GET - response: - body: '{"created":"2020-01-26T19:04:26.096897Z","id":"b233a04d-d453-4730-8f4d-c945948c8d3b","name":"PsSRIniTNebDKOEpDnzQrZASj","rack_size":60,"updated":"2020-01-26T19:04:26.096897Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "175" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Request-Id: - - wMK/SVi78W5e - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - wMK/SVi78W5e - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack/7aadeb89-bd00-4b44-8dba-79409603b38f/layout - method: GET - response: - body: '[]' - headers: - Cache-Control: - - no-cache - Content-Length: - - "2" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Location: - - /rack/7aadeb89-bd00-4b44-8dba-79409603b38f/layout - Request-Id: - - Myw9FZancUab - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - Myw9FZancUab - status: 200 OK - code: 200 - duration: "" -- request: - body: | - {"hardware_product_id":"6621614d-ad3d-42f7-8a6d-3cb94d8bd76e","rack_id":"7aadeb89-bd00-4b44-8dba-79409603b38f","rack_unit_start":1} - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/layout - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Location: - - /layout/f34b1733-0339-41af-9fb1-f6dee2bfcc04 - Request-Id: - - vYSpt3CKfsF0 - Server: - - Mojolicious (Perl) - X-Request-Id: - - vYSpt3CKfsF0 - status: 303 See Other - code: 303 - duration: "" -- request: - body: "" - form: {} - headers: - Content-Type: - - application/json - Referer: - - http://10.51.54.42:5000/layout - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/layout/f34b1733-0339-41af-9fb1-f6dee2bfcc04 - method: GET - response: - body: '{"created":"2020-01-26T19:04:26.241889Z","hardware_product_id":"6621614d-ad3d-42f7-8a6d-3cb94d8bd76e","id":"f34b1733-0339-41af-9fb1-f6dee2bfcc04","rack_id":"7aadeb89-bd00-4b44-8dba-79409603b38f","rack_name":"zNHMvLDMgbkfUIPtAhglCOwTg:RjWeunugnyPEEqdAfhxXBwAJK","rack_unit_size":1,"rack_unit_start":1,"sku":"FxCjKnuMmPTawRhYMJmKDWwrN","updated":"2020-01-26T19:04:26.241889Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "374" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Location: - - /layout/f34b1733-0339-41af-9fb1-f6dee2bfcc04 - Request-Id: - - +sxvXTymWGlW - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - +sxvXTymWGlW - status: 200 OK - code: 200 - duration: "" -- request: - body: | - {"hardware_product_id":"2eca6269-a424-45f2-a3a9-a9913b8976ba","rack_id":"7aadeb89-bd00-4b44-8dba-79409603b38f","rack_unit_start":2} - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/layout - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Location: - - /layout/2a3ef5b9-c936-478e-9e66-c097a7e16405 - Request-Id: - - /ZDuM2OHa/nS - Server: - - Mojolicious (Perl) - X-Request-Id: - - /ZDuM2OHa/nS - status: 303 See Other - code: 303 - duration: "" -- request: - body: "" - form: {} - headers: - Content-Type: - - application/json - Referer: - - http://10.51.54.42:5000/layout - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/layout/2a3ef5b9-c936-478e-9e66-c097a7e16405 - method: GET - response: - body: '{"created":"2020-01-26T19:04:26.303297Z","hardware_product_id":"2eca6269-a424-45f2-a3a9-a9913b8976ba","id":"2a3ef5b9-c936-478e-9e66-c097a7e16405","rack_id":"7aadeb89-bd00-4b44-8dba-79409603b38f","rack_name":"zNHMvLDMgbkfUIPtAhglCOwTg:RjWeunugnyPEEqdAfhxXBwAJK","rack_unit_size":2,"rack_unit_start":2,"sku":"xYxLsvSQYDsWVAxNTrUcxHIgL","updated":"2020-01-26T19:04:26.303297Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "374" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Location: - - /layout/2a3ef5b9-c936-478e-9e66-c097a7e16405 - Request-Id: - - n0uOdMchEnes - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - n0uOdMchEnes - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack/7aadeb89-bd00-4b44-8dba-79409603b38f/layout - method: GET - response: - body: '[{"created":"2020-01-26T19:04:26.241889Z","hardware_product_id":"6621614d-ad3d-42f7-8a6d-3cb94d8bd76e","id":"f34b1733-0339-41af-9fb1-f6dee2bfcc04","rack_id":"7aadeb89-bd00-4b44-8dba-79409603b38f","rack_name":"zNHMvLDMgbkfUIPtAhglCOwTg:RjWeunugnyPEEqdAfhxXBwAJK","rack_unit_size":1,"rack_unit_start":1,"sku":"FxCjKnuMmPTawRhYMJmKDWwrN","updated":"2020-01-26T19:04:26.241889Z"},{"created":"2020-01-26T19:04:26.303297Z","hardware_product_id":"2eca6269-a424-45f2-a3a9-a9913b8976ba","id":"2a3ef5b9-c936-478e-9e66-c097a7e16405","rack_id":"7aadeb89-bd00-4b44-8dba-79409603b38f","rack_name":"zNHMvLDMgbkfUIPtAhglCOwTg:RjWeunugnyPEEqdAfhxXBwAJK","rack_unit_size":2,"rack_unit_start":2,"sku":"xYxLsvSQYDsWVAxNTrUcxHIgL","updated":"2020-01-26T19:04:26.303297Z"}]' - headers: - Cache-Control: - - no-cache - Content-Length: - - "751" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Location: - - /rack/7aadeb89-bd00-4b44-8dba-79409603b38f/layout - Request-Id: - - aIHciUSFEWpz - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - aIHciUSFEWpz - status: 200 OK - code: 200 - duration: "" -- request: - body: | - [{"serial_number":"AAAAAA","sku":"xYxLsvSQYDsWVAxNTrUcxHIgL"}] - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/build/7d2b96d4-3b2a-4a24-9fb9-fbfddf35dcc9/device - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Request-Id: - - KD8nVSooyjIi - Server: - - Mojolicious (Perl) - X-Request-Id: - - KD8nVSooyjIi - status: 204 No Content - code: 204 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/device/AAAAAA - method: GET - response: - body: '{"asset_tag":null,"build_id":"7d2b96d4-3b2a-4a24-9fb9-fbfddf35dcc9","build_name":"JIrULNPxKPKZncyExKMfLfGcY","created":"2020-01-26T19:04:26.382355Z","disks":[],"hardware_product_id":"2eca6269-a424-45f2-a3a9-a9913b8976ba","health":"unknown","hostname":null,"id":"dd559c03-8901-4250-9946-0cdcf00d953c","last_seen":null,"latest_report":null,"links":[],"location":null,"nics":[],"phase":"integration","serial_number":"AAAAAA","sku":"xYxLsvSQYDsWVAxNTrUcxHIgL","system_uuid":null,"updated":"2020-01-26T19:04:26.382355Z","uptime_since":null,"validated":null}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "552" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Etag: - - '"d1e25cc0b1cb2f7ec1380a3ce54e43f9"' - Location: - - /device/dd559c03-8901-4250-9946-0cdcf00d953c - Request-Id: - - 8Ks0NjxXOuJd - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - 8Ks0NjxXOuJd - status: 200 OK - code: 200 - duration: "" -- request: - body: | - {"test_setting":"foo"} - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/device/dd559c03-8901-4250-9946-0cdcf00d953c/settings/test_setting - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Location: - - /device/dd559c03-8901-4250-9946-0cdcf00d953c - Request-Id: - - aNAaTiQktlz2 - Server: - - Mojolicious (Perl) - X-Request-Id: - - aNAaTiQktlz2 - status: 204 No Content - code: 204 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/device/dd559c03-8901-4250-9946-0cdcf00d953c - method: GET - response: - body: '{"asset_tag":null,"build_id":"7d2b96d4-3b2a-4a24-9fb9-fbfddf35dcc9","build_name":"JIrULNPxKPKZncyExKMfLfGcY","created":"2020-01-26T19:04:26.382355Z","disks":[],"hardware_product_id":"2eca6269-a424-45f2-a3a9-a9913b8976ba","health":"unknown","hostname":null,"id":"dd559c03-8901-4250-9946-0cdcf00d953c","last_seen":null,"latest_report":null,"links":[],"location":null,"nics":[],"phase":"integration","serial_number":"AAAAAA","sku":"xYxLsvSQYDsWVAxNTrUcxHIgL","system_uuid":null,"updated":"2020-01-26T19:04:26.382355Z","uptime_since":null,"validated":null}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "552" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Etag: - - '"d1e25cc0b1cb2f7ec1380a3ce54e43f9"' - Location: - - /device/dd559c03-8901-4250-9946-0cdcf00d953c - Request-Id: - - o5X/t1oHHoTS - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - o5X/t1oHHoTS - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/device/dd559c03-8901-4250-9946-0cdcf00d953c/settings/test_setting - method: GET - response: - body: '{"test_setting":"foo"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "22" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Location: - - /device/dd559c03-8901-4250-9946-0cdcf00d953c - Request-Id: - - 6AlCjMxEEJUl - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - 6AlCjMxEEJUl - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/device/dd559c03-8901-4250-9946-0cdcf00d953c/settings - method: GET - response: - body: '{"test_setting":"foo"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "22" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Location: - - /device/dd559c03-8901-4250-9946-0cdcf00d953c - Request-Id: - - w5Jq5HsZI27f - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - w5Jq5HsZI27f - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/device/dd559c03-8901-4250-9946-0cdcf00d953c/settings - method: GET - response: - body: '{"test_setting":"foo"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "22" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Location: - - /device/dd559c03-8901-4250-9946-0cdcf00d953c - Request-Id: - - T7LUXKGp20ej - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - T7LUXKGp20ej - status: 200 OK - code: 200 - duration: "" -- request: - body: | - [{"device_id":"dd559c03-8901-4250-9946-0cdcf00d953c","rack_unit_start":1}] - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack/7aadeb89-bd00-4b44-8dba-79409603b38f/assignment - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Location: - - /rack/7aadeb89-bd00-4b44-8dba-79409603b38f/assignment - Request-Id: - - gAE5NDAxwcgr - Server: - - Mojolicious (Perl) - X-Request-Id: - - gAE5NDAxwcgr - status: 303 See Other - code: 303 - duration: "" -- request: - body: "" - form: {} - headers: - Content-Type: - - application/json - Referer: - - http://10.51.54.42:5000/rack/7aadeb89-bd00-4b44-8dba-79409603b38f/assignment - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack/7aadeb89-bd00-4b44-8dba-79409603b38f/assignment - method: GET - response: - body: '[{"device_asset_tag":null,"device_id":"dd559c03-8901-4250-9946-0cdcf00d953c","device_serial_number":"whargarbl","hardware_product_name":"ZLepZJcnMPSuCGgjCKATuzdbm","rack_unit_size":1,"rack_unit_start":1,"sku":"FxCjKnuMmPTawRhYMJmKDWwrN"},{"device_asset_tag":null,"device_id":null,"hardware_product_name":"KPHacrRjdKTXHRQkYFizhiXiJ","rack_unit_size":2,"rack_unit_start":2,"sku":"xYxLsvSQYDsWVAxNTrUcxHIgL"}]' - headers: - Cache-Control: - - no-cache - Content-Length: - - "371" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Location: - - /rack/7aadeb89-bd00-4b44-8dba-79409603b38f/assignment - Request-Id: - - Ja5w7V8WG7qq - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - Ja5w7V8WG7qq - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack/7aadeb89-bd00-4b44-8dba-79409603b38f/assignment - method: GET - response: - body: '[{"device_asset_tag":null,"device_id":"dd559c03-8901-4250-9946-0cdcf00d953c","device_serial_number":"foo","hardware_product_name":"ZLepZJcnMPSuCGgjCKATuzdbm","rack_unit_size":1,"rack_unit_start":1,"sku":"FxCjKnuMmPTawRhYMJmKDWwrN"},{"device_asset_tag":null,"device_id":null,"hardware_product_name":"KPHacrRjdKTXHRQkYFizhiXiJ","rack_unit_size":2,"rack_unit_start":2,"sku":"xYxLsvSQYDsWVAxNTrUcxHIgL"}]' - headers: - Cache-Control: - - no-cache - Content-Length: - - "371" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Location: - - /rack/7aadeb89-bd00-4b44-8dba-79409603b38f/assignment - Request-Id: - - 16FVyvYMuV+p - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - 16FVyvYMuV+p - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/device/dd559c03-8901-4250-9946-0cdcf00d953c/location - method: GET - response: - body: '{"az":"rSHTRbXlBtZUUPZowzIXvGTtU","datacenter_room":"YZScFfGvMadGNUUBxvuztEhmc","rack":"zNHMvLDMgbkfUIPtAhglCOwTg:RjWeunugnyPEEqdAfhxXBwAJK","rack_unit_start":1,"target_hardware_product":{"alias":"fZUdZGfPTpNuoaETCoAXHzHdh","hardware_vendor_id":"713e2342-ed15-409d-a07c-bbca41941d92","id":"6621614d-ad3d-42f7-8a6d-3cb94d8bd76e","name":"ZLepZJcnMPSuCGgjCKATuzdbm","sku":"FxCjKnuMmPTawRhYMJmKDWwrN"}}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "398" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Location: - - /device/dd559c03-8901-4250-9946-0cdcf00d953c - Request-Id: - - O41e1xirQEXc - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - O41e1xirQEXc - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/device/dd559c03-8901-4250-9946-0cdcf00d953c - method: GET - response: - body: '{"asset_tag":null,"build_id":"7d2b96d4-3b2a-4a24-9fb9-fbfddf35dcc9","build_name":"JIrULNPxKPKZncyExKMfLfGcY","created":"2020-01-26T19:04:26.382355Z","disks":[],"hardware_product_id":"2eca6269-a424-45f2-a3a9-a9913b8976ba","health":"unknown","hostname":null,"id":"dd559c03-8901-4250-9946-0cdcf00d953c","last_seen":null,"latest_report":null,"links":[],"location":{"az":"rSHTRbXlBtZUUPZowzIXvGTtU","datacenter_room":"YZScFfGvMadGNUUBxvuztEhmc","rack":"zNHMvLDMgbkfUIPtAhglCOwTg:RjWeunugnyPEEqdAfhxXBwAJK","rack_unit_start":1,"target_hardware_product":{"alias":"fZUdZGfPTpNuoaETCoAXHzHdh","hardware_vendor_id":"713e2342-ed15-409d-a07c-bbca41941d92","id":"6621614d-ad3d-42f7-8a6d-3cb94d8bd76e","name":"ZLepZJcnMPSuCGgjCKATuzdbm","sku":"FxCjKnuMmPTawRhYMJmKDWwrN"}},"nics":[],"phase":"integration","serial_number":"AAAAAA","sku":"xYxLsvSQYDsWVAxNTrUcxHIgL","system_uuid":null,"updated":"2020-01-26T19:04:26.382355Z","uptime_since":null,"validated":null}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "946" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Etag: - - '"d1e25cc0b1cb2f7ec1380a3ce54e43f9"' - Location: - - /device/dd559c03-8901-4250-9946-0cdcf00d953c - Request-Id: - - MlbKJnoDxIFK - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - MlbKJnoDxIFK - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack/zNHMvLDMgbkfUIPtAhglCOwTg:RjWeunugnyPEEqdAfhxXBwAJK - method: GET - response: - body: '{"asset_tag":null,"build_id":"7d2b96d4-3b2a-4a24-9fb9-fbfddf35dcc9","build_name":"JIrULNPxKPKZncyExKMfLfGcY","created":"2020-01-26T19:04:26.135631Z","datacenter_room_alias":"YZScFfGvMadGNUUBxvuztEhmc","datacenter_room_id":"89ce0266-20fe-4136-a772-e2d9ace8d956","full_rack_name":"zNHMvLDMgbkfUIPtAhglCOwTg:RjWeunugnyPEEqdAfhxXBwAJK","id":"7aadeb89-bd00-4b44-8dba-79409603b38f","name":"RjWeunugnyPEEqdAfhxXBwAJK","phase":"integration","rack_role_id":"b233a04d-d453-4730-8f4d-c945948c8d3b","rack_role_name":"PsSRIniTNebDKOEpDnzQrZASj","serial_number":null,"updated":"2020-01-26T19:04:26.135631Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "593" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Location: - - /rack/7aadeb89-bd00-4b44-8dba-79409603b38f - Request-Id: - - rV6q113SMqcf - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - rV6q113SMqcf - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack_role/b233a04d-d453-4730-8f4d-c945948c8d3b - method: GET - response: - body: '{"created":"2020-01-26T19:04:26.096897Z","id":"b233a04d-d453-4730-8f4d-c945948c8d3b","name":"PsSRIniTNebDKOEpDnzQrZASj","rack_size":60,"updated":"2020-01-26T19:04:26.096897Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "175" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Request-Id: - - ZJ7qR3LYUcAi - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - ZJ7qR3LYUcAi - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/room/89ce0266-20fe-4136-a772-e2d9ace8d956 - method: GET - response: - body: '{"alias":"YZScFfGvMadGNUUBxvuztEhmc","az":"rSHTRbXlBtZUUPZowzIXvGTtU","created":"2020-01-26T19:04:26.008325Z","datacenter_id":"bc85ceef-2d58-45ae-a659-1b666f794795","id":"89ce0266-20fe-4136-a772-e2d9ace8d956","updated":"2020-01-26T19:04:26.008325Z","vendor_name":"zNHMvLDMgbkfUIPtAhglCOwTg"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "291" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Location: - - /room/89ce0266-20fe-4136-a772-e2d9ace8d956 - Request-Id: - - cJjqMBT/qTD4 - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - cJjqMBT/qTD4 - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack_role/b233a04d-d453-4730-8f4d-c945948c8d3b - method: GET - response: - body: '{"created":"2020-01-26T19:04:26.096897Z","id":"b233a04d-d453-4730-8f4d-c945948c8d3b","name":"PsSRIniTNebDKOEpDnzQrZASj","rack_size":60,"updated":"2020-01-26T19:04:26.096897Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "175" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Request-Id: - - rFokMvOBy8qX - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - rFokMvOBy8qX - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_product/2eca6269-a424-45f2-a3a9-a9913b8976ba - method: GET - response: - body: '{"alias":"VgvjJOOnrRwUYxWQFOtemUQuQ","bios_firmware":"XqkAkCZYLvgRLvYqIrtHPFhVN","cpu_num":0,"cpu_type":"AjbeLNHApsBhqcpiSwSZtssQx","created":"2020-01-26T19:04:25.859308Z","dimms_num":0,"generation_name":null,"hardware_vendor_id":"713e2342-ed15-409d-a07c-bbca41941d92","hba_firmware":null,"id":"2eca6269-a424-45f2-a3a9-a9913b8976ba","legacy_product_name":null,"name":"KPHacrRjdKTXHRQkYFizhiXiJ","nics_num":0,"nvme_ssd_num":0,"nvme_ssd_size":null,"nvme_ssd_slots":null,"prefix":null,"psu_total":0,"purpose":"uNggImrxtZCFRXriVRahxKFvd","rack_unit_size":2,"raid_lun_num":0,"ram_total":0,"sas_hdd_num":0,"sas_hdd_size":null,"sas_hdd_slots":null,"sas_ssd_num":0,"sas_ssd_size":null,"sas_ssd_slots":null,"sata_hdd_num":0,"sata_hdd_size":null,"sata_hdd_slots":null,"sata_ssd_num":0,"sata_ssd_size":null,"sata_ssd_slots":null,"sku":"xYxLsvSQYDsWVAxNTrUcxHIgL","specification":null,"updated":"2020-01-26T19:04:25.859308Z","usb_num":0,"validation_plan_id":"a30ab8b2-8a9e-4e51-8bb0-92862abd8b54"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "985" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Location: - - /hardware_product/2eca6269-a424-45f2-a3a9-a9913b8976ba - Request-Id: - - UwnKm3FJ1z+6 - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - UwnKm3FJ1z+6 - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/device/dd559c03-8901-4250-9946-0cdcf00d953c/validation_state - method: GET - response: - body: '{"error":"Entity Not Found"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "28" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Request-Id: - - Nue0rc9wjDHk - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - Nue0rc9wjDHk - status: 404 NOT FOUND - code: 404 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/device/dd559c03-8901-4250-9946-0cdcf00d953c/validation_state - method: GET - response: - body: '{"error":"Entity Not Found"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "28" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Request-Id: - - ixZVfEKuJrJL - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - ixZVfEKuJrJL - status: 404 NOT FOUND - code: 404 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/device/dd559c03-8901-4250-9946-0cdcf00d953c/phase - method: GET - response: - body: '{"id":"dd559c03-8901-4250-9946-0cdcf00d953c","phase":"integration"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "67" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Location: - - /device/dd559c03-8901-4250-9946-0cdcf00d953c - Request-Id: - - JqWLVQaJY9Ux - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - JqWLVQaJY9Ux - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/device/dd559c03-8901-4250-9946-0cdcf00d953c/location - method: DELETE - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Sun, 26 Jan 2020 19:04:26 GMT - Location: - - /device/dd559c03-8901-4250-9946-0cdcf00d953c - Request-Id: - - SPqYNyeA82gr - Server: - - Mojolicious (Perl) - X-Request-Id: - - SPqYNyeA82gr - status: 204 No Content - code: 204 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/device/dd559c03-8901-4250-9946-0cdcf00d953c - method: GET - response: - body: '{"asset_tag":null,"build_id":"7d2b96d4-3b2a-4a24-9fb9-fbfddf35dcc9","build_name":"JIrULNPxKPKZncyExKMfLfGcY","created":"2020-01-26T19:04:26.382355Z","disks":[],"hardware_product_id":"2eca6269-a424-45f2-a3a9-a9913b8976ba","health":"unknown","hostname":null,"id":"dd559c03-8901-4250-9946-0cdcf00d953c","last_seen":null,"latest_report":null,"links":[],"location":null,"nics":[],"phase":"integration","serial_number":"AAAAAA","sku":"xYxLsvSQYDsWVAxNTrUcxHIgL","system_uuid":null,"updated":"2020-01-26T19:04:26.382355Z","uptime_since":null,"validated":null}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "552" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Etag: - - '"d1e25cc0b1cb2f7ec1380a3ce54e43f9"' - Location: - - /device/dd559c03-8901-4250-9946-0cdcf00d953c - Request-Id: - - Rix792kKn1nu - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - Rix792kKn1nu - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack/7aadeb89-bd00-4b44-8dba-79409603b38f/layout - method: GET - response: - body: '[{"created":"2020-01-26T19:04:26.241889Z","hardware_product_id":"6621614d-ad3d-42f7-8a6d-3cb94d8bd76e","id":"f34b1733-0339-41af-9fb1-f6dee2bfcc04","rack_id":"7aadeb89-bd00-4b44-8dba-79409603b38f","rack_name":"zNHMvLDMgbkfUIPtAhglCOwTg:RjWeunugnyPEEqdAfhxXBwAJK","rack_unit_size":1,"rack_unit_start":1,"sku":"FxCjKnuMmPTawRhYMJmKDWwrN","updated":"2020-01-26T19:04:26.241889Z"},{"created":"2020-01-26T19:04:26.303297Z","hardware_product_id":"2eca6269-a424-45f2-a3a9-a9913b8976ba","id":"2a3ef5b9-c936-478e-9e66-c097a7e16405","rack_id":"7aadeb89-bd00-4b44-8dba-79409603b38f","rack_name":"zNHMvLDMgbkfUIPtAhglCOwTg:RjWeunugnyPEEqdAfhxXBwAJK","rack_unit_size":2,"rack_unit_start":2,"sku":"xYxLsvSQYDsWVAxNTrUcxHIgL","updated":"2020-01-26T19:04:26.303297Z"}]' - headers: - Cache-Control: - - no-cache - Content-Length: - - "751" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Location: - - /rack/7aadeb89-bd00-4b44-8dba-79409603b38f/layout - Request-Id: - - Sv6Wf3OMjeLd - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - Sv6Wf3OMjeLd - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/layout/f34b1733-0339-41af-9fb1-f6dee2bfcc04 - method: DELETE - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Request-Id: - - OhiSnajt+K+c - Server: - - Mojolicious (Perl) - X-Request-Id: - - OhiSnajt+K+c - status: 204 No Content - code: 204 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/layout/2a3ef5b9-c936-478e-9e66-c097a7e16405 - method: DELETE - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Request-Id: - - 1BOv/zAB4wau - Server: - - Mojolicious (Perl) - X-Request-Id: - - 1BOv/zAB4wau - status: 204 No Content - code: 204 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack/7aadeb89-bd00-4b44-8dba-79409603b38f - method: DELETE - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Request-Id: - - /7hM/VUO8MM/ - Server: - - Mojolicious (Perl) - X-Request-Id: - - /7hM/VUO8MM/ - status: 204 No Content - code: 204 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack_role/b233a04d-d453-4730-8f4d-c945948c8d3b - method: DELETE - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Request-Id: - - V1PMPT8xHEFL - Server: - - Mojolicious (Perl) - X-Request-Id: - - V1PMPT8xHEFL - status: 204 No Content - code: 204 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/room/89ce0266-20fe-4136-a772-e2d9ace8d956 - method: DELETE - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Request-Id: - - Pd/UAMZo4LsP - Server: - - Mojolicious (Perl) - X-Request-Id: - - Pd/UAMZo4LsP - status: 204 No Content - code: 204 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/dc/bc85ceef-2d58-45ae-a659-1b666f794795 - method: DELETE - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Request-Id: - - f8ZA/qrH1uk1 - Server: - - Mojolicious (Perl) - X-Request-Id: - - f8ZA/qrH1uk1 - status: 204 No Content - code: 204 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_product/6621614d-ad3d-42f7-8a6d-3cb94d8bd76e - method: DELETE - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Request-Id: - - c3REpBq036At - Server: - - Mojolicious (Perl) - X-Request-Id: - - c3REpBq036At - status: 204 No Content - code: 204 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_product/2eca6269-a424-45f2-a3a9-a9913b8976ba - method: DELETE - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Request-Id: - - FzgftV1RI9lB - Server: - - Mojolicious (Perl) - X-Request-Id: - - FzgftV1RI9lB - status: 204 No Content - code: 204 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_vendor/713e2342-ed15-409d-a07c-bbca41941d92 - method: DELETE - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Request-Id: - - mhjduxXzfUld - Server: - - Mojolicious (Perl) - X-Request-Id: - - mhjduxXzfUld - status: 204 No Content - code: 204 - duration: "" diff --git a/fixtures/conch-v3/hardware.yaml b/fixtures/conch-v3/hardware.yaml deleted file mode 100644 index 3649a3d..0000000 --- a/fixtures/conch-v3/hardware.yaml +++ /dev/null @@ -1,669 +0,0 @@ ---- -version: 1 -interactions: -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_vendor/MyBigVendor - method: GET - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Request-Id: - - yHryKLGiwfQs - Server: - - Mojolicious (Perl) - X-Request-Id: - - yHryKLGiwfQs - status: 410 Gone - code: 410 - duration: "" -- request: - body: "" - form: {} - headers: - Referer: - - http://10.51.54.42:5000/hardware_vendor/MyBigVendor - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_vendor/26df0913-5614-4fec-beb7-3514df2a9356 - method: GET - response: - body: '{"created":"2020-01-26T19:04:27.318653Z","id":"26df0913-5614-4fec-beb7-3514df2a9356","name":"MyBigVendor","updated":"2020-01-26T19:04:27.318653Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "146" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Request-Id: - - lH2DCHJ6c0bK - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - lH2DCHJ6c0bK - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_vendor/MyBigVendor - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Location: - - /hardware_vendor/26df0913-5614-4fec-beb7-3514df2a9356 - Request-Id: - - O/RnQuIryh2M - Server: - - Mojolicious (Perl) - X-Request-Id: - - O/RnQuIryh2M - status: 303 See Other - code: 303 - duration: "" -- request: - body: "" - form: {} - headers: - Referer: - - http://10.51.54.42:5000/hardware_vendor/MyBigVendor - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_vendor/MyBigVendor - method: GET - response: - body: '{"created":"2020-01-26T19:04:27.318653Z","id":"26df0913-5614-4fec-beb7-3514df2a9356","name":"MyBigVendor","updated":"2020-01-26T19:04:27.318653Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "146" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Request-Id: - - lH2DCHJ6c0bK - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - lH2DCHJ6c0bK - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_vendor/MyBigVendor - method: GET - response: - body: '{"created":"2020-01-26T19:04:27.318653Z","id":"26df0913-5614-4fec-beb7-3514df2a9356","name":"MyBigVendor","updated":"2020-01-26T19:04:27.318653Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "146" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Request-Id: - - sx/AkRN1wq8j - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - sx/AkRN1wq8j - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/validation_plan/a30ab8b2-8a9e-4e51-8bb0-92862abd8b54 - method: GET - response: - body: '{"created":"2019-10-09T17:21:37.861483Z","description":"Validation plan - containing all validations run in Conch v1 on servers","id":"a30ab8b2-8a9e-4e51-8bb0-92862abd8b54","name":"Conch - v1 Legacy Plan: Server"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "209" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Request-Id: - - 7HgUuktKxlmq - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - 7HgUuktKxlmq - status: 200 OK - code: 200 - duration: "" -- request: - body: | - {"alias":"VGyZdhHTSlKEevcIXLFdqLBqV","bios_firmware":"WyWzHYIRLAEpwgatcUwjipqkh","cpu_type":"XddLqzjJkILoECmoUYhruLOeT","hardware_vendor_id":"26df0913-5614-4fec-beb7-3514df2a9356","name":"vVrberRcPGVlljXeonTdwsovX","purpose":"BwdoYKPEhePiedtBHebHTdNnG","rack_unit_size":2,"sku":"aPHUOTzVZjFLgMpaBtWWZiXKw","validation_plan_id":"a30ab8b2-8a9e-4e51-8bb0-92862abd8b54"} - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_product - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Location: - - /hardware_product/6542884c-3515-49fe-9ebd-7da5da10e024 - Request-Id: - - MS4c0PaPyTic - Server: - - Mojolicious (Perl) - X-Request-Id: - - MS4c0PaPyTic - status: 303 See Other - code: 303 - duration: "" -- request: - body: "" - form: {} - headers: - Content-Type: - - application/json - Referer: - - http://10.51.54.42:5000/hardware_product - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_product/6542884c-3515-49fe-9ebd-7da5da10e024 - method: GET - response: - body: '{"alias":"VGyZdhHTSlKEevcIXLFdqLBqV","bios_firmware":"WyWzHYIRLAEpwgatcUwjipqkh","cpu_num":0,"cpu_type":"XddLqzjJkILoECmoUYhruLOeT","created":"2020-01-26T19:04:27.394823Z","dimms_num":0,"generation_name":null,"hardware_vendor_id":"26df0913-5614-4fec-beb7-3514df2a9356","hba_firmware":null,"id":"6542884c-3515-49fe-9ebd-7da5da10e024","legacy_product_name":null,"name":"vVrberRcPGVlljXeonTdwsovX","nics_num":0,"nvme_ssd_num":0,"nvme_ssd_size":null,"nvme_ssd_slots":null,"prefix":null,"psu_total":0,"purpose":"BwdoYKPEhePiedtBHebHTdNnG","rack_unit_size":2,"raid_lun_num":0,"ram_total":0,"sas_hdd_num":0,"sas_hdd_size":null,"sas_hdd_slots":null,"sas_ssd_num":0,"sas_ssd_size":null,"sas_ssd_slots":null,"sata_hdd_num":0,"sata_hdd_size":null,"sata_hdd_slots":null,"sata_ssd_num":0,"sata_ssd_size":null,"sata_ssd_slots":null,"sku":"aPHUOTzVZjFLgMpaBtWWZiXKw","specification":null,"updated":"2020-01-26T19:04:27.394823Z","usb_num":0,"validation_plan_id":"a30ab8b2-8a9e-4e51-8bb0-92862abd8b54"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "985" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Location: - - /hardware_product/6542884c-3515-49fe-9ebd-7da5da10e024 - Request-Id: - - koTTlnYgOMcC - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - koTTlnYgOMcC - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_product - method: GET - response: - body: '[{"alias":"VGyZdhHTSlKEevcIXLFdqLBqV","created":"2020-01-26T19:04:27.394823Z","generation_name":null,"id":"6542884c-3515-49fe-9ebd-7da5da10e024","name":"vVrberRcPGVlljXeonTdwsovX","sku":"aPHUOTzVZjFLgMpaBtWWZiXKw","updated":"2020-01-26T19:04:27.394823Z"}]' - headers: - Cache-Control: - - no-cache - Content-Length: - - "987" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Request-Id: - - FLz0SlzrwAvo - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - FLz0SlzrwAvo - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_product/vVrberRcPGVlljXeonTdwsovX - method: GET - response: - body: '{"alias":"VGyZdhHTSlKEevcIXLFdqLBqV","bios_firmware":"WyWzHYIRLAEpwgatcUwjipqkh","cpu_num":0,"cpu_type":"XddLqzjJkILoECmoUYhruLOeT","created":"2020-01-26T19:04:27.394823Z","dimms_num":0,"generation_name":null,"hardware_vendor_id":"26df0913-5614-4fec-beb7-3514df2a9356","hba_firmware":null,"id":"6542884c-3515-49fe-9ebd-7da5da10e024","legacy_product_name":null,"name":"vVrberRcPGVlljXeonTdwsovX","nics_num":0,"nvme_ssd_num":0,"nvme_ssd_size":null,"nvme_ssd_slots":null,"prefix":null,"psu_total":0,"purpose":"BwdoYKPEhePiedtBHebHTdNnG","rack_unit_size":2,"raid_lun_num":0,"ram_total":0,"sas_hdd_num":0,"sas_hdd_size":null,"sas_hdd_slots":null,"sas_ssd_num":0,"sas_ssd_size":null,"sas_ssd_slots":null,"sata_hdd_num":0,"sata_hdd_size":null,"sata_hdd_slots":null,"sata_ssd_num":0,"sata_ssd_size":null,"sata_ssd_slots":null,"sku":"aPHUOTzVZjFLgMpaBtWWZiXKw","specification":null,"updated":"2020-01-26T19:04:27.394823Z","usb_num":0,"validation_plan_id":"a30ab8b2-8a9e-4e51-8bb0-92862abd8b54"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "985" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Location: - - /hardware_product/6542884c-3515-49fe-9ebd-7da5da10e024 - Request-Id: - - TbsmficXO0YH - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - TbsmficXO0YH - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_product/6542884c-3515-49fe-9ebd-7da5da10e024 - method: DELETE - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Request-Id: - - l4BtZ5cGuzA5 - Server: - - Mojolicious (Perl) - X-Request-Id: - - l4BtZ5cGuzA5 - status: 204 No Content - code: 204 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_product - method: GET - response: - body: '[]' - headers: - Cache-Control: - - no-cache - Content-Length: - - "2" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Request-Id: - - 1NZOerYeQzWD - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - 1NZOerYeQzWD - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/validation_plan/a30ab8b2-8a9e-4e51-8bb0-92862abd8b54 - method: GET - response: - body: '{"created":"2019-10-09T17:21:37.861483Z","description":"Validation plan - containing all validations run in Conch v1 on servers","id":"a30ab8b2-8a9e-4e51-8bb0-92862abd8b54","name":"Conch - v1 Legacy Plan: Server"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "209" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Request-Id: - - Uy+k2xde7ghB - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - Uy+k2xde7ghB - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_vendor/MyBigVendor - method: GET - response: - body: '{"created":"2020-01-26T19:04:27.318653Z","id":"26df0913-5614-4fec-beb7-3514df2a9356","name":"MyBigVendor","updated":"2020-01-26T19:04:27.318653Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "146" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Request-Id: - - By8YIJ4rIiYz - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - By8YIJ4rIiYz - status: 200 OK - code: 200 - duration: "" -- request: - body: | - {"alias":"ujOmocHFAUuWZILajRAzVkeuO","bios_firmware":"RcgHsdxbvsvNXWQMpuLchiLgH","cpu_type":"FEkEUQAJTUIwzzxxHsXjxWJqN","hardware_vendor_id":"26df0913-5614-4fec-beb7-3514df2a9356","name":"Testy McTesterson","purpose":"FCYNIyfxlJtZmSIluDaoPNwRD","rack_unit_size":2,"sku":"test-sku-001","validation_plan_id":"a30ab8b2-8a9e-4e51-8bb0-92862abd8b54"} - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_product - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Location: - - /hardware_product/9ad55ceb-2eb7-4125-a492-3c595277b3e3 - Request-Id: - - nhTT+fVwUqSP - Server: - - Mojolicious (Perl) - X-Request-Id: - - nhTT+fVwUqSP - status: 303 See Other - code: 303 - duration: "" -- request: - body: "" - form: {} - headers: - Content-Type: - - application/json - Referer: - - http://10.51.54.42:5000/hardware_product - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_product/9ad55ceb-2eb7-4125-a492-3c595277b3e3 - method: GET - response: - body: '{"alias":"ujOmocHFAUuWZILajRAzVkeuO","created":"2020-01-26T19:04:27.567389Z","generation_name":null,"id":"9ad55ceb-2eb7-4125-a492-3c595277b3e3","name":"Testy McTesterson","sku":"test-sku-001","updated":"2020-01-26T19:04:27.567389Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "964" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Location: - - /hardware_product/9ad55ceb-2eb7-4125-a492-3c595277b3e3 - Request-Id: - - jKX0hRSPa2SP - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - jKX0hRSPa2SP - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_product - method: GET - response: - body: '[{"alias":"ujOmocHFAUuWZILajRAzVkeuO","created":"2020-01-26T19:04:27.567389Z","generation_name":null,"id":"9ad55ceb-2eb7-4125-a492-3c595277b3e3","name":"Testy McTesterson","sku":"test-sku-001","updated":"2020-01-26T19:04:27.567389Z"}]' - headers: - Cache-Control: - - no-cache - Content-Length: - - "966" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Request-Id: - - 5LybBpqXOWyy - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - 5LybBpqXOWyy - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_product/test-sku-001 - method: GET - response: - body: '{"alias":"ujOmocHFAUuWZILajRAzVkeuO","bios_firmware":"RcgHsdxbvsvNXWQMpuLchiLgH","cpu_num":0,"cpu_type":"FEkEUQAJTUIwzzxxHsXjxWJqN","created":"2020-01-26T19:04:27.567389Z","dimms_num":0,"generation_name":null,"hardware_vendor_id":"26df0913-5614-4fec-beb7-3514df2a9356","hba_firmware":null,"id":"9ad55ceb-2eb7-4125-a492-3c595277b3e3","legacy_product_name":null,"name":"Testy - McTesterson","nics_num":0,"nvme_ssd_num":0,"nvme_ssd_size":null,"nvme_ssd_slots":null,"prefix":null,"psu_total":0,"purpose":"FCYNIyfxlJtZmSIluDaoPNwRD","rack_unit_size":2,"raid_lun_num":0,"ram_total":0,"sas_hdd_num":0,"sas_hdd_size":null,"sas_hdd_slots":null,"sas_ssd_num":0,"sas_ssd_size":null,"sas_ssd_slots":null,"sata_hdd_num":0,"sata_hdd_size":null,"sata_hdd_slots":null,"sata_ssd_num":0,"sata_ssd_size":null,"sata_ssd_slots":null,"sku":"test-sku-001","specification":null,"updated":"2020-01-26T19:04:27.567389Z","usb_num":0,"validation_plan_id":"a30ab8b2-8a9e-4e51-8bb0-92862abd8b54"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "964" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Location: - - /hardware_product/9ad55ceb-2eb7-4125-a492-3c595277b3e3 - Request-Id: - - rZjWwuRlUXNA - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - rZjWwuRlUXNA - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_product/test-sku-001 - method: GET - response: - body: '{"alias":"ujOmocHFAUuWZILajRAzVkeuO","bios_firmware":"RcgHsdxbvsvNXWQMpuLchiLgH","cpu_num":0,"cpu_type":"FEkEUQAJTUIwzzxxHsXjxWJqN","created":"2020-01-26T19:04:27.567389Z","dimms_num":0,"generation_name":null,"hardware_vendor_id":"26df0913-5614-4fec-beb7-3514df2a9356","hba_firmware":null,"id":"9ad55ceb-2eb7-4125-a492-3c595277b3e3","legacy_product_name":null,"name":"Testy - McTesterson","nics_num":0,"nvme_ssd_num":0,"nvme_ssd_size":null,"nvme_ssd_slots":null,"prefix":null,"psu_total":0,"purpose":"FCYNIyfxlJtZmSIluDaoPNwRD","rack_unit_size":2,"raid_lun_num":0,"ram_total":0,"sas_hdd_num":0,"sas_hdd_size":null,"sas_hdd_slots":null,"sas_ssd_num":0,"sas_ssd_size":null,"sas_ssd_slots":null,"sata_hdd_num":0,"sata_hdd_size":null,"sata_hdd_slots":null,"sata_ssd_num":0,"sata_ssd_size":null,"sata_ssd_slots":null,"sku":"test-sku-001","specification":null,"updated":"2020-01-26T19:04:27.567389Z","usb_num":0,"validation_plan_id":"a30ab8b2-8a9e-4e51-8bb0-92862abd8b54"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "964" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Location: - - /hardware_product/9ad55ceb-2eb7-4125-a492-3c595277b3e3 - Request-Id: - - hKSVv8weWwWQ - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - hKSVv8weWwWQ - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_product/9ad55ceb-2eb7-4125-a492-3c595277b3e3 - method: DELETE - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Request-Id: - - G71rcNshbzNR - Server: - - Mojolicious (Perl) - X-Request-Id: - - G71rcNshbzNR - status: 204 No Content - code: 204 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_product - method: GET - response: - body: '[]' - headers: - Cache-Control: - - no-cache - Content-Length: - - "2" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Request-Id: - - 5YyZpFY6Qs9n - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - 5YyZpFY6Qs9n - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_vendor/26df0913-5614-4fec-beb7-3514df2a9356 - method: DELETE - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Request-Id: - - mmDKcSoCyYs3 - Server: - - Mojolicious (Perl) - X-Request-Id: - - mmDKcSoCyYs3 - status: 204 No Content - code: 204 - duration: "" diff --git a/fixtures/conch-v3/organizations.yaml b/fixtures/conch-v3/organizations.yaml deleted file mode 100644 index 177b176..0000000 --- a/fixtures/conch-v3/organizations.yaml +++ /dev/null @@ -1,152 +0,0 @@ ---- -version: 1 -interactions: -- request: - body: | - {"admins":[{"email":"conch@example.com"}],"description":"PtFFRMKgUbeBFwcRPNYNlriDu","name":"dkAXyNKWyWvFunIndMMymOgzY"} - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/organization - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Mon, 13 Jan 2020 23:16:22 GMT - Location: - - /organization/d6be199d-6304-43d2-8d29-61ddf3d06202 - Request-Id: - - J5JxRRyic9Yf - Server: - - Mojolicious (Perl) - X-Request-Id: - - J5JxRRyic9Yf - status: 303 See Other - code: 303 - duration: "" -- request: - body: "" - form: {} - headers: - Content-Type: - - application/json - Referer: - - http://10.51.54.42:5000/organization - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/organization/d6be199d-6304-43d2-8d29-61ddf3d06202 - method: GET - response: - body: '{"builds":[],"created":"2020-01-13T23:16:22.430957Z","description":"PtFFRMKgUbeBFwcRPNYNlriDu","id":"d6be199d-6304-43d2-8d29-61ddf3d06202","name":"dkAXyNKWyWvFunIndMMymOgzY","users":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh","role":"admin"}]}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "287" - Content-Type: - - application/json - Date: - - Mon, 13 Jan 2020 23:16:22 GMT - Request-Id: - - PJMJIdqFliqe - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - PJMJIdqFliqe - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/organization - method: GET - response: - body: '[{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"builds":[],"created":"2019-10-11T15:51:49.658646Z","description":"RsXzCQgZSaRzMdRaZbIHZtOrQ","id":"cc1f48dd-a1d4-4770-b5c3-f70d5a983230","name":"JvqCniFMdeXOrYBZRnoClgSRs"},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"builds":[],"created":"2020-01-13T23:16:22.430957Z","description":"PtFFRMKgUbeBFwcRPNYNlriDu","id":"d6be199d-6304-43d2-8d29-61ddf3d06202","name":"dkAXyNKWyWvFunIndMMymOgzY"},{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"builds":[],"created":"2019-10-11T15:50:43.648304Z","description":"VNYmjmsURYwMtEkjXTetcDpdX","id":"e8220d7b-1df5-4d51-b0d1-825727b7b782","name":"sLfdmgoVarPSafRquDlosTPPw"}]' - headers: - Cache-Control: - - no-cache - Content-Length: - - "823" - Content-Type: - - application/json - Date: - - Mon, 13 Jan 2020 23:16:22 GMT - Request-Id: - - PGPdpZwarWey - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - PGPdpZwarWey - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/organization/d6be199d-6304-43d2-8d29-61ddf3d06202 - method: GET - response: - body: '{"builds":[],"created":"2020-01-13T23:16:22.430957Z","description":"PtFFRMKgUbeBFwcRPNYNlriDu","id":"d6be199d-6304-43d2-8d29-61ddf3d06202","name":"dkAXyNKWyWvFunIndMMymOgzY","users":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh","role":"admin"}]}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "287" - Content-Type: - - application/json - Date: - - Mon, 13 Jan 2020 23:16:22 GMT - Request-Id: - - e0gIG9euxrr7 - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - e0gIG9euxrr7 - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/organization/d6be199d-6304-43d2-8d29-61ddf3d06202 - method: DELETE - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Mon, 13 Jan 2020 23:16:22 GMT - Request-Id: - - cbsws+UgUYUd - Server: - - Mojolicious (Perl) - X-Request-Id: - - cbsws+UgUYUd - status: 204 No Content - code: 204 - duration: "" diff --git a/fixtures/conch-v3/racks-roles.yaml b/fixtures/conch-v3/racks-roles.yaml deleted file mode 100644 index c95f6c0..0000000 --- a/fixtures/conch-v3/racks-roles.yaml +++ /dev/null @@ -1,158 +0,0 @@ ---- -version: 1 -interactions: -- request: - body: | - {"name":"YUuUQAAwilNBTkBktQLbCjoUJ","rack_size":60} - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack_role - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Mon, 13 Jan 2020 23:16:23 GMT - Location: - - /rack_role/214fc519-ce44-47e9-b4f6-f802ce3e2d4a - Request-Id: - - GNC6vNod5gX+ - Server: - - Mojolicious (Perl) - X-Request-Id: - - GNC6vNod5gX+ - status: 303 See Other - code: 303 - duration: "" -- request: - body: "" - form: {} - headers: - Content-Type: - - application/json - Referer: - - http://10.51.54.42:5000/rack_role - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack_role/214fc519-ce44-47e9-b4f6-f802ce3e2d4a - method: GET - response: - body: '{"created":"2020-01-13T23:16:23.874950Z","id":"214fc519-ce44-47e9-b4f6-f802ce3e2d4a","name":"YUuUQAAwilNBTkBktQLbCjoUJ","rack_size":60,"updated":"2020-01-13T23:16:23.874950Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "175" - Content-Type: - - application/json - Date: - - Mon, 13 Jan 2020 23:16:23 GMT - Request-Id: - - e3hHEeR2/2I3 - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - e3hHEeR2/2I3 - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack_role - method: GET - response: - body: '[{"created":"2019-10-28T16:31:45.818911Z","id":"7f8680e1-4d5e-4c33-b636-fe33a2a01c9c","name":"ANvynKZieNlwlMToIbcFTNXYt","rack_size":60,"updated":"2019-10-28T16:31:45.818911Z"},{"created":"2020-01-13T16:49:07.214623Z","id":"c186e9a1-b133-42cc-b7fa-b18aec376800","name":"AXKYqfAqbfQEfCwIEEnqeqKMC","rack_size":60,"updated":"2020-01-13T16:49:07.214623Z"},{"created":"2020-01-13T22:27:29.631654Z","id":"8adf3a9f-a8ee-495f-8b9f-6de1551ca1b7","name":"AjliDrIuoNdaSIbiEHbKSaouD","rack_size":60,"updated":"2020-01-13T22:27:29.631654Z"},{"created":"2020-01-13T22:31:59.550341Z","id":"8e5f2fbe-76e0-4ab9-8207-e7c7e757fb6a","name":"AkRqzAECSCZUlEwVBFWRzYNcg","rack_size":60,"updated":"2020-01-13T22:31:59.550341Z"},{"created":"2019-10-30T18:07:46.424421Z","id":"47a47060-6ee5-4360-96e0-5472865decc3","name":"ApOfKQujrdhJawfoJQRXTWwrK","rack_size":60,"updated":"2019-10-30T18:07:46.424421Z"},{"created":"2019-11-01T19:08:04.961255Z","id":"40faa958-dbbf-4c9b-8ca6-8231fa97dc08","name":"ArJMvNYeFZAdXYHiqaKbUlrfX","rack_size":60,"updated":"2019-11-01T19:08:04.961255Z"},{"created":"2019-10-31T19:38:36.274319Z","id":"2554258c-0e7c-40a1-9c21-9f6e48aa0a6a","name":"AyJBIjAikdMxalCJuSQqpnjjd","rack_size":60,"updated":"2019-10-31T19:38:36.274319Z"},{"created":"2019-10-31T02:47:36.036365Z","id":"c7e8e373-481d-4664-a5d6-77a12537f34b","name":"BPsFJgtcmlCTpHaDZCneBBMal","rack_size":60,"updated":"2019-10-31T02:47:36.036365Z"},{"created":"2019-10-21T16:18:52.147877Z","id":"8bb3404c-2d89-4404-bfbe-3d02c7f2223a","name":"BeKGnxlkqfHBaWaOFmgbNLDOz","rack_size":60,"updated":"2019-10-21T16:18:52.147877Z"},{"created":"2019-10-31T03:35:04.157942Z","id":"ad225646-f48a-466b-9b8c-40ba73a781ed","name":"BigAfdkPcfjMZXrKJxSqvpOWO","rack_size":60,"updated":"2019-10-31T03:35:04.157942Z"},{"created":"2019-10-28T14:52:19.855408Z","id":"a66ff9da-abba-4d44-a272-584dddea25d7","name":"BnZYMxiZEiLIiuHpNAcWCAedt","rack_size":60,"updated":"2019-10-28T14:52:19.855408Z"},{"created":"2019-10-31T14:15:00.512867Z","id":"9c5792d0-cb30-4203-b58a-6f1611131b68","name":"BocnMTvvFYdlAShPvsFxKAUbl","rack_size":60,"updated":"2019-10-31T14:15:00.512867Z"},{"created":"2019-10-28T15:43:08.276945Z","id":"10085710-28a0-4dac-8633-044c674c87de","name":"BsgGAFxlvaLOckDpvvAudfvtG","rack_size":60,"updated":"2019-10-28T15:43:08.276945Z"},{"created":"2019-10-28T20:20:53.737445Z","id":"fab3cf6e-e941-465a-8878-178b1fcbdd8c","name":"CSikhcFQhfoCTYWlnScvAkXQR","rack_size":60,"updated":"2019-10-28T20:20:53.737445Z"},{"created":"2020-01-13T22:36:42.739036Z","id":"38f7db91-7a9a-4787-9a8c-a469441a51e4","name":"CafFKirkWABBxOMKxgWIUbNDk","rack_size":60,"updated":"2020-01-13T22:36:42.739036Z"},{"created":"2019-10-21T16:19:18.157891Z","id":"1fa2f771-41b3-456b-874d-9499ad4a2059","name":"CnCdzngToLsiqFEQWOkzKMFhi","rack_size":60,"updated":"2019-10-21T16:19:18.157891Z"},{"created":"2019-10-31T03:30:53.116230Z","id":"f542bb16-2403-4547-8e9c-ba0bf6f30721","name":"CsvOOSyKABqENyYooqfizJcLC","rack_size":60,"updated":"2019-10-31T03:30:53.116230Z"},{"created":"2019-10-17T17:37:09.617412Z","id":"1528f898-301f-42c3-b34c-71e389c4c72a","name":"DDnchzoZRbsSqPndpWcDwCdoi","rack_size":60,"updated":"2019-10-17T17:37:09.617412Z"},{"created":"2019-10-30T18:11:23.503438Z","id":"62205098-bbd5-4fc4-9a76-095a7f90fdc6","name":"DUFHPlqrgNKTycTTOwZuVjjOI","rack_size":60,"updated":"2019-10-30T18:11:23.503438Z"},{"created":"2019-10-30T18:07:46.390300Z","id":"344e0b0b-04b1-454f-8c73-eaec597f9e8c","name":"DmwcnGqkSncfsmMZgZDwecyuh","rack_size":60,"updated":"2019-10-30T18:07:46.390300Z"},{"created":"2020-01-13T21:29:21.024851Z","id":"14c3fa7f-737a-4374-81ea-56efc5f8384a","name":"DnSWMzZRCHGqFdbLequpyLwNd","rack_size":60,"updated":"2020-01-13T21:29:21.024851Z"},{"created":"2020-01-13T22:28:26.165880Z","id":"7e0f4a76-90e0-4f3e-8183-18d0fc208244","name":"DympxUhxibniyUKZHDXSBCPKG","rack_size":60,"updated":"2020-01-13T22:28:26.165880Z"},{"created":"2019-10-31T02:55:25.442612Z","id":"e7670335-86d1-4c90-82cf-55aacd960fe4","name":"EJYCOpJjzWVmvEMCqdqEbYWzT","rack_size":60,"updated":"2019-10-31T02:55:25.442612Z"},{"created":"2019-10-31T19:00:25.110279Z","id":"3aea6f53-b2ac-47c6-aa84-1ca92681a8d9","name":"ELZflhcYKgEhTCQXdVBKwEOKh","rack_size":60,"updated":"2019-10-31T19:00:25.110279Z"},{"created":"2020-01-13T22:32:30.055298Z","id":"db9483a1-da38-44c0-ad4d-31bd74590c3d","name":"EQKgYyZPRhSQGVkDdoXhzcaJg","rack_size":60,"updated":"2020-01-13T22:32:30.055298Z"},{"created":"2019-11-01T20:19:36.525449Z","id":"edcd1991-355b-4323-b2cb-0ad8b4bace30","name":"EuYagKKqbPQAUHhkHqmcnywcZ","rack_size":60,"updated":"2019-11-01T20:19:36.525449Z"},{"created":"2019-10-30T18:09:14.377176Z","id":"3c04c413-80c9-4994-ab1e-bf707afa4ba9","name":"FJFVyyMbAHJeIvuEMNwgyAUhi","rack_size":60,"updated":"2019-10-30T18:09:14.377176Z"},{"created":"2019-10-28T18:24:24.890862Z","id":"41c52a15-7b46-4345-b668-e217c81b5c6a","name":"FQElBYvhJGLBrylFzneEmzMvg","rack_size":60,"updated":"2019-10-28T18:24:24.890862Z"},{"created":"2019-10-16T15:18:35.034774Z","id":"64974a20-1271-4721-afc2-6f8ac3b77b61","name":"FQFxvWMSXxTHRMjXsmJOVQEIV","rack_size":34,"updated":"2019-10-16T15:18:35.034774Z"},{"created":"2019-10-30T20:21:29.839351Z","id":"37a47ce6-bfc0-4dc0-9e09-33e089851420","name":"FSQebpdgNtlgCPPzbrBWCwHjR","rack_size":60,"updated":"2019-10-30T20:21:29.839351Z"},{"created":"2019-10-31T16:37:06.394153Z","id":"53f1fc42-4aa0-4b07-88ee-89e57f4e98e2","name":"FYVmKnBKBNCylSxPZMBOyGiFM","rack_size":60,"updated":"2019-10-31T16:37:06.394153Z"},{"created":"2019-10-30T18:09:41.252636Z","id":"5dd73e61-4dbe-4d96-8842-c30cd71a09d9","name":"FadEPijdfSTIHtdKIVJHUXDyk","rack_size":60,"updated":"2019-10-30T18:09:41.252636Z"},{"created":"2019-10-31T03:24:43.325976Z","id":"b9e42fc1-8717-4dae-9fbb-4a4054e489a7","name":"FdcEhtnfMFCpZaiRscTJaDUAG","rack_size":60,"updated":"2019-10-31T03:24:43.325976Z"},{"created":"2019-10-31T03:43:17.048526Z","id":"01976019-d230-47e2-94b1-610526b041bb","name":"FhPRXRnSFBvoDAOGQRfxBMAVn","rack_size":60,"updated":"2019-10-31T03:43:17.048526Z"},{"created":"2019-10-31T19:42:12.147289Z","id":"a21f8164-ac85-4784-8c16-04d3e9803b19","name":"FkDJMiXGDDvNPsVakHvGVwRIX","rack_size":60,"updated":"2019-10-31T19:42:12.147289Z"},{"created":"2020-01-13T17:19:07.689697Z","id":"e81a04bb-4ab0-47c2-a64a-a8be738e09f6","name":"FkzOmbZhHpWKgxLBLbYQHWcjJ","rack_size":60,"updated":"2020-01-13T17:19:07.689697Z"},{"created":"2019-10-31T03:49:25.932675Z","id":"f0ff1fee-4875-495c-b89a-c3bc518c493c","name":"FszgSXpZLqhGiYxyDtMCoPSqP","rack_size":60,"updated":"2019-10-31T03:49:25.932675Z"},{"created":"2019-10-31T02:45:24.967646Z","id":"a01af2a2-8590-4948-8efe-9af10e3ec358","name":"GGzcyXbHyBzQpyhxamOlWVeXG","rack_size":60,"updated":"2019-10-31T02:45:24.967646Z"},{"created":"2019-10-30T18:13:43.527009Z","id":"cdc13b07-9797-4b97-9d56-971d69b40441","name":"GVSxADTxjvpSnEcserubbPMHd","rack_size":60,"updated":"2019-10-30T18:13:43.527009Z"},{"created":"2020-01-13T23:04:24.877137Z","id":"9c9f1b8b-8cad-413c-b36a-f31718f1ed5d","name":"GWRoKktaMLNJYFhvfPYxSiges","rack_size":60,"updated":"2020-01-13T23:04:24.877137Z"},{"created":"2019-10-31T19:42:53.476672Z","id":"0bc79dbe-fc52-4f6e-bdd6-247ed2d22e45","name":"HNLlFKMwViRLNWrBabZVDTESq","rack_size":60,"updated":"2019-10-31T19:42:53.476672Z"},{"created":"2019-10-31T18:58:59.166121Z","id":"9db01418-45e4-4cac-a613-cc15b5c633e4","name":"HbCsyWCdUZaeVYCkJfpIHGtsV","rack_size":60,"updated":"2019-10-31T18:58:59.166121Z"},{"created":"2019-10-31T02:53:35.849697Z","id":"ced3eed9-41ad-4675-a790-3df8c32f1708","name":"HkruBuFqtgtsySBrKqcXRTvej","rack_size":60,"updated":"2019-10-31T02:53:35.849697Z"},{"created":"2020-01-13T23:11:52.128615Z","id":"e1b0747e-a098-4145-b528-a99936af7df7","name":"HnNBhtAGuIVVrOTQxgCAdgGog","rack_size":60,"updated":"2020-01-13T23:11:52.128615Z"},{"created":"2019-10-28T20:45:05.936701Z","id":"b2ac4ec3-f643-4eed-bada-79cc1eff1a75","name":"IWznjayFqHgFKjkRCOXSlJQUn","rack_size":60,"updated":"2019-10-28T20:45:05.936701Z"},{"created":"2019-10-30T18:15:28.825018Z","id":"0cd80bdd-af2c-47cc-ae52-1a59ec32b852","name":"IdXsMOuuCzXPjYbilroaHsfrp","rack_size":60,"updated":"2019-10-30T18:15:28.825018Z"},{"created":"2019-10-31T03:34:16.554370Z","id":"67794938-9584-4435-b92e-56b5f314abf9","name":"IlphQdarshDtFxHicXqkzwtEc","rack_size":60,"updated":"2019-10-31T03:34:16.554370Z"},{"created":"2019-10-31T15:11:28.924443Z","id":"736a0439-21ab-4c68-b37a-49210e8c4d1e","name":"IqshZRPPHHItmjCRutqARoUUt","rack_size":60,"updated":"2019-10-31T15:11:28.924443Z"},{"created":"2019-10-30T20:23:40.471244Z","id":"06b94eef-31a9-4632-ae5a-8820d42e0d21","name":"IyFJKedXjWIaadcLvNzFwhgxm","rack_size":60,"updated":"2019-10-30T20:23:40.471244Z"},{"created":"2020-01-13T17:25:41.977177Z","id":"6be79262-cceb-43b5-a37a-5bf765f437a6","name":"JNNpEeqnUFpBNlheLeLGreegC","rack_size":60,"updated":"2020-01-13T17:25:41.977177Z"},{"created":"2019-10-28T16:54:02.021559Z","id":"ef48fabe-42ec-4cc6-8221-8c1aec7481e3","name":"JPFPMzsBXZvjQozxUCxaVUwLC","rack_size":60,"updated":"2019-10-28T16:54:02.021559Z"},{"created":"2019-10-30T18:14:05.998376Z","id":"e46dd055-31c0-418b-892c-48ff02678a43","name":"JTLMonnojqfRipetyqxnXkXJR","rack_size":60,"updated":"2019-10-30T18:14:05.998376Z"},{"created":"2019-10-31T14:21:43.625825Z","id":"3d9728b2-d4f4-4412-b3e7-820e1c617884","name":"JqdyIJiOxckOGCiTAcCmPnmig","rack_size":60,"updated":"2019-10-31T14:21:43.625825Z"},{"created":"2019-10-28T20:46:44.607754Z","id":"37012ed7-b3cd-41d9-8c26-61821fcefeab","name":"KSBeiiGWziqFehZPGfIvkfbik","rack_size":60,"updated":"2019-10-28T20:46:44.607754Z"},{"created":"2019-10-15T19:56:12.675626Z","id":"86b30cfc-f06a-4d83-845a-df2bc08c642d","name":"KYJPXTdywmyFxaCXOHBrCIbzc","rack_size":68,"updated":"2019-10-15T19:56:12.675626Z"},{"created":"2019-10-16T16:28:11.138047Z","id":"f783cb6b-b66d-4f63-a5e7-98894577285d","name":"KbVTVPFuwHgASCMIJoGJyweHK","rack_size":31,"updated":"2019-10-16T16:28:11.138047Z"},{"created":"2019-10-17T21:06:22.548952Z","id":"b002f8df-ed5a-4106-b5e7-0427b58e3dc6","name":"KfCHGSGKRPMoFEUpJeTWcMTtQ","rack_size":60,"updated":"2019-10-17T21:06:22.548952Z"},{"created":"2019-10-31T03:32:11.227506Z","id":"010e3e02-2e2d-4fa2-b3e5-ae35b5aa4641","name":"KxJGUetETAlueVJDcObxFfOuW","rack_size":60,"updated":"2019-10-31T03:32:11.227506Z"},{"created":"2020-01-13T21:26:40.966509Z","id":"cdf4843e-c05e-4a2b-bfdd-db8aa6d6f3fe","name":"LGACDOcDbjlPoFBxamzNlXNkK","rack_size":60,"updated":"2020-01-13T21:26:40.966509Z"},{"created":"2020-01-13T22:20:05.600623Z","id":"a782e05f-9503-4877-9396-e3b40ed60fa7","name":"LWaZwZJuoUULzIrVLSuwWLBRF","rack_size":60,"updated":"2020-01-13T22:20:05.600623Z"},{"created":"2019-11-01T19:21:08.749798Z","id":"0248293e-f4bd-4a3d-af97-a6ccb9704942","name":"LYuwwBftpmEwFfiRapTjTKRev","rack_size":60,"updated":"2019-11-01T19:21:08.749798Z"},{"created":"2019-10-31T03:15:51.930304Z","id":"46feceb6-4141-4d5b-a2a6-e028cd64a9ab","name":"MQHchqMrVhCfjFXipombxTiTg","rack_size":60,"updated":"2019-10-31T03:15:51.930304Z"},{"created":"2019-10-30T20:44:43.226841Z","id":"1f267ea6-5c15-4a1a-b547-267b5acf8653","name":"MSkinJfQzdzSQSZzLstqxjvHO","rack_size":60,"updated":"2019-10-30T20:44:43.226841Z"},{"created":"2019-10-30T20:17:37.454835Z","id":"4935a4f5-9954-46bc-92a5-69b74688ff94","name":"MhvDndUpfBbLakGRKsmjNNdRs","rack_size":60,"updated":"2019-10-30T20:17:37.454835Z"},{"created":"2019-10-28T20:04:11.070648Z","id":"192e448c-11c6-4d89-914c-92ee81f662b1","name":"MjAnbYLJboFUJYwpLnLicJGIB","rack_size":60,"updated":"2019-10-28T20:04:11.070648Z"},{"created":"2019-10-31T19:04:57.303306Z","id":"d3e195a3-2890-42d9-ac47-aa1d53e6daf8","name":"MkWPinPkAZUrTGBJkIERQHsEO","rack_size":60,"updated":"2019-10-31T19:04:57.303306Z"},{"created":"2019-10-28T20:07:14.793245Z","id":"2fff4d88-ea3a-4a93-ae62-7ee35c97c797","name":"MmELcGneHWeEjqDxXnogVFSbr","rack_size":60,"updated":"2019-10-28T20:07:14.793245Z"},{"created":"2019-10-26T18:36:29.550545Z","id":"4c7d6a50-fad7-48f7-9a32-dc0f124bcc49","name":"MmRseuBPzExLbIbDTmkiSldDE","rack_size":60,"updated":"2019-10-26T18:36:29.550545Z"},{"created":"2019-10-17T15:19:04.488465Z","id":"571a8a05-9f9c-4a09-a504-91fd98fe4513","name":"MsqnPaVIZwUBRWewYLRlKrfyb","rack_size":96,"updated":"2019-10-17T15:19:04.488465Z"},{"created":"2019-10-17T16:29:30.413985Z","id":"912c61c6-5249-40d5-afd2-918421c0c3a2","name":"NLgivftpElTrFPrlbgTPcqOdU","rack_size":60,"updated":"2019-10-17T16:29:30.413985Z"},{"created":"2019-10-31T19:39:12.731527Z","id":"c826f4d6-4af7-486d-9533-80c366f695a5","name":"NhdCKNSNEpbrLdhGMcRGDViwj","rack_size":60,"updated":"2019-10-31T19:39:12.731527Z"},{"created":"2019-10-30T18:08:17.424667Z","id":"459bc0bd-b526-4a13-9dec-4ae83c360c4b","name":"NyXLBfYYjLqZyaJTbGPlYhlRQ","rack_size":60,"updated":"2019-10-30T18:08:17.424667Z"},{"created":"2019-10-28T18:24:03.386589Z","id":"1655c74c-3a77-4be0-aa70-43dd78e6b0b3","name":"OnhCBfPZcIDQvZDPYRirsncVy","rack_size":60,"updated":"2019-10-28T18:24:03.386589Z"},{"created":"2019-10-30T18:13:43.560898Z","id":"dff57478-3f40-4bf2-bc1a-21cac2f05c94","name":"OsJiHNoeGYpQLUEqaXiUNXaho","rack_size":60,"updated":"2019-10-30T18:13:43.560898Z"},{"created":"2020-01-13T23:13:04.869058Z","id":"01df2f92-ed93-4a20-913a-4d622b8b8d9e","name":"PEceaIRdNLjnvzEfkcqFDVvaj","rack_size":60,"updated":"2020-01-13T23:13:04.869058Z"},{"created":"2020-01-13T23:12:39.761597Z","id":"08f71894-4d9f-4532-8611-6dfe0770cac6","name":"PZGFlXbsPNSZXlKWsXkYjVrAt","rack_size":60,"updated":"2020-01-13T23:12:39.761597Z"},{"created":"2019-10-15T19:59:23.979794Z","id":"83311807-7bd7-4458-9541-92d48f0fa877","name":"PwVkTpPLBuMJRftYbumiUTnWU","rack_size":17,"updated":"2019-10-15T19:59:23.979794Z"},{"created":"2019-10-15T19:42:46.939254Z","id":"0ddb7742-d453-433e-acf0-4f6602a4d48c","name":"QCLNoYYQQZyUIhvJXkOQiWDhB","rack_size":78,"updated":"2019-10-15T19:42:46.939254Z"},{"created":"2020-01-13T22:08:41.249274Z","id":"76567cdf-d282-4d4f-bb70-eddf1bbad3ac","name":"QPblzdEpNCoeFbyZluThvQtcE","rack_size":60,"updated":"2020-01-13T22:08:41.249274Z"},{"created":"2019-10-30T20:16:35.407019Z","id":"7e40228b-93f7-4044-ae66-07fed300a96c","name":"QRMgKwlsVhOsHCFmDFJZzMshp","rack_size":60,"updated":"2019-10-30T20:16:35.407019Z"},{"created":"2019-10-17T16:23:02.157982Z","id":"f06bb7c9-eb7b-4f33-b3d8-22321e711ddb","name":"QTbsCrfXvzDGoEeILiIElrwEt","rack_size":60,"updated":"2019-10-17T16:23:02.157982Z"},{"created":"2019-10-31T19:40:28.106211Z","id":"4bd67802-ece2-49e0-b434-06882bc15cca","name":"QbKyGwbqigXISrMUGmgDvfrvX","rack_size":60,"updated":"2019-10-31T19:40:28.106211Z"},{"created":"2019-10-28T14:58:11.997052Z","id":"4b8ff8a0-d55b-436b-8da8-b4ac8e05ce7f","name":"QmSovieFBpiZirScnZScdKunP","rack_size":60,"updated":"2019-10-28T14:58:11.997052Z"},{"created":"2019-10-16T14:37:50.576786Z","id":"3ef9b18c-b63b-4564-8368-4726c9064344","name":"QuxoxIOgowejpeTYhXJBSprnr","rack_size":95,"updated":"2019-10-16T14:37:50.576786Z"},{"created":"2019-11-01T19:00:06.097511Z","id":"1d3bf146-db23-4437-b21e-8da185ca5e4f","name":"QwvGIYypyVzjYdlhgRhnAgElc","rack_size":60,"updated":"2019-11-01T19:00:06.097511Z"},{"created":"2019-10-31T02:51:30.544189Z","id":"de67d39b-2eed-4eca-8dcf-9c1057f735bb","name":"QxTzmHMNDWEMKtrlukzkvdHaf","rack_size":60,"updated":"2019-10-31T02:51:30.544189Z"},{"created":"2019-10-31T19:33:09.282271Z","id":"c5b3a805-7620-4f8e-a4d9-f9c860e137c1","name":"RLOGyhVYWsmWwBtXQfDArPFiQ","rack_size":60,"updated":"2019-10-31T19:33:09.282271Z"},{"created":"2019-10-31T02:59:59.776579Z","id":"ba253da2-9419-4b1f-a841-4c655067c46a","name":"RTdTjQkaDGlrrFOJBecinmqKb","rack_size":60,"updated":"2019-10-31T02:59:59.776579Z"},{"created":"2019-10-31T19:38:45.657765Z","id":"45a63324-5fb7-40cf-ba5b-5a9f78af4c78","name":"RpHItIZJlJTSpSJiaAPYKOpDh","rack_size":60,"updated":"2019-10-31T19:38:45.657765Z"},{"created":"2019-10-17T17:38:38.401361Z","id":"c7a9f571-a9a7-462f-a229-1d75a9fb5608","name":"RqHJIJKmmkdbblnfwtJXhlBXB","rack_size":60,"updated":"2019-10-17T17:38:38.401361Z"},{"created":"2019-10-31T16:46:01.428111Z","id":"8134e19a-684f-4fd3-92d4-1056848a8828","name":"RqfuTVcmNjXJrynVmucMaIcVj","rack_size":60,"updated":"2019-10-31T16:46:01.428111Z"},{"created":"2019-10-30T18:08:49.102438Z","id":"469425d3-9cba-420e-aa9f-088b3194bbef","name":"SSkGZCoYHxEimpVOaQCXFucEw","rack_size":60,"updated":"2019-10-30T18:08:49.102438Z"},{"created":"2019-10-30T18:10:33.764725Z","id":"c89e3b25-9faf-40f1-bbb9-128aea638e84","name":"SWQqTiEoxHbgZMSEajGmvwgQN","rack_size":60,"updated":"2019-10-30T18:10:33.764725Z"},{"created":"2020-01-13T22:27:48.114544Z","id":"33bbde97-7da3-432e-8f09-0799ac935575","name":"SzCXzsplpCUQrIBFIVLmmXcqF","rack_size":60,"updated":"2020-01-13T22:27:48.114544Z"},{"created":"2019-10-28T19:27:02.069691Z","id":"2be99c49-5ffe-468b-a034-6ec8458d8705","name":"TUQIxzlGtuvaetlkNRoJwQYgM","rack_size":60,"updated":"2019-10-28T19:27:02.069691Z"},{"created":"2019-10-31T03:28:59.412158Z","id":"34783500-2142-4f57-9dcb-37a52e2a7062","name":"TZslBiymvzPEbKyYBfQaCrAwc","rack_size":60,"updated":"2019-10-31T03:28:59.412158Z"},{"created":"2019-10-30T20:22:07.179441Z","id":"6cb9a7cf-e08e-4c3c-b897-239eb182c467","name":"TctbjYEDWofBSyrDdsQSeOHnq","rack_size":60,"updated":"2019-10-30T20:22:07.179441Z"},{"created":"2019-10-28T15:38:59.194821Z","id":"97b35de5-648c-43ac-a8af-4ab8ab42a024","name":"TeLiNofGzNlveGQCGJRPiEIHF","rack_size":60,"updated":"2019-10-28T15:38:59.194821Z"},{"created":"2019-11-01T18:03:13.036058Z","id":"0b9ae31e-5ef4-4734-b9a2-1ff30e33a0b9","name":"TestRackRole","rack_size":60,"updated":"2019-11-01T18:03:13.036058Z"},{"created":"2019-10-17T15:29:27.311798Z","id":"f2b25097-a273-4c7f-99a1-45ab03089486","name":"TvgnboWTAjlJorNaJYPqPghdT","rack_size":24,"updated":"2019-10-17T15:29:27.311798Z"},{"created":"2020-01-13T21:32:31.961408Z","id":"43fa41ba-df23-40b4-a6aa-f6174353b285","name":"UUdjXeAtoYpwZZfmzFcoevMwy","rack_size":60,"updated":"2020-01-13T21:32:31.961408Z"},{"created":"2019-10-30T20:26:20.568294Z","id":"3d9fb6a8-1c46-4398-884f-1afc8d48e2f6","name":"UZTJZoVPLTikwfvcjBdAJbQBb","rack_size":60,"updated":"2019-10-30T20:26:20.568294Z"},{"created":"2019-10-31T19:02:15.091132Z","id":"a693cfd9-af0e-4f6d-b70a-232b4874179d","name":"UjWHiGhbFyvaPRVsrdGeHMZOS","rack_size":60,"updated":"2019-10-31T19:02:15.091132Z"},{"created":"2020-01-13T22:30:07.072642Z","id":"36427d84-2c66-4feb-9201-d09cbe782b01","name":"UpsxkVVHpoaDLkFFjaxqNVHtX","rack_size":60,"updated":"2020-01-13T22:30:07.072642Z"},{"created":"2019-10-16T18:24:48.329160Z","id":"d46a38bb-9eb5-45dd-a6d3-1f8875f54dad","name":"UtwlRIGpQeCafZKoeCkxroyJI","rack_size":67,"updated":"2019-10-16T18:24:48.329160Z"},{"created":"2019-10-30T20:23:11.555829Z","id":"3ca1f0c3-270b-4d9c-b28a-4349dba4520f","name":"VMirEHfhPdEBWbgIvLkizGtTm","rack_size":60,"updated":"2019-10-30T20:23:11.555829Z"},{"created":"2019-10-31T03:14:15.827424Z","id":"5597b1da-75e0-4674-9523-2f5e77edd30c","name":"VRNmzwVFbkWFIJiAyqtbhRmso","rack_size":60,"updated":"2019-10-31T03:14:15.827424Z"},{"created":"2019-10-30T18:09:14.416518Z","id":"74a2b269-0251-4e6d-b753-d4b947c108c0","name":"VWxaWocXUTygtWlVJHNXEiXdU","rack_size":60,"updated":"2019-10-30T18:09:14.416518Z"},{"created":"2019-10-16T14:38:03.796108Z","id":"da39728e-770a-4072-b0f4-ba0759a330eb","name":"VhCPBJqBAdgUaixwjIwUwqVUP","rack_size":28,"updated":"2019-10-16T14:38:03.796108Z"},{"created":"2019-10-18T14:45:34.371878Z","id":"c94907aa-5ec8-419e-a7b9-9333fa44bb30","name":"VqOKEhSpyNkIXgrdMCTxCZLar","rack_size":60,"updated":"2019-10-18T14:45:34.371878Z"},{"created":"2019-10-15T20:24:01.932925Z","id":"895651b8-c76b-4daa-9c9b-50a1f904234a","name":"VvrUZZXGVlyycoiSspsSbetWf","rack_size":55,"updated":"2019-10-15T20:24:01.932925Z"},{"created":"2019-10-28T14:45:10.635166Z","id":"a2ead937-c42b-4b7d-9a54-8d167a407537","name":"WAaNYpMpKntjcrGHgbEqbaDZZ","rack_size":60,"updated":"2019-10-28T14:45:10.635166Z"},{"created":"2019-10-31T03:40:42.630370Z","id":"2a428959-91ad-450b-933f-23c83b7bc6f0","name":"WQXyNJBErdauVPCiiWeEMBvjX","rack_size":60,"updated":"2019-10-31T03:40:42.630370Z"},{"created":"2019-10-16T18:31:54.084152Z","id":"c629df0a-b3f9-44b7-a79b-f4529ce7e787","name":"WjinkzRlJJnHIViGBSzhXPkYg","rack_size":67,"updated":"2019-10-16T18:31:54.084152Z"},{"created":"2019-10-30T18:11:23.540469Z","id":"a49e53ad-88f4-4d7e-9168-c2e31feceeb1","name":"XBIWGvoYAIvFvYMDMeFEmXCAy","rack_size":60,"updated":"2019-10-30T18:11:23.540469Z"},{"created":"2019-10-31T15:11:13.716267Z","id":"457b7ddb-b328-4aeb-be5a-e16e561bf74e","name":"XNDRvmXGIGfnfJBRCtKZlYwRp","rack_size":60,"updated":"2019-10-31T15:11:13.716267Z"},{"created":"2019-10-30T18:12:54.901414Z","id":"9f36ef1f-73d5-4db4-86ff-c4f91f49e2e8","name":"XpEItgMxVzXyNfWDxohtevLOv","rack_size":60,"updated":"2019-10-30T18:12:54.901414Z"},{"created":"2019-10-28T15:43:30.525238Z","id":"3fbd9e89-feb2-4606-ba9d-c55dc393eebe","name":"XuGNARZRSaNakkanvCZffGihk","rack_size":60,"updated":"2019-10-28T15:43:30.525238Z"},{"created":"2019-10-31T03:26:11.584289Z","id":"15725d36-8fc7-49bd-b43e-41dada09c9c0","name":"YHCQXEgePckDiDbieAlsXEmtX","rack_size":60,"updated":"2019-10-31T03:26:11.584289Z"},{"created":"2019-10-31T19:42:18.413086Z","id":"0b6c0c49-3b22-46b2-8d8a-3dbf7d508513","name":"YLwImXsSErsmbNspGRIClCliS","rack_size":60,"updated":"2019-10-31T19:42:18.413086Z"},{"created":"2020-01-13T23:16:23.874950Z","id":"214fc519-ce44-47e9-b4f6-f802ce3e2d4a","name":"YUuUQAAwilNBTkBktQLbCjoUJ","rack_size":60,"updated":"2020-01-13T23:16:23.874950Z"},{"created":"2019-10-28T19:28:16.511803Z","id":"d9164fb4-a4ff-4cbd-aa4d-cde039636f0f","name":"ZeUhhLtDdMidMfnGOCDcqBsUS","rack_size":60,"updated":"2019-10-28T19:28:16.511803Z"},{"created":"2019-10-31T03:32:37.719619Z","id":"16c867ab-0edb-43d3-a293-6c7be0523f1f","name":"ZtxfQZhLFhTFQZmJeGNRDXKbn","rack_size":60,"updated":"2019-10-31T03:32:37.719619Z"},{"created":"2019-10-31T14:20:15.790051Z","id":"4c0544e7-cdaf-474a-93c9-0e95f94747b0","name":"aHrdEryhMUIElYAWsXGSdnjDR","rack_size":60,"updated":"2019-10-31T14:20:15.790051Z"},{"created":"2020-01-13T22:43:30.017240Z","id":"0c10ea99-20fd-41e0-b21c-d1901d636768","name":"aNgumgioRpHVFRVmmZnBJCtjK","rack_size":60,"updated":"2020-01-13T22:43:30.017240Z"},{"created":"2019-10-15T19:41:25.803734Z","id":"ecf0a41d-7064-4446-b694-612438d86838","name":"abhDRCAJIOWingSZDACeCAvzY","rack_size":25,"updated":"2019-10-15T19:41:25.803734Z"},{"created":"2019-10-17T21:05:27.603587Z","id":"751fb7cb-2f99-4e68-806f-73a29f7c6b7e","name":"ahDPcpEfpLSYWixDAvItTTBLZ","rack_size":60,"updated":"2019-10-17T21:05:27.603587Z"},{"created":"2019-10-28T20:32:53.836862Z","id":"a3774cf2-8781-4788-b354-e18aef50cdb0","name":"aqVskkCrutWzduOgXwdmLjXYN","rack_size":60,"updated":"2019-10-28T20:32:53.836862Z"},{"created":"2019-10-28T16:22:12.987818Z","id":"41781509-b723-4806-ba59-e86b73ba3512","name":"bAXHQyKkLBueSSZUmgCoexWZh","rack_size":60,"updated":"2019-10-28T16:22:12.987818Z"},{"created":"2019-10-31T02:45:16.618082Z","id":"8e160ead-d64b-4658-a362-3c7212f2ff87","name":"bYtkVdBwTFTHUeIcLMTFgPBex","rack_size":60,"updated":"2019-10-31T02:45:16.618082Z"},{"created":"2019-10-30T20:12:05.794967Z","id":"d65fba84-9ec7-4296-b99b-6778c9da1cb4","name":"bZMAxYUpUDGXVdxIxJvpMITHn","rack_size":60,"updated":"2019-10-30T20:12:05.794967Z"},{"created":"2019-10-16T16:31:16.503097Z","id":"180b58bc-8a92-48a4-b513-7e26ba89d16f","name":"buXMgSmMaIDVGdGAvgwoaaBDD","rack_size":7,"updated":"2019-10-16T16:31:16.503097Z"},{"created":"2019-10-30T18:07:03.406861Z","id":"ae840132-4e5d-47e0-8ab0-49d884a1048c","name":"cBbDmRjLCtosbYSLLhmJjvoxm","rack_size":60,"updated":"2019-10-30T18:07:03.406861Z"},{"created":"2019-10-28T16:30:41.868663Z","id":"e9dafc21-7b3d-4d7f-b6ef-5ee60780201a","name":"cDdSBOOpAscPXFBAFUrELDVcv","rack_size":60,"updated":"2019-10-28T16:30:41.868663Z"},{"created":"2019-10-30T20:25:08.235453Z","id":"6ea10757-680b-41cf-b0b6-5f551e5e11f9","name":"cHFCriWrkBlDvRDqeTdIyNNrK","rack_size":60,"updated":"2019-10-30T20:25:08.235453Z"},{"created":"2019-10-31T19:03:24.551608Z","id":"e952b18f-6a34-45e7-905f-fdaf78bb4fe2","name":"cIgkCXrUIWkKQxcpsUFfaOXEP","rack_size":60,"updated":"2019-10-31T19:03:24.551608Z"},{"created":"2019-10-30T18:15:28.787832Z","id":"98dbfea6-9594-4245-afb6-4a8afab9c4f3","name":"cYAYoCDSJXpuQQjGXRNkBBDfm","rack_size":60,"updated":"2019-10-30T18:15:28.787832Z"},{"created":"2020-01-13T16:50:25.472807Z","id":"7b8422d8-d39a-4dbb-9ea6-fec0c7e8f60f","name":"clmNWdGPjtcjzPrLzEpIijTeM","rack_size":60,"updated":"2020-01-13T16:50:25.472807Z"},{"created":"2019-10-30T18:14:06.035801Z","id":"bacbe39b-c0fa-43b3-8a28-1e91da24b38a","name":"cmSdZPDzsRWhQomNHXKrLAbbI","rack_size":60,"updated":"2019-10-30T18:14:06.035801Z"},{"created":"2019-10-31T19:33:43.060606Z","id":"3ae432a1-75ad-47fc-baf9-834d5fe7640d","name":"cmVxoVkhOKiXFpmrxiUJhlLDv","rack_size":60,"updated":"2019-10-31T19:33:43.060606Z"},{"created":"2019-10-31T14:14:52.740179Z","id":"e2dcc7fe-5b97-4bd8-af13-7d37816a78eb","name":"dUPjRrFXBueGrEhXAIVkmMnXB","rack_size":60,"updated":"2019-10-31T14:14:52.740179Z"},{"created":"2019-10-31T03:27:55.318409Z","id":"75d35e2f-e8ee-45d5-8657-aeee6ba43485","name":"dlsvDVLtNLDIcOHyxETlLlfKY","rack_size":60,"updated":"2019-10-31T03:27:55.318409Z"},{"created":"2019-10-30T20:13:46.754484Z","id":"226fe730-88f7-4cc2-8970-81e7d7c287f5","name":"dttGSOPQAUBKIAcVCfoFrGpQW","rack_size":60,"updated":"2019-10-30T20:13:46.754484Z"},{"created":"2020-01-13T22:27:42.545956Z","id":"5da4b8e1-6c62-4f7a-a174-a295410c97da","name":"eRgSyUdwoBWRPfIgLUTcyRffu","rack_size":60,"updated":"2020-01-13T22:27:42.545956Z"},{"created":"2019-10-31T02:52:42.090005Z","id":"6c6574c4-4856-4389-b60f-bf7a1ac87514","name":"ehsaWSEiyCGprpceNdAfhamIp","rack_size":60,"updated":"2019-10-31T02:52:42.090005Z"},{"created":"2019-10-28T14:46:04.324632Z","id":"b828e519-53ad-4aac-8adc-24f51f560ed8","name":"epTTrjNDJSwYmSwyssIyXfOhl","rack_size":60,"updated":"2019-10-28T14:46:04.324632Z"},{"created":"2019-10-28T20:42:19.550452Z","id":"68c0d606-1326-486e-8879-96c3488d9189","name":"fHPgqtoqWgcFcJlMzqsakHige","rack_size":60,"updated":"2019-10-28T20:42:19.550452Z"},{"created":"2020-01-13T17:23:05.699737Z","id":"280df49b-bd1b-455b-8f64-5d4f23bb26d1","name":"fZdSSCMzVIpCKgDdDNVLPxAaH","rack_size":60,"updated":"2020-01-13T17:23:05.699737Z"},{"created":"2020-01-13T21:08:24.356205Z","id":"49c477a2-aa7b-4cef-82da-afb499054698","name":"fcMTAcDVuPFwuLjQSZhfhESBq","rack_size":60,"updated":"2020-01-13T21:08:24.356205Z"},{"created":"2019-10-16T18:32:21.794Z","id":"383f409d-86a0-4b26-bb4a-1c8750e201e2","name":"frMkKLAvERyxpLgTGpvVjMvgH","rack_size":45,"updated":"2019-10-16T18:32:21.794Z"},{"created":"2019-10-31T15:18:15.590283Z","id":"f0db1bfd-5f8c-4c2b-b6c6-81e57a172e2a","name":"ftycfkNuEOTVnZfmFkJjFpfJF","rack_size":60,"updated":"2019-10-31T15:18:15.590283Z"},{"created":"2019-10-31T19:33:02.192664Z","id":"3e44fa1d-e6ed-4352-a85b-b210b8bfe2b9","name":"gIQyIboAGxKrTdyetFBbwUkMC","rack_size":60,"updated":"2019-10-31T19:33:02.192664Z"},{"created":"2020-01-13T16:45:14.497180Z","id":"416ae87d-3ffb-4f67-8d0e-b13709333e60","name":"gUDgHVAMUpykupPLpWoHpwKoi","rack_size":60,"updated":"2020-01-13T16:45:14.497180Z"},{"created":"2019-10-15T20:21:43.675091Z","id":"d57238b6-a42d-4de6-9e1a-aa3884d52af6","name":"gVHcOTKqJrTAGgugUoLvIJHcF","rack_size":20,"updated":"2019-10-15T20:21:43.675091Z"},{"created":"2019-10-28T19:35:00.391251Z","id":"4b121464-6e26-42c9-8d71-b4798fc45001","name":"glIxjtsJdvhPYkMpFddKIgHdx","rack_size":60,"updated":"2019-10-28T19:35:00.391251Z"},{"created":"2020-01-13T17:25:12.166947Z","id":"930b056c-bb83-49d3-8ccd-7664f634c7f0","name":"gyHLVYfSucFELfZRcocKcUttG","rack_size":60,"updated":"2020-01-13T17:25:12.166947Z"},{"created":"2020-01-13T17:24:40.670612Z","id":"de778fcc-0901-4587-82e3-75f81274de79","name":"hDeUebuoIwODkUUYcERMXcaMz","rack_size":60,"updated":"2020-01-13T17:24:40.670612Z"},{"created":"2019-10-31T03:33:45.452530Z","id":"04e730f9-1b79-4a81-985b-8fb9b2cbbad6","name":"hcEZrrawCkDVPAEhiJLsEzVRM","rack_size":60,"updated":"2019-10-31T03:33:45.452530Z"},{"created":"2020-01-13T23:10:58.722105Z","id":"6f6cc99d-4023-4c31-81ff-b1df8b5576e4","name":"heqiivLkXKfJoEvHcjqmODQzv","rack_size":60,"updated":"2020-01-13T23:10:58.722105Z"},{"created":"2019-10-30T18:09:41.292637Z","id":"b8e9a5fe-a7bc-4ddb-9021-a8c832751568","name":"hjRRpHWIjSrMsuEJSsyWYXJJq","rack_size":60,"updated":"2019-10-30T18:09:41.292637Z"},{"created":"2020-01-13T22:31:14.341856Z","id":"9ec6b46e-c146-4e48-83b3-a538424c7ae5","name":"hqCRLwqHxneAZumlaWvCaEioZ","rack_size":60,"updated":"2020-01-13T22:31:14.341856Z"},{"created":"2019-10-28T16:31:24.174553Z","id":"e828b354-9d4f-4d90-b2a1-25b441a8282d","name":"iCpxUILQVsvRmMNrDfiloGxoO","rack_size":60,"updated":"2019-10-28T16:31:24.174553Z"},{"created":"2019-10-31T02:10:12.649458Z","id":"bdc84ef6-e6cf-4464-ad20-2d1931edcbe7","name":"iJKuxnDdotyJMNnFZkUJeRNov","rack_size":60,"updated":"2019-10-31T02:10:12.649458Z"},{"created":"2019-10-31T14:24:08.883074Z","id":"d1375907-0a13-4a5d-a5b7-b2fc0af117bb","name":"iLhNzrhPWnQgKZPbrAkuYfTVO","rack_size":60,"updated":"2019-10-31T14:24:08.883074Z"},{"created":"2019-10-31T18:57:44.372027Z","id":"0cc8b9db-92be-4e69-94a4-e32858542973","name":"iQHiKUvePiFjmLoKQbcEyBziY","rack_size":60,"updated":"2019-10-31T18:57:44.372027Z"},{"created":"2019-10-31T16:45:31.486066Z","id":"110d4c16-33b3-4262-82c9-b311e5ad17b0","name":"iZcrUEtdnHdodhxTZFYTjOLyB","rack_size":60,"updated":"2019-10-31T16:45:31.486066Z"},{"created":"2019-10-30T18:17:15.486461Z","id":"eb0d4864-22b4-4961-9143-155ccc2b384e","name":"iuRBQEREoPHyzApauEjKleXbt","rack_size":60,"updated":"2019-10-30T18:17:15.486461Z"},{"created":"2019-10-31T14:20:58.913816Z","id":"8d2f7315-7d87-4611-aca9-a8dc2363e4eb","name":"jOBjANJEpnTfYKvtmBMmEMoIT","rack_size":60,"updated":"2019-10-31T14:20:58.913816Z"},{"created":"2019-10-28T14:53:19.179651Z","id":"718c49e6-03bc-41c5-b1b1-7a761b33cb51","name":"jTRACDwLgxDXQuJJkxvyJsbRZ","rack_size":60,"updated":"2019-10-28T14:53:19.179651Z"},{"created":"2019-10-28T15:44:10.266120Z","id":"1f58815f-8522-4660-99fc-74f843f1000c","name":"jVhtlTLgaHvMqszyXTkYfBoiU","rack_size":60,"updated":"2019-10-28T15:44:10.266120Z"},{"created":"2019-10-31T03:44:39.344697Z","id":"f42b4f42-3569-4529-ae54-c8f76467f610","name":"jhvSGqAVltgmixNGUATbImuqQ","rack_size":60,"updated":"2019-10-31T03:44:39.344697Z"},{"created":"2019-10-31T19:40:47.279849Z","id":"70e6aa90-c4f3-49f1-b454-b5bd33ba586c","name":"jiPKRnjmfwxHLRiOnTxZuQKDA","rack_size":60,"updated":"2019-10-31T19:40:47.279849Z"},{"created":"2020-01-13T21:05:17.567781Z","id":"61781d6b-619c-49e2-a039-6283e1c72365","name":"jpzvXRfqXGdNreyzYMaClTWZy","rack_size":60,"updated":"2020-01-13T21:05:17.567781Z"},{"created":"2019-10-16T18:10:54.631328Z","id":"aac7747b-fe32-4fa9-bd8d-2f8710c6d4d9","name":"kCHUaIjCCqwaWMwurjcQnnoNq","rack_size":98,"updated":"2019-10-16T18:10:54.631328Z"},{"created":"2019-10-30T18:07:03.452947Z","id":"df76c888-e033-4062-afeb-5cc6a6a81693","name":"kILQpeoCeSrWzuJDMvCekBjNm","rack_size":60,"updated":"2019-10-30T18:07:03.452947Z"},{"created":"2020-01-13T17:27:42.957435Z","id":"22536e70-0ac4-4c6b-a993-fe0cc5e54d81","name":"kLRXPFrttazjuqiTckCNNnNqy","rack_size":60,"updated":"2020-01-13T17:27:42.957435Z"},{"created":"2019-10-17T16:23:36.544451Z","id":"ca5aa583-899f-44c4-87c4-02f6c8d028bf","name":"kMttMzjLQxEBGNThfgXOwPLAd","rack_size":60,"updated":"2019-10-17T16:23:36.544451Z"},{"created":"2019-10-31T16:45:15.508077Z","id":"29e9d120-9f37-4ec4-bfd3-db28238503d3","name":"knTeCejiDnmytyNpzhqIAZaPS","rack_size":60,"updated":"2019-10-31T16:45:15.508077Z"},{"created":"2019-10-31T19:03:10.673621Z","id":"3abca686-80e1-4a6e-833b-433971407132","name":"kqCVUfObeIInimKEEpEkVopBK","rack_size":60,"updated":"2019-10-31T19:03:10.673621Z"},{"created":"2019-10-31T19:39:45.513936Z","id":"f178a159-019a-4128-a79c-7845d07cd307","name":"kwWvCYAyydjaJApLyjQaEfZWh","rack_size":60,"updated":"2019-10-31T19:39:45.513936Z"},{"created":"2019-10-30T20:26:10.911637Z","id":"1b54aa5b-dfda-40ab-8130-e73fa719b527","name":"lcMjCXnsNptzKbaxSzOEpGIFh","rack_size":60,"updated":"2019-10-30T20:26:10.911637Z"},{"created":"2020-01-13T17:21:19.743365Z","id":"37c05e46-8be0-4847-b167-f52c2a9118eb","name":"likKsSdbknbRwgkdIoTmOXRyI","rack_size":60,"updated":"2020-01-13T17:21:19.743365Z"},{"created":"2020-01-13T22:30:27.644733Z","id":"89b1fba5-843b-47e8-84c2-7a6314b8337e","name":"lsloVTtQuJrCnCYuhDnXOTRsa","rack_size":60,"updated":"2020-01-13T22:30:27.644733Z"},{"created":"2020-01-13T21:26:06.426632Z","id":"599bd4df-efbe-4e65-bb5f-16da9a0b4b0b","name":"mDWKrlFbPMyteLoTbMQcYzbdJ","rack_size":60,"updated":"2020-01-13T21:26:06.426632Z"},{"created":"2019-10-28T16:54:39.723426Z","id":"3c1524ae-993f-41c0-981c-68a4b3178747","name":"mHNaRZjKKmnqLQeFcWMWEhoji","rack_size":60,"updated":"2019-10-28T16:54:39.723426Z"},{"created":"2019-10-31T14:06:31.013591Z","id":"defc8c80-18d1-4ab9-b5dc-985d97c75732","name":"mbpsSlFFjYUUjrPsxMjbXzCUL","rack_size":60,"updated":"2019-10-31T14:06:31.013591Z"},{"created":"2019-10-28T20:47:28.221152Z","id":"019ef631-3f15-4f5c-9db5-5e3ed056fc82","name":"mdaffiFJOtPOVfpVqkfYvNkhJ","rack_size":60,"updated":"2019-10-28T20:47:28.221152Z"},{"created":"2020-01-13T23:02:55.165664Z","id":"db4a63f6-bed6-43df-9baa-368d67a5da1a","name":"mjRiZPKskRhlCGyVbsZVYSxWC","rack_size":60,"updated":"2020-01-13T23:02:55.165664Z"},{"created":"2019-10-31T02:48:33.647796Z","id":"be19723e-ef74-488e-a97e-363881155cc5","name":"mnhZZqznrRPPSxwqCHFYXgUnj","rack_size":60,"updated":"2019-10-31T02:48:33.647796Z"},{"created":"2019-10-31T03:35:35.894503Z","id":"722d13d4-b617-43b5-9273-06ebf4dc291a","name":"moXOxkSrdwcRivmgVwfAgcXNh","rack_size":60,"updated":"2019-10-31T03:35:35.894503Z"},{"created":"2019-10-28T19:33:41.845511Z","id":"dffb3eac-27f6-47ec-b733-540761bd5e55","name":"myqlWtIvWLjIOJxCBrbGLlxRl","rack_size":60,"updated":"2019-10-28T19:33:41.845511Z"},{"created":"2019-10-28T19:26:28.343066Z","id":"60a68709-3c49-4d3d-a0e8-c1a1f0444efb","name":"nLonZfvEuFHQsESrhHeRtiiuD","rack_size":60,"updated":"2019-10-28T19:26:28.343066Z"},{"created":"2019-11-01T19:25:23.481147Z","id":"391027db-61ca-4d2d-85d5-dc5f88c2aee9","name":"nSmvvIsJqMhGdUInhRsJbxUNB","rack_size":60,"updated":"2019-11-01T19:25:23.481147Z"},{"created":"2019-10-17T15:24:19.465733Z","id":"db636bcb-0902-41a5-8d5f-cb44cf8cdfcd","name":"njOXXhlZrQRZzkkUpfrXDVCEk","rack_size":77,"updated":"2019-10-17T15:24:19.465733Z"},{"created":"2019-10-31T02:45:48.402608Z","id":"fb6f8dac-9ade-43e1-bb31-a97a3b6451a2","name":"nmIVqbsknyzeLvCIwCYNEXXev","rack_size":60,"updated":"2019-10-31T02:45:48.402608Z"},{"created":"2019-10-31T03:36:51.055255Z","id":"2ebae6b2-4de5-4958-a364-c309138b986a","name":"npQwPRvUKsUXDMSWUiCSQUlOD","rack_size":60,"updated":"2019-10-31T03:36:51.055255Z"},{"created":"2019-10-17T15:20:10.325396Z","id":"92b6b0e4-9bac-49c2-8685-3f3c32f630c2","name":"nvkJFhaLsHGrfXxrZAkIiHHnn","rack_size":88,"updated":"2019-10-17T15:20:10.325396Z"},{"created":"2019-10-31T15:11:40.539815Z","id":"70c28452-c6f8-4536-aeca-b9926de74f00","name":"ooQsVDpQjWNBbEUEIwKoxEWrE","rack_size":60,"updated":"2019-10-31T15:11:40.539815Z"},{"created":"2019-10-30T20:09:47.796738Z","id":"aef41925-0dbb-4b20-9d99-1703a941d7ac","name":"pMUdexumfBVNwqjumuvqhvRfk","rack_size":60,"updated":"2019-10-30T20:09:47.796738Z"},{"created":"2019-10-15T19:41:04.777497Z","id":"50136695-b976-4e4f-b26a-9e1e41707eea","name":"pOdXXyXFtyckGJnacMneNLJdO","rack_size":62,"updated":"2019-10-15T19:41:04.777497Z"},{"created":"2019-11-01T19:02:35.149372Z","id":"e4f8efc9-13f6-48a6-a4fd-8a18ac29ee18","name":"pSsDvatIavzBIZMLXPsWRWzRJ","rack_size":60,"updated":"2019-11-01T19:02:35.149372Z"},{"created":"2019-10-31T03:41:32.499058Z","id":"c4e45a44-a798-452e-b43c-10d8dee23981","name":"pjysJexXxYRlPWIqxgALqDzye","rack_size":60,"updated":"2019-10-31T03:41:32.499058Z"},{"created":"2019-10-28T14:54:40.590620Z","id":"02f19edb-8911-4430-bb99-d2335815db8e","name":"poJXhrOwxdOokjBEYvGHYdfkg","rack_size":60,"updated":"2019-10-28T14:54:40.590620Z"},{"created":"2019-10-30T20:44:03.660419Z","id":"cd76a38c-981e-4523-9245-f5ec57a099fe","name":"pvrxOZNWjJjaJTtUzAkfellqA","rack_size":60,"updated":"2019-10-30T20:44:03.660419Z"},{"created":"2019-11-01T21:34:37.832879Z","id":"5d2c4531-22df-498d-9283-25fe1fd335d2","name":"qXSAyfcqUYiDiUFVclbhMQsHM","rack_size":60,"updated":"2019-11-01T21:34:37.832879Z"},{"created":"2019-10-31T02:54:25.689130Z","id":"0cfd8ae1-81cb-479f-a466-c54b45c0f287","name":"qcVdCJYpWfJiiLTdNBwsRdTGt","rack_size":60,"updated":"2019-10-31T02:54:25.689130Z"},{"created":"2019-10-17T15:19:56.451853Z","id":"eccde8d0-81d8-4df1-9018-847196838673","name":"rQweBxzklEPgkNEAiVllzgpLW","rack_size":2,"updated":"2019-10-17T15:19:56.451853Z"},{"created":"2019-10-31T03:42:52.676885Z","id":"7cbbb67e-b455-4446-ba18-4ef54c8b620b","name":"rYHwpCXwRfHoDDuBupTnOLztO","rack_size":60,"updated":"2019-10-31T03:42:52.676885Z"},{"created":"2020-01-13T21:06:46.100124Z","id":"9cdca560-75ff-460c-ba12-40825d6c78e6","name":"rcrNPTNwhrVnhFxIeyFZVJhqg","rack_size":60,"updated":"2020-01-13T21:06:46.100124Z"},{"created":"2019-10-17T20:07:00.464096Z","id":"5e4c5656-bd77-4342-89b6-5a934400de75","name":"sSrktTzkFRkJXTnypBNHVmqCT","rack_size":60,"updated":"2019-10-17T20:07:00.464096Z"},{"created":"2020-01-13T17:22:04.857228Z","id":"f1df6207-7583-4608-b835-8455f0eb6ef2","name":"sTJyAqGjBSvkEDjcudFSOYKEO","rack_size":60,"updated":"2020-01-13T17:22:04.857228Z"},{"created":"2019-10-31T19:40:53.545188Z","id":"4b547783-4393-4515-ba99-7cee26a78627","name":"sWEiYMMrcdBYCFYkdYlhOekJi","rack_size":60,"updated":"2019-10-31T19:40:53.545188Z"},{"created":"2020-01-13T23:01:27.735188Z","id":"65c40b26-21ee-4e4a-bb76-7a4fe9ef4cda","name":"sYzekQWLrnTmwZJzpLGGnXBfE","rack_size":60,"updated":"2020-01-13T23:01:27.735188Z"},{"created":"2019-10-21T16:15:14.378202Z","id":"aa5e031e-b32e-4d6c-a023-11e325366051","name":"swSiUgMreiegaWsmFMzerKUVW","rack_size":60,"updated":"2019-10-21T16:15:14.378202Z"},{"created":"2019-10-31T02:57:49.084595Z","id":"52ec81be-caf8-416e-8f3b-8806023242d0","name":"tCCQXucjDVQhTwTBCUzPPgKQw","rack_size":60,"updated":"2019-10-31T02:57:49.084595Z"},{"created":"2020-01-13T22:36:56.824886Z","id":"ef5e6357-8f57-4174-87d0-13a4c33e3011","name":"tGImwFEmOFTZCBwPHucXJctRD","rack_size":60,"updated":"2020-01-13T22:36:56.824886Z"},{"created":"2019-10-30T18:12:54.855633Z","id":"07888502-e1b3-439b-b55d-1b60385e9d55","name":"tXOvHwsuIXelmDsvzKlvNpVFS","rack_size":60,"updated":"2019-10-30T18:12:54.855633Z"},{"created":"2019-10-31T03:05:58.053594Z","id":"406b5a2b-1ff5-4389-a1c3-71d62eb39d65","name":"tsbypjsItGiHbsQUoPxNyjdVv","rack_size":60,"updated":"2019-10-31T03:05:58.053594Z"},{"created":"2019-10-30T18:10:33.727300Z","id":"abf1ee82-4961-4490-b56c-3a3e751860d9","name":"tygPIwdHCqFZstgfAptWChiqU","rack_size":60,"updated":"2019-10-30T18:10:33.727300Z"},{"created":"2019-10-31T14:14:15.178595Z","id":"1bed657e-91e5-476d-8afd-6941f72a3895","name":"uASRaxfNScIQuBPgEAeVXphAT","rack_size":60,"updated":"2019-10-31T14:14:15.178595Z"},{"created":"2019-10-17T15:18:15.127142Z","id":"0fa640b9-f4d3-49f4-97bd-63a526e5ecb0","name":"uVjtGPgvLkUHyONnxFrQfZpZw","rack_size":40,"updated":"2019-10-17T15:18:15.127142Z"},{"created":"2020-01-13T21:27:55.435113Z","id":"fd073fa6-eb5c-40c9-b4d5-a62364d4e61a","name":"ujaNmzpIAVcCRyJgXqIDVFZXj","rack_size":60,"updated":"2020-01-13T21:27:55.435113Z"},{"created":"2020-01-13T22:29:04.244061Z","id":"3d225cd5-74cc-4029-bfff-9d8808c6108c","name":"uogcJfETrdvvmMXUZktHKyTpn","rack_size":60,"updated":"2020-01-13T22:29:04.244061Z"},{"created":"2019-10-17T15:20:22.971467Z","id":"dc52e373-fce6-401b-9e94-b5139aff8fa9","name":"upciCsLwffBpAsibJIGtHRzwJ","rack_size":94,"updated":"2019-10-17T15:20:22.971467Z"},{"created":"2019-10-31T14:28:50.116215Z","id":"2ca6365f-5097-4e72-8f6c-7b6fb6b6cae9","name":"uxNEJfKfnZODaJpCymHnUfczo","rack_size":60,"updated":"2019-10-31T14:28:50.116215Z"},{"created":"2019-10-31T03:44:01.296478Z","id":"3f8798ee-90e1-46fc-bd96-28c0ed8948bb","name":"uyRmTGTzwaRJBtJFazyAYMxdR","rack_size":60,"updated":"2019-10-31T03:44:01.296478Z"},{"created":"2020-01-13T22:28:34.937446Z","id":"3c56aa0f-3a5b-434f-bb62-5aa2e59a7f23","name":"vAxyyGqyMHnbLrlGwrAQdAurV","rack_size":60,"updated":"2020-01-13T22:28:34.937446Z"},{"created":"2020-01-13T21:30:44.380163Z","id":"0ebcbc06-30b8-443b-8438-c0a3b91dd91f","name":"vXmGNdcivHWbwhBiSrsbtuQMQ","rack_size":60,"updated":"2020-01-13T21:30:44.380163Z"},{"created":"2020-01-11T05:48:31.664216Z","id":"e0d5fa64-3b53-4f3a-bd4b-0e99cab0727f","name":"vZOcWjMCJzNTjikRaCoHAUBzE","rack_size":60,"updated":"2020-01-11T05:48:31.664216Z"},{"created":"2019-10-31T03:43:31.855395Z","id":"e626d06f-3950-4cec-9e5b-613a5fd128b2","name":"vdpHiOAuhCWvUUKZIfVcmQyhN","rack_size":60,"updated":"2019-10-31T03:43:31.855395Z"},{"created":"2019-10-28T16:22:40.643782Z","id":"c082072b-e7a0-4c05-a1b0-a5dd8d278e4f","name":"vgNvlvTriqspCVvkmVjynissO","rack_size":60,"updated":"2019-10-28T16:22:40.643782Z"},{"created":"2020-01-13T22:19:50.891524Z","id":"ea33b68e-e824-4764-9117-66d199e16feb","name":"viLaPLchWPQOGWYLYclikKehR","rack_size":60,"updated":"2020-01-13T22:19:50.891524Z"},{"created":"2019-10-15T19:42:54.983343Z","id":"85c32439-b556-4c74-a70b-627a34f5ba33","name":"vigDOkNKvFIGZHeFgNxiQhCDZ","rack_size":64,"updated":"2019-10-15T19:42:54.983343Z"},{"created":"2019-10-28T20:44:39.403250Z","id":"30c51095-4b4f-4cc0-b5d1-6a82bc189161","name":"vtZbxrinOmObiTYnLiwyxvEtb","rack_size":60,"updated":"2019-10-28T20:44:39.403250Z"},{"created":"2019-10-31T19:00:05.866129Z","id":"539fa7c7-2ac6-409f-8357-ab2b5b3c3b94","name":"wDgywJogNXVPFzmhFWOiKSKXQ","rack_size":60,"updated":"2019-10-31T19:00:05.866129Z"},{"created":"2019-11-01T20:27:50.931603Z","id":"b321eab6-5a9d-4e23-b459-4d367e0c30fc","name":"wKgxLtusMOYJbwtVeUzcobCYa","rack_size":65,"updated":"2019-11-01T20:27:50.931603Z"},{"created":"2019-10-28T14:49:54.787755Z","id":"135518ef-69c6-42e7-8fdf-52d8426dc62b","name":"wNbSpQOaKQnXZnhgndcmEsTpg","rack_size":60,"updated":"2019-10-28T14:49:54.787755Z"},{"created":"2019-10-31T14:24:31.330868Z","id":"817b372a-3991-4aac-9d7d-71e9b708fe8f","name":"wXyEKxOEMiEmOmJBaGUquxLkM","rack_size":60,"updated":"2019-10-31T14:24:31.330868Z"},{"created":"2020-01-13T16:43:28.587747Z","id":"85a37aa2-1c15-4603-8caa-ef6a8011a1de","name":"wdlWfdkgZEQNHPKNEMkviKLWs","rack_size":60,"updated":"2020-01-13T16:43:28.587747Z"},{"created":"2020-01-13T16:41:55.608633Z","id":"4732aa90-2a02-47cc-b87f-6b860c23b986","name":"wjrgmfrWucuPuWMtDKbXkHJuj","rack_size":60,"updated":"2020-01-13T16:41:55.608633Z"},{"created":"2019-10-30T20:12:59.243845Z","id":"09737c9c-b0be-46ec-9e8a-604f11deecd3","name":"xSwyBdVkbrBmlTzUbboVLmoVb","rack_size":60,"updated":"2019-10-30T20:12:59.243845Z"},{"created":"2020-01-13T21:28:21.194556Z","id":"e79efa60-31cf-4f02-8136-2d7d2526d0ac","name":"xYdtbygMsSRKnZSxignxcOnIs","rack_size":60,"updated":"2020-01-13T21:28:21.194556Z"},{"created":"2019-10-15T19:54:05.201040Z","id":"6a4857e6-4528-4000-888c-ffe31093671b","name":"xvuxBzekXxoijGTlslnqMkbvZ","rack_size":17,"updated":"2019-10-15T19:54:05.201040Z"},{"created":"2019-10-31T19:00:12.797062Z","id":"cff9f8bf-d393-4313-9437-4906d4886aba","name":"yDobeTPXwFljBvasQxpTMeWCa","rack_size":60,"updated":"2019-10-31T19:00:12.797062Z"},{"created":"2020-01-13T22:45:21.092289Z","id":"958d2e56-4631-4cd4-84f0-97c0290a58b8","name":"yPMrHlubTKFHpzIONmtnHiuoc","rack_size":60,"updated":"2020-01-13T22:45:21.092289Z"},{"created":"2019-10-28T20:07:30.310078Z","id":"bbdf7e91-54a8-4d5e-af89-b5f62db72d0a","name":"yYVqUXspINfxjHNBMZMUMWXes","rack_size":60,"updated":"2019-10-28T20:07:30.310078Z"},{"created":"2020-01-13T16:27:42.127414Z","id":"bebbda04-feaf-43e0-a2d9-2fd728b2ceec","name":"yenAJqIHwCabobJKJQAJTHpSs","rack_size":60,"updated":"2020-01-13T16:27:42.127414Z"},{"created":"2019-10-16T18:25:16.591975Z","id":"a1fcfae7-d299-4d4e-bee3-104077e33874","name":"yiPhSPYLHaaCbIUEWfxOkoVOf","rack_size":91,"updated":"2019-10-16T18:25:16.591975Z"},{"created":"2019-10-24T18:31:45.298296Z","id":"68db3149-2ea6-425d-afb3-9ae55dce2bea","name":"ypMTimsubvISNHsDYGQUpXYHE","rack_size":60,"updated":"2019-10-24T18:31:45.298296Z"},{"created":"2019-10-31T03:19:31.423112Z","id":"14096881-fdcb-4f51-b052-7ec12475a04a","name":"zBguPYtOUwCFVrFVcxSXEEuhv","rack_size":60,"updated":"2019-10-31T03:19:31.423112Z"},{"created":"2019-10-30T18:16:47.308501Z","id":"31a3eb68-86b6-4e0f-a8e3-239509a5efec","name":"zJYglWYAAhkyeqZNmAmCZcusK","rack_size":60,"updated":"2019-10-30T18:16:47.308501Z"},{"created":"2020-01-13T22:37:54.384037Z","id":"834d0dab-0026-459f-924d-3610a2f92e6a","name":"zNdiGKOHOOnOIRmEaAIwELJmX","rack_size":60,"updated":"2020-01-13T22:37:54.384037Z"},{"created":"2019-10-17T16:29:16.585327Z","id":"c29e383c-17de-4dbe-a5bb-ffec7fd5fc77","name":"zXQebgKEGGHRXukfUrRWadOLf","rack_size":60,"updated":"2019-10-17T16:29:16.585327Z"},{"created":"2019-10-31T03:23:28.644934Z","id":"04f2edd1-c649-497a-998d-6c53f53fefb6","name":"zZFbxXKnxsIipovzaEKovlNdg","rack_size":60,"updated":"2019-10-31T03:23:28.644934Z"},{"created":"2019-10-31T03:29:44.690417Z","id":"92d2b34e-f67b-4cb9-a032-4869c96fee8d","name":"ziJHVnYNRzskAcloMXLgTkcLd","rack_size":60,"updated":"2019-10-31T03:29:44.690417Z"},{"created":"2019-10-16T18:19:38.701208Z","id":"cafc7369-8b25-420f-8e2f-756c7c758efa","name":"zjaCMnQptKVkrYrWoZecvwcmi","rack_size":56,"updated":"2019-10-16T18:19:38.701208Z"},{"created":"2019-10-17T16:23:45.834148Z","id":"3543f3be-8b3a-41d2-bef0-91307554a03a","name":"zljEDboHBVkLKxWQlyqTNSXZs","rack_size":60,"updated":"2019-10-17T16:23:45.834148Z"},{"created":"2019-10-30T18:08:17.460842Z","id":"969b5ace-ecb0-452d-b224-f428bbec83e2","name":"zmqvUbNmRYKMMNKVRCUYPnOIn","rack_size":60,"updated":"2019-10-30T18:08:17.460842Z"},{"created":"2019-10-30T18:08:49.136860Z","id":"01768ee5-2f48-430d-807d-accbdf6fa1b1","name":"ztnBdqPDtIgyGVuTSYXmUIhNm","rack_size":60,"updated":"2019-10-30T18:08:49.136860Z"},{"created":"2019-10-31T02:51:10.645683Z","id":"bcf662a7-b3e8-4cbb-a5a3-8c73ecab9e04","name":"ztqqAVTKENwDQWHtmQKMMIFUs","rack_size":60,"updated":"2019-10-31T02:51:10.645683Z"}]' - headers: - Cache-Control: - - no-cache - Content-Length: - - "45740" - Content-Type: - - application/json - Date: - - Mon, 13 Jan 2020 23:16:23 GMT - Request-Id: - - fPL/c03jql82 - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - fPL/c03jql82 - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack_role/214fc519-ce44-47e9-b4f6-f802ce3e2d4a - method: GET - response: - body: '{"created":"2020-01-13T23:16:23.874950Z","id":"214fc519-ce44-47e9-b4f6-f802ce3e2d4a","name":"YUuUQAAwilNBTkBktQLbCjoUJ","rack_size":60,"updated":"2020-01-13T23:16:23.874950Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "175" - Content-Type: - - application/json - Date: - - Mon, 13 Jan 2020 23:16:24 GMT - Request-Id: - - qHn7XcIwUMAs - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - qHn7XcIwUMAs - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack_role/YUuUQAAwilNBTkBktQLbCjoUJ - method: GET - response: - body: '{"created":"2020-01-13T23:16:23.874950Z","id":"214fc519-ce44-47e9-b4f6-f802ce3e2d4a","name":"YUuUQAAwilNBTkBktQLbCjoUJ","rack_size":60,"updated":"2020-01-13T23:16:23.874950Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "175" - Content-Type: - - application/json - Date: - - Mon, 13 Jan 2020 23:16:24 GMT - Request-Id: - - w4qBcL9GVCAb - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - w4qBcL9GVCAb - status: 200 OK - code: 200 - duration: "" diff --git a/fixtures/conch-v3/racks.yaml b/fixtures/conch-v3/racks.yaml deleted file mode 100644 index 22af5c6..0000000 --- a/fixtures/conch-v3/racks.yaml +++ /dev/null @@ -1,1301 +0,0 @@ ---- -version: 1 -interactions: -- request: - body: | - {"name":"ZHUfcaVHyETwvBjCbSzrvORQb","rack_size":60} - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack_role - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Location: - - /rack_role/7c795de7-9ac6-4bc2-aaca-9ff0e2a99ef0 - Request-Id: - - 0vfOKzb8EBJh - Server: - - Mojolicious (Perl) - X-Request-Id: - - 0vfOKzb8EBJh - status: 303 See Other - code: 303 - duration: "" -- request: - body: "" - form: {} - headers: - Content-Type: - - application/json - Referer: - - http://10.51.54.42:5000/rack_role - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack_role/7c795de7-9ac6-4bc2-aaca-9ff0e2a99ef0 - method: GET - response: - body: '{"created":"2020-01-26T19:04:27.963266Z","id":"7c795de7-9ac6-4bc2-aaca-9ff0e2a99ef0","name":"ZHUfcaVHyETwvBjCbSzrvORQb","rack_size":60,"updated":"2020-01-26T19:04:27.963266Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "175" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Request-Id: - - 1kN4DE8CfJxB - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - 1kN4DE8CfJxB - status: 200 OK - code: 200 - duration: "" -- request: - body: | - {"location":"pMwuTfBzqDIvygBQlXGSujHFC","region":"cqKbObYEpySHaBapdkTsqpCbw","vendor":"JVpDAsNbghvVnEOoVCXefIqcu","vendor_name":"helkfqqzCfcKGOnefVwjTxpdP"} - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/dc - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Sun, 26 Jan 2020 19:04:27 GMT - Location: - - /dc/7833a223-1395-4541-a471-d91d7ba846ab - Request-Id: - - 5ZT28naHLau3 - Server: - - Mojolicious (Perl) - X-Request-Id: - - 5ZT28naHLau3 - status: 201 Created - code: 201 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/dc/7833a223-1395-4541-a471-d91d7ba846ab - method: GET - response: - body: '{"created":"2020-01-26T19:04:28.002386Z","id":"7833a223-1395-4541-a471-d91d7ba846ab","location":"pMwuTfBzqDIvygBQlXGSujHFC","region":"cqKbObYEpySHaBapdkTsqpCbw","updated":"2020-01-26T19:04:28.002386Z","vendor":"JVpDAsNbghvVnEOoVCXefIqcu","vendor_name":"helkfqqzCfcKGOnefVwjTxpdP"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "280" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Request-Id: - - U/oYOpmOCU9X - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - U/oYOpmOCU9X - status: 200 OK - code: 200 - duration: "" -- request: - body: | - {"alias":"vjdBBzhinOfupWTAsEKOvwGew","az":"AMfKdCbXyumqdNdnHVylqSswg","datacenter_id":"7833a223-1395-4541-a471-d91d7ba846ab","vendor_name":"OuzjBCPvhKBFhyRpMGYglHBfi"} - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/room - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Location: - - /room/7facfdc8-4155-4a68-9f28-2ce6b72f1d84 - Request-Id: - - 1r3LeF1lnNse - Server: - - Mojolicious (Perl) - X-Request-Id: - - 1r3LeF1lnNse - status: 303 See Other - code: 303 - duration: "" -- request: - body: "" - form: {} - headers: - Content-Type: - - application/json - Referer: - - http://10.51.54.42:5000/room - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/room/7facfdc8-4155-4a68-9f28-2ce6b72f1d84 - method: GET - response: - body: '{"alias":"vjdBBzhinOfupWTAsEKOvwGew","az":"AMfKdCbXyumqdNdnHVylqSswg","created":"2020-01-26T19:04:28.041112Z","datacenter_id":"7833a223-1395-4541-a471-d91d7ba846ab","id":"7facfdc8-4155-4a68-9f28-2ce6b72f1d84","updated":"2020-01-26T19:04:28.041112Z","vendor_name":"OuzjBCPvhKBFhyRpMGYglHBfi"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "291" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Location: - - /room/7facfdc8-4155-4a68-9f28-2ce6b72f1d84 - Request-Id: - - qe6TSXhLeTz5 - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - qe6TSXhLeTz5 - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_vendor/MyBigVendor - method: GET - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Request-Id: - - 1nzwPoQqof06 - Server: - - Mojolicious (Perl) - X-Request-Id: - - 1nzwPoQqof06 - status: 410 Gone - code: 410 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_vendor/MyBigVendor - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Location: - - /hardware_vendor/MyBigVendor - Request-Id: - - Zs8yWfSYpwtd - Server: - - Mojolicious (Perl) - X-Request-Id: - - Zs8yWfSYpwtd - status: 303 See Other - code: 303 - duration: "" -- request: - body: "" - form: {} - headers: - Referer: - - http://10.51.54.42:5000/hardware_vendor/MyBigVendor - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_vendor/MyBigVendor - method: GET - response: - body: '{"created":"2020-01-26T19:04:28.095805Z","id":"3d28519c-66c8-4342-8220-4e1cae887e2d","name":"MyBigVendor","updated":"2020-01-26T19:04:28.095805Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "146" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Request-Id: - - E+iPCa8uPYMq - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - E+iPCa8uPYMq - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_vendor/MyBigVendor - method: GET - response: - body: '{"created":"2020-01-26T19:04:28.095805Z","id":"3d28519c-66c8-4342-8220-4e1cae887e2d","name":"MyBigVendor","updated":"2020-01-26T19:04:28.095805Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "146" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Request-Id: - - ieHJQROFOy9O - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - ieHJQROFOy9O - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/validation_plan/a30ab8b2-8a9e-4e51-8bb0-92862abd8b54 - method: GET - response: - body: '{"created":"2019-10-09T17:21:37.861483Z","description":"Validation plan - containing all validations run in Conch v1 on servers","id":"a30ab8b2-8a9e-4e51-8bb0-92862abd8b54","name":"Conch - v1 Legacy Plan: Server"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "209" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Request-Id: - - uDYc6dTe9fR+ - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - uDYc6dTe9fR+ - status: 200 OK - code: 200 - duration: "" -- request: - body: | - {"alias":"HrZIMrVpcWqZgaUulBthQcOag","bios_firmware":"MnUVOnKQRdqBIceuwfwrWEjhW","cpu_type":"VnLtfCOjaztZvISSPHEiVKiQZ","hardware_vendor_id":"3d28519c-66c8-4342-8220-4e1cae887e2d","name":"oYVmnkaHmHTuHKrcEesRozYeT","purpose":"znqMcuZEZIkFJlfstVGYwPpkq","rack_unit_size":1,"sku":"qDwDpLdrLFrPEqjOOUrmyGirY","validation_plan_id":"a30ab8b2-8a9e-4e51-8bb0-92862abd8b54"} - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_product - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Location: - - /hardware_product/4d97ab05-0d97-47aa-862d-82bb2ee0468a - Request-Id: - - OSPhfPpzmMLm - Server: - - Mojolicious (Perl) - X-Request-Id: - - OSPhfPpzmMLm - status: 303 See Other - code: 303 - duration: "" -- request: - body: "" - form: {} - headers: - Content-Type: - - application/json - Referer: - - http://10.51.54.42:5000/hardware_product - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_product/4d97ab05-0d97-47aa-862d-82bb2ee0468a - method: GET - response: - body: '{"alias":"HrZIMrVpcWqZgaUulBthQcOag","created":"2020-01-26T19:04:28.181783Z","generation_name":null,"id":"4d97ab05-0d97-47aa-862d-82bb2ee0468a","name":"oYVmnkaHmHTuHKrcEesRozYeT","sku":"qDwDpLdrLFrPEqjOOUrmyGirY","updated":"2020-01-26T19:04:28.181783Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "985" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Location: - - /hardware_product/4d97ab05-0d97-47aa-862d-82bb2ee0468a - Request-Id: - - oC97IiVy1afF - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - oC97IiVy1afF - status: 200 OK - code: 200 - duration: "" -- request: - body: | - {"alias":"kzdrzOFbSzwABLXZJgiRbeCyZ","bios_firmware":"YBMTTKmRmcMJdeudbCHnLdWTA","cpu_type":"IjGVEsUTFgtcqCgykUYFkzSlh","hardware_vendor_id":"3d28519c-66c8-4342-8220-4e1cae887e2d","name":"dpVmuHtPowtbsWxgeQGGhDxzq","purpose":"aqjoVIMEkFxHTVJUbjbuQOjnq","rack_unit_size":1,"sku":"kCKOViIEFgkEhdhcQFxItjUmb","validation_plan_id":"a30ab8b2-8a9e-4e51-8bb0-92862abd8b54"} - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_product - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Location: - - /hardware_product/cd87e849-5520-4601-86ef-5998ae6cde12 - Request-Id: - - 3pbS9B/+gQaW - Server: - - Mojolicious (Perl) - X-Request-Id: - - 3pbS9B/+gQaW - status: 303 See Other - code: 303 - duration: "" -- request: - body: "" - form: {} - headers: - Content-Type: - - application/json - Referer: - - http://10.51.54.42:5000/hardware_product - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_product/cd87e849-5520-4601-86ef-5998ae6cde12 - method: GET - response: - body: '{"alias":"kzdrzOFbSzwABLXZJgiRbeCyZ","created":"2020-01-26T19:04:28.239898Z","generation_name":null,"id":"cd87e849-5520-4601-86ef-5998ae6cde12","name":"dpVmuHtPowtbsWxgeQGGhDxzq","sku":"kCKOViIEFgkEhdhcQFxItjUmb","updated":"2020-01-26T19:04:28.239898Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "985" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Location: - - /hardware_product/cd87e849-5520-4601-86ef-5998ae6cde12 - Request-Id: - - mfeqoXXxDNPO - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - mfeqoXXxDNPO - status: 200 OK - code: 200 - duration: "" -- request: - body: | - {"admins":[{"email":"conch@example.com"}],"description":"AMhZHoxzPwNZsyYmcTflWMKqY","name":"aQsXFjCLcMZdOKGwPLDAuldld"} - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/build - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Location: - - /build/2a089cd3-a9c3-4c38-be7a-e04e1c36706a - Request-Id: - - 3wX2gtaUCHUw - Server: - - Mojolicious (Perl) - X-Request-Id: - - 3wX2gtaUCHUw - status: 303 See Other - code: 303 - duration: "" -- request: - body: "" - form: {} - headers: - Content-Type: - - application/json - Referer: - - http://10.51.54.42:5000/build - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/build/2a089cd3-a9c3-4c38-be7a-e04e1c36706a - method: GET - response: - body: '{"admins":[{"email":"conch@example.com","id":"1ebc992d-ced6-47fa-be0c-3f966a9eb42f","name":"Kosh"}],"completed":null,"completed_user":null,"created":"2020-01-26T19:04:28.289251Z","description":"AMhZHoxzPwNZsyYmcTflWMKqY","id":"2a089cd3-a9c3-4c38-be7a-e04e1c36706a","name":"aQsXFjCLcMZdOKGwPLDAuldld","started":null}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "315" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Request-Id: - - c3im2cDU7wCy - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - c3im2cDU7wCy - status: 200 OK - code: 200 - duration: "" -- request: - body: | - {"build_id":"2a089cd3-a9c3-4c38-be7a-e04e1c36706a","datacenter_room_id":"7facfdc8-4155-4a68-9f28-2ce6b72f1d84","name":"gbOLAevgQXeOSzVeuqdsOCoZT","phase":"integration","rack_role_id":"7c795de7-9ac6-4bc2-aaca-9ff0e2a99ef0"} - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Location: - - /rack/bca95573-9ca0-4dc1-adc5-6a608c8227d2 - Request-Id: - - B8DxwdR1pTZ1 - Server: - - Mojolicious (Perl) - X-Request-Id: - - B8DxwdR1pTZ1 - status: 303 See Other - code: 303 - duration: "" -- request: - body: "" - form: {} - headers: - Content-Type: - - application/json - Referer: - - http://10.51.54.42:5000/rack - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack/bca95573-9ca0-4dc1-adc5-6a608c8227d2 - method: GET - response: - body: '{"asset_tag":null,"build_id":"2a089cd3-a9c3-4c38-be7a-e04e1c36706a","build_name":"aQsXFjCLcMZdOKGwPLDAuldld","created":"2020-01-26T19:04:28.356986Z","datacenter_room_alias":"vjdBBzhinOfupWTAsEKOvwGew","datacenter_room_id":"7facfdc8-4155-4a68-9f28-2ce6b72f1d84","full_rack_name":"OuzjBCPvhKBFhyRpMGYglHBfi:gbOLAevgQXeOSzVeuqdsOCoZT","id":"bca95573-9ca0-4dc1-adc5-6a608c8227d2","name":"gbOLAevgQXeOSzVeuqdsOCoZT","phase":"integration","rack_role_id":"7c795de7-9ac6-4bc2-aaca-9ff0e2a99ef0","rack_role_name":"ZHUfcaVHyETwvBjCbSzrvORQb","serial_number":null,"updated":"2020-01-26T19:04:28.356986Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "593" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Location: - - /rack/bca95573-9ca0-4dc1-adc5-6a608c8227d2 - Request-Id: - - iTw7q6l2+1q3 - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - iTw7q6l2+1q3 - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack_role/7c795de7-9ac6-4bc2-aaca-9ff0e2a99ef0 - method: GET - response: - body: '{"created":"2020-01-26T19:04:27.963266Z","id":"7c795de7-9ac6-4bc2-aaca-9ff0e2a99ef0","name":"ZHUfcaVHyETwvBjCbSzrvORQb","rack_size":60,"updated":"2020-01-26T19:04:27.963266Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "175" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Request-Id: - - MaLAen5l28+d - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - MaLAen5l28+d - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack/bca95573-9ca0-4dc1-adc5-6a608c8227d2 - method: GET - response: - body: '{"asset_tag":null,"build_id":"2a089cd3-a9c3-4c38-be7a-e04e1c36706a","build_name":"aQsXFjCLcMZdOKGwPLDAuldld","created":"2020-01-26T19:04:28.356986Z","datacenter_room_alias":"vjdBBzhinOfupWTAsEKOvwGew","datacenter_room_id":"7facfdc8-4155-4a68-9f28-2ce6b72f1d84","full_rack_name":"OuzjBCPvhKBFhyRpMGYglHBfi:gbOLAevgQXeOSzVeuqdsOCoZT","id":"bca95573-9ca0-4dc1-adc5-6a608c8227d2","name":"gbOLAevgQXeOSzVeuqdsOCoZT","phase":"integration","rack_role_id":"7c795de7-9ac6-4bc2-aaca-9ff0e2a99ef0","rack_role_name":"ZHUfcaVHyETwvBjCbSzrvORQb","serial_number":null,"updated":"2020-01-26T19:04:28.356986Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "593" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Location: - - /rack/bca95573-9ca0-4dc1-adc5-6a608c8227d2 - Request-Id: - - Q0ButRl319N8 - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - Q0ButRl319N8 - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack_role/7c795de7-9ac6-4bc2-aaca-9ff0e2a99ef0 - method: GET - response: - body: '{"created":"2020-01-26T19:04:27.963266Z","id":"7c795de7-9ac6-4bc2-aaca-9ff0e2a99ef0","name":"ZHUfcaVHyETwvBjCbSzrvORQb","rack_size":60,"updated":"2020-01-26T19:04:27.963266Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "175" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Request-Id: - - XDpC6/W0Lo/I - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - XDpC6/W0Lo/I - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/room/7facfdc8-4155-4a68-9f28-2ce6b72f1d84 - method: GET - response: - body: '{"alias":"vjdBBzhinOfupWTAsEKOvwGew","az":"AMfKdCbXyumqdNdnHVylqSswg","created":"2020-01-26T19:04:28.041112Z","datacenter_id":"7833a223-1395-4541-a471-d91d7ba846ab","id":"7facfdc8-4155-4a68-9f28-2ce6b72f1d84","updated":"2020-01-26T19:04:28.041112Z","vendor_name":"OuzjBCPvhKBFhyRpMGYglHBfi"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "291" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Location: - - /room/7facfdc8-4155-4a68-9f28-2ce6b72f1d84 - Request-Id: - - SBKPJqjxeG4i - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - SBKPJqjxeG4i - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack/bca95573-9ca0-4dc1-adc5-6a608c8227d2/layout - method: GET - response: - body: '[]' - headers: - Cache-Control: - - no-cache - Content-Length: - - "2" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Location: - - /rack/bca95573-9ca0-4dc1-adc5-6a608c8227d2/layout - Request-Id: - - 3RphLu/ynYqI - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - 3RphLu/ynYqI - status: 200 OK - code: 200 - duration: "" -- request: - body: | - {"hardware_product_id":"cd87e849-5520-4601-86ef-5998ae6cde12","rack_id":"bca95573-9ca0-4dc1-adc5-6a608c8227d2","rack_unit_start":1} - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/layout - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Location: - - /layout/35bbf62f-1fbf-40c2-97af-485ab568ae99 - Request-Id: - - RGinlFOtuFhY - Server: - - Mojolicious (Perl) - X-Request-Id: - - RGinlFOtuFhY - status: 303 See Other - code: 303 - duration: "" -- request: - body: "" - form: {} - headers: - Content-Type: - - application/json - Referer: - - http://10.51.54.42:5000/layout - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/layout/35bbf62f-1fbf-40c2-97af-485ab568ae99 - method: GET - response: - body: '{"created":"2020-01-26T19:04:28.530904Z","hardware_product_id":"cd87e849-5520-4601-86ef-5998ae6cde12","id":"35bbf62f-1fbf-40c2-97af-485ab568ae99","rack_id":"bca95573-9ca0-4dc1-adc5-6a608c8227d2","rack_name":"OuzjBCPvhKBFhyRpMGYglHBfi:gbOLAevgQXeOSzVeuqdsOCoZT","rack_unit_size":1,"rack_unit_start":1,"sku":"kCKOViIEFgkEhdhcQFxItjUmb","updated":"2020-01-26T19:04:28.530904Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "374" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Location: - - /layout/35bbf62f-1fbf-40c2-97af-485ab568ae99 - Request-Id: - - /4UVPxwvbuMn - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - /4UVPxwvbuMn - status: 200 OK - code: 200 - duration: "" -- request: - body: | - {"hardware_product_id":"4d97ab05-0d97-47aa-862d-82bb2ee0468a","rack_id":"bca95573-9ca0-4dc1-adc5-6a608c8227d2","rack_unit_start":2} - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/layout - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Location: - - /layout/d0453671-06f8-43b7-a435-d9bc46740c67 - Request-Id: - - 2Qdcr2mGr4aG - Server: - - Mojolicious (Perl) - X-Request-Id: - - 2Qdcr2mGr4aG - status: 303 See Other - code: 303 - duration: "" -- request: - body: "" - form: {} - headers: - Content-Type: - - application/json - Referer: - - http://10.51.54.42:5000/layout - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/layout/d0453671-06f8-43b7-a435-d9bc46740c67 - method: GET - response: - body: '{"created":"2020-01-26T19:04:28.596937Z","hardware_product_id":"4d97ab05-0d97-47aa-862d-82bb2ee0468a","id":"d0453671-06f8-43b7-a435-d9bc46740c67","rack_id":"bca95573-9ca0-4dc1-adc5-6a608c8227d2","rack_name":"OuzjBCPvhKBFhyRpMGYglHBfi:gbOLAevgQXeOSzVeuqdsOCoZT","rack_unit_size":1,"rack_unit_start":2,"sku":"qDwDpLdrLFrPEqjOOUrmyGirY","updated":"2020-01-26T19:04:28.596937Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "374" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Location: - - /layout/d0453671-06f8-43b7-a435-d9bc46740c67 - Request-Id: - - ZsDc+jhMWIhV - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - ZsDc+jhMWIhV - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack/bca95573-9ca0-4dc1-adc5-6a608c8227d2/layout - method: GET - response: - body: '[{"created":"2020-01-26T19:04:28.530904Z","hardware_product_id":"cd87e849-5520-4601-86ef-5998ae6cde12","id":"35bbf62f-1fbf-40c2-97af-485ab568ae99","rack_id":"bca95573-9ca0-4dc1-adc5-6a608c8227d2","rack_name":"OuzjBCPvhKBFhyRpMGYglHBfi:gbOLAevgQXeOSzVeuqdsOCoZT","rack_unit_size":1,"rack_unit_start":1,"sku":"kCKOViIEFgkEhdhcQFxItjUmb","updated":"2020-01-26T19:04:28.530904Z"},{"created":"2020-01-26T19:04:28.596937Z","hardware_product_id":"4d97ab05-0d97-47aa-862d-82bb2ee0468a","id":"d0453671-06f8-43b7-a435-d9bc46740c67","rack_id":"bca95573-9ca0-4dc1-adc5-6a608c8227d2","rack_name":"OuzjBCPvhKBFhyRpMGYglHBfi:gbOLAevgQXeOSzVeuqdsOCoZT","rack_unit_size":1,"rack_unit_start":2,"sku":"qDwDpLdrLFrPEqjOOUrmyGirY","updated":"2020-01-26T19:04:28.596937Z"}]' - headers: - Cache-Control: - - no-cache - Content-Length: - - "751" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Location: - - /rack/bca95573-9ca0-4dc1-adc5-6a608c8227d2/layout - Request-Id: - - Q5cAe11DtfJJ - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - Q5cAe11DtfJJ - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack/bca95573-9ca0-4dc1-adc5-6a608c8227d2/layout - method: GET - response: - body: '[{"created":"2020-01-26T19:04:28.530904Z","hardware_product_id":"cd87e849-5520-4601-86ef-5998ae6cde12","id":"35bbf62f-1fbf-40c2-97af-485ab568ae99","rack_id":"bca95573-9ca0-4dc1-adc5-6a608c8227d2","rack_name":"OuzjBCPvhKBFhyRpMGYglHBfi:gbOLAevgQXeOSzVeuqdsOCoZT","rack_unit_size":1,"rack_unit_start":1,"sku":"kCKOViIEFgkEhdhcQFxItjUmb","updated":"2020-01-26T19:04:28.530904Z"},{"created":"2020-01-26T19:04:28.596937Z","hardware_product_id":"4d97ab05-0d97-47aa-862d-82bb2ee0468a","id":"d0453671-06f8-43b7-a435-d9bc46740c67","rack_id":"bca95573-9ca0-4dc1-adc5-6a608c8227d2","rack_name":"OuzjBCPvhKBFhyRpMGYglHBfi:gbOLAevgQXeOSzVeuqdsOCoZT","rack_unit_size":1,"rack_unit_start":2,"sku":"qDwDpLdrLFrPEqjOOUrmyGirY","updated":"2020-01-26T19:04:28.596937Z"}]' - headers: - Cache-Control: - - no-cache - Content-Length: - - "751" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Location: - - /rack/bca95573-9ca0-4dc1-adc5-6a608c8227d2/layout - Request-Id: - - LVX3660fo3iT - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - LVX3660fo3iT - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/layout/35bbf62f-1fbf-40c2-97af-485ab568ae99 - method: DELETE - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Request-Id: - - i5IEvFrzuJhv - Server: - - Mojolicious (Perl) - X-Request-Id: - - i5IEvFrzuJhv - status: 204 No Content - code: 204 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/layout/d0453671-06f8-43b7-a435-d9bc46740c67 - method: DELETE - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Request-Id: - - iT08B5K1AJZu - Server: - - Mojolicious (Perl) - X-Request-Id: - - iT08B5K1AJZu - status: 204 No Content - code: 204 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack/bca95573-9ca0-4dc1-adc5-6a608c8227d2 - method: DELETE - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Request-Id: - - rRMtWX3hQaot - Server: - - Mojolicious (Perl) - X-Request-Id: - - rRMtWX3hQaot - status: 204 No Content - code: 204 - duration: "" -- request: - body: | - {"build_id":"2a089cd3-a9c3-4c38-be7a-e04e1c36706a","datacenter_room_id":"7facfdc8-4155-4a68-9f28-2ce6b72f1d84","name":"IsQZkOvCDvgXVktirYJznAkaL","phase":"integration","rack_role_id":"7c795de7-9ac6-4bc2-aaca-9ff0e2a99ef0"} - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Location: - - /rack/1a66390c-0aef-472c-9db8-2a28e55d7c18 - Request-Id: - - OjejwIQRJ0U9 - Server: - - Mojolicious (Perl) - X-Request-Id: - - OjejwIQRJ0U9 - status: 303 See Other - code: 303 - duration: "" -- request: - body: "" - form: {} - headers: - Content-Type: - - application/json - Referer: - - http://10.51.54.42:5000/rack - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack/1a66390c-0aef-472c-9db8-2a28e55d7c18 - method: GET - response: - body: '{"asset_tag":null,"build_id":"2a089cd3-a9c3-4c38-be7a-e04e1c36706a","build_name":"aQsXFjCLcMZdOKGwPLDAuldld","created":"2020-01-26T19:04:28.768430Z","datacenter_room_alias":"vjdBBzhinOfupWTAsEKOvwGew","datacenter_room_id":"7facfdc8-4155-4a68-9f28-2ce6b72f1d84","full_rack_name":"OuzjBCPvhKBFhyRpMGYglHBfi:IsQZkOvCDvgXVktirYJznAkaL","id":"1a66390c-0aef-472c-9db8-2a28e55d7c18","name":"IsQZkOvCDvgXVktirYJznAkaL","phase":"integration","rack_role_id":"7c795de7-9ac6-4bc2-aaca-9ff0e2a99ef0","rack_role_name":"ZHUfcaVHyETwvBjCbSzrvORQb","serial_number":null,"updated":"2020-01-26T19:04:28.768430Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "593" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Location: - - /rack/1a66390c-0aef-472c-9db8-2a28e55d7c18 - Request-Id: - - 9jcqMTo4yvAH - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - 9jcqMTo4yvAH - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack_role/7c795de7-9ac6-4bc2-aaca-9ff0e2a99ef0 - method: GET - response: - body: '{"created":"2020-01-26T19:04:27.963266Z","id":"7c795de7-9ac6-4bc2-aaca-9ff0e2a99ef0","name":"ZHUfcaVHyETwvBjCbSzrvORQb","rack_size":60,"updated":"2020-01-26T19:04:27.963266Z"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "175" - Content-Type: - - application/json - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Request-Id: - - msuBEwJVKflx - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - msuBEwJVKflx - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack/1a66390c-0aef-472c-9db8-2a28e55d7c18 - method: DELETE - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Request-Id: - - ww51X0YPDUna - Server: - - Mojolicious (Perl) - X-Request-Id: - - ww51X0YPDUna - status: 204 No Content - code: 204 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_product/cd87e849-5520-4601-86ef-5998ae6cde12 - method: DELETE - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Request-Id: - - 8Z/VaSizYrrc - Server: - - Mojolicious (Perl) - X-Request-Id: - - 8Z/VaSizYrrc - status: 204 No Content - code: 204 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_product/4d97ab05-0d97-47aa-862d-82bb2ee0468a - method: DELETE - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Request-Id: - - PiMlCqijk5jb - Server: - - Mojolicious (Perl) - X-Request-Id: - - PiMlCqijk5jb - status: 204 No Content - code: 204 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/hardware_vendor/3d28519c-66c8-4342-8220-4e1cae887e2d - method: DELETE - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Request-Id: - - f+IRfQ8YIzw/ - Server: - - Mojolicious (Perl) - X-Request-Id: - - f+IRfQ8YIzw/ - status: 204 No Content - code: 204 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/room/7facfdc8-4155-4a68-9f28-2ce6b72f1d84 - method: DELETE - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Request-Id: - - zTNzjDVNesDF - Server: - - Mojolicious (Perl) - X-Request-Id: - - zTNzjDVNesDF - status: 204 No Content - code: 204 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/dc/7833a223-1395-4541-a471-d91d7ba846ab - method: DELETE - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Request-Id: - - vATUZMCt5AUw - Server: - - Mojolicious (Perl) - X-Request-Id: - - vATUZMCt5AUw - status: 204 No Content - code: 204 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/rack_role/7c795de7-9ac6-4bc2-aaca-9ff0e2a99ef0 - method: DELETE - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Sun, 26 Jan 2020 19:04:28 GMT - Request-Id: - - YYyTVzCtYI+9 - Server: - - Mojolicious (Perl) - X-Request-Id: - - YYyTVzCtYI+9 - status: 204 No Content - code: 204 - duration: "" diff --git a/fixtures/conch-v3/relays.yaml b/fixtures/conch-v3/relays.yaml deleted file mode 100644 index edfddfb..0000000 --- a/fixtures/conch-v3/relays.yaml +++ /dev/null @@ -1,146 +0,0 @@ ---- -version: 1 -interactions: -- request: - body: | - {"serial":"sAWCXAbDHumkCUsrvQpvjFJwv","version":"ztbSVjaDAEtViwTWMACxeZjhm","ipaddr":"81.174.119.130","name":"dxffasDNjHAUOSIVacQhmBbIX","ssh_port":57} - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/relay/sAWCXAbDHumkCUsrvQpvjFJwv/register - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Mon, 13 Jan 2020 23:16:23 GMT - Location: - - /relay/5540951c-9f85-4812-9dc9-ef9ae9adb8af - Request-Id: - - GnH9z5gbo2lb - Server: - - Mojolicious (Perl) - X-Request-Id: - - GnH9z5gbo2lb - status: 204 No Content - code: 204 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/relay/5540951c-9f85-4812-9dc9-ef9ae9adb8af - method: GET - response: - body: '{"created":"2020-01-13T22:46:03.232942Z","id":"5540951c-9f85-4812-9dc9-ef9ae9adb8af","ipaddr":"81.174.119.130","last_seen":"2020-01-13T23:16:23.756431Z","name":"dxffasDNjHAUOSIVacQhmBbIX","serial_number":"sAWCXAbDHumkCUsrvQpvjFJwv","ssh_port":57,"updated":"2020-01-13T23:16:23.756431Z","user_id":"c3d2c2e6-e3c0-48ca-a8b0-f97e842888f5","version":"ztbSVjaDAEtViwTWMACxeZjhm"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "373" - Content-Type: - - application/json - Date: - - Mon, 13 Jan 2020 23:16:23 GMT - Request-Id: - - o8Jgz1VJGyCc - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - o8Jgz1VJGyCc - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/relay/?no_devices=1 - method: GET - response: - body: '[{"created":"2019-10-15T18:41:10.671455Z","id":"230dda21-39a4-4488-bcbf-779dcece2e4a","ipaddr":"173.178.209.86","last_seen":"2019-10-15T18:41:10.677782Z","name":"nPcQEWgOjWboENGaASZLyXVqv","serial_number":"AVFmNCiKRQisJqnDFHRtpMOKv","ssh_port":55,"updated":"2019-10-15T18:41:10.671455Z","user_id":"c3d2c2e6-e3c0-48ca-a8b0-f97e842888f5","version":"iVUlRjcNsiAiicGbhacvQFwWh"},{"created":"2019-10-15T18:40:14.601549Z","id":"ed399b4e-2ac6-407c-b29d-475581c21ff2","ipaddr":"134.164.88.148","last_seen":"2019-10-15T18:40:14.607725Z","name":"nYcfCwRsbIHWsYDXdeBNcRBhZ","serial_number":"LifyUKqvoNYrpsBrEiNYtrmdA","ssh_port":50,"updated":"2019-10-15T18:40:14.601549Z","user_id":"c3d2c2e6-e3c0-48ca-a8b0-f97e842888f5","version":"nePPjjYXATrrhgDqpvqfaGlNV"},{"created":"2019-10-15T18:39:12.640376Z","id":"01b48135-3b07-431d-b00e-98560712b53b","ipaddr":"235.36.102.200","last_seen":"2019-10-15T18:39:12.649978Z","name":"UzzsTQIlQPFtqNYOONSBUYbvb","serial_number":"PdixqhYWvwXknoXeFHqoTKQaZ","ssh_port":69,"updated":"2019-10-15T18:39:12.640376Z","user_id":"c3d2c2e6-e3c0-48ca-a8b0-f97e842888f5","version":"iUTNbqrHwGbJyukNhfiWPqXzI"},{"created":"2019-10-15T18:38:56.300094Z","id":"7ff08b49-d320-411d-aa55-29e577b80a25","ipaddr":"248.35.101.7","last_seen":"2019-10-15T18:38:56.308636Z","name":"iHFOubTsquazhQJyDMncBOHpT","serial_number":"QcEorjHAnAoJKmgebqqpXqQhI","ssh_port":9,"updated":"2019-10-15T18:38:56.300094Z","user_id":"c3d2c2e6-e3c0-48ca-a8b0-f97e842888f5","version":"UOgDYiDzoFMDaICbvYCCFdmXT"},{"created":"2019-10-15T18:39:36.442122Z","id":"77adce84-1118-4769-ac73-b6568f560d4c","ipaddr":"77.251.115.244","last_seen":"2019-10-15T18:39:36.450130Z","name":"SkqVyFMMIAQROlagJzqiVYEDw","serial_number":"XipLGCccigFdqAyKRRqUPRbRq","ssh_port":96,"updated":"2019-10-15T18:39:36.442122Z","user_id":"c3d2c2e6-e3c0-48ca-a8b0-f97e842888f5","version":"XxVaxPgxqOwJrzSmcjEUPnBhb"},{"created":"2019-10-15T18:42:52.540622Z","id":"d20f070d-1ab5-4cfe-9d44-c67c2123a313","ipaddr":"187.241.91.120","last_seen":"2019-10-15T18:42:52.547737Z","name":"ZrmMabDfEWTsRpKxOJUuMHfND","serial_number":"mhZgglbszIOJblBDPDscKNsMA","ssh_port":22,"updated":"2019-10-15T18:42:52.540622Z","user_id":"c3d2c2e6-e3c0-48ca-a8b0-f97e842888f5","version":"woxUkttahhLsMkDTXuqfBYiZG"},{"created":"2020-01-13T22:46:03.232942Z","id":"5540951c-9f85-4812-9dc9-ef9ae9adb8af","ipaddr":"81.174.119.130","last_seen":"2020-01-13T23:16:23.756431Z","name":"dxffasDNjHAUOSIVacQhmBbIX","serial_number":"sAWCXAbDHumkCUsrvQpvjFJwv","ssh_port":57,"updated":"2020-01-13T23:16:23.756431Z","user_id":"c3d2c2e6-e3c0-48ca-a8b0-f97e842888f5","version":"ztbSVjaDAEtViwTWMACxeZjhm"}]' - headers: - Cache-Control: - - no-cache - Content-Length: - - "2616" - Content-Type: - - application/json - Date: - - Mon, 13 Jan 2020 23:16:23 GMT - Request-Id: - - 0a13+D6rZBvk - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - 0a13+D6rZBvk - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/relay/sAWCXAbDHumkCUsrvQpvjFJwv - method: GET - response: - body: '{"created":"2020-01-13T22:46:03.232942Z","id":"5540951c-9f85-4812-9dc9-ef9ae9adb8af","ipaddr":"81.174.119.130","last_seen":"2020-01-13T23:16:23.756431Z","name":"dxffasDNjHAUOSIVacQhmBbIX","serial_number":"sAWCXAbDHumkCUsrvQpvjFJwv","ssh_port":57,"updated":"2020-01-13T23:16:23.756431Z","user_id":"c3d2c2e6-e3c0-48ca-a8b0-f97e842888f5","version":"ztbSVjaDAEtViwTWMACxeZjhm"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "373" - Content-Type: - - application/json - Date: - - Mon, 13 Jan 2020 23:16:23 GMT - Request-Id: - - wDQFqNm48BA4 - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - wDQFqNm48BA4 - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/relay/sAWCXAbDHumkCUsrvQpvjFJwv - method: DELETE - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Mon, 13 Jan 2020 23:16:23 GMT - Request-Id: - - 5jbkHZxlwZqP - Server: - - Mojolicious (Perl) - X-Request-Id: - - 5jbkHZxlwZqP - status: 204 No Content - code: 204 - duration: "" diff --git a/fixtures/conch-v3/rooms.yaml b/fixtures/conch-v3/rooms.yaml deleted file mode 100644 index a4cd0cf..0000000 --- a/fixtures/conch-v3/rooms.yaml +++ /dev/null @@ -1,271 +0,0 @@ ---- -version: 1 -interactions: -- request: - body: | - {"location":"fvaqOeglsqvtzhelPaayaNLaR","region":"ilyBxjCVhWTMNjezTdHrAJqpZ","vendor":"DBpuNxsbLmMYbWpnvrstlcaNT","vendor_name":"XscPOfUssegOxCxGDsNwIXOcK"} - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/dc - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Mon, 13 Jan 2020 23:16:24 GMT - Location: - - /dc/013604b3-4cea-4880-b224-4dc5fe46dd11 - Request-Id: - - 3l3e97Sh+qIa - Server: - - Mojolicious (Perl) - X-Request-Id: - - 3l3e97Sh+qIa - status: 201 Created - code: 201 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/dc/013604b3-4cea-4880-b224-4dc5fe46dd11 - method: GET - response: - body: '{"created":"2020-01-13T23:16:24.070493Z","id":"013604b3-4cea-4880-b224-4dc5fe46dd11","location":"fvaqOeglsqvtzhelPaayaNLaR","region":"ilyBxjCVhWTMNjezTdHrAJqpZ","updated":"2020-01-13T23:16:24.070493Z","vendor":"DBpuNxsbLmMYbWpnvrstlcaNT","vendor_name":"XscPOfUssegOxCxGDsNwIXOcK"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "280" - Content-Type: - - application/json - Date: - - Mon, 13 Jan 2020 23:16:24 GMT - Request-Id: - - SaBvXbA4ro02 - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - SaBvXbA4ro02 - status: 200 OK - code: 200 - duration: "" -- request: - body: | - {"alias":"LyJjNHLJrGFJEdWHbFzseONMt","az":"djwFSmkqRXLtKbBZePLBZJggI","datacenter_id":"013604b3-4cea-4880-b224-4dc5fe46dd11","vendor_name":"vuHLjudtinAEfYrfoxBaDmOWf"} - form: {} - headers: - Content-Type: - - application/json - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/room - method: POST - response: - body: "" - headers: - Cache-Control: - - no-cache - Content-Length: - - "0" - Date: - - Mon, 13 Jan 2020 23:16:24 GMT - Location: - - /room/9855c736-2479-4996-be7d-de9f8c470ceb - Request-Id: - - JqrwI3LUc406 - Server: - - Mojolicious (Perl) - X-Request-Id: - - JqrwI3LUc406 - status: 303 See Other - code: 303 - duration: "" -- request: - body: "" - form: {} - headers: - Content-Type: - - application/json - Referer: - - http://10.51.54.42:5000/room - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/room/9855c736-2479-4996-be7d-de9f8c470ceb - method: GET - response: - body: '{"alias":"LyJjNHLJrGFJEdWHbFzseONMt","az":"djwFSmkqRXLtKbBZePLBZJggI","created":"2020-01-13T23:16:24.112506Z","datacenter_id":"013604b3-4cea-4880-b224-4dc5fe46dd11","id":"9855c736-2479-4996-be7d-de9f8c470ceb","updated":"2020-01-13T23:16:24.112506Z","vendor_name":"vuHLjudtinAEfYrfoxBaDmOWf"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "291" - Content-Type: - - application/json - Date: - - Mon, 13 Jan 2020 23:16:24 GMT - Location: - - /room/9855c736-2479-4996-be7d-de9f8c470ceb - Request-Id: - - JOdoQ6fiPEQx - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - JOdoQ6fiPEQx - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/room - method: GET - response: - body: '[{"alias":"AFWxzWqEqKUTMIKdnSivWGBCa","az":"XcXtdUWrzjrdpJrRrQIymOLUl","created":"2019-10-31T19:38:36.235222Z","datacenter_id":"6ce4a3cc-ff7d-4d6f-ae00-e3225cef413c","id":"9bbb16e3-bf59-42dc-ab90-e50a1679bdc4","updated":"2019-10-31T19:38:36.235222Z","vendor_name":"WTVScBynxIBKbLRHkSXZWyalO"},{"alias":"AJMoYWYTBOhSmlkBRQJrrjpje","az":"euncDQYWsNazmODUCuDdcdzVv","created":"2019-10-31T14:20:58.877685Z","datacenter_id":"229e95a0-c439-46c2-a725-3cad31f28d1f","id":"7000995e-5d43-41af-8c64-b7ee32378780","updated":"2019-10-31T14:20:58.877685Z","vendor_name":"KtaOdnbsRlbOcXOTVSRINuCcR"},{"alias":"AbjMBvvWSBsRhFogphdBjzylG","az":"tdVyKqEAvGvlDRQGOzyUwsfqB","created":"2019-10-31T15:11:40.503475Z","datacenter_id":"776d17a5-fc8b-4d75-acf1-297ea35b7d35","id":"6aaec91d-7e37-4274-abac-8166421fb53d","updated":"2019-10-31T15:11:40.503475Z","vendor_name":"cJOtnTuTiPDdAcMZicySAovws"},{"alias":"AyjrgyYacvFEjFGIFDumXklPa","az":"FldBFxcbfNuqHPiPJIKLrQGmk","created":"2019-10-28T16:54:39.687927Z","datacenter_id":"88450bde-05a7-4f32-bc20-172062f40610","id":"f5b24552-b6c5-4d3b-b49d-561f438b36a2","updated":"2019-10-28T16:54:39.687927Z","vendor_name":"QvWxQxbJIYRyWeqgPloLrxcop"},{"alias":"AyneoxGTrVCTRhCerbgRqQVCg","az":"gusKUQpBAPavvBjblDKhIKZNe","created":"2019-10-28T14:58:11.959434Z","datacenter_id":"1b188fdb-04e0-4a16-add0-5047e82ebfe5","id":"dfe56ca2-f44f-4446-b5d4-2b344b82358d","updated":"2019-10-28T14:58:11.959434Z","vendor_name":"AdnQciqwngcNtyPCvOvAcAknt"},{"alias":"BnvmoMJtpmqrbLdZSStZndvSA","az":"BTpYESypGfYMnVIbyccQLAhNL","created":"2019-10-17T16:23:02.118917Z","datacenter_id":"80a68b60-8d26-4d5f-8123-63b827f01b13","id":"db1fadb4-e94f-4ca4-a5aa-751149b4270c","updated":"2019-10-17T16:23:02.118917Z","vendor_name":"breMljlRCFnKvdEnymejBlScT"},{"alias":"CCjSWmOpudNeNBWGVQGMRAeyl","az":"kUpsxLpXMAFatGqxtwPqkoqXb","created":"2019-10-31T19:33:09.244773Z","datacenter_id":"f6bbf00e-667b-476a-9d74-37564594556b","id":"73d1e806-1e3d-429a-a783-332aa0a170bc","updated":"2019-10-31T19:33:09.244773Z","vendor_name":"kfdupdZixLXOyIgZMWyrCYWaE"},{"alias":"CkMgVFUEdBTnelqGZgOhwJCpF","az":"oQardbGLJrOTVNulIzUwdNMxX","created":"2020-01-13T16:50:25.430798Z","datacenter_id":"5862c0a5-5888-4e9f-86a2-f25743e5e4d4","id":"7312ef2e-791f-406d-8855-81a7b64d02ab","updated":"2020-01-13T16:50:25.430798Z","vendor_name":"IGoLzggZXAyHOtZOOUUvWZAkJ"},{"alias":"CkYlfYbeUbHdoisaxwShBjYpT","az":"oCxmHqWXQRunOEreBIwOhDbDa","created":"2019-10-31T03:36:50.950779Z","datacenter_id":"3ad418c3-11df-48a6-b0fb-bbd4c68f6125","id":"eba86ec1-13b1-4fe9-bada-532310b2b1a2","updated":"2019-10-31T03:36:50.950779Z","vendor_name":"GbKeZJzCFqngfLomxvJZXepyY"},{"alias":"CnAYgTXjTcNFQGpGpOejdCfsN","az":"KMqPYXRylyHglyetqvRIoDImm","created":"2019-10-31T19:40:53.507341Z","datacenter_id":"c9d6f9cb-b57f-4b0a-9945-a70fea57b0ae","id":"075d585e-541f-4823-a422-d896961bcb51","updated":"2019-10-31T19:40:53.507341Z","vendor_name":"ZWjUgZkzEKuGFOFGgCkdsgrdS"},{"alias":"CwjeNxaIDmlyMitODDnQYKjQv","az":"ZpOvwnvEextCYuAOYGhbWChbw","created":"2019-10-28T14:53:19.142001Z","datacenter_id":"9689425a-520d-4bd0-95a1-961bb92a9067","id":"a0e71d08-71d3-4db1-b192-69f85c45cdad","updated":"2019-10-28T14:53:19.142001Z","vendor_name":"wbrdjPLvZtElvOaPUjXVVRWIC"},{"alias":"DXKtaOFHYRfqgAwXgmRXSIEsS","az":"MsTenoPpaSFGqAFjabTKDKdme","created":"2019-10-31T03:32:11.122164Z","datacenter_id":"46352f08-f912-4526-a779-afcd29260596","id":"e3b216be-9b6d-489a-9894-2504045cd1fd","updated":"2019-10-31T03:32:11.122164Z","vendor_name":"HHsBLPxzhZTMcjXkqMdexSDkD"},{"alias":"EHNioGKANfZCkHUMLwsEttGSt","az":"WjCmAlhmmeVMEHxlAWKjXaZzQ","created":"2019-10-28T20:45:05.897545Z","datacenter_id":"54733021-9a3e-4f1c-86ee-ad642a5aaeca","id":"87658185-9886-4d74-96e0-431b6fd53e76","updated":"2019-10-28T20:45:05.897545Z","vendor_name":"XXtbKuOvVYmEBghpJTiTeraMQ"},{"alias":"FbrXSZfNNExLdRRRkLhxcORSQ","az":"WePDMPICaWqSwKUXDjuQbOWDS","created":"2019-10-31T03:43:31.741222Z","datacenter_id":"4008bbad-6636-478f-a1b1-b1202429eb2e","id":"7550ed94-26b2-40dd-b331-53eefd2a92af","updated":"2019-10-31T03:43:31.741222Z","vendor_name":"IPlnWoyLoxMTnpMvrTZDZMavW"},{"alias":"GCSTjNTbQoMdRULQATeJtOcWz","az":"ZApOVtRjNfXbpIwZcxJMYPjXW","created":"2019-10-30T20:22:07.256399Z","datacenter_id":"29b23bc6-cca2-4029-b910-210dc68a3baa","id":"68c9d253-1ab4-42fb-8390-b29ae231a726","updated":"2019-10-30T20:22:07.256399Z","vendor_name":"biZcXngLsGSCxmjSVGKpUmuNo"},{"alias":"GXivORtMzPEbhujIaBMihzPXL","az":"ORUkRiLbTijpnmAJuPTnfPhSW","created":"2019-10-16T14:38:03.757846Z","datacenter_id":"d7650902-3351-4dd7-86bb-e10739351c7c","id":"5d96d71d-994a-442b-bdc9-ddb480a6537b","updated":"2019-10-16T14:38:03.757846Z","vendor_name":"vVjahkBoYjbtehpKJbVtAvoVD"},{"alias":"GwMXJyiXsTjCoeoYejbEXCeWx","az":"tXraqOpnBspRRVELHkogLBCgI","created":"2019-11-01T19:08:04.847296Z","datacenter_id":"efc6bcd5-4c56-417d-a390-cba128f80b18","id":"2753262a-1fd9-47db-b9df-ab9ecc5f7491","updated":"2019-11-01T19:08:04.847296Z","vendor_name":"QmkCxkDriCKaaHWDopcXWoBNw"},{"alias":"HFUnkRinDhXVitVSHNISasiHD","az":"pZHGFPJYZHdIPjFDsSJqodrjg","created":"2019-10-31T15:11:13.679667Z","datacenter_id":"4456c008-97ba-4f0a-977e-dc767a02e3c5","id":"e5ad0403-dce4-4d1c-ab54-d4d8b1bc4aca","updated":"2019-10-31T15:11:13.679667Z","vendor_name":"aTQBwwnimwOfUTKdulgWuJWTs"},{"alias":"HJdOqMSNSmJbVGctONMlycADR","az":"ibXFclolvSuyQImLZOdpDAimH","created":"2019-10-15T19:23:57.302241Z","datacenter_id":"361079f0-3568-4b16-ab8b-f908b8ead823","id":"cc703466-9f04-4491-8751-f68ec710e9cd","updated":"2019-10-15T19:23:57.302241Z","vendor_name":"KWmRLZfnLgTyJCQrFmwNOjoCi"},{"alias":"HPpFhkzIbAnwmsQTjXKvUwxsU","az":"NwBAZHRllsvRqHXSgLPDVkZrm","created":"2019-11-01T19:02:35.113505Z","datacenter_id":"a98a4657-6269-4608-9aa6-946f1f67d07b","id":"29c0aba0-30ed-4c54-a79a-a02afcc44b01","updated":"2019-11-01T19:02:35.113505Z","vendor_name":"tsBOBaAMlcDRINTaJaPZpGCTB"},{"alias":"HPrHYDvLGVarDgtCLngccCGQm","az":"HcooNokQjspvkukfzHXHgehxY","created":"2019-10-28T16:31:24.135626Z","datacenter_id":"23095256-a460-481c-b836-7f6bd4b83f33","id":"fb69f0fb-a702-4a3c-8962-ef0709840243","updated":"2019-10-28T16:31:24.135626Z","vendor_name":"ebYvqNpcxisMANShAZGfbGkHX"},{"alias":"HpCjQAjnCNZtrXAAnklQWhLtK","az":"pfvvfrfakkreqvUsavOTssmYM","created":"2019-10-31T03:19:31.308594Z","datacenter_id":"195b167e-258a-43a1-adfc-10b27d3b6ff3","id":"0765662c-c14c-4ce7-956b-96c08787a4fa","updated":"2019-10-31T03:19:31.308594Z","vendor_name":"ATInuYYNzTrGuTVqelwVgMXvH"},{"alias":"JONKGJdvcSDJgwXEFIjlDengE","az":"axvhYLtahFNEVMPugxHwZsGKB","created":"2020-01-13T22:27:48.014689Z","datacenter_id":"fe87bbf4-028f-44f1-ac7f-284df563b3d6","id":"9d4db47d-4e13-495f-9f93-8dbf3eaa5a0a","updated":"2020-01-13T22:27:48.014689Z","vendor_name":"hsgwhmuNmkERDUghZpFygBLNR"},{"alias":"JXkpPIhHuaphHtqOTjznhzLLb","az":"VaUbmKIpNGstrZYMxDEtFRoLb","created":"2020-01-13T17:19:07.651335Z","datacenter_id":"41f71b06-6242-4201-9760-3e53c35704c5","id":"e3c6873e-d6d9-480f-90b1-da1154a082e9","updated":"2020-01-13T17:19:07.651335Z","vendor_name":"uiZvjjWwqFBEmmbmUpbWryRKz"},{"alias":"JbJlSqeHugJgbznnaTlhMSjcm","az":"tbvlhOajEVLtznbXKjaJVkZkA","created":"2020-01-13T22:43:30.102715Z","datacenter_id":"47912904-d398-4151-9a3b-8431d33a0512","id":"14de1645-ce0b-4837-b367-33e3734177b5","updated":"2020-01-13T22:43:30.102715Z","vendor_name":"mYCVVPPipIYLNqhrACcxCmHDD"},{"alias":"JpbfeHNFwUVBFfWldMbQiCTVf","az":"KlSapaghyWreZucQpTblkurvU","created":"2019-10-16T15:18:34.995152Z","datacenter_id":"d87054e3-967c-4880-9c75-3cf3f97aa9c5","id":"e3ca7303-612d-4d3c-a252-406c47cc0385","updated":"2019-10-16T15:18:34.995152Z","vendor_name":"GwnJGZdOrbofKqwCpRdYCQdWG"},{"alias":"KBVoXJZdxAqEliFKaIjGXQZnt","az":"oUwZdUdmVCIVvHpLXGjWRSGKb","created":"2019-10-31T19:38:45.617941Z","datacenter_id":"81f00a54-9461-4571-877a-8c62fc7ac3f2","id":"8b156c6b-bb44-43c8-b972-3f99ff807606","updated":"2019-10-31T19:38:45.617941Z","vendor_name":"RtzzpQSupLsLYLapatZXsHDZf"},{"alias":"LBfoQDaYsTYAiFgyezWtoPTIw","az":"mcDCcfCWPUJPybWEhZhJZQdub","created":"2020-01-13T17:21:19.686237Z","datacenter_id":"3d14d83f-df07-4c57-8d37-b4df6445df33","id":"56a975a6-7475-4823-be72-bfdcc0e49441","updated":"2020-01-13T17:21:19.686237Z","vendor_name":"mzZNLCCBNXPkITcRaPtGuyPnC"},{"alias":"LPKFimozlpgUiaLsWXaVRCalw","az":"MALqTxJjSvpiJafCEBSERKTwd","created":"2019-10-15T16:37:57.096845Z","datacenter_id":"98ffd143-dfb4-4793-b50e-ebf8fe5527e4","id":"eec38be4-880c-4fc6-a5e5-d023cb27f089","updated":"2019-10-15T16:37:57.096845Z","vendor_name":"sPxHVLWtHoqvlaoLkvTQIlNxw"},{"alias":"LsQmcXSBGjsVeIMCmlQiapdSn","az":"tCuyoIRsfhRPRDtBlHVZGTuXM","created":"2019-10-31T03:30:53.003796Z","datacenter_id":"b8336463-a25f-4ee1-bee5-434c2bf2c893","id":"574f1c02-bc38-41df-ae35-559465007d7a","updated":"2019-10-31T03:30:53.003796Z","vendor_name":"kkDNOzWANoSImmlgZTRTzmBXp"},{"alias":"LskSCHcxUyQKjQISozwUwWiVv","az":"TQugcEjiyJzrZQrXjHQwiGuon","created":"2019-10-17T20:07:00.428695Z","datacenter_id":"237035d5-8477-446d-bc90-a81eb51c198b","id":"23b9bb1e-9cc2-483f-8dac-89f8bb6c268f","updated":"2019-10-17T20:07:00.428695Z","vendor_name":"QhWjawLDqXxyAWmJIUOMZGBMy"},{"alias":"LwJNAzknXKJWnJYDYWdkwOGet","az":"albNxepDcKDAcyZQIEtzgqVJL","created":"2019-10-31T03:32:37.607471Z","datacenter_id":"06015839-c137-415c-a004-8c4780c21584","id":"c56e4c7f-e531-4f39-8c77-b21fd190ec7f","updated":"2019-10-31T03:32:37.607471Z","vendor_name":"yECOrVXDkjYzkidXdwFIxGdhO"},{"alias":"LyJjNHLJrGFJEdWHbFzseONMt","az":"djwFSmkqRXLtKbBZePLBZJggI","created":"2020-01-13T23:16:24.112506Z","datacenter_id":"013604b3-4cea-4880-b224-4dc5fe46dd11","id":"9855c736-2479-4996-be7d-de9f8c470ceb","updated":"2020-01-13T23:16:24.112506Z","vendor_name":"vuHLjudtinAEfYrfoxBaDmOWf"},{"alias":"MAFlCElitlOwBoIZZQAADyiGp","az":"vnZxgZPqnXbGqrLgcxKVBWOaX","created":"2020-01-13T16:45:14.456335Z","datacenter_id":"9360bd08-8d08-4d17-960a-c2235571507b","id":"fd9a57d8-6d8f-4b62-b55b-88c0514b9cc7","updated":"2020-01-13T16:45:14.456335Z","vendor_name":"FzlAUeHButLIpZnUASFTqqqxa"},{"alias":"MopJmtzwqvHdiHdvvsONRFcZV","az":"XMYpTCXYgqeEWzfmYZrjmhEsy","created":"2019-10-31T14:28:50.081272Z","datacenter_id":"4cd6ac2f-8c7a-4fe8-bde0-e2032125b478","id":"bbd7c7a5-cfe3-4790-86f5-e95b835cc166","updated":"2019-10-31T14:28:50.081272Z","vendor_name":"mNYAQKpSJTykkLsEFtkaAGhfi"},{"alias":"MpecCexLMZaulGOiQaeDRMUYW","az":"cDFQiuNWRWBSNpIPizUKbZQbZ","created":"2019-10-31T16:37:06.356764Z","datacenter_id":"ad583845-8845-4d0e-beb6-2e561017e7ed","id":"8a27730b-c4e9-48d5-9f61-171e0292c015","updated":"2019-10-31T16:37:06.356764Z","vendor_name":"JJcKZZSnwCHrKtluyPesciQqA"},{"alias":"MpekCMBJzDucEpELtpHsTFCnL","az":"qdWWKSRPNwvNCcyygoNrhtPbv","created":"2019-10-31T03:26:11.476427Z","datacenter_id":"678e2e1d-e193-44cd-805d-fd3282054dd7","id":"ef56a5d1-8160-413f-9d39-b5039ae1a554","updated":"2019-10-31T03:26:11.476427Z","vendor_name":"JyvltzhvBdWEqOQpmNrItbJfh"},{"alias":"MycgOwssebFZkhpaUztoZpmKl","az":"UrRcRBzcTZjmoQhPFcJuANwGC","created":"2019-10-31T03:49:25.827607Z","datacenter_id":"6d5b7786-0acd-4d8f-95d7-c833820ef3a1","id":"8ba376d2-0add-46b2-807c-8cecf40ff8dc","updated":"2019-10-31T03:49:25.827607Z","vendor_name":"UviYTtQUlUQthFpqIhuelBBTW"},{"alias":"MytZdgVygrwPZwiEhSqtCJgkI","az":"UdNPWBiKESuZpEoKfkUhmgQev","created":"2019-10-31T19:00:12.758963Z","datacenter_id":"0a7401aa-bce3-4b61-8d53-c03c02fc3b10","id":"cdf400d4-8949-4ea2-884f-70e171eda867","updated":"2019-10-31T19:00:12.758963Z","vendor_name":"uspJYsClKEsOuRGBOdcfqqAXi"},{"alias":"NUMoaeQrWGsMkFXgJdhkFqkEe","az":"NOwPsUWeIGHTUdUKhSuYPXnUi","created":"2019-10-28T14:52:19.817936Z","datacenter_id":"a5c0c517-6747-4e97-8c99-8c7a5667435b","id":"d4c9d083-15db-467b-8c4a-dc602704a5d6","updated":"2019-10-28T14:52:19.817936Z","vendor_name":"FtHhzMIKNrNKUYPAZvWZJnEyW"},{"alias":"NWAoxVNDUEuHHSZbaaQgfrUvK","az":"ydiOuIMsUgStPpMmsEkVjwwkW","created":"2019-10-30T20:23:40.555037Z","datacenter_id":"f0cad974-835b-4c2d-8761-8b560cf92831","id":"9ae720b6-f9d4-46e3-951f-015ea61f7d64","updated":"2019-10-30T20:23:40.555037Z","vendor_name":"XOpzQrwFrYhGBPTPigUEFjHWY"},{"alias":"NhjaccPpkBHTXfaUkUToIPbjh","az":"VmQIKfbLMjBfqIUVBzHzygahT","created":"2019-10-28T20:07:14.757090Z","datacenter_id":"aacdd36e-16ea-4e56-8bb2-cd70cf42c893","id":"ca75f8f6-2783-4f48-aa6b-3754f3614608","updated":"2019-10-28T20:07:14.757090Z","vendor_name":"WagHBztFPzUQFPoOfjpeVhJjT"},{"alias":"OIAZuWdlcEFDTiGkwNIpEKCEF","az":"XySTDKBznjIHcLTEhyYOhmFVJ","created":"2019-11-01T20:27:50.891186Z","datacenter_id":"94c5f2a2-a91e-47aa-acb2-8dcbbacb4889","id":"aa8c644c-32b2-4c65-ad51-df9aaedc2d5e","updated":"2019-11-01T20:27:50.891186Z","vendor_name":"VCUNsJSlDJlcwOrsFrbNcUaOe"},{"alias":"OKAnDnJCkeapcioxLAVUTqccq","az":"iZhlhMYgbUJffuWTpwdprHPwR","created":"2019-10-21T16:19:18.118667Z","datacenter_id":"48140a9e-f76b-4d4f-ae39-49551d4ed5ee","id":"ad9030ae-7881-43f3-955e-4cd4d9c63ff5","updated":"2019-10-21T16:19:18.118667Z","vendor_name":"MKWoXmeMrIdqpfpgjoCwYlUQT"},{"alias":"OyWKawzwzjZGZQOpASJVTFORj","az":"sMOjiAsMzkmtDlrkweBCXJQFo","created":"2019-10-31T19:39:45.479455Z","datacenter_id":"49579549-ad23-4480-ae39-a578616f3147","id":"5b193aca-ff6b-4212-aae7-3ad0755db723","updated":"2019-10-31T19:39:45.479455Z","vendor_name":"gvMwJmybmoGtDRGCKsFllvJFb"},{"alias":"OyZTuUFxGXizjIMJxELXnGUCO","az":"jlrDEnTEqZVFzxwKrvHesItaG","created":"2019-10-28T16:30:41.833774Z","datacenter_id":"811c087e-62ed-4a38-9cd8-10322f987759","id":"7d6e15d0-1725-4f46-88bc-4be80efca991","updated":"2019-10-28T16:30:41.833774Z","vendor_name":"nystonTCHTMgudDFTKJvZzJmZ"},{"alias":"PABDriqrKiqtlDctqxAPBtUwQ","az":"WKONaVoQzSFcKGwncBjwcSYOq","created":"2019-10-28T18:24:24.852111Z","datacenter_id":"143cfb5b-5028-4b0c-87cd-307fc9878233","id":"73a07a66-20ca-48bf-975d-52c9939388fa","updated":"2019-10-28T18:24:24.852111Z","vendor_name":"vMrVgRjoiAIyetlpHvoXxTVlK"},{"alias":"PFhscZavFTvXjusfkQskXTFwr","az":"mFOpHhQapBTuICnptsuTizwKK","created":"2020-01-13T17:27:42.916712Z","datacenter_id":"272e71dc-79ae-4c34-82fc-1c94227bcf11","id":"b2bb1c5e-cf18-4ecd-a2f1-08a4a12d3593","updated":"2020-01-13T17:27:42.916712Z","vendor_name":"ERiZvhEiodtdosbkyptIImyyP"},{"alias":"PXwtcVZkmnaghCZmzEAeLWBdW","az":"cQMFryxySpekBKwFmvnGZnLPg","created":"2020-01-13T21:08:24.251956Z","datacenter_id":"42ab22c3-b403-41bb-b0a6-7a31298a135b","id":"df46af95-ff34-495e-a1cf-411a711ea15f","updated":"2020-01-13T21:08:24.251956Z","vendor_name":"WqhZIavRlGagvIqheGaMBSygO"},{"alias":"PzoLgmPAsCuCFaeyKKVyAEfMz","az":"ZOuHOlAIgHualuoDndGuZLGkW","created":"2019-10-28T16:22:40.609549Z","datacenter_id":"a3908aad-237d-48c4-b63f-a87afa04d990","id":"3106c0f9-4f63-4a51-8fe7-99e947ef7ecd","updated":"2019-10-28T16:22:40.609549Z","vendor_name":"nbPKRKqMGdoIbgcfKEyltxmHI"},{"alias":"QQYLCXUDuifEhkSDvpyOLukGR","az":"BRYZoMxdzdRTjrXzXDKTKKFeM","created":"2020-01-13T22:28:26.065673Z","datacenter_id":"f6cf781c-c800-4b40-842d-fd7addc0424c","id":"89b11d22-e5e7-4f78-ba30-4d15f2cf1e9e","updated":"2020-01-13T22:28:26.065673Z","vendor_name":"lDdJegkeAxUclCbiqbeUqsFgE"},{"alias":"QbyxPNHsUmjQkpgTMFmGRyYOO","az":"gHdPupRZJvFZMdpsiXgFbAGKs","created":"2019-10-28T14:54:40.554953Z","datacenter_id":"fe0eb14b-ed2a-4377-b492-49dd6d0abbe2","id":"077ba8f0-8a2c-4f49-b830-d8b64c6d51b5","updated":"2019-10-28T14:54:40.554953Z","vendor_name":"EAmKgTSdlfqhtehyCEvgRyBNC"},{"alias":"QpEBgvPNdoTJeIPrHWUwEFAIR","az":"xMvTtlzOLyXmZLNIVbabtwdWC","created":"2019-10-28T20:20:53.702484Z","datacenter_id":"5e77c072-4028-44f1-b669-f3ab1b63c1b9","id":"ee450752-ae06-4937-b148-5509331d2fca","updated":"2019-10-28T20:20:53.702484Z","vendor_name":"IYnCHwTzlrGxTDuEQfRfHqFyr"},{"alias":"QuNRUYnFdBfracLYqCmkwrDcR","az":"wQvscdLuFLYjCdyRxDKfDvPuj","created":"2019-10-31T03:43:16.938600Z","datacenter_id":"dff66693-6abb-4668-a2de-8b433721b83a","id":"a15d6b75-f3f2-40f8-8188-27ef735d6925","updated":"2019-10-31T03:43:16.938600Z","vendor_name":"RrrtjPWXEPrUHqxtjVWJpHvCV"},{"alias":"RDNRfcGUgkvlkCaBYRrQbmDmm","az":"pPPnSVIYApKYwZBELbzgdKFIh","created":"2020-01-13T22:31:14.245377Z","datacenter_id":"bbddb004-11c2-40d6-8c8f-6ab191216140","id":"9952c52e-b743-4ca2-a7ac-8d0c7a67ced4","updated":"2020-01-13T22:31:14.245377Z","vendor_name":"AcqUpzbWCPoRwtsNkLFKnTQct"},{"alias":"RckefVgObASbdFVWzENvmyGlI","az":"CvMPVAfnuQdUGKnBjDhtUgVkm","created":"2019-11-01T19:00:06.060826Z","datacenter_id":"886ed1f2-cb8b-42f3-b3d1-4b58f8e1994f","id":"d504c4da-2e97-49d9-8102-7d1418e0791c","updated":"2019-11-01T19:00:06.060826Z","vendor_name":"zqrcIBCOJnTrsuSlAlVGytOPj"},{"alias":"RgPRbgjQqQgMuQQkiLAzFBRmT","az":"pzmSdFZttJPiZjnmgGrNBxJQw","created":"2020-01-13T22:37:54.469892Z","datacenter_id":"f33fc43f-6571-4c58-840e-7b07c97659e2","id":"782c5d45-e761-4a68-9098-6c3ecff9538c","updated":"2020-01-13T22:37:54.469892Z","vendor_name":"LBXrrfwyiGnlpoOmSYJJdlABg"},{"alias":"SWlHMPApULirxhoFoYKMkgqgE","az":"HLpBimHytjXkiSOPtbjqicHYb","created":"2020-01-11T05:48:31.617478Z","datacenter_id":"cfc0ca58-8a1f-4b98-8db6-c1e1588ab7c5","id":"f0dc7723-c3d3-40cb-a48f-93e7cc9b4b5c","updated":"2020-01-11T05:48:31.617478Z","vendor_name":"PbpKSySNKHSrcsWqxdeYnEbVG"},{"alias":"SeNcytjoEbdxbsGMLWJOaiYBq","az":"xPqVCTTwipgDaEdSrzJGrKbEL","created":"2019-10-31T14:20:15.752149Z","datacenter_id":"ade89ff5-a037-4295-baa7-b1a3902251c1","id":"131a859b-7367-4506-af3c-7474840e30ac","updated":"2019-10-31T14:20:15.752149Z","vendor_name":"sCiirdEtheEOZmRPplDcJfEWF"},{"alias":"StFZuKzLyfwZYazdemAooLZsG","az":"DcNPekLpSLQhhioMRVkhnmbUd","created":"2019-10-28T19:35:00.351510Z","datacenter_id":"6c51c1aa-fa05-45da-86c7-65c5346775b2","id":"e174ae7f-722c-4046-bdee-c3cda61d53fb","updated":"2019-10-28T19:35:00.351510Z","vendor_name":"puXdINTwHIywStbmDemhaGtaS"},{"alias":"SvbgNypdUJStoJEfqvRrUuHQt","az":"yNTACEBZBKZdRharBmISFqehr","created":"2019-10-24T18:31:45.263237Z","datacenter_id":"bb241ed2-50c6-493a-9339-f7ca879f3774","id":"2f675ee2-7913-4b86-9127-fb22c633f53d","updated":"2019-10-24T18:31:45.263237Z","vendor_name":"EcKHsUGuFexOnwfaBbiCWPMRi"},{"alias":"TBRtdXsnofOfQMNDJPgkCjaAN","az":"wNVCPZtHtBJPFsyfMaoAnVfOu","created":"2019-10-15T19:41:04.741348Z","datacenter_id":"ceb364c5-7b7e-4a59-9ae6-7921a5329cbf","id":"e51b95b9-c4d0-41af-8a4d-185ff0f27a64","updated":"2019-10-15T19:41:04.741348Z","vendor_name":"wAqsNlYeWNPYwgiNSFIlKfweL"},{"alias":"TMcHvSMHQxQMkTpwJwTxMEAnp","az":"SVTDYJwbbUtBCPkupPYvCQQMc","created":"2019-10-28T14:45:10.599520Z","datacenter_id":"86c74065-5a7e-455c-beec-800b4e5df1ab","id":"b2e32be9-0d4f-4afe-be9b-ea95225c3d69","updated":"2019-10-28T14:45:10.599520Z","vendor_name":"ELecDAWOxolTJNjDenXEKhbsu"},{"alias":"TestRoom","az":"IbmqtUaWbRqljCPJMTsCmOEFu","created":"2019-11-01T18:03:13.000500Z","datacenter_id":"383a1e41-b923-4ab6-b1b6-1dc15afc5f52","id":"4693b9c9-1ce0-44f7-b48b-19f612629353","updated":"2019-11-01T18:03:13.000500Z","vendor_name":"iYAZxzCDVELFHzQpsSZXmmlSg"},{"alias":"ToAKhMHoJrkCxhtAlPrAvnCFQ","az":"xVvHsczGqtOEccDkTXkbQLBnR","created":"2019-10-28T19:33:41.810412Z","datacenter_id":"323d96de-cd31-4a24-b6cd-c4910cd7381d","id":"979419c1-4237-4112-a4f7-dd97eddeef6a","updated":"2019-10-28T19:33:41.810412Z","vendor_name":"narnSJADoiihonHexONFimGWZ"},{"alias":"UNisthVUPaXyLgRwYfqOtzXaf","az":"UFvYHHWdVLqtNXPdTqNfNPGaK","created":"2019-10-28T20:46:44.568461Z","datacenter_id":"fc9d5a7a-a64e-4ff3-a9dc-f27b945c488d","id":"743be939-6ef6-4a0f-a880-62488a1e9afd","updated":"2019-10-28T20:46:44.568461Z","vendor_name":"eRyCAxrsrzCqntlUcvFQTgzRX"},{"alias":"UNxdsjQcbHSbvrlVIXHkSsufz","az":"wfXffQRPfMrDdMRQHMbftSaJF","created":"2019-10-30T20:21:29.913338Z","datacenter_id":"f7a17500-ca1e-416c-a5dc-649374850ecf","id":"9d67d86b-25e0-4275-91ac-1792f7c45662","updated":"2019-10-30T20:21:29.913338Z","vendor_name":"bVQfixvdOYOhdwnWfUOmlkbjQ"},{"alias":"URTHspJuRicsTmpBpHNSnCNPt","az":"TWtttbFIfiKEBhZARWYzRSdik","created":"2020-01-13T23:11:52.365754Z","datacenter_id":"dbec06e0-2df0-4c00-b148-dc9340b986b0","id":"6e5b92b0-c044-40ca-861c-1a2ba730f5ca","updated":"2020-01-13T23:11:52.365754Z","vendor_name":"WSInmjZoyuSDfrDxEJTIouEqJ"},{"alias":"UidxZeYeiodNMJUvJvTGAkVFA","az":"seUADvxaAIOMRLKWUREYvmase","created":"2019-10-28T19:27:02.025257Z","datacenter_id":"e78356fe-7b79-41d8-86cb-6d2cac2ec31c","id":"b5b1ab9b-0c2e-4659-8508-1cd5479ab5eb","updated":"2019-10-28T19:27:02.025257Z","vendor_name":"EDekWNimjtsRamytgBXkFqZNm"},{"alias":"UpPFBbDUFzbMXLTZoqvsfSrKE","az":"fABoJtheXufoUjHQLVAtrhXLT","created":"2019-10-31T02:59:59.667573Z","datacenter_id":"c1d80d58-c955-4371-ae59-eb30b6e4bd74","id":"01755bf0-428c-4be9-951a-da0a246a369c","updated":"2019-10-31T02:59:59.667573Z","vendor_name":"llGSTRTppqMwEohgvyRDCGOVG"},{"alias":"UxeoAWJOtlCvogqCrJMqnuaEo","az":"OwhMEsrUffGnyhdmEJBLUffDh","created":"2019-10-31T14:06:30.975209Z","datacenter_id":"3c982d79-dcfb-42d4-ba56-c65ed7381375","id":"9285a076-edc6-4e36-8b57-b7036433d80a","updated":"2019-10-31T14:06:30.975209Z","vendor_name":"yPEInaPAuhrqaycAvMrUUOlgw"},{"alias":"VMVMqcjrHzFYpEYufPoamdYrw","az":"ZtfhmMXmtCdhqdptUMSjSYvrC","created":"2019-10-31T03:27:55.204260Z","datacenter_id":"bd1613cb-8a32-49df-a6bf-f49da41a25e6","id":"d2638efe-c244-48c8-8ced-0c22acaaa8a4","updated":"2019-10-31T03:27:55.204260Z","vendor_name":"rVEkQAUSRvtJpIzHhMcSMvjda"},{"alias":"VnIcKIaDvhBdvAtHOYzxKlVio","az":"xPZQMvTrWdzVLHzioTslUMbki","created":"2019-10-28T20:47:28.184252Z","datacenter_id":"2b42e8c0-5827-4191-a972-841869581b9c","id":"cb4c0d02-1c5d-4fe6-9621-0d5bb3b0a6e5","updated":"2019-10-28T20:47:28.184252Z","vendor_name":"EDtSBRAKLGXHXKiWWfvMNDgjY"},{"alias":"WLaKyivepZfRFlgukZRwxHnzS","az":"OdzZfyvvHzOwJZWLqfQdiEJMa","created":"2020-01-13T21:29:20.929215Z","datacenter_id":"6ca1b8c5-567c-47fb-a831-162450966d66","id":"f6099a85-aac7-408b-8e92-7fb41db1f243","updated":"2020-01-13T21:29:20.929215Z","vendor_name":"YDjShHmmCvSharaIezWslaHhl"},{"alias":"WPLqCIRcakJNPWfzsPFWovHWM","az":"SRwoAWYalxbbYxWJgcIhBpolL","created":"2019-10-28T19:28:16.472116Z","datacenter_id":"c8b82b98-9f4f-40d7-815c-76166e453dcf","id":"176d7f4e-d805-440a-8c78-46616da32e55","updated":"2019-10-28T19:28:16.472116Z","vendor_name":"VFimxFjWMJKTOovCBJbKnHSDD"},{"alias":"WWWuMNLJkopRGMyEJfwwkIAdi","az":"ycQiqRUrkgiuBlegiyenpGxtJ","created":"2020-01-13T17:25:12.126616Z","datacenter_id":"45c81470-b77b-47f3-9237-244e6023b343","id":"3e12c710-b772-4187-a842-841244c053a2","updated":"2020-01-13T17:25:12.126616Z","vendor_name":"tpHuhGambcPniWHLBSPIRjctd"},{"alias":"WbGsQIQNLBYQUxJalUjPxAVDx","az":"PgweGhaVhKmNjghPROkqNCwRf","created":"2019-10-17T17:37:09.577637Z","datacenter_id":"a48f646f-a4bd-4873-b28c-af107cda26f8","id":"424dc41f-4abf-4a0e-92e5-2fdb48169f03","updated":"2019-10-17T17:37:09.577637Z","vendor_name":"JXVNKwpdvVtKWyIgjNKxhMALq"},{"alias":"WfQWHGKOohMgclXMsTatHprhs","az":"giVppdxGRucUEOXZOdvPcuGnJ","created":"2019-10-30T18:16:47.379436Z","datacenter_id":"c822c946-fbc5-4a51-a954-5584630b322e","id":"8094576f-3612-40d3-a73f-14abef06acc7","updated":"2019-10-30T18:16:47.379436Z","vendor_name":"iBPHDJNPzJnqjmFbcpRwlGtAb"},{"alias":"WkBFzIuXKSEuDplCrEmLPmhzf","az":"pPsCAGlGwEDXsByNuUSDrcxhp","created":"2019-10-31T02:51:30.418834Z","datacenter_id":"db2239a3-b2c9-453e-b099-e8008c99c362","id":"df596780-6667-4de1-ac43-3f79c146307b","updated":"2019-10-31T02:51:30.418834Z","vendor_name":"ngXnYqFkEUgfehbpgAopGriZV"},{"alias":"XByiKwUnYJSXsuOLrOvHYlkKS","az":"YeyisIhdhiEBwxXCZtMMmCUEj","created":"2019-10-16T14:37:50.536997Z","datacenter_id":"1e3a55c2-2487-42be-89a4-a53c19620cdd","id":"84e30c5d-24ce-4943-9c62-97ad30b2aaa6","updated":"2019-10-16T14:37:50.536997Z","vendor_name":"luVtuTlTzoMjaPZaeYlzUOAjB"},{"alias":"XDEMUBMdSqOBfFQXLPhaIUTZO","az":"JHVVQNwouVcHrScDzHuBmqwAG","created":"2019-10-30T20:12:05.870222Z","datacenter_id":"0857cd2e-f3a0-43b5-8729-a7a0443b9304","id":"418fe984-3445-46d3-a104-ffd67ba7e574","updated":"2019-10-30T20:12:05.870222Z","vendor_name":"HWvKmKcmajLNnrTUDweLtgIYH"},{"alias":"XqwLCgrceksXOLWxhTbMJRruu","az":"kjtbAaHFHijctPmKILvaIveMr","created":"2019-10-28T19:26:28.299541Z","datacenter_id":"518f3cc7-ada4-4eab-bb54-cd3a2aabaa43","id":"12a07afd-a1ac-40ef-9d7e-a1eb982aa67a","updated":"2019-10-28T19:26:28.299541Z","vendor_name":"KDvZidDLPFcEQfQJObTpXqnFU"},{"alias":"XvJKVfUksOkihvHgyonxGrhKL","az":"DlMZJCqeUJvzpSWKBDskRARhc","created":"2020-01-13T17:24:40.618177Z","datacenter_id":"31003771-f038-47b6-88e7-67203cb3fba4","id":"86c78f89-818b-49c6-9594-8854eed6df5b","updated":"2020-01-13T17:24:40.618177Z","vendor_name":"uAWtFUqmsWbfHBLMjLbPGvenb"},{"alias":"YBiClsnLNGfpqVOdQwAQYvmvd","az":"lrlTmRyKvPSzxFaASbhoGKJaY","created":"2020-01-13T21:26:40.868262Z","datacenter_id":"df433403-d279-4619-8433-4c559d0325bb","id":"034b7018-f413-45c4-9056-46147171cc57","updated":"2020-01-13T21:26:40.868262Z","vendor_name":"woFMHLsixGELlAWDDZiauxrho"},{"alias":"YLSBwMuRQNvFRvcUuRNJnuaCk","az":"xpoICNjspeDfFTVHhFeTMazUH","created":"2019-10-30T20:13:46.828538Z","datacenter_id":"8e58a6c8-4e93-444a-97ad-ec4130708f1d","id":"c45e8ed9-b2da-4f54-9a3e-1c67f4243075","updated":"2019-10-30T20:13:46.828538Z","vendor_name":"CbptjnwohALlXLgiZkEXxHSgq"},{"alias":"YMTDTkaMoIXCFkQMBUkDRfuSg","az":"QIcyKPztwYkQwJpeRMXJfaqIY","created":"2019-10-31T19:03:24.513621Z","datacenter_id":"c1890040-6ce5-402e-ab7c-f7b1e7cd9850","id":"cdf06188-e27c-4fa7-bcb9-8baff7d7400a","updated":"2019-10-31T19:03:24.513621Z","vendor_name":"leCHeuVGAGESXpNEUdSqZPYkW"},{"alias":"YkSGRKvfLXCOrzMPYEclzNnUA","az":"KhCWGdGLOfEqCZlNRPlpNxgCV","created":"2020-01-13T22:20:05.497957Z","datacenter_id":"2e7bd285-629c-41a4-b6aa-6f478ccd8d97","id":"0e46e077-20ab-49cf-b38f-dd9c29ed6456","updated":"2020-01-13T22:20:05.497957Z","vendor_name":"wBJwfktOCYYmFZZLFflmzSdGr"},{"alias":"YvfkqfHOhMIqmrwsoFNDTEpMt","az":"nITFFQPpbTZzMEbRyiuaFqkVo","created":"2019-10-28T16:54:01.987434Z","datacenter_id":"aad28ee8-69d4-4507-8740-8e132cc20f55","id":"60d316f3-8644-4ae5-988e-c135e6ee31f4","updated":"2019-10-28T16:54:01.987434Z","vendor_name":"rrfIKoJjmUjpgWkQecoQFVTxT"},{"alias":"ZCTuvgeBlDuLcGMZpdCHckuxP","az":"iMILinCElVfJocCUymXkgmCSQ","created":"2019-10-31T03:33:45.344464Z","datacenter_id":"220a1163-656a-4674-987d-0b94487c4299","id":"51f90355-b1d1-433d-ab39-51a31c5d5c7a","updated":"2019-10-31T03:33:45.344464Z","vendor_name":"BppPgpPJcedjgOFRteEngvoaZ"},{"alias":"ZFSzlVaBzjdvxzLfkYputktNz","az":"pypCluAkhSCBtMlvwpdlnwQjZ","created":"2020-01-13T22:28:34.840450Z","datacenter_id":"bb4328b4-253f-4685-aa3a-f8f0169f3c5c","id":"c20de288-3def-45a8-8677-3e47fd4f2c3c","updated":"2020-01-13T22:28:34.840450Z","vendor_name":"DoetDrHEEzGznAefgbCpdTBFy"},{"alias":"ZVQLzaFRTTZPkYUHrvxDmzZcM","az":"bzWSqaTPxBGoqLoSAPzQZOFfS","created":"2020-01-13T16:49:07.175699Z","datacenter_id":"294826b2-3d8b-41ce-84bb-69295557d9b2","id":"b33d0c64-3cde-4b7d-b3ec-0253b80c54e5","updated":"2020-01-13T16:49:07.175699Z","vendor_name":"GdFTeWMPVEaNeDivhOaUkWAFl"},{"alias":"ZbWNYcJYHDMkFKwIcUykWRGeA","az":"ZZTTgzzvSqpfDdBlqRqYhMjdY","created":"2020-01-13T21:32:31.863713Z","datacenter_id":"d972266b-6b1b-468c-a760-7cb16db48722","id":"cbafc538-8804-46a4-9a13-2b7e6f29043c","updated":"2020-01-13T21:32:31.863713Z","vendor_name":"yCsZAreloJAXSvInSuvTDuQLa"},{"alias":"aEBOsBLLmkSbklykqegRCIXAr","az":"CXaafvpjtvfrtLfhDUOFmQHLk","created":"2019-10-28T20:32:53.796915Z","datacenter_id":"0ddf080a-416a-45c4-bdf1-edc796c985cf","id":"9f1842b7-b1f1-4c5c-abd4-3588fbac36a3","updated":"2019-10-28T20:32:53.796915Z","vendor_name":"UJBHGhdXIkXiliFhBayvvMjaQ"},{"alias":"aMxDNbcKATGXCxpKGJmDtxnpx","az":"MByVdbIbRhPizkFRZIKhjpmMP","created":"2020-01-13T22:08:41.147530Z","datacenter_id":"a5960645-d999-43ad-b82b-602e6a856210","id":"86c47bfe-7312-493c-bfdc-af4d440eab40","updated":"2020-01-13T22:08:41.147530Z","vendor_name":"nJJkUpVobmklvcOttYYHlFIhU"},{"alias":"aUKOeJqAOpYTZKfEuoWehggyo","az":"TgenrEEmPHcryikOecbUMIhRN","created":"2019-10-31T02:53:35.739492Z","datacenter_id":"cc94689e-276f-4717-904d-64abdbaa583c","id":"079b7ee5-39f8-4b3c-a560-c8ebdcea644b","updated":"2019-10-31T02:53:35.739492Z","vendor_name":"qetOwdMVixFfyhCCmlBrDtOou"},{"alias":"bEnnlqrMjfbmOayalGynlnxpN","az":"LMFnDKLEFxvPCuvvjJFlmJdhL","created":"2020-01-13T22:32:29.962771Z","datacenter_id":"483a493b-7973-4426-93a5-9ecbbdf3155e","id":"792bd12a-e2b9-4c75-98ff-80fdcad3f72b","updated":"2020-01-13T22:32:29.962771Z","vendor_name":"URPHRbiHkqNSpTFPlKBrhrVRr"},{"alias":"bErMoUzdoZhGHSIbduhgMOjGa","az":"rvSuKjVRKqEGNzorKcfWpnBhJ","created":"2020-01-13T21:30:44.279226Z","datacenter_id":"507e69e3-2f76-49ac-ab4f-cb23e80c2450","id":"2cd46b1f-5679-46c3-80f0-96264512ae07","updated":"2020-01-13T21:30:44.279226Z","vendor_name":"sArIuBhCiPISNQwSpVbGdKgfp"},{"alias":"bXmVxXqZMFewMKDXmXrVeNWte","az":"GtPOqoJeGWJGwnnBcvPUKsGnX","created":"2019-10-30T20:25:08.312532Z","datacenter_id":"e5a07436-5868-48c2-887b-e916d19732e4","id":"9e79169f-7ce0-4314-bb1e-6e0bf81a5649","updated":"2019-10-30T20:25:08.312532Z","vendor_name":"sdFqBOONqnYKBfgnCjXPIehqa"},{"alias":"bszLSeWUWttJLpxLlrqZADvct","az":"nodWMBiQDSazAYlMAxbgyELyf","created":"2019-10-17T15:19:03.987195Z","datacenter_id":"62b4115e-d137-4695-a959-a1090968ae57","id":"2bd41c34-0fa4-4121-a9cb-bef96b7bb6f7","updated":"2019-10-17T15:19:03.987195Z","vendor_name":"OriQfVCaQLQmQFgktmYUWoDTA"},{"alias":"byJxTXXBLoPFJCzjYOCCqEMmD","az":"FVxQjyZvSdcSoNkHpkAwcTVht","created":"2019-10-28T20:04:11.032126Z","datacenter_id":"91bd392a-6f0c-4e6e-b5e8-f5729b309b08","id":"f2d4db37-ac9a-4186-8ce1-04c80cbf06b4","updated":"2019-10-28T20:04:11.032126Z","vendor_name":"FMhrZiCmrUfTigNlyEjXRDVZp"},{"alias":"cPZUaBThReztwFMmSXTBArnwf","az":"aRmJOdGjKvrDnTMCruJtcntJd","created":"2019-10-31T14:14:15.141255Z","datacenter_id":"8a9fc809-3d0f-4eaf-bacc-be66ccfcdd2a","id":"ad2df49e-b75a-454a-afc0-64c5626b0e1c","updated":"2019-10-31T14:14:15.141255Z","vendor_name":"VyWztzDfvQQGZzRuctoWqjBCg"},{"alias":"clmPvhFFJfsMbQdrOarSAvAdJ","az":"xmWErWtzOkqwEQonpPftZMIwH","created":"2020-01-13T22:27:42.447192Z","datacenter_id":"8294c9bd-d1df-4cfb-babd-b20031f4073a","id":"d0941843-6b80-4446-a33e-9c59fc9aeff0","updated":"2020-01-13T22:27:42.447192Z","vendor_name":"xlyOKyZnFYQSwGuJClHFpXcaR"},{"alias":"crNjvbKAiGuhEtTbUvolPQYob","az":"CkRZulgehnAbZrJQOJtLcFIqk","created":"2019-10-31T03:14:15.716986Z","datacenter_id":"ea8fd82a-fdf7-4245-aea9-4e665c00d161","id":"5a1d0688-7739-4525-9b9b-88b522e6f027","updated":"2019-10-31T03:14:15.716986Z","vendor_name":"CRxvHXArzpShfwenAPsSPddNl"},{"alias":"cugGHJhpBCjjNVhNzcoYoguiQ","az":"pZdINNQvDDmIBObstEojatRls","created":"2020-01-13T21:27:55.339478Z","datacenter_id":"2fa781fb-84e6-4d5b-a786-ca8bdc27284c","id":"230708b7-737b-4c51-819b-121ea900b50e","updated":"2020-01-13T21:27:55.339478Z","vendor_name":"LAJZEcCduKeqzDHKKroXirzHg"},{"alias":"dUtmEJRZhVwjvazRTSfRAxEmS","az":"DXOzZiKHPDICTagmwTsImDEZR","created":"2020-01-13T17:25:41.934816Z","datacenter_id":"23c598e6-98b7-4974-b68a-440390900eba","id":"e2192bf0-d855-4aad-b050-f86e6ac488ae","updated":"2020-01-13T17:25:41.934816Z","vendor_name":"IholosoTYGBmaKjBrQydQcmsK"},{"alias":"dVscmVbOuhdVXcVFZZHflAgfn","az":"xxJWYtrfjCZhENUUiYnhFsQOV","created":"2020-01-13T21:06:46.002826Z","datacenter_id":"a9e7cd35-4548-475d-9fda-575481d83c33","id":"42cf2c86-a0cb-40e9-a1c4-bf233dff986a","updated":"2020-01-13T21:06:46.002826Z","vendor_name":"kwvztxaCgmJvBDDaltokyLCUK"},{"alias":"dzbpMflXICPOjtkdxgshKcPbw","az":"SRxSuOXkFrNDSUFRuDRvWscsj","created":"2019-10-31T15:11:28.885669Z","datacenter_id":"2db20edb-8728-4c34-a88d-0529fd760f32","id":"cd11ecb8-5e32-4f34-9857-d897b680da50","updated":"2019-10-31T15:11:28.885669Z","vendor_name":"rmmQoKPfGFioquVZmHloimrxN"},{"alias":"eJPoZacKeQlkVZlHeHUYzEpCJ","az":"CPdqKGhTzFhLdLYUwKmwKxGNS","created":"2019-10-30T20:23:11.629257Z","datacenter_id":"54800858-4fbf-4f09-8421-824260a49be3","id":"5e5a6b10-d737-4b03-960e-c5c833190c81","updated":"2019-10-30T20:23:11.629257Z","vendor_name":"woDPWLOciNCnVEehVVpYmJgqz"},{"alias":"epfGNJoqqIoOiCdfsRQAsbJJS","az":"AszgXUkrLSFfMNpWXmDtIpiAo","created":"2019-10-31T02:54:25.581825Z","datacenter_id":"5ec0eaa2-fb58-48de-931e-99216390572c","id":"bee93d0b-75bf-45b2-8565-3f55a6d868ea","updated":"2019-10-31T02:54:25.581825Z","vendor_name":"fMIIXDYPEeQjVvrYAxROYvfci"},{"alias":"exIjaUoKMYdWCyRWXDCrazldg","az":"gaDJCzzpDMkmnCuISFddEnZHq","created":"2019-10-17T21:06:22.510261Z","datacenter_id":"be0da3f1-0156-43a1-91a1-909585f55c4a","id":"e5943683-54c0-4fbd-976d-7245e50aaca7","updated":"2019-10-17T21:06:22.510261Z","vendor_name":"xbxPZkoqLWmJElYBCGgnGZINy"},{"alias":"fGeakgNaCimtejWgQWIkridji","az":"JdAFHBPRaKugYAoclIZrEOdKj","created":"2019-10-31T03:41:32.391569Z","datacenter_id":"5b072996-c518-4b70-ae3b-4fbe911f7585","id":"1cdaa005-ea4f-4d8b-8eeb-d66a98e9f67b","updated":"2019-10-31T03:41:32.391569Z","vendor_name":"CvdnpkCSXSMEZwuHWgAkJLzjT"},{"alias":"fgukBPBDByJBUbttDkqQoporA","az":"vyRxKoSmOWFikjqGvNHJRwxhK","created":"2019-10-28T20:07:30.269977Z","datacenter_id":"d4e1e71c-becf-461e-a081-380cf2019f4a","id":"7281647b-7976-4d5a-8366-891bf93aa39a","updated":"2019-10-28T20:07:30.269977Z","vendor_name":"FQlGYyTVHKZpwIxvlfmIJCTjY"},{"alias":"gIyRMgsBKDGOuIWAIiexHUnIt","az":"FzDrCCIyTJmdZXLggKZrUDCRw","created":"2019-10-28T20:44:39.366496Z","datacenter_id":"ef35d596-5c85-40b4-bbc3-5e897453236a","id":"5253dc1a-d69d-4df5-8a94-87d747d65fc3","updated":"2019-10-28T20:44:39.366496Z","vendor_name":"NIWChohqDojWmuGMoHDSRpDRr"},{"alias":"gMnzNsWkFdJyCBJQaGMreAbEu","az":"aHYyLRLtFoKjQLhRekhCtyqDH","created":"2019-10-31T03:34:16.431602Z","datacenter_id":"a61804fd-0067-46f1-9113-4f0665bfde70","id":"25227bfd-2298-4330-a928-81e5a12b1367","updated":"2019-10-31T03:34:16.431602Z","vendor_name":"VUhYIsZNRbFadgVBWrqawnTEf"},{"alias":"gQbKSpReUiUGsnSepfAuPSiaD","az":"QdnsvhsFzmsSDNtVnIGEvCNVZ","created":"2019-10-31T03:44:01.183859Z","datacenter_id":"f25336e4-de60-47cd-82f6-620f99a96a83","id":"9e1bd1d6-40b5-43b8-9d45-1cabec5a2442","updated":"2019-10-31T03:44:01.183859Z","vendor_name":"atxaDZrdIjFHESNcDxsXdoKeo"},{"alias":"gSukCjXHtTJXshXtYgaCjsoQT","az":"MqByvaXFjHKWUopHuaZRzqxGI","created":"2019-10-31T19:42:12.107249Z","datacenter_id":"8f1ffcb7-8c82-484d-949f-fd0f64e1490c","id":"6af52e0e-ba99-45ce-aa1e-74f0a7a73cc8","updated":"2019-10-31T19:42:12.107249Z","vendor_name":"cLVmRMXTOzlpAutXNeGtFCaCp"},{"alias":"gXhATpmdGjuqVHFlfxccqIJPl","az":"SSsqgKhNzCOpNdYorjTessscr","created":"2019-10-31T03:35:04.046831Z","datacenter_id":"a3847fbe-0062-42a5-b7b4-45b27a29bb0a","id":"0913a62f-37a5-4f1b-a0e2-5f4509cbd82f","updated":"2019-10-31T03:35:04.046831Z","vendor_name":"VYbijYOAcfGHTuvODyZBLFZzy"},{"alias":"glWseqHknfDUzpsgXhMpupCkH","az":"KExeEXobALFRgIBGASdEgMmdM","created":"2020-01-13T21:05:17.528005Z","datacenter_id":"391a63c9-79db-4805-8ffc-c480bad968cc","id":"adccc68a-9072-420e-9e21-fc3880dd6cb2","updated":"2020-01-13T21:05:17.528005Z","vendor_name":"IurPYHZHpHJxNrkxszdwKKvaw"},{"alias":"hIPSGHlBgvhZXvhADodiGhgsB","az":"gwhVkxqgUgYYpfwnPRSnWtFbF","created":"2019-10-31T15:18:15.553801Z","datacenter_id":"f5a5fc3e-9011-4425-a127-b42e70a4bf2b","id":"1273fbb9-db87-4797-abca-5c0a34609c3a","updated":"2019-10-31T15:18:15.553801Z","vendor_name":"DulsJmVzrLdIIYExyafIXqAqf"},{"alias":"hIaKxZkbIjJBlVVFjZWUFpLNO","az":"SDkoUZXtfaMlZJSNSLLhIJLee","created":"2019-10-31T02:51:10.528707Z","datacenter_id":"bdc48d4a-9daf-4302-a880-7657ef899182","id":"4a1efc5e-261d-4336-ad3f-16df7797d4df","updated":"2019-10-31T02:51:10.528707Z","vendor_name":"fiyktEmDfFciuORsgcwXxfdeA"},{"alias":"hRQtkEdfLpZTAWQldnfqcZjbK","az":"EduKddvcPsENTshQawUXCjRgV","created":"2020-01-13T21:28:21.099603Z","datacenter_id":"9f0ff7bd-8821-4f24-aac6-b1854a0cb82d","id":"814db396-1917-4b99-9509-0dd52b97cabc","updated":"2020-01-13T21:28:21.099603Z","vendor_name":"DnuXkWQQJBYufGUOGRgHcfflz"},{"alias":"hrBiqZtAhpoaoDgjkytTCqblo","az":"dNihSEHmFfdVzoluWtnPWzbfo","created":"2020-01-13T22:36:42.818335Z","datacenter_id":"0cba9936-0ed3-4fad-aed7-c7993ead130e","id":"845a052a-5bab-4a8c-aa62-848569949a2f","updated":"2020-01-13T22:36:42.818335Z","vendor_name":"YKYUeNHFvyQEOsVizvcjQIVBb"},{"alias":"iBqVMAwEMYtFUqpmmKqpHLZkv","az":"xaXPQGtOBaallAmwMLXQUammh","created":"2020-01-13T16:41:55.569340Z","datacenter_id":"4dd8899f-67b3-4784-b19e-e541b0d6f1fc","id":"0802f52b-92d3-4d15-bbff-be573e5b1da1","updated":"2020-01-13T16:41:55.569340Z","vendor_name":"ysjAllZegbYhFIzgXGxiWdXbC"},{"alias":"iHUMhDUyzwbaXzJuOPoRkStga","az":"crCQtvXDjPUuRiDKPNSolkYJu","created":"2019-10-31T19:42:18.364116Z","datacenter_id":"016e3f71-81b9-4273-8fa7-d642d979b2c7","id":"a653a5db-839d-46a8-9f6a-5774ab515de0","updated":"2019-10-31T19:42:18.364116Z","vendor_name":"xNuWUZMdxMUQUYlZEXvjaeSTG"},{"alias":"iMVyuAsFXxJVGEAEAgQDiAikW","az":"AwlERRNCyoCuFfSQpECChvDaH","created":"2019-10-30T20:09:47.867888Z","datacenter_id":"8d912aea-0f4f-4cd5-8d5e-af666d8e119f","id":"7cee9287-a794-4171-a3c4-fb64546d4eb5","updated":"2019-10-30T20:09:47.867888Z","vendor_name":"zGuWdTeeEFDEWVoNZCCgZUdMs"},{"alias":"iOCHAuGYYZYxurhXMKnTYnpus","az":"USekBrjJWuPfPDwjfFUiltTXA","created":"2019-10-17T16:23:45.796866Z","datacenter_id":"df17d213-f8a3-4ed2-aca2-061d5f3b123a","id":"806b10a8-dcd4-4db8-8ceb-5279cd5f0aa4","updated":"2019-10-17T16:23:45.796866Z","vendor_name":"SEOvtyPJwOTEkAVKWLcZTCSCH"},{"alias":"iQqclPzHPgSqkXapDYSrYsSsq","az":"rmlyCjLujqqMKpswBIegMvbry","created":"2019-10-31T03:42:52.567315Z","datacenter_id":"cf6e4687-5786-443b-bc70-ff8445fcc283","id":"6fea4ce0-e069-4636-b0f1-a0d431ab5a63","updated":"2019-10-31T03:42:52.567315Z","vendor_name":"rYzipPWPDYoHCjymAUbLPWbqZ"},{"alias":"iYpFGOPdMLwnBctZbpbxUjQda","az":"JfurJEcBmuunRNKzkusWFEAKU","created":"2019-10-30T18:15:28.901057Z","datacenter_id":"3e9a56ad-a4e0-4d42-b438-47d56ae66f2e","id":"bbe41450-287e-4ac3-8cd8-4efdda6fd7ef","updated":"2019-10-30T18:15:28.901057Z","vendor_name":"mLoZEwSkAyWvnGSguPOjPVScj"},{"alias":"igqXWDtEtlvksbbfZExwCudgq","az":"wXvVCedSkqzyqXqsUjUcHMMQO","created":"2019-10-17T15:18:15.084493Z","datacenter_id":"5942ed14-0028-47b0-9296-28b8b23e6416","id":"d5f4ed8b-58f2-4c7c-8844-c52d3885eedf","updated":"2019-10-17T15:18:15.084493Z","vendor_name":"MsjRsAqqrVUbprkrzvAwjthmg"},{"alias":"ijGUrooaiGHoiwqhQSMeUftXG","az":"PjpuuLjoxXzbgcZTvKTNnJgKG","created":"2020-01-13T22:45:21.171277Z","datacenter_id":"72956036-54a5-47f2-8db3-c4a17a2238e0","id":"dfd41956-2bf1-4728-898b-aa8c8040e602","updated":"2020-01-13T22:45:21.171277Z","vendor_name":"qlWEjQMTjFufyCawkuunrWXxJ"},{"alias":"ioLhYtEalGNRKflRhoImTBiQZ","az":"pJiELNMIoEirIfZHiYYEKergm","created":"2019-10-21T16:18:52.109240Z","datacenter_id":"fbb97020-9ea8-4aee-b0f6-afa3a684fa47","id":"d77b5f98-b467-401e-8f07-d998039d850d","updated":"2019-10-21T16:18:52.109240Z","vendor_name":"CKyzBkUzEcPwJqIOMkAjVKqwE"},{"alias":"jXyTaEkLwrsxHPVSvAptKYWvj","az":"TQIcOhMbqnmGnfTzlizzHqMkR","created":"2019-10-31T19:40:28.063780Z","datacenter_id":"069415c6-b4ff-4572-b6e7-e6d609f3c834","id":"1b51be89-372a-4f73-832d-7ac67857cea8","updated":"2019-10-31T19:40:28.063780Z","vendor_name":"XZWSMRTUmzkAVXhxMFZipQcoz"},{"alias":"jZBfuCuuSoEAtMNfgmslUbDvD","az":"OqHAbrPdkcKPBoakysiCtIQfB","created":"2019-10-31T03:24:43.217176Z","datacenter_id":"c78ce907-6ded-4293-bf73-a8756461f676","id":"c51a94b1-13f3-4e99-a3ea-67df09566938","updated":"2019-10-31T03:24:43.217176Z","vendor_name":"SFmIQPEDUoegSDeqevvtrIwGh"},{"alias":"jdqMdzhkYFCyIbqFlQSLQCCKX","az":"TszClQiRgAFRjjdvkZaZMGfUR","created":"2019-10-31T14:15:00.475750Z","datacenter_id":"56aa74c7-5ff7-4c5d-9ec9-1fb2f0495e03","id":"1b9f9c64-69a6-4c25-8c57-1f1f1328b1a8","updated":"2019-10-31T14:15:00.475750Z","vendor_name":"BtycqCXUqatDFdWGFhjQNDYsm"},{"alias":"jfYbLWBsXMDCcwmLebfuzflKB","az":"BHGerHjwDHPjgHzFJXtfvxdbs","created":"2019-10-28T15:38:59.159562Z","datacenter_id":"80c7e272-e96a-4a2c-85c9-c23db2971820","id":"2a9709e4-cafe-422e-a24a-f9c498607648","updated":"2019-10-28T15:38:59.159562Z","vendor_name":"hDdIPVlIgJsrBlEupIOTTGgkn"},{"alias":"jzvUBStJxIvzcbuSqeidudIWT","az":"dQeulPhucrIxpyOuoyyiCXWEA","created":"2019-10-31T14:24:08.847790Z","datacenter_id":"038fb0f4-8775-449a-b08a-89a9b8b0cf58","id":"4ea81566-87c2-43cf-a749-a58fa1bb328d","updated":"2019-10-31T14:24:08.847790Z","vendor_name":"hzPhmEgKVIBiXdyNKhHNoreRS"},{"alias":"kCbQDkiADwtaVCGDwsVSGnwXx","az":"DzFBrewiwtdSLDoisVNNbCnlA","created":"2019-10-15T19:41:25.764885Z","datacenter_id":"3ea9a214-8f76-4da5-a3d2-09044b690593","id":"528455db-7c49-4f6d-a062-5a5cdc23ec05","updated":"2019-10-15T19:41:25.764885Z","vendor_name":"VqJTEsZdzNBQHUCzJseiIwZRa"},{"alias":"kNZtqkOwksJFVOpxBRtJxrFfY","az":"ZXHnDIapUXBQJAGFjAGhyTfJT","created":"2019-10-31T19:33:43.006580Z","datacenter_id":"024d89b1-c09c-4b49-9e0b-358ee60ff366","id":"0b5c6ae5-95bb-40b9-a8f1-a2d4517f738d","updated":"2019-10-31T19:33:43.006580Z","vendor_name":"dSZdSPmOxdkrZhrBLEptcsYuH"},{"alias":"ktUZVAHszbFOivUeuolyVeRJn","az":"glLQTLRsOlqhpDsmNvzOSwEZy","created":"2020-01-13T22:36:56.902372Z","datacenter_id":"cb9aea27-2870-44e6-bdf1-95e329dfb197","id":"d7dd05a7-f552-44f1-9afb-00bb5e1ddb91","updated":"2020-01-13T22:36:56.902372Z","vendor_name":"bawISUCmAmuNOHBAdWRVFcPvQ"},{"alias":"kxYmZaxfbPYqPWebypPPfFoGQ","az":"tKwdLDgnkZgXlJnxKhZRkVzyY","created":"2019-10-31T14:24:31.296749Z","datacenter_id":"a6aa3b24-ccab-42b2-bb35-077757cfb44b","id":"0af79fc7-ec35-4a55-bc1b-8201f1be0b9e","updated":"2019-10-31T14:24:31.296749Z","vendor_name":"pzFflkenMbisXuMlROqcJaZTf"},{"alias":"lDxIkLwgkcZIynucugQSOdumy","az":"OucvQclEGAAandGhMJLXvLXgB","created":"2019-10-31T02:57:48.979556Z","datacenter_id":"302467ee-c7bd-4a2b-a92a-8688710695ec","id":"4702650e-a078-41c3-ad69-99abc247429a","updated":"2019-10-31T02:57:48.979556Z","vendor_name":"hOLGwreQVfZKHhKdcVNNydmGg"},{"alias":"lLEWOskJbFNpKuBBZmgrlcgIb","az":"cNgNLAiwvKTphQHErYjpfuUJf","created":"2019-10-31T19:00:25.072727Z","datacenter_id":"2ebd682b-892d-4867-b869-c978c927d942","id":"0ceb902b-b02c-4e83-812f-24691ba92f0d","updated":"2019-10-31T19:00:25.072727Z","vendor_name":"eBHWqpTCTynwCZXrTPklQnRrb"},{"alias":"mJkUHykRSdPDBSQRmUYzkjaQw","az":"USeHKHiGRLbSimzXlgWHatfxp","created":"2019-10-15T19:42:46.891152Z","datacenter_id":"f5af60b2-c45a-4714-b81b-e0baae39f24d","id":"8c899c77-738f-4a7f-a1fd-fced7a82d019","updated":"2019-10-15T19:42:46.891152Z","vendor_name":"GqNXWhpgsiiTPBrKVpSqYIvlU"},{"alias":"mfKOrxBvQQoKQlxLhDayjOKUO","az":"bvtxpNsGqMhDNksSlNBzUehqY","created":"2019-10-31T03:35:35.780958Z","datacenter_id":"46d617fd-2f0c-41fb-8b11-f83e7e963190","id":"8ae2f0c4-3cc6-4e95-a335-69f949b7b8e5","updated":"2019-10-31T03:35:35.780958Z","vendor_name":"PbliefXzhyeGmiRevNnUAogQF"},{"alias":"miVqcMBnbtdkwuKPXFFIxQuiM","az":"VoyGFZdLraTBojMjcChRdnFMc","created":"2020-01-13T16:27:42.087940Z","datacenter_id":"dc2422a9-893f-4bc0-b000-d3a5d91c237a","id":"b11f0e67-afc9-4e42-a9eb-5f98ddc1a3bc","updated":"2020-01-13T16:27:42.087940Z","vendor_name":"UBEwHyvHyVIyREgjEostaimfP"},{"alias":"mphuCFkkcPwaQfBBokIddDjKr","az":"ymoeUGbqxsbSogLdFurCijDNm","created":"2019-10-26T18:36:29.513704Z","datacenter_id":"2680e58b-4b51-46b9-9c9f-d0c49f31403e","id":"9deec2a7-9bd4-4492-a61f-587cc9ca31ad","updated":"2019-10-26T18:36:29.513704Z","vendor_name":"TKJWGirwpBgrNOJKNfDUTRDtS"},{"alias":"nFouRsTjVeXvaTCUurtKfxTxg","az":"ILgADLaCtpcRhOizkVyzbtINJ","created":"2020-01-13T17:23:05.657543Z","datacenter_id":"5c56e1b7-174d-4b97-97c5-8ab1cbab6390","id":"76801080-07f0-454d-8bd7-8e5421bce0cc","updated":"2020-01-13T17:23:05.657543Z","vendor_name":"LbKrUForNdvqjqPJzTyNZXufr"},{"alias":"njnPhVFwtPgejtIMeUZepwUEI","az":"oWckSQAzKFbXpmtimLvnuigqT","created":"2019-10-28T16:31:45.783239Z","datacenter_id":"b2323218-b496-4c94-868e-b45c9a1cf2f3","id":"d1e162c7-3484-4688-8cd6-e00acd064047","updated":"2019-10-28T16:31:45.783239Z","vendor_name":"nacvsnneYfXEpVaZKJNTcwzff"},{"alias":"oCMFCHAWBtjdYpYjLuWtFfDTq","az":"MjgyTJziVQdXQDeYbiJgjerYl","created":"2019-10-30T20:12:59.311961Z","datacenter_id":"d2e27073-4ae2-4c3a-87ce-14311c895a83","id":"a07fcde3-cf3b-4cd0-b3f8-f51ed9a1a08f","updated":"2019-10-30T20:12:59.311961Z","vendor_name":"JBjpxoEIulAoXQOwBipJvfADI"},{"alias":"ogFpenioVAIyFtdYKnbBwbtMH","az":"ZuOTwcTxZVydhiuVuAoIBoOva","created":"2019-10-28T15:43:08.236638Z","datacenter_id":"dc5bd5e0-ed2b-4389-a9e6-ca02afbca357","id":"6736c37d-5c81-4021-9705-8ab7391fe015","updated":"2019-10-28T15:43:08.236638Z","vendor_name":"VCGZWavYbyRLWPNBjtaPbdYYd"},{"alias":"ojDpJuOANVOpEeaaVtPrirRzc","az":"xoYAIwfYxIdaTRKlmIMvqMdCA","created":"2019-10-15T16:39:23.545160Z","datacenter_id":"3eeabbdb-998a-48fd-83c4-9d6553b23290","id":"b0259a3b-115f-4367-a684-d774d8b52857","updated":"2019-10-15T16:39:23.545160Z","vendor_name":"SLXuzEdzBzQKOasrTKheIJnOb"},{"alias":"pIVFuyHziXNyGgVIciMFTiMkN","az":"ZhHLROCGVcqImzUNxyTbroQHG","created":"2019-10-28T15:44:10.229850Z","datacenter_id":"0457bb04-64e7-44ab-a0d3-d745bcc719e9","id":"c6517417-019a-49f7-8fba-35701560dffa","updated":"2019-10-28T15:44:10.229850Z","vendor_name":"NaRnDfPJhUmhnmuLqPyQAihxb"},{"alias":"pKmJrqWdSfaMBLAiPGqeaOqNr","az":"WezvsNizbHfdSgPiqhTwVqHju","created":"2020-01-13T22:19:50.794443Z","datacenter_id":"2878c084-9607-499f-a0ef-7d6277bf4833","id":"800d159b-ba7a-425f-8641-acaee071215a","updated":"2020-01-13T22:19:50.794443Z","vendor_name":"lnCJEIRbMYQZVxfusHbcxIWDD"},{"alias":"pMVZaIUlinABmcjspQjftOlMo","az":"mmkZErAirsJkzhXXUaomyVAZn","created":"2020-01-13T22:29:04.139990Z","datacenter_id":"9060b1bd-c368-4247-ad96-248b75783f25","id":"af7e4f2e-a423-4bef-a941-6272ffcc0d2e","updated":"2020-01-13T22:29:04.139990Z","vendor_name":"szbHCqfddsBSgUjIpLFIkHUXR"},{"alias":"pUKsujLNRfsnwBSOdJWiFqhQU","az":"VWMHOnLDiqlsjyjWVHMPSfFqN","created":"2019-10-31T03:23:28.532313Z","datacenter_id":"4dd8e387-fd74-44e7-815e-e1ab81138d50","id":"ae738968-e295-4c37-b238-bf4a942daf01","updated":"2019-10-31T03:23:28.532313Z","vendor_name":"KfZcHLLakNIFQOSZTLkKtlbCc"},{"alias":"ptpbXpbLAUtrtNXBfxyhmHjJD","az":"zspljarBnccFDrgsmwiSEochG","created":"2020-01-13T22:30:05.932078Z","datacenter_id":"52e9f8e7-d8fc-4217-8d62-99055a06fbe0","id":"95921361-8cd2-460f-997a-a57d53ab7e2a","updated":"2020-01-13T22:30:05.932078Z","vendor_name":"NBOGdFHClzjlYdGNZjoCnkkdu"},{"alias":"pvLlwDSPbOppyprVngqqvcvCU","az":"ZvaTTahjlWKMSvbNHNSeYrLOk","created":"2019-10-31T02:52:41.980958Z","datacenter_id":"b5703b38-e251-4ded-a8ca-7c959244ef90","id":"aebc8ec0-9ac6-4eb1-a0e5-633e513d523c","updated":"2019-10-31T02:52:41.980958Z","vendor_name":"CBAUZUOqKlBdZuwkLogfoKHAB"},{"alias":"pxJuVaEwQMyCTgSfamFzvfAgf","az":"iQJrVwnhvDNiSvhEgfTWYPCYP","created":"2019-10-17T16:23:36.506259Z","datacenter_id":"a9cc15aa-7625-444b-bb14-62e5daff61e9","id":"16f5afac-e94d-4283-bb69-4bddc60d14fc","updated":"2019-10-17T16:23:36.506259Z","vendor_name":"wLfJYcAEGDYQqlpNaliQmfplp"},{"alias":"qKdWDJUineoWehAVYJmDoCFHq","az":"ZpYCUxMLwGZGEAqCkGyTKLBWu","created":"2019-10-15T19:42:54.948080Z","datacenter_id":"d8859539-13db-44e2-9e00-ee1fbffbc9eb","id":"8414d4dd-9f9e-45f4-ab05-5e155de71441","updated":"2019-10-15T19:42:54.948080Z","vendor_name":"AuEtdenkNwCCQBKsooeeyMLSR"},{"alias":"qPHOJwtKGJoxfZjFnSZNivxBB","az":"abMXtBbKqIAZTsTIyPoecCTMt","created":"2020-01-13T22:31:59.459448Z","datacenter_id":"9f53d480-5ba9-4a28-a29b-026cc3ffe780","id":"5c27827d-d677-4820-9fe9-f8d0c50a40fd","updated":"2020-01-13T22:31:59.459448Z","vendor_name":"AkAfBcTHazeNyxOpTbOhBbdnV"},{"alias":"qacKQWbSBZnjwPKeOZYebbCDb","az":"tVoEaBjyTveFwgCfjozwFsccK","created":"2019-10-31T02:55:25.331813Z","datacenter_id":"3016b422-4790-4416-b066-eaf9baab2b45","id":"eb16da0f-b50e-4dd2-aabe-c7a9cba1b6a2","updated":"2019-10-31T02:55:25.331813Z","vendor_name":"LsDGBXgSRmUqPLKBEpITvryOU"},{"alias":"qpuTJXLVomZvYblfOUCSewlpU","az":"XCrJvmiQFIHyAIPmZXOdAqCXr","created":"2019-10-30T18:17:15.557754Z","datacenter_id":"c087446f-6eb7-4a74-82d7-174d54dc2fa4","id":"16550d2a-cec4-4cf4-a160-87b9dc05b640","updated":"2019-10-30T18:17:15.557754Z","vendor_name":"UgsdYYTrcnLIdXwljgTpGgocK"},{"alias":"rVvodbENIupojmTzQMsszfDyF","az":"jhfalnfMSLAKphDlPDvlVZzQX","created":"2019-10-15T19:23:38.962129Z","datacenter_id":"acff059d-b38d-40df-a062-2d5eea18c496","id":"b165cffb-c580-46c0-ba24-a4928b938bb8","updated":"2019-10-15T19:23:38.962129Z","vendor_name":"XgQFibpBUvdqGUMjLQplVykZV"},{"alias":"rYDAnEuGMQyrifLlMeQnghWPZ","az":"tuhEhcatLBGsHyaEdeEdntuPy","created":"2020-01-13T21:26:06.327749Z","datacenter_id":"cfcd368d-f153-4e3c-9273-279279371998","id":"02ddb180-6b72-4d1f-bd72-8fa92f5d6cb1","updated":"2020-01-13T21:26:06.327749Z","vendor_name":"halWBzowJpAwmrZKMHxOtkyyt"},{"alias":"rcmutgcmlTCoqTebAwUSzknbJ","az":"veusVFOxtFNCHOIwYnMLYjHru","created":"2019-10-17T21:05:27.565306Z","datacenter_id":"76238328-3f68-4315-82d6-816ec07aaafe","id":"f1736554-a4f1-4b1d-b99e-7a04cd75a2b5","updated":"2019-10-17T21:05:27.565306Z","vendor_name":"RyoxOpIsLvjMkSjKTcKKgilNu"},{"alias":"rhrrUbptpXhmAjZkEqvYQvYZi","az":"ZlIsOIdxZToPBJvhTustynjwb","created":"2020-01-13T23:10:58.955620Z","datacenter_id":"01740fca-cfc5-499e-bec0-09b61d5dbde9","id":"c44f834a-3141-4c51-8e52-a2194f278351","updated":"2020-01-13T23:10:58.955620Z","vendor_name":"UMxRqwwDytulryDiCqhUfFxpn"},{"alias":"snxtuATldjVrkyccVcFqYeZbH","az":"BoQAfvZdQqtkIUzzVZzHSYshN","created":"2020-01-13T17:22:04.816874Z","datacenter_id":"b074ee84-e98d-445f-8b15-494d760bf113","id":"41de2295-2b76-4d4c-936b-132c86ceeb7c","updated":"2020-01-13T17:22:04.816874Z","vendor_name":"ERjNfqmRFUubTSrjzeYNqtHKg"},{"alias":"szOpUSYMecHRYDprOiCCQZlIV","az":"tvaLhNMlFtWcQHtTmsrRAVZVc","created":"2020-01-13T22:27:29.532955Z","datacenter_id":"82a52efc-419c-4a6f-aaf4-2f67db6e0072","id":"4d97052d-9627-4938-93ff-f9b9df1c6217","updated":"2020-01-13T22:27:29.532955Z","vendor_name":"pnkhVDqnbsJaZGMXnntJtSUXH"},{"alias":"taxgVdZoICJooHzmkaVmjFBSV","az":"jtkGZcvXswEyyADmTkPOcHMjy","created":"2019-10-28T16:22:12.953676Z","datacenter_id":"2e9afd4a-f336-494f-8df6-5bb768bec901","id":"5898094c-e6ff-42de-ae8c-878c23b265d2","updated":"2019-10-28T16:22:12.953676Z","vendor_name":"OOdYOXPULtYintSYHfuSUwnAr"},{"alias":"tkzGViqMFFHantqjArkfQFBkN","az":"LAsbfugJfcGjXtaKbfehUaBgv","created":"2020-01-13T16:43:28.545633Z","datacenter_id":"ade745df-7a01-43a2-a503-64035f1416f8","id":"0a85acca-9dde-4ccb-9cb7-79eab3ff249a","updated":"2020-01-13T16:43:28.545633Z","vendor_name":"DrihFlxKzlWpqTJkHfcbWchfU"},{"alias":"tzQEjPtdzrQauukGvGtikXKIC","az":"OiEMMAcgdhzkpLvuoxIxdulqA","created":"2019-10-28T20:42:19.509580Z","datacenter_id":"1e648300-33ac-44aa-abc5-4e0a94426217","id":"3234badf-f266-4104-af94-e9138e021d59","updated":"2019-10-28T20:42:19.509580Z","vendor_name":"mouPeopRNAPBuqAtHSAwmxQKo"},{"alias":"uZnVIwKOflaedDxHDLzuMFkvv","az":"rnKXSMqpfKSEnSEDtRtuhpTQn","created":"2019-10-31T03:05:57.942826Z","datacenter_id":"1384355d-5cb8-4bdc-9ea7-772db8fc90dd","id":"4bf68d7e-6ecc-4a85-972d-331728b9b5ac","updated":"2019-10-31T03:05:57.942826Z","vendor_name":"povLXJevvDWBEFUrcUIVcUhtY"},{"alias":"ukgErWyJbgYxROXuRQXwLQvIt","az":"MEeLsMYHCUbXoaInDrDktnktB","created":"2019-10-18T14:45:34.338319Z","datacenter_id":"d99e06ab-e324-407f-9254-4d81ce89bc3d","id":"89b88223-61ea-4087-92ec-c899dc21f655","updated":"2019-10-18T14:45:34.338319Z","vendor_name":"WqBKFQpCbbJwTlyJqoefelryA"},{"alias":"wAnqwAKZwPwswnDNSsGzjVkyw","az":"BMxoTEgMJsvTLknpwhDplFhDZ","created":"2020-01-13T22:30:27.545505Z","datacenter_id":"ac601b3b-04ed-4009-aaa4-c19ad5ffce52","id":"62472c22-7966-4228-817f-a0677047c662","updated":"2020-01-13T22:30:27.545505Z","vendor_name":"fHQUJIJBtwyjvRoQAIaLncYtz"},{"alias":"wBfJeIWveMlvVKpCcxAjWUofx","az":"UyTWKWFWpoLsXTFEccbkRTOEQ","created":"2019-10-17T17:38:38.367855Z","datacenter_id":"c24093a0-da30-4bee-afb6-bba98ef60230","id":"cccb0008-201f-4e42-86d2-918857a10704","updated":"2019-10-17T17:38:38.367855Z","vendor_name":"tnwBWQgIWIqwZWMsnhuSZwbDf"},{"alias":"wDbbqTGpAAQujzZaLwjwYboma","az":"yoOSNGbwdBOYiTjPMQGaIFMoX","created":"2019-10-28T18:24:03.343079Z","datacenter_id":"ebf736c9-992a-4cd8-b058-7fb52854bf0d","id":"115877f1-d890-454f-b8bd-3de07dfe0b12","updated":"2019-10-28T18:24:03.343079Z","vendor_name":"rYTYXIPufkWFWFkOzAjizThIo"},{"alias":"wKnUGhcAahOdTfXTzqDhFsxRb","az":"kdsuzvnhLzGhkMuKDwGBJOoxg","created":"2019-10-28T14:49:54.749768Z","datacenter_id":"afaba904-226e-4436-8609-f4aefb496185","id":"05a18f07-0cc3-4a9f-b0b3-a3dae8c99e3f","updated":"2019-10-28T14:49:54.749768Z","vendor_name":"MYxQnhAWIhAmAPkVXfpKcyPic"},{"alias":"wUYWOybaOqDJbqOziOXRYPRFR","az":"qZKptLKLIEPMngrRhZTvFeIGA","created":"2019-10-28T15:43:30.048676Z","datacenter_id":"279025dd-fb09-4d70-9554-654a6783d458","id":"48ff37ba-0ae4-4bee-bf3f-475758738c38","updated":"2019-10-28T15:43:30.048676Z","vendor_name":"daRXzeuSpwhdcguOGhDWnNmmY"},{"alias":"wgNkZIgyhFpDsRSjGQoXjgfkJ","az":"tduSIqlqFgMnCXKYTXrcZKpDI","created":"2019-10-17T16:29:16.545207Z","datacenter_id":"9d32856d-0a10-4c20-86f3-f527b4330492","id":"9e645953-9656-4105-9174-36e684cd59e2","updated":"2019-10-17T16:29:16.545207Z","vendor_name":"NUhTYfuvliDRJeSwxSbaKXnvA"},{"alias":"wmIIyBxDyDKskrIGfHzYlQpUV","az":"dddvQZVtxPZGkUTUGODuTmSVG","created":"2019-10-30T20:16:35.476753Z","datacenter_id":"0c119af8-d6c1-45ab-8845-8da4bd8e052a","id":"08d785f7-87b5-4ab4-bf69-c6e2802b568c","updated":"2019-10-30T20:16:35.476753Z","vendor_name":"LpytPNnxvQcQTpPBSVWhlbMNw"},{"alias":"xIRNYVaRKjJZJuLxhAmUFBFDq","az":"YXfqMwkcHeOYuqhmUxWDOlYTZ","created":"2019-10-30T20:17:37.527776Z","datacenter_id":"611840bb-b0f4-40a1-95be-8753959f4fb4","id":"809c606d-1de8-4163-ba9a-dd23f0d0f148","updated":"2019-10-30T20:17:37.527776Z","vendor_name":"iMIjOVJArOJzfGaUPYeOwiMBe"},{"alias":"xiDDgfVIGeorMprbRwlZkpRoh","az":"TJMEwCNimsakrpUdfbaqkQCxP","created":"2019-10-31T03:40:42.513551Z","datacenter_id":"c1cea351-cbef-4759-ba7d-0292ced6bdce","id":"45e51fef-4d05-4193-891e-0699154273d0","updated":"2019-10-31T03:40:42.513551Z","vendor_name":"wXOYcDxQlRJJOezlyHKuXCURs"},{"alias":"yLuPJwgGZuUIuCbOXkqCbhbBw","az":"XyEykyYHQDUDNlhheXspNHzXP","created":"2019-10-21T16:15:14.341100Z","datacenter_id":"6b1e0c52-6681-451f-ac87-5e61affe9f00","id":"5bf01716-61e8-4e90-ab8a-00c85cc3b5be","updated":"2019-10-21T16:15:14.341100Z","vendor_name":"SwrEGzEBLqOdiHxVjByBWThxi"},{"alias":"yegxCZoYWCQibKQtVrgmBfElT","az":"FHggqlmYlgkzPPwFOSgHZVTPt","created":"2019-10-31T03:28:59.305060Z","datacenter_id":"a6962fdc-97d5-4d99-96d6-11e1435ec441","id":"fff76037-a0e1-423d-81b2-59470bd16db3","updated":"2019-10-31T03:28:59.305060Z","vendor_name":"dsOJrLHwHrCcjYiMysBfvAvYF"},{"alias":"yiiVtHwltbBsfLxduEJrRJgvQ","az":"xWqerXydyYZSKqOlpkviCMqye","created":"2019-10-31T03:29:44.586423Z","datacenter_id":"7df897b1-4952-4def-afc9-0d21d90e775e","id":"732f1e65-cf5c-4ad5-bb68-63ea43ce4376","updated":"2019-10-31T03:29:44.586423Z","vendor_name":"pJWPoEtBsZqSJWxHZFwRxBnye"},{"alias":"zBJEtywaGkKKtVyBJhuDWSzoF","az":"UTRSRnmvaaDZwaTmcCvbXzFFc","created":"2019-10-28T14:46:04.285883Z","datacenter_id":"32a14586-548e-4018-b65a-9e1278ea8644","id":"5e2a5945-8c16-46e5-8f19-4eae84015003","updated":"2019-10-28T14:46:04.285883Z","vendor_name":"bLdPTknicfMEXlvABgCzPnond"},{"alias":"zIrPVKbChSrJJbvrjLxGkrJqh","az":"GxMgGWNADNiaXIBLaVWYaqJnw","created":"2019-10-31T03:44:39.232091Z","datacenter_id":"c5728856-4cb3-49bc-b3ae-7b07ebda0027","id":"70718d02-38d9-499f-b976-bb9317cf0879","updated":"2019-10-31T03:44:39.232091Z","vendor_name":"RIKbQKyuprsFmgkIqLEQwVCFQ"},{"alias":"zcBlNuAVYLBfUCfAaXiwjCniK","az":"RXZimQthxelWTpPGSFDApIABA","created":"2019-10-31T14:21:43.591835Z","datacenter_id":"7bbf78aa-362f-4db7-ab8c-88b16594f1db","id":"36524bb8-25bf-4a80-be01-4a832cbca78e","updated":"2019-10-31T14:21:43.591835Z","vendor_name":"RkpsgWujOBCXHHWJfLaRpkRNt"},{"alias":"zhIckwxaKNFMVegqGyEufvHGV","az":"hCMhiquPkxDxVVXskCRPsUsJc","created":"2019-10-31T03:15:51.819597Z","datacenter_id":"39211320-03b3-4d69-9bc6-2fee6a12e20f","id":"0c94597b-d5ff-47f2-9370-d9255120b5e4","updated":"2019-10-31T03:15:51.819597Z","vendor_name":"DIIRNqeqkHSwxcRTFugLOmZPS"},{"alias":"zwCrVERuEiRkFKSyazsVhrgku","az":"AqbxXGtYkTTCbKIhgUDWleGWF","created":"2019-10-31T14:14:52.702107Z","datacenter_id":"812bd02f-42b2-49bb-aaa4-639fb659646d","id":"1d27a3f3-1d8a-4a16-9a03-07fd250b2f57","updated":"2019-10-31T14:14:52.702107Z","vendor_name":"kSYstLgybTewGWNElINZMuiXp"}]' - headers: - Cache-Control: - - no-cache - Content-Length: - - "55464" - Content-Type: - - application/json - Date: - - Mon, 13 Jan 2020 23:16:24 GMT - Request-Id: - - lEw83IAKgLAk - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - lEw83IAKgLAk - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/room/9855c736-2479-4996-be7d-de9f8c470ceb - method: GET - response: - body: '{"alias":"LyJjNHLJrGFJEdWHbFzseONMt","az":"djwFSmkqRXLtKbBZePLBZJggI","created":"2020-01-13T23:16:24.112506Z","datacenter_id":"013604b3-4cea-4880-b224-4dc5fe46dd11","id":"9855c736-2479-4996-be7d-de9f8c470ceb","updated":"2020-01-13T23:16:24.112506Z","vendor_name":"vuHLjudtinAEfYrfoxBaDmOWf"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "291" - Content-Type: - - application/json - Date: - - Mon, 13 Jan 2020 23:16:24 GMT - Location: - - /room/9855c736-2479-4996-be7d-de9f8c470ceb - Request-Id: - - 5kOfrEgQ23C0 - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - 5kOfrEgQ23C0 - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/room/9855c736-2479-4996-be7d-de9f8c470ceb/rack - method: GET - response: - body: '[]' - headers: - Cache-Control: - - no-cache - Content-Length: - - "2" - Content-Type: - - application/json - Date: - - Mon, 13 Jan 2020 23:16:24 GMT - Request-Id: - - sp68tuQv9bj1 - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - sp68tuQv9bj1 - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/room/9855c736-2479-4996-be7d-de9f8c470ceb - method: DELETE - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Mon, 13 Jan 2020 23:16:24 GMT - Request-Id: - - fDsPvxbg6aod - Server: - - Mojolicious (Perl) - X-Request-Id: - - fDsPvxbg6aod - status: 204 No Content - code: 204 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/dc/013604b3-4cea-4880-b224-4dc5fe46dd11 - method: DELETE - response: - body: "" - headers: - Cache-Control: - - no-cache - Date: - - Mon, 13 Jan 2020 23:16:24 GMT - Request-Id: - - Prk6TQ85KNaE - Server: - - Mojolicious (Perl) - X-Request-Id: - - Prk6TQ85KNaE - status: 204 No Content - code: 204 - duration: "" diff --git a/fixtures/conch-v3/user.yaml b/fixtures/conch-v3/user.yaml deleted file mode 100644 index a3fb937..0000000 --- a/fixtures/conch-v3/user.yaml +++ /dev/null @@ -1,64 +0,0 @@ ---- -version: 1 -interactions: -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/user/me - method: GET - response: - body: '{"builds":[],"created":"2019-10-09T17:21:43.700385Z","email":"conch@exmaple.com","force_password_change":false,"id":"c3d2c2e6-e3c0-48ca-a8b0-f97e842888f5","is_admin":true,"last_login":null,"last_seen":"2020-01-13T23:16:24.355614Z","name":"conch","organizations":[],"refuse_session_auth":false,"workspaces":[{"description":"Global - workspace","id":"8bb8f3a8-0f1e-4f1d-92a9-3d108f5743c8","name":"GLOBAL","parent_workspace_id":null,"role":"admin"}]}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "445" - Content-Type: - - application/json - Date: - - Mon, 13 Jan 2020 23:16:24 GMT - Request-Id: - - ERrTZvln1emQ - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - ERrTZvln1emQ - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/user/me/settings - method: GET - response: - body: '{}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "2" - Content-Type: - - application/json - Date: - - Mon, 13 Jan 2020 23:16:24 GMT - Request-Id: - - WdbvPjzxwOqj - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Request-Id: - - WdbvPjzxwOqj - status: 200 OK - code: 200 - duration: "" diff --git a/fixtures/conch-v3/workspace.yaml b/fixtures/conch-v3/workspace.yaml deleted file mode 100644 index 798a60c..0000000 --- a/fixtures/conch-v3/workspace.yaml +++ /dev/null @@ -1,101 +0,0 @@ ---- -version: 1 -interactions: -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/workspace - method: GET - response: - body: '[{"description":"Global workspace","id":"8bb8f3a8-0f1e-4f1d-92a9-3d108f5743c8","name":"GLOBAL","parent_workspace_id":null,"role":"admin"}]' - headers: - Cache-Control: - - no-cache - Content-Length: - - "138" - Content-Type: - - application/json - Date: - - Mon, 13 Jan 2020 23:16:24 GMT - Request-Id: - - 0ErQNS638ElE - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Deprecated: - - this endpoint is deprecated and will be removed in api v3.1 - X-Request-Id: - - 0ErQNS638ElE - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/workspace/GLOBAL - method: GET - response: - body: '{"description":"Global workspace","id":"8bb8f3a8-0f1e-4f1d-92a9-3d108f5743c8","name":"GLOBAL","parent_workspace_id":null,"role":"admin"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "136" - Content-Type: - - application/json - Date: - - Mon, 13 Jan 2020 23:16:24 GMT - Request-Id: - - Zd1SZOkQjgdK - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Deprecated: - - this endpoint is deprecated and will be removed in api v3.1 - - this endpoint is deprecated and will be removed in api v3.1 - X-Request-Id: - - Zd1SZOkQjgdK - status: 200 OK - code: 200 - duration: "" -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/workspace/GLOBAL - method: GET - response: - body: '{"description":"Global workspace","id":"8bb8f3a8-0f1e-4f1d-92a9-3d108f5743c8","name":"GLOBAL","parent_workspace_id":null,"role":"admin"}' - headers: - Cache-Control: - - no-cache - Content-Length: - - "136" - Content-Type: - - application/json - Date: - - Mon, 13 Jan 2020 23:16:24 GMT - Request-Id: - - Db+S7nWfY+Zx - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-b4-0-gb99fbfff - X-Deprecated: - - this endpoint is deprecated and will be removed in api v3.1 - - this endpoint is deprecated and will be removed in api v3.1 - X-Request-Id: - - Db+S7nWfY+Zx - status: 200 OK - code: 200 - duration: "" diff --git a/fixtures/json-schema/request/BuildAddOrganization.yaml b/fixtures/json-schema/request/BuildAddOrganization.yaml deleted file mode 100644 index f729a2e..0000000 --- a/fixtures/json-schema/request/BuildAddOrganization.yaml +++ /dev/null @@ -1,34 +0,0 @@ ---- -version: 1 -interactions: -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/schema/request/BuildAddOrganization - method: GET - response: - body: '{"$id":"urn:request.BuildAddOrganization.schema.json","$schema":"http:\/\/json-schema.org\/draft-07\/schema#","additionalProperties":false,"definitions":{"role":{"description":"corresponds - to role_enum in the database","enum":["ro","rw","admin"],"type":"string"},"uuid":{"pattern":"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$","type":"string"}},"properties":{"organization_id":{"$ref":"\/definitions\/uuid"},"role":{"$ref":"\/definitions\/role"}},"required":["organization_id","role"],"title":"BuildAddOrganization","type":"object"}' - headers: - Content-Length: - - "551" - Content-Type: - - application/json - Date: - - Fri, 01 Nov 2019 21:34:30 GMT - Last-Modified: - - Fri, 01 Nov 2019 20:19:12 GMT - Request-Id: - - PuH73qzIb28t - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-a8-0-gd0dc7f2e - X-Request-Id: - - PuH73qzIb28t - status: 200 OK - code: 200 - duration: "" diff --git a/fixtures/json-schema/request/BuildAddUser.yaml b/fixtures/json-schema/request/BuildAddUser.yaml deleted file mode 100644 index 9bf9701..0000000 --- a/fixtures/json-schema/request/BuildAddUser.yaml +++ /dev/null @@ -1,35 +0,0 @@ ---- -version: 1 -interactions: -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/schema/request/BuildAddUser - method: GET - response: - body: '{"$id":"urn:request.BuildAddUser.schema.json","$schema":"http:\/\/json-schema.org\/draft-07\/schema#","additionalProperties":false,"definitions":{"email_address":{"allOf":[{"format":"email","type":"string"},{"$ref":"\/definitions\/mojo_relaxed_placeholder"}]},"mojo_relaxed_placeholder":{"description":"see - https:\/\/metacpan.org\/pod\/Mojolicious::Guides::Routing#Relaxed-placeholders","pattern":"^[^\/]+$","type":"string"},"role":{"description":"corresponds - to role_enum in the database","enum":["ro","rw","admin"],"type":"string"},"uuid":{"pattern":"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$","type":"string"}},"oneOf":[{"required":["user_id"]},{"required":["email"]}],"properties":{"email":{"$ref":"\/definitions\/email_address"},"role":{"$ref":"\/definitions\/role"},"user_id":{"$ref":"\/definitions\/uuid"}},"required":["role"],"title":"BuildAddUser","type":"object"}' - headers: - Content-Length: - - "894" - Content-Type: - - application/json - Date: - - Fri, 01 Nov 2019 21:34:30 GMT - Last-Modified: - - Fri, 01 Nov 2019 20:19:12 GMT - Request-Id: - - gaxpytAmMHEA - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-a8-0-gd0dc7f2e - X-Request-Id: - - gaxpytAmMHEA - status: 200 OK - code: 200 - duration: "" diff --git a/fixtures/json-schema/request/BuildCreate.yaml b/fixtures/json-schema/request/BuildCreate.yaml deleted file mode 100644 index a4cda95..0000000 --- a/fixtures/json-schema/request/BuildCreate.yaml +++ /dev/null @@ -1,35 +0,0 @@ ---- -version: 1 -interactions: -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/schema/request/BuildCreate - method: GET - response: - body: '{"$id":"urn:request.BuildCreate.schema.json","$schema":"http:\/\/json-schema.org\/draft-07\/schema#","additionalProperties":false,"definitions":{"email_address":{"allOf":[{"format":"email","type":"string"},{"$ref":"\/definitions\/mojo_relaxed_placeholder"}]},"mojo_relaxed_placeholder":{"description":"see - https:\/\/metacpan.org\/pod\/Mojolicious::Guides::Routing#Relaxed-placeholders","pattern":"^[^\/]+$","type":"string"},"mojo_standard_placeholder":{"description":"see - https:\/\/metacpan.org\/pod\/Mojolicious::Guides::Routing#Standard-placeholders","pattern":"^[^\/.]+$","type":"string"},"uuid":{"pattern":"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$","type":"string"}},"properties":{"admins":{"items":{"additionalProperties":false,"oneOf":[{"required":["user_id"]},{"required":["email"]}],"properties":{"email":{"$ref":"\/definitions\/email_address"},"user_id":{"$ref":"\/definitions\/uuid"}},"type":"object"},"minItems":1,"type":"array","uniqueItems":true},"description":{"type":"string"},"name":{"$ref":"\/definitions\/mojo_standard_placeholder"}},"required":["name","admins"],"title":"BuildCreate","type":"object"}' - headers: - Content-Length: - - "1141" - Content-Type: - - application/json - Date: - - Fri, 01 Nov 2019 21:34:30 GMT - Last-Modified: - - Fri, 01 Nov 2019 20:19:12 GMT - Request-Id: - - fIWA5RekJuec - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-a8-0-gd0dc7f2e - X-Request-Id: - - fIWA5RekJuec - status: 200 OK - code: 200 - duration: "" diff --git a/fixtures/json-schema/request/BuildCreateDevice.yaml b/fixtures/json-schema/request/BuildCreateDevice.yaml deleted file mode 100644 index 7b9a583..0000000 --- a/fixtures/json-schema/request/BuildCreateDevice.yaml +++ /dev/null @@ -1,34 +0,0 @@ ---- -version: 1 -interactions: -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/schema/request/BuildCreateDevice - method: GET - response: - body: '{"$id":"urn:request.BuildCreateDevice.schema.json","$schema":"http:\/\/json-schema.org\/draft-07\/schema#","definitions":{"device_asset_tag":{"pattern":"^\\S+$","type":"string"},"device_serial_number":{"allOf":[{"pattern":"^\\S+$","type":"string"},{"$ref":"\/definitions\/mojo_standard_placeholder"}]},"mojo_standard_placeholder":{"description":"see - https:\/\/metacpan.org\/pod\/Mojolicious::Guides::Routing#Standard-placeholders","pattern":"^[^\/.]+$","type":"string"},"uuid":{"pattern":"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$","type":"string"}},"items":{"additionalProperties":false,"anyOf":[{"required":["id"]},{"required":["serial_number"]}],"properties":{"asset_tag":{"oneOf":[{"$ref":"\/definitions\/device_asset_tag"},{"type":"null"}]},"id":{"$ref":"\/definitions\/uuid"},"links":{"items":{"format":"uri","type":"string"},"type":"array","uniqueItems":true},"serial_number":{"$ref":"\/definitions\/device_serial_number"},"sku":{"type":"string"}},"required":["sku"],"type":"object"},"title":"BuildCreateDevice","type":"array","uniqueItems":true}' - headers: - Content-Length: - - "1074" - Content-Type: - - application/json - Date: - - Fri, 01 Nov 2019 21:34:30 GMT - Last-Modified: - - Fri, 01 Nov 2019 20:19:12 GMT - Request-Id: - - eV37EMmAG3no - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-a8-0-gd0dc7f2e - X-Request-Id: - - eV37EMmAG3no - status: 200 OK - code: 200 - duration: "" diff --git a/fixtures/json-schema/request/DatacenterRoomCreate.yaml b/fixtures/json-schema/request/DatacenterRoomCreate.yaml deleted file mode 100644 index e4bb497..0000000 --- a/fixtures/json-schema/request/DatacenterRoomCreate.yaml +++ /dev/null @@ -1,33 +0,0 @@ ---- -version: 1 -interactions: -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/schema/request/DatacenterRoomCreate - method: GET - response: - body: '{"$id":"urn:request.DatacenterRoomCreate.schema.json","$schema":"http:\/\/json-schema.org\/draft-07\/schema#","additionalProperties":false,"definitions":{"non_empty_string":{"minLength":1,"type":"string"},"uuid":{"pattern":"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$","type":"string"}},"properties":{"alias":{"$ref":"\/definitions\/non_empty_string"},"az":{"$ref":"\/definitions\/non_empty_string"},"datacenter_id":{"$ref":"\/definitions\/uuid"},"vendor_name":{"$ref":"\/definitions\/non_empty_string"}},"required":["datacenter_id","az","alias"],"title":"DatacenterRoomCreate","type":"object"}' - headers: - Content-Length: - - "613" - Content-Type: - - application/json - Date: - - Fri, 01 Nov 2019 21:34:38 GMT - Last-Modified: - - Fri, 01 Nov 2019 20:19:12 GMT - Request-Id: - - VCJFTr3KdsWE - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-a8-0-gd0dc7f2e - X-Request-Id: - - VCJFTr3KdsWE - status: 200 OK - code: 200 - duration: "" diff --git a/fixtures/json-schema/request/HardwareProductCreate.yaml b/fixtures/json-schema/request/HardwareProductCreate.yaml deleted file mode 100644 index 352f300..0000000 --- a/fixtures/json-schema/request/HardwareProductCreate.yaml +++ /dev/null @@ -1,33 +0,0 @@ ---- -version: 1 -interactions: -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/schema/request/HardwareProductCreate - method: GET - response: - body: '{"$id":"urn:request.HardwareProductCreate.schema.json","$schema":"http:\/\/json-schema.org\/draft-07\/schema#","allOf":[{"$ref":"\/definitions\/HardwareProductUpdate"},{"required":["name","alias","hardware_vendor_id","sku","rack_unit_size","validation_plan_id","purpose","bios_firmware","cpu_type"]}],"definitions":{"HardwareProductUpdate":{"additionalProperties":false,"minProperties":1,"properties":{"alias":{"$ref":"\/definitions\/mojo_standard_placeholder"},"bios_firmware":{"type":"string"},"cpu_num":{"type":"integer"},"cpu_type":{"type":"string"},"dimms_num":{"type":"integer"},"generation_name":{"$ref":"\/definitions\/non_empty_string"},"hardware_vendor_id":{"$ref":"\/definitions\/uuid"},"hba_firmware":{"oneOf":[{"type":"string"},{"type":"null"}]},"legacy_product_name":{"oneOf":[{"$ref":"\/definitions\/non_empty_string"},{"type":"null"}]},"name":{"$ref":"\/definitions\/mojo_standard_placeholder"},"nics_num":{"type":"integer"},"nvme_ssd_num":{"type":"integer"},"nvme_ssd_size":{"oneOf":[{"type":"integer"},{"type":"null"}]},"nvme_ssd_slots":{"oneOf":[{"type":"string"},{"type":"null"}]},"prefix":{"oneOf":[{"$ref":"\/definitions\/non_empty_string"},{"type":"null"}]},"psu_total":{"type":"integer"},"purpose":{"type":"string"},"rack_unit_size":{"$ref":"\/definitions\/positive_integer"},"raid_lun_num":{"type":"integer"},"ram_total":{"type":"integer"},"sas_hdd_num":{"type":"integer"},"sas_hdd_size":{"oneOf":[{"type":"integer"},{"type":"null"}]},"sas_hdd_slots":{"oneOf":[{"type":"string"},{"type":"null"}]},"sas_ssd_num":{"type":"integer"},"sas_ssd_size":{"oneOf":[{"type":"integer"},{"type":"null"}]},"sas_ssd_slots":{"oneOf":[{"type":"string"},{"type":"null"}]},"sata_hdd_num":{"type":"integer"},"sata_hdd_size":{"oneOf":[{"type":"integer"},{"type":"null"}]},"sata_hdd_slots":{"oneOf":[{"type":"string"},{"type":"null"}]},"sata_ssd_num":{"type":"integer"},"sata_ssd_size":{"oneOf":[{"type":"integer"},{"type":"null"}]},"sata_ssd_slots":{"oneOf":[{"type":"string"},{"type":"null"}]},"sku":{"$ref":"\/definitions\/mojo_standard_placeholder"},"specification":{"description":"json blob of additional data","oneOf":[{"type":"string"},{"type":"null"}]},"usb_num":{"type":"integer"},"validation_plan_id":{"$ref":"\/definitions\/uuid"}},"type":"object"},"mojo_standard_placeholder":{"description":"see https:\/\/metacpan.org\/pod\/Mojolicious::Guides::Routing#Standard-placeholders","pattern":"^[^\/.]+$","type":"string"},"non_empty_string":{"minLength":1,"type":"string"},"positive_integer":{"minimum":1,"type":"integer"},"uuid":{"pattern":"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$","type":"string"}},"title":"HardwareProductCreate"}' - headers: - Content-Length: - - "3369" - Content-Type: - - application/json - Date: - - Fri, 01 Nov 2019 21:34:32 GMT - Last-Modified: - - Fri, 01 Nov 2019 20:19:12 GMT - Request-Id: - - oYAvxhYm04fq - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-a8-0-gd0dc7f2e - X-Request-Id: - - oYAvxhYm04fq - status: 200 OK - code: 200 - duration: "" diff --git a/fixtures/json-schema/request/OrganizationAddUser.yaml b/fixtures/json-schema/request/OrganizationAddUser.yaml deleted file mode 100644 index e13290a..0000000 --- a/fixtures/json-schema/request/OrganizationAddUser.yaml +++ /dev/null @@ -1,35 +0,0 @@ ---- -version: 1 -interactions: -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/schema/request/OrganizationAddUser - method: GET - response: - body: '{"$id":"urn:request.OrganizationAddUser.schema.json","$schema":"http:\/\/json-schema.org\/draft-07\/schema#","additionalProperties":false,"definitions":{"email_address":{"allOf":[{"format":"email","type":"string"},{"$ref":"\/definitions\/mojo_relaxed_placeholder"}]},"mojo_relaxed_placeholder":{"description":"see - https:\/\/metacpan.org\/pod\/Mojolicious::Guides::Routing#Relaxed-placeholders","pattern":"^[^\/]+$","type":"string"},"role":{"description":"corresponds - to role_enum in the database","enum":["ro","rw","admin"],"type":"string"},"uuid":{"pattern":"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$","type":"string"}},"oneOf":[{"required":["user_id"]},{"required":["email"]}],"properties":{"email":{"$ref":"\/definitions\/email_address"},"role":{"$ref":"\/definitions\/role"},"user_id":{"$ref":"\/definitions\/uuid"}},"required":["role"],"title":"OrganizationAddUser","type":"object"}' - headers: - Content-Length: - - "908" - Content-Type: - - application/json - Date: - - Fri, 01 Nov 2019 21:34:32 GMT - Last-Modified: - - Fri, 01 Nov 2019 20:19:12 GMT - Request-Id: - - d0XYf98BdB2O - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-a8-0-gd0dc7f2e - X-Request-Id: - - d0XYf98BdB2O - status: 200 OK - code: 200 - duration: "" diff --git a/fixtures/json-schema/request/OrganizationCreate.yaml b/fixtures/json-schema/request/OrganizationCreate.yaml deleted file mode 100644 index 2acca87..0000000 --- a/fixtures/json-schema/request/OrganizationCreate.yaml +++ /dev/null @@ -1,35 +0,0 @@ ---- -version: 1 -interactions: -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/schema/request/OrganizationCreate - method: GET - response: - body: '{"$id":"urn:request.OrganizationCreate.schema.json","$schema":"http:\/\/json-schema.org\/draft-07\/schema#","additionalProperties":false,"definitions":{"email_address":{"allOf":[{"format":"email","type":"string"},{"$ref":"\/definitions\/mojo_relaxed_placeholder"}]},"mojo_relaxed_placeholder":{"description":"see - https:\/\/metacpan.org\/pod\/Mojolicious::Guides::Routing#Relaxed-placeholders","pattern":"^[^\/]+$","type":"string"},"mojo_standard_placeholder":{"description":"see - https:\/\/metacpan.org\/pod\/Mojolicious::Guides::Routing#Standard-placeholders","pattern":"^[^\/.]+$","type":"string"},"uuid":{"pattern":"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$","type":"string"}},"properties":{"admins":{"items":{"additionalProperties":false,"oneOf":[{"required":["user_id"]},{"required":["email"]}],"properties":{"email":{"$ref":"\/definitions\/email_address"},"user_id":{"$ref":"\/definitions\/uuid"}},"type":"object"},"minItems":1,"type":"array","uniqueItems":true},"description":{"type":"string"},"name":{"$ref":"\/definitions\/mojo_standard_placeholder"}},"required":["name","admins"],"title":"OrganizationCreate","type":"object"}' - headers: - Content-Length: - - "1155" - Content-Type: - - application/json - Date: - - Fri, 01 Nov 2019 21:34:32 GMT - Last-Modified: - - Fri, 01 Nov 2019 20:19:12 GMT - Request-Id: - - RPYKCT8FpSe3 - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-a8-0-gd0dc7f2e - X-Request-Id: - - RPYKCT8FpSe3 - status: 200 OK - code: 200 - duration: "" diff --git a/fixtures/json-schema/request/RackAssignmentUpdates.yaml b/fixtures/json-schema/request/RackAssignmentUpdates.yaml deleted file mode 100644 index 20c43ce..0000000 --- a/fixtures/json-schema/request/RackAssignmentUpdates.yaml +++ /dev/null @@ -1,34 +0,0 @@ ---- -version: 1 -interactions: -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/schema/request/RackAssignmentUpdates - method: GET - response: - body: '{"$id":"urn:request.RackAssignmentUpdates.schema.json","$schema":"http:\/\/json-schema.org\/draft-07\/schema#","definitions":{"RackAssignmentUpdate":{"additionalProperties":false,"anyOf":[{"required":["device_id"]},{"required":["device_serial_number"]}],"properties":{"device_asset_tag":{"oneOf":[{"$ref":"\/definitions\/device_asset_tag"},{"type":"null"}]},"device_id":{"$ref":"\/definitions\/uuid"},"device_serial_number":{"$ref":"\/definitions\/device_serial_number"},"rack_unit_start":{"$ref":"\/definitions\/positive_integer"}},"required":["rack_unit_start"],"type":"object"},"device_asset_tag":{"pattern":"^\\S+$","type":"string"},"device_serial_number":{"allOf":[{"pattern":"^\\S+$","type":"string"},{"$ref":"\/definitions\/mojo_standard_placeholder"}]},"mojo_standard_placeholder":{"description":"see - https:\/\/metacpan.org\/pod\/Mojolicious::Guides::Routing#Standard-placeholders","pattern":"^[^\/.]+$","type":"string"},"positive_integer":{"minimum":1,"type":"integer"},"uuid":{"pattern":"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$","type":"string"}},"items":{"$ref":"\/definitions\/RackAssignmentUpdate"},"title":"RackAssignmentUpdates","type":"array","uniqueItems":true}' - headers: - Content-Length: - - "1201" - Content-Type: - - application/json - Date: - - Fri, 01 Nov 2019 21:34:37 GMT - Last-Modified: - - Fri, 01 Nov 2019 20:19:12 GMT - Request-Id: - - SAuFOeDHwcUg - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-a8-0-gd0dc7f2e - X-Request-Id: - - SAuFOeDHwcUg - status: 200 OK - code: 200 - duration: "" diff --git a/fixtures/json-schema/request/RackLayoutCreate.yaml b/fixtures/json-schema/request/RackLayoutCreate.yaml deleted file mode 100644 index 2077242..0000000 --- a/fixtures/json-schema/request/RackLayoutCreate.yaml +++ /dev/null @@ -1,33 +0,0 @@ ---- -version: 1 -interactions: -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/schema/request/RackLayoutCreate - method: GET - response: - body: '{"$id":"urn:request.RackLayoutCreate.schema.json","$schema":"http:\/\/json-schema.org\/draft-07\/schema#","additionalProperties":false,"definitions":{"positive_integer":{"minimum":1,"type":"integer"},"uuid":{"pattern":"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$","type":"string"}},"properties":{"hardware_product_id":{"$ref":"\/definitions\/uuid"},"rack_id":{"$ref":"\/definitions\/uuid"},"rack_unit_start":{"$ref":"\/definitions\/positive_integer"}},"required":["rack_id","hardware_product_id","rack_unit_start"],"title":"RackLayoutCreate","type":"object"}' - headers: - Content-Length: - - "577" - Content-Type: - - application/json - Date: - - Fri, 01 Nov 2019 21:34:37 GMT - Last-Modified: - - Fri, 01 Nov 2019 20:19:12 GMT - Request-Id: - - Izgd8nkPgRe0 - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-a8-0-gd0dc7f2e - X-Request-Id: - - Izgd8nkPgRe0 - status: 200 OK - code: 200 - duration: "" diff --git a/fixtures/json-schema/request/RackRoleCreate.yaml b/fixtures/json-schema/request/RackRoleCreate.yaml deleted file mode 100644 index 8363416..0000000 --- a/fixtures/json-schema/request/RackRoleCreate.yaml +++ /dev/null @@ -1,34 +0,0 @@ ---- -version: 1 -interactions: -- request: - body: "" - form: {} - headers: - User-Agent: - - Conch/3.x-testing - url: http://10.51.54.42:5000/schema/request/RackRoleCreate - method: GET - response: - body: '{"$id":"urn:request.RackRoleCreate.schema.json","$schema":"http:\/\/json-schema.org\/draft-07\/schema#","additionalProperties":false,"definitions":{"mojo_standard_placeholder":{"description":"see - https:\/\/metacpan.org\/pod\/Mojolicious::Guides::Routing#Standard-placeholders","pattern":"^[^\/.]+$","type":"string"},"positive_integer":{"minimum":1,"type":"integer"}},"properties":{"name":{"$ref":"\/definitions\/mojo_standard_placeholder"},"rack_size":{"$ref":"\/definitions\/positive_integer"}},"required":["name","rack_size"],"title":"RackRoleCreate","type":"object"}' - headers: - Content-Length: - - "569" - Content-Type: - - application/json - Date: - - Fri, 01 Nov 2019 21:34:37 GMT - Last-Modified: - - Fri, 01 Nov 2019 20:19:12 GMT - Request-Id: - - tZJMMc21V1iG - Server: - - Mojolicious (Perl) - X-Conch-Api: - - v3.0.0-a8-0-gd0dc7f2e - X-Request-Id: - - tZJMMc21V1iG - status: 200 OK - code: 200 - duration: "" diff --git a/fixtures_test.go b/fixtures_test.go deleted file mode 100644 index ac2b20d..0000000 --- a/fixtures_test.go +++ /dev/null @@ -1,386 +0,0 @@ -package main - -import ( - "math/rand" - "reflect" - - "github.com/bxcodec/faker" - "github.com/gofrs/uuid" -) - -// currently ValidationPlans are immutable in every API -// changing this is part of the plans for v3.1 -// but for now we just inline one of the plan IDs -const validationPlanID = "a30ab8b2-8a9e-4e51-8bb0-92862abd8b54" - -func init() { - faker.AddProvider("uuid", func(v reflect.Value) (interface{}, error) { - return uuid.NewV4() - }) - - faker.AddProvider("rack_unit_size", func(v reflect.Value) (interface{}, error) { - return rand.Intn(2) + 1, nil - }) - - lastRSU := 0 - faker.AddProvider("rack_unit_start", func(v reflect.Value) (interface{}, error) { - if lastRSU >= 60 { - lastRSU = 0 - } - lastRSU += 1 - return lastRSU, nil - }) - - faker.AddProvider("rack_size", func(v reflect.Value) (interface{}, error) { - return 60, nil - }) -} - -type Fixture struct { - dc Datacenter - room Room - role RackRole - rack Rack - rackLayout RackLayout - hardwareVendor HardwareVendor - switchProduct HardwareProduct - serverProduct HardwareProduct - validationPlan ValidationPlan - build Build - - // reset function - reset func() -} - -func (f *Fixture) addReset(reset func()) *Fixture { - next := f.reset - f.reset = func() { - reset() - if next != nil { - next() - } - } - return f -} - -func (f *Fixture) setupBuild() *Fixture { - if f.build.ID != (uuid.UUID{}) { - return f - } - - mock := newTestBuild() - f.build = API.Builds().Create( - mock.Name, - mock.Description, - []map[string]string{{"email": "conch@example.com"}}, - ) - - return f -} - -func (f *Fixture) setupHardwareVendor() *Fixture { - if f.hardwareVendor != (HardwareVendor{}) { - return f - } - - //mock := newTestHardwareVendor() - f.hardwareVendor = API.Hardware().FindOrCreateVendor("MyBigVendor") - - return f.addReset(func() { - API.Hardware().DeleteVendor(f.hardwareVendor.ID) - }) -} - -func (f *Fixture) setupValidationPlan() *Fixture { - if f.validationPlan != (ValidationPlan{}) { - return f - } - - id, _ := uuid.FromString(validationPlanID) - f.validationPlan = API.Validations().GetPlan(id) - - return f -} - -func (f *Fixture) setupHardwareProducts() *Fixture { - f.setupHardwareVendor() - f.setupValidationPlan() - - mswp := newTestHardwareProduct() - f.switchProduct = API.Hardware().Create( - mswp.Name, - mswp.Alias, - f.hardwareVendor.ID, - mswp.SKU, - mswp.RackUnitSize, - f.validationPlan.ID, - mswp.Purpose, - mswp.BiosFirmware, - mswp.CpuType, - ) - - msvp := newTestHardwareProduct() - f.serverProduct = API.Hardware().Create( - msvp.Name, - msvp.Alias, - f.hardwareVendor.ID, - msvp.SKU, - msvp.RackUnitSize, - f.validationPlan.ID, - msvp.Purpose, - msvp.BiosFirmware, - msvp.CpuType, - ) - - return f.addReset(func() { - API.Hardware().Delete(f.serverProduct.ID) - API.Hardware().Delete(f.switchProduct.ID) - }) -} - -func (f *Fixture) setupDatacenter() *Fixture { - if f.dc != (Datacenter{}) { - return f - } - - mock := newTestDatacenter() - f.dc = API.Datacenters().CreateFromStruct(mock) - return f.addReset(func() { - API.Datacenters().Delete(f.dc.ID) - }) -} - -func (f *Fixture) setupRoom() *Fixture { - if f.room != (Room{}) { - return f - } - - f.setupDatacenter() - - mockRoom := newTestRoom() - mockRoom.DatacenterID = f.dc.ID - f.room = API.Rooms().CreateFromStruct(mockRoom) - - return f.addReset(func() { - API.Rooms().Delete(f.room.ID) - }) -} - -func (f *Fixture) setupRackRole() *Fixture { - if f.role != (RackRole{}) { - return f - } - - mock := newTestRackRole() - f.role = API.RackRoles().CreateFromStruct(mock) - - return f.addReset(func() { - API.RackRoles().Delete(f.role.ID) - }) -} - -func (f *Fixture) setupRack() *Fixture { - if f.rack != (Rack{}) { - return f - } - - f.setupRoom() - f.setupBuild() - f.setupRackRole() - - mock := newTestRack() - mock.RoomID = f.room.ID - mock.RoleID = f.role.ID - mock.Phase = "integration" - mock.BuildID = f.build.ID - f.rack = API.Racks().CreateFromStruct(mock) - - return f.addReset(func() { - API.Racks().Delete(f.rack.ID) - }) -} - -func (f *Fixture) setupRackLayout() *Fixture { - if f.rackLayout != nil { - return f - } - - f.setupHardwareProducts() - f.setupRack() - - rl := RackLayoutUpdates{ - { - RU: 1, - ProductID: f.serverProduct.ID, - }, - { - RU: 1 + f.serverProduct.RackUnitSize, - ProductID: f.switchProduct.ID, - }, - } - - rackID := f.rack.ID - f.rackLayout = API.Racks().CreateLayout(rackID, rl) - - return f.addReset(func() { - for _, row := range API.Racks().Layouts(rackID) { - API.Racks().DeleteLayoutSlot(row.ID) - } - }) -} - -func newFixture() Fixture { - return Fixture{} -} - -func newTestDatacenter() (dc Datacenter) { - err := faker.FakeData(&dc) - if err != nil { - panic(err) - } - return -} - -func newTestBuildList() (list BuildList) { - err := faker.FakeData(&list) - if err != nil { - panic(err) - } - return -} - -func newTestBuild() (build Build) { - err := faker.FakeData(&build) - if err != nil { - panic(err) - } - return -} - -func newTestUser() (user UserAndRole) { - err := faker.FakeData(&user) - if err != nil { - panic(err) - } - return -} - -func newTestUserAndRoles() (list UserAndRoles) { - err := faker.FakeData(&list) - if err != nil { - panic(err) - } - return -} - -func newTestOrganization() (org Org) { - err := faker.FakeData(&org) - if err != nil { - panic(err) - } - return -} - -func newTestOrganizationUser() (ou OrganizationUser) { - err := faker.FakeData(&ou) - if err != nil { - panic(err) - } - return -} - -func newTestOrgAndRoles() (list OrgAndRoles) { - err := faker.FakeData(&list) - if err != nil { - panic(err) - } - return -} - -func newTestOrgList() (list OrgList) { - err := faker.FakeData(&list) - if err != nil { - panic(err) - } - return -} - -func newTestDeviceList() (list DeviceList) { - err := faker.FakeData(&list) - if err != nil { - panic(err) - } - return -} - -func newTestRack() (rack Rack) { - err := faker.FakeData(&rack) - if err != nil { - panic(err) - } - return -} - -func newTestRackList() (list RackList) { - err := faker.FakeData(&list) - if err != nil { - panic(err) - } - return -} - -func newTestRoom() (room Room) { - err := faker.FakeData(&room) - if err != nil { - panic(err) - } - return -} - -func newTestRelay() (relay Relay) { - err := faker.FakeData(&relay) - if err != nil { - panic(err) - } - return -} - -func newTestRackRole() (role RackRole) { - err := faker.FakeData(&role) - if err != nil { - panic(err) - } - return -} - -func newTestHardwareProduct() (hp HardwareProduct) { - err := faker.FakeData(&hp) - if err != nil { - panic(err) - } - return -} - -func newTestHardwareVendor() (hv HardwareVendor) { - err := faker.FakeData(&hv) - if err != nil { - panic(err) - } - return -} - -func newTestRackAssignmentUpdates() (ra RackAssignmentUpdates) { - err := faker.FakeData(&ra) - if err != nil { - panic(err) - } - return -} - -func newTestDevice() (d deviceCore) { - err := faker.FakeData(&d) - if err != nil { - panic(err) - } - return -} diff --git a/go.mod b/go.mod index a904c0d..1d724d5 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,13 @@ module github.com/joyent/kosh go 1.15 require ( - github.com/bxcodec/faker v2.0.1+incompatible - github.com/davecgh/go-spew v1.1.1 github.com/dghubble/sling v1.3.0 github.com/dnaeon/go-vcr v1.0.1 github.com/gofrs/uuid v3.2.0+incompatible github.com/jawher/mow.cli v1.1.0 github.com/mattn/go-runewidth v0.0.4 // indirect github.com/olekukonko/tablewriter v0.0.1 - github.com/qri-io/jsonschema v0.1.1 + github.com/qri-io/jsonschema v0.2.0 github.com/stretchr/testify v1.4.0 gopkg.in/yaml.v2 v2.2.4 // indirect ) diff --git a/go.sum b/go.sum index 4451cc4..3df8222 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/bxcodec/faker v2.0.1+incompatible h1:P0KUpUw5w6WJXwrPfv35oc91i4d8nf40Nwln+M/+faA= -github.com/bxcodec/faker v2.0.1+incompatible/go.mod h1:BNzfpVdTwnFJ6GtfYTcQu6l6rHShT+veBxNCnjCx5XM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -19,10 +17,10 @@ github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8u github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/qri-io/jsonpointer v0.1.0 h1:OcTtTmorodUCRc2CZhj/ZwOET8zVj6uo0ArEmzoThZI= -github.com/qri-io/jsonpointer v0.1.0/go.mod h1:DnJPaYgiKu56EuDp8TU5wFLdZIcAnb/uH9v37ZaMV64= -github.com/qri-io/jsonschema v0.1.1 h1:t//Doa/gvMqJ0bDhG7PGIKfaWGGxRVaffp+bcvBGGEk= -github.com/qri-io/jsonschema v0.1.1/go.mod h1:QpzJ6gBQ0GYgGmh7mDQ1YsvvhSgE4rYj0k8t5MBOmUY= +github.com/qri-io/jsonpointer v0.1.1 h1:prVZBZLL6TW5vsSB9fFHFAMBLI4b0ri5vribQlTJiBA= +github.com/qri-io/jsonpointer v0.1.1/go.mod h1:DnJPaYgiKu56EuDp8TU5wFLdZIcAnb/uH9v37ZaMV64= +github.com/qri-io/jsonschema v0.2.0 h1:is8lirh3HYwTkC0e+4jL/vWEHwzPLojnl4FWkUoeEPU= +github.com/qri-io/jsonschema v0.2.0/go.mod h1:g7DPkiOsK1xv6T/Ao5scXRkd+yTFygcANPBaaqW+VrI= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/hardware.go b/hardware.go deleted file mode 100644 index 5bce009..0000000 --- a/hardware.go +++ /dev/null @@ -1,413 +0,0 @@ -// Copyright Joyent, Inc. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package main - -//lint:file-ignore U1000 WIP - -import ( - "bytes" - "fmt" - "net/url" - "strings" - - "time" - - "github.com/gofrs/uuid" - cli "github.com/jawher/mow.cli" - "github.com/joyent/kosh/tables" - "github.com/joyent/kosh/template" - "github.com/olekukonko/tablewriter" - // "github.com/olekukonko/tablewriter" -) - -type Hardware struct { - *Conch -} - -func (c *Conch) Hardware() *Hardware { - return &Hardware{c} -} - -type HardwareProductSummaries []HardwareProductSummary - -func (hps HardwareProductSummaries) String() string { - if API.JsonOnly { - return API.AsJSON(hps) - } - - tableString := &strings.Builder{} - table := tablewriter.NewWriter(tableString) - tables.TableToMarkdown(table) - - table.SetHeader([]string{ - "ID", - "SKU", - "Name", - "Alias", - "GenerationName", - "Created", - "Updated", - }) - - for _, hp := range hps { - table.Append([]string{ - template.CutUUID(hp.ID.String()), - hp.SKU, - hp.Name, - hp.Alias, - hp.GenerationName, - hp.Created.String(), - hp.Updated.String(), - }) - } - table.Render() - return tableString.String() -} - -func (hp HardwareProduct) HardwareVendor() HardwareVendor { - return API.Hardware().GetVendor(hp.HardwareVendorID.String()) -} - -func (hp HardwareProduct) ValidationPlan() ValidationPlan { - return API.Validations().GetPlan(hp.ValidationPlanID) -} - -type HardwareProductSummary struct { - ID uuid.UUID `json:"id" faker:"uuid"` - Name string `json:"name"` - Alias string `json:"alias"` - GenerationName string `json:"generation_name,omitempty"` - SKU string `json:"sku"` - Created time.Time `json:"created" faker:"-"` - Updated time.Time `json:"updated" faker:"-"` -} - -type HardwareProduct struct { - ID uuid.UUID `json:"id" faker:"uuid"` - Name string `json:"name"` - Alias string `json:"alias"` - Prefix string `json:"prefix,omitempty"` - GenerationName string `json:"generation_name,omitempty"` - LegacyProductName string `json:"legacy_product_name,omitempty"` - SKU string `json:"sku"` - Specification string `json:"specification,omitempty"` - RackUnitSize int `json:"rack_unit_size" faker:"rack_unit_size"` - - HardwareVendorID uuid.UUID `json:"hardware_vendor_id" faker:"uuid"` - ValidationPlanID uuid.UUID `json:"validation_plan_id,omitempty" faker:"-"` - - BiosFirmware string `json:"bios_firmware"` - CpuNum int `json:"cpu_num"` - CpuType string `json:"cpu_type"` - DimmsNum int `json:"dimms_num"` - HbaFirmware string `json:"hba_firmware,omitempty"` - NicsNum int `json:"nics_num"` - Purpose string `json:"purpose"` - RamTotal int `json:"ram_total"` - SasHddSlots string `json:"sas_hdd_slots,omitempty"` - SataHddSlots string `json:"sata_hdd_slots,omitempty"` - SataSsdSlots string `json:"sata_ssd_slots,omitempty"` - SasSsdSlots string `json:"sas_ssd_slots,omitempty"` - NvmeSsdSlots string `json:"nvme_ssd_slots,omitempty"` - UsbNum int `json:"usb_num"` - - // NOTE the pointers. 0 is a valid value so zero values aren't - PsuTotal *int `json:"psu_total,omitempty"` - RaidLunNum *int `json:"raid_lun_num,omitempty"` - - SasHddNum *int `json:"sas_hdd_num,omitempty"` - SasHddSize *int `json:"sas_hdd_size,omitempty"` - - SataHddNum *int `json:"sata_hdd_num,omitempty"` - SataHddSize *int `json:"sata_hdd_size,omitempty"` - - SataSsdNum *int `json:"sata_ssd_num,omitempty"` - SataSsdSize *int `json:"sata_ssd_size,omitempty"` - - SasSsdNum *int `json:"sas_ssd_num,omitempty"` - SasSsdSize *int `json:"sas_ssd_size,omitempty"` - - NvmeSsdNum *int `json:"nvme_ssd_num,omitempty"` - NvmeSsdSize *int `json:"nvme_ssd_size,omitempty"` - - Created time.Time `json:"created" faker:"-"` - Updated time.Time `json:"updated" faker:"-"` -} - -func (hp HardwareProduct) String() string { - if API.JsonOnly { - return API.AsJSON(hp) - } - t, err := template.NewTemplate().Parse(hardwareProductTemplate) - if err != nil { - panic(err) - } - - buf := new(bytes.Buffer) - if err := t.Execute(buf, hp); err != nil { - panic(err) - } - - return buf.String() -} - -func (h *Hardware) GetAllProducts() (hps HardwareProductSummaries) { - res := h.Do(h.Sling().New().Get("/hardware_product")) - if ok := res.Parse(&hps); !ok { - panic(res) - } - return hps -} - -func (h *Hardware) GetProduct(id uuid.UUID) (hp HardwareProduct) { - return h.GetProductByString(id.String()) -} - -func (h *Hardware) GetProductByString(s string) (hp HardwareProduct) { - uri := fmt.Sprintf("/hardware_product/%s", url.PathEscape(s)) - res := h.Do(h.Sling().New().Get(uri)) - if ok := res.Parse(&hp); !ok { - panic(res) - } - - return hp -} - -func (h Hardware) GetProductByName(name string) HardwareProduct { - return h.GetProductByString(name) -} - -func (h Hardware) GetProductBySku(sku string) HardwareProduct { - return h.GetProductByString(sku) -} - -func (h Hardware) GetProductByAlias(alias string) HardwareProduct { - return h.GetProductByString(alias) -} - -func (h *Hardware) Create( - name, alias string, - vendorID uuid.UUID, - SKU string, - rackUnitSize int, - validationPlanID uuid.UUID, - Purpose string, - BiosFirmware string, - CpuType string, -) (hp HardwareProduct) { - payload := make(map[string]interface{}) - payload["name"] = name - payload["alias"] = alias - payload["hardware_vendor_id"] = vendorID - payload["sku"] = SKU - payload["rack_unit_size"] = rackUnitSize - payload["validation_plan_id"] = validationPlanID - payload["purpose"] = Purpose - payload["bios_firmware"] = BiosFirmware - payload["cpu_type"] = CpuType - - res := h.Do(h.Sling().New().Post("/hardware_product"). - Set("Content-Type", "application/json"). - BodyJSON(payload), - ) - - if ok := res.Parse(&hp); !ok { - panic(res) - } - - return hp -} - -func (h *Hardware) Delete(ID uuid.UUID) { - uri := fmt.Sprintf("/hardware_product/%s", url.PathEscape(ID.String())) - res := h.Do(h.Sling().New().Delete(uri)) - - if res.StatusCode() != 204 { - // I know this is weird. Like in other places, it should be impossible - // to reach here unless the status code is 204. The API returns 204 - // (which gets us here) or 409 (which will explode before it gets here). - // If we got here via some other code, then there's some new behavior - // that we need to know about. - panic(res) - } - -} - -type HardwareVendor struct { - ID uuid.UUID `json:"id" faker:"uuid"` - Name string `json:"name"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` -} - -func (h *Hardware) GetAllVendors() (hvs []HardwareVendor) { - res := h.Do(h.Sling().Get("/hardware_vendor/")) - if ok := res.Parse(&hvs); !ok { - panic(res) - } - - return -} - -func (h *Hardware) GetVendor(name string) (hv HardwareVendor) { - uri := fmt.Sprintf("/hardware_vendor/%s", url.PathEscape(name)) - - res := h.Do(h.Sling().New().Get(uri)) - if ok := res.Parse(&hv); !ok { - panic(res) - } - - return -} - -func (h *Hardware) CreateVendor(name string) (hv HardwareVendor) { - uri := fmt.Sprintf("/hardware_vendor/%s", url.PathEscape(name)) - - _ = h.Do(h.Sling().New().Post(uri)) - - return h.GetVendor(name) -} - -func (h *Hardware) FindOrCreateVendor(name string) (hv HardwareVendor) { - // if we fail to Find the vendor we panic, - // really we need to calm down and just create a new Vendor - defer func() { - if r := recover(); r != nil { - hv = h.CreateVendor(name) - } - }() - hv = h.GetVendor(name) - return -} - -func (h *Hardware) DeleteVendor(id uuid.UUID) { - uri := fmt.Sprintf("/hardware_vendor/%s", url.PathEscape(id.String())) - res := h.Do(h.Sling().New().Delete(uri)) - - if res.StatusCode() != 204 { - // I know this is weird. Like in other places, it should be impossible - // to reach here unless the status code is 204. The API returns 204 - // (which gets us here) or 409 (which will explode before it gets here). - // If we got here via some other code, then there's some new behavior - // that we need to know about. - panic(res) - } - -} - -/* - kosh hardware products create \ - --name Foo \ - --alias Bar \ - --vendor MyBigVendor \ - --SKU FooBle-001 \ - --rack-unit-size 4 \ - --validation-plan ServerPlan \ - --purpose Server \ - --biosFirmware firmware23.dat \ - --cpuType Intel -*/ - -func cmdCreateProduct(cmd *cli.Cmd) { - var ( - name = cmd.StringOpt("name", "", "Name of the hardware product") - alias = cmd.StringOpt("alias", "", "Alias for the hardware product") - vendor = cmd.StringOpt("vendor", "", "Vendor of the hardware product") - SKU = cmd.StringOpt("sku", "", "SKU for the hardware product") - rackUnitSize = cmd.IntOpt("rack-unit-size", 2, "RU size of the hardware product") - validationPlanOpt = cmd.StringOpt("validation-plan", "", "Name of the plan to validate the product against") - purpose = cmd.StringOpt("purpose", "", "Purpose of the product") - biosFirmware = cmd.StringOpt("bios-firmware", "", "BIOS firmware version for the product") - cpuType = cmd.StringOpt("cpu-type", "", "CPU type for the product") - ) - - cmd.Spec = "--sku --name --alias --vendor --validation-plan --purpose --bios-firmware --cpu-type [OPTIONS]" - cmd.Action = func() { - validationPlan := API.Validations().GetPlanByName(*validationPlanOpt) - vendor := API.Hardware().GetVendor(*vendor) - fmt.Println(API.Hardware().Create( - *name, - *alias, - vendor.ID, - *SKU, - *rackUnitSize, - validationPlan.ID, - *purpose, - *biosFirmware, - *cpuType, - )) - } -} - -// kosh hardware products get -func cmdListProducts(cmd *cli.Cmd) { - cmd.Action = func() { - fmt.Println(API.Hardware().GetAllProducts()) - } -} - -func initHardwareCli(cmd *cli.Cli) { - - cmd.Command("hardware", "Work with hardware profiles and vendors", func(cmd *cli.Cmd) { - cmd.Command("products", "Work with hardware products", func(cmd *cli.Cmd) { - cmd.Command("create", "Create a hardware product", cmdCreateProduct) - cmd.Command("get ls", "Get a list of all hardware products", cmdListProducts) - }) - - cmd.Command("product", "Work with a hardware product", func(cmd *cli.Cmd) { - var hp HardwareProduct - idArg := cmd.StringArg("PRODUCT", "", "The SKU, UUID, alias, or name of the hardware product.") - cmd.Before = func() { - hp = API.Hardware().GetProductByString(*idArg) - if (hp == HardwareProduct{}) { - panic("Hardware Product not found for " + *idArg) - } - } - cmd.Command("get", "Show a hardware vendor's details", func(cmd *cli.Cmd) { - cmd.Action = func() { fmt.Println(hp) } - }) - cmd.Command("delete rm", "Remove a hardware product", func(cmd *cli.Cmd) { - cmd.Action = func() { - API.Hardware().Delete(hp.ID) - fmt.Println(API.Hardware().GetAllProducts()) - } - }) - }) - - cmd.Command("vendors", "Work with hardware vendors", func(cmd *cli.Cmd) { - cmd.Command("get ls", "Get a list of all hardware vendors", func(cmd *cli.Cmd) { - API.Hardware().GetAllVendors() - }) - cmd.Command("create", "Create a hardware vendor", func(cmd *cli.Cmd) { - name := cmd.StringArg("NAME", "", "The name of the hardware vendor.") - cmd.Action = func() { API.Hardware().FindOrCreateVendor(*name) } - }) - }) - - cmd.Command("vendor", "Work a specific hardware vendor", func(cmd *cli.Cmd) { - var hv HardwareVendor - idArg := cmd.StringArg("NAME", "", "The name, or UUID of the hardware vendor.") - - // grab the Vendor for the given ID - cmd.Before = func() { - hv = API.Hardware().GetVendor(*idArg) - if (hv == HardwareVendor{}) { - panic("Hardware Vendor not found for " + *idArg) - } - } - - cmd.Command("get", "Show a hardware vendor's details", func(cmd *cli.Cmd) { - cmd.Action = func() { fmt.Println(hv) } - }) - cmd.Command("delete rm", "Remove a hardware vendor", func(cmd *cli.Cmd) { - cmd.Action = func() { API.Hardware().DeleteVendor(hv.ID) } - }) - }) - }) -} - -func init() { initHardwareCli(App) } diff --git a/hardware_integration_test.go b/hardware_integration_test.go deleted file mode 100644 index 1df8b21..0000000 --- a/hardware_integration_test.go +++ /dev/null @@ -1,129 +0,0 @@ -package main - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestHarwareProductIntegration(t *testing.T) { - defer errorHandler() - - setupAPIClient() - - r := setupRecorder("fixtures/conch-v3/hardware") - defer r() // Make sure recorder is stopped once done with it - - f := newFixture() - f.setupHardwareVendor() - f.setupValidationPlan() - defer f.reset() - - t.Run("API", func(t *testing.T) { - var testHardwareProduct HardwareProduct - var testHardwareProductSummary HardwareProductSummary - - t.Run("create", func(t *testing.T) { - defer errorHandler() - mock := newTestHardwareProduct() - testHardwareProduct = API.Hardware().Create( - mock.Name, - mock.Alias, - f.hardwareVendor.ID, - mock.SKU, - mock.RackUnitSize, - f.validationPlan.ID, - mock.Purpose, - mock.BiosFirmware, - mock.CpuType, - ) - - testHardwareProductSummary = HardwareProductSummary{ - ID: testHardwareProduct.ID, - Name: testHardwareProduct.Name, - Alias: testHardwareProduct.Alias, - GenerationName: testHardwareProduct.GenerationName, - SKU: testHardwareProduct.SKU, - Created: testHardwareProduct.Created, - Updated: testHardwareProduct.Updated, - } - - assert.NotNil(t, testHardwareProduct.ID) - }) - - t.Run("get all products", func(t *testing.T) { - got := API.Hardware().GetAllProducts() - assert.Equal(t, HardwareProductSummaries{testHardwareProductSummary}, got) - }) - - t.Run("get product by name", func(t *testing.T) { - defer errorHandler() - got := API.Hardware().GetProductByName(testHardwareProduct.Name) - assert.Equal(t, testHardwareProduct, got) - }) - - t.Run("delete product", func(t *testing.T) { - defer errorHandler() - API.Hardware().Delete(testHardwareProduct.ID) - got := API.Hardware().GetAllProducts() - assert.Equal(t, HardwareProductSummaries{}, got) - }) - }) - /* TODO: Figure out timezones - t.Run("cli", func(t *testing.T) { - mock := newTestHardwareProduct() - mock.SKU = "test-sku-001" - mock.Name = "Testy McTesterson" - - cases := []struct { - name string - cli []string - want string - }{ - { - "create", - []string{ - "kosh", "hardware", "products", "create", - "--sku", mock.SKU, - "--name", mock.Name, - "--alias", mock.Alias, - "--vendor", f.hardwareVendor.Name, - "--rack-unit-size", strconv.Itoa(mock.RackUnitSize), - "--validation-plan", f.validationPlan.ID.String(), - "--purpose", mock.Purpose, - "--bios-firmware", mock.BiosFirmware, - "--cpu-type", mock.CpuType, - }, - "\nID: 9ad55ceb-2eb7-4125-a492-3c595277b3e3\nName: Testy McTesterson\nSKU: test-sku-001\n\nCreated: 2020-01-26 19:04:27 +0000 UTC\nUpdated: 2020-01-26 19:04:27 +0000 UTC\n\n", - }, - { - "products ls", - []string{"kosh", "hardware", "products", "ls"}, - "| ID | SKU | NAME | ALIAS | GENERATIONNAME | CREATED | UPDATED |\n|----------|--------------|-------------------|---------------------------|----------------|--------------------------------------|--------------------------------------|\n| 9ad55ceb | test-sku-001 | Testy McTesterson | ujOmocHFAUuWZILajRAzVkeuO | | 2020-01-26 19:04:27.567389 +0000 UTC | 2020-01-26 19:04:27.567389 +0000 UTC |\n\n", - }, - { - "product SKU get", - []string{"kosh", "hardware", "product", mock.SKU, "get"}, - "\nID: 9ad55ceb-2eb7-4125-a492-3c595277b3e3\nName: Testy McTesterson\nSKU: test-sku-001\n\nCreated: 2020-01-26 19:04:27 +0000 UTC\nUpdated: 2020-01-26 19:04:27 +0000 UTC\n\n", - }, - { - "product SKU delete", - []string{"kosh", "hardware", "product", mock.SKU, "rm"}, - HardwareProductSummaries{}.String() + "\n", - }, - } - for _, cas := range cases { - defer errorHandler() - t.Run(cas.name, func(t *testing.T) { - defer errorHandler() - t.Logf("Testing %+v", cas.cli) - app := cli.App("kosh", "Command line interface for Conch") - initHardwareCli(app) - got := captureOutput(func() { app.Run(cas.cli) }) - assert.Equal(t, cas.want, got) - }) - } - }) - */ - -} diff --git a/hardware_test.go b/hardware_test.go deleted file mode 100644 index 836eece..0000000 --- a/hardware_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestHardwareProductCreate(t *testing.T) { - spy := requestSpy{} - var got HardwareProduct - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - body, _ := ioutil.ReadAll(r.Body) - assertJSONSchema(t, body, "request/HardwareProductCreate") - json.NewDecoder(bytes.NewBuffer(body)).Decode(&got) - json.NewEncoder(w).Encode(got) - })) - defer server.Close() - - API.URL = server.URL - - h := API.Hardware() - - mock := newTestHardwareProduct() - want := h.Create( - mock.Name, - mock.Alias, - mock.HardwareVendorID, - mock.SKU, - mock.RackUnitSize+1, - mock.ValidationPlanID, - mock.Purpose, - mock.BiosFirmware, - mock.CpuType, - ) - - assertRequestCount(t, spy.requestCount, 1) - assertRequestPath(t, spy.requestPath, "/hardware_product") - assertRequestMethod(t, spy.requestMethod, "POST") - assert.Equal(t, got, want) -} - -func TestHardwareProductDelete(t *testing.T) { - spy := requestSpy{} - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - w.WriteHeader(204) - - })) - defer server.Close() - - API.URL = server.URL - - hp := newTestHardwareProduct() - h := API.Hardware() - - h.Delete(hp.ID) - - assertRequestMethod(t, spy.requestMethod, "DELETE") - assertRequestCount(t, spy.requestCount, 1) - assertRequestPath(t, spy.requestPath, fmt.Sprintf("/hardware_product/%s", hp.ID)) -} - -func TestHardwareVendorCreate(t *testing.T) { - spy := requestSpy{} - - mock := newTestHardwareVendor() - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - json.NewEncoder(w).Encode(mock) - })) - defer server.Close() - - API.URL = server.URL - - h := API.Hardware() - - _ = h.CreateVendor(mock.Name) - - assertRequestCount(t, spy.requestCount, 2) - assertRequestPath(t, spy.requestPath, fmt.Sprintf("/hardware_vendor/%s", mock.Name)) - assertRequestMethod(t, spy.requestMethod, "GET") -} - -func TestHardwareVendorDelete(t *testing.T) { - spy := requestSpy{} - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - w.WriteHeader(204) - - })) - defer server.Close() - - API.URL = server.URL - - hv := newTestHardwareVendor() - h := API.Hardware() - - h.DeleteVendor(hv.ID) - - assertRequestMethod(t, spy.requestMethod, "DELETE") - assertRequestCount(t, spy.requestCount, 1) - assertRequestPath(t, spy.requestPath, fmt.Sprintf("/hardware_vendor/%s", hv.ID)) -} diff --git a/integration_test.go b/integration_test.go deleted file mode 100644 index 4ec0339..0000000 --- a/integration_test.go +++ /dev/null @@ -1,99 +0,0 @@ -package main - -import ( - "net/http" - "net/url" - "os" - - "github.com/dnaeon/go-vcr/cassette" - "github.com/dnaeon/go-vcr/recorder" -) - -func setupAPIClient() { - Version = "3.x-testing" - API.URL = os.Getenv("KOSH_URL") - API.Token = os.Getenv("KOSH_TOKEN") - API.StrictParsing = true - API.DevelMode = true - - if _, err := os.Stat("fixtures/conch-v3"); err == nil { - return - } else if os.IsNotExist(err) { - if API.Token == "" { - panic("Must supply a token in KOSH_TOKEN") - } - } else { - panic(err) - } -} - -func setupRecorder(fixture string) func() { - defer errorHandler() - // TODO: we need to re-think the test fixtures entirely - r, err := recorder.New(fixture) - if err != nil { - panic(err) - } - - // strip out our authentication headers - r.AddFilter(func(i *cassette.Interaction) error { - delete(i.Request.Headers, "Authorization") - return nil - }) - - // ignore hostnames when fetching from the casset - r.SetMatcher(func(r *http.Request, i cassette.Request) bool { - iURL, _ := url.Parse(i.URL) - return r.Method == i.Method && r.URL.Path == iURL.Path - }) - - oldClient := API.HTTP - API.HTTP = &http.Client{ - Transport: r, // Inject as transport! - } - - return func() { - API.HTTP = oldClient - r.Stop() - } -} - -/* -func capture() func() (string, error) { - r, w, err := os.Pipe() - if err != nil { - panic(err) - } - - done := make(chan error, 1) - - save := os.Stdout - os.Stdout = w - - var buf strings.Builder - - go func() { - _, err := io.Copy(&buf, r) - r.Close() - done <- err - }() - - return func() (string, error) { - os.Stdout = save - w.Close() - err := <-done - return buf.String(), err - } - -} - -func captureOutput(f func()) string { - done := capture() - f() - out, err := done() - if err != nil { - panic(err) - } - return out -} -*/ diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..b917f52 --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,55 @@ +package logger + +import ( + "fmt" + "log" + "net/http" + "net/http/httputil" +) + +type Interface interface { + Deubg(...interface{}) + Info(...interface{}) +} + +type Logger struct { + LevelDebug bool + LevelInfo bool +} + +func New() Logger { return Logger{} } + +func (l Logger) Debug(messages ...interface{}) { + if l.LevelDebug { + for _, m := range messages { + if m == nil { + continue + } + switch t := m.(type) { + case *http.Request: + dump, e := httputil.DumpRequestOut(t, true) + if e != nil { + log.Println("Got error:", e) + } + log.Println("Request:", string(dump)) + case *http.Response: + dump, e := httputil.DumpResponse(t, true) + if e != nil { + l.Debug("Dump Response Error:", e) + } + l.Debug("Response:", string(dump)) + + default: + log.Println(t) + } + } + } +} + +func (l Logger) Info(messages ...interface{}) { + if l.LevelInfo { + for _, m := range messages { + fmt.Println(m) + } + } +} diff --git a/main.go b/main.go index 9473e10..73fbeec 100644 --- a/main.go +++ b/main.go @@ -1,192 +1,20 @@ -// Copyright Joyent, Inc. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - package main -//lint:file-ignore U1000 WIP - import ( - "fmt" "os" - "os/user" - "regexp" - "runtime" - "text/template" - "time" - "github.com/gofrs/uuid" - cli "github.com/jawher/mow.cli" - "github.com/olekukonko/tablewriter" -) - -const ( - ProductionURL = "https://conch.joyent.us" - StagingURL = "https://staging.conch.joyent.us" - DateFormat = "2006-01-02 15:04:05 -0700 MST" + "github.com/joyent/kosh/cli" ) var ( + // Version holds the current version of the app, injected as part of the build process Version string - GitRev string - API = &Conch{} - App = cli.App("kosh", "Command line interface for Conch") + // GitRev holds the current Git Revision, injected as part of the build process + GitRev string ) -func buildUserAgent() map[string]string { - var isRoot bool - if current, err := user.Current(); err == nil { - if current.Uid == "0" { - isRoot = true - } - } - - agentBits := make(map[string]string) - agent := fmt.Sprintf( - "%s (%s; %s; r=%v)", - GitRev, - runtime.GOOS, - runtime.GOARCH, - isRoot, - ) - - agentBits["Kosh"] = agent - return agentBits -} - -func TimeStr(t time.Time) string { - if t.IsZero() { - return "" - } - return t.Local().Format(DateFormat) -} - -func CutUUID(id string) string { - re := regexp.MustCompile("^(.+?)-") - bits := re.FindStringSubmatch(id) - if len(bits) > 0 { - return bits[1] - } - return id -} - -func FindUUID(id string, list []uuid.UUID) (bool, uuid.UUID) { - re := regexp.MustCompile(fmt.Sprintf("^%s", id)) - for _, item := range list { - if re.MatchString(item.String()) { - return true, item - } - } - return false, uuid.UUID{} -} - -func IsSysAdmin() bool { - return API.Users().Me().IsAdmin -} - -func RequireSysAdmin() { - if !IsSysAdmin() { - panic("This action requires Conch systems administrator privileges") - } -} - -/***/ - -func init() { - tokenOpt := App.String(cli.StringOpt{ - Name: "token", - Value: "", - Desc: "API token", - EnvVar: "KOSH_TOKEN", - }) - - environmentOpt := App.String(cli.StringOpt{ - Name: "environment env", - Value: "production", - Desc: "Specify the environment to be used: production, staging, development (provide URL in the --url parameter)", - EnvVar: "KOSH_ENV", - }) - - urlOpt := App.String(cli.StringOpt{ - Name: "url", - Value: "", - Desc: "If the environment is 'development', this specifies the API URL. Ignored if --environment is 'production' or 'staging'", - EnvVar: "KOSH_URL", - }) - - jsonOnlyOpt := App.Bool(cli.BoolOpt{ - Name: "json", - Value: false, - Desc: "Output JSON only", - EnvVar: "KOSH_JSON_ONLY", - }) - - strictParseOpt := App.Bool(cli.BoolOpt{ - Name: "strict", - Value: false, - Desc: "Intended for developers. Parse API responses strictly, not allowing new fields", - EnvVar: "KOSH_DEVEL_STRICT", - }) - - develOpt := App.Bool(cli.BoolOpt{ - Name: "developer", - Value: false, - Desc: "Activate developer mode. This disables most user-friendly protections, is noisy, and switches to developer-friendly output where appropriate", - EnvVar: "KOSH_DEVEL_MODE", - }) - - App.Before = func() { - if len(*environmentOpt) > 0 { - if (*environmentOpt == "development") && (len(*urlOpt) == 0) { - panic("--url must be provided if --environment=development is set") - } - } - - switch *environmentOpt { - case "staging": - API.URL = StagingURL - case "development": - API.URL = *urlOpt - default: - API.URL = ProductionURL - } - - if *tokenOpt == "" { - panic("please provide a token") - } - - API.JsonOnly = *jsonOnlyOpt - API.Token = *tokenOpt - API.UserAgent = buildUserAgent() - API.StrictParsing = *strictParseOpt - API.DevelMode = *develOpt - } - - App.Version("version", Version) - - App.Command( - "version", - "Get more detailed version info than --version", - func(cmd *cli.Cmd) { - - cmd.Action = func() { - fmt.Printf( - "Kosh %s\n"+ - " Git Revision: %s\n", - Version, - GitRev, - ) - } - }, - ) - -} - func main() { - defer errorHandler() - // BUG(sungo): github version check foo - _ = App.Run(os.Args) + app := cli.NewApp(cli.NewConfig(Version, GitRev)) + app.Run(os.Args) } diff --git a/organizations.go b/organizations.go deleted file mode 100644 index 38cbff9..0000000 --- a/organizations.go +++ /dev/null @@ -1,380 +0,0 @@ -package main - -import ( - "fmt" - "net/url" - "sort" - "strings" - "time" - - "github.com/gofrs/uuid" - cli "github.com/jawher/mow.cli" - "github.com/joyent/kosh/tables" - "github.com/joyent/kosh/template" -) - -type Organizations struct { - *Conch -} - -func (c *Conch) Organizations() *Organizations { - return &Organizations{c} -} - -type Org struct { - ID uuid.UUID `json:"id" faker:"uuid"` - Name string `json:"name"` - Description string `json:"description"` - Created time.Time `json:"created" faker:"-"` - Admins UserAndRoles `json:"admins" faker:"-"` - Builds BuildList `json:"builds" faker:"-"` - Users UserAndRoles `json:"users" faker:"-"` -} - -type OrgAndRole struct { - Org - Role string `json:"role"` -} - -type OrgAndRoles []OrgAndRole - -func (o OrgAndRoles) Len() int { - return len(o) -} - -func (o OrgAndRoles) Swap(i, j int) { - o[i], o[j] = o[j], o[i] -} - -func (o OrgAndRoles) Less(i, j int) bool { - return o[i].Name < o[j].Name -} - -func (o OrgAndRoles) String() string { - sort.Sort(o) - if API.JsonOnly { - return API.AsJSON(o) - } - - tableString := &strings.Builder{} - table := tables.NewTable(tableString) - tables.TableToMarkdown(table) - - table.SetHeader([]string{ - "Name", - "Role", - "Description", - }) - - for _, org := range o { - table.Append([]string{ - org.Name, - org.Role, - org.Description, - }) - } - - table.Render() - return tableString.String() -} - -type OrgList []Org - -func (o OrgList) Len() int { - return len(o) -} - -func (o OrgList) Swap(i, j int) { - o[i], o[j] = o[j], o[i] -} - -func (o OrgList) Less(i, j int) bool { - return o[i].Name < o[j].Name -} - -func (o OrgList) String() string { - sort.Sort(o) - if API.JsonOnly { - return API.AsJSON(o) - } - - tableString := &strings.Builder{} - table := tables.NewTable(tableString) - tables.TableToMarkdown(table) - - table.SetHeader([]string{ - "Name", - "Role", - "Description", - }) - - for _, org := range o { - table.Append([]string{ - org.Name, - org.Description, - }) - } - - table.Render() - return tableString.String() -} - -func (o Org) String() string { - if API.JsonOnly { - return API.AsJSON(o) - } - - t, err := template.NewTemplate().Parse(organizationTemplate) - if err != nil { - panic(err) - } - - buf := &strings.Builder{} - - if err := t.Execute(buf, o); err != nil { - panic(err) - } - - return buf.String() -} - -func (o *Organizations) GetAll() (list OrgList) { - res := o.Do(o.Sling().Get("/organization")) - if ok := res.Parse(&list); !ok { - panic(res) - } - return list -} - -func (o *Organizations) Get(ID uuid.UUID) Org { - var org Org - uri := fmt.Sprintf("/organization/%s", url.PathEscape(ID.String())) - res := o.Do(o.Sling().Get(uri)) - if ok := res.Parse(&org); !ok { - panic(res) - } - return org -} - -func (o *Organizations) GetByName(name string) Org { - var org Org - uri := fmt.Sprintf("/organization/%s", url.PathEscape(name)) - res := o.Do(o.Sling().Get(uri)) - if ok := res.Parse(&org); !ok { - panic(res) - } - return org -} - -func (o *Organizations) Create(name, description string, admins []map[string]string) (org Org) { - payload := make(map[string]interface{}) - payload["name"] = name - payload["admins"] = admins - if description != "" { - payload["description"] = description - } - - res := o.Do(o.Sling().New().Post("/organization"). - Set("Content-Type", "application/json"). - BodyJSON(payload), - ) - - if ok := res.Parse(&org); !ok { - panic(res) - } - - return -} - -func (o *Organizations) Delete(ID uuid.UUID) { - uri := fmt.Sprintf("/organization/%s", url.PathEscape(ID.String())) - _ = o.Do(o.Sling().Delete(uri)) -} - -type OrganizationUser struct { - ID uuid.UUID `json:"id" faker:"uuid"` - Email string `json:"email" faker:"email"` - Name string `json:"name"` - Role string `json:"role"` -} - -func (o *Organizations) GetUsers(ID uuid.UUID) (users []OrganizationUser) { - uri := fmt.Sprintf("/organization/%s/user", url.PathEscape(ID.String())) - res := o.Do(o.Sling().Get(uri)) - if ok := res.Parse(&users); !ok { - panic(res) - } - return -} - -func (o *Organizations) AddUser(orgID uuid.UUID, email, role string, sendEmail bool) { - uri := fmt.Sprintf("/organization/%s/user", url.PathEscape(orgID.String())) - - payload := make(map[string]string) - payload["email"] = email - payload["role"] = role - - send := 0 - if sendEmail { - send = 1 - } - q := struct { - SendEmail int `url:"send_mail"` - }{send} - - _ = o.Do( - o.Sling().Post(uri). - Set("Content-Type", "application/json"). - QueryStruct(q). - BodyJSON(payload), - ) - -} - -// userID is a string because it may be a UUID or an Email, the API accepts both -func (o *Organizations) RemoveUser(orgID uuid.UUID, userID string, sendEmail bool) bool { - uri := fmt.Sprintf( - "/organization/%s/user/%s", - url.PathEscape(orgID.String()), - url.PathEscape(userID), - ) - - send := 0 - if sendEmail { - send = 1 - } - q := struct { - SendEmail int `url:"send_mail"` - }{send} - - res := o.Do(o.Sling().Delete(uri).QueryStruct(q)) - - return res.StatusCode() == 204 -} - -func init() { - - App.Command("organizations orgs", "Work with organizations", func(cmd *cli.Cmd) { - cmd.Command("get ls", "Get a list of all organizations", func(cmd *cli.Cmd) { - cmd.Action = func() { - fmt.Println(API.Organizations().GetAll()) - } - }) - - cmd.Command("create", "Create a new organization", func(cmd *cli.Cmd) { - nameArg := cmd.StringArg("NAME", "", "Name of the new organization") - - descOpt := cmd.StringOpt("description", "", "A description of the organization") - adminEmailArg := cmd.StringOpt( - "admin", - "", - "Email address for the (initial) admin user for the organization. This does *not* create the user.", - ) - - cmd.Spec = "NAME [OPTIONS]" - cmd.Action = func() { - API.Organizations().Create( - *nameArg, - *descOpt, - []map[string]string{{"email": *adminEmailArg}}, - ) - } - }) - - }) - - App.Command("organization org", "Work with a specific organization", func(cmd *cli.Cmd) { - var o Org - organizationNameArg := cmd.StringArg("NAME", "", "Name or ID of the Organization") - - cmd.Spec = "NAME" - cmd.Before = func() { - o = API.Organizations().GetByName(*organizationNameArg) - // TODO(sungo): should we verify that the organization exists? - } - - cmd.Command("get", "Get information about a single organization by its name", func(cmd *cli.Cmd) { - - cmd.Action = func() { - fmt.Println(o) - } - }) - - cmd.Command("delete rm", "Remove a specific organization", func(cmd *cli.Cmd) { - - cmd.Action = func() { - API.Organizations().Delete(o.ID) - } - }) - - cmd.Command("users", "Manage users in a specific organization", func(cmd *cli.Cmd) { - - cmd.Command("get ls", "Get a list of users in an organization", func(cmd *cli.Cmd) { - cmd.Action = func() { - fmt.Println(API.Organizations().GetUsers(o.ID)) - } - }) - - cmd.Command("add", "Add a user to an organization", func(cmd *cli.Cmd) { - userEmailArg := cmd.StringArg( - "EMAIL", - "", - "The email of the user to add to the organization. Does *not* create the user", - ) - - roleOpt := cmd.StringOpt( - "role", - "ro", - "The role for the user. One of: "+prettyBuildRoleList(), - ) - - sendEmailOpt := cmd.BoolOpt( - "send-email", - true, - "Send email to the target user, notifying them of the change", - ) - - cmd.Spec = "EMAIL [OPTIONS]" - cmd.Action = func() { - if !okBuildRole(*roleOpt) { - panic(fmt.Errorf( - "'role' value must be one of: %s", - prettyBuildRoleList(), - )) - } - API.Organizations().AddUser( - o.ID, - *userEmailArg, - *roleOpt, - *sendEmailOpt, - ) - fmt.Println(API.Organizations().GetUsers(o.ID)) - } - - }) - - cmd.Command("remove rm", "remove a user from an organization", func(cmd *cli.Cmd) { - userEmailArg := cmd.StringArg( - "EMAIL", - "", - "The email or ID of the user to modify", - ) - - sendEmailOpt := cmd.BoolOpt( - "send-email", - true, - "Send email to the target user, notifying them of the change", - ) - cmd.Spec = "EMAIL [OPTIONS]" - cmd.Action = func() { - API.Organizations().RemoveUser( - o.ID, - *userEmailArg, - *sendEmailOpt, - ) - fmt.Println(API.Organizations().GetUsers(o.ID)) - } - }) - }) - }) -} diff --git a/organizations_integration_test.go b/organizations_integration_test.go deleted file mode 100644 index e8a03de..0000000 --- a/organizations_integration_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package main - -import ( - "testing" -) - -var org Org - -func TestOrganizationAPIIntegration(t *testing.T) { - setupAPIClient() - r := setupRecorder("fixtures/conch-v3/organizations") - defer r() // Make sure recorder is stopped once done with it - - t.Run("create", func(t *testing.T) { - mock := newTestOrganization() - org = API.Organizations().Create( - mock.Name, - mock.Description, - []map[string]string{{"email": "conch@example.com"}}, - ) - }) - - t.Run("get-all", func(t *testing.T) { - _ = API.Organizations().GetAll() - }) - - t.Run("get-one", func(t *testing.T) { - _ = API.Organizations().Get(org.ID) - }) - - t.Run("delete", func(t *testing.T) { - API.Organizations().Delete(org.ID) - }) -} diff --git a/organizations_test.go b/organizations_test.go deleted file mode 100644 index 07a978f..0000000 --- a/organizations_test.go +++ /dev/null @@ -1,258 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestOrganizationsGet(t *testing.T) { - orgList := newTestOrgList() - spy := requestSpy{} - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - orgID := r.URL.Path[len("/organization/"):] - for _, org := range orgList { - if org.ID.String() == orgID { - json.NewEncoder(w).Encode(org) - return - } - } - })) - defer server.Close() - - API.URL = server.URL - o := API.Organizations() - - for i, org := range orgList { - t.Run(org.Name, func(t *testing.T) { - got := o.Get(org.ID) - - assertRequestMethod(t, spy.requestMethod, "GET") - assertRequestCount(t, spy.requestCount, i+1) - assertRequestPath(t, spy.requestPath, fmt.Sprintf("/organization/%s", org.ID)) - assert.Equal(t, got, org) - }) - } - -} - -func TestOrganizationsGetByName(t *testing.T) { - orgList := newTestOrgList() - spy := requestSpy{} - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - orgName := r.URL.Path[len("/organization/"):] - for _, org := range orgList { - if org.Name == orgName { - json.NewEncoder(w).Encode(org) - return - } - } - })) - defer server.Close() - - API.URL = server.URL - o := API.Organizations() - - for i, org := range orgList { - t.Run(org.Name, func(t *testing.T) { - got := o.GetByName(org.Name) - - assertRequestMethod(t, spy.requestMethod, "GET") - assertRequestCount(t, spy.requestCount, i+1) - assertRequestPath(t, spy.requestPath, fmt.Sprintf("/organization/%s", org.Name)) - assert.Equal(t, got, org) - }) - } - -} - -func TestOrganizationsGetAll(t *testing.T) { - orgList := newTestOrgList() - spy := requestSpy{} - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - json.NewEncoder(w).Encode(orgList) - })) - defer server.Close() - - API.URL = server.URL - - o := API.Organizations() - - got := o.GetAll() - - assertRequestCount(t, spy.requestCount, 1) - assertRequestPath(t, spy.requestPath, "/organization") - assert.Equal(t, got, orgList) -} - -func TestOrganizationsCreate(t *testing.T) { - spy := requestSpy{} - var got Org - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - body, _ := ioutil.ReadAll(r.Body) - assertJSONSchema(t, body, "request/OrganizationCreate") - json.NewDecoder(bytes.NewBuffer(body)).Decode(&got) - json.NewEncoder(w).Encode(got) - })) - defer server.Close() - - API.URL = server.URL - - o := API.Organizations() - - want := o.Create("Z", "Z Org", []map[string]string{{"email": "admin@example.com"}}) - - assertRequestCount(t, spy.requestCount, 1) - assertRequestPath(t, spy.requestPath, "/organization") - assertRequestMethod(t, spy.requestMethod, "POST") - if got.Name != "Z" { - t.Errorf("Invalid requestBody. Got %v expected something with name Z", got) - } - - // now let's check what we made in the server is what we got in the client - assert.Equal(t, got, want) -} - -func TestOrganizationsDelete(t *testing.T) { - orgList := newTestOrgList() - org := orgList[0] - spy := requestSpy{} - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - })) - defer server.Close() - - API.URL = server.URL - - o := API.Organizations() - - o.Delete(org.ID) - - assertRequestMethod(t, spy.requestMethod, "DELETE") - assertRequestCount(t, spy.requestCount, 1) - assertRequestPath(t, spy.requestPath, fmt.Sprintf("/organization/%s", org.ID)) -} - -func TestOrganizationsGetUsers(t *testing.T) { - spy := requestSpy{} - org := newTestOrganization() - userList := []OrganizationUser{ - newTestOrganizationUser(), - } - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - json.NewEncoder(w).Encode(userList) - })) - defer server.Close() - - API.URL = server.URL - - o := API.Organizations() - - got := o.GetUsers(org.ID) - - assertRequestMethod(t, spy.requestMethod, "GET") - assertRequestCount(t, spy.requestCount, 1) - assertRequestPath(t, spy.requestPath, fmt.Sprintf("/organization/%s/user", org.ID)) - assert.Equal(t, got, userList) -} - -func TestOrganizationsAddUser(t *testing.T) { - spy := requestSpy{} - org := newTestOrganization() - user := newTestOrganizationUser() - var got OrganizationUser - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - body, _ := ioutil.ReadAll(r.Body) - assertJSONSchema(t, body, "request/OrganizationAddUser") - json.NewDecoder(bytes.NewBuffer(body)).Decode(&got) - })) - defer server.Close() - - API.URL = server.URL - - o := API.Organizations() - - o.AddUser(org.ID, user.Email, "admin", false) - - assertRequestMethod(t, spy.requestMethod, "POST") - assertRequestCount(t, spy.requestCount, 1) - assertRequestPath(t, spy.requestPath, fmt.Sprintf("/organization/%s/user", org.ID)) -} - -func TestOrganizationsRemoveUser(t *testing.T) { - spy := requestSpy{} - org := newTestOrganization() - user := newTestOrganizationUser() - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - })) - defer server.Close() - - API.URL = server.URL - - o := API.Organizations() - - o.RemoveUser(org.ID, user.Email, false) - - assertRequestMethod(t, spy.requestMethod, "DELETE") - assertRequestCount(t, spy.requestCount, 1) - assertRequestPath(t, spy.requestPath, fmt.Sprintf("/organization/%s/user/%s", org.ID, user.Email)) -} - -/* - Helpers -*/ - -type requestSpy struct { - requestCount int - requestPath string - requestMethod string -} - -func (rs *requestSpy) onRequest(r *http.Request) { - rs.requestCount++ - rs.requestMethod = r.Method - rs.requestPath = r.URL.Path -} - -func assertRequestMethod(t *testing.T, got, want string) { - t.Helper() - if got != want { - t.Errorf("Request method wrong, got %s wanted %s", got, want) - } - -} - -func assertRequestPath(t *testing.T, got, want string) { - t.Helper() - if got != want { - t.Errorf("Request path wrong, got %s wanted %s", got, want) - } -} - -func assertRequestCount(t *testing.T, got, want int) { - t.Helper() - if got != want { - t.Errorf("Wrong number of requests, got %d wanted %d", got, want) - } -} diff --git a/racks.go b/racks.go deleted file mode 100644 index a082c7d..0000000 --- a/racks.go +++ /dev/null @@ -1,888 +0,0 @@ -// Copyright Joyent, Inc. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package main - -//lint:file-ignore U1000 WIP - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "io/ioutil" - "net/url" - "os" - "sort" - "strconv" - "strings" - "time" - - "github.com/gofrs/uuid" - cli "github.com/jawher/mow.cli" - "github.com/joyent/kosh/tables" - "github.com/joyent/kosh/template" -) - -type Racks struct { - *Conch -} - -func (c *Conch) Racks() *Racks { - return &Racks{c} -} - -type RackList []Rack -type Rack struct { - ID uuid.UUID `json:"id" faker:"uuid"` - Name string `json:"name"` - FullName string `json:"full_rack_name"` - RoomID uuid.UUID `json:"datacenter_room_id" faker:"uuid"` - RoomAlias string `json:"datacenter_room_alias"` - RoleID uuid.UUID `json:"rack_role_id" faker:"uuid"` - RoleName string `json:"rack_role_name"` - SerialNumber string `json:"serial_number,omitempty"` - AssetTag string `json:"asset_tag,omitempty"` - Phase string `json:"phase"` - Created time.Time `json:"created" faker:"-"` - Updated time.Time `json:"updated" faker:"-"` - BuildID uuid.UUID `json:"build_id" faker:"uuid"` - BuildName string `json:"build_name"` - - Role RackRole `json:"-" faker:"-"` - Room Room `json:"-" faker:"-"` -} - -func (rl RackList) String() string { - if API.JsonOnly { - return API.AsJSON(rl) - } - - tableString := &strings.Builder{} - table := tables.NewTable(tableString) - tables.TableToMarkdown(table) - - table.SetHeader([]string{ - "ID", - "Name", - "Room", - "Role", - "Serial Number", - "Asset Tag", - "Phase", - "Created", - "Updated", - }) - - for _, r := range rl { - var role string - if (r.RoleID != uuid.UUID{}) { - role = r.Role.Name - } - - var room string - if (r.RoomID != uuid.UUID{}) { - room = r.Room.Alias - } - - table.Append([]string{ - template.CutUUID(r.ID.String()), - r.Name, - room, - role, - r.SerialNumber, - r.AssetTag, - r.Phase, - template.TimeStr(r.Created), - template.TimeStr(r.Updated), - }) - } - - table.Render() - return tableString.String() - -} - -func (r Rack) String() string { - if API.JsonOnly { - return API.AsJSON(r) - } - - t, err := template.NewTemplate().Parse(rackTemplate) - if err != nil { - panic(err) - } - - buf := new(bytes.Buffer) - if err := t.Execute(buf, r); err != nil { - panic(err) - } - - return buf.String() -} - -func (r *Racks) FindID(id string) (bool, uuid.UUID) { - rack := r.GetByName(id) - return true, rack.ID -} - -func (r *Racks) GetByName(name string) Rack { - var rack Rack - uri := fmt.Sprintf( - "/rack/%s", - url.PathEscape(name), - ) - - res := r.Do(r.Sling().Get(uri)) - if ok := res.Parse(&rack); !ok { - panic(res) - } - - if (rack.RoleID != uuid.UUID{}) { - rack.Role = API.RackRoles().Get(rack.RoleID) - } - - if (rack.RoomID != uuid.UUID{}) { - rack.Room = API.Rooms().Get(rack.RoomID) - } - - return rack -} - -func (r *Racks) Get(id uuid.UUID) Rack { - var rack Rack - uri := fmt.Sprintf( - "/rack/%s", - url.PathEscape(id.String()), - ) - - res := r.Do(r.Sling().Get(uri)) - if ok := res.Parse(&rack); !ok { - panic(res) - } - - if (rack.RoleID != uuid.UUID{}) { - rack.Role = API.RackRoles().Get(rack.RoleID) - } - - if (rack.RoomID != uuid.UUID{}) { - rack.Room = API.Rooms().Get(rack.RoomID) - } - - return rack -} - -func (r *Racks) CreateFromStruct(rack Rack) Rack { - return r.Create(rack.Name, rack.RoomID, rack.RoleID, rack.Phase, rack.BuildID) -} - -func (r *Racks) Create(name string, roomID uuid.UUID, roleID uuid.UUID, phase string, buildID uuid.UUID) Rack { - payload := make(map[string]string) - if name == "" { - panic(errors.New("'name' cannot be empty")) - } - payload["name"] = name - - if (roomID == uuid.UUID{}) { - panic(errors.New("'roomID' cannot be empty")) - } - payload["datacenter_room_id"] = roomID.String() - - if (roleID == uuid.UUID{}) { - panic(errors.New("'roleID' cannot be empty")) - } - payload["rack_role_id"] = roleID.String() - - if (buildID == uuid.UUID{}) { - panic(errors.New("'buildID' cannot be empty'")) - } - payload["build_id"] = buildID.String() - - if phase != "" { - payload["phase"] = phase - } - - var rack Rack - - // We get a 303 on success - res := r.Do( - r.Sling().New().Post("/rack"). - Set("Content-Type", "application/json"). - BodyJSON(payload), - ) - - if ok := res.Parse(&rack); !ok { - panic(res) - } - - if (rack.RoleID != uuid.UUID{}) { - rack.Role = API.RackRoles().Get(rack.RoleID) - } - - return rack -} - -func (r *Racks) Update( - id uuid.UUID, - newName string, - roomID uuid.UUID, - roleID uuid.UUID, - phase string, - serialNumber *string, - assetTag *string, -) Rack { - - payload := make(map[string]interface{}) - if newName != "" { - payload["name"] = newName - } - - if (roomID != uuid.UUID{}) { - payload["datacenter_room_id"] = roomID.String() - } - - if (roleID != uuid.UUID{}) { - payload["rack_role_id"] = roleID.String() - } - - if phase != "" { - payload["phase"] = phase - } - - if serialNumber == nil { - payload["serial_number"] = nil - } else if *serialNumber != "" { - payload["serial_number"] = *serialNumber - } - - if assetTag == nil { - payload["asset_tag"] = nil - } else if *assetTag != "" { - payload["asset_tag"] = *assetTag - } - - if len(payload) == 0 { - return r.Get(id) - } - - var rack Rack - - uri := fmt.Sprintf( - "/rack/%s", - url.PathEscape(id.String()), - ) - - // We get a 303 on success - res := r.Do( - r.Sling().New().Post(uri). - Set("Content-Type", "application/json"). - BodyJSON(payload), - ) - - if ok := res.Parse(&rack); !ok { - panic(res) - } - - if (rack.RoleID != uuid.UUID{}) { - rack.Role = API.RackRoles().Get(rack.RoleID) - } - - return rack -} - -func (r *Racks) Delete(id uuid.UUID) { - uri := fmt.Sprintf( - "/rack/%s", - url.PathEscape(id.String()), - ) - - res := r.Do(r.Sling().New().Delete(uri)) - - if res.StatusCode() != 204 { - // I know this is weird. Like in other places, it should be impossible - // to reach here unless the status code is 204. The API returns 204 - // (which gets us here) or 409 (which will explode before it gets here). - // If we got here via some other code, then there's some new behavior - // that we need to know about. - - panic(res) - } -} - -/****/ -type RackLayoutSlot struct { - ID uuid.UUID `json:"id" faker:"uuid"` - RackID uuid.UUID `json:"rack_id" faker:"uuid"` - RackName string `json:"rack_name"` - SKU string `json:"sku"` - HardwareProductID uuid.UUID `json:"hardware_product_id" faker:"uuid"` - RackUnitStart int `json:"rack_unit_start" faker:"rack_unit_start"` - RackUnitSize int `json:"rack_unit_size" faker:"rack_unit_size"` - Created time.Time `json:"created" faker:"-"` - Updated time.Time `json:"updated" faker:"-"` -} - -type RackLayout []RackLayoutSlot - -func (r RackLayout) Len() int { - return len(r) -} - -func (r RackLayout) Swap(i, j int) { - r[i], r[j] = r[j], r[i] -} - -func (r RackLayout) Less(i, j int) bool { - return r[i].RackUnitStart > r[j].RackUnitStart -} - -func (rl RackLayout) String() string { - sort.Sort(rl) - if API.JsonOnly { - return API.AsJSON(rl) - } - - tableString := &strings.Builder{} - table := tables.NewTable(tableString) - tables.TableToMarkdown(table) - - table.SetHeader([]string{ - "Rack Unit Start", - "Rack Unit Size", - "ID", - "Hardware Product", - "Created", - "Updated", - }) - - products := make(map[uuid.UUID]HardwareProduct) - - for _, r := range rl { - var hpName = "" - if (r.HardwareProductID != uuid.UUID{}) { - var hp HardwareProduct - if _, ok := products[r.HardwareProductID]; ok { - hp = products[r.HardwareProductID] - } else { - hp = API.Hardware().GetProduct(r.HardwareProductID) - products[r.HardwareProductID] = hp - } - - hpName = fmt.Sprintf( - "%s (%s)", - hp.Alias, - hp.Name, - ) - - } - table.Append([]string{ - strconv.Itoa(r.RackUnitStart), - strconv.Itoa(r.RackUnitSize), - template.CutUUID(r.ID.String()), - hpName, - template.TimeStr(r.Created), - template.TimeStr(r.Updated), - }) - } - - table.Render() - return tableString.String() -} - -func (rl RackLayout) Export() string { - type Slot struct { - RU int `json:"ru_start"` - ProductID uuid.UUID `json:"product_id,omitempty" faker:"uuid"` - ProductName string `json:"product_name,omitempty"` - ProductAlias string `json:"product_alias,omitempty"` - } - slots := make([]Slot, 0) - - sort.Sort(rl) - - hpCache := make(map[uuid.UUID]HardwareProduct) - - for _, slot := range rl { - if _, ok := hpCache[slot.HardwareProductID]; !ok { - hpCache[slot.HardwareProductID] = API.Hardware().GetProduct(slot.HardwareProductID) - } - - slots = append(slots, Slot{ - slot.RackUnitStart, - slot.HardwareProductID, - hpCache[slot.HardwareProductID].Name, - hpCache[slot.HardwareProductID].Alias, - }) - } - - return API.AsJSON(slots) -} - -type RackLayoutSlotUpdate struct { - RU int `json:"ru_start"` - ProductID uuid.UUID `json:"product_id,omitempty" faker:"uuid"` - ProductName string `json:"product_name,omitempty"` - ProductAlias string `json:"product_alias,omitempty"` -} - -type RackLayoutUpdates []RackLayoutSlotUpdate - -func (r *Racks) ImportLayout(rackID uuid.UUID, b []byte) RackLayout { - - imported := make(RackLayoutUpdates, 0) - if err := json.Unmarshal(b, &imported); err != nil { - panic(err) - } - return r.CreateLayout(rackID, imported) -} - -func (r *Racks) CreateLayout(rackID uuid.UUID, updates RackLayoutUpdates) RackLayout { - - hpCache := make(map[string]HardwareProduct) - slots := make(RackLayoutUpdates, 0) - - for _, row := range updates { - var slot RackLayoutSlotUpdate - - slot.RU = row.RU - slot.ProductID = row.ProductID - if (row.ProductID != uuid.UUID{}) { - slots = append(slots, slot) - continue - } - - if row.ProductName != "" { - if hp, ok := hpCache[row.ProductName]; ok { - hpCache[row.ProductName] = hp - } else { - hpCache[row.ProductName] = API.Hardware().GetProductByName(row.ProductName) - } - slot.ProductID = hpCache[row.ProductName].ID - } else if row.ProductAlias != "" { - if hp, ok := hpCache[row.ProductAlias]; ok { - hpCache[row.ProductAlias] = hp - } else { - hpCache[row.ProductAlias] = API.Hardware().GetProductByAlias(row.ProductAlias) - } - slot.ProductID = hpCache[row.ProductAlias].ID - } else { - panic(fmt.Errorf("RU %d entry does not have a product id, name, or alias", row.RU)) - } - slots = append(slots, slot) - } - - // There is no way to do this atomically. The api has no way to perform - // this action other than deleting each row at a time and then putting them - // back. If this seems really risky to you, then we are of the same mind. - for _, row := range r.Layouts(rackID) { - r.DeleteLayoutSlot(row.ID) - } - for _, slot := range slots { - r.SaveLayoutSlot(rackID, slot.RU, slot.ProductID) - } - - return r.Layouts(rackID) -} - -func (r *Racks) Layouts(id uuid.UUID) RackLayout { - uri := fmt.Sprintf( - "/rack/%s/layout", - url.PathEscape(id.String()), - ) - - layouts := make(RackLayout, 0) - - res := r.Do(r.Sling().New().Get(uri)) - if ok := res.Parse(&layouts); !ok { - panic(res) - } - - return layouts -} - -func (r *Racks) DeleteLayoutSlot(id uuid.UUID) { - uri := fmt.Sprintf( - "/layout/%s", - url.PathEscape(id.String()), - ) - - if res := r.DoBadly(r.Sling().New().Delete(uri)); res.StatusCode() != 204 { - panic(res) - } -} - -func (r *Racks) SaveLayoutSlot(rackID uuid.UUID, ruStart int, hardwareProductID uuid.UUID) (l RackLayoutSlot) { - payload := make(map[string]interface{}) - payload["rack_id"] = rackID.String() - payload["hardware_product_id"] = hardwareProductID.String() - payload["rack_unit_start"] = ruStart - - res := r.Do( - r.Sling().New().Post("/layout"). - Set("Content-Type", "application/json"). - BodyJSON(payload), - ) - if ok := res.Parse(&l); !ok { - panic(res) - } - return l -} - -/****/ - -type RackAssignment struct { - DeviceID uuid.UUID `json:"device_id" faker:"uuid"` - SKU string `json:"sku"` - DeviceSerialNumber string `json:"device_serial_number"` - DeviceAssetTag string `json:"device_asset_tag,omitempty"` - HardwareProductName string `json:"hardware_product_name,omitempty"` - RackUnitStart int `json:"rack_unit_start" faker:"rack_unit_start"` - RackUnitSize int `json:"rack_unit_size" faker:"rack_unit_size"` -} - -type RackAssignments []RackAssignment - -func (r RackAssignments) Len() int { - return len(r) -} - -func (r RackAssignments) Swap(i, j int) { - r[i], r[j] = r[j], r[i] -} - -func (r RackAssignments) Less(i, j int) bool { - return r[i].RackUnitStart > r[j].RackUnitStart -} - -func (a RackAssignments) String() string { - sort.Sort(a) - if API.JsonOnly { - return API.AsJSON(a) - } - - tableString := &strings.Builder{} - table := tables.NewTable(tableString) - tables.TableToMarkdown(table) - - table.SetHeader([]string{ - "Device Serial", - "Device Asset Tag", - "Hardware Product", - "Rack Unit Start", - "Rack Unit Size", - }) - - for _, r := range a { - var serial string - if (r.DeviceID != uuid.UUID{}) { - serial = API.Devices().Get(r.DeviceID.String()).Serial - } - - table.Append([]string{ - serial, - r.DeviceAssetTag, - r.HardwareProductName, - strconv.Itoa(r.RackUnitStart), - strconv.Itoa(r.RackUnitSize), - }) - } - - table.Render() - return tableString.String() - -} - -func (r *Racks) Assignments(id uuid.UUID) RackAssignments { - uri := fmt.Sprintf( - "/rack/%s/assignment", - url.PathEscape(id.String()), - ) - - assignments := make(RackAssignments, 0) - res := r.Do(r.Sling().New().Get(uri)) - if ok := res.Parse(&assignments); !ok { - panic(res) - } - return assignments -} - -type RackAssignmentUpdate struct { - DeviceID uuid.UUID `json:"device_id" faker:"uuid"` - // FIXME: either device_id or device_serial_number are acceptable - DeviceAssetTag string `json:"device_asset_tag,omitempty"` - RackUnitStart int `json:"rack_unit_start" faker:"rack_unit_start"` -} - -type RackAssignmentUpdates []RackAssignmentUpdate - -func (r *Racks) UpdateAssignments(id uuid.UUID, updates RackAssignmentUpdates) RackAssignments { - uri := fmt.Sprintf( - "/rack/%s/assignment", - url.PathEscape(id.String()), - ) - - r.Do( - r.Sling().New().Post(uri). - Set("Content-Type", "application/json"). - BodyJSON(updates), - ) - - return r.Assignments(id) -} - -// Import assignments from JSON -func (r *Racks) ImportAssignments(id uuid.UUID, b []byte) RackAssignments { - imported := make(RackAssignmentUpdates, 0) - if err := json.Unmarshal(b, &imported); err != nil { - panic(err) - } - return r.UpdateAssignments(id, imported) -} - -/****/ - -func init() { - App.Command("racks", "Work with datacenter racks", func(cmd *cli.Cmd) { - cmd.Before = RequireSysAdmin - - cmd.Command("create", "Create a new rack", func(cmd *cli.Cmd) { - var ( - nameOpt = cmd.StringOpt("name", "", "Name of the rack") - roomAliasOpt = cmd.StringOpt("room", "", "Alias of the datacenter room") - roleNameOpt = cmd.StringOpt("role", "", "Name of the role") - buildNameOpt = cmd.StringOpt("build", "", "Build for the rack") - phaseOpt = cmd.StringOpt("phase", "", "Optional phase for the rack") - ) - - cmd.Spec = "--name --room --role [OPTIONS]" - cmd.Action = func() { - var ( - roomID uuid.UUID - roleID uuid.UUID - buildID uuid.UUID - ok bool - ) - - // The user can be very silly and supply something like - // `--name ""` which will pass the cli lib's requirement - // check but is still crap - if *nameOpt == "" { - panic(errors.New("--name is required")) - } - - if *roomAliasOpt == "" { - panic(errors.New("--room is required")) - } else { - if ok, roomID = API.Rooms().FindID(*roomAliasOpt); !ok { - panic(errors.New("could not find room")) - } - } - - if *roleNameOpt == "" { - panic(errors.New("--role is required")) - } else { - if ok, roleID = API.RackRoles().FindID(*roleNameOpt); !ok { - panic(errors.New("could not find rack role")) - } - } - - if *buildNameOpt == "" { - panic(errors.New("--build is required")) - } else { - build := API.Builds().GetByName(*buildNameOpt) - buildID = build.ID - } - fmt.Println(API.Racks().Create( - *nameOpt, - roomID, - roleID, - *phaseOpt, - buildID, - )) - } - }) - }) - - App.Command("rack", "Work with a single rack", func(cmd *cli.Cmd) { - var rackID uuid.UUID - - idArg := cmd.StringArg( - "UUID", - "", - "The UUID of the rack. Short UUIDs are *not* accepted, unless you are a Conch sysadmin", - ) - - cmd.Spec = "UUID" - - cmd.Before = func() { - - // BUG(sungo) GetAll() is locked to sysadmin permissions currently. - // That prevents us from being able to get a full rack list for - // normal users. - if IsSysAdmin() { - var ok bool - - if ok, rackID = API.Racks().FindID(*idArg); !ok { - panic(errors.New("could not find the rack")) - } - } else { - var err error - rackID, err = uuid.FromString(*idArg) - if err != nil { - panic(err) - } - } - } - - cmd.Command("get", "Get a single rack", func(cmd *cli.Cmd) { - cmd.Action = func() { - fmt.Println(API.Racks().Get(rackID)) - } - }) - - cmd.Command("update", "Update information about a single rack", func(cmd *cli.Cmd) { - var ( - nameOpt = cmd.StringOpt("name", "", "Name of the rack") - roomAliasOpt = cmd.StringOpt("room", "", "Alias of the datacenter room") - roleNameOpt = cmd.StringOpt("role", "", "Name of the role") - phaseOpt = cmd.StringOpt("phase", "", "Phase for the rack") - - serialNumberOpt = cmd.StringOpt("serial-number", "", "Serial number of the rack") - clearSerialOpt = cmd.BoolOpt("clear-serial-number", false, "Delete the serial number. Overrides --serial-number") - - assetTagOpt = cmd.StringOpt("asset-tag", "", "Asset Tag of the rack") - clearAssetTagOpt = cmd.BoolOpt("clear-asset-tag", false, "Delete the asset tag. Overrides --asset-tag") - ) - - cmd.Action = func() { - var ( - roomID uuid.UUID - roleID uuid.UUID - ok bool - serial *string - assetTag *string - ) - - if *roomAliasOpt != "" { - if ok, roomID = API.Rooms().FindID(*roomAliasOpt); !ok { - panic(errors.New("could not find room")) - } - } - if *roleNameOpt != "" { - if ok, roleID = API.RackRoles().FindID(*roleNameOpt); !ok { - panic(errors.New("could not find rack role")) - } - } - - var empty = "" - - if *clearSerialOpt { - serial = nil - } else if *serialNumberOpt != "" { - serial = serialNumberOpt - } else { - serial = &empty - } - - if *clearAssetTagOpt { - assetTag = nil - } else if *assetTagOpt != "" { - assetTag = assetTagOpt - } else { - assetTag = &empty - } - - fmt.Println(API.Racks().Update( - rackID, - *nameOpt, - roomID, - roleID, - *phaseOpt, - serial, - assetTag, - )) - - } - }) - - cmd.Command("delete rm", "Delete a rack", func(cmd *cli.Cmd) { - cmd.Before = RequireSysAdmin - cmd.Action = func() { - API.Racks().Delete(rackID) - fmt.Println("Done.") - } - }) - - cmd.Command("layout", "The layout of the rack", func(cmd *cli.Cmd) { - cmd.Command("get", "Get the layout of a rack", func(cmd *cli.Cmd) { - cmd.Action = func() { - fmt.Println(API.Racks().Layouts(rackID)) - } - }) - - cmd.Command("export", "Export the layout of the rack as JSON", func(cmd *cli.Cmd) { - cmd.Action = func() { - fmt.Println(API.Racks().Layouts(rackID).Export()) - } - }) - - cmd.Command("import", "Import the layout of this rack (using the same format as 'export')", func(cmd *cli.Cmd) { - var ( - filePathArg = cmd.StringArg("FILE", "-", "Path to a JSON file that defines the layout. '-' indicates STDIN") - overwriteOpt = cmd.BoolOpt("overwrite", false, "If the rack has an existing layout, *overwrite* it. This is a destructive action") - ) - cmd.Action = func() { - layout := API.Racks().Layouts(rackID) - if len(layout) > 0 { - if !*overwriteOpt { - panic("rack already has a layout. use --overwrite to force") - } - } - - var b []byte - var err error - if *filePathArg == "-" { - b, err = ioutil.ReadAll(os.Stdin) - } else { - b, err = ioutil.ReadFile(*filePathArg) - } - if err != nil { - panic(err) - } - - fmt.Println(API.Racks().ImportLayout(rackID, b)) - } - }) - }) - - cmd.Command("assign", "Assign devices to rack slots, using the `--json` output from 'assignments'", func(cmd *cli.Cmd) { - filePathArg := cmd.StringArg("FILE", "-", "Path to a JSON file to use as the data source. '-' indicates STDIN") - cmd.Action = func() { - - var b []byte - var err error - if *filePathArg == "-" { - b, err = ioutil.ReadAll(os.Stdin) - } else { - b, err = ioutil.ReadFile(*filePathArg) - } - if err != nil { - panic(err) - } - - fmt.Println(API.Racks().ImportAssignments(rackID, b)) - - } - }) - - cmd.Command("assignments", "The devices assigned to the rack", func(cmd *cli.Cmd) { - cmd.Action = func() { - fmt.Println(API.Racks().Assignments(rackID)) - } - }) - }) - -} diff --git a/racks_integration_test.go b/racks_integration_test.go deleted file mode 100644 index 55f46f6..0000000 --- a/racks_integration_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package main - -import ( - "testing" -) - -const TestRack = "7632cbbc" - -func TestRacksAPIIntegration(t *testing.T) { - defer errorHandler() - - setupAPIClient() - r := setupRecorder("fixtures/conch-v3/racks") - defer r() // Make sure recorder is stopped once done with it - - f := newFixture() - f.setupRackRole() - f.setupRoom() - f.setupHardwareProducts() - f.setupBuild() - defer f.reset() - - var testRack Rack - t.Run("create a new Rack", func(t *testing.T) { - - mock := newTestRack() - testRack = API.Racks().Create( - mock.Name, - f.room.ID, - f.role.ID, - "integration", - f.build.ID, - ) - }) - - /* t.Run("fetch all Racks", func(t *testing.T) { - defer errorHandler() - list := API.Racks().GetAll() - t.Logf("got %v", list) - }) - */ - t.Run("fetch a single rack", func(t *testing.T) { - defer errorHandler() - list := API.Racks().Get(testRack.ID) - t.Logf("got %v", list) - }) - - t.Run("create a new Rack Layout", func(t *testing.T) { - defer errorHandler() - - mrl := RackLayoutUpdates{ - { - RU: 1, - ProductID: f.serverProduct.ID, - }, - { - RU: 1 + f.serverProduct.RackUnitSize, - ProductID: f.switchProduct.ID, - }, - } - _ = API.Racks().CreateLayout( - testRack.ID, - mrl, - ) - }) - /* - t.Run("create a new Rack Assignment", func(t *testing.T) { - mock := newTestRackAssignmentUpdates() - _ = API.Racks().UpdateAssignments( - testRack.ID, - mock, - ) - }) - */ - t.Run("remove a Rack", func(t *testing.T) { - defer errorHandler() - - for _, row := range API.Racks().Layouts(testRack.ID) { - API.Racks().DeleteLayoutSlot(row.ID) - } - - API.Racks().Delete(testRack.ID) - }) - - // other tests - t.Run("create a new Rack from struct", func(t *testing.T) { - defer errorHandler() - mock := newTestRack() - mock.RoomID = f.room.ID - mock.RoleID = f.role.ID - mock.BuildID = f.build.ID - mock.Phase = "integration" - testRack := API.Racks().CreateFromStruct(mock) - API.Racks().Delete(testRack.ID) - }) - -} diff --git a/racks_test.go b/racks_test.go deleted file mode 100644 index af171bd..0000000 --- a/racks_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" -) - -func TestRackAssignments(t *testing.T) { - - t.Run("update rack assignments", func(t *testing.T) { - spy := requestSpy{} - var got RackAssignments - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - if r.Method == "POST" { - body, _ := ioutil.ReadAll(r.Body) - assertJSONSchema(t, body, "request/RackAssignmentUpdates") - json.NewDecoder(bytes.NewBuffer(body)).Decode(&got) - } else { - json.NewEncoder(w).Encode(got) - } - })) - - defer server.Close() - - API.URL = server.URL - - newAssignments := newTestRackAssignmentUpdates() - rack := newTestRack() - _ = API.Racks().UpdateAssignments(rack.ID, newAssignments) - - assertRequestCount(t, spy.requestCount, 2) - assertRequestPath(t, spy.requestPath, fmt.Sprintf("/rack/%s/assignment", rack.ID)) - }) - -} - -func TestRackLayout(t *testing.T) { - - t.Run("create rack layout", func(t *testing.T) { - //defer errorHandler() - spy := requestSpy{} - var got RackLayout - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - if r.Method == "POST" { - body, _ := ioutil.ReadAll(r.Body) - assertJSONSchema(t, body, "request/RackLayoutCreate") - json.NewDecoder(bytes.NewBuffer(body)).Decode(&got) - json.NewEncoder(w).Encode(got) - } else { - json.NewEncoder(w).Encode([]string{}) - } - })) - - defer server.Close() - - API.URL = server.URL - svp := newTestHardwareProduct() - mrl := RackLayoutUpdates{ - { - RU: 1, - ProductID: svp.ID, - }, - } - rack := newTestRack() - _ = API.Racks().CreateLayout(rack.ID, mrl) - - assertRequestCount(t, spy.requestCount, 3) - assertRequestPath(t, spy.requestPath, fmt.Sprintf("/rack/%s/layout", rack.ID)) - }) - -} diff --git a/relays.go b/relays.go deleted file mode 100644 index 9737884..0000000 --- a/relays.go +++ /dev/null @@ -1,310 +0,0 @@ -// Copyright Joyent, Inc. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package main - -import ( - "bytes" - "fmt" - "net/url" - "regexp" - "sort" - "strconv" - "strings" - "time" - - "github.com/gofrs/uuid" - cli "github.com/jawher/mow.cli" - "github.com/joyent/kosh/tables" - "github.com/joyent/kosh/template" -) - -type Relays struct { - *Conch -} - -func (c *Conch) Relays() *Relays { - return &Relays{c} -} - -type RelayList []Relay - -func (r RelayList) Len() int { - return len(r) -} - -func (r RelayList) Swap(i, j int) { - r[i], r[j] = r[j], r[i] -} - -func (r RelayList) Less(i, j int) bool { - return r[i].Updated.Before(r[j].Updated) -} - -func (rl RelayList) String() string { - sort.Sort(rl) - if API.JsonOnly { - return API.AsJSON(rl) - } - - tableString := &strings.Builder{} - table := tables.NewTable(tableString) - tables.TableToMarkdown(table) - - table.SetHeader([]string{ - "Serial", - "Name", - "Version", - "IP", - "SSH Port", - "Updated", - }) - - for _, r := range rl { - table.Append([]string{ - r.SerialNumber, - r.Name, - r.Version, - r.IpAddr, - strconv.Itoa(r.SshPort), - template.TimeStr(r.Updated), - }) - } - - table.Render() - return tableString.String() - -} - -type Relay struct { - ID uuid.UUID `json:"id" faker:"uuid"` - SerialNumber string `json:"serial_number"` - Name string `json:"name,omitempty"` - Version string `json:"version,omitempty"` - IpAddr string `json:"ipaddr,omitempty" faker:"ipv4"` - SshPort int `json:"ssh_port"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` - LastSeen time.Time `json:"last_seen,omitempty"` - UserID uuid.UUID `json:"user_id,omitempty" faker:"uuid"` -} - -func (r Relay) String() string { - if API.JsonOnly { - return API.AsJSON(r) - } - - t, err := template.NewTemplate().Parse(relayTemplate) - if err != nil { - panic(err) - } - - buf := new(bytes.Buffer) - - if err := t.Execute(buf, r); err != nil { - panic(err) - } - - return buf.String() - -} - -func (r *Relays) GetAll() RelayList { - rl := make(RelayList, 0) - - res := r.Do(r.Sling().New().Get("/relay/?no_devices=1")) - if ok := res.Parse(&rl); !ok { - panic(res) - } - - return rl -} - -func (r *Relays) Get(identifier string) (relay Relay) { - uri := fmt.Sprintf( - "/relay/%s", - identifier, - ) - - res := r.Do(r.Sling().New().Get(uri)) - if ok := res.Parse(&relay); !ok { - panic(res) - } - return relay -} - -func (r *Relays) Register( - serial string, - version string, - ipaddr string, - name string, - sshPort int, -) Relay { - if serial == "" { - panic("please provide a serial number") - } - - out := struct { - Serial string `json:"serial"` - Version string `json:"version,omitempty"` - IpAddr string `json:"ipaddr,omitempty"` - Name string `json:"name,omitempty"` - SshPort int `json:"ssh_port,omitempty"` - }{ - serial, - version, - ipaddr, - name, - sshPort, - } - - uri := fmt.Sprintf( - "/relay/%s/register", - url.PathEscape(serial), - ) - - res := r.Do( - r.Sling().Post(uri). - Set("Content-Type", "application/json"). - BodyJSON(out), - ) - - var relay Relay - if ok := res.Parse(&relay); !ok { - panic(res) - } - return relay -} - -func (r *Relays) Delete(identifier string) { - uri := fmt.Sprintf("/relay/%s", url.PathEscape(identifier)) - res := r.Do(r.Sling().New().Delete(uri)) - - if res.StatusCode() != 204 { - // I know this is weird. Like in other places, it should be impossible - // to reach here unless the status code is 204. The API returns 204 - // (which gets us here) or 409 (which will explode before it gets here). - // If we got here via some other code, then there's some new behavior - // that we need to know about. - panic(res) - } -} - -func init() { - App.Command("relays", "Perform actions against the whole list of relays", func(cmd *cli.Cmd) { - cmd.Command("get", "Get a list of relays", func(cmd *cli.Cmd) { - cmd.Action = func() { fmt.Println(API.Relays().GetAll()) } - }) - - cmd.Command("find", "Find relays by name", func(cmd *cli.Cmd) { - var ( - relays = cmd.StringsArg("RELAYS", nil, "List of regular expressions to match against relay IDs") - andOpt = cmd.BoolOpt("and", false, "Match the list as a logical AND") - ) - - cmd.Spec = "[OPTIONS] RELAYS..." - cmd.LongDesc = ` -Takes a list of regular expressions and matches those against the IDs of all known relays. - -The default behavior is to match as a logical OR but this behavior can be changed by providing the --and flag - -For instance: - -* "conch relays find drd" will find all relays with 'drd' in their ID. For perl folks, this is essentially 'm/drd/' -* "conch relays find '^ams-'" will find all relays with IDs that begin with 'ams-' -* "conch relays find drd '^ams-' will find all relays with IDs that contain 'drd' OR begin with 'ams-' -* "conch relays find --and drd '^ams-' will find all relays with IDs that contain 'drd' AND begin with '^ams-'` - - cmd.Action = func() { - if *relays == nil { - panic("please provide a list of regular expressions") - } - - // If a user for some strange reason gives us a relay name of "", the - // cli lib will pass it on to us. That name is obviously useless so - // let's filter it out. - relayREs := make([]*regexp.Regexp, 0) - for _, matcher := range *relays { - if matcher == "" { - continue - } - re, err := regexp.Compile(matcher) - if err != nil { - panic(err) - } - - relayREs = append(relayREs, re) - } - if len(relayREs) == 0 { - panic("please provide a list of regular expressions") - } - - results := make(RelayList, 0) - for _, relay := range API.Relays().GetAll() { - matched := 0 - for _, re := range relayREs { - if re.MatchString(relay.SerialNumber) { - if *andOpt { - matched++ - } else { - results = append(results, relay) - continue - } - } - } - if *andOpt { - if matched == len(relayREs) { - results = append(results, relay) - } - } - } - - fmt.Println(results) - } - }) - }) - - App.Command("relay", "Perform actions against a single relay", func(cmd *cli.Cmd) { - relayArg := cmd.StringArg( - "RELAY", - "", - "ID of the relay", - ) - - cmd.Spec = "RELAY" - - cmd.Command("get", "Get data about a single relay", func(cmd *cli.Cmd) { - cmd.Action = func() { fmt.Println(API.Relays().Get(*relayArg)) } - }) - - cmd.Command("register", "Register a relay with the API", func(cmd *cli.Cmd) { - - var ( - versionOpt = cmd.StringOpt("version", "", "The version of the relay") - sshPortOpt = cmd.IntOpt("ssh_port port", 22, "The SSH port for the relay") - ipAddrOpt = cmd.StringOpt("ipaddr ip", "", "The IP address for the relay") - nameOpt = cmd.StringOpt("name", "", "The name of the relay") - ) - - cmd.Action = func() { - fmt.Println(API.Relays().Register( - *relayArg, - *versionOpt, - *ipAddrOpt, - *nameOpt, - *sshPortOpt, - )) - } - }) - cmd.Command("delete rm", "Delete a relay", func(cmd *cli.Cmd) { - cmd.Action = func() { - API.Relays().Delete(*relayArg) - fmt.Println(API.Relays().GetAll()) - } - }) - }) - -} diff --git a/relays_integration_test.go b/relays_integration_test.go deleted file mode 100644 index 0f02e44..0000000 --- a/relays_integration_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "testing" -) - -var testRelay Relay - -const serialNumber = "sAWCXAbDHumkCUsrvQpvjFJwv" - -func TestRelaysAPIIntegration(t *testing.T) { - setupAPIClient() - r := setupRecorder("fixtures/conch-v3/relays") - defer r() // Make sure recorder is stopped once done with it - - t.Run("register relay", func(t *testing.T) { - defer errorHandler() - mock := newTestRelay() - testRelay = API.Relays().Register( - serialNumber, - mock.Version, - mock.IpAddr, - mock.Name, - mock.SshPort, - ) - }) - - t.Run("get all relays", func(t *testing.T) { - defer errorHandler() - list := API.Relays().GetAll() - t.Logf("got %v", list) - }) - - t.Run("get one relay", func(t *testing.T) { - defer errorHandler() - list := API.Relays().Get(testRelay.SerialNumber) - t.Logf("got %v", list) - }) - - t.Run("remove a relay", func(t *testing.T) { - defer errorHandler() - API.Relays().Delete(testRelay.SerialNumber) - }) -} diff --git a/roles.go b/roles.go deleted file mode 100644 index 9cc804e..0000000 --- a/roles.go +++ /dev/null @@ -1,320 +0,0 @@ -// Copyright Joyent, Inc. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package main - -//lint:file-ignore U1000 WIP - -import ( - "bytes" - "errors" - "fmt" - "net/url" - "sort" - "strconv" - "strings" - "time" - - "github.com/gofrs/uuid" - cli "github.com/jawher/mow.cli" - "github.com/joyent/kosh/tables" - "github.com/joyent/kosh/template" - "github.com/olekukonko/tablewriter" -) - -type RackRoles struct { - *Conch -} - -func (c *Conch) RackRoles() *RackRoles { - return &RackRoles{c} -} - -/****/ - -type RackRoleList []RackRole - -func (r RackRoleList) Len() int { - return len(r) -} - -func (r RackRoleList) Swap(i, j int) { - r[i], r[j] = r[j], r[i] -} - -func (r RackRoleList) Less(i, j int) bool { - return r[i].Name < r[j].Name -} - -/****/ - -type RackRole struct { - ID uuid.UUID `json:"id" faker:"uuid"` - Name string `json:"name"` - RackSize int `json:"rack_size" faker:"rack_size"` - Created time.Time `json:"created" faker:"-"` - Updated time.Time `json:"updated" faker:"-"` -} - -func (rl RackRoleList) String() string { - sort.Sort(rl) - if API.JsonOnly { - return API.AsJSON(rl) - } - - tableString := &strings.Builder{} - table := tablewriter.NewWriter(tableString) - tables.TableToMarkdown(table) - - table.SetHeader([]string{ - "Name", - "RackSize", - "Created", - "Updated", - }) - - for _, r := range rl { - table.Append([]string{ - r.Name, - strconv.Itoa(r.RackSize), - template.TimeStr(r.Created), - template.TimeStr(r.Updated), - }) - } - - table.Render() - return tableString.String() - -} - -func (r RackRole) String() string { - if API.JsonOnly { - return API.AsJSON(r) - } - - t, err := template.NewTemplate().Parse(rackRoleTemplate) - if err != nil { - panic(err) - } - - buf := new(bytes.Buffer) - if err := t.Execute(buf, r); err != nil { - panic(err) - } - - return buf.String() -} - -/****/ - -func (r *RackRoles) GetAll() RackRoleList { - rl := make(RackRoleList, 0) - res := r.Do(r.Sling().Get("/rack_role")) - if ok := res.Parse(&rl); !ok { - panic(res) - } - return rl -} - -func (r *RackRoles) Get(id uuid.UUID) RackRole { - var role RackRole - - uri := fmt.Sprintf( - "/rack_role/%s", - url.PathEscape(id.String()), - ) - - res := r.Do(r.Sling().Get(uri)) - if ok := res.Parse(&role); !ok { - panic(res) - } - return role -} - -func (r *RackRoles) GetByName(name string) RackRole { - var role RackRole - - uri := fmt.Sprintf( - "/rack_role/%s", - url.PathEscape(name), - ) - - res := r.Do(r.Sling().Get(uri)) - if ok := res.Parse(&role); !ok { - panic(res) - } - return role -} - -func (r *RackRoles) FindID(name string) (bool, uuid.UUID) { - var role RackRole - - uri := fmt.Sprintf( - "/rack_role/%s", - url.PathEscape(name), - ) - - res := r.DoBadly(r.Sling().Get(uri)) - if res.IsError() { - return false, uuid.UUID{} - } - - return res.Parse(&role), role.ID -} - -func (r *RackRoles) Create(name string, rackSize int) RackRole { - if name == "" { - panic(errors.New("'name' is required")) - } - - if rackSize == 0 { - panic(errors.New("'rackSize' is required and cannot be 0")) - } - - payload := make(map[string]interface{}) - payload["name"] = name - payload["rack_size"] = rackSize - - var role RackRole - - // We get a 303 on success - res := r.Do( - r.Sling().New().Post("/rack_role"). - Set("Content-Type", "application/json"). - BodyJSON(payload), - ) - - if ok := res.Parse(&role); !ok { - panic(res) - } - - return role -} - -func (r *RackRoles) CreateFromStruct(role RackRole) RackRole { - return r.Create( - role.Name, - role.RackSize, - ) -} - -func (r *RackRoles) Update(id uuid.UUID, newName string, rackSize int) RackRole { - payload := make(map[string]interface{}) - if newName != "" { - payload["name"] = newName - } - if rackSize > 0 { - payload["rack_size"] = rackSize - } - - var role RackRole - - uri := fmt.Sprintf( - "/rack_role/%s", - url.PathEscape(id.String()), - ) - - // We get a 303 on success - res := r.Do( - r.Sling().New().Post(uri). - Set("Content-Type", "application/json"). - BodyJSON(payload), - ) - - if ok := res.Parse(&role); !ok { - panic(res) - } - - return role -} - -func (r *RackRoles) Delete(id uuid.UUID) { - uri := fmt.Sprintf("/rack_role/%s", url.PathEscape(id.String())) - res := r.Do(r.Sling().New().Delete(uri)) - - if res.StatusCode() != 204 { - // I know this is weird. Like in other places, it should be impossible - // to reach here unless the status code is 204. The API returns 204 - // (which gets us here) or 409 (which will explode before it gets here). - // If we got here via some other code, then there's some new behavior - // that we need to know about. - panic(res) - } -} - -/****/ - -func init() { - App.Command("roles", "Work with datacenter rack roles", func(cmd *cli.Cmd) { - cmd.Before = RequireSysAdmin - cmd.Command("get", "Get a list of all rack roles", func(cmd *cli.Cmd) { - cmd.Action = func() { fmt.Println(API.RackRoles().GetAll()) } - }) - - cmd.Command("create", "Create a new rack role", func(cmd *cli.Cmd) { - var ( - nameOpt = cmd.StringOpt("name", "", "The name of the role") - rackSizeOpt = cmd.IntOpt("rack-size", 0, "Size of the rack necessary for this role") - ) - - cmd.Spec = "--name --rack-size" - cmd.Action = func() { - if *nameOpt == "" { - panic(errors.New("--name is required")) - } - - if *rackSizeOpt == 0 { - panic(errors.New("--rack-size is required and cannot be 0")) - } - - fmt.Println(API.RackRoles().Create(*nameOpt, *rackSizeOpt)) - } - }) - }) - - App.Command("role", "Work with a single rack role", func(cmd *cli.Cmd) { - var roleID uuid.UUID - - nameArg := cmd.StringArg( - "NAME", - "", - "The name of the rack role", - ) - - cmd.Spec = "NAME" - - cmd.Before = func() { - RequireSysAdmin() - var ok bool - - if ok, roleID = API.RackRoles().FindID(*nameArg); !ok { - panic(errors.New("could not find the role")) - } - } - - cmd.Command("get", "Get information about a single rack role", func(cmd *cli.Cmd) { - cmd.Action = func() { fmt.Println(API.RackRoles().Get(roleID)) } - }) - - cmd.Command("update", "Update information about a single rack role", func(cmd *cli.Cmd) { - var ( - nameOpt = cmd.StringOpt("name", "", "The name of the role") - rackSizeOpt = cmd.IntOpt("rack-size", 0, "Size of the rack necessary for this role") - ) - - cmd.Action = func() { - fmt.Println(API.RackRoles().Update(roleID, *nameOpt, *rackSizeOpt)) - } - }) - - cmd.Command("delete", "Delete a single rack role", func(cmd *cli.Cmd) { - cmd.Action = func() { - API.RackRoles().Delete(roleID) - fmt.Println(API.RackRoles().GetAll()) - } - }) - }) -} diff --git a/roles_integration_test.go b/roles_integration_test.go deleted file mode 100644 index caa3783..0000000 --- a/roles_integration_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package main - -import ( - "testing" -) - -func TestRackRoleAPIIntegration(t *testing.T) { - setupAPIClient() - r := setupRecorder("fixtures/conch-v3/racks-roles") - defer r() // Make sure recorder is stopped once done with it - - var testRackRole RackRole - t.Run("create a rack role", func(t *testing.T) { - defer errorHandler() - mockRole := newTestRackRole() - testRackRole = API.RackRoles().CreateFromStruct(mockRole) - }) - - t.Run("get all rack roles", func(t *testing.T) { - defer errorHandler() - list := API.RackRoles().GetAll() - t.Logf("got %v", list) - }) - - t.Run("get a rack role", func(t *testing.T) { - defer errorHandler() - list := API.RackRoles().Get(testRackRole.ID) - t.Logf("got %v", list) - }) - - t.Run("get a role by name", func(t *testing.T) { - defer errorHandler() - list := API.RackRoles().GetByName(testRackRole.Name) - t.Logf("got %v", list) - }) -} diff --git a/roles_test.go b/roles_test.go deleted file mode 100644 index 71d2ac1..0000000 --- a/roles_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestRackRoleCreateFromStruct(t *testing.T) { - spy := requestSpy{} - var got RackRole - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - body, _ := ioutil.ReadAll(r.Body) - assertJSONSchema(t, body, "request/RackRoleCreate") - json.NewDecoder(bytes.NewBuffer(body)).Decode(&got) - json.NewEncoder(w).Encode(got) - })) - defer server.Close() - - API.URL = server.URL - - r := API.RackRoles() - - want := r.CreateFromStruct(newTestRackRole()) - - assertRequestCount(t, spy.requestCount, 1) - assertRequestPath(t, spy.requestPath, "/rack_role") - assertRequestMethod(t, spy.requestMethod, "POST") - assert.Equal(t, got, want) -} diff --git a/rooms.go b/rooms.go deleted file mode 100644 index e701ed2..0000000 --- a/rooms.go +++ /dev/null @@ -1,382 +0,0 @@ -// Copyright Joyent, Inc. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package main - -//lint:file-ignore U1000 WIP - -import ( - "bytes" - "errors" - "fmt" - "net/url" - "sort" - "strings" - "time" - - "github.com/gofrs/uuid" - cli "github.com/jawher/mow.cli" - "github.com/joyent/kosh/tables" - "github.com/joyent/kosh/template" -) - -type Rooms struct { - *Conch -} - -func (c *Conch) Rooms() *Rooms { - return &Rooms{c} -} - -/****/ - -// This is called DatacenterRoomsDetailed in the json schema -type RoomList []Room - -func (r RoomList) Len() int { - return len(r) -} - -func (r RoomList) Swap(i, j int) { - r[i], r[j] = r[j], r[i] -} - -func (r RoomList) Less(i, j int) bool { - return r[i].Alias < r[j].Alias -} - -func (dr RoomList) String() string { - sort.Sort(dr) - if API.JsonOnly { - return API.AsJSON(dr) - } - - tableString := &strings.Builder{} - table := tables.NewTable(tableString) - tables.TableToMarkdown(table) - - table.SetHeader([]string{ - "ID", - "Alias", - "AZ", - "Vendor Name", - "Datacenter ID", - "Created", - "Updated", - }) - - for _, r := range dr { - table.Append([]string{ - template.CutUUID(r.ID.String()), - r.Alias, - r.AZ, - r.VendorName, - template.CutUUID(r.DatacenterID.String()), - template.TimeStr(r.Created), - template.TimeStr(r.Updated), - }) - } - - table.Render() - return tableString.String() - -} - -// This is called DatacenterRoomDetailed in the json schema -type Room struct { - ID uuid.UUID `json:"id" faker:"uuid"` - AZ string `json:"az"` - Alias string `json:"alias"` - VendorName string `json:"vendor_name,omitempty"` - DatacenterID uuid.UUID `json:"datacenter_id" faker:"uuid"` - Created time.Time `json:"created" faker:"-"` - Updated time.Time `json:"updated" faker:"-"` -} - -func (r Room) String() string { - if API.JsonOnly { - return API.AsJSON(r) - } - - t, err := template.NewTemplate().Parse(roomTemplate) - if err != nil { - panic(err) - } - - buf := new(bytes.Buffer) - if err := t.Execute(buf, r); err != nil { - panic(err) - } - - return buf.String() -} - -// Accepting partial UUIDs, full UUIDs, and 'alias' -func (r *Rooms) FindID(id string) (bool, uuid.UUID) { - ids := make([]uuid.UUID, 0) - for _, room := range r.GetAll() { - if room.Alias == id { - return true, room.ID - } - ids = append(ids, room.ID) - } - - return FindUUID(id, ids) -} - -func (r *Rooms) GetAll() RoomList { - rl := make(RoomList, 0) - res := r.Do(r.Sling().Get("/room")) - if ok := res.Parse(&rl); !ok { - panic(res) - } - return rl -} - -func (r *Rooms) Get(id uuid.UUID) Room { - var room Room - uri := fmt.Sprintf( - "/room/%s", - url.PathEscape(id.String()), - ) - - res := r.Do(r.Sling().Get(uri)) - if ok := res.Parse(&room); !ok { - panic(res) - } - return room -} - -func (r *Rooms) Create(datacenterID uuid.UUID, az string, alias string, vendorName string) Room { - payload := make(map[string]string) - if (datacenterID == uuid.UUID{}) { - panic(errors.New("'datacenterID' cannot be empty")) - } - payload["datacenter_id"] = datacenterID.String() - - if az == "" { - panic(errors.New("'az' cannot be empty")) - } - payload["az"] = az - - if alias == "" { - panic(errors.New("'alias' cannot be empty")) - } - payload["alias"] = alias - - if vendorName != "" { - payload["vendor_name"] = vendorName - } - - /**/ - - var room Room - - // We get a 303 on success - res := r.Do( - r.Sling().New().Post("/room"). - Set("Content-Type", "application/json"). - BodyJSON(payload), - ) - if ok := res.Parse(&room); !ok { - panic(res) - } - - return room -} - -func (r *Rooms) CreateFromStruct(room Room) Room { - return r.Create( - room.DatacenterID, - room.AZ, - room.Alias, - room.VendorName, - ) -} - -func (r *Rooms) Update(id uuid.UUID, datacenterID uuid.UUID, az string, alias string, vendorName string) Room { - payload := make(map[string]string) - if (datacenterID != uuid.UUID{}) { - payload["datacenter_id"] = datacenterID.String() - } - - if az != "" { - payload["az"] = az - } - - if alias != "" { - payload["alias"] = alias - } - - if vendorName != "" { - payload["vendor_name"] = vendorName - } - - /**/ - - var room Room - uri := fmt.Sprintf( - "/room/%s", - url.PathEscape(id.String()), - ) - - // We get a 303 on success - res := r.Do( - r.Sling().New().Post(uri). - Set("Content-Type", "application/json"). - BodyJSON(payload), - ) - if ok := res.Parse(&room); !ok { - panic(res) - } - - return room -} - -func (r *Rooms) Delete(id uuid.UUID) { - uri := fmt.Sprintf("/room/%s", url.PathEscape(id.String())) - res := r.Do(r.Sling().New().Delete(uri)) - - if res.StatusCode() != 204 { - // I know this is weird. Like in other places, it should be impossible - // to reach here unless the status code is 204. The API returns 204 - // (which gets us here) or 409 (which will explode before it gets here). - // If we got here via some other code, then there's some new behavior - // that we need to know about. - panic(res) - } -} - -func (r *Rooms) Racks(id uuid.UUID) RackList { - uri := fmt.Sprintf("/room/%s/rack", url.PathEscape(id.String())) - - rl := make(RackList, 0) - res := r.Do(r.Sling().New().Get(uri)) - if ok := res.Parse(&rl); !ok { - panic(res) - } - return rl -} - -func init() { - App.Command("rooms", "Work with datacenter rooms", func(cmd *cli.Cmd) { - cmd.Before = RequireSysAdmin - cmd.Command("get", "Get a list of all rooms", func(cmd *cli.Cmd) { - cmd.Action = func() { - fmt.Println(API.Rooms().GetAll()) - } - }) - cmd.Command("create", "Create a single room", func(cmd *cli.Cmd) { - var ( - aliasOpt = cmd.StringOpt("alias", "", "Alias") - azOpt = cmd.StringOpt("az", "", "AZ") - datacenterIdOpt = cmd.StringOpt("datacenter-id", "", "Datacenter UUID (first segment of UUID accepted)") - vendorNameOpt = cmd.StringOpt("vendor-name", "", "Vendor Name") - ) - - cmd.Spec = "--datacenter-id --alias --az [OPTIONS]" - cmd.Action = func() { - // The user can be very silly and supply something like - // '--alias ""' which will pass the cli lib's requirement - // check but is still crap - if *aliasOpt == "" { - panic(errors.New("--alias is required")) - } - if *azOpt == "" { - panic(errors.New("--az is required")) - } - if *datacenterIdOpt == "" { - panic(errors.New("--datacenter-id is required")) - } - - var datacenterID uuid.UUID - var ok bool - if ok, datacenterID = API.Datacenters().FindDatacenterID(*datacenterIdOpt); !ok { - panic(errors.New("could not find the datacenter")) - } - - fmt.Println(API.Rooms().Create( - datacenterID, - *azOpt, - *aliasOpt, - *vendorNameOpt, - )) - } - }) - - }) - - App.Command("room", "Deal with a single datacenter room", func(cmd *cli.Cmd) { - var roomID uuid.UUID - - aliasArg := cmd.StringArg( - "ALIAS", - "", - "The unique alias of the datacenter room", - ) - - cmd.Spec = "ALIAS" - - cmd.Before = func() { - RequireSysAdmin() - var ok bool - - if ok, roomID = API.Rooms().FindID(*aliasArg); !ok { - panic(errors.New("could not find the room")) - } - } - - cmd.Command("get", "Information about a single room", func(cmd *cli.Cmd) { - cmd.Action = func() { - fmt.Println(API.Rooms().Get(roomID)) - } - }) - - cmd.Command("update", "Update information about a single room", func(cmd *cli.Cmd) { - var ( - aliasOpt = cmd.StringOpt("alias", "", "Alias") - azOpt = cmd.StringOpt("az", "", "AZ") - datacenterIdOpt = cmd.StringOpt("datacenter-id", "", "Datacenter UUID (first segment of UUID accepted)") - vendorNameOpt = cmd.StringOpt("vendor-name", "", "Vendor Name") - ) - - cmd.Action = func() { - var datacenterID uuid.UUID - - if *datacenterIdOpt != "" { - var ok bool - if ok, datacenterID = API.Datacenters().FindDatacenterID(*datacenterIdOpt); !ok { - panic(errors.New("could not find the datacenter")) - } - } - - fmt.Println(API.Rooms().Update( - roomID, - datacenterID, - *azOpt, - *aliasOpt, - *vendorNameOpt, - )) - } - }) - - cmd.Command("delete", "Delete a single room", func(cmd *cli.Cmd) { - cmd.Action = func() { - // Lower layers panic if there's a problem - API.Rooms().Delete(roomID) - - fmt.Println(API.Rooms().GetAll()) - } - }) - - cmd.Command("racks", "View the racks assigned to a single room", func(cmd *cli.Cmd) { - cmd.Action = func() { - fmt.Println(API.Rooms().Racks(roomID)) - } - }) - - }) -} diff --git a/rooms_integration_test.go b/rooms_integration_test.go deleted file mode 100644 index fbe2358..0000000 --- a/rooms_integration_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package main - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestRoomsAPIIntegration(t *testing.T) { - setupAPIClient() - r := setupRecorder("fixtures/conch-v3/rooms") - defer r() // Make sure recorder is stopped once done with it - - dc := API.Datacenters().CreateFromStruct(newTestDatacenter()) - defer API.Datacenters().Delete(dc.ID) - - var testRoom Room - t.Run("Create a New Room", func(t *testing.T) { - want := newTestRoom() - testRoom = API.Rooms().Create( - dc.ID, - want.AZ, - want.Alias, - want.VendorName, - ) - assert.NotNil(t, testRoom.ID) - // TODO write a functional test here -- perigrin - }) - - t.Run("Get all rooms", func(t *testing.T) { - defer errorHandler() - list := API.Rooms().GetAll() - t.Logf("got %v", list) - }) - - t.Run("Get one room", func(t *testing.T) { - defer errorHandler() - list := API.Rooms().Get(testRoom.ID) - t.Logf("got %v", list) - }) - - t.Run("List all racks in a room", func(t *testing.T) { - defer errorHandler() - list := API.Rooms().Racks(testRoom.ID) - t.Logf("got %v", list) - }) - - t.Run("Remove a room", func(t *testing.T) { - defer errorHandler() - API.Rooms().Delete(testRoom.ID) - }) -} diff --git a/rooms_test.go b/rooms_test.go deleted file mode 100644 index 9431c26..0000000 --- a/rooms_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestRoomsCreateFromStruct(t *testing.T) { - spy := requestSpy{} - var got Room - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - spy.onRequest(r) - body, _ := ioutil.ReadAll(r.Body) - assertJSONSchema(t, body, "request/DatacenterRoomCreate") - json.NewDecoder(bytes.NewBuffer(body)).Decode(&got) - json.NewEncoder(w).Encode(got) - })) - defer server.Close() - - API.URL = server.URL - - r := API.Rooms() - - want := r.CreateFromStruct(newTestRoom()) - - assertRequestCount(t, spy.requestCount, 1) - assertRequestPath(t, spy.requestPath, "/room") - assertRequestMethod(t, spy.requestMethod, "POST") - assert.Equal(t, got, want) -} diff --git a/schema.go b/schema.go deleted file mode 100644 index 8a72e1b..0000000 --- a/schema.go +++ /dev/null @@ -1,26 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/qri-io/jsonschema" -) - -type Schema struct { - *Conch -} - -func (c *Conch) Schema() *Schema { - return &Schema{c} -} - -func (s *Schema) Get(name string) *jsonschema.RootSchema { - uri := fmt.Sprintf("/schema/%s", name) - rs := &jsonschema.RootSchema{} - - res := s.Do(s.Sling().Get(uri)) - if ok := res.Parse(&rs); !ok { - panic(res) - } - return rs -} diff --git a/schema_test.go b/schema_test.go deleted file mode 100644 index 0b91805..0000000 --- a/schema_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "net/http/httptest" - "os" - "testing" -) - -func TestJSONSchemaGet(t *testing.T) { - name := "request/Login" - valid := []byte(`{ - "email":"test@example.com", - "password":"123456" - }`) - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // output from the API's '/schema/request/Login` endpoint - fmt.Fprintf(w, ` - { - "$id":"urn:request.Login.schema.json", - "$schema":"http:\/\/json-schema.org\/draft-07\/schema#", - "additionalProperties":false, - "definitions":{ - "email_address":{ - "allOf":[ - {"format":"email","type":"string"}, - {"$ref":"\/definitions\/mojo_relaxed_placeholder"} - ] - }, - "mojo_relaxed_placeholder":{ - "description":"see https:\/\/metacpan.org\/pod\/Mojolicious::Guides::Routing#Relaxed-placeholders", - "pattern":"^[^\/]+$","type":"string" - }, - "non_empty_string":{ - "minLength":1, - "type":"string" - }, - "uuid":{ - "pattern":"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", - "type":"string" - } - }, - "oneOf":[ - {"required":["user_id"]}, - {"required":["email"]} - ], - "properties":{ - "email":{"$ref":"\/definitions\/email_address"}, - "password":{"$ref":"\/definitions\/non_empty_string"}, - "user_id":{"$ref":"\/definitions\/uuid"}}, - "required":["password"], - "title":"Login", - "type":"object" - } - `) - })) - - API.URL = server.URL - rs := API.Schema().Get(name) - - if rs == nil { - t.Fatalf("Couldn't get root schema") - } - - if errors, _ := rs.ValidateBytes(valid); len(errors) > 0 { - t.Errorf("Couldn't validate valid JSON: %v", errors) - } -} - -func overrideURL(newURL string) func() { - oldURL := API.URL - API.URL = newURL - return func() { API.URL = oldURL } -} - -func currentURL() string { - if os.Getenv("KOSH_URL") != "" { - return os.Getenv("KOSH_URL") - } - return "https://edge.conch.joyent.us" -} -func assertJSONSchema(t *testing.T, got []byte, name string) { - t.Helper() - restoreURL := overrideURL(currentURL()) - defer restoreURL() - restoreClient := setupRecorder("fixtures/json-schema/" + name) - defer restoreClient() - - rs := API.Schema().Get(name) - if errors, _ := rs.ValidateBytes(got); len(errors) > 0 { - t.Errorf("Errors validating: %v", errors) - } -} diff --git a/tables/tables.go b/tables/tables.go index d40732c..04bd787 100644 --- a/tables/tables.go +++ b/tables/tables.go @@ -2,12 +2,16 @@ package tables import ( "io" + "sort" + "strings" "github.com/olekukonko/tablewriter" ) func NewTable(writer io.Writer) *tablewriter.Table { - return tablewriter.NewWriter(writer) + table := tablewriter.NewWriter(writer) + TableToMarkdown(table) + return table } func TableToMarkdown(table *tablewriter.Table) { @@ -15,3 +19,22 @@ func TableToMarkdown(table *tablewriter.Table) { table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) table.SetCenterSeparator("|") } + +type Tabulable interface { + Headers() []string + ForEach(func([]string)) + sort.Interface +} + +func Render(list Tabulable) string { + sort.Sort(list) + + tableString := &strings.Builder{} + table := NewTable(tableString) + + table.SetHeader(list.Headers()) + list.ForEach(table.Append) + + table.Render() + return tableString.String() +} diff --git a/template/template.go b/template/template.go index cc9d914..c33ee7c 100644 --- a/template/template.go +++ b/template/template.go @@ -1,13 +1,17 @@ package template import ( + "bytes" "html/template" "regexp" "time" + + "github.com/joyent/kosh/tables" ) -const DateFormat = "2006-01-02 15:04:05 -0700 MST" +const dateFormat = "2006-01-02 15:04:05 -0700 MST" +// CutUUID - trims a UUID down to a short readable version func CutUUID(id string) string { re := regexp.MustCompile("^(.+?)-") bits := re.FindStringSubmatch(id) @@ -17,16 +21,43 @@ func CutUUID(id string) string { return id } +// TimeStr fformats a time value into something human readable func TimeStr(t time.Time) string { if t.IsZero() { return "" } - return t.Local().Format(DateFormat) + return t.Local().Format(dateFormat) +} + +func Table(t tables.Tabulable) string { + return tables.Render(t) } +// NewTemplate returns a new template instance func NewTemplate() *template.Template { return template.New("wat").Funcs(template.FuncMap{ "CutUUID": func(id string) string { return CutUUID(id) }, "TimeStr": func(t time.Time) string { return TimeStr(t) }, + "Table": Table, }) } + +type Templated interface { + Template() string +} + +// Render takes a struct, and a template string and returns a string +func Render(data Templated) (string, error) { + template := data.Template() + t, err := NewTemplate().Parse(template) + if err != nil { + return "", err // TODO get logging in here + } + + buf := new(bytes.Buffer) + if err := t.Execute(buf, data); err != nil { + return "", err // TODO get logging in here + } + + return buf.String(), nil +} diff --git a/templates.go b/templates.go deleted file mode 100644 index 367ed78..0000000 --- a/templates.go +++ /dev/null @@ -1,267 +0,0 @@ -// Copyright Joyent, Inc. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package main - -const validationStateWithResultsTemplate = ` -ID: {{ .ID }} -Created: {{ TimeStr .Created }} -Completed: {{ TimeStr .Completed }} -Status: {{ .Status }} -Validation Plan: {{ .ValidationPlan.Name }}{{ if len .Results }} - -Results: -{{ .Results }} -{{ end }} -` - -const deviceTemplate = ` -ID: {{ .ID }} -Serial: {{ .Serial }} -Asset Tag: {{ .AssetTag }} -Hostname: {{ .Hostname }} -System UUID: {{ .SystemUUID }} - -Phase: {{ .Phase }} -Health: {{ .Health }} -Validated: {{ if not $.Validated.IsZero }}{{ .Validated.Local }}{{ end }} - -Created: {{ TimeStr .Created }} -Updated: {{ TimeStr .Updated }} -Last Seen: {{ TimeStr .LastSeen }}{{ if .Links }} - -Links: {{ range .Links }} - - {{ $ }} -{{ end }}{{ end }} - -Hardware: - Name: {{ .HardwareProduct.Name }} - Legacy Name: {{ .HardwareProduct.LegacyProductName }} - Alias: {{ .HardwareProduct.Alias }} - Prefix: {{ .HardwareProduct.Prefix }} - SKU: {{ .HardwareProduct.SKU }} - Generation Name: {{ .HardwareProduct.GenerationName }} - -Location: {{- if ne .Phase "integration" }} ** Device has left integration. This data is historic and likely not accurate. **{{ end }} - AZ: {{ .Location.Room.AZ }} - Datacenter: - ID: {{ .Location.Datacenter.ID }} - Vendor: {{ .Location.Datacenter.Vendor }} / {{ .Location.Datacenter.VendorName }} - Region: {{ .Location.Datacenter.Region }} - Location: {{ .Location.Datacenter.Location }} - - Room: - ID: {{ .Location.Room.ID }} - Alias: {{ .Location.Room.Alias }} - Vendor Name: {{ .Location.Room.VendorName }} - - Rack: - ID: {{ .Location.Rack.ID }} - Name: {{ .Location.Rack.Name }}{{ if ne .RackRole.Name "" }} - Role: {{ .RackRole.Name }}{{ end }} - Phase: {{ .Location.Rack.Phase }} - RU: {{ .Location.RackUnitStart }} - - -Network Interfaces: {{ range .Nics }} - - {{ .InterfaceName }} - {{ .Mac }} - Type: {{ .InterfaceType }} - Vendor: {{ .InterfaceVendor }}{{ if ne .PeerMac "" }} - Peer: {{ .PeerMac }}{{ end }}{{ if ne .PeerSwitch "" }} - {{ .PeerSwitch }}{{ end }} -{{ end }} -Disks:{{range $name, $slots := .Enclosures}} - Enclosure: {{ $name }}{{ range $slots }} - Slot: {{ .Slot }} - SN: {{ .SerialNumber }} - Type: {{ .DriveType }} - Vendor: {{ .Vendor }} - Model: {{ .Model }} - Size: {{ .Size }} - Health: {{ .Health }} - Firmware: {{ .Firmware }} - Transport: {{ .Transport }} -{{ end }}{{ end }} - -Validations: -{{ .Validations }} -` - -const workspaceRelayTemplate = ` -ID: {{ .ID }} -Name: {{ .Alias }} -Version: {{ .Version }} -Created: {{ TimeStr .Created }} -Updated: {{ TimeStr .Updated }} - -Last Seen: {{ TimeStr .LastSeen }} - -IP Address: {{ .IpAddr }} -SSH Port: {{ .SshPort }} - -Location: - AZ: {{ .Location.AZ }} - Rack Name: {{ .Location.RackName }} - Rack Unit: {{ .Location.RackUnitStart }} - Rack ID: {{ .Location.RackID }} -` - -const relayTemplate = ` -ID: {{ .ID }} -Serial Number: {{ .SerialNumber }} -Name: {{ .Name }} -Version: {{ .Version }} -Created: {{ TimeStr .Created }} -Updated: {{ TimeStr .Updated }} - -IP Address: {{ .IpAddr }} -SSH Port: {{ .SshPort }} -` - -const rackSummaryTemplate = ` -Name: {{ .AZ }} {{ .Name }} -ID: {{ .ID }} -Size: {{ .RackSize }} -Phase: {{ .Phase }} -Device Progress: {{ range .Statuses }} - * {{ .Status }}: {{ .Count -}} -{{end}} -` - -const workspaceTemplate = ` -Name: {{ .Name }} -ID: {{ .ID }} -Description: {{ .Description }} -Your Role: {{ .Role }} -Your Role Was Derived From: {{ if eq "" $.Via }}[Direct Assignment]{{ else }}{{ .Via }}{{ end }} -` - -const detailedUserTemplate = ` -ID: {{ .ID }} -Name: {{ .Name }} -Email: {{ .Email }} -System Admin: {{ if $.IsAdmin }}Yes{{ else }}No{{ end }} - -Created: {{ TimeStr .Created }} -Last Login: {{ if $.LastLogin.IsZero }}Never/Unknown{{ else }}{{ TimeStr .LastLogin }}{{ end }} - - -Workspaces: -{{ .Workspaces }} - -Organizations: -{{ .Organizations }} -` - -const organizationTemplate = ` -Name: {{ .Name }} -ID: {{ .ID }} -Description: {{ .Description }} -` - -const datacenterTemplate = ` -ID: {{ .ID }} -Vendor: {{ .Vendor }} -Vendor Name: {{ .VendorName }} -Region: {{ .Region }} -Location: {{ .Location }} - -Created: {{ TimeStr .Created }} -Updated: {{ TimeStr .Updated }} -` - -const roomTemplate = ` -ID: {{ .ID }} -Alias: {{ .Alias }} -AZ: {{ .AZ }} -Vendor Name: {{ .VendorName }} -Datacenter ID: {{ .DatacenterID }} - -Created: {{ TimeStr .Created }} -Updated: {{ TimeStr .Updated }} -` - -const rackRoleTemplate = ` -Name: {{ .Name }} -Rack Size: {{ .RackSize }} - -Created: {{ TimeStr .Created }} -Updated: {{ TimeStr .Updated }} -` - -const rackTemplate = ` -ID: {{ .ID }} -Name: {{ .Name }} -Serial Number: {{ .SerialNumber }} -Asset Tag: {{ .AssetTag }} -Phase: {{ .Phase }} -Role: {{ .Role.Name }} -Room: {{ .Room.Alias }} - -Created: {{ TimeStr .Created }} -Updated: {{ TimeStr .Updated }} -` - -const deviceLocationTemplate = ` -Datacenter: - ID: {{ .Datacenter.ID }} - Vendor: {{ .Datacenter.Vendor }} - Vendor Name: {{ .Datacenter.VendorName }} - Region: {{ .Datacenter.Region }} - Location: {{ .Datacenter.Location }} - -Room: - ID: {{ .Room.ID }} - Alias: {{ .Room.Alias }} - AZ: {{ .Room.AZ }} - Vendor Name: {{ .Room.VendorName }} - -Rack: - ID: {{ .Rack.ID }} - Name: {{ .Rack.Name }} - Serial Number: {{ .Rack.SerialNumber }} - Asset Tag: {{ .Rack.AssetTag }} - Phase: {{ .Rack.Phase }} - Role: {{ .Rack.Role.Name }} - -Rack Unit Start: {{ .RackUnitStart }} -` - -const deviceNicTemplate = ` -Name: {{ .InterfaceName }} -Vendor: {{ .InterfaceVendor }} - -IP Address: {{ .IpAddress }} -MAC: {{ .MAC }} -MTU: {{ .MTU }} -State: {{ .State }} - -Device ID: {{ .DeviceID }} -` - -const buildTemplate = ` -Name: {{ .Name }} -Description: {{ .Description }} -Created: {{ .Created }} -Started: {{ .Started }} -Completed: {{ .Completed }} -Marked Complete By: {{ .CompletedUser.Email }} -` - -const validationPlanTemplate = ` -ID: {{ .ID }} -Name: {{ .Name }} -Description: {{ .Description }} -Created: {{ .Created }} -` - -const hardwareProductTemplate = ` -ID: {{ .ID }} -Name: {{ .Name }} -SKU: {{ .SKU }} - -Created: {{ TimeStr .Created }} -Updated: {{ TimeStr .Updated }} -` diff --git a/user.go b/user.go deleted file mode 100644 index 4102671..0000000 --- a/user.go +++ /dev/null @@ -1,354 +0,0 @@ -// Copyright Joyent, Inc. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package main - -//lint:file-ignore U1000 WIP - -import ( - "encoding/json" - "fmt" - "net/url" - "sort" - "strings" - "time" - - "github.com/gofrs/uuid" - cli "github.com/jawher/mow.cli" - "github.com/joyent/kosh/tables" - "github.com/joyent/kosh/template" -) - -type Users struct { - *Conch -} - -func (c *Conch) Users() *Users { - return &Users{c} -} - -type UserAndRole struct { - ID uuid.UUID `json:"id" faker:"uuid"` - Name string `json:"name"` - Email string `json:"email" faker:"email"` - Role string `json:"role"` -} - -type UserAndRoles []UserAndRole - -func (u UserAndRoles) Len() int { - return len(u) -} - -func (u UserAndRoles) Swap(i, j int) { - u[i], u[j] = u[j], u[i] -} - -func (u UserAndRoles) Less(i, j int) bool { - return u[i].Name < u[j].Name -} - -func (ur UserAndRoles) String() string { - sort.Sort(ur) - if API.JsonOnly { - return API.AsJSON(ur) - } - - tableString := &strings.Builder{} - table := tables.NewTable(tableString) - tables.TableToMarkdown(table) - - table.SetHeader([]string{ - "ID", - "Name", - "Email", - "Role", - }) - - for _, u := range ur { - table.Append([]string{ - u.ID.String(), - u.Name, - u.Email, - u.Role, - }) - } - - table.Render() - return tableString.String() -} - -/*****/ - -// In the json schema, DetailedUser is UserDetailed and DetailedUsers is UsersDetailed - -type DetailedUser struct { - ID uuid.UUID `json:"id" faker:"uuid"` - Name string `json:"name"` - Email string `json:"email"` - Created time.Time `json:"created"` - LastLogin time.Time `json:"last_login,omitempty"` - LastSeen time.Time `json:"last_seen,omitempty"` - RefuseSessionAuth bool `json:"refuse_session_auth"` - ForcePasswordChange bool `json:"force_password_change"` - IsAdmin bool `json:"is_admin"` - Workspaces WorkspaceAndRoles `json:"workspaces"` - Organizations OrgAndRoles `json:"organizations"` - Builds interface{} `json:"builds"` // TODO build support -} - -func (u DetailedUser) String() string { - if API.JsonOnly { - return API.AsJSON(u) - } - - t, err := template.NewTemplate().Parse(detailedUserTemplate) - if err != nil { - panic(err) - } - - buf := &strings.Builder{} - - if err := t.Execute(buf, u); err != nil { - panic(err) - } - - return buf.String() -} - -type DetailedUsers []DetailedUser - -func (u DetailedUsers) Len() int { - return len(u) -} - -func (u DetailedUsers) Swap(i, j int) { - u[i], u[j] = u[j], u[i] -} - -func (u DetailedUsers) Less(i, j int) bool { - return u[i].Name < u[j].Name -} - -func (u *Users) Me() (user DetailedUser) { - res := u.Do(u.Sling().Get("/user/me")) - if ok := res.Parse(&user); !ok { - panic(res) - } - ret := make(WorkspaceAndRoles, 0) - cache := make(map[uuid.UUID]string) - - for _, ws := range user.Workspaces { - if (ws.ParentID != uuid.UUID{}) { - if _, ok := cache[ws.ParentID]; !ok { - cache[ws.ParentID] = API.Workspaces().Get(ws.ParentID).Name - } - - ws.Parent = cache[ws.ParentID] - } - - if (ws.RoleVia != uuid.UUID{}) { - if _, ok := cache[ws.RoleVia]; !ok { - cache[ws.RoleVia] = API.Workspaces().Get(ws.RoleVia).Name - } - - ws.Via = cache[ws.RoleVia] - } - - ret = append(ret, ws) - } - - user.Workspaces = ret - - return user -} - -type UserSettings map[string]interface{} - -func (u UserSettings) String() string { - if API.JsonOnly { - return API.AsJSON(u) - } - - keys := make([]string, 0) - for setting := range u { - keys = append(keys, setting) - } - sort.Strings(keys) - - tableString := &strings.Builder{} - table := tables.NewTable(tableString) - tables.TableToMarkdown(table) - - table.SetHeader([]string{ - "Key", - "Value", - }) - - for _, key := range keys { - table.Append([]string{ - key, - fmt.Sprintf("%v", u[key]), - }) - } - - table.Render() - return tableString.String() -} - -func (u *Users) MySettings() UserSettings { - settings := make(UserSettings) - - res := u.Do(u.Sling().Get("/user/me/settings")) - if ok := res.Parse(&settings); !ok { - panic(res) - } - - return settings -} - -func (u *Users) GetMySetting(name string) interface{} { - uri := fmt.Sprintf( - "/user/me/settings/%s", - url.PathEscape(name), - ) - - data := make(map[string]interface{}) - - res := u.DoBadly(u.Sling().Get(uri)) - if res.StatusCode() == 404 { - return "" - } - if res.IsError() { - panic(res) - } - - if ok := res.Parse(&data); !ok { - panic(res) - } - - return data[name] -} - -func (u *Users) SetMySetting(name string, value string) interface{} { - var userData interface{} - - if err := json.Unmarshal([]byte(value), &userData); err != nil { - // If the value doesn't parse properly as JSON, we assume it's - // literal. This catches the single-value case where we want - // { "foo": "bar" } by just letting the user pass in a name of - // "foo" and a value of "bar" - - // The perhaps surprising side effect is that crappy JSON will - // enter the database as a string. - userData = value - } - - data := make(map[string]interface{}) - data[name] = userData - - uri := fmt.Sprintf( - "/user/me/settings/%s", - url.PathEscape(name), - ) - - // This endpoint either returns errors or a 204. We catch errors elsewhere. - _ = u.Do( - u.Sling().New().Post(uri). - Set("Content-Type", "application/json"). - BodyJSON(data), - ) - - // Pivot into a get so the caller can make sure the data was stored properly - return u.GetMySetting(name) -} - -func (u *Users) DeleteMySetting(name string) { - uri := fmt.Sprintf( - "/user/me/settings/%s", - url.PathEscape(name), - ) - - res := u.DoBadly(u.Sling().Delete(uri)) - if res.StatusCode() == 404 { - return - } - if res.IsError() { - panic(res) - } -} - -/*****/ - -func init() { - App.Command("whoami", "Display details of the current user", func(cmd *cli.Cmd) { - cmd.Action = func() { - fmt.Println(API.Users().Me()) - } - }) - - App.Command("user", "Commands for dealing with the current user (you)", func(cmd *cli.Cmd) { - cmd.Command("profile", "View your Conch profile", func(cmd *cli.Cmd) { - cmd.Action = func() { - fmt.Println(API.Users().Me()) - } - }) - - cmd.Command("settings", "Get the settings for the current user", func(cmd *cli.Cmd) { - cmd.Action = func() { - fmt.Println(API.Users().MySettings()) - } - }) - - cmd.Command("setting", "Commands for dealing with a single setting for the current user", func(cmd *cli.Cmd) { - settingNameArg := cmd.StringArg("NAME", "", "The string name of a setting") - - cmd.Spec = "NAME" - - cmd.Command("get", "Get a setting for the current user", func(cmd *cli.Cmd) { - cmd.Action = func() { - setting := API.Users().GetMySetting(*settingNameArg) - if API.JsonOnly { - out := make(map[string]interface{}) - out[*settingNameArg] = setting - API.PrintJSON(out) - return - } - - fmt.Printf("%s\n", setting) - } - }) - - cmd.Command("set", "Set a setting for the current user", func(cmd *cli.Cmd) { - valueArg := cmd.StringArg("VALUE", "", "The new value of the setting") - - cmd.Spec = "VALUE" - - cmd.Action = func() { - setting := API.Users().SetMySetting( - *settingNameArg, - *valueArg, - ) - - if API.JsonOnly { - out := make(map[string]interface{}) - out[*settingNameArg] = setting - API.PrintJSON(out) - return - } - - fmt.Printf("%s\n", setting) - } - }) - - cmd.Command("delete", "Delete a setting for the current user", func(cmd *cli.Cmd) { - cmd.Action = func() { - API.Users().DeleteMySetting(*settingNameArg) - } - }) - }) - }) -} diff --git a/user_integration_test.go b/user_integration_test.go deleted file mode 100644 index 808c0a2..0000000 --- a/user_integration_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package main - -import ( - "testing" -) - -func TestUserAPIIntergration(t *testing.T) { - setupAPIClient() - r := setupRecorder("fixtures/conch-v3/user") - defer r() // Make sure recorder is stopped once done with it - - t.Run("me", func(t *testing.T) { - _ = API.Users().Me() - }) - - t.Run("me-settings", func(t *testing.T) { - _ = API.Users().MySettings() - }) -} diff --git a/validations.go b/validations.go deleted file mode 100644 index 302b098..0000000 --- a/validations.go +++ /dev/null @@ -1,292 +0,0 @@ -// Copyright Joyent, Inc. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package main - -import ( - "bytes" - "errors" - "fmt" - "net/url" - "sort" - "strings" - "time" - - "github.com/gofrs/uuid" - cli "github.com/jawher/mow.cli" - "github.com/joyent/kosh/tables" - "github.com/joyent/kosh/template" -) - -type Validations struct { - *Conch -} - -func (c *Conch) Validations() *Validations { - return &Validations{c} -} - -/***/ -type ValidationPlan struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Created time.Time `json:"created"` -} - -func (v ValidationPlan) String() string { - if API.JsonOnly { - return API.AsJSON(v) - } - - t, err := template.NewTemplate().Parse(validationPlanTemplate) - if err != nil { - panic(err) - } - - buf := new(bytes.Buffer) - - if err := t.Execute(buf, v); err != nil { - panic(err) - } - - return buf.String() - -} - -type ValidationPlans []ValidationPlan - -func (v ValidationPlans) Len() int { - return len(v) -} - -func (v ValidationPlans) Swap(i, j int) { - v[i], v[j] = v[j], v[i] -} - -func (v ValidationPlans) Less(i, j int) bool { - return v[i].Name < v[j].Name -} - -func (v ValidationPlans) String() string { - sort.Sort(v) - if API.JsonOnly { - return API.AsJSON(v) - } - if len(v) == 0 { - return "" - } - - tableString := &strings.Builder{} - table := tables.NewTable(tableString) - tables.TableToMarkdown(table) - table.SetRowLine(true) - - table.SetHeader([]string{ - "ID", - "Name", - "Description", - "Created", - }) - - for _, p := range v { - table.Append([]string{ - template.CutUUID(p.ID.String()), - p.Name, - p.Description, - p.Created.String(), - }) - } - - table.Render() - return tableString.String() -} - -func (v Validations) GetPlan(id uuid.UUID) (vp ValidationPlan) { - if (id == uuid.UUID{}) { - return vp - } - - uri := fmt.Sprintf("/validation_plan/%s", url.PathEscape(id.String())) - res := v.Do(v.Sling().New().Get(uri)) - if ok := res.Parse(&vp); !ok { - panic(res) - } - - return vp -} - -func (v Validations) GetAllPlans() (list ValidationPlans) { - - res := v.Do(v.Sling().New().Get("/validation_plan")) - if ok := res.Parse(&list); !ok { - panic(res) - } - - return -} - -func (v Validations) FindPlanID(id string) (bool, uuid.UUID) { - ids := make([]uuid.UUID, 0) - for _, vp := range v.GetAllPlans() { - ids = append(ids, vp.ID) - } - - return FindUUID(id, ids) -} - -func (v Validations) GetPlanByName(name string) (vp ValidationPlan) { - - uri := fmt.Sprintf("/validation_plan/%s", url.PathEscape(name)) - res := v.Do(v.Sling().New().Get(uri)) - if ok := res.Parse(&vp); !ok { - panic(res) - } - - return vp -} - -/***/ - -type ValidationResult struct { - ID uuid.UUID `json:"id,omitempty"` - Category string `json:"category"` - Component string `json:"component,omitempty"` - HardwareProductID uuid.UUID `json:"hardware_product_id"` - Hint string `json:"hint,omitempty"` - Message string `json:"message"` - Order int `json:"order"` - Status string `json:"status"` - ValidationID uuid.UUID `json:"validation_id"` -} - -type ValidationResults []ValidationResult - -func (v ValidationResults) Len() int { - return len(v) -} - -func (v ValidationResults) Swap(i, j int) { - v[i], v[j] = v[j], v[i] -} - -func (v ValidationResults) Less(i, j int) bool { - return v[i].Category < v[j].Category -} - -func (v ValidationResults) String() string { - sort.Sort(v) - if API.JsonOnly { - return API.AsJSON(v) - } - if len(v) == 0 { - return "" - } - - tableString := &strings.Builder{} - table := tables.NewTable(tableString) - tables.TableToMarkdown(table) - table.SetRowLine(true) - - table.SetHeader([]string{ - "Status", - "Category", - "Component", - "Message", - }) - - for _, r := range v { - table.Append([]string{ - r.Status, - r.Category, - r.Component, - r.Message, - }) - } - - table.Render() - return tableString.String() -} - -/***/ - -type ValidationStateWithResults struct { - ID uuid.UUID `json:"id"` - Completed time.Time `json:"completed"` - Created time.Time `json:"created"` - DeviceID uuid.UUID `json:"device_id"` - Status string `json:"status"` - ValidationPlanID uuid.UUID `json:"validation_plan_id"` - DeviceReportID uuid.UUID `json:"device_report_id"` - - Results ValidationResults `json:"results"` -} - -func (v ValidationStateWithResults) String() string { - if API.JsonOnly { - return API.AsJSON(v) - } - - type extendedVsR struct { - ValidationStateWithResults - ValidationPlan ValidationPlan `json:"-"` - } - - out := extendedVsR{ - v, - API.Validations().GetPlan(v.ValidationPlanID), - } - - t, err := template.NewTemplate().Parse(validationStateWithResultsTemplate) - if err != nil { - panic(err) - } - - buf := new(bytes.Buffer) - - if err := t.Execute(buf, out); err != nil { - panic(err) - } - - return buf.String() -} - -func init() { - - App.Command("validation", "Work with validations", func(cmd *cli.Cmd) { - v := API.Validations() - cmd.Command("plans", "Work with validation plans", func(cmd *cli.Cmd) { - cmd.Command("get ls", "Get a list of all plans", func(cmd *cli.Cmd) { - cmd.Action = func() { - fmt.Println(v.GetAllPlans()) - } - }) - - }) - cmd.Command("plan", "Work with a specific validation plan", func(cmd *cli.Cmd) { - var p ValidationPlan - idArg := cmd.StringArg("UUID", "", "UUID of the Validation Plan, Short IDs accepted") - - cmd.Spec = "UUID" - cmd.Before = func() { - ok, planID := v.FindPlanID(*idArg) - if !ok { - panic(errors.New("could not find the validation plan")) - } - p = v.GetPlan(planID) - } - - cmd.Command("get", "Get information about a single build by its name", func(cmd *cli.Cmd) { - - cmd.Action = func() { - fmt.Println(p) - } - }) - - }) - }) - -} diff --git a/workspaces.go b/workspaces.go deleted file mode 100644 index 2dc511c..0000000 --- a/workspaces.go +++ /dev/null @@ -1,1172 +0,0 @@ -// Copyright Joyent, Inc. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package main - -import ( - "bytes" - "errors" - "fmt" - "net/url" - "sort" - "strconv" - "strings" - "time" - - "github.com/gofrs/uuid" - cli "github.com/jawher/mow.cli" - "github.com/joyent/kosh/tables" - "github.com/joyent/kosh/template" -) - -type Workspaces struct { - *Conch -} - -func (c *Conch) Workspaces() *Workspaces { - return &Workspaces{c} -} - -/****/ - -var WorkspaceRoleList = []string{"admin", "rw", "ro"} - -func prettyWorkspaceRoleList() string { - return strings.Join(WorkspaceRoleList, ", ") -} - -func okWorkspaceRole(role string) bool { - for _, b := range WorkspaceRoleList { - if role == b { - return true - } - } - return false -} - -/****/ - -type WorkspaceAndRole struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - Description string `json:"description,omitempty"` - ParentID uuid.UUID `json:"parent_workspace_id,omitempty"` - Role string `json:"role"` - RoleVia uuid.UUID `json:"role_via"` - - // These are for user friendly variants of those UUIDs - Parent string `json:"parent"` - Via string `json:"via"` -} - -func (w WorkspaceAndRole) String() string { - if API.JsonOnly { - return API.AsJSON(w) - } - - t, err := template.NewTemplate().Parse(workspaceTemplate) - if err != nil { - panic(err) - } - - buf := new(bytes.Buffer) - - if err := t.Execute(buf, w); err != nil { - panic(err) - } - - return buf.String() -} - -type WorkspaceAndRoles []WorkspaceAndRole - -func (w WorkspaceAndRoles) Len() int { - return len(w) -} - -func (w WorkspaceAndRoles) Swap(i, j int) { - w[i], w[j] = w[j], w[i] -} - -func (w WorkspaceAndRoles) Less(i, j int) bool { - return w[i].Name < w[j].Name -} - -func (w WorkspaceAndRoles) String() string { - sort.Sort(w) - if API.JsonOnly { - return API.AsJSON(w) - } - - tableString := &strings.Builder{} - table := tables.NewTable(tableString) - tables.TableToMarkdown(table) - - table.SetHeader([]string{ - "Name", - "Role", - "Description", - "Role Via", - "Parent", - }) - - for _, ws := range w { - table.Append([]string{ - ws.Name, - ws.Role, - ws.Description, - ws.Via, - ws.Parent, - }) - } - - table.Render() - return tableString.String() -} - -/****/ - -func (w *Workspaces) GetAll() WorkspaceAndRoles { - list := make(WorkspaceAndRoles, 0) - - res := w.Do(w.Sling().Get("/workspace")) - if ok := res.Parse(&list); !ok { - panic(res) - } - - ret := make(WorkspaceAndRoles, 0) - - cache := make(map[uuid.UUID]string) - - for _, ws := range list { - if (ws.ParentID != uuid.UUID{}) { - if _, ok := cache[ws.ParentID]; !ok { - cache[ws.ParentID] = w.Get(ws.ParentID).Name - } - - ws.Parent = cache[ws.ParentID] - } - - if (ws.RoleVia != uuid.UUID{}) { - if _, ok := cache[ws.RoleVia]; !ok { - cache[ws.RoleVia] = w.Get(ws.RoleVia).Name - } - - ws.Via = cache[ws.RoleVia] - } - - ret = append(ret, ws) - } - - return ret -} - -func (w *Workspaces) Get(id uuid.UUID) (ws WorkspaceAndRole) { - res := w.Do(w.Sling().Get("/workspace/" + url.PathEscape(id.String()))) - if ok := res.Parse(&ws); !ok { - panic(res) - } - if (ws.ParentID != uuid.UUID{}) { - ws.Parent = w.Get(ws.ParentID).Name - } - - if (ws.RoleVia != uuid.UUID{}) { - ws.Via = w.Get(ws.ParentID).Name - } - - return ws -} - -func (w *Workspaces) GetByName(name string) (ws WorkspaceAndRole) { - res := w.Do(w.Sling().Get("/workspace/" + url.PathEscape(name))) - if ok := res.Parse(&ws); !ok { - panic(res) - } - - if (ws.ParentID != uuid.UUID{}) { - ws.Parent = w.Get(ws.ParentID).Name - } - - if (ws.RoleVia != uuid.UUID{}) { - ws.Via = w.Get(ws.ParentID).Name - } - - return ws -} - -func (w *Workspaces) Create(parent string, sub string, desc string) (ws WorkspaceAndRole) { - uri := fmt.Sprintf( - "/workspace/%s/child", - url.PathEscape(parent), - ) - - payload := make(map[string]string) - payload["name"] = sub - if desc != "" { - payload["description"] = desc - } - - res := w.Do( - w.Sling().New().Post(uri). - Set("Content-Type", "application/json"). - BodyJSON(payload), - ) - - if ok := res.Parse(&ws); !ok { - panic(res) - } - - if (ws.ParentID != uuid.UUID{}) { - ws.Parent = w.Get(ws.ParentID).Name - } - - if (ws.RoleVia != uuid.UUID{}) { - ws.Via = w.Get(ws.ParentID).Name - } - - return ws -} - -/***/ - -type WorkspaceUser struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - Email string `json:"email"` - Role string `json:"role"` - RoleVia uuid.UUID `json:"role_via"` - - Via string `json:"via"` -} - -type WorkspaceUsers []WorkspaceUser - -func (w WorkspaceUsers) String() string { - if API.JsonOnly { - return API.AsJSON(w) - } - - tableString := &strings.Builder{} - table := tables.NewTable(tableString) - tables.TableToMarkdown(table) - - table.SetHeader([]string{ - "Name", - "Email", - "Role", - "Role Via", - }) - - sort.Sort(w) - - for _, user := range w { - table.Append([]string{ - user.Name, - user.Email, - user.Role, - user.Via, - }) - } - - table.Render() - - return tableString.String() -} - -func (w WorkspaceUsers) Len() int { - return len(w) -} - -func (w WorkspaceUsers) Swap(i, j int) { - w[i], w[j] = w[j], w[i] -} - -func (w WorkspaceUsers) Less(i, j int) bool { - return w[i].Name < w[j].Name -} - -/***/ - -func (w *Workspaces) GetUsers(name string) WorkspaceUsers { - list := make(WorkspaceUsers, 0) - users := make(WorkspaceUsers, 0) - - url := fmt.Sprintf( - "/workspace/%s/user", - url.PathEscape(name), - ) - - res := w.Do(w.Sling().Get(url)) - if ok := res.Parse(&list); !ok { - panic(res) - } - - for _, u := range list { - if (u.RoleVia != uuid.UUID{}) { - u.Via = w.Get(u.RoleVia).Name - } - users = append(users, u) - } - - return users -} - -func (w *Workspaces) AddOrModifyUser(workspace string, email string, role string, sendEmail bool) bool { - payload := make(map[string]string) - - if email == "" { - panic(errors.New("email address is required")) - } else { - payload["email"] = email - } - - if role == "" { - payload["role"] = "ro" - } else { - payload["role"] = role - } - - params := make(map[string]int) - if sendEmail { - params["send_email"] = 1 - } else { - params["send_email"] = 0 - } - - uri := fmt.Sprintf( - "/workspace/%s/user", - url.PathEscape(workspace), - ) - - p := struct { - Email string `json:"email"` - Role string `json:"role"` - }{payload["email"], payload["role"]} - - q := struct { - SendEmail int `url:"send_mail"` - }{params["send_email"]} - - res := w.Do( - w.Sling().Post(uri). - Set("Content-Type", "application/json"). - BodyJSON(p). - QueryStruct(q), - ) - - // NOTE: at time of writing, the only possible success response is a 204. - // Everything else is a 400 or above. Here, that translates to a panic if - // we didn't get a 20x. if we got a 20x, it's a 204. In the end, this will - // return true or panic. - return res.StatusCode() == 204 -} - -func (w *Workspaces) RemoveUser(workspace string, email string, sendEmail bool) bool { - - params := make(map[string]int) - if sendEmail { - params["send_email"] = 1 - } else { - params["send_email"] = 0 - } - q := struct { - SendEmail int `url:"send_mail"` - }{params["send_email"]} - - uri := fmt.Sprintf( - "/workspace/%s/user/%s", - url.PathEscape(workspace), - url.PathEscape(email), - ) - res := w.Do(w.Sling().Delete(uri).QueryStruct(q)) - - // NOTE: at time of writing, the only possible success response is a 204. - // Everything else is a 400 or above. Here, that translates to a panic if - // we didn't get a 200. if we got a 200, it's a 204. In the end, this will - // return true or panic. - return res.StatusCode() == 204 - -} - -/***/ - -func (w *Workspaces) GetDevices(name string, health string, validated *bool) DeviceList { - var opts interface{} - - valid := 0 - if (validated != nil) && *validated { - valid = 1 - } - - if (health != "") && (validated != nil) { - opts = struct { - Health string `url:"health"` - Validated int `url:"validated"` - }{url.PathEscape(health), valid} - } else if health != "" { - opts = struct { - Health string `url:"health"` - }{url.PathEscape(health)} - } else if validated != nil { - opts = struct { - Validated int `url:"validated"` - }{valid} - } - - url := fmt.Sprintf( - "/workspace/%s/device", - url.PathEscape(name), - ) - devices := make(DeviceList, 0) - - res := w.Do(w.Sling().Get(url).QueryStruct(opts)) - if ok := res.Parse(&devices); !ok { - panic(res) - } - - return devices -} - -/***/ - -func (w *Workspaces) GetDirectChildren(name string) WorkspaceAndRoles { - children := w.GetChildren(name) - directs := make(WorkspaceAndRoles, 0) - - for _, child := range children { - if child.Parent == name { - directs = append(directs, child) - } - } - return directs -} - -func (w *Workspaces) GetChildren(name string) WorkspaceAndRoles { - list := make(WorkspaceAndRoles, 0) - - uri := fmt.Sprintf("/workspace/%s/child", url.PathEscape(name)) - res := w.Do(w.Sling().Get(uri)) - if ok := res.Parse(&list); !ok { - panic(res) - } - - ret := make(WorkspaceAndRoles, 0) - - cache := make(map[uuid.UUID]string) - - for _, ws := range list { - if (ws.ParentID != uuid.UUID{}) { - if _, ok := cache[ws.ParentID]; !ok { - cache[ws.ParentID] = w.Get(ws.ParentID).Name - } - - ws.Parent = cache[ws.ParentID] - } - - if (ws.RoleVia != uuid.UUID{}) { - if _, ok := cache[ws.RoleVia]; !ok { - cache[ws.RoleVia] = w.Get(ws.RoleVia).Name - } - - ws.Via = cache[ws.RoleVia] - } - - ret = append(ret, ws) - } - - return ret -} - -/***/ - -// The DeviceProgress element is a map where the string is 'valid' or of the -// 'device_health' type containing 'error', 'fail', 'unknown', 'pass' -type WorkspaceRackSummary struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - Phase string `json:"phase"` - RoleName string `json:"role_name"` - RackSize int `json:"rack_size" faker:"rack_size"` - DeviceProgress map[string]int `json:"device_progress"` -} - -type WorkspaceRackSummaries map[string][]WorkspaceRackSummary - -// The AZ gets lost in this conversion -func (summaries WorkspaceRackSummaries) Slice() []WorkspaceRackSummary { - s := make([]WorkspaceRackSummary, 0) - - for _, values := range summaries { - s = append(s, values...) - } - return s -} - -func (summaries WorkspaceRackSummaries) String() string { - if API.JsonOnly { - return API.AsJSON(summaries) - } - - var output string - - keys := make([]string, 0) - for az := range summaries { - keys = append(keys, az) - } - - sort.Strings(keys) - - for _, az := range keys { - for _, summary := range summaries[az] { - type status struct { - Status string - Count int - } - statusii := make([]*status, 0) - - statusStrs := make([]string, 0) - for str := range summary.DeviceProgress { - statusStrs = append(statusStrs, str) - } - sort.Strings(statusStrs) - - for _, statusStr := range statusStrs { - statusii = append(statusii, &status{ - Status: statusStr, - Count: summary.DeviceProgress[statusStr], - }) - } - - s := struct { - WorkspaceRackSummary - AZ string - Statuses []*status - }{summary, az, statusii} - - t, err := template.NewTemplate().Parse(rackSummaryTemplate) - if err != nil { - panic(err) - } - - buf := new(bytes.Buffer) - - if err := t.Execute(buf, s); err != nil { - panic(err) - } - - output = output + buf.String() - } - } - return output -} - -/****/ - -func (w *Workspaces) GetRackSummaries(name string) WorkspaceRackSummaries { - summaries := make(WorkspaceRackSummaries) - - url := fmt.Sprintf( - "/workspace/%s/rack", - url.PathEscape(name), - ) - - res := w.Do(w.Sling().Get(url)) - - if ok := res.Parse(&summaries); !ok { - panic(res) - } - - return summaries -} - -/***/ - -type WorkspaceRelay struct { - ID string `json:"id"` - Name string `json:"name,omitempty"` - Version string `json:"version,omitempty"` - IpAddr string `json:"ipaddr,omitempty"` - SshPort int `json:"ssh_port,omitempty"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` - LastSeen time.Time `json:"last_seen"` - NumDevices int `json:"num_devices"` - Location struct { - RackID uuid.UUID `json:"rack_id"` - RackName string `json:"rack_name"` - RackUnitStart int `json:"rack_unit_start" faker:"rack_unit_start"` - RoleName string `json:"role_name"` - AZ string `json:"az"` - } `json:"location"` -} - -func (w WorkspaceRelay) String() string { - if API.JsonOnly { - return API.AsJSON(w) - } - - t, err := template.NewTemplate().Parse(workspaceRelayTemplate) - if err != nil { - panic(err) - } - - buf := new(bytes.Buffer) - - if err := t.Execute(buf, w); err != nil { - panic(err) - } - - return buf.String() -} - -type WorkspaceRelays []WorkspaceRelay - -func (w WorkspaceRelays) Len() int { - return len(w) -} - -func (w WorkspaceRelays) Swap(i, j int) { - w[i], w[j] = w[j], w[i] -} - -func (w WorkspaceRelays) Less(i, j int) bool { - return w[i].LastSeen.Unix() < w[j].LastSeen.Unix() -} - -func (w WorkspaceRelays) String() string { - sort.Sort(w) - if API.JsonOnly { - return API.AsJSON(w) - } - - tableString := &strings.Builder{} - table := tables.NewTable(tableString) - tables.TableToMarkdown(table) - - table.SetHeader([]string{ - "ID", - "Version", - "IP", - "Port", - "Last Seen", - "Location", - }) - - for _, r := range w { - rackID := "" - if (r.Location.RackID != uuid.UUID{}) { - rackID = fmt.Sprintf( - "[%s]", - template.CutUUID(r.Location.RackID.String()), - ) - } - location := fmt.Sprintf( - "%s %s - RU %d %s", - r.Location.AZ, - r.Location.RackName, - r.Location.RackUnitStart, - rackID, - ) - table.Append([]string{ - r.ID, - r.Version, - r.IpAddr, - strconv.Itoa(r.SshPort), - template.TimeStr(r.LastSeen), - location, - }) - } - - table.Render() - return tableString.String() -} - -func (w *Workspaces) GetRelays(name string) WorkspaceRelays { - relays := make(WorkspaceRelays, 0) - - uri := fmt.Sprintf( - "/workspace/%s/relay", - url.PathEscape(name), - ) - - res := w.Do(w.Sling().Get(uri)) - if ok := res.Parse(&relays); !ok { - panic(res) - } - - return relays -} - -func (w *Workspaces) GetRelayDevices(workspace string, relay string) DeviceList { - devices := make(DeviceList, 0) - - uri := fmt.Sprintf( - "/workspace/%s/relay/%s/device", - url.PathEscape(workspace), - url.PathEscape(relay), - ) - - res := w.Do(w.Sling().Get(uri)) - if ok := res.Parse(&devices); !ok { - panic(res) - } - - return devices -} - -/******/ - -func (w *Workspaces) AddRack(workspace string, rackID uuid.UUID) WorkspaceRackSummaries { - // I really explictly am not supporting the full functionality of this - // endpoint. Tehnically, it supports updating the rack's serial number and - // asset tag at the same time you add it to the workspace. That's.. yeah. - // If you want to change those fields, use the function for updating a - // rack's data, not this one. - uri := fmt.Sprintf( - "/workspace/%s/rack", - url.PathEscape(workspace), - ) - - payload := struct { - ID string `json:"id"` - }{rackID.String()} - - // Ignoring the return because we're about to pivot into a different call. - // If this call errors out, it'll panic on its own - _ = w.Do( - w.Sling().New().Post(uri). - Set("Content-Type", "application/json"). - BodyJSON(payload), - ) - - return w.GetRackSummaries(workspace) -} - -func (w *Workspaces) RemoveRack(workspace string, rackID uuid.UUID) WorkspaceRackSummaries { - uri := fmt.Sprintf( - "/workspace/%s/rack/%s", - url.PathEscape(workspace), - url.PathEscape(rackID.String()), - ) - - // Ignoring the return because we're about to pivot into a different call. - // If this call errors out, it'll panic on its own - _ = w.Do(w.Sling().New().Delete(uri)) - - return w.GetRackSummaries(workspace) -} - -func (w *Workspaces) FindRackID(workspace string, id string) (bool, uuid.UUID) { - - summaries := make([]WorkspaceRackSummary, 0) - - summaries = append( - summaries, - w.GetRackSummaries(workspace).Slice()..., - ) - - ws := w.GetByName(workspace) - if ws.Parent != "" { - summaries = append( - summaries, - w.GetRackSummaries(ws.Parent).Slice()..., - ) - } - - ids := make([]uuid.UUID, 0) - for _, s := range summaries { - ids = append(ids, s.ID) - } - - return FindUUID(id, ids) -} - -/******/ - -func init() { - App.Command("workspaces", "Get a list of all workspaces you have access to", func(cmd *cli.Cmd) { - cmd.Action = func() { - fmt.Println(API.Workspaces().GetAll()) - } - }) - - App.Command("workspace", "Deal with a single workspace", func(cmd *cli.Cmd) { - var workspaceName string - workspaceNameArg := cmd.StringArg( - "NAME", - "", - "The string name of the workspace") - - cmd.Spec = "NAME" - cmd.Before = func() { - workspaceName = *workspaceNameArg - // TODO(sungo): should we verify that the workspace exists? - } - - cmd.Command("get", "Get information about a single workspace, using its name", func(cmd *cli.Cmd) { - cmd.Action = func() { - fmt.Println(API.Workspaces().GetByName(workspaceName)) - } - }) - - cmd.Command("create", "Create a new subworkspace", func(cmd *cli.Cmd) { - nameArg := cmd.StringArg( - "SUB", - "", - "Name of the new subworkspace", - ) - - descOpt := cmd.StringOpt( - "description desc", - "", - "A description of the workspace", - ) - - cmd.Spec = "SUB [OPTIONS]" - cmd.Action = func() { - fmt.Println(API.Workspaces().Create( - workspaceName, - *nameArg, - *descOpt, - )) - } - }) - - cmd.Command("add", "Add various structures to a single workspace", func(cmd *cli.Cmd) { - cmd.Command("user", "Add a user to a workspace", func(cmd *cli.Cmd) { - userEmailArg := cmd.StringArg( - "EMAIL", - "", - "The email of the user to add to the workspace. Does *not* create the user", - ) - - roleOpt := cmd.StringOpt( - "role", - "ro", - "The role for the user. One of: "+prettyWorkspaceRoleList(), - ) - - sendEmailOpt := cmd.BoolOpt( - "send-email", - true, - "Send email to the target user, notifying them of the change", - ) - - cmd.Spec = "EMAIL [OPTIONS]" - cmd.Action = func() { - if !okWorkspaceRole(*roleOpt) { - panic(fmt.Errorf( - "'role' value must be one of: %s", - prettyWorkspaceRoleList(), - )) - } - - if ok := API.Workspaces().AddOrModifyUser( - workspaceName, - *userEmailArg, - *roleOpt, - *sendEmailOpt, - ); ok { - - fmt.Println(API.Workspaces().GetUsers(workspaceName)) - - } else { - // It should be impossible to reach this - // code as the lower code panics in all - // known failure conditions. - panic(errors.New("failure")) - } - } - }) - - cmd.Command("rack", "Add a rack to a workspace", func(cmd *cli.Cmd) { - idArg := cmd.StringArg( - "UUID", - "", - "The UUID of the rack to add. Short UUIDs (first segment) accepted", - ) - - cmd.Spec = "UUID" - cmd.Action = func() { - - var rackID uuid.UUID - var ok bool - - if ok, rackID = API.Workspaces().FindRackID(workspaceName, *idArg); !ok { - panic(errors.New("could not locate the rack in either this workspace or its parent")) - } - - fmt.Println(API.Workspaces().AddRack( - workspaceName, - rackID, - )) - } - }) - }) - - cmd.Command("update", "Update various structures in a single workspace", func(cmd *cli.Cmd) { - cmd.Command("user", "Update a user in a workspace", func(cmd *cli.Cmd) { - userEmailArg := cmd.StringArg( - "EMAIL", - "", - "The email of the user to modify", - ) - - roleOpt := cmd.StringOpt( - "role", - "ro", - "The role for the user. One of: "+prettyWorkspaceRoleList(), - ) - - sendEmailOpt := cmd.BoolOpt( - "send-email", - true, - "Send email to the target user, notifying them of the change", - ) - - cmd.Spec = "EMAIL [OPTIONS]" - cmd.Action = func() { - if !okWorkspaceRole(*roleOpt) { - panic(fmt.Errorf( - "'role' value must be one of: %s", - prettyWorkspaceRoleList(), - )) - } - - if ok := API.Workspaces().AddOrModifyUser( - workspaceName, - *userEmailArg, - *roleOpt, - *sendEmailOpt, - ); ok { - - fmt.Println(API.Workspaces().GetUsers(workspaceName)) - } else { - // It should be impossible to reach this - // code as the lower code panics in all - // known failure conditions. - panic(errors.New("failure")) - } - } - }) - }) - - cmd.Command("remove rm", "Remove various structures from a single workspace", func(cmd *cli.Cmd) { - cmd.Command("user", "Remove a user from a workspace", func(cmd *cli.Cmd) { - userEmailArg := cmd.StringArg( - "EMAIL", - "", - "The email of the user to modify", - ) - - sendEmailOpt := cmd.BoolOpt( - "send-email", - true, - "Send email to the target user, notifying them of the change", - ) - - cmd.Spec = "EMAIL [OPTIONS]" - cmd.Action = func() { - if ok := API.Workspaces().RemoveUser( - workspaceName, - *userEmailArg, - *sendEmailOpt, - ); ok { - fmt.Println(API.Workspaces().GetUsers(workspaceName)) - } else { - // It should be impossible to reach this - // code as the lower code panics in all - // known failure conditions. - panic(errors.New("failure")) - } - } - }) - - cmd.Command("rack", "Remove a rack from a workspace", func(cmd *cli.Cmd) { - idArg := cmd.StringArg( - "UUID", - "", - "The UUID of the rack to remove. Short UUIDs (first segment) accepted", - ) - - cmd.Spec = "UUID" - cmd.Action = func() { - - var id uuid.UUID - var err error - if id, err = uuid.FromString(*idArg); err != nil { - if ok, rackID := API.Workspaces().FindRackID(workspaceName, *idArg); ok { - id = rackID - } else { - panic(errors.New("could not locate the rack in either this workspace or its parent")) - } - } - - if (id == uuid.UUID{}) { - panic(errors.New("could not locate the rack in either this workspace or its parent")) - } - - fmt.Println(API.Workspaces().RemoveRack(workspaceName, id)) - - } - }) - - }) - - cmd.Command("relays", "Get a list of relays assigned to a single workspace", func(cmd *cli.Cmd) { - cmd.Action = func() { - fmt.Println(API.Workspaces().GetRelays(workspaceName)) - } - }) - - cmd.Command("relay", "Deal with a single relay", func(cmd *cli.Cmd) { - cmd.Command("get", "Get information about a single relay", func(cmd *cli.Cmd) { - relayArg := cmd.StringArg( - "RELAY", - "", - "ID of the relay", - ) - - cmd.Spec = "RELAY" - cmd.Action = func() { - relays := API.Workspaces().GetRelays(workspaceName) - - for _, relay := range relays { - if relay.ID != *relayArg { - continue - } - - fmt.Println(relay) - return - } - panic(errors.New("relay not found")) - } - }) - - cmd.Command("devices", "Get the device list for a relay", func(cmd *cli.Cmd) { - relayArg := cmd.StringArg( - "RELAY", - "", - "ID of the relay", - ) - - cmd.Spec = "RELAY" - cmd.Action = func() { - fmt.Println(API.Workspaces().GetRelayDevices( - workspaceName, - *relayArg, - )) - } - }) - }) - - cmd.Command("children subs", "Get a list of a workspace's children", func(cmd *cli.Cmd) { - - allOpt := cmd.BoolOpt( - "all", - false, - "Retrieve all children, not just the direct lineage", - ) - - // TODO(sungo): tree mode? - cmd.Action = func() { - if *allOpt { - fmt.Println(API.Workspaces().GetChildren(workspaceName)) - } else { - fmt.Println(API.Workspaces().GetDirectChildren(workspaceName)) - } - } - }) - - cmd.Command("devices", "Get a list of devices in a single workspace, by name", func(cmd *cli.Cmd) { - healthOpt := cmd.StringOpt( - "health", - "", - "Filter by the 'health' field. Value must be one of: "+prettyDeviceHealthList(), - ) - - var validatedSetByUser bool - validatedOpt := cmd.Bool(cli.BoolOpt{ - Name: "validated", - Value: false, - Desc: "Filter by the 'validated' field", - SetByUser: &validatedSetByUser, - }) - - cmd.Action = func() { - if !validatedSetByUser { - validatedOpt = nil - } - - if *healthOpt != "" { - if !okHealth(*healthOpt) { - panic(fmt.Errorf("'health' value must be one of: %s", prettyDeviceHealthList())) - - } - } - fmt.Println(API.Workspaces().GetDevices( - workspaceName, - *healthOpt, - validatedOpt, - )) - } - }) - - cmd.Command("users", "Operate on the users assigned to a workspace", func(cmd *cli.Cmd) { - cmd.Action = func() { - fmt.Println(API.Workspaces().GetUsers(workspaceName)) - } - }) - - cmd.Command("racks", "Get a progress summary for each rack", func(cmd *cli.Cmd) { - phaseOpt := cmd.StringOpt( - "phase", - "", - "Filter based on phase name", - ) - - roleOpt := cmd.StringOpt( - "role", - "", - "Filter on role name", - ) - - cmd.Action = func() { - ret := API.Workspaces().GetRackSummaries(workspaceName) - - summaries := make(WorkspaceRackSummaries) - - for az, summary := range ret { - for _, s := range summary { - save := true - - if *phaseOpt != "" { - if s.Phase != *phaseOpt { - save = false - } - } - - if *roleOpt != "" { - if s.RoleName != *roleOpt { - save = false - } - } - - if save { - if _, ok := summaries[az]; !ok { - summaries[az] = make([]WorkspaceRackSummary, 0) - } - - summaries[az] = append(summaries[az], s) - } - } - } - - fmt.Println(summaries) - } - }) - }) -} diff --git a/workspaces_integration_test.go b/workspaces_integration_test.go deleted file mode 100644 index 184dcce..0000000 --- a/workspaces_integration_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package main - -import ( - "testing" -) - -func TestWorkspaceAPIIntergration(t *testing.T) { - - setupAPIClient() - r := setupRecorder("fixtures/conch-v3/workspace") - defer r() // Make sure recorder is stopped once done with it - - t.Run("get-all", func(t *testing.T) { - list := API.Workspaces().GetAll() - t.Logf("got %v", list) - }) - - t.Run("get-by-name", func(t *testing.T) { - _ = API.Workspaces().GetByName("GLOBAL") - }) - - t.Run("get-users", func(t *testing.T) { - _ = API.Workspaces().GetByName("GLOBAL") - }) -}